Richard Towers

We’re going on a certificate hunt!

I’ve spent a lot of my career wrangling certificates. They’re used to secure traffic on the web, they’re used in authentication protocols like SAML, they’re used for signing and encrypting various other kinds of document.

Certificates can be represented / disguised in a few different formats, and some of them are easier to spot than others.

Recently I had to find an Adobe Reader Extensions certificate embedded in a PDF document, which pushed my certificate hunting skills to the limit. This is the story of that hunt.

Why are there certificates living in a PDF?

A particularly unpleasant legacy service at work uses PDF forms, which users have to fill in using Adobe Acrobat Reader. The form filling functionality is proprietary - you can only fill in PDF forms in Acrobat if the PDF was signed by a particular key issued by Adobe. Which keys are trusted to sign these PDFs is controlled by embedded certificates.

The certificate we were using was due to expire, so we updated it. I wanted to verify that the PDFs being produced were using the new certificate and not the old one (without waiting for the old one to expire and seeing if everything broke).

Part 1: strings

PDF documents are a combination of ASCII characters and binary data. On the hope that the certificate I was looking for might be encoded in ASCII, I ran the strings command on the PDF.

$ strings file.pdf
%PDF-1.7
41 0 obj
<</Filter/FlateDecode/First 23/Length 137/N 4/Type/ObjStm>>stream
R)i/P
``Zp
...

Unfortunately, there was nothing as obvious as -----BEGIN CERTIFICATE----- (which indicates a PEM formatted certificate). The most promising bit of the file looked like this:

<</Type/Sig/Filter/Adobe.PPKLite/SubFilter/adbe.pkcs7.detached/
... snip ...
Contents<308006092886f70d01010b0500306c
...snip about 12000 characters ...
00000000000000000000000000000000>

It’s not a certificate, but the <</Type/Sig bit suggests it might be a signature, which could well contain the certificate.

So perhaps there are some certificates hiding inside the big Contents<...> string.

Part 2: A fistful of octets

The structure of a certificate is defined by X.509, which is based on ASN.1.

This structure is usually encoded using DER, and sometimes further encoded using PEM.

Which is all pretty confusing.

To find a certificate shaped needle in our haystack of Contents<30800609288... we need to know a bit about what to expect though.

Certificates always begin with an ASN.1 Sequence, which (in hexadecimal format) is represented by the number 30. The next octet (two hexadecimal characters) specifies the length of the sequence.

In DER, length encoding can be “short form” or “long form”.

Certificates are always longer than 127 bits, so the length will always be in long form. Certificates are also always longer than 256 bits, and almost always shorter than 65,536 bits, which means DER needs two octect to specify the sequence length.

This means the next octet will be 82, and that the next two octets will depend on the length of the certificate.

So we know the beginning of the certificate will be 3082.....

This is why PEM certificates always start with MII (which you might have noticed if you work with certificates a lot).

$ xxd -r -p <<< '3082' | base64    
MII=

grepping our haystack for 3082 followed by four hex characters yields a few candidates:

$ grep -a -o '3082....' file.pdf 
308205a4
3082038c
30820222
3082020a
30820609
308203f1
30820122
3082010a
30820198
308206a1
30820489
30820222
3082020a
30820130
3082020c

Encouraging, but we only expect to see three or four certificates (assuming the whole chain is included), so this is too many matches. Can we be more precise?

Part 3: A few octets more

Fortunately, the first bit of the ASN.1 Sequence which makes up a certificate is another, nested ASN.1 sequence. And this one is also (almost) always between 256 and 65,536 bits long. By the same reasoning as above, we can tell that the two octets following the four we already worked out will also be 3082. So the pattern we should look for is 3082....3082:

$ grep -a -o '3082....3082' file.pdf 
308205a43082
308206093082
308206a13082

Now we’re down to just three matching prefixes - could these be the starts of our certificates?

Part 4: The good, the bad, and the ugly

We’ve worked out the starts of our certificates, but we haven’t worked out where they end. Fortunately, openssl is happy to pick up the first certificate it sees and ignore everything after it, so we can actually skip that bit and jump straight to trying to parse the (maybe) certificates.

We’ll need to decode the hexadecimal into binary (using xxd -r -p), and then pass them to openssl (with openssl x509 -inform DER):

Taking the first prefix we found:

$ grep -a -o '308205a43082[0-9a-f]*' file.pdf | xxd -r -p | openssl x509 -inform DER
-----BEGIN CERTIFICATE-----
MIIFpDCCA4ygAwIBAgIQXfEvX1enw+GwAtiTJwzd4TANBgkqhkiG9w0BAQsFADBs
MQswCQYDVQQGEwJVUzEjMCEGA1UEChMaQWRvYmUgU3lzdGVtcyBJbmNvcnBvcmF0
... snip ...
7quYkGyLWtTtZoB5J1b7OYUraMDuqG1s39jMchHjda1GqOwsBWxDqC4HqtdY7TK2
ofZLvqTHvT4=
-----END CERTIFICATE-----

TADA! And indeed this works for the other two prefixes - there are three certificates in this PDF, a root, an intermediate and a leaf.

Conclusion

This general technique of looking for hexadecimal streams matching /3082....3082/ should be a fairly robust way of finding certificates in binary files. It might occasionally trip up if a certificate is very long or very short, and it might find some false positives (particularly if there’s other ASN.1 data in the file).

In our case we were lucky that the bit where the certificate lived was already hex encoded, but it could probably still be spotted in the hexdump of a binary file.

If the certificate were base64 encoded instead of hex encoded, you could use the same trick with the pattern /MI....CC/ (or something like that).

Bonus: PKCS7 and parsing ASN.1 with openssl

After I’d worked all of this out, I realised that the whole Contents<30800609288...> string was itself in ASN.1 format, specifically PKCS #7. This means there are a couple of easier ways to parse it (but which might not work in other files).

The prefix 3080 means “a sequence of indeterminate length” (whereas 3082 means “a sequence of length specified in the next two octets”).

Parsing the whole thing as PKCS #7:

$ grep -a -o 'Contents<[0-9a-f]*' file.pdf | cut -d '<' -f 2 | xxd -r -p | openssl pkcs7 -inform DER -noout -print
PKCS7: 
  type: pkcs7-signedData (1.2.840.113549.1.7.2)
  d.sign: 
    version: 1
    md_algs:
        algorithm: sha1 (1.3.14.3.2.26)
        parameter: NULL
    contents: 
      type: pkcs7-data (1.2.840.113549.1.7.1)
      d.data: <ABSENT>
... snip ...

(This includes the certificate data I was looking for, albeit in a slightly inconvenient format)

Or parsing as ASN.1:

$ grep -a -o 'Contents<[0-9a-f]*' file.pdf | cut -d '<' -f 2 | xxd -r -p | openssl asn1parse -inform DER
    0:d=0  hl=2 l=inf  cons: SEQUENCE          
    2:d=1  hl=2 l=   9 prim: OBJECT            :pkcs7-signedData
   13:d=1  hl=2 l=inf  cons: cont [ 0 ]        
   15:d=2  hl=2 l=inf  cons: SEQUENCE          
   17:d=3  hl=2 l=   1 prim: INTEGER           :01
   20:d=3  hl=2 l=  11 cons: SET               
   22:d=4  hl=2 l=   9 cons: SEQUENCE          
   24:d=5  hl=2 l=   5 prim: OBJECT            :sha1
   31:d=5  hl=2 l=   0 prim: NULL              
   33:d=3  hl=2 l=inf  cons: SEQUENCE