-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #82 from wwWallet/verifier-conf-extensions
Introduction of PresentationParserChain and PublicKeyResolverChain to increase flexibility
- Loading branch information
Showing
8 changed files
with
202 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }; | ||
} | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }; | ||
} | ||
} |