File

src/verifier/presentations/credential/mdocverifier/mdocverifier.service.ts

Index

Properties
Methods

Constructor

constructor(chainValidation: CredentialChainValidationService)
Parameters :
Name Type Optional
chainValidation CredentialChainValidationService No

Methods

Private buildDeviceRequest
buildDeviceRequest(docType: string, claims: Record)

Build a device request based on the docType and received claims. This creates a request that matches what was received for verification.

Parameters :
Name Type Optional
docType string No
claims Record<string | unknown> No
Returns : DeviceRequest
Private Async extractErrorDetails
extractErrorDetails(vp: string, options: VerifierOptions)

Extract error details from the mDOC document for debugging.

Parameters :
Name Type Optional
vp string No
options VerifierOptions No
Private Async extractIssuerCertInfo
extractIssuerCertInfo(mdocDoc: any, details: MdocErrorDetails)

Extract issuer certificate information from the mDOC document.

Parameters :
Name Type Optional
mdocDoc any No
details MdocErrorDetails No
Returns : Promise<void>
Private Async handleVerificationError
handleVerificationError(vp: string, error: any, options: VerifierOptions)

Handle verification errors with detailed logging.

Parameters :
Name Type Optional
vp string No
error any No
options VerifierOptions No
Private Async summarizeTrustedCerts
summarizeTrustedCerts(trustListSource: VerifierOptions)

Summarize trusted certificates for error logging.

Parameters :
Name Type Optional
trustListSource VerifierOptions No
Returns : Promise<string>
Private Async validateIssuerCertificateChain
validateIssuerCertificateChain(mdocDocument: any, options: VerifierOptions)

Validates the issuer certificate chain from the mDOC's IssuerAuth. Extracts x5chain from the COSE Sign1 structure and validates it using the shared CredentialChainValidationService.

Parameters :
Name Type Optional
mdocDocument any No
options VerifierOptions No
Async verify
verify(vp: string, sessionData: MdocSessionData, options: VerifierOptions)

Verifies an mDOC credential.

Parameters :
Name Type Optional Description
vp string No

The base64url encoded device response

sessionData MdocSessionData No

Session data for transcript generation

options VerifierOptions No

Verification options including trust list

Verification result with claims

Properties

Private Readonly logger
Type : unknown
Default value : new Logger(MdocverifierService.name)
import {
    base64,
    DeviceRequest,
    DeviceResponse,
    DocRequest,
    hex,
    ItemsRequest,
    SessionTranscript,
    Verifier,
} from "@animo-id/mdoc";
import { Injectable, Logger } from "@nestjs/common";
import * as x509 from "@peculiar/x509";
import { VerifierOptions } from "../../../../shared/trust/types";
import { mdocContext } from "../../mdoc-context";
import {
    ChainValidationResult,
    CredentialChainValidationService,
} from "../credential-chain-validation.service";

export type MdocSessionData = {
    protocol: "openid4vp";
    nonce: string;
    responseMode: string;
    clientId: string;
    responseUri: string;
};

export type MdocVerificationResult = {
    verified: boolean;
    claims: Record<string, unknown>;
    payload: string;
    docType?: string;
};

/**
 * Error details extracted from an mDOC document for debugging.
 */
interface MdocErrorDetails {
    docType: string;
    issuerCertInfo: string;
    issuerThumbprint: string;
    issuerValidity: string;
    trustedCertsSummary: string;
}

@Injectable()
export class MdocverifierService {
    private readonly logger = new Logger(MdocverifierService.name);

    constructor(
        private readonly chainValidation: CredentialChainValidationService,
    ) {}

    /**
     * Verifies an mDOC credential.
     * @param vp The base64url encoded device response
     * @param sessionData Session data for transcript generation
     * @param options Verification options including trust list
     * @returns Verification result with claims
     */
    async verify(
        vp: string,
        sessionData: MdocSessionData,
        options: VerifierOptions,
    ): Promise<MdocVerificationResult> {
        try {
            // 1) Decode the device response
            const uint8Array = Buffer.from(vp, "base64url");
            const deviceResponse = DeviceResponse.decode(uint8Array);
            const mdocDocument = deviceResponse.documents?.[0];

            if (!mdocDocument) {
                throw new Error("mDOC document not found in device response");
            }

            // 2) Extract claims from the issuer signed data
            const issuerSigned = mdocDocument.issuerSigned;
            const docType = mdocDocument.docType;

            // Get claims from the appropriate namespace
            const namespace =
                docType === "org.iso.18013.5.1.mDL"
                    ? "org.iso.18013.5.1"
                    : docType;
            const claims = issuerSigned.getPrettyClaims(namespace) || {};

            // 3) Build the session transcript for verification
            const sessionTranscript = await SessionTranscript.forOid4Vp(
                sessionData,
                mdocContext,
            );

            // 4) Build a device request (currently requesting all claims that were received)
            const deviceRequest = this.buildDeviceRequest(docType, claims);

            // 5) Verify the device response (signature, device binding, etc.)
            // Certificate chain validation is disabled here - we do it separately via CredentialChainValidationService
            await Verifier.verifyDeviceResponse(
                {
                    deviceRequest,
                    deviceResponse,
                    sessionTranscript,
                    trustedCertificates: [],
                    disableCertificateChainValidation: true,
                },
                mdocContext,
            );

            // 6) Validate certificate chain using shared CredentialChainValidationService
            // This ensures consistent trust validation with SD-JWT-VC and other formats
            const chainResult = await this.validateIssuerCertificateChain(
                mdocDocument,
                options,
            );

            if (!chainResult.verified) {
                if (chainResult.errorDetails) {
                    this.logger.warn(
                        `Certificate chain validation failed: ${chainResult.errorDetails}`,
                    );
                }
                return {
                    verified: false,
                    claims,
                    payload: vp,
                    docType,
                };
            }

            this.logger.debug(
                `MDL device response verified successfully for docType: ${docType}`,
            );

            return {
                verified: true,
                claims,
                payload: vp,
                docType,
            };
        } catch (error: any) {
            return this.handleVerificationError(vp, error, options);
        }
    }

    /**
     * Validates the issuer certificate chain from the mDOC's IssuerAuth.
     * Extracts x5chain from the COSE Sign1 structure and validates it using
     * the shared CredentialChainValidationService.
     */
    private async validateIssuerCertificateChain(
        mdocDocument: any,
        options: VerifierOptions,
    ): Promise<ChainValidationResult> {
        // Extract x5chain from IssuerAuth (COSE Sign1 unprotected headers)
        const issuerAuth = mdocDocument?.issuerSigned?.issuerAuth;
        const x5chain: Uint8Array[] | undefined = issuerAuth?.x5chain;

        if (!x5chain || x5chain.length === 0) {
            // No x5c in the credential
            if (options.policy?.requireX5c) {
                return {
                    verified: false,
                    matchedEntity: null,
                    error: "x5c_required",
                    errorDetails:
                        "Policy requires x5c but none was found in IssuerAuth",
                };
            }
            // If x5c not required, skip trust validation
            return { verified: true, matchedEntity: null };
        }

        // Convert Uint8Array[] to base64 string[] for CredentialChainValidationService
        const x5cBase64 = x5chain.map((cert) => base64.encode(cert));

        return this.chainValidation.validateChain(
            x5cBase64,
            options.trustListSource,
            {
                requireX5c: options.policy?.requireX5c,
                pinnedCertMode: options.policy?.pinnedCertMode ?? "leaf",
                serviceTypeFilter: "/Issuance",
            },
        );
    }

    /**
     * Handle verification errors with detailed logging.
     */
    private async handleVerificationError(
        vp: string,
        error: any,
        options: VerifierOptions,
    ): Promise<MdocVerificationResult> {
        const configuredTrustLists =
            options.trustListSource?.lotes?.map((l) => l.url).join(", ") ||
            "none configured";

        const details = await this.extractErrorDetails(vp, options);

        const errorDetails = [
            `Error: ${error?.message ?? error}`,
            `DocType: ${details.docType}`,
            `Issuer cert: ${details.issuerCertInfo}`,
            `Issuer thumbprint: ${details.issuerThumbprint}`,
            `Issuer validity: ${details.issuerValidity}`,
            `Configured trust lists: ${configuredTrustLists}`,
            `Trusted certs: ${details.trustedCertsSummary}`,
        ].join(" | ");

        this.logger.error(`mDOC verification failed: ${errorDetails}`);

        return {
            verified: false,
            claims: {},
            payload: vp,
            docType:
                details.docType === "unknown" ? undefined : details.docType,
        };
    }

    /**
     * Extract error details from the mDOC document for debugging.
     */
    private async extractErrorDetails(
        vp: string,
        options: VerifierOptions,
    ): Promise<MdocErrorDetails> {
        const details: MdocErrorDetails = {
            docType: "unknown",
            issuerCertInfo: "unknown",
            issuerThumbprint: "unknown",
            issuerValidity: "unknown",
            trustedCertsSummary: "unknown",
        };

        try {
            const uint8Array = Buffer.from(vp, "base64url");
            const deviceResponse = DeviceResponse.decode(uint8Array);
            const mdocDoc = deviceResponse.documents?.[0];

            if (mdocDoc?.docType) {
                details.docType = mdocDoc.docType;
            }

            // Extract issuer certificate info from the MSO
            await this.extractIssuerCertInfo(mdocDoc, details);

            // Summarize trusted certificates
            details.trustedCertsSummary = await this.summarizeTrustedCerts(
                options.trustListSource,
            );
        } catch (parseError: any) {
            this.logger.debug(
                `Could not extract additional debug info: ${parseError?.message ?? parseError}`,
            );
        }

        return details;
    }

    /**
     * Extract issuer certificate information from the mDOC document.
     */
    private async extractIssuerCertInfo(
        mdocDoc: any,
        details: MdocErrorDetails,
    ): Promise<void> {
        const issuerAuth = mdocDoc?.issuerSigned?.issuerAuth;
        if (!issuerAuth) return;

        const x5chain = issuerAuth?.x5chain;
        if (!x5chain?.length) return;

        try {
            const leafCertBytes = x5chain[0];
            const leafCert = new x509.X509Certificate(leafCertBytes);
            const thumbprint = await leafCert.getThumbprint("SHA-256");

            details.issuerThumbprint = hex.encode(new Uint8Array(thumbprint));
            details.issuerValidity = `${leafCert.notBefore.toISOString()} - ${leafCert.notAfter.toISOString()}`;
            details.issuerCertInfo = `subject="${leafCert.subject}", issuer="${leafCert.issuer}"`;
        } catch {
            // Ignore certificate parsing errors
        }
    }

    /**
     * Summarize trusted certificates for error logging.
     */
    private async summarizeTrustedCerts(
        trustListSource: VerifierOptions["trustListSource"],
    ): Promise<string> {
        const trustedCerts =
            await this.chainValidation.getTrustedCertificateBuffers(
                trustListSource,
            );

        if (trustedCerts.length === 0) {
            return "none loaded";
        }

        const certSummaries: string[] = [];
        for (const certBuf of trustedCerts.slice(0, 5)) {
            try {
                const cert = new x509.X509Certificate(new Uint8Array(certBuf));
                const thumb = hex.encode(
                    new Uint8Array(await cert.getThumbprint("SHA-256")),
                );
                certSummaries.push(
                    `${cert.subject} (${thumb.substring(0, 16)}...)`,
                );
            } catch {
                // Skip invalid certs
            }
        }

        let summary =
            certSummaries.length > 0 ? certSummaries.join("; ") : "none valid";
        if (trustedCerts.length > 5) {
            summary += ` ...and ${trustedCerts.length - 5} more`;
        }

        return summary;
    }

    /**
     * Build a device request based on the docType and received claims.
     * This creates a request that matches what was received for verification.
     */
    private buildDeviceRequest(
        docType: string,
        claims: Record<string, unknown>,
    ): DeviceRequest {
        // Build namespace map from claims
        const namespaces: Record<string, Record<string, boolean>> = {};

        // For mDL, claims are typically under org.iso.18013.5.1
        const namespace =
            docType === "org.iso.18013.5.1.mDL" ? "org.iso.18013.5.1" : docType;

        if (Object.keys(claims).length > 0) {
            namespaces[namespace] = {};
            for (const claimKey of Object.keys(claims)) {
                namespaces[namespace][claimKey] = true;
            }
        }

        return DeviceRequest.create({
            docRequests: [
                DocRequest.create({
                    itemsRequest: ItemsRequest.create({
                        docType,
                        namespaces,
                    }),
                }),
            ],
        });
    }
}

results matching ""

    No results matching ""