- wo_pkey_parse: PKCS#8 PrivateKeyInfo (wrapping PKCS#1/SEC1), bare PKCS#1
RSAPrivateKey, and bare SEC1 ECPrivateKey -> RSA (n,d) or the EC P-256
32-byte scalar. Reuses the X.509 DER reader; spans point into the buffer
- KAT: all three formats parse, and the extracted key signs a hash our
verify accepts (RSA-PSS + ECDSA); garbage rejected. test_crypto 130,
ASan/UBSan clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 819d67226a94c4cd5a765ec403e57466c64f1e65)
- wo_ecdsa_p256_sha256_sign: deterministic nonce (RFC 6979 HMAC-DRBG over
the key + message — no RNG, no nonce-reuse/bias risk), then r = (k*G).x
mod n and s = k^-1 (z + r*d) mod n
- constant-time in the secret: jmul_ct (double-and-add-always + point
cmov) for k*G, and bn_modexp_ct for k^-1 mod n and the affine inversion.
Known residual (documented): the ladder leaks k's leading-zero count (a
bit-length hint, not the key) — a complete-formula/Montgomery-ladder
upgrade is the named follow-up
- KAT: byte-for-byte vs the RFC 6979 A.2.5 P-256/SHA-256 vectors ("sample"
+ "test"), our sign verifies with our verify, determinism checked.
test_crypto 115, ASan/UBSan clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 1bc6d04f9e18180dc7d0a6e5e7021dbd588e499a)
- bn_modexp_ct: constant-time modexp for the SECRET exponent — squares and
multiplies every bit, selects the product with a mask (bn_cmov), so the
op sequence is independent of d (the existing bn_modexp branches on the
bit, fine only for the public e)
- wo_rsa_pss_sha256_sign: EMSA-PSS-ENCODE (RFC 8017 §9.1.1) + modexp with d;
caller supplies the salt (fresh in production; fixed makes the KAT
deterministic). Private key (n,d)
- KAT: deterministic sign vs a python from-spec oracle byte-for-byte
(fixed salt), our sign round-trips through our verify, tamper rejected.
test_crypto 108, ASan/UBSan clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit cf8fdfcc47b2b076b63b9c63bf56411615b0d6f9)
- crypto.c: x509_find_ext (generic extension walker) + wo_x509_basic_constraints
(cA / pathLenConstraint, absent => not a CA) + wo_x509_eku_serverauth_ok
(EKU absent, serverAuth, or anyEKU => usable; else not)
- wo_tls_verify_chain enforces decision 6: the leaf must be server-usable
(EKU), every server-sent issuer and the signing anchor must be a CA
(basicConstraints CA:TRUE) with a pathLenConstraint covering the
intermediates below it — stops a leaf masquerading as a CA
- gen_x509.py extended (folds in the wildcard leaf, adds EKU clientAuth-only,
EKU serverAuth, a non-CA intermediate + a leaf issued under it); vectors
regenerated
- KATs: extractors (test_crypto 104) + chain enforcement (test_tls 103) —
EKU serverAuth accepted, clientAuth-only rejected, leaf-under-non-CA
rejected though every signature verifies; existing chains still pass.
ASan/UBSan clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 3811418014c2c8d9bc3a9464256f52104261b1b9)
- wo_x509_check_host: match a hostname against the cert subjectAltName
dNSNames (RFC 6125) — case-insensitive, single left-most wildcard that
covers exactly one label; no SAN => refused; no legacy CN fallback.
Completes the phase-E deferred hostname check (walks the [3] extensions)
- wo_tls_client_set_host + driver enforcement: with a host set, a leaf
whose SAN does not match is refused at the Certificate step (MITM
defense); unset skips the check (offline testing only, documented unsafe)
- KAT: exact/case-insensitive/mismatch, no-SAN refused, wildcard one-label
(not zero, not sub-label) via a wildcard-SAN cert; driver refuses the
RFC 8448 leaf (no SAN) once a host is set. test_crypto 95, test_tls 91,
ASan/UBSan clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 319ce8bfcf608030f958b36ab2c8f62fd76e1740)
- defensive ASN.1/DER reader: every length/bound checked; malformation
is rejection, never over-read (truncated input KAT-gated)
- x509_parse: tbsCertificate span, sig-alg OID, signature,
SubjectPublicKeyInfo (RSA n/e or EC P-256 x/y), validity dates
- wo_x509_verify_one: one chain link's signature, dispatching to
phase-D RSA-PKCS1/PSS + ECDSA-P256 by the issuer key type
- wo_x509_parse_spki + wo_x509_check_validity (caller supplies time)
- KAT against real python-generated chains (test/gen_x509.py):
RSA CA+leaf (SHA256withRSA), EC P-256 CA+leaf (ecdsa-with-SHA256);
leaf-vs-CA, self-signed CA, wrong-issuer/tampered/truncated reject,
validity window, SPKI extraction. test_crypto 84 pass, ASan/UBSan clean
- deferred to phase F: SAN/hostname match + multi-cert chain walk to a
system CA bundle (both need the target host / trust store, known at
handshake time)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 4ec1c75f889df3612e31b9a1a77c4c927fb546c1)
- wo_ecdsa_p256_sha256_verify: NIST P-256 signature verify for EC-cert chains
and TLS 1.3 CertificateVerify
- Jacobian point arithmetic (double a=-3, general add with the H==0 special
cases), double-and-add scalar mult; field/scalar arithmetic reuses the
bignum Montgomery multiply and modexp (Fermat inverses mod p and mod n)
- validates r,s in [1,n-1] and that Q is on the curve (invalid-curve guard)
- verify-only public data -> not constant-time by design
- renamed the P-256 field mul to fpmul to avoid the clash with X25519's fmul
- VERIFIED against a python ECDSA-P256 vector; tamper + wrong-hash rejected;
test_crypto 69/0; ASan/UBSan clean; battery green
- phase D COMPLETE (RSA PKCS1+PSS + ECDSA-P256). Next E: ASN.1/X.509
(cherry picked from commit 92c996ba96d785d098c173d4ac6ace9052ef6a2f)
- wo_rsa_pkcs1_sha256_verify + wo_rsa_pss_sha256_verify (SHA-256), for the
server cert chain and TLS 1.3 CertificateVerify
- bignum: Montgomery multiply (CIOS, 64-bit limbs, __int128), modexp with the
public exponent (R^2 via 128k modular doublings, no division); MGF1-SHA256
- verification is public data only -> NOT constant-time by design (correct and
much simpler than a private-key op)
- assumes a full-length modulus for PSS emBits (standard RSA-2048/3072/4096)
- VERIFIED against python cryptography RSA-2048 vectors (PKCS#1 v1.5 + PSS,
salt 32); tamper + wrong-hash rejected; test_crypto 66/0; ASan/UBSan clean;
battery green
- internal C, no builtin/compiler change. Remaining in D: ECDSA-P256 (D2)
(cherry picked from commit 9118177fbfd03eff9757defea6931afdd68830c4)
- wo_x25519: constant-time Montgomery ladder + mask-based conditional swap,
radix-2^51 field arithmetic with __int128 products (curve25519-donna-c64,
public domain); scalar clamped, u-coord high bit masked per RFC 7748
- internal C (consumer is the TLS ECDHE handshake); no builtin/compiler change
- KAT-gated in test_crypto: RFC 7748 §5.2 both direct vectors AND the
1000-iteration base-point test; test_crypto 61/0; ASan/UBSan clean; battery green
- fixed one transcription bug found via the KAT: crecip needs 5 final squarings
(p-2 = 2^255-21 = (2^250-1)*2^5 + 11), not 3
- rv2 9 ladder: A (AEAD) + B (HKDF) + C (X25519) done; next D signatures/RSA
(cherry picked from commit f41b1c5f56caa841d1904382830baff0f75525d9)
- wo_hkdf_sha256_extract/expand (RFC 5869) + expand_label (RFC 8446 §7.1),
internal C over the existing hmac_sha256; SHA-256 (mandatory-suite hash;
SHA-384 a later add for the AES-256 suite)
- no builtin, no compiler change -- no .wo consumer yet (the TLS handshake
is the consumer); exposed for the C unit test
- KAT-gated in test_crypto: RFC 5869 Test Case 1 (PRK + 42-byte OKM) and
three HKDF-Expand-Label vectors (key/iv/derived-secret shape); 57/0,
ASan/UBSan clean; runtime battery green
- rv2 9 ladder: A (AEAD, = rv2 8) and B (HKDF) now done; next C X25519
(cherry picked from commit c8d27b6c89a80cd97a996ff7d8b64ff4895e4b26)
- no-intrinsics AES: S-box = GF(2^8) inverse via a fixed-exponent power ladder
(constant-time in the input, no tables), constant-time gf8_mul, byte-oriented
ShiftRows/MixColumns/key-expansion (AES-128 and AES-256)
- constant-time GHASH: bit-by-bit GF(2^128) multiply (mask-driven, no tables)
- aes_gcm_seal/open now dispatch: AES-NI path when present (and not forced
software), else this portable fallback -> AES-GCM works on ANY CPU, so the
phase-B no-AES-NI trap is retired
- wo_aes_force_software test hook; both hw and sw paths verified against NIST
SP 800-38D cases 4 (AES-128) and 16 (AES-256) byte-for-byte; test_crypto 48/0;
ASan/UBSan clean; full runtime battery green
- ARMv8 crypto-extension hardware path deferred (untestable on x86-64 host)
(cherry picked from commit dccf650899798401a9adac8489f34c85ed9304af)