Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions __tests__/verifier.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ describe('verifier', () => {
ephemeralReaderKey,
encodedSessionTranscript,
onCheck: (verification) => {
if (verification.check.includes('Issuer certificate must be valid') &&
verification.status === 'FAILED') {
if (
verification.id === 'ISSUER_CERTIFICATE_VALIDITY' &&
verification.check.includes('Issuer certificate must be valid') &&
verification.status === 'FAILED'
) {
called = true;
}
},
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export { Document } from './mdoc/model/Document';
export { IssuerSignedDocument } from './mdoc/model/IssuerSignedDocument';
export { DeviceSignedDocument } from './mdoc/model/DeviceSignedDocument';
export { DeviceResponse } from './mdoc/model/DeviceResponse';
export { MDLError, MDLParseError } from './mdoc/errors'
export { MDLError, MDLParseError } from './mdoc/errors';
export { VerificationAssessmentId } from './mdoc/checkCallback';
30 changes: 29 additions & 1 deletion src/mdoc/Verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import {
DiagnosticInformation,
} from './model/types';
import { UserDefinedVerificationCallback, VerificationAssessment, buildCallback, onCatCheck } from './checkCallback';
import { UserDefinedVerificationCallback, VerificationAssessment, VerificationAssessmentId, buildCallback, onCatCheck } from './checkCallback';

import { parse } from './parser';
import IssuerAuth from './model/IssuerAuth';
Expand Down Expand Up @@ -54,11 +54,13 @@ export class Verifier {
onCheck({
status: 'PASSED',
check: 'Issuer certificate must be valid',
id: VerificationAssessmentId.ISSUER_AUTH.IssuerCertificateValidity,
});
} catch (err) {
onCheck({
status: 'FAILED',
check: 'Issuer certificate must be valid',
id: VerificationAssessmentId.ISSUER_AUTH.IssuerCertificateValidity,
reason: err.message,
});
}
Expand All @@ -68,6 +70,7 @@ export class Verifier {
onCheck({
status: verificationResult ? 'PASSED' : 'FAILED',
check: 'Issuer signature must be valid',
id: VerificationAssessmentId.ISSUER_AUTH.IssuerSignatureValidity,
});

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

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

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

Expand All @@ -106,6 +112,7 @@ export class Verifier {
onCheck({
status: 'FAILED',
check: 'The document is not signed by the device.',
id: VerificationAssessmentId.DEVICE_AUTH.DocumentDeviceSignaturePresence,
});
return;
}
Expand All @@ -119,6 +126,7 @@ export class Verifier {
onCheck({
status: 'FAILED',
check: 'Device Auth must contain a deviceSignature or deviceMac element',
id: VerificationAssessmentId.DEVICE_AUTH.DeviceAuthSignatureOrMacPresence,
});
return;
}
Expand All @@ -127,6 +135,7 @@ export class Verifier {
onCheck({
status: 'FAILED',
check: 'Session Transcript Bytes missing from options, aborting device signature check',
id: VerificationAssessmentId.DEVICE_AUTH.SessionTranscriptProvided,
});
return;
}
Expand All @@ -141,6 +150,7 @@ export class Verifier {
onCheck({
status: 'FAILED',
check: 'Issuer signature must contain the device key.',
id: VerificationAssessmentId.DEVICE_AUTH.DeviceKeyAvailableInIssuerAuth,
reason: 'Unable to verify deviceAuth signature: missing device key in issuerAuth',
});
return;
Expand All @@ -163,11 +173,13 @@ export class Verifier {
onCheck({
status: verificationResult ? 'PASSED' : 'FAILED',
check: 'Device signature must be valid',
id: VerificationAssessmentId.DEVICE_AUTH.DeviceSignatureValidity,
});
} catch (err) {
onCheck({
status: 'FAILED',
check: 'Device signature must be valid',
id: VerificationAssessmentId.DEVICE_AUTH.DeviceSignatureValidity,
reason: `Unable to verify deviceAuth signature (ECDSA/EdDSA): ${err.message}`,
});
}
Expand All @@ -178,18 +190,21 @@ export class Verifier {
onCheck({
status: deviceAuth.deviceMac ? 'PASSED' : 'FAILED',
check: 'Device MAC must be present when using MAC authentication',
id: VerificationAssessmentId.DEVICE_AUTH.DeviceMacPresence,
});
if (!deviceAuth.deviceMac) { return; }

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

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

Expand All @@ -209,11 +224,13 @@ export class Verifier {
onCheck({
status: isValid ? 'PASSED' : 'FAILED',
check: 'Device MAC must be valid',
id: VerificationAssessmentId.DEVICE_AUTH.DeviceMacValidity,
});
} catch (err) {
onCheck({
status: 'FAILED',
check: 'Device MAC must be valid',
id: VerificationAssessmentId.DEVICE_AUTH.DeviceMacValidity,
reason: `Unable to verify deviceAuth MAC: ${err.message}`,
});
}
Expand All @@ -231,6 +248,7 @@ export class Verifier {
onCheck({
status: digestAlgorithm && DIGEST_ALGS[digestAlgorithm] ? 'PASSED' : 'FAILED',
check: 'Issuer Auth must include a supported digestAlgorithm element',
id: VerificationAssessmentId.DATA_INTEGRITY.IssuerAuthDigestAlgorithmSupported,
});

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

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

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

Expand All @@ -266,6 +287,7 @@ export class Verifier {
onCheck({
status: 'FAILED',
check: "The 'issuing_country' if present must match the 'countryName' in the subject field within the DS certificate",
id: VerificationAssessmentId.DATA_INTEGRITY.IssuingCountryMatchesCertificate,
reason: "The 'issuing_country' and 'issuing_jurisdiction' cannot be verified because the DS certificate was not provided",
});
} else {
Expand All @@ -275,6 +297,7 @@ export class Verifier {
onCheck({
status: invalidCountry ? 'FAILED' : 'PASSED',
check: "The 'issuing_country' if present must match the 'countryName' in the subject field within the DS certificate",
id: VerificationAssessmentId.DATA_INTEGRITY.IssuingCountryMatchesCertificate,
reason: invalidCountry ?
`The 'issuing_country' (${invalidCountry.ev.elementValue}) must match the 'countryName' (${issuerAuth.countryName}) in the subject field within the issuer certificate` :
undefined,
Expand All @@ -286,6 +309,7 @@ export class Verifier {
onCheck({
status: invalidJurisdiction ? 'FAILED' : 'PASSED',
check: "The 'issuing_jurisdiction' if present must match the 'stateOrProvinceName' in the subject field within the DS certificate",
id: VerificationAssessmentId.DATA_INTEGRITY.IssuingJurisdictionMatchesCertificate,
reason: invalidJurisdiction ?
`The 'issuing_jurisdiction' (${invalidJurisdiction.ev.elementValue}) must match the 'stateOrProvinceName' (${issuerAuth.stateOrProvince}) in the subject field within the issuer certificate` :
undefined,
Expand Down Expand Up @@ -318,18 +342,21 @@ export class Verifier {
onCheck({
status: dr.version ? 'PASSED' : 'FAILED',
check: 'Device Response must include "version" element.',
id: VerificationAssessmentId.DOCUMENT_FORMAT.DeviceResponseVersionPresence,
category: 'DOCUMENT_FORMAT',
});

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

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

Expand Down Expand Up @@ -359,6 +386,7 @@ export class Verifier {
): Promise<DiagnosticInformation> {
const dr: VerificationAssessment[] = [];
const decoded = await this.verify(
// @ts-ignore
encodedDeviceResponse,
{
...options,
Expand Down
52 changes: 46 additions & 6 deletions src/mdoc/checkCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,60 @@ import { MDLError } from './errors';

const log = debug('mdl');

export const VerificationAssessmentId = {
ISSUER_AUTH: {
IssuerCertificateValidity: 'ISSUER_CERTIFICATE_VALIDITY',
IssuerSignatureValidity: 'ISSUER_SIGNATURE_VALIDITY',
MsoSignedDateWithinCertificateValidity: 'MSO_SIGNED_DATE_WITHIN_CERTIFICATE_VALIDITY',
MsoValidityAtVerificationTime: 'MSO_VALIDITY_AT_VERIFICATION_TIME',
IssuerSubjectCountryNamePresence: 'ISSUER_SUBJECT_COUNTRY_NAME_PRESENCE',
},

DEVICE_AUTH: {
DocumentDeviceSignaturePresence: 'DOCUMENT_DEVICE_SIGNATURE_PRESENCE',
DeviceAuthSignatureOrMacPresence: 'DEVICE_AUTH_SIGNATURE_OR_MAC_PRESENCE',
SessionTranscriptProvided: 'SESSION_TRANSCRIPT_PROVIDED',
DeviceKeyAvailableInIssuerAuth: 'DEVICE_KEY_AVAILABLE_IN_ISSUERAUTH',
DeviceSignatureValidity: 'DEVICE_SIGNATURE_VALIDITY',
DeviceMacPresence: 'DEVICE_MAC_PRESENCE',
DeviceMacAlgorithmCorrectness: 'DEVICE_MAC_ALGORITHM_CORRECTNESS',
EphemeralKeyPresence: 'EPHEMERAL_KEY_PRESENCE',
DeviceMacValidity: 'DEVICE_MAC_VALIDITY',
},

DATA_INTEGRITY: {
IssuerAuthDigestAlgorithmSupported: 'ISSUER_AUTH_DIGEST_ALGORITHM_SUPPORTED',
IssuerAuthNamespaceDigestPresence: 'ISSUER_AUTH_NAMESPACE_DIGEST_PRESENCE',
AttributeDigestMatch: 'ATTRIBUTE_DIGEST_MATCH',
IssuingCountryMatchesCertificate: 'ISSUING_COUNTRY_MATCHES_CERTIFICATE',
IssuingJurisdictionMatchesCertificate: 'ISSUING_JURISDICTION_MATCHES_CERTIFICATE',
},

DOCUMENT_FORMAT: {
DeviceResponseVersionPresence: 'DEVICE_RESPONSE_VERSION_PRESENCE',
DeviceResponseVersionSupported: 'DEVICE_RESPONSE_VERSION_SUPPORTED',
DeviceResponseDocumentPresence: 'DEVICE_RESPONSE_DOCUMENT_PRESENCE',
},
} as const;

export type VerificationAssessment = {
status: 'PASSED' | 'FAILED' | 'WARNING',
category: 'DOCUMENT_FORMAT' | 'DEVICE_AUTH' | 'ISSUER_AUTH' | 'DATA_INTEGRITY',
check: string,
reason?: string,
};
} & {
[C in keyof typeof VerificationAssessmentId]: {
category: C;
id: typeof VerificationAssessmentId[C][keyof typeof VerificationAssessmentId[C]];
};
}[keyof typeof VerificationAssessmentId];

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

export const defaultCallback: VerificationCallback = ((verification) => {
log(`Verification: ${verification.check} => ${verification.status}`);
if (verification.status !== 'FAILED') return;
throw new MDLError(verification.reason ?? verification.check);
throw new MDLError(verification.reason ?? verification.check, verification.id);
});

export const buildCallback = (callback?: UserDefinedVerificationCallback): VerificationCallback => {
Expand All @@ -26,8 +66,8 @@ export const buildCallback = (callback?: UserDefinedVerificationCallback): Verif
};
};

export const onCatCheck = (onCheck: UserDefinedVerificationCallback, category: VerificationAssessment['category']) => {
return (item: Omit<VerificationAssessment, 'category'>) => {
onCheck({ ...item, category }, defaultCallback);
export const onCatCheck = <C extends keyof typeof VerificationAssessmentId>(onCheck: UserDefinedVerificationCallback, category: C) => {
return (item: Omit<Extract<VerificationAssessment, { category: C }>, 'category'>) => {
onCheck({ ...item, category } as VerificationAssessment, defaultCallback);
};
};
5 changes: 4 additions & 1 deletion src/mdoc/errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export class MDLError extends Error {
constructor(message?: string) {
public code?: string;

constructor(message: string, code?: string) {
super(message);
this.name = new.target.name;
this.code = code;
Object.setPrototypeOf(this, new.target.prototype);
}
}
Expand Down