From 631506d36feabafc6eb354f365a9e365af60bdbf Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Wed, 9 Sep 2026 03:06:20 +0200 Subject: [PATCH] feat(crypto): constant-time RSA-PSS signing (rv2 9 phase G1a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 (cherry picked from commit cf8fdfcc47b2b076b63b9c63bf56411615b0d6f9) --- runtime/src/crypto.c | 79 ++++++++++++++++++++++++++++ runtime/src/crypto.h | 8 +++ runtime/test/rsa_sign_vectors.h | 93 +++++++++++++++++++++++++++++++++ runtime/test/test_crypto.c | 17 ++++++ 4 files changed, 197 insertions(+) create mode 100644 runtime/test/rsa_sign_vectors.h diff --git a/runtime/src/crypto.c b/runtime/src/crypto.c index 2f1a2fb..61ab407 100644 --- a/runtime/src/crypto.c +++ b/runtime/src/crypto.c @@ -1183,6 +1183,85 @@ static int rsa_recover(const uint8_t *n, size_t nlen, const uint8_t *e, return 0; } +/* ---- RSA private-key ops (rv2 9 phase G1: signing) ----------------------- + * Signing touches the SECRET exponent, so the modexp must be constant-time. + * bn_modexp above branches on the exponent bit (fine for the public e); this + * one squares AND multiplies every bit and selects the result with a mask, so + * the operation sequence is independent of d. */ + +/* Constant-time conditional move: dst = mask ? src : dst (mask all-ones/zero). */ +static void bn_cmov(uint64_t *dst, const uint64_t *src, uint64_t mask, int k) { + for (int i = 0; i < k; i++) dst[i] = (dst[i] & ~mask) | (src[i] & mask); +} +/* out = base^e mod m, constant-time in e (the secret exponent). */ +static void bn_modexp_ct(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]; + uint64_t sq[RSA_MAXW], prod[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++) { + 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); + mont_mul(x, one, rsq, m, n0, k); /* x = R (Montgomery 1) */ + for (size_t bi = 0; bi < elen * 8; bi++) { + uint8_t bit = (e[bi / 8] >> (7 - (bi % 8))) & 1; + mont_mul(sq, x, x, m, n0, k); /* x = x^2 */ + for (int i = 0; i < k; i++) x[i] = sq[i]; + mont_mul(prod, x, aR, m, n0, k); /* always compute x*base ... */ + bn_cmov(x, prod, (uint64_t)0 - (uint64_t)bit, k); /* ... select on bit */ + } + mont_mul(out, x, one, m, n0, k); +} + +/* RSA-PSS sign over SHA-256 (RFC 8017 §9.1.1 / §8.1.1). The 32-byte message + * hash and the salt are inputs — the caller supplies fresh salt (a fixed salt + * makes the KAT deterministic). Private key is (n, d), both big-endian. Writes + * nlen signature bytes. Requires a top-bit-set (full-length) modulus. 0 ok, + * -1 on a bad size. */ +int wo_rsa_pss_sha256_sign(const uint8_t *n, size_t nlen, const uint8_t *d, + size_t dlen, const uint8_t mhash[32], + const uint8_t *salt, size_t saltlen, uint8_t *out) { + const size_t hLen = 32, emLen = nlen; + if (nlen == 0 || (n[0] & 0x80) == 0) return -1; /* need modBits = 8*nlen */ + if (saltlen + hLen + 2 > emLen) return -1; + + /* H = SHA256(0x00*8 || mHash || salt) */ + uint8_t mp[8 + 32 + 64]; + if (saltlen > 64) return -1; + memset(mp, 0, 8); + memcpy(mp + 8, mhash, 32); + memcpy(mp + 40, salt, saltlen); + uint8_t H[32]; + wo_sha256(mp, 8 + 32 + saltlen, H); + + /* EM = maskedDB || H || 0xbc, DB = PS || 0x01 || salt */ + uint8_t em[RSA_MAXW * 8]; + size_t dblen = emLen - hLen - 1; + memset(em, 0, dblen); + em[dblen - saltlen - 1] = 0x01; + memcpy(em + dblen - saltlen, salt, saltlen); + uint8_t dbmask[RSA_MAXW * 8]; + mgf1_sha256(H, hLen, dbmask, dblen); + for (size_t i = 0; i < dblen; i++) em[i] ^= dbmask[i]; + em[0] &= 0x7f; /* clear the top bit */ + memcpy(em + dblen, H, hLen); + em[emLen - 1] = 0xbc; + + /* signature = EM^d mod n */ + uint64_t N[RSA_MAXW], M[RSA_MAXW], SIG[RSA_MAXW]; + int k = bn_from_be(N, n, nlen); + if (k < 0 || (N[0] & 1) == 0) return -1; + if (bn_from_be(M, em, emLen) < 0) return -1; + if (bn_ge(M, N, k)) return -1; + bn_modexp_ct(SIG, M, N, k, d, dlen); + bn_to_be(out, nlen, SIG, 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]) { diff --git a/runtime/src/crypto.h b/runtime/src/crypto.h index 36ad781..213db83 100644 --- a/runtime/src/crypto.h +++ b/runtime/src/crypto.h @@ -63,6 +63,14 @@ 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); +/* RSA-PSS SIGN over SHA-256 (rv2 9 phase G1). Private key (n, d) big-endian; + * caller supplies the salt (fresh in production, fixed for a KAT). Writes nlen + * signature bytes. The modexp with the secret d is constant-time. 0 ok, -1 on + * a bad size. */ +int wo_rsa_pss_sha256_sign(const uint8_t *n, size_t nlen, const uint8_t *d, + size_t dlen, const uint8_t mhash[32], + const uint8_t *salt, size_t saltlen, uint8_t *out); + /* ECDSA-P256 verify (rv2 9 phase D). Public key (qx,qy) affine, signature * (r,s), 32-byte SHA-256 hash; all big-endian. 1 valid, 0 otherwise. */ int wo_ecdsa_p256_sha256_verify(const uint8_t qx[32], const uint8_t qy[32], diff --git a/runtime/test/rsa_sign_vectors.h b/runtime/test/rsa_sign_vectors.h new file mode 100644 index 0000000..d3dc7eb --- /dev/null +++ b/runtime/test/rsa_sign_vectors.h @@ -0,0 +1,93 @@ +/* Generated: RSA-2048 PSS-SHA256 sign KAT (fixed salt -> deterministic). + * n/e/d, mhash, salt, and the from-spec expected signature. */ +static const unsigned char rsasig_n[] = { + 0xb7,0x83,0x5a,0x15,0x85,0x7d,0xca,0xe6,0x60,0x90,0xa2,0xc8, + 0x51,0x55,0x5c,0x06,0xac,0x30,0x7d,0xd4,0x23,0x5b,0x79,0x0c, + 0xd1,0x32,0x8b,0x21,0x91,0x7e,0x44,0xb4,0xfa,0x92,0x20,0x4b, + 0xfb,0x68,0xa8,0x95,0x12,0xea,0x14,0xa8,0xfa,0xdd,0x93,0x5f, + 0x66,0x25,0xc7,0xdf,0xbb,0x36,0xe4,0xc2,0xc2,0x85,0x93,0xa8, + 0xdd,0x91,0x57,0x80,0x3d,0x26,0x0d,0x83,0xe7,0x1f,0x24,0xd9, + 0x0e,0xd6,0x15,0x32,0x4a,0x1c,0x59,0xd3,0xf9,0x42,0x65,0xdd, + 0x1d,0x2e,0xf8,0x31,0x92,0xac,0xa3,0xa2,0xa2,0xa9,0x29,0xf3, + 0x8b,0xc7,0x89,0x4f,0x48,0x88,0xac,0x68,0xc9,0xc6,0xa3,0xd9, + 0x2f,0x13,0x63,0x7d,0xab,0xdd,0xa2,0xfd,0x53,0x97,0x6d,0xda, + 0x71,0xcd,0x5a,0xdc,0xf4,0x48,0xa4,0xbc,0xba,0xaf,0xe1,0xf3, + 0x34,0xb7,0xfb,0x19,0x1e,0xa5,0xf1,0x34,0x07,0x72,0xd0,0x5b, + 0x58,0xbe,0xcc,0x11,0x11,0xa7,0x9a,0x27,0x32,0x3c,0x43,0x0e, + 0x68,0xa4,0xe1,0x98,0x4d,0x3c,0xc0,0x12,0x39,0xf0,0xb0,0x32, + 0x85,0x23,0x3e,0xa0,0x0e,0xbb,0xc9,0xd0,0x51,0x6d,0x3a,0x5c, + 0xa9,0xf5,0x58,0xbf,0xb2,0xf7,0xc4,0x27,0x0e,0xee,0xe2,0x56, + 0xbe,0x50,0xa1,0xf9,0xbf,0x44,0xfe,0xaa,0xa0,0x82,0x7a,0x82, + 0x78,0x75,0x11,0xe9,0x96,0xe0,0xd8,0x79,0xfa,0x57,0x84,0xa0, + 0x1b,0x04,0xe8,0x39,0xb3,0x72,0x82,0x23,0xfe,0x64,0xbc,0xd6, + 0xc8,0xae,0x24,0x5e,0x17,0xbc,0xda,0xf8,0x6f,0xb9,0x67,0x2c, + 0x99,0xb2,0x92,0xd2,0xe6,0x9d,0x06,0xf2,0xf2,0x74,0x23,0xef, + 0xe2,0xab,0x88,0x8d, +}; + +static const unsigned char rsasig_e[] = { + 0x01,0x00,0x01, +}; + +static const unsigned char rsasig_d[] = { + 0x27,0x8e,0xc1,0xe9,0x67,0xb8,0x20,0xf7,0x9e,0x13,0x2f,0x73, + 0xbc,0xcc,0x88,0xa4,0xcb,0x93,0x3c,0x5c,0x71,0x2e,0xb3,0x9e, + 0x46,0xad,0xfd,0x7d,0xc3,0xee,0x13,0x03,0x6c,0x0b,0xf9,0xb8, + 0x47,0x3e,0x5d,0x30,0x9d,0x3e,0x26,0x2b,0xf2,0xbf,0xb6,0x97, + 0xd6,0xde,0x08,0x02,0xbb,0x49,0x6e,0xf0,0x68,0x9c,0x00,0xa3, + 0x62,0xf7,0x84,0x84,0x19,0x2a,0x4d,0xb9,0x84,0x25,0x9b,0x7c, + 0xca,0x8c,0xed,0x4e,0xc4,0xd8,0xed,0xa8,0x1a,0xcf,0xec,0x43, + 0x48,0x9a,0x2a,0x58,0x0d,0x44,0xf7,0x95,0x04,0x39,0x30,0xd8, + 0xd5,0xe5,0xb2,0x3c,0x8b,0xe7,0x22,0x3b,0x08,0x5b,0xb0,0x50, + 0x0d,0xac,0xc1,0x42,0x82,0xbc,0xa8,0xf8,0xb0,0x7f,0x30,0xe7, + 0xe8,0xa6,0x1a,0x93,0x0c,0x79,0x68,0x41,0x05,0x04,0x02,0x72, + 0xc8,0x6e,0xe4,0x73,0xa2,0xba,0x9c,0xbf,0xa3,0xb0,0x24,0xe8, + 0x99,0xbd,0x74,0xda,0x65,0xab,0x66,0x07,0x87,0x98,0x11,0x01, + 0x5b,0xdd,0x1d,0x3b,0x0c,0xef,0x9b,0x26,0xb7,0x82,0x47,0x5a, + 0x0a,0x05,0x4b,0xf0,0x80,0x09,0x22,0xe1,0x5d,0x4f,0x08,0xd2, + 0xab,0x05,0x84,0xef,0x33,0xd0,0xe0,0xad,0x05,0x30,0xee,0x48, + 0xc8,0x0b,0x8c,0x4f,0xbe,0xf6,0x96,0x71,0xa1,0x43,0x27,0x8a, + 0xac,0xa5,0x3d,0x59,0x92,0x4f,0x65,0xe7,0x81,0x19,0x1d,0x49, + 0xb4,0x9e,0x71,0x4b,0xd5,0x2d,0xb5,0xf2,0x72,0x0e,0x22,0xa8, + 0xfc,0xa5,0xb5,0xe8,0x22,0x89,0xdc,0x38,0x48,0x47,0x70,0x66, + 0x73,0x28,0x16,0xd5,0xac,0x45,0xc6,0xa8,0x74,0xae,0x3b,0x9e, + 0xdc,0x9a,0x60,0xc5, +}; + +static const unsigned char rsasig_mhash[] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b, + 0x0c,0x0d,0x0e,0x0f,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17, + 0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f, +}; + +static const unsigned char rsasig_salt[] = { + 0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5, + 0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5, + 0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5,0xa5, +}; + +static const unsigned char rsasig_expect[] = { + 0x06,0xf5,0x56,0x60,0xfa,0x34,0x07,0x13,0x1d,0x65,0x1c,0x00, + 0xc8,0xd1,0xa8,0x91,0x7d,0x1a,0xc8,0x27,0xb1,0x6b,0x4d,0x20, + 0x5b,0x7b,0x7f,0x9d,0x27,0xba,0xa6,0x5b,0x6a,0x26,0x50,0x63, + 0x0d,0x02,0x0c,0xab,0x4c,0x29,0x8e,0x6c,0x61,0x4f,0xdb,0x5b, + 0xcf,0xb5,0xe6,0x2f,0x25,0x10,0x10,0x6a,0x0b,0x65,0x6a,0xf3, + 0xa9,0x62,0x8d,0x56,0x30,0x0d,0x12,0x24,0x9c,0x34,0x43,0xe5, + 0x74,0x35,0x73,0xbd,0x8c,0x22,0x32,0x24,0xe2,0xc1,0x22,0xb8, + 0x4f,0xd3,0x2d,0x0e,0xc4,0xe6,0xc6,0x3e,0xdd,0xfb,0xfd,0xe9, + 0xed,0x6e,0x65,0x0e,0x8f,0xc0,0x30,0x10,0x9a,0x8a,0x8f,0xfd, + 0xec,0x01,0x4a,0x07,0x3b,0xa9,0xcc,0x32,0x8c,0x9f,0xf0,0xd5, + 0x19,0x2f,0x90,0xc8,0x0a,0x96,0x41,0x77,0xd2,0x18,0xa5,0xfd, + 0x35,0xcb,0x42,0x96,0x66,0x15,0x07,0x95,0x6c,0x11,0x0d,0xe3, + 0xc7,0xe7,0xf3,0x7f,0xe9,0x86,0x83,0x22,0x47,0xa9,0xb3,0x4d, + 0xe8,0xcc,0x96,0x95,0xe1,0x33,0x53,0xc3,0x17,0x7d,0xc7,0x79, + 0x6b,0x5c,0x27,0x71,0x4b,0x39,0xe2,0x72,0x43,0x01,0x77,0x83, + 0x9a,0xb7,0x5b,0xd9,0x0e,0xc2,0x83,0x0b,0x81,0x61,0xff,0x3b, + 0x5b,0xed,0xaa,0xe9,0xc3,0xa5,0xfb,0x4c,0xf6,0xb8,0xf0,0xf3, + 0x2d,0x63,0x35,0x34,0x11,0x42,0x2c,0x7d,0x02,0x0e,0x33,0x49, + 0x33,0x46,0xce,0x2c,0x28,0x19,0x6e,0xfd,0xd2,0xc5,0x75,0x7a, + 0xd0,0xfa,0x96,0xa3,0x15,0x69,0x83,0x53,0x0d,0x89,0xd1,0x59, + 0x9b,0x60,0xef,0xc5,0xd1,0x7f,0x43,0x92,0xaf,0x49,0x33,0xf0, + 0x78,0xa6,0x6e,0x11, +}; + diff --git a/runtime/test/test_crypto.c b/runtime/test/test_crypto.c index db9d575..eb193a8 100644 --- a/runtime/test/test_crypto.c +++ b/runtime/test/test_crypto.c @@ -8,6 +8,7 @@ #include "crypto.h" #include "t.h" #include "x509_vectors.h" +#include "rsa_sign_vectors.h" static void hex(const uint8_t *d, size_t n, char *out) { static const char *h = "0123456789abcdef"; @@ -455,5 +456,21 @@ int main(void) { T_CHECK(wo_x509_eku_serverauth_ok(kat_leaf_eku_client, sizeof kat_leaf_eku_client) == 0); /* clientAuth only */ } + /* RSA-PSS SIGN (rv2 9 phase G1) — deterministic (fixed salt) vs python + * from-spec, and our sign round-trips through our verify. */ + { + uint8_t sig[256]; + int rc = wo_rsa_pss_sha256_sign(rsasig_n, sizeof rsasig_n, rsasig_d, + sizeof rsasig_d, rsasig_mhash, rsasig_salt, + sizeof rsasig_salt, sig); + T_CHECK(rc == 0); + T_CHECK(memcmp(sig, rsasig_expect, 256) == 0); /* byte-for-byte */ + T_CHECK(wo_rsa_pss_sha256_verify(rsasig_n, sizeof rsasig_n, rsasig_e, + sizeof rsasig_e, sig, 256, rsasig_mhash, 32) == 1); + sig[100] ^= 1; /* tamper */ + T_CHECK(wo_rsa_pss_sha256_verify(rsasig_n, sizeof rsasig_n, rsasig_e, + sizeof rsasig_e, sig, 256, rsasig_mhash, 32) == 0); + } + return t_report("test_crypto"); }