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:
330
lib/crypto/sha256/sha256.ww
Normal file
330
lib/crypto/sha256/sha256.ww
Normal file
@@ -0,0 +1,330 @@
|
||||
// crypto/sha256 — SHA-256 (FIPS 180-4) over the hash::hash interface.
|
||||
// Port of ref/hare/crypto/sha256/sha256.ha. The digest is computed
|
||||
// 64-byte block at a time; partial writes buffer in `x` until a full
|
||||
// block accumulates, and [[sum]] appends the 0x80/zero/bit-length
|
||||
// padding before reading out the eight state words big-endian.
|
||||
//
|
||||
// u32 WRAPPING. SHA-256 is defined over 32-bit modular arithmetic. ww's
|
||||
// `uint` is a 64-bit machine word, so this module uses `u32` throughout
|
||||
// (matching Hare) and relies on cgen truncating u32 add/shift/rotate to
|
||||
// 32 bits. The NIST vectors in sha256_test.ww are the oracle: a digest
|
||||
// mismatch would mean cgen promoted a u32 op to 64-bit without wrapping
|
||||
// (a cgen bug to STOP+report, NOT to mask here).
|
||||
//
|
||||
// DIVERGENCES from sha256.ha:
|
||||
// - sum() is SINGLE-SHOT, not re-entrant: Hare's state-snapshot copy
|
||||
// hits an array-field deref-copy cgen bug (see [[sumfn]]). This is
|
||||
// the one BEHAVIORAL divergence (blocked on a filed cgen fix); the
|
||||
// rest below are semantics-preserving spellings.
|
||||
// - `state` embeds [[hash.hash]], whose first field is an inline
|
||||
// io.vtable (not Hare's `stream: io::stream` pointer); see
|
||||
// lib/hash/hash.ww. The vtable slots are wired post-construction in
|
||||
// [[sha256]] rather than pointing at a module-level const vtable
|
||||
// (the base64/memio/io convention; ww has no const-vtable-of-fn-ptrs
|
||||
// idiom).
|
||||
// - slice-to-slice copy assignment (`h.x[a..b] = b[..n]`) is spelled
|
||||
// as a byte loop — ww has no slice-copy assignment (cf lib/memio's
|
||||
// flat-field note). Same observable effect.
|
||||
// - close() zeros the `h` state words with a loop instead of Hare's
|
||||
// `(s.h[..]: *[*]u8)[..len*size(u32)]` reinterpret + bytes::zero —
|
||||
// ww avoids the `*[*]u8` cast; bytes.zero still clears `x`.
|
||||
// - the eight beputu32 digest writes are a loop (stride routed through
|
||||
// size(u32) per CLAUDE.md rule 13) rather than Hare's unrolled 8.
|
||||
package sha256;
|
||||
|
||||
import bytes;
|
||||
import crypto.math;
|
||||
import endian;
|
||||
import hash;
|
||||
import io;
|
||||
import os;
|
||||
|
||||
// ref/hare/crypto/sha256/sha256.ha:11.
|
||||
export def SZ: size = 32;
|
||||
|
||||
// ref/hare/crypto/sha256/sha256.ha:14.
|
||||
export def BLOCKSZ: size = 64;
|
||||
|
||||
// ref/hare/crypto/sha256/sha256.ha:17-24. u32 suffixes: these scalar
|
||||
// defs don't ride #251 array-element narrowing, and four exceed 2^31.
|
||||
def init0: u32 = 0x6A09E667u32;
|
||||
def init1: u32 = 0xBB67AE85u32;
|
||||
def init2: u32 = 0x3C6EF372u32;
|
||||
def init3: u32 = 0xA54FF53Au32;
|
||||
def init4: u32 = 0x510E527Fu32;
|
||||
def init5: u32 = 0x9B05688Cu32;
|
||||
def init6: u32 = 0x1F83D9ABu32;
|
||||
def init7: u32 = 0x5BE0CD19u32;
|
||||
|
||||
// ref/hare/crypto/sha256/sha256.ha:26-38. The round constants (first 32
|
||||
// bits of the fractional parts of the cube roots of the first 64
|
||||
// primes). Bare hex literals narrow to u32 via #251 array-element
|
||||
// narrowing (same as base64's decmap).
|
||||
def k: [64]u32 = [
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
||||
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||||
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
||||
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
||||
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
||||
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
||||
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
||||
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||||
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||
];
|
||||
|
||||
// ref/hare/crypto/sha256/sha256.ha:40-46. Embeds [[hash.hash]] (whose
|
||||
// first field is the inline vtable) so `&st` casts to `*hash.hash` and
|
||||
// `&st.vt` to `io.stream`. `h` = the eight running state words, `x` =
|
||||
// the partial-block buffer, `nx` = bytes buffered in `x`, `ln` = total
|
||||
// bytes written.
|
||||
export type state = struct {
|
||||
hash.hash,
|
||||
h: [8]u32,
|
||||
x: [64]u8, // BLOCKSZ; array dims require an integer literal
|
||||
nx: size,
|
||||
ln: size,
|
||||
};
|
||||
|
||||
// sha256 — construct a SHA-256 hash. ref/hare/crypto/sha256/sha256.ha:58-69.
|
||||
// Wires the vtable + interface fn-ptr slots inline (base64/memio
|
||||
// convention) then resets to the initial state. Returned BY VALUE; the
|
||||
// caller passes `&st` (as `*hash.hash`) to the hash dispatchers.
|
||||
export fn sha256() state = {
|
||||
let sha: state;
|
||||
sha.vt.writer = (&writefn): *io.writer;
|
||||
sha.vt.closer = (&closefn): *io.closer;
|
||||
sha.sum = (&sumfn): *fn(h: *hash.hash, buf: []u8) void;
|
||||
sha.reset = (&resetfn): *fn(h: *hash.hash) void;
|
||||
sha.sz = SZ;
|
||||
sha.bsz = BLOCKSZ;
|
||||
hash.reset((&sha): *hash.hash);
|
||||
return sha;
|
||||
};
|
||||
|
||||
// ref/hare/crypto/sha256/sha256.ha:71-83.
|
||||
fn resetfn(h: *hash.hash) void = {
|
||||
let st: *state = h: *state;
|
||||
st.h[0] = init0;
|
||||
st.h[1] = init1;
|
||||
st.h[2] = init2;
|
||||
st.h[3] = init3;
|
||||
st.h[4] = init4;
|
||||
st.h[5] = init5;
|
||||
st.h[6] = init6;
|
||||
st.h[7] = init7;
|
||||
st.nx = 0: size;
|
||||
st.ln = 0: size;
|
||||
};
|
||||
|
||||
// writefn — the io.writer slot. ref/hare/crypto/sha256/sha256.ha:85-113.
|
||||
// Buffers a partial block in `x`, processes every whole block inline,
|
||||
// and stashes the trailing remainder. Returns the input byte count
|
||||
// (never errors). Slice copies are byte loops (header divergence).
|
||||
fn writefn(s: io.stream, buf: []u8) (size | io.error) = {
|
||||
let st: *state = s: *state;
|
||||
let b: []u8 = buf;
|
||||
let total: size = b.len: size;
|
||||
st.ln += total;
|
||||
let bs: i32 = BLOCKSZ: i32;
|
||||
|
||||
if (st.nx > 0: size) {
|
||||
let avail: size = BLOCKSZ - st.nx;
|
||||
let take: size = avail;
|
||||
if ((b.len: size) <= avail) { take = b.len: size; };
|
||||
let base: i32 = st.nx: i32;
|
||||
let ti: i32 = take: i32;
|
||||
let i: i32 = 0;
|
||||
for (i < ti) {
|
||||
st.x[base + i] = b[i];
|
||||
i += 1;
|
||||
};
|
||||
st.nx += take;
|
||||
if (st.nx == BLOCKSZ) {
|
||||
block(st, st.x[0:bs]);
|
||||
st.nx = 0: size;
|
||||
};
|
||||
b = b[ti:b.len];
|
||||
};
|
||||
|
||||
if ((b.len: size) >= BLOCKSZ) {
|
||||
let mask: i32 = bs - 1;
|
||||
let nfull: i32 = b.len & ~mask;
|
||||
block(st, b[0:nfull]);
|
||||
b = b[nfull:b.len];
|
||||
};
|
||||
|
||||
if (b.len > 0) {
|
||||
let i: i32 = 0;
|
||||
for (i < b.len) {
|
||||
st.x[i] = b[i];
|
||||
i += 1;
|
||||
};
|
||||
st.nx = b.len: size;
|
||||
};
|
||||
return total;
|
||||
};
|
||||
|
||||
// sumfn — finalize and emit the digest. ref/hare/crypto/sha256/sha256.ha:115-143.
|
||||
//
|
||||
// DIVERGENCE (re-entrancy / blocked on a cgen bug). Hare snapshots the
|
||||
// state — `let copy = *h; let h = ©` — so sum() is non-destructive
|
||||
// and the caller can keep writing afterwards. That deref-copy of an
|
||||
// array-containing struct miscompiles in ww cgen (the copied `h`/`x`
|
||||
// arrays come back zeroed; minimal repro: `type t = struct { h: [4]u32 };
|
||||
// let c: t = *(&s);` reads c.h back wrong — array-field + pointer-deref
|
||||
// copy, a sibling of the #135/#252 array-field cgen family). Filed for
|
||||
// ken; flagged for a drew fidelity ruling. Until it lands, sum() runs on
|
||||
// the live state and is therefore SINGLE-SHOT: writing or summing again
|
||||
// after a sum() yields wrong results. Every current caller does exactly
|
||||
// one terminal sum(), so the NIST digests are unaffected (all vectors,
|
||||
// incl. the multi-block million-'a' stream, verify byte-identical). The
|
||||
// final close() still wipes the live state per Hare.
|
||||
fn sumfn(h: *hash.hash, buf: []u8) void = {
|
||||
let st: *state = h: *state;
|
||||
|
||||
let ln: size = st.ln;
|
||||
let tmp: [64]u8 = [0...];
|
||||
tmp[0] = 0x80u8;
|
||||
|
||||
// Pad so the message + 0x80 + zeros ends 8 bytes (the bit-length
|
||||
// field) shy of a block boundary.
|
||||
let lenbytes: size = size(u64);
|
||||
let thresh: size = BLOCKSZ - lenbytes;
|
||||
let m: size = ln % BLOCKSZ;
|
||||
let pad: size = thresh - m;
|
||||
if (m >= thresh) { pad = (BLOCKSZ + thresh) - m; };
|
||||
let padn: i32 = pad: i32;
|
||||
match (writefn(&st.vt, tmp[0:padn])) {
|
||||
case let z: size => void;
|
||||
case let e: io.error => abort("sha256.sum: pad write errored");
|
||||
};
|
||||
|
||||
ln <<= 3; // bytes -> bits
|
||||
endian.beputu64(tmp[0:8], ln: u64);
|
||||
let lb: i32 = lenbytes: i32;
|
||||
match (writefn(&st.vt, tmp[0:lb])) {
|
||||
case let z: size => void;
|
||||
case let e: io.error => abort("sha256.sum: length write errored");
|
||||
};
|
||||
|
||||
os.assert(st.nx == 0: size, "sha256.sum: residual partial block");
|
||||
|
||||
let stride: i32 = size(u32): i32;
|
||||
let i: i32 = 0;
|
||||
for (i < 8) {
|
||||
let off: i32 = i * stride;
|
||||
endian.beputu32(buf[off:off + stride], st.h[i]);
|
||||
i += 1;
|
||||
};
|
||||
|
||||
hash.close(h);
|
||||
};
|
||||
|
||||
// block — process every whole 64-byte block in `buf`.
|
||||
// ref/hare/crypto/sha256/sha256.ha:146-213. Param renamed `st` (Hare's
|
||||
// `h`) so the 8th compression working var can stay `hh` without
|
||||
// shadowing the state pointer.
|
||||
fn block(st: *state, buf: []u8) void = {
|
||||
let w: [64]u32 = [0...];
|
||||
let h0: u32 = st.h[0];
|
||||
let h1: u32 = st.h[1];
|
||||
let h2: u32 = st.h[2];
|
||||
let h3: u32 = st.h[3];
|
||||
let h4: u32 = st.h[4];
|
||||
let h5: u32 = st.h[5];
|
||||
let h6: u32 = st.h[6];
|
||||
let h7: u32 = st.h[7];
|
||||
let bs: i32 = BLOCKSZ: i32;
|
||||
let b: []u8 = buf;
|
||||
|
||||
for (b.len >= bs) {
|
||||
let i: i32 = 0;
|
||||
for (i < 16) {
|
||||
let j: i32 = i * 4;
|
||||
w[i] = ((b[j]: u32) << 24u32)
|
||||
| ((b[j + 1]: u32) << 16u32)
|
||||
| ((b[j + 2]: u32) << 8u32)
|
||||
| (b[j + 3]: u32);
|
||||
i += 1;
|
||||
};
|
||||
|
||||
i = 16;
|
||||
for (i < 64) {
|
||||
let v1: u32 = w[i - 2];
|
||||
let t1: u32 = math.rotr32(v1, 17)
|
||||
^ math.rotr32(v1, 19)
|
||||
^ (v1 >> 10u32);
|
||||
let v2: u32 = w[i - 15];
|
||||
let t2: u32 = math.rotr32(v2, 7)
|
||||
^ math.rotr32(v2, 18)
|
||||
^ (v2 >> 3u32);
|
||||
w[i] = t1 + w[i - 7] + t2 + w[i - 16];
|
||||
i += 1;
|
||||
};
|
||||
|
||||
let a: u32 = h0;
|
||||
let bb: u32 = h1;
|
||||
let c: u32 = h2;
|
||||
let d: u32 = h3;
|
||||
let e: u32 = h4;
|
||||
let f: u32 = h5;
|
||||
let g: u32 = h6;
|
||||
let hh: u32 = h7;
|
||||
i = 0;
|
||||
for (i < 64) {
|
||||
let t1: u32 = hh
|
||||
+ (math.rotr32(e, 6)
|
||||
^ math.rotr32(e, 11)
|
||||
^ math.rotr32(e, 25))
|
||||
+ ((e & f) ^ (~e & g)) + k[i] + w[i];
|
||||
let t2: u32 = (math.rotr32(a, 2)
|
||||
^ math.rotr32(a, 13)
|
||||
^ math.rotr32(a, 22))
|
||||
+ ((a & bb) ^ (a & c) ^ (bb & c));
|
||||
hh = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + t1;
|
||||
d = c;
|
||||
c = bb;
|
||||
bb = a;
|
||||
a = t1 + t2;
|
||||
i += 1;
|
||||
};
|
||||
|
||||
h0 += a;
|
||||
h1 += bb;
|
||||
h2 += c;
|
||||
h3 += d;
|
||||
h4 += e;
|
||||
h5 += f;
|
||||
h6 += g;
|
||||
h7 += hh;
|
||||
|
||||
b = b[bs:b.len];
|
||||
};
|
||||
|
||||
st.h[0] = h0;
|
||||
st.h[1] = h1;
|
||||
st.h[2] = h2;
|
||||
st.h[3] = h3;
|
||||
st.h[4] = h4;
|
||||
st.h[5] = h5;
|
||||
st.h[6] = h6;
|
||||
st.h[7] = h7;
|
||||
};
|
||||
|
||||
// closefn — wipe sensitive state. ref/hare/crypto/sha256/sha256.ha:215-219.
|
||||
// h zeroed via a loop (header divergence); x via bytes.zero array-decay.
|
||||
fn closefn(s: io.stream) (void | io.error) = {
|
||||
let st: *state = s: *state;
|
||||
let i: i32 = 0;
|
||||
for (i < 8) {
|
||||
st.h[i] = 0u32;
|
||||
i += 1;
|
||||
};
|
||||
bytes.zero(st.x);
|
||||
return void;
|
||||
};
|
||||
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