From cf108a67b6c49b352f5fd5e0245b3f2400fb9f84 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 6 Sep 2024 17:17:42 -0400 Subject: [PATCH 1/9] update deps; remove unused code; fix tests --- package.json | 13 +++------- src/app.js | 1 - src/didAuth.js | 17 +++++-------- src/generate.js | 24 ------------------ src/securityLoader.js | 35 --------------------------- src/test-fixtures/nocks/directTest.js | 2 +- src/test-fixtures/nocks/vprTest.js | 2 +- 7 files changed, 12 insertions(+), 82 deletions(-) delete mode 100644 src/generate.js delete mode 100644 src/securityLoader.js diff --git a/package.json b/package.json index 9b1ea8e..f27581a 100644 --- a/package.json +++ b/package.json @@ -10,20 +10,15 @@ "test": "NODE_OPTIONS=--experimental-vm-modules npx mocha --timeout 10000 -r dotenv/config dotenv_config_path=src/test-fixtures/.env.testing src/app.test.js " }, "dependencies": { - "@digitalbazaar/did-io": "^2.0.0", - "@digitalbazaar/did-method-key": "^5.1.0", - "@digitalbazaar/ed25519-signature-2020": "^5.2.0", + "@digitalbazaar/ed25519-signature-2020": "^5.4.0", "@digitalbazaar/ed25519-verification-key-2020": "^4.1.0", - "@digitalbazaar/vc": "^6.0.1", + "@digitalbazaar/vc": "^7.0.0", + "@digitalcredentials/security-document-loader": "^6.0.0", "axios": "^1.4.0", "cors": "^2.8.5", - "credentials-context": "^2.0.0", "debug": "~2.6.9", - "did-context": "^3.1.1", "dotenv": "^16.0.3", - "ed25519-signature-2020-context": "^1.1.0", "express": "~4.16.1", - "jsonld-document-loader": "^2.0.0", "morgan": "~1.9.1", "winston": "^3.9.0" }, @@ -38,7 +33,7 @@ "dcc" ], "engines": { - "node": ">=16.0" + "node": ">=20.0" }, "author": { "name": "Digital Credentials Consortium", diff --git a/src/app.js b/src/app.js index 40389c2..491ad05 100644 --- a/src/app.js +++ b/src/app.js @@ -10,7 +10,6 @@ import verifyAuthHeader from './verifyAuthHeader.js' import { getConfig } from './config.js' import testVC from './testVC.js'; import CoordinatorException from './CoordinatorException.js'; -import { getSignedDIDAuth, verifyDIDAuth } from './didAuth.js'; async function callService(endpoint, body) { diff --git a/src/didAuth.js b/src/didAuth.js index 2ae17eb..761107f 100644 --- a/src/didAuth.js +++ b/src/didAuth.js @@ -1,11 +1,12 @@ -import {signPresentation, createPresentation, verify} from '@digitalbazaar/vc'; +import {signPresentation, createPresentation} from '@digitalbazaar/vc'; import {Ed25519VerificationKey2020} from '@digitalbazaar/ed25519-verification-key-2020'; import {Ed25519Signature2020} from '@digitalbazaar/ed25519-signature-2020'; -import { securityLoader } from './securityLoader.js'; + +import { securityLoader } from '@digitalcredentials/security-document-loader' const documentLoader = securityLoader().build() -const signingKeyPairForTesting = await Ed25519VerificationKey2020.generate( +const key = await Ed25519VerificationKey2020.generate( { seed: new Uint8Array ([ 217, 87, 166, 30, 75, 106, 132, 55, @@ -16,18 +17,12 @@ const signingKeyPairForTesting = await Ed25519VerificationKey2020.generate( controller: "did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP" } ) -const suiteForSigning = new Ed25519Signature2020({key: signingKeyPairForTesting}); -const suiteForVerification = new Ed25519Signature2020(); +const suite = new Ed25519Signature2020({key}); export const getSignedDIDAuth = async (holder = 'did:ex:12345', challenge) => { const presentation = createPresentation({holder}); return await signPresentation({ - presentation, suite: suiteForSigning, challenge, documentLoader + presentation, suite, challenge, documentLoader }); } -export const verifyDIDAuth = async (presentation, challenge) => { - const result = await verify({presentation, challenge, suite: suiteForVerification, documentLoader}); - return result.verified -} - diff --git a/src/generate.js b/src/generate.js deleted file mode 100644 index 5eed1cd..0000000 --- a/src/generate.js +++ /dev/null @@ -1,24 +0,0 @@ -import { generateSecretKeySeed, decodeSecretKeySeed } from '@digitalcredentials/bnid'; -import { driver } from '@digitalcredentials/did-method-key'; - -export default async function generateSeed() { - const seed = await generateSecretKeySeed(); - const decodedSeed = await decodeSeed(seed) - const didKeyDriver = driver(); - const { didDocument } = await didKeyDriver.generate({ seed: decodedSeed }); - const did = didDocument.id - return {seed, did, didDocument} -} - -const decodeSeed = async (secretKeySeed) => { - let secretKeySeedBytes // Uint8Array; - if (secretKeySeed.startsWith('z')) { - // This is a multibase-decoded key seed, like those generated by @digitalcredentials/did-cli - secretKeySeedBytes = decodeSecretKeySeed({ secretKeySeed }); - } else if (secretKeySeed.length >= 32) { - secretKeySeedBytes = (new TextEncoder()).encode(secretKeySeed).slice(0, 32); - } else { - throw TypeError('"secretKeySeed" must be at least 32 bytes, preferably multibase-encoded.'); - } - return secretKeySeedBytes; -} \ No newline at end of file diff --git a/src/securityLoader.js b/src/securityLoader.js deleted file mode 100644 index 8dca752..0000000 --- a/src/securityLoader.js +++ /dev/null @@ -1,35 +0,0 @@ -import * as didKey from '@digitalbazaar/did-method-key'; -import didContext from 'did-context'; -import ed25519 from 'ed25519-signature-2020-context'; -import credentialsContext from 'credentials-context'; -import { JsonLdDocumentLoader } from 'jsonld-document-loader'; -import { Ed25519VerificationKey2020 } from '@digitalbazaar/ed25519-verification-key-2020' -import { CachedResolver } from '@digitalbazaar/did-io'; - -const didKeyDriver = didKey.driver(); -didKeyDriver.use({ - multibaseMultikeyHeader: 'z6Mk', - fromMultibase: Ed25519VerificationKey2020.from -}); - -const resolver = new CachedResolver(); -resolver.use(didKeyDriver); - -export function securityLoader() { - - const staticLoader = new JsonLdDocumentLoader(); - - staticLoader.addStatic(ed25519.constants.CONTEXT_URL, - ed25519.contexts.get(ed25519.constants.CONTEXT_URL)); - - staticLoader.addStatic(didContext.constants.DID_CONTEXT_URL, - didContext.contexts.get(didContext.constants.DID_CONTEXT_URL)); - - staticLoader.addStatic(credentialsContext.constants.CREDENTIALS_CONTEXT_V1_URL, - credentialsContext.contexts.get(credentialsContext.constants.CREDENTIALS_CONTEXT_V1_URL)); - - staticLoader.setDidResolver(resolver); - - return staticLoader - -} diff --git a/src/test-fixtures/nocks/directTest.js b/src/test-fixtures/nocks/directTest.js index 5b366e3..ca7a280 100644 --- a/src/test-fixtures/nocks/directTest.js +++ b/src/test-fixtures/nocks/directTest.js @@ -95,7 +95,7 @@ const signedVcWithId = { const signedVp = { "@context": [ - "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/ns/credentials/v2", "https://w3id.org/security/suites/ed25519-2020/v1" ], "type": [ diff --git a/src/test-fixtures/nocks/vprTest.js b/src/test-fixtures/nocks/vprTest.js index 39969ae..88aa036 100644 --- a/src/test-fixtures/nocks/vprTest.js +++ b/src/test-fixtures/nocks/vprTest.js @@ -50,7 +50,7 @@ nock('http://localhost:4004', {"encodedQueryParams":true}) // that we generate as part of the test nock('http://localhost:4004', {"encodedQueryParams":true}) - .post('/exchange/34913bac-b4d7-4c98-a598-c259e8d2925e/199ea83d-3b8d-44f0-8509-d10ce02f5c7c', {"@context":["https://www.w3.org/2018/credentials/v1","https://w3id.org/security/suites/ed25519-2020/v1"],"type":["VerifiablePresentation"],"holder":/.+/i,"proof":{"type":"Ed25519Signature2020","created":/.+/i,"verificationMethod":"did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP#z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP","proofPurpose":"authentication","challenge":"199ea83d-3b8d-44f0-8509-d10ce02f5c7c","proofValue":/.+/i}}) + .post('/exchange/34913bac-b4d7-4c98-a598-c259e8d2925e/199ea83d-3b8d-44f0-8509-d10ce02f5c7c', {"@context":["https://www.w3.org/ns/credentials/v2","https://w3id.org/security/suites/ed25519-2020/v1"],"type":["VerifiablePresentation"],"holder":/.+/i,"proof":{"type":"Ed25519Signature2020","created":/.+/i,"verificationMethod":"did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP#z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP","proofPurpose":"authentication","challenge":"199ea83d-3b8d-44f0-8509-d10ce02f5c7c","proofValue":/.+/i}}) .reply(200, {"vc":{"@context":["https://www.w3.org/2018/credentials/v1","https://purl.imsglobal.org/spec/ob/v3p0/context.json","https://w3id.org/vc/status-list/2021/v1"],"id":"urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1","type":["VerifiableCredential","OpenBadgeCredential"],"issuer":{"id":"did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC","type":"Profile","name":"University of Wonderful","description":"The most wonderful university","url":"https://wonderful.edu/","image":{"id":"https://user-images.githubusercontent.com/947005/133544904-29d6139d-2e7b-4fe2-b6e9-7d1022bb6a45.png","type":"Image"}},"issuanceDate":"2020-01-01T00:00:00Z","name":"A Simply Wonderful Course","credentialSubject":{"type":"AchievementSubject","achievement":{"id":"http://wonderful.wonderful","type":"Achievement","criteria":{"narrative":"Completion of the Wonderful Course - well done you!"},"description":"Wonderful.","name":"Introduction to Wonderfullness"}}},"retrievalId":"someId","tenantName":"UN_PROTECTED_TEST","exchangeHost":"http://localhost:4005","transactionId":"199ea83d-3b8d-44f0-8509-d10ce02f5c7c","exchangeId":"34913bac-b4d7-4c98-a598-c259e8d2925e"}, [ 'X-Powered-By', 'Express', From 1834d7232320a092f1085d3e73da3de9df3a7c34 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 6 Sep 2024 17:41:58 -0400 Subject: [PATCH 2/9] update node version in action workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a1155d..07766e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From ce07cc6f085c1c963f4cda1449f66134a3664c44 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sat, 7 Sep 2024 08:05:26 -0400 Subject: [PATCH 3/9] fix status update endpoint --- src/app.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/app.js b/src/app.js index 491ad05..54ceed8 100644 --- a/src/app.js +++ b/src/app.js @@ -24,7 +24,7 @@ async function callService(endpoint, body) { export async function build(opts = {}) { - const { enableStatusService, coordinatorService, statusService, signingService, transactionService, exchangeHost } = getConfig(); + const { enableStatusService, statusService, signingService, transactionService, exchangeHost } = getConfig(); var app = express(); // Add the middleware to write access logs app.use(accessLogger()); @@ -112,7 +112,6 @@ export async function build(opts = {}) { app.post('/exchange/setup', async (req, res, next) => { try { - //const tenantName = req.params.tenantName //the issuer instance/tenant with which to sign const exchangeData = req.body; // TODO: CHECK THE INCOMING DATA FOR CORRECTNESS HERE if (!exchangeData || !Object.keys(exchangeData).length) throw new CoordinatorException(400, 'You must provide data for the exchange. Check the README for details.') @@ -183,7 +182,6 @@ export async function build(opts = {}) { await callService(`http://${statusService}/credentials/status/allocate`, unSignedVC) : unSignedVC - // sign the credential const signedVC = await callService(`http://${signingService}/instance/${tenantName.toLowerCase()}/credentials/sign`, vcReadyToSign) return res.json(signedVC) @@ -201,12 +199,18 @@ export async function build(opts = {}) { async (req, res, next) => { if (!enableStatusService) return res.status(405).send('The status service has not been enabled.') try { - await verifyAccess(req.headers.authorization, req.params.tenantName) - const updateResult = await callService(`http://${statusService}/instance/${tenantName}/credentials/sign`, vcWithStatus) - return res.json(updateResult) + await verifyAuthHeader(req.headers.authorization, req.params.tenantName) + const statusUpdate = req.body + // NOTE: we throw the error here which will then be caught by middleware errorhandler + if (!statusUpdate || !Object.keys(statusUpdate).length) throw new CoordinatorException(400, 'A status update must be provided in the body.') + const updateResult = await callService(`http://${statusService}/credentials/status`, statusUpdate) + return res.json(updateResult) } catch (error) { - // have to catch and forward async errors to middleware: - next(error) + if (error.response?.status === 404) { + next({ code: 404, message: 'Not found.' }) + } + // otherwise, forward the error to middleware: + next(error) } }) From 875f74ea3d9897faf57f401508a6bb98764127eb Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sat, 7 Sep 2024 09:16:48 -0400 Subject: [PATCH 4/9] add did-key and did-web generator endpoints --- src/app.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app.js b/src/app.js index 54ceed8..b31e5fb 100644 --- a/src/app.js +++ b/src/app.js @@ -219,6 +219,17 @@ export async function build(opts = {}) { return res.json(response.data) }); + app.get('/did-key-generator', async (req, res, next) => { + const response = await axios.get(`http://${signingService}/did-key-generator`) + return res.json(response.data) + }) + + app.post('/did-web-generator', async (req, res, next) => { + const body = req.body + const response = await axios.post(`http://${signingService}/did-web-generator`, body) + return res.json(response.data) + }) + // Attach the error handling middleware calls, in order they should run app.use(errorLogger) app.use(errorHandler) From 7ed3bda54076e41878d6e923916cfaa541272360 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sat, 7 Sep 2024 17:45:20 -0400 Subject: [PATCH 5/9] add status list proxy call --- src/app.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/app.js b/src/app.js index b31e5fb..8211954 100644 --- a/src/app.js +++ b/src/app.js @@ -214,6 +214,23 @@ export async function build(opts = {}) { } }) + app.get('/status/:statusCredentialId', async function (req, res, next) { + if (!enableStatusService) next({ code: 405, message: 'The status service has not been enabled.' }) + const statusCredentialId = req.params.statusCredentialId + try { + const { data: statusCredential } = await axios.get(`http://${statusService}/${statusCredentialId}`) + return res.status(200).json(statusCredential) + } catch (error) { + if (error.response.status === 404) { + next({ code: 404, message: 'No status credential found for that id.' }) + } else { + next(error) + } + } + return res.status(500).send({ message: 'Server error.' }) + }) + + app.get('/seedgen', async (req, res, next) => { const response = await axios.get(`http://${signingService}/seedgen`) return res.json(response.data) From fb58babb7b1d49e5c8a87ea44fc592e6bc9c61bc Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Sun, 8 Sep 2024 21:02:24 -0400 Subject: [PATCH 6/9] add status tests --- src/app.js | 6 +- src/app.test.js | 132 +++++++++++++++++- src/test-fixtures/.env.testing | 2 +- src/test-fixtures/nocks/status_list_nock.js | 34 +++++ .../nocks/unknown_status_list_nock.js | 7 + 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 src/test-fixtures/nocks/status_list_nock.js create mode 100644 src/test-fixtures/nocks/unknown_status_list_nock.js diff --git a/src/app.js b/src/app.js index 8211954..6c84973 100644 --- a/src/app.js +++ b/src/app.js @@ -197,7 +197,7 @@ export async function build(opts = {}) { // the body will look like: {credentialId: '23kdr', credentialStatus: [{type: 'BitstringStatusListCredential', status: 'revoked'}]} app.post('/instance/:tenantName/credentials/status', async (req, res, next) => { - if (!enableStatusService) return res.status(405).send('The status service has not been enabled.') + if (!getConfig().enableStatusService) return res.status(405).send('The status service has not been enabled.') try { await verifyAuthHeader(req.headers.authorization, req.params.tenantName) const statusUpdate = req.body @@ -215,7 +215,7 @@ export async function build(opts = {}) { }) app.get('/status/:statusCredentialId', async function (req, res, next) { - if (!enableStatusService) next({ code: 405, message: 'The status service has not been enabled.' }) + if (!getConfig().enableStatusService) next({ code: 405, message: 'The status service has not been enabled.' }) const statusCredentialId = req.params.statusCredentialId try { const { data: statusCredential } = await axios.get(`http://${statusService}/${statusCredentialId}`) @@ -229,7 +229,7 @@ export async function build(opts = {}) { } return res.status(500).send({ message: 'Server error.' }) }) - + app.get('/seedgen', async (req, res, next) => { const response = await axios.get(`http://${signingService}/seedgen`) diff --git a/src/app.test.js b/src/app.test.js index 0cb8fb0..0bdc59a 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -8,10 +8,13 @@ import unprotectedWalletQueryNock from './test-fixtures/nocks/unprotectedWalletQ import protectedWalletQueryNock from './test-fixtures/nocks/protectedWalletQuery.js'; import unProtectedRandomWalletQuery from './test-fixtures/nocks/unProtectedRandomWalletQuery.js'; import vprTestNocks from './test-fixtures/nocks/vprTest.js' +import unknownStatusListNock from './test-fixtures/nocks/unknown_status_list_nock.js' +import statusListNock from './test-fixtures/nocks/status_list_nock.js' import { getSignedDIDAuth } from './didAuth.js'; import { build } from './app.js'; +import { resetConfig } from './config.js' const exchangeSetupPath = '/exchange/setup' const unprotectedTenantName = "UN_PROTECTED_TEST" @@ -34,6 +37,9 @@ const checkForUnexpectedCalls = () => { nock.emitter.on('no match', noMatchHandler); } +//nock.recorder.rec() + + describe('api', () => { before(async () => { @@ -43,6 +49,7 @@ describe('api', () => { statusUpdateBody = { "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", "credentialStatus": [{ "type": "BitstringStatusListCredential", "status": "revoked" }] } }); + after(() => { }) @@ -324,4 +331,127 @@ describe('api', () => { }) }) -}) + + + describe('POST /instance/:instanceId/credentials/status', () => { + before(async () => { + resetConfig() + process.env.ENABLE_STATUS_SERVICE = true + + }); + + after(async () => { + resetConfig() + process.env.ENABLE_STATUS_SERVICE = false + }); + + it('returns 400 if no body', done => { + request(app) + .post(`/instance/${unprotectedTenantName}/credentials/status`) + .expect('Content-Type', /json/) + .expect(400, done) + }) + + it('returns 401 if tenant token is missing from auth header', done => { + request(app) + .post(`/instance/${protectedTenantName}/credentials/status`) + .send(statusUpdateBody) + .expect('Content-Type', /json/) + .expect(401, done) + }) + + + + it('returns 403 if token is not valid', done => { + request(app) + .post(`/instance/${protectedTenantName}/credentials/status`) + .set('Authorization', 'Bearer ThisIsABadToken') + .send(statusUpdateBody) + .expect('Content-Type', /json/) + .expect(403, done) + }) + + it('returns 401 if token is not marked as Bearer', done => { + request(app) + .post(`/instance/${protectedTenantName}/credentials/status`) + .set('Authorization', `${protectedTenantToken}`) + .send(statusUpdateBody) + .expect('Content-Type', /json/) + .expect(401, done) + }) + + it('returns 404 if no seed for tenant name', done => { + request(app) + .post('/instance/wrongTenantName/credentials/status') + .set('Authorization', `${protectedTenantToken}`) + .send(statusUpdateBody) + .expect(404, done) + .expect('Content-Type', /json/) + }) + + /* it('returns 403 when trying to use token for a different tenant', done => { + request(app) + .post(`/instance/${protectedTenantName}/credentials/status`) + .set('Authorization', `Bearer ${testTenantToken2}`) + .send(statusUpdateBody) + .expect('Content-Type', /json/) + .expect(403, done) + }) */ + //still AVE TO TRY these: +/* + it('update unprotected status when token not set for tenant in config', done => { + unprotectedStatusUpdateNock() + request(app) + .post(`/instance/${unprotectedTenantName}/credentials/status`) + .send(statusUpdateBody) + .expect('Content-Type', /json/) + .expect(200, done) + }) + + it('returns 404 for unknown cred id', async () => { + unknownStatusIdNock() + const statusUpdateBodyWithUnknownId = JSON.parse(JSON.stringify(statusUpdateBody)) + statusUpdateBodyWithUnknownId.credentialId = 'kj09ij' + const response = await request(app) + .post('/instance/protected_test/credentials/status') + .set('Authorization', `Bearer ${testTenantToken}`) + .send(statusUpdateBodyWithUnknownId) + + expect(response.header['content-type']).to.have.string('json') + expect(response.status).to.equal(404) + })*/ + }) + + describe('GET /status/:statusCredentialId', () => { + before(async () => { + resetConfig() + process.env.ENABLE_STATUS_SERVICE = true + + }); + + after(async () => { + resetConfig() + process.env.ENABLE_STATUS_SERVICE = false + }); + + it('returns 404 for unknown status credential id', async () => { + unknownStatusListNock() + const response = await request(app) + .get('/status/9898u') + expect(response.header['content-type']).to.have.string('json') + expect(response.status).to.equal(404) + }) + + + it('returns credential status list from status service', async () => { + statusListNock() + const response = await request(app) + .get('/status/slAwJe6GGR6mBojlGW5U') + expect(response.header['content-type']).to.have.string('json') + expect(response.status).to.equal(200) + const returnedList = JSON.parse(JSON.stringify(response.body)) + // this proof value comes from the nock: + expect(returnedList.proof.proofValue).to.equal('z4y3GawinQg1aCqbYqZM8dmDpbmtFa3kE6tFefdXvLi5iby25dvmVwLNZrfcFPyhpshrhCWB76pdSZchVve3K1Znr') + }) + }) +}) \ No newline at end of file diff --git a/src/test-fixtures/.env.testing b/src/test-fixtures/.env.testing index f3ebf68..e45288f 100644 --- a/src/test-fixtures/.env.testing +++ b/src/test-fixtures/.env.testing @@ -8,7 +8,7 @@ # default is false # ENABLE_ACCESS_LOGGING=true # default is false -# ENABLE_STATUS_SERVICE=true + ENABLE_STATUS_SERVICE=false # set the service endpoints # defaults are as follows diff --git a/src/test-fixtures/nocks/status_list_nock.js b/src/test-fixtures/nocks/status_list_nock.js new file mode 100644 index 0000000..df6725e --- /dev/null +++ b/src/test-fixtures/nocks/status_list_nock.js @@ -0,0 +1,34 @@ +import nock from 'nock' + +const theList = `{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "https://sincere-bonefish-currently.ngrok-free.app/slAwJe6GGR6mBojlGW5U", + "type": [ + "VerifiableCredential", + "BitstringStatusListCredential" + ], + "credentialSubject": { + "id": "https://sincere-bonefish-currently.ngrok-free.app/slAwJe6GGR6mBojlGW5U#list", + "type": "BitstringStatusList", + "encodedList": "uH4sIAAAAAAAAA-3BIQEAAAACICf4f60vTEADAAAAAAAAAAAAAADwN_wEBkHUMAAA", + "statusPurpose": "revocation" + }, + "issuer": "did:key:z6Mkg165pEHaUPxkY4NxToor7suxzawEmdT1DEWq3e1Nr2VR", + "validFrom": "2024-09-03T15:24:19.685Z", + "proof": { + "type": "Ed25519Signature2020", + "created": "2024-09-03T15:24:19Z", + "verificationMethod": "did:key:z6Mkg165pEHaUPxkY4NxToor7suxzawEmdT1DEWq3e1Nr2VR#z6Mkg165pEHaUPxkY4NxToor7suxzawEmdT1DEWq3e1Nr2VR", + "proofPurpose": "assertionMethod", + "proofValue": "z4y3GawinQg1aCqbYqZM8dmDpbmtFa3kE6tFefdXvLi5iby25dvmVwLNZrfcFPyhpshrhCWB76pdSZchVve3K1Znr" + } +}` + +export default () => { + nock('http://localhost:4008') + .get('/slAwJe6GGR6mBojlGW5U') + .reply(200, theList) +} diff --git a/src/test-fixtures/nocks/unknown_status_list_nock.js b/src/test-fixtures/nocks/unknown_status_list_nock.js new file mode 100644 index 0000000..6bb349a --- /dev/null +++ b/src/test-fixtures/nocks/unknown_status_list_nock.js @@ -0,0 +1,7 @@ +import nock from 'nock' + +export default () => { + nock('http://localhost:4008') + .get('/9898u') + .reply(404, { code: 404, message: 'No status credential found for that id.' }) +} From 54b1997bd49d7684703af9f74322938af85cd6e7 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 9 Sep 2024 10:28:52 -0400 Subject: [PATCH 7/9] add more status tests --- src/app.test.js | 16 ++++++----- .../nocks/protected_status_update.js | 27 +++++++++++++++++++ .../nocks/unknown_status_id_nock.js | 27 +++++++++++++++++++ .../nocks/unprotected_status_update.js | 27 +++++++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 src/test-fixtures/nocks/protected_status_update.js create mode 100644 src/test-fixtures/nocks/unknown_status_id_nock.js create mode 100644 src/test-fixtures/nocks/unprotected_status_update.js diff --git a/src/app.test.js b/src/app.test.js index 0bdc59a..56982ee 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -11,6 +11,10 @@ import vprTestNocks from './test-fixtures/nocks/vprTest.js' import unknownStatusListNock from './test-fixtures/nocks/unknown_status_list_nock.js' import statusListNock from './test-fixtures/nocks/status_list_nock.js' +import unprotectedStatusUpdateNock from './test-fixtures/nocks/unprotected_status_update.js' +import unknownStatusIdNock from './test-fixtures/nocks/unknown_status_id_nock.js' +import protectedStatusUpdateNock from './test-fixtures/nocks/protected_status_update.js' + import { getSignedDIDAuth } from './didAuth.js'; import { build } from './app.js'; @@ -397,8 +401,8 @@ describe('api', () => { .expect('Content-Type', /json/) .expect(403, done) }) */ - //still AVE TO TRY these: -/* + + it('update unprotected status when token not set for tenant in config', done => { unprotectedStatusUpdateNock() request(app) @@ -407,19 +411,19 @@ describe('api', () => { .expect('Content-Type', /json/) .expect(200, done) }) - + it('returns 404 for unknown cred id', async () => { unknownStatusIdNock() const statusUpdateBodyWithUnknownId = JSON.parse(JSON.stringify(statusUpdateBody)) statusUpdateBodyWithUnknownId.credentialId = 'kj09ij' const response = await request(app) - .post('/instance/protected_test/credentials/status') - .set('Authorization', `Bearer ${testTenantToken}`) + .post(`/instance/${protectedTenantName}/credentials/status`) + .set('Authorization', `Bearer ${protectedTenantToken}`) .send(statusUpdateBodyWithUnknownId) expect(response.header['content-type']).to.have.string('json') expect(response.status).to.equal(404) - })*/ + }) }) describe('GET /status/:statusCredentialId', () => { diff --git a/src/test-fixtures/nocks/protected_status_update.js b/src/test-fixtures/nocks/protected_status_update.js new file mode 100644 index 0000000..8cda10b --- /dev/null +++ b/src/test-fixtures/nocks/protected_status_update.js @@ -0,0 +1,27 @@ +import nock from 'nock' + +export default () => { + nock('http://localhost:4008', { encodedQueryParams: true }) + .post('/credentials/status', { + credentialId: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', + credentialStatus: [{ type: 'BitstringStatusListCredential', status: 'revoked' }] + }) + .reply(200, { code: 200, message: 'Credential status successfully updated.' }, [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'application/json; charset=utf-8', + 'Content-Length', + '64', + 'ETag', + 'W/"40-QIRY/d4PUONYie1grSHLdg5/hcs"', + 'Date', + 'Wed, 23 Aug 2023 20:42:41 GMT', + 'Connection', + 'keep-alive', + 'Keep-Alive', + 'timeout=5' + ]) +} diff --git a/src/test-fixtures/nocks/unknown_status_id_nock.js b/src/test-fixtures/nocks/unknown_status_id_nock.js new file mode 100644 index 0000000..dc967eb --- /dev/null +++ b/src/test-fixtures/nocks/unknown_status_id_nock.js @@ -0,0 +1,27 @@ +import nock from 'nock' + +export default () => { + nock('http://localhost:4008', { encodedQueryParams: true }) + .post('/credentials/status', { + credentialId: 'kj09ij', + credentialStatus: [{ type: 'BitstringStatusListCredential', status: 'revoked' }] + }) + .reply(404, { code: 404, message: 'Credential ID not found.' }, [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'application/json; charset=utf-8', + 'Content-Length', + '49', + 'ETag', + 'W/"31-lbV74I+iXZStkOsz/GAY1mIZewQ"', + 'Date', + 'Wed, 23 Aug 2023 20:44:24 GMT', + 'Connection', + 'keep-alive', + 'Keep-Alive', + 'timeout=5' + ]) +} diff --git a/src/test-fixtures/nocks/unprotected_status_update.js b/src/test-fixtures/nocks/unprotected_status_update.js new file mode 100644 index 0000000..c3228ec --- /dev/null +++ b/src/test-fixtures/nocks/unprotected_status_update.js @@ -0,0 +1,27 @@ +import nock from 'nock' + +export default () => { + nock('http://localhost:4008', { encodedQueryParams: true }) + .post('/credentials/status', { + credentialId: 'urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1', + credentialStatus: [{ type: 'BitstringStatusListCredential', status: 'revoked' }] + }) + .reply(200, 'Credential status successfully updated', [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'text/html; charset=utf-8', + 'Content-Length', + '38', + 'ETag', + 'W/"26-5PplkKqVB9H6fTLVTOo/weOUkgE"', + 'Date', + 'Wed, 23 Aug 2023 13:01:55 GMT', + 'Connection', + 'keep-alive', + 'Keep-Alive', + 'timeout=5' + ]) +} From d633f080176a90bd2ca375b0a05c1cf9da049e15 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 9 Sep 2024 10:36:35 -0400 Subject: [PATCH 8/9] finish tests --- src/app.test.js | 8 ++++++-- src/test-fixtures/.env.testing | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app.test.js b/src/app.test.js index 56982ee..7981d7a 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -23,9 +23,12 @@ import { resetConfig } from './config.js' const exchangeSetupPath = '/exchange/setup' const unprotectedTenantName = "UN_PROTECTED_TEST" const protectedTenantName = "PROTECTED_TEST" +const protectedTenantName2 = "PROTECTED_TEST_2" const randomTenantName = "RANDOM_TEST" + let unprotectedTenantToken let protectedTenantToken +let testTenantToken2 let randomToken let statusUpdateBody @@ -49,6 +52,7 @@ describe('api', () => { before(async () => { unprotectedTenantToken = process.env[`TENANT_TOKEN_${unprotectedTenantName}`] protectedTenantToken = process.env[`TENANT_TOKEN_${protectedTenantName}`] + testTenantToken2 = process.env[`TENANT_TOKEN_${protectedTenantName2}`] randomToken = process.env[`TENANT_TOKEN_${randomTenantName}`] statusUpdateBody = { "credentialId": "urn:uuid:951b475e-b795-43bc-ba8f-a2d01efd2eb1", "credentialStatus": [{ "type": "BitstringStatusListCredential", "status": "revoked" }] } }); @@ -393,14 +397,14 @@ describe('api', () => { .expect('Content-Type', /json/) }) - /* it('returns 403 when trying to use token for a different tenant', done => { + it('returns 403 when trying to use token for a different tenant', done => { request(app) .post(`/instance/${protectedTenantName}/credentials/status`) .set('Authorization', `Bearer ${testTenantToken2}`) .send(statusUpdateBody) .expect('Content-Type', /json/) .expect(403, done) - }) */ + }) it('update unprotected status when token not set for tenant in config', done => { diff --git a/src/test-fixtures/.env.testing b/src/test-fixtures/.env.testing index e45288f..bb7af71 100644 --- a/src/test-fixtures/.env.testing +++ b/src/test-fixtures/.env.testing @@ -25,6 +25,7 @@ TENANT_TOKEN_UN_PROTECTED_TEST=UNPROTECTED TENANT_TOKEN_PROTECTED_TEST=jds +TENANT_TOKEN_PROTECTED_TEST_2=hgf TENANT_TOKEN_RANDOM_TEST=UNPROTECTED # The tenant name is specified in the issuing/status invocations like so From 1a74bf4f7d6b7e6e1f5a0a9a77ab3d174544c35e Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Mon, 9 Sep 2024 17:28:15 -0400 Subject: [PATCH 9/9] add tests for did seed generation --- src/app.test.js | 42 +++++++++++++++++++- src/test-fixtures/nocks/did-key-generator.js | 25 ++++++++++++ src/test-fixtures/nocks/did-web-generator.js | 25 ++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/test-fixtures/nocks/did-key-generator.js create mode 100644 src/test-fixtures/nocks/did-web-generator.js diff --git a/src/app.test.js b/src/app.test.js index 7981d7a..b91bca2 100644 --- a/src/app.test.js +++ b/src/app.test.js @@ -10,10 +10,10 @@ import unProtectedRandomWalletQuery from './test-fixtures/nocks/unProtectedRando import vprTestNocks from './test-fixtures/nocks/vprTest.js' import unknownStatusListNock from './test-fixtures/nocks/unknown_status_list_nock.js' import statusListNock from './test-fixtures/nocks/status_list_nock.js' - +import didWebGeneratorNock from './test-fixtures/nocks/did-web-generator.js' import unprotectedStatusUpdateNock from './test-fixtures/nocks/unprotected_status_update.js' import unknownStatusIdNock from './test-fixtures/nocks/unknown_status_id_nock.js' -import protectedStatusUpdateNock from './test-fixtures/nocks/protected_status_update.js' +import didKeyGeneratorNock from './test-fixtures/nocks/did-key-generator.js' import { getSignedDIDAuth } from './didAuth.js'; @@ -462,4 +462,42 @@ describe('api', () => { expect(returnedList.proof.proofValue).to.equal('z4y3GawinQg1aCqbYqZM8dmDpbmtFa3kE6tFefdXvLi5iby25dvmVwLNZrfcFPyhpshrhCWB76pdSZchVve3K1Znr') }) }) + + describe('/did-web-generator', () => { + it('returns a new did:web', async () => { + didWebGeneratorNock() + await request(app) + .post(`/did-web-generator`) + .send({ + url: 'https://raw.githubusercontent.com/jchartrand/didWebTest/main' + }) + .expect('Content-Type', /json/) + .expect((res) => { + expect(res.body.seed).to.exist + expect(res.body.didDocument.id).to.eql( + 'did:web:raw.githubusercontent.com:jchartrand:didWebTest:main' + ) + expect(res.body.did).to.eql( + 'did:web:raw.githubusercontent.com:jchartrand:didWebTest:main' + ) + }) + .expect(200) + }) + }) + + describe('/did-key-generator', () => { + it('returns a new did:key', async () => { + didKeyGeneratorNock() + await request(app) + .get(`/did-key-generator`) + .expect('Content-Type', /json/) + .expect((res) => { + expect(res.body.seed).to.exist + expect(res.body.didDocument.id).to.contain('did:key') + expect(res.body.did).to.contain('did:key') + }) + .expect(200) + }) + }) + }) \ No newline at end of file diff --git a/src/test-fixtures/nocks/did-key-generator.js b/src/test-fixtures/nocks/did-key-generator.js new file mode 100644 index 0000000..dd296cb --- /dev/null +++ b/src/test-fixtures/nocks/did-key-generator.js @@ -0,0 +1,25 @@ +import nock from 'nock' + +export default () => { + + nock('http://localhost:4006', {"encodedQueryParams":true}) + .get('/did-key-generator') + .reply(200, {"seed":"z1Ak67saZZnW6e41kw8dFNgWPQC7kb4MgmKrB5Saj9fTQtR","decodedSeed":{"0":204,"1":42,"2":201,"3":165,"4":122,"5":6,"6":144,"7":61,"8":229,"9":222,"10":134,"11":94,"12":219,"13":6,"14":191,"15":210,"16":103,"17":71,"18":241,"19":109,"20":215,"21":216,"22":2,"23":72,"24":19,"25":144,"26":20,"27":32,"28":178,"29":172,"30":81,"31":82},"did":"did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha","didDocument":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/suites/ed25519-2020/v1","https://w3id.org/security/suites/x25519-2020/v1"],"id":"did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha","verificationMethod":[{"id":"did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha#z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha","type":"Ed25519VerificationKey2020","controller":"did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha","publicKeyMultibase":"z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha"}],"authentication":["did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha#z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha"],"assertionMethod":["did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha#z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha"],"capabilityDelegation":["did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha#z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha"],"capabilityInvocation":["did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha#z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha"],"keyAgreement":[{"id":"did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha#z6LScqycbURGxJztJiSkK6MKZJhujV7PygbA2BHexuriMAdr","type":"X25519KeyAgreementKey2020","controller":"did:key:z6Mkq5Vr5kEEUF6M8U7AgSKBVppXyNQBoWnyb1vMqedzN3ha","publicKeyMultibase":"z6LScqycbURGxJztJiSkK6MKZJhujV7PygbA2BHexuriMAdr"}]}}, [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'application/json; charset=utf-8', + 'Content-Length', + '1776', + 'ETag', + 'W/"6f0-JdoCppKv4nbnXnQDbZJxlFrXG7o"', + 'Date', + 'Mon, 09 Sep 2024 18:06:31 GMT', + 'Connection', + 'keep-alive', + 'Keep-Alive', + 'timeout=5' +]); +} diff --git a/src/test-fixtures/nocks/did-web-generator.js b/src/test-fixtures/nocks/did-web-generator.js new file mode 100644 index 0000000..142ef22 --- /dev/null +++ b/src/test-fixtures/nocks/did-web-generator.js @@ -0,0 +1,25 @@ +import nock from 'nock' + +export default () => { + + nock('http://localhost:4006', {"encodedQueryParams":true}) + .post('/did-web-generator', {"url":"https://raw.githubusercontent.com/jchartrand/didWebTest/main"}) + .reply(200, {"seed":"z1AhHNg9RSiUrjKAeTLXeYjJG5xE4fyZsxfFwKkXM4PhPMn","decodedSeed":{"0":162,"1":121,"2":218,"3":124,"4":128,"5":83,"6":6,"7":120,"8":155,"9":52,"10":67,"11":214,"12":9,"13":96,"14":98,"15":108,"16":249,"17":187,"18":45,"19":240,"20":184,"21":177,"22":120,"23":176,"24":189,"25":125,"26":247,"27":38,"28":7,"29":64,"30":91,"31":77},"did":"did:web:raw.githubusercontent.com:jchartrand:didWebTest:main","didDocument":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/suites/ed25519-2020/v1","https://w3id.org/security/suites/x25519-2020/v1"],"id":"did:web:raw.githubusercontent.com:jchartrand:didWebTest:main","assertionMethod":[{"id":"did:web:raw.githubusercontent.com:jchartrand:didWebTest:main#z6Mkoy5dFU7xajPV2QGEif1cz43To6go9Yhtf1T39TXCSMrS","type":"Ed25519VerificationKey2020","controller":"did:web:raw.githubusercontent.com:jchartrand:didWebTest:main","publicKeyMultibase":"z6Mkoy5dFU7xajPV2QGEif1cz43To6go9Yhtf1T39TXCSMrS"}]}}, [ + 'X-Powered-By', + 'Express', + 'Access-Control-Allow-Origin', + '*', + 'Content-Type', + 'application/json; charset=utf-8', + 'Content-Length', + '957', + 'ETag', + 'W/"3bd-+yFc27zaIMTPRG+dm/pWiYDTumc"', + 'Date', + 'Mon, 09 Sep 2024 17:55:01 GMT', + 'Connection', + 'keep-alive', + 'Keep-Alive', + 'timeout=5' +]); +}