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_LENbytes from the system CSPRNG.- hotp
- RFC 4226 §5.3’s HOTP, truncated to
digitsdecimal digits. - provisioning_
uri - Builds the
otpauth://totp/<issuer>:<account>?…URI. - step_at
- RFC 6238 §4’s
T: the number of wholePERIOD_SECONDSwindows since the epoch. - totp_at
- The code for one time step.
- verify
- Verifies
codeagainstsecretaroundnow_unix, returning the time step it matched.