import type { EbsiEnvConfiguration, EbsiIssuer, } from "@europeum-ebsi/verifiable-credential"; import { createJWT, decodeJWT } from "@europeum-ebsi/did-jwt"; import { getResolver as getEbsiDidResolver } from "@europeum-ebsi/ebsi-did-resolver"; import { getResolver as getKeyDidResolver } from "@europeum-ebsi/key-did-resolver"; import { AXIOS_TIMEOUT, validateConfig, validateDid, validateHeaderJwk, } from "@europeum-ebsi/verifiable-credential"; import { Resolver } from "did-resolver"; import type { CreateVerifiablePresentationJwtOptions, Schemas, VerifyPresentationJwtOptions, VpJwtPayload, } from "./types.ts"; import { validateContext, validateCredentials, validateEbsiVerifiablePresentation, validateHolder, validateJwtProps, validateSignature, validateTimestamp, validateType, } from "./validators.ts"; export * from "./types.ts"; export * from "./validators.ts"; /** * Creates a Verifiable Presentation JWT given an `Schemas["Presentation"]` and an `EbsiIssuer`. * * This method transforms the payload into the [JWT encoding](https://www.w3.org/TR/vc-data-model-1.1/#jwt-encoding) * described in the [W3C VC spec](https://www.w3.org/TR/vc-data-model-1.1/) and then validated to conform to the EBSI * required spec. * * The `holder` is then used to assign an algorithm and sign the JWT. * @param payload - `Schemas["Presentation"]` * @param holder - `EbsiIssuer` of the Presentation JWT (holder of the VC), signer and algorithm that will sign the token * @param audience - represents the identity of the intended audience (DID, URI) * @param config - EBSI environment configuration * @param options - `CreateVerifiablePresentationOptions` allows to pass additional values to the resulting JWT payload * @returns a `Promise` that resolves to the JWT encoded verifiable presentation or rejects with `ValidationError` if the * `payload` is not W3C compliant */ export async function createVerifiablePresentationJwt( payload: Schemas["Presentation"], holder: EbsiIssuer, audience: string, config: EbsiEnvConfiguration, options?: CreateVerifiablePresentationJwtOptions, ): Promise; export async function createVerifiablePresentationJwt( payload: unknown, holder: EbsiIssuer, audience: string, config: EbsiEnvConfiguration, options?: CreateVerifiablePresentationJwtOptions, ): Promise { validateConfig(config); // Set default options const opts: Required = { axiosHeaders: {}, exp: undefined, header: {}, nbf: undefined, nonce: undefined, proofPurpose: "authentication", skipHolderDidResolutionValidation: false, skipValidation: false, timeout: AXIOS_TIMEOUT, // Let the VC library define the default values verifyCredentialOptions: {}, ...options, }; // Always verify that the payload is conform to the the Verifiable Presentation schema validateEbsiVerifiablePresentation(payload, config, opts.timeout); const iat = Math.floor(Date.now() / 1000); if (opts.skipValidation) { validateDid(holder.did); } else { validateContext(payload["@context"]); validateType(payload.type); const resolver = new Resolver({ ...getEbsiDidResolver({ // TODO: implement fallback registry: `https://${config.hosts[0]!}/did-registry/${config.services["did-registry"]}/identifiers`, }), ...getKeyDidResolver(), }); // Validate VP holder await validateHolder( payload, holder.did, holder.kid, holder.alg, iat, resolver, opts.proofPurpose, opts.timeout, opts.skipHolderDidResolutionValidation, opts.axiosHeaders, ); // If options?.nbf or options?.exp are provided, validate them if (opts.nbf) { validateTimestamp(opts.nbf); } if (opts.exp) { validateTimestamp(opts.exp); } // Validate VC JWTs await validateCredentials(payload, holder.did, config, { axiosHeaders: opts.axiosHeaders, timeout: opts.timeout, ...opts.verifyCredentialOptions, }); } const decodedVcPayloads = (payload.verifiableCredential as string[]).map( (vcJwt) => decodeJWT(vcJwt).payload, ); // Prepare payload // See https://www.w3.org/TR/vc-data-model-1.1/#jwt-encoding const vpJwtPayload = { /** * `iss` MUST represent the issuer property of a verifiable credential. */ iss: holder.did, /** * `jti` MUST represent the `id` property of the verifiable presentation. */ jti: typeof payload["id"] === "string" ? payload["id"] : "", /** * `sub` MUST represent the `id` property contained in the verifiable credential subject. */ sub: holder.did, /** * Time before which the Verifiable Presentation MUST NOT be accepted for processing. */ ...(opts.nbf && { nbf: opts.nbf }), /** * Expiration time after which the Verifiable Presentation MUST NOT be accepted for processing. */ ...(opts.exp && { exp: opts.exp }), /** * `aud` MUST represent (i.e., identify) the intended audience of the verifiable presentation * (i.e., the verifier intended by the presenting holder to receive and verify the verifiable presentation). * @see https://www.w3.org/TR/vc-data-model-1.1/#jwt-encoding */ aud: audience, /** * Time at which the Verifiable Presentation has been issued. */ iat, /** * `nonce` can be used to prevent replay attacks, e.g. during a VP exchange. */ ...(opts.nonce && { nonce: opts.nonce }), /** * vp: JSON object, which MUST be present in a JWT verifiable presentation. * The object contains the verifiable presentation according to this specification. */ vp: payload, } satisfies VpJwtPayload; // Provide default nbf value if none was provided if (!vpJwtPayload.nbf) { // Extract "nbf" from VCs const vcNbf = decodedVcPayloads.map((vc) => vc.nbf ?? 0).filter(Boolean); // Assign VP nbf the highest nbf if (vcNbf.length > 0) { vpJwtPayload.nbf = Math.max(...vcNbf); } } // Provide default exp value if none was provided if (!vpJwtPayload.exp) { // Extract "exp" from VCs const vcExp = decodedVcPayloads.map((vc) => vc.exp ?? 0).filter(Boolean); // Assign VP exp the lowest exp if (vcExp.length > 0) { vpJwtPayload.exp = Math.min(...vcExp); } else { // Throw an error if no exp can be found throw new Error( "The VP JWT requires an exp claim. Please provide one using the 'options' parameter.", ); } } // Add kid to JWT header const { alg, kid, signer } = holder; const vpJwt = createJWT( vpJwtPayload, { signer }, { alg, kid, typ: "JWT", ...opts.header }, ); return vpJwt; } /** * Verifies and validates an EBSI Verifiable Presentation that is encoded as a JWT according to the EBSI and W3C specs. * @param presentation - the presentation to be verified. Currently only the JWT encoding is supported by this library * @param audience - represents the identity of the intended audience (DID, URI) * @param config - EBSI environment configuration * @param options - optional verification options that need to be satisfied * @returns a `Promise` that resolves to a Verifiable Presentation or rejects with `ValidationError` if the input is * not EBSI compliant or the VerifyPresentationJwtOptions are not satisfied. */ export async function verifyPresentationJwt( presentation: string, audience: string, config: EbsiEnvConfiguration, options?: VerifyPresentationJwtOptions, ): Promise { validateConfig(config); // Set default options const opts: Required = { axiosHeaders: {}, clockSkew: 0, proofPurpose: "authentication", skipHolderDidResolutionValidation: false, skipSignatureValidation: false, timeout: AXIOS_TIMEOUT, validAt: Math.floor(Date.now() / 1000), // Let the VC library define the default values verifyCredentialOptions: {}, ...options, }; const { header, payload } = validateJwtProps( presentation, audience, config, opts, ); const resolver = new Resolver({ ...getEbsiDidResolver({ // TODO: implement fallback registry: `https://${config.hosts[0]!}/did-registry/${config.services["did-registry"]}/identifiers`, }), ...getKeyDidResolver(), }); const { alg, kid } = header; const { iat, iss, vp } = payload; validateContext(vp["@context"]); validateType(vp.type); // Validate VP holder const didAuthenticator = await validateHolder( vp, iss, kid, alg, iat, resolver, opts.proofPurpose, opts.timeout, opts.skipHolderDidResolutionValidation, opts.axiosHeaders, ); // Validate VC JWTs await validateCredentials(vp, iss, config, { axiosHeaders: opts.axiosHeaders, clockSkew: opts.clockSkew, timeout: opts.timeout, validAt: opts.validAt, ...opts.verifyCredentialOptions, }); // Validate signature if (didAuthenticator && !opts.skipSignatureValidation) { const resultVerifyJWT = await validateSignature( presentation, resolver, didAuthenticator, ); if (header.jwk) { validateHeaderJwk(header.jwk, resultVerifyJWT.signer.publicKeyJwk); } } return vp; }