A journey into verifying signatures on x.509 certificates

Edouard Buschini on 2017-05-13

IT is a strange world. It makes you obsessed with “problems” that don’t exist just for the sake of curiosity.

Well it happened to me, when I should have had a relaxing time.. On a Saturday..

I always have been interested in cryptography since I started computer science. But I’m not an expert at all, this post is just about fun into analyzing how digital signatures could be verified by your browser using publicly available data: x.509 certificates.

Certificates are at the heart of establishing a secure connection to a server. It’s like some bank representative asking you on the phone, personal questions to validate your identity and therefor establishing some trust between you and she — Actually, this analogy is an awful process, it never proves you really are the person you are pretending to be. That’s where certificates come handy, it uses mathematical proofs to make sure you are talking to the bank securely. They are distributed in the x.509 format which encapsulates the public key among other things— if you don’t know what public/private key is, I highly encourage you, to check it out.

Let’s begin..

First, some basic checks.

Here are two screenshots. The first is what the browser consider a valid certificate. The second is invalid.

Good news, Medium.com’s certificate is valid!
Self signed certificates are not valid by default.

Not has been verified by a third party? Valid certificate? Wow that’s bold claims! How do you know for sure? Well a good part comes from digital signatures.

Since I’m not a cryptographer and won’t be able to understand a thing, I’m going to use — like us mortals — OpenSSL.

Step one: Save the whole chain’s certificates.

openssl s_client -connect medium.com:443 -showcerts < /dev/null

You’ll see two certificates. Save the first one in medium.com.crt and the second one in root.crt. The format used is PEM. It includes the BEGIN CERTIFICATE and END CERTIFICATE delimiters — don’t forget to include those!

Why save two certificates? Because all together they form a chain, the certificate is signed by its parent’s certificate’s private key, thus validating the children’s certificate, until the parent is a certificate installed on the computer: therefor trusted. A certificate chain is said trusted, if and only if all certificates are validated by its parent. A chain can have one certificate — it is said self signed — or multiple — usually 2 or 3.

Step two: Extract the public key from root.crt.

openssl x509 -in root.crt -noout -pubkey > root.key

Step three: Extract the signature from medium.com.crt.

Use this to see what the signature looks like:

openssl x509 -noout -text -in medium.com.crt

Signature is at the end:

Signature Algorithm: sha256WithRSAEncryption

It tells us, the signature is encrypted using RSA and the hash has been computed using sha256.

One way to extract the signature is using dd. But first we need where to look to extract the raw data. The certificate must be in DER format then we need to parse it using ans.1.

It goes like this:

openssl x509 -in medium.com.crt -outform der | openssl asn1parse -inform der

The output is messy, don’t worry we’ll go through it, it’s easy.

    0:d=0  hl=4 l=1901 cons: SEQUENCE
    4:d=1  hl=4 l=1621 cons: SEQUENCE
    1629:d=1  hl=2 l=  13 cons: SEQUENCE
    1631:d=2  hl=2 l=   9 prim: OBJECT   :sha256WithRSAEncryption
    1642:d=2  hl=2 l=   0 prim: NULL
    1644:d=1  hl=4 l= 257 prim: BIT STRING

According to RFC 3280 section 4.1 the asn.1 config looks like:

Certificate  ::=  SEQUENCE  {
        tbsCertificate       TBSCertificate,
        signatureAlgorithm   AlgorithmIdentifier,
        signatureValue       BIT STRING  }

What does it tell us? Well d= is the depth, hl=is the header length and l=is the content length. So d=0 is the root object, the next d=1is the first child object until the next d=1 and so on. Looking at the x.509 asn.1 configuration, signatureValue is the last child from the root — so the last d=1.

Extracting the signature goes like this:

openssl x509 -in medium.com.crt -outform der \
| dd skip=$((4+4+1621+2+13+4+1)) bs=1 \
> medium.com.sig

What’s that is this4+4+1621+2+13+4+1 number? Turn’s out that’s the RSA signature! In order to extract it we had to tell dd to discard a lot of data: the headers of each objects and the objects — tbsCertificate, signatureAlgorith and the signatureValue header. Go ahead and match the numbers by yourself!

Wait a second, I don’t see a 1. Did you lie to me? Of course not! Looking closely at the content length: it’s 257 bytes long. Or the RSA signature should be only 256 bytes long. The leading byte of BIT STRING is used for padding. Meaning if the content is not a multiple of 8 bits this byte will make up for it. Since the leading byte is 0x00 we can safely discard it.

If you want to make sure, check for yourself:

openssl x509 -in medium.com.crt -outform der \
| dd skip=$((4+4+1621+2+13+4)) count=1 bs=1 \
| xxd -ps -c 1

Step four: Decrypt the signature.

We’re going to use rsautl:

openssl rsautl -verify -pubin -inkey root.key -in medium.com.sig | hexdump

Doesn’t looks like a sha256 hash! Sigh. Mhm what format could it be? asn.1 maybe?

openssl rsautl -verify -pubin -inkey root.key -in medium.com.sig \
| openssl asn1parse -inform der
    0:d=0  hl=2 l=  49 cons: SEQUENCE
    2:d=1  hl=2 l=  13 cons: SEQUENCE
    4:d=2  hl=2 l=   9 prim: OBJECT            :sha256
   15:d=2  hl=2 l=   0 prim: NULL
   17:d=1  hl=2 l=  32 prim: OCTET STRING      [HEX DUMP]:FCCA7EA7FC1DBB08F608B55A198CE0323D6C8A8103E9B9E9FCA65068070910EE

Bingo! 32bits OCTET STRING looks like pretty much what we could need!

Here is the final command for one liner’s lovers:

openssl x509 -in medium.com.crt -outform der \
| dd skip=$((4+4+1621+2+13+4+1)) bs=1 2>/dev/null \
| openssl rsautl -verify -pubin -inkey root.key -in medium.com.sig \
| dd skip=$((2+2+13+2)) bs=1 2> /dev/null \
| xxd -ps -c 32

And the sha256 hash to verify is: fcca7ea7fc1dbb08f608b55a198ce0323d6c8a8103e9b9e9fca65068070910ee!

Step five: verify the hash.

Back to our RFC3280 section — which by the way, contained the answer to step 4:

The signatureValue field contains a digital signature computed upon
the ASN.1 DER encoded tbsCertificate.  The ASN.1 DER encoded
tbsCertificate is used as the input to the signature function.

So the value is the hash of the tbsCertificate — tbs meaning: to be signed. Which makes sense because you can’t sign the entire certificate containing the signature.. Which came first? The chicken or the egg?

For the moment of truth we are going to need dd again. This time we are going to extract the tbsCertificate. Now that you are asn1 extractors experts, the next command is self explanatory.

openssl x509 -outform der -in medium.com.crt \
| dd bs=1 count=$((4+1621)) skip=4 \
| shasum -a 256

Victory! Our journey is finally done my friends.

We successfully verified thatmedium.com's certificate was signed by a root certificate that we fully trust. We can now proceed and log in!

Woah, that was a lot of steps! Good things computers are fast!

To sum up:

Step one: Save the certificates. Step two: Extract the public key of the root's certificate. Step three: Extract the signature. Step four: Decrypt the signature. Step five: Verify the hash.

Thank you for reading, I hope you learned and enjoyed it as I did. I’ll try to write more article on stuff I enjoy finding and understanding.