(** 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