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,81 @@
# 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
```ts
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.
```ts
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.