diff --git a/package.json b/package.json index ca3626f..3b9c5df 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "type": "module", "scripts": { "test": "mocha tests/ --reporter @digitalbazaar/mocha-w3c-interop-reporter --reporter-options abstract=\"$PWD/abstract.hbs\",reportDir=\"$PWD/reports\",respec=\"$PWD/respecConfig.json\",suiteLog='./suite.log',templateData=\"$PWD/reports/index.json\",title=\"VC v2.0 Interoperability Report\" --timeout 15000 --preserve-symlinks", - "lint": "eslint ." + "lint": "eslint .", + "test-issuance": "node test.mjs" }, "repository": { "type": "git", @@ -38,6 +39,9 @@ "@digitalbazaar/data-integrity": "^2.5.0", "@digitalbazaar/data-integrity-context": "^2.0.1", "@digitalbazaar/ed25519-multikey": "^1.0.1", + "@digitalbazaar/did-method-key": "^5.2.0", + "@digitalbazaar/ed25519-signature-2020": "^5.4.0", + "@digitalbazaar/ezcap": "^4.1.0", "@digitalbazaar/eddsa-rdfc-2022-cryptosuite": "^1.1.0", "@digitalbazaar/mocha-w3c-interop-reporter": "^1.5.0", "@digitalbazaar/vc": "^7.0.0", diff --git a/tests/4.13.2-envelopes.js b/tests/4.13.2-envelopes.js index 0dd8893..b4aed04 100644 --- a/tests/4.13.2-envelopes.js +++ b/tests/4.13.2-envelopes.js @@ -9,7 +9,9 @@ import { generateCredential, generateEnvelope, secureCredential, - setupMatrix + setupMatrix, + verifyCredential, + verifyPresentation } from './helpers.js'; import { vc_jwt, @@ -18,7 +20,6 @@ import { import assert from 'node:assert/strict'; import chai from 'chai'; import {filterByTag} from 'vc-test-suite-implementations'; -import {TestEndpoints} from './TestEndpoints.js'; const should = chai.should(); @@ -29,17 +30,16 @@ const {match} = filterByTag({tags: [tag]}); describe('Enveloped Verifiable Credentials', function() { setupMatrix.call(this, match); for(const [name, implementation] of match) { - const endpoints = new TestEndpoints({implementation, tag}); const issuer = implementation.issuers?.find( issuer => issuer.tags.has(tag)) || null; const verifier = implementation.verifiers?.find( verifier => verifier.tags.has(tag)) || null; describe(name, function() { - let envelopedCredential; + let verifiableCredential; let negativeFixture; before(async function() { - envelopedCredential = generateEnvelope({ + verifiableCredential = generateEnvelope({ type: 'EnvelopedVerifiableCredential', id: `data:application/vc+jwt,${vc_jwt}` }); @@ -57,22 +57,26 @@ describe('Enveloped Verifiable Credentials', function() { {issuer, credential: generateCredential()}); should.exist(issuedVc, 'Expected credential to be issued.'); issuedVc.should.have.property('@context'); + verifiableCredential = issuedVc; } if(verifier) { - await assert.doesNotReject(endpoints.verify(envelopedCredential), + await assert.doesNotReject( + verifyCredential({verifier, verifiableCredential}), 'Failed to accept an enveloped VC.'); // Replace context with an empty array - negativeFixture = structuredClone(envelopedCredential); + negativeFixture = structuredClone(verifiableCredential); negativeFixture['@context'] = []; - await assert.rejects(endpoints.verify(negativeFixture), - 'Failed to reject an enveloped VC with an empty context.'); + await assert.rejects( + verifyCredential({verifier, negativeFixture}), + 'Failed to reject an enveloped VC with invalid context.'); // Replace context with an invalid value - negativeFixture = structuredClone(envelopedCredential); + negativeFixture = structuredClone(verifiableCredential); negativeFixture['@context'] = 'https://www.w3.org/ns/credentials/examples/v2'; - await assert.rejects(endpoints.verify(negativeFixture), - 'Failed to reject an enveloped VC with an invalid context.'); + await assert.rejects( + verifyCredential({verifier, negativeFixture}), + 'Failed to reject an enveloped VC with invalid context.'); } }); @@ -85,18 +89,19 @@ describe('Enveloped Verifiable Credentials', function() { const issuedVc = await secureCredential( {issuer, credential: generateCredential()}); should.exist(issuedVc, 'Expected credential to be issued.'); - issuedVc.should.have.property('id').that.does - .include('data:', - `Expecting id field to be a 'data:' scheme URL [RFC2397].`); + issuedVc.should.have.property('@context'); + verifiableCredential = issuedVc; } if(verifier) { - await assert.doesNotReject(endpoints.verify(envelopedCredential), + await assert.doesNotReject( + verifyCredential({verifier, verifiableCredential}), 'Failed to accept an enveloped VC.'); // Remove data uri portion of the id field - negativeFixture = structuredClone(envelopedCredential); + negativeFixture = structuredClone(verifiableCredential); negativeFixture.id = negativeFixture.id.split(',').pop(); - await assert.rejects(endpoints.verify(negativeFixture), + await assert.rejects( + verifyCredential({verifier, negativeFixture}), 'Failed to reject an enveloped VC with an invalid data url id.'); } }); @@ -108,25 +113,27 @@ describe('Enveloped Verifiable Credentials', function() { const issuedVc = await secureCredential( {issuer, credential: generateCredential()}); should.exist(issuedVc, 'Expected credential to be issued.'); - issuedVc.should.have.property('type').that.is.equal( - 'EnvelopedVerifiableCredential', - `Expecting type field to be EnvelopedVerifiableCredential`); + issuedVc.should.have.property('@context'); + verifiableCredential = issuedVc; } if(verifier) { - await assert.doesNotReject(endpoints.verify(envelopedCredential), + await assert.doesNotReject( + verifyCredential({verifier, verifiableCredential}), 'Failed to accept an enveloped VC.'); // Remove type field - negativeFixture = structuredClone(envelopedCredential); + negativeFixture = structuredClone(verifiableCredential); delete negativeFixture.type; - await assert.rejects(endpoints.verify(negativeFixture), + await assert.rejects( + verifyCredential({verifier, negativeFixture}), 'Failed to reject an enveloped VC with an enveloped VC with a ' + 'missing `type`.'); // Replace type field - negativeFixture = structuredClone(envelopedCredential); - negativeFixture.type = ['VerifiableCredential']; - await assert.rejects(endpoints.verify(negativeFixture), + negativeFixture = structuredClone(verifiableCredential); + negativeFixture.type = 'VerifiableCredential'; + await assert.rejects( + verifyCredential({verifier, negativeFixture}), 'Failed to reject an enveloped VC with an ' + 'invalid `type`.'); } @@ -139,17 +146,16 @@ describe('Enveloped Verifiable Credentials', function() { describe('Enveloped Verifiable Presentations', function() { setupMatrix.call(this, match); for(const [name, implementation] of match) { - const endpoints = new TestEndpoints({implementation, tag}); const vpVerifier = implementation.vpVerifiers?.find( vpVerifier => vpVerifier.tags.has(tag)) || null; describe(name, function() { - let envelopedPresentation; + let verifiablePresentation; let negativeFixture; before(async function() { - envelopedPresentation = generateEnvelope({ + verifiablePresentation = generateEnvelope({ type: 'EnvelopedVerifiablePresentation', - id: `data:application/vp+jwt,${vp_jwt}` + id: `data:application/jwt,${vp_jwt}` }); }); beforeEach(addPerTestMetadata); @@ -162,21 +168,22 @@ describe('Enveloped Verifiable Presentations', function() { this.test.link = `https://w3c.github.io/vc-data-model/#enveloped-verifiable-presentations:~:text=The%20%40context%20property%20of%20the%20object%20MUST%20be%20present%20and%20include%20a%20context%2C%20such%20as%20the%20base%20context%20for%20this%20specification%2C%20that%20defines%20at%20least%20the%20id%2C%20type%2C%20and%20EnvelopedVerifiablePresentation%20terms%20as%20defined%20by%20the%20base%20context%20provided%20by%20this%20specification.`; if(vpVerifier) { - await assert.doesNotReject(endpoints.verifyVp(envelopedPresentation), + await assert.doesNotReject( + verifyPresentation({vpVerifier, verifiablePresentation}), 'Failed to accept an enveloped VP.'); // Replace context field with empty array - negativeFixture = structuredClone(envelopedPresentation); + negativeFixture = structuredClone(verifiablePresentation); negativeFixture['@context'] = []; await assert.rejects( - endpoints.verifyVp(negativeFixture), + verifyPresentation({vpVerifier, negativeFixture}), 'Failed to reject Enveloped VP missing contexts.'); // Replace context field with invalid context - negativeFixture = structuredClone(envelopedPresentation); + negativeFixture = structuredClone(verifiablePresentation); negativeFixture['@context'] = ['https://www.w3.org/ns/credentials/examples/v2']; await assert.rejects( - endpoints.verifyVp(negativeFixture), + verifyPresentation({vpVerifier, negativeFixture}), 'Failed to reject Enveloped VP missing contexts.'); } }); @@ -188,14 +195,15 @@ describe('Enveloped Verifiable Presentations', function() { this.test.link = `https://w3c.github.io/vc-data-model/#enveloped-verifiable-presentations:~:text=The%20id%20value%20of%20the%20object%20MUST%20be%20a%20data%3A%20URL%20%5BRFC2397%5D%20that%20expresses%20a%20secured%20verifiable%20presentation%20using%20an%20enveloping%20securing%20mechanism%2C%20such%20as%20Securing%20Verifiable%20Credentials%20using%20JOSE%20and%20COSE%20%5BVC%2DJOSE%2DCOSE%5D.`; if(vpVerifier) { - await assert.doesNotReject(endpoints.verifyVp(envelopedPresentation), + await assert.doesNotReject( + verifyPresentation({vpVerifier, verifiablePresentation}), 'Failed to accept an enveloped VP.'); // Remove data uri portion from id field - negativeFixture = structuredClone(envelopedPresentation); + negativeFixture = structuredClone(verifiablePresentation); negativeFixture.id = negativeFixture.id.split(',').pop(); await assert.rejects( - endpoints.verifyVp(negativeFixture), + verifyPresentation({vpVerifier, negativeFixture}), 'Failed to reject Enveloped VP with an id that is not a data url.'); } }); @@ -205,14 +213,15 @@ describe('Enveloped Verifiable Presentations', function() { this.test.link = `https://w3c.github.io/vc-data-model/#enveloped-verifiable-presentations:~:text=The%20type%20value%20of%20the%20object%20MUST%20be%20EnvelopedVerifiablePresentation.`; if(vpVerifier) { - await assert.doesNotReject(endpoints.verifyVp(envelopedPresentation), + await assert.doesNotReject( + verifyPresentation({vpVerifier, verifiablePresentation}), 'Failed to accept an enveloped VP.'); // Replace type field - negativeFixture = structuredClone(envelopedPresentation); + negativeFixture = structuredClone(verifiablePresentation); negativeFixture.type = ['VerifiablePresentation']; await assert.rejects( - endpoints.verifyVp(negativeFixture), + verifyPresentation({vpVerifier, negativeFixture}), 'Failed to reject VP w/o type "EnvelopedVerifiablePresentation".'); } }); diff --git a/tests/fixtures.js b/tests/fixtures.js index 5549438..6b1f352 100644 --- a/tests/fixtures.js +++ b/tests/fixtures.js @@ -1,6 +1,7 @@ /* eslint-disable max-len */ +export const challenge = '123456789'; export const vc_jwt = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwidmFsaWRGcm9tIjoiMjAxMC0wMS0wMVQwMDowMDowMFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsImRlZ3JlZSI6eyJ0eXBlIjoiQmFjaGVsb3JEZWdyZWUiLCJuYW1lIjoiQmFjaGVsb3Igb2YgU2NpZW5jZSBhbmQgQXJ0cyJ9fX0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5lZHUvaXNzdWVycy81NjUwNDkiLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.0fkQPZOKlD0Sl0A798KBUNMOdGq90McQQIEtKU9tgSd9K1kRcxWVKDXQJcn_FJqLvo2bk793EHk-RTeEL1HyAQ'; -export const vp_jwt = 'eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMzODQifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZUFsdW1uaUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDE5OjIzOjI0WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2V4YW1wbGUub3JnL2V4YW1wbGVzL2RlZ3JlZS5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.d2k4O3FytQJf83kLh-HsXuPvh6yeOlhJELVo5TF71gu7elslQyOf2ZItAXrtbXF4Kz9WivNdztOayz4VUQ0Mwa8yCDZkP9B2pH-9S_tcAFxeoeJ6Z4XnFuL_DOfkR1fP'; +export const vp_jwt = 'eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJFZERTQSIsICJraWQiOiAiZGlkOmtleTp6Nk1rajhoM2t6V1pyUGl1Y295WTlMR0NUcFhoQ3FCb1gzZG9EbUh6NU1hUHhudmkjejZNa2o4aDNreldaclBpdWNveVk5TEdDVHBYaENxQm9YM2RvRG1IejVNYVB4bnZpIn0.eyJpc3MiOiAiZGlkOmtleTp6Nk1rajhoM2t6V1pyUGl1Y295WTlMR0NUcFhoQ3FCb1gzZG9EbUh6NU1hUHhudmkiLCAiYXVkIjogImV4YW1wbGUuY29tIiwgIm5vbmNlIjogIjEyMzQ1Njc4OSIsICJ2cCI6IHsiQGNvbnRleHQiOiBbImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy92MiJdLCAidHlwZSI6IFsiVmVyaWZpYWJsZVByZXNlbnRhdGlvbiJdLCAiaG9sZGVyIjogImRpZDprZXk6ejZNa2o4aDNreldaclBpdWNveVk5TEdDVHBYaENxQm9YM2RvRG1IejVNYVB4bnZpIiwgInZlcmlmaWFibGVDcmVkZW50aWFsIjogW3siQGNvbnRleHQiOiAiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIiwgInR5cGUiOiAiRW52ZWxvcGVkVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCAiaWQiOiAiZGF0YTphcHBsaWNhdGlvbi9qd3QsZXlKaGJHY2lPaUpGVXpJMU5pSXNJbXRwWkNJNkltUnBaRHByWlhrNmVrUnVZV1ZVVVV0RVlsaFJWRFJpWVhSNFMyTnVUVkZaVVRoS2VXNXFjVU0xV0hveGRVMXJOV1YxUnpKb2REaHVjU042Ukc1aFpWUlJTMFJpV0ZGVU5HSmhkSGhMWTI1TlVWbFJPRXA1Ym1weFF6VlllakYxVFdzMVpYVkhNbWgwT0c1eEluMC5leUpwYzNNaU9pSmthV1E2YTJWNU9ucEVibUZsVkZGTFJHSllVVlEwWW1GMGVFdGpiazFSV1ZFNFNubHVhbkZETlZoNk1YVk5helZsZFVjeWFIUTRibkVpTENKemRXSWlPaUprYVdRNlpYaGhiWEJzWlRwaGJHbGpaU0lzSW5aaklqcDdJa0JqYjI1MFpYaDBJanBiSW1oMGRIQnpPaTh2ZDNkM0xuY3pMbTl5Wnk5dWN5OWpjbVZrWlc1MGFXRnNjeTkyTWlKZExDSjBlWEJsSWpwYklsWmxjbWxtYVdGaWJHVkRjbVZrWlc1MGFXRnNJbDBzSW1OeVpXUmxiblJwWVd4VGRXSnFaV04wSWpwN0ltbGtJam9pWkdsa09tVjRZVzF3YkdVNllXeHBZMlVpTENKdVlXMWxJam9pUVd4cFkyVWlmU3dpYVhOemRXVnlJam9pWkdsa09tdGxlVHA2Ukc1aFpWUlJTMFJpV0ZGVU5HSmhkSGhMWTI1TlVWbFJPRXA1Ym1weFF6VlllakYxVFdzMVpYVkhNbWgwT0c1eEluMTkucUU4VVliU0pmSWRZVV9MU3d4TEVJRHRqVHBMTGFwNkVZNTNKR3dhZy1UM3dyX2FROVNRem1pcUZFMUZQWEZnZkFVOEdIT2tTWTdBUm1feDVBRnZPTkEifV19fQ.9L0gNj8NbDTMZhCCNDL-q6aSKEddxuJKsX0FGGb7qooHsQQ7i_ZHQEo3hL2jzowWvHzrX4PGTrAhc8j4SIkFCg'; export const credential = { '@context': ['https://www.w3.org/ns/credentials/v2'], type: ['VerifiableCredential'], @@ -22,7 +23,6 @@ export const envelopedPresentation = { type: 'EnvelopedVerifiablePresentation', id: `data:application/vp+jwt,${vp_jwt}` }; - export const relatedResource = { id: 'https://www.w3.org/ns/credentials/v2', mediaType: 'application/ld+json', diff --git a/tests/helpers.js b/tests/helpers.js index 03af474..4af01a9 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -1,3 +1,6 @@ +import {challenge} from './fixtures.js'; +import {makeZcapRequest} from './zcapHandler.js'; + export function setupMatrix(match) { // this will tell the report // to make an interop matrix with this suite @@ -44,15 +47,56 @@ export const secureCredential = async ({ const {settings: {id: issuerId, options = {}}} = issuer; credential.issuer = issuerId; const body = {credential, options}; - const {data, result, error} = await issuer.post({json: body}); - if(!result || !result.ok) { - console.warn( - `initial vc creation failed for ${(result || error)?.requestUrl}`, - error - ); - return null; + if(issuer.settings.zcap) { + const response = await makeZcapRequest(issuer.settings, body); + return response?.data?.verifiableCredential; + } else { + const {data, result, error} = await issuer.post({json: body}); + if(!result || !result.ok) { + error; + return null; + } + return data; + } +}; + +export const verifyCredential = async ({ + verifier, + verifiableCredential, +}) => { + const {settings: {options = {}}} = verifier; + const body = {verifiableCredential, options}; + if(verifier.settings.zcap) { + const response = await makeZcapRequest(verifier.settings, body); + return response?.data?.verifiableCredential; + } else { + const {data, result, error} = await verifier.post({json: body}); + if(!result || !result.ok) { + error; + return null; + } + return data; + } +}; + +export const verifyPresentation = async ({ + vpVerifier, + verifiablePresentation, +}) => { + const {settings: {options = {}}} = vpVerifier; + options.challenge = challenge; + const body = {verifiablePresentation, options}; + if(vpVerifier.settings.zcap) { + const response = await makeZcapRequest(vpVerifier.settings, body); + return response?.data?.verifiableCredential; + } else { + const {data, result, error} = await vpVerifier.post({json: body}); + if(!result || !result.ok) { + error; + return null; + } + return data; } - return data; }; export function generateCredential({ @@ -83,3 +127,6 @@ export function generateEnvelope({ }; return envelopeCredential; } + +export const secureEnvelope = async ({ +}) => {}; diff --git a/tests/zcapHandler.js b/tests/zcapHandler.js new file mode 100644 index 0000000..d44c09b --- /dev/null +++ b/tests/zcapHandler.js @@ -0,0 +1,29 @@ +import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey'; +import {decodeSecretKeySeed} from 'bnid'; +import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020'; +import {ZcapClient} from '@digitalbazaar/ezcap'; + +export async function makeZcapRequest(settings, body) { + const capability = JSON.parse(settings.zcap.capability); + const controller = capability.controller; + // only supports did key + const id = controller + '#' + controller.slice('did:key:'.length); + const verificationKeyPair = await Ed25519Multikey.generate({ + id, + controller, + seed: decodeSecretKeySeed({ + secretKeySeed: process.env[settings.zcap.keySeed] + }) + }); + const zcapClient = new ZcapClient({ + SuiteClass: Ed25519Signature2020, + invocationSigner: verificationKeyPair.signer(), + delegationSigner: verificationKeyPair.signer(), + }); + const response = await zcapClient.write({ + url: settings.endpoint, + json: body, + capability: JSON.parse(settings.zcap.capability) + }); + return response; +}