Initial Commit
This commit is contained in:
103
openspec/archive/2026-05-10-crypto-curve-p384-p521/proposal.md
Normal file
103
openspec/archive/2026-05-10-crypto-curve-p384-p521/proposal.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# `@suaveplan/crypto-curve` — Add P-384 (`secp384r1`) and P-521 (`secp521r1`) primitives
|
||||
|
||||
**Package**: `@suaveplan/crypto-curve` (extension)
|
||||
**Status**: PROPOSED
|
||||
**Type**: capability-extension
|
||||
**Date**: 2026-05-10
|
||||
**Tier**: universal
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/cbor` ships RFC 9052 COSE_Sign1 verify but the ES384 / ES512
|
||||
algorithm branches throw `CborError(UNEXPECTED_TYPE)` with the message
|
||||
*"not implemented in @suaveplan/crypto-curve yet"* because the curve
|
||||
primitives were not previously exposed. Downstream consumers (WebAuthn
|
||||
attestation, federation tokens, FIDO2 device certificates) need both
|
||||
algorithms to interoperate with mainstream issuers — Apple's WebAuthn
|
||||
attestation, in particular, signs with ES384 in some configurations.
|
||||
|
||||
`@noble/curves@1.9.7` (already installed across the workspace) exposes
|
||||
`p384` and `p521` with the same surface contract as `p256`. The work is
|
||||
purely additive — mirroring the existing `p256` per-primitive layout for
|
||||
two more curves and threading them through to the COSE verifier.
|
||||
|
||||
## What changes
|
||||
|
||||
### New primitives in `@suaveplan/crypto-curve`
|
||||
|
||||
#### P-384
|
||||
|
||||
- ADDED: `primitives/p384/p384.ts` — re-exports the select layer.
|
||||
- ADDED: `primitives/p384/p384.select/p384.select.ts` — selector pinned
|
||||
to the noble implementation (generated forwarder, mirroring p256).
|
||||
- ADDED: `primitives/p384/impl/p384.noble/p384.noble.ts` — noble-backed
|
||||
`getPublicKey`, `sign`, `verify`, `randomPrivateKey`.
|
||||
- ADDED: `primitives/p384/impl/p384.native/p384.native.ts` —
|
||||
`PrimitiveNotImplemented` stubs matching the p256 native pattern.
|
||||
|
||||
#### P-521
|
||||
|
||||
- ADDED: `primitives/p521/p521.ts` and full mirror of the p384 layout
|
||||
(`p521.select`, `impl/p521.noble`, `impl/p521.native`).
|
||||
|
||||
#### Barrel + subpath exports
|
||||
|
||||
- ADDED: `export * as p384 from "./primitives/p384/p384.js"` to root barrel.
|
||||
- ADDED: `export * as p521 from "./primitives/p521/p521.js"` to root barrel.
|
||||
- ADDED: `./p384` and `./p521` subpath exports in `package.json`.
|
||||
- ADDED: corresponding entries in `vite.config.ts` external list (so
|
||||
consumers can tree-shake to the individual primitive).
|
||||
|
||||
### COSE wiring in `@suaveplan/cbor`
|
||||
|
||||
- MODIFIED: `verifyCoseSign1` switch — `ES384` and `ES512` branches call
|
||||
`p384.verify(sig, sha384(tbs), pk)` and `p521.verify(sig, sha512(tbs), pk)`
|
||||
respectively (replacing the `UNEXPECTED_TYPE` placeholder throws).
|
||||
- MODIFIED: `ec2ToCompressedPoint` — generalised to accept coordinate
|
||||
length (32 / 48 / 66) so the same helper compresses P-256 / P-384 / P-521.
|
||||
|
||||
### Dependencies
|
||||
|
||||
No new dependencies. `@noble/curves@1.9.7` is the catalog-pinned version
|
||||
that ships both `p384` and `p521` subpath exports.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- WebCrypto-native impls — the existing `p256.native` stub pattern
|
||||
(`PrimitiveNotImplemented`) is preserved for both new curves; an opt-in
|
||||
WebCrypto path lands in a separate change when needed.
|
||||
- COSE Sign1 / Mac0 / Encrypt0 surfaces beyond the alg switch — already
|
||||
wired generically and need no changes.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Requirement | Source | Notes |
|
||||
|---|---|---|
|
||||
| P-384 ECDSA | FIPS 186-4 §6 + NIST SP 800-186 | `@noble/curves/p384` |
|
||||
| P-521 ECDSA | FIPS 186-4 §6 + NIST SP 800-186 | `@noble/curves/p521` |
|
||||
| COSE ES384 / ES512 | RFC 9053 §2.1 | Already mapped in `alg-map.ts` |
|
||||
| Per-primitive selector layout | Existing crypto-curve convention | mirrors p256 |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `packages/crypto/crypto-curve/src/primitives/p384/p384.test.ts` — noble
|
||||
- native impl: sign / verify round-trip, signature length, tampered-sig
|
||||
rejection, native `PrimitiveNotImplemented` throw.
|
||||
- `packages/crypto/crypto-curve/src/primitives/p521/p521.test.ts` — same.
|
||||
- `packages/vault/cbor/src/cose/sign1.test.ts` — extend to cover ES384
|
||||
and ES512 round-trip via the new primitives.
|
||||
|
||||
## Migration impact
|
||||
|
||||
Additive. No existing consumers break — the deferred branches in cbor
|
||||
were already throwing.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
`@suaveplan/cbor` — ES384 / ES512 paths become operational; previously
|
||||
threw `UNEXPECTED_TYPE`.
|
||||
|
||||
For all other packages: no behavioural change. The new primitives are
|
||||
opt-in subpath imports.
|
||||
@@ -0,0 +1,129 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: crypto-curve (crypto-curve-p384-p521)
|
||||
|
||||
**Package**: `@suaveplan/crypto-curve`
|
||||
**Status**: PROPOSED — additive delta; folded into canonical spec on archive.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: NIST P-384 (secp384r1) ECDSA primitive
|
||||
|
||||
The package SHALL export a `p384` namespace from the root barrel and a
|
||||
matching `./p384` subpath export, providing `getPublicKey`, `sign`,
|
||||
`verify`, and `randomPrivateKey`. Private keys SHALL be 48 bytes,
|
||||
compressed public keys SHALL be 49 bytes, and compact ECDSA signatures
|
||||
SHALL be 96 bytes. Callers SHALL pre-hash messages; the module operates
|
||||
on digests. The selector SHALL forward to the noble-backed implementation
|
||||
by default; the native implementation SHALL throw
|
||||
`PrimitiveNotImplemented("p384")` until a WebCrypto bridge lands.
|
||||
|
||||
#### Scenario: P-384 sign + verify round-trip
|
||||
|
||||
- **GIVEN** a freshly-generated P-384 private key and 48-byte digest
|
||||
- **WHEN** `verify(sign(digest, sk), digest, getPublicKey(sk))` is computed
|
||||
- **THEN** the result is `true`
|
||||
|
||||
#### Scenario: P-384 verify rejects tampered signature
|
||||
|
||||
- **WHEN** a single byte of a valid P-384 signature is XORed with `0xff`
|
||||
- **THEN** `verify` returns `false`
|
||||
|
||||
#### Scenario: P-384 verify returns false on malformed inputs
|
||||
|
||||
- **WHEN** `verify` is invoked with non-`Uint8Array` inputs or empty
|
||||
buffers
|
||||
- **THEN** the call returns `false` (does not throw)
|
||||
|
||||
**Verified by:**
|
||||
`packages/crypto/crypto-curve/src/primitives/p384/p384.test.ts::"sign + verify round-trips"`
|
||||
AND `…::"verify returns false for tampered signature (no throw)"`
|
||||
AND `…::"verify returns false for bogus input (caught)"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: NIST P-521 (secp521r1) ECDSA primitive
|
||||
|
||||
The package SHALL export a `p521` namespace from the root barrel and a
|
||||
matching `./p521` subpath export, providing `getPublicKey`, `sign`,
|
||||
`verify`, and `randomPrivateKey`. Private keys SHALL be 66 bytes (521-bit
|
||||
scalar left-padded to a byte boundary), compressed public keys SHALL be
|
||||
67 bytes, and compact ECDSA signatures SHALL be 132 bytes. Callers SHALL
|
||||
pre-hash messages; the module operates on digests. The selector SHALL
|
||||
forward to the noble-backed implementation by default; the native
|
||||
implementation SHALL throw `PrimitiveNotImplemented("p521")`.
|
||||
|
||||
#### Scenario: P-521 sign + verify round-trip
|
||||
|
||||
- **GIVEN** a freshly-generated P-521 private key and 64-byte digest
|
||||
- **WHEN** `verify(sign(digest, sk), digest, getPublicKey(sk))` is computed
|
||||
- **THEN** the result is `true` and the signature is 132 bytes long
|
||||
|
||||
#### Scenario: P-521 verify rejects tampered signature
|
||||
|
||||
- **WHEN** a single byte of a valid P-521 signature is XORed with `0xff`
|
||||
- **THEN** `verify` returns `false`
|
||||
|
||||
#### Scenario: P-521 verify returns false on malformed inputs
|
||||
|
||||
- **WHEN** `verify` is invoked with non-`Uint8Array` inputs or empty
|
||||
buffers
|
||||
- **THEN** the call returns `false` (does not throw)
|
||||
|
||||
**Verified by:**
|
||||
`packages/crypto/crypto-curve/src/primitives/p521/p521.test.ts::"sign + verify round-trips"`
|
||||
AND `…::"verify returns false for tampered signature (no throw)"`
|
||||
AND `…::"verify returns false for bogus input (caught)"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Native stubs preserve layout
|
||||
|
||||
The package SHALL ship native-backend placeholder modules
|
||||
(`primitives/p384/impl/p384.native/`,
|
||||
`primitives/p521/impl/p521.native/`) whose every exported function throws
|
||||
`PrimitiveNotImplemented(<curve>)`. The placeholders SHALL match the
|
||||
existing `p256.native` pattern so the selector switch is mechanical when
|
||||
a real native implementation lands.
|
||||
|
||||
#### Scenario: P-384 native stub throws on every operation
|
||||
|
||||
- **WHEN** any of `getPublicKey`, `sign`, `verify`, or `randomPrivateKey`
|
||||
is invoked on the native module
|
||||
- **THEN** the call throws `PrimitiveNotImplemented("p384")`
|
||||
|
||||
#### Scenario: P-521 native stub throws on every operation
|
||||
|
||||
- **WHEN** the same operations are invoked on the P-521 native module
|
||||
- **THEN** the call throws `PrimitiveNotImplemented("p521")`
|
||||
|
||||
**Verified by:**
|
||||
`…/p384/p384.test.ts::"every operation throws PrimitiveNotImplemented"`
|
||||
AND `…/p521/p521.test.ts::"every operation throws PrimitiveNotImplemented"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports for P-384 and P-521
|
||||
|
||||
The package SHALL declare `./p384` and `./p521` entries in
|
||||
`package.json` `exports` so consumers tree-shake to the individual
|
||||
primitive. The `vite.config.ts` build entry list SHALL include both new
|
||||
primitives so the generated `dist/` tree carries the expected `.d.ts` and
|
||||
`.js` files at the published path.
|
||||
|
||||
#### Scenario: P-384 subpath resolves
|
||||
|
||||
- **WHEN** a consumer imports `@suaveplan/crypto-curve/p384`
|
||||
- **THEN** the import resolves with no `Module not found` error and
|
||||
exposes `getPublicKey`, `sign`, `verify`, `randomPrivateKey`
|
||||
|
||||
#### Scenario: P-521 subpath resolves
|
||||
|
||||
- **WHEN** a consumer imports `@suaveplan/crypto-curve/p521`
|
||||
- **THEN** the import resolves with no `Module not found` error and
|
||||
exposes the same four symbols
|
||||
|
||||
**Verified by:** TypeScript compile of consumers (e.g.
|
||||
`packages/vault/cbor/src/cose/sign1.ts`) that import these subpaths.
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
44
openspec/archive/2026-05-10-crypto-curve-p384-p521/tasks.md
Normal file
44
openspec/archive/2026-05-10-crypto-curve-p384-p521/tasks.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Tasks — `@suaveplan/crypto-curve` — add P-384 + P-521
|
||||
|
||||
## 1. P-384 primitive
|
||||
|
||||
- [x] 1.1 `src/primitives/p384/p384.ts` (re-export barrel)
|
||||
- [x] 1.2 `src/primitives/p384/p384.select/p384.select.ts` (generated forwarder)
|
||||
- [x] 1.3 `src/primitives/p384/impl/p384.noble/p384.noble.ts` (noble-backed impl)
|
||||
- [x] 1.4 `src/primitives/p384/impl/p384.native/p384.native.ts` (PrimitiveNotImplemented stubs)
|
||||
- [x] 1.5 `src/primitives/p384/p384.test.ts` (noble round-trip + native stub coverage)
|
||||
- [x] 1.6 Sibling `.md` for every `.ts` (≥200 words)
|
||||
|
||||
## 2. P-521 primitive
|
||||
|
||||
- [x] 2.1 `src/primitives/p521/p521.ts`
|
||||
- [x] 2.2 `src/primitives/p521/p521.select/p521.select.ts`
|
||||
- [x] 2.3 `src/primitives/p521/impl/p521.noble/p521.noble.ts`
|
||||
- [x] 2.4 `src/primitives/p521/impl/p521.native/p521.native.ts`
|
||||
- [x] 2.5 `src/primitives/p521/p521.test.ts`
|
||||
- [x] 2.6 Sibling `.md` for every `.ts`
|
||||
|
||||
## 3. Barrel + exports
|
||||
|
||||
- [x] 3.1 `src/index.ts` — add `export * as p384` / `export * as p521`
|
||||
- [x] 3.2 `package.json` — add `./p384` and `./p521` subpath exports
|
||||
- [x] 3.3 `vite.config.ts` — add new entry points
|
||||
|
||||
## 4. cbor wiring
|
||||
|
||||
- [x] 4.1 `cose/sign1.ts` — replace ES384/ES512 placeholder throws with real p384 / p521 verify paths
|
||||
- [x] 4.2 `cose/sign1.ts` — generalise `ec2ToCompressedPoint` to accept coordinate length (32 / 48 / 66)
|
||||
- [x] 4.3 `cose/sign1.test.ts` — replace not-implemented test with ES384 + ES512 end-to-end round-trips
|
||||
- [x] 4.4 `vite.config.ts` — add `@suaveplan/crypto-curve/p384` and `/p521` to externals
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [x] 5.1 `bunx turbo lint typecheck test:coverage build --filter=@suaveplan/crypto-curve` — green
|
||||
- [x] 5.2 `bunx turbo lint typecheck test:coverage build --filter=@suaveplan/cbor` — green
|
||||
- [x] 5.3 Coverage 100/100/100/100 on both packages
|
||||
- [x] 5.4 681 cbor tests pass
|
||||
|
||||
## 6. Archive
|
||||
|
||||
- [x] 6.1 Fold spec delta into `openspec/specs/crypto-curve/spec.md`
|
||||
- [x] 6.2 Move `openspec/changes/crypto-curve-p384-p521/` → `openspec/archive/2026-05-10-crypto-curve-p384-p521/`
|
||||
Reference in New Issue
Block a user