From 916dcff8a894f2f504ddcb26dc011d7aaac5e463 Mon Sep 17 00:00:00 2001 From: Patrick Monnerat Date: Fri, 2 Nov 2012 16:53:32 +0100 Subject: [PATCH] ASN1/X509: implement limited string conversion. Add getDN() options. Warning: converted strings must not be used for matching DNs. --- phpseclib/File/ASN1.php | 121 ++++++++++++++++++++++++- phpseclib/File/X509.php | 194 +++++++++++++++++++++++++++++++++------- 2 files changed, 284 insertions(+), 31 deletions(-) diff --git a/phpseclib/File/ASN1.php b/phpseclib/File/ASN1.php index f326e674..ea323e1d 100644 --- a/phpseclib/File/ASN1.php +++ b/phpseclib/File/ASN1.php @@ -203,7 +203,7 @@ class File_ASN1 { * Others are mapped as a choice, with an extra indexing level. * * @var Array - * @access private + * @access public */ var $ANYmap = array( FILE_ASN1_TYPE_BOOLEAN => true, @@ -230,6 +230,25 @@ class File_ASN1 { FILE_ASN1_TYPE_BMP_STRING => 'bmpString' ); + /** + * String type to character size mapping table. + * + * Non-convertable types are absent from this table. + * size == 0 indicates variable length encoding. + * + * @var Array + * @access public + */ + var $stringTypeSize = array( + FILE_ASN1_TYPE_UTF8_STRING => 0, + FILE_ASN1_TYPE_BMP_STRING => 2, + FILE_ASN1_TYPE_UNIVERSAL_STRING => 4, + FILE_ASN1_TYPE_PRINTABLE_STRING => 1, + FILE_ASN1_TYPE_TELETEX_STRING => 1, + FILE_ASN1_TYPE_IA5_STRING => 1, + FILE_ASN1_TYPE_VISIBLE_STRING => 1, + ); + /** * Parse BER-encoding * @@ -1152,4 +1171,104 @@ class File_ASN1 { $string = substr($string, $index); return $substr; } + + /** + * String type conversion + * + * This is a lazy conversion, dealing only with character size. + * No real conversion table is used. + * + * @param String $in + * @param optional Integer $from + * @param optional Integer $to + * @return String + * @access public + */ + + function convert($in, $from = FILE_ASN1_TYPE_UTF8_STRING, $to = FILE_ASN1_TYPE_UTF8_STRING) + { + if (!isset($this->stringTypeSize[$from]) || !isset($this->stringTypeSize[$to])) { + return false; + } + $insize = $this->stringTypeSize[$from]; + $outsize = $this->stringTypeSize[$to]; + $inlength = strlen($in); + $out = ''; + + for ($i = 0; $i < $inlength;) { + if ($inlength - $i < $insize) { + return false; + } + + // Get an input character as a 32-bit value. + $c = ord($in[$i++]); + switch (true) { + case $insize == 4: + $c = ($c << 8) | ord($in[$i++]); + $c = ($c << 8) | ord($in[$i++]); + case $insize == 2: + $c = ($c << 8) | ord($in[$i++]); + case $insize == 1: + break; + case ($c & 0x80) == 0x00: + break; + case ($c & 0x40) == 0x00: + return false; + default: + $bit = 6; + do { + if ($bit > 25 || $i >= $inlength || (ord($in[$i]) & 0xC0) != 0x80) { + return false; + } + $c = ($c << 6) | (ord($in[$i++]) & 0x3F); + $bit += 5; + $mask = 1 << $bit; + } while ($c & $bit); + $c &= $mask - 1; + break; + } + + // Convert and append the character to output string. + $v = ''; + switch (true) { + case $outsize == 4: + $v .= chr($c & 0xFF); + $c >>= 8; + $v .= chr($c & 0xFF); + $c >>= 8; + case $outsize == 2: + $v .= chr($c & 0xFF); + $c >>= 8; + case $outsize == 1: + $v .= chr($c & 0xFF); + $c >>= 8; + if ($c) { + return false; + } + break; + case ($c & 0x80000000) != 0: + return false; + case $c >= 0x04000000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x04000000; + case $c >= 0x00200000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00200000; + case $c >= 0x00010000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00010000; + case $c >= 0x00000800: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00000800; + case $c >= 0x00000080: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x000000C0; + default: + $v .= chr($c); + break; + } + $out .= strrev($v); + } + return $out; + } } diff --git a/phpseclib/File/X509.php b/phpseclib/File/X509.php index d63cfcf6..6b849552 100644 --- a/phpseclib/File/X509.php +++ b/phpseclib/File/X509.php @@ -59,6 +59,16 @@ if (!class_exists('File_ASN1')) { */ define('FILE_X509_VALIDATE_SIGNATURE_BY_CA', 1); +/** + * Name format tokens for the getDN() method. + */ +define('FILE_X509_DN_ARRAY', 0); // Internal array representation. +define('FILE_X509_DN_STRING', 1); // String. +define('FILE_X509_DN_ASN1', 2); // ASN.1 element. +define('FILE_X509_DN_OPENSSL', 3); // OpenSSL compatible array. +define('FILE_X509_DN_CANON', 4); // Canonical ASN.1 element. +define('FILE_X509_DN_HASH', 5); // Name hash for file indexing. + /** * Pure-PHP X.509 Parser * @@ -102,6 +112,7 @@ class File_X509 { var $netscape_comment; var $netscape_ca_policy_url; + var $Name; var $CRLNumber; var $CRLReason; var $IssuingDistributionPoint; @@ -287,7 +298,7 @@ class File_X509 { 'children' => $RelativeDistinguishedName ); - $Name = array( + $this->Name = array( 'type' => FILE_ASN1_TYPE_CHOICE, 'children' => array( 'rdnSequence' => $RDNSequence @@ -383,9 +394,9 @@ class File_X509 { ) + $Version, 'serialNumber' => $CertificateSerialNumber, 'signature' => $AlgorithmIdentifier, - 'issuer' => $Name, + 'issuer' => $this->Name, 'validity' => $Validity, - 'subject' => $Name, + 'subject' => $this->Name, 'subjectPublicKeyInfo' => $SubjectPublicKeyInfo, // implicit means that the T in the TLV structure is to be rewritten, regardless of the type 'issuerUniqueID' => array( @@ -678,7 +689,7 @@ class File_X509 { 'constant' => 4, 'optional' => true, 'explicit' => true - ) + $Name, + ) + $this->Name, 'ediPartyName' => array( 'constant' => 5, 'optional' => true, @@ -1013,7 +1024,7 @@ class File_X509 { 'type' => FILE_ASN1_TYPE_INTEGER, 'mapping' => array('v1') ), - 'subject' => $Name, + 'subject' => $this->Name, 'subjectPKInfo' => $SubjectPublicKeyInfo, 'attributes' => array( 'constant' => 0, @@ -1051,7 +1062,7 @@ class File_X509 { 'default' => 'v1' ) + $Version, 'signature' => $AlgorithmIdentifier, - 'issuer' => $Name, + 'issuer' => $this->Name, 'thisUpdate' => $Time, 'nextUpdate' => array( 'optional' => true @@ -2119,11 +2130,24 @@ class File_X509 { $dn = $dn['rdnSequence']; $result = array(); + $asn1 = new File_ASN1(); for ($i = 0; $i < count($dn); $i++) { if ($dn[$i][0]['type'] == $propName) { $v = $dn[$i][0]['value']; - if (!$withType && is_array($v) && count($v) == 1) { - $v = array_pop($v); + if (!$withType && is_array($v)) { + foreach ($v as $type => $s) { + $type = array_search($type, $asn1->ANYmap, true); + if ($type !== false && isset($asn1->stringTypeSize[$type])) { + $s = $asn1->convert($s, $type); + if ($s !== false) { + $v = $s; + break; + } + } + } + if (is_array($v)) { + $v = array_pop($v); // Always strip data type. + } } $result[] = $v; } @@ -2163,7 +2187,7 @@ class File_X509 { } // handles everything else - $results = preg_split('#((?:^|, |/)(?:C=|O=|OU=|CN=|L=|ST=|SN=|postalCode=|streetAddress=|emailAddress=|serialNumber=|organizationalUnitName=|title=|description=|role=|x500UniqueIdentifier=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); + $results = preg_split('#((?:^|, *|/)(?:C=|O=|OU=|CN=|L=|ST=|SN=|postalCode=|streetAddress=|emailAddress=|serialNumber=|organizationalUnitName=|title=|description=|role=|x500UniqueIdentifier=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); for ($i = 1; $i < count($results); $i+=2) { $prop = trim($results[$i], ', =/'); $value = $results[$i + 1]; @@ -2178,23 +2202,88 @@ class File_X509 { /** * Get the Distinguished Name for a certificates subject * - * @param Boolean $string optional + * @param Mixed $format optional * @param Array $dn optional * @access public * @return Boolean */ - function getDN($string = false, $dn = NULL) + function getDN($format = FILE_X509_DN_ARRAY, $dn = NULL) { if (!isset($dn)) { $dn = $this->dn; } - if (!$string) { - return $dn; + switch ((int) $format) { + case FILE_X509_DN_ARRAY: + return $dn; + case FILE_X509_DN_ASN1: + $asn1 = new File_ASN1(); + $asn1->loadOIDs($this->oids); + $filters = array(); + $filters['rdnSequence']['value'] = array('type' => FILE_ASN1_TYPE_UTF8_STRING); + $asn1->loadFilters($filters); + return $asn1->encodeDER($dn, $this->Name); + case FILE_X509_DN_OPENSSL: + $dn = $this->getDN(FILE_X509_DN_STRING, $dn); + if ($dn === false) { + return false; + } + $attrs = preg_split('#((?:^|, *|/)[a-z][a-z0-9]*=)#i', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); + $dn = array(); + for ($i = 1; $i < count($attrs); $i += 2) { + $prop = trim($attrs[$i], ', =/'); + $value = $attrs[$i + 1]; + if (!isset($dn[$prop])) { + $dn[$prop] = $value; + } else { + $dn[$prop] = array_merge((array) $dn[$prop], array($value)); + } + } + return $dn; + case FILE_X509_DN_CANON: + // No SEQUENCE around RDNs and all string values normalized as + // trimmed lowercase UTF-8 with all spacing as one blank. + $asn1 = new File_ASN1(); + $asn1->loadOIDs($this->oids); + $filters = array(); + $filters['value'] = array('type' => FILE_ASN1_TYPE_UTF8_STRING); + $asn1->loadFilters($filters); + $RelDN = $this->Name['children']['rdnSequence']['children']; + $result = ''; + foreach ($dn['rdnSequence'] as $rdn) { + foreach ($rdn as &$attr) { + if (is_array($attr['value'])) { + foreach ($attr['value'] as $type => $v) { + $type = array_search($type, $asn1->ANYmap, true); + if ($type !== false && isset($asn1->stringTypeSize[$type])) { + $v = $asn1->convert($v, $type); + if ($v !== false) { + $v = preg_replace('/\s+/', ' ', $v); + $attr['value'] = strtolower(trim($v)); + break; + } + } + } + } + } + $result .= $asn1->encodeDER($rdn, $RelDN); + } + return $result; + case FILE_X509_DN_HASH: + $dn = $this->getDN(FILE_X509_DN_CANON, $dn); + if (!class_exists('Crypt_Hash')) { + require_once('Crypt/Hash.php'); + } + $hash = new Crypt_Hash('sha1'); + $hash = $hash->hash($dn); + extract(unpack('Vhash', $hash)); + return strtolower(bin2hex(pack('N', $hash))); } + // Defaut is to return a string. $start = true; $output = ''; + $asn1 = new File_ASN1(); foreach ($dn['rdnSequence'] as $field) { $prop = $field[0]['type']; $value = $field[0]['value']; @@ -2234,8 +2323,20 @@ class File_X509 { if (!$start) { $output.= $delim; } - if (is_array($value) && count($value) == 1) { - $value = array_pop($value); // Always strip data type. + if (is_array($value)) { + foreach ($value as $type => $v) { + $type = array_search($type, $asn1->ANYmap, true); + if ($type !== false && isset($asn1->stringTypeSize[$type])) { + $v = $asn1->convert($v, $type); + if ($v !== false) { + $value = $v; + break; + } + } + } + if (is_array($value)) { + $value = array_pop($value); // Always strip data type. + } } $output.= $desc . $value; $start = false; @@ -2245,35 +2346,52 @@ class File_X509 { } /** - * Get the Distinguished Name for a certificates issuer + * Get the Distinguished Name for a certificate/crl issuer * - * @param Boolean $string optional + * @param Integer $format optional * @access public * @return Mixed */ - function getIssuerDN($string = false) + function getIssuerDN($format = FILE_X509_DN_ARRAY) { - if (!isset($this->currentCert) || !is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { - return false; + switch (true) { + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDN($format, $this->currentCert['tbsCertificate']['issuer']); + case isset($this->currentCert['tbsCertList']): + return $this->getDN($format, $this->currentCert['tbsCertList']['issuer']); } - return $this->getDN($string, $this->currentCert['tbsCertificate']['issuer']); + return false; } /** + * Get the Distinguished Name for a certificate/csr subject * Alias of getDN() * - * @param Boolean $string optional + * @param Integer $format optional * @access public * @return Mixed */ - function getSubjectDN($string = false) + function getSubjectDN($format = FILE_X509_DN_ARRAY) { - return $this->getDN($string); + switch (true) { + case !empty($this->dn): + return $this->getDN($format); + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDN($format, $this->currentCert['tbsCertificate']['subject']); + case isset($this->currentCert['certificationRequestInfo']): + return $this->getDN($format, $this->currentCert['certificationRequestInfo']['subject']); + } + + return false; } /** - * Get an individual Distinguished Name property for a certificates issuer + * Get an individual Distinguished Name property for a certificate/crl issuer * * @param String $propName * @param Boolean $withType optional @@ -2282,15 +2400,20 @@ class File_X509 { */ function getIssuerDNProp($propName, $withType = false) { - if (!isset($this->currentCert) || !is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { - return false; + switch (true) { + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDNProp($propname, $this->currentCert['tbsCertificate']['issuer'], $withType); + case isset($this->currentCert['tbsCertList']): + return $this->getDNProp($propname, $this->currentCert['tbsCertList']['issuer'], $withType); } - return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['issuer'], $withType); + return false; } /** - * Alias of getDNProp() + * Get an individual Distinguished Name property for a certificate/csr subject * * @param String $propName * @param Boolean $withType optional @@ -2299,7 +2422,18 @@ class File_X509 { */ function getSubjectDNProp($propName, $withType = false) { - return $this->getDNProp($propName, NULL, $withType); + switch (true) { + case !empty($this->dn): + return $this->getDNProp($propName, NULL, $withType); + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['subject'], $withType); + case isset($this->currentCert['certificationRequestInfo']): + return $this->getDNProp($propname, $this->currentCert['certificationRequestInfo']['subject'], $withType); + } + + return false; } /**