Skip to content

check argon2id and the hmac jwts against outside implementations - #642

Merged
kacy merged 2 commits into
mainfrom
argon2-known-answer-vectors
Aug 3, 2026
Merged

check argon2id and the hmac jwts against outside implementations#642
kacy merged 2 commits into
mainfrom
argon2-known-answer-vectors

Conversation

@kacy

@kacy kacy commented Aug 3, 2026

Copy link
Copy Markdown
Owner

std.crypto.password had 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_hs512 were 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 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 — 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 HMAC over the same signing input, matching byte for byte. the tokens are checked in as expected values, so sign_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 failed
  • doc coverage clean on both
  • the password suite still runs in under a second: the vectors keep memory small except in the row where memory is the thing under test

notes

  • ps256 is deliberately not vectored — pss salts randomly, so there is no byte-equality to assert. es256 already carries the rfc 7515 a.3 vector.
  • the remaining testing gap is structural rather than missing vectors: assert_eq is undefined outside a test block, 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.

kacy added 2 commits August 3, 2026 17:39
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.
@kacy
kacy merged commit 29c4181 into main Aug 3, 2026
2 checks passed
@kacy
kacy deleted the argon2-known-answer-vectors branch August 3, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant