From 5fc32b49260b9e18ebd3cd6fea252fa786e78493 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 23 Aug 2026 00:42:43 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20iteration=2034=20=E2=80=94=20digest=20b?= =?UTF-8?q?uiltins=20sha1/sha256/hmac=5Fsha256=20(ids=2085-87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runtime/src/crypto.c: hand-rolled cores, whole-value over Bytes, allocation-free tails; VM half returns fresh Bytes, WO_T_BOUNDS on wrong class id (Bytes builtins' message shape) - test_crypto: RFC 3174 + FIPS 180-4 + RFC 4231 (incl. long-key case 6) + 63/64/65 block-boundary sweep + the RFC 6455 handshake input, 18/0 - compiler surface flat per house convention (sha1, not crypto.sha1 — matches base64_encode): types.ml signatures + result types, emit.ml ids/dispatch/arity/known-list/drop-table (fresh-Bytes entries so results get their drops) - corpus run/crypto-digests pins the .wo path through base64_encode - no .wob bump (ticks-84 precedent); WO_B_MAX 87; surface doc rows - slice marker + board row: iteration 24 (absorbing 31+34) executing - battery 12/12 fresh-built Co-Authored-By: Claude Fable 5 --- compiler/src/emit.ml | 15 +- compiler/src/types.ml | 5 + .../2026-08-23-chat-ws-lifecycle.md | 12 + docs/plan/oop-vm/08-builtin-surface.md | 3 + docs/stories/00-status.md | 2 +- runtime/src/builtin.c | 3 + runtime/src/crypto.c | 252 ++++++++++++++++++ runtime/src/crypto.h | 19 ++ runtime/src/wob.h | 8 +- runtime/test/test_crypto.c | 112 ++++++++ tests/corpus/run/crypto-digests/fixture.out | 3 + tests/corpus/run/crypto-digests/fixture.wo | 13 + 12 files changed, 444 insertions(+), 3 deletions(-) create mode 100644 docs/in-progress/2026-08-23-chat-ws-lifecycle.md create mode 100644 runtime/src/crypto.c create mode 100644 runtime/src/crypto.h create mode 100644 runtime/test/test_crypto.c create mode 100644 tests/corpus/run/crypto-digests/fixture.out create mode 100644 tests/corpus/run/crypto-digests/fixture.wo diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index c2740ae..0fec3cb 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -285,6 +285,9 @@ let b_base64_encode = 80 let b_base64_decode = 81 let b_bytes_of_text = 82 let b_text_of_bytes = 83 +let b_sha1 = 85 +let b_sha256 = 86 +let b_hmac_sha256 = 87 let b_split = 28 let b_split_ws = 29 let b_join = 30 @@ -1083,6 +1086,7 @@ let builtin_ret (name : string) (argty : Ast.field_ty option) : Ast.field_ty opt | "float_to_text" | "base64_encode" | "text_of_bytes" -> Some (Scalar "Text") | "bytes_eq" -> Some (Scalar "Bool") | "bytes_slice" | "bytes_concat" | "bytes_of_text" -> Some (Scalar "Bytes") + | "sha1" | "sha256" | "hmac_sha256" -> Some (Scalar "Bytes") | "base64_decode" -> Some (Nullable (Scalar "Bytes")) | _ -> None @@ -1099,7 +1103,9 @@ let is_builtin_name (n : string) = (* iteration 19: Float bridges and Bytes surface *) "float"; "trunc"; "parse_float"; "float_to_text"; "float_cmp"; "bytes_len"; "bytes_at"; "bytes_slice"; "bytes_eq"; "bytes_concat"; "base64_encode"; "base64_decode"; - "bytes_of_text"; "text_of_bytes" ] + "bytes_of_text"; "text_of_bytes"; + (* iteration 34: digests *) + "sha1"; "sha256"; "hmac_sha256" ] (* ---- unions and variants (haxe-parity Task 4) ------------------------ @@ -3630,6 +3636,8 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : || id = b_float_of_int || id = b_trunc || id = b_parse_float || id = b_float_to_text || id = b_bytes_len || id = b_base64_encode || id = b_base64_decode || id = b_bytes_of_text || id = b_text_of_bytes + (* iteration 34, one argument *) + || id = b_sha1 || id = b_sha256 then 1 else if id = b_multi_push || id = b_multi_get || id = b_map_get || id = b_map_has @@ -3640,6 +3648,8 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : || id = b_map_key_at || id = b_map_val_at (* iteration 19, two arguments *) || id = b_float_cmp || id = b_bytes_at || id = b_bytes_eq || id = b_bytes_concat + (* iteration 34, two arguments *) + || id = b_hmac_sha256 then 2 else 3 (* b_bytes_slice lands here with substr's shape: (value, start, len) *) in @@ -3755,6 +3765,9 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : | "base64_decode" -> fixed b_base64_decode | "bytes_of_text" -> fixed b_bytes_of_text | "text_of_bytes" -> fixed b_text_of_bytes + | "sha1" -> fixed b_sha1 + | "sha256" -> fixed b_sha256 + | "hmac_sha256" -> fixed b_hmac_sha256 | "multi_new" | "map_new" -> let is_map = name = "map_new" in if args <> [] then bad (Printf.sprintf "builtin `%s` takes no arguments" name) diff --git a/compiler/src/types.ml b/compiler/src/types.ml index 2cdc973..e1b4425 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -835,6 +835,10 @@ let builtin_signatures : (string * int * builtin_arg_req list) list = ("base64_decode", 1, [ ReqText ]); ("bytes_of_text", 1, [ ReqText ]); ("text_of_bytes", 1, [ ReqBytes ]); + (* iteration 34: digests. Bytes in, Bytes out; HMAC is key-then-message. *) + ("sha1", 1, [ ReqBytes ]); + ("sha256", 1, [ ReqBytes ]); + ("hmac_sha256", 2, [ ReqBytes; ReqBytes ]); ] let rec unwrap_nullable (t : typ) : typ = @@ -1040,6 +1044,7 @@ let builtin_confident_ret (name : string) (arg0 : typ option) : typ option = | "float_to_text" | "base64_encode" | "text_of_bytes" -> Some (TScalar "Text") | "bytes_eq" -> Some (TScalar "Bool") | "bytes_slice" | "bytes_concat" | "bytes_of_text" -> Some (TScalar "Bytes") + | "sha1" | "sha256" | "hmac_sha256" -> Some (TScalar "Bytes") (* malformed base64 is nil, not a trap: it arrives from the network *) | "base64_decode" -> Some (TNullable (TScalar "Bytes")) | _ -> None diff --git a/docs/in-progress/2026-08-23-chat-ws-lifecycle.md b/docs/in-progress/2026-08-23-chat-ws-lifecycle.md new file mode 100644 index 0000000..0229e6f --- /dev/null +++ b/docs/in-progress/2026-08-23-chat-ws-lifecycle.md @@ -0,0 +1,12 @@ +# In progress — chat + actor lifecycle (iteration 24, absorbing 31 + 34) + +Active slice, branch `chat-ws-lifecycle`. Spec: +[`../superpowers/specs/2026-08-23-chat-websocket-actor-lifecycle-design.md`](../superpowers/specs/2026-08-23-chat-websocket-actor-lifecycle-design.md) +· plan: +[`../superpowers/plans/2026-08-23-chat-ws-lifecycle.md`](../superpowers/plans/2026-08-23-chat-ws-lifecycle.md). + +Stages: 1 crypto builtins (85–87) · 2 lifecycle (cap/`call`/monitor/ +`time.after`, ids 88–90, WO_T_ACTOR, WO-E226) · 3 framework WS +(`ws_accept` + `wsframe`) · 4 chat sample + gate · 5 closeout. + +This file is deleted when the slice lands (board convention). diff --git a/docs/plan/oop-vm/08-builtin-surface.md b/docs/plan/oop-vm/08-builtin-surface.md index df6f0b6..c1e82be 100644 --- a/docs/plan/oop-vm/08-builtin-surface.md +++ b/docs/plan/oop-vm/08-builtin-surface.md @@ -274,6 +274,9 @@ unset `env.get` are nil. | `time.local(ms)` | `-> TimeParts` | `{ year, month, day, hour, minute, second, dow }`, dow 0 = Sunday | | `time.iso(ms)` | `-> Text` | UTC, second precision | | `time.ticks()` | `-> Int` | CLOCK_MONOTONIC microseconds (id 84, iteration 22's bench clock) — monotone, never wall time; only differences mean anything | +| `sha1(bytes)` | `-> Bytes` | 20-byte digest (id 85, iteration 34) — exists because RFC 6455's Sec-WebSocket-Accept demands SHA-1 | +| `sha256(bytes)` | `-> Bytes` | 32-byte digest (id 86, iteration 34) | +| `hmac_sha256(key, msg)` | `-> Bytes` | RFC 2104 over SHA-256, both args Bytes (id 87, iteration 34); key > 64 bytes hashed first | | `env.get(name)` | `-> ?Text` | unset is nil | | `env.stopping()` | `-> Bool` | SIGTERM/SIGINT latch, handlers installed on first use | | `net.listen(host, port)` | `-> Int` | IPv4, SO_REUSEADDR, backlog 64; returns an fd | diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index bb1c03b..0d3d76a 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -265,7 +265,7 @@ that sequences its tasks. Read one, approve, then the next starts. | -------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- | | Language | 🔄 [iteration 36 — operator parity](language-runtime-database/in-progress/36-operator-parity.md): `not`, bitwise `& \| ^ << >>`, hex/binary/`_` literals, compound assigns — CODE LANDED 2026-08-22 (branch operator-parity, `.wob` v6, all gates green; reference project `.dev/reference/go` drove the design). Awaiting the developer's MANUAL pass on `docs/examples/operators/` (no test fixtures by directive); unblocks story 34's pure-`.wo` HMAC question | [plan](../superpowers/plans/2026-08-22-operator-parity.md) | | Language | the framework v1-polish slice landed 2026-08-20 (branch framework-v1, awaiting merge); next per the order: brainstorm 20/21's forks | [order](#implementation-order-re-sequenced-2026-08-21--concurrency-chain) | -| Runtime | nothing active — 22 landed 2026-08-21; next per the chain: iteration 31's spec brainstorm | [order](#implementation-order-re-sequenced-2026-08-21--concurrency-chain) | +| Runtime | 🔄 **iteration 24 (absorbing 31 + 34): chat + actor lifecycle** — spec + plan approved 2026-08-23 (24 absorbs 31 by directive; 34 resolved C-builtins); executing on branch `chat-ws-lifecycle` | [marker](../in-progress/2026-08-23-chat-ws-lifecycle.md) · [plan](../superpowers/plans/2026-08-23-chat-ws-lifecycle.md) | The active slice's marker doc lives in [`in-progress/`](../in-progress/) — one file, deleted when the slice lands. Everything else pending is the diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index cef1fa9..8438f2a 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -10,6 +10,7 @@ #include #include "cont.h" +#include "crypto.h" #include "gc.h" /* type checks on receiver headers: wrong native class traps BOUNDS */ @@ -172,6 +173,8 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { return wo_builtin_json(vm, R, ins, msg); if ((C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) || C == WO_B_TIME_TICKS) return wo_builtin_sys(vm, R, ins, msg); + if (C >= WO_B_SHA1 && C <= WO_B_HMAC_SHA256) + return wo_builtin_crypto(vm, R, ins, msg); if (C >= WO_B_DB_INSERT && C <= WO_B_DB_PROBE) { /* arc stage 3: the database is an actor on shard 0. A worker shard * has no engine by design — its statement marshals, parks, resumes diff --git a/runtime/src/crypto.c b/runtime/src/crypto.c new file mode 100644 index 0000000..c262ae8 --- /dev/null +++ b/runtime/src/crypto.c @@ -0,0 +1,252 @@ +/* crypto.c — SHA-1, SHA-256, HMAC-SHA256 (iteration 34). Hand-rolled, + * libc-only, whole-value: init/update/final collapsed into one pass over + * one buffer, because every builtin here takes complete Bytes — there is + * no streaming surface. Vectors: RFC 3174, FIPS 180-4, RFC 4231 — pinned + * in test/test_crypto.c. SHA-1 exists for the WebSocket handshake + * (Sec-WebSocket-Accept is SHA-1 by RFC 6455, not a choice). */ +#include "crypto.h" + +#include + +#include "obj.h" +#include "wob.h" + +static uint32_t rotl32(uint32_t x, int n) { return (x << n) | (x >> (32 - n)); } +static uint32_t rotr32(uint32_t x, int n) { return (x >> n) | (x << (32 - n)); } + +/* Both digests consume the message in 64-byte blocks with the same + * padding scheme (0x80, zeros, 64-bit big-endian bit length). The tail + * is at most two blocks; building it on the stack keeps the cores + * allocation-free. */ + +static void sha1_block(uint32_t h[5], const uint8_t *p) { + uint32_t w[80]; + for (int i = 0; i < 16; i++) + w[i] = (uint32_t)p[i * 4] << 24 | (uint32_t)p[i * 4 + 1] << 16 | + (uint32_t)p[i * 4 + 2] << 8 | p[i * 4 + 3]; + for (int i = 16; i < 80; i++) + w[i] = rotl32(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4]; + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999u; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1u; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDCu; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6u; + } + uint32_t t = rotl32(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = rotl32(b, 30); + b = a; + a = t; + } + h[0] += a; + h[1] += b; + h[2] += c; + h[3] += d; + h[4] += e; +} + +void wo_sha1(const uint8_t *msg, size_t len, uint8_t out[20]) { + uint32_t h[5] = {0x67452301u, 0xEFCDAB89u, 0x98BADCFEu, 0x10325476u, + 0xC3D2E1F0u}; + size_t i = 0; + for (; i + 64 <= len; i += 64) sha1_block(h, msg + i); + uint8_t tail[128]; + size_t rem = len - i; + memcpy(tail, msg + i, rem); + tail[rem] = 0x80; + size_t tlen = rem + 1 <= 56 ? 64 : 128; + memset(tail + rem + 1, 0, tlen - rem - 1 - 8); + uint64_t bits = (uint64_t)len * 8; + for (int b = 0; b < 8; b++) tail[tlen - 1 - b] = (uint8_t)(bits >> (8 * b)); + sha1_block(h, tail); + if (tlen == 128) sha1_block(h, tail + 64); + for (int w = 0; w < 5; w++) { + out[w * 4] = (uint8_t)(h[w] >> 24); + out[w * 4 + 1] = (uint8_t)(h[w] >> 16); + out[w * 4 + 2] = (uint8_t)(h[w] >> 8); + out[w * 4 + 3] = (uint8_t)h[w]; + } +} + +static const uint32_t K256[64] = { + 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}; + +static void sha256_block(uint32_t h[8], const uint8_t *p) { + uint32_t w[64]; + for (int i = 0; i < 16; i++) + w[i] = (uint32_t)p[i * 4] << 24 | (uint32_t)p[i * 4 + 1] << 16 | + (uint32_t)p[i * 4 + 2] << 8 | p[i * 4 + 3]; + for (int i = 16; i < 64; i++) { + uint32_t s0 = rotr32(w[i - 15], 7) ^ rotr32(w[i - 15], 18) ^ (w[i - 15] >> 3); + uint32_t s1 = rotr32(w[i - 2], 17) ^ rotr32(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3]; + uint32_t e = h[4], f = h[5], g = h[6], hh = h[7]; + for (int i = 0; i < 64; i++) { + uint32_t S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25); + uint32_t ch = (e & f) ^ (~e & g); + uint32_t t1 = hh + S1 + ch + K256[i] + w[i]; + uint32_t S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22); + uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + uint32_t t2 = S0 + maj; + hh = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + h[0] += a; + h[1] += b; + h[2] += c; + h[3] += d; + h[4] += e; + h[5] += f; + h[6] += g; + h[7] += hh; +} + +void wo_sha256(const uint8_t *msg, size_t len, uint8_t out[32]) { + uint32_t h[8] = {0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au, + 0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u}; + size_t i = 0; + for (; i + 64 <= len; i += 64) sha256_block(h, msg + i); + uint8_t tail[128]; + size_t rem = len - i; + memcpy(tail, msg + i, rem); + tail[rem] = 0x80; + size_t tlen = rem + 1 <= 56 ? 64 : 128; + memset(tail + rem + 1, 0, tlen - rem - 1 - 8); + uint64_t bits = (uint64_t)len * 8; + for (int b = 0; b < 8; b++) tail[tlen - 1 - b] = (uint8_t)(bits >> (8 * b)); + sha256_block(h, tail); + if (tlen == 128) sha256_block(h, tail + 64); + for (int w = 0; w < 8; w++) { + out[w * 4] = (uint8_t)(h[w] >> 24); + out[w * 4 + 1] = (uint8_t)(h[w] >> 16); + out[w * 4 + 2] = (uint8_t)(h[w] >> 8); + out[w * 4 + 3] = (uint8_t)h[w]; + } +} + +/* RFC 2104 over SHA-256: a key longer than the 64-byte block is hashed + * first; shorter keys zero-pad. Two passes, no allocation. */ +void wo_hmac_sha256(const uint8_t *key, size_t klen, const uint8_t *msg, + size_t mlen, uint8_t out[32]) { + uint8_t k[64] = {0}; + if (klen > 64) { + wo_sha256(key, klen, k); /* leaves 32 bytes, rest stays zero */ + } else { + memcpy(k, key, klen); + } + uint8_t ipad[64], opad[64]; + for (int i = 0; i < 64; i++) { + ipad[i] = k[i] ^ 0x36; + opad[i] = k[i] ^ 0x5c; + } + /* inner = sha256(ipad || msg) — the message can be arbitrarily long, so + * the inner pass re-runs the block loop by hand instead of concatenating */ + uint32_t h[8] = {0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au, + 0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u}; + sha256_block(h, ipad); + size_t i = 0; + for (; i + 64 <= mlen; i += 64) sha256_block(h, msg + i); + uint8_t tail[128]; + size_t rem = mlen - i; + memcpy(tail, msg + i, rem); + tail[rem] = 0x80; + size_t tlen = rem + 1 <= 56 ? 64 : 128; + memset(tail + rem + 1, 0, tlen - rem - 1 - 8); + uint64_t bits = ((uint64_t)mlen + 64) * 8; /* +64: the ipad block */ + for (int b = 0; b < 8; b++) tail[tlen - 1 - b] = (uint8_t)(bits >> (8 * b)); + sha256_block(h, tail); + if (tlen == 128) sha256_block(h, tail + 64); + uint8_t inner[32]; + for (int w = 0; w < 8; w++) { + inner[w * 4] = (uint8_t)(h[w] >> 24); + inner[w * 4 + 1] = (uint8_t)(h[w] >> 16); + inner[w * 4 + 2] = (uint8_t)(h[w] >> 8); + inner[w * 4 + 3] = (uint8_t)h[w]; + } + uint8_t outer[96]; + memcpy(outer, opad, 64); + memcpy(outer + 64, inner, 32); + wo_sha256(outer, 96, out); +} + +/* The VM half: Bytes in, fresh Bytes out. Wrong class id traps + * WO_T_BOUNDS with the Bytes builtins' message shape. */ +static const wo_str *arg_bytes(uint64_t r, const char **msg) { + const wo_str *b = (const wo_str *)(uintptr_t)r; + if (!b || b->h.class_id != WO_CLS_BYTES) { + *msg = "not a bytes value"; + return NULL; + } + return b; +} + +int wo_builtin_crypto(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { + wo_rt *rt = &vm->rt; + uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins); + uint8_t digest[32]; + uint32_t dlen; + switch (C) { + case WO_B_SHA1: { + const wo_str *b = arg_bytes(R[B], msg); + if (!b) return WO_T_BOUNDS; + wo_sha1((const uint8_t *)b->data, b->len, digest); + dlen = 20; + break; + } + case WO_B_SHA256: { + const wo_str *b = arg_bytes(R[B], msg); + if (!b) return WO_T_BOUNDS; + wo_sha256((const uint8_t *)b->data, b->len, digest); + dlen = 32; + break; + } + case WO_B_HMAC_SHA256: { + const wo_str *k = arg_bytes(R[B], msg); + const wo_str *m = k ? arg_bytes(R[B + 1], msg) : NULL; + if (!m) return WO_T_BOUNDS; + wo_hmac_sha256((const uint8_t *)k->data, k->len, + (const uint8_t *)m->data, m->len, digest); + dlen = 32; + break; + } + default: + *msg = "unknown crypto builtin"; + return WO_T_BOUNDS; + } + wo_str *out = wo_bytes_new(rt, (const char *)digest, dlen); + if (!out) { + *msg = "out of memory"; + return WO_T_OOM; + } + R[A] = (uint64_t)(uintptr_t)out; + return 0; +} diff --git a/runtime/src/crypto.h b/runtime/src/crypto.h new file mode 100644 index 0000000..f3efe09 --- /dev/null +++ b/runtime/src/crypto.h @@ -0,0 +1,19 @@ +/* crypto.h — hand-rolled digests (iteration 34). Whole-value, libc-only. + * The raw cores are exposed for the unit test; the VM enters through + * wo_builtin_crypto (ids WO_B_SHA1..WO_B_HMAC_SHA256). */ +#ifndef WO_CRYPTO_H +#define WO_CRYPTO_H + +#include +#include + +#include "vm.h" + +void wo_sha1(const uint8_t *msg, size_t len, uint8_t out[20]); +void wo_sha256(const uint8_t *msg, size_t len, uint8_t out[32]); +void wo_hmac_sha256(const uint8_t *key, size_t klen, const uint8_t *msg, + size_t mlen, uint8_t out[32]); + +int wo_builtin_crypto(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg); + +#endif diff --git a/runtime/src/wob.h b/runtime/src/wob.h index 2a7d959..b8d5ed4 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -451,9 +451,15 @@ enum { * the bench clock (iteration 22). Monotone, * never wall time: immune to NTP steps; only * differences mean anything. */ + /* ---- iteration 34: digests (crypto.c). Whole-value over Bytes; SHA-1 + * exists because RFC 6455's Sec-WebSocket-Accept demands it. ---- */ + WO_B_SHA1 = 85, /* (bytes) -> fresh 20-byte Bytes */ + WO_B_SHA256 = 86, /* (bytes) -> fresh 32-byte Bytes */ + WO_B_HMAC_SHA256 = 87, /* (key bytes, msg bytes) -> fresh 32-byte Bytes, + * RFC 2104 (key > 64 bytes hashed first) */ }; -#define WO_B_MAX 84u +#define WO_B_MAX 87u /* ids at or above this one live in sysio.c, not builtin.c */ #define WO_B_SYS_FIRST WO_B_FS_EXISTS diff --git a/runtime/test/test_crypto.c b/runtime/test/test_crypto.c new file mode 100644 index 0000000..7e87a1e --- /dev/null +++ b/runtime/test/test_crypto.c @@ -0,0 +1,112 @@ +/* test_crypto — the digest cores against published vectors: SHA-1 (RFC + * 3174), SHA-256 (FIPS 180-4), HMAC-SHA256 (RFC 4231 cases 1-4), plus a + * 63/64/65-byte block-boundary sweep. Boundary constants precomputed with + * python3: hashlib.sha256(b"a"*63).hexdigest() etc. */ +#include +#include + +#include "crypto.h" +#include "t.h" + +static void hex(const uint8_t *d, size_t n, char *out) { + static const char *h = "0123456789abcdef"; + for (size_t i = 0; i < n; i++) { + out[i * 2] = h[d[i] >> 4]; + out[i * 2 + 1] = h[d[i] & 15]; + } + out[n * 2] = 0; +} + +static void t_sha1(const char *msg, size_t len, const char *want) { + uint8_t d[20]; + char got[41]; + wo_sha1((const uint8_t *)msg, len, d); + hex(d, 20, got); + T_CHECK(strcmp(got, want) == 0); +} + +static void t_sha256(const char *msg, size_t len, const char *want) { + uint8_t d[32]; + char got[65]; + wo_sha256((const uint8_t *)msg, len, d); + hex(d, 32, got); + T_CHECK(strcmp(got, want) == 0); +} + +static void t_hmac(const uint8_t *key, size_t klen, const char *msg, + size_t mlen, const char *want) { + uint8_t d[32]; + char got[65]; + wo_hmac_sha256(key, klen, (const uint8_t *)msg, mlen, d); + hex(d, 32, got); + T_CHECK(strcmp(got, want) == 0); +} + +int main(void) { + /* RFC 3174 */ + t_sha1("abc", 3, "a9993e364706816aba3e25717850c26c9cd0d89d"); + t_sha1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, + "84983e441c3bd26ebaae4aa1f95129e5e54670f1"); + t_sha1("", 0, "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + /* the WebSocket handshake's exact input (RFC 6455 §1.3 worked example): + * sha1("dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11") */ + t_sha1("dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 60, + "b37a4f2cc0624f1690f64606cf385945b2bec4ea"); + + /* FIPS 180-4 */ + t_sha256("abc", 3, + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + t_sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"); + t_sha256("", 0, + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + + /* block boundaries: python3 -c 'import hashlib + [print(hashlib.sha256(b"a"*n).hexdigest()) for n in (63,64,65)]' */ + char a65[65]; + memset(a65, 'a', 65); + t_sha256(a65, 63, + "7d3e74a05d7db15bce4ad9ec0658ea98e3f06eeecf16b4c6fff2da457ddc2f34"); + t_sha256(a65, 64, + "ffe054fe7ae0cb6dc65c3af9b61d5209f439851db43d0ba5997337df154668eb"); + t_sha256(a65, 65, + "635361c48bb9eab14198e76ea8ab7f1a41685d6ad62aa9146d301d4f17eb0ae0"); + /* same sweep for sha1: hashlib.sha1 */ + t_sha1(a65, 63, "03f09f5b158a7a8cdad920bddc29b81c18a551f5"); + t_sha1(a65, 64, "0098ba824b5c16427bd7a1122a5a442a25ec644d"); + t_sha1(a65, 65, "11655326c708d70319be2610e8a57d9a5b959d3b"); + + /* RFC 4231 */ + { + uint8_t k1[20]; + memset(k1, 0x0b, 20); + t_hmac(k1, 20, "Hi There", 8, + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); + } + t_hmac((const uint8_t *)"Jefe", 4, "what do ya want for nothing?", 28, + "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"); + { + uint8_t k3[20], m3[50]; + memset(k3, 0xaa, 20); + memset(m3, 0xdd, 50); + t_hmac(k3, 20, (const char *)m3, 50, + "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"); + } + { + uint8_t k4[25], m4[50]; + for (int i = 0; i < 25; i++) k4[i] = (uint8_t)(i + 1); + memset(m4, 0xcd, 50); + t_hmac(k4, 25, (const char *)m4, 50, + "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b"); + } + /* long-key path (key > 64 bytes is hashed first): RFC 4231 case 6 */ + { + uint8_t k6[131]; + memset(k6, 0xaa, 131); + t_hmac(k6, 131, "Test Using Larger Than Block-Size Key - Hash Key First", + 54, + "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"); + } + + return t_report("test_crypto"); +} diff --git a/tests/corpus/run/crypto-digests/fixture.out b/tests/corpus/run/crypto-digests/fixture.out new file mode 100644 index 0000000..1843339 --- /dev/null +++ b/tests/corpus/run/crypto-digests/fixture.out @@ -0,0 +1,3 @@ +sha1: qZk+NkcGgWq6PiVxeFDCbJzQ2J0= +sha256: ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0= +hmac: W9zBRr9gdU5qBCQmCJV1x1oAPwidJzmDnexYuWTsOEM= diff --git a/tests/corpus/run/crypto-digests/fixture.wo b/tests/corpus/run/crypto-digests/fixture.wo new file mode 100644 index 0000000..34b4840 --- /dev/null +++ b/tests/corpus/run/crypto-digests/fixture.wo @@ -0,0 +1,13 @@ +-- iteration 34: digests end to end from .wo — sha1/sha256/hmac_sha256 +-- over Bytes, printed through base64_encode (iteration 19). Expected +-- values are the RFC vectors: SHA-1("abc"), SHA-256("abc"), and +-- HMAC-SHA256 key "Jefe" / RFC 4231 case 2's message. +fn main() -> Int { + let abc = bytes_of_text("abc"); + print("sha1: ${base64_encode(sha1(abc))}"); + print("sha256: ${base64_encode(sha256(abc))}"); + let key = bytes_of_text("Jefe"); + let msg = bytes_of_text("what do ya want for nothing?"); + print("hmac: ${base64_encode(hmac_sha256(key, msg))}"); + return 0; +}