@moq-web/cat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cbor.js ADDED
@@ -0,0 +1,379 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ // ============================================================================
4
+ // Constants
5
+ // ============================================================================
6
+ const MAX_SAFE_DECODE_DEPTH = 32;
7
+ const MAX_ENCODE_DEPTH = 32;
8
+ const MAX_DECODE_LENGTH = 1_048_576; // 1 MiB
9
+ // ============================================================================
10
+ // Encoder
11
+ // ============================================================================
12
+ /**
13
+ * Encode a CBOR value to bytes.
14
+ */
15
+ export function cborEncode(value) {
16
+ const parts = [];
17
+ encodeValue(parts, value, 0);
18
+ return concatenate(parts);
19
+ }
20
+ /**
21
+ * Encode a CBOR tagged value.
22
+ */
23
+ export function cborEncodeTagged(tag, value) {
24
+ const parts = [];
25
+ parts.push(encodeHead(6, tag));
26
+ encodeValue(parts, value, 0);
27
+ return concatenate(parts);
28
+ }
29
+ function encodeValue(parts, value, depth) {
30
+ if (depth > MAX_ENCODE_DEPTH) {
31
+ throw new CborError('CBOR encoding depth exceeded');
32
+ }
33
+ if (value === null) {
34
+ parts.push(new Uint8Array([0xf6])); // simple(22)
35
+ return;
36
+ }
37
+ if (value === true) {
38
+ parts.push(new Uint8Array([0xf5])); // simple(21)
39
+ return;
40
+ }
41
+ if (value === false) {
42
+ parts.push(new Uint8Array([0xf4])); // simple(20)
43
+ return;
44
+ }
45
+ if (typeof value === 'number') {
46
+ // L3: Reject numbers that exceed safe integer range for encoding
47
+ if (value > Number.MAX_SAFE_INTEGER || value < -Number.MAX_SAFE_INTEGER) {
48
+ throw new CborError('Number exceeds safe integer range; use bigint for values > 2^53');
49
+ }
50
+ if (value >= 0) {
51
+ parts.push(encodeHead(0, value));
52
+ }
53
+ else {
54
+ parts.push(encodeHead(1, -1 - value));
55
+ }
56
+ return;
57
+ }
58
+ if (typeof value === 'bigint') {
59
+ if (value >= 0n) {
60
+ parts.push(encodeHeadBigInt(0, value));
61
+ }
62
+ else {
63
+ parts.push(encodeHeadBigInt(1, -1n - value));
64
+ }
65
+ return;
66
+ }
67
+ if (typeof value === 'string') {
68
+ const encoded = new TextEncoder().encode(value);
69
+ parts.push(encodeHead(3, encoded.length));
70
+ parts.push(encoded);
71
+ return;
72
+ }
73
+ if (value instanceof Uint8Array) {
74
+ parts.push(encodeHead(2, value.length));
75
+ parts.push(value);
76
+ return;
77
+ }
78
+ if (Array.isArray(value)) {
79
+ parts.push(encodeHead(4, value.length));
80
+ for (const item of value) {
81
+ encodeValue(parts, item, depth + 1);
82
+ }
83
+ return;
84
+ }
85
+ if (value instanceof Map) {
86
+ // Deterministic encoding: sort by encoded key bytes (CBOR canonical)
87
+ const entries = Array.from(value.entries());
88
+ const encodedEntries = [];
89
+ for (const [key, val] of entries) {
90
+ const keyParts = [];
91
+ encodeValue(keyParts, key, depth + 1);
92
+ encodedEntries.push({ keyBytes: concatenate(keyParts), key, val });
93
+ }
94
+ // Sort by encoded key bytes (length first, then lexicographic)
95
+ encodedEntries.sort((a, b) => {
96
+ if (a.keyBytes.length !== b.keyBytes.length)
97
+ return a.keyBytes.length - b.keyBytes.length;
98
+ for (let i = 0; i < a.keyBytes.length; i++) {
99
+ if (a.keyBytes[i] !== b.keyBytes[i])
100
+ return a.keyBytes[i] - b.keyBytes[i];
101
+ }
102
+ return 0;
103
+ });
104
+ parts.push(encodeHead(5, encodedEntries.length));
105
+ for (const entry of encodedEntries) {
106
+ parts.push(entry.keyBytes);
107
+ encodeValue(parts, entry.val, depth + 1);
108
+ }
109
+ return;
110
+ }
111
+ throw new CborError(`Unsupported CBOR value type: ${typeof value}`);
112
+ }
113
+ /**
114
+ * Encode CBOR major type + argument header.
115
+ */
116
+ function encodeHead(majorType, value) {
117
+ const mt = majorType << 5;
118
+ if (value < 24) {
119
+ return new Uint8Array([mt | value]);
120
+ }
121
+ if (value < 0x100) {
122
+ return new Uint8Array([mt | 24, value]);
123
+ }
124
+ if (value < 0x10000) {
125
+ return new Uint8Array([mt | 25, (value >> 8) & 0xff, value & 0xff]);
126
+ }
127
+ if (value < 0x100000000) {
128
+ return new Uint8Array([
129
+ mt | 26,
130
+ (value >> 24) & 0xff,
131
+ (value >> 16) & 0xff,
132
+ (value >> 8) & 0xff,
133
+ value & 0xff,
134
+ ]);
135
+ }
136
+ // 8-byte encoding — use BigInt path for precision safety
137
+ return encodeHeadBigInt(majorType, BigInt(value));
138
+ }
139
+ /**
140
+ * Encode CBOR major type + argument header for bigint values.
141
+ */
142
+ function encodeHeadBigInt(majorType, value) {
143
+ if (value < 24n) {
144
+ return new Uint8Array([(majorType << 5) | Number(value)]);
145
+ }
146
+ if (value < 0x100n) {
147
+ return new Uint8Array([(majorType << 5) | 24, Number(value)]);
148
+ }
149
+ if (value < 0x10000n) {
150
+ const n = Number(value);
151
+ return new Uint8Array([(majorType << 5) | 25, (n >> 8) & 0xff, n & 0xff]);
152
+ }
153
+ if (value < 0x100000000n) {
154
+ const n = Number(value);
155
+ return new Uint8Array([
156
+ (majorType << 5) | 26,
157
+ (n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff,
158
+ ]);
159
+ }
160
+ const buf = new Uint8Array(9);
161
+ buf[0] = (majorType << 5) | 27;
162
+ const view = new DataView(buf.buffer);
163
+ view.setBigUint64(1, value, false);
164
+ return buf;
165
+ }
166
+ // ============================================================================
167
+ // Decoder
168
+ // ============================================================================
169
+ /**
170
+ * Decode a CBOR value from bytes.
171
+ *
172
+ * @param data - The CBOR bytes
173
+ * @param offset - Starting offset (default 0)
174
+ * @returns Decoded value and number of bytes consumed
175
+ */
176
+ export function cborDecode(data, offset = 0) {
177
+ if (data.length === 0 || offset >= data.length) {
178
+ throw new CborError('Empty CBOR input');
179
+ }
180
+ const result = decodeValue(data, offset, 0);
181
+ return { value: result.value, bytesRead: result.offset - offset };
182
+ }
183
+ /**
184
+ * Decode a CBOR tagged value. If the data starts with a tag, returns the tag
185
+ * and inner value. Otherwise returns tag=-1 and the decoded value.
186
+ */
187
+ export function cborDecodeTagged(data, offset = 0) {
188
+ if (offset >= data.length) {
189
+ throw new CborError('Empty CBOR input');
190
+ }
191
+ const initial = data[offset];
192
+ const majorType = initial >> 5;
193
+ if (majorType === 6) {
194
+ // Tag
195
+ const { value: tag, newOffset } = readArgument(data, offset);
196
+ const inner = decodeValue(data, newOffset, 0);
197
+ return { tag: Number(tag), value: inner.value, bytesRead: inner.offset - offset };
198
+ }
199
+ const result = decodeValue(data, offset, 0);
200
+ return { tag: -1, value: result.value, bytesRead: result.offset - offset };
201
+ }
202
+ function decodeValue(data, offset, depth) {
203
+ if (depth > MAX_SAFE_DECODE_DEPTH) {
204
+ throw new CborError('CBOR nesting depth exceeded');
205
+ }
206
+ if (offset >= data.length) {
207
+ throw new CborError('Unexpected end of CBOR data');
208
+ }
209
+ const initial = data[offset];
210
+ const majorType = initial >> 5;
211
+ switch (majorType) {
212
+ case 0: { // unsigned integer
213
+ const { value, newOffset } = readArgument(data, offset);
214
+ return { value: value <= Number.MAX_SAFE_INTEGER ? Number(value) : value, offset: newOffset };
215
+ }
216
+ case 1: { // negative integer
217
+ const { value, newOffset } = readArgument(data, offset);
218
+ if (value <= Number.MAX_SAFE_INTEGER) {
219
+ return { value: -1 - Number(value), offset: newOffset };
220
+ }
221
+ return { value: -1n - value, offset: newOffset };
222
+ }
223
+ case 2: { // byte string
224
+ const { value: len, newOffset } = readArgument(data, offset);
225
+ const length = Number(len);
226
+ validateLength(length, data.length - newOffset);
227
+ const bytes = data.slice(newOffset, newOffset + length);
228
+ return { value: bytes, offset: newOffset + length };
229
+ }
230
+ case 3: { // text string
231
+ const { value: len, newOffset } = readArgument(data, offset);
232
+ const length = Number(len);
233
+ validateLength(length, data.length - newOffset);
234
+ const bytes = data.slice(newOffset, newOffset + length);
235
+ return { value: new TextDecoder().decode(bytes), offset: newOffset + length };
236
+ }
237
+ case 4: { // array
238
+ const { value: len, newOffset } = readArgument(data, offset);
239
+ const length = Number(len);
240
+ if (length > MAX_DECODE_LENGTH) {
241
+ throw new CborError(`Array too large: ${length}`);
242
+ }
243
+ const arr = [];
244
+ let pos = newOffset;
245
+ for (let i = 0; i < length; i++) {
246
+ const item = decodeValue(data, pos, depth + 1);
247
+ arr.push(item.value);
248
+ pos = item.offset;
249
+ }
250
+ return { value: arr, offset: pos };
251
+ }
252
+ case 5: { // map
253
+ const { value: len, newOffset } = readArgument(data, offset);
254
+ const length = Number(len);
255
+ if (length > MAX_DECODE_LENGTH) {
256
+ throw new CborError(`Map too large: ${length}`);
257
+ }
258
+ const map = new Map();
259
+ let pos = newOffset;
260
+ for (let i = 0; i < length; i++) {
261
+ const keyResult = decodeValue(data, pos, depth + 1);
262
+ const key = keyResult.value;
263
+ pos = keyResult.offset;
264
+ const valResult = decodeValue(data, pos, depth + 1);
265
+ pos = valResult.offset;
266
+ let mapKey;
267
+ if (typeof key === 'number' || typeof key === 'string') {
268
+ mapKey = key;
269
+ }
270
+ else if (typeof key === 'bigint') {
271
+ mapKey = Number(key);
272
+ }
273
+ else {
274
+ throw new CborError(`Unsupported map key type: ${typeof key}`);
275
+ }
276
+ // Reject duplicate map keys (RFC 8949 Section 5.6)
277
+ if (map.has(mapKey)) {
278
+ throw new CborError(`Duplicate CBOR map key: ${mapKey}`);
279
+ }
280
+ map.set(mapKey, valResult.value);
281
+ }
282
+ return { value: map, offset: pos };
283
+ }
284
+ case 6: { // tag — reject unknown tags in security context
285
+ const { value: tagNum, newOffset } = readArgument(data, offset);
286
+ const tag = Number(tagNum);
287
+ // Only allow COSE_Sign1 tag (18) — reject all others
288
+ if (tag !== 18) {
289
+ throw new CborError(`Unsupported CBOR tag: ${tag}`);
290
+ }
291
+ const inner = decodeValue(data, newOffset, depth + 1);
292
+ return { value: inner.value, offset: inner.offset };
293
+ }
294
+ case 7: { // simple values
295
+ const additionalInfo = initial & 0x1f;
296
+ if (additionalInfo === 20)
297
+ return { value: false, offset: offset + 1 };
298
+ if (additionalInfo === 21)
299
+ return { value: true, offset: offset + 1 };
300
+ if (additionalInfo === 22)
301
+ return { value: null, offset: offset + 1 };
302
+ // Float16/32/64 not supported
303
+ if (additionalInfo >= 25 && additionalInfo <= 27) {
304
+ throw new CborError('Float CBOR values not supported');
305
+ }
306
+ return { value: additionalInfo, offset: offset + 1 };
307
+ }
308
+ default:
309
+ throw new CborError(`Unknown CBOR major type: ${majorType}`);
310
+ }
311
+ }
312
+ /**
313
+ * Read CBOR argument (additional info + extended value).
314
+ */
315
+ function readArgument(data, offset) {
316
+ const initial = data[offset];
317
+ const additionalInfo = initial & 0x1f;
318
+ offset++;
319
+ if (additionalInfo < 24) {
320
+ return { value: BigInt(additionalInfo), newOffset: offset };
321
+ }
322
+ if (additionalInfo === 24) {
323
+ if (offset >= data.length)
324
+ throw new CborError('Unexpected end of CBOR data');
325
+ return { value: BigInt(data[offset]), newOffset: offset + 1 };
326
+ }
327
+ if (additionalInfo === 25) {
328
+ if (offset + 2 > data.length)
329
+ throw new CborError('Unexpected end of CBOR data');
330
+ const value = (data[offset] << 8) | data[offset + 1];
331
+ return { value: BigInt(value), newOffset: offset + 2 };
332
+ }
333
+ if (additionalInfo === 26) {
334
+ if (offset + 4 > data.length)
335
+ throw new CborError('Unexpected end of CBOR data');
336
+ const value = ((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]) >>> 0;
337
+ return { value: BigInt(value), newOffset: offset + 4 };
338
+ }
339
+ if (additionalInfo === 27) {
340
+ if (offset + 8 > data.length)
341
+ throw new CborError('Unexpected end of CBOR data');
342
+ const view = new DataView(data.buffer, data.byteOffset + offset, 8);
343
+ const value = view.getBigUint64(0, false);
344
+ return { value, newOffset: offset + 8 };
345
+ }
346
+ throw new CborError(`Invalid CBOR additional info: ${additionalInfo}`);
347
+ }
348
+ function validateLength(length, available) {
349
+ if (length > MAX_DECODE_LENGTH) {
350
+ throw new CborError(`Data too large: ${length}`);
351
+ }
352
+ if (length > available) {
353
+ throw new CborError(`Insufficient data: need ${length}, have ${available}`);
354
+ }
355
+ }
356
+ // ============================================================================
357
+ // Helpers
358
+ // ============================================================================
359
+ function concatenate(parts) {
360
+ let totalLength = 0;
361
+ for (const p of parts)
362
+ totalLength += p.length;
363
+ const result = new Uint8Array(totalLength);
364
+ let offset = 0;
365
+ for (const p of parts) {
366
+ result.set(p, offset);
367
+ offset += p.length;
368
+ }
369
+ return result;
370
+ }
371
+ /**
372
+ * CBOR encoding/decoding error.
373
+ */
374
+ export class CborError extends Error {
375
+ constructor(message) {
376
+ super(message);
377
+ this.name = 'CborError';
378
+ }
379
+ }
package/dist/cose.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { CoseAlgorithm, type CoseSign1, type CborValue } from './types.js';
2
+ /**
3
+ * Encode a COSE_Sign1 structure to CBOR bytes.
4
+ * Produces a bare 4-element array (no CBOR Tag 18) for relay compatibility.
5
+ */
6
+ export declare function coseSign1Encode(sign1: CoseSign1): Uint8Array;
7
+ /**
8
+ * Decode a COSE_Sign1 structure from CBOR bytes.
9
+ * Handles both CBOR Tag 18 and bare 4-element array.
10
+ *
11
+ * @throws {CoseError} If the data is not a valid COSE_Sign1 structure
12
+ */
13
+ export declare function coseSign1Decode(data: Uint8Array): CoseSign1;
14
+ /**
15
+ * Construct the Sig_structure for COSE_Sign1 signing/verification.
16
+ *
17
+ * Sig_structure = [
18
+ * context : "Signature1",
19
+ * body_protected : bstr,
20
+ * external_aad : bstr,
21
+ * payload : bstr
22
+ * ]
23
+ */
24
+ export declare function coseSign1SigStructure(protectedHeader: Uint8Array, payload: Uint8Array, externalAAD?: Uint8Array): Uint8Array;
25
+ /**
26
+ * Construct the MAC_structure for COSE_Mac0 (RFC 9052 Section 6.3).
27
+ *
28
+ * MAC_structure = [
29
+ * context : "MAC0",
30
+ * body_protected : bstr,
31
+ * external_aad : bstr,
32
+ * payload : bstr
33
+ * ]
34
+ */
35
+ export declare function coseMac0MacStructure(protectedHeader: Uint8Array, payload: Uint8Array, externalAAD?: Uint8Array): Uint8Array;
36
+ /**
37
+ * Create and sign a COSE_Sign1 or COSE_Mac0 structure.
38
+ * Automatically selects Sig_structure vs MAC_structure based on algorithm.
39
+ */
40
+ export declare function coseSign1Sign(algorithm: CoseAlgorithm, protectedHeaders: Map<number, CborValue>, payload: Uint8Array, privateKey: CryptoKey, unprotectedHeaders?: Map<number, CborValue>): Promise<CoseSign1>;
41
+ /**
42
+ * Verify the signature of a COSE_Sign1 structure.
43
+ *
44
+ * Accepts an optional `requiredAlgorithm` to prevent algorithm confusion.
45
+ * When provided, the token's algorithm must match exactly.
46
+ *
47
+ * Only catches DOMException from WebCrypto. Other errors propagate.
48
+ */
49
+ export declare function coseSign1Verify(sign1: CoseSign1, publicKey: CryptoKey, requiredAlgorithm?: CoseAlgorithm): Promise<boolean>;
50
+ /**
51
+ * Extract the algorithm from a COSE_Sign1 protected header.
52
+ *
53
+ * @throws {CoseError} If the algorithm is missing or unsupported
54
+ */
55
+ export declare function coseSign1GetAlgorithm(sign1: CoseSign1): CoseAlgorithm;
56
+ /**
57
+ * Decode the protected header of a COSE_Sign1 as a map.
58
+ *
59
+ * Rejects truly empty headers (0 bytes). Accepts empty CBOR map (0xa0).
60
+ */
61
+ export declare function coseSign1DecodeProtectedHeader(protectedHeader: Uint8Array): Map<number, CborValue>;
62
+ /**
63
+ * COSE encoding/decoding error.
64
+ */
65
+ export declare class CoseError extends Error {
66
+ constructor(message: string);
67
+ }
package/dist/cose.js ADDED
@@ -0,0 +1,253 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /**
4
+ * @fileoverview COSE_Sign1 Implementation (RFC 9052)
5
+ *
6
+ * Implements COSE_Sign1 structure for creating and verifying
7
+ * signed tokens using ECDSA (ES256/ES384/ES512) via WebCrypto.
8
+ */
9
+ import { cborEncode, cborDecode, cborDecodeTagged } from './cbor.js';
10
+ import { CoseHeaderParam, COSE_ALG_PARAMS, } from './types.js';
11
+ /** COSE_Sign1 CBOR tag number */
12
+ const COSE_SIGN1_TAG = 18;
13
+ /**
14
+ * Convert Uint8Array to ArrayBuffer for WebCrypto API compatibility.
15
+ */
16
+ function toArrayBuffer(arr) {
17
+ return arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
18
+ }
19
+ // ============================================================================
20
+ // Encoding / Decoding
21
+ // ============================================================================
22
+ /**
23
+ * Encode a COSE_Sign1 structure to CBOR bytes.
24
+ * Produces a bare 4-element array (no CBOR Tag 18) for relay compatibility.
25
+ */
26
+ export function coseSign1Encode(sign1) {
27
+ const array = [
28
+ sign1.protectedHeader,
29
+ sign1.unprotectedHeader,
30
+ sign1.payload,
31
+ sign1.signature,
32
+ ];
33
+ return cborEncode(array);
34
+ }
35
+ /**
36
+ * Decode a COSE_Sign1 structure from CBOR bytes.
37
+ * Handles both CBOR Tag 18 and bare 4-element array.
38
+ *
39
+ * @throws {CoseError} If the data is not a valid COSE_Sign1 structure
40
+ */
41
+ export function coseSign1Decode(data) {
42
+ const { tag, value } = cborDecodeTagged(data);
43
+ // Accept both tagged (18) and bare array
44
+ if (tag !== -1 && tag !== COSE_SIGN1_TAG) {
45
+ throw new CoseError(`Unexpected CBOR tag: ${tag}, expected ${COSE_SIGN1_TAG}`);
46
+ }
47
+ if (!Array.isArray(value) || value.length !== 4) {
48
+ throw new CoseError(`COSE_Sign1 must be a 4-element array, got ${Array.isArray(value) ? value.length : typeof value}`);
49
+ }
50
+ const [protectedHeader, unprotectedHeader, payload, signature] = value;
51
+ if (!(protectedHeader instanceof Uint8Array)) {
52
+ throw new CoseError('COSE_Sign1 protected header must be a bstr');
53
+ }
54
+ if (!(payload instanceof Uint8Array)) {
55
+ throw new CoseError('COSE_Sign1 payload must be a bstr');
56
+ }
57
+ if (!(signature instanceof Uint8Array)) {
58
+ throw new CoseError('COSE_Sign1 signature must be a bstr');
59
+ }
60
+ // Decode unprotected header map
61
+ let unprotectedMap;
62
+ if (unprotectedHeader instanceof Map) {
63
+ unprotectedMap = unprotectedHeader;
64
+ }
65
+ else {
66
+ unprotectedMap = new Map();
67
+ }
68
+ return {
69
+ protectedHeader,
70
+ unprotectedHeader: unprotectedMap,
71
+ payload,
72
+ signature,
73
+ };
74
+ }
75
+ // ============================================================================
76
+ // Sig_structure (RFC 9052 Section 4.4)
77
+ // ============================================================================
78
+ /**
79
+ * Construct the Sig_structure for COSE_Sign1 signing/verification.
80
+ *
81
+ * Sig_structure = [
82
+ * context : "Signature1",
83
+ * body_protected : bstr,
84
+ * external_aad : bstr,
85
+ * payload : bstr
86
+ * ]
87
+ */
88
+ export function coseSign1SigStructure(protectedHeader, payload, externalAAD = new Uint8Array(0)) {
89
+ const structure = [
90
+ 'Signature1',
91
+ protectedHeader,
92
+ externalAAD,
93
+ payload,
94
+ ];
95
+ return cborEncode(structure);
96
+ }
97
+ /**
98
+ * Construct the MAC_structure for COSE_Mac0 (RFC 9052 Section 6.3).
99
+ *
100
+ * MAC_structure = [
101
+ * context : "MAC0",
102
+ * body_protected : bstr,
103
+ * external_aad : bstr,
104
+ * payload : bstr
105
+ * ]
106
+ */
107
+ export function coseMac0MacStructure(protectedHeader, payload, externalAAD = new Uint8Array(0)) {
108
+ const structure = [
109
+ 'MAC0',
110
+ protectedHeader,
111
+ externalAAD,
112
+ payload,
113
+ ];
114
+ return cborEncode(structure);
115
+ }
116
+ // ============================================================================
117
+ // Sign / Verify
118
+ // ============================================================================
119
+ /**
120
+ * Create and sign a COSE_Sign1 or COSE_Mac0 structure.
121
+ * Automatically selects Sig_structure vs MAC_structure based on algorithm.
122
+ */
123
+ export async function coseSign1Sign(algorithm, protectedHeaders, payload, privateKey, unprotectedHeaders) {
124
+ const algParams = COSE_ALG_PARAMS[algorithm];
125
+ if (!algParams) {
126
+ throw new CoseError(`Unsupported COSE algorithm: ${algorithm}`);
127
+ }
128
+ // Build protected header map with algorithm
129
+ const headerMap = new Map(protectedHeaders);
130
+ headerMap.set(CoseHeaderParam.ALG, algorithm);
131
+ // Encode protected header to CBOR
132
+ const protectedHeader = cborEncode(headerMap);
133
+ // Construct signing/MAC input based on algorithm type
134
+ const toBeSigned = algParams.isMac
135
+ ? coseMac0MacStructure(protectedHeader, payload)
136
+ : coseSign1SigStructure(protectedHeader, payload);
137
+ // Sign or MAC with WebCrypto
138
+ let signatureBuffer;
139
+ if (algParams.isMac) {
140
+ signatureBuffer = await crypto.subtle.sign(algParams.name, privateKey, toArrayBuffer(toBeSigned));
141
+ }
142
+ else {
143
+ signatureBuffer = await crypto.subtle.sign({ name: algParams.name, hash: algParams.hash }, privateKey, toArrayBuffer(toBeSigned));
144
+ }
145
+ const signature = new Uint8Array(signatureBuffer);
146
+ // Validate signature/tag length
147
+ if (signature.length !== algParams.sigLength) {
148
+ throw new CoseError(`Unexpected signature length: ${signature.length}, expected ${algParams.sigLength}`);
149
+ }
150
+ return {
151
+ protectedHeader,
152
+ unprotectedHeader: unprotectedHeaders ?? new Map(),
153
+ payload,
154
+ signature,
155
+ };
156
+ }
157
+ /**
158
+ * Verify the signature of a COSE_Sign1 structure.
159
+ *
160
+ * Accepts an optional `requiredAlgorithm` to prevent algorithm confusion.
161
+ * When provided, the token's algorithm must match exactly.
162
+ *
163
+ * Only catches DOMException from WebCrypto. Other errors propagate.
164
+ */
165
+ export async function coseSign1Verify(sign1, publicKey, requiredAlgorithm) {
166
+ // Extract algorithm from protected header
167
+ const algorithm = coseSign1GetAlgorithm(sign1);
168
+ const algParams = COSE_ALG_PARAMS[algorithm];
169
+ if (!algParams) {
170
+ return false;
171
+ }
172
+ // Enforce required algorithm if specified
173
+ if (requiredAlgorithm !== undefined && algorithm !== requiredAlgorithm) {
174
+ return false;
175
+ }
176
+ // For ECDSA: verify algorithm matches the public key's curve
177
+ if (!algParams.isMac) {
178
+ const keyAlg = publicKey.algorithm;
179
+ if (keyAlg.namedCurve && keyAlg.namedCurve !== algParams.namedCurve) {
180
+ return false;
181
+ }
182
+ }
183
+ // Validate signature/tag length
184
+ if (sign1.signature.length !== algParams.sigLength) {
185
+ return false;
186
+ }
187
+ // Construct signing/MAC input based on algorithm type
188
+ const toBeVerified = algParams.isMac
189
+ ? coseMac0MacStructure(sign1.protectedHeader, sign1.payload)
190
+ : coseSign1SigStructure(sign1.protectedHeader, sign1.payload);
191
+ // Only catch DOMException from WebCrypto, let other errors propagate
192
+ try {
193
+ if (algParams.isMac) {
194
+ return await crypto.subtle.verify(algParams.name, publicKey, // for HMAC, this is the shared secret key
195
+ toArrayBuffer(sign1.signature), toArrayBuffer(toBeVerified));
196
+ }
197
+ return await crypto.subtle.verify({ name: algParams.name, hash: algParams.hash }, publicKey, toArrayBuffer(sign1.signature), toArrayBuffer(toBeVerified));
198
+ }
199
+ catch (e) {
200
+ if (e instanceof DOMException) {
201
+ return false;
202
+ }
203
+ throw e;
204
+ }
205
+ }
206
+ /**
207
+ * Extract the algorithm from a COSE_Sign1 protected header.
208
+ *
209
+ * @throws {CoseError} If the algorithm is missing or unsupported
210
+ */
211
+ export function coseSign1GetAlgorithm(sign1) {
212
+ // Empty protected header (zero bytes) is invalid — must contain at least an empty map
213
+ if (sign1.protectedHeader.length === 0) {
214
+ throw new CoseError('Empty protected header');
215
+ }
216
+ const decoded = cborDecode(sign1.protectedHeader);
217
+ if (!(decoded.value instanceof Map)) {
218
+ throw new CoseError('Protected header must be a CBOR map');
219
+ }
220
+ const alg = decoded.value.get(CoseHeaderParam.ALG);
221
+ if (alg === undefined) {
222
+ throw new CoseError('Algorithm (alg) not found in protected header');
223
+ }
224
+ const algValue = typeof alg === 'number' ? alg : Number(alg);
225
+ if (!(algValue in COSE_ALG_PARAMS)) {
226
+ throw new CoseError(`Unsupported algorithm: ${algValue}`);
227
+ }
228
+ return algValue;
229
+ }
230
+ /**
231
+ * Decode the protected header of a COSE_Sign1 as a map.
232
+ *
233
+ * Rejects truly empty headers (0 bytes). Accepts empty CBOR map (0xa0).
234
+ */
235
+ export function coseSign1DecodeProtectedHeader(protectedHeader) {
236
+ if (protectedHeader.length === 0) {
237
+ throw new CoseError('Protected header must not be empty');
238
+ }
239
+ const decoded = cborDecode(protectedHeader);
240
+ if (!(decoded.value instanceof Map)) {
241
+ throw new CoseError('Protected header must be a CBOR map');
242
+ }
243
+ return decoded.value;
244
+ }
245
+ /**
246
+ * COSE encoding/decoding error.
247
+ */
248
+ export class CoseError extends Error {
249
+ constructor(message) {
250
+ super(message);
251
+ this.name = 'CoseError';
252
+ }
253
+ }