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 string →
accounts add(oraccounts uriif they give the fullotpauth://link). - QR code on screen →
ykman 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:
| Option | Effect | Default |
|---|---|---|
--touch | computing this account’s code needs a button press | off |
--oath-type {TOTP,HOTP} | counter-based vs time-based | TOTP |
--algorithm {SHA1,SHA256,SHA512} | HMAC hash | SHA1 |
--digits {6,7,8} | code length | 6 |
--period N | TOTP step in seconds | 30 |
--counter N | HOTP starting counter | 0 |
--force | overwrite 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. --touchper 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 oathand Yubico Authenticator) is tracked in interop.md.
Troubleshooting
ykmanfinds no reader / “Failed to connect”: on Linux this is almost alwaysscdaemonholding the CCID interface after agpgcall — apply thedisable-ccidline from linux.md and rungpgconf --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 resetis the only way out, and it deletes every account.