# sign/pss ## Purpose RSASSA-PSS signature generation per RFC 8017 §8.1.1. Hashes the message, draws a random salt of the configured length, runs the EMSA-PSS encoder, then converts the result through the CRT private-key operation. The output is a signature of length exactly `byteLength(n)`. PSS is a probabilistic scheme — distinct calls with the same inputs produce different signatures unless the salt length is zero. ## Features - RFC 8017 §8.1.1 conformant - Configurable `hash`, `mgfHash`, `saltLength` - `saltLength` defaults to `hashOutputLength(hash)` - `mgfHash` defaults to `hash` - Injectable `Rng` from `@suaveplan/types` for deterministic-replay test scenarios - Default RNG: `randomBytes` from `@suaveplan/crypto-utils` (CSPRNG-backed) - CRT-aware private operation (~3-4× faster than naive `m^d mod n` on 2048-bit keys) - **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"}` - Records `crypto_rsa.sign_duration_ms{scheme="pss",hash,engine}` and outcome counter on every call ## Usage: Basic Example ```ts import { rsaSignPss } from "@suaveplan/crypto-rsa/sign/pss"; const message = new TextEncoder().encode("hello, signed world"); const signature = rsaSignPss(privateKey, message, { hash: "SHA-256" }); console.log(signature.length); // === byteLength(n) ``` ## Usage: Advanced Example The `rng` option accepts any `Rng` shape from `@suaveplan/types`. A seeded RNG produces deterministic signatures, useful for golden-file testing or reproducible audit trails. The `saltLength: 0` shortcut also produces deterministic PSS without needing a custom RNG. ```ts import { rsaSignPss } from "@suaveplan/crypto-rsa/sign/pss"; import type { Rng } from "@suaveplan/types"; // Deterministic PSS via empty salt. const detSig = rsaSignPss(privateKey, message, { hash: "SHA-256", saltLength: 0, }); // Deterministic PSS via seeded RNG (reproducible for fuzz cases). const seeded: Rng = makeMyMulberry32Rng(0xdeadbeef); const reproducible = rsaSignPss(privateKey, message, { hash: "SHA-256", rng: seeded, }); // Cross-hash PSS — message hashed with SHA-256, MGF1 driven by SHA-1. const xHash = rsaSignPss(privateKey, message, { hash: "SHA-256", mgfHash: "SHA-1", }); ``` ## API Reference ### `rsaSignPss(privateKey, message, options): Uint8Array` - `privateKey: RsaPrivateKey` - `message: Uint8Array` - `options.hash: RsaHashAlgorithm` - `options.saltLength?: number` — defaults to `hashOutputLength(hash)`; must be non-negative integer - `options.mgfHash?: RsaHashAlgorithm` — defaults to `options.hash` - `options.rng?: Rng` — defaults to `randomBytes` from `@suaveplan/crypto-utils` - `options.allowLegacy?: boolean` - `options.allowOversize?: boolean` - `options.blinding?: boolean` — defaults to `true`; opting out emits `crypto_rsa.blinding_disabled_total{operation="sign"}` - **returns** `Uint8Array` of length `byteLength(n)` - **throws** `CryptoRsaError(CRYPTO_RSA.MODULUS_TOO_SHORT)` / `MODULUS_TOO_LARGE` / `UNSUPPORTED_HASH` / `INVALID_KEY` / `INVALID_SIGNATURE` (on bad RNG output or bad salt length) ## Implementation Notes - `emBits = modulusBits - 1` per RFC 8017 §8.1.1 step 2. The encoder takes `emBits` (not `emLen` bytes) so the leftmost-bit clearance can be enforced correctly. - The signature byte length is always `byteLength(n) = ceil(modulusBits/8)`. When `modulusBits` is a multiple of 8 (the common case), this equals `emLen + 1` because `emBits = modulusBits - 1` rounds down by one byte. The `i2osp(s, Math.ceil(modulusBits / 8))` call pads the signature to the modulus byte length on the wire. - The `rng.nextBytes` path is used when available (CSPRNGs typically expose it directly). When only `nextUint32` is implemented, the salt is drawn 4 bytes at a time. Both paths produce the same statistical uniformity assuming the underlying RNG is uniform. - Salt length of `0` short-circuits the RNG entirely — useful for deterministic PSS validation. - **Blinding** — when enabled (default) the encoded message integer is multiplied by `r^e mod n` for a freshly sampled `r ∈ Z*_n`, the CRT private operation runs against that blinded value, and the unblinding step `s = s' * r^{-1} mod n` is applied before `i2osp`. The signature output is bit-identical to the unblinded path. PSS verifiers do not observe the difference because verification only depends on the public key. Disable only for benchmarks; the < 5% overhead does not justify the timing-side-channel exposure in production.