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

3.3 KiB

verify/pkcs1v15

Purpose

RSASSA-PKCS1-v1_5 signature verification per RFC 8017 §8.2.2. Recovers the encoded message from signature via s^e mod n, reconstructs the expected EMSA-PKCS1-v1_5 encoding of message with the same hash, and compares the two byte strings constant-time.

Features

  • RFC 8017 §8.2.2 conformant
  • Constant-time encoded-message compare via constantTimeEqual from @suaveplan/crypto-utils
  • Returns false on rejection (no exception); throws only on structural malformation (signature length mismatch)
  • Accepts either an RsaPublicKey or an RsaPrivateKey (both carry n and e)
  • Modulus-length policy gates run on every call
  • Records crypto_rsa.verify_duration_ms and crypto_rsa.outcome_total{operation,outcome} on every call

Usage: Basic Example

import { rsaVerifyPkcs1v15 } from "@suaveplan/crypto-rsa/verify/pkcs1v15";
import { rsaSignPkcs1v15 } from "@suaveplan/crypto-rsa/sign/pkcs1v15";

const message = new TextEncoder().encode("hello, signed world");
const signature = rsaSignPkcs1v15(privateKey, message, { hash: "SHA-256" });
const ok = rsaVerifyPkcs1v15(publicKey, message, signature, {
    hash: "SHA-256",
});
console.log(ok); // true

Usage: Advanced Example

A bit-flipped signature, a tampered message, or a hash mismatch all return false without revealing which one tripped the gate.

import { rsaVerifyPkcs1v15 } from "@suaveplan/crypto-rsa/verify/pkcs1v15";

const tampered = new Uint8Array(signature);
tampered[0] = tampered[0] ^ 0x01;
console.log(
    rsaVerifyPkcs1v15(publicKey, message, tampered, { hash: "SHA-256" })
); // false

console.log(
    rsaVerifyPkcs1v15(publicKey, message, signature, { hash: "SHA-512" })
); // false (hash mismatch)

API Reference

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

  • key: RsaPublicKey | RsaPrivateKey
  • message: Uint8Array — the bytes that were signed
  • signature: Uint8Array — must be exactly byteLength(n) long
  • options.hash: RsaHashAlgorithm — must match the hash used at sign time
  • options.allowLegacy?: boolean
  • options.allowOversize?: boolean
  • returns true when the signature is valid, false on any 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

  • The signature integer s = os2ip(signature) is checked against s < n before the modular exponentiation; signatures with s >= n are rejected with false rather than allowed to wrap around.
  • Comparison uses constantTimeEqual(recovered, expected) — never === and never byte-by-byte short-circuit. This is the verify-side defence against the timing-oracle attacks described in Bleichenbacher 2006 (the SHA-1 padding-oracle exploit) and follow-ups.
  • The encoded message is reconstructed every call rather than carried alongside the signature. This is the RFC 8017 §8.2.2 design — the verifier MUST regenerate EM from the public inputs to detect signature substitution.
  • The verify_duration_ms histogram is recorded on every code path (success, rejection, structural error) so service-side dashboards see the full latency distribution including the failure modes.