// 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. A failing row aborts via the assert/abort // builtin (task #5 @test conversion). Expected digests come through the // (separately tested) // hex.decodestr so the vectors stay readable. package sha256_test; import bytes; import crypto.sha256; import errors; import hash; import encoding.hex; import strings; // 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 => { assert(!(!bytes.equal(got, w))); }; case let e: errors.invalid => abort(); }; }; 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"); }; // reentrant — sum() is non-destructive: it pads+finalizes a snapshot, so // the live hash survives. Summing twice yields the same digest, and a // write after a sum() continues the same stream. Pins the faithful // state-snapshot restored once #265 fold-1 unblocked the deref-rhs // aggregate copy `let copy = *h` (see sha256.ww [[sumfn]]). @test fn reentrant() void = { let st: sha256.state = sha256.sha256(); let h: *hash.hash = (&st): *hash.hash; hash.write(h, strings.toutf8("abc")); let out1: [32]u8; let out2: [32]u8; hash.sum(h, out1[0:32]); hash.sum(h, out2[0:32]); assert(!(!bytes.equal(out1[0:32], out2[0:32]))); checkbytes(out1[0:32], "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); // Write more after the sum(): the post-sum stream is uncorrupted, so // the running digest of "abc"+"def" matches a one-shot hash of "abcdef". hash.write(h, strings.toutf8("def")); let out3: [32]u8; hash.sum(h, out3[0:32]); let want: [32]u8; dohash(strings.toutf8("abcdef"), want[0:32]); assert(!(!bytes.equal(out3[0:32], want[0:32]))); }; // 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; assert(!(hash.sz(h) != 32: size)); assert(!(hash.bsz(h) != 64: size)); }; export fn main() i32 = { empty(); abc(); twoblockpad(); millionas(); reentrant(); sizes(); return 0; };