feat(crypto): constant-time RSA-PSS signing (rv2 9 phase G1a)

- 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)
This commit is contained in:
shoney.arickathil 2026-09-09 03:06:20 +02:00
parent 82446652e4
commit 631506d36f
4 changed files with 197 additions and 0 deletions

View file

@ -1183,6 +1183,85 @@ static int rsa_recover(const uint8_t *n, size_t nlen, const uint8_t *e,
return 0; 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, 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, size_t elen, const uint8_t *sig, size_t siglen,
const uint8_t hash[32]) { const uint8_t hash[32]) {

View file

@ -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, size_t elen, const uint8_t *sig, size_t siglen,
const uint8_t mhash[32], size_t saltlen); 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 /* 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. */ * (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], int wo_ecdsa_p256_sha256_verify(const uint8_t qx[32], const uint8_t qy[32],

View file

@ -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,
};

View file

@ -8,6 +8,7 @@
#include "crypto.h" #include "crypto.h"
#include "t.h" #include "t.h"
#include "x509_vectors.h" #include "x509_vectors.h"
#include "rsa_sign_vectors.h"
static void hex(const uint8_t *d, size_t n, char *out) { static void hex(const uint8_t *d, size_t n, char *out) {
static const char *h = "0123456789abcdef"; 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 */ 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"); return t_report("test_crypto");
} }