first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# 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.