rootiest

rootiest / PIV Text Storage

Last active 4 weeks ago

Like 1

A guide for writing secrets/notes to an unused PIV object to store them securely on a hardware device

PIV-Key-Notes.md Raw

Storing Notes and Secrets in PIV Data Objects

This guide shows how to store small notes or secrets on the PIV application of a YubiKey or an RP2350-based key (such as RS-Key), protected by the PIV PIN.

Contents

How It Works

The PIV application exists to hold cryptographic keys and certificates. It also holds data objects: small blobs, each addressed by an object ID (for example 0x5FC121). ykman can write any file into a data object and read it back.

Data objects are independent of the PIV keys and certificates. You do not need to generate keys or certificates first.

Several data objects can only be read after the PIN is entered. Those objects make a convenient, portable place to keep a short list of critical secrets.

Storage Limits

Space is small, so keep the stored text short.

Device Max size per object Total PIV storage
YubiKey 4 / 5 ~3,052 bytes ~51,200 bytes (shared with certificates)
YubiKey NEO ~2,025 bytes —
RS-Key (RP2350) 1,900 bytes 1,408 KB (shared with all applets)

YubiKey figures come from Yubico's documentation. Certificates and data objects share the total storage, so a key full of certificates has less room for notes.

RS-Key figures come from the firmware source. The per-object cap (MAX_OBJECT in crates/rsk-piv) keeps a response inside the 2 KiB CCID buffer. The total is the KVMAIN flash partition, which FIDO, PIV, OATH, and OpenPGP all share. A 16 MB board uses the same 1,408 KB partition as a 4 MB board; the extra flash is left unused. A 2 MB build (KVMAIN=896K) has 896 KB.

Real-World Example

My full secrets file was 12,371 bytes. I cut it to 3,223 bytes by removing whitespace, comments, and redundant entries. The trimmed file keeps only the most important secrets, especially the accounts that give access to everything else.

At 3,223 bytes the file was still too large for one object. gzip -9 reduced it to 1,465 bytes, which fits in a single object on both RS-Key and YubiKey.

Choosing a Data Object

Recommended Objects

These three objects are rarely used for their intended purpose, and all three require the PIN to read. That makes them the best choices for secret storage.

Object ID Name Suggested use
0x5FC121 Cardholder Iris Images Primary vault. No conflict with management keys or certificates.
0x5FC108 Cardholder Facial Image Secondary vault. A second file, a split file, or backup secrets.
0x5FC103 Cardholder Fingerprints Tertiary vault. Emergency codes or other small secrets.

I use 0x5FC121 as my primary vault and keep 0x5FC103 for emergency codes. I may use 0x5FC108 for a PGP private key later.

Caution

Do not use 0x5FC109 (Printed Information). YubiKey uses it to store a PIN-protected management key, and ykman refuses to write it in that case. Objects that do not require the PIN (such as CHUID or the certificate objects) can be read by anyone who holds the key.

All Standard PIV Data Objects

Most of these objects are needed for normal PIV authentication, or have a reserved purpose. They are listed for reference.

Object ID Name Read access Write access Purpose
0x5FC102 Cardholder Unique Identifier (CHUID) Always Management key Card identifier, expiry, and optional signature. Used by physical access control systems (PACS).
0x5FC105 X.509 Certificate for PIV Authentication Always Management key Certificate for slot 9a. Used for workstation and system logins.
0x5FC10A X.509 Certificate for Digital Signature Always Management key Certificate for slot 9c. Used to sign documents, Git commits, and email (S/MIME).
0x5FC10B X.509 Certificate for Key Management Always Management key Certificate for slot 9d. Used for asymmetric decryption.
0x5FC101 X.509 Certificate for Card Authentication Always Management key Certificate for slot 9e. Used by door readers and services that do not ask for a PIN.
0x5FC109 Printed Information PIN Management key Badge text (name, employee ID). YubiKey reuses it for the PIN-protected management key.
0x5FC103 Cardholder Fingerprints PIN Management key Fingerprint templates (CBEFF format).
0x5FC108 Cardholder Facial Image PIN Management key Facial photo (JPEG / JPEG 2000).
0x5FC121 Cardholder Iris Images PIN Management key Iris images (CBEFF format).
0x5FC107 Card Capability Container (CCC) Always Management key Describes the card's data model and supported features.
0x5FC106 Security Object Always Management key Signed SHA-256 digests of the other data objects, for tamper evidence.
0x5FC10C Key History Object Always Management key Tracks retired key management certificates in slots 82–95.
0x5FFF01 YubiKey Attestation Certificate Always Factory Yubico extension for slot f9. Proves that keys were generated on the device.

Prerequisites

  • A YubiKey or RP2350 key with the PIV application enabled.
  • ykman installed.
  • A PIV PIN. The factory default is 123456. Change it before you store secrets.
  • The PIV management key. Writing a data object needs the management key, not the PIN. If your management key is PIN-protected, ykman asks only for the PIN.

To change the PIN, and to replace the default management key with a random, PIN-protected one:

ykman piv access change-pin
ykman piv access change-management-key --generate --protect

The shell examples below use bash or zsh syntax. For fish, see the note on fish.

Writing a Secret

1. Create the Secret File

Work in $XDG_RUNTIME_DIR (usually /run/user/<uid>). That directory lives in RAM and only your user can read it. /tmp can be on disk and is visible to other users.

nano "$XDG_RUNTIME_DIR/secret.txt"

2. Check the Size

wc -c "$XDG_RUNTIME_DIR/secret.txt"
# 1465 /run/user/1000/secret.txt

Compare the number with the storage limits.

3a. Write the File As-Is

If the file fits, write it directly:

ykman piv objects import 0x5fc121 "$XDG_RUNTIME_DIR/secret.txt"
# Enter PIN:
# Object imported.

3b. Compress, Then Write

If the file is too large, compress it. xz often wins on larger text files, and gzip often wins on very small ones, so try both:

gzip -9 -c "$XDG_RUNTIME_DIR/secret.txt" | wc -c
xz -9 -e -c "$XDG_RUNTIME_DIR/secret.txt" | wc -c

Write the smaller result straight to the key. Process substitution passes the compressed data to ykman without writing a compressed file to disk:

# gzip
ykman piv objects import 0x5fc121 <(gzip -9 -c "$XDG_RUNTIME_DIR/secret.txt")
# xz
ykman piv objects import 0x5fc121 <(xz -9 -e -c "$XDG_RUNTIME_DIR/secret.txt")

If neither result fits, trim the file further or split it across two objects.

Important

Do not pipe data into ykman ... import -. When stdin is not a terminal, ykman reads the PIN or management key from the first line of stdin. That line is then consumed as a PIN, and the stored data is incomplete.

4. Verify the Write

Read the object back and compare it with the source. No output means a match.

# Uncompressed
cmp <(ykman piv objects export 0x5fc121 -) "$XDG_RUNTIME_DIR/secret.txt"
# gzip or xz: decompress the stored copy before you compare
cmp <(ykman piv objects export 0x5fc121 - | gunzip) "$XDG_RUNTIME_DIR/secret.txt"
cmp <(ykman piv objects export 0x5fc121 - | xz -d) "$XDG_RUNTIME_DIR/secret.txt"

Then clean up.

Reading a Secret

Export to - to send the data to stdout. The PIN prompt goes to stderr, so the secret goes to the terminal and never touches the disk.

# Uncompressed
ykman piv objects export 0x5fc121 -
# Compressed with gzip
ykman piv objects export 0x5fc121 - | gunzip
# Compressed with xz
ykman piv objects export 0x5fc121 - | xz -d

Append | less to page through a long file. To save a copy to a file instead:

ykman piv objects export 0x5fc121 - | gunzip > "$XDG_RUNTIME_DIR/secret.txt"

Writing Without a Plaintext File

These methods write a short secret without creating a plaintext file.

Prompt for the secret (recommended). -s hides the typed text, and the secret stays out of your shell history:

read -rsp "Secret: " secret && echo &&
  ykman piv objects import 0x5fc121 <(printf '%s' "$secret"); unset secret

Type the secret into the command. This is quick, but the secret is saved in your shell history unless you prevent that (in bash and zsh, start the command with a space when HISTCONTROL=ignorespace or setopt HIST_IGNORE_SPACE is set):

 ykman piv objects import 0x5fc121 <(printf '%s' "my secret text")

fish Shell

fish has no <(...) process substitution. Use psub instead:

ykman piv objects import 0x5fc121 (gzip -9 -c $XDG_RUNTIME_DIR/secret.txt | psub)
read -s -P "Secret: " secret; and ykman piv objects import 0x5fc121 (printf '%s' $secret | psub); set -e secret

Reading with export 0x5fc121 - | gunzip works in fish unchanged.

Security Considerations

  • The PIN is the only protection. Data on the key is not encrypted. Anyone who has the key and the PIN can read the objects. For extra protection, encrypt the file first (for example with age or gpg). Compress before you encrypt, because encrypted data does not compress.
  • The PIN has limited retries. After 3 wrong attempts (by default) the PIN is blocked. You then need the PUK to unblock it (default PUK: 12345678 — change it).
  • Reset erases everything. ykman piv reset deletes all data objects.
  • The key is not a backup. Keep another copy of these secrets in a separate safe place, in case the key is lost or damaged.
  • Change the default management key. With the default key, anyone who holds your key can overwrite or delete your objects without knowing the PIN. They cannot read the PIN-protected objects.
  • Temporary files leak. Delete every plaintext and compressed copy when you are done. Editors can also leave swap or backup files (for example .secret.txt.swp or secret.txt~).

Cleanup

Keep any copy you need in a secure place first. Then remove the working files:

rm -f "$XDG_RUNTIME_DIR"/secret.txt* "$XDG_RUNTIME_DIR"/.secret.txt*
# Check that nothing is left
ls -la "$XDG_RUNTIME_DIR" | grep -i secret

$XDG_RUNTIME_DIR is in RAM, so rm is enough and the data is gone at the next reboot.

If you worked in a directory on disk, overwrite the files before you delete them:

shred -u /path/to/secret.txt*

Note

shred is reliable only on hard disk drives with non-journaling file systems. On SSDs, and on journaling or copy-on-write file systems (ext4 with data journaling, Btrfs, ZFS), old copies can survive. Full-disk encryption, or working in RAM from the start, is the real protection.

Quick Reference

# Write (compressed)
ykman piv objects import 0x5fc121 <(gzip -9 -c "$XDG_RUNTIME_DIR/secret.txt")

# Read (compressed)
ykman piv objects export 0x5fc121 - | gunzip

# Write a short secret from a hidden prompt
read -rsp "Secret: " secret && echo &&
  ykman piv objects import 0x5fc121 <(printf '%s' "$secret"); unset secret

# Clean up
rm -f "$XDG_RUNTIME_DIR"/secret.txt*