From d0bd66c704a309c1823801125eb0ab51e7a7f8c3 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 8 Sep 2026 16:32:40 +0200 Subject: [PATCH] feat(crypto): RSA signature verification (rv2 9 phase D part 1, PKCS1 + PSS) - 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) --- runtime/src/crypto.c | 171 +++++++++++++++++++++++++++++++++++++ runtime/src/crypto.h | 10 +++ runtime/test/test_crypto.c | 23 +++++ 3 files changed, 204 insertions(+) diff --git a/runtime/src/crypto.c b/runtime/src/crypto.c index da21913..c71f193 100644 --- a/runtime/src/crypto.c +++ b/runtime/src/crypto.c @@ -1062,6 +1062,177 @@ void wo_x25519(uint8_t out[32], const uint8_t scalar[32], fcontract(out, x); } +/* ---- RSA signature verification (rv2 9 phase D, part 1) ----------------- + * PKCS#1 v1.5 and PSS over SHA-256, for the server certificate chain and the + * TLS 1.3 CertificateVerify. Verification touches ONLY public data (public key + * + signature), so it needs no constant-time discipline — a plain bignum + * modexp with the small public exponent. Montgomery multiplication (CIOS, + * 64-bit limbs, 128-bit products). Internal C; consumer is the TLS handshake. + * Vectors: RSA-2048 PKCS1v15 + PSS in test_crypto.c. */ +#define RSA_MAXW 66 /* up to ~4224-bit modulus */ + +static int bn_from_be(uint64_t *w, const uint8_t *b, size_t len) { + int k = (int)((len + 7) / 8); + if (k > RSA_MAXW || k == 0) return -1; + for (int i = 0; i < k; i++) w[i] = 0; + for (size_t i = 0; i < len; i++) + w[i / 8] |= (uint64_t)b[len - 1 - i] << (8 * (i % 8)); + return k; +} +static void bn_to_be(uint8_t *b, size_t len, const uint64_t *w, int k) { + for (size_t i = 0; i < len; i++) + b[len - 1 - i] = (i / 8 < (size_t)k) ? (uint8_t)(w[i / 8] >> (8 * (i % 8))) : 0; +} +static int bn_ge(const uint64_t *a, const uint64_t *b, int k) { + for (int i = k - 1; i >= 0; i--) { if (a[i] > b[i]) return 1; if (a[i] < b[i]) return 0; } + return 1; +} +static void bn_sub(uint64_t *out, const uint64_t *a, const uint64_t *b, int k) { + u128 borrow = 0; + for (int i = 0; i < k; i++) { + u128 d = (u128)a[i] - b[i] - borrow; + out[i] = (uint64_t)d; + borrow = (uint64_t)(d >> 64) & 1; + } +} +static uint64_t bn_shl1(uint64_t *a, int k) { + uint64_t carry = 0; + for (int i = 0; i < k; i++) { uint64_t nc = a[i] >> 63; a[i] = (a[i] << 1) | carry; carry = nc; } + return carry; +} +static uint64_t inv64(uint64_t a) { /* a odd: a^-1 mod 2^64, Newton */ + uint64_t x = a; + for (int i = 0; i < 5; i++) x *= 2 - a * x; + return x; +} +static void mont_mul(uint64_t *out, const uint64_t *a, const uint64_t *b, + const uint64_t *m, uint64_t n0, int k) { + uint64_t t[RSA_MAXW + 2]; + for (int i = 0; i < k + 2; i++) t[i] = 0; + for (int i = 0; i < k; i++) { + u128 carry = 0; + for (int j = 0; j < k; j++) { + u128 s = (u128)a[i] * b[j] + t[j] + carry; + t[j] = (uint64_t)s; carry = s >> 64; + } + u128 s = (u128)t[k] + carry; t[k] = (uint64_t)s; t[k + 1] += (uint64_t)(s >> 64); + uint64_t mp = (uint64_t)((u128)t[0] * n0); + carry = ((u128)mp * m[0] + t[0]) >> 64; + for (int j = 1; j < k; j++) { + u128 s2 = (u128)mp * m[j] + t[j] + carry; + t[j - 1] = (uint64_t)s2; carry = s2 >> 64; + } + u128 s3 = (u128)t[k] + carry; t[k - 1] = (uint64_t)s3; carry = s3 >> 64; + t[k] = t[k + 1] + (uint64_t)carry; t[k + 1] = 0; + } + if (t[k] || bn_ge(t, m, k)) bn_sub(t, t, m, k); + for (int i = 0; i < k; i++) out[i] = t[i]; +} +/* out = base^e mod m (e big-endian bytes, public exponent). */ +static void bn_modexp(uint64_t *out, const uint64_t *base, const uint64_t *m, + int k, const uint8_t *e, size_t elen) { + uint64_t n0 = 0 - inv64(m[0]); + uint64_t rsq[RSA_MAXW], aR[RSA_MAXW], x[RSA_MAXW], one[RSA_MAXW], tmp[RSA_MAXW]; + for (int i = 0; i < k; i++) { rsq[i] = 0; one[i] = 0; } + rsq[0] = 1; one[0] = 1; + for (int i = 0; i < 128 * k; i++) { /* rsq = 2^(128k) mod m */ + uint64_t of = bn_shl1(rsq, k); + if (of || bn_ge(rsq, m, k)) bn_sub(rsq, rsq, m, k); + } + mont_mul(aR, base, rsq, m, n0, k); /* base -> Montgomery */ + mont_mul(x, one, rsq, m, n0, k); /* x = R mod m (== 1 in Montgomery) */ + for (size_t bi = 0; bi < elen * 8; bi++) { + uint8_t bit = (e[bi / 8] >> (7 - (bi % 8))) & 1; + mont_mul(tmp, x, x, m, n0, k); + for (int i = 0; i < k; i++) x[i] = tmp[i]; + if (bit) { mont_mul(tmp, x, aR, m, n0, k); for (int i = 0; i < k; i++) x[i] = tmp[i]; } + } + mont_mul(out, x, one, m, n0, k); /* out of Montgomery */ +} + +static void mgf1_sha256(const uint8_t *seed, size_t seedlen, uint8_t *mask, + size_t masklen) { + size_t done = 0; + uint32_t counter = 0; + uint8_t in[96]; + while (done < masklen) { + memcpy(in, seed, seedlen); + in[seedlen] = (uint8_t)(counter >> 24); in[seedlen + 1] = (uint8_t)(counter >> 16); + in[seedlen + 2] = (uint8_t)(counter >> 8); in[seedlen + 3] = (uint8_t)counter; + uint8_t d[32]; + wo_sha256(in, seedlen + 4, d); + size_t n = masklen - done < 32 ? masklen - done : 32; + memcpy(mask + done, d, n); + done += n; counter++; + } +} + +/* Common front: s^e mod n into em[nlen]. 0 ok, -1 malformed. */ +static int rsa_recover(const uint8_t *n, size_t nlen, const uint8_t *e, + size_t elen, const uint8_t *sig, size_t siglen, + uint8_t *em) { + if (siglen != nlen) return -1; + uint64_t N[RSA_MAXW], S[RSA_MAXW], EM[RSA_MAXW]; + int k = bn_from_be(N, n, nlen); + int ks = bn_from_be(S, sig, siglen); + if (k < 0 || ks < 0 || (N[0] & 1) == 0) return -1; + for (int i = ks; i < k; i++) S[i] = 0; + if (bn_ge(S, N, k)) return -1; + bn_modexp(EM, S, N, k, e, elen); + bn_to_be(em, nlen, EM, k); + return 0; +} + +int wo_rsa_pkcs1_sha256_verify(const uint8_t *n, size_t nlen, const uint8_t *e, + size_t elen, const uint8_t *sig, size_t siglen, + const uint8_t hash[32]) { + static const uint8_t di[] = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, + 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 0x01, 0x05, 0x00, 0x04, 0x20 }; + uint8_t em[RSA_MAXW * 8]; + if (nlen > sizeof(em)) return 0; + if (rsa_recover(n, nlen, e, elen, sig, siglen, em) != 0) return 0; + size_t tlen = sizeof(di) + 32; + if (nlen < tlen + 11) return 0; + size_t pslen = nlen - tlen - 3; + if (em[0] != 0x00 || em[1] != 0x01) return 0; + for (size_t i = 0; i < pslen; i++) if (em[2 + i] != 0xff) return 0; + if (em[2 + pslen] != 0x00) return 0; + if (memcmp(em + 3 + pslen, di, sizeof(di)) != 0) return 0; + if (memcmp(em + 3 + pslen + sizeof(di), hash, 32) != 0) return 0; + return 1; +} + +/* EMSA-PSS verify, SHA-256, assuming a full-length modulus (emBits = + * 8*nlen-1 — true for standard RSA-2048/3072/4096 keys). */ +int wo_rsa_pss_sha256_verify(const uint8_t *n, size_t nlen, const uint8_t *e, + size_t elen, const uint8_t *sig, size_t siglen, + const uint8_t mhash[32], size_t saltlen) { + uint8_t em[RSA_MAXW * 8]; + if (nlen > sizeof(em) || saltlen > 64) return 0; + if (rsa_recover(n, nlen, e, elen, sig, siglen, em) != 0) return 0; + size_t hLen = 32, emLen = nlen; + if (emLen < hLen + saltlen + 2) return 0; + if (em[emLen - 1] != 0xbc) return 0; + if (em[0] & 0x80) return 0; /* the one top bit (8*emLen-1 emBits) must be 0 */ + size_t dbLen = emLen - hLen - 1; + const uint8_t *H = em + dbLen; + uint8_t db[RSA_MAXW * 8]; + mgf1_sha256(H, hLen, db, dbLen); + for (size_t i = 0; i < dbLen; i++) db[i] ^= em[i]; + db[0] &= 0x7f; + size_t i = 0; + while (i < dbLen - saltlen - 1 && db[i] == 0) i++; + if (i != dbLen - saltlen - 1 || db[i] != 0x01) return 0; + const uint8_t *salt = db + dbLen - saltlen; + uint8_t mp[8 + 32 + 64], hp[32]; + memset(mp, 0, 8); + memcpy(mp + 8, mhash, 32); + memcpy(mp + 40, salt, saltlen); + wo_sha256(mp, 8 + 32 + saltlen, hp); + return memcmp(hp, H, 32) == 0 ? 1 : 0; +} + /* The VM half: Bytes in, fresh Bytes out. Wrong class id traps * WO_T_BOUNDS with the Bytes builtins' message shape. */ static const wo_str *arg_bytes(uint64_t r, const char **msg) { diff --git a/runtime/src/crypto.h b/runtime/src/crypto.h index f2cabeb..8821180 100644 --- a/runtime/src/crypto.h +++ b/runtime/src/crypto.h @@ -53,6 +53,16 @@ int wo_hkdf_sha256_expand_label(const uint8_t secret[32], const char *label, void wo_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32]); +/* RSA signature verification (rv2 9 phase D, SHA-256). Public-key only, so + * not constant-time by design. n/e/sig big-endian; hash is the 32-byte digest. + * Returns 1 on a valid signature, 0 otherwise. */ +int wo_rsa_pkcs1_sha256_verify(const uint8_t *n, size_t nlen, const uint8_t *e, + size_t elen, const uint8_t *sig, size_t siglen, + const uint8_t hash[32]); +int wo_rsa_pss_sha256_verify(const uint8_t *n, size_t nlen, const uint8_t *e, + size_t elen, const uint8_t *sig, size_t siglen, + const uint8_t mhash[32], size_t saltlen); + int wo_builtin_crypto(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg); #endif diff --git a/runtime/test/test_crypto.c b/runtime/test/test_crypto.c index e53a5f8..4caa293 100644 --- a/runtime/test/test_crypto.c +++ b/runtime/test/test_crypto.c @@ -325,5 +325,28 @@ int main(void) { "684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51") == 0); } + /* RSA-2048 signature verification (rv2 9 phase D) — vectors from + * python cryptography (PKCS#1 v1.5 and PSS over SHA-256). */ + { + static const char *N = "dacbb5ff5cc8c341b7027135cca2e76619abf23c4a043b25d1fe424eae008923a007836775669bf783683f6ef4ed4f25d492078a4557e737f7ec680131f7f5ab0bcc7e11554edeefbb2b5d2c8c2deb6af6e2054a3b75f580b2a1d16186b12fb5c1715991e9bb5d2646631b4bd14484157f5b300c61f902f74727813b5d0e5b872bbf7ea72e0f7b80d47bb5049f1f54cca186b9d1055767be79403a9e0796c45e17dd00de5cca8ee2e315525267f0369fdca6d18a0c06b651de59913d6a7d227400e12cd8e0b67121cf26dad92073bfa67b8d9b0016ae9dd2de058baa07f0bcde7bfa7786aafe7ff82046f71f48960ce81a68ff62bdd4658cca3837dfb1ed1b19"; + static const char *E = "010001"; + static const char *H = "1e1f1eb2f15f5ba5f16363e4c45d0e58ee171e7050bb088dd5125d1f536afe25"; + static const char *S1 = "a7bfe20510abd104f6eae7c440a1851c6f7cbd15266f671eff6096fa22b5cd61bb45cdaa819b39a25a20d08e019391282f00dd4ad4d02dbfc3da6e12930dbced8e0cbd65004b8955ae8f7eb8bf9fed477f6502e2a0e523665295ceb212155499dac4f40cca9c5038920678afda12f8f0591be4c7a3167efa0e30566dc207bbde47ca52e061ed7c557d37899698d9c5947b4ddc90286e50ca2d57114371307b50bf603759fd592b8e815398889ab6664b898126d56171acee58b1cd3130b0f2dcc85d0f0ccdcf586914bdb8a53a2985095206cb5bed3712531438b5b9b2861ac25819549b6bc6a7f7b19557c7825563b38e302996d317d4e5f9c6dc6356460268"; + static const char *SP = "9c66f584c7781cc0a599585a01ef2d892eba67005e353e51ff677e3e64b3d45543703118872d76bebf6e17a1b0b8a6a08186ef2bde6f4f9b264952c62cde8c1ea2ab2635fd022f5b0d358e98835871e4212112a47445796e87c0d7df4f674c9ed726a90f92bfe72c99b8015a786e08f3176b296c70c8bb815bfd32869a795a9d8b046416d145eb476ea6a02ecac046f7f8da4365e047cd2ea1e5da78fb76ca8c5f762db1136599e423beb864a24be7f6344aeb51e1973fe6885d2d1de378cc3ffa3e5a3cbcd95331397c4650c792f3003bffb5418d2df833298c6a6f2dcd86e07e5684a46ab25c3ba6cb712fecce77750a1a2e8baecf41455b837569423a3a71"; + uint8_t n[256], e[8], h[32], s[256]; + size_t nl = unhex(N, n), el = unhex(E, e); + unhex(H, h); + size_t sl = unhex(S1, s); + T_CHECK(wo_rsa_pkcs1_sha256_verify(n, nl, e, el, s, sl, h) == 1); + s[10] ^= 0x01; /* tamper */ + T_CHECK(wo_rsa_pkcs1_sha256_verify(n, nl, e, el, s, sl, h) == 0); + sl = unhex(SP, s); + T_CHECK(wo_rsa_pss_sha256_verify(n, nl, e, el, s, sl, h, 32) == 1); + s[10] ^= 0x01; + T_CHECK(wo_rsa_pss_sha256_verify(n, nl, e, el, s, sl, h, 32) == 0); + unhex(H, h); h[0] ^= 0x01; sl = unhex(S1, s); /* wrong hash rejected */ + T_CHECK(wo_rsa_pkcs1_sha256_verify(n, nl, e, el, s, sl, h) == 0); + } + return t_report("test_crypto"); }