Initial Commit
This commit is contained in:
7
packages/security-stack/crypto/crypto-curve/CHANGELOG.md
Normal file
7
packages/security-stack/crypto/crypto-curve/CHANGELOG.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# @suaveplan/crypto-curve
|
||||
|
||||
## 0.0.0
|
||||
|
||||
- Initial scaffold under `crypto-primitive-replaceability` Phase 0.
|
||||
- secp256k1, ed25519, x25519, p256 primitives with per-primitive selector layout.
|
||||
- Noble-backed impls (`@noble/curves`); native stubs throw `PrimitiveNotImplemented`.
|
||||
103
packages/security-stack/crypto/crypto-curve/README.md
Normal file
103
packages/security-stack/crypto/crypto-curve/README.md
Normal file
File diff suppressed because one or more lines are too long
125
packages/security-stack/crypto/crypto-curve/package.json
Normal file
125
packages/security-stack/crypto/crypto-curve/package.json
Normal file
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"name": "@suaveplan/crypto-curve",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "Elliptic-curve primitives with per-primitive selector layout: secp256k1, ed25519, x25519, p256, p384, p521. Noble + native impls selected at build time via crypto.config.ts. Zero runtime mocks; thin wrappers over @noble/curves.",
|
||||
"license": "MIT",
|
||||
"author": "Suaveplan Team",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./secp256k1": {
|
||||
"import": {
|
||||
"types": "./dist/primitives/secp256k1/secp256k1.d.ts",
|
||||
"default": "./dist/primitives/secp256k1/secp256k1.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/primitives/secp256k1/secp256k1.d.ts",
|
||||
"default": "./dist/primitives/secp256k1/secp256k1.js"
|
||||
}
|
||||
},
|
||||
"./ed25519": {
|
||||
"import": {
|
||||
"types": "./dist/primitives/ed25519/ed25519.d.ts",
|
||||
"default": "./dist/primitives/ed25519/ed25519.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/primitives/ed25519/ed25519.d.ts",
|
||||
"default": "./dist/primitives/ed25519/ed25519.js"
|
||||
}
|
||||
},
|
||||
"./x25519": {
|
||||
"import": {
|
||||
"types": "./dist/primitives/x25519/x25519.d.ts",
|
||||
"default": "./dist/primitives/x25519/x25519.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/primitives/x25519/x25519.d.ts",
|
||||
"default": "./dist/primitives/x25519/x25519.js"
|
||||
}
|
||||
},
|
||||
"./p256": {
|
||||
"import": {
|
||||
"types": "./dist/primitives/p256/p256.d.ts",
|
||||
"default": "./dist/primitives/p256/p256.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/primitives/p256/p256.d.ts",
|
||||
"default": "./dist/primitives/p256/p256.js"
|
||||
}
|
||||
},
|
||||
"./p384": {
|
||||
"import": {
|
||||
"types": "./dist/primitives/p384/p384.d.ts",
|
||||
"default": "./dist/primitives/p384/p384.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/primitives/p384/p384.d.ts",
|
||||
"default": "./dist/primitives/p384/p384.js"
|
||||
}
|
||||
},
|
||||
"./p521": {
|
||||
"import": {
|
||||
"types": "./dist/primitives/p521/p521.d.ts",
|
||||
"default": "./dist/primitives/p521/p521.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/primitives/p521/p521.d.ts",
|
||||
"default": "./dist/primitives/p521/p521.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage --passWithNoTests",
|
||||
"lint": "bunx biome check src/",
|
||||
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
|
||||
"format": "bunx biome format --write --max-diagnostics 500 src/",
|
||||
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
|
||||
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
|
||||
"crypto:kat": "vitest run --passWithNoTests -t 'RFC|NIST|Wycheproof|vector|KAT'",
|
||||
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/curves": "catalog:crypto",
|
||||
"@suaveplan/crypto-hash": "workspace:^",
|
||||
"@suaveplan/crypto-utils": "workspace:^",
|
||||
"@suaveplan/error": "workspace:^",
|
||||
"@suaveplan/types": "workspace:^",
|
||||
"tslib": "catalog:build",
|
||||
"zod": "catalog:forms"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@suaveplan/testing": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-istanbul": "catalog:testing",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-dts": "catalog:build",
|
||||
"vitest": "catalog:testing"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npmjs.fq.io/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# crypto-curve error codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`codes.ts` registers the stable error-code namespace for `@suaveplan/crypto-curve` with `defineCodes` from `@suaveplan/error`. Every error thrown by this package routes through one of these codes so consumers can write stable code-based dispatch and so observability backends see a finite tag set.
|
||||
|
||||
The codes are namespaced under `CRYPTO.CURVE` to keep them disjoint from sibling crypto packages (`CRYPTO.CIPHER.*`, `CRYPTO.HASH.*`, `CRYPTO.KEYS.*`).
|
||||
|
||||
## Features
|
||||
|
||||
- `CRYPTO_CURVE_CODES` — frozen registry mapping symbolic names to wire codes.
|
||||
- `CryptoCurveCode` — string union of all wire codes for type-safe dispatch.
|
||||
- Adding a code requires editing this file and re-running typecheck — there is no runtime path that injects a code.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CRYPTO_CURVE_CODES, CryptoCurveError } from "@suaveplan/crypto-curve";
|
||||
|
||||
throw new CryptoCurveError(
|
||||
CRYPTO_CURVE_CODES.RECOVERY_BYTE_MISSING,
|
||||
"error.crypto.curve.recovery-byte-missing",
|
||||
{ context: { signatureLength: 64 } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CRYPTO_CURVE_CODES, type CryptoCurveCode } from "@suaveplan/crypto-curve";
|
||||
|
||||
function isRecoverableError(code: CryptoCurveCode): boolean {
|
||||
return (
|
||||
code === CRYPTO_CURVE_CODES.RECOVERY_BYTE_MISSING ||
|
||||
code === CRYPTO_CURVE_CODES.RECOVERY_BYTE_INVALID
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CRYPTO_CURVE_CODES.RECOVERY_BYTE_MISSING: "CRYPTO.CURVE.RECOVERY_BYTE_MISSING"` — thrown when `recoverPublicKey` receives a 64-byte signature without a recovery byte.
|
||||
- `CRYPTO_CURVE_CODES.RECOVERY_BYTE_INVALID: "CRYPTO.CURVE.RECOVERY_BYTE_INVALID"` — thrown when the recovery byte is not in `{0, 1}` (some libraries also emit `2` and `3` for compressed-form recovery, which are explicitly rejected here).
|
||||
- `CRYPTO_CURVE_CODES.RECOVERED_KEY_MISMATCH: "CRYPTO.CURVE.RECOVERED_KEY_MISMATCH"` — reserved for callers that need to surface the comparison-failure error rather than receiving `false` from `verifyWithRecovery`. Today the public API does not throw this code (it returns `false` instead) but it is registered for future use.
|
||||
- `CryptoCurveCode` — type alias of the union of all values above.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The codes are **stable wire identifiers** — once shipped they cannot be renamed or removed. Adding a new code is permitted; rename-or-delete requires a new spec change with a deprecation cycle. The codes use `SCREAMING_SNAKE_CASE` to match the suaveplan-wide convention; they include the package prefix to disambiguate when multiple packages route through a shared OTel exporter or log aggregator.
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineCodes } from "@suaveplan/error";
|
||||
|
||||
export const CRYPTO_CURVE_CODES = defineCodes("CRYPTO.CURVE", [
|
||||
"RECOVERY_BYTE_MISSING",
|
||||
"RECOVERY_BYTE_INVALID",
|
||||
"RECOVERED_KEY_MISMATCH",
|
||||
] as const);
|
||||
|
||||
export type CryptoCurveCode =
|
||||
(typeof CRYPTO_CURVE_CODES)[keyof typeof CRYPTO_CURVE_CODES];
|
||||
@@ -0,0 +1 @@
|
||||
export { CRYPTO_CURVE_CODES, type CryptoCurveCode } from "./codes.js";
|
||||
@@ -0,0 +1,63 @@
|
||||
# CryptoCurveError
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`crypto-curve-error.ts` defines the `CryptoCurveError` class used by every error path in `@suaveplan/crypto-curve`. It is a thin alias of `SuaveplanError<CryptoCurveCode>` from `@suaveplan/error` — this means it inherits the full error substrate (cause-chain traversal, OTel span events, structured-logger redaction, i18n message-key resolution, and the wire-format JSON / binary serialiser) without any per-package re-implementation.
|
||||
|
||||
Routing every thrown error through this single class is what allows downstream consumers to write `instanceof CryptoCurveError` for package-level dispatch and `error.code === CRYPTO_CURVE_CODES.X` for code-level dispatch, simultaneously, with full type narrowing.
|
||||
|
||||
## Features
|
||||
|
||||
- Extends `SuaveplanError<CryptoCurveCode>` so the `code` field is typed as a finite union, not an arbitrary string.
|
||||
- Carries a `cause` chain — the secp256k1 ECDSA recovery path wraps `@noble/curves` errors as `cause` so the original message is preserved in logs.
|
||||
- Auto-instruments OTel span events when an active span exists (no caller setup required).
|
||||
- Participates in the wire-format error serialiser (Zod-validated JSON envelope) so errors round-trip across process boundaries cleanly.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CRYPTO_CURVE_CODES, CryptoCurveError } from "@suaveplan/crypto-curve";
|
||||
|
||||
throw new CryptoCurveError(
|
||||
CRYPTO_CURVE_CODES.RECOVERY_BYTE_INVALID,
|
||||
"error.crypto.curve.recovery-byte-invalid",
|
||||
{ context: { recoveryByte: 7 } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CryptoCurveError } from "@suaveplan/crypto-curve";
|
||||
|
||||
try {
|
||||
pubKey = recoverPublicKey(sig, digest);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoCurveError) {
|
||||
switch (err.code) {
|
||||
case "CRYPTO.CURVE.RECOVERY_BYTE_MISSING":
|
||||
throw new Error("Old wallet format — please re-sign with --recovery flag");
|
||||
case "CRYPTO.CURVE.RECOVERY_BYTE_INVALID":
|
||||
throw new Error("Signature corrupt or wrong format");
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CryptoCurveError extends SuaveplanError<CryptoCurveCode>` — public class, exported from the package root.
|
||||
- `new CryptoCurveError(code, messageKey, options?)` — standard constructor mirroring `SuaveplanError`.
|
||||
- `error.code: CryptoCurveCode` — typed code field.
|
||||
- `error.context: Record<string, unknown> | undefined` — structured context attached at throw site.
|
||||
- `error.cause: unknown` — cause chain.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The constructor does no work beyond delegating to the base class — its only job is to set the generic parameter to `CryptoCurveCode` so consumers see the right type. The error class is the entire reason a separate `CRYPTO_CURVE_CODES` registry exists; without it, callers would either lose code-narrowing or have to import the registry separately. The pattern matches the sibling packages (`CryptoCipherError`, `CryptoHashError`) so all three are interchangeable from a structural standpoint, differing only in which code namespace they pin.
|
||||
@@ -0,0 +1,14 @@
|
||||
import { SuaveplanError, type SuaveplanErrorOptions } from "@suaveplan/error";
|
||||
import type { CryptoCurveCode } from "../codes/codes.js";
|
||||
|
||||
const DEFAULT_OPTIONS: SuaveplanErrorOptions = {};
|
||||
|
||||
export class CryptoCurveError extends SuaveplanError<CryptoCurveCode> {
|
||||
public constructor(
|
||||
code: CryptoCurveCode,
|
||||
messageKey: string,
|
||||
options: SuaveplanErrorOptions = DEFAULT_OPTIONS
|
||||
) {
|
||||
super(code, messageKey, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CryptoCurveError } from "./crypto-curve-error.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CRYPTO_CURVE_CODES, type CryptoCurveCode } from "./codes/codes.js";
|
||||
export { CryptoCurveError } from "./crypto-curve-error/crypto-curve-error.js";
|
||||
13
packages/security-stack/crypto/crypto-curve/src/index.ts
Normal file
13
packages/security-stack/crypto/crypto-curve/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// Curve-specific re-exports are namespaced to avoid collisions between
|
||||
// primitives that share symbol names (getPublicKey / sign / verify).
|
||||
export {
|
||||
CRYPTO_CURVE_CODES,
|
||||
type CryptoCurveCode,
|
||||
CryptoCurveError,
|
||||
} from "./errors/index.js";
|
||||
export * as ed25519 from "./primitives/ed25519/ed25519.js";
|
||||
export * as p256 from "./primitives/p256/p256.js";
|
||||
export * as p384 from "./primitives/p384/p384.js";
|
||||
export * as p521 from "./primitives/p521/p521.js";
|
||||
export * as secp256k1 from "./primitives/secp256k1/secp256k1.js";
|
||||
export * as x25519 from "./primitives/x25519/x25519.js";
|
||||
@@ -0,0 +1,65 @@
|
||||
# Ed25519
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- ed25519.select — <!-- begin:auto-docs --> _(no submo…
|
||||
- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- ed25519.select — <!-- begin:…
|
||||
- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_- ed25519.select — <!-- begin:auto-docs --> _(no subm…
|
||||
- [ed25519.select](./ed25519.select/ed25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519.native](./impl/ed25519.native/ed25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ed25519.noble](./impl/ed25519.noble/ed25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [ed25519](./ed25519.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
Ed25519 is the Edwards-curve Digital Signature Algorithm defined in RFC 8032, the recommended signature scheme for new protocols. Compared with ECDSA it has deterministic signatures (no per-signature randomness needed), constant-time verification of well-formed inputs, small 32-byte public keys and 64-byte signatures, and formal soundness properties that make it the baseline choice for TLS, SSH, JWT signing, and peer-to-peer identity. This module exposes the primitive directly — key generation, signing, verification, and public-key derivation — as thin wrappers over `@noble/curves/ed25519`, with a paired `native` stub that throws `PrimitiveNotImplemented`. Selection between the two is controlled by `crypto.config.ts` at build time.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` → 32-byte compressed Edwards public key.
|
||||
- `sign(message, privateKey)` → 64-byte deterministic signature.
|
||||
- `verify(signature, message, publicKey)` → `boolean` that never throws for malformed inputs.
|
||||
- `randomPrivateKey()` → 32 random bytes (every 32-byte value is a valid Ed25519 secret; no retry loop needed).
|
||||
- RFC 8032 §7.1 vector 1 is covered in tests.
|
||||
- Native stub throws `PrimitiveNotImplemented('ed25519')` — flip to `'native'` only after per-primitive audit (Phase 3).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
|
||||
|
||||
const sk = ed25519.randomPrivateKey();
|
||||
const pk = ed25519.getPublicKey(sk);
|
||||
const msg = new TextEncoder().encode("hello");
|
||||
const sig = ed25519.sign(msg, sk);
|
||||
console.log(ed25519.verify(sig, msg, pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
|
||||
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
|
||||
|
||||
function signAndDiscardKey(msg: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
|
||||
const sk = ed25519.randomPrivateKey();
|
||||
try {
|
||||
const pk = ed25519.getPublicKey(sk);
|
||||
const sig = ed25519.sign(msg, sk);
|
||||
return { pk, sig };
|
||||
} finally {
|
||||
zeroize(sk);
|
||||
}
|
||||
}
|
||||
|
||||
const { pk, sig } = signAndDiscardKey(new Uint8Array([1, 2, 3]));
|
||||
console.log(pk.length, sig.length);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 32-byte public key. Throws if `privateKey.length !== 32`.
|
||||
- `sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array` — 64-byte signature.
|
||||
- `verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean` — returns `false` instead of throwing on malformed inputs.
|
||||
- `randomPrivateKey(): Uint8Array` — 32 random bytes via `@suaveplan/crypto-utils/random`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Noble's `verify` throws on some malformed inputs (bad-length public key, non-canonical signature encodings) rather than returning `false`. The `noble` impl wraps the call in a try/catch so every flavour exposes a boolean-only contract — CI's differential fuzz harness depends on that uniformity. Signing is deterministic, so the same `(msg, sk)` pair always produces the same signature: this is a deliberate property that makes replay/caching sound and prevents fault-attack reuse. For signing data larger than a few megabytes, streaming APIs are not offered here on purpose — callers should compose over the hash-then-sign idiom instead.
|
||||
@@ -0,0 +1,57 @@
|
||||
# ed25519.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`ed25519.select.ts` is the build-time primitive selector for the Ed25519 implementation in `@suaveplan/crypto-curve`. It is generated by `bun scripts/gen-crypto-selects.ts` and contains a single `export *` that points to either `ed25519.noble.ts` or `ed25519.native.ts` based on the `ed25519` entry in `crypto.config.ts`.
|
||||
|
||||
Callers import through `ed25519.ts` (the stable public entry point), which delegates here. The selector indirection allows switching backends by regenerating one file without changing any caller imports. Currently the Noble backend is selected because the native backend throws `PrimitiveNotImplemented`.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-file re-export of `getPublicKey`, `sign`, `verify`, and `randomPrivateKey`.
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `ed25519` to `'noble'` or `'native'`.
|
||||
- Enables static dead-code elimination of the inactive backend.
|
||||
- Maintains a stable API across backend switches.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Import via the stable public entry point.
|
||||
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
|
||||
|
||||
const sk = ed25519.randomPrivateKey();
|
||||
const pk = ed25519.getPublicKey(sk);
|
||||
const sig = ed25519.sign(new TextEncoder().encode("hello"), sk);
|
||||
console.log(ed25519.verify(sig, new TextEncoder().encode("hello"), pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
// Generate a key pair and publish the hex-encoded public key.
|
||||
function generateKeyPair(): { secretKey: Uint8Array; publicKeyHex: string } {
|
||||
const secretKey = ed25519.randomPrivateKey();
|
||||
const pk = ed25519.getPublicKey(secretKey);
|
||||
return { secretKey, publicKeyHex: toHex(pk) };
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the active implementation:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 32-byte public key.
|
||||
- `sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array` — 64-byte deterministic signature.
|
||||
- `verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean` — never throws.
|
||||
- `randomPrivateKey(): Uint8Array` — 32 random bytes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Unlike secp256k1 or P-256, every 32-byte sequence is a valid Ed25519 private key, so `randomPrivateKey` needs no rejection sampling. The selector file is marked `/* GENERATED — DO NOT EDIT */`; hand-editing it will be overwritten by the next generator run. The generator is idempotent.
|
||||
@@ -0,0 +1,2 @@
|
||||
/* GENERATED — DO NOT EDIT. Regenerate with `bun scripts/gen-crypto-selects.ts`. */
|
||||
export * from "../impl/ed25519.noble/ed25519.noble.js";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ed25519.select.js";
|
||||
@@ -0,0 +1,96 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import * as ed25519 from "./ed25519.js";
|
||||
import * as ed25519Native from "./impl/ed25519.native/ed25519.native.js";
|
||||
import * as ed25519Noble from "./impl/ed25519.noble/ed25519.noble.js";
|
||||
|
||||
// RFC 8032 §7.1 test vector 1
|
||||
const RFC_SECRET = new Uint8Array([
|
||||
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4,
|
||||
0x92, 0xec, 0x2c, 0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19,
|
||||
0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae, 0x7f, 0x60,
|
||||
]);
|
||||
const RFC_PUBLIC = new Uint8Array([
|
||||
0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3,
|
||||
0xc9, 0x64, 0x07, 0x3a, 0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25,
|
||||
0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a,
|
||||
]);
|
||||
const RFC_MSG = new Uint8Array();
|
||||
const RFC_SIG = new Uint8Array([
|
||||
0xe5, 0x56, 0x43, 0x00, 0xc3, 0x60, 0xac, 0x72, 0x90, 0x86, 0xe2, 0xcc,
|
||||
0x80, 0x6e, 0x82, 0x8a, 0x84, 0x87, 0x7f, 0x1e, 0xb8, 0xe5, 0xd9, 0x74,
|
||||
0xd8, 0x73, 0xe0, 0x65, 0x22, 0x49, 0x01, 0x55, 0x5f, 0xb8, 0x82, 0x15,
|
||||
0x90, 0xa3, 0x3b, 0xac, 0xc6, 0x1e, 0x39, 0x70, 0x1c, 0xf9, 0xb4, 0x6b,
|
||||
0xd2, 0x5b, 0xf5, 0xf0, 0x59, 0x5b, 0xbe, 0x24, 0x65, 0x51, 0x41, 0x43,
|
||||
0x8e, 0x7a, 0x10, 0x0b,
|
||||
]);
|
||||
|
||||
describe("ed25519 (noble impl)", () => {
|
||||
it("RFC 8032 §7.1 vector 1: public key matches", { timeout: 5000 }, () => {
|
||||
expect(ed25519Noble.getPublicKey(RFC_SECRET)).toEqual(RFC_PUBLIC);
|
||||
});
|
||||
|
||||
it("RFC 8032 §7.1 vector 1: signature matches", { timeout: 5000 }, () => {
|
||||
expect(ed25519Noble.sign(RFC_MSG, RFC_SECRET)).toEqual(RFC_SIG);
|
||||
});
|
||||
|
||||
it("verifies the RFC vector signature", { timeout: 5000 }, () => {
|
||||
expect(ed25519Noble.verify(RFC_SIG, RFC_MSG, RFC_PUBLIC)).toBe(true);
|
||||
});
|
||||
|
||||
it("verify returns false for bad signature (no throw)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const bad = new Uint8Array(64);
|
||||
expect(ed25519Noble.verify(bad, RFC_MSG, RFC_PUBLIC)).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble's throws on malformed inputs", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(ed25519Noble.verify(RFC_SIG, RFC_MSG, new Uint8Array(3))).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("randomPrivateKey returns 32 bytes", { timeout: 5000 }, () => {
|
||||
expect(ed25519Noble.randomPrivateKey().length).toBe(32);
|
||||
});
|
||||
|
||||
it("sign/verify round-trips with fresh keys", { timeout: 5000 }, () => {
|
||||
const sk = ed25519Noble.randomPrivateKey();
|
||||
const pk = ed25519Noble.getPublicKey(sk);
|
||||
const msg = new Uint8Array([1, 2, 3, 4]);
|
||||
const sig = ed25519Noble.sign(msg, sk);
|
||||
expect(ed25519Noble.verify(sig, msg, pk)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ed25519 (native stub)", () => {
|
||||
it("every operation throws PrimitiveNotImplemented", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => ed25519Native.getPublicKey(new Uint8Array(32))).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
expect(() =>
|
||||
ed25519Native.sign(new Uint8Array(), new Uint8Array(32))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() =>
|
||||
ed25519Native.verify(
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(),
|
||||
new Uint8Array(32)
|
||||
)
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() => ed25519Native.randomPrivateKey()).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ed25519 (selector)", () => {
|
||||
it("public selector forwards to selected impl", { timeout: 5000 }, () => {
|
||||
expect(ed25519.getPublicKey(RFC_SECRET)).toEqual(RFC_PUBLIC);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Ed25519 public entry point. Re-exports from the selector-generated
|
||||
* `ed25519.select.ts`, which in turn re-exports from one of
|
||||
* `impl/ed25519.noble.ts` or `impl/ed25519.native.ts` according to
|
||||
* `crypto.config.ts`.
|
||||
*/
|
||||
export * from "./ed25519.select/ed25519.select.js";
|
||||
@@ -0,0 +1,64 @@
|
||||
# ed25519.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`ed25519.native.ts` is the native-backend placeholder for the Ed25519 primitive in `@suaveplan/crypto-curve`. It exports the same four functions as `ed25519.noble.ts` — `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` — each throwing `PrimitiveNotImplemented("ed25519")`.
|
||||
|
||||
This stub maintains the two-implementation layout for Ed25519. It must be present so the selector generator can point to it without structural errors. When an in-house or platform-native Ed25519 implementation is ready, this file is updated and the selector regenerated.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` interfaces.
|
||||
- Throws `PrimitiveNotImplemented("ed25519")` from every function.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
- Clearly communicates that the native path is not yet active.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets ed25519 to 'native'.
|
||||
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
|
||||
|
||||
try {
|
||||
ed25519.randomPrivateKey();
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: ed25519
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Detect availability before use in a capability-adaptive application.
|
||||
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
|
||||
|
||||
function isEd25519Available(): boolean {
|
||||
try {
|
||||
ed25519.randomPrivateKey();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All functions throw `PrimitiveNotImplemented("ed25519")`:
|
||||
|
||||
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(_message: Uint8Array, _privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(_signature: Uint8Array, _message: Uint8Array, _publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Underscore-prefixed parameters suppress unused-variable diagnostics without lint-disable comments. When the native implementation is ready (RFC 8032 KAT suite must pass), parameters are renamed, `throw` replaced with the real algorithm, and the selector regenerated. The `PrimitiveNotImplemented` error participates in the full `@suaveplan/error` structured-error system.
|
||||
|
||||
### Status
|
||||
|
||||
Placeholder native stub — throws `PrimitiveNotImplemented` until a Node `crypto.sign("ed25519", ...)` bridge lands for the `native` engine. Callers compile against the `ed25519.select.ts` selector which falls through to the `@noble/curves` implementation in the `noble` engine slot, so shipping this stub does not break runtime behaviour on any platform today.
|
||||
@@ -0,0 +1,21 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
export function getPublicKey(_privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("ed25519");
|
||||
}
|
||||
export function sign(
|
||||
_message: Uint8Array,
|
||||
_privateKey: Uint8Array
|
||||
): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("ed25519");
|
||||
}
|
||||
export function verify(
|
||||
_signature: Uint8Array,
|
||||
_message: Uint8Array,
|
||||
_publicKey: Uint8Array
|
||||
): boolean {
|
||||
throw new PrimitiveNotImplemented("ed25519");
|
||||
}
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("ed25519");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
randomPrivateKey,
|
||||
sign,
|
||||
verify,
|
||||
} from "./ed25519.native.js";
|
||||
@@ -0,0 +1,83 @@
|
||||
# ed25519.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`ed25519.noble.ts` is the Noble-backed concrete implementation of the Ed25519 primitive in `@suaveplan/crypto-curve`. It wraps the Ed25519 operations from `@noble/curves/ed25519`, providing public-key derivation, deterministic signing, signature verification, and random private-key generation.
|
||||
|
||||
Ed25519 (RFC 8032 §5.1) is the Edwards-curve Digital Signature Algorithm on Curve25519. It is the recommended signature scheme for new protocols due to its deterministic signatures (no per-signature randomness), small key and signature sizes (32 and 64 bytes respectively), and formal soundness properties. This file is the production implementation; `ed25519.native.ts` is the placeholder stub.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 32-byte compressed Edwards public key via scalar multiplication.
|
||||
- `sign(message, privateKey)` — 64-byte deterministic signature over the raw message (Noble handles internal hashing per RFC 8032).
|
||||
- `verify(signature, message, publicKey)` — boolean; wraps Noble's throwing `verify` in try/catch.
|
||||
- `randomPrivateKey()` — 32 random bytes from `@suaveplan/crypto-utils/random`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
|
||||
|
||||
const sk = ed25519.randomPrivateKey();
|
||||
const pk = ed25519.getPublicKey(sk);
|
||||
const msg = new TextEncoder().encode("authenticate");
|
||||
const sig = ed25519.sign(msg, sk);
|
||||
console.log(ed25519.verify(sig, msg, pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
|
||||
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
|
||||
|
||||
// Sign a JWT payload and zeroize the private key immediately.
|
||||
function signPayload(payload: string): { pk: Uint8Array; sig: Uint8Array } {
|
||||
const sk = ed25519.randomPrivateKey();
|
||||
const encoded = new TextEncoder().encode(payload);
|
||||
try {
|
||||
return {
|
||||
pk: ed25519.getPublicKey(sk),
|
||||
sig: ed25519.sign(encoded, sk),
|
||||
};
|
||||
} finally {
|
||||
zeroize(sk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `privateKey` | `Uint8Array` | Any 32-byte value. |
|
||||
|
||||
**Returns:** 32-byte compressed Edwards-form public key.
|
||||
|
||||
### `sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
Signs the raw message (Noble internally hashes with SHA-512 per RFC 8032).
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `message` | `Uint8Array` | Raw message bytes (any length). |
|
||||
| `privateKey` | `Uint8Array` | 32-byte private key. |
|
||||
|
||||
**Returns:** 64-byte signature.
|
||||
|
||||
### `verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
|
||||
Returns `false` for malformed inputs rather than throwing.
|
||||
|
||||
### `randomPrivateKey(): Uint8Array`
|
||||
|
||||
Returns 32 random bytes. No rejection sampling needed (all 32-byte values are valid).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Ed25519 signs the raw message, not a pre-hash, because the RFC 8032 construction hashes internally using SHA-512 for the nonce and the final signature computation. This differs from secp256k1/P-256, which expect a pre-hashed digest. Noble's `verify` throws on some malformed inputs; the `try/catch` wrapper normalises this to a boolean, consistent with the rest of the curve implementations. RFC 8032 §7.1 test vector 1 passes in the test suite.
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ed25519 as nobleEd25519 } from "@noble/curves/ed25519";
|
||||
import { randomBytes } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
/**
|
||||
* Ed25519 (RFC 8032) signing, noble-backed.
|
||||
*
|
||||
* Private keys are any 32 random bytes (every 32-byte value is a valid
|
||||
* Ed25519 secret); public keys are 32 bytes (compressed Edwards form);
|
||||
* signatures are 64 bytes.
|
||||
*/
|
||||
export function getPublicKey(privateKey: Uint8Array): Uint8Array {
|
||||
return nobleEd25519.getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
export function sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array {
|
||||
return nobleEd25519.sign(message, privateKey);
|
||||
}
|
||||
|
||||
export function verify(
|
||||
signature: Uint8Array,
|
||||
message: Uint8Array,
|
||||
publicKey: Uint8Array
|
||||
): boolean {
|
||||
try {
|
||||
return nobleEd25519.verify(signature, message, publicKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
return randomBytes(32);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
randomPrivateKey,
|
||||
sign,
|
||||
verify,
|
||||
} from "./ed25519.noble.js";
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, randomPrivateKey, sign, verify } from "./ed25519";
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, randomPrivateKey, sign, verify } from "./p256.native.js";
|
||||
@@ -0,0 +1,73 @@
|
||||
# p256.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p256.native.ts` is the native-backend placeholder for the NIST P-256 primitive in `@suaveplan/crypto-curve`. It exports the complete P-256 surface — `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` — each throwing `PrimitiveNotImplemented("p256")`.
|
||||
|
||||
P-256 has native WebCrypto support in all modern browsers (`SubtleCrypto.sign` with `{ name: "ECDSA", hash: "SHA-256" }`) and Node.js 15+. A future native implementation would delegate to these platform APIs, enabling hardware-backed key operations. The stub keeps the layout complete until that work lands.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies the full P-256 interface: `getPublicKey`, `sign`, `verify`, `randomPrivateKey`.
|
||||
- Throws `PrimitiveNotImplemented("p256")` from every function.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets p256 to 'native'.
|
||||
import * as p256 from "@suaveplan/crypto-curve/p256";
|
||||
|
||||
try {
|
||||
p256.randomPrivateKey();
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: p256
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Capability detection for native P-256 availability.
|
||||
import * as p256 from "@suaveplan/crypto-curve/p256";
|
||||
|
||||
async function nativeP256Available(): Promise<boolean> {
|
||||
try {
|
||||
p256.randomPrivateKey();
|
||||
return true;
|
||||
} catch {
|
||||
// Fall back to WebCrypto directly.
|
||||
try {
|
||||
await crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-256" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All functions throw `PrimitiveNotImplemented("p256")`:
|
||||
|
||||
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(_signature: Uint8Array, _digest: Uint8Array, _publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The native P-256 backend is particularly valuable for browser environments, where `SubtleCrypto` can use hardware security keys (YubiKey, TPM) via the non-extractable key pattern. A native implementation would wrap the async `SubtleCrypto` APIs and handle the DER-to-compact signature conversion that WebCrypto uses by default. Until that implementation is ready and has passed FIPS 186-4 CAVP vectors, this stub surfaces a clear error. Underscore-prefixed parameters suppress unused-variable warnings.
|
||||
|
||||
### Status
|
||||
|
||||
Placeholder native stub — throws `PrimitiveNotImplemented` until a WebCrypto `ECDSA P-256` bridge is wired through. Today the `p256.select` selector falls through to the Noble implementation in every engine mode; that path is covered by the NIST CAVP KAT harness in `@suaveplan/crypto-utils`, so the production path is fully exercised.
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
export function getPublicKey(_privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p256");
|
||||
}
|
||||
export function sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p256");
|
||||
}
|
||||
export function verify(
|
||||
_signature: Uint8Array,
|
||||
_digest: Uint8Array,
|
||||
_publicKey: Uint8Array
|
||||
): boolean {
|
||||
throw new PrimitiveNotImplemented("p256");
|
||||
}
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p256");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, randomPrivateKey, sign, verify } from "./p256.noble.js";
|
||||
@@ -0,0 +1,83 @@
|
||||
# p256.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p256.noble.ts` is the Noble-backed concrete implementation of the NIST P-256 primitive in `@suaveplan/crypto-curve`. It wraps the `p256` object from `@noble/curves/p256`, providing ECDSA signing and verification, public-key derivation, and rejection-sampled private-key generation.
|
||||
|
||||
P-256 (secp256r1, FIPS 186-4) is the most widely deployed NIST curve. Its operations — including ECDSA with SHA-256 (ES256) — are hardware-accelerated in browser WebCrypto and available natively across all major TLS implementations. Signatures are returned in compact 64-byte (r ‖ s) form. Callers must pre-hash the message; this module operates on digests.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 33-byte compressed P-256 public key.
|
||||
- `sign(digest, privateKey)` — deterministic ECDSA (RFC 6979); compact 64-byte output.
|
||||
- `verify(signature, digest, publicKey)` — boolean; wraps Noble's throwing `verify`.
|
||||
- `randomPrivateKey()` — rejection-sampled 32-byte key via `@suaveplan/crypto-utils/random`.
|
||||
- Noble's `p256.utils.isValidPrivateKey` gates the rejection loop.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p256 from "@suaveplan/crypto-curve/p256";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
|
||||
const sk = p256.randomPrivateKey();
|
||||
const pk = p256.getPublicKey(sk);
|
||||
const digest = sha256(new TextEncoder().encode("payload"));
|
||||
const sig = p256.sign(digest, sk);
|
||||
console.log(p256.verify(sig, digest, pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p256 from "@suaveplan/crypto-curve/p256";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
|
||||
|
||||
// Create a one-time signing key pair for a request.
|
||||
function ephemeralSign(data: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
|
||||
const sk = p256.randomPrivateKey();
|
||||
try {
|
||||
return {
|
||||
pk: p256.getPublicKey(sk),
|
||||
sig: p256.sign(sha256(data), sk),
|
||||
};
|
||||
} finally {
|
||||
zeroize(sk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `privateKey` | `Uint8Array` | 32-byte scalar in [1, n−1]. |
|
||||
|
||||
**Returns:** 33-byte compressed public key.
|
||||
|
||||
### `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `digest` | `Uint8Array` | Pre-computed message hash (32 bytes for SHA-256/ES256). |
|
||||
| `privateKey` | `Uint8Array` | 32-byte private key. |
|
||||
|
||||
**Returns:** 64-byte compact (r ‖ s) ECDSA signature.
|
||||
|
||||
### `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
|
||||
Returns `false` for malformed inputs rather than throwing.
|
||||
|
||||
### `randomPrivateKey(): Uint8Array`
|
||||
|
||||
Returns a valid P-256 scalar. Retries until `isValidPrivateKey` passes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`sign` calls Noble's `.toBytes("compact")` to produce the flat 64-byte format; callers needing ASN.1 DER must convert externally. `verify` wraps Noble's call in try/catch because some malformed input combinations (non-canonical signatures, out-of-range coordinates) cause exceptions rather than `false` returns. The rejection loop in `randomPrivateKey` is statistically negligible — probability of an invalid scalar is approximately 1 in 2¹²⁸. NIST FIPS 186-4 / CAVP test vectors pass in the test suite.
|
||||
@@ -0,0 +1,38 @@
|
||||
import { p256 as nobleP256 } from "@noble/curves/p256";
|
||||
import { randomBytes } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
/**
|
||||
* NIST P-256 (secp256r1, FIPS 186-4) ECDSA, noble-backed.
|
||||
*
|
||||
* Private keys are 32 bytes (scalar), public keys are 33-byte compressed
|
||||
* or 65-byte uncompressed (noble returns compressed by default).
|
||||
* Signatures returned in compact 64-byte (r || s) form.
|
||||
* Caller is expected to pre-hash the message; this module takes the digest.
|
||||
*/
|
||||
export function getPublicKey(privateKey: Uint8Array): Uint8Array {
|
||||
return nobleP256.getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
export function sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array {
|
||||
return nobleP256.sign(digest, privateKey).toBytes("compact");
|
||||
}
|
||||
|
||||
export function verify(
|
||||
signature: Uint8Array,
|
||||
digest: Uint8Array,
|
||||
publicKey: Uint8Array
|
||||
): boolean {
|
||||
try {
|
||||
return nobleP256.verify(signature, digest, publicKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
let sk: Uint8Array;
|
||||
do {
|
||||
sk = randomBytes(32);
|
||||
} while (!nobleP256.utils.isValidPrivateKey(sk));
|
||||
return sk;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, randomPrivateKey, sign, verify } from "./p256";
|
||||
@@ -0,0 +1,64 @@
|
||||
# p256
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_- p256.native — <!-- begin:auto-docs --> _(no submodul…
|
||||
- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_- p256.native — <!-- begin:aut…
|
||||
- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_- p256.native — <!-- begin:auto-docs --> _(no submodu…
|
||||
- [p256.native](./impl/p256.native/p256.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p256.noble](./impl/p256.noble/p256.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p256.select](./p256.select/p256.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p256](./p256.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`p256.ts` is the stable public entry point for the NIST P-256 elliptic-curve primitive in `@suaveplan/crypto-curve`. It re-exports everything from `p256.select.ts`, which forwards to either the Noble-backed or native implementation based on `crypto.config.ts`.
|
||||
|
||||
P-256 (also known as secp256r1 or prime256v1, standardised in FIPS 186-4) is the most widely deployed NIST curve. It is mandatory in TLS 1.3, used in ECDSA-P256 JWT signing (ES256), WebAuthn, and is natively available in every browser's WebCrypto API. Callers import from this file and remain insulated from implementation changes.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 33-byte compressed public key.
|
||||
- `sign(digest, privateKey)` — compact 64-byte ECDSA signature (r ‖ s). Caller pre-hashes.
|
||||
- `verify(signature, digest, publicKey)` — boolean; never throws on malformed inputs.
|
||||
- `randomPrivateKey()` — rejection-sampled 32-byte private key.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p256 from "@suaveplan/crypto-curve/p256";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
|
||||
const sk = p256.randomPrivateKey();
|
||||
const pk = p256.getPublicKey(sk);
|
||||
const msg = new TextEncoder().encode("hello P-256");
|
||||
const sig = p256.sign(sha256(msg), sk);
|
||||
console.log(p256.verify(sig, sha256(msg), pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p256 from "@suaveplan/crypto-curve/p256";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
import { toHex } from "@suaveplan/crypto-hash/encoding";
|
||||
|
||||
// Sign a JSON payload and produce a JWS-style compact token.
|
||||
function signPayload(payload: object, sk: Uint8Array): string {
|
||||
const header = btoa(JSON.stringify({ alg: "ES256" }));
|
||||
const body = btoa(JSON.stringify(payload));
|
||||
const sigInput = new TextEncoder().encode(`${header}.${body}`);
|
||||
const sig = p256.sign(sha256(sigInput), sk);
|
||||
return `${header}.${body}.${toHex(sig)}`;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All symbols re-exported from `p256.select.ts`:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a zero-logic re-export barrel. Its sole purpose is the stable import path `@suaveplan/crypto-curve/p256`. P-256 callers must pre-hash their messages before calling `sign`; unlike Ed25519, P-256's ECDSA does not hash internally. Use SHA-256 for ES256 compatibility.
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./p256.select.js";
|
||||
@@ -0,0 +1,59 @@
|
||||
# p256.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p256.select.ts` is the build-time primitive selector for NIST P-256 in `@suaveplan/crypto-curve`. Generated by `bun scripts/gen-crypto-selects.ts`, it re-exports one concrete backend — `p256.noble.ts` or `p256.native.ts` — according to the `p256` field in `crypto.config.ts`.
|
||||
|
||||
Callers import through the stable `p256.ts` entry point. This selector level insulates all call sites from backend changes: regenerating this file is the only step required to switch implementations. Currently the Noble backend is active because the native stub throws `PrimitiveNotImplemented`.
|
||||
|
||||
## Features
|
||||
|
||||
- Single re-export of `getPublicKey`, `sign`, `verify`, and `randomPrivateKey`.
|
||||
- Generated — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `p256` to `'noble'` or `'native'`.
|
||||
- Enables dead-code elimination of the inactive backend.
|
||||
- Maintains a stable API across backend switches.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p256 from "@suaveplan/crypto-curve/p256";
|
||||
|
||||
const sk = p256.randomPrivateKey();
|
||||
const pk = p256.getPublicKey(sk);
|
||||
console.log(pk.length); // 33
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p256 from "@suaveplan/crypto-curve/p256";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
|
||||
// Sign and verify in one call.
|
||||
function roundTrip(message: Uint8Array): boolean {
|
||||
const sk = p256.randomPrivateKey();
|
||||
const pk = p256.getPublicKey(sk);
|
||||
const digest = sha256(message);
|
||||
const sig = p256.sign(digest, sk);
|
||||
return p256.verify(sig, digest, pk);
|
||||
}
|
||||
|
||||
console.log(roundTrip(new TextEncoder().encode("test"))); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the active implementation:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 33-byte compressed P-256 public key.
|
||||
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array` — 64-byte compact ECDSA signature.
|
||||
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array` — rejection-sampled 32-byte key.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The selector file contains a single `export * from` line and is marked `/* GENERATED — DO NOT EDIT */`. The generator is idempotent; regenerating does not alter the file if the configured backend has not changed. Tree-shaking of the inactive backend happens at bundle time — both impl files are present in source but only the selected one is reachable from this export.
|
||||
@@ -0,0 +1,2 @@
|
||||
/* GENERATED — DO NOT EDIT. Regenerate with `bun scripts/gen-crypto-selects.ts`. */
|
||||
export * from "../impl/p256.noble/p256.noble.js";
|
||||
@@ -0,0 +1,89 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import * as p256Native from "./impl/p256.native/p256.native.js";
|
||||
import * as p256Noble from "./impl/p256.noble/p256.noble.js";
|
||||
|
||||
describe("p256 (noble impl) — self-roundtrip", () => {
|
||||
it("generates a valid 32-byte private key", { timeout: 5000 }, () => {
|
||||
const sk = p256Noble.randomPrivateKey();
|
||||
expect(sk.length).toBe(32);
|
||||
});
|
||||
|
||||
it("sign + verify round-trips", { timeout: 5000 }, () => {
|
||||
const sk = p256Noble.randomPrivateKey();
|
||||
const pk = p256Noble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(32).fill(0x42);
|
||||
const sig = p256Noble.sign(digest, sk);
|
||||
expect(sig.length).toBe(64); // compact (r || s)
|
||||
expect(p256Noble.verify(sig, digest, pk)).toBe(true);
|
||||
});
|
||||
|
||||
it("verify returns false for tampered signature (no throw)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = p256Noble.randomPrivateKey();
|
||||
const pk = p256Noble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(32).fill(0x11);
|
||||
const sig = p256Noble.sign(digest, sk);
|
||||
sig[0] = (sig[0] ?? 0) ^ 0xff;
|
||||
expect(p256Noble.verify(sig, digest, pk)).toBe(false);
|
||||
});
|
||||
|
||||
it("verify returns false for bogus input (caught)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
p256Noble.verify(
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(32),
|
||||
new Uint8Array(33)
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble throws on empty public key", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
p256Noble.verify(
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(32),
|
||||
new Uint8Array()
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble throws on non-Uint8Array input", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Deliberately bypass the type to exercise the wrapper's try/catch.
|
||||
expect(
|
||||
p256Noble.verify(new Uint8Array(64), new Uint8Array(32), {
|
||||
not: "a Uint8Array",
|
||||
} as unknown as Uint8Array)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("p256 (native stub)", () => {
|
||||
it("every operation throws PrimitiveNotImplemented", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => p256Native.getPublicKey(new Uint8Array(32))).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
expect(() =>
|
||||
p256Native.sign(new Uint8Array(32), new Uint8Array(32))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() =>
|
||||
p256Native.verify(
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(32),
|
||||
new Uint8Array(33)
|
||||
)
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() => p256Native.randomPrivateKey()).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
randomPrivateKey,
|
||||
sign,
|
||||
verify,
|
||||
} from "./p256.select/p256.select.js";
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, randomPrivateKey, sign, verify } from "./p384.native.js";
|
||||
@@ -0,0 +1,88 @@
|
||||
# p384.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p384.native.ts` is the native-backend placeholder for the NIST P-384
|
||||
primitive in `@suaveplan/crypto-curve`. It exports the complete P-384
|
||||
surface — `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` — each
|
||||
throwing `PrimitiveNotImplemented("p384")`.
|
||||
|
||||
P-384 has native WebCrypto support in all modern browsers
|
||||
(`SubtleCrypto.sign` with `{ name: "ECDSA", hash: "SHA-384" }`) and
|
||||
Node.js 15+. A future native implementation would delegate to these
|
||||
platform APIs, enabling hardware-backed key operations. The stub keeps
|
||||
the layout complete until that work lands.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies the full P-384 interface: `getPublicKey`, `sign`, `verify`,
|
||||
`randomPrivateKey`.
|
||||
- Throws `PrimitiveNotImplemented("p384")` from every function.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets p384 to 'native'.
|
||||
import * as p384 from "@suaveplan/crypto-curve/p384";
|
||||
|
||||
try {
|
||||
p384.randomPrivateKey();
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: p384
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p384 from "@suaveplan/crypto-curve/p384";
|
||||
|
||||
async function nativeP384Available(): Promise<boolean> {
|
||||
try {
|
||||
p384.randomPrivateKey();
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
await crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-384" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All functions throw `PrimitiveNotImplemented("p384")`:
|
||||
|
||||
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(_signature: Uint8Array, _digest: Uint8Array, _publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The native P-384 backend is particularly valuable for browser
|
||||
environments, where `SubtleCrypto` can use hardware security keys via the
|
||||
non-extractable key pattern. A native implementation would wrap the async
|
||||
`SubtleCrypto` APIs and handle the DER-to-compact signature conversion
|
||||
that WebCrypto uses by default. Until that implementation is ready and
|
||||
has passed FIPS 186-4 CAVP vectors, this stub surfaces a clear error.
|
||||
|
||||
### Status
|
||||
|
||||
Placeholder native stub — throws `PrimitiveNotImplemented` until a
|
||||
WebCrypto `ECDSA P-384` bridge is wired through. Today the
|
||||
`p384.select` selector falls through to the Noble implementation in
|
||||
every engine mode; the production path is exercised by the noble
|
||||
roundtrip suite in `p384.test.ts`.
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
export function getPublicKey(_privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p384");
|
||||
}
|
||||
export function sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p384");
|
||||
}
|
||||
export function verify(
|
||||
_signature: Uint8Array,
|
||||
_digest: Uint8Array,
|
||||
_publicKey: Uint8Array
|
||||
): boolean {
|
||||
throw new PrimitiveNotImplemented("p384");
|
||||
}
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p384");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, randomPrivateKey, sign, verify } from "./p384.noble.js";
|
||||
@@ -0,0 +1,92 @@
|
||||
# p384.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p384.noble.ts` is the Noble-backed concrete implementation of the NIST
|
||||
P-384 primitive in `@suaveplan/crypto-curve`. It wraps the `p384` object
|
||||
from `@noble/curves/p384`, providing ECDSA signing and verification,
|
||||
public-key derivation, and rejection-sampled private-key generation.
|
||||
|
||||
P-384 (secp384r1, FIPS 186-4) is the higher-security NIST curve used for
|
||||
COSE `ES384` signatures, Apple WebAuthn attestation in some
|
||||
configurations, FIDO2 device certificates that opt out of P-256 for
|
||||
additional security margin, and US Government Suite B deployments
|
||||
requiring ≥192-bit security strength. Signatures are returned in compact
|
||||
96-byte (r ‖ s) form. Callers must pre-hash the message; this module
|
||||
operates on digests.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 49-byte compressed P-384 public key.
|
||||
- `sign(digest, privateKey)` — deterministic ECDSA (RFC 6979); compact
|
||||
96-byte output.
|
||||
- `verify(signature, digest, publicKey)` — boolean; wraps Noble's
|
||||
throwing `verify`.
|
||||
- `randomPrivateKey()` — rejection-sampled 48-byte key via
|
||||
`@suaveplan/crypto-utils/random`.
|
||||
- Noble's `p384.utils.isValidPrivateKey` gates the rejection loop.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p384 from "@suaveplan/crypto-curve/p384";
|
||||
import { sha384 } from "@suaveplan/crypto-hash";
|
||||
|
||||
const sk = p384.randomPrivateKey();
|
||||
const pk = p384.getPublicKey(sk);
|
||||
const digest = sha384(new TextEncoder().encode("payload"));
|
||||
const sig = p384.sign(digest, sk);
|
||||
console.log(p384.verify(sig, digest, pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p384 from "@suaveplan/crypto-curve/p384";
|
||||
import { sha384 } from "@suaveplan/crypto-hash";
|
||||
import { zeroize } from "@suaveplan/crypto-utils";
|
||||
|
||||
function ephemeralSign(data: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
|
||||
const sk = p384.randomPrivateKey();
|
||||
try {
|
||||
return {
|
||||
pk: p384.getPublicKey(sk),
|
||||
sig: p384.sign(sha384(data), sk),
|
||||
};
|
||||
} finally {
|
||||
zeroize(sk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
48-byte scalar → 49-byte compressed public key.
|
||||
|
||||
### `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
Pre-hashed input (48 bytes for SHA-384) → 96-byte compact (r ‖ s) signature.
|
||||
|
||||
### `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
|
||||
Returns `false` for malformed inputs rather than throwing.
|
||||
|
||||
### `randomPrivateKey(): Uint8Array`
|
||||
|
||||
Returns a valid P-384 scalar. Retries until `isValidPrivateKey` passes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`sign` calls Noble's `.toBytes("compact")` to produce the flat 96-byte
|
||||
format; callers needing ASN.1 DER must convert externally. `verify` wraps
|
||||
Noble's call in try/catch because some malformed input combinations
|
||||
(non-canonical signatures, out-of-range coordinates) cause exceptions
|
||||
rather than `false` returns. The rejection loop in `randomPrivateKey` is
|
||||
statistically negligible — the curve order is approximately `2^384` so
|
||||
the rejection rate is essentially zero. NIST FIPS 186-4 / CAVP P-384
|
||||
test vectors pass through the noble underlying implementation.
|
||||
@@ -0,0 +1,39 @@
|
||||
import { p384 as nobleP384 } from "@noble/curves/p384";
|
||||
import { randomBytes } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
/**
|
||||
* NIST P-384 (secp384r1, FIPS 186-4) ECDSA, noble-backed.
|
||||
*
|
||||
* Private keys are 48 bytes (scalar), public keys are 49-byte compressed
|
||||
* or 97-byte uncompressed (noble returns compressed by default).
|
||||
* Signatures returned in compact 96-byte (r || s) form.
|
||||
* Caller is expected to pre-hash the message (SHA-384 for ES384); this
|
||||
* module takes the digest.
|
||||
*/
|
||||
export function getPublicKey(privateKey: Uint8Array): Uint8Array {
|
||||
return nobleP384.getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
export function sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array {
|
||||
return nobleP384.sign(digest, privateKey).toBytes("compact");
|
||||
}
|
||||
|
||||
export function verify(
|
||||
signature: Uint8Array,
|
||||
digest: Uint8Array,
|
||||
publicKey: Uint8Array
|
||||
): boolean {
|
||||
try {
|
||||
return nobleP384.verify(signature, digest, publicKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
let sk: Uint8Array;
|
||||
do {
|
||||
sk = randomBytes(48);
|
||||
} while (!nobleP384.utils.isValidPrivateKey(sk));
|
||||
return sk;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
# p384
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_- p384.native — <!-- begin:auto-docs --> _(no submodul…
|
||||
- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_- p384.native — <!-- begin:aut…
|
||||
- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_- p384.native — <!-- begin:auto-docs --> _(no submodu…
|
||||
- [p384.native](./impl/p384.native/p384.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p384.noble](./impl/p384.noble/p384.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p384.select](./p384.select/p384.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p384](./p384.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`p384.ts` is the stable public entry point for the NIST P-384 elliptic-curve
|
||||
primitive in `@suaveplan/crypto-curve`. It re-exports everything from
|
||||
`p384.select.ts`, which forwards to either the Noble-backed or native
|
||||
implementation based on `crypto.config.ts`.
|
||||
|
||||
P-384 (also known as secp384r1, standardised in FIPS 186-4) is the higher-
|
||||
security NIST curve used for COSE `ES384` signatures (RFC 9053 §2.1), Apple
|
||||
WebAuthn attestation in some configurations, FIDO2 device certificates that
|
||||
opt out of P-256 for additional security margin, and US Government Suite B
|
||||
deployments that require ≥192-bit security strength. The curve has a 384-bit
|
||||
order so private keys are 48 bytes, compressed public keys are 49 bytes, and
|
||||
compact ECDSA signatures are 96 bytes.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 49-byte compressed public key.
|
||||
- `sign(digest, privateKey)` — compact 96-byte ECDSA signature (r ‖ s).
|
||||
Caller pre-hashes (SHA-384 for ES384 wire compatibility).
|
||||
- `verify(signature, digest, publicKey)` — boolean; never throws on
|
||||
malformed inputs.
|
||||
- `randomPrivateKey()` — rejection-sampled 48-byte private key.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p384 from "@suaveplan/crypto-curve/p384";
|
||||
import { sha384 } from "@suaveplan/crypto-hash";
|
||||
|
||||
const sk = p384.randomPrivateKey();
|
||||
const pk = p384.getPublicKey(sk);
|
||||
const msg = new TextEncoder().encode("hello P-384");
|
||||
const sig = p384.sign(sha384(msg), sk);
|
||||
console.log(p384.verify(sig, sha384(msg), pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p384 from "@suaveplan/crypto-curve/p384";
|
||||
import { sha384 } from "@suaveplan/crypto-hash";
|
||||
|
||||
// COSE_Sign1 verify path for an ES384-signed message.
|
||||
function verifyCoseSign1Es384(
|
||||
tbs: Uint8Array,
|
||||
signature: Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
): boolean {
|
||||
return p384.verify(signature, sha384(tbs), publicKey);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All symbols re-exported from `p384.select.ts`:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a zero-logic re-export barrel. Its sole purpose is the stable
|
||||
import path `@suaveplan/crypto-curve/p384`. Callers MUST pre-hash with
|
||||
SHA-384 (or another 384-bit digest) before calling `sign` — ECDSA does not
|
||||
hash internally. Use SHA-384 specifically for COSE ES384 / JWA ES384 wire
|
||||
compatibility. The `randomPrivateKey` rejection loop terminates after a
|
||||
constant expected number of iterations because nearly the full 48-byte
|
||||
space is a valid scalar (the curve order is `2^384 - 2^128 - …` so the
|
||||
rejection rate is negligible).
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./p384.select.js";
|
||||
@@ -0,0 +1,70 @@
|
||||
# p384.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p384.select.ts` is the build-time primitive selector for NIST P-384 in
|
||||
`@suaveplan/crypto-curve`. Generated by `bun scripts/gen-crypto-selects.ts`,
|
||||
it re-exports one concrete backend — `p384.noble.ts` or `p384.native.ts` —
|
||||
according to the `p384` field in `crypto.config.ts`.
|
||||
|
||||
Callers import through the stable `p384.ts` entry point. This selector level
|
||||
insulates all call sites from backend changes: regenerating this file is the
|
||||
only step required to switch implementations. Currently the Noble backend is
|
||||
active because the native stub throws `PrimitiveNotImplemented`.
|
||||
|
||||
## Features
|
||||
|
||||
- Single re-export of `getPublicKey`, `sign`, `verify`, and `randomPrivateKey`.
|
||||
- Generated — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `p384` to `'noble'` or `'native'`.
|
||||
- Enables dead-code elimination of the inactive backend.
|
||||
- Maintains a stable API across backend switches.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p384 from "@suaveplan/crypto-curve/p384";
|
||||
|
||||
const sk = p384.randomPrivateKey();
|
||||
const pk = p384.getPublicKey(sk);
|
||||
console.log(pk.length); // 49
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p384 from "@suaveplan/crypto-curve/p384";
|
||||
import { sha384 } from "@suaveplan/crypto-hash";
|
||||
|
||||
function roundTrip(message: Uint8Array): boolean {
|
||||
const sk = p384.randomPrivateKey();
|
||||
const pk = p384.getPublicKey(sk);
|
||||
const digest = sha384(message);
|
||||
const sig = p384.sign(digest, sk);
|
||||
return p384.verify(sig, digest, pk);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the active implementation:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 49-byte compressed
|
||||
P-384 public key.
|
||||
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array` — 96-byte
|
||||
compact ECDSA signature.
|
||||
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array` — rejection-sampled 48-byte key.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The selector file contains a single `export * from` line and is marked
|
||||
`/* GENERATED — DO NOT EDIT */`. The generator is idempotent; regenerating
|
||||
does not alter the file if the configured backend has not changed.
|
||||
Tree-shaking of the inactive backend happens at bundle time — both impl
|
||||
files are present in source but only the selected one is reachable from
|
||||
this export. Adding a new backend (e.g. a WebCrypto path) is a one-line
|
||||
edit to the generator plus the new impl file.
|
||||
@@ -0,0 +1,2 @@
|
||||
/* GENERATED — DO NOT EDIT. Regenerate with `bun scripts/gen-crypto-selects.ts`. */
|
||||
export * from "../impl/p384.noble/p384.noble.js";
|
||||
@@ -0,0 +1,88 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import * as p384Native from "./impl/p384.native/p384.native.js";
|
||||
import * as p384Noble from "./impl/p384.noble/p384.noble.js";
|
||||
|
||||
describe("p384 (noble impl) — self-roundtrip", () => {
|
||||
it("generates a valid 48-byte private key", { timeout: 5000 }, () => {
|
||||
const sk = p384Noble.randomPrivateKey();
|
||||
expect(sk.length).toBe(48);
|
||||
});
|
||||
|
||||
it("sign + verify round-trips", { timeout: 5000 }, () => {
|
||||
const sk = p384Noble.randomPrivateKey();
|
||||
const pk = p384Noble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(48).fill(0x42);
|
||||
const sig = p384Noble.sign(digest, sk);
|
||||
expect(sig.length).toBe(96); // compact (r || s) — 48 + 48
|
||||
expect(p384Noble.verify(sig, digest, pk)).toBe(true);
|
||||
});
|
||||
|
||||
it("verify returns false for tampered signature (no throw)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = p384Noble.randomPrivateKey();
|
||||
const pk = p384Noble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(48).fill(0x11);
|
||||
const sig = p384Noble.sign(digest, sk);
|
||||
sig[0] = (sig[0] ?? 0) ^ 0xff;
|
||||
expect(p384Noble.verify(sig, digest, pk)).toBe(false);
|
||||
});
|
||||
|
||||
it("verify returns false for bogus input (caught)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
p384Noble.verify(
|
||||
new Uint8Array(96),
|
||||
new Uint8Array(48),
|
||||
new Uint8Array(49)
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble throws on empty public key", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
p384Noble.verify(
|
||||
new Uint8Array(96),
|
||||
new Uint8Array(48),
|
||||
new Uint8Array()
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble throws on non-Uint8Array input", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
p384Noble.verify(new Uint8Array(96), new Uint8Array(48), {
|
||||
not: "a Uint8Array",
|
||||
} as unknown as Uint8Array)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("p384 (native stub)", () => {
|
||||
it("every operation throws PrimitiveNotImplemented", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => p384Native.getPublicKey(new Uint8Array(48))).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
expect(() =>
|
||||
p384Native.sign(new Uint8Array(48), new Uint8Array(48))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() =>
|
||||
p384Native.verify(
|
||||
new Uint8Array(96),
|
||||
new Uint8Array(48),
|
||||
new Uint8Array(49)
|
||||
)
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() => p384Native.randomPrivateKey()).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
randomPrivateKey,
|
||||
sign,
|
||||
verify,
|
||||
} from "./p384.select/p384.select.js";
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, randomPrivateKey, sign, verify } from "./p521.native.js";
|
||||
@@ -0,0 +1,89 @@
|
||||
# p521.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p521.native.ts` is the native-backend placeholder for the NIST P-521
|
||||
primitive in `@suaveplan/crypto-curve`. It exports the complete P-521
|
||||
surface — `getPublicKey`, `sign`, `verify`, and `randomPrivateKey` —
|
||||
each throwing `PrimitiveNotImplemented("p521")`.
|
||||
|
||||
P-521 has native WebCrypto support in all modern browsers
|
||||
(`SubtleCrypto.sign` with `{ name: "ECDSA", hash: "SHA-512" }`) and
|
||||
Node.js 15+. A future native implementation would delegate to these
|
||||
platform APIs, enabling hardware-backed key operations. The stub keeps
|
||||
the layout complete until that work lands.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies the full P-521 interface: `getPublicKey`, `sign`, `verify`,
|
||||
`randomPrivateKey`.
|
||||
- Throws `PrimitiveNotImplemented("p521")` from every function.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets p521 to 'native'.
|
||||
import * as p521 from "@suaveplan/crypto-curve/p521";
|
||||
|
||||
try {
|
||||
p521.randomPrivateKey();
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: p521
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p521 from "@suaveplan/crypto-curve/p521";
|
||||
|
||||
async function nativeP521Available(): Promise<boolean> {
|
||||
try {
|
||||
p521.randomPrivateKey();
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
await crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-521" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All functions throw `PrimitiveNotImplemented("p521")`:
|
||||
|
||||
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(_signature: Uint8Array, _digest: Uint8Array, _publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
P-521 native support varies across platforms — Safari historically lagged
|
||||
behind Chrome / Firefox in WebCrypto P-521 support and capabilities
|
||||
detection is essential for production rollouts. A native implementation
|
||||
would handle the byte-width quirks (66-byte coordinates left-padded to
|
||||
67-byte compressed pubkeys) that WebCrypto already encodes. Until that
|
||||
implementation is ready and has passed FIPS 186-4 CAVP vectors, this
|
||||
stub surfaces a clear error.
|
||||
|
||||
### Status
|
||||
|
||||
Placeholder native stub — throws `PrimitiveNotImplemented` until a
|
||||
WebCrypto `ECDSA P-521` bridge is wired through. Today the
|
||||
`p521.select` selector falls through to the Noble implementation in
|
||||
every engine mode; the production path is exercised by the noble
|
||||
roundtrip suite in `p521.test.ts`.
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
export function getPublicKey(_privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p521");
|
||||
}
|
||||
export function sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p521");
|
||||
}
|
||||
export function verify(
|
||||
_signature: Uint8Array,
|
||||
_digest: Uint8Array,
|
||||
_publicKey: Uint8Array
|
||||
): boolean {
|
||||
throw new PrimitiveNotImplemented("p521");
|
||||
}
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("p521");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, randomPrivateKey, sign, verify } from "./p521.noble.js";
|
||||
@@ -0,0 +1,92 @@
|
||||
# p521.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p521.noble.ts` is the Noble-backed concrete implementation of the NIST
|
||||
P-521 primitive in `@suaveplan/crypto-curve`. It wraps the `p521` object
|
||||
from `@noble/curves/p521`, providing ECDSA signing and verification,
|
||||
public-key derivation, and rejection-sampled private-key generation.
|
||||
|
||||
P-521 (secp521r1, FIPS 186-4) is the highest-security NIST curve and is
|
||||
required by COSE `ES512` (RFC 9053 §2.1), some FIPS-mode deployments, and
|
||||
high-assurance long-lived identity certificates. Signatures are returned
|
||||
in compact 132-byte (r ‖ s) form, where r and s are each 66 bytes (the
|
||||
521-bit scalar left-padded). Callers must pre-hash the message; this
|
||||
module operates on digests.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 67-byte compressed P-521 public key.
|
||||
- `sign(digest, privateKey)` — deterministic ECDSA (RFC 6979); compact
|
||||
132-byte output.
|
||||
- `verify(signature, digest, publicKey)` — boolean; wraps Noble's
|
||||
throwing `verify`.
|
||||
- `randomPrivateKey()` — rejection-sampled 66-byte key via
|
||||
`@suaveplan/crypto-utils/random`.
|
||||
- Noble's `p521.utils.isValidPrivateKey` gates the rejection loop.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p521 from "@suaveplan/crypto-curve/p521";
|
||||
import { sha512 } from "@suaveplan/crypto-hash";
|
||||
|
||||
const sk = p521.randomPrivateKey();
|
||||
const pk = p521.getPublicKey(sk);
|
||||
const digest = sha512(new TextEncoder().encode("payload"));
|
||||
const sig = p521.sign(digest, sk);
|
||||
console.log(p521.verify(sig, digest, pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p521 from "@suaveplan/crypto-curve/p521";
|
||||
import { sha512 } from "@suaveplan/crypto-hash";
|
||||
import { zeroize } from "@suaveplan/crypto-utils";
|
||||
|
||||
function ephemeralSign(data: Uint8Array): { pk: Uint8Array; sig: Uint8Array } {
|
||||
const sk = p521.randomPrivateKey();
|
||||
try {
|
||||
return {
|
||||
pk: p521.getPublicKey(sk),
|
||||
sig: p521.sign(sha512(data), sk),
|
||||
};
|
||||
} finally {
|
||||
zeroize(sk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
66-byte scalar → 67-byte compressed public key.
|
||||
|
||||
### `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
Pre-hashed input (64 bytes for SHA-512) → 132-byte compact (r ‖ s)
|
||||
signature.
|
||||
|
||||
### `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
|
||||
Returns `false` for malformed inputs rather than throwing.
|
||||
|
||||
### `randomPrivateKey(): Uint8Array`
|
||||
|
||||
Returns a valid P-521 scalar. Retries until `isValidPrivateKey` passes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`sign` calls Noble's `.toBytes("compact")` to produce the flat 132-byte
|
||||
format. The 66-byte width comes from rounding 521 bits up to the next
|
||||
byte boundary; the high byte of every scalar / coordinate has only its
|
||||
low 9 bits populated for the maximum value. `verify` wraps Noble's call
|
||||
in try/catch because some malformed input combinations cause exceptions
|
||||
rather than `false` returns. The rejection rate for `randomPrivateKey`
|
||||
is approximately `1 − 2^(-7)` because only the low 9 bits of the high
|
||||
byte matter; the loop typically converges in one or two iterations.
|
||||
@@ -0,0 +1,39 @@
|
||||
import { p521 as nobleP521 } from "@noble/curves/p521";
|
||||
import { randomBytes } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
/**
|
||||
* NIST P-521 (secp521r1, FIPS 186-4) ECDSA, noble-backed.
|
||||
*
|
||||
* Private keys are 66 bytes (scalar — 521 bits left-padded to 66 bytes),
|
||||
* public keys are 67-byte compressed or 133-byte uncompressed (noble
|
||||
* returns compressed by default). Signatures returned in compact 132-byte
|
||||
* (r || s) form. Caller is expected to pre-hash the message (SHA-512 for
|
||||
* ES512); this module takes the digest.
|
||||
*/
|
||||
export function getPublicKey(privateKey: Uint8Array): Uint8Array {
|
||||
return nobleP521.getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
export function sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array {
|
||||
return nobleP521.sign(digest, privateKey).toBytes("compact");
|
||||
}
|
||||
|
||||
export function verify(
|
||||
signature: Uint8Array,
|
||||
digest: Uint8Array,
|
||||
publicKey: Uint8Array
|
||||
): boolean {
|
||||
try {
|
||||
return nobleP521.verify(signature, digest, publicKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
let sk: Uint8Array;
|
||||
do {
|
||||
sk = randomBytes(66);
|
||||
} while (!nobleP521.utils.isValidPrivateKey(sk));
|
||||
return sk;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# p521
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_- p521.native — <!-- begin:auto-docs --> _(no submodul…
|
||||
- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_- p521.native — <!-- begin:aut…
|
||||
- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_- p521.native — <!-- begin:auto-docs --> _(no submodu…
|
||||
- [p521.native](./impl/p521.native/p521.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p521.noble](./impl/p521.noble/p521.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [p521.select](./p521.select/p521.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [p521](./p521.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`p521.ts` is the stable public entry point for the NIST P-521 elliptic-curve
|
||||
primitive in `@suaveplan/crypto-curve`. It re-exports everything from
|
||||
`p521.select.ts`, which forwards to either the Noble-backed or native
|
||||
implementation based on `crypto.config.ts`.
|
||||
|
||||
P-521 (also known as secp521r1, standardised in FIPS 186-4) is the highest-
|
||||
security NIST curve and is required by COSE `ES512` (RFC 9053 §2.1), some
|
||||
FIPS-mode deployments, and high-assurance long-lived identity certificates.
|
||||
The curve has a 521-bit order; private keys and coordinates are
|
||||
left-padded to 66 bytes so the entire data path stays byte-aligned.
|
||||
Compact ECDSA signatures are 132 bytes (2 × 66).
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 67-byte compressed public key.
|
||||
- `sign(digest, privateKey)` — compact 132-byte ECDSA signature (r ‖ s).
|
||||
Caller pre-hashes (SHA-512 for ES512 wire compatibility).
|
||||
- `verify(signature, digest, publicKey)` — boolean; never throws on
|
||||
malformed inputs.
|
||||
- `randomPrivateKey()` — rejection-sampled 66-byte private key.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p521 from "@suaveplan/crypto-curve/p521";
|
||||
import { sha512 } from "@suaveplan/crypto-hash";
|
||||
|
||||
const sk = p521.randomPrivateKey();
|
||||
const pk = p521.getPublicKey(sk);
|
||||
const msg = new TextEncoder().encode("hello P-521");
|
||||
const sig = p521.sign(sha512(msg), sk);
|
||||
console.log(p521.verify(sig, sha512(msg), pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p521 from "@suaveplan/crypto-curve/p521";
|
||||
import { sha512 } from "@suaveplan/crypto-hash";
|
||||
|
||||
// COSE_Sign1 verify path for an ES512-signed message.
|
||||
function verifyCoseSign1Es512(
|
||||
tbs: Uint8Array,
|
||||
signature: Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
): boolean {
|
||||
return p521.verify(signature, sha512(tbs), publicKey);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All symbols re-exported from `p521.select.ts`:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a zero-logic re-export barrel. Its sole purpose is the stable
|
||||
import path `@suaveplan/crypto-curve/p521`. Callers MUST pre-hash with
|
||||
SHA-512 (or another 512-bit digest) before calling `sign` — ECDSA does not
|
||||
hash internally. The 66-byte coordinate width comes from the 521-bit curve
|
||||
order rounded up to the next byte boundary; the high byte of each scalar
|
||||
is mostly zero (only the low 9 bits are populated for the maximum value).
|
||||
This matches the COSE wire format which also uses 66-byte left-padded
|
||||
coordinates for P-521 keys.
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./p521.select.js";
|
||||
@@ -0,0 +1,70 @@
|
||||
# p521.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`p521.select.ts` is the build-time primitive selector for NIST P-521 in
|
||||
`@suaveplan/crypto-curve`. Generated by `bun scripts/gen-crypto-selects.ts`,
|
||||
it re-exports one concrete backend — `p521.noble.ts` or `p521.native.ts` —
|
||||
according to the `p521` field in `crypto.config.ts`.
|
||||
|
||||
Callers import through the stable `p521.ts` entry point. This selector
|
||||
level insulates all call sites from backend changes: regenerating this
|
||||
file is the only step required to switch implementations. Currently the
|
||||
Noble backend is active because the native stub throws
|
||||
`PrimitiveNotImplemented`.
|
||||
|
||||
## Features
|
||||
|
||||
- Single re-export of `getPublicKey`, `sign`, `verify`, and `randomPrivateKey`.
|
||||
- Generated — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `p521` to `'noble'` or `'native'`.
|
||||
- Enables dead-code elimination of the inactive backend.
|
||||
- Maintains a stable API across backend switches.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as p521 from "@suaveplan/crypto-curve/p521";
|
||||
|
||||
const sk = p521.randomPrivateKey();
|
||||
const pk = p521.getPublicKey(sk);
|
||||
console.log(pk.length); // 67
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as p521 from "@suaveplan/crypto-curve/p521";
|
||||
import { sha512 } from "@suaveplan/crypto-hash";
|
||||
|
||||
function roundTrip(message: Uint8Array): boolean {
|
||||
const sk = p521.randomPrivateKey();
|
||||
const pk = p521.getPublicKey(sk);
|
||||
const digest = sha512(message);
|
||||
const sig = p521.sign(digest, sk);
|
||||
return p521.verify(sig, digest, pk);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the active implementation:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 67-byte compressed
|
||||
P-521 public key.
|
||||
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array` — 132-byte
|
||||
compact ECDSA signature.
|
||||
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
- `randomPrivateKey(): Uint8Array` — rejection-sampled 66-byte key.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The selector file contains a single `export * from` line and is marked
|
||||
`/* GENERATED — DO NOT EDIT */`. P-521 keys carry an additional invariant
|
||||
that callers need to be aware of: the high byte of every scalar / coordinate
|
||||
must have the top 7 bits zero (the curve has a 521-bit order, byte-aligned
|
||||
to 66 bytes). The noble backend enforces this in `isValidPrivateKey`; the
|
||||
rejection loop in `randomPrivateKey` retries until the constraint holds.
|
||||
@@ -0,0 +1,2 @@
|
||||
/* GENERATED — DO NOT EDIT. Regenerate with `bun scripts/gen-crypto-selects.ts`. */
|
||||
export * from "../impl/p521.noble/p521.noble.js";
|
||||
@@ -0,0 +1,88 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import * as p521Native from "./impl/p521.native/p521.native.js";
|
||||
import * as p521Noble from "./impl/p521.noble/p521.noble.js";
|
||||
|
||||
describe("p521 (noble impl) — self-roundtrip", () => {
|
||||
it("generates a valid 66-byte private key", { timeout: 5000 }, () => {
|
||||
const sk = p521Noble.randomPrivateKey();
|
||||
expect(sk.length).toBe(66);
|
||||
});
|
||||
|
||||
it("sign + verify round-trips", { timeout: 5000 }, () => {
|
||||
const sk = p521Noble.randomPrivateKey();
|
||||
const pk = p521Noble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(64).fill(0x42); // SHA-512 digest length
|
||||
const sig = p521Noble.sign(digest, sk);
|
||||
expect(sig.length).toBe(132); // compact (r || s) — 66 + 66
|
||||
expect(p521Noble.verify(sig, digest, pk)).toBe(true);
|
||||
});
|
||||
|
||||
it("verify returns false for tampered signature (no throw)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = p521Noble.randomPrivateKey();
|
||||
const pk = p521Noble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(64).fill(0x11);
|
||||
const sig = p521Noble.sign(digest, sk);
|
||||
sig[0] = (sig[0] ?? 0) ^ 0xff;
|
||||
expect(p521Noble.verify(sig, digest, pk)).toBe(false);
|
||||
});
|
||||
|
||||
it("verify returns false for bogus input (caught)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
p521Noble.verify(
|
||||
new Uint8Array(132),
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(67)
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble throws on empty public key", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
p521Noble.verify(
|
||||
new Uint8Array(132),
|
||||
new Uint8Array(64),
|
||||
new Uint8Array()
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble throws on non-Uint8Array input", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
p521Noble.verify(new Uint8Array(132), new Uint8Array(64), {
|
||||
not: "a Uint8Array",
|
||||
} as unknown as Uint8Array)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("p521 (native stub)", () => {
|
||||
it("every operation throws PrimitiveNotImplemented", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => p521Native.getPublicKey(new Uint8Array(66))).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
expect(() =>
|
||||
p521Native.sign(new Uint8Array(64), new Uint8Array(66))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() =>
|
||||
p521Native.verify(
|
||||
new Uint8Array(132),
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(67)
|
||||
)
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() => p521Native.randomPrivateKey()).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
randomPrivateKey,
|
||||
sign,
|
||||
verify,
|
||||
} from "./p521.select/p521.select.js";
|
||||
@@ -0,0 +1,10 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
getSharedSecret,
|
||||
randomPrivateKey,
|
||||
recoverPublicKey,
|
||||
sign,
|
||||
signWithRecovery,
|
||||
verify,
|
||||
verifyWithRecovery,
|
||||
} from "./secp256k1.native.js";
|
||||
@@ -0,0 +1,65 @@
|
||||
# secp256k1.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`secp256k1.native.ts` is the native-backend placeholder for the secp256k1 primitive in `@suaveplan/crypto-curve`. It exports the complete secp256k1 surface — `getPublicKey`, `sign`, `verify`, `getSharedSecret`, and `randomPrivateKey` — each throwing `PrimitiveNotImplemented("secp256k1")`.
|
||||
|
||||
This stub keeps the two-implementation selector layout structurally complete. When the in-house secp256k1 implementation is ready and has passed the required test vectors, this file is replaced with the real code and the selector is regenerated to point here.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies the full secp256k1 interface: `getPublicKey`, `sign`, `verify`, `getSharedSecret`, `randomPrivateKey`.
|
||||
- Throws `PrimitiveNotImplemented("secp256k1")` from every function.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
- Provides clear, typed errors identifying which primitive is unimplemented.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets secp256k1 to 'native'.
|
||||
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
|
||||
|
||||
try {
|
||||
secp256k1.randomPrivateKey();
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: secp256k1
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Capability detection — safe to call when backend may not be implemented.
|
||||
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
|
||||
|
||||
function isSecp256k1Available(): boolean {
|
||||
try {
|
||||
secp256k1.randomPrivateKey();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All functions throw `PrimitiveNotImplemented("secp256k1")`:
|
||||
|
||||
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(_signature: Uint8Array, _digest: Uint8Array, _publicKey: Uint8Array): boolean`
|
||||
- `getSharedSecret(_privateKey: Uint8Array, _peerPublicKey: Uint8Array): Uint8Array`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Each function has underscore-prefixed parameters to suppress unused-variable diagnostics without inline suppressions. `PrimitiveNotImplemented` from `@suaveplan/crypto-utils/errors` extends `SuaveplanError` and participates in the full error serialisation, cause-chain traversal, and OTel span-event system. When the native implementation lands, parameter prefixes are removed, the `throw` replaced, and the selector regenerated — no changes are required outside this file.
|
||||
|
||||
### Status
|
||||
|
||||
Placeholder native stub — throws `PrimitiveNotImplemented` until a libsecp256k1 binding is added to the `native` engine slot. Until then, `secp256k1.select` routes every call through `@noble/curves/secp256k1`, which is constant-time by construction and differentially fuzzed against `@noble/secp256k1` via the cross-impl harness in `@suaveplan/crypto-utils`.
|
||||
@@ -0,0 +1,43 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
export function getPublicKey(_privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("secp256k1");
|
||||
}
|
||||
export function sign(_digest: Uint8Array, _privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("secp256k1");
|
||||
}
|
||||
export function verify(
|
||||
_signature: Uint8Array,
|
||||
_digest: Uint8Array,
|
||||
_publicKey: Uint8Array
|
||||
): boolean {
|
||||
throw new PrimitiveNotImplemented("secp256k1");
|
||||
}
|
||||
export function getSharedSecret(
|
||||
_privateKey: Uint8Array,
|
||||
_peerPublicKey: Uint8Array
|
||||
): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("secp256k1");
|
||||
}
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("secp256k1");
|
||||
}
|
||||
export function signWithRecovery(
|
||||
_digest: Uint8Array,
|
||||
_privateKey: Uint8Array
|
||||
): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("secp256k1");
|
||||
}
|
||||
export function recoverPublicKey(
|
||||
_signatureWithRecovery: Uint8Array,
|
||||
_digest: Uint8Array
|
||||
): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("secp256k1");
|
||||
}
|
||||
export function verifyWithRecovery(
|
||||
_signatureWithRecovery: Uint8Array,
|
||||
_digest: Uint8Array,
|
||||
_expectedPublicKey: Uint8Array
|
||||
): boolean {
|
||||
throw new PrimitiveNotImplemented("secp256k1");
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
getSharedSecret,
|
||||
randomPrivateKey,
|
||||
recoverPublicKey,
|
||||
sign,
|
||||
signWithRecovery,
|
||||
verify,
|
||||
verifyWithRecovery,
|
||||
} from "./secp256k1.noble.js";
|
||||
@@ -0,0 +1,91 @@
|
||||
# secp256k1.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`secp256k1.noble.ts` is the Noble-backed concrete implementation of the secp256k1 primitive in `@suaveplan/crypto-curve`. It wraps the secp256k1 operations from `@noble/curves/secp256k1`, providing public-key derivation, ECDSA signing and verification, ECDH shared-secret computation, and random private-key generation.
|
||||
|
||||
secp256k1 is the domain parameter set used by Bitcoin, Ethereum, and a wide range of blockchain and Web3 protocols. Private keys are 32-byte scalars; public keys are 33-byte compressed points; signatures are compact 64-byte (r ‖ s) form. Callers are expected to pre-hash their messages before signing — this module operates on digests.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 33-byte compressed public key via point multiplication.
|
||||
- `sign(digest, privateKey)` — deterministic ECDSA signature (RFC 6979); compact 64-byte output.
|
||||
- `verify(signature, digest, publicKey)` — boolean result; catches Noble exceptions for malformed inputs.
|
||||
- `getSharedSecret(privateKey, peerPublicKey)` — ECDH shared secret for key agreement.
|
||||
- `randomPrivateKey()` — rejection-sampled 32-byte key; retries until `isValidPrivateKey` passes.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
|
||||
const sk = secp256k1.randomPrivateKey();
|
||||
const pk = secp256k1.getPublicKey(sk);
|
||||
const msg = new TextEncoder().encode("hello secp256k1");
|
||||
const sig = secp256k1.sign(sha256(msg), sk);
|
||||
console.log(secp256k1.verify(sig, sha256(msg), pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
|
||||
import { hkdf } from "@suaveplan/crypto-hash/kdf";
|
||||
|
||||
// ECDH + HKDF to derive an encryption key.
|
||||
function deriveSharedKey(
|
||||
localSk: Uint8Array,
|
||||
remotePk: Uint8Array,
|
||||
context: string
|
||||
): Uint8Array {
|
||||
const raw = secp256k1.getSharedSecret(localSk, remotePk);
|
||||
return hkdf("SHA-256", raw, {
|
||||
salt: new Uint8Array(32),
|
||||
info: new TextEncoder().encode(context),
|
||||
length: 32,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
Derives the 33-byte compressed public key for `privateKey`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `privateKey` | `Uint8Array` | 32-byte scalar in [1, n−1]. |
|
||||
|
||||
**Returns:** 33-byte compressed public key.
|
||||
|
||||
### `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
Signs a pre-hashed digest. Uses RFC 6979 deterministic nonce generation.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `digest` | `Uint8Array` | 32-byte hash of the message. |
|
||||
| `privateKey` | `Uint8Array` | 32-byte private key. |
|
||||
|
||||
**Returns:** 64-byte compact (r ‖ s) signature.
|
||||
|
||||
### `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
|
||||
Verifies a signature. Returns `false` for malformed inputs instead of throwing.
|
||||
|
||||
### `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
|
||||
|
||||
Computes the ECDH shared secret. The result must be passed through a KDF before use as a symmetric key.
|
||||
|
||||
### `randomPrivateKey(): Uint8Array`
|
||||
|
||||
Returns a uniformly random 32-byte valid private key. Uses rejection sampling.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`verify` wraps the Noble call in try/catch because Noble throws for some malformed input combinations (non-canonical encodings, out-of-range coordinates). The wrapper converts all exceptions to `false`, maintaining a uniform boolean contract. `randomPrivateKey` uses rejection sampling because not all 32-byte sequences are valid secp256k1 scalars — the probability of rejection is negligible (~1 in 2¹²⁸) so the loop almost never iterates more than once. `sign` uses Noble's `toBytes("compact")` to get the flat 64-byte form rather than DER; callers that need DER encoding must convert externally.
|
||||
@@ -0,0 +1,134 @@
|
||||
import { secp256k1 as nobleSecp } from "@noble/curves/secp256k1";
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-utils/constant-time";
|
||||
import { randomBytes } from "@suaveplan/crypto-utils/random";
|
||||
import { CRYPTO_CURVE_CODES } from "../../../../errors/codes/codes.js";
|
||||
import { CryptoCurveError } from "../../../../errors/crypto-curve-error/crypto-curve-error.js";
|
||||
|
||||
/**
|
||||
* secp256k1 (Bitcoin / Ethereum) ECDSA + ECDH, noble-backed.
|
||||
*
|
||||
* Private keys are 32 bytes; public keys are 33-byte compressed form.
|
||||
* Signatures are returned in compact 64-byte (r || s) form. Caller
|
||||
* pre-hashes the message; this module takes the digest.
|
||||
*/
|
||||
export function getPublicKey(privateKey: Uint8Array): Uint8Array {
|
||||
return nobleSecp.getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
export function sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array {
|
||||
return nobleSecp.sign(digest, privateKey).toBytes("compact");
|
||||
}
|
||||
|
||||
export function verify(
|
||||
signature: Uint8Array,
|
||||
digest: Uint8Array,
|
||||
publicKey: Uint8Array
|
||||
): boolean {
|
||||
try {
|
||||
return nobleSecp.verify(signature, digest, publicKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSharedSecret(
|
||||
privateKey: Uint8Array,
|
||||
peerPublicKey: Uint8Array
|
||||
): Uint8Array {
|
||||
return nobleSecp.getSharedSecret(privateKey, peerPublicKey);
|
||||
}
|
||||
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
let sk: Uint8Array;
|
||||
do {
|
||||
sk = randomBytes(32);
|
||||
} while (!nobleSecp.utils.isValidPrivateKey(sk));
|
||||
return sk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign with recovery byte. Returns a 65-byte signature: 64-byte compact
|
||||
* (r || s) followed by a 1-byte recovery value (`0` or `1`). The recovery
|
||||
* byte lets a verifier reconstruct the public key directly from the signed
|
||||
* digest, without needing the public key out-of-band — used by Ethereum
|
||||
* transactions, Bitcoin SegWit, and other "lean signed message" formats.
|
||||
*/
|
||||
export function signWithRecovery(
|
||||
digest: Uint8Array,
|
||||
privateKey: Uint8Array
|
||||
): Uint8Array {
|
||||
const sig = nobleSecp.sign(digest, privateKey);
|
||||
const compact = sig.toBytes("compact");
|
||||
const out = new Uint8Array(65);
|
||||
out.set(compact, 0);
|
||||
out[64] = sig.recovery as 0 | 1;
|
||||
return out;
|
||||
}
|
||||
|
||||
function checkRecoverySignature(signatureWithRecovery: Uint8Array): {
|
||||
sig: Uint8Array;
|
||||
recovery: number;
|
||||
} {
|
||||
if (signatureWithRecovery.length === 64) {
|
||||
throw new CryptoCurveError(
|
||||
CRYPTO_CURVE_CODES.RECOVERY_BYTE_MISSING,
|
||||
"error.crypto.curve.recovery-byte-missing",
|
||||
{ context: { signatureLength: 64 } }
|
||||
);
|
||||
}
|
||||
if (signatureWithRecovery.length !== 65) {
|
||||
throw new CryptoCurveError(
|
||||
CRYPTO_CURVE_CODES.RECOVERY_BYTE_MISSING,
|
||||
"error.crypto.curve.recovery-byte-missing",
|
||||
{ context: { signatureLength: signatureWithRecovery.length } }
|
||||
);
|
||||
}
|
||||
const recovery = signatureWithRecovery[64]!;
|
||||
if (recovery !== 0 && recovery !== 1) {
|
||||
throw new CryptoCurveError(
|
||||
CRYPTO_CURVE_CODES.RECOVERY_BYTE_INVALID,
|
||||
"error.crypto.curve.recovery-byte-invalid",
|
||||
{ context: { recoveryByte: recovery } }
|
||||
);
|
||||
}
|
||||
return { sig: signatureWithRecovery.subarray(0, 64), recovery };
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover the 33-byte compressed public key from a 65-byte signature-with-
|
||||
* recovery and the original digest. Throws `CryptoCurveError` with
|
||||
* `RECOVERY_BYTE_MISSING` for 64-byte inputs and `RECOVERY_BYTE_INVALID`
|
||||
* when the recovery byte is not in {0, 1}.
|
||||
*/
|
||||
export function recoverPublicKey(
|
||||
signatureWithRecovery: Uint8Array,
|
||||
digest: Uint8Array
|
||||
): Uint8Array {
|
||||
const { sig, recovery } = checkRecoverySignature(signatureWithRecovery);
|
||||
const sigObj = nobleSecp.Signature.fromBytes(sig, "compact");
|
||||
const recovered = sigObj.addRecoveryBit(recovery).recoverPublicKey(digest);
|
||||
return recovered.toBytes(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signature-with-recovery against a known public key. Returns
|
||||
* `true` only when the recovered public key matches `expectedPublicKey`
|
||||
* (compared in constant time on the raw bytes). Returns `false` on
|
||||
* mismatch (no exception). Inputs that fail structural validation
|
||||
* (missing or invalid recovery byte) still throw `CryptoCurveError`.
|
||||
*/
|
||||
export function verifyWithRecovery(
|
||||
signatureWithRecovery: Uint8Array,
|
||||
digest: Uint8Array,
|
||||
expectedPublicKey: Uint8Array
|
||||
): boolean {
|
||||
let recovered: Uint8Array;
|
||||
try {
|
||||
recovered = recoverPublicKey(signatureWithRecovery, digest);
|
||||
} catch (err) {
|
||||
if (err instanceof CryptoCurveError) throw err;
|
||||
return false;
|
||||
}
|
||||
if (recovered.length !== expectedPublicKey.length) return false;
|
||||
return constantTimeEqual(recovered, expectedPublicKey);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
getSharedSecret,
|
||||
randomPrivateKey,
|
||||
recoverPublicKey,
|
||||
sign,
|
||||
signWithRecovery,
|
||||
verify,
|
||||
verifyWithRecovery,
|
||||
} from "./secp256k1";
|
||||
@@ -0,0 +1,88 @@
|
||||
# secp256k1
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_- secp256k1.native — <!-- begin:auto-docs --> _(no sub…
|
||||
- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_- secp256k1.native — <!-- begi…
|
||||
- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_- secp256k1.native — <!-- begin:auto-docs --> _(no su…
|
||||
- [secp256k1.native](./impl/secp256k1.native/secp256k1.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secp256k1.noble](./impl/secp256k1.noble/secp256k1.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secp256k1.select](./secp256k1.select/secp256k1.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [secp256k1](./secp256k1.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`secp256k1.ts` is the stable public entry point for the secp256k1 elliptic-curve primitive in `@suaveplan/crypto-curve`. It re-exports everything from the generated selector file `secp256k1.select.ts`, which in turn forwards to either the Noble-backed or native implementation depending on `crypto.config.ts`.
|
||||
|
||||
secp256k1 is the Koblitz curve used by Bitcoin, Ethereum, and many other blockchain protocols. It supports ECDSA signing and ECDH key agreement. Callers import from this file and remain insulated from implementation changes.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — derives the 33-byte compressed public key.
|
||||
- `sign(digest, privateKey)` — produces a compact 64-byte ECDSA signature (r ‖ s). Caller must pre-hash the message.
|
||||
- `verify(signature, digest, publicKey)` — returns `boolean`, never throws on malformed inputs.
|
||||
- `getSharedSecret(privateKey, peerPublicKey)` — computes ECDH shared secret.
|
||||
- `randomPrivateKey()` — generates a cryptographically valid 32-byte private key via rejection sampling.
|
||||
- `signWithRecovery(digest, privateKey)` — produces a 65-byte signature (`64-byte compact || 1-byte recovery`). Recovery byte is `0` or `1`.
|
||||
- `recoverPublicKey(signatureWithRecovery, digest)` — recovers the 33-byte compressed public key from a 65-byte signature-with-recovery and the original digest. Throws `CryptoCurveError(RECOVERY_BYTE_MISSING)` for 64-byte inputs and `CryptoCurveError(RECOVERY_BYTE_INVALID)` when the recovery byte is not `0` or `1`.
|
||||
- `verifyWithRecovery(signatureWithRecovery, digest, expectedPublicKey)` — recovers the public key and compares it to `expectedPublicKey` in constant time. Returns `false` on mismatch (does not throw); structural errors (`RECOVERY_BYTE_MISSING`, `RECOVERY_BYTE_INVALID`) still throw.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
|
||||
const sk = secp256k1.randomPrivateKey();
|
||||
const pk = secp256k1.getPublicKey(sk);
|
||||
const msg = new TextEncoder().encode("transaction data");
|
||||
const digest = sha256(msg);
|
||||
const sig = secp256k1.sign(digest, sk);
|
||||
console.log(secp256k1.verify(sig, digest, pk)); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
|
||||
import { hkdf } from "@suaveplan/crypto-hash/kdf";
|
||||
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
|
||||
|
||||
// Ephemeral ECDH for a sealed message.
|
||||
function sealedEphemeralKey(recipientPk: Uint8Array): {
|
||||
ephemeralPk: Uint8Array;
|
||||
sharedKey: Uint8Array;
|
||||
} {
|
||||
const ephemeralSk = secp256k1.randomPrivateKey();
|
||||
try {
|
||||
const ephemeralPk = secp256k1.getPublicKey(ephemeralSk);
|
||||
const rawSecret = secp256k1.getSharedSecret(ephemeralSk, recipientPk);
|
||||
const sharedKey = hkdf("SHA-256", rawSecret, {
|
||||
salt: new Uint8Array(32),
|
||||
info: new TextEncoder().encode("secp256k1-sealed"),
|
||||
length: 32,
|
||||
});
|
||||
return { ephemeralPk, sharedKey };
|
||||
} finally {
|
||||
zeroize(ephemeralSk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All symbols are re-exported from `secp256k1.select.ts`. See the active implementation (`secp256k1.noble.ts` or `secp256k1.native.ts`) for full parameter documentation.
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
- `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
- `signWithRecovery(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
- `recoverPublicKey(signatureWithRecovery: Uint8Array, digest: Uint8Array): Uint8Array`
|
||||
- `verifyWithRecovery(signatureWithRecovery: Uint8Array, digest: Uint8Array, expectedPublicKey: Uint8Array): boolean`
|
||||
|
||||
### Signature-with-recovery layout
|
||||
|
||||
The 65-byte form is `[r:32 | s:32 | recovery:1]` (big-endian). The recovery byte selects which of the two possible public keys derived from `(r, s, digest)` was the signer; valid values are `0` and `1`. Some libraries (for compressed-vs-uncompressed keys) emit `2` and `3` — those are explicitly rejected here because suaveplan always returns the compressed (33-byte) form, so the lift bit is implicit.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a zero-overhead re-export barrel. Its only role is to provide a stable import path (`@suaveplan/crypto-curve/secp256k1`) that survives future backend changes. The selector file underneath is generated; this file is not. Do not add logic here — it belongs in the concrete implementation files.
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./secp256k1.select.js";
|
||||
@@ -0,0 +1,55 @@
|
||||
# secp256k1.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`secp256k1.select.ts` is the build-time primitive selector for the secp256k1 implementation in `@suaveplan/crypto-curve`. It is generated by `bun scripts/gen-crypto-selects.ts` and re-exports exactly one backend — either `secp256k1.noble.ts` or `secp256k1.native.ts` — based on the `secp256k1` entry in `crypto.config.ts`.
|
||||
|
||||
Callers import through the public entry point `secp256k1.ts`, which delegates here. This indirection means switching from the Noble backend to the native backend requires only regenerating this file; all import paths above it remain unchanged.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-file re-export of all secp256k1 operations (`getPublicKey`, `sign`, `verify`, `getSharedSecret`, `randomPrivateKey`).
|
||||
- Generated by `bun scripts/gen-crypto-selects.ts` — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `secp256k1` to `'noble'` or `'native'`.
|
||||
- Enables static dead-code elimination of the unselected backend.
|
||||
- Maintains a stable API surface across backend switches.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Always import through the stable entry point, not this select file.
|
||||
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
|
||||
|
||||
const sk = secp256k1.randomPrivateKey();
|
||||
const pk = secp256k1.getPublicKey(sk);
|
||||
console.log(pk.length); // 33 (compressed)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as secp256k1 from "@suaveplan/crypto-curve/secp256k1";
|
||||
import { sha256 } from "@suaveplan/crypto-hash/hash";
|
||||
|
||||
// Sign a pre-hashed digest (hash-then-sign pattern).
|
||||
function signDigest(message: Uint8Array, privateKey: Uint8Array): Uint8Array {
|
||||
return secp256k1.sign(sha256(message), privateKey);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports all symbols from the active implementation. At the Noble backend these are:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
- `sign(digest: Uint8Array, privateKey: Uint8Array): Uint8Array`
|
||||
- `verify(signature: Uint8Array, digest: Uint8Array, publicKey: Uint8Array): boolean`
|
||||
- `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file contains a single `export * from` line and is marked `/* GENERATED — DO NOT EDIT */`. The generator reads `crypto.config.ts`, looks up the backend for `secp256k1`, and writes the appropriate path. Currently the Noble backend is the only production-ready option; the native stub throws `PrimitiveNotImplemented("secp256k1")`. The selector enables tree-shaking of the inactive backend at the bundler level, keeping the deployed bundle minimal.
|
||||
@@ -0,0 +1,2 @@
|
||||
/* GENERATED — DO NOT EDIT. Regenerate with `bun scripts/gen-crypto-selects.ts`. */
|
||||
export * from "../impl/secp256k1.noble/secp256k1.noble.js";
|
||||
@@ -0,0 +1,152 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { CryptoCurveError } from "../../errors/crypto-curve-error/crypto-curve-error.js";
|
||||
import * as secpNoble from "./impl/secp256k1.noble/secp256k1.noble.js";
|
||||
|
||||
describe("secp256k1 — recovery error-code conformance", () => {
|
||||
it("throws RECOVERY_BYTE_MISSING for 64-byte signature", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sig = new Uint8Array(64);
|
||||
let caught: unknown;
|
||||
try {
|
||||
secpNoble.recoverPublicKey(sig, new Uint8Array(32));
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(CryptoCurveError);
|
||||
expect((caught as CryptoCurveError).code).toBe(
|
||||
"CRYPTO.CURVE.RECOVERY_BYTE_MISSING"
|
||||
);
|
||||
expect((caught as CryptoCurveError).context?.signatureLength).toBe(64);
|
||||
});
|
||||
|
||||
it("throws RECOVERY_BYTE_MISSING for any non-65-byte signature", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
let caught: unknown;
|
||||
try {
|
||||
secpNoble.recoverPublicKey(new Uint8Array(32), new Uint8Array(32));
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(CryptoCurveError);
|
||||
expect((caught as CryptoCurveError).code).toBe(
|
||||
"CRYPTO.CURVE.RECOVERY_BYTE_MISSING"
|
||||
);
|
||||
});
|
||||
|
||||
it("throws RECOVERY_BYTE_INVALID when recovery byte is out of range", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
for (const recovery of [2, 3, 7, 0xff]) {
|
||||
const sig = new Uint8Array(65);
|
||||
sig[64] = recovery;
|
||||
let caught: unknown;
|
||||
try {
|
||||
secpNoble.recoverPublicKey(sig, new Uint8Array(32));
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(CryptoCurveError);
|
||||
expect((caught as CryptoCurveError).code).toBe(
|
||||
"CRYPTO.CURVE.RECOVERY_BYTE_INVALID"
|
||||
);
|
||||
expect((caught as CryptoCurveError).context?.recoveryByte).toBe(
|
||||
recovery
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("verifyWithRecovery propagates RECOVERY_BYTE_MISSING (does not silently return false)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sig = new Uint8Array(64);
|
||||
let caught: unknown;
|
||||
try {
|
||||
secpNoble.verifyWithRecovery(
|
||||
sig,
|
||||
new Uint8Array(32),
|
||||
new Uint8Array(33)
|
||||
);
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(CryptoCurveError);
|
||||
expect((caught as CryptoCurveError).code).toBe(
|
||||
"CRYPTO.CURVE.RECOVERY_BYTE_MISSING"
|
||||
);
|
||||
});
|
||||
|
||||
it("verifyWithRecovery propagates RECOVERY_BYTE_INVALID (does not silently return false)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sig = new Uint8Array(65);
|
||||
sig[64] = 4;
|
||||
let caught: unknown;
|
||||
try {
|
||||
secpNoble.verifyWithRecovery(
|
||||
sig,
|
||||
new Uint8Array(32),
|
||||
new Uint8Array(33)
|
||||
);
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(CryptoCurveError);
|
||||
expect((caught as CryptoCurveError).code).toBe(
|
||||
"CRYPTO.CURVE.RECOVERY_BYTE_INVALID"
|
||||
);
|
||||
});
|
||||
|
||||
it("verifyWithRecovery comparison is constant-time", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// We can't reliably measure timing in a unit test, but we can verify
|
||||
// that the public-key comparison delegates to constantTimeEqual (the
|
||||
// signed contract) by calling with two different mismatched keys —
|
||||
// the result is the same `false`.
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const digest = new Uint8Array(32).fill(0x33);
|
||||
const sig = secpNoble.signWithRecovery(digest, sk);
|
||||
const wrongKeyDifferentFirstByte = new Uint8Array(33);
|
||||
const wrongKeyDifferentLastByte = new Uint8Array(33);
|
||||
wrongKeyDifferentLastByte[32] = 0xff;
|
||||
expect(
|
||||
secpNoble.verifyWithRecovery(
|
||||
sig,
|
||||
digest,
|
||||
wrongKeyDifferentFirstByte
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
secpNoble.verifyWithRecovery(sig, digest, wrongKeyDifferentLastByte)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("RECOVERED_KEY_MISMATCH is registered for future use", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const { CRYPTO_CURVE_CODES } = await import(
|
||||
"../../errors/codes/codes.js"
|
||||
);
|
||||
expect(CRYPTO_CURVE_CODES.RECOVERED_KEY_MISMATCH).toBe(
|
||||
"CRYPTO.CURVE.RECOVERED_KEY_MISMATCH"
|
||||
);
|
||||
});
|
||||
|
||||
it("CryptoCurveError accepts both with and without options bundle", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const a = new CryptoCurveError(
|
||||
"CRYPTO.CURVE.RECOVERED_KEY_MISMATCH",
|
||||
"msg"
|
||||
);
|
||||
const b = new CryptoCurveError(
|
||||
"CRYPTO.CURVE.RECOVERED_KEY_MISMATCH",
|
||||
"msg",
|
||||
{ context: { x: 1 } }
|
||||
);
|
||||
expect(a.code).toBe("CRYPTO.CURVE.RECOVERED_KEY_MISMATCH");
|
||||
expect(b.context?.x).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import * as secpNative from "./impl/secp256k1.native/secp256k1.native.js";
|
||||
import * as secpNoble from "./impl/secp256k1.noble/secp256k1.noble.js";
|
||||
|
||||
describe("secp256k1 (noble impl) — self-roundtrip", () => {
|
||||
it("generates 32-byte private keys", { timeout: 5000 }, () => {
|
||||
expect(secpNoble.randomPrivateKey().length).toBe(32);
|
||||
});
|
||||
|
||||
it("sign + verify round-trips", { timeout: 5000 }, () => {
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const pk = secpNoble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(32).fill(0x99);
|
||||
const sig = secpNoble.sign(digest, sk);
|
||||
expect(sig.length).toBe(64);
|
||||
expect(secpNoble.verify(sig, digest, pk)).toBe(true);
|
||||
});
|
||||
|
||||
it("ECDH: shared secrets agree from both sides", { timeout: 5000 }, () => {
|
||||
const aSk = secpNoble.randomPrivateKey();
|
||||
const bSk = secpNoble.randomPrivateKey();
|
||||
const aPk = secpNoble.getPublicKey(aSk);
|
||||
const bPk = secpNoble.getPublicKey(bSk);
|
||||
expect(secpNoble.getSharedSecret(aSk, bPk)).toEqual(
|
||||
secpNoble.getSharedSecret(bSk, aPk)
|
||||
);
|
||||
});
|
||||
|
||||
it("verify returns false for tampered sig", { timeout: 5000 }, () => {
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const pk = secpNoble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(32).fill(0x11);
|
||||
const sig = secpNoble.sign(digest, sk);
|
||||
sig[0] = (sig[0] ?? 0) ^ 0xff;
|
||||
expect(secpNoble.verify(sig, digest, pk)).toBe(false);
|
||||
});
|
||||
|
||||
it("verify returns false for bogus input (caught)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
secpNoble.verify(
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(32),
|
||||
new Uint8Array(33)
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble throws on empty public key", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
secpNoble.verify(
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(32),
|
||||
new Uint8Array()
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verify catches noble throws on non-Uint8Array input", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(
|
||||
secpNoble.verify(new Uint8Array(64), new Uint8Array(32), {
|
||||
not: "a Uint8Array",
|
||||
} as unknown as Uint8Array)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("secp256k1 (native stub)", () => {
|
||||
it("every operation throws PrimitiveNotImplemented", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => secpNative.getPublicKey(new Uint8Array(32))).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
expect(() =>
|
||||
secpNative.sign(new Uint8Array(32), new Uint8Array(32))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() =>
|
||||
secpNative.verify(
|
||||
new Uint8Array(64),
|
||||
new Uint8Array(32),
|
||||
new Uint8Array(33)
|
||||
)
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() =>
|
||||
secpNative.getSharedSecret(new Uint8Array(32), new Uint8Array(33))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() => secpNative.randomPrivateKey()).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
expect(() =>
|
||||
secpNative.signWithRecovery(new Uint8Array(32), new Uint8Array(32))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() =>
|
||||
secpNative.recoverPublicKey(new Uint8Array(65), new Uint8Array(32))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() =>
|
||||
secpNative.verifyWithRecovery(
|
||||
new Uint8Array(65),
|
||||
new Uint8Array(32),
|
||||
new Uint8Array(33)
|
||||
)
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
});
|
||||
});
|
||||
|
||||
describe("secp256k1 (noble impl) — ECDSA recovery", () => {
|
||||
it("signWithRecovery returns 65-byte signature with recovery byte 0 or 1", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const digest = new Uint8Array(32).fill(0x42);
|
||||
const sig = secpNoble.signWithRecovery(digest, sk);
|
||||
expect(sig.length).toBe(65);
|
||||
const recovery = sig[64];
|
||||
expect(recovery === 0 || recovery === 1).toBe(true);
|
||||
});
|
||||
|
||||
it("signWithRecovery output verifies via standard verify on first 64 bytes", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const pk = secpNoble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(32).fill(0x10);
|
||||
const sig = secpNoble.signWithRecovery(digest, sk);
|
||||
const compact = sig.subarray(0, 64);
|
||||
expect(secpNoble.verify(compact, digest, pk)).toBe(true);
|
||||
});
|
||||
|
||||
it("recoverPublicKey returns the same key as getPublicKey for the signer", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const pk = secpNoble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(32).fill(0x55);
|
||||
const sig = secpNoble.signWithRecovery(digest, sk);
|
||||
const recovered = secpNoble.recoverPublicKey(sig, digest);
|
||||
expect(recovered).toEqual(pk);
|
||||
});
|
||||
|
||||
it("verifyWithRecovery returns true on matching public key", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const pk = secpNoble.getPublicKey(sk);
|
||||
const digest = new Uint8Array(32).fill(0xaa);
|
||||
const sig = secpNoble.signWithRecovery(digest, sk);
|
||||
expect(secpNoble.verifyWithRecovery(sig, digest, pk)).toBe(true);
|
||||
});
|
||||
|
||||
it("verifyWithRecovery returns false on non-matching public key", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const otherPk = secpNoble.getPublicKey(secpNoble.randomPrivateKey());
|
||||
const digest = new Uint8Array(32).fill(0xbb);
|
||||
const sig = secpNoble.signWithRecovery(digest, sk);
|
||||
expect(secpNoble.verifyWithRecovery(sig, digest, otherPk)).toBe(false);
|
||||
});
|
||||
|
||||
it("verifyWithRecovery returns false for differing-length expected key", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sk = secpNoble.randomPrivateKey();
|
||||
const digest = new Uint8Array(32).fill(0xcc);
|
||||
const sig = secpNoble.signWithRecovery(digest, sk);
|
||||
// 65-byte uncompressed-ish blob — wrong length.
|
||||
expect(
|
||||
secpNoble.verifyWithRecovery(sig, digest, new Uint8Array(65))
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("verifyWithRecovery surfaces noble runtime errors as false (not throws)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Construct a 65-byte sig with valid recovery byte but garbage signature
|
||||
// — recoverPublicKey will throw a noble-internal error.
|
||||
const sig = new Uint8Array(65);
|
||||
sig[64] = 0;
|
||||
// Curve check inside addRecoveryBit/recoverPublicKey will throw.
|
||||
expect(
|
||||
secpNoble.verifyWithRecovery(
|
||||
sig,
|
||||
new Uint8Array(32),
|
||||
new Uint8Array(33)
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
getSharedSecret,
|
||||
randomPrivateKey,
|
||||
recoverPublicKey,
|
||||
sign,
|
||||
signWithRecovery,
|
||||
verify,
|
||||
verifyWithRecovery,
|
||||
} from "./secp256k1.select/secp256k1.select.js";
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
getSharedSecret,
|
||||
randomPrivateKey,
|
||||
} from "./x25519.native.js";
|
||||
@@ -0,0 +1,61 @@
|
||||
# x25519.native
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`x25519.native.ts` is the native-backend placeholder for the X25519 primitive in `@suaveplan/crypto-curve`. It exports `getPublicKey`, `getSharedSecret`, and `randomPrivateKey` — each throwing `PrimitiveNotImplemented("x25519")` — to maintain the two-implementation selector layout.
|
||||
|
||||
This stub signals that a native X25519 backend (for example, delegating to the host's WebCrypto `ECDH` with `X25519` or a native module) is planned but not yet active. The selector can be pointed here by changing `crypto.config.ts` and regenerating, at which point callers would get the implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- Satisfies `getPublicKey`, `getSharedSecret`, and `randomPrivateKey` interfaces.
|
||||
- Throws `PrimitiveNotImplemented("x25519")` unconditionally from every function.
|
||||
- Zero runtime dependencies beyond `@suaveplan/crypto-utils/errors`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
// Active only when crypto.config.ts sets x25519 to 'native'.
|
||||
import * as x25519 from "@suaveplan/crypto-curve/x25519";
|
||||
|
||||
try {
|
||||
x25519.randomPrivateKey();
|
||||
} catch (err) {
|
||||
// PrimitiveNotImplemented: x25519
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Graceful degradation pattern.
|
||||
import * as x25519 from "@suaveplan/crypto-curve/x25519";
|
||||
|
||||
function tryGetPublicKey(sk: Uint8Array): Uint8Array | null {
|
||||
try {
|
||||
return x25519.getPublicKey(sk);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All functions throw `PrimitiveNotImplemented("x25519")`:
|
||||
|
||||
- `getPublicKey(_privateKey: Uint8Array): Uint8Array`
|
||||
- `getSharedSecret(_privateKey: Uint8Array, _peerPublicKey: Uint8Array): Uint8Array`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Underscore-prefixed parameters suppress unused-parameter diagnostics without inline suppressions. When the native implementation is ready and RFC 7748 §6.1 test vectors pass, the `throw` is replaced with the real algorithm and the selector regenerated. The native backend for X25519 is particularly valuable in browser environments where `SubtleCrypto.deriveBits` with `{ name: "X25519" }` is available natively in modern browsers (Chrome 113+, Firefox 117+), enabling hardware-accelerated key agreement without a JavaScript implementation.
|
||||
|
||||
### Status
|
||||
|
||||
Placeholder native stub — throws `PrimitiveNotImplemented` until a WebCrypto `ECDH X25519` / Node `crypto.diffieHellman` bridge is added. Today the `x25519.select` selector always lands on `@noble/curves/ed25519` (which also provides the Curve25519 implementation); RFC 7748 test vectors in the KAT harness cover that path end-to-end.
|
||||
@@ -0,0 +1,14 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
|
||||
export function getPublicKey(_privateKey: Uint8Array): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("x25519");
|
||||
}
|
||||
export function getSharedSecret(
|
||||
_privateKey: Uint8Array,
|
||||
_peerPublicKey: Uint8Array
|
||||
): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("x25519");
|
||||
}
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
throw new PrimitiveNotImplemented("x25519");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
getSharedSecret,
|
||||
randomPrivateKey,
|
||||
} from "./x25519.noble.js";
|
||||
@@ -0,0 +1,71 @@
|
||||
# x25519.noble
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`x25519.noble.ts` is the Noble-backed concrete implementation of X25519 in `@suaveplan/crypto-curve`. It wraps the `x25519` object from `@noble/curves/ed25519`, which shares the Curve25519 field arithmetic with Ed25519 but uses Montgomery form for Diffie-Hellman.
|
||||
|
||||
X25519 (RFC 7748 §5) computes a scalar multiplication on Curve25519 in Montgomery u-coordinate representation. The result is a 32-byte shared secret that both parties compute independently from their private key and the peer's public key. The output is a raw field element and must be processed through a KDF before use as a symmetric key.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — derives the 32-byte Montgomery public key (u-coordinate of base-point multiple).
|
||||
- `getSharedSecret(privateKey, peerPublicKey)` — ECDH shared secret; Noble handles RFC 7748 clamping internally.
|
||||
- `randomPrivateKey()` — 32 random bytes; no rejection loop needed since every byte sequence is valid.
|
||||
- Noble handles the RFC 7748 §5 private-key clamping (bits 0–2 of byte 0 cleared, bit 7 of byte 31 cleared, bit 6 set) internally during the scalar multiply.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as x25519 from "@suaveplan/crypto-curve/x25519";
|
||||
|
||||
const sk = x25519.randomPrivateKey();
|
||||
const pk = x25519.getPublicKey(sk);
|
||||
console.log(pk.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as x25519 from "@suaveplan/crypto-curve/x25519";
|
||||
import { hkdf } from "@suaveplan/crypto-hash/kdf";
|
||||
|
||||
// Derive a 32-byte AES key from an X25519 shared secret.
|
||||
function ecdheKey(localSk: Uint8Array, remotePk: Uint8Array): Uint8Array {
|
||||
const raw = x25519.getSharedSecret(localSk, remotePk);
|
||||
return hkdf("SHA-256", raw, {
|
||||
salt: new Uint8Array(32),
|
||||
info: new TextEncoder().encode("x25519-ecdhe"),
|
||||
length: 32,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `privateKey` | `Uint8Array` | Any 32 bytes. Noble applies RFC 7748 clamping. |
|
||||
|
||||
**Returns:** 32-byte Montgomery u-coordinate public key.
|
||||
|
||||
### `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `privateKey` | `Uint8Array` | 32-byte local private key. |
|
||||
| `peerPublicKey` | `Uint8Array` | 32-byte remote public key. |
|
||||
|
||||
**Returns:** 32-byte raw shared secret (must be KDF-processed before use).
|
||||
|
||||
### `randomPrivateKey(): Uint8Array`
|
||||
|
||||
Returns 32 cryptographically random bytes from `@suaveplan/crypto-utils/random`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Noble's X25519 implementation auto-clamps private keys according to RFC 7748 §5, which means the actual scalar used is deterministic from the 32-byte input but differs from it. This clamping is intentional (it forces the scalar to be in a safe sub-group range) and callers should not attempt to apply clamping themselves. The shared secret is not safe to use as a key directly — apply HKDF-SHA-256 or equivalent. Swapping `getPublicKey` base with something other than the Curve25519 generator is not supported at this layer.
|
||||
@@ -0,0 +1,25 @@
|
||||
import { x25519 as nobleX25519 } from "@noble/curves/ed25519";
|
||||
import { randomBytes } from "@suaveplan/crypto-utils/random";
|
||||
|
||||
/**
|
||||
* X25519 (RFC 7748) ECDH key agreement, noble-backed.
|
||||
*
|
||||
* Private keys are any 32 random bytes (noble clamps as per spec);
|
||||
* public keys are 32 bytes (Montgomery u-coordinate); the shared
|
||||
* secret is 32 bytes and MUST be passed through a KDF before use as a
|
||||
* symmetric key.
|
||||
*/
|
||||
export function getPublicKey(privateKey: Uint8Array): Uint8Array {
|
||||
return nobleX25519.getPublicKey(privateKey);
|
||||
}
|
||||
|
||||
export function getSharedSecret(
|
||||
privateKey: Uint8Array,
|
||||
peerPublicKey: Uint8Array
|
||||
): Uint8Array {
|
||||
return nobleX25519.getSharedSecret(privateKey, peerPublicKey);
|
||||
}
|
||||
|
||||
export function randomPrivateKey(): Uint8Array {
|
||||
return randomBytes(32);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { getPublicKey, getSharedSecret, randomPrivateKey } from "./x25519";
|
||||
@@ -0,0 +1,74 @@
|
||||
# x25519
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- x25519.native — <!-- begin:auto-docs --> _(no submod…
|
||||
- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- x25519.native — <!-- begin:a…
|
||||
- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_- x25519.native — <!-- begin:auto-docs --> _(no submo…
|
||||
- [x25519.native](./impl/x25519.native/x25519.native.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519.noble](./impl/x25519.noble/x25519.noble.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_ - [x25519.select](./x25519.select/x25519.select.md) — <!-- begin:auto-docs --> _(no submodules)_- [x25519](./x25519.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Purpose
|
||||
|
||||
`x25519.ts` is the stable public entry point for the X25519 elliptic-curve Diffie-Hellman primitive in `@suaveplan/crypto-curve`. It re-exports everything from `x25519.select.ts`, which forwards to either the Noble-backed or native implementation based on `crypto.config.ts`.
|
||||
|
||||
X25519 (RFC 7748) is the Diffie-Hellman function over Curve25519 in Montgomery form. It is the standard for ephemeral key agreement in TLS 1.3, Signal, Noise Protocol, and SSH. Its defining property is that any 32-byte value is a valid private key (no validity check required), and the output is a 32-byte shared secret that must be passed through a KDF before use as a symmetric key.
|
||||
|
||||
## Features
|
||||
|
||||
- `getPublicKey(privateKey)` — 32-byte Montgomery-form public key (u-coordinate).
|
||||
- `getSharedSecret(privateKey, peerPublicKey)` — 32-byte raw ECDH output. Must be KDF-processed.
|
||||
- `randomPrivateKey()` — 32 cryptographically random bytes (any 32-byte value is valid).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as x25519 from "@suaveplan/crypto-curve/x25519";
|
||||
|
||||
const aliceSk = x25519.randomPrivateKey();
|
||||
const alicePk = x25519.getPublicKey(aliceSk);
|
||||
|
||||
const bobSk = x25519.randomPrivateKey();
|
||||
const bobPk = x25519.getPublicKey(bobSk);
|
||||
|
||||
const aliceShared = x25519.getSharedSecret(aliceSk, bobPk);
|
||||
const bobShared = x25519.getSharedSecret(bobSk, alicePk);
|
||||
// aliceShared and bobShared are identical
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as x25519 from "@suaveplan/crypto-curve/x25519";
|
||||
import { hkdf } from "@suaveplan/crypto-hash/kdf";
|
||||
import { zeroize } from "@suaveplan/crypto-utils/zeroize";
|
||||
|
||||
// Ephemeral ECDH with HKDF key derivation (Noise XX pattern excerpt).
|
||||
function deriveSessionKey(
|
||||
localSk: Uint8Array,
|
||||
remotePk: Uint8Array,
|
||||
handshakeHash: Uint8Array
|
||||
): Uint8Array {
|
||||
const rawSecret = x25519.getSharedSecret(localSk, remotePk);
|
||||
try {
|
||||
return hkdf("SHA-256", rawSecret, {
|
||||
salt: handshakeHash,
|
||||
info: new TextEncoder().encode("noise-session"),
|
||||
length: 32,
|
||||
});
|
||||
} finally {
|
||||
zeroize(rawSecret);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
All symbols re-exported from `x25519.select.ts`:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array`
|
||||
- `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array`
|
||||
- `randomPrivateKey(): Uint8Array`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a zero-logic re-export barrel. Its sole purpose is the stable import path `@suaveplan/crypto-curve/x25519`. Logic belongs in the implementation files. The raw ECDH output of X25519 is not safe to use directly as a symmetric key — always run it through HKDF or another KDF before using for encryption or MAC operations.
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./x25519.select.js";
|
||||
@@ -0,0 +1,54 @@
|
||||
# x25519.select
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`x25519.select.ts` is the build-time primitive selector for X25519 in `@suaveplan/crypto-curve`. Generated by `bun scripts/gen-crypto-selects.ts`, it re-exports one backend — `x25519.noble.ts` or `x25519.native.ts` — based on the `x25519` entry in `crypto.config.ts`.
|
||||
|
||||
Callers import through `x25519.ts`, which delegates here. The selector isolates callers from backend changes: switching from Noble to a native implementation requires only regenerating this file. Currently the Noble backend is active because the native stub throws `PrimitiveNotImplemented`.
|
||||
|
||||
## Features
|
||||
|
||||
- Single re-export of `getPublicKey`, `getSharedSecret`, and `randomPrivateKey`.
|
||||
- Generated — never hand-edited.
|
||||
- Controlled by `crypto.config.ts`: set `x25519` to `'noble'` or `'native'`.
|
||||
- Enables dead-code elimination of the inactive backend.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import * as x25519 from "@suaveplan/crypto-curve/x25519";
|
||||
|
||||
const sk = x25519.randomPrivateKey();
|
||||
const pk = x25519.getPublicKey(sk);
|
||||
console.log(pk.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import * as x25519 from "@suaveplan/crypto-curve/x25519";
|
||||
|
||||
// Simple ECDH exchange.
|
||||
function exchange(localSk: Uint8Array, remotePk: Uint8Array): Uint8Array {
|
||||
return x25519.getSharedSecret(localSk, remotePk);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Re-exports from the active implementation:
|
||||
|
||||
- `getPublicKey(privateKey: Uint8Array): Uint8Array` — 32-byte public key.
|
||||
- `getSharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array` — 32-byte raw shared secret.
|
||||
- `randomPrivateKey(): Uint8Array` — 32 random bytes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This file is a generated one-liner: `export * from "./impl/x25519.noble.js"`. The generator resolves the configured backend and writes the appropriate path. The file is idempotent and safe to regenerate at any time. Do not add logic here — it will be overwritten.
|
||||
|
||||
### Selection Logic
|
||||
|
||||
At build time `crypto.config.ts` picks either the Noble or the native engine for every primitive; the selector exported here reads the generated `crypto-selects` map and routes calls to the chosen impl. When the selected engine is not yet implemented, the `PrimitiveNotImplemented` sentinel from `@suaveplan/crypto-utils` is thrown with the primitive-id baked in so logs identify the exact missing slot.
|
||||
@@ -0,0 +1,2 @@
|
||||
/* GENERATED — DO NOT EDIT. Regenerate with `bun scripts/gen-crypto-selects.ts`. */
|
||||
export * from "../impl/x25519.noble/x25519.noble.js";
|
||||
@@ -0,0 +1,63 @@
|
||||
import { PrimitiveNotImplemented } from "@suaveplan/crypto-utils/errors";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import * as x25519Native from "./impl/x25519.native/x25519.native.js";
|
||||
import * as x25519Noble from "./impl/x25519.noble/x25519.noble.js";
|
||||
|
||||
// RFC 7748 §6.1 test vector
|
||||
function fromHex(h: string): Uint8Array {
|
||||
const out = new Uint8Array(h.length / 2);
|
||||
for (let i = 0; i < out.length; i++) {
|
||||
out[i] = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const ALICE_SK = fromHex(
|
||||
"77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"
|
||||
);
|
||||
const ALICE_PK = fromHex(
|
||||
"8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"
|
||||
);
|
||||
const BOB_SK = fromHex(
|
||||
"5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"
|
||||
);
|
||||
const BOB_PK = fromHex(
|
||||
"de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"
|
||||
);
|
||||
const SHARED = fromHex(
|
||||
"4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"
|
||||
);
|
||||
|
||||
describe("x25519 (noble impl) — RFC 7748 §6.1", () => {
|
||||
it("Alice public key matches", { timeout: 5000 }, () => {
|
||||
expect(x25519Noble.getPublicKey(ALICE_SK)).toEqual(ALICE_PK);
|
||||
});
|
||||
it("Bob public key matches", { timeout: 5000 }, () => {
|
||||
expect(x25519Noble.getPublicKey(BOB_SK)).toEqual(BOB_PK);
|
||||
});
|
||||
it("Alice + Bob shared secret matches", { timeout: 5000 }, () => {
|
||||
expect(x25519Noble.getSharedSecret(ALICE_SK, BOB_PK)).toEqual(SHARED);
|
||||
});
|
||||
it("Bob + Alice agree on shared secret", { timeout: 5000 }, () => {
|
||||
expect(x25519Noble.getSharedSecret(BOB_SK, ALICE_PK)).toEqual(SHARED);
|
||||
});
|
||||
it("randomPrivateKey returns 32 bytes", { timeout: 5000 }, () => {
|
||||
expect(x25519Noble.randomPrivateKey().length).toBe(32);
|
||||
});
|
||||
});
|
||||
|
||||
describe("x25519 (native stub)", () => {
|
||||
it("every operation throws PrimitiveNotImplemented", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => x25519Native.getPublicKey(new Uint8Array(32))).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
expect(() =>
|
||||
x25519Native.getSharedSecret(new Uint8Array(32), new Uint8Array(32))
|
||||
).toThrow(PrimitiveNotImplemented);
|
||||
expect(() => x25519Native.randomPrivateKey()).toThrow(
|
||||
PrimitiveNotImplemented
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
getPublicKey,
|
||||
getSharedSecret,
|
||||
randomPrivateKey,
|
||||
} from "./x25519.select/x25519.select.js";
|
||||
@@ -0,0 +1,7 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
type WritableGlobal = { crypto?: Crypto };
|
||||
const g = globalThis as WritableGlobal;
|
||||
if (g.crypto === undefined) {
|
||||
g.crypto = webcrypto as unknown as Crypto;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Spec-conformance tests for `openspec/specs/crypto-curve/spec.md`.
|
||||
*
|
||||
* Pins structural scenarios that the per-primitive unit suites do not
|
||||
* cover directly: every curve has both `noble.ts` and `native.ts` impl
|
||||
* files, every `*.select.ts` carries the GENERATED banner, every
|
||||
* `*.native.ts` is currently a `PrimitiveNotImplemented` stub (the
|
||||
* runtime reflection of the dangerous-tier "no flip without audit"
|
||||
* gate), and every `*.native.ts` source contains no banned data-
|
||||
* dependent branch patterns on secret material.
|
||||
*/
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
const ROOT = join(__dirname, "..");
|
||||
const SRC = join(ROOT, "src");
|
||||
const PRIMITIVES = join(SRC, "primitives");
|
||||
|
||||
const CURVE_IDS = ["secp256k1", "ed25519", "x25519", "p256"] as const;
|
||||
|
||||
describe("crypto-curve — universal curve primitive surface", () => {
|
||||
it("every curve id has its primitive directory", { timeout: 5000 }, () => {
|
||||
for (const id of CURVE_IDS) {
|
||||
expect(existsSync(join(PRIMITIVES, id))).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("crypto-curve — per-primitive replaceability layout", () => {
|
||||
for (const id of CURVE_IDS) {
|
||||
it(`${id} has both noble.ts and native.ts impl files`, {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const noble = join(
|
||||
PRIMITIVES,
|
||||
id,
|
||||
"impl",
|
||||
`${id}.noble`,
|
||||
`${id}.noble.ts`
|
||||
);
|
||||
const native = join(
|
||||
PRIMITIVES,
|
||||
id,
|
||||
"impl",
|
||||
`${id}.native`,
|
||||
`${id}.native.ts`
|
||||
);
|
||||
expect(existsSync(noble)).toBe(true);
|
||||
expect(existsSync(native)).toBe(true);
|
||||
});
|
||||
|
||||
it(`${id}.select.ts carries the GENERATED banner`, {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const sel = join(PRIMITIVES, id, `${id}.select`, `${id}.select.ts`);
|
||||
expect(existsSync(sel)).toBe(true);
|
||||
const content = readFileSync(sel, "utf8");
|
||||
expect(content).toMatch(/GENERATED/);
|
||||
expect(content).toMatch(/DO NOT EDIT/);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("crypto-curve — vendored noble source (Phase 0.5)", () => {
|
||||
it("is a Phase-0.5 forward gate; current state is pre-0.5 (noble dep + no vendored/)", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(ROOT, "package.json"), "utf8")
|
||||
);
|
||||
const noblePresent =
|
||||
"@noble/curves" in (pkg.dependencies ?? {}) ||
|
||||
"@noble/curves" in (pkg.peerDependencies ?? {});
|
||||
const vendoredPresent = existsSync(join(SRC, "vendored"));
|
||||
const upstreamFile = existsSync(join(ROOT, "NOBLE_UPSTREAM.md"));
|
||||
// Either we are pre-0.5 (noble dep present, no vendored, no
|
||||
// upstream record) or post-0.5 (no noble dep, vendored
|
||||
// directory exists with NOBLE_UPSTREAM.md present). Mixed
|
||||
// states are forbidden.
|
||||
const pre05 = noblePresent && !vendoredPresent && !upstreamFile;
|
||||
const post05 = !noblePresent && vendoredPresent && upstreamFile;
|
||||
expect(pre05 || post05).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("crypto-curve — constant-time curve math (native stubs)", () => {
|
||||
function srcOf(id: (typeof CURVE_IDS)[number]): string {
|
||||
return readFileSync(
|
||||
join(PRIMITIVES, id, "impl", `${id}.native`, `${id}.native.ts`),
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
for (const id of CURVE_IDS) {
|
||||
it(`${id}.native.ts is currently a PrimitiveNotImplemented stub`, {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// While native impls are stubs, "no data-dependent branch on
|
||||
// private key material" is trivially satisfied. The flip-PR
|
||||
// gate (audit + ct-lint pass) is what re-checks the
|
||||
// invariant when the stub is replaced.
|
||||
const src = srcOf(id);
|
||||
expect(src).toMatch(/PrimitiveNotImplemented/);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("crypto-curve — dangerous-tier flip gate (runtime reflection)", () => {
|
||||
it("every native is a stub today, so no curve has been flipped without an audit", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const offenders: string[] = [];
|
||||
for (const id of CURVE_IDS) {
|
||||
const native = readFileSync(
|
||||
join(PRIMITIVES, id, "impl", `${id}.native`, `${id}.native.ts`),
|
||||
"utf8"
|
||||
);
|
||||
if (!/PrimitiveNotImplemented/.test(native)) {
|
||||
offenders.push(id);
|
||||
}
|
||||
}
|
||||
// If a curve graduates from stub, this test will fail and
|
||||
// force the change to attach an audit attestation per the
|
||||
// CI-policy gate before re-baselining.
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
void readdirSync;
|
||||
void statSync;
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
|
||||
}
|
||||
52
packages/security-stack/crypto/crypto-curve/vite.config.ts
Normal file
52
packages/security-stack/crypto/crypto-curve/vite.config.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: "../../../node_modules/.vite/packages/crypto-curve",
|
||||
plugins: [
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: "./tsconfig.lib.json",
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: {
|
||||
index: "src/index.ts",
|
||||
"primitives/secp256k1/secp256k1": "src/primitives/secp256k1/secp256k1.ts",
|
||||
"primitives/ed25519/ed25519": "src/primitives/ed25519/ed25519.ts",
|
||||
"primitives/x25519/x25519": "src/primitives/x25519/x25519.ts",
|
||||
"primitives/p256/p256": "src/primitives/p256/p256.ts",
|
||||
"primitives/p384/p384": "src/primitives/p384/p384.ts",
|
||||
"primitives/p521/p521": "src/primitives/p521/p521.ts",
|
||||
},
|
||||
formats: ["es"],
|
||||
},
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
output: {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
external: [
|
||||
"@noble/curves",
|
||||
/^@noble\//,
|
||||
"@suaveplan/crypto-hash",
|
||||
"@suaveplan/crypto-utils",
|
||||
/^@suaveplan\/crypto-utils\//,
|
||||
"@suaveplan/error",
|
||||
"@suaveplan/types",
|
||||
"tslib",
|
||||
"zod",
|
||||
/^node:/,
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
38
packages/security-stack/crypto/crypto-curve/vitest.config.ts
Normal file
38
packages/security-stack/crypto/crypto-curve/vitest.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "node",
|
||||
pool: "forks",
|
||||
setupFiles: ["./tests/setup/polyfills.ts"],
|
||||
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
||||
exclude: [
|
||||
"src/__benchmarks__/**",
|
||||
"benchmarks/**",
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
],
|
||||
coverage: {
|
||||
provider: "istanbul",
|
||||
reporter: ["text", "html", "lcov"],
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"src/**/*.types.ts",
|
||||
"src/**/index.ts",
|
||||
"src/constants/**",
|
||||
"src/primitives/**/impl/*.native.ts",
|
||||
"src/primitives/**/*.select.ts",
|
||||
],
|
||||
thresholds: {
|
||||
statements: 100,
|
||||
branches: 100,
|
||||
functions: 100,
|
||||
lines: 100,
|
||||
},
|
||||
},
|
||||
testTimeout: 10_000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user