perlder

How to decode DER-encoded X.509 certificate subject in perl?


300f310d300b0603550403130446616B65

The above is hex bytes of a DER-encoded X.509 certificate subject. when it is decoded, it should return string "Fake" which is certificate subject. How to do it in perl.


Solution

  • This is easy to decode manually:

    Just to double-check, the output of perl -e "print pack qw/H* 300f310d300b0603550403130446616b65/" | openssl asn1parse -inform DER is:

        0:d=0  hl=2 l=  15 cons: SEQUENCE          
        2:d=1  hl=2 l=  13 cons: SET               
        4:d=2  hl=2 l=  11 cons: SEQUENCE          
        6:d=3  hl=2 l=   3 prim: OBJECT            :commonName
       11:d=3  hl=2 l=   4 prim: PRINTABLESTRING   :Fake
    

    Here is an ad hoc parser:

    $_ = pack('H*', '300f310d300b0603550403130446616b65');
    s@^\x30.\x31.\x30.\x06(.)@@s or die;
    length >= ord($1) or die;
    substr($_, 0, ord($1)) = "";
    s@^\x13.@@s or die;
    print "$_\n";  #: Fake
    

    Let's try to parse it with the Perl DER parser library Convert::ASN1. For that we need type definitions for the ->prepare(...) method. Let's try to guess the types.

    I copy-pasted 300f310d300b0603550403130446616b65 to an online ASN.1 decoder: https://lapo.it/asn1js/#MA8xDTALBgNVBAMTBEZha2U , with the following result:

    Certificate SEQUENCE (1 elem)
      tbsCertificate TBSCertificate [?] SET (1 elem)
        serialNumber CertificateSerialNumber [?] SEQUENCE (2 elem)
          OBJECT IDENTIFIER 2.5.4.3 commonName (X.520 DN component)
          PrintableString Fake
    

    The keywords like tbsCertificate and CertificateSerialNumber pointed me to https://www.rfc-editor.org/rfc/rfc5280, but I noticed that the types above were autodetected incorrectly, because CertificateSerialNumber is an INTEGER there. By looking at the RFC document, I guessed these types:

    RDNSequence = SEQUENCE (1 elem)
      RelativeDistinguishedName = SET (1 elem)
        AttributeTypeAndValue = SEQUENCE (2 elem)
          type OBJECT IDENTIFIER = 2.5.4.3 commonName (X.520 DN component)
          value PrintableString = Fake
    

    The corresponding ASN.1 type definitions extracted from the RFC document https://www.rfc-editor.org/rfc/rfc5280:

    RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
    
    RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
    
    AttributeTypeAndValue ::= SEQUENCE {
         type     AttributeType,
         value    AttributeValue  
    }
    
    AttributeType ::= OBJECT IDENTIFIER
    
    AttributeValue ::= ANY -- DEFINED BY AttributeType   
    

    Let's try to feed these types to the ->prepare(...) method of the Perl module Convert::ASN1. With a Google search for Convert::ASN1 RDNSequence, I've found this example code: https://github.com/gbarr/perl-Convert-ASN1/blob/master/examples/x509decode , based on this and the Convert::ASN1 documentation I wrote the following Perl script:

    use warnings;
    use strict;
    use Convert::ASN1;  # sudo apt-get install libconvert-asn1-perl
    use Data::Dumper qw();
    
    my $asn = Convert::ASN1->new;
    die "fatal: ASN.1 prepare: " . $asn->error unless $asn->prepare(q<
    -- ASN.1 from RFC2459 and X.509(2001)
    -- Adapted for use with Convert::ASN1
    
    -- attribute data types --
    
    Attribute ::= SEQUENCE {
            type                    AttributeType,
            values                  SET OF AttributeValue
                    -- at least one value is required -- 
            }
    
    AttributeType ::= OBJECT IDENTIFIER
    
    AttributeValue ::= DirectoryString  --ANY 
    
    AttributeTypeAndValue ::= SEQUENCE {
            type                    AttributeType,
            value                   AttributeValue
            }
    
    
    -- naming data types --
    
    Name ::= CHOICE { -- only one possibility for now 
            rdnSequence             RDNSequence                     
            }
    
    RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
    
    DistinguishedName ::= RDNSequence
    
    RelativeDistinguishedName ::= 
            SET OF AttributeTypeAndValue  --SET SIZE (1 .. MAX) OF
    
    
    -- Directory string type --
    
    DirectoryString ::= CHOICE {
            teletexString           TeletexString,  --(SIZE (1..MAX)),
            printableString         PrintableString,  --(SIZE (1..MAX)),
            bmpString               BMPString,  --(SIZE (1..MAX)),
            universalString         UniversalString,  --(SIZE (1..MAX)),
            utf8String              UTF8String,  --(SIZE (1..MAX)),
            ia5String               IA5String  --added for EmailAddress
            }
    >);
    
    my $asn_rdns = $asn->find('RDNSequence');
    my $der = $_ = pack('H*', '300f310d300b0603550403130446616b65');
    my $value = $asn_rdns->decode($der);
    die "fatal: ASN.1 decode: " . $asn_rdns->error unless $value;
    print Data::Dumper::Dumper($value);
    

    Output:

    $VAR1 = [
              [
                {
                  'value' => {
                               'printableString' => 'Fake'
                             },
                  'type' => '2.5.4.3'
                }
              ]
            ];
    

    Here you go.