wp/wp-includes/Requests/src/Ssl.php
changeset 21 48c4eec2b7e6
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * SSL utilities for Requests
       
     4  *
       
     5  * @package Requests\Utilities
       
     6  */
       
     7 
       
     8 namespace WpOrg\Requests;
       
     9 
       
    10 use WpOrg\Requests\Exception\InvalidArgument;
       
    11 use WpOrg\Requests\Utility\InputValidator;
       
    12 
       
    13 /**
       
    14  * SSL utilities for Requests
       
    15  *
       
    16  * Collection of utilities for working with and verifying SSL certificates.
       
    17  *
       
    18  * @package Requests\Utilities
       
    19  */
       
    20 final class Ssl {
       
    21 	/**
       
    22 	 * Verify the certificate against common name and subject alternative names
       
    23 	 *
       
    24 	 * Unfortunately, PHP doesn't check the certificate against the alternative
       
    25 	 * names, leading things like 'https://www.github.com/' to be invalid.
       
    26 	 *
       
    27 	 * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
       
    28 	 *
       
    29 	 * @param string|Stringable $host Host name to verify against
       
    30 	 * @param array $cert Certificate data from openssl_x509_parse()
       
    31 	 * @return bool
       
    32 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $host argument is not a string or a stringable object.
       
    33 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cert argument is not an array or array accessible.
       
    34 	 */
       
    35 	public static function verify_certificate($host, $cert) {
       
    36 		if (InputValidator::is_string_or_stringable($host) === false) {
       
    37 			throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host));
       
    38 		}
       
    39 
       
    40 		if (InputValidator::has_array_access($cert) === false) {
       
    41 			throw InvalidArgument::create(2, '$cert', 'array|ArrayAccess', gettype($cert));
       
    42 		}
       
    43 
       
    44 		$has_dns_alt = false;
       
    45 
       
    46 		// Check the subjectAltName
       
    47 		if (!empty($cert['extensions']['subjectAltName'])) {
       
    48 			$altnames = explode(',', $cert['extensions']['subjectAltName']);
       
    49 			foreach ($altnames as $altname) {
       
    50 				$altname = trim($altname);
       
    51 				if (strpos($altname, 'DNS:') !== 0) {
       
    52 					continue;
       
    53 				}
       
    54 
       
    55 				$has_dns_alt = true;
       
    56 
       
    57 				// Strip the 'DNS:' prefix and trim whitespace
       
    58 				$altname = trim(substr($altname, 4));
       
    59 
       
    60 				// Check for a match
       
    61 				if (self::match_domain($host, $altname) === true) {
       
    62 					return true;
       
    63 				}
       
    64 			}
       
    65 
       
    66 			if ($has_dns_alt === true) {
       
    67 				return false;
       
    68 			}
       
    69 		}
       
    70 
       
    71 		// Fall back to checking the common name if we didn't get any dNSName
       
    72 		// alt names, as per RFC2818
       
    73 		if (!empty($cert['subject']['CN'])) {
       
    74 			// Check for a match
       
    75 			return (self::match_domain($host, $cert['subject']['CN']) === true);
       
    76 		}
       
    77 
       
    78 		return false;
       
    79 	}
       
    80 
       
    81 	/**
       
    82 	 * Verify that a reference name is valid
       
    83 	 *
       
    84 	 * Verifies a dNSName for HTTPS usage, (almost) as per Firefox's rules:
       
    85 	 * - Wildcards can only occur in a name with more than 3 components
       
    86 	 * - Wildcards can only occur as the last character in the first
       
    87 	 *   component
       
    88 	 * - Wildcards may be preceded by additional characters
       
    89 	 *
       
    90 	 * We modify these rules to be a bit stricter and only allow the wildcard
       
    91 	 * character to be the full first component; that is, with the exclusion of
       
    92 	 * the third rule.
       
    93 	 *
       
    94 	 * @param string|Stringable $reference Reference dNSName
       
    95 	 * @return boolean Is the name valid?
       
    96 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object.
       
    97 	 */
       
    98 	public static function verify_reference_name($reference) {
       
    99 		if (InputValidator::is_string_or_stringable($reference) === false) {
       
   100 			throw InvalidArgument::create(1, '$reference', 'string|Stringable', gettype($reference));
       
   101 		}
       
   102 
       
   103 		if ($reference === '') {
       
   104 			return false;
       
   105 		}
       
   106 
       
   107 		if (preg_match('`\s`', $reference) > 0) {
       
   108 			// Whitespace detected. This can never be a dNSName.
       
   109 			return false;
       
   110 		}
       
   111 
       
   112 		$parts = explode('.', $reference);
       
   113 		if ($parts !== array_filter($parts)) {
       
   114 			// DNSName cannot contain two dots next to each other.
       
   115 			return false;
       
   116 		}
       
   117 
       
   118 		// Check the first part of the name
       
   119 		$first = array_shift($parts);
       
   120 
       
   121 		if (strpos($first, '*') !== false) {
       
   122 			// Check that the wildcard is the full part
       
   123 			if ($first !== '*') {
       
   124 				return false;
       
   125 			}
       
   126 
       
   127 			// Check that we have at least 3 components (including first)
       
   128 			if (count($parts) < 2) {
       
   129 				return false;
       
   130 			}
       
   131 		}
       
   132 
       
   133 		// Check the remaining parts
       
   134 		foreach ($parts as $part) {
       
   135 			if (strpos($part, '*') !== false) {
       
   136 				return false;
       
   137 			}
       
   138 		}
       
   139 
       
   140 		// Nothing found, verified!
       
   141 		return true;
       
   142 	}
       
   143 
       
   144 	/**
       
   145 	 * Match a hostname against a dNSName reference
       
   146 	 *
       
   147 	 * @param string|Stringable $host Requested host
       
   148 	 * @param string|Stringable $reference dNSName to match against
       
   149 	 * @return boolean Does the domain match?
       
   150 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When either of the passed arguments is not a string or a stringable object.
       
   151 	 */
       
   152 	public static function match_domain($host, $reference) {
       
   153 		if (InputValidator::is_string_or_stringable($host) === false) {
       
   154 			throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host));
       
   155 		}
       
   156 
       
   157 		// Check if the reference is blocklisted first
       
   158 		if (self::verify_reference_name($reference) !== true) {
       
   159 			return false;
       
   160 		}
       
   161 
       
   162 		// Check for a direct match
       
   163 		if ((string) $host === (string) $reference) {
       
   164 			return true;
       
   165 		}
       
   166 
       
   167 		// Calculate the valid wildcard match if the host is not an IP address
       
   168 		// Also validates that the host has 3 parts or more, as per Firefox's ruleset,
       
   169 		// as a wildcard reference is only allowed with 3 parts or more, so the
       
   170 		// comparison will never match if host doesn't contain 3 parts or more as well.
       
   171 		if (ip2long($host) === false) {
       
   172 			$parts    = explode('.', $host);
       
   173 			$parts[0] = '*';
       
   174 			$wildcard = implode('.', $parts);
       
   175 			if ($wildcard === (string) $reference) {
       
   176 				return true;
       
   177 			}
       
   178 		}
       
   179 
       
   180 		return false;
       
   181 	}
       
   182 }