first commit
This commit is contained in:
105
docs/security-stack/crypto/crypto-rsa/src/sign/pss.md
Normal file
105
docs/security-stack/crypto/crypto-rsa/src/sign/pss.md
Normal 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.
|
||||
Reference in New Issue
Block a user