first commit
This commit is contained in:
67
docs/foundation/flow/cbor/src/cose/alg-map.md
Normal file
67
docs/foundation/flow/cbor/src/cose/alg-map.md
Normal 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.
|
||||
71
docs/foundation/flow/cbor/src/cose/cose-key.md
Normal file
71
docs/foundation/flow/cbor/src/cose/cose-key.md
Normal 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.
|
||||
55
docs/foundation/flow/cbor/src/cose/encrypt.md
Normal file
55
docs/foundation/flow/cbor/src/cose/encrypt.md
Normal 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.
|
||||
54
docs/foundation/flow/cbor/src/cose/encrypt0.md
Normal file
54
docs/foundation/flow/cbor/src/cose/encrypt0.md
Normal 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.
|
||||
43
docs/foundation/flow/cbor/src/cose/headers.md
Normal file
43
docs/foundation/flow/cbor/src/cose/headers.md
Normal 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.
|
||||
70
docs/foundation/flow/cbor/src/cose/jwk.md
Normal file
70
docs/foundation/flow/cbor/src/cose/jwk.md
Normal 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.
|
||||
55
docs/foundation/flow/cbor/src/cose/mac.md
Normal file
55
docs/foundation/flow/cbor/src/cose/mac.md
Normal 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.
|
||||
50
docs/foundation/flow/cbor/src/cose/mac0.md
Normal file
50
docs/foundation/flow/cbor/src/cose/mac0.md
Normal 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.
|
||||
48
docs/foundation/flow/cbor/src/cose/sig-structure.md
Normal file
48
docs/foundation/flow/cbor/src/cose/sig-structure.md
Normal 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.
|
||||
58
docs/foundation/flow/cbor/src/cose/sign.md
Normal file
58
docs/foundation/flow/cbor/src/cose/sign.md
Normal 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.
|
||||
60
docs/foundation/flow/cbor/src/cose/sign1.md
Normal file
60
docs/foundation/flow/cbor/src/cose/sign1.md
Normal 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.
|
||||
Reference in New Issue
Block a user