Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I parse subjectAltName extension data using pyasn1?

I have some data that pyOpenSSL gave me, '0\r\x82\x0bexample.com'. This should be the value of a subjectAltName X509 extension. I tried to encode the necessary parts of the ASN1 specification for this extension using pyasn1 (and based on one of the pyasn1 examples):

from pyasn1.type import univ, constraint, char, namedtype

from pyasn1.codec.der.decoder import decode

MAX = 64

class DirectoryString(univ.Choice):
    componentType = namedtype.NamedTypes(
        namedtype.NamedType(
            'teletexString', char.TeletexString().subtype(
                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
        namedtype.NamedType(
            'printableString', char.PrintableString().subtype(
                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
        namedtype.NamedType(
            'universalString', char.UniversalString().subtype(
                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
        namedtype.NamedType(
            'utf8String', char.UTF8String().subtype(
                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
        namedtype.NamedType(
            'bmpString', char.BMPString().subtype(
                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
        namedtype.NamedType(
            'ia5String', char.IA5String().subtype(
                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
        )


class AttributeValue(DirectoryString):
    pass


class AttributeType(univ.ObjectIdentifier):
    pass


class AttributeTypeAndValue(univ.Sequence):
    componentType = namedtype.NamedTypes(
        namedtype.NamedType('type', AttributeType()),
        namedtype.NamedType('value', AttributeValue()),
        )


class RelativeDistinguishedName(univ.SetOf):
    componentType = AttributeTypeAndValue()

class RDNSequence(univ.SequenceOf):
    componentType = RelativeDistinguishedName()


class Name(univ.Choice):
    componentType = namedtype.NamedTypes(
        namedtype.NamedType('', RDNSequence()),
        )


class Extension(univ.Sequence):
    componentType = namedtype.NamedTypes(
        namedtype.NamedType('extnID', univ.ObjectIdentifier()),
        namedtype.DefaultedNamedType('critical', univ.Boolean('False')),
        namedtype.NamedType('extnValue', univ.OctetString()),
        )


class Extensions(univ.SequenceOf):
    componentType = Extension()
    sizeSpec = univ.SequenceOf.sizeSpec + constraint.ValueSizeConstraint(1, MAX)


class GeneralName(univ.Choice):
    componentType = namedtype.NamedTypes(
        # namedtype.NamedType('otherName', AnotherName()),
        namedtype.NamedType('rfc822Name', char.IA5String()),
        namedtype.NamedType('dNSName', char.IA5String()),
        # namedtype.NamedType('x400Address', ORAddress()),
        namedtype.NamedType('directoryName', Name()),
        # namedtype.NamedType('ediPartyName', EDIPartyName()),
        namedtype.NamedType('uniformResourceIdentifier', char.IA5String()),
        namedtype.NamedType('iPAddress', univ.OctetString()),
        namedtype.NamedType('registeredID', univ.ObjectIdentifier()),
        )


class GeneralNames(univ.SequenceOf):
    componentType = GeneralName()
    sizeSpec = univ.SequenceOf.sizeSpec + constraint.ValueSizeConstraint(1, MAX)


class SubjectAltName(GeneralNames):
    pass

print decode('0\r\x82\x0bexample.com', asn1Spec=GeneralNames())

Clearly I got a little bored near the end and didn't fully specify the GeneralName type. However, the test string should contain a dNSName, not one of the skipped values, so I hope it doesn't matter.

When the program is run, it fails with an error I'm not able to interpret:

Traceback (most recent call last):
  File "x509.py", line 94, in <module>
    print decode('0\r\x82\x0bexample.com', asn1Spec=GeneralNames())
  File "/usr/lib/pymodules/python2.6/pyasn1/v1/codec/ber/decoder.py", line 493, in __call__
    length, stGetValueDecoder, decodeFun
  File "/usr/lib/pymodules/python2.6/pyasn1/v1/codec/ber/decoder.py", line 202, in valueDecoder
    substrate, asn1Spec
  File "/usr/lib/pymodules/python2.6/pyasn1/v1/codec/ber/decoder.py", line 453, in __call__
    __chosenSpec.getTypeMap().has_key(tagSet):
  File "/usr/lib/pymodules/python2.6/pyasn1/v1/type/univ.py", line 608, in getTypeMap
    return Set.getComponentTypeMap(self)
  File "/usr/lib/pymodules/python2.6/pyasn1/v1/type/univ.py", line 535, in getComponentTypeMap
    def getComponentTypeMap(self): return self._componentType.getTypeMap(1)
  File "/usr/lib/pymodules/python2.6/pyasn1/v1/type/namedtype.py", line 126, in getTypeMap
    'Duplicate type %s in map %s'%(k,self.__typeMap)
pyasn1.error.PyAsn1Error: Duplicate type TagSet(Tag(tagClass=0, tagFormat=0, tagId=22)) in map {TagSet(Tag(tagClass=0, tagFormat=0, tagId=22)): IA5String()}

Any tips on where I went wrong and how to successfully parse this extension type with pyasn1 would be much appreciated.

like image 586
Jean-Paul Calderone Avatar asked Apr 01 '11 23:04

Jean-Paul Calderone


2 Answers

I posted this question on the pyasn1-users list and Ilya Etingof (the author of pyasn1) pointed out my mistake. In brief, each NamedType in GeneralName.componentType needs to be given tag information. This is done with the subtype method. For example, instead of:

namedtype.NamedType('rfc822Name', char.IA5String()),

the definition should be:

namedtype.NamedType('rfc822Name', char.IA5String().subtype(
        implicitTag=tag.Tag(tag.tagClassContext,
                            tag.tagFormatSimple, 1))),

where 1 comes from the ASN.1 definition of GeneralName:

GeneralName ::= CHOICE {
   otherName                       [0]     OtherName,
   rfc822Name                      [1]     IA5String,
   dNSName                         [2]     IA5String,
   x400Address                     [3]     ORAddress,
   directoryName                   [4]     Name,
   ediPartyName                    [5]     EDIPartyName,
   uniformResourceIdentifier       [6]     IA5String,
   iPAddress                       [7]     OCTET STRING,
   registeredID                    [8]     OBJECT IDENTIFIER
}

After defining a tag for each of these fields of the componentType, parsing succeeds:

(GeneralNames().setComponentByPosition(
    0, GeneralName().setComponentByPosition(1, IA5String('example.com'))), '')
like image 91
Jean-Paul Calderone Avatar answered Sep 26 '22 01:09

Jean-Paul Calderone


Coming in way late with this answer but instead of writing the ASN.1 Schema by hand you can also use the RF2459 module provided in pyasn1-modules (also authored by Ilya Etingof)

Minimally this code should work and will hopefully be enough to get you started on more complex ANS.1 constructs. Make sure you have run pip install pyasn1, pip install pyasn1-modules and pip install pyopenssl otherwise you'll get import errors.

# Import pyasn and the proper decode function
import pyasn1
from pyasn1.codec.der.decoder import decode as asn1_decoder

# Import SubjectAltName from rfc2459 module
from pyasn1_modules.rfc2459 import SubjectAltName

# Import native Python type encoder
from pyasn1.codec.native.encoder import encode as nat_encoder

# Import OpenSSL tools for working with certs.
from OpenSSL import crypto
# Read raw certificate file
with open('PATH/TO/CERTIFICATE.crt', 'r') as cert_f:
    raw_cert = cert_f.read()

cert = crypto.load_certificate(crypto.FILETYPE_PEM, raw_cert)

# Note this example assumes SubjectAltName is the only Extension for this cert. 
raw_alt_names = cert.get_extension(0).get_data()

decoded_alt_names, _ = asn1_decoder(raw_alt_names, asn1Spec=SubjectAltName())

# Unless a raw string of ASN.1 is what you need encode back to native Python types
py_alt_names = nat_encoder(decoded_alt_names)

# And Finally a plain Python list of UTF-8 encoded strings representing the SubjectAltNames
subject_alt_names = [ x['dNSName'].decode('utf-8') for x in py_alt_names]

The output of this will be something like

['cdn1.example.com', 'cdn2.example.com']

If the cert you are working on has multiple extensions you will need to use get_extension_count from the X509 object and get_short_name from the X509Extension object provided in pyopenssl.

like image 24
TheDude Avatar answered Sep 23 '22 01:09

TheDude