From 94b29b7190d33a82a12bc1f243e72a3b7a10d40a Mon Sep 17 00:00:00 2001 From: F-Node-Karlsruhe Date: Tue, 23 Jul 2024 11:27:18 +0200 Subject: [PATCH] refactor gs verifier Signed-off-by: F-Node-Karlsruhe --- api/__tests__/gs1.test.ts | 114 +++++++++++++++ api/src/routers/verify/index.ts | 20 ++- api/src/routes/verify/index.ts | 218 +++++++++++++++++------------ api/src/services/verifier/gs1.ts | 37 +++++ api/src/services/verifier/index.ts | 54 ++----- 5 files changed, 301 insertions(+), 142 deletions(-) create mode 100644 api/__tests__/gs1.test.ts diff --git a/api/__tests__/gs1.test.ts b/api/__tests__/gs1.test.ts new file mode 100644 index 0000000..4ffd025 --- /dev/null +++ b/api/__tests__/gs1.test.ts @@ -0,0 +1,114 @@ +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", + }, +}; + +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); + }); +}); 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/verifier/gs1.ts b/api/src/services/verifier/gs1.ts index 0cb2c87..a96f999 100644 --- a/api/src/services/verifier/gs1.ts +++ b/api/src/services/verifier/gs1.ts @@ -12,9 +12,12 @@ import { } from "@gs1us/vc-verifier-rules"; import { documentLoader } from "../documentLoader/index.js"; +import { Verifier } from "./index.js"; export const gs1CredentialTypes = [ "OrganizationDataCredential", + "GS1PrefixLicenseCredential", + "GS1CompanyPrefixLicenseCredential", "KeyCredential", "ProductDataCredential", ]; @@ -28,6 +31,11 @@ export function isGs1Credential(credential: VerifiableCredential): boolean { credential.type.some((type: string) => gs1CredentialTypes.includes(type)) ); } +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 @@ -58,3 +66,32 @@ export async function verifyGS1Credentials( 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 dfc4608..80105ef 100644 --- a/api/src/services/verifier/index.ts +++ b/api/src/services/verifier/index.ts @@ -151,24 +151,12 @@ export class Verifier { } } } else { - if (isGs1Credential(credential)) { - result = await checkGS1Credential( - verifyCredential({ - credential, - suite, - documentLoader, - checkStatus, - }), - credential - ); - } else { - result = await verifyCredential({ - credential, - suite, - documentLoader, - checkStatus, - }); - } + result = await verifyCredential({ + credential, + suite, + documentLoader, + checkStatus, + }); } } @@ -183,28 +171,14 @@ export class Verifier { const checkStatus = getCheckStatus(getPresentationStatus(presentation)); - if (isGs1Credential(presentation)) { - result = await verifyGS1Credentials( - verify({ - presentation, - suite, - documentLoader, - challenge, - domain, - checkStatus, - }), - presentation - ); - } else { - result = await verify({ - presentation, - suite, - documentLoader, - challenge, - domain, - checkStatus, - }); - } + result = await verify({ + presentation, + suite, + documentLoader, + challenge, + domain, + checkStatus, + }); } if (!result) throw Error("Provided verifiable object is of unknown type!");