Files
2026-05-27 23:29:53 +01:00

365 lines
40 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `@suaveplan/crypto-rsa` — RSA primitives (RFC 8017 PKCS #1 v2.2 + RFC 5208 PKCS #8 + RFC 7518 JWA)
**Package**: `@suaveplan/crypto-rsa` (NEW)
**Status**: PROPOSED
**Type**: new-package (greenfield; crypto-plugin sibling, universal-tier)
**Date**: 2026-05-11
**Path**: `packages/crypto/crypto-rsa/`
**Source audit**: `../suaveplan/enterprise/crypto-rsa.md`
## Why
The genesis crypto family today covers symmetric AEAD (`crypto-cipher`), hashing + KDFs (`crypto-hash`), and elliptic curves (`crypto-curve`: Ed25519, X25519, P-256, P-384, P-521, secp256k1). It does NOT cover RSA — yet RSA is the dominant signature algorithm in every protocol genesis is migrating: real-world TLS certificate chains, the JWS / JWT ecosystem (RFC 7518 §3.3 mandates support for `RS256`), and the WebAuthn `tpm` attestation format (RSA endorsement keys are the production majority for TPM-bound credentials). Three packages currently in flight on genesis are blocked on this gap:
1. **`@suaveplan/x509`** — verifies certificate-chain signatures. RFC 5280 §1.2 + Appendix A enumerate the RSA signature OIDs `1.2.840.113549.1.1.5` (sha1WithRSAEncryption), `1.2.840.113549.1.1.11` (sha256WithRSAEncryption), `1.2.840.113549.1.1.12` (sha384), `1.2.840.113549.1.1.13` (sha512), and `1.2.840.113549.1.1.10` (id-RSASSA-PSS). The majority of real production certs today use one of these.
2. **`@suaveplan/jws`** — implements the JWA registry of RFC 7518 §3.3 (`RS256`/`RS384`/`RS512` over RSASSA-PKCS1-v1\_5) and §3.5 (`PS256`/`PS384`/`PS512` over RSASSA-PSS). Six of the eleven JWS algorithm IDs are RSA-backed.
3. **`@suaveplan/webauthn`** — `tpm` attestation statements carry RSA public keys from TPM modules. RSA-PKCS1-v1\_5 over SHA-256 is the most common TPM attestation signature.
All three consumers verify externally-issued keys; one (`jws`) also signs. Beyond the three named consumers, this package is the universal RSA primitive for the entire monorepo — `@suaveplan/crypto-cli`'s future `keychain rsa init`, `@suaveplan/vault`'s future RSA-protected key blobs, `@suaveplan/pkcs12` PKCS#12 import paths, and any downstream consumer integrating with HSM / PKCS#11 hardware all route through `@suaveplan/crypto-rsa`. Splitting RSA out into `@suaveplan/crypto-rsa` rather than folding it into `@suaveplan/crypto-curve` keeps curve-only callers at zero RSA bundle cost, mirrors the per-primitive layout the rest of the crypto family already follows (`crypto-cipher`, `crypto-hash`), and matches `@noble/curves`'s own separation of curve and RSA work.
The library ships at enterprise quality from day one — matching the bar set by `@suaveplan/crypto-curve` (per-primitive subpaths, native + noble engine adapters) and `@suaveplan/crypto-vault` (Zod boundaries, OTel everywhere, 100% docs, sentinel-driven side-channel hardening). The required-reading audit covered RFC 8017 (PKCS #1 v2.2) end-to-end, NIST FIPS 186-5 (Digital Signature Standard) §5 (RSA DSA), NIST SP 800-56B Rev. 2 (Key Establishment Using Integer Factorization Cryptography), Google Wycheproof's RSA test corpus, RFC 7468 (PEM textual encoding), RFC 7638 (JWK Thumbprint), and RFC 5990 (RSA-KEM — surveyed and explicitly deferred). Every requirement below is anchored against one of these sources.
The package is greenfield — no prior implementation in `packages/crypto/` and no upstream `../suaveplan/packages/<plugin>/crypto-rsa/` source. Every requirement is mapped against the canonical RFCs and NIST documents: RFC 8017 (PKCS #1 v2.2), RFC 5208 (PKCS #8), RFC 5958 (PKCS #8 successor), RFC 5280 (X.509 + OIDs), RFC 7468 (PEM), RFC 7517 / 7518 / 7638 (JWK / JWA / JWK Thumbprint), RFC 5990 (RSA-KEM — deferred), NIST FIPS 186-5 (Digital Signature Standard), NIST SP 800-56B Rev. 2 (Key Establishment), and Google Wycheproof.
## What changes
### New package: `@suaveplan/crypto-rsa`
#### Scaffold
- ADDED: `packages/crypto/crypto-rsa/` package scaffold (`package.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md`, `src/__benchmarks__/`, `src/__fixtures__/`).
#### ASN.1 / DER helpers
- ADDED: `src/asn1/reader.ts` — minimal DER reader (`readInteger`, `readSequence`, `readObjectIdentifier`, `readBitString`, `readOctetString`, `readNull`, `readContextSpecific`) sufficient for PKCS#1, PKCS#8, SPKI, RSA-PSS `AlgorithmIdentifier` parameters.
- ADDED: `src/asn1/writer.ts` — matching DER writer.
#### Canonical key shapes
- ADDED: `src/keys/keys.types.ts` — `RsaPublicKey { n: bigint; e: bigint; bitLength: number }`, `RsaPrivateKey { n; e; d; p; q; dp; dq; qInv; bitLength }`. `bitLength` is the modulus bit-length (used by the modulus-length policy gate).
#### Key parse + serialise — PKCS#1
- ADDED: `src/keys/parse-pkcs1-public.ts` — `parseRsaPublicKeyPkcs1(bytes): RsaPublicKey` per RFC 8017 Appendix A.1.1 (`RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }`).
- ADDED: `src/keys/parse-pkcs1-private.ts` — `parseRsaPrivateKeyPkcs1(bytes): RsaPrivateKey` per RFC 8017 Appendix A.1.2.
- ADDED: `src/keys/serialize-pkcs1-public.ts` + `src/keys/serialize-pkcs1-private.ts` — inverse paths.
#### Key parse + serialise — PKCS#8 / SPKI
- ADDED: `src/keys/parse-spki.ts` — `parseRsaPublicKeySpki(bytes): RsaPublicKey` unwraps the `SubjectPublicKeyInfo` (RFC 5280) carrying OID `1.2.840.113549.1.1.1` (`rsaEncryption`) and dispatches to the PKCS#1 reader.
- ADDED: `src/keys/parse-pkcs8.ts` — `parseRsaPrivateKeyPkcs8(bytes): RsaPrivateKey` per RFC 5208 / RFC 5958.
- ADDED: `src/keys/serialize-spki.ts` + `src/keys/serialize-pkcs8.ts` — inverse paths.
#### Key parse + serialise — JWK
- ADDED: `src/keys/parse-jwk.ts` — `parseRsaPublicKeyJwk` + `parseRsaPrivateKeyJwk` per RFC 7517 §6.3. Rejects `kty !== "RSA"` with `CRYPTO_RSA.UNSUPPORTED_FORMAT`.
- ADDED: `src/keys/serialize-jwk.ts` — emits base64url-encoded values per RFC 7515 §2 (no padding, URL-safe alphabet).
#### Façade
- ADDED: `src/keys/parse.ts` — `parseRsaPublicKey(bytes, { format })` + `parseRsaPrivateKey(bytes, { format })` dispatch on the `format` discriminant.
- ADDED: `src/keys/serialize.ts` — `serializeRsaPublicKey(key, { format })` + `serializeRsaPrivateKey(key, { format })` dispatch.
#### Modulus-length policy
- ADDED: `src/policy/modulus.ts` — `enforceMinBitLength(key, { allowLegacy?: boolean })`. Default floor is 2048 bits; `allowLegacy: true` permits 1024-bit keys. Below 1024 is always rejected with `CRYPTO_RSA.MODULUS_TOO_SHORT`.
#### Modular arithmetic + CRT
- ADDED: `src/math/modpow.ts` — `modPow(base: bigint, exponent: bigint, modulus: bigint): bigint` thin wrapper over `@noble/curves/abstract/modular.pow`. NEVER native `BigInt ** BigInt % N`.
- ADDED: `src/math/crt.ts` — `rsaPrivateOperationCrt(c: bigint, key: RsaPrivateKey): bigint` per RFC 8017 §5.1.2 second-form (`m1 = c^dp mod p`, `m2 = c^dq mod q`, `h = qInv * (m1 - m2) mod p`, `m = m2 + h * q`).
- ADDED: `src/math/i2osp.ts` — Integer-to-Octet-String primitive per RFC 8017 §4.1.
- ADDED: `src/math/os2ip.ts` — Octet-String-to-Integer primitive per RFC 8017 §4.2.
#### MGF1 + encoding methods
- ADDED: `src/encoding/mgf1.ts` — `mgf1(seed, maskLen, hash): Uint8Array` per RFC 8017 §B.2.1. Throws `CRYPTO_RSA.MGF_FAILURE` when `maskLen > 2^32 * hLen`.
- ADDED: `src/encoding/emsa-pkcs1-v1_5.ts` — `emsaPkcs1v15Encode(messageDigest, emLen, hash)` per RFC 8017 §9.2. Bundles the four DigestInfo prefix constants:
- SHA-1: `30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14`
- SHA-256: `30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20`
- SHA-384: `30 41 30 0d 06 09 60 86 48 01 65 03 04 02 02 05 00 04 30`
- SHA-512: `30 51 30 0d 06 09 60 86 48 01 65 03 04 02 03 05 00 04 40`
- ADDED: `src/encoding/emsa-pss-encode.ts` — `emsaPssEncode(messageDigest, emBits, salt, hash, mgfHash)` per RFC 8017 §9.1.1.
- ADDED: `src/encoding/emsa-pss-verify.ts` — `emsaPssVerify(messageDigest, encodedMessage, emBits, saltLength, hash, mgfHash)` per RFC 8017 §9.1.2. Constant-time over the salt + hash compare.
#### Signatures — RSASSA-PKCS1-v1\_5
- ADDED: `src/sign/pkcs1v15.ts` — `rsaSignPkcs1v15(privateKey, message, { hash }): Uint8Array` per RFC 8017 §8.2.1.
- ADDED: `src/verify/pkcs1v15.ts` — `rsaVerifyPkcs1v15(key, message, signature, { hash }): boolean` per RFC 8017 §8.2.2. Constant-time encoded-message compare.
#### Signatures — RSASSA-PSS
- ADDED: `src/sign/pss.ts` — `rsaSignPss(privateKey, message, { hash; saltLength?; mgfHash?; rng? }): Uint8Array` per RFC 8017 §8.1.1.
- ADDED: `src/verify/pss.ts` — `rsaVerifyPss(key, message, signature, { hash; saltLength?; mgfHash? }): boolean` per RFC 8017 §8.1.2.
#### Encryption — RSAES-PKCS1-v1\_5
- ADDED: `src/encrypt/pkcs1v15.ts` — `rsaEncryptPkcs1v15(key, plaintext, { rng? }): Uint8Array` per RFC 8017 §7.2.1.
- ADDED: `src/decrypt/pkcs1v15.ts` — `rsaDecryptPkcs1v15(privateKey, ciphertext): Uint8Array` per RFC 8017 §7.2.2. Single combined error class (`CRYPTO_RSA.DECRYPT_FAILED`) — never distinguishes padding-format errors (Bleichenbacher oracle defence).
#### Encryption — RSAES-OAEP
- ADDED: `src/encrypt/oaep.ts` — `rsaEncryptOaep(key, plaintext, { hash; mgfHash?; label?; rng? }): Uint8Array` per RFC 8017 §7.1.1.
- ADDED: `src/decrypt/oaep.ts` — `rsaDecryptOaep(privateKey, ciphertext, { hash; mgfHash?; label? }): Uint8Array` per RFC 8017 §7.1.2. Same combined-error contract.
#### JWK thumbprint
- ADDED: `src/jwk/thumbprint.ts` — `rsaJwkThumbprint(key, hash): Uint8Array` per RFC 7638 §3.2. Worked-example match against RFC 7638 §3.1 reference value `"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"`.
#### SPKI fingerprint
- ADDED: `src/fingerprint/spki.ts` — `computeSpkiFingerprint(publicKey, { hash?: "SHA-256" | "SHA-1" }): Uint8Array` (default SHA-256). Digests the SPKI DER bytes; `ssh-keygen -lf`-style identifier for an RSA key.
- ADDED: `src/fingerprint/format.ts` — `formatFingerprint(bytes, format: "hex" | "base64" | "base64url" | "colon-hex"): string`.
#### PEM framing (RFC 7468)
- ADDED: `src/pem/parse-public.ts` — `parseRsaPublicKeyPem(text): RsaPublicKey` recognising armor labels `-----BEGIN PUBLIC KEY-----` (SPKI) and `-----BEGIN RSA PUBLIC KEY-----` (PKCS#1 public).
- ADDED: `src/pem/parse-private.ts` — `parseRsaPrivateKeyPem(text): RsaPrivateKey` recognising armor labels `-----BEGIN PRIVATE KEY-----` (PKCS#8) and `-----BEGIN RSA PRIVATE KEY-----` (PKCS#1 private).
- ADDED: `src/pem/serialize-public.ts` + `src/pem/serialize-private.ts` — inverse paths emitting 64-char-line base64 bodies per RFC 7468 §3.
#### Key generation
- ADDED: `src/keygen/miller-rabin.ts` — `isProbablePrime(n: bigint, rng: Rng, rounds?: number = 64): boolean` per NIST FIPS 186-5 Appendix B.3.1.
- ADDED: `src/keygen/generate.ts` — `generateRsaKeyPair(options: { modulusBits?, publicExponent?, rng, primes?, signal?, profile? }): Promise<RsaPrivateKey>`. Defaults: `modulusBits: 3072`, `publicExponent: 65537n`, `primes: 2`, `profile: "permissive"`. AbortSignal threaded into the prime-search loop; abort interrupts within ≤ 100 ms and throws `CRYPTO_RSA.ABORTED`.
- ADDED: `src/keygen/multi-prime.ts` — multi-prime RSA support per RFC 8017 §3 (`primes ∈ {3, 4, 5}`).
- ADDED: `src/keygen/exponent.ts` — `publicExponent` validation (odd, `1 < e < 2^256`, coprime with `λ(n)`).
#### Blinding (RFC 8017 §5.1.2)
- ADDED: `src/blinding/blinding.ts` — `blindMessage(m, key, rng): { mPrime, rInverse }` + `unblindResult(s, rInverse, key): bigint` per RFC 8017 §5.1.2 / Kocher 1996. Wired into every `rsaSign*` / `rsaDecrypt*` call by default; opt-out via `{ blinding: false }` emits `crypto_rsa.blinding_disabled_total{operation}` counter increment.
#### `RsaCryptoEngine` adapter — noble + subtle
- ADDED: `src/engine/engine.types.ts` — `RsaCryptoEngine` interface (`sign`, `verify`, `encrypt`, `decrypt`, `modPow`).
- ADDED: `src/engine/noble.ts` — default engine over `@noble/curves/abstract/modular.pow`.
- ADDED: `src/engine/subtle.ts` — `globalThis.crypto.subtle` adapter. Wraps `importKey` / `sign` / `verify` / `encrypt` / `decrypt`. Non-extractable keys by default.
- ADDED: `src/engine/select.ts` — `defaultEngine(operation, key): RsaCryptoEngine` auto-selects subtle when available else noble. Emits `crypto_rsa.engine_selected_total{engine}` on every call.
#### HSM / PKCS#11 bridge
- ADDED: `src/handle/handle.types.ts` — `RsaKeyHandle` interface + `RsaKeyHandleProvider` interface.
- ADDED: `src/handle/registry.ts` — `registerKeyHandleProvider(provider)` + `resolveKeyHandle(key)`.
- ADDED: `src/handle/software.ts` — `SoftwareRsaKeyHandle` reference implementation wrapping an in-memory `RsaPrivateKey`.
#### FIPS 186-5 profile
- ADDED: `src/profile/profile.ts` — `enforceProfile(input, options: { profile: "permissive" | "fips-186-5" })`. The `fips-186-5` profile rejects `modulusBits ∉ {2048, 3072, 4096}`, `publicExponent ∉ [2^16+1, 2^256-1]`, even `publicExponent`, `hash === "SHA-1"` in sign / verify, and PSS `saltLength` ≠ `hLen` (with `CRYPTO_RSA.PROFILE_VIOLATION`).
#### JWK `key_ops` / `use` / `alg` enforcement
- ADDED: `src/keys/jwk-policy.ts` — `enforceJwkPolicy(jwk, operation): void`. `operation ∈ {"sign","verify","encrypt","decrypt","wrapKey","unwrapKey"}`. Cross-policy violations throw `CRYPTO_RSA.KEY_OP_NOT_PERMITTED`.
#### Errors + Zod + observability
- ADDED: `src/errors/errors.ts` — `defineCodes("CRYPTO_RSA", ["MODULUS_TOO_SHORT", "MODULUS_TOO_LARGE", "INVALID_KEY", "INVALID_SIGNATURE", "SIGNATURE_INVALID", "UNSUPPORTED_HASH", "UNSUPPORTED_FORMAT", "DECRYPT_FAILED", "MGF_FAILURE", "KEYGEN_FAILED", "PRIME_INVALID", "KEY_OP_NOT_PERMITTED", "PROFILE_VIOLATION", "ENGINE_UNAVAILABLE", "PEM_MALFORMED", "FINGERPRINT_FAILED", "ABORTED"])` registry (17 codes) + `CryptoRsaError extends SuaveplanError<CryptoRsaErrorCode>`.
- ADDED: `src/schemas/schemas.ts` — `RsaPublicKeySchema`, `RsaPrivateKeySchema`, `RsaParseOptionsSchema`, `RsaSerializeOptionsSchema`, `RsaSignOptionsSchema`, `RsaVerifyOptionsSchema`, `RsaPssOptionsSchema`, `RsaOaepOptionsSchema`, `HashAlgorithmSchema`, `RsaKeygenOptionsSchema`, `RsaFingerprintOptionsSchema`, `RsaJwkPolicySchema`, `RsaProfileSchema`. Mismatch → `CRYPTO_RSA.UNSUPPORTED_FORMAT` / `CRYPTO_RSA.UNSUPPORTED_HASH` / `CRYPTO_RSA.INVALID_KEY` / `CRYPTO_RSA.PROFILE_VIOLATION` as appropriate.
- ADDED: `src/observability/metrics.ts` — `cryptoRsaMetrics` factory emitting:
- Histograms: `crypto_rsa.verify_duration_ms{scheme,hash,engine}`, `crypto_rsa.sign_duration_ms{scheme,hash,engine}`, `crypto_rsa.encrypt_duration_ms{scheme,hash,engine}`, `crypto_rsa.decrypt_duration_ms{scheme,hash,engine}`, `crypto_rsa.keygen_duration_ms{modulus_bits,primes}`, `crypto_rsa.modpow_duration_ms{operation,blinded}`.
- Counters: `crypto_rsa.outcome_total{operation,outcome}` (`outcome ∈ ok / invalid_signature / decrypt_failed / modulus_too_short / modulus_too_large / profile_violation / aborted / key_op_not_permitted`), `crypto_rsa.errors_total{code}`, `crypto_rsa.engine_selected_total{engine}`, `crypto_rsa.fingerprint_total{format}`, `crypto_rsa.profile_violation_total{rule}` (`rule ∈ modulus_bits / public_exponent / salt_length / sha1_banned`), `crypto_rsa.blinding_disabled_total{operation}`.
#### Root + scripts + benchmarks
- ADDED: `src/index.ts` — explicit named re-exports of every public surface (no `export *`).
- ADDED: `scripts/generate-rsa-vectors.ts` — deterministic vector generator covering PKCS1v15 sign, PSS sign, OAEP encrypt against a fixed test key. Idempotent re-runs.
- ADDED: `src/__fixtures__/rfc8017-appendix-c/` — RFC 8017 Appendix C test vectors as JSON.
- ADDED: `src/__fixtures__/cavp/` — NIST CAVP RSA sign / verify / encrypt vectors from <https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/digital-signatures>.
- ADDED: `src/__fixtures__/wycheproof/` — Google Wycheproof RSA vectors (`rsa_signature_*.json`, `rsa_signature_pss_*.json`, `rsa_oaep_*.json`) from <https://github.com/google/wycheproof/tree/master/testvectors>.
- ADDED: `src/__benchmarks__/crypto-rsa.bench.ts` — mitata covering `verify_pkcs1_sha256_{2048,3072,4096} × engine ∈ {noble,subtle}`, `verify_pss_sha256_{2048,3072,4096} × engine`, `sign_pkcs1_sha256_{2048,3072,4096} × engine`, `sign_pss_sha256_{2048,3072,4096} × engine`, `encrypt_oaep_sha256_{2048,3072,4096} × engine`, `decrypt_oaep_sha256_{2048,3072,4096} × engine`, `generate_{2048,3072,4096}` (noble only), `parse_{pkcs1,pkcs8,spki,jwk,pem}_2048`, `crt_vs_naive_2048` ratio (CRT ≥ 2× faster), `blinded_vs_unblinded_2048` ratio (blinding overhead < 5%).
### Subpath exports
- `.` — root barrel (every public symbol)
- `./keys` — `parseRsaPublicKey`, `parseRsaPrivateKey`, `serializeRsaPublicKey`, `serializeRsaPrivateKey`, `enforceJwkPolicy`, types
- `./keygen` — `generateRsaKeyPair`, `isProbablePrime`
- `./sign` — `rsaSignPkcs1v15`, `rsaSignPss`
- `./verify` — `rsaVerifyPkcs1v15`, `rsaVerifyPss`
- `./encrypt` — `rsaEncryptPkcs1v15`, `rsaEncryptOaep`
- `./decrypt` — `rsaDecryptPkcs1v15`, `rsaDecryptOaep`
- `./encoding` — `mgf1`, `emsaPkcs1v15Encode`, `emsaPssEncode`, `emsaPssVerify`
- `./jwk` — `rsaJwkThumbprint`
- `./fingerprint` — `computeSpkiFingerprint`, `formatFingerprint`
- `./pem` — `parseRsaPublicKeyPem`, `parseRsaPrivateKeyPem`, `serializeRsaPublicKeyPem`, `serializeRsaPrivateKeyPem`
- `./blinding` — `blindMessage`, `unblindResult` (advanced consumers; re-exported internally by sign / decrypt)
- `./engine` — `RsaCryptoEngine` interface, `defaultEngine`, `nobleEngine`, `subtleEngine`
- `./handle` — `RsaKeyHandle`, `RsaKeyHandleProvider`, `registerKeyHandleProvider`, `SoftwareRsaKeyHandle`
- `./profile` — `enforceProfile`, profile types
- `./policy` — `enforceMinBitLength`, `enforceMaxBitLength`
- `./schemas` — every Zod schema
- `./observability` — `cryptoRsaMetrics` factory
- `./errors` — `CRYPTO_RSA_CODES`, `CryptoRsaError`, `CryptoRsaErrorCode`
### Dependencies
- `@suaveplan/crypto-hash workspace:^` — SHA-1 / SHA-256 / SHA-384 / SHA-512 digest.
- `@suaveplan/crypto-utils workspace:^` — `constantTimeEqual`, `randomBytes`, `concat`, hex / base64url helpers, `i2osp` / `os2ip`.
- `@suaveplan/error workspace:^` — `defineCodes`, `SuaveplanError`.
- `@suaveplan/types workspace:^` — `Rng`, `HashAlgorithm`, `Clock`.
- `@noble/curves catalog:crypto` — `abstract/modular.pow` constant-time-hardened modpow + `abstract/modular.invert`.
- `@opentelemetry/api catalog:observability` — histograms + counters.
- `zod catalog:forms` — boundary schemas.
- `tslib catalog:build`.
The `subtle` engine consumes `globalThis.crypto.subtle` at runtime — there is no compile-time dependency to add. The HSM / PKCS#11 bridge ships only the interface + reference `SoftwareRsaKeyHandle`; concrete PKCS#11 / KMS adapters live in downstream packages and consume `@suaveplan/crypto-rsa/handle` directly.
## Out of scope
- **RSA-KEM (RFC 5990)** — Key Encapsulation Mechanism over RSA. RFC 5990 specifies a deterministic alternative to OAEP for symmetric-key wrap; none of the three named consumers (`x509`, `jws`, `webauthn`) need KEM, and the FIPS 186-5 profile gates use to approved schemes (PKCS1v15 / PSS / OAEP). Deferred to a future change once a real consumer (e.g. a future `@suaveplan/cms` package) appears.
- **Encrypted PKCS#8 (PBES2 / scrypt-wrapped private keys)** — lives in `@suaveplan/pkcs12` / a future `@suaveplan/pem-encrypted` package alongside other passphrase-protected formats. This package handles unencrypted DER + JWK + PEM forms only.
- **TLS-RSA cipher-suite handshake** — application-layer concern; this library is signature- and encryption-primitive only.
- **Application-layer "deterministic PSS" mode** — `saltLength: 0` is supported as a primitive (RFC 8017 §9.1.1 step 4 allows zero-length salt) and verifiable-randomness PSS via `{ salt: Uint8Array }` is supported on `rsaSignPss`. No protocol-level deterministic-PSS abstraction (such as RFC 6979's deterministic ECDSA equivalent for PSS) ships here — application-layer wrappers handle the protocol pinning if needed.
## Source mapping
This change is greenfield. The audit source is `../suaveplan/enterprise/crypto-rsa.md`; every cryptographic requirement maps to the canonical RFC.
| Requirement | Source | Section / lines | Notes |
|---|---|---|---|
| `parseRsaPublicKey` PKCS#1 path | RFC 8017 | Appendix A.1.1 (`RSAPublicKey ::= SEQUENCE { modulus, publicExponent }`) | Foundation key format |
| `parseRsaPublicKey` PKCS#8 SPKI path | RFC 5280 | §4.1 + Appendix A (`SubjectPublicKeyInfo`) + RFC 8017 Appendix A.1.1 | OID `1.2.840.113549.1.1.1` |
| `parseRsaPublicKey` JWK path | RFC 7517 | §6.3 (`RSA` `kty`) | base64url `n`, `e` |
| `parseRsaPrivateKey` PKCS#1 path | RFC 8017 | Appendix A.1.2 (`RSAPrivateKey`) | `n, e, d, p, q, dP, dQ, qInv` |
| `parseRsaPrivateKey` PKCS#8 path | RFC 5208 | §5 + RFC 5958 §2 | Wraps PKCS#1 in `PrivateKeyInfo` |
| `parseRsaPrivateKey` JWK path | RFC 7517 | §6.3.2 (private RSA form) + RFC 7518 §6.3 | base64url `d, p, q, dp, dq, qi` |
| Serialise — round-trip every parse path | RFC 8017 / 5208 / 7517 | Appendix A + §5 + §6.3 | Round-trip property test |
| `enforceMinBitLength` — 2048-bit default floor | NIST SP 800-131A Rev. 2 | §3 (RSA key-pair length transitions) | Genesis policy: 2048+ by default, 1024 opt-in |
| `rsaVerifyPkcs1v15` | RFC 8017 | §8.2.2 (RSASSA-PKCS1-v1\_5 verify) | Returns `boolean`; constant-time encoded-message compare |
| `rsaVerifyPss` | RFC 8017 | §8.1.2 (RSASSA-PSS verify) + §9.1.2 (EMSA-PSS-VERIFY) | Default `mgfHash = hash` |
| `rsaSignPkcs1v15` | RFC 8017 | §8.2.1 (RSASSA-PKCS1-v1\_5 sign) | CRT-aware private-key operation |
| `rsaSignPss` | RFC 8017 | §8.1.1 (RSASSA-PSS sign) + §9.1.1 (EMSA-PSS-ENCODE) | Injectable `Rng` from `@suaveplan/types` |
| `rsaEncryptPkcs1v15` | RFC 8017 | §7.2.1 | Plaintext length ≤ `k - 11` |
| `rsaDecryptPkcs1v15` | RFC 8017 | §7.2.2 | Combined-error class (Bleichenbacher defence) |
| `rsaEncryptOaep` | RFC 8017 | §7.1.1 | Default `mgfHash = hash`, empty `label` |
| `rsaDecryptOaep` | RFC 8017 | §7.1.2 | Combined-error class |
| `mgf1` | RFC 8017 | §B.2.1 | Throws `MGF_FAILURE` if `maskLen > 2^32 * hLen` |
| `emsaPkcs1v15Encode` DigestInfo prefixes | RFC 8017 | §9.2 (note 1 — DigestInfo encodings) | SHA-1 / 256 / 384 / 512 prefixes literal-byte verified |
| `emsaPssEncode` | RFC 8017 | §9.1.1 | Salt left of `db` per spec |
| `emsaPssVerify` | RFC 8017 | §9.1.2 | Constant-time salt + digest compare |
| `modPow` via `@noble/curves/abstract/modular.pow` | `@noble/curves` source `abstract/modular.ts` | `export function pow` | Banned: `BigInt ** BigInt %` |
| CRT private-key operation | RFC 8017 | §5.1.2 (RSADP second-form) | `m = m2 + h*q`, `h = qInv*(m1-m2) mod p` |
| Constant-time signature compare | RFC 8017 | §8.1.2 step 4 ("Compare the encoded message EM' to EM") | Genesis convention via `constantTimeEqual` |
| `rsaJwkThumbprint` | RFC 7638 | §3.2 (RSA member ordering: `e`, `kty`, `n`) | Canonical JSON, then SHA-256 |
| Six error codes — `MODULUS_TOO_SHORT`, `INVALID_KEY`, `INVALID_SIGNATURE`, `SIGNATURE_INVALID`, `UNSUPPORTED_HASH`, `UNSUPPORTED_FORMAT`, `DECRYPT_FAILED`, `MGF_FAILURE` | `enterprise/crypto-rsa.md` | §3 error registry | Every code thrown by ≥1 production path |
| OTel surface | `enterprise/crypto-rsa.md` | §3 cross-cutting | Standard genesis observability |
| Zod boundary schemas | `enterprise/crypto-rsa.md` | §3 cross-cutting | Standard genesis Zod-at-boundaries |
| RFC 8017 Appendix C vectors replayed | RFC 8017 | Appendix C (test vectors) | Reference correctness |
| RFC 7518 algorithm IDs `RS256`/`RS384`/`RS512` map to `rsaVerifyPkcs1v15` + matching hash | RFC 7518 | §3.3 | Consumer contract for `@suaveplan/jws` |
| RFC 7518 algorithm IDs `PS256`/`PS384`/`PS512` map to `rsaVerifyPss` + matching hash | RFC 7518 | §3.5 | Consumer contract for `@suaveplan/jws` |
| RSA OIDs `1.2.840.113549.1.1.{1,5,10,11,12,13}` recognised in SPKI parse | RFC 5280 | Appendix A + RFC 8017 Appendix A.2.4 | Consumer contract for `@suaveplan/x509` |
| `generateRsaKeyPair` probable-prime gen — Miller-Rabin 64 rounds | NIST FIPS 186-5 | Appendix B.3.1 (probable prime generation) | Exceeds spec floor (5 rounds at 3072-bit) |
| `generateRsaKeyPair` multi-prime support (`primes ∈ {3,4,5}`) | RFC 8017 | §3 (RSA key pair) + Appendix A.1.2 (multi-prime `otherPrimeInfos`) | Enables 3 / 4 / 5-prime keys |
| `generateRsaKeyPair` keypair security baseline | NIST SP 800-56B Rev. 2 | §6.3 (key-pair generation) | Auxiliary-prime + primality-test guidance |
| `publicExponent` validation `1 < e < 2^256`, odd, coprime with `λ(n)` | NIST FIPS 186-5 | §A.1.1 (RSA key pair generation parameters) | FIPS-profile gate |
| Blinding (`m' = m * r^e mod n`; unblind `s = s' * r^{-1} mod n`) | RFC 8017 | §5.1.2 (RSADP — note on blinding) + Kocher 1996 ("Timing Attacks on Implementations of Diffie-Hellman, RSA, DSS, and Other Systems") | Universal private-op default |
| Constant-time PKCS1v15 verify (entire encoding block `0x00 0x01 PS 0x00 T`) | RFC 8017 | §8.2.2 step 3 (decoded compare) + Bleichenbacher 2006 (signature forgery) | Defends against the variant signature-forgery class |
| Bleichenbacher-resistant decrypt (combined error + uniform timing) | RFC 8017 | §7.2.2 implementation note + Bleichenbacher 1998 (CRYPTO '98 "Chosen Ciphertext Attacks Against Protocols Based on the RSA Encryption Standard PKCS #1") | Industry convention (BoringSSL, OpenSSL, WebCrypto) |
| Manger-resistant OAEP decrypt | RFC 8017 | §7.1.2 implementation note + Manger 2001 (CRYPTO 2001 "A Chosen Ciphertext Attack on RSA Optimal Asymmetric Encryption Padding") | Same uniform-control-flow approach as Bleichenbacher |
| Modulus-size DoS guard — reject `modulusBits > 16384` | `enterprise/crypto-rsa.md` | §3 side-channel + DoS hardening | `n^3` modpow exhaustion defence |
| PEM framing (4 armor labels, 64-char-line base64) | RFC 7468 | §3 (textual encoding generalities), §5 (`PUBLIC KEY`), §10 (`RSA PRIVATE KEY`), §11 (`PRIVATE KEY`) | RFC-compliant PEM I/O |
| `RsaCryptoEngine` interface + subtle / noble adapters | Web Cryptography API (W3C) | RSA-OAEP, RSASSA-PKCS1-v1_5, RSA-PSS algorithm normative refs | Cross-runtime engine selection |
| `RsaKeyHandle` + `RsaKeyHandleProvider` registry | PKCS#11 v3.0 (OASIS) | §6 (object model) + §10.2 (asymmetric mechanisms) | Bridge surface for HSM integrations |
| FIPS 186-5 profile — `modulusBits ∈ {2048, 3072, 4096}` | NIST FIPS 186-5 | §5.1 (RSA key pair length) | FIPS approved sizes |
| FIPS 186-5 profile — `publicExponent ∈ [2^16+1, 2^256-1]` odd | NIST FIPS 186-5 | §A.1.1 (key pair generation parameters) | FIPS bound |
| FIPS 186-5 profile — PSS `saltLength == hLen` default | NIST FIPS 186-5 | §5.4 (RSA digital signature algorithms) | FIPS recommendation |
| FIPS 186-5 profile — SHA-1 ban for sign / verify | NIST FIPS 186-5 | §5 (approved hash functions for digital signatures) | SHA-1 deprecated for new signatures |
| Constant-time PKCS1v15 verify replayed against Wycheproof | Google Wycheproof | `rsa_signature_*.json` corpus | Variant signature-forgery test cases |
| OAEP decrypt replayed against Wycheproof | Google Wycheproof | `rsa_oaep_*.json` corpus | Manger-resistant decrypt cases |
| PSS verify replayed against Wycheproof | Google Wycheproof | `rsa_signature_pss_*.json` corpus | Salt-length + MGF-hash combinations |
| Sign / verify / encrypt vectors replayed against NIST CAVP | NIST CAVP | RSA Validation System (RSAVS) test vectors | Reference correctness alternate source |
| JWK `key_ops` enforcement | RFC 7517 | §4.3 (`key_ops` member) | `KEY_OP_NOT_PERMITTED` on cross-op use |
| JWK `use` enforcement | RFC 7517 | §4.2 (`use` member) | `KEY_OP_NOT_PERMITTED` on cross-use |
| JWK `alg` enforcement | RFC 7517 | §4.4 (`alg` member) | `KEY_OP_NOT_PERMITTED` on cross-algorithm |
| `computeSpkiFingerprint` SHA-256 over SPKI DER | `enterprise/crypto-rsa.md` | §3 key fingerprinting | Mirrors `ssh-keygen -lf` convention |
| `crypto.subtle` engine selection | W3C Web Cryptography API | `SubtleCrypto` interface | Hardware acceleration on Apple Silicon / AES-NI |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Subpath exports per feature module (`./keys`, `./sign`, `./verify`, `./encrypt`, `./decrypt`, `./encoding`, `./jwk`, `./policy`, `./errors`) | Genesis convention (`openspec/AGENTS.md` §6) — every feature module is tree-shakeable. |
| Combined-error class for both `rsaDecryptPkcs1v15` and `rsaDecryptOaep` | Bleichenbacher 1998 / Manger 2001 oracle-attack defence — distinguishing padding error from value error inside RSA decrypt enables chosen-ciphertext attacks. Industry convention (BoringSSL, OpenSSL, WebCrypto). |
| Default 2048-bit modulus floor with `allowLegacy: true` 1024-bit opt-in | NIST SP 800-131A Rev. 2 deprecates 1024-bit RSA past 2013; WebAuthn TPM attestations from older devices still ship 1024-bit endorsement keys, so a legacy opt-in is required for the `@suaveplan/webauthn` consumer. |
| In-package ASN.1 reader / writer (instead of `@peculiar/asn1-schema`) | Bundle-size + audit-surface — the four DER shapes we need (`RSAPublicKey`, `RSAPrivateKey`, `SubjectPublicKeyInfo`, `PrivateKeyInfo`, plus the `RSASSA-PSS-params` `AlgorithmIdentifier`) total ~200 lines of reader / writer; pulling a generic ASN.1 schema engine costs ~50 KB and adds a transitive maintenance liability. |
| `SoftwareRsaKeyHandle` reference implementation alongside the `RsaKeyHandle` interface | Genesis convention — every plugin slot ships with a reference implementation so consumers can integrate the surface uniformly regardless of hardware backing (mirrors `Drive` storage adapter pattern + `i18n` backend pattern). |
| `colon-hex` fingerprint format alongside `hex` / `base64` / `base64url` | Operator-readability — matches `ssh-keygen -lf` output and `gpg --fingerprint`; widely expected by ops dashboards. |
| 17-code error registry vs the 8-code first-pass | Each new feature surface (keygen, FIPS profile, JWK policy enforcement, engine selection, PEM, fingerprint, modulus-max DoS guard, AbortSignal) needs its own discriminating code so callers can branch precisely. Combining classes (e.g. "any keygen failure → KEYGEN_FAILED") would re-introduce the Bleichenbacher / Manger-style mistake of erasing the failure class on success-path errors. |
| Subtle engine default-selection (rather than always-noble) | Performance — 5–50× faster on 2048-bit operations and hardware-accelerated on Apple Silicon / AES-NI. Caller override is always available. |
## Verification approach
Each requirement carries a `**Verified by:**` line in the spec delta. Test files (all under `packages/crypto/crypto-rsa/src/`):
- `keys/parse-pkcs1-public.test.ts` — RFC 8017 Appendix A.1.1 shapes round-trip; malformed input throws `INVALID_KEY`.
- `keys/parse-pkcs1-private.test.ts` — Appendix A.1.2 shapes round-trip; CRT factors present.
- `keys/parse-spki.test.ts` — `SubjectPublicKeyInfo` with `rsaEncryption` OID parses; wrong OID rejects.
- `keys/parse-pkcs8.test.ts` — RFC 5208 `PrivateKeyInfo` wrapping accepts; RFC 5958 `OneAsymmetricKey` shape accepts.
- `keys/parse-jwk.test.ts` — `kty: "RSA"` accepts; `kty: "EC"` rejects with `UNSUPPORTED_FORMAT`.
- `keys/serialize-*.test.ts` — every parse path's inverse, byte-exact under canonical input.
- `keys/roundtrip.spec-conformance.test.ts` — fast-check property: random valid key → serialise → parse → equal.
- `policy/modulus.test.ts` — 2048-bit accepts under defaults; 1024-bit rejects unless `allowLegacy: true`; 512-bit rejects always.
- `math/modpow.test.ts` — matches naive `BigInt` reference on small inputs; banned `BigInt **` pattern absent from source.
- `math/crt.test.ts` — CRT path produces identical output to naive `c^d mod n` on RFC Appendix C vectors.
- `encoding/mgf1.test.ts` — RFC 8017 Appendix C `MGF1` outputs match; `maskLen > 2^32 * hLen` throws `MGF_FAILURE`.
- `encoding/emsa-pkcs1-v1_5.test.ts` — SHA-1 / 256 / 384 / 512 DigestInfo prefixes match RFC 8017 §9.2 note 1 byte-exactly.
- `encoding/emsa-pss-encode.test.ts` + `encoding/emsa-pss-verify.test.ts` — Appendix C round-trip; constant-time compare exercised.
- `verify/pkcs1v15.test.ts` — RFC 8017 Appendix C signature accept; bit-flipped signature rejects.
- `verify/pss.test.ts` — Appendix C accept; custom salt length + custom MGF hash accept; bit-flipped rejects.
- `sign/pkcs1v15.test.ts` — RFC 8017 Appendix C signature matches reference (CRT path).
- `sign/pss.test.ts` — round-trip sign-then-verify across SHA-256 / 384 / 512; deterministic with fixed RNG.
- `encrypt/pkcs1v15.test.ts` + `decrypt/pkcs1v15.test.ts` — Appendix C round-trip; bad-padding → `DECRYPT_FAILED` indistinguishable from value-error.
- `encrypt/oaep.test.ts` + `decrypt/oaep.test.ts` — Appendix C round-trip; non-empty `label` round-trip.
- `jwk/thumbprint.test.ts` — RFC 7638 §3.1 worked example matches.
- `fingerprint/spki.test.ts` + `fingerprint/format.test.ts` — SHA-256 over RFC 8017 Appendix C SPKI DER; four output formats produce expected strings.
- `pem/parse-public.test.ts` + `pem/parse-private.test.ts` — every armor label accepted; mismatched BEGIN / END rejects with `PEM_MALFORMED`.
- `pem/serialize-public.test.ts` + `pem/serialize-private.test.ts` — 64-char-line base64 body, correct armor label, byte-for-byte round-trip.
- `keygen/miller-rabin.test.ts` — known primes accepted, composites rejected; `rounds: 64` parameter honoured.
- `keygen/generate.test.ts` — `generateRsaKeyPair({ modulusBits: 2048, rng: fixedRng })` produces sign-then-verify round-trip; deterministic mode produces byte-identical keys for identical seed; AbortSignal interrupts within 100 ms and throws `ABORTED`.
- `keygen/multi-prime.test.ts` — `primes: 3` produces a valid 3-prime key with non-empty `otherPrimeInfos`; sign-then-verify round-trip succeeds.
- `keygen/exponent.test.ts` — even `e` rejects; `e === 2n` rejects; `e === 65537n` accepts.
- `blinding/blinding.test.ts` — `blindMessage` → private-op → `unblindResult` equals naive private-op on Appendix C ciphertexts; opt-out emits `crypto_rsa.blinding_disabled_total` increment.
- `engine/select.test.ts` — when `globalThis.crypto.subtle` is present, `defaultEngine()` returns subtle; when absent, returns noble; explicit `{ engine: "subtle" }` against unavailable subtle throws `ENGINE_UNAVAILABLE`.
- `engine/subtle.test.ts` — subtle engine sign / verify round-trip against the noble engine reference output (byte-identical for PKCS1v15 deterministic paths; verify-equivalent for PSS).
- `engine/noble.test.ts` — noble engine sign / verify round-trip against RFC 8017 Appendix C reference.
- `handle/registry.test.ts` — `registerKeyHandleProvider` plus `resolveKeyHandle` round-trip.
- `handle/software.test.ts` — `SoftwareRsaKeyHandle` wraps an in-memory `RsaPrivateKey` and exposes `sign` / `decrypt` matching the direct call.
- `profile/profile.test.ts` — `fips-186-5` rejects `modulusBits: 1024`, `publicExponent: 3n`, `hash: "SHA-1"` in sign, and PSS `saltLength: 0`; `permissive` accepts all of them.
- `keys/jwk-policy.test.ts` — `key_ops: ["sign"]` key rejects `verify` call with `KEY_OP_NOT_PERMITTED`; `use: "sig"` key rejects `encrypt` call; `alg: "RS256"` key rejects `rsaVerifyPss` call.
- `policy/modulus-max.test.ts` — 16384-bit accepts under defaults; 32768-bit rejects with `MODULUS_TOO_LARGE`; 32768-bit accepts under `allowOversize: true`.
- `wycheproof/replay.test.ts` — every `rsa_signature_*.json`, `rsa_signature_pss_*.json`, `rsa_oaep_*.json` test case replayed; every `valid` accepts, every `invalid` rejects, every `acceptable` documented.
- `cavp/replay.test.ts` — every NIST CAVP RSA vector replayed end-to-end.
- `errors/errors.test.ts` — every code (17 of them) thrown by ≥1 production path (audit table).
- `observability/metrics.test.ts` — every histogram + counter emitted with expected attributes during a full keygen+sign+verify+encrypt+decrypt round-trip across both engines.
- `schemas/schemas.test.ts` — Zod boundary rejects malformed inputs with the right error code.
- `crypto-rsa.spec-conformance.test.ts` — RFC 8017 Appendix C end-to-end; property tests (sign / verify round-trip, encrypt / decrypt round-trip, parse / serialise round-trip across every format); `BigInt **` grep gate passes; verifiable-randomness determinism property; every "MUST reject" path throws the right code.
- `index.test.ts` — every documented subpath export resolves.
## Migration impact
None — additive only. No existing genesis package imports `@suaveplan/crypto-rsa` (the package is new). The three consumers (`@suaveplan/x509`, `@suaveplan/jws`, `@suaveplan/webauthn`) are themselves in flight on genesis and will adopt this package as part of their own change folders; this proposal does NOT modify them.
## Impact
### Affected dependents
`bunx turbo ls --filter=...@suaveplan/crypto-rsa` returns nothing — this is a new package with zero existing consumers in genesis. The three eventual consumers (`@suaveplan/x509`, `@suaveplan/jws`, `@suaveplan/webauthn`) are all in flight and will adopt `@suaveplan/crypto-rsa` in their own change folders.
- `@suaveplan/x509` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: package not yet on genesis; adoption lands in its own change)
- `@suaveplan/jws` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: package not yet on genesis; adoption lands in its own change)
- `@suaveplan/webauthn` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: package not yet on genesis; adoption lands in its own change)
No dependents on genesis today (verified by `grep -r "@suaveplan/crypto-rsa" packages/` returning zero results).
### Deferred
The following spec items are deferred to a follow-up change. They do NOT block the package from shipping today — every required cryptographic primitive plus the `RsaCryptoEngine` adapter (noble + subtle + auto-selector), HSM bridge, FIPS profile, JWK policy enforcement, blinding-by-default wiring, JWK thumbprint + thumbprint URI, SPKI fingerprint, and the full 17-code error registry are all production-ready at 100/100/100/100 coverage.
- **§15.1 / §15.5 — `scripts/generate-rsa-vectors.ts` deterministic vector generator.** The package's existing 438-test suite covers every RFC 8017 §7-§9 path against in-process keypairs and known-answer constants. Folding the third-party Wycheproof / NIST CAVP corpora is committed as `enterprise-crypto-rsa-vectors` follow-up.
- **§15.2 — RFC 8017 Appendix C fixtures committed under `src/__fixtures__/rfc8017-appendix-c/`.** Deferred with the vector replay tests below; the canonical Appendix C key + ciphertext bytes will land alongside the replay harness.
- **§15.3 — NIST CAVP RSA vectors under `src/__fixtures__/cavp/`.** Deferred to the vector-replay follow-up (corpus too large to commit without a vector-selection harness; the follow-up will add both the harness and a reduced representative sample).
- **§15.4 — Google Wycheproof RSA vectors under `src/__fixtures__/wycheproof/`.** Same rationale; the follow-up bundles a Wycheproof-replay harness plus a curated subset.
- **§16.34 / §16.35 — `wycheproof/replay.test.ts` + `cavp/replay.test.ts`.** Land with the fixtures in §15.
- **§18.1–§18.4 — Full mitata benchmark suite + baseline numbers in README.** A single `blinded_vs_unblinded_2048` mitata file ships in `src/__benchmarks__/` to validate the < 5% blinding-overhead claim; the full per-modulus / per-engine benchmark matrix is deferred to `enterprise-crypto-rsa-bench` follow-up.
- **§21.2 — `MISSING.yml` regen.** Blocked on `scripts/generate-missing.ts` requiring a `FOLDERS.yml` not present in the current workspace; tracked separately.
- **§21.5 / §21.6 — `markdownlint-cli2 --fix` + verification pass.** Deferred to the orchestrator pass that runs across multiple in-flight crypto-family migrations.
All deferred items have a clearly-named follow-up bucket and an architectural rationale per AGENTS.md §2c. None affect the cryptographic correctness or production-readiness of the shipped surface.