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

Jc update vc libs #18

Merged
merged 9 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
13 changes: 4 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -38,7 +33,7 @@
"dcc"
],
"engines": {
"node": ">=16.0"
"node": ">=20.0"
},
"author": {
"name": "Digital Credentials Consortium",
Expand Down
51 changes: 41 additions & 10 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand All @@ -25,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());
Expand Down Expand Up @@ -113,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.')
Expand Down Expand Up @@ -184,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)
Expand All @@ -200,22 +197,56 @@ 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 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)
}
})

app.get('/status/:statusCredentialId', async function (req, res, next) {
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}`)
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)
});

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)
Expand Down
178 changes: 177 additions & 1 deletion src/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,27 @@ 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 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 didKeyGeneratorNock from './test-fixtures/nocks/did-key-generator.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"
const protectedTenantName = "PROTECTED_TEST"
const protectedTenantName2 = "PROTECTED_TEST_2"
const randomTenantName = "RANDOM_TEST"

let unprotectedTenantToken
let protectedTenantToken
let testTenantToken2
let randomToken

let statusUpdateBody
Expand All @@ -34,15 +44,20 @@ const checkForUnexpectedCalls = () => {
nock.emitter.on('no match', noMatchHandler);
}

//nock.recorder.rec()


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" }] }
});


after(() => {

})
Expand Down Expand Up @@ -324,4 +339,165 @@ 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)
})


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/${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', () => {
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')
})
})

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)
})
})

})
17 changes: 6 additions & 11 deletions src/didAuth.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
}

Loading
Loading