Skip to main content

Module totp

Module totp 

Source
Expand description

RFC 6238 time-based one-time passwords, over RFC 4226’s HOTP.

Hand-rolled on ring::hmac, which is already this crate’s crypto backend everywhere else, rather than pulling a TOTP crate in: the whole primitive is an HMAC, an eight-byte counter and RFC 4226 §5.4’s dynamic truncation, and both RFCs publish test vectors, so the hand-rolled version is checkable against the same authority a dependency would be. Same bar crate::admin::password’s module doc sets when it rejects Argon2id.

This module holds no database access and no I/O – the split acme_proxy_core::eab (pure verification) and acme_proxy_store::eab (persistence) already make. The replay guard RFC 6238 §5.2 asks for needs a row, so it lives in AdminUser::claim_totp_step; verify deliberately knows nothing about it.

§Why HMAC-SHA-1

crates/core/src/eab.rs uses HMAC-SHA256, so SHA-256 looks like the house style here. It is the wrong choice: Google Authenticator ignores the algorithm= parameter of an otpauth:// URI and always computes SHA-1, so an operator enrolling with the most widely deployed authenticator would get an entry producing wrong codes forever, with no diagnosis available from either side. A second factor nobody can enrol is not a second factor.

SHA-1’s collision attacks do not weaken HMAC-SHA1 – HMAC’s security rests on the compression function being a PRF, not on collision resistance, which is why RFC 6238 §1.2 and NIST SP 800-107 both still specify it here. The ring constant is named HMAC_SHA1_FOR_LEGACY_USE_ONLY, so this comment is the reason not to “fix” it.

The stored secret carries no algorithm tag, so changing this later means re-enrolment – one acme-proxy admin user totp reset per operator, which is exactly why that command exists.

Structs§

Enrolment
A generated enrolment: the bytes to store, and the two things the operator must see exactly once.

Constants§

DIGITS
Code length. Six is what every authenticator renders.
ISSUER
The issuer this server names itself as in an otpauth:// URI. It is what an authenticator app shows above the code.
PERIOD_SECONDS
RFC 6238 §4’s default time step, and universally assumed by clients.
SECRET_LEN
160 bits: RFC 4226 §4 R6’s floor, what every authenticator expects, and exactly 32 base32 characters – so the encoder never has to pad and the operator never has to type a =.
SKEW_STEPS
RFC 6238 §5.2 permits one step of clock skew either way. More widens the guessing window for no usability gain.

Functions§

account_label
The account half of an otpauth:// label: alice@admin.example.com.
base32_encode
RFC 4648 §6 base32: uppercase, unpadded.
begin_enrolment
Mints a secret and the two representations of it the enrolment page shows.
generate_secret
SECRET_LEN bytes from the system CSPRNG.
hotp
RFC 4226 §5.3’s HOTP, truncated to digits decimal digits.
provisioning_uri
Builds the otpauth://totp/<issuer>:<account>?… URI.
step_at
RFC 6238 §4’s T: the number of whole PERIOD_SECONDS windows since the epoch.
totp_at
The code for one time step.
verify
Verifies code against secret around now_unix, returning the time step it matched.