Search code examples
x509phpseclib

phpseclib asn1 parser issue with MS cert SAN


I am attempting to parse some M$ generated certs and finding that the phpseclib ASN1 decodeBER function is choking on some OID's. I would really like to understand this function better and its behavior in this case.

Here is an example cert for discussion:

-----BEGIN CERTIFICATE-----
MIIG1jCCBL6gAwIBAgITUAAAAA0qg8bE6DhrLAAAAAAADTANBgkqhkiG9w0BAQsF
ADAiMSAwHgYDVQQDExcuU2VjdXJlIEVudGVycHJpc2UgQ0EgMTAeFw0xNTAyMjMx
NTE1MDdaFw0xNjAyMjMxNTE1MDdaMD8xFjAUBgoJkiaJk/IsZAEZFgZzZWN1cmUx
DjAMBgNVBAMTBVVzZXJzMRUwEwYDVQQDEwxtZXRhY2xhc3NpbmcwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMdG1CzR/gTalbLN9J+2cvMGeD7wsR7S78
HU5hdwE+kECROjRAcjFBOR57ezSDrkmhkTzo28tj0oAHjOh8N9vuXtASfZSCXugx
H+ImJ+E7PA4aXBp+0H2hohW9sXNNCFiVNmJLX66O4bxIeKtVRq/+eSNijV4OOEkC
zMyTHAUbOFP0t6KoJtM1syNoQ1+fKdfcjz5XtiEzSVcp2zf0MwNFSeZSgGQ0jh8A
Kd6YVKA8ZnrqOWZxKETT+bBNTjIT0ggjQfzcE4zW2RzrN7zWabUowoU92+DAp4s3
sAEywX9ISSge62DEzTnZZSf9bpoScAfT8raRFA3BkoJ/s4c4CgfPAgMBAAGjggLm
MIIC4jAdBgNVHQ4EFgQULlIyJL9+ZwAI/SkVdsJMxFOVp+EwHwYDVR0jBBgwFoAU
5nEIMEUT5mMd1WepmviwgK7dIzwwggEKBgNVHR8EggEBMIH+MIH7oIH4oIH1hoG5
bGRhcDovLy9DTj0uU2VjdXJlJTIwRW50ZXJwcmlzZSUyMENBJTIwMSxDTj1hdXRo
LENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxD
Tj1Db25maWd1cmF0aW9uLERDPXNlY3VyZT9jZXJ0aWZpY2F0ZVJldm9jYXRpb25M
aXN0P2Jhc2U/b2JqZWN0Q2xhc3M9Y1JMRGlzdHJpYnV0aW9uUG9pbnSGN2h0dHA6
Ly9jcmwuc2VjdXJlb2JzY3VyZS5jb20vP2FjdGlvbj1jcmwmY2E9ZW50ZXJwcmlz
ZTEwgccGCCsGAQUFBwEBBIG6MIG3MIG0BggrBgEFBQcwAoaBp2xkYXA6Ly8vQ049
LlNlY3VyZSUyMEVudGVycHJpc2UlMjBDQSUyMDEsQ049QUlBLENOPVB1YmxpYyUy
MEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9
c2VjdXJlP2NBQ2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0
aW9uQXV0aG9yaXR5MBcGCSsGAQQBgjcUAgQKHggAVQBzAGUAcjAOBgNVHQ8BAf8E
BAMCBaAwKQYDVR0lBCIwIAYKKwYBBAGCNwoDBAYIKwYBBQUHAwQGCCsGAQUFBwMC
MC4GA1UdEQQnMCWgIwYKKwYBBAGCNxQCA6AVDBNtZXRhY2xhc3NpbmdAc2VjdXJl
MEQGCSqGSIb3DQEJDwQ3MDUwDgYIKoZIhvcNAwICAgCAMA4GCCqGSIb3DQMEAgIA
gDAHBgUrDgMCBzAKBggqhkiG9w0DBzANBgkqhkiG9w0BAQsFAAOCAgEAKNmjYh+h
cObJEM0CWgz50jOYKZ4M5iIxoAWgrYY9Pv+0O9aPjvPLzjd5bY322L8lxh5wy5my
DKmip+irzjdVdxzQfoyy+ceODmCbX9L6MfEDn0RBzdwjLe1/eOxE1na0sZztrVCc
yt5nI91NNGZJUcVqVQsIA/25FWlkvo/FTfuqTuXdQiEVM5MCKJI915anmTdugy+G
0CmBJALIxtyz5P7sZhaHZFNdpKnx82QsauErqjP9H0RXc6VXX5qt+tEDvYfSlFcc
0lv3aQnV/eIdfm7APJkQ3lmNWWQwdkVf7adXJ7KAAPHSt1yvSbVxThJR/jmIkyeQ
XW/TOP5m7JI/GrmvdlzI1AgwJ+zO8fOmCDuif99pDb1CvkzQ65RZ8p5J1ZV6hzlb
VvOhn4LDnT1jnTcEqigmx1gxM/5ifvMorXn/ItMjKPlb72vHpeF7OeKE8GHsvZAm
osHcKyJXbTIcXchmpZX1efbmCMJBqHgJ/qBTBMl9BX0+YqbTZyabRJSs9ezbTRn0
oRYl21Q8EnvS71CemxEUkSsKJmfJKkQNCsOjc8AbX/V/X9R7LJkH3UEx6K2zQQKK
k6m17mi63YW/+iPCGOWZ2qXmY5HPEyyF2L4L4IDryFJ+8xLyw3pH9/yp5aHZDtp6
833K6qyjgHJT+fUzSEYpiwF5rSBJIGClOCY=
-----END CERTIFICATE-----

For simple testing purposes this is my driver:

$X509 = new File_X509();
$BER = $X509->_extractBER($BER);
$ASN1 = new File_ASN1();
$ASN1->loadOIDs($X509->oids);
$DECODED = $ASN1->decodeBER( $BER );
Utility::dumper($DECODED);

I think we are running into trouble ~line 423:

        case FILE_ASN1_TYPE_OCTET_STRING:
          if (!$constructed) {
                $current['content'] = $content;
            } else {
                $current['content'] = '';
                $length = 0;
                while (substr($content, 0, 2) != "\0\0") {
                    $temp = $this->_decode_ber($content, $length + $start);
                    $this->_string_shift($content, $temp['length']);
                    // all subtags should be octet strings
                    //if ($temp['type'] != FILE_ASN1_TYPE_OCTET_STRING) {
                    //    return false;
                    //}
                    $current['content'].= $temp['content'];
                    $length+= $temp['length'];
                }
                if (substr($content, 0, 2) == "\0\0") {
                    $length+= 2; // +2 for the EOC
                }
            }
            break;

Specifically in dumping the output of array element 7 (subjectAltName)

[7] => Array
    (
        [start] => 1104
        [headerlength] => 2
        [type] => 16
        [content] => Array
            (
                [0] => Array
                    (
                        [start] => 1106
                        [headerlength] => 2
                        [type] => 6
                        [content] => 2.5.29.17
                        [length] => 5
                    )

                [1] => Array
                    (
                        [start] => 1111
                        [headerlength] => 2
                        [type] => 4
                        [content] => 0% #^F+^F^A^D^A^Â7^T^B^C ^U^L^Smetaclassing@secure
                        [length] => 41
                    )

            )

        [length] => 48
    )

It appears that the content should be parsed into an array instead of a string of binary junk. I believe it should parse into something more like this:

SEQUENCE(2 elem)
 OBJECT IDENTIFIER2.5.29.17
  OCTET STRING(1 elem)
   SEQUENCE(1 elem)
   Offset: 1113
   Length: 2+37
   (constructed)
  Value:
   (1 elem)
    [0](2 elem)
     OBJECT IDENTIFIER1.3.6.1.4.1.311.20.2.3
    [0](1 elem)
     UTF8String metaclassing@secure

I believe this is a "constructed" field, however up at line ~298 the bitwise operation happening is a little beyond my understanding:

$constructed = ($type >> 5) & 1;

I have written a corner-case matching hex values to force $constructed to 1 for this specific array element but didnt see much of an improvement in parsing. Wondering what the next best step is to remedy this? I really appreciate your help and thoughts. Thanks!


Solution

  • you should attempt to recurse OCTET_STRING contents and attempt to decode the nested record to see if it decodes as valid ASN.1. All certificate extension values are placed as a nested to OCTET_STRING structure: enter image description here

    From what I know, the following primitive tags are used in primitive form only: BOOLEAN, INTEGER, NULL, OBJECT_IDENTIFIER, REAL, ENUMERATED and RELATIVE_OID. The rest primitive tags may have embedded content. Although, this list is not definitive, OCTET_STRING definitely may be a container even if its tag is encoded in a primitive form. This applies to BIT_STRING as well.