Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduction of PresentationParserChain and PublicKeyResolverChain to increase flexibility #82

Merged
merged 3 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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].reverse()) {
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].reverse()) {
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" };
}
}
Loading