/
attestationToken.ts
474 lines (422 loc) · 14.3 KB
/
attestationToken.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../jsrsasign.d.ts"/>
import * as jsrsasign from "jsrsasign";
import { JsonWebKey } from "../generated/models";
import { base64UrlDecodeString, hexToBase64 } from "../utils/base64";
import { AttestationSigningKey } from "./attestationSigningKey";
import { bytesToString } from "../utils/utf8";
import { AttestationSigner } from "./attestationSigner";
import * as Mappers from "../generated/models/mappers";
import { TypeDeserializer } from "../utils/typeDeserializer";
import { base64EncodeByteArray } from "../utils/base64";
/**
* Options used to validate attestation tokens.
*
* @typeparam issuer - if provided, specifies the expected issuer of the attestation token.
* @typeparam validateExpirationTime - if true, validate the expiration time in the token.
* @typeparam validateNotBeforeTime - if true, validate the "not before" time in the token.
* @typeparam validateToken - if true, validate the token.
* @typeparam timeValidationSlack - the validation time slack in the time based validations.
*
* @remarks
*
* If validateToken, validateNotBeforeTime, or validateExpirationTime are not
* provided, they are all assumed to be 'true'.
*
*/
export interface AttestationTokenValidationOptions {
/**
* If true, validate the attestation token, if false, skip validation.
*/
validateToken?: boolean;
/**
* If true, validate the expiration time for the token.
*/
validateExpirationTime?: boolean;
/**
* If true, validate the "not before" time for the token.
*/
validateNotBeforeTime?: boolean;
/**
* If true, validate the issuer of the token.
*/
validateIssuer?: boolean;
/**
* The expected issuer for the {@link AttestationToken}. Only checked if {@link validateIssuer} is set.
*/
expectedIssuer?: string;
/**
* Tolerance time (in seconds) used to accound for clock drift between the local machine
* and the server creating the token.
*/
timeValidationSlack?: number;
/**
* Validation Callback which allows customers to provide their own validation
* functionality for the attestation token. This can be used to validate
* the signing certificate in AttestationSigner.
*
* @remarks
*
* If there is a problem with token validation, the validaitonCallback is expected
* to throw an exception.
*/
validationCallback?: (token: AttestationToken, signer?: AttestationSigner) => void;
}
/**
*
* An AttestationToken represents an RFC 7515 JSON Web Signature object.
*
* It can represent either the token returned by the attestation service,
* or it can be used to create a token locally which can be used to verify
* attestation policy changes.
*/
export class AttestationToken {
/**
* @internal
*
* @param token - Attetation token returned by the attestation service.
*/
constructor(token: string) {
this._token = token;
const pieces = token.split(".");
if (pieces.length !== 3) {
throw Error("Incorrectly formatted token:");
}
this._headerBytes = base64UrlDecodeString(pieces[0]);
this._header = safeJsonParse(bytesToString(this._headerBytes));
this._bodyBytes = base64UrlDecodeString(pieces[1]);
this._body = safeJsonParse(bytesToString(this._bodyBytes));
// this._signature = base64UrlDecodeString(pieces[2]);
this._jwsVerifier = jsrsasign.KJUR.jws.JWS.parse(token);
}
private _token: string;
private _headerBytes: Uint8Array;
private _header: any;
private _bodyBytes: Uint8Array;
private _body: any;
// private _signature: Uint8Array;
private _jwsVerifier: any; // jsrsasign.KJUR.jws.JWS.JWSResult;
/**
* Returns the deserialized body of the AttestationToken object.
*
* @returns The body of the attestation token as an object.
*/
public getBody(): any {
return this._jwsVerifier.payloadObj;
}
/**
* the token to a string.
*
* @remarks
* Serializes the token to a string.
*
* @returns The token serialized to a RFC 7515 JSON Web Signature.
*/
public serialize(): string {
return this._token;
}
/**
* Validates the attestation token to verify that it is semantically correct.
*
* @param possibleSigners - the set of possible signers for this attestation token.
* @param options - validation options
*/
public validateToken(
possibleSigners?: AttestationSigner[],
options: AttestationTokenValidationOptions = {
validateExpirationTime: true,
validateToken: true,
validateNotBeforeTime: true
}
): void {
if (!options.validateToken) {
return;
}
let foundSigner: AttestationSigner | undefined = undefined;
if (this.algorithm !== "none") {
const signers = this.getCandidateSigners(possibleSigners);
signers.some((signer) => {
const cert = this.certFromSigner(signer);
// const pubKeyObj = cert.getPublicKey();
const isValid = jsrsasign.KJUR.jws.JWS.verify(this._token, cert);
if (isValid) {
foundSigner = signer;
return;
}
});
if (foundSigner === undefined) {
throw new Error("Attestation Token is not properly signed.");
}
}
// If the token has a body, check the expiration time and issuer.
if (this._body !== undefined) {
this.validateTimeProperties(options);
this.validateIssuer(options);
}
if (options.validationCallback !== undefined) {
// If there is a validation error, the validationCallback will throw a customer
// defined exception.
options.validationCallback(this, foundSigner);
}
}
private validateIssuer(options: AttestationTokenValidationOptions): void {
if (this.issuer && options.validateIssuer) {
if (this.issuer !== options.expectedIssuer) {
throw new Error(
"Found issuer: " + this.issuer + "; expected issuer: " + options.expectedIssuer
);
}
}
}
/**
* Validate the expiration and notbefore time claims in the JSON web token.
*
* @param options - Options to be used validating the time properties.
*/
private validateTimeProperties(options: AttestationTokenValidationOptions): void {
// Calculate the current time as a number of seconds since the start of the
// Unix epoch.
const timeNow = Math.floor(new Date().getTime() / 1000);
// Validate expiration time.
if (this.expirationTime !== undefined && options.validateExpirationTime) {
const expTime = this.expirationTime.getTime() / 1000;
if (timeNow > expTime) {
const delta = timeNow - expTime;
if (delta > (options.timeValidationSlack ?? 0)) {
throw new Error("AttestationToken has expired.");
}
}
}
// Validate not before time.
if (this.notBeforeTime !== undefined && options.validateNotBeforeTime) {
const nbfTime = this.notBeforeTime.getTime() / 1000;
if (nbfTime > timeNow) {
const delta = nbfTime - timeNow;
if (delta > (options.timeValidationSlack ?? 0)) {
throw new Error("AttestationToken is not yet valid.");
}
}
}
}
private certFromSigner(signer: AttestationSigner): string {
let pemCert: string;
// PEM encode the certificate.
pemCert = "-----BEGIN CERTIFICATE-----\r\n";
pemCert += base64EncodeByteArray(signer.certificates[0]);
pemCert += "\r\n-----END CERTIFICATE-----\r\n";
// const cert = new X509();
//
// cert.readCertPEM(pemCert);
return pemCert;
}
private getCandidateSigners(
possibleSigningCertificates?: AttestationSigner[]
): AttestationSigner[] {
const candidateSigners = new Array<AttestationSigner>();
const desiredKeyId = this.keyId;
if (desiredKeyId !== undefined && possibleSigningCertificates !== undefined) {
possibleSigningCertificates.forEach((possibleSigner) => {
if (possibleSigner.keyId === desiredKeyId) {
candidateSigners.push(possibleSigner);
}
});
// If we didn't find any candidate signers looking through the provided
// signing certificates, then maybe there's a certificate chain in the
// token itself that might be used to sign the token.
if (candidateSigners.length === 0) {
if (this.certificateChain !== undefined && this.certificateChain !== null) {
candidateSigners.push(this.certificateChain);
}
}
} else {
possibleSigningCertificates?.map((value) => candidateSigners.push(value));
if (this.certificateChain !== undefined) {
candidateSigners.push(this.certificateChain);
}
}
return candidateSigners;
}
/** ********* JSON WEB SIGNATURE (RFC 7515) PROPERTIES */
/**
* Returns the algorithm from the header of the JSON Web Signature.
*
* See {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.1 | RFC 7515 Section 4.1.1})
* for details.
*
* If the value of algorithm is "none" it indicates that the token is unsecured.
*/
public get algorithm(): string {
return this._header?.alg;
}
/**
* Json Web Signature Header "kid".
* See {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.4 | RFC 7515 Section 4.1.4})
* for details.
*/
public get keyId(): string | undefined {
return this._header.kid;
}
/**
* Json Web Signature Header "crit".
*
* See {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11 | RFC 7515 Section 4.1.11})
* for details.
*
*/
public get critical(): boolean | undefined {
return this._header.crit;
}
/**
* Json Web Token Header "content type".
* See {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 | RFC 7515 Section 4.1.10})
*
*/
public get contentType(): string | undefined {
return this._header.cty;
}
/**
* Json Web Token Header "key URL".
*
* @see {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.2 | RFC 7515 Section 4.1.2})
*
*/
public get keyUrl(): string | undefined {
return this._header.jku;
}
/**
* Json Web Token Header "X509 Url".
* @see {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.5 | RFC 7515 Section 4.1.5})
*
*/
public get x509Url(): string | undefined {
return this._header.x5u;
}
/** Json Web Token Header "Typ".
*
* @see {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.9 | RFC 7515 Section 4.1.9})
*
*/
public get type(): string | undefined {
return this._header.typ;
}
/**
* Json Web Token Header "x509 thumprint".
* See {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.7 | RFC 7515 Section 4.1.7})
*/
public get certificateThumbprint(): string | undefined {
return this._header.x5t;
}
/** Json Web Token Header "x509 SHA256 thumprint".
*
* See {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.8 | RFC 7515 Section 4.1.8})
*
*/
public get certificateSha256Thumbprint(): string | undefined {
return this._header["x5t#256"];
}
/** Json Web Token Header "x509 certificate chain".
*
* See {@link https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.6 | RFC 7515 Section 4.1.6})
*
*/
public get certificateChain(): AttestationSigner | undefined {
let jwk: JsonWebKey;
if (this._header.jwk !== undefined) {
jwk = TypeDeserializer.deserialize(
this._header.jwk,
[Mappers.JsonWebKey],
"JsonWebKey"
) as JsonWebKey;
} else {
jwk = TypeDeserializer.deserialize(
this._header,
{ JsonWebKey: Mappers.JsonWebKey },
"JsonWebKey"
) as JsonWebKey;
}
return new AttestationSigner(jwk);
}
/** ********* JSON WEB TOKEN (RFC 7519) PROPERTIES */
/** Issuer of the attestation token.
* See {@link https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6 | RFC 7519 Section 4.1.6})
* for details.
*/
public get issuer(): string | undefined {
return this._body.iss;
}
/** Expiration time for the token, from JWT body.
*
* See {@link https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4 | RFC 7519 Section 4.1.4})
* for details.
*/
public get expirationTime(): Date | undefined {
return this._body.exp ? new Date(this._body.exp * 1000) : undefined;
}
/** Issuance time for the token, from JWT body.
*
* See {@link https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6 | RFC 7519 Section 4.1.6})
* for details.
*/
public get issuedAtTime(): Date | undefined {
return this._body.iat ? new Date(this._body.iat * 1000) : undefined;
}
/**
* Not Before time for the token, from JWT body.
*
* See {@link https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5 | RFC 7519 Section 4.1.5})
* for details.
*/
public get notBeforeTime(): Date | undefined {
return this._body.nbf ? new Date(this._body.nbf * 1000) : undefined;
}
/**
* Creates a new attestation token from a body and signing key.
* @param body - stringified body of the body of the token to be created.
* @param signer - Optional signing key used to sign the newly created token.
* @returns an {@link AttestationToken | attestation token}
*/
public static create(params: {
body?: string;
signer?: AttestationSigningKey;
}): AttestationToken {
const header: {
alg: string;
[k: string]: any;
} = { alg: "none" };
if (params.signer) {
const x5c = new jsrsasign.X509();
x5c.readCertPEM(params.signer?.certificate);
const pubKey = x5c.getPublicKey();
if (pubKey instanceof jsrsasign.RSAKey) {
header.alg = "RS256";
} else if (pubKey instanceof jsrsasign.KJUR.crypto.ECDSA) {
header.alg = "ES256";
} else {
throw new Error("Unknown public key type: " + typeof pubKey);
}
header.x5c = [hexToBase64(x5c.hex)];
} else {
header.alg = "none";
}
const encodedToken = jsrsasign.KJUR.jws.JWS.sign(
header.alg,
header,
params.body ?? "",
params.signer?.key
);
return new AttestationToken(encodedToken);
}
}
function isObject(thing: any): boolean {
return Object.prototype.toString.call(thing) === "[object Object]";
}
function safeJsonParse(thing: any): any {
if (isObject(thing)) return thing;
try {
return JSON.parse(thing);
} catch (e) {
return undefined;
}
}