Skip to content

Commit

Permalink
introduction of PresentationParserChain and PublicKeyResolverChain in…
Browse files Browse the repository at this point in the history
…crease flexibility
  • Loading branch information
kkmanos committed Nov 21, 2024
1 parent 795299b commit 9a8ee59
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 3 deletions.
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;
}
}
4 changes: 4 additions & 0 deletions src/vp_token/IPresentationParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export interface IPresentationParser {
parse(presentationRawFormat: string | object): Promise<{ credentialImage: string, credentialPayload: any } | { error: "PARSE_ERROR" }>;
}
5 changes: 5 additions & 0 deletions src/vp_token/IPublicKeyResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { KeyLike } from "jose";

export interface IPublicKeyResolver {
resolve(rawPresentation: string | object, format: string): Promise<{ publicKey: KeyLike, isTrusted: boolean } | { error: "UNABLE_TO_RESOLVE_PUBKEY" }>;
}
25 changes: 25 additions & 0 deletions src/vp_token/PresentationParserChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IPresentationParser } from "./IPresentationParser";
import { sdJwtDefaultParser } from "./sdJwtDefaultParser";

export class PresentationParserChain {

constructor(private parserList: IPresentationParser[] = [
sdJwtDefaultParser
]) { }

addParser(p: IPresentationParser): this {
this.parserList.push(p);
return this;
}

async parse(rawPresentation: any): Promise<{ credentialImage: string, credentialPayload: any } | { error: "PARSE_ERROR" }> {
for (const p of this.parserList) {
const result = await p.parse(rawPresentation);
if ('error' in result) {
continue;
}
return { ...result };
}
return { error: "PARSE_ERROR" };
}
}
26 changes: 26 additions & 0 deletions src/vp_token/PublicKeyResolverChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { KeyLike } from "jose";
import { IPublicKeyResolver } from "./IPublicKeyResolver";
import { sdJwtPublicKeyResolverUsingX5CHeader } from "./sdJwtPublicKeyResolverUsingX5CHeader";

export class PublicKeyResolverChain {

constructor(private resolverList: IPublicKeyResolver[] = [
sdJwtPublicKeyResolverUsingX5CHeader
]) { }

addResolver(p: IPublicKeyResolver): this {
this.resolverList.push(p);
return this;
}

async resolve(rawPresentation: any, format: string): Promise<{ publicKey: KeyLike, isTrusted: boolean } | { error: "UNABLE_TO_RESOLVE_PUBKEY" }> {
for (const p of this.resolverList) {
const result = await p.resolve(rawPresentation, format);
if ('error' in result) {
continue;
}
return { ...result };
}
return { error: "UNABLE_TO_RESOLVE_PUBKEY" };
}
}
87 changes: 87 additions & 0 deletions src/vp_token/sdJwtDefaultParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { config } from "../../config";
import { generateDataUriFromSvg } from "../lib/generateDataUriFromSvg";
import {
HasherAlgorithm,
HasherAndAlgorithm,
SdJwt,
} from '@sd-jwt/core'

import base64url from "base64url";
import crypto from 'node:crypto';
import axios from "axios";
import { IPresentationParser } from "./IPresentationParser";


// Encoding the string into a Uint8Array
const hasherAndAlgorithm: HasherAndAlgorithm = {
hasher: (input: string) => {
// return crypto.subtle.digest('SHA-256', encoder.encode(input)).then((v) => new Uint8Array(v));
return new Promise((resolve, _reject) => {
const hash = crypto.createHash('sha256');
hash.update(input);
resolve(new Uint8Array(hash.digest()));
});
},
algorithm: HasherAlgorithm.Sha256
}

export const sdJwtDefaultParser: IPresentationParser = {
async parse(presentationRawFormat) {
if (typeof presentationRawFormat != 'string') {
return { error: "PARSE_ERROR" };
}

try {
let defaultLocale = 'en-US';
let credentialImage = null;
let credentialPayload = null;

if (presentationRawFormat.includes('~')) {
const parsedCredential = await SdJwt.fromCompact<Record<string, unknown>, any>(presentationRawFormat)
.withHasher(hasherAndAlgorithm)
.getPrettyClaims();
const sdJwtHeader = JSON.parse(base64url.decode(presentationRawFormat.split('.')[0])) as any;
credentialPayload = parsedCredential;
console.log("Parsed credential = ", parsedCredential)
const credentialIssuerMetadata = await axios.get(parsedCredential.iss + "/.well-known/openid-credential-issuer").catch(() => null);
let fistImageUri;
if (!credentialIssuerMetadata) {
console.error("Couldnt get image for the credential " + presentationRawFormat);
}
else {
console.log("Credential issuer metadata = ", credentialIssuerMetadata?.data)
fistImageUri = Object.values(credentialIssuerMetadata?.data?.credential_configurations_supported).map((conf: any) => {
if (conf?.vct == parsedCredential?.vct) {
return conf?.display && conf?.display[0] && conf?.display[0]?.background_image?.uri ? conf?.display[0]?.background_image?.uri : undefined;
}
return undefined;
}).filter((val) => val)[0];
}

if (sdJwtHeader?.vctm && sdJwtHeader?.vctm?.display?.length > 0 && sdJwtHeader?.vctm?.display[0][defaultLocale]?.rendering?.svg_templates.length > 0 && sdJwtHeader?.vctm?.display[0][defaultLocale]?.rendering?.svg_templates[0]?.uri) {
const response = await axios.get(sdJwtHeader?.vctm?.display[0][defaultLocale].rendering.svg_templates[0].uri);
const svgText = response.data;
const pathsWithValues: any[] = [];
const dataUri = generateDataUriFromSvg(svgText, pathsWithValues); // replaces all with empty string
credentialImage = dataUri;
}
else if (sdJwtHeader?.vctm && sdJwtHeader?.vctm?.display?.length > 0 && sdJwtHeader?.vctm?.display[0][defaultLocale]?.rendering?.simple?.logo?.uri) {
credentialImage = sdJwtHeader?.vctm?.display[0][defaultLocale]?.rendering?.simple?.logo?.uri;
}
else if (fistImageUri) {
credentialImage = fistImageUri;
}
else {
credentialImage = config.url + "/images/card.png";
}
return { credentialImage, credentialPayload };
}

return { error: "PARSE_ERROR" };
}
catch (err) {
console.error(err);
return { error: "PARSE_ERROR" };
}
},
}
40 changes: 40 additions & 0 deletions src/vp_token/sdJwtPublicKeyResolverUsingX5CHeader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import base64url from "base64url";
import { VerifiableCredentialFormat } from "../types/oid4vci";
import { IPublicKeyResolver } from "./IPublicKeyResolver";
import { importX509, JWTHeaderParameters, KeyLike } from "jose";
import { verifyCertificateChain } from "../util/verifyCertificateChain";
import { config } from "../../config";

export const sdJwtPublicKeyResolverUsingX5CHeader: IPublicKeyResolver = {
async resolve(rawPresentation: string | object, format: string): Promise<{ publicKey: KeyLike, isTrusted: boolean } | { error: "UNABLE_TO_RESOLVE_PUBKEY" }> {
if (format != VerifiableCredentialFormat.VC_SD_JWT || typeof rawPresentation != 'string') {
return { error: "UNABLE_TO_RESOLVE_PUBKEY" };
}

let isTrusted = false;
const [h, , ] = rawPresentation.split('.');
const header = JSON.parse(base64url.decode(h)) as JWTHeaderParameters;
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");
isTrusted = false;
}
isTrusted = true;
console.info("Chain is trusted");
const cert = await importX509(pemCerts[0], header['alg'] as string);
return { isTrusted: isTrusted, publicKey: cert };
}
return { error: "UNABLE_TO_RESOLVE_PUBKEY" };
}
}

0 comments on commit 9a8ee59

Please sign in to comment.