4.5 KiB
4.5 KiB
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 saltLengthdefaults tohashOutputLength(hash)mgfHashdefaults tohash- Injectable
Rngfrom@suaveplan/typesfor deterministic-replay test scenarios - Default RNG:
randomBytesfrom@suaveplan/crypto-utils(CSPRNG-backed) - CRT-aware private operation (~3-4× faster than naive
m^d mod non 2048-bit keys) - RSA blinding (RFC 8017 §5.1.2 / Kocher 1996) wired by default — opt-out via
{ blinding: false }emitscrypto_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
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.
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: RsaPrivateKeymessage: Uint8Arrayoptions.hash: RsaHashAlgorithmoptions.saltLength?: number— defaults tohashOutputLength(hash); must be non-negative integeroptions.mgfHash?: RsaHashAlgorithm— defaults tooptions.hashoptions.rng?: Rng— defaults torandomBytesfrom@suaveplan/crypto-utilsoptions.allowLegacy?: booleanoptions.allowOversize?: booleanoptions.blinding?: boolean— defaults totrue; opting out emitscrypto_rsa.blinding_disabled_total{operation="sign"}- returns
Uint8Arrayof lengthbyteLength(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 - 1per RFC 8017 §8.1.1 step 2. The encoder takesemBits(notemLenbytes) so the leftmost-bit clearance can be enforced correctly.- The signature byte length is always
byteLength(n) = ceil(modulusBits/8). WhenmodulusBitsis a multiple of 8 (the common case), this equalsemLen + 1becauseemBits = modulusBits - 1rounds down by one byte. Thei2osp(s, Math.ceil(modulusBits / 8))call pads the signature to the modulus byte length on the wire. - The
rng.nextBytespath is used when available (CSPRNGs typically expose it directly). When onlynextUint32is 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
0short-circuits the RNG entirely — useful for deterministic PSS validation. - Blinding — when enabled (default) the encoded message integer is
multiplied by
r^e mod nfor a freshly sampledr ∈ Z*_n, the CRT private operation runs against that blinded value, and the unblinding steps = s' * r^{-1} mod nis applied beforei2osp. 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.