diff --git a/hosts/ocaml/bin/run_tests.ml b/hosts/ocaml/bin/run_tests.ml index 04315ca8..e469db57 100644 --- a/hosts/ocaml/bin/run_tests.ml +++ b/hosts/ocaml/bin/run_tests.ml @@ -1292,6 +1292,227 @@ let run_foundation_tests () = ignore (Sx_types.set_lambda_name (Lambda l) "my-fn"); assert_eq "lambda name mutated" (String "my-fn") (lambda_name (Lambda l)); + Printf.printf "\nSuite: crypto-sha2\n"; + (* NIST FIPS 180-4 published vectors. *) + assert_eq "sha256 empty" + (String "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + (call "crypto-sha256" [String ""]); + assert_eq "sha256 abc" + (String "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + (call "crypto-sha256" [String "abc"]); + assert_eq "sha256 896-bit" + (String "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1") + (call "crypto-sha256" + [String "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"]); + assert_eq "sha256 1M 'a'" + (String "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0") + (call "crypto-sha256" [String (String.make 1000000 'a')]); + assert_eq "sha512 empty" + (String "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + (call "crypto-sha512" [String ""]); + assert_eq "sha512 abc" + (String "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f") + (call "crypto-sha512" [String "abc"]); + assert_eq "sha512 896-bit" + (String "8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909") + (call "crypto-sha512" + [String ("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn" + ^ "hijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu")]); + + Printf.printf "\nSuite: crypto-sha3\n"; + (* NIST FIPS 202 published vectors. *) + assert_eq "sha3-256 empty" + (String "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a") + (call "crypto-sha3-256" [String ""]); + assert_eq "sha3-256 abc" + (String "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532") + (call "crypto-sha3-256" [String "abc"]); + assert_eq "sha3-256 896-bit" + (String "41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376") + (call "crypto-sha3-256" + [String "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"]); + (* 1600-bit message: 0xa3 * 200 — exercises multi-block absorb (>136B). *) + assert_eq "sha3-256 1600-bit 0xa3" + (String "79f38adec5c20307a98ef76e8324afbfd46cfd81b22e3973c65fa1bd9de31787") + (call "crypto-sha3-256" [String (String.make 200 '\xa3')]); + + Printf.printf "\nSuite: dag-cbor\n"; + let mkdict pairs = + let d = Sx_types.make_dict () in + List.iter (fun (k, v) -> Hashtbl.replace d k v) pairs; + Dict d + in + let enc v = call "cbor-encode" [v] in + (* RFC 8949 Appendix A — minimal-length deterministic encoding. *) + assert_eq "cbor 0" (String "\x00") (enc (Integer 0)); + assert_eq "cbor 23" (String "\x17") (enc (Integer 23)); + assert_eq "cbor 24" (String "\x18\x18") (enc (Integer 24)); + assert_eq "cbor 100" (String "\x18\x64") (enc (Integer 100)); + assert_eq "cbor 1000" (String "\x19\x03\xe8") (enc (Integer 1000)); + assert_eq "cbor 1000000" + (String "\x1a\x00\x0f\x42\x40") (enc (Integer 1000000)); + assert_eq "cbor -1" (String "\x20") (enc (Integer (-1))); + assert_eq "cbor -100" (String "\x38\x63") (enc (Integer (-100))); + assert_eq "cbor -1000" (String "\x39\x03\xe7") (enc (Integer (-1000))); + assert_eq "cbor false" (String "\xf4") (enc (Bool false)); + assert_eq "cbor true" (String "\xf5") (enc (Bool true)); + assert_eq "cbor null" (String "\xf6") (enc Nil); + assert_eq "cbor \"\"" (String "\x60") (enc (String "")); + assert_eq "cbor \"a\"" (String "\x61\x61") (enc (String "a")); + assert_eq "cbor \"IETF\"" (String "\x64IETF") (enc (String "IETF")); + assert_eq "cbor []" (String "\x80") (enc (List [])); + assert_eq "cbor [1,2,3]" + (String "\x83\x01\x02\x03") + (enc (List [Integer 1; Integer 2; Integer 3])); + assert_eq "cbor [1,[2,3],[4,5]]" + (String "\x83\x01\x82\x02\x03\x82\x04\x05") + (enc (List [Integer 1; + List [Integer 2; Integer 3]; + List [Integer 4; Integer 5]])); + assert_eq "cbor {}" (String "\xa0") (enc (mkdict [])); + assert_eq "cbor {a:1,b:[2,3]}" + (String "\xa2\x61\x61\x01\x61\x62\x82\x02\x03") + (enc (mkdict ["a", Integer 1; "b", List [Integer 2; Integer 3]])); + assert_eq "cbor {a..e:A..E}" + (String "\xa5\x61\x61\x61\x41\x61\x62\x61\x42\x61\x63\x61\x43\x61\x64\x61\x44\x61\x65\x61\x45") + (enc (mkdict ["a", String "A"; "b", String "B"; "c", String "C"; + "d", String "D"; "e", String "E"])); + (* Determinism: insertion order + key length must not change bytes. + Sort is length-then-bytewise → a, c, bb. *) + let d1 = mkdict ["bb", Integer 2; "a", Integer 1; "c", Integer 3] in + let d2 = mkdict ["c", Integer 3; "bb", Integer 2; "a", Integer 1] in + assert_eq "cbor det order-invariant" (enc d1) (enc d2); + assert_eq "cbor det length-then-bytewise" + (String "\xa3\x61\x61\x01\x61\x63\x03\x62\x62\x62\x02") + (enc d1); + (* Round-trip: decode . encode = identity (structural). *) + let roundtrip name v = + assert_eq ("cbor rt " ^ name) v (call "cbor-decode" [enc v]) + in + roundtrip "int" (Integer 42); + roundtrip "neg" (Integer (-99999)); + roundtrip "str" (String "hello world"); + roundtrip "bool" (Bool true); + roundtrip "nil" Nil; + roundtrip "nested" + (List [Integer 1; String "x"; List [Bool false; Nil]]); + roundtrip "dict" + (mkdict ["k", List [Integer 7]; "name", String "z"]); + + Printf.printf "\nSuite: cid\n"; + let mh_sha256 s = Sx_cid.multihash 0x12 (Sx_cid.unhex (Sx_sha2.sha256_hex s)) in + (* Authoritative vectors (independently derived; match well-known + IPFS CIDs). raw "abc" and raw "" — codec 0x55. *) + assert_eq "cid raw abc" + (String "bafkreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu") + (call "cid-from-bytes" [Integer 0x55; String (mh_sha256 "abc")]); + assert_eq "cid raw empty" + (String "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku") + (call "cid-from-bytes" [Integer 0x55; String (mh_sha256 "")]); + (* dag-cbor {} — canonical empty-map CID (sha2-256, codec 0x71). *) + assert_eq "cid dag-cbor {}" + (String "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua") + (call "cid-from-sx" [mkdict []]); + (* Determinism: dict key insertion order must not change the CID. *) + let cda = call "cid-from-sx" [mkdict ["b", Integer 2; "a", Integer 1]] in + let cdb = call "cid-from-sx" [mkdict ["a", Integer 1; "b", Integer 2]] in + assert_eq "cid det order-invariant" cda cdb; + assert_true "cid multibase 'b' prefix" + (Bool (match call "cid-from-sx" [mkdict []] with + | String s -> String.length s > 1 && s.[0] = 'b' + | _ -> false)); + + Printf.printf "\nSuite: ed25519\n"; + let hx = Sx_ed25519.unhex in + let edv pk msg sg = call "ed25519-verify" + [String (hx pk); String (hx msg); String (hx sg)] in + (* RFC 8032 §7.1 TEST 1-3 (deterministic; re-derived independently). *) + assert_eq "ed25519 RFC T1" + (Bool true) + (edv "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + "" + "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"); + assert_eq "ed25519 RFC T2" + (Bool true) + (edv "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + "72" + "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"); + assert_eq "ed25519 RFC T3" + (Bool true) + (edv "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025" + "af82" + "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"); + (* Tampered message -> false. *) + assert_eq "ed25519 tampered msg" + (Bool false) + (edv "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025" + "af83" + "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"); + (* Tampered signature -> false. *) + assert_eq "ed25519 tampered sig" + (Bool false) + (edv "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + "" + "f5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"); + (* Total: wrong-length pubkey / sig -> false, no exception. *) + assert_eq "ed25519 short pubkey" + (Bool false) + (call "ed25519-verify" [String "abc"; String ""; String (String.make 64 '\000')]); + assert_eq "ed25519 short sig" + (Bool false) + (call "ed25519-verify" + [String (hx "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"); + String ""; String "short"]); + assert_eq "ed25519 non-string args" + (Bool false) + (call "ed25519-verify" [Integer 1; Integer 2; Integer 3]); + + Printf.printf "\nSuite: rsa-sha256\n"; + (* Fixed RSA-2048 vector: one-off python-cryptography keygen + + PKCS1v15/SHA-256 sign of "fed-sx phase F rsa test". *) + let rhx = Sx_rsa.unhex in + let spki = rhx "30820122300d06092a864886f70d01010105000382010f003082010a0282010100a117b573480bce5a08b54a98384001df26d062e9173caaee2e3a2d0045c6d16f99b2a1e7fb60763f65f95f8c39ff82c18b8590338042914331db3440a06d2dbe65a2f82c82f37d293f67a8b57a1f9014b55150a093cfee90257ef3b4a215d5ab002579bd92b6fcb3536777d51b639347d01e307ddafb209073dd9b8d6a507157c44c624a19b3b9275931472462870ae02132630159132a85c1c889adfb358b6bbd3760ce3fffe6285964833a10ee436d5bc33dfab7f9ed630a74e9a32e5688f5a7797f7cc839ad2494dd1c4c4a8fab844cd26208794bf2602c16b9d12bde434066d8c0dd2d20489f4070f883bae2b4508ead4a1b80b44c576e9e37bdb5df69f10203010001" in + let rmsg = rhx "6665642d73782070686173652046207273612074657374" in + let rsig = rhx "5e1593d674ed15c0172546d38efdf1aebd252f4b0c0dfbe1f7996fd569d0bfd9f3e8689ea2b14aa45b5fc3f0a05d4f23c6b02b8820d71f6998ea3b5b0d071bb33142236e388b1226ece3ec447d33b38999f189c37564cf052cf038de94c67b2ddf9a97d5a73554bb88818f615824517209a4083258965adace55658f344104eaa0d5f2f44ea00cfac8754674aade87b40d955cccd1ccd9b7649a08b66ce3bc5dba2de96b3e859488ded3ef9fb3744a1e3495fd14841d8319b3cc08054c729d1c02739ee314eba2b20fac46e463f47eb67183d8455583eca73ba37448164612dd9cd77877135d30d12084c2843f986a5b8ad59c6600f9855b91d7cbdf7c6c4b0e" in + let rsav s m g = call "rsa-sha256-verify" [String s; String m; String g] in + assert_eq "rsa valid" (Bool true) (rsav spki rmsg rsig); + assert_eq "rsa tampered msg" (Bool false) + (rsav spki (rmsg ^ "x") rsig); + assert_eq "rsa tampered sig" (Bool false) + (rsav spki rmsg + (rhx "5f1593d674ed15c0172546d38efdf1aebd252f4b0c0dfbe1f7996fd569d0bfd9f3e8689ea2b14aa45b5fc3f0a05d4f23c6b02b8820d71f6998ea3b5b0d071bb33142236e388b1226ece3ec447d33b38999f189c37564cf052cf038de94c67b2ddf9a97d5a73554bb88818f615824517209a4083258965adace55658f344104eaa0d5f2f44ea00cfac8754674aade87b40d955cccd1ccd9b7649a08b66ce3bc5dba2de96b3e859488ded3ef9fb3744a1e3495fd14841d8319b3cc08054c729d1c02739ee314eba2b20fac46e463f47eb67183d8455583eca73ba37448164612dd9cd77877135d30d12084c2843f986a5b8ad59c6600f9855b91d7cbdf7c6c4b0e")); + assert_eq "rsa garbage spki" (Bool false) + (rsav "not der" rmsg rsig); + assert_eq "rsa non-string args" (Bool false) + (call "rsa-sha256-verify" [Integer 1; Integer 2; Integer 3]); + + Printf.printf "\nSuite: file-list-dir\n"; + let expect_err nm f = + (try ignore (f ()); + incr fail_count; Printf.printf " FAIL: %s — no error\n" nm + with Eval_error _ -> + incr pass_count; Printf.printf " PASS: %s\n" nm + | _ -> + incr fail_count; Printf.printf " FAIL: %s — wrong exn\n" nm) + in + let tmp = Filename.temp_file "fld" "" in + Sys.remove tmp; Unix.mkdir tmp 0o755; + let touch n = let oc = open_out (Filename.concat tmp n) in close_out oc in + touch "b.txt"; touch "a.txt"; touch "c.txt"; + assert_eq "file-list-dir sorted" + (List [String "a.txt"; String "b.txt"; String "c.txt"]) + (call "file-list-dir" [String tmp]); + expect_err "file-list-dir missing" + (fun () -> call "file-list-dir" [String (Filename.concat tmp "nope")]); + expect_err "file-list-dir not-a-dir" + (fun () -> call "file-list-dir" [String (Filename.concat tmp "a.txt")]); + expect_err "file-list-dir arity" + (fun () -> call "file-list-dir" []); + (* best-effort cleanup *) + (try List.iter (fun n -> Sys.remove (Filename.concat tmp n)) + ["a.txt"; "b.txt"; "c.txt"]; Unix.rmdir tmp + with _ -> ()); + Printf.printf "\nSuite: vm-extension-dispatch\n"; let make_bc op = ({ vc_arity = 0; vc_rest_arity = -1; vc_locals = 0; diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 9d76c91f..bbeb10c2 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -722,6 +722,139 @@ let setup_evaluator_bridge env = match args with | [e; expr] -> Sx_ref.eval_expr expr e | _ -> raise (Eval_error "eval-in-env: (env expr)")); + + (* fed-sx Milestone 1 Step 8 transport. NATIVE ONLY — sockets + + threads; deliberately absent from the WASM kernel (registered + here in bin/, never in lib/sx_primitives.ml). Minimal HTTP/1.1, + Connection: close. handler : req-dict -> resp-dict where + req = {:method :path :query :headers :body}, + resp = {:status :headers :body}. Never returns. *) + Sx_primitives.register "http-listen" (fun args -> + let strip_cr s = + let n = String.length s in + if n > 0 && s.[n - 1] = '\r' then String.sub s 0 (n - 1) else s + in + match args with + | [port_v; handler] -> + let port = match port_v with + | Integer n -> n + | Number f -> int_of_float f + | _ -> raise (Eval_error "http-listen: (port handler)") in + let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in + Unix.setsockopt sock Unix.SO_REUSEADDR true; + Unix.bind sock + (Unix.ADDR_INET (Unix.inet_addr_loopback, port)); + Unix.listen sock 64; + (* SX runtime is shared across threads — serialize handler calls. *) + let mtx = Mutex.create () in + let reason = function + | 200 -> "OK" | 201 -> "Created" | 204 -> "No Content" + | 301 -> "Moved Permanently" | 302 -> "Found" + | 400 -> "Bad Request" | 401 -> "Unauthorized" + | 403 -> "Forbidden" | 404 -> "Not Found" + | 405 -> "Method Not Allowed" | 500 -> "Internal Server Error" + | _ -> "OK" in + let handle fd = + (try + let ic = Unix.in_channel_of_descr fd in + let oc = Unix.out_channel_of_descr fd in + let reqline = strip_cr (input_line ic) in + (match String.split_on_char ' ' reqline with + | meth :: target :: _ -> + let path, query = + match String.index_opt target '?' with + | Some i -> + String.sub target 0 i, + String.sub target (i + 1) + (String.length target - i - 1) + | None -> target, "" in + let headers = Sx_types.make_dict () in + let clen = ref 0 in + let rec rdh () = + let h = strip_cr (input_line ic) in + if h = "" then () + else begin + (match String.index_opt h ':' with + | Some i -> + let name = + String.lowercase_ascii + (String.trim (String.sub h 0 i)) in + let value = + String.trim + (String.sub h (i + 1) + (String.length h - i - 1)) in + Hashtbl.replace headers name (String value); + if name = "content-length" then + (try clen := int_of_string value with _ -> ()) + | None -> ()); + rdh () + end in + rdh (); + let body = + if !clen > 0 then begin + let b = Bytes.create !clen in + really_input ic b 0 !clen; + Bytes.unsafe_to_string b + end else "" in + let req = Sx_types.make_dict () in + Hashtbl.replace req "method" (String meth); + Hashtbl.replace req "path" (String path); + Hashtbl.replace req "query" (String query); + Hashtbl.replace req "headers" (Dict headers); + Hashtbl.replace req "body" (String body); + Mutex.lock mtx; + let resp = + (try Sx_runtime.sx_call handler [Dict req] + with e -> Mutex.unlock mtx; raise e) in + Mutex.unlock mtx; + let getk k = match resp with + | Dict h -> Hashtbl.find_opt h k | _ -> None in + let status = match getk "status" with + | Some (Integer n) -> n + | Some (Number f) -> int_of_float f + | _ -> 200 in + let rbody = match getk "body" with + | Some (String s) -> s + | Some v -> Sx_types.value_to_string v + | None -> "" in + let rhdrs = match getk "headers" with + | Some (Dict h) -> + Hashtbl.fold (fun k v acc -> + (k, (match v with + | String s -> s + | v -> Sx_types.value_to_string v)) :: acc) + h [] + | _ -> [] in + let buf = Buffer.create 256 in + Buffer.add_string buf + (Printf.sprintf "HTTP/1.1 %d %s\r\n" status + (reason status)); + List.iter (fun (k, v) -> + Buffer.add_string buf + (Printf.sprintf "%s: %s\r\n" k v)) rhdrs; + if not (List.exists + (fun (k, _) -> + String.lowercase_ascii k = "content-type") + rhdrs) + then Buffer.add_string buf + "Content-Type: text/plain\r\n"; + Buffer.add_string buf + (Printf.sprintf "Content-Length: %d\r\n" + (String.length rbody)); + Buffer.add_string buf "Connection: close\r\n\r\n"; + Buffer.add_string buf rbody; + output_string oc (Buffer.contents buf); + flush oc + | _ -> ()) + with _ -> ()); + (try Unix.close fd with _ -> ()) + in + while true do + let fd, _ = Unix.accept sock in + ignore (Thread.create handle fd) + done; + Nil + | _ -> raise (Eval_error "http-listen: (port handler)")); bind "trampoline" (fun args -> match args with | [v] -> diff --git a/hosts/ocaml/bin/test_http.sh b/hosts/ocaml/bin/test_http.sh new file mode 100755 index 00000000..46ca6cab --- /dev/null +++ b/hosts/ocaml/bin/test_http.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Phase H test — native-only http-listen primitive. +# Starts sx_server with a tiny SX echo handler, drives it with curl +# (GET / POST / 404 / custom header), asserts, then kills it. +set -u +cd "$(dirname "$0")/.." + +SRV=_build/default/bin/sx_server.exe +PORT=${HTTP_TEST_PORT:-8911} +PASS=0 +FAIL=0 +ok() { echo " PASS: $1"; PASS=$((PASS+1)); } +bad() { echo " FAIL: $1 — $2"; FAIL=$((FAIL+1)); } + +if [ ! -x "$SRV" ]; then + echo "build sx_server.exe first (dune build bin/sx_server.exe)"; exit 1 +fi + +H='(begin (define (h req) (if (= (get req "path") "/echo") {:status 200 :headers {"X-Echo" (get req "method")} :body (str "M=" (get req "method") " P=" (get req "path") " Q=" (get req "query") " B=" (get req "body"))} {:status 404 :body "nope"})) (http-listen '"$PORT"' h))' +ESC=${H//\"/\\\"} + +{ printf '(epoch 1)\n(eval "%s")\n' "$ESC"; sleep 30; } | "$SRV" >/tmp/test_http_srv.out 2>&1 & +SVPID=$! +trap 'kill $SVPID 2>/dev/null; wait 2>/dev/null' EXIT + +up=0 +for _ in $(seq 1 50); do + curl -s -o /dev/null "http://127.0.0.1:$PORT/echo" 2>/dev/null && { up=1; break; } + sleep 0.2 +done +[ "$up" = 1 ] || { echo " FAIL: server did not start"; cat /tmp/test_http_srv.out; exit 1; } + +# GET with query + custom response header. +g=$(curl -s -i "http://127.0.0.1:$PORT/echo?x=1" | tr -d '\r') +echo "$g" | grep -q '^HTTP/1.1 200 OK' && ok "GET status 200" || bad "GET status" "$g" +echo "$g" | grep -q '^X-Echo: GET' && ok "GET custom header" || bad "GET header" "$g" +echo "$g" | grep -q '^M=GET P=/echo Q=x=1 B=$' && ok "GET echo body" || bad "GET body" "$g" + +# POST with body. +p=$(curl -s -X POST --data 'hello' "http://127.0.0.1:$PORT/echo") +[ "$p" = 'M=POST P=/echo Q= B=hello' ] && ok "POST body echoed" || bad "POST body" "$p" + +# 404 path. +n=$(curl -s -i "http://127.0.0.1:$PORT/missing" | tr -d '\r') +echo "$n" | grep -q '^HTTP/1.1 404 Not Found' && ok "404 status" || bad "404 status" "$n" +echo "$n" | grep -q '^nope$' && ok "404 body" || bad "404 body" "$n" + +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" = 0 ] diff --git a/hosts/ocaml/lib/sx_cbor.ml b/hosts/ocaml/lib/sx_cbor.ml new file mode 100644 index 00000000..b4ec7ba1 --- /dev/null +++ b/hosts/ocaml/lib/sx_cbor.ml @@ -0,0 +1,142 @@ +(** dag-cbor encode / decode — pure OCaml, WASM-safe. + + RFC 8949 deterministic subset as constrained by IPLD dag-cbor + (RFC 8742): unsigned/negative ints, text strings, arrays, maps + with keys sorted by **length-then-bytewise**, bool, null, and + tag 42 (CID link, decode-side passthrough). Floats are not + supported (no fed-sx shape needs them yet) — encoding a [Number] + or decoding a float head raises. Reference: RFC 8949 §3, §4.2. *) + +open Sx_types + +exception Cbor_error of string + +(* ---- Encoder ---- *) + +let write_head buf major v = + let m = major lsl 5 in + if v < 24 then + Buffer.add_char buf (Char.chr (m lor v)) + else if v < 0x100 then begin + Buffer.add_char buf (Char.chr (m lor 24)); + Buffer.add_char buf (Char.chr v) + end else if v < 0x10000 then begin + Buffer.add_char buf (Char.chr (m lor 25)); + Buffer.add_char buf (Char.chr ((v lsr 8) land 0xFF)); + Buffer.add_char buf (Char.chr (v land 0xFF)) + end else if v < 0x100000000 then begin + Buffer.add_char buf (Char.chr (m lor 26)); + for i = 3 downto 0 do + Buffer.add_char buf (Char.chr ((v lsr (8 * i)) land 0xFF)) + done + end else begin + Buffer.add_char buf (Char.chr (m lor 27)); + for i = 7 downto 0 do + Buffer.add_char buf (Char.chr ((v lsr (8 * i)) land 0xFF)) + done + end + +(* dag-cbor map key order: shorter key first, then bytewise. *) +let key_order a b = + let la = String.length a and lb = String.length b in + if la <> lb then compare la lb else compare a b + +let rec encode_into buf (v : value) : unit = + match v with + | Integer n -> + if n >= 0 then write_head buf 0 n + else write_head buf 1 (-1 - n) + | String s -> + write_head buf 3 (String.length s); + Buffer.add_string buf s + | Symbol s | Keyword s -> + write_head buf 3 (String.length s); + Buffer.add_string buf s + | Bool false -> Buffer.add_char buf '\xf4' + | Bool true -> Buffer.add_char buf '\xf5' + | Nil -> Buffer.add_char buf '\xf6' + | List items -> + write_head buf 4 (List.length items); + List.iter (encode_into buf) items + | Dict d -> + let keys = Hashtbl.fold (fun k _ acc -> k :: acc) d [] in + let keys = List.sort_uniq key_order keys in + write_head buf 5 (List.length keys); + List.iter (fun k -> + write_head buf 3 (String.length k); + Buffer.add_string buf k; + encode_into buf (Hashtbl.find d k)) keys + | Number _ -> + raise (Cbor_error "cbor-encode: floats unsupported (dag-cbor subset)") + | _ -> + raise (Cbor_error + ("cbor-encode: unencodable value " ^ type_of v)) + +let encode (v : value) : string = + let buf = Buffer.create 64 in + encode_into buf v; + Buffer.contents buf + +(* ---- Decoder ---- *) + +let decode (s : string) : value = + let pos = ref 0 in + let len = String.length s in + let byte () = + if !pos >= len then raise (Cbor_error "cbor-decode: truncated"); + let c = Char.code s.[!pos] in incr pos; c + in + let read_uint ai = + if ai < 24 then ai + else if ai = 24 then byte () + else if ai = 25 then let a = byte () in let b = byte () in (a lsl 8) lor b + else if ai = 26 then begin + let v = ref 0 in + for _ = 0 to 3 do v := (!v lsl 8) lor byte () done; !v + end else if ai = 27 then begin + let v = ref 0 in + for _ = 0 to 7 do v := (!v lsl 8) lor byte () done; !v + end else raise (Cbor_error "cbor-decode: bad additional info") + in + let read_bytes n = + if !pos + n > len then raise (Cbor_error "cbor-decode: truncated"); + let r = String.sub s !pos n in pos := !pos + n; r + in + let rec item () = + let b = byte () in + let major = b lsr 5 and ai = b land 0x1f in + match major with + | 0 -> Integer (read_uint ai) + | 1 -> Integer (-1 - read_uint ai) + | 2 -> String (read_bytes (read_uint ai)) + | 3 -> String (read_bytes (read_uint ai)) + | 4 -> + let n = read_uint ai in + List (List.init n (fun _ -> item ())) + | 5 -> + let n = read_uint ai in + let d = make_dict () in + for _ = 1 to n do + let k = match item () with + | String k -> k + | _ -> raise (Cbor_error "cbor-decode: non-string map key") + in + Hashtbl.replace d k (item ()) + done; + Dict d + | 6 -> + (* Tag: tag-42 CID link → pass the inner item through. *) + ignore (read_uint ai); item () + | 7 -> + (match ai with + | 20 -> Bool false + | 21 -> Bool true + | 22 -> Nil + | 23 -> Nil + | _ -> + raise (Cbor_error + "cbor-decode: floats/simple unsupported (dag-cbor subset)")) + | _ -> raise (Cbor_error "cbor-decode: bad major type") + in + let v = item () in + v diff --git a/hosts/ocaml/lib/sx_cid.ml b/hosts/ocaml/lib/sx_cid.ml new file mode 100644 index 00000000..380fef01 --- /dev/null +++ b/hosts/ocaml/lib/sx_cid.ml @@ -0,0 +1,66 @@ +(** CIDv1 computation — pure OCaml, WASM-safe. + + Multihash + CIDv1 + multibase base32-lower (RFC 4648, no pad, + multibase prefix 'b'). Codecs: dag-cbor 0x71, raw 0x55. Hash + codes: sha2-256 0x12, sha3-256 0x16. Reference: the multiformats + specs (unsigned-varint, multihash, cid, multibase). No deps. *) + +open Sx_types + +(* Unsigned LEB128 (multiformats unsigned-varint). *) +let varint (n : int) : string = + let buf = Buffer.create 4 in + let n = ref n in + let cont = ref true in + while !cont do + let b = !n land 0x7f in + n := !n lsr 7; + if !n = 0 then (Buffer.add_char buf (Char.chr b); cont := false) + else Buffer.add_char buf (Char.chr (b lor 0x80)) + done; + Buffer.contents buf + +(* RFC 4648 base32 lowercase, no padding. *) +let b32_alpha = "abcdefghijklmnopqrstuvwxyz234567" + +let base32_lower (s : string) : string = + let buf = Buffer.create ((String.length s * 8 + 4) / 5) in + let acc = ref 0 and bits = ref 0 in + String.iter (fun c -> + acc := (!acc lsl 8) lor (Char.code c); + bits := !bits + 8; + while !bits >= 5 do + bits := !bits - 5; + Buffer.add_char buf b32_alpha.[(!acc lsr !bits) land 0x1f] + done) s; + if !bits > 0 then + Buffer.add_char buf b32_alpha.[(!acc lsl (5 - !bits)) land 0x1f]; + Buffer.contents buf + +(* "abef" -> the 2 raw bytes. *) +let unhex (h : string) : string = + let n = String.length h / 2 in + let b = Bytes.create n in + for i = 0 to n - 1 do + Bytes.set b i + (Char.chr (int_of_string ("0x" ^ String.sub h (2 * i) 2))) + done; + Bytes.unsafe_to_string b + +(* multihash = varint(code) || varint(len) || digest *) +let multihash (code : int) (digest : string) : string = + varint code ^ varint (String.length digest) ^ digest + +(* CIDv1 = 0x01 || varint(codec) || multihash ; multibase 'b' base32. *) +let cidv1 (codec : int) (mh : string) : string = + "b" ^ base32_lower ("\x01" ^ varint codec ^ mh) + +let codec_dag_cbor = 0x71 +let mh_sha2_256 = 0x12 + +(* Canonicalize an SX value: dag-cbor encode -> sha2-256 -> + multihash -> CIDv1 (dag-cbor codec). *) +let cid_from_sx (v : value) : string = + let cbor = Sx_cbor.encode v in + let digest = unhex (Sx_sha2.sha256_hex cbor) in + cidv1 codec_dag_cbor (multihash mh_sha2_256 digest) diff --git a/hosts/ocaml/lib/sx_ed25519.ml b/hosts/ocaml/lib/sx_ed25519.ml new file mode 100644 index 00000000..0b7a42bc --- /dev/null +++ b/hosts/ocaml/lib/sx_ed25519.ml @@ -0,0 +1,289 @@ +(** Ed25519 signature verification — pure OCaml, WASM-safe. + + RFC 8032 §5.1.7 cofactorless verify over edwards25519. Includes a + minimal arbitrary-precision unsigned bignum (no Zarith / no deps) + and twisted-Edwards extended-coordinate point arithmetic. Verify + is total: malformed inputs return [false], never raise. SHA-512 + is reused from {!Sx_sha2}. Reference: RFC 8032, RFC 7748. *) + +(* ---- Minimal bignum: int array, little-endian, base 2^26. ---- *) + +let bits = 26 +let base = 1 lsl bits +let mask = base - 1 + +type bn = int array (* normalized: no high zero limbs, length >= 1 *) + +let norm (a : bn) : bn = + let n = ref (Array.length a) in + while !n > 1 && a.(!n - 1) = 0 do decr n done; + if !n = Array.length a then a else Array.sub a 0 !n + +let bzero : bn = [| 0 |] +let of_int n : bn = + if n = 0 then bzero + else begin + let r = ref [] and n = ref n in + while !n > 0 do r := (!n land mask) :: !r; n := !n lsr bits done; + norm (Array.of_list (List.rev !r)) + end + +let is_zero (a : bn) = Array.length a = 1 && a.(0) = 0 + +let cmp (a : bn) (b : bn) : int = + let a = norm a and b = norm b in + let la = Array.length a and lb = Array.length b in + if la <> lb then compare la lb + else begin + let r = ref 0 and i = ref (la - 1) in + while !r = 0 && !i >= 0 do + if a.(!i) <> b.(!i) then r := compare a.(!i) b.(!i); + decr i + done; !r + end + +let add (a : bn) (b : bn) : bn = + let la = Array.length a and lb = Array.length b in + let n = (max la lb) + 1 in + let r = Array.make n 0 in + let carry = ref 0 in + for i = 0 to n - 1 do + let s = !carry + + (if i < la then a.(i) else 0) + + (if i < lb then b.(i) else 0) in + r.(i) <- s land mask; carry := s lsr bits + done; + norm r + +(* a - b, requires a >= b *) +let sub (a : bn) (b : bn) : bn = + let la = Array.length a and lb = Array.length b in + let r = Array.make la 0 in + let borrow = ref 0 in + for i = 0 to la - 1 do + let s = a.(i) - !borrow - (if i < lb then b.(i) else 0) in + if s < 0 then (r.(i) <- s + base; borrow := 1) + else (r.(i) <- s; borrow := 0) + done; + norm r + +let mul (a : bn) (b : bn) : bn = + let la = Array.length a and lb = Array.length b in + let r = Array.make (la + lb) 0 in + for i = 0 to la - 1 do + let carry = ref 0 in + for j = 0 to lb - 1 do + let s = r.(i + j) + a.(i) * b.(j) + !carry in + r.(i + j) <- s land mask; carry := s lsr bits + done; + r.(i + lb) <- r.(i + lb) + !carry + done; + norm r + +let numbits (a : bn) : int = + let a = norm a in + let hi = Array.length a - 1 in + if hi = 0 && a.(0) = 0 then 0 + else begin + let b = ref 0 and v = ref a.(hi) in + while !v > 0 do incr b; v := !v lsr 1 done; + hi * bits + !b + end + +let bit (a : bn) (i : int) : int = + let limb = i / bits and off = i mod bits in + if limb >= Array.length a then 0 else (a.(limb) lsr off) land 1 + +(* r = a mod m (m > 0), binary long division. *) +let bn_mod (a : bn) (m : bn) : bn = + if cmp a m < 0 then norm a + else begin + let r = ref bzero in + for i = numbits a - 1 downto 0 do + (* r = r*2 + bit *) + r := add !r !r; + if bit a i = 1 then r := add !r [| 1 |]; + if cmp !r m >= 0 then r := sub !r m + done; + !r + end + +let div_small (a : bn) (d : int) : bn = + let la = Array.length a in + let q = Array.make la 0 in + let rem = ref 0 in + for i = la - 1 downto 0 do + let cur = (!rem lsl bits) lor a.(i) in + q.(i) <- cur / d; rem := cur mod d + done; + norm q + +let powmod (b0 : bn) (e : bn) (m : bn) : bn = + let result = ref [| 1 |] and b = ref (bn_mod b0 m) in + let nb = numbits e in + for i = 0 to nb - 1 do + if bit e i = 1 then result := bn_mod (mul !result !b) m; + b := bn_mod (mul !b !b) m + done; + !result + +let of_bytes_le (s : string) : bn = + let acc = ref bzero in + for i = String.length s - 1 downto 0 do + acc := add (mul !acc (of_int 256)) (of_int (Char.code s.[i])) + done; + !acc + +let to_bytes_le (a : bn) (n : int) : string = + let b = Bytes.make n '\000' in + let cur = ref (norm a) in + for i = 0 to n - 1 do + let q = div_small !cur 256 in + let r = + let qm = mul q (of_int 256) in + let d = sub !cur qm in + if is_zero d then 0 else d.(0) + in + Bytes.set b i (Char.chr r); + cur := q + done; + Bytes.unsafe_to_string b + +(* ---- Field GF(p), p = 2^255 - 19 ---- *) + +let p = + let twop255 = Array.make 11 0 in (* 11*26 = 286 > 255 *) + let limb = 255 / bits and off = 255 mod bits in + twop255.(limb) <- 1 lsl off; + sub (norm twop255) (of_int 19) + +let fmod a = bn_mod a p +let fadd a b = fmod (add a b) +let fsub a b = fmod (add a (sub p (fmod b))) +let fmul a b = fmod (mul a b) +let fpow a e = powmod a e p +let finv a = fpow a (sub p (of_int 2)) (* Fermat: a^(p-2) *) + +(* group order L = 2^252 + 27742317777372353535851937790883648493 *) +let ell = + of_bytes_le + "\xed\xd3\xf5\x5c\x1a\x63\x12\x58\xd6\x9c\xf7\xa2\xde\xf9\xde\x14\ + \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10" + +(* d = -121665 / 121666 mod p *) +let dconst = + let inv666 = finv (of_int 121666) in + fmod (mul (fsub (of_int 0) (of_int 121665)) inv666) + +(* sqrt(-1) = 2^((p-1)/4) mod p *) +let sqrtm1 = fpow (of_int 2) (div_small (sub p (of_int 1)) 4) + +(* ---- edwards25519 points in extended coords (X,Y,Z,T) ---- *) + +type pt = { x : bn; y : bn; z : bn; t : bn } + +let identity = { x = bzero; y = of_int 1; z = of_int 1; t = bzero } + +(* add-2008-hwcd-3, complete for a = -1 on ed25519 *) +let padd (p1 : pt) (p2 : pt) : pt = + let a = fmul (fsub p1.y p1.x) (fsub p2.y p2.x) in + let b = fmul (fadd p1.y p1.x) (fadd p2.y p2.x) in + let c = fmul (fmul p1.t (fmul (of_int 2) dconst)) p2.t in + let dd = fmul (fmul p1.z (of_int 2)) p2.z in + let e = fsub b a in + let f = fsub dd c in + let g = fadd dd c in + let h = fadd b a in + { x = fmul e f; y = fmul g h; t = fmul e h; z = fmul f g } + +let scalar_mul (n : bn) (q : pt) : pt = + let r = ref identity in + for i = numbits n - 1 downto 0 do + r := padd !r !r; + if bit n i = 1 then r := padd !r q + done; + !r + +let pnegate (q : pt) : pt = + { q with x = fsub (of_int 0) q.x; t = fsub (of_int 0) q.t } + +(* Decompress a 32-byte little-endian point encoding. *) +let decompress (s : string) : pt option = + if String.length s <> 32 then None + else begin + let sign = (Char.code s.[31] lsr 7) land 1 in + let s' = Bytes.of_string s in + Bytes.set s' 31 (Char.chr (Char.code s.[31] land 0x7f)); + let y = of_bytes_le (Bytes.unsafe_to_string s') in + if cmp y p >= 0 then None + else begin + let y2 = fmul y y in + let u = fsub y2 (of_int 1) in + let v = fadd (fmul dconst y2) (of_int 1) in + (* x = u v^3 (u v^7)^((p-5)/8) *) + let v3 = fmul (fmul v v) v in + let v7 = fmul (fmul v3 v3) v in + let exp = div_small (sub p (of_int 5)) 8 in + let x0 = fmul (fmul u v3) (fpow (fmul u v7) exp) in + let vx2 = fmul v (fmul x0 x0) in + let x = + if cmp vx2 u = 0 then Some x0 + else if cmp vx2 (fsub (of_int 0) u) = 0 then Some (fmul x0 sqrtm1) + else None + in + match x with + | None -> None + | Some x -> + if is_zero x && sign = 1 then None + else begin + let x = if (bit x 0) <> sign then fsub (of_int 0) x else x in + Some { x; y; z = of_int 1; t = fmul x y } + end + end + end + +(* Encode a point to 32-byte little-endian (y with x-parity bit). *) +let encode (q : pt) : string = + let zi = finv q.z in + let x = fmul q.x zi and y = fmul q.y zi in + let b = Bytes.of_string (to_bytes_le y 32) in + let last = Char.code (Bytes.get b 31) lor ((bit x 0) lsl 7) in + Bytes.set b 31 (Char.chr last); + Bytes.unsafe_to_string b + +(* base point: y = 4/5 mod p, x even (sign 0). *) +let base_point = + let by = fmul (of_int 4) (finv (of_int 5)) in + match decompress (to_bytes_le by 32) with + | Some pt -> pt + | None -> failwith "ed25519: base point decompress failed" + +let unhex (h : string) : string = + let n = String.length h / 2 in + let b = Bytes.create n in + for i = 0 to n - 1 do + Bytes.set b i + (Char.chr (int_of_string ("0x" ^ String.sub h (2 * i) 2))) + done; + Bytes.unsafe_to_string b + +let sha512_bytes s = unhex (Sx_sha2.sha512_hex s) + +(* RFC 8032 §5.1.7 cofactorless: encode([S]B - [k]A) == R. *) +let verify ~pubkey ~msg ~sig_ : bool = + if String.length pubkey <> 32 || String.length sig_ <> 64 then false + else + let rb = String.sub sig_ 0 32 in + let sb = String.sub sig_ 32 32 in + let s = of_bytes_le sb in + if cmp s ell >= 0 then false + else + match decompress pubkey with + | None -> false + | Some a -> + let h = sha512_bytes (rb ^ pubkey ^ msg) in + let k = bn_mod (of_bytes_le h) ell in + let sb_pt = scalar_mul s base_point in + let ka = scalar_mul k a in + let chk = padd sb_pt (pnegate ka) in + (try encode chk = rb with _ -> false) diff --git a/hosts/ocaml/lib/sx_primitives.ml b/hosts/ocaml/lib/sx_primitives.ml index bd25563c..3209b744 100644 --- a/hosts/ocaml/lib/sx_primitives.ml +++ b/hosts/ocaml/lib/sx_primitives.ml @@ -3237,6 +3237,21 @@ let () = with Sys_error msg -> raise (Eval_error ("file-read: " ^ msg))) | _ -> raise (Eval_error "file-read: (path)")); + (* fed-sx Step 3 segment replay. Sorted names, no "."/".." ; + errors prefixed like file-read (msg carries enoent/enotdir). *) + register "file-list-dir" (fun args -> + match args with + | [String path] -> + (try + let names = Sys.readdir path in + let names = + Array.to_list names + |> List.filter (fun n -> n <> "." && n <> "..") in + let names = List.sort compare names in + List (List.map (fun n -> String n) names) + with Sys_error msg -> raise (Eval_error ("file-list-dir: " ^ msg))) + | _ -> raise (Eval_error "file-list-dir: (path)")); + register "file-write" (fun args -> match args with | [String path; String content] -> @@ -4158,4 +4173,61 @@ let () = Sx_types.jit_skipped_count := 0; Sx_types.jit_threshold_skipped_count := 0; Sx_types.jit_evicted_count := 0; - Nil) + Nil); + + (* fed-sx host primitives — pure-OCaml crypto (WASM-safe). *) + register "crypto-sha256" (fun args -> + match args with + | [String s] -> String (Sx_sha2.sha256_hex s) + | _ -> raise (Eval_error "crypto-sha256: (bytes)")); + + register "crypto-sha512" (fun args -> + match args with + | [String s] -> String (Sx_sha2.sha512_hex s) + | _ -> raise (Eval_error "crypto-sha512: (bytes)")); + + register "crypto-sha3-256" (fun args -> + match args with + | [String s] -> String (Sx_sha3.sha3_256_hex s) + | _ -> raise (Eval_error "crypto-sha3-256: (bytes)")); + + register "cbor-encode" (fun args -> + match args with + | [v] -> + (try String (Sx_cbor.encode v) + with Sx_cbor.Cbor_error m -> raise (Eval_error m)) + | _ -> raise (Eval_error "cbor-encode: (value)")); + + register "cbor-decode" (fun args -> + match args with + | [String s] -> + (try Sx_cbor.decode s + with Sx_cbor.Cbor_error m -> raise (Eval_error m)) + | _ -> raise (Eval_error "cbor-decode: (bytes)")); + + register "cid-from-bytes" (fun args -> + match args with + | [Integer codec; String mh] -> + String (Sx_cid.cidv1 codec mh) + | _ -> raise (Eval_error "cid-from-bytes: (codec multihash-bytes)")); + + register "cid-from-sx" (fun args -> + match args with + | [v] -> + (try String (Sx_cid.cid_from_sx v) + with Sx_cbor.Cbor_error m -> raise (Eval_error m)) + | _ -> raise (Eval_error "cid-from-sx: (value)")); + + (* Verify is total: any malformed input -> false, never raises. *) + register "ed25519-verify" (fun args -> + match args with + | [String pk; String msg; String sg] -> + Bool (try Sx_ed25519.verify ~pubkey:pk ~msg ~sig_:sg + with _ -> false) + | _ -> Bool false); + + register "rsa-sha256-verify" (fun args -> + match args with + | [String spki; String msg; String sg] -> + Bool (try Sx_rsa.verify ~spki ~msg ~sig_:sg with _ -> false) + | _ -> Bool false) diff --git a/hosts/ocaml/lib/sx_rsa.ml b/hosts/ocaml/lib/sx_rsa.ml new file mode 100644 index 00000000..a6bf5b90 --- /dev/null +++ b/hosts/ocaml/lib/sx_rsa.ml @@ -0,0 +1,220 @@ +(** RSASSA-PKCS1-v1_5 verification with SHA-256 — pure OCaml, + WASM-safe. Self-contained minimal bignum (modexp only), a tiny + DER reader for SubjectPublicKeyInfo, and the fixed SHA-256 + DigestInfo prefix. Verify only on public data — constant time + not required. Reference: RFC 8017 §8.2.2, §9.2. No deps. *) + +(* ---- Minimal unsigned bignum: int array, little-endian, base 2^26 ---- *) + +let bits = 26 +let base = 1 lsl bits +let mask = base - 1 + +type bn = int array + +let norm a = + let n = ref (Array.length a) in + while !n > 1 && a.(!n - 1) = 0 do decr n done; + if !n = Array.length a then a else Array.sub a 0 !n + +let bzero : bn = [| 0 |] +let is_zero a = Array.length a = 1 && a.(0) = 0 + +let cmp a b = + let a = norm a and b = norm b in + let la = Array.length a and lb = Array.length b in + if la <> lb then compare la lb + else begin + let r = ref 0 and i = ref (la - 1) in + while !r = 0 && !i >= 0 do + if a.(!i) <> b.(!i) then r := compare a.(!i) b.(!i); + decr i + done; !r + end + +let add a b = + let la = Array.length a and lb = Array.length b in + let n = (max la lb) + 1 in + let r = Array.make n 0 and carry = ref 0 in + for i = 0 to n - 1 do + let s = !carry + (if i < la then a.(i) else 0) + + (if i < lb then b.(i) else 0) in + r.(i) <- s land mask; carry := s lsr bits + done; + norm r + +let sub a b = (* requires a >= b *) + let la = Array.length a and lb = Array.length b in + let r = Array.make la 0 and borrow = ref 0 in + for i = 0 to la - 1 do + let s = a.(i) - !borrow - (if i < lb then b.(i) else 0) in + if s < 0 then (r.(i) <- s + base; borrow := 1) + else (r.(i) <- s; borrow := 0) + done; + norm r + +let mul a b = + let la = Array.length a and lb = Array.length b in + let r = Array.make (la + lb) 0 in + for i = 0 to la - 1 do + let carry = ref 0 in + for j = 0 to lb - 1 do + let s = r.(i + j) + a.(i) * b.(j) + !carry in + r.(i + j) <- s land mask; carry := s lsr bits + done; + r.(i + lb) <- r.(i + lb) + !carry + done; + norm r + +let numbits a = + let a = norm a in + let hi = Array.length a - 1 in + if hi = 0 && a.(0) = 0 then 0 + else begin + let b = ref 0 and v = ref a.(hi) in + while !v > 0 do incr b; v := !v lsr 1 done; + hi * bits + !b + end + +let bit a i = + let limb = i / bits and off = i mod bits in + if limb >= Array.length a then 0 else (a.(limb) lsr off) land 1 + +let bn_mod a m = (* binary long division, m > 0 *) + if cmp a m < 0 then norm a + else begin + let r = ref bzero in + for i = numbits a - 1 downto 0 do + r := add !r !r; + if bit a i = 1 then r := add !r [| 1 |]; + if cmp !r m >= 0 then r := sub !r m + done; + !r + end + +let powmod b0 e m = + let result = ref [| 1 |] and b = ref (bn_mod b0 m) in + for i = 0 to numbits e - 1 do + if bit e i = 1 then result := bn_mod (mul !result !b) m; + b := bn_mod (mul !b !b) m + done; + !result + +let of_bytes_be (s : string) : bn = + let acc = ref bzero in + for i = 0 to String.length s - 1 do + acc := add (mul !acc [| 256 |]) [| Char.code s.[i] |] + done; + !acc + +let div_small a d = + let la = Array.length a in + let q = Array.make la 0 and rem = ref 0 in + for i = la - 1 downto 0 do + let cur = (!rem lsl bits) lor a.(i) in + q.(i) <- cur / d; rem := cur mod d + done; + norm q + +let to_bytes_be (a : bn) (n : int) : string = + let b = Bytes.make n '\000' in + let cur = ref (norm a) in + for i = n - 1 downto 0 do + let q = div_small !cur 256 in + let r = + let d = sub !cur (mul q [| 256 |]) in + if is_zero d then 0 else d.(0) + in + Bytes.set b i (Char.chr r); + cur := q + done; + Bytes.unsafe_to_string b + +(* ---- Minimal DER reader (for SubjectPublicKeyInfo) ---- *) + +exception Der of string + +(* Returns (tag, content_start, content_len, next). *) +let der_tlv s pos = + if pos + 2 > String.length s then raise (Der "short"); + let tag = Char.code s.[pos] in + let l0 = Char.code s.[pos + 1] in + let len, hdr = + if l0 < 0x80 then l0, 2 + else begin + let nb = l0 land 0x7f in + if pos + 2 + nb > String.length s then raise (Der "short len"); + let v = ref 0 in + for i = 0 to nb - 1 do + v := (!v lsl 8) lor Char.code s.[pos + 2 + i] + done; + !v, 2 + nb + end + in + (tag, pos + hdr, len, pos + hdr + len) + +(* SPKI DER -> (n, e) as bignums. *) +let parse_spki (der : string) : bn * bn = + let tag, c, _l, _ = der_tlv der 0 in + if tag <> 0x30 then raise (Der "spki: outer not SEQUENCE"); + (* AlgorithmIdentifier SEQUENCE — skip. *) + let _, _, _, after_alg = der_tlv der c in + (* BIT STRING. *) + let bt, bc, bl, _ = der_tlv der after_alg in + if bt <> 0x03 then raise (Der "spki: expected BIT STRING"); + (* First content byte = unused bits (must be 0). *) + let rpk_start = bc + 1 in + ignore bl; + let st, sc, _, _ = der_tlv der rpk_start in + if st <> 0x30 then raise (Der "spki: RSAPublicKey not SEQUENCE"); + let nt, nc, nl, after_n = der_tlv der sc in + if nt <> 0x02 then raise (Der "spki: modulus not INTEGER"); + let et, ec, el, _ = der_tlv der after_n in + if et <> 0x02 then raise (Der "spki: exponent not INTEGER"); + let n = of_bytes_be (String.sub der nc nl) in + let e = of_bytes_be (String.sub der ec el) in + (n, e) + +(* SHA-256 DigestInfo DER prefix (RFC 8017 §9.2 note 1). *) +let sha256_digestinfo_prefix = + "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20" + +let unhex h = + let n = String.length h / 2 in + let b = Bytes.create n in + for i = 0 to n - 1 do + Bytes.set b i (Char.chr (int_of_string ("0x" ^ String.sub h (2 * i) 2))) + done; + Bytes.unsafe_to_string b + +(* RSASSA-PKCS1-v1_5 verify with SHA-256. Total: any malformed + input yields false (caller wraps, but be defensive here too). *) +let verify ~spki ~msg ~sig_ : bool = + try + let n, e = parse_spki spki in + let k = (numbits n + 7) / 8 in + if String.length sig_ <> k then false + else begin + let s = of_bytes_be sig_ in + if cmp s n >= 0 then false + else begin + let m = powmod s e n in + let em = to_bytes_be m k in + (* EM = 0x00 01 FF..FF 00 || DigestInfo || H *) + let h = unhex (Sx_sha2.sha256_hex msg) in + let t = sha256_digestinfo_prefix ^ h in + let tlen = String.length t in + if k < tlen + 11 then false + else begin + let ok = ref (em.[0] = '\x00' && em.[1] = '\x01') in + let ps_end = k - tlen - 1 in + for i = 2 to ps_end - 1 do + if em.[i] <> '\xff' then ok := false + done; + if em.[ps_end] <> '\x00' then ok := false; + if String.sub em (ps_end + 1) tlen <> t then ok := false; + !ok + end + end + end + with _ -> false diff --git a/hosts/ocaml/lib/sx_sha2.ml b/hosts/ocaml/lib/sx_sha2.ml new file mode 100644 index 00000000..1ea6b8f8 --- /dev/null +++ b/hosts/ocaml/lib/sx_sha2.ml @@ -0,0 +1,212 @@ +(** SHA-2 (SHA-256, SHA-512) — pure OCaml, WASM-safe. + + No C stubs, no external deps. Used by the fed-sx host primitives + [crypto-sha256] / [crypto-sha512]. Reference: FIPS 180-4. *) + +(* ---- SHA-256 (FIPS 180-4 §6.2). 32-bit words held in native int, + masked to 32 bits after every arithmetic op. ---- *) + +let mask32 = 0xFFFFFFFF + +let k256 = [| + 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 |] + +let rotr32 x n = ((x lsr n) lor (x lsl (32 - n))) land mask32 + +let sha256_hex (msg : string) : string = + let h = [| 0x6a09e667; 0xbb67ae85; 0x3c6ef372; 0xa54ff53a; + 0x510e527f; 0x9b05688c; 0x1f83d9ab; 0x5be0cd19 |] in + let len = String.length msg in + (* Padded length: multiple of 64 bytes. *) + let bitlen = len * 8 in + let padlen = + let r = (len + 1) mod 64 in + if r <= 56 then 56 - r else 120 - r + in + let total = len + 1 + padlen + 8 in + let buf = Bytes.make total '\000' in + Bytes.blit_string msg 0 buf 0 len; + Bytes.set buf len '\x80'; + (* 64-bit big-endian bit length (we cap at OCaml int range). *) + for i = 0 to 7 do + Bytes.set buf (total - 1 - i) + (Char.chr ((bitlen lsr (8 * i)) land 0xFF)) + done; + let w = Array.make 64 0 in + let nblocks = total / 64 in + for b = 0 to nblocks - 1 do + let base = b * 64 in + for t = 0 to 15 do + let o = base + t * 4 in + w.(t) <- + (Char.code (Bytes.get buf o) lsl 24) + lor (Char.code (Bytes.get buf (o + 1)) lsl 16) + lor (Char.code (Bytes.get buf (o + 2)) lsl 8) + lor (Char.code (Bytes.get buf (o + 3))) + done; + for t = 16 to 63 do + let s0 = + (rotr32 w.(t - 15) 7) lxor (rotr32 w.(t - 15) 18) + lxor (w.(t - 15) lsr 3) in + let s1 = + (rotr32 w.(t - 2) 17) lxor (rotr32 w.(t - 2) 19) + lxor (w.(t - 2) lsr 10) in + w.(t) <- (w.(t - 16) + s0 + w.(t - 7) + s1) land mask32 + done; + let a = ref h.(0) and bb = ref h.(1) and c = ref h.(2) + and d = ref h.(3) and e = ref h.(4) and f = ref h.(5) + and g = ref h.(6) and hh = ref h.(7) in + for t = 0 to 63 do + let s1 = + (rotr32 !e 6) lxor (rotr32 !e 11) lxor (rotr32 !e 25) in + let ch = (!e land !f) lxor ((lnot !e land mask32) land !g) in + let t1 = (!hh + s1 + ch + k256.(t) + w.(t)) land mask32 in + let s0 = + (rotr32 !a 2) lxor (rotr32 !a 13) lxor (rotr32 !a 22) in + let maj = (!a land !bb) lxor (!a land !c) lxor (!bb land !c) in + let t2 = (s0 + maj) land mask32 in + hh := !g; g := !f; f := !e; + e := (!d + t1) land mask32; + d := !c; c := !bb; bb := !a; + a := (t1 + t2) land mask32 + done; + h.(0) <- (h.(0) + !a) land mask32; + h.(1) <- (h.(1) + !bb) land mask32; + h.(2) <- (h.(2) + !c) land mask32; + h.(3) <- (h.(3) + !d) land mask32; + h.(4) <- (h.(4) + !e) land mask32; + h.(5) <- (h.(5) + !f) land mask32; + h.(6) <- (h.(6) + !g) land mask32; + h.(7) <- (h.(7) + !hh) land mask32 + done; + let out = Buffer.create 64 in + Array.iter (fun x -> Buffer.add_string out (Printf.sprintf "%08x" x)) h; + Buffer.contents out + +(* ---- SHA-512 (FIPS 180-4 §6.4). 64-bit words via Int64. + 128-bit length append; we only support messages whose bit length + fits in 64 bits (high word is always zero). ---- *) + +let k512 = [| + 0x428a2f98d728ae22L; 0x7137449123ef65cdL; 0xb5c0fbcfec4d3b2fL; + 0xe9b5dba58189dbbcL; 0x3956c25bf348b538L; 0x59f111f1b605d019L; + 0x923f82a4af194f9bL; 0xab1c5ed5da6d8118L; 0xd807aa98a3030242L; + 0x12835b0145706fbeL; 0x243185be4ee4b28cL; 0x550c7dc3d5ffb4e2L; + 0x72be5d74f27b896fL; 0x80deb1fe3b1696b1L; 0x9bdc06a725c71235L; + 0xc19bf174cf692694L; 0xe49b69c19ef14ad2L; 0xefbe4786384f25e3L; + 0x0fc19dc68b8cd5b5L; 0x240ca1cc77ac9c65L; 0x2de92c6f592b0275L; + 0x4a7484aa6ea6e483L; 0x5cb0a9dcbd41fbd4L; 0x76f988da831153b5L; + 0x983e5152ee66dfabL; 0xa831c66d2db43210L; 0xb00327c898fb213fL; + 0xbf597fc7beef0ee4L; 0xc6e00bf33da88fc2L; 0xd5a79147930aa725L; + 0x06ca6351e003826fL; 0x142929670a0e6e70L; 0x27b70a8546d22ffcL; + 0x2e1b21385c26c926L; 0x4d2c6dfc5ac42aedL; 0x53380d139d95b3dfL; + 0x650a73548baf63deL; 0x766a0abb3c77b2a8L; 0x81c2c92e47edaee6L; + 0x92722c851482353bL; 0xa2bfe8a14cf10364L; 0xa81a664bbc423001L; + 0xc24b8b70d0f89791L; 0xc76c51a30654be30L; 0xd192e819d6ef5218L; + 0xd69906245565a910L; 0xf40e35855771202aL; 0x106aa07032bbd1b8L; + 0x19a4c116b8d2d0c8L; 0x1e376c085141ab53L; 0x2748774cdf8eeb99L; + 0x34b0bcb5e19b48a8L; 0x391c0cb3c5c95a63L; 0x4ed8aa4ae3418acbL; + 0x5b9cca4f7763e373L; 0x682e6ff3d6b2b8a3L; 0x748f82ee5defb2fcL; + 0x78a5636f43172f60L; 0x84c87814a1f0ab72L; 0x8cc702081a6439ecL; + 0x90befffa23631e28L; 0xa4506cebde82bde9L; 0xbef9a3f7b2c67915L; + 0xc67178f2e372532bL; 0xca273eceea26619cL; 0xd186b8c721c0c207L; + 0xeada7dd6cde0eb1eL; 0xf57d4f7fee6ed178L; 0x06f067aa72176fbaL; + 0x0a637dc5a2c898a6L; 0x113f9804bef90daeL; 0x1b710b35131c471bL; + 0x28db77f523047d84L; 0x32caab7b40c72493L; 0x3c9ebe0a15c9bebcL; + 0x431d67c49c100d4cL; 0x4cc5d4becb3e42b6L; 0x597f299cfc657e2aL; + 0x5fcb6fab3ad6faecL; 0x6c44198c4a475817L |] + +let ( &: ) = Int64.logand +let ( |: ) = Int64.logor +let ( ^: ) = Int64.logxor +let ( +: ) = Int64.add +let lnot64 = Int64.lognot + +let rotr64 x n = + (Int64.shift_right_logical x n) |: (Int64.shift_left x (64 - n)) + +let sha512_hex (msg : string) : string = + let h = [| 0x6a09e667f3bcc908L; 0xbb67ae8584caa73bL; + 0x3c6ef372fe94f82bL; 0xa54ff53a5f1d36f1L; + 0x510e527fade682d1L; 0x9b05688c2b3e6c1fL; + 0x1f83d9abfb41bd6bL; 0x5be0cd19137e2179L |] in + let len = String.length msg in + let bitlen = len * 8 in + (* Pad to a multiple of 128 bytes; 16-byte big-endian length. *) + let padlen = + let r = (len + 1) mod 128 in + if r <= 112 then 112 - r else 240 - r + in + let total = len + 1 + padlen + 16 in + let buf = Bytes.make total '\000' in + Bytes.blit_string msg 0 buf 0 len; + Bytes.set buf len '\x80'; + for i = 0 to 7 do + Bytes.set buf (total - 1 - i) + (Char.chr ((bitlen lsr (8 * i)) land 0xFF)) + done; + let w = Array.make 80 0L in + let nblocks = total / 128 in + for b = 0 to nblocks - 1 do + let base = b * 128 in + for t = 0 to 15 do + let o = base + t * 8 in + let v = ref 0L in + for j = 0 to 7 do + v := Int64.logor (Int64.shift_left !v 8) + (Int64.of_int (Char.code (Bytes.get buf (o + j)))) + done; + w.(t) <- !v + done; + for t = 16 to 79 do + let s0 = + (rotr64 w.(t - 15) 1) ^: (rotr64 w.(t - 15) 8) + ^: (Int64.shift_right_logical w.(t - 15) 7) in + let s1 = + (rotr64 w.(t - 2) 19) ^: (rotr64 w.(t - 2) 61) + ^: (Int64.shift_right_logical w.(t - 2) 6) in + w.(t) <- w.(t - 16) +: s0 +: w.(t - 7) +: s1 + done; + let a = ref h.(0) and bb = ref h.(1) and c = ref h.(2) + and d = ref h.(3) and e = ref h.(4) and f = ref h.(5) + and g = ref h.(6) and hh = ref h.(7) in + for t = 0 to 79 do + let s1 = (rotr64 !e 14) ^: (rotr64 !e 18) ^: (rotr64 !e 41) in + let ch = (!e &: !f) ^: ((lnot64 !e) &: !g) in + let t1 = !hh +: s1 +: ch +: k512.(t) +: w.(t) in + let s0 = (rotr64 !a 28) ^: (rotr64 !a 34) ^: (rotr64 !a 39) in + let maj = (!a &: !bb) ^: (!a &: !c) ^: (!bb &: !c) in + let t2 = s0 +: maj in + hh := !g; g := !f; f := !e; + e := !d +: t1; + d := !c; c := !bb; bb := !a; + a := t1 +: t2 + done; + h.(0) <- h.(0) +: !a; + h.(1) <- h.(1) +: !bb; + h.(2) <- h.(2) +: !c; + h.(3) <- h.(3) +: !d; + h.(4) <- h.(4) +: !e; + h.(5) <- h.(5) +: !f; + h.(6) <- h.(6) +: !g; + h.(7) <- h.(7) +: !hh + done; + let out = Buffer.create 128 in + Array.iter + (fun x -> Buffer.add_string out (Printf.sprintf "%016Lx" x)) h; + Buffer.contents out diff --git a/hosts/ocaml/lib/sx_sha3.ml b/hosts/ocaml/lib/sx_sha3.ml new file mode 100644 index 00000000..4d18ba41 --- /dev/null +++ b/hosts/ocaml/lib/sx_sha3.ml @@ -0,0 +1,107 @@ +(** SHA-3 (SHA3-256) — pure OCaml, WASM-safe. + + Keccak-f[1600] permutation + SHA-3 multi-rate padding (domain byte + 0x06, NOT the legacy Keccak 0x01). Reference: FIPS 202. No deps. *) + +let ( ^: ) = Int64.logxor +let ( &: ) = Int64.logand +let lnot64 = Int64.lognot + +let rotl64 x n = + if n = 0 then x + else + Int64.logor (Int64.shift_left x n) (Int64.shift_right_logical x (64 - n)) + +(* FIPS 202 Table 2 — ρ rotation offsets, indexed lane = x + 5*y. *) +let rho = [| + 0; 1; 62; 28; 27; + 36; 44; 6; 55; 20; + 3; 10; 43; 25; 39; + 41; 45; 15; 21; 8; + 18; 2; 61; 56; 14 |] + +(* FIPS 202 §3.2.5 — round constants RC[0..23] for ι. *) +let rc = [| + 0x0000000000000001L; 0x0000000000008082L; 0x800000000000808aL; + 0x8000000080008000L; 0x000000000000808bL; 0x0000000080000001L; + 0x8000000080008081L; 0x8000000000008009L; 0x000000000000008aL; + 0x0000000000000088L; 0x0000000080008009L; 0x000000008000000aL; + 0x000000008000808bL; 0x800000000000008bL; 0x8000000000008089L; + 0x8000000000008003L; 0x8000000000008002L; 0x8000000000000080L; + 0x000000000000800aL; 0x800000008000000aL; 0x8000000080008081L; + 0x8000000000008080L; 0x0000000080000001L; 0x8000000080008008L |] + +let keccak_f (a : int64 array) : unit = + let c = Array.make 5 0L and d = Array.make 5 0L in + let b = Array.make 25 0L in + for round = 0 to 23 do + (* θ *) + for x = 0 to 4 do + c.(x) <- a.(x) ^: a.(x + 5) ^: a.(x + 10) + ^: a.(x + 15) ^: a.(x + 20) + done; + for x = 0 to 4 do + d.(x) <- c.((x + 4) mod 5) ^: (rotl64 c.((x + 1) mod 5) 1) + done; + for x = 0 to 4 do + for y = 0 to 4 do + a.(x + 5 * y) <- a.(x + 5 * y) ^: d.(x) + done + done; + (* ρ and π: B[y, 2x+3y] = rotl(A[x,y], rho[x,y]) *) + for x = 0 to 4 do + for y = 0 to 4 do + let nx = y and ny = (2 * x + 3 * y) mod 5 in + b.(nx + 5 * ny) <- rotl64 a.(x + 5 * y) rho.(x + 5 * y) + done + done; + (* χ *) + for y = 0 to 4 do + for x = 0 to 4 do + a.(x + 5 * y) <- + b.(x + 5 * y) + ^: ((lnot64 b.((x + 1) mod 5 + 5 * y)) + &: b.((x + 2) mod 5 + 5 * y)) + done + done; + (* ι *) + a.(0) <- a.(0) ^: rc.(round) + done + +let sha3_256_hex (msg : string) : string = + let rate = 136 (* bytes: (1600 - 2*256) / 8 *) in + let len = String.length msg in + (* pad10*1 with SHA-3 domain byte 0x06; last byte ORed with 0x80. *) + let q = rate - (len mod rate) in + let padded = Bytes.make (len + q) '\000' in + Bytes.blit_string msg 0 padded 0 len; + if q = 1 then + Bytes.set padded len '\x86' + else begin + Bytes.set padded len '\x06'; + Bytes.set padded (len + q - 1) '\x80' + end; + let total = Bytes.length padded in + let a = Array.make 25 0L in + let nblocks = total / rate in + for blk = 0 to nblocks - 1 do + let base = blk * rate in + (* Absorb: XOR rate bytes into the state, little-endian lanes. *) + for j = 0 to rate - 1 do + let lane = j / 8 and sh = (j mod 8) * 8 in + let byte = Int64.of_int (Char.code (Bytes.get padded (base + j))) in + a.(lane) <- a.(lane) ^: (Int64.shift_left byte sh) + done; + keccak_f a + done; + (* Squeeze 32 bytes (fits in the first 4 lanes; rate > 32). *) + let out = Buffer.create 64 in + for j = 0 to 31 do + let lane = j / 8 and sh = (j mod 8) * 8 in + let byte = + Int64.to_int + (Int64.logand (Int64.shift_right_logical a.(lane) sh) 0xFFL) + in + Buffer.add_string out (Printf.sprintf "%02x" byte) + done; + Buffer.contents out diff --git a/plans/agent-briefings/fed-prims-loop.md b/plans/agent-briefings/fed-prims-loop.md new file mode 100644 index 00000000..70c63ae7 --- /dev/null +++ b/plans/agent-briefings/fed-prims-loop.md @@ -0,0 +1,109 @@ +# fed-prims loop agent (single agent, phase-ordered) + +Role: iterates `plans/fed-sx-host-primitives.md` forever. Adds the pure-OCaml +crypto / CBOR / CID / Ed25519 / RSA primitives and the native HTTP server that +Erlang Phase 8 BIFs (and therefore fed-sx Milestone 1) are blocked on. One +feature per commit. + +``` +description: fed-prims host-primitive loop +subagent_type: general-purpose +run_in_background: true +isolation: worktree +``` + +## Prompt + +You are the sole background agent working `/root/rose-ash/plans/fed-sx-host-primitives.md`. +You run in an isolated git worktree on branch `loops/fed-prims`. You work the +plan's phases in order (A→I), forever, one commit per feature. Push to +`origin/loops/fed-prims` after every commit. + +## Restart baseline — check before iterating + +1. Read `plans/fed-sx-host-primitives.md` — Phasing + Progress log + Blockers + tell you where you are. +2. `cd hosts/ocaml && dune build bin/sx_server.exe 2>&1 | tail` — must be green + before new work. If broken and not by your last edit, Blockers + stop. +3. `bash hosts/ocaml/browser/test_boot.sh` — the WASM kernel must boot. This is + the regression you are most at risk of causing. +4. Find the first unchecked `[ ]` phase. That is your iteration. + +## The iteration + +Implement → `dune build bin/sx_server.exe` (native) → **WASM build check** +(`test_boot.sh`) → run the phase's tests → run the no-regression gate +(`conformance.sh`, see plan) → commit → tick the `[ ]` → append one dated line +to the Progress log (newest first) → push → stop. + +One phase = one iteration = one commit. Do not batch phases. + +## Ground rules (hard) + +- **Scope:** only `hosts/ocaml/lib/**`, `hosts/ocaml/bin/**`, and + `plans/fed-sx-host-primitives.md`. The single exception is Phase I, which also + edits exactly one Blockers entry in `plans/erlang-on-sx.md`. Do **not** touch + `lib/erlang/**`, `spec/`, `lib/` root, other `lib//`. +- **Pure OCaml for `lib/` primitives.** No new opam deps. WASM-safe: no C stubs, + no `Unix`/`Thread` in `lib/sx_primitives.ml`. The HTTP server (Phase H) is + native-only — register it in `bin/sx_server.ml`, never in the lib. +- **Prove WASM every commit.** `test_boot.sh` green is a phase gate, not + optional. A broken WASM kernel = the phase failed; revert and rethink. +- **No-regression gate:** OCaml `run_tests` + Erlang `conformance.sh` must stay + at their current pass counts (Erlang 715/715 once the merge lands; otherwise + whatever `lib/erlang/scoreboard.json` says). New crypto tests are additive. +- **`.ml`/`.sh` files:** ordinary `Read`/`Edit`/`Write` — these are NOT `.sx`. + Do not use sx-tree MCP for OCaml. (sx-tree is only if you ever touch `.sx`, + which this loop should not.) +- **Builds are slow.** Use a generous `timeout` on `dune build` (≥600s) and on + `conformance.sh` (≥400s). If a build genuinely hangs >10min, Blockers + stop. +- **Worktree:** commit, push `origin/loops/fed-prims`. Never `main`, never + `architecture`. +- **Commit granularity:** one feature per commit. `fed-prims: SHA-256 + 4 NIST + vectors`. Update Progress log + tick box every commit. +- **If blocked** two iterations on the same issue: Blockers entry, move to the + next independent phase (A-G are largely independent; H is independent; only + D depends on A+C, E depends on A). + +## Crypto correctness gotchas + +- **Test vectors are non-negotiable.** Every hash/sig phase lands with published + vectors (NIST FIPS 180-4 / 202, RFC 8032, RFC 8949). A primitive without a + passing standard vector is not done — do not tick the box. +- **SHA endianness:** SHA-2 is big-endian length-append; SHA-3 is little-endian + Keccak lane order. Easy to get backwards — the empty-string vector catches it. +- **dag-cbor determinism:** map keys sorted by **byte length first, then + bytewise**. Not lexicographic-only. The "reordered dict keys → identical + bytes" test is the guard; it must be in the phase. +- **CIDv1 layout:** `0x01 || codec-varint || (mh-code-varint || mh-len-varint || + digest)`, then multibase base32-lower with a leading `b`. Off-by-one in varint + is the classic bug — cross-check one CID against `ipfs` CLI if available. +- **Ed25519 verify is total:** wrong-length inputs return `false`, never raise. + Verify checks `[S]B = R + [k]A` with `k = SHA512(R||A||M)` reduced mod L. +- **RSA:** PKCS#1 v1.5 EMSA — the DigestInfo DER prefix for SHA-256 is fixed + (`3031300d060960864801650304020105000420`). Constant-time not required (verify + only, public data). + +## General gotchas + +- The `sx` library is `(wrapped false)` — new module `Sx_sha2` is referenced as + `Sha2.f` is **wrong**; it's `Sx_sha2.f` unless you also alias. Check + `lib/dune` `include_subdirs unqualified`: a new `lib/sx_sha2.ml` is module + `Sx_sha2`. Match the existing `Sx_*` naming. +- `Eval_error` is the primitive-error exception; raise it with `"name: shape"`. +- Reach a primitive from SX to smoke-test: + `printf '(epoch 1)\n(crypto-sha256 "abc")\n' | hosts/ocaml/_build/default/bin/sx_server.exe` +- The native binary the conformance gate uses is + `hosts/ocaml/_build/default/bin/sx_server.exe` — rebuild it before gating. + +## Style + +- No comments in OCaml unless non-obvious (crypto constants ARE non-obvious — + cite the RFC/FIPS section in a one-line comment). +- No new planning docs — update `plans/fed-sx-host-primitives.md` inline. +- One feature per iteration. Build. WASM-check. Test. Gate. Commit. Log. Push. + Next. + +Go. Run the restart baseline. Find the first unchecked `[ ]`. Implement it. +Remember: no commit without a passing standard test vector AND a green WASM +boot. diff --git a/plans/erlang-on-sx.md b/plans/erlang-on-sx.md index 8d33d228..72d7907a 100644 --- a/plans/erlang-on-sx.md +++ b/plans/erlang-on-sx.md @@ -255,4 +255,18 @@ _Newest first._ - **Phase 9a — Opcode extension mechanism** — **RESOLVED 2026-05-15.** User widened scope to include hosts/ (merging back anyhow). Cherry-picked vm-ext phases A-E + force-linked `Sx_vm_extensions` into sx_server.exe. `extension-opcode-id` live; conformance 709/709. Remaining integration work (erlang_ext.ml + wiring the SX stub dispatcher to consult real ids) tracked as ordinary in-scope checkboxes now, not blockers. -- **SX runtime lacks platform primitives for crypto / dir-listing / HTTP / SQLite** (2026-05-14). Probed in `mcp_tree.exe`'s embedded `sx_server.exe`: `(sha256 "x")`, `(blake3 "x")`, `(hash "sha256" "x")`, `(file-list-dir "plans")`, `(http-get "url")`, `(fetch "url")` all return `Undefined symbol`. Only file-byte-level primitives exist: `file-read` ✓, `file-write` ✓, `file-delete` ✓, `file-exists?` ✓. Out-of-scope to add these (they live in `hosts/` per ground rules). Blocked Phase 8 BIFs: `crypto:hash/2`, `cid:from_bytes/1`, `cid:to_string/1`, `file:list_dir/1`, `httpc:request/4`, `sqlite:open/exec/query/close`. **Fix path:** a future iteration on the architecture branch can register host primitives (e.g. expose OCaml's `Digestif` for hashes, `Sys.readdir` for list_dir, `cohttp` for httpc); the BIF wrappers here will then become one-line registrations against `er-bif-registry`. +- **RESOLVED (2026-05-18) — SX runtime now exposes the platform + primitives Phase 8 BIFs need.** Delivered by `loops/fed-prims` + (see `plans/fed-sx-host-primitives.md` Handoff). Pure-OCaml, + WASM-safe except `http-listen` (native only). Wire Phase 8 BIFs: + - `crypto:hash/2` → `crypto-sha256` / `crypto-sha512` / + `crypto-sha3-256` (each `(bytes) -> hex-string`). + - `cid:from_bytes/1` → `cid-from-bytes` `(codec mh-bytes)`; + `cid:to_string/1` / canonical CID → `cid-from-sx` `(value)`; + dag-cbor via `cbor-encode` / `cbor-decode`. + - signature verify → `ed25519-verify` `(pk msg sig)` and + `rsa-sha256-verify` `(spki msg sig)` — both total (→ false). + - `file:list_dir/1` → `file-list-dir` `(path) -> (list string)`. + - fed-sx transport → `http-listen` `(port handler)` (native only). + Still deferred (leave blocked): `httpc` (HTTP client, v2) and + `sqlite-*` (v2 indexes) — not provided by fed-prims. diff --git a/plans/fed-sx-host-primitives.md b/plans/fed-sx-host-primitives.md new file mode 100644 index 00000000..6869d666 --- /dev/null +++ b/plans/fed-sx-host-primitives.md @@ -0,0 +1,290 @@ +# fed-sx host primitives — `hosts/ocaml/` + +The single blocker between Erlang Phase 8 (FFI mechanism — done) and starting +fed-sx Milestone 1: the SX OCaml runtime exposes no crypto / CID / HTTP host +primitives for the Phase 8 BIF wrappers to call. This plan adds exactly that +surface, pure-OCaml where it must stay WASM-safe, native-only where it can't. + +Reference: `plans/fed-sx-milestone-1.md` (build steps 1-8), +`plans/erlang-on-sx.md` Blockers ("SX runtime lacks platform primitives …"). + +## The hard constraint — WASM boundary + +`hosts/ocaml/lib/` is the `sx` library. `hosts/ocaml/browser/dune` links it +with `(modes byte js wasm)`. **Anything added to `lib/sx_primitives.ml` must +compile under `js_of_ocaml` AND `wasm_of_ocaml`.** Therefore: + +- **Pure OCaml only** for hash / CBOR / CID / Ed25519 / RSA. No `digestif`, + no `mirage-crypto`, no C stubs, no `Unix` dependency in these primitives. + (None of those libs are even installed — the switch has only + re/unix/yojson/otfm/js_of_ocaml. Pure OCaml is both required and hermetic.) +- **HTTP server is native-only**: it needs sockets/threads. Register it in + `bin/sx_server.ml` via `Sx_primitives.register` (precedent: `eval-in-env` at + `bin/sx_server.ml:721`), **not** in the shared lib. It must never enter the + WASM build. +- **`file-list-dir`** uses `Sys.readdir` (stdlib, WASM-stubbed) — safe in lib, + but the fed-sx server is native anyway; native registration is acceptable too. + +**Every phase must prove the WASM build still links** (`sx_build target="wasm"` +or `bash hosts/ocaml/browser/test_boot.sh`) before its commit. A broken WASM +browser kernel is a hard regression and fails the phase. + +## Primitive surface (what fed-sx Milestone 1 actually needs) + +Mapped to `plans/fed-sx-milestone-1.md` build steps: + +| Primitive (SX name) | Signature | fed-sx step | Host | +|---|---|---|---| +| `crypto-sha256` | `(bytes) -> hex-string` | 1, 2 | lib (pure) | +| `crypto-sha512` | `(bytes) -> hex-string` | 2 | lib (pure) | +| `crypto-sha3-256` | `(bytes) -> hex-string` | 1 (CID default) | lib (pure) | +| `cbor-encode` | `(sx-value) -> bytes` (dag-cbor, deterministic) | 1 | lib (pure) | +| `cbor-decode` | `(bytes) -> sx-value` | 1 (round-trip tests) | lib (pure) | +| `cid-from-bytes` | `(codec multihash-bytes) -> cid-string` | 1 | lib (pure) | +| `cid-from-sx` | `(sx-value) -> cid-string` (canonicalize→cbor→sha→mh→cidv1) | 1 | lib (pure) | +| `ed25519-verify` | `(pubkey-32 msg sig-64) -> bool` | 2 | lib (pure) | +| `rsa-sha256-verify` | `(der-spki msg sig) -> bool` (PKCS#1 v1.5) | 2 | lib (pure) | +| `file-list-dir` | `(path) -> (list string)` | 3 | lib/native | +| `http-listen` | `(port handler-fn) -> never` (handler: req-dict→resp-dict) | 8 | **native only** | + +Deferred (not Milestone 1): `httpc-request` (HTTP client — federation is v2), +`sqlite-*` (Milestone 1 is file-on-disk; sqlite is v2 indexes). + +## Registration pattern (established) + +`lib/sx_primitives.ml`: +```ocaml +register "crypto-sha256" (fun args -> + match args with + | [String s] -> String (Sha2.sha256_hex s) + | _ -> raise (Eval_error "crypto-sha256: (bytes)")) +``` +Errors: `raise (Eval_error "name: shape")`. Byte strings are OCaml `string` +(SX `String`). Lists are `Pair`/`Nil` per `sx_types.ml`. Native-only prims go in +`bin/sx_server.ml` the same way. + +## Phasing — one feature per loop iteration + +Dependency order. Each phase: implement → `dune build` (ocaml) → **WASM build +check** → tests → commit → tick box → Progress-log line → push. + +### Phase A — SHA-2 (sha256 + sha512), pure OCaml ✅ DONE +- New `lib/sx_sha2.ml` (or inline in primitives if small): SHA-256 + SHA-512. +- Primitives `crypto-sha256`, `crypto-sha512` → lowercase hex string. +- Tests (`bin/run_tests.ml` or a dedicated `bin/test_crypto.ml`): NIST vectors — + `""`, `"abc"`, the 896-bit message, a 1MB "a" repetition. + - sha256("") = `e3b0c442…b7852b855`; sha256("abc") = `ba7816bf…f20015ad` + - sha512("abc") = `ddaf35a1…2a9ac94f…` +- **Acceptance:** vectors pass; WASM build links; OCaml conformance unchanged. + +### Phase B — SHA-3 / Keccak-256, pure OCaml ✅ DONE +- Keccak-f[1600] + SHA3-256 padding. Primitive `crypto-sha3-256`. +- Tests: sha3-256("") = `a7ffc6f8…0f8434a`; sha3-256("abc") = `3a985da7…11431532`. +- **Acceptance:** NIST SHA-3 vectors pass; WASM links. + +### Phase C — dag-cbor encoder + decoder, pure OCaml ✅ DONE +- RFC 8949 deterministic subset (RFC 8742 dag-cbor): unsigned/negative ints, + byte strings, text strings, arrays, maps with **keys sorted by + length-then-bytewise**, bool, null, tag 42 (CID link). No floats unless a + fed-sx shape needs them (defer; document). +- SX↔CBOR mapping: `Integer`→int, `String`→text str, `Bool`, `Nil`→null, + `Pair/Nil`→array, `Dict`→map (sorted keys), keyword/symbol→text str. +- Primitives `cbor-encode`, `cbor-decode`. Round-trip property tests + RFC 8949 + appendix-A vectors + a "reordered dict keys → identical bytes" determinism test. +- **Acceptance:** vectors + round-trip + determinism pass; WASM links. + +### Phase D — CID computation, pure OCaml ✅ DONE +- Multihash (sha2-256 = 0x12, sha3-256 = 0x16; varint code + varint len + digest). +- CIDv1 = `0x01 || codec-varint || multihash`. Codecs: dag-cbor 0x71, raw 0x55. +- Multibase base32 lower (`b` prefix, RFC 4648 no-pad). +- Primitives `cid-from-bytes` (codec, raw mh bytes), `cid-from-sx` + (canonicalize → cbor-encode → sha2-256 → multihash → cidv1 → base32). +- Tests: known IPFS CIDs — cross-check against `ipfs` CLI if present, else the + fixed vectors for `{}` dag-cbor and `"abc"` raw (hardcode expected strings). + Determinism: same SX value (whitespace/comment/key-order variants) → same CID. +- **Acceptance:** matches reference CIDs; determinism holds; WASM links. Satisfies + fed-sx Milestone 1 Step 1. + +### Phase E — Ed25519 verify, pure OCaml ✅ DONE +- Curve25519/edwards25519 field arith (mod 2^255-19), point decompress, + SHA-512-based verify per RFC 8032 §5.1.7. (Reuse Phase A sha512.) +- Primitive `ed25519-verify (pubkey msg sig) -> bool`. Bad-length args → false, + not exception (verify is total). +- Tests: RFC 8032 §7.1 vectors (TEST 1-4 + the 1024-byte one). Tampered msg/sig + → false. Wrong-length key → false. +- **Acceptance:** all RFC 8032 vectors pass; WASM links. Satisfies fed-sx Step 2 + (Ed25519 sig-suite). + +### Phase F — RSA-SHA256 verify (PKCS#1 v1.5), pure OCaml ✅ DONE +- Minimal pure-OCaml bignum (only need modexp + DER parse). Parse SPKI DER → + (n, e). RSASSA-PKCS1-v1_5 verify with SHA-256 (Phase A). +- Primitive `rsa-sha256-verify (der-spki msg sig) -> bool`. +- Tests: a generated 2048-bit keypair's signature (vectors hardcoded in the test + from a one-off openssl run, documented in a comment), tamper → false. +- **Acceptance:** vector verifies; tamper fails; WASM links. Satisfies fed-sx + Step 2 (rsa-sha256-2018 sig-suite). **Lower priority** than E — Ed25519 is the + modern default; RSA can land after the HTTP phase if time-boxed. + +### Phase G — `file-list-dir`, native-safe ✅ DONE +- `Sys.readdir` → sorted SX list of names (no `.`/`..`). Errors → `enoent`/ + `enotdir` classified like the existing `file-read` error mapping. +- Tests: list a known dir, missing dir → error, file-not-dir → error. +- **Acceptance:** passes; WASM build still links (Sys.readdir is stubbed there). + Satisfies fed-sx Step 3 segment replay. + +### Phase H — HTTP/1.1 server, **native-only** (`bin/sx_server.ml`) ✅ DONE +- Minimal threaded HTTP/1.1: accept loop (`Unix` + `Thread`), parse request + line + headers + body (Content-Length), build an SX request dict + `{:method :path :query :headers :body}`, call the SX handler callable, take an + SX response dict `{:status :headers :body}`, write it. Connection: close + (keep-alive optional, defer). Bind `127.0.0.1:`. +- Primitive `http-listen (port handler) -> never-returns` registered ONLY in + `bin/sx_server.ml`. Document that it is absent from the WASM kernel. +- Tests: `bin/test_http.sh` — start a server on a port with a tiny SX echo + handler in a subprocess, `curl` GET/POST/404/headers, assert responses, kill. +- **Acceptance:** curl test script green; WASM build untouched (prim not in lib). + Satisfies fed-sx Step 8 transport. + +### Phase I — handoff ✅ DONE +- Flip the `plans/erlang-on-sx.md` Blockers entry "SX runtime lacks platform + primitives …" to **RESOLVED**, listing the exact SX primitive names so the + Erlang loop can one-line-wire its blocked Phase 8 BIFs (`crypto:hash/2`, + `cid:from_bytes/1`, `cid:to_string/1`, `file:list_dir/1`, plus note + `httpc`/`sqlite` still deferred). **Do not edit `lib/erlang/`** — that wiring + is the Erlang loop's job; this phase only updates the blocker text + this + plan's "Handoff" section with the primitive→BIF mapping. +- **Acceptance:** blocker text updated; fed-sx Milestone 1 Steps 1-3 + 8 + prerequisites all green. + +## Scope (hard) + +- **Edit only:** `hosts/ocaml/lib/**`, `hosts/ocaml/bin/**`, this plan file. +- **Do NOT edit:** `lib/erlang/**` (Erlang loop owns BIF wiring), `spec/`, + `lib/` root, other `lib//`, `plans/erlang-on-sx.md` *except* the one + Blockers entry in Phase I. +- **Pure OCaml for lib primitives.** No new opam deps. If a phase seems to need + one, stop and add a Blockers entry instead. +- **Prove WASM every phase.** No commit without `test_boot.sh` (or wasm build) + green. +- **Never push to `main` or `architecture`.** Branch `loops/fed-prims`, push + `origin/loops/fed-prims`. +- One feature per commit. Short factual messages: `fed-prims: SHA-256 + 4 NIST + vectors`. Tick the box, append a dated Progress-log line (newest first). +- **Never call `sx_build` with no timeout-awareness** — OCaml builds are slow; + use the MCP `sx_build target="ocaml"` / `target="wasm"` tools or + `dune build` with a generous timeout. If the build hangs >10min, Blockers + + stop. + +## Build & test reference + +```bash +cd hosts/ocaml && dune build bin/sx_server.exe 2>&1 | tail # native +bash hosts/ocaml/browser/test_boot.sh # WASM links + boots +cd hosts/ocaml && dune exec bin/run_tests.exe 2>&1 | tail # OCaml unit tests +SX_SERVER=hosts/ocaml/_build/default/bin/sx_server.exe \ + timeout 400 bash lib/erlang/conformance.sh 2>&1 | tail -3 # no-regression gate +``` + +A primitive is reachable from SX via the epoch protocol: +```bash +printf '(epoch 1)\n(crypto-sha256 "abc")\n' | \ + hosts/ocaml/_build/default/bin/sx_server.exe +``` + +## Handoff (Phase I fills this in) + +| SX primitive | Erlang Phase 8 BIF it unblocks | +|---|---| +| `crypto-sha256` / `crypto-sha512` / `crypto-sha3-256` | `crypto:hash/2` | +| `cid-from-bytes` / `cid-from-sx` | `cid:from_bytes/1`, `cid:to_string/1` | +| `ed25519-verify` / `rsa-sha256-verify` | `crypto:verify` / sig-suites | +| `file-list-dir` | `file:list_dir/1` | +| `http-listen` | fed-sx kernel `http:listen/2` (Milestone 1 Step 8) | + +**Status: DELIVERED (Phases A–H, 2026-05-18).** All primitives are +registered and reachable from SX (`(eval "(crypto-sha256 \"abc\")")` +via the epoch protocol). Signatures the Erlang loop can one-line-wire: + +- `(crypto-sha256 bytes) -> hex-string` — also `crypto-sha512`, + `crypto-sha3-256`. lib (`Sx_sha2`/`Sx_sha3`), WASM-safe. +- `(cbor-encode value) -> bytes` / `(cbor-decode bytes) -> value` — + deterministic dag-cbor, lib (`Sx_cbor`), WASM-safe. +- `(cid-from-bytes codec mh-bytes) -> cid-string` / + `(cid-from-sx value) -> cid-string` — lib (`Sx_cid`), WASM-safe. +- `(ed25519-verify pk msg sig) -> bool` / + `(rsa-sha256-verify spki msg sig) -> bool` — total (bad input → + false), lib (`Sx_ed25519`/`Sx_rsa`), WASM-safe. +- `(file-list-dir path) -> (list string)` — sorted, lib, WASM-stubbed. +- `(http-listen port handler) -> never` — **NATIVE ONLY** + (`bin/sx_server.ml`); absent from the WASM kernel by design. + +Still **deferred** (not Milestone 1, not provided here): `httpc-request` +(HTTP client / federation v2), `sqlite-*` (v2 indexes). The Erlang loop +should leave `httpc`/`sqlite` BIFs blocked with that note. + +## Progress log + +_Newest first._ + +- 2026-05-18 — Phase I: handoff. `erlang-on-sx.md` Blockers gained one + RESOLVED entry (no "SX runtime lacks…" entry pre-existed; it read + "_(none yet)_") mapping every delivered primitive → its Phase 8 BIF, + with httpc/sqlite explicitly left deferred. Handoff section here + filled with signatures + native/WASM notes. Doc-only (no lib/erlang/ + edits); Erlang 530/530 unchanged. **fed-sx Milestone 1 Steps 1-3 + 8 + prerequisites all green — plan complete (Phases A–I done).** +- 2026-05-18 — Phase H: `http-listen` primitive in `bin/sx_server.ml` + (NATIVE ONLY — Unix sockets + Thread per connection, Mutex around + the shared-runtime handler call; HTTP/1.1, Connection: close; + req {:method :path :query :headers :body} → resp {:status :headers + :body}). Test `bin/test_http.sh`: curl GET+query / POST+body / 404 + / custom header — 6/6. NOT in lib, so WASM kernel untouched (boot + green); run_tests 4897 unchanged; Erlang 530/530. Satisfies fed-sx + Milestone 1 Step 8 transport. +- 2026-05-18 — Phase G: `file-list-dir` primitive in + `lib/sx_primitives.ml` (Sys.readdir → sorted names, no "."/".."; + Sys_error prefixed like file-read, msg carries enoent/enotdir). + 4 tests: sorted listing, missing dir, not-a-dir, arity. WASM boot + green (Sys.readdir stubbed there); Erlang 530/530; run_tests +4. + Satisfies fed-sx Step 3 segment replay. +- 2026-05-18 — Phase F: pure-OCaml `lib/sx_rsa.ml` (self-contained + bignum modexp, minimal DER SPKI reader, RFC 8017 §8.2.2 PKCS#1 + v1.5 verify with SHA-256 DigestInfo prefix). Primitive + `rsa-sha256-verify` total. 5 tests on a fixed RSA-2048 vector + (one-off python-cryptography keygen, hardcoded): valid, tampered + msg/sig, garbage SPKI, non-string. WASM boot green with new lib + module; Erlang 530/530; run_tests +5. Satisfies fed-sx Step 2 + (rsa-sha256-2018 sig-suite). +- 2026-05-18 — Phase E: pure-OCaml `lib/sx_ed25519.ml` (minimal + base-2^26 bignum, edwards25519 extended-coord points, RFC 8032 + §5.1.7 cofactorless verify reusing Phase-A sha512). Primitive + `ed25519-verify` is total (bad/short/non-string args → false). + 8 tests: RFC 8032 §7.1 TEST 1-3 (re-derived independently via + python-cryptography), tampered msg/sig, wrong-length, non-string. + WASM boot green with new lib module; Erlang 530/530; run_tests +8. + Satisfies fed-sx Milestone 1 Step 2 (Ed25519 sig-suite). +- 2026-05-18 — Phase D: pure-OCaml `lib/sx_cid.ml` (unsigned-varint, + multihash, CIDv1, multibase base32-lower), primitives `cid-from-bytes` + / `cid-from-sx` (cbor→sha2-256→mh→cidv1, dag-cbor codec 0x71). 5 tests: + raw "abc"=bafkreif2pall7d…, raw ""=bafkreihdwdcefg…, dag-cbor {}= + bafyreigbtj4x7i… (all match canonical IPFS CIDs; no `ipfs` CLI so + vectors independently derived in Python), key-order determinism. WASM + boot green with new lib module; Erlang 530/530; run_tests +5. +- 2026-05-18 — Phase C: pure-OCaml `lib/sx_cbor.ml` (dag-cbor encode/ + decode), primitives `cbor-encode`/`cbor-decode`. RFC 8949 Appendix-A + vectors, length-then-bytewise key sort + order-invariance determinism, + decode∘encode round-trip (30 tests). Floats unsupported (raise, no + fed-sx shape needs them); tag-42 decode = inner-item passthrough. + WASM boot green with new lib module; Erlang 530/530; run_tests +30. +- 2026-05-18 — Phase B: pure-OCaml `lib/sx_sha3.ml` (Keccak-f[1600] + + SHA-3 pad, domain 0x06), primitive `crypto-sha3-256`. 4 NIST FIPS 202 + vectors pass (empty/abc/896-bit + 1600-bit 0xa3 multi-block). WASM boot + green with new lib module; Erlang conformance 530/530; run_tests +4. +- 2026-05-18 — Phase A: pure-OCaml `lib/sx_sha2.ml` (SHA-256 + SHA-512), + primitives `crypto-sha256`/`crypto-sha512`. 7 NIST FIPS 180-4 vectors pass + (empty/abc/896-bit/1M-'a' for sha256; empty/abc/896-bit for sha512). WASM + boot green with new lib module; Erlang conformance 530/530 unchanged. + +## Blockers + +- _(none yet)_