Parsing SSH Private Keys, Public Keys, and Certificates
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
$ 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 | xxd00000000: 0000 0007 7373 682d 7273 61 00 0000 03 01 .... 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 | xxd00000000: 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 01 00 0000 81 00 .... .... ... .... . -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 74 00 0000 0066 58f6 68 00 0000 guest ....fX.h ... <--name2---> <----valid after---> <-valid 2nd name = guest valid after = 00 0000 ... 68 = 0x6658f668 00000110: 0066 cf9d b4 00 0000 25 00 0000 0e 73 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 73 00 0000 0f 00 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 0b 31 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 67 00 0000 00 00 -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 17 70 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 public key is a list on elements, and the first
element in the list is a string.
i.e. After
N = number of keys , there will be auint32 specifying the size of the public key, and anotheruint32 specifying the size of the stringssh-rsa . - The private keys will not be encrypted if the
ciphertname isnone . checkint in the private key helps to verify if the decryption is successful. When the decryption of the private keys are successful, bothcheckint values will be the same.-
The
padding at the end of a private key ensures that the size of the private key is always a multiple of the cipher block size. Also, this padding will be bytes0x01 ,0x02 ,0x03 , etc. in order.
The following script can parse unencrypted RSA private keys:
The end