Skip to content

Commit

Permalink
Merge pull request #23 from wwWallet/selective-disclosure-jwt
Browse files Browse the repository at this point in the history
Selective disclosure jwt
  • Loading branch information
kkmanos authored Mar 19, 2024
2 parents e22f428 + e97cd0d commit f0cf28a
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 155 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
"dependencies": {
"@digitalbazaar/did-io": "^2.0.0",
"@digitalbazaar/did-method-key": "^5.1.0",
"@sd-jwt/core": "^0.2.1",
"@transmute/did-key-ed25519": "^0.3.0-unstable.10",
"@wwwallet/ssi-sdk": "^1.0.6",
"@wwwallet/ssi-sdk": "^1.0.8",
"ajv": "^8.11.0",
"axios": "^0.27.2",
"base64url": "^3.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class EdiplomasBlueprint implements SupportedCredentialProtocol {
},
};
const { jws } = await this.getCredentialIssuerConfig().getCredentialSigner()
.sign(payload, {});
.sign(payload, {}, null);
const response = {
format: this.getFormat(),
credential: jws
Expand Down
33 changes: 30 additions & 3 deletions src/entities/VerifiablePresentation.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ export enum VerificationStatus {
VALID
}

export type ClaimRecord = {
name: string;
value: string;
};

export type PresentationClaims = {
[descriptor_id: string]: Array<ClaimRecord>;
}

@Entity({ name: "verifiable_presentation" })
export class VerifiablePresentationEntity {
@PrimaryGeneratedColumn()
Expand Down Expand Up @@ -54,14 +63,32 @@ export class VerifiablePresentationEntity {
}


@Column({ name: "format", type: "enum", enum: VerifiableCredentialFormat, nullable: true })
format?: VerifiableCredentialFormat;

@Column({ name: "claims", type: "blob", nullable: true })
// @ts-ignore
/**
* Includes the claims that were requested from the presentation definition
*/
private _claims?: Buffer;
set claims(value: PresentationClaims | null) {
if (value) {
this._claims = Buffer.from(JSON.stringify(value));
return;
}
this._claims = undefined;
}

get claims(): PresentationClaims | null {
if (this._claims) {
return JSON.parse(this._claims?.toString()) as PresentationClaims;
}
return null;
}

@Column({ name: "date", type: "date", nullable: true })
date?: Date;

@Column({ name: "format", type: "enum", enum: VerifiableCredentialFormat, nullable: true })
format?: VerifiableCredentialFormat;

@Column({ name: "status", type: "boolean", nullable: true })
status?: boolean;
Expand Down
125 changes: 88 additions & 37 deletions src/services/OpenidForPresentationReceivingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@ import { VerifiableCredentialFormat } from "../types/oid4vci";
import { AuthorizationRequestQueryParamsSchemaType } from "../types/oid4vci";
import { TYPES } from "./types";
import { importJWK, jwtVerify } from "jose";
import { randomUUID } from "crypto";
import { KeyLike, createHash, randomUUID, verify } from "crypto";
import base64url from "base64url";
import { PresentationDefinitionType, PresentationSubmission } from "@wwwallet/ssi-sdk";
import 'reflect-metadata';
import { JSONPath } from "jsonpath-plus";
import { Repository } from "typeorm";
import { VerifiablePresentationEntity } from "../entities/VerifiablePresentation.entity";
import { ClaimRecord, PresentationClaims, VerifiablePresentationEntity } from "../entities/VerifiablePresentation.entity";
import AppDataSource from "../AppDataSource";
import { verificationCallback } from "../configuration/verificationCallback";
import { AuthorizationServerState } from "../entities/AuthorizationServerState.entity";
import config from "../../config";
import { DidKeyResolverService } from "./DidKeyResolverService";
import { HasherAlgorithm, HasherAndAlgorithm, SdJwt, SignatureAndEncryptionAlgorithm, Verifier } from "@sd-jwt/core";

const hasherAndAlgorithm: HasherAndAlgorithm = {
hasher: (input: string) => createHash('sha256').update(input).digest(),
algorithm: HasherAlgorithm.Sha256
}

type VerifierState = {
callbackEndpoint?: string;
Expand Down Expand Up @@ -217,15 +222,10 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta

}
else if (vp_token) {
const header = JSON.parse(base64url.decode(vp_token.split('.')[0])) as { kid: string, alg: string };
const jwk = await this.didKeyResolverService.getPublicKeyJwk(header.kid.split('#')[0]);
const pubKey = await importJWK(jwk, header.alg as string);

try {
const { payload } = await jwtVerify(vp_token, pubKey, {
clockTolerance: CLOCK_TOLERANCE
// audience: this.configurationService.getConfiguration().baseUrl,
});
const payload = JSON.parse(base64url.decode(vp_token.split('.')[1])) as any;

const { nonce } = payload;
// load verifier state by nonce
if (!verifierState) {
Expand Down Expand Up @@ -287,7 +287,7 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
if (state) {
msg = { ...msg, state } as any;
}
const { error, error_description } = await this.validateVpToken(vp_token, presentationSubmissionObject as PresentationSubmission);
const { presentationClaims, error, error_description } = await this.validateVpToken(vp_token, presentationSubmissionObject as PresentationSubmission);
if (error && error_description) {
msg = { ...msg, error: error.message, error_description: error_description?.message };
console.error(msg);
Expand All @@ -298,13 +298,12 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta

// store presentation
const newVerifiablePresentation = new VerifiablePresentationEntity()
newVerifiablePresentation.format = VerifiableCredentialFormat.JWT_VC_JSON;
newVerifiablePresentation.presentation_definition_id = presentation_submission.definition_id;
newVerifiablePresentation.claims = presentationClaims ?? null;
newVerifiablePresentation.status = true;
newVerifiablePresentation.raw_presentation = vp_token;
newVerifiablePresentation.presentation_submission = presentationSubmissionObject;
newVerifiablePresentation.date = new Date();
console.log("Verifier state id = ", verifierStateId)
newVerifiablePresentation.state = verifierStateId as string;

this.verifiablePresentationRepository.save(newVerifiablePresentation);
Expand Down Expand Up @@ -338,7 +337,8 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
throw new Error("OpenID4VP Authorization Response failed. Path not implemented");
}

private async validateVpToken(vp_token: string, presentation_submission: PresentationSubmission): Promise<{ error?: Error, error_description?: Error}> {
private async validateVpToken(vp_token: string, presentation_submission: PresentationSubmission): Promise<{ presentationClaims?: PresentationClaims, error?: Error, error_description?: Error}> {
let presentationClaims: PresentationClaims = {};
const payload = JSON.parse(base64url.decode(vp_token.split('.')[1])) as { nonce: string, vp: { verifiableCredential: string[] } };

const verifierStateId = nonces.get(payload.nonce);
Expand All @@ -354,32 +354,82 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
}


console.log("Presentation SUB = ", presentation_submission)
for (const desc of presentation_submission.descriptor_map) {
if (!presentationClaims[desc.id]) {
presentationClaims[desc.id] = [];
}
const path = desc?.path as string;
let vcjwt = JSONPath({ json: payload.vp, path: path });
let vcjwt = JSONPath({ json: payload.vp, path: path })[0];
if (vcjwt.length == 0) {
return { error: new Error("VC_NOT_FOUND"), error_description: new Error(`Path on descriptor ${desc.id} not matching to a credential`)};
}
vcjwt = vcjwt[0]; // get the first result

// if (await this.isExpired(vcjwt)) {
// const msg = { error: new Error("access_denied"), error_description: new Error(`${desc.id} is expired`) };
// console.error(msg)
// return msg;
// }
// if (await this.isNotValidYet(vcjwt)) {
// const msg = { error: new Error("access_denied"), error_description: new Error(`${desc.id} is not valid yet`) };
// console.error(msg)
// return msg;
// }
// if (await this.isRevoked(vcjwt)) {
// const msg = { error: new Error("access_denied"), error_description: new Error(`${desc.id} is revoked`) };
// console.error(msg)
// return msg;
// }
const input_descriptor = verifierState.presentation_definition.input_descriptors.filter((input_desc) => input_desc.id == desc.id)[0];
if (!input_descriptor) {
return { error: new Error("Input descriptor not found") };
}

if (desc.format == VerifiableCredentialFormat.VC_SD_JWT) {
const requiredClaimNames = input_descriptor.constraints.fields.map((field) => {
const fieldPath = field.path[0];
const splittedPath = fieldPath.split('.');
return splittedPath[splittedPath.length - 1]; // return last part of the path
});

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

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

const issuerPublicKeyJwk = await this.didKeyResolverService.getPublicKeyJwk(issuerDID);
const alg = (JSON.parse(base64url.decode(vcjwt.split('.')[0])) as any).alg;
const issuerPublicKey = await importJWK(issuerPublicKeyJwk, alg);
const verifyCb: Verifier = ({ header, message, signature }) => {
if (header.alg !== SignatureAndEncryptionAlgorithm.ES256) {
throw new Error('only ES256 is supported')
}
return verify(null, Buffer.from(message), issuerPublicKey as KeyLike, signature)
}

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

input_descriptor.constraints.fields.map((field) => {
if (!presentationClaims[desc.id]) {
presentationClaims[desc.id] = []; // initialize
}
const fieldPath = field.path[0]; // get first path
const value = String(JSONPath({ path: fieldPath, json: prettyClaims.vc as any })[0]);
const splittedPath = fieldPath.split('.');
const claimName = splittedPath[splittedPath.length - 1];
presentationClaims[desc.id].push({ name: claimName, value: value } as ClaimRecord);
});
console.log("Verification result = ", verificationResult)
if (!verificationResult.isValid) {
return { error: new Error("SD_JWT_VERIFICATION_FAILURE"), error_description: new Error(`Verification result ${JSON.stringify(verificationResult)}`) };
}
}
else if (desc.format == VerifiableCredentialFormat.JWT_VC) {
const jwtPayload = (JSON.parse(base64url.decode(vcjwt.split('.')[1])) as any);
const issuerDID = jwtPayload.iss;
const issuerPublicKeyJwk = await this.didKeyResolverService.getPublicKeyJwk(issuerDID);
const alg = (JSON.parse(base64url.decode(vcjwt.split('.')[0])) as any).alg;
const issuerPublicKey = await importJWK(issuerPublicKeyJwk, alg);

await jwtVerify(vcjwt, issuerPublicKey);

input_descriptor.constraints.fields.map((field) => {
if (!presentationClaims[desc.id]) {
presentationClaims[desc.id] = []; // initialize
}
const fieldPath = field.path[0]; // get first path
const value = String(JSONPath({ path: fieldPath, json: jwtPayload.vc })[0]);
const splittedPath = fieldPath.split('.');
const claimName = splittedPath[splittedPath.length - 1];
presentationClaims[desc.id].push({ name: claimName, value: value } as ClaimRecord);
});
}
}
return {};
return { presentationClaims };
}

//@ts-ignore
Expand Down Expand Up @@ -415,16 +465,17 @@ export class OpenidForPresentationsReceivingService implements OpenidForPresenta
}


public async getPresentationByState(state: string): Promise<{ status: boolean, presentation?: string }> {
public async getPresentationByState(state: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, rawPresentation?: string }> {
const vp = await this.verifiablePresentationRepository.createQueryBuilder('vp')
.where("state = :state", { state: state })
.getOne();

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

if (vp)
return { status: true, presentation: vp.raw_presentation };
return { status: true, presentationClaims: vp.claims, rawPresentation: vp?.raw_presentation };
else
return { status: false };
}
Expand Down
5 changes: 3 additions & 2 deletions src/services/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { OpenidForPresentationsConfiguration } from "./types/OpenidForPresentati
import 'reflect-metadata';
import { AuthorizationDetailsSchemaType, CredentialSupported } from "../types/oid4vci";
import { CredentialIssuersRepository } from "../lib/CredentialIssuersRepository";
import { PresentationClaims } from "../entities/VerifiablePresentation.entity";

export interface CredentialSigner {
sign(payload: any, headers: JWTHeaderParameters | {}): Promise<{ jws: string }>;
sign(payload: any, headers: JWTHeaderParameters | {}, disclosureFrame: any | undefined): Promise<{ jws: string }>;
getPublicKeyJwk(): Promise<{ jwk: JWK }>;
getDID(): Promise<{ did: string }>;
}
Expand Down Expand Up @@ -36,7 +37,7 @@ export interface OpenidForPresentationsReceivingInterface {

generateAuthorizationRequestURL(ctx: { req: Request, res: Response }, presentation_definition_id: string, directPostEndpoint?: string): Promise<{ url: URL; stateId: string }>;
getPresentationDefinitionHandler(ctx: { req: Request, res: Response }): Promise<void>;
getPresentationByState(state: string): Promise<{ status: boolean, presentation?: string }>;
getPresentationByState(state: string): Promise<{ status: boolean, presentationClaims?: PresentationClaims, rawPresentation?: string }>;

/**
* @throws
Expand Down
5 changes: 5 additions & 0 deletions src/types/oid4vci/oid4vci.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ export type ProofPayload = {
export enum VerifiableCredentialFormat {
JWT_VC_JSON = "jwt_vc_json",
JWT_VC = "jwt_vc",
VC_SD_JWT = "vc+sd-jwt",
}

export enum VerifiablePresentationFormat {
JWT_VP = "jwt_vp"
}

export enum ProofType {
Expand Down
18 changes: 9 additions & 9 deletions src/verifier/verifierRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { appContainer } from "../services/inversify.config";
import { TYPES } from "../services/types";
import locale from "../configuration/locale";
import * as qrcode from 'qrcode';
import base64url from "base64url";
import config from "../../config";
import base64url from "base64url";

const verifierRouter = Router();
// const verifiablePresentationRepository: Repository<VerifiablePresentationEntity> = AppDataSource.getRepository(VerifiablePresentationEntity);
Expand All @@ -27,17 +27,17 @@ verifierRouter.get('/public/definitions', async (req, res) => {

verifierRouter.get('/success/status', async (req, res) => { // response with the status of the presentation (this endpoint should be protected)
const state = req.query.state;
const {status, presentation} = await openidForPresentationReceivingService.getPresentationByState(state as string);
if (!presentation) {
const {status, presentationClaims, rawPresentation } = await openidForPresentationReceivingService.getPresentationByState(state as string);
if (!presentationClaims) {
return res.send({ status: false, error: "Presentation not received" });
}
return res.send({ status, presentation });
return res.send({ status, presentationClaims, presentation: rawPresentation });
})

verifierRouter.get('/success', async (req, res) => {
const state = req.query.state;
const {status, presentation} = await openidForPresentationReceivingService.getPresentationByState(state as string);
if (!presentation) {
const {status, presentationClaims, rawPresentation } = await openidForPresentationReceivingService.getPresentationByState(state as string);
if (!presentationClaims || !rawPresentation) {
return res.render('error.pug', {
msg: "Failed to get presentation",
code: 0,
Expand All @@ -46,16 +46,16 @@ verifierRouter.get('/success', async (req, res) => {
})
}

const presentationPayload = JSON.parse(base64url.decode(presentation.split('.')[1])) as any;
const presentationPayload = JSON.parse(base64url.decode(rawPresentation.split('.')[1])) as any;
const credentials = presentationPayload.vp.verifiableCredential.map((vcString: any) => {
return JSON.parse(base64url.decode(vcString.split('.')[1]));
}).map((credential: any) => credential.vc);

console.log("Credential payloads = ", credentials)

return res.render('verifier/success.pug', {
lang: req.lang,
locale: locale[req.lang],
status: status,
presentationClaims: presentationClaims,
credentialPayloads: credentials,
})
})
Expand Down
Loading

0 comments on commit f0cf28a

Please sign in to comment.