diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 638cd44..6d9d4fd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,9 @@ name: Tests -on: [ push ] +on: [push] jobs: test: - runs-on: ubuntu-latest steps: @@ -13,7 +12,9 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18 - + + - run: npm i @gs1us/vc-verifier-rules + working-directory: ./api - run: npm ci working-directory: ./api - run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b4404..abd17bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ VC Verifier Changelog WIP --- +2.0.0 (2024-07-23) + +- add gs1 verification endpoint +- use gs1 endpoint on gs1 credential + 1.7.8 (2024-06-13) --- diff --git a/api/__tests__/gs1.test.ts b/api/__tests__/gs1.test.ts new file mode 100644 index 0000000..7a428a4 --- /dev/null +++ b/api/__tests__/gs1.test.ts @@ -0,0 +1,280 @@ +import request from "supertest"; + +import server from "../src/index"; + +afterAll((done) => { + server.close(); + done(); +}); + +const licenceKeyCredential: any = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://ref.gs1.org/gs1/vc/license-context", + "https://w3id.org/security/suites/ed25519-2020/v1", + { + name: "https://schema.org/name", + description: "https://schema.org/description", + image: "https://schema.org/image", + }, + "https://w3id.org/vc-revocation-list-2020/v1", + ], + id: "https://id.gs1.org/vc/license/gs1_prefix/08", + type: ["VerifiableCredential", "GS1PrefixLicenseCredential"], + issuer: "did:web:id.gs1.org", + name: "GS1 Prefix License", + description: + "FOR DEMONSTRATION PURPOSES ONLY: NOT TO BE USED FOR PRODUCTION GRADE SYSTEMS! A company prefix that complies with GS1 Standards (a “GS1 Company Prefix”) is a unique identification number that is assigned to just your company by GS1 US. It’s the foundation of GS1 Standards and can be found in all of the GS1 Identification Numbers.", + issuanceDate: "2023-05-19T13:39:41.368Z", + credentialSubject: { + id: "did:web:cbpvsvip-vc.gs1us.org", + organization: { + "gs1:partyGLN": "0614141000005", + "gs1:organizationName": "GS1 US", + }, + licenseValue: "08", + alternativeLicenseValue: "8", + }, + proof: { + type: "Ed25519Signature2020", + created: "2023-05-19T13:39:41Z", + verificationMethod: + "did:web:id.gs1.org#z6MkkzYByKSsaWusRbYNZGAMvdd5utsPqsGKvrc7T9jyvUrN", + proofPurpose: "assertionMethod", + proofValue: + "z56N5j6WZRwAng8f12RNNPStBBmGLaozHkdPtDmMLwZmqo1EXW3juFZYpeyU7QRh6NRGxJtxMJvAXPq4PveR2bR7m", + }, +}; + +const companyPrefixCredential: any = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://ref.gs1.org/gs1/vc/license-context", + "https://w3id.org/security/suites/ed25519-2020/v1", + { + name: "https://schema.org/name", + description: "https://schema.org/description", + image: "https://schema.org/image", + }, + "https://w3id.org/vc-revocation-list-2020/v1", + ], + issuer: "did:web:cbpvsvip-vc.gs1us.org", + name: "GS1 Company Prefix License", + description: + "THIS GS1 DIGITAL LICENSE CREDENTIAL IS FOR TESTING PURPOSES ONLY. A GS1 Company Prefix License is issued by a GS1 Member Organization or GS1 Global Office and allocated to a user company or to itself for the purpose of generating tier 1 GS1 identification keys.", + issuanceDate: "2021-05-11T10:50:36.701Z", + id: "http://did-vc.gs1us.org/vc/license/08600057694", + type: ["VerifiableCredential", "GS1CompanyPrefixLicenseCredential"], + credentialSubject: { + id: "did:key:z6Mkfb3kW3kBP4UGqaBEQoCLBUJjdzuuuPsmdJ2LcPMvUreS/1", + organization: { + "gs1:partyGLN": "0860005769407", + "gs1:organizationName": "Healthy Tots", + }, + extendsCredential: "https://id.gs1.org/vc/license/gs1_prefix/08", + licenseValue: "08600057694", + alternativeLicenseValue: "8600057694", + }, + credentialStatus: { + id: "https://cbpvsvip-vc.dev.gs1us.org/status/2c0a1f02-d545-481b-902a-1e919cd706e2/1193", + type: "RevocationList2020Status", + revocationListIndex: 1193, + revocationListCredential: + "https://cbpvsvip-vc.dev.gs1us.org/status/2c0a1f02-d545-481b-902a-1e919cd706e2/", + }, + proof: { + type: "Ed25519Signature2020", + created: "2023-05-22T16:55:59Z", + verificationMethod: + "did:web:cbpvsvip-vc.gs1us.org#z6Mkig1nTEAxna86Pjb71SZdbX3jEdKRqG1krDdKDatiHVxt", + proofPurpose: "assertionMethod", + proofValue: + "zfWTiZ9CRLJBUUHRFa82adMZFwiAvYCsTwRjX7JaTpUnVuCTj44f9ErSGbTBWezv89MyKQ3jTLFgWUbUvB6nuJCN", + }, +}; + +const orgDataCredentialPresentation: any = { + "@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", + "https://ref.gs1.org/gs1/vc/license-context", + "https://w3id.org/security/suites/ed25519-2020/v1", + { + name: "https://schema.org/name", + description: "https://schema.org/description", + image: "https://schema.org/image", + }, + "https://w3id.org/vc-revocation-list-2020/v1", + ], + issuer: "did:web:cbpvsvip-vc.gs1us.org", + name: "GS1 Company Prefix License", + description: + "THIS GS1 DIGITAL LICENSE CREDENTIAL IS FOR TESTING PURPOSES ONLY. A GS1 Company Prefix License is issued by a GS1 Member Organization or GS1 Global Office and allocated to a user company or to itself for the purpose of generating tier 1 GS1 identification keys.", + issuanceDate: "2021-05-11T10:50:36.701Z", + id: "http://did-vc.gs1us.org/vc/license/08600057694", + type: ["VerifiableCredential", "GS1CompanyPrefixLicenseCredential"], + credentialSubject: { + id: "did:key:z6Mkfb3kW3kBP4UGqaBEQoCLBUJjdzuuuPsmdJ2LcPMvUreS/1", + organization: { + "gs1:partyGLN": "0860005769407", + "gs1:organizationName": "Healthy Tots", + }, + extendsCredential: "https://id.gs1.org/vc/license/gs1_prefix/08", + licenseValue: "08600057694", + alternativeLicenseValue: "8600057694", + }, + credentialStatus: { + id: "https://cbpvsvip-vc.dev.gs1us.org/status/2c0a1f02-d545-481b-902a-1e919cd706e2/1193", + type: "RevocationList2020Status", + revocationListIndex: 1193, + revocationListCredential: + "https://cbpvsvip-vc.dev.gs1us.org/status/2c0a1f02-d545-481b-902a-1e919cd706e2/", + }, + proof: { + type: "Ed25519Signature2020", + created: "2023-05-22T16:55:59Z", + verificationMethod: + "did:web:cbpvsvip-vc.gs1us.org#z6Mkig1nTEAxna86Pjb71SZdbX3jEdKRqG1krDdKDatiHVxt", + proofPurpose: "assertionMethod", + proofValue: + "zfWTiZ9CRLJBUUHRFa82adMZFwiAvYCsTwRjX7JaTpUnVuCTj44f9ErSGbTBWezv89MyKQ3jTLFgWUbUvB6nuJCN", + }, + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://ref.gs1.org/gs1/vc/license-context", + "https://ref.gs1.org/gs1/vc/declaration-context", + "https://w3id.org/security/suites/ed25519-2020/v1", + { + name: "https://schema.org/name", + description: "https://schema.org/description", + image: "https://schema.org/image", + }, + "https://w3id.org/vc-revocation-list-2020/v1", + ], + issuer: "did:web:cbpvsvip-vc.gs1us.org", + name: "GS1 Key Credential", + description: + "THIS GS1 DIGITAL LICENSE CREDENTIAL IS FOR TESTING PURPOSES ONLY. This is the Verifiable Credential that indicates that something has been identified. It contains no data about what has been identified as that is done via the association process. This credential is used only to indicate that the key that it contains exists and is valid.", + id: "did:key:z6MkkzTNsyFfx4VQFkSs3R7q8nKN5twGrM8538Xu7YXym6mW", + type: ["VerifiableCredential", "KeyCredential"], + credentialSubject: { + id: "https://id.gs1.org/417/0860005769407", + extendsCredential: "http://did-vc.gs1us.org/vc/license/08600057694", + }, + credentialStatus: { + id: "https://cbpvsvip-vc.dev.gs1us.org/status/2c0a1f02-d545-481b-902a-1e919cd706e2/1195", + type: "RevocationList2020Status", + revocationListIndex: 1195, + revocationListCredential: + "https://cbpvsvip-vc.dev.gs1us.org/status/2c0a1f02-d545-481b-902a-1e919cd706e2/", + }, + issuanceDate: "2023-05-22T17:02:41Z", + proof: { + type: "Ed25519Signature2020", + created: "2023-05-22T17:02:41Z", + verificationMethod: + "did:web:cbpvsvip-vc.gs1us.org#z6Mkig1nTEAxna86Pjb71SZdbX3jEdKRqG1krDdKDatiHVxt", + proofPurpose: "assertionMethod", + proofValue: + "zsZsQaGwTpbDNAwPDDK4aPoiVWYDTQcgmgRzb7CP74eEyGE4atrudRjFx7EMndFsNnWx1qh1WUSgEWa6ZTTeBPdb", + }, + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://ref.gs1.org/gs1/vc/license-context", + "https://ref.gs1.org/gs1/vc/declaration-context", + "https://ref.gs1.org/gs1/vc/organization-context", + "https://w3id.org/security/suites/ed25519-2020/v1", + { + name: "https://schema.org/name", + description: "https://schema.org/description", + image: "https://schema.org/image", + }, + "https://w3id.org/vc-revocation-list-2020/v1", + ], + issuer: "did:web:cbpvsvip-vc.gs1us.org", + name: "GS1 Party Identification Credential", + description: + "THIS GS1 DIGITAL LICENSE CREDENTIAL IS FOR TESTING PURPOSES ONLY. The party data credential is the Verifiable Credential that is shared with parties interested in the basic information associated with a party identified by a GLN.", + issuanceDate: "2021-05-11T10:50:36.701Z", + id: "did:key:z6MkfEHKfq5vmXXDs6AuE1xt58WySEoLPKLGLoWHHuF1pmVm", + type: ["VerifiableCredential", "OrganizationDataCredential"], + credentialSubject: { + id: "did:key:z6MktUvJtDf1tx6TFuxEb3NxAV3KmWx6j8BVp3jM9TheiFsX/1", + sameAs: "https://id.gs1.org/417/0860005769407", + keyAuthorization: + "did:key:z6MkkzTNsyFfx4VQFkSs3R7q8nKN5twGrM8538Xu7YXym6mW", + organization: { + "gs1:partyGLN": "0860005769407", + "gs1:organizationName": "Healthy Tots", + }, + }, + credentialStatus: { + id: "https://cbpvsvip-vc.dev.gs1us.org/status/2c0a1f02-d545-481b-902a-1e919cd706e2/1194", + type: "RevocationList2020Status", + revocationListIndex: 1194, + revocationListCredential: + "https://cbpvsvip-vc.dev.gs1us.org/status/2c0a1f02-d545-481b-902a-1e919cd706e2/", + }, + proof: { + type: "Ed25519Signature2020", + created: "2023-05-22T17:01:12Z", + verificationMethod: + "did:web:cbpvsvip-vc.gs1us.org#z6Mkig1nTEAxna86Pjb71SZdbX3jEdKRqG1krDdKDatiHVxt", + proofPurpose: "assertionMethod", + proofValue: + "z43LLp9h8SKASz3bGKYfy68SaWutdzH9Jz542LHjKwTHWEJafcPorDazU2NPydzHknmxj9rEbrr9Lkzkh5ikpxQcp", + }, + }, + ], + id: "urn:uuid:c1lb4rsf9cfamox0e1qfr5", + holder: "urn:uuid:c1lb4rsf9cfamox0e1qfr5:holder", + proof: { + type: "Ed25519Signature2020", + created: "2023-05-22T17:04:10Z", + verificationMethod: + "did:web:cbpvsvip-vc.gs1us.org#z6Mkig1nTEAxna86Pjb71SZdbX3jEdKRqG1krDdKDatiHVxt", + proofPurpose: "authentication", + challenge: "tst123", + proofValue: + "z2Mv46TpVBzJn5LM9WBg5CkBGScKkVhUyf34xmzvURXVWoqg4r3Xywwbg9AbD54Aus9KAoWFkmGhFeGUZi3fwck7G", + }, +}; + +describe("Verifier API Test for GS1 Credentials", () => { + test("Verify GS1 licence prefix credentials", async () => { + const res = await request(server) + .post("/api/verifier/gs1") + .send(licenceKeyCredential); + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty("verified"); + expect(res.body.verified).toBe(true); + }); + + test("Verify GS1 company licence prefix credentials", async () => { + const res = await request(server) + .post("/api/verifier/gs1") + .send(companyPrefixCredential); + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty("verified"); + expect(res.body.verified).toBe(true); + }); + + test("Verify GS1 data presentation", async () => { + const res = await request(server) + .post("/api/verifier/gs1") + .send(orgDataCredentialPresentation); + expect(res.statusCode).toEqual(200); + expect(res.body).toHaveProperty("verified"); + expect(res.body.verified).toBe(true); + }); +}); diff --git a/api/package-lock.json b/api/package-lock.json index c58cfdf..4b531ab 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "vc-verifier", - "version": "1.7.6", + "version": "1.7.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vc-verifier", - "version": "1.7.6", + "version": "1.7.7", "license": "AGPL-3.0", "dependencies": { "@digitalbazaar/data-integrity": "^1.4.1", @@ -14,7 +14,7 @@ "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^1.0.2", "@digitalbazaar/ed25519-signature-2018": "^4.0.0", "@digitalbazaar/ed25519-signature-2020": "^5.2.0", - "@digitalbazaar/vc": "^6.2.0", + "@digitalbazaar/vc": "^6.3.0", "@digitalbazaar/vc-revocation-list": "^5.0.1", "@digitalbazaar/vc-status-list": "^7.1.0", "cors": "^2.8.5", @@ -962,16 +962,16 @@ "integrity": "sha512-0WZa6tPiTZZF8leBtQgYAfXQePFQp2z5ivpCEN/iZguYYZ0TB9qRmWtan5XH6mNFuusHtMcyIzAcReyE6rZPhA==" }, "node_modules/@digitalbazaar/vc": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@digitalbazaar/vc/-/vc-6.2.0.tgz", - "integrity": "sha512-BzLLFJlQg+aarxdIARlD42P0cgOFPvta8PWDuCW7IFv5jXsPb9/QTqlsxRKOmcKLJzMcdqkVOSLt0tc46jXSXg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/vc/-/vc-6.3.0.tgz", + "integrity": "sha512-zgrV387lEek2NUoji8jNYRGJhlrWZnZRLfvfVdCd2/ONjcDa3eV8sM5H7s1hnTGJl8DB7ArtrhNiirxEllD0Fw==", "dependencies": { "credentials-context": "^2.0.0", "jsonld": "^8.3.1", "jsonld-signatures": "^11.2.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@digitalbazaar/vc-revocation-list": { @@ -6626,9 +6626,9 @@ "integrity": "sha512-0WZa6tPiTZZF8leBtQgYAfXQePFQp2z5ivpCEN/iZguYYZ0TB9qRmWtan5XH6mNFuusHtMcyIzAcReyE6rZPhA==" }, "@digitalbazaar/vc": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@digitalbazaar/vc/-/vc-6.2.0.tgz", - "integrity": "sha512-BzLLFJlQg+aarxdIARlD42P0cgOFPvta8PWDuCW7IFv5jXsPb9/QTqlsxRKOmcKLJzMcdqkVOSLt0tc46jXSXg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@digitalbazaar/vc/-/vc-6.3.0.tgz", + "integrity": "sha512-zgrV387lEek2NUoji8jNYRGJhlrWZnZRLfvfVdCd2/ONjcDa3eV8sM5H7s1hnTGJl8DB7ArtrhNiirxEllD0Fw==", "requires": { "credentials-context": "^2.0.0", "jsonld": "github:european-epc-competence-center/jsonld.js#cachefix", diff --git a/api/package.json b/api/package.json index fc28def..25884d8 100644 --- a/api/package.json +++ b/api/package.json @@ -31,7 +31,7 @@ "@digitalbazaar/ecdsa-sd-2023-cryptosuite": "^1.0.2", "@digitalbazaar/ed25519-signature-2018": "^4.0.0", "@digitalbazaar/ed25519-signature-2020": "^5.2.0", - "@digitalbazaar/vc": "^6.2.0", + "@digitalbazaar/vc": "^6.3.0", "@digitalbazaar/vc-revocation-list": "^5.0.1", "@digitalbazaar/vc-status-list": "^7.1.0", "cors": "^2.8.5", @@ -64,4 +64,4 @@ "overrides": { "jsonld": "$jsonld" } -} \ No newline at end of file +} diff --git a/api/src/routers/verify/index.ts b/api/src/routers/verify/index.ts index dbadadf..968efd8 100644 --- a/api/src/routers/verify/index.ts +++ b/api/src/routers/verify/index.ts @@ -1,13 +1,11 @@ -import { Router } from 'express'; -import { VerifyRoutes } from '../../routes/index.js'; - +import { Router } from "express"; +import { VerifyRoutes } from "../../routes/index.js"; const verifyRoutes = new VerifyRoutes(); -const { fetchAndVerify, verify, verifySubjectsVCs } = verifyRoutes +const { fetchAndVerify, verify, verifySubjectsVCs, verifyGS1 } = verifyRoutes; export const verifyRouter = Router(); - /** * API model of a credentialSubject * @summary The minimal form of a credential subject @@ -15,7 +13,6 @@ export const verifyRouter = Router(); * @property {string} id.required - The identifier of the identity which the credential refers to */ - /** * API model of a signed credential * @summary Refers to W3C Credential @@ -24,8 +21,8 @@ export const verifyRouter = Router(); * @property {string} id - The id of the the credential as an IRI * @property {array} type.required - The types of the credential * @property {string} issuer.required - The DID of the issuer of the credential - * @property {string} issuanceDate.required - The issuance date of the credential in ISO format 2022-09-26T09:01:07.437Z - * @property {string} expirationDate - The expiration date of the credential in ISO format 2022-09-26T09:01:07.437Z + * @property {string} issuanceDate.required - The issuance date of the credential in ISO format 2022-09-26T09:01:07.437Z + * @property {string} expirationDate - The expiration date of the credential in ISO format 2022-09-26T09:01:07.437Z * @property {CredentialSubject} credentialSubject.required - The actual claim of the credential * @property {object} proof.required - The cryptographic signature of the issuer over the credential */ @@ -104,7 +101,7 @@ export const verifyRouter = Router(); ] } */ -verifyRouter.get('/vc/:vcid', fetchAndVerify); +verifyRouter.get("/vc/:vcid", fetchAndVerify); /** * POST /api/verifier @@ -464,7 +461,7 @@ verifyRouter.get('/vc/:vcid', fetchAndVerify); } ] */ -verifyRouter.post('/', verify); +verifyRouter.post("/", verify); /** * GET /api/verifier/id/{subjectId} @@ -512,5 +509,6 @@ verifyRouter.post('/', verify); } ] */ -verifyRouter.get('/id/:subjectId', verifySubjectsVCs); +verifyRouter.get("/id/:subjectId", verifySubjectsVCs); +verifyRouter.post("/gs1", verifyGS1); diff --git a/api/src/routes/verify/index.ts b/api/src/routes/verify/index.ts index daac82f..615f6e8 100644 --- a/api/src/routes/verify/index.ts +++ b/api/src/routes/verify/index.ts @@ -1,98 +1,134 @@ -import { NextFunction, Request, response, Response } from 'express'; -import { StatusCodes } from 'http-status-codes'; +import { NextFunction, Request, response, Response } from "express"; +import { StatusCodes } from "http-status-codes"; -import { Verifier, fetch_json } from '../../services/index.js'; - -const VC_REGISTRY = process.env.VC_REGISTRY ? process.env.VC_REGISTRY : 'https://ssi.eecc.de/api/registry/vcs/'; +import { Verifier, fetch_json } from "../../services/index.js"; +import { GS1Verifier } from "../../services/verifier/gs1.js"; +const VC_REGISTRY = process.env.VC_REGISTRY + ? process.env.VC_REGISTRY + : "https://ssi.eecc.de/api/registry/vcs/"; export class VerifyRoutes { - - fetchAndVerify = async (req: Request, res: Response, next: NextFunction): Promise => { - - try { - - // fetch credential - let credential; - - try { - - credential = await fetch_json(decodeURIComponent(req.params.vcid)) as Verifiable; - - } catch (error) { - console.log(error) - return res.status(StatusCodes.NOT_FOUND).send('Credential not found!\n' + error); - } - - const result = await Verifier.verify(credential); - - return res.status(StatusCodes.OK).json(result); - - } catch (error) { - return res.status(StatusCodes.BAD_REQUEST).send('Something went wrong!\n' + error); - } - + fetchAndVerify = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + // fetch credential + let credential; + + try { + credential = (await fetch_json( + decodeURIComponent(req.params.vcid) + )) as Verifiable; + } catch (error) { + console.log(error); + return res + .status(StatusCodes.NOT_FOUND) + .send("Credential not found!\n" + error); + } + + const result = await Verifier.verify(credential); + + return res.status(StatusCodes.OK).json(result); + } catch (error) { + return res + .status(StatusCodes.BAD_REQUEST) + .send("Something went wrong!\n" + error); } - - verify = async (req: Request, res: Response, next: NextFunction): Promise => { - - try { - - // Support W3C and JWT namespaces - const challenge = req.query.challenge || req.query.nonce; - const domain = req.query.domain || req.query.audience || req.query.aud; - - if (challenge && typeof challenge != 'string') throw new Error('The challenge/nonce must be provided as a string!'); - - if (domain && typeof domain != 'string') throw new Error('The domain/audience must be provided as a string!'); - - let tasks = Promise.all(req.body.map(function (verifialbe: Verifiable) { - - return Verifier.verify(verifialbe, challenge, domain); - - })); - - // wait for all verifiables to be verified - const results = await tasks; - - return res.status(StatusCodes.OK).json(results); - - } catch (error) { - return res.status(StatusCodes.BAD_REQUEST).send('Something went wrong verifying!\n' + error); - } - + }; + + verify = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + // Support W3C and JWT namespaces + const challenge = req.query.challenge || req.query.nonce; + const domain = req.query.domain || req.query.audience || req.query.aud; + + if (challenge && typeof challenge != "string") + throw new Error("The challenge/nonce must be provided as a string!"); + + if (domain && typeof domain != "string") + throw new Error("The domain/audience must be provided as a string!"); + + let tasks = Promise.all( + req.body.map(function (verifialbe: Verifiable) { + return Verifier.verify(verifialbe, challenge, domain); + }) + ); + + // wait for all verifiables to be verified + const results = await tasks; + + return res.status(StatusCodes.OK).json(results); + } catch (error) { + console.error(error); + return res + .status(StatusCodes.BAD_REQUEST) + .send("Something went wrong verifying!\n" + error); } - - verifySubjectsVCs = async (req: Request, res: Response, next: NextFunction): Promise => { - - try { - - // fetch credentials - let credentials: Verifiable[]; - - try { - - credentials = await fetch_json(VC_REGISTRY + encodeURIComponent(req.params.subjectId)) as [Verifiable]; - - } catch (error) { - return res.status(StatusCodes.NOT_FOUND); - } - - let tasks = Promise.all(credentials.map(function (vc) { - - return Verifier.verify(vc); - - })); - - // wait for all vcs to be verified - const results = await tasks; - - return res.status(StatusCodes.OK).json(results); - - } catch (error) { - return res.status(StatusCodes.BAD_REQUEST).send('Something went wrong!\n' + error); - } - + }; + + verifySubjectsVCs = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + // fetch credentials + let credentials: Verifiable[]; + + try { + credentials = (await fetch_json( + VC_REGISTRY + encodeURIComponent(req.params.subjectId) + )) as [Verifiable]; + } catch (error) { + return res.status(StatusCodes.NOT_FOUND); + } + + let tasks = Promise.all( + credentials.map(function (vc) { + return Verifier.verify(vc); + }) + ); + + // wait for all vcs to be verified + const results = await tasks; + + return res.status(StatusCodes.OK).json(results); + } catch (error) { + return res + .status(StatusCodes.BAD_REQUEST) + .send("Something went wrong!\n" + error); } - -} \ No newline at end of file + }; + + verifyGS1 = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + const challenge = req.query.challenge || req.query.nonce; + const domain = req.query.domain || req.query.audience || req.query.aud; + try { + if (challenge && typeof challenge != "string") + throw new Error("The challenge/nonce must be provided as a string!"); + + if (domain && typeof domain != "string") + throw new Error("The domain/audience must be provided as a string!"); + + return res + .status(StatusCodes.OK) + .json(await GS1Verifier.verify(req.body, challenge, domain)); + } catch (error) { + console.error(error); + return res + .status(StatusCodes.BAD_REQUEST) + .send("Something went wrong verifying GS1 credential!\n" + error); + } + }; +} diff --git a/api/src/services/documentLoader/index.ts b/api/src/services/documentLoader/index.ts index 104a54c..28f2cb9 100644 --- a/api/src/services/documentLoader/index.ts +++ b/api/src/services/documentLoader/index.ts @@ -1,76 +1,82 @@ // @ts-ignore -import jsonldSignatures from 'jsonld-signatures'; -import { getResolver } from './didresolver.js'; -import { fetch_jsonld, fetchIPFS } from '../fetch/index.js'; -import { contexts } from './context/index.js'; +import jsonldSignatures from "jsonld-signatures"; +import { getResolver } from "./didresolver.js"; +import { fetch_jsonld, fetchIPFS } from "../fetch/index.js"; +import { contexts } from "./context/index.js"; const cache = contexts; -const uncachedStatusListCredentialTypes = ['RevocationList2020Credential', 'StatusList2021Credential'] - -const documentLoader: Promise = jsonldSignatures.extendContextLoader(async (url: string) => { +const uncachedStatusListCredentialTypes = [ + "RevocationList2020Credential", + "StatusList2021Credential", +]; +const documentLoader: (url: string) => Promise = + jsonldSignatures.extendContextLoader(async (url: string) => { // Fetch did documents - if (url.startsWith('did:')) { - - const [did, verificationMethod] = url.split('#') - - // fetch document - const didDocument: any = (await getResolver().resolve(url)).didDocument - - // if a verifcation method of the DID document is queried (not yet implemented in the official resolver) - if (verificationMethod && didDocument) { - - const verificationMethodDoc: any | undefined = didDocument.verificationMethod.filter(function (method: any) { - return method.id === url || method.id === verificationMethod; - })[0]; - - if (!verificationMethodDoc) - throw new jsonldSignatures.VerificationError( - new Error(`${verificationMethod} is an unknown verification method for ${did}`) - ); - - return { - contextUrl: null, - documentUrl: url, - // deliver verification method with the DID doc context - document: Object.assign(verificationMethodDoc, { '@context': verificationMethodDoc['@context'] || didDocument['@context'] }), - }; - - } + if (url.startsWith("did:")) { + const [did, verificationMethod] = url.split("#"); + + // fetch document + const didDocument: any = (await getResolver().resolve(url)).didDocument; + + // if a verifcation method of the DID document is queried (not yet implemented in the official resolver) + if (verificationMethod && didDocument) { + const verificationMethodDoc: any | undefined = + didDocument.verificationMethod.filter(function (method: any) { + return method.id === url || method.id === verificationMethod; + })[0]; + + if (!verificationMethodDoc) + throw new jsonldSignatures.VerificationError( + new Error( + `${verificationMethod} is an unknown verification method for ${did}` + ) + ); return { - contextUrl: null, - documentUrl: url, - document: didDocument, + contextUrl: null, + documentUrl: url, + // deliver verification method with the DID doc context + document: Object.assign(verificationMethodDoc, { + "@context": + verificationMethodDoc["@context"] || didDocument["@context"], + }), }; + } + + return { + contextUrl: null, + documentUrl: url, + document: didDocument, + }; } let document = cache.get(url); // fetch if not in cache if (!document) { - - if (url.startsWith('ipfs://')) { - - document = await fetchIPFS(url); - - } else { - - document = await fetch_jsonld(url); - - } - - if (!document.type || !Array.isArray(document.type) || !uncachedStatusListCredentialTypes.some((t: string) => document.type.includes(t))) cache.set(url, document); - + if (url.startsWith("ipfs://")) { + document = await fetchIPFS(url); + } else { + document = await fetch_jsonld(url); + } + + if ( + !document.type || + !Array.isArray(document.type) || + !uncachedStatusListCredentialTypes.some((t: string) => + document.type.includes(t) + ) + ) + cache.set(url, document); } return { - contextUrl: null, - documentUrl: url, - document: document, + contextUrl: null, + documentUrl: url, + document: document, }; + }); -}); - -export { documentLoader } \ No newline at end of file +export { documentLoader }; diff --git a/api/src/services/verifier/gs1.ts b/api/src/services/verifier/gs1.ts new file mode 100644 index 0000000..d1fb16f --- /dev/null +++ b/api/src/services/verifier/gs1.ts @@ -0,0 +1,81 @@ +import { + checkGS1CredentialPresentation, + checkGS1CredentialWithoutPresentation, + externalCredential, + verifyExternalCredential, + gs1RulesResult, + gs1RulesResultContainer, + verificationErrorCode, + VerifiableCredential, + VerifiablePresentation, + // @ts-ignore +} from "@gs1us/vc-verifier-rules"; + +import { documentLoader } from "../documentLoader/index.js"; +import { Verifier } from "./index.js"; + + +export function getVerifierFunction(challenge?: string, domain?: string) { + return async function (verifiable: any) { + return await Verifier.verify(verifiable, challenge, domain); + }; +} + +const getExternalCredential: externalCredential = async ( + url: string +): Promise => { + const extendedVC = await documentLoader(url); + return extendedVC.document; +}; + +export async function checkGS1Credential( + verifiableCredential: VerifiableCredential, + checkExternalCredential: verifyExternalCredential +): Promise { + return await checkGS1CredentialWithoutPresentation( + getExternalCredential, + checkExternalCredential, + verifiableCredential + ); +} + +// Check if the Verifiable Presentation for any GS1 Credential and if so check the GS1 Credential Rules +export async function verifyGS1Credentials( + verifiablePresentation: VerifiablePresentation, + checkExternalCredential: verifyExternalCredential +): Promise { + return await checkGS1CredentialPresentation( + getExternalCredential, + checkExternalCredential, + verifiablePresentation + ); +} + +export class GS1Verifier { + static async verify( + verifiable: Verifiable, + challenge?: string, + domain?: string + ): Promise { + let result; + if (verifiable.type.includes("VerifiableCredential")) { + result = await checkGS1Credential( + verifiable, + getVerifierFunction(challenge, domain) + ); + } + + if (verifiable.type.includes("VerifiablePresentation")) { + const presentation = verifiable as VerifiablePresentation; + + result = await verifyGS1Credentials( + presentation, + getVerifierFunction(challenge, domain) + ); + } + + if (!result) throw Error("Provided verifiable object is of unknown type!"); + + return result; + } +} diff --git a/api/src/services/verifier/index.ts b/api/src/services/verifier/index.ts index e9cacd2..023fe2e 100644 --- a/api/src/services/verifier/index.ts +++ b/api/src/services/verifier/index.ts @@ -1,173 +1,187 @@ // @ts-ignore -import { verifyCredential, verify } from '@digitalbazaar/vc'; +import { verifyCredential, verify } from "@digitalbazaar/vc"; // @ts-ignore -import { Ed25519Signature2018 } from '@digitalbazaar/ed25519-signature-2018'; +import { Ed25519Signature2018 } from "@digitalbazaar/ed25519-signature-2018"; // @ts-ignore -import { Ed25519Signature2020 } from '@digitalbazaar/ed25519-signature-2020'; +import { Ed25519Signature2020 } from "@digitalbazaar/ed25519-signature-2020"; // @ts-ignore -import { checkStatus as checkStatus2020 } from '@digitalbazaar/vc-revocation-list'; +import { checkStatus as checkStatus2020 } from "@digitalbazaar/vc-revocation-list"; // @ts-ignore -import { checkStatus as checkStatus2021 } from '@digitalbazaar/vc-status-list'; +import { checkStatus as checkStatus2021 } from "@digitalbazaar/vc-status-list"; // @ts-ignore -import * as ecdsaSd2023Cryptosuite from '@digitalbazaar/ecdsa-sd-2023-cryptosuite'; +import * as ecdsaSd2023Cryptosuite from "@digitalbazaar/ecdsa-sd-2023-cryptosuite"; // @ts-ignore -import { DataIntegrityProof } from '@digitalbazaar/data-integrity'; +import { DataIntegrityProof } from "@digitalbazaar/data-integrity"; // @ts-ignore -import jsigs from 'jsonld-signatures'; +import jsigs from "jsonld-signatures"; -import { documentLoader } from '../documentLoader/index.js'; +import { documentLoader } from "../documentLoader/index.js"; const { createVerifyCryptosuite } = ecdsaSd2023Cryptosuite; -const { purposes: { AssertionProofPurpose } } = jsigs; +const { + purposes: { AssertionProofPurpose }, +} = jsigs; function getSuite(proof: Proof): unknown[] { + switch (proof.type) { + case "Ed25519Signature2018": + return new Ed25519Signature2018(); - switch (proof.type) { + case "Ed25519Signature2020": + return new Ed25519Signature2020(); - case 'Ed25519Signature2018': return new Ed25519Signature2018(); - - case 'Ed25519Signature2020': return new Ed25519Signature2020(); - - case 'DataIntegrityProof': return new DataIntegrityProof({ - cryptosuite: createVerifyCryptosuite() - }); - - default: throw new Error(`${proof.type} not implemented`); - } + case "DataIntegrityProof": + return new DataIntegrityProof({ + cryptosuite: createVerifyCryptosuite(), + }); + default: + throw new Error(`${proof.type} not implemented`); + } } function getSuites(proof: Proof | Proof[]): unknown[] { + var suites: unknown[] = []; - var suites: unknown[] = [] + if (Array.isArray(proof)) { + proof.forEach((proof: Proof) => suites.push(getSuite(proof))); + } else { + suites = [getSuite(proof)]; + } - if (Array.isArray(proof)) { - proof.forEach((proof: Proof) => suites.push(getSuite(proof))); - } else { - suites = [getSuite(proof)] - } - - // always for status verification - suites.push(new Ed25519Signature2020()) - - return suites; + // always for status verification + suites.push(new Ed25519Signature2020()); + return suites; } -function getPresentationStatus(presentation: VerifiablePresentation): CredentialStatus[] | CredentialStatus | undefined { - - if (!presentation.verifiableCredential) return undefined; - - const credentials = ( - Array.isArray(presentation.verifiableCredential) - ? presentation.verifiableCredential - : [presentation.verifiableCredential] - ) - .filter((credential: VerifiableCredential) => credential.credentialStatus); +function getPresentationStatus( + presentation: VerifiablePresentation +): CredentialStatus[] | CredentialStatus | undefined { + if (!presentation.verifiableCredential) return undefined; - if (credentials.length == 0) return undefined; + const credentials = ( + Array.isArray(presentation.verifiableCredential) + ? presentation.verifiableCredential + : [presentation.verifiableCredential] + ).filter((credential: VerifiableCredential) => credential.credentialStatus); - if (credentials.length == 1) return credentials[0].credentialStatus; + if (credentials.length == 0) return undefined; - const statusTypes = credentials.map((credential: VerifiableCredential) => { - return Array.isArray(credential.credentialStatus) - ? credential.credentialStatus.map((credentialStatus: CredentialStatus) => credentialStatus.type) - : credential.credentialStatus.type - }); + if (credentials.length == 1) return credentials[0].credentialStatus; - // disallow multiple status types - if (new Set(statusTypes.flat(1)).size > 1) throw new Error('Currently only one status type is allowed within one presentation!'); + const statusTypes = credentials.map((credential: VerifiableCredential) => { + return Array.isArray(credential.credentialStatus) + ? credential.credentialStatus.map( + (credentialStatus: CredentialStatus) => credentialStatus.type + ) + : credential.credentialStatus.type; + }); - return credentials[0].credentialStatus; + // disallow multiple status types + if (new Set(statusTypes.flat(1)).size > 1) + throw new Error( + "Currently only one status type is allowed within one presentation!" + ); + return credentials[0].credentialStatus; } -function getCheckStatus(credentialStatus?: CredentialStatus[] | CredentialStatus): any | undefined { - // no status provided - if (!credentialStatus) return undefined; +function getCheckStatus( + credentialStatus?: CredentialStatus[] | CredentialStatus +): any | undefined { + // no status provided + if (!credentialStatus) return undefined; - let statusTypes = []; + let statusTypes = []; - if (Array.isArray(credentialStatus)) { - statusTypes = credentialStatus.map(cs => cs.type); - } - else statusTypes = [credentialStatus.type] - - if (statusTypes.includes('StatusList2021Entry')) return checkStatus2021; + if (Array.isArray(credentialStatus)) { + statusTypes = credentialStatus.map((cs) => cs.type); + } else statusTypes = [credentialStatus.type]; - if (statusTypes.includes('RevocationList2020Status')) return checkStatus2020; + if (statusTypes.includes("StatusList2021Entry")) return checkStatus2021; - throw new Error(`${statusTypes} not implemented`); + if (statusTypes.includes("RevocationList2020Status")) return checkStatus2020; + throw new Error(`${statusTypes} not implemented`); } - export class Verifier { + static async verify( + verifiable: Verifiable, + challenge?: string, + domain?: string + ): Promise { + const suite = getSuites(verifiable.proof); + + let result; + + if (verifiable.type.includes("VerifiableCredential")) { + const credential = verifiable as VerifiableCredential; + + const checkStatus = getCheckStatus(credential.credentialStatus); + + if ( + (Array.isArray(credential.proof) + ? credential.proof[0].type + : credential.proof.type) == "DataIntegrityProof" + ) { + result = await jsigs.verify(credential, { + suite, + purpose: new AssertionProofPurpose(), + documentLoader, + }); - static async verify(verifiable: Verifiable, challenge?: string, domain?: string): Promise { - - const suite = getSuites(verifiable.proof); - - let result; - - if (verifiable.type.includes('VerifiableCredential')) { - - const credential = verifiable as VerifiableCredential; - - const checkStatus = getCheckStatus(credential.credentialStatus); - - if ((Array.isArray(credential.proof) ? credential.proof[0].type : credential.proof.type) == 'DataIntegrityProof') { - - result = await jsigs.verify(credential, { - suite, - purpose: new AssertionProofPurpose(), - documentLoader - }); - - // make manual status as long as not implemented in jsigs - if (checkStatus) { - result.statusResult = await checkStatus({ - credential, - documentLoader, - suite, - verifyStatusListCredential: true, - verifyMatchingIssuers: false - }); - if (!result.statusResult.verified) { - result.verified = false; - } - } - - - } else { - - result = await verifyCredential({ credential, suite, documentLoader, checkStatus }); - - } - } - - if (verifiable.type.includes('VerifiablePresentation')) { - - const presentation = verifiable as VerifiablePresentation; - - // try to use challenge in proof if not provided in case no exchange protocol is used - if (!challenge) challenge = (Array.isArray(presentation.proof) ? presentation.proof[0].challenge : presentation.proof.challenge); - - - const checkStatus = getCheckStatus( - getPresentationStatus(presentation) - ); - - result = await verify({ presentation, suite, documentLoader, challenge, domain, checkStatus }); - + // make manual status as long as not implemented in jsigs + if (checkStatus) { + result.statusResult = await checkStatus({ + credential, + documentLoader, + suite, + verifyStatusListCredential: true, + verifyMatchingIssuers: false, + }); + if (!result.statusResult.verified) { + result.verified = false; + } } + } else { + result = await verifyCredential({ + credential, + suite, + documentLoader, + checkStatus, + }); + } + } - if (!result) throw Error('Provided verifiable object is of unknown type!'); + if (verifiable.type.includes("VerifiablePresentation")) { + const presentation = verifiable as VerifiablePresentation; + + // try to use challenge in proof if not provided in case no exchange protocol is used + if (!challenge) + challenge = Array.isArray(presentation.proof) + ? presentation.proof[0].challenge + : presentation.proof.challenge; + + const checkStatus = getCheckStatus(getPresentationStatus(presentation)); + + result = await verify({ + presentation, + suite, + documentLoader, + challenge, + domain, + checkStatus, + }); + } - // make non enumeratable errors enumeratable for the respsonse - if (result.error && !result.error.errors) result.error.name = result.error.message; + if (!result) throw Error("Provided verifiable object is of unknown type!"); - return result; - } + // make non enumeratable errors enumeratable for the respsonse + if (result.error && !result.error.errors) + result.error.name = result.error.message; -} \ No newline at end of file + return result; + } +} diff --git a/frontend/src/api.js b/frontend/src/api.js index e067c0b..d9d5bd5 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,6 +1,6 @@ import axios from 'axios' const axiosInstance = axios.create({ - baseURL: process.env.VERIFIER_API || 'https://ssi.eecc.de/api/verifier', + baseURL: process.env.VERIFIER_API || 'http://localhost:3000/api/verifier', timeout: 5000, headers: { 'Accept': 'application/ld+json,application/json,*/*' diff --git a/frontend/src/components/Credential.vue b/frontend/src/components/Credential.vue index a63ff07..2a266c1 100644 --- a/frontend/src/components/Credential.vue +++ b/frontend/src/components/Credential.vue @@ -6,7 +6,8 @@
-
{{ getCredentialType(credential) }}
+
{{ getCredentialType(credential) }}
@@ -184,7 +185,7 @@ import { Tooltip } from 'bootstrap'; import pdfMake from "pdfmake/build/pdfmake"; import pdfFonts from "pdfmake/build/vfs_fonts"; import { credentialPDF } from '../pdf.js'; -import { getPlainCredential, getCredentialType } from '../utils.js'; +import { getPlainCredential, getCredentialType, isGs1Credential } from '../utils.js'; import * as JsHashes from 'jshashes'; pdfMake.vfs = pdfFonts.pdfMake.vfs; @@ -207,7 +208,8 @@ export default { return { toast: useToast(), getPlainCredential: getPlainCredential, - getCredentialType: getCredentialType + getCredentialType: getCredentialType, + isGs1Credential: isGs1Credential } }, mounted() { diff --git a/frontend/src/utils.js b/frontend/src/utils.js index f47bb08..6e7bc94 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -1,95 +1,119 @@ -import jsonld from 'jsonld'; -import { demoAuthPresentation } from './store/demoAuth'; +import jsonld from 'jsonld' +import { demoAuthPresentation } from './store/demoAuth' export const VerifiableType = { - CREDENTIAL: 'VerifiableCredential', - PRESENTATION: 'VerifiablePresentation' -}; + CREDENTIAL: 'VerifiableCredential', + PRESENTATION: 'VerifiablePresentation', +} const IPFS_GATEWAYS = ['ipfs.io', 'ipfs.ssi.eecc.de'] export function isURL(url) { - if (typeof url != 'string') return false; - return url.startsWith('https://'); + if (typeof url != 'string') return false + return url.startsWith('https://') } export function getCredentialValue(value) { - return typeof value === 'object' ? value.value || value['@value'] || JSON.stringify(value, null, 2) : value; + return typeof value === 'object' + ? value.value || value['@value'] || JSON.stringify(value, null, 2) + : value } export function getPlainCredential(credential) { - var clean_credential = { ...credential }; - delete clean_credential.verified; - delete clean_credential.revoked; - delete clean_credential.suspended; - delete clean_credential.status; - delete clean_credential.presentation; - delete clean_credential.context; - return clean_credential; + var clean_credential = { ...credential } + delete clean_credential.verified + delete clean_credential.revoked + delete clean_credential.suspended + delete clean_credential.status + delete clean_credential.presentation + delete clean_credential.context + return clean_credential } export function getVerifiableType(verifiable) { - if (verifiable.type.includes(VerifiableType.PRESENTATION)) return VerifiableType.PRESENTATION; - return VerifiableType.CREDENTIAL; + if (verifiable.type.includes(VerifiableType.PRESENTATION)) + return VerifiableType.PRESENTATION + return VerifiableType.CREDENTIAL } export function getCredentialType(credential) { - return credential.type.length > 1 ? credential.type.filter((c) => c != 'VerifiableCredential')[0] : credential.type[0]; + return credential.type.length > 1 + ? credential.type.filter((c) => c != 'VerifiableCredential')[0] + : credential.type[0] } export function getHolder(presentation) { - if (presentation.holder) return presentation.holder; - const proof = Array.isArray(presentation.proof) ? presentation.proof[0] : presentation.proof - return proof.verificationMethod.split('#')[0]; + if (presentation.holder) return presentation.holder + const proof = Array.isArray(presentation.proof) + ? presentation.proof[0] + : presentation.proof + return proof.verificationMethod.split('#')[0] } export async function fetchIPFS(IPFSUrl) { - - var document; - - await Promise.any(IPFS_GATEWAYS.map(async (gateway) => { - - return await fetch(`https://${gateway}/ipfs/${IPFSUrl.split('ipfs://')[1]}`); - - })) - .then((result) => { - - document = result; - - }) - .catch((error) => { - console.error(error) - }) - - if (!document) throw Error('Fetching from IPFS failed'); - - return document; - + var document + + await Promise.any( + IPFS_GATEWAYS.map(async (gateway) => { + return await fetch( + `https://${gateway}/ipfs/${IPFSUrl.split('ipfs://')[1]}`, + ) + }), + ) + .then((result) => { + document = result + }) + .catch((error) => { + console.error(error) + }) + + if (!document) throw Error('Fetching from IPFS failed') + + return document } - const documentLoader = async (url) => { - let document; - if (url.startsWith('ipfs://')) { - - document = await fetchIPFS(url) - - } else document = await fetch(url); - - return { - contextUrl: null, - document: await document.json(), - documentUrl: url - }; -}; - + let document + if (url.startsWith('ipfs://')) { + document = await fetchIPFS(url) + } else document = await fetch(url) + + return { + contextUrl: null, + document: await document.json(), + documentUrl: url, + } +} export async function getContext(credential) { - const resolved = await jsonld.processContext(await jsonld.processContext(null, null), credential, { documentLoader }); - return resolved.mappings; + const resolved = await jsonld.processContext( + await jsonld.processContext(null, null), + credential, + { documentLoader }, + ) + return resolved.mappings } export function isDemoAuth(auth) { - return auth != undefined && JSON.stringify(auth) == JSON.stringify(demoAuthPresentation); + return ( + auth != undefined && + JSON.stringify(auth) == JSON.stringify(demoAuthPresentation) + ) } +const gs1CredentialTypes = [ + 'OrganizationDataCredential', + 'GS1PrefixLicenseCredential', + 'GS1CompanyPrefixLicenseCredential', + 'KeyCredential', + 'ProductDataCredential', +] + +const gs1CredentialContext = 'https://ref.gs1.org/gs1/vc/license-context' + +export function isGs1Credential(credential) { + return ( + credential['@context'].includes(gs1CredentialContext) && + credential.type.some((type) => gs1CredentialTypes.includes(type)) + ) +} diff --git a/frontend/src/views/Verify.vue b/frontend/src/views/Verify.vue index c5dc78d..1327d04 100644 --- a/frontend/src/views/Verify.vue +++ b/frontend/src/views/Verify.vue @@ -35,7 +35,7 @@