Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
# sign/pkcs1v15
## Purpose
RSASSA-PKCS1-v1\_5 signature generation per RFC 8017 §8.2.1. Hashes
the message with the configured algorithm, applies the EMSA-PKCS1-v1\_5
encoding, and runs the RSA private operation through the CRT path. The
output is a signature of length exactly `byteLength(n)`.
## Features
- RFC 8017 §8.2.1 conformant — uses the CRT private-key operation
- Supports SHA-1, SHA-256, SHA-384, SHA-512
- Modulus-length policy gates run on every call (`MODULUS_TOO_SHORT` / `MODULUS_TOO_LARGE`)
- Re-uses the canonical `i2osp` / `os2ip` math primitives
- Records `crypto_rsa.sign_duration_ms{scheme,hash,engine}` and `crypto_rsa.outcome_total{operation,outcome}` on every call
- **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"}`
- All thrown errors are `CryptoRsaError` instances; non-typed exceptions wrap as `CRYPTO_RSA.INVALID_SIGNATURE`
## Usage: Basic Example
```ts
import { rsaSignPkcs1v15 } from "@suaveplan/crypto-rsa/sign/pkcs1v15";
const message = new TextEncoder().encode("hello, signed world");
const signature = rsaSignPkcs1v15(privateKey, message, { hash: "SHA-256" });
console.log(signature.length); // === byteLength(n), e.g. 256 for 2048-bit
```
## Usage: Advanced Example
The `allowLegacy` option lowers the modulus floor from 2048 to 1024 bits
for legacy interop scenarios (e.g. WebAuthn TPM 1.2 endorsement keys).
The `allowOversize` option lifts the ceiling beyond 16384 bits for
offline tooling. Both default to `false`.
```ts
import { rsaSignPkcs1v15 } from "@suaveplan/crypto-rsa/sign/pkcs1v15";
const sig = rsaSignPkcs1v15(legacy1024PrivateKey, message, {
hash: "SHA-1",
allowLegacy: true,
});
```
## API Reference
### `rsaSignPkcs1v15(privateKey, message, options): Uint8Array`
- `privateKey: RsaPrivateKey` — canonical private key with mandatory CRT parameters
- `message: Uint8Array` — bytes to be signed; will be hashed internally
- `options.hash: RsaHashAlgorithm` — one of `"SHA-1"`, `"SHA-256"`, `"SHA-384"`, `"SHA-512"`
- `options.allowLegacy?: boolean` — see policy doc
- `options.allowOversize?: boolean` — see policy doc
- `options.blinding?: boolean` — defaults to `true`; opting out emits `crypto_rsa.blinding_disabled_total{operation="sign"}`
- `options.rng?: Rng` — randomness source for the blinding factor; defaults to `defaultRng()`
- **returns** `Uint8Array` of length `byteLength(n)`
- **throws** `CryptoRsaError(CRYPTO_RSA.MODULUS_TOO_SHORT)` / `MODULUS_TOO_LARGE` / `UNSUPPORTED_HASH` / `INVALID_KEY`
## Implementation Notes
- The output equals `i2osp(rsaPrivateOperationCrt(os2ip(em), privateKey), emLen)`
where `em = emsaPkcs1v15Encode(message, emLen, hash)` and
`emLen = byteLength(n)`. This is bit-for-bit identical to the naive
`c = m^d mod n` reference, but uses the CRT decomposition for ~3-4×
the throughput on a 2048-bit modulus.
- Encoding is deterministic — every call with the same `(message, hash)`
pair produces the same signature. This is RFC 8017 §8.2.1 behaviour:
PKCS1-v1\_5 is a deterministic signature scheme, in contrast to PSS.
- The function never short-circuits on early failure — even when an
exception is raised, the duration histogram and outcome counter are
recorded. The `outcome` attribute distinguishes `"ok"` from `"error"`.
- The engine attribute is `"noble"` because the underlying modular
exponentiation is `@noble/curves/abstract/modular.pow`. The
`RsaCryptoEngine` adapter (`./engine`) selects `"subtle"` when
`globalThis.crypto.subtle` exposes the matching algorithm.
- **Blinding** — for every call `m' = m * r^e mod n` is computed with a
freshly sampled `r ∈ Z*_n`, the CRT private operation runs against
`m'`, and the result is unblinded via `s = s' * r^{-1} mod n`. This
prevents the Kocher 1996 timing-attack class against the secret
exponent. Output is bit-for-bit identical to the unblinded path; only
the timing varies. The < 5% overhead is bench-verified.