lib/crypto/sha256: port SHA-256 over hash::hash; NIST test (989)
Port of ref/hare/crypto/sha256/sha256.ha — block-processed [64]u8
chunks, u32 modular arithmetic, hash::hash + io.writer surface. The
state embeds hash.hash (inline vtable at offset 0); the vtable + sum/
reset slots are wired post-construction (base64/memio convention).
u32 WRAPPING + vtable dispatch CONFIRMED CLEAN: all NIST vectors verify
byte-identical — empty, "abc", the 56-byte block-boundary case, and the
one-million-'a' multi-block stream (1000-byte chunks across many blocks,
stressing write()'s partial-block carry). cgen truncates u32 add/shift/
rotate to 32 bits correctly; no masking workaround needed.
Semantics-preserving spelling divergences (slice-copy as byte loops,
close()/digest loops) are noted at-site per CLAUDE.md rule 5/13.
ONE BEHAVIORAL DIVERGENCE, blocked on a cgen bug (flagged for ken/drew):
Hare's sum() snapshots the state (`let copy = *h`) so it is re-entrant.
That deref-copy of an array-containing struct miscompiles in ww cgen
(copied array fields come back zeroed). So sum() runs on the live state
and is SINGLE-SHOT until the cgen fix lands; every current caller does
one terminal sum(), so the digests are unaffected. Minimal repro:
type t = struct { h: [4]u32 };
let c: t = *(&s); // c.h reads back wrong
A sibling bug (array return-by-value zeroes the result) was also found
and is avoided in the test's buffer-based helper. Both filed for ken.
The hash/crypto modules are dead-imported (no selfhost combined.ww
regen). 9xx test numbers are full, so the run-test shares the 989
prefix with siphash (distinct `short` name; 949_* multi-file precedent).
This commit is contained in:
109
lib/crypto/sha256/sha256_test.ww
Normal file
109
lib/crypto/sha256/sha256_test.ww
Normal file
@@ -0,0 +1,109 @@
|
||||
// sha256_test — exercises lib/crypto/sha256 against the standard NIST
|
||||
// SHA-256 vectors (FIPS 180-4 examples + the classic "one million a's").
|
||||
// Run with `out/bin/ww run lib/crypto/sha256/sha256_test.ww`.
|
||||
//
|
||||
// The digest is the cgen-correctness oracle for u32 wrapping arithmetic
|
||||
// + the hash-vtable dispatch: any u32-overflow / rotate miscompile shows
|
||||
// up as a byte mismatch. Same signalled-then-fail()-with-+10 pattern as
|
||||
// the rest of the 9xx stdlib tests; the non-zero exit pinpoints the
|
||||
// failing case. Expected digests come through the (separately tested)
|
||||
// hex.decodestr so the vectors stay readable.
|
||||
package sha256;
|
||||
|
||||
import bytes;
|
||||
import crypto.sha256;
|
||||
import errors;
|
||||
import hash;
|
||||
import encoding.hex;
|
||||
import strings;
|
||||
import os;
|
||||
|
||||
let signalled: i32 = 0;
|
||||
fn fail() void = { os.exit(signalled + 10); };
|
||||
|
||||
// dohash — one-shot hash of `msg` into the caller's `out` (>= 32 bytes).
|
||||
// Mirrors the sum()-writes-into-a-buffer API (no array-by-value return).
|
||||
fn dohash(msg: []u8, out: []u8) void = {
|
||||
let st: sha256.state = sha256.sha256();
|
||||
let h: *hash.hash = (&st): *hash.hash;
|
||||
hash.write(h, msg);
|
||||
hash.sum(h, out);
|
||||
};
|
||||
|
||||
fn checkbytes(got: []u8, want: str) void = {
|
||||
match (hex.decodestr(want)) {
|
||||
case let w: []u8 => {
|
||||
if (!bytes.equal(got, w)) { fail(); };
|
||||
};
|
||||
case let e: errors.invalid => fail();
|
||||
};
|
||||
};
|
||||
|
||||
fn check(msg: []u8, want: str) void = {
|
||||
let out: [32]u8;
|
||||
dohash(msg, out[0:32]);
|
||||
checkbytes(out[0:32], want);
|
||||
};
|
||||
|
||||
// FIPS 180-4 Appendix B.1/B.2/B.3 vectors.
|
||||
|
||||
@test fn empty() void = {
|
||||
let e: [1]u8;
|
||||
check(e[0:0],
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
||||
};
|
||||
|
||||
@test fn abc() void = {
|
||||
check(strings.toutf8("abc"),
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
};
|
||||
|
||||
// 56-byte message: crosses no block boundary but lands exactly on the
|
||||
// padding edge (56 == BLOCKSZ - 8), the worst case for the pad length
|
||||
// branch in sum().
|
||||
@test fn twoblockpad() void = {
|
||||
check(strings.toutf8(
|
||||
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
|
||||
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
|
||||
};
|
||||
|
||||
// One million 'a' fed 1000 bytes at a time. 1000 is not a multiple of
|
||||
// BLOCKSZ, so this drives the partial-block carry in write() across many
|
||||
// calls and many full blocks — the strongest streaming + u32-wrapping
|
||||
// stress in the set.
|
||||
@test fn millionas() void = {
|
||||
let st: sha256.state = sha256.sha256();
|
||||
let h: *hash.hash = (&st): *hash.hash;
|
||||
let chunk: [1000]u8;
|
||||
let i: i32 = 0;
|
||||
for (i < 1000) {
|
||||
chunk[i] = 'a': u8;
|
||||
i += 1;
|
||||
};
|
||||
i = 0;
|
||||
for (i < 1000) {
|
||||
hash.write(h, chunk[0:1000]);
|
||||
i += 1;
|
||||
};
|
||||
let out: [32]u8;
|
||||
hash.sum(h, out[0:32]);
|
||||
checkbytes(out[0:32],
|
||||
"cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0");
|
||||
};
|
||||
|
||||
// sz()/bsz() report the SHA-256 constants regardless of state.
|
||||
@test fn sizes() void = {
|
||||
let st: sha256.state = sha256.sha256();
|
||||
let h: *hash.hash = (&st): *hash.hash;
|
||||
if (hash.sz(h) != 32: size) { fail(); };
|
||||
if (hash.bsz(h) != 64: size) { fail(); };
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
signalled = 1; empty();
|
||||
signalled = 2; abc();
|
||||
signalled = 3; twoblockpad();
|
||||
signalled = 4; millionas();
|
||||
signalled = 5; sizes();
|
||||
return 0;
|
||||
};
|
||||
Reference in New Issue
Block a user