Towards a test-suite for TOTP codes
Because I'm a massive nerd, I actually try to read specification documents. As I've ranted ad nauseam before, the current TOTP0 spec is irresponsibly obsolete.
The three major implementations of the spec - Google, Apple, and Yubico - all subtly disagree on how it should be implemented. Every other MFA app has their own idiosyncratic variants. The official RFC is infuriatingly vague. That's no good for a security specification. Multiple implementations are great, multiple interpretations are not.
So I've built a nascent test suite - you can use it to see if your favourite app can correctly implement the TOTP standard.
Please do contribute tests and / or feedback.
Here's what the standard actually says - see if you can find apps which don't implement it correctly.
Background
Time-based One Time Passwords are based on HOTP - HMAC-Based One-Time Password.
HOTP uses counters; a new password is regularly generated. TOTP uses time as the counter. At the time of writing this post, there have been about 1,740,800,000 seconds since the UNIX Epoc. So a TOTP with an period of 30 seconds is on counter (1,740,800,000 ➗ 30) = 58,026,666. Every 30 seconds, that counter increments by one.
Number of digits
How many digits should your 2FA token have? Google says 6 or 8. YubiCo graciously allows 7. Why those limits? Who knows!?
The HOTP specification gives an example of 6 digits. The example generates a code of 0x50ef7f19
which, in decimal, is 1357872921
. It then takes the last 6 digits to produce the code 872921
.
The TOTP RFC say:
Basically, the output of the HMAC-SHA-1 calculation is truncated to obtain user-friendly values 1.2. Background
But doesn't say how far to truncate.
There's nothing I can see in the spec that prevents an implementer using all 10. The HOTP spec, however, does place a minimum requirement - but no maximum:
Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. Depending on security requirements, Digit = 7 or more SHOULD be considered in order to extract a longer HOTP value. RFC 4226 - 5.3. Generating an HOTP Value
(As a minor point, the first digit is restricted to 0-2, so being 10 digits long isn't significantly stronger than 9 digits.)
Is a 4 digit code acceptable? The security might be weaker, but the usability is greater. Most apps will allow a one digit code to be returned. If no digits are specified, what should the default be?
Algorithm
The given algorithm in the HOTP spec is SHA-1.
In order to create the HOTP value, we will use the HMAC-SHA-1 algorithm RFC 4226 - 5.2. Description
As we now know, SHA-1 has some fundamental weaknesses. The spec comments (perhaps somewhat naïvely) about SHA-1:
The new attacks on SHA-1 have no impact on the security of HMAC-SHA-1. RFC 4226 - B.2. HMAC-SHA-1 Status
I daresay that's accurate. But the TOTP authors disagree and allow a for some different algorithms to be used. The specification for HMAC says:
HMAC can be used with any iterative cryptographic hash function, e.g., MD5, SHA-1 [Emphasis added] RFC 2104 - HMAC: Keyed-Hashing for Message Authentication
So most TOTP implementation allow SHA-1, SHA-256, and SHA-512.
TOTP implementations MAY use HMAC-SHA-256 or HMAC-SHA-512 functions […] instead of the HMAC-SHA-1 function that has been specified for the HOTP computation RFC 6238 - TOTP: Time-Based One-Time Password Algorithm
But the HOTP spec goes on to say:
Current candidates for such hash functions include SHA-1, MD5, RIPEMD-128/160. These different realizations of HMAC will be denoted by HMAC-SHA1, HMAC-MD5, HMAC-RIPEMD RFC 2104 - Introduction
So, should your TOTP app be able to handle an MD5 HMAC, or even SHA3-384? Will it? If no algorithm is specified, what should the default be?
Period
As discussed, this is what increments the counter for HOTP. The Google Spec says:
The period parameter defines a period that a TOTP code will be valid for, in seconds. The default value is 30.
The TOTP RFC says:
We RECOMMEND a default time-step size of 30 seconds 5.2. Validation and Time-Step Size
It doesn't make sense to have a negative number of second. But what about one second? What about a thousand? Lots of apps artificially restrict TOTP codes to 15, 30, or 60 seconds. But there's no specification to define a maximum or minimum value.
A user with mobility difficulties or on a high-latency connection probably wants a 5 minute validity period. Conversely, machine-to-machine communication can probably be done with a single-second (or lower) time period.
Secret
Google says the secret is
an arbitrary key value encoded in Base32 according to RFC 3548. The padding specified in RFC 3548 section 2.2 is not required and should be omitted.
Whereas Apple says it is:
An arbitrary key value encoded in Base32. Secrets should be at least 160 bits.
Can a shared secret be a single character? What about a thousand? Will padding characters cause a secret to be rejected or can they be safely stripped?
Label
The label allows you to have multiple codes for the same service. For example Big Bank:Personal Account
and Big Bank:Family Savings
. The Google spec is slightly confusing:
The issuer prefix and account name should be separated by a literal or url-encoded colon, and optional spaces may precede the account name. Neither issuer nor account name may themselves contain a colon.
What happens if they are not URl encoded? What about Matrix accounts which use a colon in their account name? Why are spaces allowed to precede the account name? Is there any practical limit to the length of these strings?
If no label is specified, what should the default be?
Issuer
Google says this parameter is:
Strongly Recommended The issuer parameter is a string value indicating the provider or service this account is associated with, URL-encoded according to RFC 3986. If the issuer parameter is absent, issuer information may be taken from the issuer prefix of the label. If both issuer parameter and issuer label prefix are present, they should be equal.
Apple merely says:
The domain of the site or app. The password manager uses this field to suggest credentials when setting up a new code generator.
Yubico equivocates with
The issuer parameter is recommended, but it can be absent. Also, the issuer parameter and issuer string in label should be equal.
If it isn't a domain, will Apple reject it? What happens if the issuer and the label don't match?
Next Steps
- If you're a user, please contribute tests or give feedback.
- If you're a developer, please check your app conforms to the specification.
- If you're from Google, Apple, Yubico, or another security company - wanna help me write up a proper RFC so this doesn't cause issues in the future?
-
Time-based One Time Passwords. Not the TV show you remember from your youth, grandad. ↩︎
@blog Do any of the errata listed (https://www.rfc-editor.org/errata_search.php?rfc=6238&rec_status=15&presentation=table) help with the issues you see? There was a thread on the security area list (saag@ietf.org) on this back in August of 2023 (https://mailarchive.ietf.org/arch/browse/saag/?q=rfc%206238) which may have touched on this.
If not, a new erratum might help trigger discussion and/or an update.
@ted_h @blog thanks! That's an interesting set of links. I'll check them out.
@Edent Wow, this is awesome, thanks 🙂 I'm refactoring my base stack auth and this will be really, really helpful. And I had absolutely no idea the standard was _this_ borked.
@blog regarding the test suite: I think for developers it would be helpful to have the test data (description, optauth uri) together with a totp code at a fixed time (for the valid ones) in a single JSON file that can be integrated into unit tests.
@blog I'd wish for a test case specifically with 8-digit TOTP, as that's what Battle.net (Blizzard's token for games like WoW/etc.) uses in practice – normally only through their own app, but their API allows getting the raw key for use with third-party TOTP apps... and I remember having had to search quite a bit for an Android app that supported 8-digit mode at all. (Settled on FreeOTP at first, later AndOTP.)
@grawity @blog thanks! Do you have an example of a raw key you could share? (With the secret redacted, obviously!)
@grawity @blog So this is why the KeepassXC TOTP feature allows you to select the key provider. There is a specific option for Steam, but I don't think battle net was in the list.
More comments on Mastodon.