diff --git a/phpseclib/File/X509.php b/phpseclib/File/X509.php index 29a0f89f..de13301d 100644 --- a/phpseclib/File/X509.php +++ b/phpseclib/File/X509.php @@ -117,12 +117,20 @@ class File_X509 { var $dn = array('rdnSequence' => array()); /** - * Public or private key + * Public key * * @var String * @access private */ - var $key; + var $publicKey; + + /** + * Private key + * + * @var String + * @access private + */ + var $privateKey; /** * Object identifiers for X.509 certificates @@ -149,6 +157,41 @@ class File_X509 { */ var $currentCert; + /** + * The signature subject + * + * There's no guarantee File_X509 is going to reencode an X.509 cert in the same way it was originally + * encoded so we take save the portion of the original cert that the signature would have made for. + * + * @var String + * @access private + */ + var $signatureSubject; + + /** + * Certificate Start Date + * + * @var String + * @access private + */ + var $startDate; + + /** + * Certificate End Date + * + * @var String + * @access private + */ + var $endDate; + + /** + * Serial Number + * + * @var String + * @access private + */ + var $serialNumber; + /** * Default Constructor. * @@ -984,6 +1027,8 @@ class File_X509 { '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', @@ -1112,12 +1157,20 @@ class File_X509 { /** * Load X.509 certificate * + * Returns an associative array describing the X.509 cert or a false if the cert failed to load + * * @param String $cert * @access public - * @return Array + * @return Mixed */ function loadX509($cert) { + if (is_array($cert) && isset($cert['tbsCertificate'])) { + $this->currentCert = $cert; + unset($this->signatureSubject); + return false; + } + $asn1 = new File_ASN1(); /* @@ -1198,6 +1251,10 @@ class File_X509 { */ function saveX509($cert) { + if (!is_array($cert) || !isset($cert['tbsCertificate'])) { + return false; + } + switch ($cert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm']) { case 'rsaEncryption': $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'] = @@ -1389,8 +1446,8 @@ class File_X509 { } switch (true) { - case time() < @strtotime($this->currentCert['tbsCertificate']['notBefore']): - case time() > @strtotime($this->currentCert['tbsCertificate']['notAfter']): + case time() < @strtotime($this->currentCert['tbsCertificate']['validity']['notBefore']): + case time() > @strtotime($this->currentCert['tbsCertificate']['validity']['notAfter']): return false; } @@ -1409,7 +1466,7 @@ class File_X509 { */ function validateSignature($options = 0) { - if (!is_array($this->currentCert)) { + if (!is_array($this->currentCert) || !isset($this->signatureSubject)) { return false; } @@ -1552,7 +1609,7 @@ class File_X509 { case 'id-at-organizationname': case 'organizationname': case 'o': - $type = 'id-at-organizationname'; + $type = 'id-at-organizationName'; break; case 'id-at-dnqualifier': case 'dnqualifier': @@ -1585,6 +1642,14 @@ class File_X509 { case 'serialnumber': $type = 'id-at-serialNumber'; break; + case 'id-at-postalcode': + case 'postalcode': + $type = 'id-at-postalCode'; + break; + case 'id-at-streetaddress': + case 'streetaddress': + $type = 'id-at-streetAddress'; + break; default: return false; } @@ -1608,8 +1673,13 @@ class File_X509 { */ function setDN($dn) { - // handles stuff generated by openssl_x509_parse() if (is_array($dn)) { + if (isset($dn['rdnSequence'])) { + $this->dn = $dn; + return true; + } + + // handles stuff generated by openssl_x509_parse() foreach ($dn as $type => $value) { if (!$this->setDNProp($type, $value)) { return false; @@ -1619,9 +1689,9 @@ class File_X509 { } // handles everything else - $results = preg_split('#((?:^|, )(?:C=|O=|OU=|CN=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); + $results = preg_split('#((?:^|, |/)(?:C=|O=|OU=|CN=|L=|ST=|postalCode=|streetAddress=|emailAddress=|serialNumber=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); for ($i = 1; $i < count($results); $i+=2) { - $type = trim($results[$i], ', ='); + $type = trim($results[$i], ', =/'); $value = $results[$i + 1]; if (!$this->setDNProp($type, $value)) { return false; @@ -1632,7 +1702,88 @@ class File_X509 { } /** - * Set public or private key + * Get the Distinguished Name for a certificates subject + * + * @param Boolean $string optional + * @access public + * @return Boolean + */ + function getDN($string = false) + { + if (!$string) { + return $this->currentCert['tbsCertificate']['subject']; + } + + $start = true; + foreach ($this->currentCert['tbsCertificate']['subject']['rdnSequence'] as $field) { + $type = $field[0]['type']; + $value = $field[0]['value']; + + $delim = ', '; + switch ($type) { + case 'id-at-countryName': + $desc = 'C='; + break; + case 'id-at-stateOrProvinceName': + $desc = 'ST='; + break; + case 'id-at-organizationName': + $desc = 'O='; + break; + case 'id-at-dnQualifier': + $desc = 'OU='; + break; + case 'id-at-commonName': + $desc = 'CN='; + break; + case 'id-at-localityName': + $desc = 'L='; + break; + default: + $delim = '/'; + $desc = preg_replace('#.+-([^-]+)$#', '$1', $type) . '='; + } + + if (!$start) { + $output.= $delim; + } + $output.= $desc . $value; + $start = false; + } + + return $output; + } + + /** + * Set public key + * + * Key needs to be a Crypt_RSA object + * + * @param Object $key + * @access public + * @return Boolean + */ + function setPublicKey($key) + { + $this->publicKey = $key; + } + + /** + * Set private key + * + * Key needs to be a Crypt_RSA object + * + * @param Object $key + * @access public + * @return Boolean + */ + function setPrivateKey($key) + { + $this->privateKey = $key; + } + + /** + * Get the public key * * Keys need to be Crypt_RSA objects * @@ -1640,9 +1791,9 @@ class File_X509 { * @access public * @return Boolean */ - function setKey($key) + function getPublicKey($key) { - $this->key = $key; + $this->publicKey = $key; } /** @@ -1685,14 +1836,224 @@ class File_X509 { if (!class_exists('Crypt_RSA')) { require_once('Crypt/RSA.php'); } - $this->key = new Crypt_RSA(); - $this->key->loadKey($key); + $this->publicKey = new Crypt_RSA(); + $this->publicKey->loadKey($key); default: - $this->key = NULL; + $this->publicKey = NULL; } $this->currentCert = $csr; return $csr; } + + /** + * Sign an X.509 certificate + * + * $issuer's private key needs to be loaded. + * $subject can be either an existing X.509 cert (if you want to resign it), + * a CSR or something with the DN and public key explicitly set. + * + * @param Crypt_X509 $issuer + * @param Crypt_X509 $subject + * @param String $signatureAlgorithm optional + * @access public + * @return Mixed + */ + function sign($issuer, $subject, $signatureAlgorithm = 'sha1WithRSAEncryption') + { + if (!is_object($issuer->privateKey) || !is_array($issuer->dn)) { + return false; + } + + $currentCert = $this->currentCert; + $signatureSubject = $this->signatureSubject; + + if (is_array($subject->currentCert) && isset($subject->currentCert['tbsCertificate'])) { + $this->currentCert = $subject->currentCert; + if (!empty($this->startDate)) { + $this->currentCert['tbsCertificate']['validity']['notBefore']['utcTime'] = $this->startDate; + unset($this->currentCert['tbsCertificate']['validity']['notBefore']['generalTime']); + } + if (!empty($this->endDate)) { + $this->currentCert['tbsCertificate']['validity']['notAfter']['utcTime'] = $this->endDate; + unset($this->currentCert['tbsCertificate']['validity']['notAfter']['generalTime']); + } + if (!empty($this->dn)) { + $this->currentCert['tbsCertificate']['subject'] = $this->dn; + } + $this->removeExtension('id-ce-authorityKeyIdentifier'); + + } else { + $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"; + + $this->currentCert = array( + 'tbsCertificate' => + array( + 'version' => 'v3', + 'serialNumber' => $this->serialNumber, // $this->setserialNumber() + 'signature' => $signatureAlgorithm, + 'issuer' => false, // this is going to be overwritten later + 'validity' => array( + 'notBefore' => array('utcTime' => $this->startDate), // $this->setStartDate() + 'notAfter' => array('utcTime' => $this->endDate) // $this->setEndDate() + ), + 'subject' => $subject->dn, + 'subjectPublicKeyInfo' => $subject->publicKey->getPublicKey() + ), + 'signatureAlgorithm' => $signatureAlgorithm, + 'signature' => false // this is going to be overwritten later + ); + } + + $this->currentCert['tbsCertificate']['issuer'] = $issuer->dn; + + $this->loadX509($this->saveX509($this->currentCert)); + + $result = $this->_sign($issuer->privateKey, $signatureAlgorithm); + + $this->currentCert = $currentCert; + $this->signatureSubject = $signatureSubject; + + return $result; + } + + /** + * X.509 certificate signing helper function. + * + * @param Object $key + * @param Crypt_X509 $subject + * @param String $signatureAlgorithm + * @access public + * @return Mixed + */ + function _sign($key, $signatureAlgorithm) + { + switch (strtolower(get_class($key))) { + case 'crypt_rsa': + switch ($signatureAlgorithm) { + case 'md2WithRSAEncryption': + case 'md5WithRSAEncryption': + case 'sha1WithRSAEncryption': + case 'sha224WithRSAEncryption': + case 'sha256WithRSAEncryption': + case 'sha384WithRSAEncryption': + case 'sha512WithRSAEncryption': + $key->setHash(preg_replace('#WithRSAEncryption$#', '', $signatureAlgorithm)); + $key->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1); + + $this->currentCert['signature'] = base64_encode("\0" . $key->sign($this->signatureSubject)); + + return $this->currentCert; + } + default: + return false; + } + } + + /** + * Set certificate start date + * + * @param String $date + * @access public + */ + function setStartDate($date) + { + $this->startDate = @date('M j H:i:s Y T', @strtotime($date)); + } + + /** + * Set certificate end date + * + * @param String $date + * @access public + */ + function setEndDate($date) + { + $this->endDate = @date('M j H:i:s Y T', @strtotime($date)); + } + + /** + * Set Serial Number + * + * @param String $serial + * @access public + */ + function setSerialNumber($serial) + { + $this->serialNumber = $serial; + } + + /** + * Remove an Extension + * + * @param String $id + * @access public + * @return Boolean + */ + function removeExtension($id) + { + if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { + return false; + } + + $result = false; + $extensions = &$this->currentCert['tbsCertificate']['extensions']; + foreach ($extensions as $key => $value) { + if ($value['extnId'] == $id) { + unset($extensions[$key]); + $result = true; + } + } + + $extensions = array_values($extensions); + + return $result; + } + + /** + * Get an Extension + * + * Returns the extension if it exists and false if not + * + * @param String $id + * @access public + * @return Mixed + */ + function getExtension($id) + { + if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { + return false; + } + + foreach ($this->currentCert['tbsCertificate']['extensions'] as $key => $value) { + if ($value['extnId'] == $id) { + return $value['extnValue']; + } + } + + return false; + } + + /** + * Returns a list of all extensions in use + * + * @access public + * @return Array + */ + function getExtensions() + { + if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { + return false; + } + + $extensions = array(); + foreach ($this->currentCert['tbsCertificate']['extensions'] as $extension) { + $extensions[] = $extension['extnId']; + } + + return $extensions; + } } \ No newline at end of file