ASN1/X509: implement limited string conversion. Add getDN() options.

Warning: converted strings must not be used for matching DNs.
This commit is contained in:
Patrick Monnerat 2012-11-02 16:53:32 +01:00
parent 26b842be5b
commit 916dcff8a8
2 changed files with 284 additions and 31 deletions

View File

@ -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;
}
}

View File

@ -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;
}
/**