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.

Async 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.

Async onApplicationBootstrap
onApplicationBootstrap()

Imports presentation configurations from a predefined directory structure.

Returns : any
onModuleInit
onModuleInit()

Initializes the SDJwtVcInstance with the necessary configurations.

Returns : void
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: PresentationConfig)

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 PresentationConfig No
  • The PresentationConfig entity to store.
Returns : any

A promise that resolves to the stored PresentationConfig entity.

Public 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, OnModuleInit } from '@nestjs/common';
import { digest, ES256 } from '@sd-jwt/crypto-nodejs';
import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc';
import { KbVerifier, Verifier } from '@sd-jwt/types';
import { importJWK, JWK, JWTPayload, jwtVerify } from 'jose';
import { firstValueFrom } from 'rxjs';
import { ResolverService } from '../resolver/resolver.service';
import { PresentationConfig } from './entities/presentation-config.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm/repository/Repository';
import { AuthResponse } from './dto/auth-response.dto';
import { ConfigService } from '@nestjs/config';
import { readdirSync, readFileSync } from 'fs';
import { join } from 'path';
import { PinoLogger } from 'nestjs-pino';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';

/**
 * Service for managing Verifiable Presentations (VPs) and handling SD-JWT-VCs.
 */
@Injectable()
export class PresentationsService implements OnModuleInit {
    /**
     * 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,
    ) {}

    /**
     * Initializes the SDJwtVcInstance with the necessary configurations.
     */
    onModuleInit() {
        this.sdjwtInstance = new SDJwtVcInstance({
            hasher: digest,
            verifier: this.verifier.bind(this),
            kbVerifier: this.kbVerifier.bind(this),
            statusListFetcher: this.statusListFetcher.bind(this),
        });
    }

    /**
     * Imports presentation configurations from a predefined directory structure.
     */
    async onApplicationBootstrap() {
        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());
            let counter = 0;
            for (const tenant of tenantFolders) {
                //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'),
                    );

                    payload.id = file.replace('.json', '');
                    const presentationExists = await this.getPresentationConfig(
                        payload.id,
                        tenant.name,
                    ).catch(() => false);
                    if (presentationExists && !force) {
                        continue; // Skip if config already exists and force is not set
                    }

                    // Validate the payload against PresentationConfig
                    const config = plainToClass(PresentationConfig, payload);
                    const validationErrors = await validate(config);

                    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 issuance 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: PresentationConfig) {
        vprequest.tenantId = tenantId;
        return this.vpRequestRepository.save(vprequest);
    }

    /**
     * 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.
     */
    async 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 storeRCID(registrationCertId: string, id: string, tenantId: string) {
        return this.vpRequestRepository.update(
            { id, tenantId },
            { registrationCert: { id: registrationCertId } },
        );
    }

    /**
     * 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> = async (
        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 ""