first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# `alg-map`
## Purpose
Bridge between IANA COSE Algorithm identifiers (`CoseAlg` literal strings or
the equivalent numeric labels from RFC 9053 §2) and the suaveplan-internal
algorithm identifiers consumed by `@suaveplan/crypto-curve`,
`@suaveplan/crypto-cipher`, and `@suaveplan/crypto-hash`. The bridge is the
single point where COSE / CWT / WebAuthn flows decide *which* suaveplan
primitive will run for a given algorithm label.
## Features
- Full IANA registry coverage for the algorithms the crypto packages
ship: ES256, ES384, ES512, EdDSA (Ed25519), RS256, RS384, RS512, HS256,
HS384, HS512, A128GCM, A192GCM, A256GCM, ChaCha20Poly1305.
- Accepts both the typed `CoseAlg` string and the raw IANA numeric label.
- Reverse mapping (`cryptoAlgToCoseAlg`) for outbound encoding.
- Throws `CborError(UNEXPECTED_TYPE)` for any algorithm the bridge does
not implement — this is the intentional point of failure when a caller
tries to use a primitive suaveplan does not (yet) ship.
## Usage: Basic Example
```ts
import { coseAlgToCryptoAlg } from "@suaveplan/cbor/cose";
const alg = coseAlgToCryptoAlg("ES256"); // "ecdsa-p256-sha256"
```
## Usage: Advanced Example
```ts
import { coseAlgToCryptoAlg, decodeCoseKey } from "@suaveplan/cbor/cose";
// CWT verification dispatch
const key = decodeCoseKey(keyBytes);
const cryptoAlg = coseAlgToCryptoAlg(key.alg ?? "ES256");
switch (cryptoAlg) {
case "ecdsa-p256-sha256":
// hand off to @suaveplan/crypto-curve.p256.verify
break;
case "ed25519":
// hand off to @suaveplan/crypto-curve.ed25519.verify
break;
// ...
}
```
## API Reference
- `coseAlgToCryptoAlg(alg: CoseAlg | number): suaveplanCryptoAlg`
- `cryptoAlgToCoseAlg(alg: suaveplanCryptoAlg): CoseAlg`
- `type suaveplanCryptoAlg` — literal-typed union of the suaveplan-internal
algorithm IDs the bridge resolves to.
Throws `CborError(UNEXPECTED_TYPE)` for unknown numeric labels and for
algorithm strings outside the supported set.
## Implementation Notes
The bridge is a static lookup table — adding a new algorithm requires a
single new entry in both the `CoseAlg` literal type and the `ALG_BRIDGE`
record, plus a corresponding entry in the IANA-driven `COSE_ALG_LABELS`.
Keeping the conversion in a separate module (rather than threading raw
strings through the codec) ensures every inbound algorithm label crosses
exactly one validation boundary before reaching the cryptographic core.

View File

@@ -0,0 +1,71 @@
# `cose-key`
## Purpose
Decodes and encodes RFC 9052 §7 `COSE_Key` structures. The decoder maps an
RFC-encoded CBOR map onto a typed discriminated union (`Ec2Key | OkpKey |
RsaKey | SymmetricKey`); the encoder reverses the mapping with deterministic
encoding so two keys with identical fields always produce byte-identical
output. WebAuthn attestation public keys, COSE_Sign1 signing keys, and CWT
verification keys all flow through this module.
## Features
- Discriminated union by `kty` — TypeScript narrows on `key.kty === "EC2"`
etc. without explicit casts.
- Per-key-type field validation: EC2 (`crv` + `x` + `y` + optional `d`),
OKP (`crv` + `x` + optional `d`), RSA (`n` + `e` + optional `d`/`p`/`q`/
`dP`/`dQ`/`qInv`), Symmetric (`k`).
- Common fields (`alg`, `kid`, `key_ops`, `baseIV`) parsed once and applied
uniformly across all key types.
- Throws `CborError(UNEXPECTED_TYPE)` for malformed maps, unknown labels,
wrong inner types, and missing required fields.
- Encoder uses RFC 8949 §4.2 deterministic encoding so the byte output is
stable across runs — required for COSE structures that include the key
bytes in signed/verified payloads.
## Usage: Basic Example
```ts
import { decodeCoseKey, encodeCoseKey } from "@suaveplan/cbor/cose";
const key = decodeCoseKey(bytes);
if (key.kty === "EC2") {
console.log("ES256 public key", key.x, key.y);
}
const wire = encodeCoseKey(key);
```
## Usage: Advanced Example
```ts
import { decodeCoseKey } from "@suaveplan/cbor/cose";
import { coseAlgToCryptoAlg } from "@suaveplan/cbor/cose";
// WebAuthn attestation flow
const attestationKey = decodeCoseKey(authData.credentialPublicKey);
if (attestationKey.kty !== "EC2") {
throw new Error("expected EC2 attestation key");
}
const cryptoAlg = coseAlgToCryptoAlg(attestationKey.alg ?? "ES256");
// Hand off to @suaveplan/crypto-curve to verify the signature.
```
## API Reference
- `decodeCoseKey(bytes: Uint8Array): CoseKey` — decode a CBOR-encoded
`COSE_Key` byte string into the typed union. Throws `CborError` for any
structural defect.
- `encodeCoseKey(key: CoseKey): Uint8Array` — deterministic-encode the key.
- See `cose.types.ts` for `CoseKey`, `Ec2Key`, `OkpKey`, `RsaKey`,
`SymmetricKey`, `CoseAlg`, `CoseCurve`, `CoseKeyOp`.
## Implementation Notes
The decoder tolerates extra map entries — RFC 9052 §7.1 explicitly permits
forward-compatible additions, and rejecting unknown labels would break
forward compatibility. The encoder, by contrast, emits only the fields
defined in the typed shape so it cannot accidentally leak extension data
that was never modelled. Common fields are read once into a small `CommonOut`
record and spread into every key shape — this avoids drift between the four
per-kty readers when a new common field is added in the future.

View File

@@ -0,0 +1,55 @@
# `encrypt`
## Purpose
`COSE_Encrypt` (RFC 9052 §5.1) — multi-recipient AEAD-encrypted COSE
message in CBOR tag 96. Each recipient holds its own wrapped copy of the
content-encryption key (CEK); the body carries one ciphertext encrypted
under the CEK. This module ships encode/decode of the wire shape plus a
kid-based recipient lookup.
## Features
- `encodeCoseEncrypt` builds the 4-tuple `[bodyProtected, bodyUnprotected,
ciphertext, recipients]`. Per-recipient triples
`[recipientProtected, recipientUnprotected, encryptedKey]` carry the
wrapped-CEK bytes verbatim — KEK wrap is delegated to whatever scheme
the recipient declares.
- `decodeCoseEncrypt` recovers the typed view including each recipient's
`protectedRaw` for any per-recipient verification flow.
- `findEncryptRecipientByKid` does a `kid` byte compare across all
recipients (looking in protected then unprotected headers).
- Empty recipient arrays are rejected per §5.1.
## Usage
```ts
import { encodeCoseEncrypt, decodeCoseEncrypt, findEncryptRecipientByKid } from "@suaveplan/cbor/cose";
const wire = encodeCoseEncrypt({
protectedHeader: { alg: "A256GCM" },
unprotectedHeader: { iv },
ciphertext: aeadOutput,
recipients: [
{ unprotectedHeader: { kid: kidA }, encryptedKey: wrappedCekA },
{ unprotectedHeader: { kid: kidB }, encryptedKey: wrappedCekB },
],
});
const msg = decodeCoseEncrypt(wire);
const mine = findEncryptRecipientByKid(msg, kidA);
```
## API
- `encodeCoseEncrypt(input)` / `decodeCoseEncrypt(bytes)`.
- `findEncryptRecipientByKid(msg, kid): CoseRecipient | undefined`.
- `COSE_ENCRYPT_TAG = 96`.
## Notes
The CEK wrapping is handled by the caller — this module is the wire-shape
codec. Common KEK schemes (`A128KW`, `A256KW`, `direct`, `ECDH-ES`,
`ECDH-ES+A256KW`) all serialise into the recipient bucket the same way:
the wrapped key bytes go in slot 2, the algorithm declaration goes in the
recipient's protected header.

View File

@@ -0,0 +1,54 @@
# `encrypt0`
## Purpose
`COSE_Encrypt0` (RFC 9052 §5.2) — single-recipient AEAD-encrypted COSE
message in CBOR tag 16. Wraps AES-256-GCM and ChaCha20-Poly1305 from
`@suaveplan/crypto-cipher` and binds the ciphertext to the canonical
`Enc_structure` (§5.3) so external AAD propagates through the AEAD
authenticator.
## Features
- `encryptCoseEncrypt0` performs the AEAD computation and returns a fully-
formed COSE message; the IV automatically lands in the unprotected header
unless the caller already placed it there.
- `decryptCoseEncrypt0` resolves the IV from either the unprotected or
protected header, recomputes `Enc_structure`, and authenticates plus
decrypts in one call. Tampered ciphertext / wrong key / wrong AAD all
raise `CborError(COSE_AEAD_FAILED)`.
- Tagged + tagless wire forms supported.
- AES-256-GCM and ChaCha20-Poly1305 covered; ES384/ES512/AES-128-GCM/etc.
are explicit "not supported" errors so the gap is visible.
## Usage
```ts
import { encryptCoseEncrypt0, decryptCoseEncrypt0, decodeCoseEncrypt0 } from "@suaveplan/cbor/cose";
const wire = encryptCoseEncrypt0({
protectedHeader: { alg: "A256GCM" },
plaintext,
key,
iv,
});
const msg = decodeCoseEncrypt0(wire);
const back = decryptCoseEncrypt0(msg, key);
```
## API
- `encryptCoseEncrypt0(input)` / `encodeCoseEncrypt0(input)`.
- `decodeCoseEncrypt0(bytes): CoseEncrypt0`.
- `decryptCoseEncrypt0(msg, key, options?): Uint8Array` — throws
`CborError(COSE_AEAD_FAILED)` on auth failure.
- `COSE_ENCRYPT0_TAG = 16`.
## Notes
The AEAD primitive's AAD argument receives the canonical `Enc_structure`
bytes — RFC 9052 §5.3 dictates this binding so an attacker cannot replay
ciphertext under a different protected header. External AAD adds a second
layer of context (e.g. transaction-id or session-id) and is mixed in as the
third element of the structure before encoding.

View File

@@ -0,0 +1,43 @@
# `headers`
## Purpose
Typed `CoseHeader` map shape covering RFC 9052 §3.1 + IANA COSE Header
Parameters (alg, crit, content type, kid, IV, Partial IV, counter signature).
Two helpers — `mapToHeader` and `headerToMap` — round-trip between the typed
shape and the plain CBOR map representation that lands on the wire.
## Features
- `alg` is typed as `CoseAlg` so the algorithm union narrows downstream.
- `crit` rejects empty arrays and non-numeric labels per §3.1.
- `contentType` accepts both numeric (IANA CoAP Content-Format) and string
(RFC 6838) forms.
- All byte-string fields validated at the boundary; non-bytes throws
`COSE_HEADER_INVALID`.
- Round-trip is total: every field in the typed shape lands on the wire and
vice versa, with no implicit drops.
## Usage
```ts
import { headerToMap, mapToHeader } from "@suaveplan/cbor/cose";
import { encode, decode } from "@suaveplan/cbor";
const m = headerToMap({ alg: "ES256", kid: new Uint8Array([0xab]) });
const wire = encode(m, { deterministic: true });
const back = mapToHeader(decode(wire) as Map<unknown, unknown>);
```
## API
- `interface CoseHeader` — typed shape.
- `mapToHeader(m: Map<unknown, unknown>): CoseHeader`.
- `headerToMap(h: CoseHeader): Map<number, unknown>`.
## Notes
`crit` must be non-empty per §3.1: an empty list is malformed because it
implies "every header is unsupported", which contradicts the receiver's
ability to verify the message. The guard catches encoder bugs that would
otherwise emit `[]` for a header set with no critical entries.

View File

@@ -0,0 +1,70 @@
# `jwk`
## Purpose
Round-trip converter between RFC 9052 `COSE_Key` instances and RFC 7517 JWK
shapes. WebAuthn attestation flows often need to hand a public key to a
WebCrypto-style verifier that consumes JWK; conversely, server-issued
JWK-formatted keys may need to land in a CBOR-encoded COSE structure. The
two conversions preserve every field that has a defined cross-format
mapping.
## Features
- `coseKeyToJwk(key)` — accepts any `CoseKey` discriminant (EC2, OKP, RSA,
Symmetric) and emits the matching JWK (`EC`, `OKP`, `RSA`, `oct`).
- `jwkToCoseKey(jwk)` — reverse mapping with full byte-string round-trip.
- Byte-string fields use RFC 4648 §5 base64url encoding without padding.
- `kid` is treated as UTF-8 bytes on the COSE side and a string on the JWK
side; conversion is lossy only if a non-UTF-8 kid is in play (rare, and
RFC 9052 §3.1 documents the convention).
- `alg` round-trips through a fixed COSE↔JWK label table so the typed
algorithm lands consistently on either side.
## Usage: Basic Example
```ts
import { coseKeyToJwk, jwkToCoseKey } from "@suaveplan/cbor/cose";
const jwk = coseKeyToJwk(coseKey);
const back = jwkToCoseKey(jwk);
```
## Usage: Advanced Example
```ts
import { decodeCoseKey, coseKeyToJwk } from "@suaveplan/cbor/cose";
// Attestation key from CTAP2
const cose = decodeCoseKey(attestationKeyBytes);
const jwk = coseKeyToJwk(cose);
// Hand off to WebCrypto
const cryptoKey = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "ECDSA", namedCurve: "P-256" },
false,
["verify"],
);
```
## API Reference
- `coseKeyToJwk(key: CoseKey): Jwk` — discriminator-aware conversion to JWK.
- `jwkToCoseKey(jwk: Jwk): CoseKey` — reverse conversion.
- `Jwk = Ec2Jwk | OkpJwk | RsaJwk | SymmetricJwk` (typed union).
Throws `CborError(UNEXPECTED_TYPE)` for incompatible curves (EC2 with a
non-P curve, OKP with a non-Ed/X curve), unknown JWK `alg` strings, and
malformed base64url input via the underlying `atob`.
## Implementation Notes
base64url encoding is implemented inline with `btoa`/`atob` rather than
pulling in `@suaveplan/crypto-utils` so this module stays free of crypto
deps. Standard base64 padding (`=`) is stripped on output and re-padded on
input — the RFC 4648 §5 spec allows omission of padding and major JWK
producers do strip it. RSA JWKs use lowercase param names (`dp`, `dq`, `qi`)
per RFC 7518 §6.3.2; COSE_Key uses the labels from RFC 8230 §4 (`-6`, `-7`,
`-8`). The bridge preserves the field-by-field correspondence.

View File

@@ -0,0 +1,55 @@
# `mac`
## Purpose
`COSE_Mac` (RFC 9052 §6.1) — multi-recipient MAC message in CBOR tag 97.
Each recipient gets a wrapped copy of the content-MAC key (CMK); the body
itself carries one MAC computed over the payload using the CMK. This module
ships encode/decode of the wire shape plus a kid-based recipient lookup.
## Features
- `encodeCoseMac` builds the 5-tuple `[bodyProtected, bodyUnprotected,
payload, tag, recipients]`. Per-recipient triples
`[recipientProtected, recipientUnprotected, encryptedKey]` carry the
wrapped-key bytes verbatim — wrap/unwrap is delegated to whatever scheme
the recipient header declares (direct, KEK, ECDH-ES, etc.).
- `decodeCoseMac` recovers the typed `CoseMac` view. Each recipient lands
in `msg.recipients[]` with its own `protectedRaw` byte string ready for
any per-recipient verification flow that needs it.
- `findRecipientByKid` does a constant-time-ish byte compare over the
recipient's `kid` (consulted in protected then unprotected headers).
- Empty recipient arrays are rejected — RFC 9052 §6.1 requires at least one.
## Usage
```ts
import { encodeCoseMac, decodeCoseMac, findRecipientByKid } from "@suaveplan/cbor/cose";
const wire = encodeCoseMac({
protectedHeader: { alg: "HS256" },
payload,
tag: macTag,
recipients: [
{ unprotectedHeader: { kid: kidA }, encryptedKey: wrapped },
{ unprotectedHeader: { kid: kidB }, encryptedKey: wrappedB },
],
});
const msg = decodeCoseMac(wire);
const mine = findRecipientByKid(msg, kidA);
```
## API
- `encodeCoseMac(input)` / `decodeCoseMac(bytes)`.
- `findRecipientByKid(msg, kid): CoseRecipient | undefined`.
- `COSE_MAC_TAG = 97`.
## Notes
The body's `tag` is computed over the canonical `MAC_structure` (RFC 9052
§6.3) using the CMK. This module does not implement the wrapping/unwrapping
of the CMK to each recipient — that lives one layer up, where the recipient-
specific algorithm (`A128KW`, `direct`, `ECDH-ES + A256KW`, …) is dispatched.
The helper here gives a typed shape for callers to plug those wraps into.

View File

@@ -0,0 +1,50 @@
# `mac0`
## Purpose
`COSE_Mac0` (RFC 9052 §6.2) — single-recipient MAC message in CBOR tag 17.
Used for symmetric-key authentication of CWT claim sets, vault attachment
manifests, and any payload where the producer and verifier share a key.
## Features
- `createCoseMac0` computes the HMAC tag in one call (HMAC-SHA-{256,384,512}
via `@suaveplan/crypto-hash`) and returns a fully-formed `COSE_Mac0` byte
sequence.
- `encodeCoseMac0` emits a pre-computed tag (useful when the MAC is generated
outside the call site, e.g. by an HSM).
- `decodeCoseMac0` parses both tagged and tagless wire forms.
- `verifyCoseMac0` re-computes the tag in constant time and rejects on any
mismatch.
- External AAD propagated through the MAC computation — required for any
CWT claim set carrying authenticated context.
## Usage
```ts
import { createCoseMac0, decodeCoseMac0, verifyCoseMac0 } from "@suaveplan/cbor/cose";
const wire = createCoseMac0({
protectedHeader: { alg: "HS256" },
payload: new TextEncoder().encode("hello"),
key,
});
const msg = decodeCoseMac0(wire);
const ok = verifyCoseMac0(msg, key);
```
## API
- `createCoseMac0(input): Uint8Array` — sign-and-encode in one call.
- `encodeCoseMac0(input): Uint8Array` — wire encode for a pre-computed tag.
- `decodeCoseMac0(bytes): CoseMac0` — parse tagged or tagless input.
- `verifyCoseMac0(msg, key, options?): boolean` — constant-time tag compare.
- `COSE_MAC0_TAG = 17`
## Notes
Constant-time comparison is implemented inline rather than reaching for
`@suaveplan/crypto-utils` to keep the dep graph minimal. The XOR-accumulator
returns `false` for any byte difference and for length mismatches — the
shape every Web-Crypto-style timing-safe equality function uses.

View File

@@ -0,0 +1,48 @@
# `sig-structure`
## Purpose
Builders for the canonical input to COSE signature, MAC, and AEAD
computations per RFC 9052 §4.4 / §6.3 / §5.3. Every signed/verified COSE
message commits to a deterministically-encoded array whose first element is
a context discriminator — `Signature1`, `Signature`, `CounterSignature`,
`MAC`, `MAC0`, `Encrypt`, `Encrypt0`, `Enc_Recipient`. The discriminator
prevents confusion attacks where a signature crafted in one context could
be replayed in another.
## Features
- `buildSign1Structure(protectedRaw, externalAad, payload)` — RFC 9052 §4.4
Sig_structure1.
- `buildSignStructure(context, bodyProtected, signProtected, externalAad, payload)`
— RFC 9052 §4.4 Sig_structure (Signature + CounterSignature).
- `buildMacStructure(context, protectedRaw, externalAad, payload)` —
RFC 9052 §6.3 MAC_structure (MAC + MAC0).
- `buildEncStructure(context, protectedRaw, externalAad)` — RFC 9052 §5.3
Enc_structure (Encrypt + Encrypt0 + Enc_Recipient).
- All output is deterministically CBOR-encoded so two callers with the same
inputs produce byte-identical bytes.
## Usage
```ts
import { buildSign1Structure } from "@suaveplan/cbor/cose";
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
const tbs = buildSign1Structure(protectedRaw, new Uint8Array(0), payload);
const sig = ed25519.sign(tbs, privateKey);
```
## API
- `buildSign1Structure(protectedRaw: Uint8Array, externalAad: Uint8Array, payload: Uint8Array): Uint8Array`
- `buildSignStructure(context: "Signature" | "CounterSignature", bodyProtected, signProtected, externalAad, payload): Uint8Array`
- `buildMacStructure(context: "MAC" | "MAC0", protectedRaw, externalAad, payload): Uint8Array`
- `buildEncStructure(context: "Encrypt" | "Encrypt0" | "Enc_Recipient", protectedRaw, externalAad): Uint8Array`
## Notes
The protected-bucket arguments are the SERIALISED byte string per §3 — not
the inner map. An empty protected bucket is a zero-length byte string; the
encoder helpers in `mac0.ts` and `sign1.ts` produce this form when the
caller's `protectedHeader` is empty.

View File

@@ -0,0 +1,58 @@
# `sign`
## Purpose
`COSE_Sign` (RFC 9052 §4.1) — multi-signer COSE message in CBOR tag 98.
Use when a payload needs to be co-signed by multiple parties (e.g. multi-
party attestation, escrowed signing). Each signer carries its own protected
header (typically with its own `alg`) and produces an independent signature
over the canonical `Sig_structure` for context `"Signature"`.
## Features
- `encodeCoseSign` builds the 4-tuple `[bodyProtected, bodyUnprotected,
payload, signatures]` with deterministic encoding and per-signer
`[sigProtected, sigUnprotected, signature]` triples.
- `decodeCoseSign` recovers the typed `CoseSign` view, including
`protectedRaw` byte strings on every level needed to recompute the
five-element `Sig_structure`.
- `verifyCoseSignAny` walks the signers in order against a 1:1 candidate-
key array. Returns the first index whose signature verifies, or `-1` on
no match. Caller decides whether one valid signer is enough or whether all
signers must verify (loop the array).
- Currently dispatches to ES256 + EdDSA via `@suaveplan/crypto-curve`;
unsupported algs return `false` (signer is skipped) rather than throwing
so the helper can scan a heterogeneous array without aborting.
## Usage
```ts
import { encodeCoseSign, decodeCoseSign, verifyCoseSignAny } from "@suaveplan/cbor/cose";
const wire = encodeCoseSign({
payload,
signatures: [
{ protectedHeader: { alg: "ES256" }, signature: sig1 },
{ protectedHeader: { alg: "EdDSA" }, signature: sig2 },
],
});
const msg = decodeCoseSign(wire);
const matchIndex = verifyCoseSignAny(msg, [keyA, keyB]);
```
## API
- `encodeCoseSign(input)` / `decodeCoseSign(bytes)`.
- `verifyCoseSignAny(msg, keys, options?): number` — returns matching index
or `-1`. Length mismatch between `keys` and `msg.signatures` throws
`UNEXPECTED_TYPE`.
- `COSE_SIGN_TAG = 98`.
## Notes
Sig_structure for multi-signer is 5-element:
`[context, body_protected, sign_protected, external_aad, payload]` — the
extra `sign_protected` discriminates the signer's own protected header from
the message-wide one. Both must be the SERIALISED byte strings, not the
inner maps.

View File

@@ -0,0 +1,60 @@
# `sign1`
## Purpose
`COSE_Sign1` (RFC 9052 §4.2) — single-signer COSE message in CBOR tag 18.
WebAuthn attestation, COSE-signed firmware, and CWT signature flows all
land here. This module ships the encode/decode/verify triple plus a typed
`CoseSign1` view.
## Features
- `encodeCoseSign1` produces both tagged (default) and tagless wire forms.
- `decodeCoseSign1` accepts either form transparently.
- `verifyCoseSign1` wires `@suaveplan/crypto-curve` (ES256, EdDSA) and uses
the canonical `Sig_structure1` from `sig-structure.ts`.
- Public-key argument is polymorphic: typed `Ec2Key` / `OkpKey` (the helper
builds the wire-level point form internally) OR a raw byte-string key for
callers that already have a primitive shape.
- `null` payload (detached signature) decodes correctly; verify rejects with
a clear "not yet supported" error.
## Usage
```ts
import { decodeCoseSign1, encodeCoseSign1, verifyCoseSign1 } from "@suaveplan/cbor/cose";
import { buildSign1Structure } from "@suaveplan/cbor/cose";
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
// Sign
const draft = decodeCoseSign1(
encodeCoseSign1({ protectedHeader: { alg: "EdDSA" }, payload, signature: new Uint8Array(64) }),
);
const tbs = buildSign1Structure(draft.protectedRaw, new Uint8Array(0), payload);
const sig = ed25519.sign(tbs, privateKey);
const wire = encodeCoseSign1({ protectedHeader: { alg: "EdDSA" }, payload, signature: sig });
// Verify
const msg = decodeCoseSign1(wire);
const ok = verifyCoseSign1(msg, publicKey);
```
## API
- `encodeCoseSign1(input: EncodeCoseSign1Input): Uint8Array`
- `decodeCoseSign1(bytes: Uint8Array): CoseSign1`
- `verifyCoseSign1(msg: CoseSign1, key: Ec2Key | OkpKey | Uint8Array, options?): boolean`
- `COSE_SIGN1_TAG = 18`
`CoseSign1` carries `protectedRaw` (the serialised inner-map bytes) so
callers can recompute Sig_structure1 verbatim — critical when verifying a
message they did not produce.
## Notes
ES256 uses noble-p256's compact `(r||s)` signature form and consumes a
SHA-256 digest of the TBS bytes (matches RFC 9053 §2.1). EdDSA verifies
the raw TBS bytes directly per RFC 8032 §5.1.7. ES384 and ES512 throw
"not implemented" today — `@suaveplan/crypto-curve` does not ship the P-384
or P-521 primitives yet, and the bridge surfaces the gap rather than
silently falling back.