File

src/verifier/presentations/presentations.service.ts

Description

Service for managing Verifiable Presentations (VPs) and handling SD-JWT-VCs.

Index

Properties
Methods

Constructor

constructor(httpService: HttpService, resolverService: ResolverService, vpRequestRepository: Repository<PresentationConfig>, configService: ConfigService, logger: PinoLogger)

Constructor for the PresentationsService.

Parameters :
Name Type Optional Description
httpService HttpService No
  • Instance of HttpService for making HTTP requests.
resolverService ResolverService No
  • Instance of ResolverService for resolving DID documents.
vpRequestRepository Repository<PresentationConfig> No
  • Repository for managing VP request configurations.
configService ConfigService No
logger PinoLogger No

Methods

deletePresentationConfig
deletePresentationConfig(id: string, tenantId: string)

Deletes a presentation configuration by its ID and tenant ID.

Parameters :
Name Type Optional Description
id string No
  • The ID of the presentation configuration to delete.
tenantId string No
  • The ID of the tenant for which to delete the configuration.
Returns : any

A promise that resolves when the deletion is complete.

getPresentationConfig
getPresentationConfig(id: string, tenantId: string)

Retrieves a presentation configuration by its ID and tenant ID.

Parameters :
Name Type Optional Description
id string No
  • The ID of the presentation configuration to retrieve.
tenantId string No
  • The ID of the tenant for which to retrieve the configuration.

A promise that resolves to the requested PresentationConfig entity.

getPresentationConfigs
getPresentationConfigs(tenantId: string)

Retrieves all presentation configurations for a given tenant.

Parameters :
Name Type Optional Description
tenantId string No
  • The ID of the tenant for which to retrieve configurations.

A promise that resolves to an array of PresentationConfig entities.

Private Async import
import()

Imports presentation configurations from a predefined directory structure.

Returns : any
Async onApplicationBootstrap
onApplicationBootstrap()

Imports presentation configurations from a predefined directory structure.

Returns : any
parseResponse
parseResponse(res: AuthResponse, requiredFields: string[], keyBindingNonce: string)

Parse the response from the wallet. It will verify the SD-JWT-VCs in the vp_token and return the parsed attestations.

Parameters :
Name Type Optional
res AuthResponse No
requiredFields string[] No
keyBindingNonce string No
Returns : any
storePresentationConfig
storePresentationConfig(tenantId: string, vprequest: PresentationConfigCreateDto)

Stores a new presentation configuration.

Parameters :
Name Type Optional Description
tenantId string No
  • The ID of the tenant for which to store the configuration.
vprequest PresentationConfigCreateDto No
  • The PresentationConfig entity to store.
Returns : any

A promise that resolves to the stored PresentationConfig entity.

Public Async storeRCID
storeRCID(registrationCertId: string, id: string, tenantId: string)

Stores the new registration certificate.

Parameters :
Name Type Optional Description
registrationCertId string No
  • The ID of the registration certificate to store.
id string No
  • The ID of the presentation configuration to update.
tenantId string No
  • The ID of the tenant for which to store the registration certificate.
Returns : any

Properties

Private kbVerifier
Type : KbVerifier
Default value : () => {...}

Verifier for keybindings. It will verify the signature of the keybinding and return true if it is valid.

Parameters :
Name
data
signature
payload
sdjwtInstance
Type : SDJwtVcInstance

Instance of SDJwtVcInstance for handling SD-JWT-VCs.

Private statusListFetcher
Type : function
Default value : () => {...}

Fetch the status list from the uri.

Parameters :
Name
uri
verifier
Type : Verifier
Default value : () => {...}

Verifier for SD-JWT-VCs. It will verify the signature of the SD-JWT-VC and return true if it is valid.

Parameters :
Name Description
data
  • The data part of the SD-JWT-VC.
signature
  • The signature of the SD-JWT-VC.
import { HttpService } from "@nestjs/axios";
import {
    ConflictException,
    Injectable,
    OnApplicationBootstrap,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { InjectRepository } from "@nestjs/typeorm";
import { digest, ES256 } from "@sd-jwt/crypto-nodejs";
import { SDJwtVcInstance } from "@sd-jwt/sd-jwt-vc";
import { KbVerifier, Verifier } from "@sd-jwt/types";
import { plainToClass } from "class-transformer";
import { validate } from "class-validator";
import { readdirSync, readFileSync } from "fs";
import { importJWK, JWK, JWTPayload, jwtVerify } from "jose";
import { PinoLogger } from "nestjs-pino";
import { join } from "path";
import { firstValueFrom } from "rxjs";
import { Repository } from "typeorm/repository/Repository";
import { ResolverService } from "../resolver/resolver.service";
import { AuthResponse } from "./dto/auth-response.dto";
import { PresentationConfigCreateDto } from "./dto/presentation-config-create.dto";
import { PresentationConfig } from "./entities/presentation-config.entity";

/**
 * Service for managing Verifiable Presentations (VPs) and handling SD-JWT-VCs.
 */
@Injectable()
export class PresentationsService implements OnApplicationBootstrap {
    /**
     * Instance of SDJwtVcInstance for handling SD-JWT-VCs.
     */
    sdjwtInstance: SDJwtVcInstance;

    /**
     * Constructor for the PresentationsService.
     * @param httpService - Instance of HttpService for making HTTP requests.
     * @param resolverService - Instance of ResolverService for resolving DID documents.
     * @param vpRequestRepository - Repository for managing VP request configurations.
     */
    constructor(
        private httpService: HttpService,
        private resolverService: ResolverService,
        @InjectRepository(PresentationConfig)
        private vpRequestRepository: Repository<PresentationConfig>,
        private configService: ConfigService,
        private logger: PinoLogger,
    ) {}

    /**
     * Imports presentation configurations from a predefined directory structure.
     */
    async onApplicationBootstrap() {
        this.sdjwtInstance = new SDJwtVcInstance({
            hasher: digest,
            verifier: this.verifier.bind(this),
            kbVerifier: this.kbVerifier.bind(this),
            statusListFetcher: this.statusListFetcher.bind(this),
        });
        await this.import();
    }

    /**
     * Imports presentation configurations from a predefined directory structure.
     */
    private async import() {
        const configPath = this.configService.getOrThrow("CONFIG_FOLDER");
        const subfolder = "presentation";
        const force = this.configService.get<boolean>("CONFIG_IMPORT_FORCE");
        if (this.configService.get<boolean>("CONFIG_IMPORT")) {
            const tenantFolders = readdirSync(configPath, {
                withFileTypes: true,
            }).filter((tenant) => tenant.isDirectory());
            for (const tenant of tenantFolders) {
                let counter = 0;
                //iterate over all elements in the folder and import them
                const path = join(configPath, tenant.name, subfolder);
                const files = readdirSync(path);
                for (const file of files) {
                    const payload = JSON.parse(
                        readFileSync(join(path, file), "utf8"),
                    );

                    const id = file.replace(".json", "");
                    payload.id = id;
                    const presentationExists = await this.getPresentationConfig(
                        id,
                        tenant.name,
                    ).catch(() => false);
                    if (presentationExists && !force) {
                        continue; // Skip if config already exists and force is not set
                    } else if (presentationExists && force) {
                        //delete old element so removed elements are not present
                        await this.vpRequestRepository.delete({
                            id,
                            tenantId: tenant.name,
                        });
                    }

                    // Validate the payload against PresentationConfig
                    const config = plainToClass(
                        PresentationConfigCreateDto,
                        payload,
                    );
                    const validationErrors = await validate(config, {
                        whitelist: true,
                        forbidUnknownValues: false, // avoid false positives on plain objects
                        forbidNonWhitelisted: false,
                        stopAtFirstError: false,
                    });

                    if (validationErrors.length > 0) {
                        this.logger.error(
                            {
                                event: "ValidationError",
                                file,
                                tenant: tenant.name,
                                errors: validationErrors.map((error) => ({
                                    property: error.property,
                                    constraints: error.constraints,
                                    value: error.value,
                                })),
                            },
                            `Validation failed for presentation config ${file} in tenant ${tenant.name}`,
                        );
                        continue; // Skip this invalid config
                    }

                    await this.storePresentationConfig(tenant.name, config);
                    counter++;
                }
                this.logger.info(
                    {
                        event: "Import",
                    },
                    `${counter} presentation configs imported for ${tenant.name}`,
                );
            }
        }
    }

    /**
     * Retrieves all presentation configurations for a given tenant.
     * @param tenantId - The ID of the tenant for which to retrieve configurations.
     * @returns A promise that resolves to an array of PresentationConfig entities.
     */
    getPresentationConfigs(tenantId: string): Promise<PresentationConfig[]> {
        return this.vpRequestRepository.find({
            where: { tenantId },
            order: { createdAt: "DESC" },
        });
    }

    /**
     * Stores a new presentation configuration.
     * @param tenantId - The ID of the tenant for which to store the configuration.
     * @param vprequest - The PresentationConfig entity to store.
     * @returns A promise that resolves to the stored PresentationConfig entity.
     */
    storePresentationConfig(
        tenantId: string,
        vprequest: PresentationConfigCreateDto,
    ) {
        return this.vpRequestRepository.save({
            ...vprequest,
            tenantId,
        });
    }

    /**
     * Deletes a presentation configuration by its ID and tenant ID.
     * @param id - The ID of the presentation configuration to delete.
     * @param tenantId - The ID of the tenant for which to delete the configuration.
     * @returns A promise that resolves when the deletion is complete.
     */
    deletePresentationConfig(id: string, tenantId: string) {
        return this.vpRequestRepository.delete({ id, tenantId });
    }

    /**
     * Retrieves a presentation configuration by its ID and tenant ID.
     * @param id - The ID of the presentation configuration to retrieve.
     * @param tenantId - The ID of the tenant for which to retrieve the configuration.
     * @returns A promise that resolves to the requested PresentationConfig entity.
     */
    getPresentationConfig(
        id: string,
        tenantId: string,
    ): Promise<PresentationConfig> {
        return this.vpRequestRepository
            .findOneByOrFail({
                id,
                tenantId,
            })
            .catch(() => {
                throw new ConflictException(`Request ID ${id} not found`);
            });
    }

    /**
     * Stores the new registration certificate.
     * @param registrationCertId - The ID of the registration certificate to store.
     * @param id - The ID of the presentation configuration to update.
     * @param tenantId - The ID of the tenant for which to store the registration certificate.
     * @returns
     */
    public async storeRCID(
        registrationCertId: string,
        id: string,
        tenantId: string,
    ) {
        const element = await this.vpRequestRepository.findOneByOrFail({
            id,
            tenantId,
        });
        element.registrationCert!.id = registrationCertId;
        await this.vpRequestRepository.save(element);
    }

    /**
     * Verifier for SD-JWT-VCs. It will verify the signature of the SD-JWT-VC and return true if it is valid.
     * @param data - The data part of the SD-JWT-VC.
     * @param signature - The signature of the SD-JWT-VC.
     * @returns
     */
    verifier: Verifier = async (data, signature) => {
        const instance = new SDJwtVcInstance({
            hasher: digest,
        });
        const decodedVC = await instance.decode(`${data}.${signature}`);
        const payload = decodedVC.jwt?.payload as JWTPayload;
        const header = decodedVC.jwt?.header as JWK;
        const publicKey = await this.resolverService.resolvePublicKey(
            payload,
            header,
        );
        const verify = await ES256.getVerifier(publicKey);
        return verify(data, signature).catch((err) => {
            console.log(err);
            return false;
        });
    };

    /**
     * Fetch the status list from the uri.
     * @param uri
     * @returns
     */
    private statusListFetcher: (uri: string) => Promise<string> = (
        uri: string,
    ) => {
        return firstValueFrom(this.httpService.get<string>(uri)).then(
            (res) => res.data,
        );
    };

    /**
     * Verifier for keybindings. It will verify the signature of the keybinding and return true if it is valid.
     * @param data
     * @param signature
     * @param payload
     * @returns
     */
    private kbVerifier: KbVerifier = async (data, signature, payload) => {
        if (!payload.cnf) {
            throw new Error("No cnf found in the payload");
        }
        const key = await importJWK(payload.cnf.jwk as JWK, "ES256");
        return jwtVerify(`${data}.${signature}`, key).then(
            () => true,
            () => false,
        );
    };

    /**
     * Parse the response from the wallet. It will verify the SD-JWT-VCs in the vp_token and return the parsed attestations.
     * @param res
     * @param requiredFields
     * @returns
     */
    parseResponse(
        res: AuthResponse,
        requiredFields: string[],
        keyBindingNonce: string,
    ) {
        const attestations = Object.keys(res.vp_token);
        const att = attestations.map((att) =>
            this.sdjwtInstance
                .verify(res.vp_token[att], {
                    requiredClaimKeys: requiredFields,
                    keyBindingNonce,
                })
                .then(
                    (result) => {
                        return {
                            id: att,
                            values: {
                                ...result.payload,
                                cnf: undefined, // remove cnf for simplicity
                                status: undefined, // remove status for simplicity
                            },
                        };
                    },
                    /* (err) => {
                        throw new Error
                        //(console.log(err);
                        return {
                            id: att,
                            error: err.message,
                        };
                    }, */
                ),
        );
        return Promise.all(att);
    }
}

results matching ""

    No results matching ""