Skip to content

Instantly share code, notes, and snippets.

@karalabe
Last active September 23, 2025 13:47
Show Gist options
  • Save karalabe/fb7ac43f3899f511b5547279c036bf4e to your computer and use it in GitHub Desktop.
Save karalabe/fb7ac43f3899f511b5547279c036bf4e to your computer and use it in GitHub Desktop.
Publicly auditable YubiHSM logs

Publicly auditable YubiHSM

Disclaimer: This is not an article with a beginning, a middle and an end for public consuption, rather a personal memo I figured I'd publish if anyone else finds it useful.

Background: I've got a genomic project (Bsky: @dark.bio, X: @dark_dot_bio) requiring secure-boot signing keys and API server identity certs/keys.

I've chosen YubiHSMs to be my roots of trust, because I don't want to mess up key handling myself; and because I want to have a public audit trail of what I've signed to soft-prove non-malice. This guide is my personal memo on how to onboard a YubiHSM into my project in a way that makes the audit logs (mostly) publicly verifiable.

Onboarding the device

Start by resetting the device to get rid of any old log entries and keys:

# Factory reset and login with default credentials
$ yubihsm> reset 0
Device successfully reset

$ yubihsm> connect
$ yubihsm> session open 1 password

# Demonstrate the empty audit log
$ yubihsm> audit get 0
0 unlogged boots found
0 unlogged authentications found
Found 2 items
item:     1 -- cmd: 0xff -- length: 65535 -- session key: 0xffff -- target key: 0xffff -- second key: 0xffff -- result: 0xff -- tick: 4294967295 -- hash: 33012657537e4842f57fa6ed3b09b25b
item:     2 -- cmd: 0x00 -- length:    0 -- session key: 0xffff -- target key: 0x0000 -- second key: 0x0000 -- result: 0x00 -- tick: 0 -- hash: fc153091560e18bca82621522c85bdba

Before doing anything, it's worthwhile to sanity check the device authenticity against Yubico's certificates. Each HSM has a burnt in certificate in slot 0 as an OPAQUE DER encoded object. That can be exported and verified against Yubico's chain of trust (rootinthsm):

$ yubihsm> get opaque 0 0 cert.der
$ openssl x509 -inform DER -in cert.der -outform PEM -out cert.pem
$ openssl verify -CAfile yubico-ca-crt.pem -untrusted yubico-int-crt.pem cert.pem

By default, no command is logged into the audit trail, only boot events. We want everything logged, but need to do it in a somewhat auditable way, so start by enabling unenforced (0x01) logging of the PUT OPTION command, then - when we actually have a log for it - force it until factory reset (0x02).

# Enable auditing option changes and enforce it (to get it logged)
$ yubihsm> put option 0 command-audit 4f01 # PUT OPTION (reversible)
$ yubihsm> put option 0 command-audit 4f02 # PUT OPTION (irreversible)
$ yubihsm> get option 0 command-audit
Option value is: 0100030004000500060007000900080040004100420043004400450046004700550056004800490057004a004b004c004d0067004e004f0250005100520053005400580059005a005b005c005d005e005f006000610062006300640065006600680069006a006b006c000a006d006e006f0070007100720073007400750076007700

# Demonstrate the audit log functioned
$ yubihsm> audit get 0
[...]
item:     3 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 1987 -- hash: 3d9380a76ae41826d90daa44c1085a62

By default, if force-audit is turned off (0x00), when the YubiHSM internal audit log (62 entries) is reached, new operations will overwrite old ones, losing the trail. Turning it on (0x01) will cause the HSM to refuse further operations until the logs are exported; and setting it to 0x02 will lock the option in until a factory reset. We want the latter one to ensure no operation goes unnoticed.

# Enforce audit-log consumption
$ yubihsm> put option 0 force-audit 02
$ yubihsm> get option 0 force-audit
Option value is: 02

# Demonstrate the audit log functioned
$ yubihsm> audit get 0
[...]
item:     4 -- cmd: 0x4f -- length:    4 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 3479 -- hash: 43d063d6f0d67b43b87cd67df2f6c2bc

By default, YubiHSM ships with a super-admin authentication key in slot 0x0001, with the password password. Whilst it is possible to change the password of a key, it's not possible to change the label from DEFAULT AUTHKEY CHANGE THIS ASAP. To create a proper admin user, a new authentication key must be created and the old one deleted. Since it's also not possible to move keys between slots, to have the final admin key on slot 1, another round of create/delete must be done. Do enable audit logs for authkey operations:

# Enable auditing auth key related commands
$ yubihsm> put option 0 command-audit 4402 # PUT ASYMMETRIC AUTHENTICATION KEY
$ yubihsm> put option 0 command-audit 6c02 # CHANGE AUTHENTICATION KEY
$ yubihsm> put option 0 command-audit 5802 # DELETE OBJECT

$ yubihsm> get option 0 command-audit
Option value is: 0100030004000500060007000900080040004100420043004402450046004700550056004800490057004a004b004c004d0067004e004f0250005100520053005400580259005a005b005c005d005e005f006000610062006300640065006600680069006a006b006c020a006d006e006f0070007100720073007400750076007700

# Create a temporary root key to delete the default with
$ yubihsm> put authkey 0 0x0002 "temp-root" 0xffff all all password
Stored Authentication key 0x0002

# Delete the default key, create a production root key
$ yubihsm> session close 0
$ yubihsm> session open 2 password
$ yubihsm> delete 0 0x0001 authentication-key
$ yubihsm> put authkey 0 0x0001 "root" 0xffff all all
Stored Authentication key 0x0001

# Delete the temporary root key
$ yubihsm> session close 0
$ yubihsm> session open 1
$ yubihsm> delete 0 0x0002 authentication-key

# Reset the password to demonstrate enabled auditing
$ yubihsm> change authkey 0 0x0001
Changed Authentication key 0x0001

# Make sure the keys are clean
$ yubihsm> list objects 0
Found 1 object(s)
id: 0x0001, type: authentication-key, algo: aes128-yubico-authentication, sequence: 2, label: root

# Demonstrate the audit log functioned
$ yubihsm> audit get 0
[...]
item:     5 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 4412 -- hash: a1709512633688ff7850aade144d52f6
item:     6 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 4557 -- hash: 164f7d15a97eb5fb2b3e4a5b16f80171
item:     7 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 4735 -- hash: ca65b39774f8060e5424c00a911d1fbd
item:     8 -- cmd: 0x44 -- length:   93 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xc4 -- tick: 5215 -- hash: 9bd660992049b4a647bded9359188612
item:     9 -- cmd: 0x58 -- length:    3 -- session key: 0x0002 -- target key: 0x0001 -- second key: 0xffff -- result: 0xd8 -- tick: 5636 -- hash: d40798bf14836875e2b16226238c5039
item:    10 -- cmd: 0x44 -- length:   93 -- session key: 0x0002 -- target key: 0x0001 -- second key: 0xffff -- result: 0xc4 -- tick: 5954 -- hash: 6aa309e6306e1398a469ce25b76d00bc
item:    11 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xd8 -- tick: 7033 -- hash: 2f83ee18bbcf3e9d9eee18561f71af2d
item:    12 -- cmd: 0x6c -- length:   35 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0xffff -- result: 0xec -- tick: 7358 -- hash: 98168e24e4918738cfe8138c35a2fd84

Auditing asymmetric keys

Before creating production keys, it's desirable to not only enable audit logs for it, but also do a dry-run of all operations we'd like to audit. These will create log entries themselves, further supporting that auditing was enabled before the production keys were created, thus no silent shenanigans could have happened afterward.

# Enable auditing asymmetric key commands
$ yubihsm> put option 0 command-audit 4602 # GENERATE ASYMMETRIC KEY
$ yubihsm> put option 0 command-audit 5602 # SIGN ECDSA
$ yubihsm> put option 0 command-audit 6a02 # SIGN EDDSA
$ yubihsm> put option 0 command-audit 4702 # SIGN RSA PKCS1
$ yubihsm> put option 0 command-audit 5502 # SIGN RSA PSS

$ yubihsm> get option 0 command-audit
Option value is: 0100030004000500060007000900080040004100420043004402450046024702550256024800490057004a004b004c004d0067004e004f0250005100520053005400580259005a005b005c005d005e005f006000610062006300640065006600680069006a026b006c020a006d006e006f0070007100720073007400750076007700

# Generate a key of each type and sign to demo audit trail
$ yubihsm> generate asymmetric 0 0x0001 temp-ecdsa-key 1 all ecp256
$ yubihsm> sign ecdsa 0 0x0001 ecdsa-sha256 data

$ yubihsm> generate asymmetric 0 0x0002 temp-eddsa-key 1 all ed25519
$ yubihsm> sign eddsa 0 0x0002 ed25519 data

$ yubihsm> generate asymmetric 0 0x0003 temp-rsa-key 1 all rsa2048
$ yubihsm> sign pkcs1v1_5 0 0x0003 rsa-pkcs1-sha256 data
$ yubihsm> sign pss 0 0x0003 rsa-pss-sha256 data

# Delete all the ephemeral test keys
$ yubihsm> delete 0 0x0001 asymmetric-key
$ yubihsm> delete 0 0x0002 asymmetric-key
$ yubihsm> delete 0 0x0003 asymmetric-key

# Demonstrate the audit log functioned
$ yubihsm> audit get 0
[...]
item:    13 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 10384 -- hash: 6c94c62c1b99a421e83fccd0af9349fb
item:    14 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 10526 -- hash: 43ab16e7bfe8fe984da5c4e029ec5655
item:    15 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 10659 -- hash: eafa2ba1ace8248297f5f016ff39eda5
item:    16 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 10772 -- hash: fdcc25a6c98945b32b865a0f29b5275f
item:    17 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 10891 -- hash: 06d0909f91066d03ae59aea73af40308
item:    18 -- cmd: 0x46 -- length:   53 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0xffff -- result: 0xc6 -- tick: 11427 -- hash: 9be0553a12eb8dbd6a0fbd1b4be579fe
item:    19 -- cmd: 0x56 -- length:   34 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0xffff -- result: 0xd6 -- tick: 12051 -- hash: 2acd83fba817ce88826c944df70489dc
item:    20 -- cmd: 0x46 -- length:   53 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xc6 -- tick: 12583 -- hash: 9edcce1671ba12cfc5118fca4beea7c1
item:    21 -- cmd: 0x6a -- length:    6 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xea -- tick: 12744 -- hash: bf9f3c596ebb93f760b21ce6ef79c02a
item:    22 -- cmd: 0x46 -- length:   53 -- session key: 0x0001 -- target key: 0x0003 -- second key: 0xffff -- result: 0xc6 -- tick: 12941 -- hash: 4e8ede160bf0f27f5bcd1f9d791d6bfe
item:    23 -- cmd: 0x47 -- length:   34 -- session key: 0x0001 -- target key: 0x0003 -- second key: 0xffff -- result: 0xc7 -- tick: 13309 -- hash: aa3c4e3c912b0a81a1aaf4f4bab24b7f
item:    24 -- cmd: 0x55 -- length:   37 -- session key: 0x0001 -- target key: 0x0003 -- second key: 0xffff -- result: 0xd5 -- tick: 13560 -- hash: 8f1fe92409675a9f716f38374db589eb
item:    25 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0xffff -- result: 0xd8 -- tick: 14697 -- hash: 5be47c78ca6aff35196d4a9ddcafdbc6
item:    26 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xd8 -- tick: 14843 -- hash: 7aad811d9ee601cdeba491e185f29350
item:    27 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0003 -- second key: 0xffff -- result: 0xd8 -- tick: 15507 -- hash: b5ba348c13b3936951901fd59b67d095

We don't care about symmetric keys and hmac keys for now; shared secrets is not really in line with the goals of the project. We also don't care about otp aead as it seems to be Yubico OTP specific. We also don't care about cleartext-imported keys as they are unsafe (all backups will be done through wrapping, which the HSM can attest to).

Auditing key wrapping

Eventually, we'd like to create a backup of the keys onto another YubiHSM. The way to do that is:

  • Create an RSA WRAP key on the other HSM and export the pubkey
  • Import the pubkey onto this HSM
  • Export the local key under wrap on this HSM
  • Import the key under wrap on the other HSM

To have an audit trail, we need to enable logging of all these operations (apart from exporting the pubkey, that's not really relevant). And to demonstrate that the audit was indeed enabled, run an end-to-end wrap run with ephemeral keys.

# Enable auditing the wrapping flow
$ yubihsm> put option 0 command-audit 5b02 # GENERATE WRAP KEY
$ yubihsm> put option 0 command-audit 7302 # PUT PUBLIC WRAP KEY
$ yubihsm> put option 0 command-audit 7402 # EXPORT RSA WRAPPED KEY
$ yubihsm> put option 0 command-audit 7502 # IMPORT RSA WRAPPED KEY
$ yubihsm> put option 0 command-audit 7602 # EXPORT RSA WRAPPED OBJECT
$ yubihsm> put option 0 command-audit 7702 # IMPORT RSA WRAPPED OBJECT

$ yubihsm> get option 0 command-audit
Option value is: 0100030004000500060007000900080040004100420043004402450046024702550256024800490057004a004b004c004d0067004e004f0250005100520053005400580259005a005b025c005d005e005f006000610062006300640065006600680069006a026b006c020a006d006e006f0070007100720073027402750276027702

# Generate a wrap key, dup the pubkey and export/import itself
$ yubihsm> generate wrapkey 0 0x0001 temp-rsa-wrap-key 1 all all rsa2048
$ yubihsm> get pubkey 0 0x0001 wrap-key wrap.pem
$ yubihsm> put pub_wrapkey 0 0x0002 temp-rsa-wrap-pubkey 1 all all wrap.pem
$ yubihsm> get rsa_wrapped 0 0x0002 public-wrap-key 0x0002 aes256 rsa-oaep-sha256 mgf1-sha256 pubkey.enc
$ yubihsm> delete 0 0x0002 public-wrap-key
$ yubihsm> put rsa_wrapped 0 0x0001 rsa-oaep-sha256 mgf1-sha256 pubkey.enc

# Generate an asymmetric key and export/import it
$ yubihsm> generate asymmetric 0 0x0003 temp-eddsa-key 1 all ed25519
$ yubihsm> get rsa_wrapped_key 0 0x0002 asymmetric-key 0x0003 aes256 rsa-oaep-sha256 mgf1-sha256 key.enc
$ yubihsm> put rsa_wrapped_key 0 0x0001 asymmetric-key 0x0004 ed25519 temp-eddsa-key-dup 1 all rsa-oaep-sha256 mgf1-sha256 key.enc

# Delete all the ephemeral test keys
$ yubihsm> delete 0 0x0001 wrap-key
$ yubihsm> delete 0 0x0002 public-wrap-key
$ yubihsm> delete 0 0x0003 asymmetric-key
$ yubihsm> delete 0 0x0004 asymmetric-key

# Demonstrate the audit log functioned
$ yubihsm> audit get 0
[...]
item:    28 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 17510 -- hash: 9a6f8a55db080e33bf09bc3d51fddaff
item:    29 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 17648 -- hash: a0bc91ef6fcd03a1ee0dae05986318a1
item:    30 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 17774 -- hash: 634450228bde177a8456040c87218659
item:    31 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 17900 -- hash: c73f6bad49f0fbca2c2bc00608157344
item:    32 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 18112 -- hash: 23d018ad91146dbb2d85f7ef670c81f5
item:    33 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 18270 -- hash: 2470746a705fe582413e36ecd2716604
item:    34 -- cmd: 0x5b -- length:   61 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0xffff -- result: 0xdb -- tick: 18611 -- hash: 05246855c1945a25389f804b72868f9f
item:    35 -- cmd: 0x73 -- length:  317 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xf3 -- tick: 19512 -- hash: 128acfd73953a1f56ff790ef6ff8f291
item:    36 -- cmd: 0x76 -- length:   40 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0x0002 -- result: 0xf6 -- tick: 19830 -- hash: ea5582e84d1a505c6efc3c430602e4d9
item:    37 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xd8 -- tick: 20038 -- hash: 158ce467208a54db412da54a88c37c07
item:    38 -- cmd: 0x77 -- length:  628 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0x0002 -- result: 0xf7 -- tick: 20174 -- hash: a74b4ff165985d1231884896ffbd9e4a
item:    39 -- cmd: 0x46 -- length:   53 -- session key: 0x0001 -- target key: 0x0003 -- second key: 0xffff -- result: 0xc6 -- tick: 20460 -- hash: 4d91399d585e67352356b215ba4f9336
item:    40 -- cmd: 0x74 -- length:   40 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0x0003 -- result: 0xf4 -- tick: 20850 -- hash: 52af77320dd8d0daecd922514f8a6ca2
item:    41 -- cmd: 0x75 -- length:  402 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0x0004 -- result: 0xf5 -- tick: 21271 -- hash: b12c5510f4ea2ebdbf410a2568b2b7dd
item:    42 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0xffff -- result: 0xd8 -- tick: 21482 -- hash: 0f303523e14a67d326441287ae915744
item:    43 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xd8 -- tick: 21621 -- hash: 8b9472e7898f39b853342571053d5adc
item:    44 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0003 -- second key: 0xffff -- result: 0xd8 -- tick: 21759 -- hash: 98fa37c5f0991e1eff22743ed4c9ea6b
item:    45 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0004 -- second key: 0xffff -- result: 0xd8 -- tick: 21898 -- hash: 5b14fe13d229f60208157cd60681c916

Opposed to symmetric keys (which we didn't care about), we do care about symmetric wraps, insofar we don't want to use them, but want to be able to demonstrate that they have not been used. As such, the same set of operations will be enabled to be audited and then round tripped:

# Enable auditing the symmetric wrapping flow
$ yubihsm> put option 0 command-audit 4c02 # PUT WRAP KEY
$ yubihsm> put option 0 command-audit 4a02 # EXPORT WRAPPED
$ yubihsm> put option 0 command-audit 4b02 # IMPORT WRAPPED

$ yubihsm> get option 0 command-audit
Option value is: 0100030004000500060007000900080040004100420043004402450046024702550256024800490057004a024b024c024d0067004e004f0250005100520053005400580259005a005b025c005d005e005f006000610062006300640065006600680069006a026b006c020a006d006e006f0070007100720073027402750276027702

# Generate a wrap key, an ed25519 key and export/import
$ yubihsm> put wrapkey 0 0x0001 wrapkey 1 all all 000102030405060708090a0b0c0d0e0f
$ yubihsm> generate asymmetric 0 0x0002 temp-eddsa-key 1 all ed25519
$ yubihsm> get wrapped 0 0x0001 asymmetric-key 0x0002 1 edkey.enc
$ yubihsm> delete 0 0x0002 asymmetric-key
$ yubihsm> put wrapped 0 0x0001 edkey.enc

# Delete all the ephemeral test keys
$ yubihsm> delete 0 0x0001 wrap-key
$ yubihsm> delete 0 0x0002 asymmetric-key

# Demonstrate the audit log functioned
$ yubihsm> audit get 0
[...]
item:    46 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 23111 -- hash: f32d224f3f4508b8705c3454b6a92199
item:    47 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 23248 -- hash: 91140f3daab7889157f455a37353aa74
item:    48 -- cmd: 0x4f -- length:    5 -- session key: 0x0001 -- target key: 0xffff -- second key: 0xffff -- result: 0xcf -- tick: 23378 -- hash: 48268219e2f97f713c61b653c2c3fb07
item:    49 -- cmd: 0x4c -- length:   77 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0xffff -- result: 0xcc -- tick: 23936 -- hash: cd2cb1e23fcd2ee67e470f5365fb3d1b
item:    50 -- cmd: 0x46 -- length:   53 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xc6 -- tick: 25024 -- hash: 4533307521e6f17ae5f9ad8711cae0ce
item:    51 -- cmd: 0x4a -- length:    6 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0x0002 -- result: 0xca -- tick: 25189 -- hash: a24909777f7f863bf7d84fe95ade5f6c
item:    52 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xd8 -- tick: 25346 -- hash: 0b75c369fbf4afaf6d1a44f30ade2eff
item:    53 -- cmd: 0x4b -- length:  218 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0x0002 -- result: 0xcb -- tick: 25470 -- hash: 07f21d100588276c0f63d9136e40935e
item:    54 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0001 -- second key: 0xffff -- result: 0xd8 -- tick: 25672 -- hash: 273cdb4fb871e3475748ac7388878c11
item:    55 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0x0002 -- second key: 0xffff -- result: 0xd8 -- tick: 25819 -- hash: acbe67d93ec4b556b7d34d0d9d8499ce

This should demonstrate in the future that no key export remains unnoticed.

Periodic forced audits

EDIT: This section is nuked, without the HSM actually signing something, there's no cryptographic proof in the audit log, so that must be incorporated too.

The enabled audit logs are in theory enough to demonstrate what the device did up until the point of the last log entry. If these logs are to be published for public verification however, there is a plausible deniability aspect (especially once a backup YubiHSM is in place too): the owner keeps using the HSM but stops publishing new logs. An outsider has no way of knowing whether the HSM is idle or just unpublished.

To force the hand of an operator, we can define an "expected" forced audit deadline, where an HSM must perform some publicly verifiable action. The catch is that the audit log contains almost no information about the data the HSM operates on, so almost anything can be faked by generating "infinite" "idle" log entries in advance and gradually consuming them upon forced audits.

There is one (relatively flexible) user controlled field in the audit log: the target key hex. We can reserve a subset for our forced audit purposes (say keys ids starting with 0xfa). When a forced audit deadline arrives, we select a short data snippet we can encode into our key slots, but one that cannot be guessed in advance. Say the latest Bitcoin block hash. Then we start fake-deleting keys in the required slots to reproduce the hash.

E.g. For block 905,664: 00000000000000000001bd710aeef0c3c67978b0379c441916dc2103a84d5707.

# Boot the HSM fresh and BACKUP existing logs
$ yubihsm> connect
$ yubihsm> session open 1
$ yubihsm> audit get 0
[...]
item:    94 -- cmd: 0x00 -- length:    0 -- session key: 0xffff -- target key: 0x0000 -- second key: 0x0000 -- result: 0x00 -- tick: 0 -- hash: 48cc56c563034ba349098b09124931bb

# Make space for the force-audit logs
$ yubihsm> audit set 0 93
yubihsm> audit get 0
Found 1 item
item:    94 -- cmd: 0x00 -- length:    0 -- session key: 0xffff -- target key: 0x0000 -- second key: 0x0000 -- result: 0x00 -- tick: 0 -- hash: 48cc56c563034ba349098b09124931bb

# Fake-delete keys following the desired hash
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa00 asymmetric-key
$ yubihsm> delete 0 0xfa01 asymmetric-key
$ yubihsm> delete 0 0xfabd asymmetric-key
$ yubihsm> delete 0 0xfa71 asymmetric-key
$ yubihsm> delete 0 0xfa0a asymmetric-key
$ yubihsm> delete 0 0xfaee asymmetric-key
$ yubihsm> delete 0 0xfaf0 asymmetric-key
$ yubihsm> delete 0 0xfac3 asymmetric-key
$ yubihsm> delete 0 0xfac6 asymmetric-key
$ yubihsm> delete 0 0xfa79 asymmetric-key
$ yubihsm> delete 0 0xfa78 asymmetric-key
$ yubihsm> delete 0 0xfab0 asymmetric-key
$ yubihsm> delete 0 0xfa37 asymmetric-key
$ yubihsm> delete 0 0xfa9c asymmetric-key
$ yubihsm> delete 0 0xfa44 asymmetric-key
$ yubihsm> delete 0 0xfa19 asymmetric-key
$ yubihsm> delete 0 0xfa16 asymmetric-key
$ yubihsm> delete 0 0xfadc asymmetric-key
$ yubihsm> delete 0 0xfa21 asymmetric-key
$ yubihsm> delete 0 0xfa03 asymmetric-key
$ yubihsm> delete 0 0xfaa8 asymmetric-key
$ yubihsm> delete 0 0xfa4d asymmetric-key
$ yubihsm> delete 0 0xfa57 asymmetric-key
$ yubihsm> delete 0 0xfa07 asymmetric-key

# Retrieve the audit logs for publication
yubihsm> audit get 0
Found 33 items
item:    94 -- cmd: 0x00 -- length:    0 -- session key: 0xffff -- target key: 0x0000 -- second key: 0x0000 -- result: 0x00 -- tick: 0 -- hash: 48cc56c563034ba349098b09124931bb
item:    95 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6664 -- hash: 7ff4e8dd19382678a7d6092d3ebfb4c2
item:    96 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6684 -- hash: 17fe59de7e89a1158d6c094f3b1b3154
item:    97 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6704 -- hash: 41954da6cd279b2b3d0af18005c6c241
item:    98 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6723 -- hash: 1c3dbc46cc07f2f65f009e8647b509f4
item:    99 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6744 -- hash: 1362c134a348fa51c10e5d8c746e0581
item:   100 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6765 -- hash: e3b26d52538ade7add29f2301f7848e4
item:   101 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6784 -- hash: 0393c5713c94794b6f4855103e9f7ae7
item:   102 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6804 -- hash: e9b11a26c38c18928920b9718fb447c3
item:   103 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa00 -- second key: 0xffff -- result: 0x0b -- tick: 6829 -- hash: 7d30a9ba0663171023ce5f0468ddb8f5
item:   104 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa01 -- second key: 0xffff -- result: 0x0b -- tick: 7019 -- hash: a2e85ff77ae0471185601fe8265769fd
item:   105 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfabd -- second key: 0xffff -- result: 0x0b -- tick: 7140 -- hash: a5233df5e5c7118a40aaac915449c276
item:   106 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa71 -- second key: 0xffff -- result: 0x0b -- tick: 7250 -- hash: 5b280893aa8ee3c02bbf906d8356926e
item:   107 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa0a -- second key: 0xffff -- result: 0x0b -- tick: 7383 -- hash: d02f5f8fd28dbcaf512031c6fd77e513
item:   108 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfaee -- second key: 0xffff -- result: 0x0b -- tick: 7496 -- hash: a83247eddbe1a0058bd6da93c06b8409
item:   109 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfaf0 -- second key: 0xffff -- result: 0x0b -- tick: 7617 -- hash: 2100ce9b1f6710076296f8524965452c
item:   110 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfac3 -- second key: 0xffff -- result: 0x0b -- tick: 7716 -- hash: 0983c2d804d2d0e229f7570ad036b640
item:   111 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfac6 -- second key: 0xffff -- result: 0x0b -- tick: 7814 -- hash: 82fd932a89f4e54c047617f626c0ade9
item:   112 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa79 -- second key: 0xffff -- result: 0x0b -- tick: 7926 -- hash: 80c9290f00cbd8337b31c9b30b939a68
item:   113 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa78 -- second key: 0xffff -- result: 0x0b -- tick: 8116 -- hash: b92fcc9570a61f2b0756b28d4e81a5d3
item:   114 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfab0 -- second key: 0xffff -- result: 0x0b -- tick: 8231 -- hash: a6715e068c39f105ade4074c57b2c5fc
item:   115 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa37 -- second key: 0xffff -- result: 0x0b -- tick: 8345 -- hash: 971eb986b451ad0e66dc79ce82705a8c
item:   116 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa9c -- second key: 0xffff -- result: 0x0b -- tick: 8491 -- hash: 1138ac5c8f66bf5fc06ae8b7bc9df4f0
item:   117 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa44 -- second key: 0xffff -- result: 0x0b -- tick: 8599 -- hash: fac016864424f74305c3319e9bd35996
item:   118 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa19 -- second key: 0xffff -- result: 0x0b -- tick: 8726 -- hash: 187f4d9121433dd70559e975d4b53ddf
item:   119 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa16 -- second key: 0xffff -- result: 0x0b -- tick: 8857 -- hash: 239c824c954fc313248cd7889c1bfe78
item:   120 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfadc -- second key: 0xffff -- result: 0x0b -- tick: 8966 -- hash: 5a4bc66c345a1ac1f6c83d8b681e23ac
item:   121 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa21 -- second key: 0xffff -- result: 0x0b -- tick: 9157 -- hash: fa85d6cd2a4e29d9e097b86a782a104d
item:   122 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa03 -- second key: 0xffff -- result: 0x0b -- tick: 9269 -- hash: c136dc6ae7c1a62d5863fa187b8a8fd9
item:   123 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfaa8 -- second key: 0xffff -- result: 0x0b -- tick: 9376 -- hash: d5723192178ccefddef6832c09d941a8
item:   124 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa4d -- second key: 0xffff -- result: 0x0b -- tick: 9477 -- hash: 54ad90e7d9ab819c92fdd8c37f0b1d4d
item:   125 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa57 -- second key: 0xffff -- result: 0x0b -- tick: 9581 -- hash: fbb25033dae6d367b86deca0caa43f1b
item:   126 -- cmd: 0x58 -- length:    3 -- session key: 0x0001 -- target key: 0xfa07 -- second key: 0xffff -- result: 0x0b -- tick: 9677 -- hash: c8ab79d797b97aa81abdb41769a4ee0c

Note the caveat, there are no keys in the 0xfa.. slot range, so all delete operations will fail. That is fine, we don't care about the outcome, just to force an audit log entry for each of them! Make sure you're not using the 0xfa.. range for anything!

The nice part of this forced audit protocol is that it relies on an operator-independent random oracle (Bitcoin network block hashes), and given the length of a hash, the operator is not forced to do the audit on one specific date, rather whenever it's convenient around some cutoff period.

Epilogue

So how does this whole public publication of audit logs help the project?

  • The logs (along with attestation certificates) can demonstrate that each key used as a root of trust within the project was generated on a genuine Yubico HSM and only resides on the origin or backup HSMs. It can be demonstrated that there never existed a cleartext, off-HSM version of any of the keys.
  • The logs can show exactly how many signatures were produced by the root keys and at approximately which intervals (the logs contain no time, so we can only rely on the forced-audit publication times) which if linked up with publicly available artifacts can show that nothing malicious was signed (i.e. no secret firmware, no secret API cert).

The solution is not perfect. The YubiHSMs were not designed for public auditability (there's no trace what data they operate on, only the type of operations thmselves), but they have quite a number of features to help do public audits if enough care is taken and protocols are followed.

I'll probably keep working on this and maybe (maybe not) publish a final, more complete guide when I have something in prod.

#!/bin/sh
# verify.sh - YubiHSM Audit Log Chain Verifier
#
# This script verifies the integrity of a (human readable) YubiHSM audit log by
# iterating over each entry and verifying that the hash matches the prev hash
# combined with the current entry data.
#
# Usage: ./verify.sh yubihsm-audit-log.txt
set -e # Exit on any error
# Validate command line arguments
if [ $# -ne 1 ]; then
echo "Usage: $0 <yubihsm-audit-log.txt>"
exit 1
fi
if [ ! -f "$1" ]; then
echo "Error: File '$1' not found"
exit 1
fi
# Process each line in the log file
while IFS= read -r line; do
# Extract the individual fields from the log entry. Ugh...
# item: N -- cmd: 0xXX -- length: N -- session key: 0xXX -- target key: 0xXX -- second key: 0xXX -- result: 0xXX -- tick: N -- hash: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
eval $(printf "%s\n" "$line" | sed -E '
s/.*item: +([0-9]+) -- cmd: +0x([0-9A-Fa-f]+) -- length: +([0-9]+) -- session key: +0x([0-9A-Fa-f]+) -- target key: +0x([0-9A-Fa-f]+) -- second key: +0x([0-9A-Fa-f]+) -- result: +0x([0-9A-Fa-f]+) -- tick: +([0-9]+) -- hash: +([0-9A-Fa-f]+).*/\
item=\1; cmd_hex=\2; len=\3; ses_hex=\4; fst_hex=\5; snd_hex=\6; res_hex=\7; tick=\8; seen_hash=\9/
')
# Convert hex values to decimal for binary packing
cmd=$((0x$cmd_hex))
ses=$((0x$ses_hex))
fst=$((0x$fst_hex))
snd=$((0x$snd_hex))
res=$((0x$res_hex))
# Skip the first item (should be all ones) and just save it's hash
if [ $item -gt 1 ]; then
# Pack entry data into hex format
# Format: item(16-bit) || cmd(8-bit) || length(16-bit) || session_key(16-bit) ||
# target_key(16-bit) || second_key(16-bit) || result(16-bit) || tick(32-bit)
item_data=$(printf "%04x%02x%04x%04x%04x%04x%02x%08x" "$item" "$cmd" "$len" "$ses" "$fst" "$snd" "$res" "$tick")
# Compute SHA256(entry_data || previous_digest) and take first 16 bytes
calc_hash=$(printf "%s%s" "$item_data" "$prev_hash" | xxd -r -p | openssl dgst -sha256 -binary | head -c 16 | xxd -p -c 256)
if [ "$calc_hash" != "$seen_hash" ]; then
echo "❌ Audit trail verification FAILED at line #$((prev_item + 1))"
echo " Expected: $seen_hash"
echo " Computed: $calc_hash"
exit 1
fi
fi
# Update previous digest for next iteration
prev_hash="$seen_hash"
prev_item="$item"
done < "$1"
# Verification complete
printf "✅ Verified %d entries.\n" "$item"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment