Files
skills/docs/security-stack/crypto/crypto-rsa/src/verify/pss.md
2026-05-29 08:20:56 +01:00

3.4 KiB

verify/pss

Purpose

RSASSA-PSS signature verification per RFC 8017 §8.1.2. Recovers the encoded message via s^e mod n, then runs the EMSA-PSS verifier against the digest of the message under the given (hash, mgfHash, saltLength) triple.

Features

  • RFC 8017 §8.1.2 conformant
  • Uses emsaPssVerify for the structural check (which uses constantTimeEqual for the salt+hash compare)
  • Returns false on rejection; throws only on signature length mismatch
  • saltLength defaults to hashOutputLength(hash)
  • mgfHash defaults to hash
  • Modulus-length policy gates run on every call
  • Records crypto_rsa.verify_duration_ms{scheme="pss",hash,engine} and outcome counter on every call

Usage: Basic Example

import { rsaVerifyPss } from "@suaveplan/crypto-rsa/verify/pss";
import { rsaSignPss } from "@suaveplan/crypto-rsa/sign/pss";

const message = new TextEncoder().encode("hello");
const sig = rsaSignPss(privateKey, message, { hash: "SHA-256" });
console.log(rsaVerifyPss(publicKey, message, sig, { hash: "SHA-256" })); // true

Usage: Advanced Example

import { rsaVerifyPss } from "@suaveplan/crypto-rsa/verify/pss";
import { rsaSignPss } from "@suaveplan/crypto-rsa/sign/pss";

// Custom saltLength: must match the value used by the signer.
const sig = rsaSignPss(privateKey, message, { hash: "SHA-256", saltLength: 16 });
console.log(rsaVerifyPss(publicKey, message, sig, { hash: "SHA-256", saltLength: 16 })); // true
console.log(rsaVerifyPss(publicKey, message, sig, { hash: "SHA-256", saltLength: 32 })); // false

// mgfHash distinct from hash.
const xSig = rsaSignPss(privateKey, message, {
    hash: "SHA-256",
    mgfHash: "SHA-1",
});
console.log(
    rsaVerifyPss(publicKey, message, xSig, {
        hash: "SHA-256",
        mgfHash: "SHA-1",
    })
); // true

API Reference

rsaVerifyPss(key, message, signature, options): boolean

  • key: RsaPublicKey | RsaPrivateKey
  • message: Uint8Array
  • signature: Uint8Array — must be exactly byteLength(n) long
  • options.hash: RsaHashAlgorithm
  • options.saltLength?: number — defaults to hashOutputLength(hash)
  • options.mgfHash?: RsaHashAlgorithm — defaults to options.hash
  • options.allowLegacy?: boolean
  • options.allowOversize?: boolean
  • returns true on valid, false on verify-level rejection
  • throws CryptoRsaError(CRYPTO_RSA.INVALID_SIGNATURE) on signature length mismatch
  • throws CryptoRsaError(CRYPTO_RSA.MODULUS_TOO_SHORT) / MODULUS_TOO_LARGE / UNSUPPORTED_HASH

Implementation Notes

  • s = os2ip(signature) is checked against s < n before the modular exponentiation; signatures with s >= n are rejected with false.
  • The encoded message produced by i2osp(s^e mod n, emLen) has length emLen = ceil((modulusBits - 1) / 8). This is one byte shorter than the signature itself when modulusBits is a multiple of 8 — the high-order zero pad is stripped here and the leftmost-bit clearance rule in emsaPssVerify enforces that the original encoding had the expected zero in that position.
  • The constant-time guarantee for the final salt+hash compare is delivered by emsaPssVerify's use of constantTimeEqual. The earlier structural checks (length mismatch, s >= n) are guarded by fast-fail paths that do NOT leak the secret-derived signal — they test only public inputs.
  • The verify_duration_ms histogram is recorded on every code path (success, rejection, structural error).