@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/cat.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { CoseAlgorithm, type CborValue, type MoqtScope, type CatToken, type CatValidationResult, type CatValidationOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decode a base64url string to bytes.
|
|
4
|
+
*
|
|
5
|
+
* Validates character set before decoding.
|
|
6
|
+
* Enforces maximum input length.
|
|
7
|
+
*/
|
|
8
|
+
export declare function base64urlDecode(str: string): Uint8Array;
|
|
9
|
+
/**
|
|
10
|
+
* Encode bytes to a base64url string (no padding).
|
|
11
|
+
*/
|
|
12
|
+
export declare function base64urlEncode(data: Uint8Array): string;
|
|
13
|
+
/**
|
|
14
|
+
* Fluent builder for creating CAT (C4M) tokens.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const token = await new CatTokenBuilder()
|
|
19
|
+
* .issuer('https://auth.example.com')
|
|
20
|
+
* .subject('user-123')
|
|
21
|
+
* .audience('moq-relay')
|
|
22
|
+
* .expiration(Date.now() / 1000 + 3600)
|
|
23
|
+
* .issuedAt()
|
|
24
|
+
* .moqtScopes([{
|
|
25
|
+
* actions: [MoqtAction.Subscribe, MoqtAction.Publish],
|
|
26
|
+
* namespaceMatch: ['room', 'abc'],
|
|
27
|
+
* }])
|
|
28
|
+
* .sign(privateKey);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare class CatTokenBuilder {
|
|
32
|
+
private _claims;
|
|
33
|
+
private _algorithm;
|
|
34
|
+
private _protectedHeaders;
|
|
35
|
+
private _unprotectedHeaders;
|
|
36
|
+
private _moqtClaimKey;
|
|
37
|
+
/** Set the issuer claim. */
|
|
38
|
+
issuer(iss: string): this;
|
|
39
|
+
/** Set the subject claim. */
|
|
40
|
+
subject(sub: string): this;
|
|
41
|
+
/** Set the audience claim. */
|
|
42
|
+
audience(aud: string | string[]): this;
|
|
43
|
+
/** Set the expiration time (Unix timestamp in seconds, or Date). */
|
|
44
|
+
expiration(exp: number | Date): this;
|
|
45
|
+
/** Set the not-before time (Unix timestamp in seconds, or Date). */
|
|
46
|
+
notBefore(nbf: number | Date): this;
|
|
47
|
+
/** Set the issued-at time. Defaults to current time if no argument given. */
|
|
48
|
+
issuedAt(iat?: number | Date): this;
|
|
49
|
+
/** Set the CWT ID (unique token identifier). */
|
|
50
|
+
cwtId(cti: Uint8Array): this;
|
|
51
|
+
/** Set the MoQT authorization scopes. */
|
|
52
|
+
moqtScopes(scopes: MoqtScope[]): this;
|
|
53
|
+
/** Set an additional claim by integer key. */
|
|
54
|
+
claim(key: number, value: CborValue): this;
|
|
55
|
+
/** Set the signing algorithm (default: ES256). */
|
|
56
|
+
withAlgorithm(alg: CoseAlgorithm): this;
|
|
57
|
+
/** Add a protected header parameter. */
|
|
58
|
+
protectedHeader(key: number, value: CborValue): this;
|
|
59
|
+
/** Add an unprotected header parameter. */
|
|
60
|
+
unprotectedHeader(key: number, value: CborValue): this;
|
|
61
|
+
/** Set the MoQT claim key (default: 327, alternative: 65000). */
|
|
62
|
+
moqtClaimKey(key: number): this;
|
|
63
|
+
/**
|
|
64
|
+
* Build the CWT claims payload (CBOR bytes) without signing.
|
|
65
|
+
* Useful for testing.
|
|
66
|
+
*/
|
|
67
|
+
buildPayload(): Uint8Array;
|
|
68
|
+
/**
|
|
69
|
+
* Sign the token and return COSE_Sign1 CBOR bytes.
|
|
70
|
+
*/
|
|
71
|
+
sign(privateKey: CryptoKey): Promise<Uint8Array>;
|
|
72
|
+
/**
|
|
73
|
+
* Sign the token and return a base64url-encoded string
|
|
74
|
+
* suitable for session.setAuthToken().
|
|
75
|
+
*/
|
|
76
|
+
signToBase64url(privateKey: CryptoKey): Promise<string>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Decoder and validator for CAT (C4M) tokens.
|
|
80
|
+
*/
|
|
81
|
+
export declare class CatTokenDecoder {
|
|
82
|
+
/**
|
|
83
|
+
* Decode a CAT token from raw COSE_Sign1 CBOR bytes.
|
|
84
|
+
* Does NOT verify the signature.
|
|
85
|
+
*
|
|
86
|
+
* Enforces maximum token size.
|
|
87
|
+
*
|
|
88
|
+
* @throws {CatError} If the token cannot be decoded
|
|
89
|
+
*/
|
|
90
|
+
static decode(data: Uint8Array): CatToken;
|
|
91
|
+
/**
|
|
92
|
+
* Decode a CAT token from a base64url-encoded string.
|
|
93
|
+
* Does NOT verify the signature.
|
|
94
|
+
*/
|
|
95
|
+
static decodeFromBase64url(encoded: string): CatToken;
|
|
96
|
+
/**
|
|
97
|
+
* Decode a CAT token from a legacy dot-separated format.
|
|
98
|
+
* The dot-separated format has 3 base64url parts: header.payload.signature.
|
|
99
|
+
* Each part is expected to contain CBOR data (NOT JSON — that would be JWT, not CWT).
|
|
100
|
+
*/
|
|
101
|
+
static decodeFromDotSeparated(dotToken: string): CatToken;
|
|
102
|
+
/**
|
|
103
|
+
* Decode from a COSE_Sign1 structure.
|
|
104
|
+
*
|
|
105
|
+
* No silent algorithm fallback — missing algorithm is an error.
|
|
106
|
+
*/
|
|
107
|
+
private static fromCoseSign1;
|
|
108
|
+
/**
|
|
109
|
+
* Fully validate a CAT token: decode, verify signature, check expiration/nbf/audience.
|
|
110
|
+
*
|
|
111
|
+
* Signature is verified FIRST, before any claims are inspected.
|
|
112
|
+
* Supports requiredAlgorithm to prevent algorithm confusion.
|
|
113
|
+
* Supports requireExp to reject tokens without expiration.
|
|
114
|
+
* Returns generic error messages — no internal details.
|
|
115
|
+
*/
|
|
116
|
+
static validate(data: Uint8Array, publicKey: CryptoKey, options?: CatValidationOptions): Promise<CatValidationResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Validate from a base64url-encoded string.
|
|
119
|
+
*/
|
|
120
|
+
static validateFromBase64url(encoded: string, publicKey: CryptoKey, options?: CatValidationOptions): Promise<CatValidationResult>;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* CAT token error.
|
|
124
|
+
*/
|
|
125
|
+
export declare class CatError extends Error {
|
|
126
|
+
constructor(message: string);
|
|
127
|
+
}
|
package/dist/cat.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright (c) 2025 Cisco Systems
|
|
2
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview CAT Token Builder and Decoder
|
|
5
|
+
*
|
|
6
|
+
* High-level API for creating, decoding, and validating
|
|
7
|
+
* Common Access Tokens (CAT/C4M) for MoQ Transport.
|
|
8
|
+
*/
|
|
9
|
+
import { coseSign1Sign, coseSign1Decode, coseSign1Encode, coseSign1Verify, coseSign1GetAlgorithm, coseSign1DecodeProtectedHeader, } from './cose.js';
|
|
10
|
+
import { cwtClaimsEncode, cwtClaimsDecode, cwtIsExpired, cwtIsNotYetValid, cwtMatchesAudience, moqtScopesEncode, } from './cwt.js';
|
|
11
|
+
import { CoseAlgorithm, CwtClaimKey, } from './types.js';
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Constants
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/** Maximum token size in bytes (8 KiB — generous for auth tokens) */
|
|
16
|
+
const MAX_TOKEN_SIZE = 8192;
|
|
17
|
+
/** Maximum base64url input length */
|
|
18
|
+
const MAX_BASE64URL_LENGTH = 12000; // ~8 KiB decoded
|
|
19
|
+
/** Valid base64url character set */
|
|
20
|
+
const BASE64URL_REGEX = /^[A-Za-z0-9_-]+$/;
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Base64url Utilities
|
|
23
|
+
// ============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Decode a base64url string to bytes.
|
|
26
|
+
*
|
|
27
|
+
* Validates character set before decoding.
|
|
28
|
+
* Enforces maximum input length.
|
|
29
|
+
*/
|
|
30
|
+
export function base64urlDecode(str) {
|
|
31
|
+
if (str.length > MAX_BASE64URL_LENGTH) {
|
|
32
|
+
throw new CatError(`base64url input too large: ${str.length} chars`);
|
|
33
|
+
}
|
|
34
|
+
if (str.length > 0 && !BASE64URL_REGEX.test(str)) {
|
|
35
|
+
throw new CatError('Invalid base64url characters');
|
|
36
|
+
}
|
|
37
|
+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
38
|
+
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
|
39
|
+
const binary = atob(base64 + padding);
|
|
40
|
+
const bytes = new Uint8Array(binary.length);
|
|
41
|
+
for (let i = 0; i < binary.length; i++)
|
|
42
|
+
bytes[i] = binary.charCodeAt(i);
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Encode bytes to a base64url string (no padding).
|
|
47
|
+
*/
|
|
48
|
+
export function base64urlEncode(data) {
|
|
49
|
+
let binary = '';
|
|
50
|
+
for (let i = 0; i < data.length; i++)
|
|
51
|
+
binary += String.fromCharCode(data[i]);
|
|
52
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
53
|
+
}
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// CatTokenBuilder
|
|
56
|
+
// ============================================================================
|
|
57
|
+
/**
|
|
58
|
+
* Fluent builder for creating CAT (C4M) tokens.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const token = await new CatTokenBuilder()
|
|
63
|
+
* .issuer('https://auth.example.com')
|
|
64
|
+
* .subject('user-123')
|
|
65
|
+
* .audience('moq-relay')
|
|
66
|
+
* .expiration(Date.now() / 1000 + 3600)
|
|
67
|
+
* .issuedAt()
|
|
68
|
+
* .moqtScopes([{
|
|
69
|
+
* actions: [MoqtAction.Subscribe, MoqtAction.Publish],
|
|
70
|
+
* namespaceMatch: ['room', 'abc'],
|
|
71
|
+
* }])
|
|
72
|
+
* .sign(privateKey);
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export class CatTokenBuilder {
|
|
76
|
+
_claims = {};
|
|
77
|
+
_algorithm = CoseAlgorithm.ES256;
|
|
78
|
+
_protectedHeaders = new Map();
|
|
79
|
+
_unprotectedHeaders = new Map();
|
|
80
|
+
_moqtClaimKey = CwtClaimKey.MOQT;
|
|
81
|
+
/** Set the issuer claim. */
|
|
82
|
+
issuer(iss) {
|
|
83
|
+
this._claims.iss = iss;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
/** Set the subject claim. */
|
|
87
|
+
subject(sub) {
|
|
88
|
+
this._claims.sub = sub;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
/** Set the audience claim. */
|
|
92
|
+
audience(aud) {
|
|
93
|
+
this._claims.aud = aud;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
/** Set the expiration time (Unix timestamp in seconds, or Date). */
|
|
97
|
+
expiration(exp) {
|
|
98
|
+
this._claims.exp = exp instanceof Date ? Math.floor(exp.getTime() / 1000) : Math.floor(exp);
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
/** Set the not-before time (Unix timestamp in seconds, or Date). */
|
|
102
|
+
notBefore(nbf) {
|
|
103
|
+
this._claims.nbf = nbf instanceof Date ? Math.floor(nbf.getTime() / 1000) : Math.floor(nbf);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
/** Set the issued-at time. Defaults to current time if no argument given. */
|
|
107
|
+
issuedAt(iat) {
|
|
108
|
+
if (iat === undefined) {
|
|
109
|
+
this._claims.iat = Math.floor(Date.now() / 1000);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
this._claims.iat = iat instanceof Date ? Math.floor(iat.getTime() / 1000) : Math.floor(iat);
|
|
113
|
+
}
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
/** Set the CWT ID (unique token identifier). */
|
|
117
|
+
cwtId(cti) {
|
|
118
|
+
this._claims.cti = cti;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
/** Set the MoQT authorization scopes. */
|
|
122
|
+
moqtScopes(scopes) {
|
|
123
|
+
this._claims.moqt = scopes;
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
/** Set an additional claim by integer key. */
|
|
127
|
+
claim(key, value) {
|
|
128
|
+
if (!this._claims.additionalClaims) {
|
|
129
|
+
this._claims.additionalClaims = new Map();
|
|
130
|
+
}
|
|
131
|
+
this._claims.additionalClaims.set(key, value);
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
/** Set the signing algorithm (default: ES256). */
|
|
135
|
+
withAlgorithm(alg) {
|
|
136
|
+
this._algorithm = alg;
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
/** Add a protected header parameter. */
|
|
140
|
+
protectedHeader(key, value) {
|
|
141
|
+
this._protectedHeaders.set(key, value);
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
/** Add an unprotected header parameter. */
|
|
145
|
+
unprotectedHeader(key, value) {
|
|
146
|
+
this._unprotectedHeaders.set(key, value);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
/** Set the MoQT claim key (default: 327, alternative: 65000). */
|
|
150
|
+
moqtClaimKey(key) {
|
|
151
|
+
this._moqtClaimKey = key;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build the CWT claims payload (CBOR bytes) without signing.
|
|
156
|
+
* Useful for testing.
|
|
157
|
+
*/
|
|
158
|
+
buildPayload() {
|
|
159
|
+
// If using a non-default MOQT claim key, move the moqt scopes
|
|
160
|
+
const claims = { ...this._claims };
|
|
161
|
+
if (this._moqtClaimKey !== CwtClaimKey.MOQT && claims.moqt) {
|
|
162
|
+
if (!claims.additionalClaims)
|
|
163
|
+
claims.additionalClaims = new Map();
|
|
164
|
+
claims.additionalClaims.set(this._moqtClaimKey, moqtScopesEncode(claims.moqt));
|
|
165
|
+
claims.moqt = undefined;
|
|
166
|
+
}
|
|
167
|
+
return cwtClaimsEncode(claims);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Sign the token and return COSE_Sign1 CBOR bytes.
|
|
171
|
+
*/
|
|
172
|
+
async sign(privateKey) {
|
|
173
|
+
const payload = cwtClaimsEncode(this._claims);
|
|
174
|
+
const sign1 = await coseSign1Sign(this._algorithm, this._protectedHeaders, payload, privateKey, this._unprotectedHeaders.size > 0 ? this._unprotectedHeaders : undefined);
|
|
175
|
+
return coseSign1Encode(sign1);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Sign the token and return a base64url-encoded string
|
|
179
|
+
* suitable for session.setAuthToken().
|
|
180
|
+
*/
|
|
181
|
+
async signToBase64url(privateKey) {
|
|
182
|
+
const bytes = await this.sign(privateKey);
|
|
183
|
+
return base64urlEncode(bytes);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// CatTokenDecoder
|
|
188
|
+
// ============================================================================
|
|
189
|
+
/**
|
|
190
|
+
* Decoder and validator for CAT (C4M) tokens.
|
|
191
|
+
*/
|
|
192
|
+
export class CatTokenDecoder {
|
|
193
|
+
/**
|
|
194
|
+
* Decode a CAT token from raw COSE_Sign1 CBOR bytes.
|
|
195
|
+
* Does NOT verify the signature.
|
|
196
|
+
*
|
|
197
|
+
* Enforces maximum token size.
|
|
198
|
+
*
|
|
199
|
+
* @throws {CatError} If the token cannot be decoded
|
|
200
|
+
*/
|
|
201
|
+
static decode(data) {
|
|
202
|
+
if (data.length > MAX_TOKEN_SIZE) {
|
|
203
|
+
throw new CatError('Token exceeds maximum size');
|
|
204
|
+
}
|
|
205
|
+
const sign1 = coseSign1Decode(data);
|
|
206
|
+
return CatTokenDecoder.fromCoseSign1(sign1);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Decode a CAT token from a base64url-encoded string.
|
|
210
|
+
* Does NOT verify the signature.
|
|
211
|
+
*/
|
|
212
|
+
static decodeFromBase64url(encoded) {
|
|
213
|
+
const data = base64urlDecode(encoded);
|
|
214
|
+
return CatTokenDecoder.decode(data);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Decode a CAT token from a legacy dot-separated format.
|
|
218
|
+
* The dot-separated format has 3 base64url parts: header.payload.signature.
|
|
219
|
+
* Each part is expected to contain CBOR data (NOT JSON — that would be JWT, not CWT).
|
|
220
|
+
*/
|
|
221
|
+
static decodeFromDotSeparated(dotToken) {
|
|
222
|
+
const parts = dotToken.split('.');
|
|
223
|
+
if (parts.length !== 3) {
|
|
224
|
+
throw new CatError('Dot-separated token must have exactly 3 parts');
|
|
225
|
+
}
|
|
226
|
+
const protectedHeader = base64urlDecode(parts[0]);
|
|
227
|
+
const payload = base64urlDecode(parts[1]);
|
|
228
|
+
const signature = base64urlDecode(parts[2]);
|
|
229
|
+
const sign1 = {
|
|
230
|
+
protectedHeader,
|
|
231
|
+
unprotectedHeader: new Map(),
|
|
232
|
+
payload,
|
|
233
|
+
signature,
|
|
234
|
+
};
|
|
235
|
+
return CatTokenDecoder.fromCoseSign1(sign1);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Decode from a COSE_Sign1 structure.
|
|
239
|
+
*
|
|
240
|
+
* No silent algorithm fallback — missing algorithm is an error.
|
|
241
|
+
*/
|
|
242
|
+
static fromCoseSign1(sign1) {
|
|
243
|
+
// Decode protected header — throws if empty or invalid
|
|
244
|
+
const header = coseSign1DecodeProtectedHeader(sign1.protectedHeader);
|
|
245
|
+
// Extract algorithm — throws if missing/unsupported, no silent default
|
|
246
|
+
const algorithm = coseSign1GetAlgorithm(sign1);
|
|
247
|
+
// Decode payload as CWT claims
|
|
248
|
+
let claims;
|
|
249
|
+
try {
|
|
250
|
+
claims = cwtClaimsDecode(sign1.payload);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
throw new CatError('Failed to decode CWT claims from token payload');
|
|
254
|
+
}
|
|
255
|
+
return { header, claims, coseSign1: sign1, algorithm };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Fully validate a CAT token: decode, verify signature, check expiration/nbf/audience.
|
|
259
|
+
*
|
|
260
|
+
* Signature is verified FIRST, before any claims are inspected.
|
|
261
|
+
* Supports requiredAlgorithm to prevent algorithm confusion.
|
|
262
|
+
* Supports requireExp to reject tokens without expiration.
|
|
263
|
+
* Returns generic error messages — no internal details.
|
|
264
|
+
*/
|
|
265
|
+
static async validate(data, publicKey, options) {
|
|
266
|
+
const clockSkew = options?.clockSkewSeconds ?? 60;
|
|
267
|
+
const now = options?.now ?? Math.floor(Date.now() / 1000);
|
|
268
|
+
// Step 1: Decode structure (but do NOT return claims to caller yet)
|
|
269
|
+
let token;
|
|
270
|
+
try {
|
|
271
|
+
token = CatTokenDecoder.decode(data);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// M3: Generic error — don't leak internal decode details
|
|
275
|
+
return { valid: false, error: 'Token decode failed' };
|
|
276
|
+
}
|
|
277
|
+
// Step 2: Verify signature FIRST — before inspecting any claims
|
|
278
|
+
let signatureValid;
|
|
279
|
+
try {
|
|
280
|
+
signatureValid = await coseSign1Verify(token.coseSign1, publicKey, options?.requiredAlgorithm);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return { valid: false, error: 'Signature verification failed' };
|
|
284
|
+
}
|
|
285
|
+
if (!signatureValid) {
|
|
286
|
+
// Do NOT return the token — claims are untrusted
|
|
287
|
+
return { valid: false, error: 'Invalid signature' };
|
|
288
|
+
}
|
|
289
|
+
// Signature is valid — now safe to inspect claims
|
|
290
|
+
// Step 3: Check that exp is present if required
|
|
291
|
+
if ((options?.requireExp ?? true) && token.claims.exp === undefined) {
|
|
292
|
+
return { valid: false, token, error: 'Token missing required exp claim' };
|
|
293
|
+
}
|
|
294
|
+
// Step 4: Check expiration
|
|
295
|
+
if (cwtIsExpired(token.claims, now, clockSkew)) {
|
|
296
|
+
return { valid: false, token, error: 'Token expired', expired: true };
|
|
297
|
+
}
|
|
298
|
+
// Step 5: Check not-before
|
|
299
|
+
if (cwtIsNotYetValid(token.claims, now, clockSkew)) {
|
|
300
|
+
return { valid: false, token, error: 'Token not yet valid' };
|
|
301
|
+
}
|
|
302
|
+
// Step 6: Check audience
|
|
303
|
+
if (options?.requiredAudience) {
|
|
304
|
+
if (!cwtMatchesAudience(token.claims, options.requiredAudience)) {
|
|
305
|
+
return { valid: false, token, error: 'Audience mismatch' };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { valid: true, token };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Validate from a base64url-encoded string.
|
|
312
|
+
*/
|
|
313
|
+
static async validateFromBase64url(encoded, publicKey, options) {
|
|
314
|
+
const data = base64urlDecode(encoded);
|
|
315
|
+
return CatTokenDecoder.validate(data, publicKey, options);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* CAT token error.
|
|
320
|
+
*/
|
|
321
|
+
export class CatError extends Error {
|
|
322
|
+
constructor(message) {
|
|
323
|
+
super(message);
|
|
324
|
+
this.name = 'CatError';
|
|
325
|
+
}
|
|
326
|
+
}
|
package/dist/cbor.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Minimal CBOR Encoder/Decoder
|
|
3
|
+
*
|
|
4
|
+
* Implements the subset of CBOR (RFC 8949) needed for COSE/CWT:
|
|
5
|
+
* - Major types 0-5, 6 (tags), 7 (simple values)
|
|
6
|
+
* - Definite-length only
|
|
7
|
+
* - Deterministic map key ordering for COSE Sig_structure
|
|
8
|
+
* - Duplicate map key rejection (RFC 8949 Section 5.6)
|
|
9
|
+
*
|
|
10
|
+
* Does NOT support: indefinite-length, floats, break codes.
|
|
11
|
+
*/
|
|
12
|
+
import type { CborValue } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Encode a CBOR value to bytes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function cborEncode(value: CborValue): Uint8Array;
|
|
17
|
+
/**
|
|
18
|
+
* Encode a CBOR tagged value.
|
|
19
|
+
*/
|
|
20
|
+
export declare function cborEncodeTagged(tag: number, value: CborValue): Uint8Array;
|
|
21
|
+
/**
|
|
22
|
+
* Decode a CBOR value from bytes.
|
|
23
|
+
*
|
|
24
|
+
* @param data - The CBOR bytes
|
|
25
|
+
* @param offset - Starting offset (default 0)
|
|
26
|
+
* @returns Decoded value and number of bytes consumed
|
|
27
|
+
*/
|
|
28
|
+
export declare function cborDecode(data: Uint8Array, offset?: number): {
|
|
29
|
+
value: CborValue;
|
|
30
|
+
bytesRead: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Decode a CBOR tagged value. If the data starts with a tag, returns the tag
|
|
34
|
+
* and inner value. Otherwise returns tag=-1 and the decoded value.
|
|
35
|
+
*/
|
|
36
|
+
export declare function cborDecodeTagged(data: Uint8Array, offset?: number): {
|
|
37
|
+
tag: number;
|
|
38
|
+
value: CborValue;
|
|
39
|
+
bytesRead: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* CBOR encoding/decoding error.
|
|
43
|
+
*/
|
|
44
|
+
export declare class CborError extends Error {
|
|
45
|
+
constructor(message: string);
|
|
46
|
+
}
|