diff --git a/data/issuer-config.json b/data/issuer-config.json index 41476b7..af84016 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -1,6 +1,6 @@ { "credential_issuer": "https://server.example.com", - "authorization_server" :"https://server.example.com", + "authorization_server": "https://server.example.com", "credential_endpoint": "https://server.example.com/credential", "deferred_credential_endpoint": "https://server.example.com/credential_deferred", "display": [ @@ -15,12 +15,8 @@ "VerifiablePortableDocumentA1": { "format": "vc+sd-jwt", "scope": "VerifiablePortableDocumentA1", - "cryptographic_binding_methods_supported": [ - "jwk" - ], - "cryptographic_suites_supported": [ - "ES256" - ], + "cryptographic_binding_methods_supported": ["jwk"], + "cryptographic_suites_supported": ["ES256"], "display": [ { "name": "Portable Document A1", @@ -62,12 +58,8 @@ "VerifiablePortableDocumentA2": { "format": "vc_jwt", "scope": "VerifiablePortableDocumentA2", - "cryptographic_binding_methods_supported": [ - "jwk" - ], - "cryptographic_suites_supported": [ - "ES256" - ], + "cryptographic_binding_methods_supported": ["jwk"], + "cryptographic_suites_supported": ["ES256"], "display": [ { "name": "Portable Document A2", @@ -109,15 +101,11 @@ "PID": { "format": "vc_jwt", "scope": "PID", - "cryptographic_binding_methods_supported": [ - "jwk" - ], - "cryptographic_suites_supported": [ - "ES256" - ], + "cryptographic_binding_methods_supported": ["jwk"], + "cryptographic_suites_supported": ["ES256"], "display": [ { - "name": "Personal Identification Information", + "name": "PID", "locale": "en-GB", "background_color": "#12107c", "text_color": "#FFFFFF" @@ -192,6 +180,515 @@ } } } + }, + "ePassportCredential": { + "format": "vc_jwt", + "scope": "ePassportCredential", + "cryptographic_binding_methods_supported": ["jwk"], + "cryptographic_suites_supported": ["ES256"], + "display": [ + { + "name": "ePassportCredential", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "credential_definition": { + "type": ["ePassportCredential"], + "claims": { + "id": { + "display": [ + { + "name": "id", + "locale": "en-GB" + } + ] + }, + "electronicPassport": { + "display": [ + { + "name": "electronicPassport", + "locale": "en-GB" + } + ], + "properties": { + "dataGroup1": { + "display": [ + { + "name": "dataGroup1", + "locale": "en-GB" + } + ], + "properties": { + "birthdate": { + "display": [ + { + "name": "birthdate", + "locale": "en-GB" + } + ] + }, + "docTypeCode": { + "display": [ + { + "name": "docTypeCode", + "locale": "en-GB" + } + ] + }, + "expiryDate": { + "display": [ + { + "name": "expiryDate", + "locale": "en-GB" + } + ] + }, + "genderCode": { + "display": [ + { + "name": "genderCode", + "locale": "en-GB" + } + ] + }, + "holdersName": { + "display": [ + { + "name": "holdersName", + "locale": "en-GB" + } + ] + }, + "issuerCode": { + "display": [ + { + "name": "issuerCode", + "locale": "en-GB" + } + ] + }, + "natlText": { + "display": [ + { + "name": "natlText", + "locale": "en-GB" + } + ] + }, + "passportNumberIdentifier": { + "display": [ + { + "name": "passportNumberIdentifier", + "locale": "en-GB" + } + ] + } + } + }, + "dataGroup15": { + "display": [ + { + "name": "dataGroup15", + "locale": "en-GB" + } + ], + "properties": { + "activeAuthentication": { + "display": [ + { + "name": "activeAuthentication", + "locale": "en-GB" + } + ], + "properties": { + "publicKeyBinaryObject": { + "display": [ + { + "name": "publicKeyBinaryObject", + "locale": "en-GB" + } + ] + } + } + } + } + }, + "dataGroup2EncodedFaceBiometrics": { + "display": [ + { + "name": "dataGroup2EncodedFaceBiometrics", + "locale": "en-GB" + } + ], + "properties": { + "faceBiometricDataEncodedPicture": { + "display": [ + { + "name": "faceBiometricDataEncodedPicture", + "locale": "en-GB" + } + ] + } + } + }, + "digitalTravelCredential": { + "display": [ + { + "name": "digitalTravelCredential", + "locale": "en-GB" + } + ], + "properties": { + "contentInfo": { + "display": [ + { + "name": "contentInfo", + "locale": "en-GB" + } + ], + "properties": { + "versionNumber": { + "display": [ + { + "name": "versionNumber", + "locale": "en-GB" + } + ] + }, + "signatureInfo": { + "display": [ + { + "name": "signatureInfo", + "locale": "en-GB" + } + ], + "properties": { + "digestHashAlgorithmIdentifier": { + "display": [ + { + "name": "digestHashAlgorithmIdentifier", + "locale": "en-GB" + } + ] + }, + "signatureAlgorithmIdentifier": { + "display": [ + { + "name": "signatureAlgorithmIdentifier", + "locale": "en-GB" + } + ] + }, + "signatureCertificateText": { + "display": [ + { + "name": "signatureCertificateText", + "locale": "en-GB" + } + ] + }, + "signatureDigestResultBinaryObject": { + "display": [ + { + "name": "signatureDigestResultBinaryObject", + "locale": "en-GB" + } + ] + }, + "signedAttributes": { + "display": [ + { + "name": "signedAttributes", + "locale": "en-GB" + } + ], + "properties": { + "attributeTypeCode": { + "display": [ + { + "name": "attributeTypeCode", + "locale": "en-GB" + } + ] + }, + "attributeValueText": { + "display": [ + { + "name": "attributeValueText", + "locale": "en-GB" + } + ] + } + } + } + } + } + } + }, + "dataCapabilitiesInfo": { + "display": [ + { + "name": "dataCapabilitiesInfo", + "locale": "en-GB" + } + ], + "properties": { + "dataTransferInterfaceTypeCode": { + "display": [ + { + "name": "dataTransferInterfaceTypeCode", + "locale": "en-GB" + } + ] + }, + "securityAssuranceLevelIndText": { + "display": [ + { + "name": "securityAssuranceLevelIndText", + "locale": "en-GB" + } + ] + }, + "userConsentInfoText": { + "display": [ + { + "name": "userConsentInfoText", + "locale": "en-GB" + } + ] + }, + "virtualComponentPresenceInd": { + "display": [ + { + "name": "virtualComponentPresenceInd", + "locale": "en-GB" + } + ] + } + } + }, + "dataContent": { + "display": [ + { + "name": "dataContent", + "locale": "en-GB" + } + ], + "properties": { + "dataGroup1": { + "display": [ + { + "name": "dataGroup1", + "locale": "en-GB" + } + ], + "properties": { + "birthdate": { + "display": [ + { + "name": "birthdate", + "locale": "en-GB" + } + ] + }, + "docTypeCode": { + "display": [ + { + "name": "docTypeCode", + "locale": "en-GB" + } + ] + }, + "expiryDate": { + "display": [ + { + "name": "expiryDate", + "locale": "en-GB" + } + ] + }, + "genderCode": { + "display": [ + { + "name": "genderCode", + "locale": "en-GB" + } + ] + }, + "holdersName": { + "display": [ + { + "name": "holdersName", + "locale": "en-GB" + } + ] + }, + "issuerCode": { + "display": [ + { + "name": "issuerCode", + "locale": "en-GB" + } + ] + }, + "natlText": { + "display": [ + { + "name": "natlText", + "locale": "en-GB" + } + ] + }, + "passportNumberIdentifier": { + "display": [ + { + "name": "passportNumberIdentifier", + "locale": "en-GB" + } + ] + }, + "personalNumberIdentifier": { + "display": [ + { + "name": "personalNumberIdentifier", + "locale": "en-GB" + } + ] + } + } + }, + "dataGroup2EncodedFaceBiometrics": { + "display": [ + { + "name": "dataGroup2EncodedFaceBiometrics", + "locale": "en-GB" + } + ], + "properties": { + "faceBiometricDataEncodedPicture": { + "display": [ + { + "name": "faceBiometricDataEncodedPicture", + "locale": "en-GB" + } + ] + } + } + }, + "docSecurityObject": { + "display": [ + { + "name": "docSecurityObject", + "locale": "en-GB" + } + ], + "properties": { + "dataGroupHash": { + "display": [ + { + "name": "dataGroupHash", + "locale": "en-GB" + } + ], + "properties": { + "dataGroupNumber": { + "display": [ + { + "name": "dataGroupNumber", + "locale": "en-GB" + } + ] + }, + "valueBinaryObject": { + "display": [ + { + "name": "valueBinaryObject", + "locale": "en-GB" + } + ] + } + } + }, + "digestHashAlgorithmIdentifier": { + "display": [ + { + "name": "digestHashAlgorithmIdentifier", + "locale": "en-GB" + } + ] + }, + "versionNumber": { + "display": [ + { + "name": "versionNumber", + "locale": "en-GB" + } + ] + } + } + } + } + }, + "docSecurityObject": { + "display": [ + { + "name": "docSecurityObject", + "locale": "en-GB" + } + ], + "properties": { + "dataGroupHash": { + "display": [ + { + "name": "dataGroupHash", + "locale": "en-GB" + } + ], + "properties": { + "dataGroupNumber": { + "display": [ + { + "name": "dataGroupNumber", + "locale": "en-GB" + } + ] + }, + "valueBinaryObject": { + "display": [ + { + "name": "valueBinaryObject", + "locale": "en-GB" + } + ] + } + } + }, + "digestHashAlgorithmIdentifier": { + "display": [ + { + "name": "digestHashAlgorithmIdentifier", + "locale": "en-GB" + } + ] + }, + "versionNumber": { + "display": [ + { + "name": "versionNumber", + "locale": "en-GB" + } + ] + } + } + } + } + } + } + } + } + } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index a90a6d2..abba75c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "SERVER_URL=https://4a82-2a02-587-8714-6000-389c-be54-a907-e4df.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://96d8-2a02-587-870b-2f00-d697-1738-3249-fb5f.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/passportRoutes.js b/routes/passportRoutes.js new file mode 100644 index 0000000..525ca99 --- /dev/null +++ b/routes/passportRoutes.js @@ -0,0 +1,80 @@ +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 passportRouter = 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"); + + +passportRouter.get(["/pre-offer-jwt-passport"], 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-passport/${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, + }); + }); + + + passportRouter.get(["/credential-offer-pre-jwt-passport/:id"], (req, res) => { + res.json({ + credential_issuer: serverURL, + credentials: ["ePassportCredential"], + grants: { + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": req.params.id, + user_pin_required: true, + }, + }, + }); + }); + + + + +export default passportRouter; \ No newline at end of file diff --git a/routes/pidroutes.js b/routes/pidroutes.js new file mode 100644 index 0000000..0c55b3b --- /dev/null +++ b/routes/pidroutes.js @@ -0,0 +1,80 @@ +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 pidRouter = 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"); + + +pidRouter.get(["/pre-offer-jwt-pid"], 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-pid/${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, + }); + }); + + + pidRouter.get(["/credential-offer-pre-jwt-pid/:id"], (req, res) => { + res.json({ + credential_issuer: serverURL, + credentials: ["PID"], + grants: { + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": req.params.id, + user_pin_required: true, + }, + }, + }); + }); + + + + +export default pidRouter; \ No newline at end of file diff --git a/routes/routes.js b/routes/routes.js index 3affc87..0adc71d 100644 --- a/routes/routes.js +++ b/routes/routes.js @@ -67,8 +67,6 @@ router.get(["/offer"], async (req, res) => { }); }); - - //pre-auth flow request sd-jwt router.get(["/credential-offer/:id"], (req, res) => { res.json({ @@ -122,9 +120,6 @@ router.get(["/credential-offer-pre-jwt/:id"], (req, res) => { }); }); - - - router.post("/token_endpoint", async (req, res) => { //pre-auth code flow const grantType = req.body.grant_type; @@ -182,6 +177,7 @@ router.post("/credential", async (req, res) => { // Accessing the body data const requestBody = req.body; const format = requestBody.format; + const requestedCredentials = requestBody.types; //TODO valiate bearer header let decodedWithHeader; let decodedHeaderSubjectDID; @@ -194,34 +190,180 @@ router.post("/credential", async (req, res) => { // console.log(credential); if (format === "jwt_vc") { - //sign as jwt - const payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - exp: Math.floor(Date.now() / 1000) + 60 * 60, // Token expiration time (1 hour from now) - iat: Math.floor(Date.now() / 1000), // Token issued at time - // nbf: Math.floor(Date.now() / 1000), - jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", - vc: { - credentialSubject: { - id: null, - given_name: "John", - last_name: "Doe", + let payload = {}; + if (requestedCredentials != null && requestedCredentials[0] === "PID") { + payload = { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60, // Token expiration time (1 hour from now) + iat: Math.floor(Date.now() / 1000), // Token issued at time + // nbf: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + credentialSubject: { + id: decodedHeaderSubjectDID, + family_name: "Doe", + given_name: "John", + birth_date: "1990-01-01", + age_over_18: true, + issuance_date: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + expiry_date: new Date( + Math.floor(Date.now() + 60 / 1000) * 1000 + ).toISOString(), + issuing_authority: "https://authority.example.com", + issuing_country: "GR", + }, + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60) * 1000 + ).toISOString(), + id: decodedHeaderSubjectDID, + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + issued: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + issuer: serverURL, + type: ["PID"], + validFrom: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), }, - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60) * 1000 - ).toISOString(), - id: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", - issuanceDate: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - issued: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), - issuer: serverURL, - type: ["VerifiablePortableDocumentA2"], - validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), - }, - // Optional claims - }; + // Optional claims + }; + } else { + if ( + requestedCredentials != null && + requestedCredentials[0] === "ePassportCredential" + ) { + 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: { + credentialSubject: { + id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID + electronicPassport: { + dataGroup1: { + birthdate: "1990-01-01", + docTypeCode: "P", + expiryDate: "2030-01-01", + genderCode: "M", + holdersName: "John Doe", + issuerCode: "GR", + natlText: "Hellenic", + passportNumberIdentifier: "123456789", + }, + dataGroup15: { + activeAuthentication: { + publicKeyBinaryObject: "somePublicKeyUri", + }, + }, + dataGroup2EncodedFaceBiometrics: { + faceBiometricDataEncodedPicture: "someBiometricUri", + }, + digitalTravelCredential: { + contentInfo: { + versionNumber: 1, + signatureInfo: { + digestHashAlgorithmIdentifier: "SHA-256", + signatureAlgorithmIdentifier: "RS256", + signatureCertificateText: "someCertificateText", + signatureDigestResultBinaryObject: "someDigestResultUri", + signedAttributes: { + attributeTypeCode: "someTypeCode", + attributeValueText: "someValueText", + }, + }, + }, + dataCapabilitiesInfo: { + dataTransferInterfaceTypeCode: "NFC", + securityAssuranceLevelIndText: "someSecurityLevel", + userConsentInfoText: "userConsentRequired", + virtualComponentPresenceInd: true, + }, + dataContent: { + dataGroup1: { + birthdate: "1990-01-01", + docTypeCode: "P", + expiryDate: "2030-01-01", + genderCode: "M", + holdersName: "John Doe", + issuerCode: "GR", + natlText: "Hellenic", + passportNumberIdentifier: "123456789", + personalNumberIdentifier: "987654321", + }, + dataGroup2EncodedFaceBiometrics: { + faceBiometricDataEncodedPicture: "someBiometricUri", + }, + docSecurityObject: { + dataGroupHash: [ + { + dataGroupNumber: 1, + valueBinaryObject: "someHashUri", + }, + ], + digestHashAlgorithmIdentifier: "SHA-256", + versionNumber: 1, + }, + }, + docSecurityObject: { + dataGroupHash: [ + { + dataGroupNumber: 1, + valueBinaryObject: "someHashUri", + }, + ], + digestHashAlgorithmIdentifier: "SHA-256", + versionNumber: 1, + }, + }, + }, + }, + type: ["ePassportCredential"], + validFrom: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + }, + }; + } else { + //sign as jwt + payload = { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60, // Token expiration time (1 hour from now) + iat: Math.floor(Date.now() / 1000), // Token issued at time + // nbf: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + credentialSubject: { + id: null, + given_name: "John", + last_name: "Doe", + }, + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60) * 1000 + ).toISOString(), + id: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + issued: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + issuer: serverURL, + type: ["VerifiablePortableDocumentA2"], + validFrom: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + }, + // Optional claims + }; + } + } const signOptions = { algorithm: "ES256", // Specify the signing algorithm diff --git a/server.js b/server.js index cc7458b..308fcb0 100644 --- a/server.js +++ b/server.js @@ -3,8 +3,10 @@ import express from "express"; import router from "./routes/routes.js"; 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 codeFlowRouter from "./routes/codeFlowJwtRoutes.js"; +import codeFlowRouterSDJWT from "./routes/codeFlowSdJwtRoutes.js"; +import pidRouter from "./routes/pidroutes.js"; +import passportRouter from "./routes/passportRoutes.js"; import bodyParser from "body-parser"; // Body parser middleware const app = express(); @@ -25,6 +27,8 @@ app.use("/", verifierRouter); app.use("/", metadataRouter); app.use("/", codeFlowRouter); app.use("/", codeFlowRouterSDJWT); +app.use("/", pidRouter); +app.use("/", passportRouter); // Start the server app.listen(port, () => {