Skip to content

Commit 554bb91

Browse files
authored
Merge pull request #50 from auth0-lab/verification_assessment_id
feat: provide a `VerificationAssessment.id` value for each verification check
2 parents aebf880 + e181db1 commit 554bb91

File tree

5 files changed

+86
-11
lines changed

5 files changed

+86
-11
lines changed

__tests__/verifier.tests.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ describe('verifier', () => {
3131
ephemeralReaderKey,
3232
encodedSessionTranscript,
3333
onCheck: (verification) => {
34-
if (verification.check.includes('Issuer certificate must be valid') &&
35-
verification.status === 'FAILED') {
34+
if (
35+
verification.id === 'ISSUER_CERTIFICATE_VALIDITY' &&
36+
verification.check.includes('Issuer certificate must be valid') &&
37+
verification.status === 'FAILED'
38+
) {
3639
called = true;
3740
}
3841
},

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export { Document } from './mdoc/model/Document';
77
export { IssuerSignedDocument } from './mdoc/model/IssuerSignedDocument';
88
export { DeviceSignedDocument } from './mdoc/model/DeviceSignedDocument';
99
export { DeviceResponse } from './mdoc/model/DeviceResponse';
10-
export { MDLError, MDLParseError } from './mdoc/errors'
10+
export { MDLError, MDLParseError } from './mdoc/errors';
11+
export { VerificationAssessmentId } from './mdoc/checkCallback';

src/mdoc/Verifier.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import {
1515
DiagnosticInformation,
1616
} from './model/types';
17-
import { UserDefinedVerificationCallback, VerificationAssessment, buildCallback, onCatCheck } from './checkCallback';
17+
import { UserDefinedVerificationCallback, VerificationAssessment, VerificationAssessmentId, buildCallback, onCatCheck } from './checkCallback';
1818

1919
import { parse } from './parser';
2020
import IssuerAuth from './model/IssuerAuth';
@@ -54,11 +54,13 @@ export class Verifier {
5454
onCheck({
5555
status: 'PASSED',
5656
check: 'Issuer certificate must be valid',
57+
id: VerificationAssessmentId.ISSUER_AUTH.IssuerCertificateValidity,
5758
});
5859
} catch (err) {
5960
onCheck({
6061
status: 'FAILED',
6162
check: 'Issuer certificate must be valid',
63+
id: VerificationAssessmentId.ISSUER_AUTH.IssuerCertificateValidity,
6264
reason: err.message,
6365
});
6466
}
@@ -68,6 +70,7 @@ export class Verifier {
6870
onCheck({
6971
status: verificationResult ? 'PASSED' : 'FAILED',
7072
check: 'Issuer signature must be valid',
73+
id: VerificationAssessmentId.ISSUER_AUTH.IssuerSignatureValidity,
7174
});
7275

7376
// Validity
@@ -77,18 +80,21 @@ export class Verifier {
7780
onCheck({
7881
status: certificate && validityInfo && (validityInfo.signed < certificate.notBefore || validityInfo.signed > certificate.notAfter) ? 'FAILED' : 'PASSED',
7982
check: 'The MSO signed date must be within the validity period of the certificate',
83+
id: VerificationAssessmentId.ISSUER_AUTH.MsoSignedDateWithinCertificateValidity,
8084
reason: `The MSO signed date (${validityInfo.signed.toUTCString()}) must be within the validity period of the certificate (${certificate.notBefore.toUTCString()} to ${certificate.notAfter.toUTCString()})`,
8185
});
8286

8387
onCheck({
8488
status: validityInfo && (now < validityInfo.validFrom || now > validityInfo.validUntil) ? 'FAILED' : 'PASSED',
8589
check: 'The MSO must be valid at the time of verification',
90+
id: VerificationAssessmentId.ISSUER_AUTH.MsoValidityAtVerificationTime,
8691
reason: `The MSO must be valid at the time of verification (${now.toUTCString()})`,
8792
});
8893

8994
onCheck({
9095
status: countryName ? 'PASSED' : 'FAILED',
9196
check: 'Country name (C) must be present in the issuer certificate\'s subject distinguished name',
97+
id: VerificationAssessmentId.ISSUER_AUTH.IssuerSubjectCountryNamePresence,
9298
});
9399
}
94100

@@ -106,6 +112,7 @@ export class Verifier {
106112
onCheck({
107113
status: 'FAILED',
108114
check: 'The document is not signed by the device.',
115+
id: VerificationAssessmentId.DEVICE_AUTH.DocumentDeviceSignaturePresence,
109116
});
110117
return;
111118
}
@@ -119,6 +126,7 @@ export class Verifier {
119126
onCheck({
120127
status: 'FAILED',
121128
check: 'Device Auth must contain a deviceSignature or deviceMac element',
129+
id: VerificationAssessmentId.DEVICE_AUTH.DeviceAuthSignatureOrMacPresence,
122130
});
123131
return;
124132
}
@@ -127,6 +135,7 @@ export class Verifier {
127135
onCheck({
128136
status: 'FAILED',
129137
check: 'Session Transcript Bytes missing from options, aborting device signature check',
138+
id: VerificationAssessmentId.DEVICE_AUTH.SessionTranscriptProvided,
130139
});
131140
return;
132141
}
@@ -141,6 +150,7 @@ export class Verifier {
141150
onCheck({
142151
status: 'FAILED',
143152
check: 'Issuer signature must contain the device key.',
153+
id: VerificationAssessmentId.DEVICE_AUTH.DeviceKeyAvailableInIssuerAuth,
144154
reason: 'Unable to verify deviceAuth signature: missing device key in issuerAuth',
145155
});
146156
return;
@@ -163,11 +173,13 @@ export class Verifier {
163173
onCheck({
164174
status: verificationResult ? 'PASSED' : 'FAILED',
165175
check: 'Device signature must be valid',
176+
id: VerificationAssessmentId.DEVICE_AUTH.DeviceSignatureValidity,
166177
});
167178
} catch (err) {
168179
onCheck({
169180
status: 'FAILED',
170181
check: 'Device signature must be valid',
182+
id: VerificationAssessmentId.DEVICE_AUTH.DeviceSignatureValidity,
171183
reason: `Unable to verify deviceAuth signature (ECDSA/EdDSA): ${err.message}`,
172184
});
173185
}
@@ -178,18 +190,21 @@ export class Verifier {
178190
onCheck({
179191
status: deviceAuth.deviceMac ? 'PASSED' : 'FAILED',
180192
check: 'Device MAC must be present when using MAC authentication',
193+
id: VerificationAssessmentId.DEVICE_AUTH.DeviceMacPresence,
181194
});
182195
if (!deviceAuth.deviceMac) { return; }
183196

184197
onCheck({
185198
status: deviceAuth.deviceMac.hasSupportedAlg() ? 'PASSED' : 'FAILED',
186199
check: 'Device MAC must use alg 5 (HMAC 256/256)',
200+
id: VerificationAssessmentId.DEVICE_AUTH.DeviceMacAlgorithmCorrectness,
187201
});
188202
if (!deviceAuth.deviceMac.hasSupportedAlg()) { return; }
189203

190204
onCheck({
191205
status: options.ephemeralPrivateKey ? 'PASSED' : 'FAILED',
192206
check: 'Ephemeral private key must be present when using MAC authentication',
207+
id: VerificationAssessmentId.DEVICE_AUTH.EphemeralKeyPresence,
193208
});
194209
if (!options.ephemeralPrivateKey) { return; }
195210

@@ -209,11 +224,13 @@ export class Verifier {
209224
onCheck({
210225
status: isValid ? 'PASSED' : 'FAILED',
211226
check: 'Device MAC must be valid',
227+
id: VerificationAssessmentId.DEVICE_AUTH.DeviceMacValidity,
212228
});
213229
} catch (err) {
214230
onCheck({
215231
status: 'FAILED',
216232
check: 'Device MAC must be valid',
233+
id: VerificationAssessmentId.DEVICE_AUTH.DeviceMacValidity,
217234
reason: `Unable to verify deviceAuth MAC: ${err.message}`,
218235
});
219236
}
@@ -231,6 +248,7 @@ export class Verifier {
231248
onCheck({
232249
status: digestAlgorithm && DIGEST_ALGS[digestAlgorithm] ? 'PASSED' : 'FAILED',
233250
check: 'Issuer Auth must include a supported digestAlgorithm element',
251+
id: VerificationAssessmentId.DATA_INTEGRITY.IssuerAuthDigestAlgorithmSupported,
234252
});
235253

236254
const nameSpaces = mdoc.issuerSigned.nameSpaces || {};
@@ -239,6 +257,7 @@ export class Verifier {
239257
onCheck({
240258
status: valueDigests.has(ns) ? 'PASSED' : 'FAILED',
241259
check: `Issuer Auth must include digests for namespace: ${ns}`,
260+
id: VerificationAssessmentId.DATA_INTEGRITY.IssuerAuthNamespaceDigestPresence,
242261
});
243262

244263
const verifications = await Promise.all(nameSpaces[ns].map(async (ev) => {
@@ -250,13 +269,15 @@ export class Verifier {
250269
onCheck({
251270
status: 'PASSED',
252271
check: `The calculated digest for ${ns}/${v.ev.elementIdentifier} attribute must match the digest in the issuerAuth element`,
272+
id: VerificationAssessmentId.DATA_INTEGRITY.AttributeDigestMatch,
253273
});
254274
});
255275

256276
verifications.filter((v) => !v.isValid).forEach((v) => {
257277
onCheck({
258278
status: 'FAILED',
259279
check: `The calculated digest for ${ns}/${v.ev.elementIdentifier} attribute must match the digest in the issuerAuth element`,
280+
id: VerificationAssessmentId.DATA_INTEGRITY.AttributeDigestMatch,
260281
});
261282
});
262283

@@ -266,6 +287,7 @@ export class Verifier {
266287
onCheck({
267288
status: 'FAILED',
268289
check: "The 'issuing_country' if present must match the 'countryName' in the subject field within the DS certificate",
290+
id: VerificationAssessmentId.DATA_INTEGRITY.IssuingCountryMatchesCertificate,
269291
reason: "The 'issuing_country' and 'issuing_jurisdiction' cannot be verified because the DS certificate was not provided",
270292
});
271293
} else {
@@ -275,6 +297,7 @@ export class Verifier {
275297
onCheck({
276298
status: invalidCountry ? 'FAILED' : 'PASSED',
277299
check: "The 'issuing_country' if present must match the 'countryName' in the subject field within the DS certificate",
300+
id: VerificationAssessmentId.DATA_INTEGRITY.IssuingCountryMatchesCertificate,
278301
reason: invalidCountry ?
279302
`The 'issuing_country' (${invalidCountry.ev.elementValue}) must match the 'countryName' (${issuerAuth.countryName}) in the subject field within the issuer certificate` :
280303
undefined,
@@ -286,6 +309,7 @@ export class Verifier {
286309
onCheck({
287310
status: invalidJurisdiction ? 'FAILED' : 'PASSED',
288311
check: "The 'issuing_jurisdiction' if present must match the 'stateOrProvinceName' in the subject field within the DS certificate",
312+
id: VerificationAssessmentId.DATA_INTEGRITY.IssuingJurisdictionMatchesCertificate,
289313
reason: invalidJurisdiction ?
290314
`The 'issuing_jurisdiction' (${invalidJurisdiction.ev.elementValue}) must match the 'stateOrProvinceName' (${issuerAuth.stateOrProvince}) in the subject field within the issuer certificate` :
291315
undefined,
@@ -318,18 +342,21 @@ export class Verifier {
318342
onCheck({
319343
status: dr.version ? 'PASSED' : 'FAILED',
320344
check: 'Device Response must include "version" element.',
345+
id: VerificationAssessmentId.DOCUMENT_FORMAT.DeviceResponseVersionPresence,
321346
category: 'DOCUMENT_FORMAT',
322347
});
323348

324349
onCheck({
325350
status: compareVersions(dr.version, '1.0') >= 0 ? 'PASSED' : 'FAILED',
326351
check: 'Device Response version must be 1.0 or greater',
352+
id: VerificationAssessmentId.DOCUMENT_FORMAT.DeviceResponseVersionSupported,
327353
category: 'DOCUMENT_FORMAT',
328354
});
329355

330356
onCheck({
331357
status: dr.documents && dr.documents.length > 0 ? 'PASSED' : 'FAILED',
332358
check: 'Device Response must include at least one document.',
359+
id: VerificationAssessmentId.DOCUMENT_FORMAT.DeviceResponseDocumentPresence,
333360
category: 'DOCUMENT_FORMAT',
334361
});
335362

@@ -359,6 +386,7 @@ export class Verifier {
359386
): Promise<DiagnosticInformation> {
360387
const dr: VerificationAssessment[] = [];
361388
const decoded = await this.verify(
389+
// @ts-ignore
362390
encodedDeviceResponse,
363391
{
364392
...options,

src/mdoc/checkCallback.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,60 @@ import { MDLError } from './errors';
33

44
const log = debug('mdl');
55

6+
export const VerificationAssessmentId = {
7+
ISSUER_AUTH: {
8+
IssuerCertificateValidity: 'ISSUER_CERTIFICATE_VALIDITY',
9+
IssuerSignatureValidity: 'ISSUER_SIGNATURE_VALIDITY',
10+
MsoSignedDateWithinCertificateValidity: 'MSO_SIGNED_DATE_WITHIN_CERTIFICATE_VALIDITY',
11+
MsoValidityAtVerificationTime: 'MSO_VALIDITY_AT_VERIFICATION_TIME',
12+
IssuerSubjectCountryNamePresence: 'ISSUER_SUBJECT_COUNTRY_NAME_PRESENCE',
13+
},
14+
15+
DEVICE_AUTH: {
16+
DocumentDeviceSignaturePresence: 'DOCUMENT_DEVICE_SIGNATURE_PRESENCE',
17+
DeviceAuthSignatureOrMacPresence: 'DEVICE_AUTH_SIGNATURE_OR_MAC_PRESENCE',
18+
SessionTranscriptProvided: 'SESSION_TRANSCRIPT_PROVIDED',
19+
DeviceKeyAvailableInIssuerAuth: 'DEVICE_KEY_AVAILABLE_IN_ISSUERAUTH',
20+
DeviceSignatureValidity: 'DEVICE_SIGNATURE_VALIDITY',
21+
DeviceMacPresence: 'DEVICE_MAC_PRESENCE',
22+
DeviceMacAlgorithmCorrectness: 'DEVICE_MAC_ALGORITHM_CORRECTNESS',
23+
EphemeralKeyPresence: 'EPHEMERAL_KEY_PRESENCE',
24+
DeviceMacValidity: 'DEVICE_MAC_VALIDITY',
25+
},
26+
27+
DATA_INTEGRITY: {
28+
IssuerAuthDigestAlgorithmSupported: 'ISSUER_AUTH_DIGEST_ALGORITHM_SUPPORTED',
29+
IssuerAuthNamespaceDigestPresence: 'ISSUER_AUTH_NAMESPACE_DIGEST_PRESENCE',
30+
AttributeDigestMatch: 'ATTRIBUTE_DIGEST_MATCH',
31+
IssuingCountryMatchesCertificate: 'ISSUING_COUNTRY_MATCHES_CERTIFICATE',
32+
IssuingJurisdictionMatchesCertificate: 'ISSUING_JURISDICTION_MATCHES_CERTIFICATE',
33+
},
34+
35+
DOCUMENT_FORMAT: {
36+
DeviceResponseVersionPresence: 'DEVICE_RESPONSE_VERSION_PRESENCE',
37+
DeviceResponseVersionSupported: 'DEVICE_RESPONSE_VERSION_SUPPORTED',
38+
DeviceResponseDocumentPresence: 'DEVICE_RESPONSE_DOCUMENT_PRESENCE',
39+
},
40+
} as const;
41+
642
export type VerificationAssessment = {
743
status: 'PASSED' | 'FAILED' | 'WARNING',
8-
category: 'DOCUMENT_FORMAT' | 'DEVICE_AUTH' | 'ISSUER_AUTH' | 'DATA_INTEGRITY',
944
check: string,
1045
reason?: string,
11-
};
46+
} & {
47+
[C in keyof typeof VerificationAssessmentId]: {
48+
category: C;
49+
id: typeof VerificationAssessmentId[C][keyof typeof VerificationAssessmentId[C]];
50+
};
51+
}[keyof typeof VerificationAssessmentId];
1252

1353
export type VerificationCallback = (item: VerificationAssessment) => void;
1454
export type UserDefinedVerificationCallback = (item: VerificationAssessment, original: VerificationCallback) => void;
1555

1656
export const defaultCallback: VerificationCallback = ((verification) => {
1757
log(`Verification: ${verification.check} => ${verification.status}`);
1858
if (verification.status !== 'FAILED') return;
19-
throw new MDLError(verification.reason ?? verification.check);
59+
throw new MDLError(verification.reason ?? verification.check, verification.id);
2060
});
2161

2262
export const buildCallback = (callback?: UserDefinedVerificationCallback): VerificationCallback => {
@@ -26,8 +66,8 @@ export const buildCallback = (callback?: UserDefinedVerificationCallback): Verif
2666
};
2767
};
2868

29-
export const onCatCheck = (onCheck: UserDefinedVerificationCallback, category: VerificationAssessment['category']) => {
30-
return (item: Omit<VerificationAssessment, 'category'>) => {
31-
onCheck({ ...item, category }, defaultCallback);
69+
export const onCatCheck = <C extends keyof typeof VerificationAssessmentId>(onCheck: UserDefinedVerificationCallback, category: C) => {
70+
return (item: Omit<Extract<VerificationAssessment, { category: C }>, 'category'>) => {
71+
onCheck({ ...item, category } as VerificationAssessment, defaultCallback);
3272
};
3373
};

src/mdoc/errors.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
export class MDLError extends Error {
2-
constructor(message?: string) {
2+
public code?: string;
3+
4+
constructor(message: string, code?: string) {
35
super(message);
46
this.name = new.target.name;
7+
this.code = code;
58
Object.setPrototypeOf(this, new.target.prototype);
69
}
710
}

0 commit comments

Comments
 (0)