check argon2id and the hmac jwts against outside implementations - #642
Merged
Conversation
every test in this module hashed with this module and verified with this module. that proves the two halves agree and nothing more: a kdf with the wrong version byte, lane count, or block-mixing order would pass all of them and still produce tags no other argon2 implementation accepts — which you discover on the day you migrate, with every stored password already written. seven known-answer vectors now come from outside, generated with openssl 3.5's ARGON2ID kdf. the first row is also the vector the argon2 reference implementation publishes for those parameters, so the chain is anchored twice. the rows vary passes, memory, lanes, and tag length independently, because each enters the algorithm at a different point and a mistake in one need not disturb the others. a second test requires every row's tag to differ from the first, which is what would catch a parameter being quietly ignored rather than used. the implementation passes all seven byte for byte, so this changes no behavior — it replaces an assumption with evidence. the whole suite still runs in under a second, since the vectors keep memory small except where memory is the thing under test.
hs384 and hs512 were covered only by a round trip: sign a token, verify it, confirm it came back. that shows the two halves of this module agree and says nothing about whether the bytes on the wire are the ones a peer will accept — a wrong digest size or a mis-assembled signing input passes a round trip every time. all three hmac tokens are now pinned. each was produced here and its signature recomputed with openssl's HMAC over the same signing input, matching byte for byte; the tokens are checked in as expected values, so sign_hs* is now an equality test rather than a self-consistency one. a second test verifies each token and then feeds each verifier a token signed at a different digest size, which must be refused. that is the confusion having three sizes side by side invites, and the reason to state it rather than assume it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
std.crypto.passwordhad eight tests and not one of them could tell you whether our argon2id is argon2id. every one hashed with this module and verified with this module, which proves the two halves agree and nothing further.sign_hs384/sign_hs512were in the same position — covered by a round trip only.that is the failure mode that showed up twice while building the zstd codec on this repo: the fse next-state offset and the predefined match-length distribution, where encoder and decoder shared the same wrong table and every round-trip test passed. only a foreign implementation caught them. for a password kdf the stakes are worse — a wrong version byte, lane count, or block-mixing order passes all eight tests and produces tags no other argon2 accepts, which you find out on migration day with every password already written.
so both are now pinned to something outside this tree.
argon2id — seven known-answer vectors generated with openssl 3.5's
ARGON2IDkdf. the first row is also the vector the argon2 reference implementation publishes for those parameters, so the chain is anchored twice. the rows vary passes, memory, lanes, and tag length independently, because each enters the algorithm at a different point and a mistake in one need not disturb the others. a second test requires every row's tag to differ from the first — that is what catches a parameter being quietly ignored rather than used, which the vectors alone would not reveal if the ignored value happened to match a default.hmac jwt — each token was produced here and its signature recomputed with
openssl dgst -sha{256,384,512} -mac HMACover the same signing input, matching byte for byte. the tokens are checked in as expected values, sosign_hs*is an equality test now. a second test feeds each verifier a token signed at a different digest size and requires refusal — the confusion that having three sizes side by side invites.our implementations pass everything byte for byte, so no behaviour changes here. this replaces an assumption with evidence.
what was tested
std/crypto/password.pith: 10 passed, 0 failed (was 8)std/crypto/jwt.pith: 20 passed, 0 failed (was 18)make run-regressions-only: 304 passed, 0 failednotes
assert_eqis undefined outside atestblock, so a table-driven runner cannot be factored into a helper and shared. that is what makes exhaustive negative cases tedious enough not to get written, and it is compiler work rather than library work. the tables here are inline loops inside their test blocks, which works fine at one-module scale.