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

Fix vc jwt tests #88

Merged
merged 12 commits into from
Aug 8, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# bedrock-vc-delivery ChangeLog

## 5.0.1 - 2024-08-dd

### Fixed
- Fix processing of VC-JWT VPs/VCs in OID4* combined workflows.

## 5.0.0 - 2024-08-05

### Added
Expand Down
83 changes: 60 additions & 23 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
* Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from '@bedrock/core';
import * as vcjwt from './vcjwt.js';
import {decodeId, generateId} from 'bnid';
import {decodeJwt} from 'jose';
import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020';
import {httpsAgent} from '@bedrock/https-agent';
import jsonata from 'jsonata';
import {serviceAgents} from '@bedrock/service-agent';
import {ZcapClient} from '@digitalbazaar/ezcap';

const {config} = bedrock;
const {config, util: {BedrockError}} = bedrock;

export async function evaluateTemplate({
workflow, exchange, typedTemplate
Expand Down Expand Up @@ -104,29 +104,66 @@ export function decodeLocalId({localId} = {}) {
}));
}

export async function unenvelopeCredential({envelopedCredential} = {}) {
let credential;
const {id} = envelopedCredential;
if(id?.startsWith('data:application/jwt,')) {
const format = 'application/jwt';
const jwt = id.slice('data:application/jwt,'.length);
const claimset = decodeJwt(jwt);
// FIXME: perform various field mappings as needed
console.log('VC-JWT claimset', credential);
return {credential: claimset.vc, format};
export async function unenvelopeCredential({
envelopedCredential, format
} = {}) {
const result = _getEnvelope({envelope: envelopedCredential, format});

// only supported format is VC-JWT at this time
const credential = vcjwt.decodeVCJWTCredential({jwt: result.envelope});
return {credential, ...result};
}

export async function unenvelopePresentation({
envelopedPresentation, format
} = {}) {
const result = _getEnvelope({envelope: envelopedPresentation, format});

// only supported format is VC-JWT at this time
const presentation = vcjwt.decodeVCJWTPresentation({jwt: result.envelope});

// unenvelope any VCs in the presentation
let {verifiableCredential = []} = presentation;
if(!Array.isArray(verifiableCredential)) {
verifiableCredential = [verifiableCredential];
}
throw new Error('Not implemented.');
if(verifiableCredential.length > 0) {
presentation.verifiableCredential = await Promise.all(
verifiableCredential.map(async vc => {
if(vc?.type !== 'EnvelopedVerifiableCredential') {
return vc;
}
const {credential} = await unenvelopeCredential({
envelopedCredential: vc
});
return credential;
}));
}
return {presentation, ...result};
}

export async function unenvelopePresentation({envelopedPresentation} = {}) {
const {id} = envelopedPresentation;
if(id?.startsWith('data:application/jwt,')) {
const format = 'application/jwt';
const jwt = id.slice('data:application/jwt,'.length);
const claimset = decodeJwt(jwt);
// FIXME: perform various field mappings as needed
console.log('VC-JWT claimset', claimset);
return {presentation: claimset.vp, format};
function _getEnvelope({envelope, format}) {
const isString = typeof envelope === 'string';
if(isString) {
// supported formats
if(format === 'application/jwt' || format === 'jwt_vc_json-ld') {
format = 'application/jwt';
}
} else {
const {id} = envelope;
if(id?.startsWith('data:application/jwt,')) {
format = 'application/jwt';
envelope = id.slice('data:application/jwt,'.length);
}
}

if(format === 'application/jwt' && envelope !== undefined) {
return {envelope, format};
}
throw new Error('Not implemented.');

throw new BedrockError(
`Unsupported credential or presentation envelope format "${format}".`, {
name: 'NotSupportedError',
details: {httpStatusCode: 400, public: true}
});
}
59 changes: 45 additions & 14 deletions lib/openId.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import * as exchanges from './exchanges.js';
import {
compile, createValidateMiddleware as validate
} from '@bedrock/validation';
import {evaluateTemplate, getWorkflowIssuerInstances} from './helpers.js';
import {
evaluateTemplate, getWorkflowIssuerInstances, unenvelopePresentation
} from './helpers.js';
import {importJWK, SignJWT} from 'jose';
import {
openIdAuthorizationResponseBody,
Expand Down Expand Up @@ -57,6 +59,8 @@ instantiating a new authorization server instance per VC exchange. */
const PRE_AUTH_GRANT_TYPE =
'urn:ietf:params:oauth:grant-type:pre-authorized_code';

const VC_CONTEXT_2 = 'https://www.w3.org/ns/credentials/v2';

// creates OID4VCI Authorization Server + Credential Delivery Server
// endpoints for each individual exchange
export async function createRoutes({
Expand Down Expand Up @@ -379,14 +383,33 @@ export async function createRoutes({
const {vp_token, presentation_submission} = req.body;

// JSON parse and validate `vp_token` and `presentation_submission`
const presentation = _jsonParse(vp_token, 'vp_token');
let presentation = _jsonParse(vp_token, 'vp_token');
const presentationSubmission = _jsonParse(
presentation_submission, 'presentation_submission');
_validate(validatePresentationSubmission, presentationSubmission);
_validate(validatePresentation, presentation);

const result = await _processAuthorizationResponse(
{req, presentation, presentationSubmission});
let envelope;
if(typeof presentation === 'string') {
// handle enveloped presentation
const {
envelope: raw, presentation: contents, format
} = await unenvelopePresentation({
envelopedPresentation: presentation,
// FIXME: check presentationSubmission for VP format
format: 'jwt_vc_json-ld'
});
_validate(validatePresentation, contents);
presentation = {
'@context': VC_CONTEXT_2,
id: `data:${format},${raw}`,
type: 'EnvelopedVerifiablePresentation'
};
envelope = {raw, contents, format};
} else {
_validate(validatePresentation, presentation);
}
const result = await _processAuthorizationResponse({
req, presentation, envelope, presentationSubmission
});
res.json(result);
}));

Expand Down Expand Up @@ -906,7 +929,7 @@ function _matchCredentialRequest(expected, cr) {
}

async function _processAuthorizationResponse({
req, presentation, presentationSubmission
req, presentation, envelope, presentationSubmission
}) {
const {config: workflow} = req.serviceObject;
const exchangeRecord = await req.getExchange();
Expand All @@ -917,17 +940,17 @@ async function _processAuthorizationResponse({
const {authorizationRequest, step} = arRequest;
({exchange} = arRequest);

// FIXME: if the VP is enveloped, remove the envelope to validate or
// run validation code after verification if necessary

// FIXME: check the VP against the presentation submission if requested
// FIXME: check the VP against "trustedIssuer" in VPR, if provided
const {presentationSchema} = step;
if(presentationSchema) {
// validate the received VP
// if the VP is enveloped, validate the contents of the envelope
const toValidate = envelope ? envelope.contents : presentation;

// validate the received VP / envelope contents
const {jsonSchema: schema} = presentationSchema;
const validate = compile({schema});
const {valid, error} = validate(presentation);
const {valid, error} = validate(toValidate);
if(!valid) {
throw error;
}
Expand All @@ -937,20 +960,21 @@ async function _processAuthorizationResponse({
const {verifiablePresentationRequest} = await oid4vp.toVpr(
{authorizationRequest});
const {allowUnprotectedPresentation = false} = step;
const {verificationMethod} = await verify({
const verifyResult = await verify({
workflow,
verifiablePresentationRequest,
presentation,
allowUnprotectedPresentation,
expectedChallenge: authorizationRequest.nonce
});
const {verificationMethod} = verifyResult;

// store VP results in variables associated with current step
const currentStep = exchange.step;
if(!exchange.variables.results) {
exchange.variables.results = {};
}
exchange.variables.results[currentStep] = {
const results = {
// common use case of DID Authentication; provide `did` for ease
// of use in template
did: verificationMethod?.controller || null,
Expand All @@ -961,6 +985,13 @@ async function _processAuthorizationResponse({
presentationSubmission
}
};
if(envelope) {
// normalize VP from inside envelope to `verifiablePresentation`
results.envelopedPresentation = presentation;
results.verifiablePresentation = verifyResult
.presentationResult.presentation;
}
exchange.variables.results[currentStep] = results;
exchange.sequence++;

// if there is something to issue, update exchange, do not complete it
Expand Down
17 changes: 12 additions & 5 deletions lib/vcapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import * as bedrock from '@bedrock/core';
import * as exchanges from './exchanges.js';
import {createChallenge as _createChallenge, verify} from './verify.js';
import {evaluateTemplate, unenvelopePresentation} from './helpers.js';
import {compile} from '@bedrock/validation';
import {evaluateTemplate} from './helpers.js';
import {issue} from './issue.js';
import {klona} from 'klona';
import {logger} from './logger.js';
Expand Down Expand Up @@ -96,15 +96,22 @@ export async function processExchange({req, res, workflow, exchange}) {
return;
}

// FIXME: if the VP is enveloped, remove the envelope to validate or
// run validation code after verification if necessary

const {presentationSchema} = step;
if(presentationSchema) {
// if the VP is enveloped, get the presentation from the envelope
let presentation;
if(receivedPresentation?.type === 'EnvelopedVerifiablePresentation') {
({presentation} = await unenvelopePresentation({
envelopedPresentation: receivedPresentation
}));
} else {
presentation = receivedPresentation;
}

// validate the received VP
const {jsonSchema: schema} = presentationSchema;
const validate = compile({schema});
const {valid, error} = validate(receivedPresentation);
const {valid, error} = validate(presentation);
if(!valid) {
throw error;
}
Expand Down
Loading