Parsing SSH Private Keys, Public Keys, and Certificates

May 2024

Certificate based authentication is considered the most secure for SSH. It works as follows:

(Note that sometimes the User CA and the Host CA are the same thing.)

Issuig User Certificate

For User CA to issue a user certificate, the user first generates a private-public key pair, and sends the public key to the CA to sign it. The following command can generate a user key:

ssh-keygen -t rsa -b 1024 -f user-key
# t: type of key
# b: number of bits in the key (using 1024 to keep things simple, use a higher value in production)
# f: output file name

This will generate two files:

  • User private key: user-key
  • $ head -n4 user-key
    -----BEGIN OPENSSH PRIVATE KEY-----
    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
    NhAAAAAwEAAQAAAIEAzGkaoL8YoyC12Yiy1vxZ9jSv5AroqegbgOAMlJRuK0dTET5YN+ot
    z8V0/niJ6LRwDPu/3A1R2O9c76ZLdTqAJ1fAfcBUPAm/MwqN4+sDCK3raJvW93CKnmdQKn    
    
  • User public key: user-key.pub
  • $ cat user-key.pub
    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDMaRqgvxijILXZiLLW/Fn2NK/kCuip6BuA4AyUlG4rR1MRPlg36i3PxXT+eInotHAM+7/cDVHY71zvpkt1OoAnV8B9wFQ8Cb8zCo3j6wMIretom9b3cIqeZ1Aqd3UuFsYi0Yyavgm2zt4yPqhkBtDMO0a0Zr0sBYBl+pYv23hVNw== ubuntu@dev    
    

The CA can sign the public key of the user as follows:

ssh-keygen -s user-ca -I user@pwnlogs.dev -n user,guest -V +90d -O source-address=10.10.10.10 user-key.pub
# -s: CA private key - generate it using `ssh-keygen -t rsa -b 4096 -f user-ca -C user-ca`
# -I: Identity of the certificate owner
# -n: Principals (names that can be used to authenticate)
# -V: Validity
# -O: Option
# user-key.pub: name of the input file (public key of the user)

Parsing User Public Key

If you take the base64 encoded part of a public key, it will look like this:

$ cat user-key.pub | cut -d' ' -f2 | base64  -d | xxd | head -n5
00000000: 0000 0007 7373 682d 7273 6100 0000 0301  ....ssh-rsa.....
00000010: 0001 0000 0181 0092 db9c c930 65b8 c6be  ...........0e...
00000020: 6a31 59b9 7f2f 4629 a849 57d8 5436 eee9  j1Y../F).IW.T6..
00000030: d23b fdc6 1322 c038 ab75 0e57 723b 5788  .;...".8.u.Wr;W.
00000040: e276 168a 3bc6 9564 47f3 988b 27da 2bda  .v..;..dG...'.+.

As per RFC 4253, this key has the following parts (RSA specific):

RSA public key: 

    string    "ssh-rsa"
    mpint     e (exponent)  
    mpint     n (modulus)

Different data types and their encodings are described under RFC 4251:

byte   : 1 byte
boolean: 1 byte
uint32 : 4 bytes in big endian format
uint64 : 8 bytes in big endian format
string : (uint32 = length of string)(actual string)
       : a string of length zero has only the `uint32` part, which is 0.
mpint  : multi-precision-int is stored as a string
       : 0 is stored as a string of zero bytes
list   : (uint32 = length of the entire list)(components of the list)

According to the above definitions, the public key is parsed as:

$ cat user-key.pub | cut -d' ' -f2 | base64  -d | xxd
00000000: 0000 0007 7373 682d 7273 6100 0000 0301  ....ssh-rsa.....
          <---A---> <-------B-------><---C----><-

A = 0000 0007         = 0x07                  = len of str = 0x07
B = 7373 682d 7273 61 = ssh-rsa               = actual str
C =   00 0000 03      = 0x03                  = len of mpint is 3 bytes

00000010: 0001 0000 0081 00cc 691a a0bf 18a3 20b5  ........i..... .
          -D-> <---E---> <------------------F----

D =   01 0001         = 0x010001 (bid endian) = 3 bytes of exponent of public key (e)
E = 0000 0081         = 0x00000081            = length of modulus (n)

00000020: d988 b2d6 fc59 f634 afe4 0ae8 a9e8 1b80  .....Y.4........
00000030: e00c 9494 6e2b 4753 113e 5837 ea2d cfc5  ....n+GS.>X7.-..
00000040: 74fe 7889 e8b4 700c fbbf dc0d 51d8 ef5c  t.x...p.....Q..\
00000050: efa6 4b75 3a80 2757 c07d c054 3c09 bf33  ..Ku:.'W.}.T<..3
00000060: 0a8d e3eb 0308 adeb 689b d6f7 708a 9e67  ........h...p..g
00000070: 502a 7775 2e16 c622 d18c 9abe 09b6 cede  P*wu..."........
00000080: 323e a864 06d0 cc3b 46b4 66bd 2c05 8065  2>.d...;F.f.,..e
00000090: fa96 2fdb 7855 37                        ../.xU7
          -------F------------------------------>

F = 00cc .... 7855 37 = 0xcc...37             = value of modulus (n)

The following script can parse RSA based public keys. The script has been written in a very descriptive manner so as to document the key format.

Parsing User Certificate

You can parse SSH certificates using:

ssh-keygen -L -f user-key-cert.pub

However, let's parse it manually. The user certificate has a similar structure, but slightly longer as it has more components:

$ cat user-key-cert.pub
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNza...Q8AFMPg8P0k67RwfkTsH4nQy2IGR4qxlNKpvxBBIhCk4wQ== ubuntu@dev

The structure of the certificate is mentioned in the OpenSSH source code itself. However, there are some ambiguity in the description as strings and lists are not distinguished in the structure. An improved description is given below:

RSA certificate

    string    "ssh-rsa-cert-v01@openssh.com"
    string    nonce
    mpint     e (exponent)
    mpint     n (modulus)
    uint64    serial
    uint32    type
    string    key id
    list      valid principals
    uint64    valid after
    uint64    valid before
    list      critical options
    list      extensions
    string    reserved
    string    signature key
    string    signature

The certiicate can be parsed as follows:

$ cat user-key-cert.pub | cut -d' ' -f2 | base64  -d | xxd
00000000: 0000 001c 7373 682d 7273 612d 6365 7274  ....ssh-rsa-cert
          <--len--> <---------certificate-type---

length of cert-type     = 0000 001c         = 0x1c

00000010: 2d76 3031 406f 7065 6e73 7368 2e63 6f6d  -v01@openssh.com
          ---certificate-type-------------------------->

certificate type        = ssh-rsa-cert-v01@openssh.com

00000020: 0000 0020 76f8 d5ea cc04 95c7 80ef 605e  ... v.........`^
          <--len--> <--------------------nonce-->

lenth of nonce          = 0000 0020 = 0x20

00000030: fc6e a761 149d 085d f4e8 a318 48ef f12d  .n.a...]....H..-
00000040: b48e c5c5 0000 0003 0100 0100 0000 8100  ................
          -nonce--> <--len--> <--e--><---len--><-

nonce = 76f8 .... 605e  = 0x76...5e
length of exponent (e)  = 0000 0003         = 0x03
exponent (e)            = 0100 01           = 0x10001
length of modulus (n)   = 00 0000 81        = 0x81

00000050: cc69 1aa0 bf18 a320 b5d9 88b2 d6fc 59f6  .i..... ......Y.
00000060: 34af e40a e8a9 e81b 80e0 0c94 946e 2b47  4............n+G
00000070: 5311 3e58 37ea 2dcf c574 fe78 89e8 b470  S.>X7.-..t.x...p
00000080: 0cfb bfdc 0d51 d8ef 5cef a64b 753a 8027  .....Q..\..Ku:.'
00000090: 57c0 7dc0 543c 09bf 330a 8de3 eb03 08ad  W.}.T<..3.......
000000a0: eb68 9bd6 f770 8a9e 6750 2a77 752e 16c6  .h...p..gP*wu...
000000b0: 22d1 8c9a be09 b6ce de32 3ea8 6406 d0cc  "........2>.d...
000000c0: 3b46 b466 bd2c 0580 65fa 962f db78 5537  ;F.f.,..e../.xU7
          ----modulus (n)----------------------->

modulus (n)             = 00 cc69 .... 5537 = 0xcc..37

000000d0: 0000 0000 0000 0000 0000 0001 0000 0010  ................
          <----- serial ----> <--type-> <--len-->

serial                  = 0000 ... 0000     = 0x0
type                    = 0000 0001         = 0x1
length of id            = 0000 0010         = 0x10

000000e0: 7573 6572 4070 776e 6c6f 6773 2e64 6576  user@pwnlogs.dev
          <-----------------id------------------>

id                      = user@pwnlogs.dev

000000f0: 0000 0011 0000 0004 7573 6572 0000 0005  ........user....
          <--len--> <--len--> <-name1-> <--len-->

length of valid principals (this is an array of names) = 0000 0011 = 0x11
length of 1st name      = 0000 0004         = 0x4
1st name                = user
length of 2nd name      = 0000 0005         = 0x5

00000100: 6775 6573 7400 0000 0066 58f6 6800 0000  guest....fX.h...
          <--name2---><----valid after---><-valid

2nd name                = guest
valid after             = 00 0000 ... 68    = 0x6658f668

00000110: 0066 cf9d b400 0000 2500 0000 0e73 6f75  .f......%....sou
          --before---><---len--><---len--><--opt1

valid before            =   00 0066 .... b4 = 0x66..b4
total length of critical options (this is an array of options) = 00 0000 25 = 0x25
length of option 1      = 00 0000 0e        = 0xe

00000120: 7263 652d 6164 6472 6573 7300 0000 0f00  rce-address.....
          ----opt1------------------><--len---><-

option 1                = source address (the value of this option is an array)
total length of source addresses list = 00 0000 0f = 0xf

00000130: 0000 0b31 302e 3130 2e31 302e 3130 0000  ...10.10.10.10..
          -len--><--------addr1------------> <---

length of 1st address   = 00 0000 0b        = 0xb
address 1               = 10.10.10.10

00000140: 0082 0000 0015 7065 726d 6974 2d58 3131  ......permit-X11
          len> <--len--> <---------extension-1---

length of extensions (this is an array of extensions) = 0000 0082 = 0x82
length of 1st extension = 0000 0015        = 0x15

00000150: 2d66 6f72 7761 7264 696e 6700 0000 0000  -forwarding.....
00000150: ---------extension-1------><--len---><-

1st extension            = permit-X11-forwarding
length of value of 1st extension = 00 0000 00 = 0x0 (=> there is no value. This is a flag)

00000160: 0000 1770 6572 6d69 742d 6167 656e 742d  ...permit-agent-
          -len--><----------------extension-2----

length of 2nd extension  = 00 0000 17       = 0x17

00000170: 666f 7277 6172 6469 6e67 0000 0000 0000  forwarding......
          --extension-2----------> <--len-->

2nd extension            = permit-agent-forwarding
length of value of 2nd extension = 0000 0000 = 0x0 (=> there is no value. This is a flag)

00000180: 0016 7065 726d 6974 2d70 6f72 742d 666f  ..permit-port-fo
00000190: 7277 6172 6469 6e67 0000 0000 0000 000a  rwarding........
000001a0: 7065 726d 6974 2d70 7479 0000 0000 0000  permit-pty......
000001b0: 000e 7065 726d 6974 2d75 7365 722d 7263  ..permit-user-rc
...
000005f0: 6534 aa6f c410 4884 2938 c1              e4.o..H.)8.

The last parts of the certificate are omitted for brevity. The following script can parse RSA based certificates entirely:

Parsing SSH Private Key

The structure of the SSH private key is again in the source code. Like the certificate, the description is not very clear. An updated description is as follows:

RSA private key:

    string  "openssh-key-v1" (called AUTH_MAGIC)
    uint32  0x00 (As if AUTH_MAGIC is NULL terminated)
    string  ciphername (="none" for unencrypted keys)
    string  kdfname [Key Derivation Function] (="none" for unencrypted keys)
    string  kdfoptions
    uint32  N = number of keys
    list    1st public key
    list    2nd public key
    ...     ...
    list    Nth public key
    encrypted(
        list    1st private key
        list    2st private key
        ...     ...
        list    Nst private key
    )

each publickey is a list with the following format:
    string  "ssh-rsa"
    mpint   e (exponent)
    mpint   n (modulus)

each private key is a list of its own with the following format:
    uint32  checkint
    uint32  checkint  (must match with previous check-int value)
    string  "ssh-rsa"
    mpint   n (modulus)
    mpint   e (exponent)
    mpint   d (private exponent)
    mpint   iqmp (inverse(Q Mod P))
    mpint   p (prime p)
    mpint   q (prime q)
    string  comment
    byte[n] padding

Notes:

The following script can parse unencrypted RSA private keys:


The end
Other Articles