Friday, May 29, 2020

A practical analysis of the SSH Certificate format

I've been messing around a wee bit with SSH certificates, and while the specification is fairly easy to read, reading the actual format was not quite as much so and there was quite a bit of trial and error involved. For the sake of posterity, I thought I'd give a rundown of a sample SSH certificate and how to parse it.

Why use SSH Certificates?
The default use for SSH is with a username/password combo. Why relying purely on username/password is a poor idea is self evident, as passwords are commonly reused/stolen, so the next step up is using SSH keys so that an attacker needs to have also acquired the SSH private key, as well as the password used to protect the key pair. SSH keys can easily be produced using OpenSSH:
$ ssh-keygen -b 384 -t ecdsa -f id_ec384 
The common procedure is then to send your public key off to your favorite IT-admin, who adds it to a list of known keys. If you're an organization of 8000 employees all working from home, your IT-admin is going to be sad and depressed indeed. 

Without going into details in the complete setup (that's a separate blog post, and can be found plenty elsewhere), what your IT-admin can do instead is set up a CA. From there they'll set up some form of RA, and what you instead can do is send in your public key, have it signed and receive a certificate back. This certificate is now your SSH passcode - the difference being that the IT-admin never had to in any way interface directly with your public key. As long as the signature is valid and the certificate hasn't expired, you're good to go.

The Specification
The specification for an RSA certificate is as follows:
78:     string    "ssh-rsa-cert-v01@openssh.com"
79:     string    nonce
80:     mpint     e
81:     mpint     n
82:     uint64    serial
83:     uint32    type
84:     string    key id
85:     string    valid principals
86:     uint64    valid after
87:     uint64    valid before
88:     string    critical options
89:     string    extensions
90:     string    reserved
91:     string    signature key
92:     string    signature
SSH certificates have no similarities to x509, as x509 was by the designers deemed to be overly complex for the purpose. The SSH certificate format also provides some nice perks that x509 does not. So, let's get into it. 

Breaking it down
A sample SSH certificate looks like this.
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg5XCeFvniKnNbRZaclB83kTyF8zITCde4uRrv4mqesn4AAAADAQABAAABAQDiFqTBdKOWPeBeP1PiKSVy8ilfNChu5/6Z3iXu3Rdtg5ozu98IoAl4MtlklDdUDzvFkB+VPD/9gqHPKK8fTOhqgUPGoiCeZP3Ktr6NR53xd1QPQDeBvOMiYkPqQXziCQiVyL1WFzN616szrxsJ1Ni7WCHXcMTKOMruLv4es8FfB03wGDbBKVzMwo0JuZCicGg2pg/o8n9BPlzW6CvjUkmvUO3ycGKibPPFkiDgyuYynIbkMcmdShhY3XSOGB4UPeA3U4OiH6Z+09K9LqogWIcjxJeK0tObORd9QnQ4k1ba+/bbEfnFIQwLyzqXXPsPQ0Ud9upxVHD1lezRNE1DAIr9AAAAAAAAAAAAAAABAAAABWVqYmNhAAAAFAAAAAZlamJjYTAAAAAGZWpiY2ExAAAAAF7Q0HgAAAAAYLCytwAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQC8ajEtOvKxSPYSRL+A1y7Ye0baYHaR65KkzaT3U5XugHKHusvVPsDlRfSl598TsMjPQhbJt0O1SefMvXCbqdj776PWok5I1ScnLKJWRKzreeslEJZdcKTOUoT9Y5sg/LxC3xXwhIz+yLm8SbvQ7yQvPMmmlg5ldwccC8/0cua/25Vrjm0JhRjgxny65s2bNkClXXLmtevhvlQ7rXMQhpGmg5th156Ny/BUac7CQPEnDkRkhfsH8zKuh0NX19Y/93bwLsI7z+zP7CJJ11C0CpZl5yi2/8vqZUkufjRu/TH78EnLdCE/bkKcn0yyahG5BTh9dInrSclgCSiEPOYojvzjAAABFAAAAAxyc2Etc2hhMi0yNTYAAAEAj4/d9pKGGReo7B1ZbVBtHD9ftBfa5fjyPnuM8qbEMtYWgbx/MlCqVf2CL6VfJe2lvsg4ZZWvHyq0XBucFQqVHMekHJ71CymAs5/boGF4efLo56Ck6FF7tqM4dYmcO0aWRRQt3DyC24bLUZ4ZcdkyAgEm4EbTj9nPyvzbR57VsP/p+6WlszrGAHPwBlZ6my0g3cwPJSxwdV0USfMUIWooMIXLS7ocVZ7a8y+HF6qC2FGYIXYSgJuBaG5jlfXOojfKwA4tzwdy3ZziHxW50WDRkPrw6ZnXeRantJfMOhJ7ol9NSt1WcdkiAHqzDbEUpOz9UF0aSwxtIQYI7ONgl29a7w== Mike's Certificate
We can see right away a nice perk, the certificate begins by telling us what's going to be inside, so off the ball we can figure out how we're going to decode it. The astute of you will notice that the main body of the certificate (between the prefix declaring the type and the comment at the end) looks a lot like Base64, which it very correctly is. If you've seen a certificate or two you'll also notice that every certificate of the same type starts with the same text, and that's because the first characters are (as stated in the specification above) the certificate type, in this case ssh-rsa-cert-v01@openssh.com. So, throw the certificate body into your favorite Base64 decoder and let's move on. 

The byte array follows a simple structure: first a four byte integer declaring the length of the structure, then the contents itself:
What you actually find inside the contents is up to the specification above, hence the importance of knowing beforehand what you're parsing. There can also be additional byte arrays of this format hidden within the payload, as we'll see in a moment. Parsing our way through the byte array, we're going to encounter the following objects, in this exact order:
Item    Description
 nonce   A 16 or 32 bit random byte array generated on the CA, simply to make collision attacks less likely.    
 e  The RSA exponent of the public key for this certificate. Note that EC keys will have the entire key in its own byte structure instead of having its values directly in the certificate. 
 n  The RSA modulus of the public key for this certificate.   
 serial  64 bit unsigned long with a serial number - up to the CA to determine the scheme
 type  32 bit unsigned integer containing a 1 (for user certificates) or a 2 (host certificates) 
 key id  A string filled in by the CA to identify the holder (end entity in x509 terms) owning the certificate.
 valid principals  Our first internal byte structure. The list of principals contains the accounts on the host machine which may be used with this certificate. An empty list will allow any user to make use of this certificate. The structure will look as follows:


 valid after  A 64 bit unsigned long containing the lower bounds of the certificate's validity. 
 valid before  A 64 bit unsigned long containing the upper bounds of the certificate's validity.
 critical options  These will only be relevant for user certificates (as there are no official options for host certificates) and may be force-command (to have the client execute a certain command on authentication to the host) and/or source-address in order to bind the client to a single IP. This is a key-value pair, so adds another layer of complexity to the byte structure


 extensions    The equivalent of x509 key usages, tells SSH what this certificate is authorized to do. There are extensions pre-defined in the specification, but it's up to you if you want to add your own as well. The structure is an array of strings much like principals


 reserved   A blank string, declared but not used at the moment.
 signature key  This is where it starts getting fun. OpenSSH allows the signature key to be of any OpenSSH usable format regardless of the key being signed, so there is for example no reason why a RSA CA can't sign a set of EC keys. The signature key is going to be parsed as a byte structure and then parsed up into its component. The structure of the key is defined in RFC 4253, and for an RSA key would be as follows:
 string "ssh-rsa"
mpint e
mpint n
The nice part is that the first thing the key does is declare itself, so you know exactly how to parse the rest of the structure. 


 signature   Last but not least, the last byte array you're going to read is the signature. This is going to be again an embedded byte structure, starting with a string declaring the signature algorithm, and after that the actual signature, the data for which is all of the preceding fields including the signing key. 


And that's pretty much it! Happy hacking, and I hope I've managed to make somebody's day a bit better.

Cheers,
Mike Agrenius Kushner
Product Owner, EJBCA