Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OATH — TOTP / HOTP codes

The device stores authenticator-app secrets and computes the 6/8-digit codes on-card, over Yubico’s YKOATH protocol (CCID). The HMAC secret is written once and never leaves the chip again: every code is derived inside the firmware and only the digits come back. Up to 255 accounts.

Clients are the stock Yubico tooling — ykman oath on the command line and the Yubico Authenticator desktop/mobile app over USB. There is no rsk oath subcommand; OATH is driven entirely through those. Both ykman oath and the Yubico Authenticator gate on a Yubico YubiKey reader name, so they only see the device on the opt-in VIDPID=Yubikey5 build; the default RS-Key build enumerates as RS-Key Security Key (VID:PID 0x1209:0x0001) and stays invisible to them. Build that flavor when you want to drive OATH from the Yubico tools (build.md).

On Linux this needs pcscd running plus the polkit rule from linux.md; if you also use gpg, the disable-ccid line there keeps scdaemon from grabbing the reader and locking ykman out.

ykman oath info        # applet version + whether an access password is set

Add accounts

# Interactive: paste the base32 secret when prompted.
ykman oath accounts add github --issuer GitHub

# Straight from an otpauth:// URI (everything — secret, issuer, digits,
# algorithm, period — is parsed out of the URI):
ykman oath accounts uri 'otpauth://totp/GitHub:me@example.com?secret=BASE32SECRET&issuer=GitHub'

The account name shown in lists is issuer:account (here GitHub:me@example.com). Most sites hand you the secret two ways at enrollment — a QR code and a “can’t scan it?” base32 string. Either works:

  • base32 stringaccounts add (or accounts uri if they give the full otpauth:// link).
  • QR code on screenykman oath accounts uri -- reads it from the primary display, or the Yubico Authenticator GUI has a Scan QR code button that grabs whatever QR is visible.

Options that matter:

OptionEffectDefault
--touchcomputing this account’s code needs a button pressoff
--oath-type {TOTP,HOTP}counter-based vs time-basedTOTP
--algorithm {SHA1,SHA256,SHA512}HMAC hashSHA1
--digits {6,7,8}code length6
--period NTOTP step in seconds30
--counter NHOTP starting counter0
--forceoverwrite an existing account of the same name without asking

All three hashes are implemented on-card (SHA-1/256/512); RFC 6238/4226 test vectors for each pass in crates/rsk-oath. Adding a name that already exists overwrites the old secret in place (--force skips the prompt) — there is one credential per name.

Get codes

ykman oath accounts code            # every TOTP account at once
ykman oath accounts code github     # one account by name substring

code with no name runs the bulk path (CALCULATE ALL). Two kinds of account are not computed there and show a placeholder instead:

  • Touch-required accounts are listed but not calculated — name them explicitly (ykman oath accounts code github) and the firmware waits for the press, then prints the code. This is deliberate: a bulk read can never make a touch account leak a code without the button.
  • HOTP accounts are never computed in bulk either (it would silently burn the counter). Name them to step the counter once.

The Yubico Authenticator GUI shows all TOTP codes live and re-derives them each period; touch and HOTP entries get a tap-to-reveal button instead.

Touch-required accounts

ykman oath accounts add aws --issuer AWS --touch
# existing account → re-add with --touch --force, or toggle it in the GUI

With --touch, the firmware refuses to compute that account’s code until the BOOTSEL button is pressed; a timeout or a declined press returns “security status not satisfied” and nothing is computed. For HOTP this gate sits before the counter advances, so a denied touch burns no counter — a refused press leaves the account exactly where it was. The button is the same physical press used by FIDO and OpenPGP UIF; only one prompt is outstanding at a time.

The OATH access password

By default the credential list and codes are readable by anything that can reach the CCID interface. An optional access password gates the applet:

ykman oath access change            # set or change the password
ykman oath access remember          # cache it for this host (keyring)
ykman oath access forget            # drop the cached password

How it works on-card: the password becomes an HMAC key (PBKDF2 over the password, salted with the device serial, done host-side by ykman). On every fresh connection the card issues a random challenge; the host must answer with HMAC(key, challenge) before any account command is allowed, and the card answers the host’s challenge with the same key (mutual proof). Selecting the applet again re-locks it. The compare is constant-time and full-length, so a truncated or guessed response can’t brute-force its way in one byte at a time.

Footguns, stated plainly:

  • The password gates listing and computing codes over CCID, not the secrets at rest. OATH blobs sit in plaintext flash — unlike PIV and OpenPGP keys, they are not individually sealed. So this password protects the live applet, not a flash image; at-rest confidentiality for OATH rests only on the RP2350 device-level protections (secure boot / BOOTSEL lockout, see threat-model.md), not on per-credential sealing.
  • There is no recovery for a forgotten access password short of ykman oath reset, which wipes every account with it.
  • --touch per account and the access password are independent hardenings — you can use either, both, or neither.

Manage

ykman oath accounts list                       # account names (extended list also flags which need touch)
ykman oath accounts list -P                    # include the period column
ykman oath accounts rename github GitHub:work  # current name → new name
ykman oath accounts delete github              # remove one account
ykman oath reset                               # wipe the OATH applet only

rename rewrites the name in place and keeps the same secret and counter; renaming to a name that already exists is rejected. delete of an unknown name is a no-op error. reset clears all accounts, the access password, and the OTP password-PIN — and nothing outside OATH (FIDO/PIV/OpenPGP survive). To wipe the whole key instead, see rsk offboard.

Notes

  • Secrets live in device flash (in plaintext — OATH blobs are not individually sealed, unlike PIV/OpenPGP); codes are computed on-card, so the secret never returns to the host after add.
  • OATH accounts are not covered by the seed backup and do not come back on a backup key: they are sealed to this chip, not derived from the FIDO seed. Keep your otpauth:// URIs/QRs somewhere safe, or re-enroll on loss — the device cannot export a secret once it is stored.
  • HOTP counters are persisted across reboots and continue from where they were; touch-required HOTP accounts only advance the counter after the touch, so there are no drive-by increments.
  • OATH interop (add → list → calculate → delete, plus TOTP crypto-verified against RFC vectors, via both ykman oath and Yubico Authenticator) is tracked in interop.md.

Troubleshooting

  • ykman finds no reader / “Failed to connect”: on Linux this is almost always scdaemon holding the CCID interface after a gpg call — apply the disable-ccid line from linux.md and run gpgconf --kill scdaemon, then retry.
  • Codes are rejected by the site: TOTP depends on the host clock — the card has no battery-backed time and trusts the timestamp ykman/the GUI sends. Fix the host’s clock (NTP) and re-read. For HOTP, a code is rejected once the server counter has moved past yours; resync on the server side.
  • “Touch” account prints nothing under accounts code: that’s expected — bulk read skips touch accounts. Name the account so the firmware prompts for the press.
  • Forgot the access password: there is no unlock; ykman oath reset is the only way out, and it deletes every account.