# sign/pkcs1v15 ## Purpose RSASSA-PKCS1-v1\_5 signature generation per RFC 8017 §8.2.1. Hashes the message with the configured algorithm, applies the EMSA-PKCS1-v1\_5 encoding, and runs the RSA private operation through the CRT path. The output is a signature of length exactly `byteLength(n)`. ## Features - RFC 8017 §8.2.1 conformant — uses the CRT private-key operation - Supports SHA-1, SHA-256, SHA-384, SHA-512 - Modulus-length policy gates run on every call (`MODULUS_TOO_SHORT` / `MODULUS_TOO_LARGE`) - Re-uses the canonical `i2osp` / `os2ip` math primitives - Records `crypto_rsa.sign_duration_ms{scheme,hash,engine}` and `crypto_rsa.outcome_total{operation,outcome}` on every call - **RSA blinding (RFC 8017 §5.1.2 / Kocher 1996) wired by default** — opt-out via `{ blinding: false }` emits `crypto_rsa.blinding_disabled_total{operation="sign"}` - All thrown errors are `CryptoRsaError` instances; non-typed exceptions wrap as `CRYPTO_RSA.INVALID_SIGNATURE` ## Usage: Basic Example ```ts import { rsaSignPkcs1v15 } from "@suaveplan/crypto-rsa/sign/pkcs1v15"; const message = new TextEncoder().encode("hello, signed world"); const signature = rsaSignPkcs1v15(privateKey, message, { hash: "SHA-256" }); console.log(signature.length); // === byteLength(n), e.g. 256 for 2048-bit ``` ## Usage: Advanced Example The `allowLegacy` option lowers the modulus floor from 2048 to 1024 bits for legacy interop scenarios (e.g. WebAuthn TPM 1.2 endorsement keys). The `allowOversize` option lifts the ceiling beyond 16384 bits for offline tooling. Both default to `false`. ```ts import { rsaSignPkcs1v15 } from "@suaveplan/crypto-rsa/sign/pkcs1v15"; const sig = rsaSignPkcs1v15(legacy1024PrivateKey, message, { hash: "SHA-1", allowLegacy: true, }); ``` ## API Reference ### `rsaSignPkcs1v15(privateKey, message, options): Uint8Array` - `privateKey: RsaPrivateKey` — canonical private key with mandatory CRT parameters - `message: Uint8Array` — bytes to be signed; will be hashed internally - `options.hash: RsaHashAlgorithm` — one of `"SHA-1"`, `"SHA-256"`, `"SHA-384"`, `"SHA-512"` - `options.allowLegacy?: boolean` — see policy doc - `options.allowOversize?: boolean` — see policy doc - `options.blinding?: boolean` — defaults to `true`; opting out emits `crypto_rsa.blinding_disabled_total{operation="sign"}` - `options.rng?: Rng` — randomness source for the blinding factor; defaults to `defaultRng()` - **returns** `Uint8Array` of length `byteLength(n)` - **throws** `CryptoRsaError(CRYPTO_RSA.MODULUS_TOO_SHORT)` / `MODULUS_TOO_LARGE` / `UNSUPPORTED_HASH` / `INVALID_KEY` ## Implementation Notes - The output equals `i2osp(rsaPrivateOperationCrt(os2ip(em), privateKey), emLen)` where `em = emsaPkcs1v15Encode(message, emLen, hash)` and `emLen = byteLength(n)`. This is bit-for-bit identical to the naive `c = m^d mod n` reference, but uses the CRT decomposition for ~3-4× the throughput on a 2048-bit modulus. - Encoding is deterministic — every call with the same `(message, hash)` pair produces the same signature. This is RFC 8017 §8.2.1 behaviour: PKCS1-v1\_5 is a deterministic signature scheme, in contrast to PSS. - The function never short-circuits on early failure — even when an exception is raised, the duration histogram and outcome counter are recorded. The `outcome` attribute distinguishes `"ok"` from `"error"`. - The engine attribute is `"noble"` because the underlying modular exponentiation is `@noble/curves/abstract/modular.pow`. The `RsaCryptoEngine` adapter (`./engine`) selects `"subtle"` when `globalThis.crypto.subtle` exposes the matching algorithm. - **Blinding** — for every call `m' = m * r^e mod n` is computed with a freshly sampled `r ∈ Z*_n`, the CRT private operation runs against `m'`, and the result is unblinded via `s = s' * r^{-1} mod n`. This prevents the Kocher 1996 timing-attack class against the secret exponent. Output is bit-for-bit identical to the unblinded path; only the timing varies. The < 5% overhead is bench-verified.