File

src/verifier/resolver/trust/x509-validation.service.ts

Index

Methods

Methods

Async buildPath
buildPath(leaf: x509.X509Certificate, presentedChain: x509.X509Certificate[], anchors: x509.X509Certificate[], extraIntermediates: x509.X509Certificate[])

Build a validated path from leaf using provided chain + anchors. Returns path leaf..anchor if successful.

Parameters :
Name Type Optional Default value
leaf x509.X509Certificate No
presentedChain x509.X509Certificate[] No
anchors x509.X509Certificate[] No
extraIntermediates x509.X509Certificate[] No []
Returns : Promise<x509.X509Certificate[]>
isCaCert
isCaCert(cert: x509.X509Certificate)

Determine whether anchor is a CA cert (basicConstraints CA=TRUE).

Parameters :
Name Type Optional
cert x509.X509Certificate No
Returns : boolean
isTimeValid
isTimeValid(cert: x509.X509Certificate, now: unknown)

Basic time validity check (you may rely on chain builder already doing this, but keeping it explicit is sometimes useful).

Parameters :
Name Type Optional Default value
cert x509.X509Certificate No
now unknown No new Date()
Returns : boolean
parseTrustAnchors
parseTrustAnchors(values: Array)
Parameters :
Name Type Optional
values Array<literal type> No
Returns : x509.X509Certificate[]
parseX5c
parseX5c(x5c: X5cInput)
Parameters :
Name Type Optional
x5c X5cInput No
Returns : x509.X509Certificate[]
Async pathMatchesTrustedEntities
pathMatchesTrustedEntities(path: x509.X509Certificate[], entities: TrustedEntity[], pinnedMode: "leaf" | "pathEnd")

Match a certificate chain against TrustedEntities. Returns the matched entity with its issuance and revocation certificates.

Policy:

  • Looks for issuance certificates (EaaIssuance service type) in each entity
  • If issuance cert is CA: require path terminates in that anchor
  • If issuance cert is not CA: treat as pin
    • pinnedMode "leaf": leaf equals pinned cert
    • pinnedMode "pathEnd": path end equals pinned cert
Parameters :
Name Type Optional Default value
path x509.X509Certificate[] No
entities TrustedEntity[] No
pinnedMode "leaf" | "pathEnd" No "leaf"

The matched entity info, or null if no match

import { Injectable } from "@nestjs/common";
import * as x509 from "@peculiar/x509";
import {
    getRevocationCert,
    ServiceTypeIdentifiers,
    TrustedEntity,
} from "./types";

type X5cInput = string[]; // base64 DER entries

function arrayBufferToHex(buffer: ArrayBuffer): string {
    return Array.from(new Uint8Array(buffer))
        .map((b) => b.toString(16).padStart(2, "0"))
        .join("");
}

function isPem(s: string): boolean {
    return s.includes("-----BEGIN CERTIFICATE-----");
}

function certFromValue(val: string | ArrayBuffer): x509.X509Certificate {
    // @peculiar/x509 accepts PEM string, base64 DER string, ArrayBuffer
    return new x509.X509Certificate(val as any);
}

/**
 * Information about a matched TrustedEntity from certificate chain validation.
 * Includes the matched issuance certificate and the associated revocation certificate
 * from the same entity.
 */
export type MatchedTrustedEntity = {
    /** The TrustedEntity that was matched */
    entity: TrustedEntity;
    /** The matched issuance certificate from this entity */
    issuanceCert: x509.X509Certificate;
    /** Thumbprint of the issuance certificate */
    issuanceThumbprint: string;
    /** Whether the issuance certificate is a CA certificate */
    issuanceIsCa: boolean;
    /** The mode that matched (ca, leaf-pinned, pathEnd-pinned) */
    matchMode: "ca" | "leaf-pinned" | "pathEnd-pinned";
    /** The revocation certificate from the same entity (if available) */
    revocationCert?: x509.X509Certificate;
    /** Thumbprint of the revocation certificate (if available) */
    revocationThumbprint?: string;
};

@Injectable()
export class X509ValidationService {
    parseX5c(x5c: X5cInput): x509.X509Certificate[] {
        return x5c.map((b64) => new x509.X509Certificate(b64));
    }

    parseTrustAnchors(
        values: Array<{ certValue: string }>,
    ): x509.X509Certificate[] {
        return values.map(({ certValue }) => {
            if (isPem(certValue)) return certFromValue(certValue);
            // could be base64 DER or something; try as-is
            return certFromValue(certValue);
        });
    }

    /**
     * Build a validated path from leaf using provided chain + anchors.
     * Returns path leaf..anchor if successful.
     */
    async buildPath(
        leaf: x509.X509Certificate,
        presentedChain: x509.X509Certificate[],
        anchors: x509.X509Certificate[],
        extraIntermediates: x509.X509Certificate[] = [],
    ): Promise<x509.X509Certificate[]> {
        const pool = [...presentedChain, ...extraIntermediates, ...anchors];
        const builder = new x509.X509ChainBuilder({ certificates: pool });
        return await builder.build(leaf);
    }

    /**
     * Determine whether anchor is a CA cert (basicConstraints CA=TRUE).
     */
    isCaCert(cert: x509.X509Certificate): boolean {
        // @peculiar/x509 provides basicConstraints extension parsing
        const bc = cert.getExtension("2.5.29.19"); // BasicConstraints
        // If missing, treat as not CA
        if (!bc) return false;
        // Peculiar returns parsed extension object with "ca" boolean for BasicConstraints
        return Boolean((bc as any).ca);
    }

    /**
     * Basic time validity check (you may rely on chain builder already doing this,
     * but keeping it explicit is sometimes useful).
     */
    isTimeValid(cert: x509.X509Certificate, now = new Date()): boolean {
        return cert.notBefore <= now && now <= cert.notAfter;
    }

    /**
     * Match a certificate chain against TrustedEntities.
     * Returns the matched entity with its issuance and revocation certificates.
     *
     * Policy:
     * - Looks for issuance certificates (EaaIssuance service type) in each entity
     * - If issuance cert is CA: require path terminates in that anchor
     * - If issuance cert is not CA: treat as pin
     *   - pinnedMode "leaf": leaf equals pinned cert
     *   - pinnedMode "pathEnd": path end equals pinned cert
     *
     * @returns The matched entity info, or null if no match
     */
    async pathMatchesTrustedEntities(
        path: x509.X509Certificate[],
        entities: TrustedEntity[],
        pinnedMode: "leaf" | "pathEnd" = "leaf",
    ): Promise<MatchedTrustedEntity | null> {
        const leaf = path[0];
        const end = path.at(-1)!;
        const leafThumb = arrayBufferToHex(await leaf.getThumbprint());
        const endThumb = arrayBufferToHex(await end.getThumbprint());

        for (const entity of entities) {
            // Find issuance certificates in this entity
            const issuanceServices = entity.services.filter(
                (s) =>
                    s.serviceTypeIdentifier ===
                    ServiceTypeIdentifiers.EaaIssuance,
            );

            for (const issuanceSvc of issuanceServices) {
                const issuanceCert = certFromValue(issuanceSvc.certValue);
                const issuanceThumb = arrayBufferToHex(
                    await issuanceCert.getThumbprint(),
                );
                const issuanceIsCa = this.isCaCert(issuanceCert);

                let matchMode: "ca" | "leaf-pinned" | "pathEnd-pinned" | null =
                    null;

                if (issuanceIsCa) {
                    // CA cert: path must terminate at this anchor
                    if (issuanceThumb === endThumb) {
                        matchMode = "ca";
                    }
                } else if (
                    pinnedMode === "leaf" &&
                    issuanceThumb === leafThumb
                ) {
                    // Pinned cert (non-CA) - leaf mode
                    matchMode = "leaf-pinned";
                } else if (
                    pinnedMode === "pathEnd" &&
                    issuanceThumb === endThumb
                ) {
                    // Pinned cert (non-CA) - pathEnd mode
                    matchMode = "pathEnd-pinned";
                }

                if (matchMode) {
                    // Found a match! Now get the revocation cert from the same entity
                    const revocationSvc = getRevocationCert(entity);
                    let revocationCert: x509.X509Certificate | undefined;
                    let revocationThumbprint: string | undefined;

                    if (revocationSvc) {
                        revocationCert = certFromValue(revocationSvc.certValue);
                        revocationThumbprint = arrayBufferToHex(
                            await revocationCert.getThumbprint(),
                        );
                    }

                    return {
                        entity,
                        issuanceCert,
                        issuanceThumbprint: issuanceThumb,
                        issuanceIsCa,
                        matchMode,
                        revocationCert,
                        revocationThumbprint,
                    };
                }
            }
        }

        return null;
    }
}

results matching ""

    No results matching ""