File

src/issuer/trust-list/trustlist.service.ts

Index

Properties
Methods

Constructor

constructor(trustListRepo: Repository<TrustList>, trustListVersionRepo: Repository, keyService: KeyService, certService: CertService, configImportService: ConfigImportService, tenantRepository: Repository<TenantEntity>, configImportOrchestrator: ConfigImportOrchestratorService)
Parameters :
Name Type Optional
trustListRepo Repository<TrustList> No
trustListVersionRepo Repository<TrustListVersion> No
keyService KeyService No
certService CertService No
configImportService ConfigImportService No
tenantRepository Repository<TenantEntity> No
configImportOrchestrator ConfigImportOrchestratorService No

Methods

Private Async buildAndSaveTrustList
buildAndSaveTrustList(config: TrustListCreateDto, tenant: TenantEntity, existing?: TrustList)

Shared logic for creating and saving a trust list (used by both API and import)

Parameters :
Name Type Optional Description
config TrustListCreateDto No

The configuration for the trust list

tenant TenantEntity No

The tenant entity

existing TrustList Yes

Optional existing trust list to update

Returns : Promise<TrustList>
create
create(values: TrustListCreateDto, tenant: TenantEntity)

Create a new trust list

Parameters :
Name Type Optional
values TrustListCreateDto No
tenant TenantEntity No
Returns : Promise<TrustList>
Private createEntityFromCert
createEntityFromCert(issuerCert: CertEntity, revocationCert: CertEntity, info: TrustListEntityInfo)
Parameters :
Name Type Optional
issuerCert CertEntity No
revocationCert CertEntity No
info TrustListEntityInfo No
Returns : TrustedEntity
Private createEntityFromData
createEntityFromData(issuerCertBase64: string, revocationCertBase64: string, info: TrustListEntityInfo)
Parameters :
Name Type Optional
issuerCertBase64 string No
revocationCertBase64 string No
info TrustListEntityInfo No
Returns : TrustedEntity
Private createEntityFromPem
createEntityFromPem(issuerCertPem: string, revocationCertPem: string, info: TrustListEntityInfo)
Parameters :
Name Type Optional
issuerCertPem string No
revocationCertPem string No
info TrustListEntityInfo No
Returns : TrustedEntity
createList
createList(tenant: TenantEntity, list: TrustedEntity[], sequenceNumber: number)
Parameters :
Name Type Optional Default value
tenant TenantEntity No
list TrustedEntity[] No
sequenceNumber number No 1
Returns : LoTE
Async exportTrustList
exportTrustList(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
findAll
findAll(tenant: TenantEntity)

Finds all trust lists for the tenant

Parameters :
Name Type Optional
tenant TenantEntity No
findOne
findOne(tenantId: string, id: string)

Find one trust list by tenantId and id

Parameters :
Name Type Optional
tenantId string No
id string No
Returns : Promise<TrustList>
formatCertEntity
formatCertEntity(cert: CertEntity)

Format CertEntity to base64 DER without PEM headers

Parameters :
Name Type Optional
cert CertEntity No
Returns : string
formatPem
formatPem(pem: string)

Format PEM string to base64 DER without PEM headers

Parameters :
Name Type Optional
pem string No
Returns : string
Async generateJwt
generateJwt(trustList: TrustList)

Generate a signed JWT for the trust list

Parameters :
Name Type Optional
trustList TrustList No
Returns : Promise<string>
getJwt
getJwt(tenantId: string, id: string)

Get the JWT of the trust list

Parameters :
Name Type Optional
tenantId string No
id string No
Returns : Promise<string>
getVersion
getVersion(tenantId: string, trustListId: string, versionId: string)

Get a specific version of a trust list

Parameters :
Name Type Optional
tenantId string No
trustListId string No
versionId string No
Returns : Promise<TrustListVersion>
getVersionHistory
getVersionHistory(tenantId: string, trustListId: string)

Get version history for a trust list

Parameters :
Name Type Optional
tenantId string No
trustListId string No
Returns : Promise<TrustListVersion[]>
Async importForTenant
importForTenant(tenantId: string)

Imports trust lists for a specific tenant from the file system.

Parameters :
Name Type Optional
tenantId string No
Returns : any
Async remove
remove(tenantId: string, id: string)

Remove a trust list

Parameters :
Name Type Optional
tenantId string No
id string No
Returns : Promise<void>
Private saveVersion
saveVersion(trustList: TrustList)

Save the current state of a trust list as a version for audit

Parameters :
Name Type Optional
trustList TrustList No
Returns : Promise<TrustListVersion>
Async update
update(tenantId: string, id: string, values: TrustListCreateDto)

Update a trust list with new entities Increments the sequence number and stores a version for audit

Parameters :
Name Type Optional
tenantId string No
id string No
values TrustListCreateDto No
Returns : Promise<TrustList>
Private validatePem
validatePem(pem: string, fieldName: string)

Validate that a string is a valid PEM certificate

Parameters :
Name Type Optional Description
pem string No

The PEM string to validate

fieldName string No

The field name for error messages

Returns : void

Properties

Public Readonly keyService
Type : KeyService
Decorators :
@Inject('KeyService')
import { X509Certificate } from "node:crypto";
import { readFileSync } from "node:fs";
import {
    BadRequestException,
    Inject,
    Injectable,
    OnApplicationBootstrap,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { plainToClass } from "class-transformer";
import { JWTHeaderParameters } from "jose";
import { Repository } from "typeorm";
import { v4 } from "uuid";
import { TenantEntity } from "../../auth/tenant/entitites/tenant.entity";
import { CertService } from "../../crypto/key/cert/cert.service";
import { CertEntity } from "../../crypto/key/entities/cert.entity";
import { CertUsage } from "../../crypto/key/entities/cert-usage.entity";
import { KeyService } from "../../crypto/key/key.service";
import { ConfigImportService } from "../../shared/utils/config-import/config-import.service";
import {
    ConfigImportOrchestratorService,
    ImportPhase,
} from "../../shared/utils/config-import/config-import-orchestrator.service";
import {
    TrustListCreateDto,
    TrustListEntityInfo,
} from "./dto/trust-list-create.dto";
import { LoTE, TrustedEntitiesList, TrustedEntity } from "./dto/types";
import { TrustList } from "./entities/trust-list.entity";
import { TrustListVersion } from "./entities/trust-list-version.entity";

export enum ServiceTypeIdentifier {
    EaaIssuance = "http://uri.etsi.org/19602/SvcType/EAA/Issuance",
    EaaRevocation = "http://uri.etsi.org/19602/SvcType/EAA/Revocation",
}

/** Default language for trust list entries */
const DEFAULT_LANG = "en";

@Injectable()
export class TrustListService {
    constructor(
        @InjectRepository(TrustList)
        private readonly trustListRepo: Repository<TrustList>,
        @InjectRepository(TrustListVersion)
        private readonly trustListVersionRepo: Repository<TrustListVersion>,
        @Inject("KeyService") public readonly keyService: KeyService,
        private readonly certService: CertService,
        private readonly configImportService: ConfigImportService,
        @InjectRepository(TenantEntity)
        private readonly tenantRepository: Repository<TenantEntity>,
        configImportOrchestrator: ConfigImportOrchestratorService,
    ) {
        configImportOrchestrator.register(
            "status-lists",
            ImportPhase.FINAL,
            (tenantId) => this.importForTenant(tenantId),
        );
    }

    /**
     * Create a new trust list
     * @param values
     * @param tenant
     * @returns
     */
    create(
        values: TrustListCreateDto,
        tenant: TenantEntity,
    ): Promise<TrustList> {
        return this.buildAndSaveTrustList(values, tenant);
    }

    /**
     * Finds all trust lists for the tenant
     * @param tenant
     * @returns
     */
    findAll(tenant: TenantEntity): Promise<TrustList[]> {
        return this.trustListRepo.findBy({ tenantId: tenant.id });
    }

    /**
     * Find one trust list by tenantId and id
     * @param tenantId
     * @param id
     * @returns
     */
    findOne(tenantId: string, id: string): Promise<TrustList> {
        return this.trustListRepo.findOneByOrFail({ tenantId, id });
    }

    async exportTrustList(
        tenantId: string,
        id: string,
    ): Promise<TrustListCreateDto> {
        const entry = await this.findOne(tenantId, id);
        return {
            id: entry.id,
            description: entry.description,
            certId: entry.certId,
            entities: entry.entityConfig ?? [],
            data: entry.data,
        };
    }

    /**
     * Update a trust list with new entities
     * Increments the sequence number and stores a version for audit
     * @param tenantId
     * @param id
     * @param values
     * @returns
     */
    async update(
        tenantId: string,
        id: string,
        values: TrustListCreateDto,
    ): Promise<TrustList> {
        const existing = await this.findOne(tenantId, id);
        const tenant = await this.tenantRepository.findOneByOrFail({
            id: tenantId,
        });

        // Store the current version for audit before updating
        await this.saveVersion(existing);

        // Update the trust list
        return this.buildAndSaveTrustList(values, tenant, existing);
    }

    /**
     * Get version history for a trust list
     * @param tenantId
     * @param trustListId
     * @returns
     */
    getVersionHistory(
        tenantId: string,
        trustListId: string,
    ): Promise<TrustListVersion[]> {
        return this.trustListVersionRepo.find({
            where: { tenantId, trustListId },
            order: { sequenceNumber: "DESC" },
        });
    }

    /**
     * Get a specific version of a trust list
     * @param tenantId
     * @param trustListId
     * @param versionId
     * @returns
     */
    getVersion(
        tenantId: string,
        trustListId: string,
        versionId: string,
    ): Promise<TrustListVersion> {
        return this.trustListVersionRepo.findOneByOrFail({
            tenantId,
            trustListId,
            id: versionId,
        });
    }

    /**
     * Remove a trust list
     * @param tenantId
     * @param id
     */
    async remove(tenantId: string, id: string): Promise<void> {
        await this.trustListRepo.delete({ tenantId, id });
    }

    /**
     * Imports trust lists for a specific tenant from the file system.
     */
    async importForTenant(tenantId: string) {
        await this.configImportService.importConfigsForTenant<TrustListCreateDto>(
            tenantId,
            {
                subfolder: "trust-lists",
                fileExtension: ".json",
                validationClass: TrustListCreateDto,
                resourceType: "trustlist",
                loadData: (filePath) => {
                    const payload = JSON.parse(readFileSync(filePath, "utf8"));
                    return plainToClass(TrustListCreateDto, payload);
                },
                checkExists: (tenantId, data) => {
                    return this.findOne(tenantId, data.id!)
                        .then(() => true)
                        .catch(() => false);
                },
                deleteExisting: async (tenantId, data) => {
                    await this.trustListRepo.delete({
                        id: data.id,
                        tenantId,
                    });
                },
                processItem: async (tenantId, config) => {
                    const tenant = await this.tenantRepository.findOneByOrFail({
                        id: tenantId,
                    });
                    await this.buildAndSaveTrustList(config, tenant);
                },
            },
        );
    }
    /**
     * Shared logic for creating and saving a trust list (used by both API and import)
     * @param config The configuration for the trust list
     * @param tenant The tenant entity
     * @param existing Optional existing trust list to update
     */
    private async buildAndSaveTrustList(
        config: TrustListCreateDto,
        tenant: TenantEntity,
        existing?: TrustList,
    ): Promise<TrustList> {
        // Validate PEM certificates for external entities
        for (const entity of config.entities || []) {
            if (entity.type === "external") {
                this.validatePem(entity.issuerCertPem, "issuerCertPem");
                this.validatePem(entity.revocationCertPem, "revocationCertPem");
            }
        }

        let cert: CertEntity;
        if (config.certId) {
            cert = await this.certService.getCertificateById(
                tenant.id,
                config.certId,
            );
            if (!cert.usages.some((c) => c.usage === CertUsage.TrustList)) {
                throw new BadRequestException(
                    `Certificate ${config.certId} is not valid for Trust List usage`,
                );
            }
        } else if (existing?.cert) {
            cert = existing.cert;
        } else {
            cert = await this.certService.findOrCreate({
                tenantId: tenant.id,
                type: CertUsage.TrustList,
            });
        }

        // Use existing trust list or create new
        const trustList =
            existing ??
            this.trustListRepo.create({ tenant, id: config.id ?? v4() });

        // Update properties
        trustList.description = config.description;
        trustList.cert = cert;
        trustList.entityConfig = config.entities;

        // Increment sequence number on updates
        if (existing) {
            trustList.sequenceNumber = (existing.sequenceNumber || 1) + 1;
        } else {
            trustList.sequenceNumber = 1;
        }

        const entries: TrustedEntity[] = [];
        for (const entity of config.entities || []) {
            if (entity.type === "internal") {
                // Internal: fetch certificates from database by ID
                const issuerCert = await this.certService.getCertificateById(
                    tenant.id,
                    entity.issuerCertId,
                );
                const revocationCert =
                    await this.certService.getCertificateById(
                        tenant.id,
                        entity.revocationCertId,
                    );
                entries.push(
                    this.createEntityFromCert(
                        issuerCert,
                        revocationCert,
                        entity.info,
                    ),
                );
            } else {
                // External: use PEM certificates directly with provided info
                entries.push(
                    this.createEntityFromPem(
                        entity.issuerCertPem,
                        entity.revocationCertPem,
                        entity.info,
                    ),
                );
            }
        }

        trustList.data = this.createList(
            tenant,
            entries,
            trustList.sequenceNumber,
        );
        trustList.jwt = await this.generateJwt(trustList);
        return this.trustListRepo.save(trustList);
    }

    /**
     * Save the current state of a trust list as a version for audit
     */
    private saveVersion(trustList: TrustList): Promise<TrustListVersion> {
        const version = this.trustListVersionRepo.create({
            trustListId: trustList.id,
            tenantId: trustList.tenantId,
            sequenceNumber: trustList.sequenceNumber || 1,
            data: trustList.data ?? {},
            entityConfig: trustList.entityConfig,
            jwt: trustList.jwt,
        });
        return this.trustListVersionRepo.save(version);
    }

    /**
     * Validate that a string is a valid PEM certificate
     * @param pem The PEM string to validate
     * @param fieldName The field name for error messages
     */
    private validatePem(pem: string, fieldName: string): void {
        if (!pem || pem.trim() === "") {
            throw new BadRequestException(`${fieldName} is required`);
        }
        try {
            new X509Certificate(pem);
        } catch {
            throw new BadRequestException(
                `${fieldName} is not a valid X.509 certificate`,
            );
        }
    }

    /**
     * Get the JWT of the trust list
     * @param tenantId
     * @param id
     * @returns
     */
    getJwt(tenantId: string, id: string): Promise<string> {
        return this.findOne(tenantId, id).then(
            (trustList) => trustList.jwt,
            (err) => {
                throw new BadRequestException(err.message);
            },
        );
    }

    /**
     * Generate a signed JWT for the trust list
     * @param id
     * @returns
     */
    async generateJwt(trustList: TrustList): Promise<string> {
        const cert = await this.certService.find({
            tenantId: trustList.tenantId,
            type: CertUsage.TrustList,
        });

        // Prepare payload and header
        const payload = { ...trustList.data };
        const protectedHeader: JWTHeaderParameters = {
            alg: "ES256",
            iat: Math.floor(Date.now() / 1000),
            typ: "JWT",
            x5c: this.certService.getCertChain(cert),
        };

        return this.keyService.signJWT(
            payload,
            protectedHeader,
            trustList.tenantId,
            cert.keyId,
        );
    }

    private createEntityFromCert(
        issuerCert: CertEntity,
        revocationCert: CertEntity,
        info: TrustListEntityInfo,
    ): TrustedEntity {
        return this.createEntityFromData(
            this.formatCertEntity(issuerCert),
            this.formatCertEntity(revocationCert),
            info,
        );
    }

    private createEntityFromPem(
        issuerCertPem: string,
        revocationCertPem: string,
        info: TrustListEntityInfo,
    ): TrustedEntity {
        return this.createEntityFromData(
            this.formatPem(issuerCertPem),
            this.formatPem(revocationCertPem),
            info,
        );
    }

    private createEntityFromData(
        issuerCertBase64: string,
        revocationCertBase64: string,
        info: TrustListEntityInfo,
    ): TrustedEntity {
        const lang = info.lang || DEFAULT_LANG;
        return {
            TrustedEntityInformation: {
                TEInformationURI: [
                    {
                        lang,
                        uriValue: info.uri || "",
                    },
                ],
                TEName: [
                    {
                        lang,
                        value: info.name,
                    },
                ],
                TEAddress: {
                    TEElectronicAddress: [
                        {
                            lang,
                            uriValue: info.contactUri || "",
                        },
                    ],
                    TEPostalAddress: [
                        {
                            Country: info.country || "",
                            lang: lang.split("-")[0], // Use short lang code for postal
                            Locality: info.locality || "",
                            PostalCode: info.postalCode || "",
                            StreetAddress: info.streetAddress || "",
                        },
                    ],
                },
            },
            TrustedEntityServices: [
                {
                    ServiceInformation: {
                        ServiceTypeIdentifier:
                            ServiceTypeIdentifier.EaaIssuance,
                        ServiceName: [
                            {
                                lang,
                                value: "EAA-Issuance-Service",
                            },
                        ],
                        ServiceDigitalIdentity: {
                            X509Certificates: [
                                {
                                    val: issuerCertBase64,
                                },
                            ],
                        },
                    },
                },
                {
                    ServiceInformation: {
                        ServiceName: [
                            {
                                lang,
                                value: "EAA-Revocation-Service",
                            },
                        ],
                        ServiceTypeIdentifier:
                            ServiceTypeIdentifier.EaaRevocation,
                        ServiceDigitalIdentity: {
                            X509Certificates: [
                                {
                                    val: revocationCertBase64,
                                },
                            ],
                        },
                    },
                },
            ],
        };
    }

    createList(
        tenant: TenantEntity,
        list: TrustedEntity[],
        sequenceNumber = 1,
    ): LoTE {
        const date = new Date();
        const nextUpdate = new Date();
        nextUpdate.setDate(date.getDate() + 30);

        return {
            ListAndSchemeInformation: {
                LoTEVersionIdentifier: 1,
                LoTESequenceNumber: sequenceNumber,
                LoTEType:
                    "http://uri.etsi.org/19602/LoTEType/EUEAAProvidersList",
                StatusDeterminationApproach:
                    "http://uri.etsi.org/19602/EUEAAProvidersList/StatusDetn/EU",
                SchemeTypeCommunityRules: [
                    {
                        lang: DEFAULT_LANG,
                        uriValue:
                            "http://uri.etsi.org/19602/EUEAAProviders/schemerules/EU",
                    },
                ],
                //TODO: add historical endpoint
                SchemeTerritory: "EU",
                NextUpdate: nextUpdate.toISOString(),
                SchemeOperatorName: [
                    {
                        lang: DEFAULT_LANG,
                        value: tenant.name,
                    },
                ],
                ListIssueDateTime: date.toISOString(),
            },
            TrustedEntitiesList: list as TrustedEntitiesList,
        };
    }

    /**
     * Format CertEntity to base64 DER without PEM headers
     * @param cert
     * @returns
     */
    formatCertEntity(cert: CertEntity): string {
        return this.formatPem(cert.crt);
    }

    /**
     * Format PEM string to base64 DER without PEM headers
     * @param pem
     * @returns
     */
    formatPem(pem: string): string {
        return pem
            .replaceAll("-----BEGIN CERTIFICATE-----", "")
            .replaceAll("-----END CERTIFICATE-----", "")
            .replaceAll(/\r?\n|\r/g, "");
    }
}

results matching ""

    No results matching ""