@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/cat.d.ts +127 -0
- package/dist/cat.js +326 -0
- package/dist/cbor.d.ts +46 -0
- package/dist/cbor.js +379 -0
- package/dist/cose.d.ts +67 -0
- package/dist/cose.js +253 -0
- package/dist/cwt.d.ts +42 -0
- package/dist/cwt.js +261 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +49 -0
- package/dist/test-utils.d.ts +71 -0
- package/dist/test-utils.js +136 -0
- package/dist/types.d.ts +178 -0
- package/dist/types.js +91 -0
- package/package.json +38 -0
package/dist/cwt.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type CwtClaims, type MoqtScope, type CborValue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encode CWT claims to a CBOR map (as bytes).
|
|
4
|
+
*/
|
|
5
|
+
export declare function cwtClaimsEncode(claims: CwtClaims): Uint8Array;
|
|
6
|
+
/**
|
|
7
|
+
* Decode CWT claims from CBOR map bytes.
|
|
8
|
+
*
|
|
9
|
+
* @throws {CwtError} If the data is not a valid CWT claims map
|
|
10
|
+
*/
|
|
11
|
+
export declare function cwtClaimsDecode(data: Uint8Array): CwtClaims;
|
|
12
|
+
/**
|
|
13
|
+
* Convert a decoded CBOR map to CwtClaims.
|
|
14
|
+
*/
|
|
15
|
+
export declare function cwtClaimsFromMap(map: Map<number | string, CborValue>): CwtClaims;
|
|
16
|
+
/**
|
|
17
|
+
* Encode MoQT scopes to a CBOR array value.
|
|
18
|
+
* Each scope: [actions_array, namespace_match?, track_match?]
|
|
19
|
+
*/
|
|
20
|
+
export declare function moqtScopesEncode(scopes: MoqtScope[]): CborValue[];
|
|
21
|
+
/**
|
|
22
|
+
* Decode MoQT scopes from a CBOR array.
|
|
23
|
+
*/
|
|
24
|
+
export declare function moqtScopesDecode(scopesArray: CborValue[]): MoqtScope[];
|
|
25
|
+
/**
|
|
26
|
+
* Check if CWT claims have expired.
|
|
27
|
+
*/
|
|
28
|
+
export declare function cwtIsExpired(claims: CwtClaims, nowSeconds?: number, clockSkewSeconds?: number): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Check if CWT claims are not yet valid (before nbf).
|
|
31
|
+
*/
|
|
32
|
+
export declare function cwtIsNotYetValid(claims: CwtClaims, nowSeconds?: number, clockSkewSeconds?: number): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Check if CWT claims match a required audience.
|
|
35
|
+
*/
|
|
36
|
+
export declare function cwtMatchesAudience(claims: CwtClaims, requiredAudience: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* CWT encoding/decoding error.
|
|
39
|
+
*/
|
|
40
|
+
export declare class CwtError extends Error {
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
package/dist/cwt.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CWT (CBOR Web Token) Claims Implementation (RFC 8392)
|
|
5
|
+
*
|
|
6
|
+
* Encodes and decodes CWT claims with integer keys, including
|
|
7
|
+
* MoQT-specific authorization scopes.
|
|
8
|
+
*/
|
|
9
|
+
import { cborEncode, cborDecode } from './cbor.js';
|
|
10
|
+
import { CwtClaimKey, MoqtAction, } from './types.js';
|
|
11
|
+
/** Standard CWT claim keys that cannot be overridden via additionalClaims */
|
|
12
|
+
const RESERVED_CLAIM_KEYS = new Set([
|
|
13
|
+
CwtClaimKey.ISS, CwtClaimKey.SUB, CwtClaimKey.AUD,
|
|
14
|
+
CwtClaimKey.EXP, CwtClaimKey.NBF, CwtClaimKey.IAT,
|
|
15
|
+
CwtClaimKey.CTI, CwtClaimKey.MOQT, 65000,
|
|
16
|
+
]);
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// CWT Claims Encode / Decode
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Encode CWT claims to a CBOR map (as bytes).
|
|
22
|
+
*/
|
|
23
|
+
export function cwtClaimsEncode(claims) {
|
|
24
|
+
const map = new Map();
|
|
25
|
+
if (claims.iss !== undefined)
|
|
26
|
+
map.set(CwtClaimKey.ISS, claims.iss);
|
|
27
|
+
if (claims.sub !== undefined)
|
|
28
|
+
map.set(CwtClaimKey.SUB, claims.sub);
|
|
29
|
+
if (claims.aud !== undefined) {
|
|
30
|
+
if (Array.isArray(claims.aud)) {
|
|
31
|
+
map.set(CwtClaimKey.AUD, claims.aud);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
map.set(CwtClaimKey.AUD, claims.aud);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (claims.exp !== undefined)
|
|
38
|
+
map.set(CwtClaimKey.EXP, claims.exp);
|
|
39
|
+
if (claims.nbf !== undefined)
|
|
40
|
+
map.set(CwtClaimKey.NBF, claims.nbf);
|
|
41
|
+
if (claims.iat !== undefined)
|
|
42
|
+
map.set(CwtClaimKey.IAT, claims.iat);
|
|
43
|
+
if (claims.cti !== undefined)
|
|
44
|
+
map.set(CwtClaimKey.CTI, claims.cti);
|
|
45
|
+
if (claims.moqt !== undefined) {
|
|
46
|
+
map.set(CwtClaimKey.MOQT, moqtScopesEncode(claims.moqt));
|
|
47
|
+
}
|
|
48
|
+
// M6: Additional claims — reject collisions with standard claim keys
|
|
49
|
+
if (claims.additionalClaims) {
|
|
50
|
+
for (const [key, value] of claims.additionalClaims) {
|
|
51
|
+
if (RESERVED_CLAIM_KEYS.has(key)) {
|
|
52
|
+
throw new CwtError(`Additional claim key ${key} conflicts with a reserved CWT claim key`);
|
|
53
|
+
}
|
|
54
|
+
map.set(key, value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return cborEncode(map);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Decode CWT claims from CBOR map bytes.
|
|
61
|
+
*
|
|
62
|
+
* @throws {CwtError} If the data is not a valid CWT claims map
|
|
63
|
+
*/
|
|
64
|
+
export function cwtClaimsDecode(data) {
|
|
65
|
+
const decoded = cborDecode(data);
|
|
66
|
+
if (!(decoded.value instanceof Map)) {
|
|
67
|
+
throw new CwtError('CWT claims must be a CBOR map');
|
|
68
|
+
}
|
|
69
|
+
return cwtClaimsFromMap(decoded.value);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Convert a decoded CBOR map to CwtClaims.
|
|
73
|
+
*/
|
|
74
|
+
export function cwtClaimsFromMap(map) {
|
|
75
|
+
const claims = {};
|
|
76
|
+
const additional = new Map();
|
|
77
|
+
for (const [key, value] of map) {
|
|
78
|
+
const numKey = typeof key === 'string' ? parseInt(key, 10) : key;
|
|
79
|
+
if (isNaN(numKey))
|
|
80
|
+
continue;
|
|
81
|
+
switch (numKey) {
|
|
82
|
+
case CwtClaimKey.ISS:
|
|
83
|
+
if (typeof value === 'string')
|
|
84
|
+
claims.iss = value;
|
|
85
|
+
break;
|
|
86
|
+
case CwtClaimKey.SUB:
|
|
87
|
+
if (typeof value === 'string')
|
|
88
|
+
claims.sub = value;
|
|
89
|
+
break;
|
|
90
|
+
case CwtClaimKey.AUD:
|
|
91
|
+
if (typeof value === 'string') {
|
|
92
|
+
claims.aud = value;
|
|
93
|
+
}
|
|
94
|
+
else if (Array.isArray(value)) {
|
|
95
|
+
claims.aud = value.filter((v) => typeof v === 'string');
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
case CwtClaimKey.EXP:
|
|
99
|
+
// L5: Validate that time claims are integers
|
|
100
|
+
if (typeof value === 'number') {
|
|
101
|
+
if (!Number.isInteger(value)) {
|
|
102
|
+
throw new CwtError(`CWT exp claim must be an integer, got ${value}`);
|
|
103
|
+
}
|
|
104
|
+
claims.exp = value;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
case CwtClaimKey.NBF:
|
|
108
|
+
if (typeof value === 'number') {
|
|
109
|
+
if (!Number.isInteger(value)) {
|
|
110
|
+
throw new CwtError(`CWT nbf claim must be an integer, got ${value}`);
|
|
111
|
+
}
|
|
112
|
+
claims.nbf = value;
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case CwtClaimKey.IAT:
|
|
116
|
+
if (typeof value === 'number') {
|
|
117
|
+
if (!Number.isInteger(value)) {
|
|
118
|
+
throw new CwtError(`CWT iat claim must be an integer, got ${value}`);
|
|
119
|
+
}
|
|
120
|
+
claims.iat = value;
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
case CwtClaimKey.CTI:
|
|
124
|
+
if (value instanceof Uint8Array)
|
|
125
|
+
claims.cti = value;
|
|
126
|
+
break;
|
|
127
|
+
case CwtClaimKey.MOQT:
|
|
128
|
+
case 65000: // Legacy MoQT claim key
|
|
129
|
+
claims.moqt = decodeMoqtScopesValue(value);
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
additional.set(numKey, value);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (additional.size > 0) {
|
|
137
|
+
claims.additionalClaims = additional;
|
|
138
|
+
}
|
|
139
|
+
return claims;
|
|
140
|
+
}
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// MoQT Scopes
|
|
143
|
+
// ============================================================================
|
|
144
|
+
/**
|
|
145
|
+
* Encode MoQT scopes to a CBOR array value.
|
|
146
|
+
* Each scope: [actions_array, namespace_match?, track_match?]
|
|
147
|
+
*/
|
|
148
|
+
export function moqtScopesEncode(scopes) {
|
|
149
|
+
return scopes.map((scope) => {
|
|
150
|
+
const entry = [scope.actions.map((a) => a)];
|
|
151
|
+
if (scope.namespaceMatch !== undefined) {
|
|
152
|
+
entry.push(scope.namespaceMatch.map((n) => n instanceof Uint8Array ? n : n));
|
|
153
|
+
}
|
|
154
|
+
if (scope.trackMatch !== undefined) {
|
|
155
|
+
// Ensure namespace is present (even if null) before track
|
|
156
|
+
if (scope.namespaceMatch === undefined) {
|
|
157
|
+
entry.push(null);
|
|
158
|
+
}
|
|
159
|
+
entry.push(scope.trackMatch instanceof Uint8Array ? scope.trackMatch : scope.trackMatch);
|
|
160
|
+
}
|
|
161
|
+
return entry;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Decode MoQT scopes from a CBOR value.
|
|
166
|
+
* Handles both direct CBOR array and bstr-wrapped CBOR.
|
|
167
|
+
*/
|
|
168
|
+
function decodeMoqtScopesValue(value) {
|
|
169
|
+
let scopesArray;
|
|
170
|
+
if (value instanceof Uint8Array) {
|
|
171
|
+
// bstr-wrapped CBOR
|
|
172
|
+
try {
|
|
173
|
+
const decoded = cborDecode(value);
|
|
174
|
+
if (!Array.isArray(decoded.value))
|
|
175
|
+
return [];
|
|
176
|
+
scopesArray = decoded.value;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (Array.isArray(value)) {
|
|
183
|
+
scopesArray = value;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
return moqtScopesDecode(scopesArray);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Decode MoQT scopes from a CBOR array.
|
|
192
|
+
*/
|
|
193
|
+
export function moqtScopesDecode(scopesArray) {
|
|
194
|
+
const scopes = [];
|
|
195
|
+
for (const scopeValue of scopesArray) {
|
|
196
|
+
if (!Array.isArray(scopeValue) || scopeValue.length === 0)
|
|
197
|
+
continue;
|
|
198
|
+
const actionsValue = scopeValue[0];
|
|
199
|
+
if (!Array.isArray(actionsValue))
|
|
200
|
+
continue;
|
|
201
|
+
const actions = actionsValue
|
|
202
|
+
.filter((a) => typeof a === 'number')
|
|
203
|
+
.filter((a) => a >= MoqtAction.ClientSetup && a <= MoqtAction.TrackStatus);
|
|
204
|
+
const scope = { actions };
|
|
205
|
+
if (scopeValue.length > 1 && scopeValue[1] !== null) {
|
|
206
|
+
const nsMatch = scopeValue[1];
|
|
207
|
+
if (Array.isArray(nsMatch)) {
|
|
208
|
+
// L6: Reject non-string/non-bytes namespace elements instead of coercing
|
|
209
|
+
scope.namespaceMatch = nsMatch.filter((n) => typeof n === 'string' || n instanceof Uint8Array);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (scopeValue.length > 2 && scopeValue[2] !== null) {
|
|
213
|
+
const trackMatch = scopeValue[2];
|
|
214
|
+
if (typeof trackMatch === 'string' || trackMatch instanceof Uint8Array) {
|
|
215
|
+
scope.trackMatch = trackMatch;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
scopes.push(scope);
|
|
219
|
+
}
|
|
220
|
+
return scopes;
|
|
221
|
+
}
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// Validation Helpers
|
|
224
|
+
// ============================================================================
|
|
225
|
+
/**
|
|
226
|
+
* Check if CWT claims have expired.
|
|
227
|
+
*/
|
|
228
|
+
export function cwtIsExpired(claims, nowSeconds, clockSkewSeconds = 60) {
|
|
229
|
+
if (claims.exp === undefined)
|
|
230
|
+
return false;
|
|
231
|
+
const now = nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
232
|
+
return now > claims.exp + clockSkewSeconds;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Check if CWT claims are not yet valid (before nbf).
|
|
236
|
+
*/
|
|
237
|
+
export function cwtIsNotYetValid(claims, nowSeconds, clockSkewSeconds = 60) {
|
|
238
|
+
if (claims.nbf === undefined)
|
|
239
|
+
return false;
|
|
240
|
+
const now = nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
241
|
+
return now < claims.nbf - clockSkewSeconds;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if CWT claims match a required audience.
|
|
245
|
+
*/
|
|
246
|
+
export function cwtMatchesAudience(claims, requiredAudience) {
|
|
247
|
+
if (claims.aud === undefined)
|
|
248
|
+
return false;
|
|
249
|
+
if (typeof claims.aud === 'string')
|
|
250
|
+
return claims.aud === requiredAudience;
|
|
251
|
+
return claims.aud.includes(requiredAudience);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* CWT encoding/decoding error.
|
|
255
|
+
*/
|
|
256
|
+
export class CwtError extends Error {
|
|
257
|
+
constructor(message) {
|
|
258
|
+
super(message);
|
|
259
|
+
this.name = 'CwtError';
|
|
260
|
+
}
|
|
261
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CAT/C4M Token Library for MoQ Transport
|
|
3
|
+
*
|
|
4
|
+
* Common Access Token (CAT) implementation using CBOR, COSE_Sign1, and CWT.
|
|
5
|
+
* Provides standards-compliant token creation, decoding, and validation
|
|
6
|
+
* for MoQ Transport authorization.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import {
|
|
13
|
+
* CatTokenBuilder,
|
|
14
|
+
* CatTokenDecoder,
|
|
15
|
+
* CoseAlgorithm,
|
|
16
|
+
* MoqtAction,
|
|
17
|
+
* } from '@moq-web/cat';
|
|
18
|
+
*
|
|
19
|
+
* // Create a token
|
|
20
|
+
* const token = await new CatTokenBuilder()
|
|
21
|
+
* .issuer('https://auth.example.com')
|
|
22
|
+
* .subject('user-123')
|
|
23
|
+
* .audience('moq-relay')
|
|
24
|
+
* .expiration(Date.now() / 1000 + 3600)
|
|
25
|
+
* .issuedAt()
|
|
26
|
+
* .moqtScopes([{
|
|
27
|
+
* actions: [MoqtAction.Subscribe],
|
|
28
|
+
* namespaceMatch: ['room', 'abc'],
|
|
29
|
+
* }])
|
|
30
|
+
* .sign(privateKey);
|
|
31
|
+
*
|
|
32
|
+
* // Validate a token
|
|
33
|
+
* const result = await CatTokenDecoder.validate(token, publicKey);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export { C4M_TOKEN_TYPE, CoseAlgorithm, CoseHeaderParam, COSE_ALG_PARAMS, CwtClaimKey, MoqtAction, type CborValue, type CborTagged, type CoseSign1, type CwtClaims, type MoqtScope, type CatToken, type CatValidationResult, type CatValidationOptions, type CoseAlgParams, } from './types.js';
|
|
37
|
+
export { cborEncode, cborDecode, cborDecodeTagged, cborEncodeTagged, CborError, } from './cbor.js';
|
|
38
|
+
export { coseSign1Encode, coseSign1Decode, coseSign1Sign, coseSign1Verify, coseSign1SigStructure, coseMac0MacStructure, coseSign1GetAlgorithm, coseSign1DecodeProtectedHeader, CoseError, } from './cose.js';
|
|
39
|
+
export { cwtClaimsEncode, cwtClaimsDecode, cwtClaimsFromMap, moqtScopesEncode, moqtScopesDecode, cwtIsExpired, cwtIsNotYetValid, cwtMatchesAudience, CwtError, } from './cwt.js';
|
|
40
|
+
export { CatTokenBuilder, CatTokenDecoder, base64urlDecode, base64urlEncode, CatError, } from './cat.js';
|
|
41
|
+
export { generateTestKeyPair, generateTestCatToken, catTokenToBase64url, generateWrongScopeToken, generateExpiredToken, generateBadSignatureToken, generateWrongKeyToken, type TestCatTokenOptions, } from './test-utils.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CAT/C4M Token Library for MoQ Transport
|
|
5
|
+
*
|
|
6
|
+
* Common Access Token (CAT) implementation using CBOR, COSE_Sign1, and CWT.
|
|
7
|
+
* Provides standards-compliant token creation, decoding, and validation
|
|
8
|
+
* for MoQ Transport authorization.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import {
|
|
15
|
+
* CatTokenBuilder,
|
|
16
|
+
* CatTokenDecoder,
|
|
17
|
+
* CoseAlgorithm,
|
|
18
|
+
* MoqtAction,
|
|
19
|
+
* } from '@moq-web/cat';
|
|
20
|
+
*
|
|
21
|
+
* // Create a token
|
|
22
|
+
* const token = await new CatTokenBuilder()
|
|
23
|
+
* .issuer('https://auth.example.com')
|
|
24
|
+
* .subject('user-123')
|
|
25
|
+
* .audience('moq-relay')
|
|
26
|
+
* .expiration(Date.now() / 1000 + 3600)
|
|
27
|
+
* .issuedAt()
|
|
28
|
+
* .moqtScopes([{
|
|
29
|
+
* actions: [MoqtAction.Subscribe],
|
|
30
|
+
* namespaceMatch: ['room', 'abc'],
|
|
31
|
+
* }])
|
|
32
|
+
* .sign(privateKey);
|
|
33
|
+
*
|
|
34
|
+
* // Validate a token
|
|
35
|
+
* const result = await CatTokenDecoder.validate(token, publicKey);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
// Types
|
|
39
|
+
export { C4M_TOKEN_TYPE, CoseAlgorithm, CoseHeaderParam, COSE_ALG_PARAMS, CwtClaimKey, MoqtAction, } from './types.js';
|
|
40
|
+
// CBOR
|
|
41
|
+
export { cborEncode, cborDecode, cborDecodeTagged, cborEncodeTagged, CborError, } from './cbor.js';
|
|
42
|
+
// COSE
|
|
43
|
+
export { coseSign1Encode, coseSign1Decode, coseSign1Sign, coseSign1Verify, coseSign1SigStructure, coseMac0MacStructure, coseSign1GetAlgorithm, coseSign1DecodeProtectedHeader, CoseError, } from './cose.js';
|
|
44
|
+
// CWT
|
|
45
|
+
export { cwtClaimsEncode, cwtClaimsDecode, cwtClaimsFromMap, moqtScopesEncode, moqtScopesDecode, cwtIsExpired, cwtIsNotYetValid, cwtMatchesAudience, CwtError, } from './cwt.js';
|
|
46
|
+
// CAT
|
|
47
|
+
export { CatTokenBuilder, CatTokenDecoder, base64urlDecode, base64urlEncode, CatError, } from './cat.js';
|
|
48
|
+
// Test utilities
|
|
49
|
+
export { generateTestKeyPair, generateTestCatToken, catTokenToBase64url, generateWrongScopeToken, generateExpiredToken, generateBadSignatureToken, generateWrongKeyToken, } from './test-utils.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CoseAlgorithm, type CwtClaims, type MoqtScope } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a test ECDSA key pair for CAT token signing/verification.
|
|
4
|
+
*
|
|
5
|
+
* WARNING: Keys are extractable by default for testing convenience.
|
|
6
|
+
* Do NOT use this function for production key generation — use
|
|
7
|
+
* crypto.subtle.generateKey directly with extractable=false.
|
|
8
|
+
*
|
|
9
|
+
* @param algorithm - COSE algorithm (default: ES256)
|
|
10
|
+
* @param extractable - Whether the private key is extractable (default: true for testing)
|
|
11
|
+
* @returns WebCrypto key pair
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateTestKeyPair(algorithm?: CoseAlgorithm, extractable?: boolean): Promise<CryptoKeyPair>;
|
|
14
|
+
export interface TestCatTokenOptions {
|
|
15
|
+
/** Signing key pair (will generate if not provided) */
|
|
16
|
+
keyPair?: CryptoKeyPair;
|
|
17
|
+
/** Partial claims to set */
|
|
18
|
+
claims?: Partial<CwtClaims>;
|
|
19
|
+
/** MoQT scopes */
|
|
20
|
+
scopes?: MoqtScope[];
|
|
21
|
+
/** Signing algorithm (default: ES256) */
|
|
22
|
+
algorithm?: CoseAlgorithm;
|
|
23
|
+
/** Generate an expired token */
|
|
24
|
+
expired?: boolean;
|
|
25
|
+
/** Token lifetime in seconds (default: 3600) */
|
|
26
|
+
lifetimeSeconds?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate a properly-formatted test CAT token (CWT/COSE_Sign1).
|
|
30
|
+
* Unlike JWT fake tokens, these use proper CBOR encoding and ECDSA signatures.
|
|
31
|
+
*
|
|
32
|
+
* @returns COSE_Sign1 CBOR bytes
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateTestCatToken(options?: TestCatTokenOptions): Promise<{
|
|
35
|
+
tokenBytes: Uint8Array;
|
|
36
|
+
keyPair: CryptoKeyPair;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Encode CAT token bytes to base64url string for session.setAuthToken().
|
|
40
|
+
*/
|
|
41
|
+
export declare function catTokenToBase64url(tokenBytes: Uint8Array): string;
|
|
42
|
+
/**
|
|
43
|
+
* Generate an invalid CAT token with a wrong namespace scope.
|
|
44
|
+
*/
|
|
45
|
+
export declare function generateWrongScopeToken(_roomId: string, keyPair?: CryptoKeyPair): Promise<{
|
|
46
|
+
tokenBytes: Uint8Array;
|
|
47
|
+
keyPair: CryptoKeyPair;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Generate an expired CAT token.
|
|
51
|
+
*/
|
|
52
|
+
export declare function generateExpiredToken(keyPair?: CryptoKeyPair): Promise<{
|
|
53
|
+
tokenBytes: Uint8Array;
|
|
54
|
+
keyPair: CryptoKeyPair;
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Generate a CAT token with a corrupted signature.
|
|
58
|
+
*/
|
|
59
|
+
export declare function generateBadSignatureToken(keyPair?: CryptoKeyPair): Promise<{
|
|
60
|
+
tokenBytes: Uint8Array;
|
|
61
|
+
keyPair: CryptoKeyPair;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Generate a CAT token signed with a different key (signature won't verify
|
|
65
|
+
* against the original key pair).
|
|
66
|
+
*/
|
|
67
|
+
export declare function generateWrongKeyToken(algorithm?: CoseAlgorithm): Promise<{
|
|
68
|
+
tokenBytes: Uint8Array;
|
|
69
|
+
signingKeyPair: CryptoKeyPair;
|
|
70
|
+
verifyKeyPair: CryptoKeyPair;
|
|
71
|
+
}>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CAT Token Test Utilities
|
|
5
|
+
*
|
|
6
|
+
* Helpers for generating test key pairs and properly-formatted
|
|
7
|
+
* CAT (CWT/COSE) tokens for testing and demos.
|
|
8
|
+
*/
|
|
9
|
+
import { CatTokenBuilder, base64urlEncode } from './cat.js';
|
|
10
|
+
import { coseSign1Encode, coseSign1Decode } from './cose.js';
|
|
11
|
+
import { CoseAlgorithm, COSE_ALG_PARAMS, MoqtAction, } from './types.js';
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Key Generation
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Generate a test ECDSA key pair for CAT token signing/verification.
|
|
17
|
+
*
|
|
18
|
+
* WARNING: Keys are extractable by default for testing convenience.
|
|
19
|
+
* Do NOT use this function for production key generation — use
|
|
20
|
+
* crypto.subtle.generateKey directly with extractable=false.
|
|
21
|
+
*
|
|
22
|
+
* @param algorithm - COSE algorithm (default: ES256)
|
|
23
|
+
* @param extractable - Whether the private key is extractable (default: true for testing)
|
|
24
|
+
* @returns WebCrypto key pair
|
|
25
|
+
*/
|
|
26
|
+
export async function generateTestKeyPair(algorithm = CoseAlgorithm.ES256, extractable = true) {
|
|
27
|
+
const params = COSE_ALG_PARAMS[algorithm];
|
|
28
|
+
if (params.isMac) {
|
|
29
|
+
// HMAC key generation
|
|
30
|
+
const key = await crypto.subtle.generateKey({ name: 'HMAC', hash: params.hash }, extractable, ['sign', 'verify']);
|
|
31
|
+
// Return same key for both sign and verify (symmetric)
|
|
32
|
+
return { privateKey: key, publicKey: key };
|
|
33
|
+
}
|
|
34
|
+
return crypto.subtle.generateKey({ name: params.name, namedCurve: params.namedCurve }, extractable, ['sign', 'verify']);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generate a properly-formatted test CAT token (CWT/COSE_Sign1).
|
|
38
|
+
* Unlike JWT fake tokens, these use proper CBOR encoding and ECDSA signatures.
|
|
39
|
+
*
|
|
40
|
+
* @returns COSE_Sign1 CBOR bytes
|
|
41
|
+
*/
|
|
42
|
+
export async function generateTestCatToken(options = {}) {
|
|
43
|
+
const algorithm = options.algorithm ?? CoseAlgorithm.ES256;
|
|
44
|
+
const keyPair = options.keyPair ?? await generateTestKeyPair(algorithm);
|
|
45
|
+
const now = Math.floor(Date.now() / 1000);
|
|
46
|
+
const lifetime = options.lifetimeSeconds ?? 3600;
|
|
47
|
+
const builder = new CatTokenBuilder().withAlgorithm(algorithm);
|
|
48
|
+
// Set default claims
|
|
49
|
+
builder
|
|
50
|
+
.issuer(options.claims?.iss ?? 'https://test.moq.example')
|
|
51
|
+
.subject(options.claims?.sub ?? 'test-user')
|
|
52
|
+
.audience(options.claims?.aud ?? 'moq-relay');
|
|
53
|
+
if (options.expired) {
|
|
54
|
+
builder.issuedAt(now - lifetime * 2);
|
|
55
|
+
builder.expiration(now - lifetime);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
builder.issuedAt(options.claims?.iat ?? now);
|
|
59
|
+
builder.expiration(options.claims?.exp ?? now + lifetime);
|
|
60
|
+
}
|
|
61
|
+
if (options.claims?.nbf !== undefined) {
|
|
62
|
+
builder.notBefore(options.claims.nbf);
|
|
63
|
+
}
|
|
64
|
+
if (options.claims?.cti) {
|
|
65
|
+
builder.cwtId(options.claims.cti);
|
|
66
|
+
}
|
|
67
|
+
// Set scopes
|
|
68
|
+
const scopes = options.scopes ?? [{
|
|
69
|
+
actions: [MoqtAction.Subscribe, MoqtAction.Publish, MoqtAction.SubscribeNamespace, MoqtAction.PublishNamespace],
|
|
70
|
+
}];
|
|
71
|
+
builder.moqtScopes(scopes);
|
|
72
|
+
const tokenBytes = await builder.sign(keyPair.privateKey);
|
|
73
|
+
return { tokenBytes, keyPair };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Encode CAT token bytes to base64url string for session.setAuthToken().
|
|
77
|
+
*/
|
|
78
|
+
export function catTokenToBase64url(tokenBytes) {
|
|
79
|
+
return base64urlEncode(tokenBytes);
|
|
80
|
+
}
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Invalid Token Generators (for denial testing)
|
|
83
|
+
// ============================================================================
|
|
84
|
+
/**
|
|
85
|
+
* Generate an invalid CAT token with a wrong namespace scope.
|
|
86
|
+
*/
|
|
87
|
+
export async function generateWrongScopeToken(_roomId, keyPair) {
|
|
88
|
+
const kp = keyPair ?? await generateTestKeyPair();
|
|
89
|
+
return generateTestCatToken({
|
|
90
|
+
keyPair: kp,
|
|
91
|
+
scopes: [{
|
|
92
|
+
actions: [MoqtAction.Subscribe],
|
|
93
|
+
namespaceMatch: ['mocha', `WRONG-ROOM-${Date.now().toString(36)}`],
|
|
94
|
+
}],
|
|
95
|
+
claims: { sub: 'wrong-scope-user' },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generate an expired CAT token.
|
|
100
|
+
*/
|
|
101
|
+
export async function generateExpiredToken(keyPair) {
|
|
102
|
+
const kp = keyPair ?? await generateTestKeyPair();
|
|
103
|
+
return generateTestCatToken({
|
|
104
|
+
keyPair: kp,
|
|
105
|
+
expired: true,
|
|
106
|
+
claims: { sub: 'expired-user' },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Generate a CAT token with a corrupted signature.
|
|
111
|
+
*/
|
|
112
|
+
export async function generateBadSignatureToken(keyPair) {
|
|
113
|
+
const kp = keyPair ?? await generateTestKeyPair();
|
|
114
|
+
const { tokenBytes } = await generateTestCatToken({ keyPair: kp });
|
|
115
|
+
// Decode, corrupt signature, re-encode
|
|
116
|
+
const sign1 = coseSign1Decode(tokenBytes);
|
|
117
|
+
const corrupted = {
|
|
118
|
+
...sign1,
|
|
119
|
+
signature: new Uint8Array(sign1.signature.length),
|
|
120
|
+
};
|
|
121
|
+
crypto.getRandomValues(corrupted.signature);
|
|
122
|
+
return { tokenBytes: coseSign1Encode(corrupted), keyPair: kp };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Generate a CAT token signed with a different key (signature won't verify
|
|
126
|
+
* against the original key pair).
|
|
127
|
+
*/
|
|
128
|
+
export async function generateWrongKeyToken(algorithm = CoseAlgorithm.ES256) {
|
|
129
|
+
const signingKeyPair = await generateTestKeyPair(algorithm);
|
|
130
|
+
const verifyKeyPair = await generateTestKeyPair(algorithm);
|
|
131
|
+
const { tokenBytes } = await generateTestCatToken({
|
|
132
|
+
keyPair: signingKeyPair,
|
|
133
|
+
claims: { sub: 'wrong-key-user' },
|
|
134
|
+
});
|
|
135
|
+
return { tokenBytes, signingKeyPair, verifyKeyPair };
|
|
136
|
+
}
|