Anatomy of OpenSSH Key Revocation List (KRL) File

Introduction

When I started working on the SSH Certificate feature for Keyper, the question of certificate revocation and its verification came up. I searched but could not find a lightweight python library for this (aim is to keep the docker image smaller than 100MB). As a first iteration, I implemented this using Python subprocess and ssh-keygen. ssh-keygen is an awesome utility and considered a Swiss Army Knife for SSH Keys. The same utility helps you when you are either setting up a certificate authority (CA) or OpenSSH Key Revocation List (KRL) file.

KRL Basics

A KRL can be created using ssh-keygen:

[manish@getafix sshca]$ ssh-keygen -k -f ca_krl

[manish@getafix sshca]$ ls -l
total 4
-rw-r--r-- 1 manish manish 44 Nov  5 11:09 ca_krl
[manish@getafix sshca]$

A Key or a Certificate is revoked by adding them to the KRL using ssh-keygen:

[manish@getafix sshca]$ ls -l
total 12
-rw-r--r-- 1 manish manish   44 Nov  5 11:09 ca_krl
-rw-r--r-- 1 manish manish 2009 Nov  5 11:13 id_rsa-cert.pub
-rw-r--r-- 1 manish manish  568 Nov  5 11:13 id_rsa.pub

[manish@getafix sshca]$ ssh-keygen -k -u -f ca_krl id_rsa.pub
Revoking from id_rsa.pub

[manish@getafix sshca]$ ssh-keygen -k -u -f ca_krl id_rsa-cert.pub
Revoking from id_rsa-cert.pub
[manish@getafix sshca]$ 

A Key or a Certificate revocation can be checked using ssh-keygen. The output is as follows when the Key/Certificate is not in the KRL:

[manish@getafix sshca]$ ssh-keygen -Q -f ca_krl id_rsa.pub
id_rsa.pub (manish@getafix): ok

[manish@getafix sshca]$ ssh-keygen -Q -f ca_krl id_rsa-cert.pub
id_rsa-cert.pub (manish@getafix): ok
[manish@getafix sshca]$ 

And, as follows when Key/Certificate is in the KRL:

[manish@getafix sshca]$ ssh-keygen -Q -f ca_krl id_rsa.pub
id_rsa.pub (manish@getafix): REVOKED

[manish@getafix sshca]$ ssh-keygen -Q -f ca_krl id_rsa-cert.pub
id_rsa-cert.pub (manish@getafix): REVOKED
[manish@getafix sshca]$ 

ssh-keygen limitation

Although ssh-keygen can revoke keys or certificates using their fingerprint or serial number, it needs full Key or the Certificate for the KRL verification. As a result, when using Keyper, each SSH server needs to be configured to send Public Key or the Certificate (configured using %k and %t in the sshd_config file). There is always an option to periodically copy the KRL file to each SSH server so that it performs local KRL lookup. I was not happy with sending the full Key or the Certificate as part of the API call during each authentication. But could neither find a way to perform KRL lookup using fingerprint or serial number using ssh-keygen nor find a lightweight Python library for it. So, I decided to write a KRL lookup myself in python. This post is about what I learned while doing this.

man ssh-keygen defines OpenSSH format Key Revocation Lists (KRLs) as “binary files specify keys or certificates to be revoked using a compact format, taking as little as one bit per certificate if they are being revoked by serial number.”

KRL Anatomy

To understand its internal structure I started with the OpenSSH source code. File krl.c has the following relevant definition:

/*
 * Trees of revoked serial numbers, key IDs and keys. This allows
 * quick searching, querying and producing lists in canonical order.
 */

/* Tree of serial numbers. XXX make smarter: really need a real sparse bitmap */
struct revoked_serial {
        u_int64_t lo, hi;
        RB_ENTRY(revoked_serial) tree_entry;
};
static int serial_cmp(struct revoked_serial *a, struct revoked_serial *b);
RB_HEAD(revoked_serial_tree, revoked_serial);
RB_GENERATE_STATIC(revoked_serial_tree, revoked_serial, tree_entry, serial_cmp);

/* Tree of key IDs */
struct revoked_key_id {
        char *key_id;
        RB_ENTRY(revoked_key_id) tree_entry;
};
static int key_id_cmp(struct revoked_key_id *a, struct revoked_key_id *b);
RB_HEAD(revoked_key_id_tree, revoked_key_id);
RB_GENERATE_STATIC(revoked_key_id_tree, revoked_key_id, tree_entry, key_id_cmp);

/* Tree of blobs (used for keys and fingerprints) */
struct revoked_blob {
        u_char *blob;
        size_t len;
        RB_ENTRY(revoked_blob) tree_entry;
};
static int blob_cmp(struct revoked_blob *a, struct revoked_blob *b);
RB_HEAD(revoked_blob_tree, revoked_blob);
RB_GENERATE_STATIC(revoked_blob_tree, revoked_blob, tree_entry, blob_cmp);

/* Tracks revoked certs for a single CA */
struct revoked_certs {
        struct sshkey *ca_key;
        struct revoked_serial_tree revoked_serials;
        struct revoked_key_id_tree revoked_key_ids;
        TAILQ_ENTRY(revoked_certs) entry;
};
TAILQ_HEAD(revoked_certs_list, revoked_certs);

struct ssh_krl {
        u_int64_t krl_version;
        u_int64_t generated_date;
        u_int64_t flags;
        char *comment;
        struct revoked_blob_tree revoked_keys;
        struct revoked_blob_tree revoked_sha1s;
        struct revoked_blob_tree revoked_sha256s;
        struct revoked_certs_list revoked_certs;
};

I started with struct ssh_krl and after spending a couple of hours trying to read and understand the OpenSSH code, my eyes were glazing. So, I went back to the internet search to see if anyone has already figured this out. I found this page.

KRL File Format

This describes the key/certificate revocation list format for OpenSSH.

1. Overall format

The KRL consists of a header and zero or more sections. The header is:

#define KRL_MAGIC   0x5353484b524c0a00ULL  /* "SSHKRL\n\0" */
#define KRL_FORMAT_VERSION  1

  uint64  KRL_MAGIC
  uint32  KRL_FORMAT_VERSION
  uint64  krl_version
  uint64  generated_date
  uint64  flags
  string  reserved
  string  comment

Where "krl_version" is a version number that increases each time the KRL
is modified, "generated_date" is the time in seconds since 1970-01-01
00:00:00 UTC that the KRL was generated, "comment" is an optional comment
and "reserved" an extension field whose contents are currently ignored.
No "flags" are currently defined.

Following the header are zero or more sections, each consisting of:

  byte  section_type
  string  section_data

Where "section_type" indicates the type of the "section_data". An exception
to this is the KRL_SECTION_SIGNATURE section, that has a slightly different
format (see below).

The available section types are:

#define KRL_SECTION_CERTIFICATES    1
#define KRL_SECTION_EXPLICIT_KEY    2
#define KRL_SECTION_FINGERPRINT_SHA1    3
#define KRL_SECTION_SIGNATURE     4
#define KRL_SECTION_FINGERPRINT_SHA256    5

2. Certificate section

These sections use type KRL_SECTION_CERTIFICATES to revoke certificates by
serial number or key ID. The consist of the CA key that issued the
certificates to be revoked and a reserved field whose contents is currently
ignored.

  string ca_key
  string reserved

Where "ca_key" is the standard SSH wire serialisation of the CA's
public key. Alternately, "ca_key" may be an empty string to indicate
the certificate section applies to all CAs (this is most useful when
revoking key IDs).

Followed by one or more sections:

  byte  cert_section_type
  string  cert_section_data

The certificate section types are:

#define KRL_SECTION_CERT_SERIAL_LIST  0x20
#define KRL_SECTION_CERT_SERIAL_RANGE 0x21
#define KRL_SECTION_CERT_SERIAL_BITMAP  0x22
#define KRL_SECTION_CERT_KEY_ID   0x23

2.1 Certificate serial list section

This section is identified as KRL_SECTION_CERT_SERIAL_LIST. It revokes
certificates by listing their serial numbers. The cert_section_data in this
case contains:

  uint64  revoked_cert_serial
  uint64  ...

This section may appear multiple times.

2.2. Certificate serial range section

These sections use type KRL_SECTION_CERT_SERIAL_RANGE and hold
a range of serial numbers of certificates:

  uint64  serial_min
  uint64  serial_max

All certificates in the range serial_min <= serial <= serial_max are
revoked.

This section may appear multiple times.

2.3. Certificate serial bitmap section

Bitmap sections use type KRL_SECTION_CERT_SERIAL_BITMAP and revoke keys
by listing their serial number in a bitmap.

  uint64  serial_offset
  mpint revoked_keys_bitmap

A bit set at index N in the bitmap corresponds to revocation of a keys with
serial number (serial_offset + N).

This section may appear multiple times.

2.4. Revoked key ID sections

KRL_SECTION_CERT_KEY_ID sections revoke particular certificate "key
ID" strings. This may be useful in revoking all certificates
associated with a particular identity, e.g. a host or a user.

  string  key_id[0]
  ...

This section must contain at least one "key_id". This section may appear
multiple times.

3. Explicit key sections

These sections, identified as KRL_SECTION_EXPLICIT_KEY, revoke keys
(not certificates). They are less space efficient than serial numbers,
but are able to revoke plain keys.

  string  public_key_blob[0]
  ....

This section must contain at least one "public_key_blob". The blob
must be a raw key (i.e. not a certificate).

This section may appear multiple times.

4. SHA1/SHA256 fingerprint sections

These sections, identified as KRL_SECTION_FINGERPRINT_SHA1 and
KRL_SECTION_FINGERPRINT_SHA256, revoke plain keys (i.e. not
certificates) by listing their hashes:

  string  public_key_hash[0]
  ....

This section must contain at least one "public_key_hash". The hash blob
is obtained by taking the SHA1 or SHA256 hash of the public key blob.
Hashes in this section must appear in numeric order, treating each hash
as a big-endian integer.

This section may appear multiple times.
...

KRL Internals in action

The above clarified a lot. However, I still wasn’t clear about how would a parser figure the length of any string in the KRL file? (for e.g. string section_data) I decided to start looking into the KRL file itself. I started with a freshly generated KRL file.

[manish@getafix sshca]$ ssh-keygen -k -f ca_krl

[manish@getafix sshca]$ hexdump -C ca_krl
00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97 00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00              |............|
0000002c
[manish@getafix sshca]$ 

It was pretty straightforward to parse header values:

KRL_MAGIC:

00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97 00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00              |............|

KRL_FORMAT_VERSION:

00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97 00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00              |............|

krl_version:

00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00   |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97 00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00              |............|

generated_date:

00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97  00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00              |............|

flags:

00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97 00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00              |............|

But 00 00 00 00 00 00 00 00 for reserved and comment confused me.

I added a key to the KRL hoping that may clarify things further. And, clarify it did!

Notice 02 right after reserved and comment:

[manish@getafix sshca]$ ssh-keygen -k -u -f ca_krl id_rsa.pub
Revoking from id_rsa.pub

[manish@getafix sshca]$ hexdump -C ca_krl 
00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97 00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 02 00 00 01  |................|
00000030  9b 00 00 01 97 00 00 00  07 73 73 68 2d 72 73 61  |.........ssh-rsa|
00000040  00 00 00 03 01 00 01 00  00 01 81 00 c0 bb 7c b3  |..............|.|
00000050  6f 37 88 c6 dc e5 dc df  1b 81 59 a1 9b 79 9d 4e  |o7........Y..y.N|
00000060  63 b7 d0 9d 7f f2 19 e4  23 7d 9c 36 9a 08 01 39  |c.......#}.6...9|
00000070  83 bf 93 a6 b5 e8 cb 78  21 89 49 e9 9b 52 15 a4  |.......x!.I..R..|
00000080  03 5a 4a cd 26 b3 ed 7d  46 7e 8d 1e 74 35 64 43  |.ZJ.&..}F~..t5dC|
00000090  23 70 0b 7c b3 a7 f6 6c  b8 0b ca 27 5f fd 56 de  |#p.|...l...'_.V.|
000000a0  7d 1e 53 90 49 b6 fc d6  0c b5 cc 71 2d 8b b7 27  |}.S.I......q-..'|
000000b0  4d 49 e0 f4 e6 1f ea 6a  02 e0 32 23 d2 75 56 12  |MI.....j..2#.uV.|
000000c0  04 2b 22 3a 27 12 f7 ca  14 a5 9e 25 93 a2 b2 93  |.+":'......%....|
000000d0  61 f5 dc 70 6d d7 ee f3  0c a0 cb 70 f8 ab d9 71  |a..pm......p...q|
000000e0  56 7e 8d 10 7c 2b ff 40  10 9f 3e 0f 60 41 92 90  |V~..|+.@..>.`A..|
000000f0  82 c7 5b c8 5c ad de 56  25 11 c9 7e a2 aa 54 7b  |..[.\..V%..~..T{|
00000100  1f ef f2 42 0f 80 11 cb  83 b4 ec 0c 33 9f 8f bd  |...B........3...|
00000110  bb 15 31 8c 6a 59 15 4a  9d 6e 5d 5c 02 1a bc 0d  |..1.jY.J.n]\....|
00000120  fb b1 bf 0e 09 e9 65 28  57 ad b8 87 a1 64 7f 62  |......e(W....d.b|
00000130  2d e1 e4 dc e8 c8 97 f8  d1 9b 9b 25 92 03 cb 7b  |-..........%...{|
00000140  84 85 27 aa 87 d8 21 9d  98 58 23 e6 ad 45 07 3b  |..'...!..X#..E.;|
00000150  8b 3a 22 cf f9 db d6 7d  d5 57 c2 f7 f8 bf 62 56  |.:"....}.W....bV|
00000160  27 ce 8b 6d 1b a5 46 50  53 e1 c8 6e fc 24 06 f1  |'..m..FPS..n.$..|
00000170  db e3 0c 2a 45 ce 2e 4b  87 b3 e7 c4 77 04 6f f2  |...*E..K....w.o.|
00000180  91 89 42 45 34 df cc b0  af a8 bc 16 c5 7c c2 42  |..BE4........|.B|
00000190  1b 6c d3 52 3a 40 2b 01  9e 24 31 5e 28 4d 82 60  |.l.R:@+..$1^(M.`|
000001a0  ff 81 d7 37 fe 09 08 6a  46 d1 5b 1d e8 c9 39 58  |...7...jF.[...9X|
000001b0  d5 01 9e 4b a2 0c db 6f  f9 c5 d3 56 2f 49 7a c0  |...K...o...V/Iz.|
000001c0  fc 6e 6c d8 ce 96 28 bf  e5 6d 5d 43              |.nl...(..m]C|
000001cc

02 means it is the beginning of the KRL_SECTION_EXPLICIT_KEY section. The next 8 bytes clarifies things further:

00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97 00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 02 00 00 01  |................|
00000030  9b 00 00 01 97 00 00 00  07 73 73 68 2d 72 73 61  |.........ssh-rsa|
00000040  00 00 00 03 01 00 01 00  00 01 81 00 c0 bb 7c b3  |..............|.|
00000050  6f 37 88 c6 dc e5 dc df  1b 81 59 a1 9b 79 9d 4e  |o7........Y..y.N|
00000060  63 b7 d0 9d 7f f2 19 e4  23 7d 9c 36 9a 08 01 39  |c.......#}.6...9|
00000070  83 bf 93 a6 b5 e8 cb 78  21 89 49 e9 9b 52 15 a4  |.......x!.I..R..|
00000080  03 5a 4a cd 26 b3 ed 7d  46 7e 8d 1e 74 35 64 43  |.ZJ.&..}F~..t5dC|
00000090  23 70 0b 7c b3 a7 f6 6c  b8 0b ca 27 5f fd 56 de  |#p.|...l...'_.V.|
000000a0  7d 1e 53 90 49 b6 fc d6  0c b5 cc 71 2d 8b b7 27  |}.S.I......q-..'|
000000b0  4d 49 e0 f4 e6 1f ea 6a  02 e0 32 23 d2 75 56 12  |MI.....j..2#.uV.|
000000c0  04 2b 22 3a 27 12 f7 ca  14 a5 9e 25 93 a2 b2 93  |.+":'......%....|
000000d0  61 f5 dc 70 6d d7 ee f3  0c a0 cb 70 f8 ab d9 71  |a..pm......p...q|
000000e0  56 7e 8d 10 7c 2b ff 40  10 9f 3e 0f 60 41 92 90  |V~..|+.@..>.`A..|
000000f0  82 c7 5b c8 5c ad de 56  25 11 c9 7e a2 aa 54 7b  |..[.\..V%..~..T{|
00000100  1f ef f2 42 0f 80 11 cb  83 b4 ec 0c 33 9f 8f bd  |...B........3...|
00000110  bb 15 31 8c 6a 59 15 4a  9d 6e 5d 5c 02 1a bc 0d  |..1.jY.J.n]\....|
00000120  fb b1 bf 0e 09 e9 65 28  57 ad b8 87 a1 64 7f 62  |......e(W....d.b|
00000130  2d e1 e4 dc e8 c8 97 f8  d1 9b 9b 25 92 03 cb 7b  |-..........%...{|
00000140  84 85 27 aa 87 d8 21 9d  98 58 23 e6 ad 45 07 3b  |..'...!..X#..E.;|
00000150  8b 3a 22 cf f9 db d6 7d  d5 57 c2 f7 f8 bf 62 56  |.:"....}.W....bV|
00000160  27 ce 8b 6d 1b a5 46 50  53 e1 c8 6e fc 24 06 f1  |'..m..FPS..n.$..|
00000170  db e3 0c 2a 45 ce 2e 4b  87 b3 e7 c4 77 04 6f f2  |...*E..K....w.o.|
00000180  91 89 42 45 34 df cc b0  af a8 bc 16 c5 7c c2 42  |..BE4........|.B|
00000190  1b 6c d3 52 3a 40 2b 01  9e 24 31 5e 28 4d 82 60  |.l.R:@+..$1^(M.`|
000001a0  ff 81 d7 37 fe 09 08 6a  46 d1 5b 1d e8 c9 39 58  |...7...jF.[...9X|
000001b0  d5 01 9e 4b a2 0c db 6f  f9 c5 d3 56 2f 49 7a c0  |...K...o...V/Iz.|
000001c0  fc 6e 6c d8 ce 96 28 bf  e5 6d 5d 43              |.nl...(..m]C|
000001cc

Notice that 00 00 01 97 (407) is 4 less then 00 00 01 9b (411). It also seem that the Key is stored base64 decoded. To verify that lets us base64 decode the Key and compare:

[manish@getafix sshca]$ echo "AAAAB3NzaC1yc2EAAAADAQABAAABgQDAu3yzbzeIxtzl3N8bgVmhm3mdTmO30J1/8hnkI32cNpoIATmDv5OmtejLeCGJSembUhWkA1pKzSaz7X1Gfo0edDVkQyNwC3yzp/ZsuAvKJ1/9Vt59HlOQSbb81gy1zHEti7cnTUng9OYf6moC4DIj0nVWEgQrIjonEvfKFKWeJZOispNh9dxwbdfu8wygy3D4q9lxVn6NEHwr/0AQnz4PYEGSkILHW8hcrd5WJRHJfqKqVHsf7/JCD4ARy4O07Awzn4+9uxUxjGpZFUqdbl1cAhq8Dfuxvw4J6WUoV624h6Fkf2It4eTc6MiX+NGbmyWSA8t7hIUnqofYIZ2YWCPmrUUHO4s6Is/529Z91VfC9/i/YlYnzottG6VGUFPhyG78JAbx2+MMKkXOLkuHs+fEdwRv8pGJQkU038ywr6i8FsV8wkIbbNNSOkArAZ4kMV4oTYJg/4HXN/4JCGpG0Vsd6Mk5WNUBnkuiDNtv+cXTVi9JesD8bmzYzpYov+VtXUM=" | base64 -d | hexdump -C
00000000  00 00 00 07 73 73 68 2d  72 73 61 00 00 00 03 01  |....ssh-rsa.....|
00000010  00 01 00 00 01 81 00 c0  bb 7c b3 6f 37 88 c6 dc  |.........|.o7...|
00000020  e5 dc df 1b 81 59 a1 9b  79 9d 4e 63 b7 d0 9d 7f  |.....Y..y.Nc....|
00000030  f2 19 e4 23 7d 9c 36 9a  08 01 39 83 bf 93 a6 b5  |...#}.6...9.....|
00000040  e8 cb 78 21 89 49 e9 9b  52 15 a4 03 5a 4a cd 26  |..x!.I..R...ZJ.&|
00000050  b3 ed 7d 46 7e 8d 1e 74  35 64 43 23 70 0b 7c b3  |..}F~..t5dC#p.|.|
00000060  a7 f6 6c b8 0b ca 27 5f  fd 56 de 7d 1e 53 90 49  |..l...'_.V.}.S.I|
00000070  b6 fc d6 0c b5 cc 71 2d  8b b7 27 4d 49 e0 f4 e6  |......q-..'MI...|
00000080  1f ea 6a 02 e0 32 23 d2  75 56 12 04 2b 22 3a 27  |..j..2#.uV..+":'|
00000090  12 f7 ca 14 a5 9e 25 93  a2 b2 93 61 f5 dc 70 6d  |......%....a..pm|
000000a0  d7 ee f3 0c a0 cb 70 f8  ab d9 71 56 7e 8d 10 7c  |......p...qV~..||
000000b0  2b ff 40 10 9f 3e 0f 60  41 92 90 82 c7 5b c8 5c  |+.@..>.`A....[.\|
000000c0  ad de 56 25 11 c9 7e a2  aa 54 7b 1f ef f2 42 0f  |..V%..~..T{...B.|
000000d0  80 11 cb 83 b4 ec 0c 33  9f 8f bd bb 15 31 8c 6a  |.......3.....1.j|
000000e0  59 15 4a 9d 6e 5d 5c 02  1a bc 0d fb b1 bf 0e 09  |Y.J.n]\.........|
000000f0  e9 65 28 57 ad b8 87 a1  64 7f 62 2d e1 e4 dc e8  |.e(W....d.b-....|
00000100  c8 97 f8 d1 9b 9b 25 92  03 cb 7b 84 85 27 aa 87  |......%...{..'..|
00000110  d8 21 9d 98 58 23 e6 ad  45 07 3b 8b 3a 22 cf f9  |.!..X#..E.;.:"..|
00000120  db d6 7d d5 57 c2 f7 f8  bf 62 56 27 ce 8b 6d 1b  |..}.W....bV'..m.|
00000130  a5 46 50 53 e1 c8 6e fc  24 06 f1 db e3 0c 2a 45  |.FPS..n.$.....*E|
00000140  ce 2e 4b 87 b3 e7 c4 77  04 6f f2 91 89 42 45 34  |..K....w.o...BE4|
00000150  df cc b0 af a8 bc 16 c5  7c c2 42 1b 6c d3 52 3a  |........|.B.l.R:|
00000160  40 2b 01 9e 24 31 5e 28  4d 82 60 ff 81 d7 37 fe  |@+..$1^(M.`...7.|
00000170  09 08 6a 46 d1 5b 1d e8  c9 39 58 d5 01 9e 4b a2  |..jF.[...9X...K.|
00000180  0c db 6f f9 c5 d3 56 2f  49 7a c0 fc 6e 6c d8 ce  |..o...V/Iz..nl..|
00000190  96 28 bf e5 6d 5d 43                              |.(..m]C|
00000197

Bingo! That is a match. Moreover, the decoded Key is of length 00000197 (407). So, 00 00 01 9b (411) means the length of the section. And, 00 00 01 97 (407) the length of the Key. So, what happen if we add another Key?

[manish@getafix sshca]$ ssh-keygen -k -u -f ca_krl id_ed25519.pub
Revoking from id_ed25519.pub

[manish@getafix sshca]$ hexdump -C ca_krl 
00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 36 97 00 00 00 00  |........_.6.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 02 00 00 01  |................|
00000030  d2 00 00 01 97 00 00 00  07 73 73 68 2d 72 73 61  |.........ssh-rsa|
00000040  00 00 00 03 01 00 01 00  00 01 81 00 c0 bb 7c b3  |..............|.|
00000050  6f 37 88 c6 dc e5 dc df  1b 81 59 a1 9b 79 9d 4e  |o7........Y..y.N|
00000060  63 b7 d0 9d 7f f2 19 e4  23 7d 9c 36 9a 08 01 39  |c.......#}.6...9|
00000070  83 bf 93 a6 b5 e8 cb 78  21 89 49 e9 9b 52 15 a4  |.......x!.I..R..|
00000080  03 5a 4a cd 26 b3 ed 7d  46 7e 8d 1e 74 35 64 43  |.ZJ.&..}F~..t5dC|
00000090  23 70 0b 7c b3 a7 f6 6c  b8 0b ca 27 5f fd 56 de  |#p.|...l...'_.V.|
000000a0  7d 1e 53 90 49 b6 fc d6  0c b5 cc 71 2d 8b b7 27  |}.S.I......q-..'|
000000b0  4d 49 e0 f4 e6 1f ea 6a  02 e0 32 23 d2 75 56 12  |MI.....j..2#.uV.|
000000c0  04 2b 22 3a 27 12 f7 ca  14 a5 9e 25 93 a2 b2 93  |.+":'......%....|
000000d0  61 f5 dc 70 6d d7 ee f3  0c a0 cb 70 f8 ab d9 71  |a..pm......p...q|
000000e0  56 7e 8d 10 7c 2b ff 40  10 9f 3e 0f 60 41 92 90  |V~..|+.@..>.`A..|
000000f0  82 c7 5b c8 5c ad de 56  25 11 c9 7e a2 aa 54 7b  |..[.\..V%..~..T{|
00000100  1f ef f2 42 0f 80 11 cb  83 b4 ec 0c 33 9f 8f bd  |...B........3...|
00000110  bb 15 31 8c 6a 59 15 4a  9d 6e 5d 5c 02 1a bc 0d  |..1.jY.J.n]\....|
00000120  fb b1 bf 0e 09 e9 65 28  57 ad b8 87 a1 64 7f 62  |......e(W....d.b|
00000130  2d e1 e4 dc e8 c8 97 f8  d1 9b 9b 25 92 03 cb 7b  |-..........%...{|
00000140  84 85 27 aa 87 d8 21 9d  98 58 23 e6 ad 45 07 3b  |..'...!..X#..E.;|
00000150  8b 3a 22 cf f9 db d6 7d  d5 57 c2 f7 f8 bf 62 56  |.:"....}.W....bV|
00000160  27 ce 8b 6d 1b a5 46 50  53 e1 c8 6e fc 24 06 f1  |'..m..FPS..n.$..|
00000170  db e3 0c 2a 45 ce 2e 4b  87 b3 e7 c4 77 04 6f f2  |...*E..K....w.o.|
00000180  91 89 42 45 34 df cc b0  af a8 bc 16 c5 7c c2 42  |..BE4........|.B|
00000190  1b 6c d3 52 3a 40 2b 01  9e 24 31 5e 28 4d 82 60  |.l.R:@+..$1^(M.`|
000001a0  ff 81 d7 37 fe 09 08 6a  46 d1 5b 1d e8 c9 39 58  |...7...jF.[...9X|
000001b0  d5 01 9e 4b a2 0c db 6f  f9 c5 d3 56 2f 49 7a c0  |...K...o...V/Iz.|
000001c0  fc 6e 6c d8 ce 96 28 bf  e5 6d 5d 43 00 00 00 33  |.nl...(..m]C...3|
000001d0  00 00 00 0b 73 73 68 2d  65 64 32 35 35 31 39 00  |....ssh-ed25519.|
000001e0  00 00 20 49 ff e8 a8 54  79 59 6b cc 73 e1 dc 05  |.. I...TyYk.s...|
000001f0  7a 47 07 4a 0e 84 56 11  f2 39 8f 50 e9 f1 11 27  |zG.J..V..9.P...'|
00000200  b2 65 63                                          |.ec|
00000203

The section length increased from 0000019b (411) to 000001d2 (466) and the 4 bytes right before the newly added Key is ```00 00 00 33`` (51), which refers to the length of the new Key.

[manish@getafix sshca]$ echo "AAAAC3NzaC1lZDI1NTE5AAAAIEn/6KhUeVlrzHPh3AV6RwdKDoRWEfI5j1Dp8REnsmVj" | base64 -d | hexdump -C
00000000  00 00 00 0b 73 73 68 2d  65 64 32 35 35 31 39 00  |....ssh-ed25519.|
00000010  00 00 20 49 ff e8 a8 54  79 59 6b cc 73 e1 dc 05  |.. I...TyYk.s...|
00000020  7a 47 07 4a 0e 84 56 11  f2 39 8f 50 e9 f1 11 27  |zG.J..V..9.P...'|
00000030  b2 65 63                                          |.ec|
00000033

So, it seems that the 4 bytes right before any string is its length. To confirm this hypothesis, Let us add a Certificate to an empty KRL.

[manish@getafix sshca]$ ssh-keygen -k -f ca_krl

[manish@getafix sshca]$ ssh-keygen -k -u -f ca_krl id_rsa-cert.pub
Revoking from id_rsa-cert.pub

[manish@getafix sshca]$ hexdump -C ca_krl 
00000000  53 53 48 4b 52 4c 0a 00  00 00 00 01 00 00 00 00  |SSHKRL..........|
00000010  00 00 00 00 00 00 00 00  5f a4 4e b8 00 00 00 00  |........_.N.....|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 01 00 00 01  |................|
00000030  ac 00 00 01 97 00 00 00  07 73 73 68 2d 72 73 61  |.........ssh-rsa|
00000040  00 00 00 03 01 00 01 00  00 01 81 00 c0 a2 f4 3e  |...............>|
00000050  a2 a0 ba 1f 1b 31 6e 67  e6 dd dc f4 e6 7f ba 52  |.....1ng.......R|
00000060  b8 8a 89 3f 4e 3d d3 2e  cb 56 c4 83 bf 7c 5c 97  |...?N=...V...|\.|
00000070  7e ac a1 67 0a ee 2a 43  d9 e4 16 53 10 2a da 59  |~..g..*C...S.*.Y|
00000080  1f 4e ef a6 38 88 ba 1b  41 0d 08 21 43 83 b1 ce  |.N..8...A..!C...|
00000090  c0 13 65 5f 59 78 28 e6  1d 35 4d ba 58 00 64 39  |..e_Yx(..5M.X.d9|
000000a0  aa 32 a6 89 56 6c ba 63  2a 76 2a b4 78 17 36 b4  |.2..Vl.c*v*.x.6.|
000000b0  00 c6 b2 83 5b 2f 2a 0b  de 9a 28 34 59 bf 85 b5  |....[/*...(4Y...|
000000c0  ea 33 e7 73 24 4f e8 f7  c5 4d a3 c1 5a b7 9a 95  |.3.s$O...M..Z...|
000000d0  38 13 e4 c6 7b 47 c4 0c  f0 9d 0c aa be 1e 1b 60  |8...{G.........`|
000000e0  e1 6d ef 52 97 6e 6d d7  17 76 f6 dc 40 39 1a 13  |.m.R.nm..v..@9..|
000000f0  3a e9 d8 e4 0e e7 86 8c  b2 72 c7 f6 db 1e cc a8  |:........r......|
00000100  6f 70 a0 ca f7 01 b7 9b  6a a1 88 56 75 4e a3 9d  |op......j..VuN..|
00000110  dd c2 4c 60 4a 56 61 bd  85 da d2 48 57 96 cd b6  |..L`JVa....HW...|
00000120  a2 fe 46 72 61 c5 b4 15  69 88 0f 9b fd cb 8c 80  |..Fra...i.......|
00000130  3d bf fe 8d f1 f9 5e 10  4c 7a 18 84 66 e6 5f e8  |=.....^.Lz..f._.|
00000140  3f 2c fc fd a4 24 04 63  64 f8 8d 53 fe bd 4b 10  |?,...$.cd..S..K.|
00000150  5b 78 0b 10 27 de a2 77  33 60 24 a9 fe 04 fe 79  |[x..'..w3`$....y|
00000160  19 cd d3 cd 24 9d 92 8b  0a 92 9f 4b 73 77 1e 8e  |....$......Ksw..|
00000170  9d 56 d8 3c 37 a6 23 f9  58 52 9c bb 8f f6 f7 f8  |.V.<7.#.XR......|
00000180  ea 29 90 c1 a2 82 f1 df  f3 42 0e 00 9a ef 01 ad  |.).......B......|
00000190  cf 0c be 5c 2f 67 01 76  a6 44 19 9e 54 36 c9 1f  |...\/g.v.D..T6..|
000001a0  6a a0 32 dc 45 db e1 9d  f9 ba 01 3a 00 9f 0f 41  |j.2.E......:...A|
000001b0  73 37 06 4f 52 4f 04 b8  d1 86 46 54 57 54 9c 63  |s7.ORO....FTWT.c|
000001c0  cf 98 70 ee 95 d0 81 d0  ab 0e f2 ef 00 00 00 00  |..p.............|
000001d0  20 00 00 00 08 00 00 00  00 00 00 04 d2           | ............|
000001dd

Notice 01 right after comment, which translates to KRL_SECTION_CERTIFICATES. Right after that is 000001ac (428) corresponding to the length of the section. This is then followed by 00000197 (407). Now, that seems familiar, as this was the length when we added an RSA Key to the KRL. It is actually the CA Public Key used to sign the certificate. A reserved string (00000000) follows the CA Key. The next is a byte, showing the certificate section type. In this case, it is 20 , which means KRL_SECTION_CERT_SERIAL_LIST. Per the documentation, the list of uint64 (8 bytes each) should follow. The 4 bytes right after indicates the length of the list (i.e. 00000008). And, 8 bytes after that indicate the serial number of the certificate (i.e 1234)

[manish@getafix sshca]$ ssh-keygen -L -f id_rsa-cert.pub 
id_rsa-cert.pub:
        Type: ssh-rsa-cert-v01@openssh.com user certificate
        Public key: RSA-CERT SHA256:RaHZyPzIZ0MfR0o7RkhFp4H7u67ByfL2WLVxx9OtdFY
        Signing CA: RSA SHA256:K1vwispwIJgFLOgsetpEXiiOUztYYClYATIB27qUvuI (using ssh-rsa)
        Key ID: "manish"
        Serial: 1234
        Valid: from 2020-11-05T13:11:00 to 2021-11-04T14:12:37
        Principals: 
                manish
        Critical Options: (none)
        Extensions: 
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc

Armed with the above knowledge, I set out to write a python code to parse the KRL file. Please note that in Keyper we use fingerprint to revoke a Key and Serial No. to revoke a Certificate. So, the code only parses the relevant sections and ignores the rest of the file. Also, it is not fully optimized yet. If you have a need to parse other sections, you can use the above knowledge and extend the code further.

When SSHKRL is initialized, it parses a KRL and creates a Python dictionary containing the list of revoked Key Fingerprints and revoked Certificate serial numbers. It has two methods is_key_revoked(key_hash) and is_cert_revoked(cert_serial). key_hash and cert_serial are Key fingerprint and certificate serial number respectively. Both methods return either True or False based on whether or not a given Key/Certificate has been revoked or not.

With this class in place, now SSH servers need not send a full Key or Certificate to perform KRL lookup.

KRL Parser

#############################################################################
#                       Confidentiality Information                         #
#                                                                           #
# This module is the confidential and proprietary information of            #
# DBSentry Corp.; it is not to be copied, reproduced, or transmitted in any #
# form, by any means, in whole or in part, nor is it to be used for any     #
# purpose other than that for which it is expressly provided without the    #
# written permission of DBSentry Corp.                                      #
#                                                                           #
# Copyright (c) 2020-2021 DBSentry Corp.  All Rights Reserved.              #
#                                                                           #
#############################################################################
import struct
import base64
from flask import current_app as app
from ..resources.errors import KeyperError, errors

"""
Implements SSH KRL Lookup.
"""

class SSHKRL(object):
    ''' SSHKRL Class '''
    ca_krl_file = ''
    krl_buf_len = 0
    krl = {}

    def __init__(self):
        app.logger.debug("Enter")
        self.ca_dir = app.config["SSH_CA_DIR"]
        self.ca_krl_file = self.ca_dir + "/" + app.config["SSH_CA_KRL_FILE"]

        try:
            with open(self.ca_krl_file, mode="rb") as krl_file:
                krlbuf = krl_file.read()
                self.krl_buf_len = len(krlbuf)
                krl_buf_ptr = 0

                app.logger.debug("KRL File Size: " + str(self.krl_buf_len))

                # Parse headers
                if (self.krl_buf_len < krl_buf_ptr + 44):
                    raise KeyperError(errors["KRLParseError"].get("msg"), errors["KRLParseError"].get("status"))

                self.krl["krl_sig"] = krlbuf[krl_buf_ptr:8]
                krl_buf_ptr += 8

                self.krl["krl_format_version"] = krlbuf[krl_buf_ptr:krl_buf_ptr+4]
                krl_buf_ptr += 4

                self.krl["krl_version"] = krlbuf[krl_buf_ptr:krl_buf_ptr+8]
                krl_buf_ptr += 8
                self.krl["krl_date"] = krlbuf[krl_buf_ptr:krl_buf_ptr+8]
                krl_buf_ptr += 8
                self.krl["krl_flags"] = krlbuf[krl_buf_ptr:krl_buf_ptr+8]
                krl_buf_ptr += 8

                krl_buf_ptr, reserved_string = self.read_string_from_buf(krlbuf, krl_buf_ptr)
                krl_buf_ptr, self.krl["krl_comment"] = self.read_string_from_buf(krlbuf, krl_buf_ptr)

                # Parse sections
                while (krl_buf_ptr < self.krl_buf_len):
                    section_type = struct.unpack('c', krlbuf[krl_buf_ptr:krl_buf_ptr+1])[0]
                    krl_buf_ptr += 1

                    krl_buf_ptr, section_data = self.read_string_from_buf(krlbuf, krl_buf_ptr)

                    if (section_type == b'\x01'):
                        section_ptr = 0
                        section_data_len = len(section_data)

                        if ("krl_certs" not in self.krl):
                            self.krl["krl_certs"] = []
                        while (section_ptr < section_data_len):
                            krl_certs = {}
                            section_ptr, krl_certs["ca_key"] = self.read_string_from_buf(section_data, section_ptr)
                            section_ptr, reserved_string = self.read_string_from_buf(section_data, section_ptr)

                            cert_section_type = struct.unpack('c', section_data[section_ptr:section_ptr+1])[0]
                            section_ptr += 1

                            section_ptr, cert_serial_list = self.read_string_from_buf(section_data, section_ptr)

                            if (cert_section_type == b'\x20'):
                                cert_serial_list_ptr = 0
                                cert_serial_list_len = len(cert_serial_list)

                                krl_certs["cert_serial_list"] = []
                                while (cert_serial_list_ptr < cert_serial_list_len):
                                    krl_certs["cert_serial_list"].append(struct.unpack('>q', cert_serial_list[cert_serial_list_ptr:cert_serial_list_ptr+8])[0])
                                    app.logger.debug("Cert Serial No: " + str(struct.unpack('>q', cert_serial_list[cert_serial_list_ptr:cert_serial_list_ptr+8])[0]))
                                    cert_serial_list_ptr += 8
                                app.logger.debug("Cert Serial List Size: " + str(len(krl_certs["cert_serial_list"])))

                            self.krl["krl_certs"].append(krl_certs)
                    elif (section_type == b'\x02'):
                        section_ptr = 0
                        section_data_len = len(section_data)
                        if ("krl_keys" not in self.krl):
                            self.krl["krl_keys"] = []
                        
                        while (section_ptr < section_data_len):
                            section_ptr, krl_key = self.read_string_from_buf(section_data, section_ptr)
                            self.krl["krl_keys"].append(krl_key)

                        app.logger.debug("KRL Keys Size: " + str(len(self.krl["krl_keys"])))
                    elif (section_type == b'\x05'):
                        section_ptr = 0
                        section_data_len = len(section_data)
                        if ("krl_key_hash" not in self.krl):
                            self.krl["krl_key_hash"] = []
                        while (section_ptr < section_data_len):
                            section_ptr, krl_key_hash = self.read_string_from_buf(section_data, section_ptr)
                            self.krl["krl_key_hash"].append(krl_key_hash)
                            app.logger.debug("Key Hash: " + str(krl_key_hash))

                        app.logger.debug("KRL Key Hash Size: " + str(len(self.krl["krl_key_hash"])))
        except OSError as e:
            app.logger.error("OS error: " + str(e))
            raise KeyperError(errors["OSError"].get("msg"), errors["OSError"].get("status"))

        app.logger.debug("Exit")

    def read_string_from_buf(self, buf, ptr):
        ''' Returns a string from buffer '''
        app.logger.debug("Enter")

        result_string = None
        result_ptr = ptr
        buf_len = len(buf)

        string_size = struct.unpack('>i', buf[result_ptr:result_ptr+4])[0]
        result_ptr += 4

        if (buf_len < result_ptr + string_size):
            app.logger.error("KRL Parse Error at section. PTR: " + str(result_ptr))
            raise KeyperError(errors["KRLParseError"].get("msg"), errors["KRLParseError"].get("status"))

        result_string = buf[result_ptr:result_ptr+string_size]
        result_ptr += string_size

        app.logger.debug("Exit")
        return result_ptr, result_string

    def is_key_revoked(self, key_hash):
        ''' Checks if key hash in KRL '''
        app.logger.debug("Enter")

        rc = False

        try:
            app.logger.debug("key_hash: " + key_hash)
            key_hash_split = key_hash.split(":")[1]
            app.logger.debug("key_hash split: " + key_hash_split)

            missing_padding = len(key_hash_split) + 4 - (len(key_hash_split) % 4) 
            app.logger.debug("missing padding: " + str(missing_padding))
            key_hash_split = key_hash_split.ljust(missing_padding, '=')
            app.logger.debug("key_hash_split:" + key_hash_split)
            
            decoded_hash = base64.b64decode(key_hash_split)
            if ("krl_key_hash" in self.krl):
                if (decoded_hash in self.krl["krl_key_hash"]):
                    rc = True
        except OSError as e:
            app.logger.error("OS error: " + str(e))
            raise KeyperError(errors["OSError"].get("msg"), errors["OSError"].get("status"))

        app.logger.debug("Exit")
        return rc

    def is_cert_revoked(self, cert_serial):
        ''' Checks if cert serial in KRL '''
        app.logger.debug("Enter")

        rc = False

        try:
            app.logger.debug("cert_serial: " + str(cert_serial))
            if ("krl_certs" in self.krl):
                for krl_cert in self.krl["krl_certs"]:
                    app.logger.debug("Revoked serial list: " + str(krl_cert["cert_serial_list"]))
                    if (cert_serial in krl_cert["cert_serial_list"]):
                        rc = True
                        break
        except OSError as e:
            app.logger.error("OS error: " + str(e))
            raise KeyperError(errors["OSError"].get("msg"), errors["OSError"].get("status"))

        app.logger.debug("Exit")
        return rc

Summation

In this post, I explained the internal structure for OpenSSH Key Revocation List (KRL) format with examples. This post also presents a Python class for KRL file parsing and performs KRL lookups without having to use ssh-keygen.

<Shameless-Plug>
Although the use of certificates results in more secure SSH authentication, SSH CA adds the burden of ssh certificate management. One can use a centralized system such as Keyper to ease that burden. Keyper is an Open Source SSH Key and Certificate-Based Authentication Manager, which also acts as an SSH Certificate Authority (CA). It standardizes and centralizes the storage of SSH public keys and SSH Certificates for all Linux users in your organization. It also saves significant time and effort it takes to manage SSH keys and certificates on each Linux Server. Keyper also maintains an active Key Revocation List, which prevents the use of Key/Cert once revoked. Keyper is a lightweight container taking less than 100MB. It supports both Docker and Podman. You can be up and running within minutes instead of days.
</Shameless-Plug>

That’s it, folks! Happy more secure SSH’ing.

Related