From 5de7a5def9ecee91d74faabf4b8e9b2e89d88cae Mon Sep 17 00:00:00 2001 From: F-Node-Karlsruhe Date: Mon, 19 Jun 2023 14:40:47 +0200 Subject: [PATCH 1/2] add auth disclosure request for credentials Signed-off-by: F-Node-Karlsruhe --- api/package.json | 2 +- frontend/package.json | 2 +- frontend/src/components/Credential.vue | 36 +++++- frontend/src/store/demoAuth.js | 56 +++++++++ frontend/src/store/index.js | 29 ++++- frontend/src/views/Verify.vue | 160 +++++++++++++++---------- 6 files changed, 210 insertions(+), 75 deletions(-) create mode 100644 frontend/src/store/demoAuth.js diff --git a/api/package.json b/api/package.json index 3bc0034..a157aaa 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "vc-verifier", - "version": "1.5.3", + "version": "1.6.0", "description": "The EECC verifier for verifiable credentials which provides an verification API as well as the corresponding UI.", "main": "index.js", "type": "module", diff --git a/frontend/package.json b/frontend/package.json index ddb4019..1845cb3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "verifier_frontend", - "version": "1.5.3", + "version": "1.6.0", "description": "Vue frontend for the EECC vc verifier API", "scripts": { "build": "vue-cli-service build", diff --git a/frontend/src/components/Credential.vue b/frontend/src/components/Credential.vue index 2ce9154..f056cc2 100644 --- a/frontend/src/components/Credential.vue +++ b/frontend/src/components/Credential.vue @@ -9,6 +9,14 @@ @@ -168,6 +185,7 @@ import { getPlainCredential } from '../utils.js'; import * as JsHashes from 'jshashes'; pdfMake.vfs = pdfFonts.pdfMake.vfs; +import { useToast } from "vue-toastification"; import TrimmedBatch from "@/components/TrimmedBatch.vue"; import QRModal from "@/components/QRModal.vue"; @@ -182,6 +200,7 @@ export default { }, data() { return { + toast: useToast(), getPlainCredential: getPlainCredential } }, @@ -190,6 +209,15 @@ export default { selector: "[data-bs-toggle='tooltip']" }) }, + computed: { + SDCredential() { + const proof = Array.isArray(this.credential.proof) ? this.credential.proof[0] : this.credential.proof; + return proof.type == 'DataIntegrityProof' + }, + disclosed() { + return this.$store.state.disclosedCredentials.includes(this.credential.id); + } + }, methods: { downloadCredentialPDF(credential) { // var win = window.open('', '_blank'); @@ -219,6 +247,10 @@ export default { getCredCompId(prefix) { const idHash = new JsHashes.SHA256().hex(this.credential.id || JSON.stringify(this.credential.proof)); return prefix + '-' + idHash.substr(idHash.length - 5, idHash.length); + }, + requestDisclosure() { + if (this.disclosed) this.toast.info('Credential already disclosed!') + else this.$store.dispatch("makeAuthenticatedRequest", this.credential.id); } } } diff --git a/frontend/src/store/demoAuth.js b/frontend/src/store/demoAuth.js new file mode 100644 index 0000000..005f874 --- /dev/null +++ b/frontend/src/store/demoAuth.js @@ -0,0 +1,56 @@ +export const demoAuthPresentation = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "ipfs://QmY9CDY2PoXLgHr2vG4u8mj27cfAyuzVxE2swt8wwFn7Rt", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "https://ssi.eecc.de/api/registry/vc/b7440bf7-2317-43e4-9857-4c85032ac4a1", + "type": [ + "VerifiableCredential", + "EECCAccessCredential" + ], + "issuer": { + "id": "did:web:ssi.eecc.de", + "image": "https://id.eecc.de/assets/img/logo_big.png", + "name": "EECC" + }, + "issuanceDate": "2023-06-19T09:58:08Z", + "credentialStatus": { + "id": "https://ssi.eecc.de/api/registry/vc/status/did:web:ssi.eecc.de/1#1", + "type": "RevocationList2020Status", + "revocationListIndex": 1, + "revocationListCredential": "https://ssi.eecc.de/api/registry/vc/status/did:web:ssi.eecc.de/1" + }, + "credentialSubject": { + "id": "https://ssi.eecc.de/verifier", + "customer_name": "VC Verifier" + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2023-06-19T09:58:08Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:web:ssi.eecc.de#z6MkoHWsmSZnHisAxnVdokYHnXaVqWFZ4H33FnNg13zyymxd", + "proofValue": "z247VYAm5GbG1sAAcqW7QwEL1mfXxaSa3N93EuLc4HeTUm219UjrPMC5gyq5xTAaDQCVMUEVTouyboY4UMTxzjekN" + } + } + ], + "holder": "did:web:ssi.eecc.de", + "proof": { + "type": "Ed25519Signature2020", + "created": "2023-06-19T10:19:04Z", + "verificationMethod": "did:web:ssi.eecc.de#z6MkoHWsmSZnHisAxnVdokYHnXaVqWFZ4H33FnNg13zyymxd", + "proofPurpose": "authentication", + "challenge": "demochallenge", + "proofValue": "z3BCT7df4bRXXTa9i79apmiou76M8LVZXGFYHA18xT6wX3PfcpjfWaMc9xhFwmGi9XTj8tth1ai1ASeQoHiGWMn1V" + } +} \ No newline at end of file diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index c524a36..e95eb4b 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -1,32 +1,51 @@ import { createStore } from 'vuex' +import { demoAuthPresentation } from './demoAuth' +import api from '../api' export default createStore({ state: { - version: '1.5.3', + version: '1.6.0', verifiables: [], + disclosedCredentials: [], VC_REGISTRY: process.env.VC_REGISTRY || 'https://ssi.eecc.de/api/registry/vcs/', OPENID_ENDPOINT: process.env.OPENID_ENDPOINT || 'https://ssi.eecc.de/api/openid/', showQRModal: false }, mutations: { showQRModal(state, payload) { - state.showQRModal = payload.value + state.showQRModal = payload.value; }, addVerifiables(state, verifiables) { - state.verifiables = state.verifiables.concat(verifiables) + state.verifiables = state.verifiables.concat(Array.isArray(verifiables) ? verifiables : [verifiables]) }, resetVerifiables(state) { state.verifiables = []; }, + addDisclosedCredential(state, credential) { + state.disclosedCredentials.push(credential.id) + }, }, actions: { addVerifiables(context, verifiables) { - this.commit('addVerifiables', verifiables) + this.commit('addVerifiables', verifiables); }, resetVerifiables() { - this.commit('resetVerifiables') + this.commit('resetVerifiables'); }, + makeAuthenticatedRequest(context, url, authPresentation) { + // mock authentication if not present + if (!authPresentation) authPresentation = demoAuthPresentation; + // folowing OID4VP with authVP extension + api.post(url, { vp: authPresentation }) + .then((res) => { + this.commit('addVerifiables', res.data) + this.commit('addDisclosedCredential', res.data) + }) + .catch((error) => { + console.log(error) + }); + } }, modules: {}, }) \ No newline at end of file diff --git a/frontend/src/views/Verify.vue b/frontend/src/views/Verify.vue index dc6c7bf..753ae46 100644 --- a/frontend/src/views/Verify.vue +++ b/frontend/src/views/Verify.vue @@ -66,7 +66,7 @@ export default { this.fetchData() .then(() => { - this.verify() + this.verifyAll() }) .catch((error) => { this.toast.error(`Something went wrong fetching the credential!\n${error}`); @@ -75,6 +75,9 @@ export default { }); }, computed: { + storeVerifiables() { + return this.$store.state.verifiables; + }, getOrderedCredentials() { return [...this.credentials].sort((a, b) => { let da = new Date(a.issuanceDate), @@ -101,9 +104,25 @@ export default { else return 'Loading credentials ...' } }, + watch: { + storeVerifiables(newVerifiables) { + if (newVerifiables.length > 0) { + this.addVerifiables(newVerifiables); + } + } + }, methods: { addCredential(credential) { - this.credentials.push(credential); + let existingCredentialIndex = this.credentials.findIndex(cred => cred.id == credential.id); + if (existingCredentialIndex != -1) { + // replace more valuable credentials + const oldFields = Object.keys(this.credentials[existingCredentialIndex].credentialSubject); + const newFields = Object.keys(credential.credentialSubject); + if (oldFields.length < newFields.length) { + this.credentials[existingCredentialIndex] = credential; + this.toast.success(`Successfully disclosed ${newFields.filter(x => !oldFields.includes(x)).join(', ')}!`) + } + } else this.credentials.push(credential); this.getContext(credential) .then(context => { // credential reference does not refer actual credentials -> find it from credentials @@ -111,6 +130,11 @@ export default { }) .catch((error) => { console.log(error) }) }, + async addVerifiables(verifiables) { + // this.verifiables = this.verifiables.concat(verifiables); + this.$store.dispatch("resetVerifiables"); + verifiables.map(async v => await this.verify(v)); + }, async fetchData() { if (this.credentialId) { @@ -125,8 +149,8 @@ export default { return } - if (this.$store.state.verifiables.length > 0) { - this.verifiables = this.$store.state.verifiables; + if (this.storeVerifiables.length > 0) { + this.verifiables = this.storeVerifiables; this.$store.dispatch("resetVerifiables"); return } @@ -177,93 +201,97 @@ export default { Object.assign(credential, credentialResult); }, - async verify() { + async verify(verifiable) { - if (this.verifiables.length == 0) { - this.toast.warning('No verifiables provided for verification!'); - return - } + if (getVerifiableType(verifiable) == VerifiableType.PRESENTATION) { - try { + const presentation = { + presentation: + { + holder: getHolder(verifiable), + challenge: Array.isArray(verifiable.proof) ? verifiable.proof[0].challenge : verifiable.proof.challenge, + domain: Array.isArray(verifiable.proof) ? verifiable.proof[0].domain : verifiable.proof.domain + } + } - this.progress = 1 + if (Array.isArray(verifiable.verifiableCredential)) verifiable.verifiableCredential.forEach( + (credential) => this.addCredential({ ...credential, presentation }) + ); + else this.addCredential({ ...verifiable.verifiableCredential, presentation }); - // forEach throws 'cannot convert undefined to object' error - const verifyTasks = Promise.all(this.verifiables.map(async (verifiable) => { + } else { + this.addCredential({ ...verifiable }); + } - if (getVerifiableType(verifiable) == VerifiableType.PRESENTATION) { + const res = await this.$api.post('/', [verifiable], { params: { challenge: this.$route.query.challenge } }); - const presentation = { - presentation: - { - holder: getHolder(verifiable), - challenge: Array.isArray(verifiable.proof) ? verifiable.proof[0].challenge : verifiable.proof.challenge, - domain: Array.isArray(verifiable.proof) ? verifiable.proof[0].domain : verifiable.proof.domain - } - } + const result = res.data[0]; - if (Array.isArray(verifiable.verifiableCredential)) verifiable.verifiableCredential.forEach( - (credential) => this.addCredential({ ...credential, presentation }) - ); - else this.addCredential({ ...verifiable.verifiableCredential, presentation }); + // verifiable is a presentations + if (getVerifiableType(verifiable) == VerifiableType.PRESENTATION) { - } else { - this.addCredential({ ...verifiable }); - } + // build presentation object with important properties for attaching to the credential + var presentation = { + verified: result.verified, + presentationResult: result.presentationResult.verified, + holder: getHolder(verifiable), + challenge: Array.isArray(verifiable.proof) ? verifiable.proof[0].challenge : verifiable.proof.challenge, + domain: Array.isArray(verifiable.proof) ? verifiable.proof[0].domain : verifiable.proof.domain, + status: 'verified!' + } - const res = await this.$api.post('/', [verifiable], { params: { challenge: this.$route.query.challenge } }); + if (presentation.presentationResult && !presentation.verified) { + presentation.status = 'partially verified!' + this.toast.warning(`Presentation${presentation.holder ? ' of holder ' + presentation.holder.id || presentation.holder + ' ' : ' '}contains invalid credentials!`); + } - const result = res.data[0]; + if (result.error) { - // verifiable is a presentations - if (getVerifiableType(verifiable) == VerifiableType.PRESENTATION) { + presentation.status = result.error.name + ': '; - // build presentation object with important properties for attaching to the credential - var presentation = { - verified: result.verified, - presentationResult: result.presentationResult.verified, - holder: getHolder(verifiable), - challenge: Array.isArray(verifiable.proof) ? verifiable.proof[0].challenge : verifiable.proof.challenge, - domain: Array.isArray(verifiable.proof) ? verifiable.proof[0].domain : verifiable.proof.domain, - status: 'verified!' - } + if (result.error.errors) result.error.errors.forEach((e) => { - if (presentation.presentationResult && !presentation.verified) { - presentation.status = 'partially verified!' - this.toast.warning(`Presentation${presentation.holder ? ' of holder ' + presentation.holder.id || presentation.holder + ' ' : ' '}contains invalid credentials!`); - } + presentation.status += e.message + '\n'; - if (result.error) { + }) - presentation.status = result.error.name + ': '; + this.toast.error(`Verification of presentation${presentation.holder ? ' of holder ' + presentation.holder.id || presentation.holder + ' ' : ' '} failed!\n${presentation.status}`); - if (result.error.errors) result.error.errors.forEach((e) => { + } + + // contains array of credentials + if (Array.isArray(verifiable.verifiableCredential)) + verifiable.verifiableCredential.forEach( + credential => this.assignResult( + credential.id, + result.credentialResults.find(credRes => credRes.credentialId == credential.id), + presentation + )); - presentation.status += e.message + '\n'; + // contains single credential object + else this.assignResult(verifiable.verifiableCredential.id, result[0], presentation); - }) + } - this.toast.error(`Verification of presentation${presentation.holder ? ' of holder ' + presentation.holder.id || presentation.holder + ' ' : ' '} failed!\n${presentation.status}`); + else this.assignResult(verifiable.id, result) - } + verifiable.verified = result.verified; + }, + async verifyAll() { - // contains array of credentials - if (Array.isArray(verifiable.verifiableCredential)) - verifiable.verifiableCredential.forEach( - credential => this.assignResult( - credential.id, - result.credentialResults.find(credRes => credRes.credentialId == credential.id), - presentation - )); + if (this.verifiables.length == 0) { + this.toast.warning('No verifiables provided for verification!'); + return + } - // contains single credential object - else this.assignResult(verifiable.verifiableCredential.id, result[0], presentation); + try { - } + this.progress = 1 - else this.assignResult(verifiable.id, result) + // forEach throws 'cannot convert undefined to object' error + const verifyTasks = Promise.all(this.verifiables.map(async (verifiable) => { - verifiable.verified = result.verified; + await this.verify(verifiable); this.progress += 1 From e42c7dfd45f421eec80f414e2f2e5011b9b06a5a Mon Sep 17 00:00:00 2001 From: F-Node-Karlsruhe Date: Mon, 19 Jun 2023 14:42:22 +0200 Subject: [PATCH 2/2] update changelog Signed-off-by: F-Node-Karlsruhe --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ab698..2d4fbee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ WIP --- +1.6.0 (2023-06-19) +--- + +- add ability to request disclosure of single credentials + + 1.5.3 (2023-06-14) ---