Skip to content

Commit

Permalink
Merge pull request #81 from wwWallet/verification-flow-fixes
Browse files Browse the repository at this point in the history
Combined presentation and other fixes
  • Loading branch information
kkmanos authored Nov 22, 2024
2 parents 3a761db + 13a7025 commit 151ef8d
Show file tree
Hide file tree
Showing 14 changed files with 367 additions and 226 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ export class GenericVIDAuthenticationComponent extends AuthenticationComponent {
}

private async handleCallback(req: Request, res: Response): Promise<any> {
const result = await this.openidForPresentationReceivingService.getPresentationBySessionId({ req, res });
if (!req.cookies['session_id']) {
return false;
}
const result = await this.openidForPresentationReceivingService.getPresentationBySessionId(req.cookies['session_id']);
if (!result.status) {
return false;
}
Expand Down
8 changes: 8 additions & 0 deletions src/configuration/verifier/VerifierConfigurationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import { authorizationServerMetadataConfiguration } from "../../authorizationSer
import { config } from "../../../config";
import { VerifierConfigurationInterface } from "../../services/interfaces";
import "reflect-metadata";
import { PresentationParserChain } from "../../vp_token/PresentationParserChain";
import { PublicKeyResolverChain } from "../../vp_token/PublicKeyResolverChain";



@injectable()
export class VerifierConfigurationService implements VerifierConfigurationInterface {
getPresentationParserChain(): PresentationParserChain {
return new PresentationParserChain();
}
getPublicKeyResolverChain(): PublicKeyResolverChain {
return new PublicKeyResolverChain();
}

getPresentationDefinitions(): any[] {
return []
Expand Down
183 changes: 58 additions & 125 deletions src/services/OpenidForPresentationReceivingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Request, Response } from 'express'
import { OpenidForPresentationsReceivingInterface, VerifierConfigurationInterface } from "./interfaces";
import { VerifiableCredentialFormat } from "../types/oid4vci";
import { TYPES } from "./types";
import { compactDecrypt, exportJWK, generateKeyPair, importJWK, importPKCS8, importX509, jwtVerify, SignJWT } from "jose";
import { X509Certificate, createHash, randomUUID } from "crypto";
import { compactDecrypt, exportJWK, generateKeyPair, importJWK, importPKCS8, jwtVerify, SignJWT } from "jose";
import { createHash, randomUUID } from "crypto";
import base64url from "base64url";
import 'reflect-metadata';
import { JSONPath } from "jsonpath-plus";
Expand All @@ -14,10 +14,10 @@ import { config } from "../../config";
import { HasherAlgorithm, HasherAndAlgorithm, SdJwt, SignatureAndEncryptionAlgorithm, Verifier } from "@sd-jwt/core";
import fs from 'fs';
import path from "path";
import crypto from 'node:crypto';
import { ClaimRecord, PresentationClaims, RelyingPartyState } from "../entities/RelyingPartyState.entity";
import { generateRandomIdentifier } from "../lib/generateRandomIdentifier";
import * as z from 'zod';
import { verifyKbJwt } from "../util/verifyKbJwt";

const privateKeyPem = fs.readFileSync(path.join(__dirname, "../../../keys/pem.server.key"), 'utf-8').toString();
const x5c = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../keys/x5c.server.json")).toString()) as Array<string>;
Expand All @@ -37,23 +37,6 @@ const hasherAndAlgorithm: HasherAndAlgorithm = {
algorithm: HasherAlgorithm.Sha256
}

async function verifyCertificateChain(rootCert: string, pemCertChain: string[]) {
const x509TrustAnchor = new X509Certificate(rootCert);
const isLastCertTrusted = new X509Certificate(pemCertChain[pemCertChain.length - 1]).verify(x509TrustAnchor.publicKey);
if (!isLastCertTrusted) {
return false;
}
for (let i = 0; i < pemCertChain.length; i++) {
if (pemCertChain[i + 1]) {
const isTrustedCert = new X509Certificate(pemCertChain[i]).verify(new X509Certificate(pemCertChain[i + 1]).publicKey);
if (!isTrustedCert) {
return false;
}
}
}
return true;
}

function uint8ArrayToBase64Url(array: any) {
// Convert the Uint8Array to a binary string
let binaryString = '';
Expand Down Expand Up @@ -242,10 +225,11 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
if (!payload.presentation_submission) {
throw new Error("Encrypted Response: presentation_submission is missing");
}
rpState.response_code = generateRandomIdentifier(8);
rpState.response_code = base64url.encode(randomUUID());
rpState.encrypted_response = ctx.req.body.response;
rpState.presentation_submission = payload.presentation_submission;
rpState.vp_token = payload.vp_token;
console.log("Encoding....")
rpState.vp_token = base64url.encode(JSON.stringify(payload.vp_token));
rpState.date_created = new Date();
console.log("Stored rp state = ", rpState)
await this.rpStateRepository.save(rpState);
Expand Down Expand Up @@ -273,88 +257,69 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
if (!rpState) {
throw new Error("Couldn't get rp state with state");
}
rpState.response_code = generateRandomIdentifier(8);
rpState.response_code = base64url.encode(randomUUID());
rpState.presentation_submission = presentation_submission;
rpState.vp_token = vp_token;
rpState.vp_token = base64url.encode(JSON.stringify(vp_token));
rpState.date_created = new Date();
await this.rpStateRepository.save(rpState);
ctx.res.send({ redirect_uri: rpState.callback_endpoint + '#response_code=' + rpState.response_code })
return;
}

private async validateVpToken(vp_token: string, presentation_submission: any, rpState: RelyingPartyState): Promise<{ presentationClaims?: PresentationClaims, error?: Error, error_description?: Error }> {
private async validateVpToken(vp_token_list: string[] | string, presentation_submission: any, rpState: RelyingPartyState): Promise<{ presentationClaims?: PresentationClaims, error?: Error, error_description?: Error }> {
let presentationClaims: PresentationClaims = {};

for (const desc of presentation_submission.descriptor_map) {
if (!presentationClaims[desc.id]) {
presentationClaims[desc.id] = [];
}


const path = desc.path as string;
const jsonPathResult = JSONPath({ json: vp_token_list, path: path });
if (!jsonPathResult || !(typeof jsonPathResult[0] == 'string')) {
console.log(`Couldn't find vp_token for path ${path}`);
throw new Error(`Couldn't find vp_token for path ${path}`);
}
const vp_token = jsonPathResult[0];
if (desc.format == VerifiableCredentialFormat.VC_SD_JWT) {
const sdJwt = vp_token.split('~').slice(0, -1).join('~') + '~';
const kbJwt = vp_token.split('~')[vp_token.split('~').length - 1] as string;
const path = desc?.path as string;
console.log("Path = ", path)

const input_descriptor = rpState!.presentation_definition!.input_descriptors.filter((input_desc: any) => input_desc.id == desc.id)[0];
if (!input_descriptor) {
return { error: new Error("Input descriptor not found") };
}
const requiredClaimNames = input_descriptor.constraints.fields.map((field: any) => {
const fieldPath = field.path[0];
const splittedPath = fieldPath.split('.');
return splittedPath[splittedPath.length - 1]; // return last part of the path
});

const parsedSdJwt = SdJwt.fromCompact(sdJwt).withHasher(hasherAndAlgorithm);

const jwtPayload = (JSON.parse(base64url.decode(sdJwt.split('.')[1])) as any);

// kbjwt validation
try {
const { alg } = JSON.parse(base64url.decode(kbJwt.split('.')[0])) as { alg: string };
const publicKey = await importJWK(jwtPayload.cnf.jwk, alg);
await jwtVerify(kbJwt, publicKey);
}
catch (err) {
const kbJwtValidationResult = await verifyKbJwt(vp_token, { aud: rpState.audience, nonce: rpState.nonce });
if (!kbJwtValidationResult) {
return { error: new Error("PRESENTATION_RESPONSE:INVALID_KB_JWT"), error_description: new Error("KB JWT validation failed") };
}
console.info("Passed KBJWT verification...");

const verifyCb: Verifier = async ({ header, message, signature }) => {
if (header.alg !== SignatureAndEncryptionAlgorithm.ES256) {
throw new Error('only ES256 is supported')
}
if (header['x5c'] && header['x5c'] instanceof Array && header['x5c'][0]) {
const pemCerts = header['x5c'].map(cert => {
const pemCert = `-----BEGIN CERTIFICATE-----\n${cert}\n-----END CERTIFICATE-----`;
return pemCert;
});

// check if at least one root certificate verifies this credential
const result: boolean[] = await Promise.all(config.trustedRootCertificates.map(async (rootCert: string) => {
return verifyCertificateChain(rootCert, pemCerts);
}));


if (config.trustedRootCertificates.length != 0 && !result.includes(true)) {
console.log("Chain is not trusted");
return false;
}
console.info("Chain is trusted");
const cert = await importX509(pemCerts[0], 'ES256');
const verificationResult = await jwtVerify(message + '.' + uint8ArrayToBase64Url(signature), cert).then(() => true).catch((err: any) => {
console.log("Error verifying")
console.error(err);
return false;
});
console.log("JWT verification result = ", verificationResult);
return verificationResult;

const publicKeyResolutionResult = await this.configurationService.getPublicKeyResolverChain().resolve(vp_token, VerifiableCredentialFormat.VC_SD_JWT);
if ('error' in publicKeyResolutionResult) {
return false;
}
return false;

if (!publicKeyResolutionResult.isTrusted) {
return false;
}
const verificationResult = await jwtVerify(message + '.' + uint8ArrayToBase64Url(signature), publicKeyResolutionResult.publicKey).then(() => true).catch((err: any) => {
console.log("Error verifying")
console.error(err);
return false;
});
return verificationResult;
}

const verificationResult = await parsedSdJwt.verify(verifyCb, requiredClaimNames);
const verificationResult = await parsedSdJwt.verify(verifyCb);
const prettyClaims = await parsedSdJwt.getPrettyClaims();

input_descriptor.constraints.fields.map((field: any) => {
Expand All @@ -364,12 +329,16 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
const fieldPath = field.path[0]; // get first path
const fieldName = (field as any).name;
const value = String(JSONPath({ path: fieldPath, json: prettyClaims.vc as any ?? prettyClaims })[0]);
if (!value) {
return { error: new Error("VALUE_NOT_FOUND"), error_description: new Error(`Verification result: Not all values are present as requested from the presentation_definition`) };
}

const splittedPath = fieldPath.split('.');
const claimName = fieldName ? fieldName : splittedPath[splittedPath.length - 1];
presentationClaims[desc.id].push({ name: claimName, value: typeof value == 'object' ? JSON.stringify(value) : value } as ClaimRecord);
});

if (!verificationResult.isSignatureValid || !verificationResult.areRequiredClaimsIncluded) {
if (!verificationResult.isSignatureValid) {
return { error: new Error("SD_JWT_VERIFICATION_FAILURE"), error_description: new Error(`Verification result ${JSON.stringify(verificationResult)}`) };
}
}
Expand All @@ -379,62 +348,23 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
}


public async getPresentationBySessionId(ctx: { req: Request, res: Response }): Promise<{ status: true, rpState: RelyingPartyState } | { status: false }> {

if (!ctx.req.cookies['session_id']) {
console.error("Missing session id");
return { status: false };
}
public async getPresentationBySessionId(sessionId: string): Promise<{ status: true, presentations: unknown[], rpState: RelyingPartyState } | { status: false }> {

const rpState = await this.rpStateRepository.createQueryBuilder()
.where("session_id = :session_id", { session_id: ctx.req.cookies['session_id'] })
.where("session_id = :session_id", { session_id: sessionId })
.getOne();

if (!rpState) {
console.error("Couldn't get rpState with the session_id " + ctx.req.cookies['session_id']);
console.error("Couldn't get rpState with the session_id " + sessionId);
return { status: false };
}

if (!rpState.presentation_submission) {
console.error("Presentation has not been sent. session_id " + ctx.req.cookies['session_id']);
return { status: false };
if (!rpState.presentation_submission || !rpState.vp_token) {
console.error("Presentation has not been sent. session_id " + sessionId);
return { status: false };
}

console.log("RP state = ", rpState)
const vp_token = rpState.vp_token as string;

if (rpState.presentation_submission?.descriptor_map[0].format == 'vc+sd-jwt') {
try {
await (async function validateKbJwt() {
const sdJwt = vp_token.split('~').slice(0, -1).join('~') + '~';
const kbJwt = vp_token.split('~')[vp_token.split('~').length - 1] as string;
const { sd_hash, nonce, aud } = JSON.parse(base64url.decode(kbJwt.split('.')[1])) as any;
async function calculateHash(text: string) {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashBuffer = await crypto.webcrypto.subtle.digest('SHA-256', data);
const base64String = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
const base64UrlString = base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
return base64UrlString;
}
if (await calculateHash(sdJwt) != sd_hash) {
throw new Error("Wrong sd_hash");
}
if (aud != rpState.audience) {
throw new Error("Wrong aud");
}

if (nonce != rpState.nonce) {
throw new Error("Wrong nonce");
}
return { sdJwt };
})();
}
catch(err) {
console.error(err);
return { status: false };
}

}
const vp_token = JSON.parse(base64url.decode(rpState.vp_token)) as string[] | string;

const { presentationClaims, error, error_description } = await this.validateVpToken(vp_token, rpState.presentation_submission as any, rpState);

Expand All @@ -448,23 +378,26 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
await this.rpStateRepository.save(rpState);
}
if (rpState) {
return { status: true, rpState };
return { status: true, rpState, presentations: vp_token instanceof Array ? vp_token : [vp_token] };
}
return { status: false };
}

public async getPresentationById(id: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, rawPresentation?: string }> {
const vp = await this.rpStateRepository.createQueryBuilder('vp')
public async getPresentationById(id: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, presentations?: unknown[] }> {
const rpState = await this.rpStateRepository.createQueryBuilder('vp')
.where("id = :id", { id: id })
.getOne();

if (!vp?.vp_token || !vp.claims) {
if (!rpState?.vp_token || !rpState.claims) {
return { status: false };
}

if (vp)
return { status: true, presentationClaims: vp.claims, rawPresentation: vp?.vp_token };
else
return { status: false };
const vp_token = JSON.parse(base64url.decode(rpState.vp_token)) as string[] | string;

if (rpState) {
return { status: true, presentationClaims: rpState.claims, presentations: vp_token instanceof Array ? vp_token : [vp_token] };
}

return { status: false };
}
}
10 changes: 7 additions & 3 deletions src/services/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { SupportedCredentialProtocol } from "../lib/CredentialIssuerConfig/Suppo
import { CredentialView } from "../authorization/types";
import { AuthorizationServerState } from "../entities/AuthorizationServerState.entity";
import { PresentationClaims, RelyingPartyState } from "../entities/RelyingPartyState.entity";
import { PresentationParserChain } from "../vp_token/PresentationParserChain";
import { PublicKeyResolverChain } from "../vp_token/PublicKeyResolverChain";

export interface CredentialSigner {
sign(payload: any, headers?: any, disclosureFrame?: any): Promise<{ jws: string }>;
Expand All @@ -32,15 +34,17 @@ export interface OpenidForPresentationsReceivingInterface {

getSignedRequestObject(ctx: { req: Request, res: Response }): Promise<any>;
generateAuthorizationRequestURL(ctx: { req: Request, res: Response }, presentationDefinition: object, sessionId: string, callbackEndpoint?: string): Promise<{ url: URL; stateId: string }>;
getPresentationBySessionId(ctx: { req: Request, res: Response }): Promise<{ status: true, rpState: RelyingPartyState } | { status: false }>;
getPresentationById(id: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, rawPresentation?: string }>;
getPresentationBySessionId(sessionId: string): Promise<{ status: true, rpState: RelyingPartyState, presentations: unknown[] } | { status: false }>;
getPresentationById(id: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, presentations?: unknown[] }>;
responseHandler(ctx: { req: Request, res: Response }): Promise<void>;
}


export interface VerifierConfigurationInterface {
getConfiguration(): OpenidForPresentationsConfiguration;
getPresentationDefinitions(): any[];
getPresentationParserChain(): PresentationParserChain;
getPublicKeyResolverChain(): PublicKeyResolverChain;
}


Expand Down Expand Up @@ -82,4 +86,4 @@ export interface CredentialDataModel {

export interface CredentialDataModelRegistry extends CredentialDataModel {
register(dm: CredentialDataModel): void;
}
}
18 changes: 18 additions & 0 deletions src/util/verifyCertificateChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { X509Certificate } from "crypto";

export async function verifyCertificateChain(rootCert: string, pemCertChain: string[]) {
const x509TrustAnchor = new X509Certificate(rootCert);
const isLastCertTrusted = new X509Certificate(pemCertChain[pemCertChain.length - 1]).verify(x509TrustAnchor.publicKey);
if (!isLastCertTrusted) {
return false;
}
for (let i = 0; i < pemCertChain.length; i++) {
if (pemCertChain[i + 1]) {
const isTrustedCert = new X509Certificate(pemCertChain[i]).verify(new X509Certificate(pemCertChain[i + 1]).publicKey);
if (!isTrustedCert) {
return false;
}
}
}
return true;
}
Loading

0 comments on commit 151ef8d

Please sign in to comment.