diff --git a/data/issuer-config.json b/data/issuer-config.json index 15b7936..e128cc0 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -882,7 +882,137 @@ } } } + }, + "ferryBoardingPassCredential": { + "format": "vc_jwt", + "scope": "ferryBoardingPassCredential", + "cryptographic_binding_methods_supported": ["jwk"], + "cryptographic_suites_supported": ["ES256"], + "display": [ + { + "name": "Ferry Boarding Pass", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "credential_definition": { + "type": ["ferryBoardingPassCredential"], + "claims": { + "identifier": { + "display": [ + { + "name": "Identifier", + "locale": "en-GB" + } + ] + }, + "ticketQR": { + "display": [ + { + "name": "Ticket QR Code", + "locale": "en-GB" + } + ] + }, + "ticketNumber": { + "display": [ + { + "name": "Ticket Number", + "locale": "en-GB" + } + ] + }, + "ticketLet": { + "display": [ + { + "name": "Ticket Letter", + "locale": "en-GB" + } + ] + }, + "lastName": { + "display": [ + { + "name": "Last Name", + "locale": "en-GB" + } + ] + }, + "firstName": { + "display": [ + { + "name": "First Name", + "locale": "en-GB" + } + ] + }, + "seatType": { + "display": [ + { + "name": "Seat Type", + "locale": "en-GB" + } + ] + }, + "seatNumber": { + "display": [ + { + "name": "Seat Number", + "locale": "en-GB" + } + ] + }, + "departureDate": { + "display": [ + { + "name": "Departure Date", + "locale": "en-GB" + } + ] + }, + "departureTime": { + "display": [ + { + "name": "Departure Time", + "locale": "en-GB" + } + ] + }, + "arrivalDate": { + "display": [ + { + "name": "Arrival Date", + "locale": "en-GB" + } + ] + }, + "arrivalTime": { + "display": [ + { + "name": "Arrival Time", + "locale": "en-GB" + } + ] + }, + "arrivalPort": { + "display": [ + { + "name": "Arrival Port", + "locale": "en-GB" + } + ] + }, + "vesselDescription": { + "display": [ + { + "name": "Vessel Description", + "locale": "en-GB" + } + ] + } + } + } } - } } diff --git a/data/presentation_definition_ferryboardingpass.json b/data/presentation_definition_ferryboardingpass.json new file mode 100644 index 0000000..b1027c7 --- /dev/null +++ b/data/presentation_definition_ferryboardingpass.json @@ -0,0 +1,66 @@ +{ + "id": "d49ee616-0e8d-4698-aff5-2a8a2362652d", + "name": "ferry-boarding-pass-proof", + "format": { + "jwt_vc": { + "alg": ["ES256", "ES384"] + } + }, + "input_descriptors": [ + { + "id": "abd4acb1-1dcb-41ad-8596-ceb1401a69c7", + "format": { + "jwt_vc": { + "alg": ["ES256", "ES384"] + } + }, + "constraints": { + "fields": [ + { + "path": ["$.credentialSubject.identifier", "$.vc.credentialSubject.identifier"] + }, + { + "path": ["$.credentialSubject.ticketQR", "$.vc.credentialSubject.ticketQR"] + }, + { + "path": ["$.credentialSubject.ticketNumber", "$.vc.credentialSubject.ticketNumber"] + }, + { + "path": ["$.credentialSubject.ticketLet", "$.vc.credentialSubject.ticketLet"] + }, + { + "path": ["$.credentialSubject.lastName", "$.vc.credentialSubject.lastName"] + }, + { + "path": ["$.credentialSubject.firstName", "$.vc.credentialSubject.firstName"] + }, + { + "path": ["$.credentialSubject.seatType", "$.vc.credentialSubject.seatType"] + }, + { + "path": ["$.credentialSubject.seatNumber", "$.vc.credentialSubject.seatNumber"] + }, + { + "path": ["$.credentialSubject.departureDate", "$.vc.credentialSubject.departureDate"] + }, + { + "path": ["$.credentialSubject.departureTime", "$.vc.credentialSubject.departureTime"] + }, + { + "path": ["$.credentialSubject.arrivalDate", "$.vc.credentialSubject.arrivalDate"] + }, + { + "path": ["$.credentialSubject.arrivalTime", "$.vc.credentialSubject.arrivalTime"] + }, + { + "path": ["$.credentialSubject.arrivalPort", "$.vc.credentialSubject.arrivalPort"] + }, + { + "path": ["$.credentialSubject.vesselDescription", "$.vc.credentialSubject.vesselDescription"] + } + ] + }, + "limit_disclosure": "required" + } + ] +} diff --git a/routes/boardingPassRoutes.js b/routes/boardingPassRoutes.js new file mode 100644 index 0000000..f11fbfe --- /dev/null +++ b/routes/boardingPassRoutes.js @@ -0,0 +1,76 @@ +import express from "express"; +import fs from "fs"; +import { v4 as uuidv4 } from "uuid"; +import { + pemToJWK, + generateNonce, + base64UrlEncodeSha256, +} from "../utils/cryptoUtils.js"; +import { + buildAccessToken, + generateRefreshToken, + buildIdToken, +} from "../utils/tokenUtils.js"; + +import { + getAuthCodeSessions, + getPreCodeSessions, +} from "../services/cacheService.js"; + +import { SDJwtVcInstance } from "@sd-jwt/sd-jwt-vc"; +import { + createSignerVerifier, + digest, + generateSalt, +} from "../utils/sdjwtUtils.js"; +import jwt from "jsonwebtoken"; + +import qr from "qr-image"; +import imageDataURI from "image-data-uri"; +import { streamToBuffer } from "@jorgeferrero/stream-to-buffer"; + +const boardingPassRouter = express.Router(); + +const serverURL = process.env.SERVER_URL || "http://localhost:3000"; + +const privateKey = fs.readFileSync("./private-key.pem", "utf-8"); +const publicKeyPem = fs.readFileSync("./public-key.pem", "utf-8"); + +boardingPassRouter.get(["/pre-offer-jwt-bpass"], async (req, res) => { + const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); + const preSessions = getPreCodeSessions(); + if (preSessions.sessions.indexOf(uuid) < 0) { + preSessions.sessions.push(uuid); + preSessions.results.push({ sessionId: uuid, status: "pending" }); + } + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-pre-jwt-bpass/${uuid}`; //OfferUUID + let code = qr.image(credentialOffer, { + type: "png", + ec_level: "H", + size: 10, + margin: 10, + }); + let mediaType = "PNG"; + let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); + res.json({ + qr: encodedQR, + deepLink: credentialOffer, + sessionId: uuid, + }); +}); + +boardingPassRouter.get(["/credential-offer-pre-jwt-bpass/:id"], (req, res) => { + res.json({ + credential_issuer: serverURL, + credentials: ["ferryBoardingPassCredential"], + grants: { + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": req.params.id, + user_pin_required: true, + }, + }, + }); +}); + + +export default boardingPassRouter; diff --git a/routes/routes.js b/routes/routes.js index 5848472..967fad3 100644 --- a/routes/routes.js +++ b/routes/routes.js @@ -422,6 +422,49 @@ router.post("/credential", async (req, res) => { ).toISOString(), }, }; + } else if ( + requestedCredentials != null && + requestedCredentials[0] === "ferryBoardingPassCredential" + ) { + payload = { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + iat: Math.floor(Date.now() / 1000), // Token issued at time + exp: Math.floor(Date.now() / 1000) + 60 * 60, // Token expiration time (1 hour from now) + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + type: ["VerifiableCredential", "ferryBoardingPassCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + issuer: serverURL, + credentialSubject: { + id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID + identifier: "John Doe", + ticketQR: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", + ticketNumber: "ABC123456789", + ticketLet: "A", + lastName: "Doe", + firstName: "John", + seatType: "Economy", + seatNumber: "12A", + departureDate: "2023-11-30", + departureTime: "13:07:34", + arrivalDate: "2023-11-30", + arrivalTime: "15:30:00", + arrivalPort: "NYC", + vesselDescription: "Ferry XYZ", + }, + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60) * 1000 + ).toISOString(), + validFrom: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + }, + }; } else { //sign as jwt payload = { diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index 6f2f9b1..29afbf0 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -41,6 +41,13 @@ const presentation_definition_educational_id = JSON.parse( const presentation_definition_alliance_id = JSON.parse( fs.readFileSync("./data/presentation_definition_alliance_id.json", "utf-8") ); + +const presentation_definition_ferryboardingpass = JSON.parse( + fs.readFileSync( + "./data/presentation_definition_ferryboardingpass.json", + "utf-8" + ) +); // const jwks = pemToJWK(publicKeyPem, "public"); @@ -260,11 +267,13 @@ verifierRouter.get("/vpRequest/:type/:id", async (req, res) => { presentationDefinition = presentation_definition_pid; } else if (type === "epassport") { presentationDefinition = presentation_definition_epass; - } else if (type === "educationId" || type === "educationid" ) { + } else if (type === "educationId" || type === "educationid") { presentationDefinition = presentation_definition_educational_id; - } else if(type==="allianceId" || type === "allianceid"){ + } else if (type === "allianceId" || type === "allianceid") { presentationDefinition = presentation_definition_alliance_id; - }else { + } else if (type === "ferryboardingpass") { + presentationDefinition = presentation_definition_ferryboardingpass; + } else { return res.status(400).type("text/plain").send("Invalid type parameter"); } diff --git a/server.js b/server.js index 75a9f07..c045b68 100644 --- a/server.js +++ b/server.js @@ -5,6 +5,7 @@ import verifierRouter from "./routes/verifierRoutes.js"; import metadataRouter from "./routes/metadataroutes.js"; import codeFlowRouter from "./routes/codeFlowJwtRoutes.js"; import codeFlowRouterSDJWT from "./routes/codeFlowSdJwtRoutes.js"; +import boardingPassRouter from "./routes/boardingPassRoutes.js"; import pidRouter from "./routes/pidroutes.js"; import passportRouter from "./routes/passportRoutes.js"; import educationalRouter from "./routes/educationalRoutes.js"; @@ -31,6 +32,7 @@ app.use("/", codeFlowRouterSDJWT); app.use("/", pidRouter); app.use("/", passportRouter); app.use("/", educationalRouter); +app.use("/", boardingPassRouter); // Start the server app.listen(port, () => {