From 3dd9e2b318ae09db577bf8392ac9f9b5f76ca9a9 Mon Sep 17 00:00:00 2001 From: Jim Wigginton Date: Sun, 22 Apr 2012 06:00:55 +0000 Subject: [PATCH] - id-at-organizationalUnitName was misnamed as id-at-dnQualifier - make it so CA's can't be loaded if the keyusage extension doesn't permit their being loaded - implement validateURL() function stub - add support for a few more DN attributes - add removeDNProp(), getDNProp() and setDomain() - fixed some issues preventing new certs from being signed git-svn-id: http://phpseclib.svn.sourceforge.net/svnroot/phpseclib/trunk@213 21d32557-59b3-4da0-833f-c5933fad653e --- phpseclib/File/X509.php | 218 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 196 insertions(+), 22 deletions(-) diff --git a/phpseclib/File/X509.php b/phpseclib/File/X509.php index 440b9d63..8103595e 100644 --- a/phpseclib/File/X509.php +++ b/phpseclib/File/X509.php @@ -1029,17 +1029,17 @@ class File_X509 { '2.5.4.7' => 'id-at-localityName', '2.5.4.8' => 'id-at-stateOrProvinceName', '2.5.4.10' => 'id-at-organizationName', - '2.5.4.11' => 'id-at-dnQualifier', + '2.5.4.11' => 'id-at-organizationalUnitName', '2.5.4.12' => 'id-at-title', '2.5.4.46' => 'id-at-dnQualifier', '2.5.4.6' => 'id-at-countryName', '2.5.4.5' => 'id-at-serialNumber', '2.5.4.65' => 'id-at-pseudonym', + '2.5.4.17' => 'id-at-postalCode', + '2.5.4.9' => 'id-at-streetAddress', '0.9.2342.19200300.100.1.25' => 'id-domainComponent', '1.2.840.113549.1.9' => 'pkcs-9', '1.2.840.113549.1.9.1' => 'id-emailAddress', - '2.5.4.17' => 'id-at-postalCode', - '2.5.4.9' => 'id-at-streetAddress', '2.5.29' => 'id-ce', '2.5.29.35' => 'id-ce-authorityKeyIdentifier', '2.5.29.14' => 'id-ce-subjectKeyIdentifier', @@ -1249,6 +1249,7 @@ class File_X509 { $key = $this->_reformatKey($x509['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], $key); $this->currentCert = $x509; + $this->dn = $x509['tbsCertificate']['subject']; return $x509; } @@ -1419,23 +1420,96 @@ class File_X509 { * * @param String $cert * @access public + * @return Boolean */ function loadCA($cert) { - $this->CAs[] = $this->loadX509($cert); + /* From RFC5280 "PKIX Certificate and CRL Profile": + + If the keyUsage extension is present, then the subject public key + MUST NOT be used to verify signatures on certificates or CRLs unless + the corresponding keyCertSign or cRLSign bit is set. */ + $cert = $this->loadX509($cert); + if (!$cert) { + return false; + } + + $keyUsage = $x509->getExtension('id-ce-keyUsage'); + if ($keyUsage && !in_array('keyCertSign', $keyUsage)) { + return false; + } + + $this->CAs[] = $cert; unset($this->currentCert); unset($this->signatureSubject); + + return true; } /** * Validate an X.509 certificate against a URL * + * From RFC2818 "HTTP over TLS": + * + * Matching is performed using the matching rules specified by + * [RFC2459]. If more than one identity of a given type is present in + * the certificate (e.g., more than one dNSName name, a match in any one + * of the set is considered acceptable.) Names may contain the wildcard + * character * which is considered to match any single domain name + * component or component fragment. E.g., *.a.com matches foo.a.com but + * not bar.foo.a.com. f*.com matches foo.com but not bar.com. + * * @param String $url * @access public * @return Boolean */ function validateURL($url) { + if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { + return false; + } + + $components = parse_url($url); + if (!isset($components['host'])) { + return false; + } + + if ($names = $this->getExtension('id-ce-subjectAltName')) { + foreach ($names as $key => $value) { + $value = str_replace(array('.', '*'), array('\.', '[^.]*'), $value); + switch ($key) { + case 'dNSName': + /* From RFC2818 "HTTP over TLS": + + If a subjectAltName extension of type dNSName is present, that MUST + be used as the identity. Otherwise, the (most specific) Common Name + field in the Subject field of the certificate MUST be used. Although + the use of the Common Name is existing practice, it is deprecated and + Certification Authorities are encouraged to use the dNSName instead. */ + if (preg_match('#^' . $value . '$#', $components['host'])) { + return true; + } + break; + case 'iPAddress': + /* From RFC2818 "HTTP over TLS": + + In some cases, the URI is specified as an IP address rather than a + hostname. In this case, the iPAddress subjectAltName must be present + in the certificate and must exactly match the IP in the URI. */ + if (preg_match('#(?:\d{1-3}\.){4}#', $components['host'] . '.') && preg_match('#^' . $value . '$#', $components['host'])) { + return true; + } + } + } + return false; + } + + if ($value = $this->getDNProp('id-at-commonName')) { + $value = str_replace(array('.', '*'), array('\.', '[^.]*'), $value[0]); + return preg_match('#^' . $value . '$#', $components['host']); + } + + return false; } /** @@ -1680,6 +1754,33 @@ class File_X509 { case 'streetaddress': $type = 'id-at-streetAddress'; break; + case 'id-at-name': + case 'name': + $type = 'id-at-name'; + case 'id-at-givenname': + case 'givenname': + $type = 'id-at-givenName'; + break; + case 'id-at-surname': + case 'surname': + $type = 'id-at-surname'; + break; + case 'id-at-initials': + case 'initials': + $type = 'id-at-initials'; + break; + case 'id-at-generationqualifier': + case 'generationqualifier': + $type = 'id-at-generationQualifier'; + break; + case 'id-at-organizationalunitname': + case 'organizationalunitname': + $type = 'id-at-organizationalUnitName'; + break; + case 'id-at-pseudonym': + case 'pseudonym': + $type = 'id-at-pseudonym'; + break; default: return false; } @@ -1694,6 +1795,53 @@ class File_X509 { return true; } + /** + * Remove Distinguished Name properties + * + * @param String $propName + * @access public + */ + function removeDNProp($propName) + { + if (empty($this->dn)) { + return; + } + + $dn = &$this->dn['rdnSequence']; + $size = count($dn); + for ($i = 0; $i < $size; $i++) { + if ($dn[$i][0]['type'] == $propName) { + unset($dn[$i]); + } + } + + $dn = array_values($dn); + } + + /** + * Get Distinguished Name properties + * + * @param String $propName + * @return Mixed + * @access public + */ + function getDNProp($propName) + { + if (empty($this->dn)) { + return false; + } + + $dn = $this->dn['rdnSequence']; + $result = array(); + for ($i = 0; $i < $size; $i++) { + if ($dn[$i][0]['type'] == $propName) { + $result[] = $propName; + } + } + + return $result; + } + /** * Set a Distinguished Name * @@ -1741,7 +1889,7 @@ class File_X509 { function getDN($string = false, $dn = NULL) { if (!isset($dn)) { - $dn = $this->currentCert['tbsCertificate']['subject']; + $dn = $this->dn; } if (!$string) { @@ -1809,7 +1957,6 @@ class File_X509 { * * @param Object $key * @access public - * @return Boolean */ function setPrivateKey($key) { @@ -1817,17 +1964,14 @@ class File_X509 { } /** - * Get the public key + * Gets the public key * - * Keys need to be Crypt_RSA objects - * - * @param Object $key * @access public - * @return Boolean + * @return Object */ - function getPublicKey($key) + function getPublicKey() { - $this->publicKey = $key; + //return } /** @@ -1908,6 +2052,9 @@ class File_X509 { $signatureSubject = $this->signatureSubject; if (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertificate'])) { + $this->currentCert['tbsCertificate']['signature']['algorithm'] = + $this->currentCert['signatureAlgorithm']['algorithm'] = + $signatureAlgorithm; $this->currentCert = $subject->currentCert; if (!empty($this->startDate)) { $this->currentCert['tbsCertificate']['validity']['notBefore']['utcTime'] = $this->startDate; @@ -1927,21 +2074,24 @@ class File_X509 { $this->currentCert['tbsCertificate']['subjectPublicKeyInfo'] = $subjectPublicKey; } $this->removeExtension('id-ce-authorityKeyIdentifier'); + if (isset($subject->domains)) { + $this->removeExtension('id-ce-subjectAltName'); + } } else { if (!isset($subject->publicKey)) { return false; } - $startDate = empty($this->startDate) ? $this->startDate : @date('M j H:i:s Y T'); - $endDate = empty($this->endDate) ? $this->endDate : @date('M j H:i:s Y T', strtotime('+1 year')); - $serialNumber = empty($this->serialNumber) ? $this->serialNumber : "\0"; + $startDate = !empty($this->startDate) ? $this->startDate : @date('M j H:i:s Y T'); + $endDate = !empty($this->endDate) ? $this->endDate : @date('M j H:i:s Y T', strtotime('+1 year')); + $serialNumber = !empty($this->serialNumber) ? $this->serialNumber : new Math_BigInteger(); $this->currentCert = array( 'tbsCertificate' => array( 'version' => 'v3', 'serialNumber' => $serialNumber, // $this->setserialNumber() - 'signature' => $signatureAlgorithm, + 'signature' => array('algorithm' => $signatureAlgorithm), 'issuer' => false, // this is going to be overwritten later 'validity' => array( 'notBefore' => array('utcTime' => $startDate), // $this->setStartDate() @@ -1950,7 +2100,7 @@ class File_X509 { 'subject' => $subject->dn, 'subjectPublicKeyInfo' => $subjectPublicKey ), - 'signatureAlgorithm' => $signatureAlgorithm, + 'signatureAlgorithm' => array('algorithm' => $signatureAlgorithm), 'signature' => false // this is going to be overwritten later ); } @@ -1986,6 +2136,18 @@ class File_X509 { ); } + if (isset($subject->domains) && count($subject->domains) > 1) { + $this->currentCert['tbsCertificate']['extensions'][] = array( + 'extnId' => 'id-ce-subjectAltName', + 'critical' => false, + 'extnValue' => array() + ); + $last = count($this->currentCert['tbsCertificate']['extensions']) - 1; + foreach ($subject->domains as $domain) { + $this->currentCert['tbsCertificate']['extensions'][$last]['extnValue'][] = array('dNSName' => $domain); + } + } + // resync $this->signatureSubject $this->loadX509($this->saveX509($this->currentCert)); @@ -2022,7 +2184,6 @@ class File_X509 { $key->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1); $this->currentCert['signature'] = base64_encode("\0" . $key->sign($this->signatureSubject)); - return $this->currentCert; } default: @@ -2152,7 +2313,7 @@ class File_X509 { * Sets the authority key identifier * * This is used by the id-ce-authorityKeyIdentifier and the id-ce-subjectKeyIdentifier extensions. - * + * * @param String $value * @access public */ @@ -2167,8 +2328,8 @@ class File_X509 { /** * Format a public key as appropriate - * - * @access public + * + * @access private * @return Array */ function _formatSubjectPublicKey() @@ -2187,4 +2348,17 @@ class File_X509 { return false; } } + + /** + * Set the domain name's which the cert is to be valid for + * + * @access public + * @return Array + */ + function setDomain() + { + $this->domains = func_get_args(); + $this->removeDNProp('id-at-commonName'); + $this->setDNProp('id-at-commonName', $this->domains[0]); + } } \ No newline at end of file