From 835d2fd06a6d4e9e69fe8c74233da835b42a6f1f Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Mon, 23 Sep 2024 14:45:14 +0300 Subject: [PATCH 01/37] first atttempt at oidcv13 --- data/issuer-config.json | 1000 +------------------- data/oauth-config.json | 57 +- routes/codeFlowJwtRoutes.js | 141 +-- routes/codeFlowSdJwtRoutes.js | 294 +++++- routes/jwtVcRoutes.js | 90 ++ routes/metadataroutes.js | 12 + routes/{routes.js => preAuthSDjwRoutes.js} | 325 +++---- routes/verifierRoutes.js | 30 +- server.js | 2 +- services/cacheService.js | 18 + utils/cryptoUtils.js | 49 +- utils/personasUtils.js | 65 ++ 12 files changed, 722 insertions(+), 1361 deletions(-) create mode 100644 routes/jwtVcRoutes.js rename routes/{routes.js => preAuthSDjwRoutes.js} (85%) create mode 100644 utils/personasUtils.js diff --git a/data/issuer-config.json b/data/issuer-config.json index 33e6701..c36f12d 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -8,15 +8,15 @@ "name": "UAegean", "location": "UAegean", "locale": "en-GB", - "description": "For queries about how we are managing your data please contact the Data Protection Officer." + "description": "University of the Aegean RFC Issuer for EWC" } ], - "credentials_supported": { + "credential_configurations_supported": { "VerifiablePortableDocumentA1": { "format": "vc+sd-jwt", "scope": "VerifiablePortableDocumentA1", "cryptographic_binding_methods_supported": ["jwk"], - "cryptographic_suites_supported": ["ES256"], + "credential_signing_alg_values_supported": ["ES256"], "display": [ { "name": "Portable Document A1", @@ -35,8 +35,8 @@ "locale": "en-GB" }, { - "name": "Vorname", - "locale": "de-DE" + "name": "ΟΝΟΜΑ", + "locale": "gr-GR" } ] }, @@ -47,998 +47,14 @@ "locale": "en-GB" }, { - "name": "Nachname", - "locale": "de-DE" - } - ] - } - } - } - }, - "VerifiablePortableDocumentA2": { - "format": "jwt_vc_json", - "scope": "VerifiablePortableDocumentA2", - "cryptographic_binding_methods_supported": ["jwk"], - "cryptographic_suites_supported": ["ES256"], - "display": [ - { - "name": "Portable Document A2", - "locale": "en-GB", - "background_color": "#12107c", - "text_color": "#FFFFFF" - } - ], - "credential_definition": { - "type": ["VerifiablePortableDocumentA2"], - "claims": { - "given_name": { - "display": [ - { - "name": "Given Name", - "locale": "en-GB" - }, - { - "name": "Vorname", - "locale": "de-DE" - } - ] - }, - "last_name": { - "display": [ - { - "name": "Surname", - "locale": "en-GB" - }, - { - "name": "Nachname", - "locale": "de-DE" - } - ] - } - } - } - }, - "PID": { - "format": "jwt_vc_json", - "scope": "PID", - "cryptographic_binding_methods_supported": ["jwk"], - "cryptographic_suites_supported": ["ES256"], - "display": [ - { - "name": "PID", - "locale": "en-GB", - "background_color": "#12107c", - "text_color": "#FFFFFF", - "logo":{ - "url":"https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", - "alt_text":"UAegean" - - } - } - ], - "credential_definition": { - "type": ["PID"], - "claims": { - "given_name": { - "display": [ - { - "name": "Given Name", - "locale": "en-GB" - } - ] - }, - "family_name": { - "display": [ - { - "name": "Surname", - "locale": "en-GB" - } - ] - }, - "birth_date": { - "display": [ - { - "name": "Birth Date", - "locale": "en-GB" - } - ] - }, - "age_over_18": { - "display": [ - { - "name": "Age Over 18", - "locale": "en-GB" - } - ] - }, - "issuance_date": { - "display": [ - { - "name": "Issuance Date", - "locale": "en-GB" - } - ] - }, - "expiry_date": { - "display": [ - { - "name": "Expiry Date", - "locale": "en-GB" - } - ] - }, - "issuing_authority": { - "display": [ - { - "name": "Issuing Authority", - "locale": "en-GB" - } - ] - }, - "issuing_country": { - "display": [ - { - "name": "Issuing Country", - "locale": "en-GB" - } - ] - } - } - } - }, - "ePassportCredential": { - "format": "jwt_vc_json", - "scope": "ePassportCredential", - "cryptographic_binding_methods_supported": ["jwk"], - "cryptographic_suites_supported": ["ES256"], - "display": [ - { - "name": "ePassportCredential", - "locale": "en-GB", - "background_color": "#12107c", - "text_color": "#FFFFFF", - "logo":{ - "url":"https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", - "alt_text":"UAegean" - - } - - } - ], - "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" - } - ] - } - } - } - } - } - } - } - } - } - }, - - "StudentID": { - "format": "jwt_vc_json", - "scope": "StudentID", - "cryptographic_binding_methods_supported": ["jwk"], - "cryptographic_suites_supported": ["ES256"], - "display": [ - { - "name": "StudentID", - "locale": "en-GB", - "background_color": "#12107c", - "text_color": "#FFFFFF", - "logo":{ - "url":"https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", - "alt_text":"UAegean" - - } - } - ], - "credential_definition": { - "type": ["StudentID"], - "claims": { - "identifier": { - "display": [ - { - "name": "Identifier", - "locale": "en-GB" - } - ] - }, - "schacPersonalUniqueCode": { - "display": [ - { - "name": "SCHAC Personal Unique Code", - "locale": "en-GB" - } - ] - }, - "schacPersonalUniqueID": { - "display": [ - { - "name": "SCHAC Personal Unique ID", - "locale": "en-GB" - } - ] - }, - "schacHomeOrganization": { - "display": [ - { - "name": "SCHAC Home Organization", - "locale": "en-GB" - } - ] - }, - "familyName": { - "display": [ - { - "name": "Family Name", - "locale": "en-GB" - } - ] - }, - "firstName": { - "display": [ - { - "name": "First Name", - "locale": "en-GB" - } - ] - }, - "displayName": { - "display": [ - { - "name": "Display Name", - "locale": "en-GB" - } - ] - }, - "dateOfBirth": { - "display": [ - { - "name": "Date of Birth", - "locale": "en-GB" - } - ] - }, - "commonName": { - "display": [ - { - "name": "Common Name", - "locale": "en-GB" - } - ] - }, - "mail": { - "display": [ - { - "name": "Email", - "locale": "en-GB" - } - ] - }, - "eduPersonPrincipalName": { - "display": [ - { - "name": "eduPerson Principal Name", - "locale": "en-GB" - } - ] - }, - "eduPersonPrimaryAffiliation": { - "display": [ - { - "name": "eduPerson Primary Affiliation", - "locale": "en-GB" - } - ] - }, - "eduPersonAffiliation": { - "display": [ - { - "name": "eduPerson Affiliation", - "locale": "en-GB" - } - ] - }, - "eduPersonScopedAffiliation": { - "display": [ - { - "name": "eduPerson Scoped Affiliation", - "locale": "en-GB" - } - ] - }, - "eduPersonAssurance": { - "display": [ - { - "name": "eduPerson Assurance", - "locale": "en-GB" - } - ] - } - } - } - }, - "allianceIDCredential": { - "format": "jwt_vc_json", - "scope": "allianceIDCredential", - "cryptographic_binding_methods_supported": ["jwk"], - "cryptographic_suites_supported": ["ES256"], - "display": [ - { - "name": "AllianceIDCredential", - "locale": "en-GB", - "background_color": "#12107c", - "text_color": "#FFFFFF", - "logo":{ - "url":"https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", - "alt_text":"UAegean" - - } - } - ], - "credential_definition": { - "type": ["allianceIDCredential"], - "claims": { - "identifier": { - "display": [ - { - "name": "Identifier", - "locale": "en-GB" - } - ], - "properties": { - "schemeID": { - "display": [ - { - "name": "Scheme ID", - "locale": "en-GB" - } - ] - }, - "value": { - "display": [ - { - "name": "Value", - "locale": "en-GB" - } - ] - }, - "id": { - "display": [ - { - "name": "ID", - "locale": "en-GB" - } - ] - } - } - } - } - } - }, - "ferryBoardingPassCredential": { - "format": "jwt_vc_json", - "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", - "logo":{ - "url":"https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", - "alt_text":"UAegean" - - } - } - ], - "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" + "name": "ΕΠΩΝΥΜΟ", + "locale": "gr-GR" } ] } } } } + } } diff --git a/data/oauth-config.json b/data/oauth-config.json index 3c47120..6036f0e 100644 --- a/data/oauth-config.json +++ b/data/oauth-config.json @@ -1,50 +1,32 @@ { "issuer": "https://server.example.com", "authorization_endpoint": "https://server.example.com/authorize", + "pushed_authorization_request_endpoint": "https://server.example.com/par", + "require_pushed_authorization_requests": true, "token_endpoint": "https://server.example.com/token", "jwks_uri": "https://server.example.com/jwks", - "scopes_supported": [ - "openid" - ], - "response_types_supported": [ - "vp_token", - "id_token" - ], - "response_modes_supported": [ - "query" - ], - "grant_types_supported": [ - "authorization_code" - ], - "subject_types_supported": [ - "public" - ], - "id_token_signing_alg_values_supported": [ - "ES256" - ], - "request_object_signing_alg_values_supported": [ - "ES256" - ], + "scopes_supported": ["openid"], + "response_types_supported": ["code", "vp_token", "id_token"], + "response_modes_supported": ["query"], + "grant_types_supported": ["authorization_code"], + "subject_types_supported": ["public", "pairwise"], + "id_token_signing_alg_values_supported": ["ES256"], + + "pre-authorized_grant_anonymous_access_supported": true, + + "request_object_signing_alg_values_supported": ["ES256"], "request_parameter_supported": true, "request_uri_parameter_supported": true, - "token_endpoint_auth_methods_supported": [ - "private_key_jwt" - ], + "token_endpoint_auth_methods_supported": ["none"], "request_authentication_methods_supported": { - "authorization_endpoint": [ - "request_object" - ] + "authorization_endpoint": ["request_object"] }, "vp_formats_supported": { "jwt_vp": { - "alg_values_supported": [ - "ES256" - ] + "alg_values_supported": ["ES256"] }, "jwt_vc_json": { - "alg_values_supported": [ - "ES256" - ] + "alg_values_supported": ["ES256"] } }, "subject_syntax_types_supported": [ @@ -52,12 +34,9 @@ "did:ebsi:v1", "did:ebsi:v2" ], - "subject_trust_frameworks_supported": [ - "ebsi", - "ewc-issuer-trust-list" - ], + "subject_trust_frameworks_supported": ["ebsi", "ewc-issuer-trust-list"], "id_token_types_supported": [ "subject_signed_id_token", "attester_signed_id_token" ] -} \ No newline at end of file +} diff --git a/routes/codeFlowJwtRoutes.js b/routes/codeFlowJwtRoutes.js index 83b8c59..784a355 100644 --- a/routes/codeFlowJwtRoutes.js +++ b/routes/codeFlowJwtRoutes.js @@ -1,7 +1,7 @@ import express from "express"; import fs from "fs"; import { v4 as uuidv4 } from "uuid"; -import { generateNonce, buildVpRequestJwt } from "../utils/cryptoUtils.js"; +import { generateNonce, buildVpRequestJSON } from "../utils/cryptoUtils.js"; import { getAuthCodeSessions } from "../services/cacheService.js"; import qr from "qr-image"; @@ -57,148 +57,9 @@ codeFlowRouter.get(["/credential-offer-code/:id"], (req, res) => { }); }); -codeFlowRouter.get("/authorize", async (req, res) => { - const responseType = req.query.response_type; - const scope = req.query.scope; - const issuerState = decodeURIComponent(req.query.issuer_state); // This can be associated with the ITB session - const state = req.query.state; - const clientId = decodeURIComponent(req.query.client_id); //DID of the holder requesting the credential - let authorizationDetails = ""; - try { - authorizationDetails = decodeURIComponent(req.query.authorization_details); //TODO this contains the credentials requested - } catch (error) { - console.log( - "No credentials requested! req.query.authorization_details missing!" - ); - errors.push( - "No credentials requested! req.query.authorization_details missing!" - ); - } - - const redirectUri = decodeURIComponent(req.query.redirect_uri); - const nonce = req.query.nonce; - const codeChallenge = decodeURIComponent(req.query.code_challenge); - const codeChallengeMethod = req.query.code_challenge_method; //this should equal to S256 - try { - const clientMetadata = JSON.parse( - decodeURIComponent(req.query.client_metadata) - ); - } catch (error) { - console.log("client_metadata was missing"); - console.log(error); - } - //validations - let errors = []; - /* - [{"format":"jwt_vc", - "locations":["https://issuer.example.com"], - "type":"openid_credential", - "types":["VerifiableCredential","VerifiableAttestation","VerifiablePortableDocumentA1"]}] - */ - if (!authorizationDetails) { - errors.push("no credentials requested"); - console.log(`no credentials requested`); - } else { - try{ - if (authorizationDetails.credential_definition) { - console.log( - `credential ${authorizationDetails.credential_definition.type} was requested` - ); - } else if (authorizationDetails.types) { - //EBSI style - console.log(`credential ${authorizationDetails.types} was requested`); - } - }catch(error){ - errors.push("no credentials requested"); - } - - } - if (responseType !== "code") { - errors.push("Invalid response_type"); - } - if (!scope.includes("openid")) { - errors.push("Invalid scope"); - } - - // If validations pass, redirect with a 302 Found response - const authorizationCode = null; //"SplxlOBeZQQYbYS6WxSbIA"; - const codeSessions = getAuthCodeSessions(); - if (codeSessions.sessions.indexOf(issuerState) >= 0) { - codeSessions.requests.push({ - challenge: codeChallenge, - method: codeChallengeMethod, - sessionId: authorizationCode, - issuerState: issuerState, - state: state, - }); - codeSessions.results.push({ - sessionId: authorizationCode, - issuerState: issuerState, - state: state, - status: "pending", - }); - codeSessions.walletSessions.push(state); // push state as send by wallet - } else { - console.log("ITB session not found"); - } - // codeSessions.sessions.push(issuerState); - - // for normal response not requesting VP from wallet - //const redirectUrl = `${redirectUri}?code=${authorizationCode}&state=${state}`; - - //5.1.5. Dynamic Credential Request https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-12.html#name-successful-authorization-re - const vpRequestJWT = buildVpRequestJwt( - state, - nonce, - clientId, - "response_uri", - null, - "jwk", - serverURL, - privateKey - ); - const redirectUrl = `http://localhost:8080?state=${state}&client_id=${clientId}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request=${vpRequestJWT}`; - - if (errors.length > 0) { - console.error("Validation errors:", errors); - let error_description = ""; - errors.forEach((element) => { - error_description += element + " "; - }); - const encodedErrorDescription = encodeURIComponent( - error_description.trim() - ); - const errorRedirectUrl = `${redirectUri}?error=invalid_request&error_description=${encodedErrorDescription}`; - //TODO mark the codeFlowSession as failed - return res.redirect(302, errorRedirectUrl); - } else { - return res.redirect(302, redirectUrl); - } -}); - -codeFlowRouter.post("/direct_post_vci", async (req, res) => { - console.log("direct_post VP for VCI is below!"); - let state = req.body["state"]; - let jwt = req.body["id_token"]; - if (jwt) { - const codeSessions = getAuthCodeSessions(); - const authorizationCode = generateNonce(16); - updateIssuerStateWithAuthCode( - authorizationCode, - state, - codeSessions.walletSessions, - codeSessions.results, - codeSessions.requests - ); - const redirectUrl = `http://localhost:8080?code=${authorizationCode}&state=${state}`; - return res.redirect(302, redirectUrl); - } else { - return res.sendStatus(500); - } -}); function updateIssuerStateWithAuthCode( code, diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 6c3b7e7..be6fa62 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -1,7 +1,12 @@ import express from "express"; import fs from "fs"; import { v4 as uuidv4 } from "uuid"; -import { getAuthCodeSessions } from "../services/cacheService.js"; +import { + getAuthCodeSessions, + getPushedAuthorizationRequests, + getSessionsAuthorizationDetail, + getAuthCodeAuthorizationDetail +} from "../services/cacheService.js"; import qr from "qr-image"; import imageDataURI from "image-data-uri"; @@ -11,8 +16,6 @@ const codeFlowRouterSDJWT = express.Router(); const serverURL = process.env.SERVER_URL || "http://localhost:3000"; - - // auth code flow codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); @@ -51,4 +54,289 @@ codeFlowRouterSDJWT.get(["/credential-offer-code-sd-jwt/:id"], (req, res) => { }); }); +/*************************************************************** + * Push Authoriozation Request Endpoints + * https://datatracker.ietf.org/doc/html/rfc9126 + ***************************************************************/ +codeFlowRouterSDJWT.post("/par", async (req, res) => { + const client_id = req.body.client_id; //DID of the holder requesting the credential + const scope = req.body.scope; + const response_type = req.body.response_type; + const redirect_uri = req.body.redirect_uri; + const code_challenge = req.body.code_challenge; + const code_challenge_method = req.body.code_challenge_method; + const claims = req.body.claims; + const state = req.body.state; + const authorizationHeader = req.get(Authorization); + const responseType = req.body.response_type; + const issuerState = decodeURIComponent(req.body.issuer_state); // This can be associated with the ITB session + let authorizationDetails = ""; + + let requestURI = "urn:aegean.gr:" + uuidv4(); + let parRequests = getPushedAuthorizationRequests(); + parRequests.set(requestURI, { + client_id: client_id, + scope: scope, + response_type: response_type, + redirect_uri: redirect_uri, + code_challenge: code_challenge, + code_challenge_method: code_challenge_method, + claims: claims, + state: state, + authorizationHeader: authorizationHeader, + responseType: responseType, + issuerState: issuerState, + authorizationDetails: authorizationDetails, + }); + + res.json({ + request_uri: requestURI, + expires_in: 90, + }); +}); + +/********************************************************************************* + * Authorisation request + * + * Two ways to request authorization + * One way is to use the authorization_details request parameter with one or more authorization details objects of type openid_credential + * Second way is through the use of scope + ****************************************************************/ +codeFlowRouterSDJWT.get("/authorize", async (req, res) => { + let response_type = req.query.response_type; + let scope = req.query.scope; + let issuerState = decodeURIComponent(req.query.issuer_state); // This can be associated with the ITB session + let state = req.query.state; + let client_id = decodeURIComponent(req.query.client_id); //DID of the holder requesting the credential + let authorizationDetails = req.query.authorization_details; + let redirect_uri = req.query.redirect_uri; + let code_challenge = req.query.code_challenge; + let code_challenge_method = req.query.code_challenge_method; + let authorizationHeader = req.headers['authorization']; // Fetch the 'Authorization' header + + //validations + let errors = []; + + //check for par + const request_uri = req.query.request_uri; + if (request_uri) { + const parRequest = getPushedAuthorizationRequests() + ? getPushedAuthorizationRequests().get(request_uri) + : null; + if (parRequest) { + client_id = parRequest.client_id; + scope = parRequest.scope; + response_type = parRequest.response_type; + redirect_uri = parRequest.redirect_uri; + code_challenge = parRequest.code_challenge; + code_challenge_method = parRequest.code_challenge_method; + claims = parRequest.claims; + state = parRequest.state; + authorizationHeader = parRequest.authorizationHeader; + issuerState = parRequest.issuerState; + authorizationDetails = parRequest = authorizationDetails; + response_type = parRequest.response_type; + } else { + console.log( + "ERROR: request_uri present in authorization endpoint, but no par request cached for request_uri" + + request_uri + ); + } + } + + const redirectUri = req.query.redirect_uri + ? decodeURIComponent(req.query.redirect_uri) + : "localhost:8080"; + const nonce = req.query.nonce; + const codeChallenge = decodeURIComponent(req.query.code_challenge); + const codeChallengeMethod = req.query.code_challenge_method; //this should equal to S256 + try { + const clientMetadata = JSON.parse( + decodeURIComponent(req.query.client_metadata) + ); + } catch (error) { + console.log("client_metadata was missing"); + console.log(error); + } + + // ************CASE 1: With authorizationDetails + /* required parameters: response_type, client_id, code_challenge, + optional: code_challenge_method, authorization_details, redirect_uri, issuer_state + */ + if (authorizationDetails) { + + + + if (authorization_details && !response_type) + errors.push("authorizationDetails missing response_type"); + if (authorizationDetails && !client_id) + errors.push("authorizationDetails missing client_id"); + if (authorizationDetails && !code_challenge) + errors.push("authorizationDetails missing code_challenge"); + + try { + authorizationDetails = decodeURIComponent(authorizationDetails); + if (authorizationDetails.length > 0) { + authorizationDetails.forEach((item) => { + let cred = fetchVCTorCredentialConfigId(item); + // cache authorizationDetails for the direct_post endpoint (it is needed to assosiate it with the auth. code generated there) + getSessionsAuthorizationDetail().set(issuerState,decodeURIComponent(authorizationDetails)) + console.log("requested credentials: " + cred); + }); + } + } catch (error) { + console.log("error parsing authorization details" + authorizationDetails); + errors.push("error parsing authorization details"); + } + } + + // ************CASE 2: With Scope instead of authorization_details + + /* + [{"format":"jwt_vc", + "locations":["https://issuer.example.com"], + "type":"openid_credential", + "types":["VerifiableCredential","VerifiableAttestation","VerifiablePortableDocumentA1"]}] + */ + if (!authorizationDetails) { + console.log("authorization_details not found trying scope"); + if (scope) { + console.log("requested credentials: " + scope); + } else { + errors.push("no credentials requested"); + console.log(`no credentials requested`); + } + } + + if (response_type !== "code") { + errors.push("Invalid response_type"); + } + if (!scope.includes("openid")) { + errors.push("Invalid scope"); + } + + const authorizationCode = null; //"SplxlOBeZQQYbYS6WxSbIA"; + const codeSessions = getAuthCodeSessions(); + if (codeSessions.sessions.indexOf(issuerState) >= 0) { + codeSessions.requests.push({ + challenge: codeChallenge, + method: codeChallengeMethod, + sessionId: authorizationCode, + issuerState: issuerState, + state: state, + }); + codeSessions.results.push({ + sessionId: authorizationCode, + issuerState: issuerState, + state: state, + status: "pending", + }); + codeSessions.walletSessions.push(state); // push state as send by wallet + } else { + console.log("ITB session not found"); + } + + + /* + Authorizatiton Endpoint Response: + */ + + //The holder wallet then responds with an id_token signed by the DID to the direct post endpoint. + + if (errors.length > 0) { + console.error("Validation errors:", errors); + let error_description = ""; + errors.forEach((element) => { + error_description += element + " "; + }); + const encodedErrorDescription = encodeURIComponent( + error_description.trim() + ); + const errorRedirectUrl = `${redirectUri}?error=invalid_request&error_description=${encodedErrorDescription}`; + //TODO mark the codeFlowSession as failed + return res.redirect(302, errorRedirectUrl); + } else { + /* + //5.1.5. Dynamic Credential Request https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-12.html#name-successful-authorization-requesttt + The credential issuer can optionally request additional details to authenticate the client e.g. DID authentication. In this case, the authorisation response will contain a response_mode parameter with the value direct_post. A sample response is as given: + + client_id, Decentralised identifier + redirect_uri, For redirection of the response + response_type ( if the issuer requests DID authentication.), + response_mode (The value must be direct_post) + scope, The value must be openid + nonce, + request_uri: The authorisation server’s private key signed the request. + */ + const redirectUrl = `http://localhost:8080? + state=${state} + &client_id=${client_id} + &redirect_uri=${serverURL}/direct_post_vci + &response_type=id_token + &response_mode=direct_post + &scope=openid + &nonce=${nonce} + &request_uri=${serverURL}/request_uri_dynamic`; + return res.redirect(302, redirectUrl); + } +}); + +codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { + const vpRequestJWT = buildVpRequestJSON( + client_id, + "response_uri", + null, + privateKey + ); + + res.send(vpRequestJWT); +}); + +/* + presentation by the wallet during an Issuance part of the Dynamic Credential Request +*/ +codeFlowRouterSDJWT.post("/direct_post_vci", async (req, res) => { + console.log("direct_post VP for VCI is below!"); + let state = req.body["state"]; + let jwt = req.body["id_token"]; + console.log("direct_post_vci received jwt is::"); + consnole.log(jwt); + + +// + const authorizatiton_details = getSessionsAuthorizationDetail().get(state) + + if (jwt) { + const codeSessions = getAuthCodeSessions(); + const authorizationCode = generateNonce(16); + //cache authorizatiton_detatils witth the generated code. this is needed for the token_endpoint + getAuthCodeAuthorizationDetail().set(authorizationCode,authorizatiton_details) + + + updateIssuerStateWithAuthCode( + authorizationCode, + state, + codeSessions.walletSessions, + codeSessions.results, + codeSessions.requests + ); + const redirectUrl = `http://localhost:8080?code=${authorizationCode}&state=${state}`; + return res.redirect(302, redirectUrl); + } else { + return res.sendStatus(500); + } +}); + +// Function to fetch either vct or credential_configuration_id +function fetchVCTorCredentialConfigId(data) { + // Check for vct first, fallback to credential_configuration_id if not found + if (firstItem.vct) { + return firstItem.vct; + } else if (firstItem.credential_configuration_id) { + return firstItem.credential_configuration_id; + } else { + return null; // Return null if neither is found + } +} + export default codeFlowRouterSDJWT; diff --git a/routes/jwtVcRoutes.js b/routes/jwtVcRoutes.js new file mode 100644 index 0000000..c6d65aa --- /dev/null +++ b/routes/jwtVcRoutes.js @@ -0,0 +1,90 @@ +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"; +import { request } from "http"; + +const router = 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"); + + + + + +// *************** +///pre-auth flow jwt_vc_json +router.get(["/pre-offer-jwt"], 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/${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, + }); +}); + + + +function getPersonaPart(inputString) { + const personaKey = "persona="; + const personaIndex = inputString.indexOf(personaKey); + + if (personaIndex === -1) { + return null; // "persona=" not found in the string + } + + // Split the string based on "persona=" + const parts = inputString.split(personaKey); + + // Return the part after "persona=" + return parts[1] || null; +} + + + + + +export default router; diff --git a/routes/metadataroutes.js b/routes/metadataroutes.js index 1070f47..c3d3624 100644 --- a/routes/metadataroutes.js +++ b/routes/metadataroutes.js @@ -17,6 +17,11 @@ const oauthConfig = JSON.parse( const jwks = pemToJWK(publicKeyPem, "public"); + +/** + * Credential Issuer metadata + */ + metadataRouter.get( "/.well-known/openid-credential-issuer", async (req, res) => { @@ -30,6 +35,9 @@ metadataRouter.get( } ); +/** + * Authorization Server Metadata + */ metadataRouter.get( [ "/.well-known/oauth-authorization-server", @@ -39,12 +47,16 @@ metadataRouter.get( async (req, res) => { oauthConfig.issuer = serverURL; oauthConfig.authorization_endpoint = serverURL + "/authorize"; + oauthConfig.pushed_authorization_request_endpoint = serverURL + "/par"; oauthConfig.token_endpoint = serverURL + "/token_endpoint"; oauthConfig.jwks_uri = serverURL + "/jwks"; res.type("application/json").send(oauthConfig); } ); + + + metadataRouter.get(["/", "/jwks"], (req, res) => { res.json({ keys: [ diff --git a/routes/routes.js b/routes/preAuthSDjwRoutes.js similarity index 85% rename from routes/routes.js rename to routes/preAuthSDjwRoutes.js index 21febf6..9f7dfcd 100644 --- a/routes/routes.js +++ b/routes/preAuthSDjwRoutes.js @@ -12,9 +12,13 @@ import { buildIdToken, } from "../utils/tokenUtils.js"; +import {getCredentialSubjectForPersona, getPersonaFromAccessToken} from "../utils/personasUtils.js" + + import { getAuthCodeSessions, getPreCodeSessions, + getAuthCodeAuthorizationDetail, } from "../services/cacheService.js"; import { SDJwtVcInstance } from "@sd-jwt/sd-jwt-vc"; @@ -40,19 +44,22 @@ const publicKeyPem = fs.readFileSync("./public-key.pem", "utf-8"); console.log("privateKey"); console.log(privateKey); +// ****************************************************************** +// ************* CREDENTIAL OFFER ENDPOINTS ************************* +// ****************************************************************** + ///pre-auth flow sd-jwt -router.get(["/offer"], async (req, res) => { +/* + Generates a VCI request with pre-authorised flow with a transaction code +*/ +router.get(["/offer-tx-code"], 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" }); } - - // console.log("active sessions"); - // console.log(issuanceResults); - let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer/${uuid}`; //OfferUUID - + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-tx-code/${uuid}`; //OfferUUID let code = qr.image(credentialOffer, { type: "png", ec_level: "H", @@ -68,30 +75,38 @@ router.get(["/offer"], async (req, res) => { }); }); -//pre-auth flow request sd-jwt -router.get(["/credential-offer/:id"], (req, res) => { +/** + * pre-authorised flow with a transaction code, credential offer + */ +router.get(["/credential-offer-tx-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credentials: ["VerifiablePortableDocumentA1"], + credential_configuration_ids: ["VerifiablePortableDocumentA1"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, - user_pin_required: true, + tx_code: { + length: 4, + input_mode: "numeric", + description: + "Please provide the one-time code that was sent via e-mail or offline", + }, }, }, }); }); -// *************** -///pre-auth flow jwt_vc_json -router.get(["/pre-offer-jwt"], async (req, res) => { +/** + * pre-authorised flow without a transaction code request + */ +router.get(["/offer-no-code"], 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/${uuid}`; //OfferUUID + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-no-code/${uuid}`; //OfferUUID let code = qr.image(credentialOffer, { type: "png", ec_level: "H", @@ -107,53 +122,48 @@ router.get(["/pre-offer-jwt"], async (req, res) => { }); }); -//pre-auth flow request sd-jwt -router.get(["/credential-offer-pre-jwt/:id"], (req, res) => { +/** + * pre-authorised flow no transaction code request endpoint + */ +router.get(["/ccredential-offer-no-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credentials: ["VerifiablePortableDocumentA2"], + credential_configuration_ids: ["VerifiablePortableDocumentA1"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, - user_pin_required: true, }, }, }); }); -function getPersonaPart(inputString) { - const personaKey = "persona="; - const personaIndex = inputString.indexOf(personaKey); +// ********************************************************************* - if (personaIndex === -1) { - return null; // "persona=" not found in the string - } - - // Split the string based on "persona=" - const parts = inputString.split(personaKey); - - // Return the part after "persona=" - return parts[1] || null; -} +// ***************************************************************** +// ************* TOKEN ENDPOINTS *********************************** +// ***************************************************************** router.post("/token_endpoint", async (req, res) => { + // Fetch the Authorization header + const authorizationHeader = req.headers["authorization"]; // Fetch the 'Authorization' header + console.log("token_endpoint authorizatiotn header-" + authorizationHeader); + //pre-auth code flow - const grantType = req.body.grant_type; const preAuthorizedCode = req.body["pre-authorized_code"]; // req.body["pre-authorized_code"] - const userPin = req.body["user_pin"]; + const tx_code = req.body["tx_code"]; + //code flow + const grantType = req.body.grant_type; + const client_id = req.body.client_id; const code = req.body["code"]; //TODO check the code ... const code_verifier = req.body["code_verifier"]; const redirect_uri = req.body["redirect_uri"]; - // console.log("token_endpoint parameters received"); - // console.log(grantType); - // console.log(preAuthorizedCode); - // console.log(userPin); - // console.log("---------"); - let generatedAccessToken = buildAccessToken(serverURL, privateKey); + //TODO CHECK IF THE AUTHORIZATION REQUEST WAS done via a authorization_details or scope parameter + let authorization_details = getAuthCodeAuthorizationDetail().get(code); + if (grantType == "urn:ietf:params:oauth:grant-type:pre-authorized_code") { console.log("pre-auth code flow"); const preSessions = getPreCodeSessions(); @@ -164,14 +174,12 @@ router.post("/token_endpoint", async (req, res) => { ); preSessions.results[index].status = "success"; preSessions.accessTokens[index] = generatedAccessToken; - let personaId = getPersonaPart(preAuthorizedCode); if (personaId) { preSessions.personas[index] = personaId; } else { preSessions.personas[index] = null; } - // console.log("pre-auth code flow" + preSessions.results[index].status); } } else { @@ -186,17 +194,54 @@ router.post("/token_endpoint", async (req, res) => { } } //TODO return error if code flow validation fails and is not a pre-auth flow - res.json({ - access_token: generatedAccessToken, - refresh_token: generateRefreshToken(), - token_type: "bearer", - expires_in: 86400, - id_token: buildIdToken(serverURL, privateKey), - c_nonce: generateNonce(), - c_nonce_expires_in: 86400, - }); + + if (authorization_details) { + /*{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ", + "token_type": "bearer", + "expires_in": 86400, + "c_nonce": "tZignsnFbp", + "c_nonce_expires_in": 86400, + "authorization_details": [ + { + "type": "openid_credential", + "credential_configuration_id": "VerifiablePortableDocumentA1", + "credential_identifiers": [ "VerifiablePortableDocumentA1-Spain", "VerifiablePortableDocumentA1-Sweden", "VerifiablePortableDocumentA1-Germany" ] + }]}*/ + res.json({ + access_token: generatedAccessToken, + refresh_token: generateRefreshToken(), + token_type: "bearer", + expires_in: 86400, + // id_token: buildIdToken(serverURL, privateKey), + c_nonce: generateNonce(), + c_nonce_expires_in: 86400, + authorization_details: authorizatiton_details, + }); + } else { + res.json({ + /* "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ", + "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI4a5k..zEF", + "token_type": "bearer", + "expires_in": 86400, + "id_token": "eyJodHRwOi8vbWF0dHIvdGVuYW50L..3Mz", + "c_nonce": "PAPPf3h9lexTv3WYHZx8ajTe", + "c_nonce_expires_in": 86400 */ + + access_token: generatedAccessToken, + refresh_token: generateRefreshToken(), + token_type: "bearer", + expires_in: 86400, + id_token: buildIdToken(serverURL, privateKey), + c_nonce: generateNonce(), + c_nonce_expires_in: 86400, + }); + } }); +// ***************************************************************** +// ************* CREDENTIAL ENDPOINTS ****************************** +// ***************************************************************** + router.post("/credential", async (req, res) => { // console.log("7 ROUTE /credential CALLED!!!!!!"); const authHeader = req.headers["authorization"]; @@ -204,7 +249,7 @@ router.post("/credential", async (req, res) => { // Accessing the body data const requestBody = req.body; const format = requestBody.format; - const requestedCredentials = requestBody.credential_definition + let requestedCredentials = requestBody.credential_definition ? requestBody.credential_definition.type : null; //removed requestBody.types to conform to RFC001 //TODO valiate bearer header @@ -229,70 +274,9 @@ router.post("/credential", async (req, res) => { preSessions.accessTokens ); - let 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", - }; - if (persona === "1") { - credentialSubject = { - id: decodedHeaderSubjectDID, - family_name: "Conti", - given_name: "Mario", - birth_date: "1988-11-12", - 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: "IT", - }; - } else if (persona === "2") { - credentialSubject = { - id: decodedHeaderSubjectDID, - family_name: "Matkalainen", - given_name: "Hannah", - birth_date: "2005-02-07", - 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: "FI", - }; - } else if (persona === "3") { - credentialSubject = { - id: decodedHeaderSubjectDID, - family_name: "Fischer", - given_name: "Felix", - birth_date: "1953-01-23", - 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: "FI", - }; - } + // Get the persona-specific credentialSubject + const credentialSubject = getCredentialSubjectForPersona(persona, decodedHeaderSubjectDID); + payload = { iss: serverURL, @@ -761,42 +745,48 @@ router.post("/credential", async (req, res) => { c_nonce_expires_in: 86400, }); } else { - // console.log("Token:", token); - // console.log("Request Body:", requestBody); - const { signer, verifier } = await createSignerVerifier( - pemToJWK(privateKey, "private"), - pemToJWK(publicKeyPem, "public") - ); - const sdjwt = new SDJwtVcInstance({ - signer, - verifier, - signAlg: "ES256", - hasher: digest, - hashAlg: "SHA-256", - saltGenerator: generateSalt, - }); - const claims = { - given_name: "John", - last_name: "Doe", - }; - const disclosureFrame = { - _sd: ["given_name", "last_name"], - }; - const credential = await sdjwt.issue( - { - iss: serverURL, - iat: new Date().getTime(), - vct: "VerifiablePortableDocumentA1", - ...claims, - }, - disclosureFrame - ); - res.json({ - format: "vc+sd-jwt", - credential: credential, - c_nonce: generateNonce(), - c_nonce_expires_in: 86400, - }); + + if (format === "vc+sd-jwt") { + // console.log("Token:", token); + // console.log("Request Body:", requestBody); + const { signer, verifier } = await createSignerVerifier( + pemToJWK(privateKey, "private"), + pemToJWK(publicKeyPem, "public") + ); + const sdjwt = new SDJwtVcInstance({ + signer, + verifier, + signAlg: "ES256", + hasher: digest, + hashAlg: "SHA-256", + saltGenerator: generateSalt, + }); + const claims = { + given_name: "John", + last_name: "Doe", + }; + const disclosureFrame = { + _sd: ["given_name", "last_name"], + }; + const credential = await sdjwt.issue( + { + iss: serverURL, + iat: new Date().getTime(), + vct: "VerifiablePortableDocumentA1", + ...claims, + }, + disclosureFrame + ); + res.json({ + format: "vc+sd-jwt", + credential: credential, + c_nonce: generateNonce(), + c_nonce_expires_in: 86400, + }); + } else { + console.log("UNSUPPORTED FORMAT!"); + console.log(format); + } } }); //issuerConfig.credential_endpoint = serverURL + "/credential"; @@ -879,16 +869,7 @@ function checkIfExistsIssuanceStatus( return null; } -function getPersonaFromAccessToken(accessToken, personas, accessTokens) { - let persona = null; - for (let i = 0; i < accessTokens.length; i++) { - console.log(accessTokens[i]) - if (accessTokens[i] === accessToken) { - persona = personas[i]; - } - } - return persona; -} + async function validatePKCE(sessions, code, code_verifier, issuanceResults) { for (let i = 0; i < sessions.length; i++) { @@ -899,9 +880,31 @@ async function validatePKCE(sessions, code, code_verifier, issuanceResults) { if (tester === challenge) { issuanceResults[i].status = "success"; console.log("code flow status:" + issuanceResults[i].status); + } else { + console.log( + "PCKE ERROR tester and challenge do not match " + + tester + + "-" + + challenge + ); } } } } +function getPersonaPart(inputString) { + const personaKey = "persona="; + const personaIndex = inputString.indexOf(personaKey); + + if (personaIndex === -1) { + return null; // "persona=" not found in the string + } + + // Split the string based on "persona=" + const parts = inputString.split(personaKey); + + // Return the part after "persona=" + return parts[1] || null; +} + export default router; diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index fe0f66e..fae6959 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -5,7 +5,7 @@ import { pemToJWK, generateNonce, decryptJWE, - buildVpRequestJwt, + buildVpRequestJSON, } from "../utils/cryptoUtils.js"; import { decodeSdJwt, getClaims } from "@sd-jwt/decode"; import { digest } from "@sd-jwt/crypto-nodejs"; @@ -119,7 +119,7 @@ verifierRouter.get("/vpRequest/:id", async (req, res) => { claims: null, }); - let jwtToken = buildVpRequestJwt( + let jwtToken = buildVpRequestJSON( stateParam, nonce, clientId, @@ -309,27 +309,29 @@ verifierRouter.get("/vpRequest/:type/:id", async (req, res) => { } else { return res.status(400).type("text/plain").send("Invalid type parameter"); } + /* + TODO the vpRequest/:type/:id endpoint defined in routes/verifierRoutes.js, + that is used for JWT secured Authorization Requests (JAR), + returns a raw JSON object instead of a JWT. + + According to the JAR standard (RFC9101), the endpoint defined in request_uri should present a JWT in the response body, + never raw JSON. + Also, as has been mentioned before, when a client_id_scheme of redirect_uri is used, + the authorization request must not be signed, + so perhaps the conformance backend shouldn't use JAR at all with this scheme. +*/ - // let jwtToken = buildVpRequestJwt( - // stateParam, - // nonce, - // clientId, - // response_uri, - // presentationDefinition, - // jwks, - // serverURL, - // privateKey - // ); let vpRequest = { client_metadata: { client_name: "UAegean EWC Verifier", - logo_uri: "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + logo_uri: + "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", location: "Greece", cover_uri: "string", description: "EWC pilot case verification", }, client_id: clientId, - client_id_scheme: "redirect_uri", + client_id_scheme: "redirect_uri", //TODO change this response_uri: response_uri, response_type: "vp_token", response_mode: "direct_post", diff --git a/server.js b/server.js index c045b68..050e44c 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,6 @@ // main file import express from "express"; -import router from "./routes/routes.js"; +import router from "./routes/preAuthSDjwRoutes.js"; import verifierRouter from "./routes/verifierRoutes.js"; import metadataRouter from "./routes/metadataroutes.js"; import codeFlowRouter from "./routes/codeFlowJwtRoutes.js"; diff --git a/services/cacheService.js b/services/cacheService.js index 7b3a4f3..449445e 100644 --- a/services/cacheService.js +++ b/services/cacheService.js @@ -8,6 +8,10 @@ let issuerCodeSessions = []; let codeFlowRequests = []; let codeFlowRequestsResults = []; +let pushedAuthorizationRequests = new Map(); +let sessionsAuthorizationDetail = new Map(); +let authCodeAuthorizationDetail = new Map(); + export function getPreCodeSessions() { return { sessions: sessions, @@ -25,3 +29,17 @@ export function getAuthCodeSessions() { results: codeFlowRequestsResults, }; } + +export function getPushedAuthorizationRequests() { + return pushedAuthorizationRequests; +} + + +export function getSessionsAuthorizationDetail() { + return sessionsAuthorizationDetail; +} + + +export function getAuthCodeAuthorizationDetail() { + return authCodeAuthorizationDetail; +} \ No newline at end of file diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index b740802..a000967 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -27,7 +27,7 @@ export function generateNonce(length = 12) { return crypto.randomBytes(length).toString("hex"); } -export function buildVpRequestJwt( +export function buildVpRequestJSON( state, nonce, client_id, @@ -72,16 +72,7 @@ export function buildVpRequestJwt( // response_mode: "direct_post", client_metadata : "", - // response_uri: response_uri, //TODO Note: If the Client Identifier scheme redirect_uri is used in conjunction with the Response Mode direct_post, and the response_uri parameter is present, the client_id value MUST be equal to the response_uri value - // iss: serverURL, - // state: state, - // exp: Math.floor(Date.now() / 1000) + 60, - // nonce: nonce, - // iat: Math.floor(Date.now() / 1000), - // nbf: Math.floor(Date.now() / 1000), - // redirect_uri: redirect_uri, - // scope: "openid", - + // response_uri: response_uri, //TODO Note: If the Client Identifier scheme redirect_uri is used in conjunction with the Response Mode direct_post, and the response_uri parameter is present, the client_id value MUST be equal to the response_uri value }; // const header = { @@ -97,6 +88,42 @@ export function buildVpRequestJwt( return jwtPayload; } + + +export function buildVpRequestJWT( + client_id, + response_uri, + presentation_definition, + privateKey +) { + + let jwtPayload = { + response_type: "vp_token", + client_id: client_id, + client_id_scheme: "redirect_uri", + presentation_definition: presentation_definition, + redirect_uri: response_uri, + // response_mode: "direct_post", + client_metadata : "", + // response_uri: response_uri, //TODO Note: If the Client Identifier scheme redirect_uri is used in conjunction with the Response Mode direct_post, and the response_uri parameter is present, the client_id value MUST be equal to the response_uri value + }; + + const header = { + alg: "ES256", + kid: `aegean#authentication-key`, //this kid needs to be resolvable from the did.json endpoint + }; + + const token = jwt.sign(jwtPayload, privateKey, { + algorithm: "ES256", + noTimestamp: true, + header, + }); + return token; +} + + + + export async function decryptJWE(jweToken, privateKeyPEM) { try { const privateKey = crypto.createPrivateKey(privateKeyPEM); diff --git a/utils/personasUtils.js b/utils/personasUtils.js new file mode 100644 index 0000000..c3dba3b --- /dev/null +++ b/utils/personasUtils.js @@ -0,0 +1,65 @@ + +export function getCredentialSubjectForPersona(persona, decodedHeaderSubjectDID) { + const issuanceDate = new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(); + const expiryDate = new Date( + Math.floor(Date.now() + 60 / 1000) * 1000 + ).toISOString(); + + const defaultData = { + id: decodedHeaderSubjectDID, + issuance_date: issuanceDate, + expiry_date: expiryDate, + issuing_authority: "https://aegean.gr", + age_over_18: true, + }; + + switch (persona) { + case "1": + return { + ...defaultData, + family_name: "Conti", + given_name: "Mario", + birth_date: "1988-11-12", + issuing_country: "IT", + }; + case "2": + return { + ...defaultData, + family_name: "Matkalainen", + given_name: "Hannah", + birth_date: "2005-02-07", + issuing_country: "FI", + }; + case "3": + return { + ...defaultData, + family_name: "Fischer", + given_name: "Felix", + birth_date: "1953-01-23", + issuing_country: "FI", + }; + default: + return { + ...defaultData, + family_name: "Doe", + given_name: "John", + birth_date: "1990-01-01", + issuing_country: "GR", + }; + } +} + + + +export function getPersonaFromAccessToken(accessToken, personas, accessTokens) { + let persona = null; + for (let i = 0; i < accessTokens.length; i++) { + console.log(accessTokens[i]); + if (accessTokens[i] === accessToken) { + persona = personas[i]; + } + } + return persona; + } \ No newline at end of file From 881896fe0b5b7a8aed07789903eeae0fdcb20460 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Tue, 24 Sep 2024 14:18:08 +0300 Subject: [PATCH 02/37] working v13 pre-auth flow with and without tx --- data/issuer-config.json | 69 +- openapi.yml | 1894 +++++++++++++++++++++++++++++++++ package-lock.json | 428 ++++++++ package.json | 3 +- routes/codeFlowSdJwtRoutes.js | 15 +- routes/preAuthSDjwRoutes.js | 2 +- server.js | 26 + 7 files changed, 2393 insertions(+), 44 deletions(-) create mode 100644 openapi.yml diff --git a/data/issuer-config.json b/data/issuer-config.json index c36f12d..e34b740 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -5,18 +5,22 @@ "deferred_credential_endpoint": "https://server.example.com/credential_deferred", "display": [ { - "name": "UAegean", - "location": "UAegean", + "name": "Issuer", + "location": "Belgium", "locale": "en-GB", - "description": "University of the Aegean RFC Issuer for EWC" + "description": "For queries about how we manage your data please contact the Data Protection Officer." } ], "credential_configurations_supported": { "VerifiablePortableDocumentA1": { "format": "vc+sd-jwt", "scope": "VerifiablePortableDocumentA1", - "cryptographic_binding_methods_supported": ["jwk"], - "credential_signing_alg_values_supported": ["ES256"], + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "credential_signing_alg_values_supported": [ + "ES256" + ], "display": [ { "name": "Portable Document A1", @@ -25,36 +29,33 @@ "text_color": "#FFFFFF" } ], - "credential_definition": { - "vct": "VerifiablePortableDocumentA1", - "claims": { - "given_name": { - "display": [ - { - "name": "Given Name", - "locale": "en-GB" - }, - { - "name": "ΟΝΟΜΑ", - "locale": "gr-GR" - } - ] - }, - "last_name": { - "display": [ - { - "name": "Surname", - "locale": "en-GB" - }, - { - "name": "ΕΠΩΝΥΜΟ", - "locale": "gr-GR" - } - ] - } + "vct": "VerifiablePortableDocumentA1", + "claims": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-GB" + }, + { + "name": "Vorname", + "locale": "de-DE" + } + ] + }, + "last_name": { + "display": [ + { + "name": "Surname", + "locale": "en-GB" + }, + { + "name": "Nachname", + "locale": "de-DE" + } + ] } } } - } -} +} \ No newline at end of file diff --git a/openapi.yml b/openapi.yml new file mode 100644 index 0000000..ce3e448 --- /dev/null +++ b/openapi.yml @@ -0,0 +1,1894 @@ +components: + schemas: + AuthorisationCodeGrant: + properties: + authorization_code: + description: Authorisation code grant + properties: + authorization_server: + description: '"OPTIONAL. String that the Wallet can use to identify + the Authorization Server to use with this grant type when `authorization_servers` + parameter in the Credential Issuer metadata has multiple entries. It + MUST NOT be used otherwise. The value of this parameter MUST match + with one of the values in the `authorization_servers` array obtained + from the Credential Issuer metadata." + + ' + type: string + issuer_state: + description: '"OPTIONAL. String value created by the Credential Issuer + and opaque to the Wallet that is used to bind the subsequent Authorization + Request with the Credential Issuer to a context set up during previous + steps. If the Wallet decides to use the Authorization Code Flow and + received a value for this parameter, it MUST include it in the subsequent + Authorization Request to the Credential Issuer as the `issuer_state` + parameter value" + + ' + type: string + type: object + required: + - authorization_code + title: Authorisation code grant + type: object + AuthorisationCodeTokenRequest: + $ref: '#/components/schemas/AuthorisationCodeGrant' + AuthorisationDetails: + properties: + credential_definition: + properties: + type: + items: + type: string + type: array + required: + - type + type: object + format: + type: string + locations: + items: + type: string + type: array + type: + type: string + required: + - type + - locations + - format + - credential_definition + title: Authorisation Details + type: object + AuthorisationResponse: + properties: + client_id: + description: Decentralised identifier + type: string + nonce: + description: A value used to associate a client session with an ID token + and to mitigate replay attacks. + type: string + redirect_uri: + description: For redirection of the response + type: string + request_uri: + description: The authorisation server's private key signed the request. + type: string + response_mode: + description: The value must be 'direct_post' + type: string + response_type: + description: The value must be 'id_token' if the issuer requests DID authentication. + type: string + scope: + description: The value must be 'openid' + type: string + state: + description: The client uses an opaque value to maintain the state between + the request and callback. + type: string + title: Authorisation response + type: object + AuthorisationServerMetadata: + description: As described in Chapter 3 of [OpenID Connect Discovery 1.0 specification](https://openid.net/specs/openid-connect-discovery-1_0.html). + properties: + authorization_endpoint: + description: REQUIRED. URL of the OP's OAuth 2.0 Authorization Endpoint + [OpenID.Core]. This URL MUST use the https scheme and MAY contain port, + path, and query parameter components. + type: string + id_token_signing_alg_values_supported: + description: REQUIRED. JSON array containing a list of the JWS signing algorithms + (alg values) supported by the OP for the ID Token to encode the Claims + in a JWT [JWT]. The algorithm RS256 MUST be included. The value none MAY + be supported but MUST NOT be used unless the Response Type used returns + no ID Token from the Authorization Endpoint (such as when using the Authorization + Code Flow). + items: + type: string + type: array + issuer: + description: REQUIRED. URL using the https scheme with no query or fragment + components that the OP asserts as its Issuer Identifier. + type: string + jwks_uri: + description: REQUIRED. URL of the OP's JWK Set [JWK] document, which MUST + use the https scheme. This contains the signing key(s) the RP uses to + validate signatures from the OP. The JWK Set MAY also contain the Server's + encryption key(s), which are used by RPs to encrypt requests to the Server. + When both signing and encryption keys are made available, a use (public + key use) parameter value is REQUIRED for all keys in the referenced JWK + Set to indicate each key's intended usage. Although some algorithms allow + the same key to be used for both signatures and encryption, doing so is + NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used + to provide X.509 representations of keys provided. When used, the bare + key values MUST still be present and MUST match those in the certificate. + The JWK Set MUST NOT contain private or symmetric key values. + type: string + pre-authorized_grant_anonymous_access_supported: + description: OPTIONAL. A boolean indicating whether the Credential Issuer + accepts a Token Request with a Pre-Authorized Code but without a client_id. + The default is false. + type: boolean + pushed_authorization_request_endpoint: + description: The URL of the pushed authorization request endpoint at which + a client can post an authorization request to exchange for a request_uri + value usable at the authorization server. + type: string + require_pushed_authorization_requests: + description: Boolean parameter indicating whether the authorization server + accepts authorization request data only via PAR. If omitted, the default + value is false. + type: boolean + response_types_supported: + description: REQUIRED. JSON array containing a list of the OAuth 2.0 response_type + values that this OP supports. + items: + type: string + type: array + subject_types_supported: + description: REQUIRED. JSON array containing a list of the Subject Identifier + types that this OP supports. Valid types include pairwise and public. + items: + type: string + type: array + token_endpoint: + description: URL of the OP's OAuth 2.0 Token Endpoint [OpenID.Core]. This + URL MUST use the https scheme and MAY contain port, path, and query parameter + components. + type: string + required: + - issuer + - authorization_endpoint + - token_endpoint + - jwks_uri + - response_types_supported + - subject_types_supported + - id_token_signing_alg_values_supported + title: Authorisation Server Metadata + type: object + CredentialIssuerMetadata: + properties: + authorization_servers: + description: 'OPTIONAL. Array of strings, where each string is an identifier + of the OAuth 2.0 Authorization Server (as defined in [RFC8414]) the Credential + Issuer relies on for authorization. If this parameter is omitted, the + entity providing the Credential Issuer is also acting as the Authorization + Server, i.e., the Credential Issuer''s identifier is used to obtain the + Authorization Server metadata. The actual OAuth 2.0 Authorization Server + metadata is obtained from the oauth-authorization-server well-known location + as defined in Section 3 of [RFC8414]. When there are multiple entries + in the array, the Wallet may be able to determine which Authorization + Server to use by querying the metadata; for example, by examining the + grant_types_supported values, the Wallet can filter the server to use + based on the grant type it plans to use. When the Wallet is using authorization_server + parameter in the Credential Offer as a hint to determine which Authorization + Server to use out of multiple, the Wallet MUST NOT proceed with the flow + if the authorization_server Credential Offer parameter value does not + match any of the entries in the authorization_servers array. + + ' + type: string + batch_credential_endpoint: + description: OPTIONAL. URL of the Credential Issuer's Batch Credential Endpoint. + This URL MUST use the https scheme and MAY contain port, path, and query + parameter components. If omitted, the Credential Issuer does not support + the Batch Credential Endpoint. + type: string + credential_configurations_supported: + oneOf: + - $ref: '#/components/schemas/JwtVcJsonCredentialConfiguration' + - $ref: '#/components/schemas/IetfSdJwtVcCredentialConfiguration' + - $ref: '#/components/schemas/MsoMdocCredentialConfiguration' + credential_endpoint: + description: REQUIRED. URL of the Credential Issuer's Credential Endpoint. + This URL MUST use the https scheme and MAY contain port, path, and query + parameter components. + type: string + credential_identifiers_supported: + description: 'OPTIONAL. Boolean value specifying whether the Credential + Issuer supports returning credential_identifiers parameter in the authorization_details + Token Response parameter, with true indicating support. If omitted, the + default value is false. + + ' + type: boolean + credential_issuer: + description: REQUIRED. The Credential Issuer's identifier, as defined in + Section 11.2.1. + type: string + credential_response_encryption: + description: OPTIONAL. Object containing information about whether the Credential + Issuer supports encryption of the Credential and Batch Credential Response + on top of TLS. + properties: + alg_values_supported: + description: 'REQUIRED. Array containing a list of the JWE [RFC7516] + encryption algorithms (alg values) [RFC7518] supported by the Credential + and Batch Credential Endpoint to encode the Credential or Batch Credential + Response in a JWT [RFC7519]. + + ' + items: + type: string + type: array + enc_values_supported: + description: 'REQUIRED. Array containing a list of the JWE [RFC7516] + encryption algorithms (enc values) [RFC7518] supported by the Credential + and Batch Credential Endpoint to encode the Credential or Batch Credential + Response in a JWT [RFC7519]. + + ' + items: + type: string + type: array + encryption_required: + description: 'REQUIRED. Boolean value specifying whether the Credential + Issuer requires the additional encryption on top of TLS for the Credential + Response. If the value is true, the Credential Issuer requires encryption + for every Credential Response and therefore the Wallet MUST provide + encryption keys in the Credential Request. If the value is false, + the Wallet MAY chose whether it provides encryption keys or not. + + ' + type: boolean + type: object + deferred_credential_endpoint: + description: OPTIONAL. URL of the Credential Issuer's Deferred Credential + Endpoint. This URL MUST use the https scheme and MAY contain port, path, + and query parameter components. If omitted, the Credential Issuer does + not support the Deferred Credential Endpoint. + type: string + display: + description: OPTIONAL. Array of objects, where each object contains display + properties of a Credential Issuer for a certain language. + items: + properties: + locale: + description: String value that identifies the language of this object + represented as a language tag taken from values defined in BCP47 + [RFC5646]. There MUST be only one object for each language identifier. + type: string + logo: + description: OPTIONAL. Object with information about the logo of the + Credential Issuer. + properties: + alt_text: + description: 'OPTIONAL. String value of the alternative text for + the logo image. + + ' + type: string + uri: + description: 'REQUIRED. String value that contains a URI where + the Wallet can obtain the logo of the Credential Issuer. The + Wallet needs to determine the scheme, since the URI value could + use the https: scheme, the data: scheme, etc. + + ' + type: string + required: + - uri + - alt_text + type: object + name: + description: OPTIONAL. String value of a display name for the Credential + Issuer. + type: string + type: object + type: array + notification_endpoint: + description: OPTIONAL. URL of the Credential Issuer's Notification Endpoint. + This URL MUST use the https scheme and MAY contain port, path, and query + parameter components. If omitted, the Credential Issuer does not support + the Notification Endpoint. + type: string + signed_metadata: + description: 'OPTIONAL. String that is a signed JWT. This JWT contains + Credential Issuer metadata parameters as claims. The signed metadata + MUST be secured using JSON Web Signature (JWS) [RFC7515] and MUST contain + an iat (Issued At) claim, an iss (Issuer) claim denoting the party attesting + to the claims in the signed metadata, and sub (Subject) claim matching + the Credential Issuer identifier. If the Wallet supports signed metadata, + metadata values conveyed in the signed JWT MUST take precedence over the + corresponding values conveyed using plain JSON elements. If the Credential + Issuer wants to enforce use of signed metadata, it omits the respective + metadata parameters from the unsigned part of the Credential Issuer metadata. A + signed_metadata metadata value MUST NOT appear as a claim in the JWT. The + Wallet MUST establish trust in the signer of the metadata, and obtain + the keys to validate the signature before processing the metadata. The + concrete mechanism how to do that is out of scope of this specification + and MAY be defined in the profiles of this specification. + + ' + type: string + required: + - credential_issuer + - authorization_servers + - credential_endpoint + - credential_configurations_supported + title: Credential Issuer Metadata + type: object + CredentialOffer: + properties: + credential_configuration_ids: + description: '"REQUIRED. Array of unique strings that each identify one + of the keys in the name/value pairs stored in the `credential_configurations_supported` + Credential Issuer metadata. The Wallet uses these string values to obtain + the respective object that contains information about the Credential being + offered. For example, these string values can be used to obtain scope + values to be used in the Authorization Request." + + ' + items: + type: string + type: array + credential_issuer: + description: '"REQUIRED. The URL of the Credential Issuer, from which the + Wallet is requested to obtain one or more Credentials. The Wallet can + also use it to obtain the Credential Issuer''s Metadata." + + ' + type: string + grants: + oneOf: + - $ref: '#/components/schemas/AuthorisationCodeGrant' + - $ref: '#/components/schemas/PreAuthorisedCodeGrant' + required: + - credential_issuer + - credential_configuration_ids + - grants + title: Credential Offer + type: object + CredentialRequest: + properties: + format: + description: 'REQUIRED when the credential_identifiers parameter was not + returned from the Token Response. MUST NOT be used otherwise. + + ' + type: string + proof: + description: 'OPTIONAL. Contains the proof of possession of the cryptographic + key material the issued Credential would be bound to. + + ' + properties: + credential_identifier: + description: 'REQUIRED when credential_identifiers parameter was returned + from the Token Response. MUST NOT be used otherwise. + + ' + type: string + credential_response_encryption: + description: 'OPTIONAL. Contains information for encrypting the Credential + Response. + + ' + properties: + alg: + description: 'REQUIRED. JWE alg algorithm for encrypting Credential + Responses. + + ' + type: string + enc: + description: 'REQUIRED. JWE enc algorithm for encrypting Credential + Responses. + + ' + type: string + jwk: + description: 'REQUIRED. Contains a single public key as a JWK used + for encrypting the Credential Response. + + ' + type: object + type: object + proof_type: + description: 'REQUIRED. Denotes the key proof type. + + ' + type: string + type: object + required: + - proof_type + - jwk + - alg + - enc + type: object + DeferredCredentialErrorResponse: + properties: + error: + description: 'REQUIRED. Error code indicating the type of error that occurred. + + ' + enum: + - issuance_pending + - invalid_transaction_id + type: string + error_description: + description: 'OPTIONAL. Human-readable ASCII text providing additional information about + the error. + + ' + type: string + invalid_transaction_id: + description: 'Error response when the Deferred Credential Request contains + an invalid transaction_id. + + ' + type: string + issuance_pending: + description: 'Error response when the Credential issuance is still pending. + + ' + properties: + interval: + description: 'OPTIONAL. The minimum amount of time in seconds that the + Wallet needs to wait before sending a new request to the Deferred + Credential Endpoint. Default value is 5 if not provided. + + ' + type: integer + type: object + required: + - error + title: Deferred Credential Error Response + type: object + DeferredCredentialRequest: + properties: + transaction_id: + description: 'REQUIRED. String identifying a Deferred Issuance transaction. The + Credential Issuer MUST invalidate the transaction_id after the Credential + for which it was meant has been obtained by the Wallet. + + ' + type: string + required: + - transaction_id + title: Deferred Credential Request + type: object + DeferredCredentialResponse: + properties: + c_nonce: + description: 'OPTIONAL. String containing a nonce to be used to create a + proof of possession of key material when requesting a Credential. The + Wallet MUST use this nonce value for its subsequent Credential Requests until + the Credential Issuer provides a fresh nonce. + + ' + type: string + c_nonce_expires_in: + description: 'OPTIONAL. Number denoting the lifetime in seconds of the c_nonce. + + ' + type: number + transaction_id: + description: 'OPTIONAL. String identifying a Deferred Issuance transaction. + This claim is contained in the response if the Credential Issuer was + unable to immediately issue the Credential. It MUST be present when the + credential parameter is not returned. It MUST be invalidated after the + Credential for which it was meant has been obtained by the Wallet. + + ' + type: string + required: + - transaction_id + title: Deferred Credential Response + type: object + IetfSdJwtVcCredentialConfiguration: + properties: + claims: + additionalProperties: true + description: 'OPTIONAL. Object containing a list of name/value pairs, where + each name identifies a claim about the subject offered in the Credential. The + value can be another such object (nested data structures), or an array + of such objects. + + ' + type: object + credential_signing_alg_values_supported: + description: 'OPTIONAL. Array of case sensitive strings that identify the + algorithms that the Issuer uses to sign the issued Credential. + + ' + items: + type: string + type: array + cryptographic_binding_methods_supported: + description: 'OPTIONAL. Array of case sensitive strings that identify the + representation of the cryptographic key material that the issued Credential + is bound to. Support for keys in JWK format [RFC7517] is indicated by + the value jwk. Support for keys expressed as a COSE Key object [RFC8152] + (for example, used in [ISO.18013-5]) is indicated by the value cose_key. + When the Cryptographic Binding Method is a DID, valid values are a did: + prefix followed by a method-name using a syntax as defined in Section + 3.1 of [DID-Core], but without a :and method-specific-id. For example, + support for the DID method with a method-name "example" would be represented + by did:example. + + ' + items: + type: string + type: array + display: + description: Array of objects, where each object contains the display properties + of the supported Credential for a certain language. + properties: + background_color: + description: 'OPTIONAL. String value of a background color of the Credential + represented as numerical color values defined in CSS Color Module + Level 37 [CSS-Color]. + + ' + type: string + background_image: + description: 'OPTIONAL. Object with information about the background + image of the Credential. + + ' + properties: + uri: + description: 'REQUIRED. String value that contains a URI where the + Wallet can obtain the background image of the Credential from + the Credential Issuer. The Wallet needs to determine the scheme, + since the URI value could use the https: scheme, the data: scheme, + etc. + + ' + type: string + type: object + description: + description: 'OPTIONAL. String value of a description of the Credential. + + ' + type: string + locale: + description: 'OPTIONAL. String value that identifies the language of + this object represented as a language tag taken from values defined + in BCP47 [RFC5646]. Multiple display objects MAY be included for separate + languages. There MUST be only one object for each language identifier. + + ' + type: string + logo: + description: 'OPTIONAL. Object with information about the logo of the + Credential. + + ' + properties: + alt_text: + description: 'OPTIONAL. String value of the alternative text for + the logo image. + + ' + type: string + uri: + description: 'REQUIRED. String value that contains a URI where the + Wallet can obtain the logo of the Credential from the Credential + Issuer. The Wallet needs to determine the scheme, since the URI + value could use the https: scheme, the data: scheme, etc + + ' + type: string + type: object + name: + description: REQUIRED. String value of a display name for the Credential. + type: string + text_color: + description: 'OPTIONAL. String value of a text color of the Credential + represented as numerical color values defined in CSS Color Module + Level 37 [CSS-Color]. + + ' + type: string + type: object + format: + description: 'REQUIRED. A JSON string identifying the format of this Credential, + i.e., `jwt_vc_json` or `mso_mdoc` or `vc+sd-jwt`. Depending on the format + value, the object contains further elements defining the type and (optionally) + particular claims the Credential MAY contain and information about how + to display the Credential. + + ' + type: string + order: + description: OPTIONAL. An array of the claim name values that lists them + in the order they should be displayed by the Wallet. + items: + type: string + type: array + proof_types_supported: + description: 'Object that describes specifics of the key proof(s) that the + Credential Issuer supports. This object contains a list of name/value + pairs, where each name is a unique identifier of the supported proof type(s). This + identifier is also used by the Wallet in the Credential Request. + + ' + properties: + proof_signing_alg_values_supported: + description: 'REQUIRED. Array of case sensitive strings that identify + the algorithms that the Issuer supports for this proof type. The + Wallet uses one of them to sign the proof. + + ' + items: + type: string + type: array + type: object + scope: + description: 'OPTIONAL. A JSON string identifying the scope value that this + Credential Issuer supports for this particular Credential. The value + can be the same across multiple credential_configurations_supported objects. The + Authorization Server MUST be able to uniquely identify the Credential + Issuer based on the scope value. The Wallet can use this value in the + Authorization Request. Scope values in this Credential Issuer metadata + MAY duplicate those in the `scopes_supported parameter` of the Authorization + Server. + + ' + type: string + vct: + description: REQUIRED. String designating the type of a Credential, as defined + in [I-D.ietf-oauth-sd-jwt-vc]. + type: string + required: + - vct + - format + title: IETF SD-JWT VC + type: object + InTimeCredentialErrorResponse: + properties: + error: + description: 'REQUIRED. The error parameter SHOULD be a single ASCII error + code from the following: + + ' + enum: + - invalid_credential_request + - unsupported_credential_type + - unsupported_credential_format + - invalid_proof + - invalid_encryption_parameters + type: string + error_description: + description: 'OPTIONAL. The error_description parameter MUST be a human-readable + ASCII text, providing any additional information used to assist the Client + implementers in understanding the occurred error. The values for the + error_description parameter MUST NOT include characters outside the set + %x20-21 / %x23-5B / %x5D-7E. + + ' + type: string + title: InTime Credential Error Response + type: object + InTimeCredentialResponse: + properties: + c_nonce: + description: 'OPTIONAL. String containing a nonce to be used to create a + proof of possession of key material when requesting a Credential. The + Wallet MUST use this nonce value for its subsequent Credential Requests until + the Credential Issuer provides a fresh nonce. + + ' + type: string + c_nonce_expires_in: + description: 'OPTIONAL. Number denoting the lifetime in seconds of the c_nonce. + + ' + type: number + credential: + description: OPTIONAL. Contains issued Credential. It MUST be present when + transaction_id is not returned. It MAY be a string or an object, depending + on the Credential format. + oneOf: + - type: string + - type: object + required: + - credential + title: InTime Credential Response + type: object + JwtVcJsonCredentialConfiguration: + properties: + credential_definition: + description: REQUIRED. Object containing the detailed description of the + Credential type. + properties: + credentialSubject: + additionalProperties: true + description: 'OPTIONAL. Object containing a list of name/value pairs, + where each name identifies a claim offered in the Credential. The + value can be another such object (nested data structures), or an array + of such objects. + + ' + type: object + type: + description: REQUIRED. Array designating the types a certain Credential + type supports, according to [VC_DATA], Section 4.3. + items: + type: string + type: array + type: object + credential_signing_alg_values_supported: + description: 'OPTIONAL. Array of case sensitive strings that identify the + algorithms that the Issuer uses to sign the issued Credential. + + ' + items: + type: string + type: array + cryptographic_binding_methods_supported: + description: 'OPTIONAL. Array of case sensitive strings that identify the + representation of the cryptographic key material that the issued Credential + is bound to. Support for keys in JWK format [RFC7517] is indicated by + the value jwk. Support for keys expressed as a COSE Key object [RFC8152] + (for example, used in [ISO.18013-5]) is indicated by the value cose_key. + When the Cryptographic Binding Method is a DID, valid values are a did: + prefix followed by a method-name using a syntax as defined in Section + 3.1 of [DID-Core], but without a :and method-specific-id. For example, + support for the DID method with a method-name "example" would be represented + by did:example. + + ' + items: + type: string + type: array + display: + description: Array of objects, where each object contains the display properties + of the supported Credential for a certain language. + properties: + background_color: + description: 'OPTIONAL. String value of a background color of the Credential + represented as numerical color values defined in CSS Color Module + Level 37 [CSS-Color]. + + ' + type: string + background_image: + description: 'OPTIONAL. Object with information about the background + image of the Credential. + + ' + properties: + uri: + description: 'REQUIRED. String value that contains a URI where the + Wallet can obtain the background image of the Credential from + the Credential Issuer. The Wallet needs to determine the scheme, + since the URI value could use the https: scheme, the data: scheme, + etc. + + ' + type: string + type: object + description: + description: 'OPTIONAL. String value of a description of the Credential. + + ' + type: string + locale: + description: 'OPTIONAL. String value that identifies the language of + this object represented as a language tag taken from values defined + in BCP47 [RFC5646]. Multiple display objects MAY be included for separate + languages. There MUST be only one object for each language identifier. + + ' + type: string + logo: + description: 'OPTIONAL. Object with information about the logo of the + Credential. + + ' + properties: + alt_text: + description: 'OPTIONAL. String value of the alternative text for + the logo image. + + ' + type: string + uri: + description: 'REQUIRED. String value that contains a URI where the + Wallet can obtain the logo of the Credential from the Credential + Issuer. The Wallet needs to determine the scheme, since the URI + value could use the https: scheme, the data: scheme, etc + + ' + type: string + type: object + name: + description: REQUIRED. String value of a display name for the Credential. + type: string + text_color: + description: 'OPTIONAL. String value of a text color of the Credential + represented as numerical color values defined in CSS Color Module + Level 37 [CSS-Color]. + + ' + type: string + type: object + format: + description: 'REQUIRED. A JSON string identifying the format of this Credential, + i.e., `jwt_vc_json` or `mso_mdoc` or `vc+sd-jwt`. Depending on the format + value, the object contains further elements defining the type and (optionally) + particular claims the Credential MAY contain and information about how + to display the Credential. + + ' + type: string + order: + description: OPTIONAL. Array of the claim name values that lists them in + the order they should be displayed by the Wallet. + items: + type: string + type: array + proof_types_supported: + description: 'Object that describes specifics of the key proof(s) that the + Credential Issuer supports. This object contains a list of name/value + pairs, where each name is a unique identifier of the supported proof type(s). This + identifier is also used by the Wallet in the Credential Request. + + ' + properties: + proof_signing_alg_values_supported: + description: 'REQUIRED. Array of case sensitive strings that identify + the algorithms that the Issuer supports for this proof type. The + Wallet uses one of them to sign the proof. + + ' + items: + type: string + type: array + type: object + scope: + description: 'OPTIONAL. A JSON string identifying the scope value that this + Credential Issuer supports for this particular Credential. The value + can be the same across multiple credential_configurations_supported objects. The + Authorization Server MUST be able to uniquely identify the Credential + Issuer based on the scope value. The Wallet can use this value in the + Authorization Request. Scope values in this Credential Issuer metadata + MAY duplicate those in the `scopes_supported parameter` of the Authorization + Server. + + ' + type: string + required: + - credential_definition + - format + title: VC Signed as a JWT + type: object + MsoMdocCredentialConfiguration: + properties: + claims: + additionalProperties: true + description: 'OPTIONAL. Object containing a list of name/value pairs, where + the name is a certain namespace as defined in [ISO.18013-5] (or any profile + of it), and the value is an object. This object also contains a list + of name/value pairs, where the name is a claim name value that is defined + in the respective namespace and is offered in the Credential. + + ' + type: object + credential_signing_alg_values_supported: + description: 'OPTIONAL. Array of case sensitive strings that identify the + algorithms that the Issuer uses to sign the issued Credential. + + ' + items: + type: string + type: array + cryptographic_binding_methods_supported: + description: 'OPTIONAL. Array of case sensitive strings that identify the + representation of the cryptographic key material that the issued Credential + is bound to. Support for keys in JWK format [RFC7517] is indicated by + the value jwk. Support for keys expressed as a COSE Key object [RFC8152] + (for example, used in [ISO.18013-5]) is indicated by the value cose_key. + When the Cryptographic Binding Method is a DID, valid values are a did: + prefix followed by a method-name using a syntax as defined in Section + 3.1 of [DID-Core], but without a :and method-specific-id. For example, + support for the DID method with a method-name "example" would be represented + by did:example. + + ' + items: + type: string + type: array + display: + description: Array of objects, where each object contains the display properties + of the supported Credential for a certain language. + properties: + background_color: + description: 'OPTIONAL. String value of a background color of the Credential + represented as numerical color values defined in CSS Color Module + Level 37 [CSS-Color]. + + ' + type: string + background_image: + description: 'OPTIONAL. Object with information about the background + image of the Credential. + + ' + properties: + uri: + description: 'REQUIRED. String value that contains a URI where the + Wallet can obtain the background image of the Credential from + the Credential Issuer. The Wallet needs to determine the scheme, + since the URI value could use the https: scheme, the data: scheme, + etc. + + ' + type: string + type: object + description: + description: 'OPTIONAL. String value of a description of the Credential. + + ' + type: string + locale: + description: 'OPTIONAL. String value that identifies the language of + this object represented as a language tag taken from values defined + in BCP47 [RFC5646]. Multiple display objects MAY be included for separate + languages. There MUST be only one object for each language identifier. + + ' + type: string + logo: + description: 'OPTIONAL. Object with information about the logo of the + Credential. + + ' + properties: + alt_text: + description: 'OPTIONAL. String value of the alternative text for + the logo image. + + ' + type: string + uri: + description: 'REQUIRED. String value that contains a URI where the + Wallet can obtain the logo of the Credential from the Credential + Issuer. The Wallet needs to determine the scheme, since the URI + value could use the https: scheme, the data: scheme, etc + + ' + type: string + type: object + name: + description: REQUIRED. String value of a display name for the Credential. + type: string + text_color: + description: 'OPTIONAL. String value of a text color of the Credential + represented as numerical color values defined in CSS Color Module + Level 37 [CSS-Color]. + + ' + type: string + type: object + doctype: + description: REQUIRED. String identifying the Credential type, as defined + in [ISO.18013-5]. + type: string + format: + description: 'REQUIRED. A JSON string identifying the format of this Credential, + i.e., `jwt_vc_json` or `mso_mdoc` or `vc+sd-jwt`. Depending on the format + value, the object contains further elements defining the type and (optionally) + particular claims the Credential MAY contain and information about how + to display the Credential. + + ' + type: string + order: + description: OPTIONAL. Array of namespaced claim name values that lists + them in the order they should be displayed by the Wallet. The values MUST + be two strings separated by a tilde ('~') character, where the first string + is a namespace value and a second is a claim name value. For example, + `org.iso.18013.5.1~given_name". + items: + type: string + type: array + proof_types_supported: + description: 'Object that describes specifics of the key proof(s) that the + Credential Issuer supports. This object contains a list of name/value + pairs, where each name is a unique identifier of the supported proof type(s). This + identifier is also used by the Wallet in the Credential Request. + + ' + properties: + proof_signing_alg_values_supported: + description: 'REQUIRED. Array of case sensitive strings that identify + the algorithms that the Issuer supports for this proof type. The + Wallet uses one of them to sign the proof. + + ' + items: + type: string + type: array + type: object + scope: + description: 'OPTIONAL. A JSON string identifying the scope value that this + Credential Issuer supports for this particular Credential. The value + can be the same across multiple credential_configurations_supported objects. The + Authorization Server MUST be able to uniquely identify the Credential + Issuer based on the scope value. The Wallet can use this value in the + Authorization Request. Scope values in this Credential Issuer metadata + MAY duplicate those in the `scopes_supported parameter` of the Authorization + Server. + + ' + type: string + required: + - doctype + - format + title: ISO mDL + type: object + PreAuthorisedCodeGrant: + properties: + urn:ietf:params:oauth:grant-type:pre-authorized_code: + description: Pre-authorised code grant + properties: + pre-authorized_code: + description: '"REQUIRED. The code representing the Credential Issuer''s + authorization for the Wallet to obtain Credentials of a certain type. This + code MUST be short lived and single use. If the Wallet decides to + use the Pre-Authorized Code Flow, this parameter value MUST be included + in the subsequent Token Request with the Pre-Authorized Code Flow." + + ' + type: string + tx_code: + description: '"OPTIONAL. Object specifying whether the Authorization + Server expects presentation of a Transaction Code by the End-User + along with the Token Request in a Pre-Authorized Code Flow. If the + Authorization Server does not expect a Transaction Code, this object + is absent; this is the default. The Transaction Code is intended + to bind the Pre-Authorized Code to a certain transaction to prevent + replay of this code by an attacker that, for example, scanned the + QR code while standing behind the legitimate End-User. It is RECOMMENDED + to send the Transaction Code via a separate channel. If the Wallet + decides to use the Pre-Authorized Code Flow, the Transaction Code + value MUST be sent in the tx_code parameter with the respective Token + Request. If no length or description is given, this object may be + empty, indicating that a Transaction Code is required." + + ' + properties: + description: + description: '"OPTIONAL. String containing guidance for the Holder + of the Wallet on how to obtain the Transaction Code, e.g., describing + over which communication channel it is delivered. The Wallet + is RECOMMENDED to display this description next to the Transaction + Code input screen to improve the user experience. The length + of the string MUST NOT exceed 300 characters. The description + does not support internationalization, however the Issuer MAY + detect the Holder''s language by previous communication or an + HTTP Accept-Language header within an HTTP GET request for a Credential + Offer URI." + + ' + type: string + input_mode: + description: '"OPTIONAL. String specifying the input character set. Possible + values are numeric (only digits) and text (any characters). The + default is numeric." + + ' + type: string + length: + description: '"OPTIONAL. Integer specifying the length of the Transaction + Code. This helps the Wallet to render the input screen and improve + the user experience." + + ' + type: integer + type: object + type: object + required: + - urn:ietf:params:oauth:grant-type:pre-authorized_code + title: Pre-authorised code grant + type: object + PreAuthorisedCodeTokenRequest: + properties: + grant_type: + description: 'The type of grant being used. For pre-authorized code flow, + this must be set to ''urn:ietf:params:oauth:grant-type:pre-authorized_code''. + + ' + example: urn:ietf:params:oauth:grant-type:pre-authorized_code + type: string + pre-authorized_code: + description: 'The pre-authorized code issued by the authorization server. + This code is used to request an access token. + + ' + type: string + tx_code: + description: 'A transaction code that helps identify and correlate the request + with the original authorization transaction. + + ' + type: string + required: + - grant_type + - pre-authorized_code + - tx_code + title: Token Request (for pre-authorised code grant) + type: object + TokenErrorResponse: + properties: + authorization_pending: + description: 'OPTIONAL. Detailed description for authorization_pending errors. + + ' + properties: + interval: + description: 'OPTIONAL. The minimum amount of time in seconds that the + Wallet needs to wait before sending a new request. Default value + is 5 if not provided. + + ' + type: integer + type: object + error: + description: 'REQUIRED. Error code indicating the type of error that occurred. + + ' + enum: + - invalid_request + - invalid_grant + - invalid_client + - authorization_pending + - slow_down + type: string + error_description: + description: 'OPTIONAL. Human-readable ASCII text providing additional information about + the error. + + ' + type: string + slow_down: + description: 'OPTIONAL. Detailed description for slow_down errors. + + ' + properties: + interval_increase: + description: 'OPTIONAL. The amount of time in seconds by which to increase + the polling interval. + + ' + type: integer + type: object + required: + - error + title: Token Error Response + type: object + TokenResponse: + properties: + access_token: + description: 'The access token issued by the authorization server. This + token is used to access protected resources. + + ' + example: eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..sHQ + type: string + authorization_details: + items: + properties: + credential_configuration_id: + description: 'REQUIRED when `authorization_details` is used. The identifier + for the Credential configuration. + + ' + example: UniversityDegreeCredential + type: string + credential_identifiers: + description: 'OPTIONAL. An array of strings, each uniquely identifying + a Credential that can be issued using the Access Token. These Credentials + correspond to the same entry in the `credential_configurations_supported` + Credential Issuer metadata but may contain different claims or subsets + of claims. + + ' + example: + - CivilEngineeringDegree-2023 + - ElectricalEngineeringDegree-2023 + items: + type: string + type: array + type: + description: 'REQUIRED. The type of authorization details. Must be + "openid_credential" when used to request issuance of a specific + Credential type. + + ' + example: openid_credential + type: string + type: object + type: array + c_nonce: + description: 'OPTIONAL. A nonce used when creating proof of possession of + the key proof. This nonce must be used for subsequent requests until a + fresh nonce is provided by the Credential Issuer. + + ' + example: tZignsnFbp + type: string + c_nonce_expires_in: + description: 'OPTIONAL. The lifetime in seconds of the `c_nonce`. After + this time, the `c_nonce` will expire. + + ' + example: 86400 + type: integer + expires_in: + description: 'The lifetime in seconds of the access token. After this time, + the token will expire and a new one must be requested. + + ' + example: 86400 + type: integer + token_type: + description: 'The type of the token issued. Typically this is "bearer". + + ' + example: bearer + type: string + required: + - access_token + - token_type + - expires_in + title: Token Response + type: object +info: + contact: + name: EWC + url: https://eudiwalletconsortium.org/ + description: The EWC APIs are defined to be used across all wallet providers within + EWC according the [EWC RFCs](https://github.com/EWC-consortium/eudi-wallet-rfcs). + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + title: EWC APIs + version: 2024.3.1 +openapi: 3.0.0 +paths: + /rfc001/.well-known/oauth-authorization-server: + get: + description: The Authorization server metadata contain metadata describing authorisation + server configuration. + operationId: authorisationServerMetadata + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorisationServerMetadata' + description: '' + summary: Authorisation Server Metadata + tags: + - 'EWC RFC001: Issue Verifiable Credential' + /rfc001/.well-known/openid-credential-issuer: + get: + description: The credential issuer metdata contains information on the Credential + Issuer's technical capabilities, supported Credentials, and (internationalized) + display information. + operationId: credentialIssuerMetadata + responses: + '200': + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/CredentialIssuerMetadata' + description: '' + summary: Credential Issuer Metadata + tags: + - 'EWC RFC001: Issue Verifiable Credential' + /rfc001/authorize: + get: + description: 'An Authorization Request is an OAuth 2.0 Authorization Request + as defined in Section 4.1.1 of [RFC6749]. This request seeks to obtain authorization + to access the Credential Endpoint. + + ' + operationId: authorizationRequest + parameters: + - description: 'REQUIRED. The value must be "code" to request an authorization + code. + + ' + in: query + name: response_type + required: true + schema: + type: string + - description: 'REQUIRED. The identifier for the client making the request. + + ' + in: query + name: client_id + required: true + schema: + type: string + - description: 'REQUIRED. The code challenge used for Proof Key for Code Exchange + (PKCE) as specified in OAuth 2.0 for Public Clients. + + ' + in: query + name: code_challenge + required: true + schema: + type: string + - description: 'OPTIONAL. The method used to transform the code verifier. Defaults + to "plain" if not present. Possible values are "S256" or "plain", as defined + in PKCE for OAuth 2.0. + + ' + in: query + name: code_challenge_method + required: true + schema: + type: string + - description: 'OPTIONAL. Provides fine-grained access details as specified + in the OAuth 2.0 Rich Authorization Requests specification. The `authorization_details` + parameter, as defined in Section 2 of [RFC9396], should be used to convey + the specifics of the Credentials the Wallet intends to obtain. This specification + introduces a new authorization details type, `openid_credential`. + + ' + in: query + name: authorization_details + required: false + schema: + type: string + - description: 'OPTIONAL. The redirection endpoint where the authorization server + will send the user-agent after authorization is complete. + + ' + in: query + name: redirect_uri + required: false + schema: + type: string + - description: 'OPTIONAL. Defines the scope of the Credential request. If the + Wallet is unsure of the scope value, it can discover it using the ''scope'' + parameter from the Credential Issuer''s metadata. This parameter assists + in identifying the appropriate Credential configuration based on the Credential + Offer or other parameters from the Credential Issuer. + + ' + in: query + name: scope + required: false + schema: + type: string + - description: 'OPTIONAL. A string value representing a specific processing + context at the Credential Issuer. This value is usually provided in a Credential + Offer from the Credential Issuer to the Wallet and is used to pass the issuer_state + value back to the Credential Issuer. + + ' + in: query + name: issuer_state + required: false + schema: + type: string + responses: + '302': + description: Authorization Response + headers: + Location: + description: 'The URL to which the user-agent will be redirected after + successful authorization. This URL contains the authorization code + and any additional parameters as specified in the OAuth 2.0 Authorization + framework. + + ' + schema: + type: string + summary: Authorization Request + tags: + - 'EWC RFC001: Issue Verifiable Credential' + /rfc001/credential: + post: + requestBody: + content: + application/json: + schema: + oneOf: + - properties: + credential_definition: + description: 'REQUIRED when the format parameter is present in + the Credential Request. MUST NOT be used otherwise. Contains + the detailed description of the Credential type. + + ' + properties: + credentialSubject: + description: 'OPTIONAL. + + ' + type: object + type: + description: 'REQUIRED. The credential issued by the Credential + Issuer MUST contain at least the values listed in this claim. + + ' + type: array + type: object + format: + description: 'REQUIRED when the credential_identifiers parameter + was not returned from the Token Response. MUST NOT be used otherwise. + + ' + type: string + proof: + description: 'OPTIONAL. Contains the proof of possession of the + cryptographic key material the issued Credential would be bound + to. + + ' + properties: + credential_identifier: + description: 'REQUIRED when credential_identifiers parameter + was returned from the Token Response. MUST NOT be used otherwise. + + ' + type: string + credential_response_encryption: + description: 'OPTIONAL. Contains information for encrypting + the Credential Response. + + ' + properties: + alg: + description: 'REQUIRED. JWE alg algorithm for encrypting + Credential Responses. + + ' + type: string + enc: + description: 'REQUIRED. JWE enc algorithm for encrypting + Credential Responses. + + ' + type: string + jwk: + description: 'REQUIRED. Contains a single public key as + a JWK used for encrypting the Credential Response. + + ' + type: object + type: object + proof_type: + description: 'REQUIRED. Denotes the key proof type. + + ' + type: string + type: object + required: + - proof_type + - jwk + - alg + - enc + - credential_definition + title: Credential Request (VC Signed as a JWT) + type: object + - properties: + claims: + description: 'OPTIONAL. + + ' + type: object + doctype: + description: 'REQUIRED when the format parameter is present in + the Credential Request. MUST NOT be used otherwise. The Credential + issued by the Credential Issuer MUST contain at least the values + listed in this claim. + + ' + type: string + format: + description: 'REQUIRED when the credential_identifiers parameter + was not returned from the Token Response. MUST NOT be used otherwise. + + ' + type: string + proof: + description: 'OPTIONAL. Contains the proof of possession of the + cryptographic key material the issued Credential would be bound + to. + + ' + properties: + credential_identifier: + description: 'REQUIRED when credential_identifiers parameter + was returned from the Token Response. MUST NOT be used otherwise. + + ' + type: string + credential_response_encryption: + description: 'OPTIONAL. Contains information for encrypting + the Credential Response. + + ' + properties: + alg: + description: 'REQUIRED. JWE alg algorithm for encrypting + Credential Responses. + + ' + type: string + enc: + description: 'REQUIRED. JWE enc algorithm for encrypting + Credential Responses. + + ' + type: string + jwk: + description: 'REQUIRED. Contains a single public key as + a JWK used for encrypting the Credential Response. + + ' + type: object + type: object + proof_type: + description: 'REQUIRED. Denotes the key proof type. + + ' + type: string + type: object + required: + - proof_type + - jwk + - alg + - enc + - doctype + title: Credential Request (ISO mDL) + type: object + - properties: + claims: + description: 'OPTIONAL. + + ' + type: object + format: + description: 'REQUIRED when the credential_identifiers parameter + was not returned from the Token Response. MUST NOT be used otherwise. + + ' + type: string + proof: + description: 'OPTIONAL. Contains the proof of possession of the + cryptographic key material the issued Credential would be bound + to. + + ' + properties: + credential_identifier: + description: 'REQUIRED when credential_identifiers parameter + was returned from the Token Response. MUST NOT be used otherwise. + + ' + type: string + credential_response_encryption: + description: 'OPTIONAL. Contains information for encrypting + the Credential Response. + + ' + properties: + alg: + description: 'REQUIRED. JWE alg algorithm for encrypting + Credential Responses. + + ' + type: string + enc: + description: 'REQUIRED. JWE enc algorithm for encrypting + Credential Responses. + + ' + type: string + jwk: + description: 'REQUIRED. Contains a single public key as + a JWK used for encrypting the Credential Response. + + ' + type: object + type: object + proof_type: + description: 'REQUIRED. Denotes the key proof type. + + ' + type: string + type: object + vct: + description: 'REQUIRED when the format parameter is present in + the Credential Request. MUST NOT be used otherwise. This claim + contains the type value of the Credential that the Wallet requests + the Credential Issuer to issue. + + ' + type: string + required: + - proof_type + - jwk + - alg + - enc + - vct + title: Credential Request (IETF SD-JWT VC) + type: object + required: true + responses: + '200': + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/InTimeCredentialResponse' + - $ref: '#/components/schemas/DeferredCredentialResponse' + description: Credential response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/InTimeCredentialErrorResponse' + description: Error Response + summary: Credential Request + tags: + - 'EWC RFC001: Issue Verifiable Credential' + /rfc001/credential_offer: + get: + description: The Credential Issuer sends Credential Offer using an HTTP GET + request or an HTTP redirect to the Wallet's Credential Offer Endpoint + operationId: credentialOfferEndpoint + parameters: + - description: Object with the Credential Offer parameters. This MUST NOT be + present when the `credential_offer_uri` parameter is present. + in: query + name: credential_offer + schema: + type: string + - description: String that is a URL using the https scheme referencing a resource + containing a JSON object with the Credential Offer parameters. This MUST + NOT be present when the `credential_offer` parameter is present. + in: query + name: credential_offer_uri + schema: + type: string + responses: + '200': + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/CredentialOffer' + description: '' + summary: Credential Offer Endpoint + tags: + - 'EWC RFC001: Issue Verifiable Credential' + /rfc001/deferred_credential: + post: + parameters: + - description: Acceptance token from the credential response + in: header + name: Authorization + schema: + example: Bearer eyJ0eXAi...KTjcrDMg + type: string + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/DeferredCredentialRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/InTimeCredentialResponse' + description: Credential response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/DeferredCredentialErrorResponse' + description: Error Response + summary: Deferred Credential Request + tags: + - 'EWC RFC001: Issue Verifiable Credential' + /rfc001/par: + post: + description: 'A Pushed Authorization Request (PAR) allows a client to submit + its authorization request parameters to the authorization server in advance, + providing a PAR endpoint that stores these parameters and returns a request + URI. The client then uses this URI in its authorization request to the authorization + endpoint. This method enhances security by avoiding the exposure of sensitive + parameters in the URL. + + ' + operationId: pushedAuthorizationRequest + requestBody: + content: + application/x-www-form-urlencoded: + schema: + properties: + authorization_details: + description: 'OPTIONAL. Provides fine-grained access details as + specified in the OAuth 2.0 Rich Authorization Requests specification. + The `authorization_details` parameter, as defined in Section 2 + of [RFC9396], should be used to convey the specifics of the Credentials + the Wallet intends to obtain. This specification introduces a + new authorization details type, `openid_credential`. + + ' + type: string + client_id: + description: 'REQUIRED. The identifier for the client making the + request. + + ' + type: string + code_challenge: + description: 'REQUIRED. The code challenge used for Proof Key for + Code Exchange (PKCE) as specified in OAuth 2.0 for Public Clients. + + ' + type: string + code_challenge_method: + description: 'OPTIONAL. The method used to transform the code verifier. + Defaults to "plain" if not present. Possible values are "S256" + or "plain", as defined in PKCE for OAuth 2.0. + + ' + type: string + issuer_state: + description: 'OPTIONAL. A string value representing a specific processing + context at the Credential Issuer. This value is usually provided + in a Credential Offer from the Credential Issuer to the Wallet + and is used to pass the issuer_state value back to the Credential + Issuer. + + ' + type: string + redirect_uri: + description: 'OPTIONAL. The redirection endpoint where the authorization + server will send the user-agent after authorization is complete. + + ' + type: string + response_type: + description: 'REQUIRED. The value must be "code" to request an authorization + code. + + ' + type: string + scope: + description: 'OPTIONAL. Defines the scope of the Credential request. + If the Wallet is unsure of the scope value, it can discover it + using the ''scope'' parameter from the Credential Issuer''s metadata. + This parameter assists in identifying the appropriate Credential + configuration based on the Credential Offer or other parameters + from the Credential Issuer. + + ' + type: string + required: + - response_type + - client_id + - code_challenge + - code_challenge_method + type: object + required: true + responses: + '201': + content: + application/json: + schema: + properties: + expires_in: + description: 'The lifetime in seconds of the request_uri. After + this time, the request_uri will no longer be valid. + + ' + type: integer + request_uri: + description: 'The URI that can be used in an authorization request + to reference the parameters submitted in the PAR request. + + ' + type: string + type: object + description: Pushed Authorization Request Created + summary: Pushed Authorization Request + tags: + - 'EWC RFC001: Issue Verifiable Credential' + /rfc001/token: + post: + requestBody: + content: + application/x-www-form-urlencoded: + schema: + oneOf: + - properties: + code: + description: 'The authorization code received from the authorization + server in response to the authorization request. This code is + used to obtain an access token from the token endpoint. + + ' + type: string + code_verifier: + description: 'The code verifier used in Proof Key for Code Exchange + (PKCE). This is included in the token request to validate the + code challenge that was used in the initial authorization request. + + ' + type: string + grant_type: + description: 'The type of the grant being used. For example, ''authorization_code'' + indicates that the authorization code grant type is being used. + + ' + example: authorization_code + type: string + redirect_uri: + description: 'The redirect URI used in the authorization request. + It must match the redirect URI used during the authorization + request to ensure consistency and prevent attacks. + + ' + type: string + required: + - grant_type + - code + title: Token Request (for authorisation code grant) + type: object + - $ref: '#/components/schemas/PreAuthorisedCodeTokenRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + description: Token response + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/TokenErrorResponse' + description: Error Response + summary: Token request + tags: + - 'EWC RFC001: Issue Verifiable Credential' +servers: +- description: Example server + url: https://example.com +tags: +- description: 'This consists of endpoints for EWC RFC001: Issue Verifiable Credential' + name: 'EWC RFC001: Issue Verifiable Credential' +- description: 'This consists of endpoints for EWC RFC002: Present Verifiable Credential' + name: 'EWC RFC002: Present Verifiable Credential' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f1fc4d2..4f745c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@sd-jwt/crypto-nodejs": "^0.6.1", "@sd-jwt/sd-jwt-vc": "^0.6.1", "express": "^4.18.3", + "express-openapi-validator": "^5.3.7", "image-data-uri": "^2.0.1", "jose": "^5.2.3", "jsonwebtoken": "^9.0.2", @@ -23,11 +24,32 @@ "waltid-sd-jwt": "^1.2311200231.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.0.tgz", + "integrity": "sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, "node_modules/@jorgeferrero/stream-to-buffer": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@jorgeferrero/stream-to-buffer/-/stream-to-buffer-2.0.6.tgz", "integrity": "sha512-G7jiBkMwDdYADntdV/qzl9H4Lkch0RyektqXl5+KhktduP4/rE1RaTz0wKZFdC2dKo3S0JaxoUUC3uDeXmI7EQ==" }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "node_modules/@sd-jwt/core": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@sd-jwt/core/-/core-0.6.1.tgz", @@ -106,6 +128,105 @@ "node": ">=16" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "license": "MIT", @@ -132,6 +253,42 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/ansi-escape-sequences": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz", @@ -159,6 +316,16 @@ "node": ">=4" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, "node_modules/array-back": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-2.0.0.tgz", @@ -304,6 +471,22 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "license": "MIT", @@ -391,6 +574,20 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", @@ -596,6 +793,78 @@ "node": ">= 0.10.0" } }, + "node_modules/express-openapi-validator": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-5.3.7.tgz", + "integrity": "sha512-AFlIXvICrPWJvWtrsfpL212kyvmyzThcZ1ASnnsRqmzxSo/3SV+J7M1oEsFYbqo7AYQPGtSBKy7eheGM6wPwag==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.7.0", + "@types/multer": "^1.4.12", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^2.1.1", + "content-type": "^1.0.5", + "json-schema-traverse": "^1.0.0", + "lodash.clonedeep": "^4.5.0", + "lodash.get": "^4.4.2", + "media-typer": "^1.1.0", + "multer": "^1.4.5-lts.1", + "ono": "^7.1.3", + "path-to-regexp": "^8.1.0" + }, + "peerDependencies": { + "express": "*" + } + }, + "node_modules/express-openapi-validator/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/express-openapi-validator/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/express-openapi-validator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/express-openapi-validator/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express-openapi-validator/node_modules/path-to-regexp": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.1.0.tgz", + "integrity": "sha512-Bqn3vc8CMHty6zuD+tG23s6v2kwxslHEhTj4eYaVKGIEB+YX/2wd0/rgXLFD9G9id9KCtbVy/3ZgmvZjpa0UdQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -619,6 +888,11 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==" + }, "node_modules/finalhandler": { "version": "1.2.0", "license": "MIT", @@ -1065,6 +1339,11 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1083,6 +1362,17 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -1194,6 +1484,16 @@ "version": "4.17.21", "license": "MIT" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1316,10 +1616,46 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.0.0", "license": "MIT" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/negotiator": { "version": "0.6.3", "license": "MIT", @@ -1357,6 +1693,14 @@ "node": "*" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "license": "MIT", @@ -1436,6 +1780,14 @@ "wrappy": "1" } }, + "node_modules/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-9jnfVriq7uJM4o5ganUY54ntUm+5EK21EGaQ5NWnkWg3zz5ywbbonlBguRcnmF1/HDiIe3zxNxXcO1YPBmPcQQ==", + "dependencies": { + "@jsdevtools/ono": "7.1.3" + } + }, "node_modules/p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -1514,6 +1866,11 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "license": "MIT", @@ -1576,6 +1933,25 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/reduce-flatten": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", @@ -1632,6 +2008,14 @@ "uuid": "bin/uuid" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -1789,6 +2173,27 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/stringify-parameters": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stringify-parameters/-/stringify-parameters-0.0.4.tgz", @@ -1898,11 +2303,21 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typical": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", "integrity": "sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==" }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, "node_modules/unpack-string": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/unpack-string/-/unpack-string-0.0.2.tgz", @@ -1923,6 +2338,11 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "license": "MIT", @@ -1996,6 +2416,14 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 48685d2..5a247ab 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://a6bf-2a02-587-870d-e900-8ba6-393a-385b-5730.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://f86b-2a02-587-870d-e900-5849-ca14-ce0f-823a.ngrok-free.app node server.js" }, "author": "", "license": "ISC", @@ -16,6 +16,7 @@ "@sd-jwt/crypto-nodejs": "^0.6.1", "@sd-jwt/sd-jwt-vc": "^0.6.1", "express": "^4.18.3", + "express-openapi-validator": "^5.3.7", "image-data-uri": "^2.0.1", "jose": "^5.2.3", "jsonwebtoken": "^9.0.2", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index be6fa62..c6fe1e9 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -16,7 +16,9 @@ const codeFlowRouterSDJWT = express.Router(); const serverURL = process.env.SERVER_URL || "http://localhost:3000"; -// auth code flow +// ****************************************************************** +// ************* CREDENTIAL OFFER ENDPOINTS ************************* +// ****************************************************************** codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); const codeSessions = getAuthCodeSessions(); @@ -45,10 +47,10 @@ codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { codeFlowRouterSDJWT.get(["/credential-offer-code-sd-jwt/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credentials: ["VerifiablePortableDocumentA1"], + credential_configuration_ids: ["VerifiablePortableDocumentA1"], grants: { - authorization_code: { - issuer_state: req.params.id, + "authorization_code": { + "issuer_state": req.params.id, }, }, }); @@ -164,10 +166,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { optional: code_challenge_method, authorization_details, redirect_uri, issuer_state */ if (authorizationDetails) { - - - - if (authorization_details && !response_type) + if (authorizationDetails && !response_type) errors.push("authorizationDetails missing response_type"); if (authorizationDetails && !client_id) errors.push("authorizationDetails missing client_id"); diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 9f7dfcd..bb08a95 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -125,7 +125,7 @@ router.get(["/offer-no-code"], async (req, res) => { /** * pre-authorised flow no transaction code request endpoint */ -router.get(["/ccredential-offer-no-code/:id"], (req, res) => { +router.get(["/credential-offer-no-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, credential_configuration_ids: ["VerifiablePortableDocumentA1"], diff --git a/server.js b/server.js index 050e44c..ea6b704 100644 --- a/server.js +++ b/server.js @@ -10,6 +10,12 @@ import pidRouter from "./routes/pidroutes.js"; import passportRouter from "./routes/passportRoutes.js"; import educationalRouter from "./routes/educationalRoutes.js"; import bodyParser from "body-parser"; // Body parser middleware +import * as OpenApiValidator from "express-openapi-validator"; + +import path from "path"; + +// Path to your OpenAPI spec file (JSON or YAML) +const apiSpec = path.join(process.cwd(), "openapi.yaml"); const app = express(); const port = 3000; @@ -24,6 +30,17 @@ app.use((req, res, next) => { ${req.method} ${req.url}`); next(); // Pass control to the next middleware function }); + + +// const apiSpecPath = path.join(process.cwd(), "openapi.yml"); +// app.use( +// OpenApiValidator.middleware({ +// apiSpec: apiSpecPath, +// validateRequests: false, // (default) +// validateResponses: false, // false by default +// // ignorePaths: [/^\/offer-code-sd-jwt$/, /^\/offer-no-code$/, /^\/offer-tx-code$/], +// }) +// ); app.use("/", router); app.use("/", verifierRouter); app.use("/", metadataRouter); @@ -34,6 +51,15 @@ app.use("/", passportRouter); app.use("/", educationalRouter); app.use("/", boardingPassRouter); +// Error handler for validation errors +app.use((err, req, res, next) => { + // Format error response + res.status(err.status || 500).json({ + message: err.message, + errors: err.errors, + }); +}); + // Start the server app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); From 9e6692bc50e97f96c81f3650ceab027fcc6dbe04 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 26 Sep 2024 11:43:03 +0300 Subject: [PATCH 03/37] cnf binding for sdjwt and updated issuer metadata --- data/issuer-config.json | 44 +- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 14 +- routes/metadataroutes.js | 2 +- routes/preAuthSDjwRoutes.js | 895 +++++++++++++++++----------------- 5 files changed, 487 insertions(+), 470 deletions(-) diff --git a/data/issuer-config.json b/data/issuer-config.json index e34b740..ac824a7 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -1,8 +1,10 @@ { "credential_issuer": "https://server.example.com", - "authorization_server": "https://server.example.com", + "authorization_servers": ["https://server.example.com"], "credential_endpoint": "https://server.example.com/credential", "deferred_credential_endpoint": "https://server.example.com/credential_deferred", + "notification_endpoint": "https://server.example.com/notification", + "batch_credential_endpoint":"https://server.example.com/batch", "display": [ { "name": "Issuer", @@ -12,24 +14,8 @@ } ], "credential_configurations_supported": { - "VerifiablePortableDocumentA1": { - "format": "vc+sd-jwt", - "scope": "VerifiablePortableDocumentA1", - "cryptographic_binding_methods_supported": [ - "jwk" - ], - "credential_signing_alg_values_supported": [ - "ES256" - ], - "display": [ - { - "name": "Portable Document A1", - "locale": "en-GB", - "background_color": "#12107c", - "text_color": "#FFFFFF" - } - ], - "vct": "VerifiablePortableDocumentA1", + "VerifiablePortableDocumentA1SDJWT": { + "scope": "VerifiablePortableDocumentA1SDJWT", "claims": { "given_name": { "display": [ @@ -55,7 +41,25 @@ } ] } + }, + "cryptographic_binding_methods_supported": ["jwk"], + "display": [ + { + "name": "SD-JWT Portable Document A1", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "credential_signing_alg_values_supported": ["ES256"], + "format": "vc+sd-jwt", + + "vct": "VerifiablePortableDocumentA1SDJWT", + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": ["ES256"] + } } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 5a247ab..f81fceb 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://f86b-2a02-587-870d-e900-5849-ca14-ce0f-823a.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://76f4-2a02-587-870d-e900-21e2-aefa-c1ad-a90d.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index c6fe1e9..0c1d3df 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -47,7 +47,7 @@ codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { codeFlowRouterSDJWT.get(["/credential-offer-code-sd-jwt/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA1"], + credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], grants: { "authorization_code": { "issuer_state": req.params.id, @@ -69,7 +69,7 @@ codeFlowRouterSDJWT.post("/par", async (req, res) => { const code_challenge_method = req.body.code_challenge_method; const claims = req.body.claims; const state = req.body.state; - const authorizationHeader = req.get(Authorization); + const authorizationHeader = req.get("Authorization"); const responseType = req.body.response_type; const issuerState = decodeURIComponent(req.body.issuer_state); // This can be associated with the ITB session let authorizationDetails = ""; @@ -115,6 +115,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { let code_challenge = req.query.code_challenge; let code_challenge_method = req.query.code_challenge_method; let authorizationHeader = req.headers['authorization']; // Fetch the 'Authorization' header + let claims="" //validations let errors = []; @@ -136,7 +137,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { state = parRequest.state; authorizationHeader = parRequest.authorizationHeader; issuerState = parRequest.issuerState; - authorizationDetails = parRequest = authorizationDetails; + authorizationDetails = parRequest.authorizationDetails; response_type = parRequest.response_type; } else { console.log( @@ -210,9 +211,10 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { if (response_type !== "code") { errors.push("Invalid response_type"); } - if (!scope.includes("openid")) { - errors.push("Invalid scope"); - } + // removed due to https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#name-using-scope-parameter-to-re + // if (!scope.includes("openid")) { + // errors.push("Invalid scope"); + // } const authorizationCode = null; //"SplxlOBeZQQYbYS6WxSbIA"; const codeSessions = getAuthCodeSessions(); diff --git a/routes/metadataroutes.js b/routes/metadataroutes.js index c3d3624..eb71e74 100644 --- a/routes/metadataroutes.js +++ b/routes/metadataroutes.js @@ -26,7 +26,7 @@ metadataRouter.get( "/.well-known/openid-credential-issuer", async (req, res) => { issuerConfig.credential_issuer = serverURL; - issuerConfig.authorization_server = serverURL; + issuerConfig.authorization_servers = [serverURL]; issuerConfig.credential_endpoint = serverURL + "/credential"; issuerConfig.deferred_credential_endpoint = serverURL + "/credential_deferred"; diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index bb08a95..f2bc5a6 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -12,8 +12,10 @@ import { buildIdToken, } from "../utils/tokenUtils.js"; -import {getCredentialSubjectForPersona, getPersonaFromAccessToken} from "../utils/personasUtils.js" - +import { + getCredentialSubjectForPersona, + getPersonaFromAccessToken, +} from "../utils/personasUtils.js"; import { getAuthCodeSessions, @@ -81,7 +83,7 @@ router.get(["/offer-tx-code"], async (req, res) => { router.get(["/credential-offer-tx-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA1"], + credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, @@ -128,7 +130,7 @@ router.get(["/offer-no-code"], async (req, res) => { router.get(["/credential-offer-no-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA1"], + credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, @@ -258,324 +260,169 @@ router.post("/credential", async (req, res) => { if (requestBody.proof && requestBody.proof.jwt) { // console.log(requestBody.proof.jwt) decodedWithHeader = jwt.decode(requestBody.proof.jwt, { complete: true }); + let holderJWKS = decodedWithHeader.header.jwk; // console.log(decodedWithHeader.payload.iss); decodedHeaderSubjectDID = decodedWithHeader.payload.iss; - } - // console.log(credential); - if (format === "jwt_vc_json") { - let payload = {}; - if (requestedCredentials != null && requestedCredentials[0] === "PID") { - //get persona if existing from accessToken - const preSessions = getPreCodeSessions(); - let persona = getPersonaFromAccessToken( - token, - preSessions.personas, - preSessions.accessTokens - ); + // console.log(credential); + if (format === "jwt_vc_json") { + let payload = {}; + if (requestedCredentials != null && requestedCredentials[0] === "PID") { + //get persona if existing from accessToken + const preSessions = getPreCodeSessions(); + let persona = getPersonaFromAccessToken( + token, + preSessions.personas, + preSessions.accessTokens + ); + + // Get the persona-specific credentialSubject + const credentialSubject = getCredentialSubjectForPersona( + persona, + decodedHeaderSubjectDID + ); - // Get the persona-specific credentialSubject - const credentialSubject = getCredentialSubjectForPersona(persona, decodedHeaderSubjectDID); - - - payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 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: credentialSubject, - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 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"], - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://europa.eu/2018/credentials/eudi/pid/v1", - ], - issuer: serverURL, - validFrom: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - }, - // Optional claims - }; - } else { - if ( - requestedCredentials != null && - requestedCredentials[0] === "ePassportCredential" - ) { payload = { iss: serverURL, sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Token expiration time (1 hour from now) 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) + // nbf: Math.floor(Date.now() / 1000), 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"], + credentialSubject: credentialSubject, + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 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"], "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://schemas.prod.digitalcredentials.iata.org/contexts/iata_credential.jsonld", + "https://europa.eu/2018/credentials/eudi/pid/v1", ], issuer: serverURL, validFrom: new Date( Math.floor(Date.now() / 1000) * 1000 ).toISOString(), }, + // Optional claims }; } else { if ( - (requestedCredentials != null && - requestedCredentials[0] === "EducationalID") || - requestedCredentials[0] === "StudentID" + requestedCredentials != null && + requestedCredentials[0] === "ePassportCredential" ) { - const preSessions = getPreCodeSessions(); - let persona = getPersonaFromAccessToken( - token, - preSessions.personas, - preSessions.accessTokens - ); - payload = { iss: serverURL, sub: decodedHeaderSubjectDID || "", iat: Math.floor(Date.now() / 1000), // Token issued at time - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Token expiration time (1 hour from now) + 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: ["StudentID"], //["EducationalID"], - "@context": ["https://www.w3.org/2018/credentials/v1"], - issuer: serverURL, credentialSubject: { - id: decodedHeaderSubjectDID || "", - identifier: "john.doe@university.edu", - schacPersonalUniqueCode: [ - "urn:schac:personalUniqueCode:int:esi:university.edu:12345", - ], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Doe", - firstName: "John", - displayName: "John Doe", - dateOfBirth: "1990-01-01", - commonName: "Johnathan Doe", - mail: "john.doe@university.edu", - eduPersonPrincipalName: "john.doe@university.edu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@university.edu"], - eduPersonAssurance: [ - "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", - ], + 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, + }, + }, + }, }, - issuanceDate: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 - ).toISOString(), + type: ["ePassportCredential"], + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemas.prod.digitalcredentials.iata.org/contexts/iata_credential.jsonld", + ], + issuer: serverURL, validFrom: new Date( Math.floor(Date.now() / 1000) * 1000 ).toISOString(), }, }; - - if (persona === "1") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", - identifier: "mario.conti@ewc.eu", - schacPersonalUniqueCode: [ - "urn:schac:personalUniqueCode:int:esi:university.edu:12345", - ], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Conti", - firstName: "Mario", - displayName: "Mario Conti", - dateOfBirth: "1990-01-01", - commonName: "Mario Contri", - mail: "mario.conti@ewc.eu", - eduPersonPrincipalName: "mario.conti@ewc.eu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@ewc.eu"], - eduPersonAssurance: [ - "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", - ], - }; - } else if (persona === "2") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", - identifier: "hannah@ewc.eu", - schacPersonalUniqueCode: [ - "urn:schac:personalUniqueCode:int:esi:university.edu:12345", - ], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Matkalainen", - firstName: "Hannah", - displayName: "Hannah Matkalainen", - dateOfBirth: "1990-01-01", - commonName: "Hannah Matkalainen", - mail: "hannah@ewc.eu", - eduPersonPrincipalName: "hannah@ewc.eu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@ewc.eu"], - eduPersonAssurance: [ - "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", - ], - }; - } else if (persona === "3") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", - identifier: "felix@ewc.eu", - schacPersonalUniqueCode: [ - "urn:schac:personalUniqueCode:int:esi:university.edu:12345", - ], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Fischer", - firstName: "Felix", - displayName: "Felix Fischer", - dateOfBirth: "1990-01-01", - commonName: "Felix Fischer", - mail: "felix@ewc.eu", - eduPersonPrincipalName: "felix@ewc.eu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@ewc.eu"], - eduPersonAssurance: [ - "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", - ], - }; - } } else { if ( - requestedCredentials != null && - requestedCredentials[0] === "allianceIDCredential" - ) { - payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - iat: Math.floor(Date.now() / 1000), // Token issued at time - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Token expiration time (1 hour from now) - jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", - vc: { - type: ["allianceIDCredential"], - "@context": ["https://www.w3.org/2018/credentials/v1"], - issuer: serverURL, - credentialSubject: { - id: decodedHeaderSubjectDID, // Replace with the actual subject DID - identifier: { - schemeID: "European Student Identifier", - value: - "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", - id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", - }, - }, - issuanceDate: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 - ).toISOString(), - validFrom: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - }, - }; - } else if ( - requestedCredentials != null && - requestedCredentials[0] === "ferryBoardingPassCredential" + (requestedCredentials != null && + requestedCredentials[0] === "EducationalID") || + requestedCredentials[0] === "StudentID" ) { const preSessions = getPreCodeSessions(); let persona = getPersonaFromAccessToken( @@ -591,9 +438,31 @@ router.post("/credential", async (req, res) => { exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Token expiration time (1 hour from now) jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", vc: { - type: ["VerifiableCredential", "ferryBoardingPassCredential"], + type: ["StudentID"], //["EducationalID"], "@context": ["https://www.w3.org/2018/credentials/v1"], issuer: serverURL, + credentialSubject: { + id: decodedHeaderSubjectDID || "", + identifier: "john.doe@university.edu", + schacPersonalUniqueCode: [ + "urn:schac:personalUniqueCode:int:esi:university.edu:12345", + ], + schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", + schacHomeOrganization: "university.edu", + familyName: "Doe", + firstName: "John", + displayName: "John Doe", + dateOfBirth: "1990-01-01", + commonName: "Johnathan Doe", + mail: "john.doe@university.edu", + eduPersonPrincipalName: "john.doe@university.edu", + eduPersonPrimaryAffiliation: "student", + eduPersonAffiliation: ["member", "student"], + eduPersonScopedAffiliation: ["student@university.edu"], + eduPersonAssurance: [ + "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", + ], + }, issuanceDate: new Date( Math.floor(Date.now() / 1000) * 1000 ).toISOString(), @@ -603,190 +472,334 @@ router.post("/credential", async (req, res) => { validFrom: new Date( Math.floor(Date.now() / 1000) * 1000 ).toISOString(), - 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", - }, }, }; if (persona === "1") { payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID - identifier: "Mario Conti", - ticketQR: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", - ticketNumber: "3022", - ticketLet: "Y", - lastName: "Conti", + id: decodedHeaderSubjectDID || "", + identifier: "mario.conti@ewc.eu", + schacPersonalUniqueCode: [ + "urn:schac:personalUniqueCode:int:esi:university.edu:12345", + ], + schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", + schacHomeOrganization: "university.edu", + familyName: "Conti", firstName: "Mario", - seatType: "Economy", - seatNumber: "12A", - departureDate: "2024-08-17", - departureTime: "13:07:34", - arrivalDate: "2024-08-17", - arrivalTime: "15:30:00", - arrivalPort: "MYKONOS TEST", - vesselDescription: "MYKONOS TEST", + displayName: "Mario Conti", + dateOfBirth: "1990-01-01", + commonName: "Mario Contri", + mail: "mario.conti@ewc.eu", + eduPersonPrincipalName: "mario.conti@ewc.eu", + eduPersonPrimaryAffiliation: "student", + eduPersonAffiliation: ["member", "student"], + eduPersonScopedAffiliation: ["student@ewc.eu"], + eduPersonAssurance: [ + "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", + ], }; } else if (persona === "2") { payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID - identifier: "Hannah Matkalainen", - ticketQR: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", - ticketNumber: "3022", - ticketLet: "Y", - lastName: "Matkalainen", + id: decodedHeaderSubjectDID || "", + identifier: "hannah@ewc.eu", + schacPersonalUniqueCode: [ + "urn:schac:personalUniqueCode:int:esi:university.edu:12345", + ], + schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", + schacHomeOrganization: "university.edu", + familyName: "Matkalainen", firstName: "Hannah", - seatType: "Economy", - seatNumber: "12A", - departureDate: "2024-08-17", - departureTime: "13:07:34", - arrivalDate: "2024-08-17", - arrivalTime: "15:30:00", - arrivalPort: "MYKONOS TEST", - vesselDescription: "MYKONOS TEST", + displayName: "Hannah Matkalainen", + dateOfBirth: "1990-01-01", + commonName: "Hannah Matkalainen", + mail: "hannah@ewc.eu", + eduPersonPrincipalName: "hannah@ewc.eu", + eduPersonPrimaryAffiliation: "student", + eduPersonAffiliation: ["member", "student"], + eduPersonScopedAffiliation: ["student@ewc.eu"], + eduPersonAssurance: [ + "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", + ], }; } else if (persona === "3") { payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID - identifier: "Felix Fischer", - ticketQR: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", - ticketNumber: "3022", - ticketLet: "Y", - lastName: "Fischer", + id: decodedHeaderSubjectDID || "", + identifier: "felix@ewc.eu", + schacPersonalUniqueCode: [ + "urn:schac:personalUniqueCode:int:esi:university.edu:12345", + ], + schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", + schacHomeOrganization: "university.edu", + familyName: "Fischer", firstName: "Felix", - seatType: "Economy", - seatNumber: "12A", - departureDate: "2024-08-17", - departureTime: "13:07:34", - arrivalDate: "2024-08-17", - arrivalTime: "15:30:00", - arrivalPort: "MYKONOS TEST", - vesselDescription: "MYKONOS TEST", + displayName: "Felix Fischer", + dateOfBirth: "1990-01-01", + commonName: "Felix Fischer", + mail: "felix@ewc.eu", + eduPersonPrincipalName: "felix@ewc.eu", + eduPersonPrimaryAffiliation: "student", + eduPersonAffiliation: ["member", "student"], + eduPersonScopedAffiliation: ["student@ewc.eu"], + eduPersonAssurance: [ + "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", + ], }; } } else { - //sign as jwt - payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 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", + if ( + requestedCredentials != null && + requestedCredentials[0] === "allianceIDCredential" + ) { + payload = { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + iat: Math.floor(Date.now() / 1000), // Token issued at time + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Token expiration time (1 hour from now) + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + type: ["allianceIDCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + issuer: serverURL, + credentialSubject: { + id: decodedHeaderSubjectDID, // Replace with the actual subject DID + identifier: { + schemeID: "European Student Identifier", + value: + "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", + id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", + }, + }, + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 + ).toISOString(), + validFrom: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), }, - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 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 - }; + }; + } else if ( + requestedCredentials != null && + requestedCredentials[0] === "ferryBoardingPassCredential" + ) { + const preSessions = getPreCodeSessions(); + let persona = getPersonaFromAccessToken( + token, + preSessions.personas, + preSessions.accessTokens + ); + + payload = { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + iat: Math.floor(Date.now() / 1000), // Token issued at time + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 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, + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 + ).toISOString(), + validFrom: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + 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", + }, + }, + }; + + if (persona === "1") { + payload.vc.credentialSubject = { + id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID + identifier: "Mario Conti", + ticketQR: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", + ticketNumber: "3022", + ticketLet: "Y", + lastName: "Conti", + firstName: "Mario", + seatType: "Economy", + seatNumber: "12A", + departureDate: "2024-08-17", + departureTime: "13:07:34", + arrivalDate: "2024-08-17", + arrivalTime: "15:30:00", + arrivalPort: "MYKONOS TEST", + vesselDescription: "MYKONOS TEST", + }; + } else if (persona === "2") { + payload.vc.credentialSubject = { + id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID + identifier: "Hannah Matkalainen", + ticketQR: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", + ticketNumber: "3022", + ticketLet: "Y", + lastName: "Matkalainen", + firstName: "Hannah", + seatType: "Economy", + seatNumber: "12A", + departureDate: "2024-08-17", + departureTime: "13:07:34", + arrivalDate: "2024-08-17", + arrivalTime: "15:30:00", + arrivalPort: "MYKONOS TEST", + vesselDescription: "MYKONOS TEST", + }; + } else if (persona === "3") { + payload.vc.credentialSubject = { + id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID + identifier: "Felix Fischer", + ticketQR: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", + ticketNumber: "3022", + ticketLet: "Y", + lastName: "Fischer", + firstName: "Felix", + seatType: "Economy", + seatNumber: "12A", + departureDate: "2024-08-17", + departureTime: "13:07:34", + arrivalDate: "2024-08-17", + arrivalTime: "15:30:00", + arrivalPort: "MYKONOS TEST", + vesselDescription: "MYKONOS TEST", + }; + } + } else { + //sign as jwt + payload = { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 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 * 24 * 30) * 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 - }; - - // Define additional JWT header fields - const additionalHeaders = { - kid: "aegean#authentication-key", - typ: "JWT", - }; - // Sign the token - const idtoken = jwt.sign(payload, privateKey, { - ...signOptions, - header: additionalHeaders, // Include additional headers separately - }); - - // console.log(idtoken); - /* jwt format */ - res.json({ - format: "jwt_vc_json", - credential: idtoken, - c_nonce: generateNonce(), - c_nonce_expires_in: 86400, - }); - } else { - - if (format === "vc+sd-jwt") { - // console.log("Token:", token); - // console.log("Request Body:", requestBody); - const { signer, verifier } = await createSignerVerifier( - pemToJWK(privateKey, "private"), - pemToJWK(publicKeyPem, "public") - ); - const sdjwt = new SDJwtVcInstance({ - signer, - verifier, - signAlg: "ES256", - hasher: digest, - hashAlg: "SHA-256", - saltGenerator: generateSalt, - }); - const claims = { - given_name: "John", - last_name: "Doe", + const signOptions = { + algorithm: "ES256", // Specify the signing algorithm }; - const disclosureFrame = { - _sd: ["given_name", "last_name"], + + // Define additional JWT header fields + const additionalHeaders = { + kid: "aegean#authentication-key", + typ: "JWT", }; - const credential = await sdjwt.issue( - { - iss: serverURL, - iat: new Date().getTime(), - vct: "VerifiablePortableDocumentA1", - ...claims, - }, - disclosureFrame - ); + // Sign the token + const idtoken = jwt.sign(payload, privateKey, { + ...signOptions, + header: additionalHeaders, // Include additional headers separately + }); + + // console.log(idtoken); + + /* jwt format */ res.json({ - format: "vc+sd-jwt", - credential: credential, + format: "jwt_vc_json", + credential: idtoken, c_nonce: generateNonce(), c_nonce_expires_in: 86400, }); } else { - console.log("UNSUPPORTED FORMAT!"); - console.log(format); + if (format === "vc+sd-jwt") { + // console.log("Token:", token); + // console.log("Request Body:", requestBody); + const { signer, verifier } = await createSignerVerifier( + pemToJWK(privateKey, "private"), + pemToJWK(publicKeyPem, "public") + ); + const sdjwt = new SDJwtVcInstance({ + signer, + verifier, + signAlg: "ES256", + hasher: digest, + hashAlg: "SHA-256", + saltGenerator: generateSalt, + }); + const claims = { + given_name: "John", + last_name: "Doe", + }; + const cnf = holderJWKS; + + const disclosureFrame = { + _sd: ["given_name", "last_name"], + }; + const credential = await sdjwt.issue( + { + iss: serverURL, + iat: new Date().getTime(), + vct: "VerifiablePortableDocumentA1SDJWT", + ...claims, + cnf: cnf, + }, + disclosureFrame + ); + res.json({ + format: "vc+sd-jwt", + credential: credential, + c_nonce: generateNonce(), + c_nonce_expires_in: 86400, + }); + } else { + console.log("UNSUPPORTED FORMAT!"); + console.log(format); + } } + } else { + console.log("NO keybinding info found!!!"); + console.log(requestBody.proof); } }); //issuerConfig.credential_endpoint = serverURL + "/credential"; @@ -869,8 +882,6 @@ function checkIfExistsIssuanceStatus( return null; } - - async function validatePKCE(sessions, code, code_verifier, issuanceResults) { for (let i = 0; i < sessions.length; i++) { let element = sessions[i]; From 7292e465c949e82f5cc28663caa6983acae6c2b9 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 26 Sep 2024 12:24:00 +0300 Subject: [PATCH 04/37] typo in cnf --- routes/preAuthSDjwRoutes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index f2bc5a6..0c22c30 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -771,7 +771,7 @@ router.post("/credential", async (req, res) => { given_name: "John", last_name: "Doe", }; - const cnf = holderJWKS; + const cnf = { jwk: holderJWKS }; const disclosureFrame = { _sd: ["given_name", "last_name"], @@ -786,6 +786,7 @@ router.post("/credential", async (req, res) => { }, disclosureFrame ); + res.json({ format: "vc+sd-jwt", credential: credential, From beb80aa311ed27fd2ee9b5230705b1be6711bf52 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Fri, 4 Oct 2024 18:35:39 +0300 Subject: [PATCH 05/37] v13 updates and dynamic vp request --- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 110 ++++++++++++++++++++++++++-------- routes/preAuthSDjwRoutes.js | 2 +- utils/cryptoUtils.js | 38 ++++++++---- 4 files changed, 113 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index f81fceb..9d4c05d 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://76f4-2a02-587-870d-e900-21e2-aefa-c1ad-a90d.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://9722-2a02-587-8710-c200-390f-19d1-d8ee-3fd3.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 0c1d3df..69de0f3 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -1,20 +1,24 @@ import express from "express"; import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; import { v4 as uuidv4 } from "uuid"; import { getAuthCodeSessions, getPushedAuthorizationRequests, getSessionsAuthorizationDetail, - getAuthCodeAuthorizationDetail + getAuthCodeAuthorizationDetail, } from "../services/cacheService.js"; import qr from "qr-image"; import imageDataURI from "image-data-uri"; import { streamToBuffer } from "@jorgeferrero/stream-to-buffer"; +import { generateNonce, buildVpRequestJWT } from "../utils/cryptoUtils.js"; const codeFlowRouterSDJWT = express.Router(); const serverURL = process.env.SERVER_URL || "http://localhost:3000"; +const privateKey = fs.readFileSync("./private-key.pem", "utf-8"); // ****************************************************************** // ************* CREDENTIAL OFFER ENDPOINTS ************************* @@ -49,8 +53,8 @@ codeFlowRouterSDJWT.get(["/credential-offer-code-sd-jwt/:id"], (req, res) => { credential_issuer: serverURL, credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], grants: { - "authorization_code": { - "issuer_state": req.params.id, + authorization_code: { + issuer_state: req.params.id, }, }, }); @@ -114,8 +118,8 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { let redirect_uri = req.query.redirect_uri; let code_challenge = req.query.code_challenge; let code_challenge_method = req.query.code_challenge_method; - let authorizationHeader = req.headers['authorization']; // Fetch the 'Authorization' header - let claims="" + let authorizationHeader = req.headers["authorization"]; // Fetch the 'Authorization' header + let claims = ""; //validations let errors = []; @@ -147,16 +151,20 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { } } - const redirectUri = req.query.redirect_uri - ? decodeURIComponent(req.query.redirect_uri) - : "localhost:8080"; + const redirectUri = redirect_uri + ? decodeURIComponent(redirect_uri) + : "openid4vp://"; const nonce = req.query.nonce; const codeChallenge = decodeURIComponent(req.query.code_challenge); const codeChallengeMethod = req.query.code_challenge_method; //this should equal to S256 try { - const clientMetadata = JSON.parse( - decodeURIComponent(req.query.client_metadata) - ); + if (req.query.client_metadata) { + const clientMetadata = JSON.parse( + decodeURIComponent(req.query.client_metadata) + ); + } else { + console.log("client_metadata was missing"); + } } catch (error) { console.log("client_metadata was missing"); console.log(error); @@ -180,7 +188,10 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { authorizationDetails.forEach((item) => { let cred = fetchVCTorCredentialConfigId(item); // cache authorizationDetails for the direct_post endpoint (it is needed to assosiate it with the auth. code generated there) - getSessionsAuthorizationDetail().set(issuerState,decodeURIComponent(authorizationDetails)) + getSessionsAuthorizationDetail().set( + issuerState, + decodeURIComponent(authorizationDetails) + ); console.log("requested credentials: " + cred); }); } @@ -237,7 +248,6 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { console.log("ITB session not found"); } - /* Authorizatiton Endpoint Response: */ @@ -269,7 +279,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { nonce, request_uri: The authorisation server’s private key signed the request. */ - const redirectUrl = `http://localhost:8080? + const redirectUrl = `${redirectUri}? state=${state} &client_id=${client_id} &redirect_uri=${serverURL}/direct_post_vci @@ -283,11 +293,62 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { }); codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { - const vpRequestJWT = buildVpRequestJSON( + let uuid = uuidv4(); + let client_id = serverURL + "/direct_post" + "/" + uuid; + const response_uri = serverURL + "/direct_post" + "/" + uuid; + + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + // Construct the absolute path to verifier-config.json + const configPath = path.join(__dirname, "..", "data", "verifier-config.json"); + // Read and parse the JSON file + // const clientMetadata = JSON.parse(fs.readFileSync(configPath, "utf-8")); + + // clientMetadata.presentation_definition_uri = + // serverURL + "/presentation-definition/" + uuid; + // clientMetadata.redirect_uris = [response_uri]; + // clientMetadata.client_id = client_id; + const clientMetadata = { + vp_formats: { + jwt_vp: { + alg: ["EdDSA", "ES256K"], + }, + ldp_vp: { + proof_type: ["Ed25519Signature2018"], + }, + }, + }; + + const vpRequestJWT = buildVpRequestJWT( client_id, - "response_uri", - null, - privateKey + response_uri, + { + id: "def-123", + input_descriptors: [ + { + id: "id-1", + schema: [ + { + uri: "https://example.org/credentials/schema/EmployeeCredential", + }, + ], + constraints: { + fields: [ + { + path: ["$.employeeId"], + filter: { + type: "string", + pattern: "^[0-9]{6}$", + }, + }, + ], + }, + }, + ], + }, + privateKey, + "redirect_uri", + clientMetadata ); res.send(vpRequestJWT); @@ -303,16 +364,17 @@ codeFlowRouterSDJWT.post("/direct_post_vci", async (req, res) => { console.log("direct_post_vci received jwt is::"); consnole.log(jwt); - -// - const authorizatiton_details = getSessionsAuthorizationDetail().get(state) + // + const authorizatiton_details = getSessionsAuthorizationDetail().get(state); if (jwt) { const codeSessions = getAuthCodeSessions(); const authorizationCode = generateNonce(16); //cache authorizatiton_detatils witth the generated code. this is needed for the token_endpoint - getAuthCodeAuthorizationDetail().set(authorizationCode,authorizatiton_details) - + getAuthCodeAuthorizationDetail().set( + authorizationCode, + authorizatiton_details + ); updateIssuerStateWithAuthCode( authorizationCode, @@ -321,7 +383,7 @@ codeFlowRouterSDJWT.post("/direct_post_vci", async (req, res) => { codeSessions.results, codeSessions.requests ); - const redirectUrl = `http://localhost:8080?code=${authorizationCode}&state=${state}`; + const redirectUrl = `${redirectUri}?code=${authorizationCode}&state=${state}`; return res.redirect(302, redirectUrl); } else { return res.sendStatus(500); diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 0c22c30..1c32686 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -779,7 +779,7 @@ router.post("/credential", async (req, res) => { const credential = await sdjwt.issue( { iss: serverURL, - iat: new Date().getTime(), + iat: Math.floor(Date.now() / 1000), vct: "VerifiablePortableDocumentA1SDJWT", ...claims, cnf: cnf, diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index a000967..8c08319 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -94,31 +94,43 @@ export function buildVpRequestJWT( client_id, response_uri, presentation_definition, - privateKey + privateKey, + client_id_scheme = "redirect_uri", // Default to "redirect_uri" + client_metadata = {} // Default to an empty object ) { - + // Construct the JWT payload let jwtPayload = { response_type: "vp_token", client_id: client_id, - client_id_scheme: "redirect_uri", + client_id_scheme: client_id_scheme, presentation_definition: presentation_definition, redirect_uri: response_uri, - // response_mode: "direct_post", - client_metadata : "", - // response_uri: response_uri, //TODO Note: If the Client Identifier scheme redirect_uri is used in conjunction with the Response Mode direct_post, and the response_uri parameter is present, the client_id value MUST be equal to the response_uri value + client_metadata: client_metadata, // }; + // Define the JWT header const header = { alg: "ES256", - kid: `aegean#authentication-key`, //this kid needs to be resolvable from the did.json endpoint + kid: `aegean#authentication-key`, // Ensure this kid is resolvable from the did.json endpoint }; - const token = jwt.sign(jwtPayload, privateKey, { - algorithm: "ES256", - noTimestamp: true, - header, - }); - return token; + // Conditional signing based on client_id_scheme + if (client_id_scheme !== "redirect_uri") { + // Sign the JWT as per the scheme's requirements + const token = jwt.sign(jwtPayload, privateKey, { + algorithm: "ES256", + noTimestamp: true, // Retain if necessary + header, + }); + return token; + } else { + // Do NOT sign the JWT for "redirect_uri" scheme + // Depending on your application's requirements, you might: + // a) Return the payload as a plain object + // b) Serialize it differently + // Here, we'll return the payload without signing + return jwtPayload; + } } From c3152718ea394d18ac97cfd01d5217e57d336d16 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Fri, 4 Oct 2024 19:26:19 +0300 Subject: [PATCH 06/37] bug for vp requuestt --- data/presentation_definition.json | 2 +- routes/codeFlowSdJwtRoutes.js | 35 ++++++++++--------------------- routes/verifierRoutes.js | 4 ++++ utils/cryptoUtils.js | 35 ++++++++++++++++++++----------- 4 files changed, 39 insertions(+), 37 deletions(-) diff --git a/data/presentation_definition.json b/data/presentation_definition.json index dd1655f..fc7bad5 100644 --- a/data/presentation_definition.json +++ b/data/presentation_definition.json @@ -26,7 +26,7 @@ "path": ["$.vct"], "filter": { "type": "string", - "const": "VerifiablePortableDocumentA1" + "const": "VerifiablePortableDocumentA2" } } ], diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 69de0f3..f8679c5 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -301,6 +301,13 @@ codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { const __dirname = path.dirname(__filename); // Construct the absolute path to verifier-config.json const configPath = path.join(__dirname, "..", "data", "verifier-config.json"); + const presentation_definition_sdJwt = JSON.parse(fs.readFileSync(path.join( + __dirname, + "..", + "data", + "presentation_definition.json" + ))); + // Read and parse the JSON file // const clientMetadata = JSON.parse(fs.readFileSync(configPath, "utf-8")); @@ -322,35 +329,15 @@ codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { const vpRequestJWT = buildVpRequestJWT( client_id, response_uri, - { - id: "def-123", - input_descriptors: [ - { - id: "id-1", - schema: [ - { - uri: "https://example.org/credentials/schema/EmployeeCredential", - }, - ], - constraints: { - fields: [ - { - path: ["$.employeeId"], - filter: { - type: "string", - pattern: "^[0-9]{6}$", - }, - }, - ], - }, - }, - ], - }, + presentation_definition_sdJwt, privateKey, "redirect_uri", clientMetadata ); + console.log("Dynamic VP request ") + console.log(JSON.stringify(vpRequestJWT, null, 2)); + res.send(vpRequestJWT); }); diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index fae6959..6748809 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -129,6 +129,10 @@ verifierRouter.get("/vpRequest/:id", async (req, res) => { serverURL, privateKey ); + + console.log("VP request ") + console.log(JSON.stringify(jwtToken, null, 2)); + res.type("text/plain").send(jwtToken); }); diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index 8c08319..b9f02c2 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -70,9 +70,21 @@ export function buildVpRequestJSON( presentation_definition: presentation_definition, redirect_uri: response_uri, // response_mode: "direct_post", - client_metadata : "", - - // response_uri: response_uri, //TODO Note: If the Client Identifier scheme redirect_uri is used in conjunction with the Response Mode direct_post, and the response_uri parameter is present, the client_id value MUST be equal to the response_uri value + nonce: "n-0S6_WzA2Mj", + state: "af0ifjsldkj", + client_metadata: { + vp_formats: { + jwt_vp: { + alg: ["EdDSA", "ES256K"], + }, + ldp_vp: { + proof_type: ["Ed25519Signature2018"], + }, + }, + // Add any additional required metadata here + }, + + // response_uri: response_uri, //TODO Note: If the Client Identifier scheme redirect_uri is used in conjunction with the Response Mode direct_post, and the response_uri parameter is present, the client_id value MUST be equal to the response_uri value }; // const header = { @@ -88,8 +100,6 @@ export function buildVpRequestJSON( return jwtPayload; } - - export function buildVpRequestJWT( client_id, response_uri, @@ -105,7 +115,9 @@ export function buildVpRequestJWT( client_id_scheme: client_id_scheme, presentation_definition: presentation_definition, redirect_uri: response_uri, - client_metadata: client_metadata, // + nonce: "n-0S6_WzA2Mj", + state: "af0ifjsldkj", + client_metadata: client_metadata, // }; // Define the JWT header @@ -133,9 +145,6 @@ export function buildVpRequestJWT( } } - - - export async function decryptJWE(jweToken, privateKeyPEM) { try { const privateKey = crypto.createPrivateKey(privateKeyPEM); @@ -155,14 +164,13 @@ export async function decryptJWE(jweToken, privateKeyPEM) { } } - export async function base64UrlEncodeSha256(codeVerifier) { // Convert the code verifier string to an ArrayBuffer with ASCII encoding const encoder = new TextEncoder(); const data = encoder.encode(codeVerifier); // Calculate the SHA-256 hash of the ArrayBuffer - const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); // Convert the ArrayBuffer to a Uint8Array const hashArray = new Uint8Array(hashBuffer); @@ -171,7 +179,10 @@ export async function base64UrlEncodeSha256(codeVerifier) { const base64String = btoa(String.fromCharCode.apply(null, hashArray)); // Convert Base64 to Base64URL by replacing '+' with '-', '/' with '_', and stripping '=' - const base64UrlString = base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + const base64UrlString = base64String + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); return base64UrlString; } From 3f16c9422a63f00160458844d624425ffef2d73f Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Fri, 4 Oct 2024 20:28:50 +0300 Subject: [PATCH 07/37] typos in code flow with authorization details --- routes/codeFlowSdJwtRoutes.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index f8679c5..464a153 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -76,7 +76,7 @@ codeFlowRouterSDJWT.post("/par", async (req, res) => { const authorizationHeader = req.get("Authorization"); const responseType = req.body.response_type; const issuerState = decodeURIComponent(req.body.issuer_state); // This can be associated with the ITB session - let authorizationDetails = ""; + let authorizationDetails = JSON.parse(req.body.authorization_details); let requestURI = "urn:aegean.gr:" + uuidv4(); let parRequests = getPushedAuthorizationRequests(); @@ -349,7 +349,7 @@ codeFlowRouterSDJWT.post("/direct_post_vci", async (req, res) => { let state = req.body["state"]; let jwt = req.body["id_token"]; console.log("direct_post_vci received jwt is::"); - consnole.log(jwt); + console.log(jwt); // const authorizatiton_details = getSessionsAuthorizationDetail().get(state); @@ -380,10 +380,10 @@ codeFlowRouterSDJWT.post("/direct_post_vci", async (req, res) => { // Function to fetch either vct or credential_configuration_id function fetchVCTorCredentialConfigId(data) { // Check for vct first, fallback to credential_configuration_id if not found - if (firstItem.vct) { - return firstItem.vct; - } else if (firstItem.credential_configuration_id) { - return firstItem.credential_configuration_id; + if (data.vct) { + return data.vct; + } else if (data.credential_configuration_id) { + return data.credential_configuration_id; } else { return null; // Return null if neither is found } From e5221f9be349efcc8a3b909de47a6b565c1c5a70 Mon Sep 17 00:00:00 2001 From: George J Padayatti Date: Sat, 5 Oct 2024 12:24:38 +0530 Subject: [PATCH 08/37] Fix #17: OpenAPI specification for conformance backend endpoints Signed-off-by: George J Padayatti --- openapi/conformance-backend-endpoint.yaml | 148 ++++++++++++++++++ .../wallet-to-wallet-endpoints.yaml | 0 2 files changed, 148 insertions(+) create mode 100644 openapi/conformance-backend-endpoint.yaml rename openapi.yml => openapi/wallet-to-wallet-endpoints.yaml (100%) diff --git a/openapi/conformance-backend-endpoint.yaml b/openapi/conformance-backend-endpoint.yaml new file mode 100644 index 0000000..fa994cd --- /dev/null +++ b/openapi/conformance-backend-endpoint.yaml @@ -0,0 +1,148 @@ +openapi: 3.0.0 +info: + title: ITB Conformance Backend + version: "1.0.0" +servers: + - description: Example conformance server + url: https://example.conformance.com +paths: + /offer-code-sd-jwt: + get: + summary: Credential offer (SD-JWT) + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /offer-no-code: + get: + summary: Credential Offer without transaction code + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /offer-tx-code: + get: + summary: Credential Offer with transaction code + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /pre-offer-jwt-bpass: + get: + summary: Pre Authorised Credential Offer (Boarding Pass) + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /pre-offer-jwt-edu: + get: + summary: Pre Authorised Credential Offer (Education ID) + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /pre-offer-jwt-alliance: + get: + summary: Pre Authorised Credential Offer (Alliance) + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /pre-offer-jwt-passport: + get: + summary: Pre Authorised Credential Offer (Passport) + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /pre-offer-jwt-pid: + get: + summary: Pre Authorised Credential Offer (PID) + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /offer-pid-persona: + get: + summary: Pre Authorised Credential Offer (PID with persona) + parameters: + - in: query + name: personaId + schema: + type: string + required: true + example: "2" + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /offer: + get: + summary: Credential Offer + responses: + "200": + description: Successful response + tags: + - "EWC RFC001: Issue Verifiable Credential" + /vp-request/pid: + get: + summary: Verification Request (PID) + responses: + "200": + description: Successful response + tags: + - "EWC RFC002: Present Verifiable Credential" + /vp-request/epassport: + get: + summary: Verification Request (EPassport) + responses: + "200": + description: Successful response + tags: + - "EWC RFC002: Present Verifiable Credential" + /vp-request/educationid: + get: + summary: Verification Request (Education ID) + responses: + "200": + description: Successful response + tags: + - "EWC RFC002: Present Verifiable Credential" + /vp-request/allianceid: + get: + summary: Verification Request (Alliance ID) + responses: + "200": + description: Successful response + tags: + - "EWC RFC002: Present Verifiable Credential" + /vp-request/erua-id: + get: + summary: Verification Request (EURA ID) + responses: + "200": + description: Successful response + tags: + - "EWC RFC002: Present Verifiable Credential" + /vp-request/ferryboardingpass: + get: + summary: Verification Request (Ferry Boarding Pass) + responses: + "200": + description: Successful response + tags: + - "EWC RFC002: Present Verifiable Credential" +tags: + - description: "This consists of conformance endpoints for EWC RFC001: Issue Verifiable Credential" + name: "EWC RFC001: Issue Verifiable Credential" + - description: "This consists of conformance endpoints for EWC RFC002: Present Verifiable Credential" + name: "EWC RFC002: Present Verifiable Credential" diff --git a/openapi.yml b/openapi/wallet-to-wallet-endpoints.yaml similarity index 100% rename from openapi.yml rename to openapi/wallet-to-wallet-endpoints.yaml From cdc49273028609fdb6ca403c23a08922d701882a Mon Sep 17 00:00:00 2001 From: George J Padayatti Date: Mon, 7 Oct 2024 10:36:00 +0530 Subject: [PATCH 09/37] Fix #19: Issues in staging branch Signed-off-by: George J Padayatti --- routes/codeFlowJwtRoutes.js | 2 +- routes/codeFlowSdJwtRoutes.js | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/routes/codeFlowJwtRoutes.js b/routes/codeFlowJwtRoutes.js index 784a355..817ee9d 100644 --- a/routes/codeFlowJwtRoutes.js +++ b/routes/codeFlowJwtRoutes.js @@ -61,7 +61,7 @@ codeFlowRouter.get(["/credential-offer-code/:id"], (req, res) => { -function updateIssuerStateWithAuthCode( +export function updateIssuerStateWithAuthCode( code, walletState, walletSessions, diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 464a153..17ff93f 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -14,6 +14,7 @@ import qr from "qr-image"; import imageDataURI from "image-data-uri"; import { streamToBuffer } from "@jorgeferrero/stream-to-buffer"; import { generateNonce, buildVpRequestJWT } from "../utils/cryptoUtils.js"; +import { updateIssuerStateWithAuthCode } from "./codeFlowJwtRoutes.js"; const codeFlowRouterSDJWT = express.Router(); @@ -76,7 +77,8 @@ codeFlowRouterSDJWT.post("/par", async (req, res) => { const authorizationHeader = req.get("Authorization"); const responseType = req.body.response_type; const issuerState = decodeURIComponent(req.body.issuer_state); // This can be associated with the ITB session - let authorizationDetails = JSON.parse(req.body.authorization_details); + let authorizationDetails = req.body.authorization_details; + const clientMetadata = req.body.client_metadata let requestURI = "urn:aegean.gr:" + uuidv4(); let parRequests = getPushedAuthorizationRequests(); @@ -93,6 +95,7 @@ codeFlowRouterSDJWT.post("/par", async (req, res) => { responseType: responseType, issuerState: issuerState, authorizationDetails: authorizationDetails, + clientMetadata: clientMetadata }); res.json({ @@ -120,6 +123,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { let code_challenge_method = req.query.code_challenge_method; let authorizationHeader = req.headers["authorization"]; // Fetch the 'Authorization' header let claims = ""; + let client_metadata = req.query.client_metadata; //validations let errors = []; @@ -143,10 +147,11 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { issuerState = parRequest.issuerState; authorizationDetails = parRequest.authorizationDetails; response_type = parRequest.response_type; + client_metadata = parRequest.clientMetadata; } else { console.log( "ERROR: request_uri present in authorization endpoint, but no par request cached for request_uri" + - request_uri + request_uri ); } } @@ -158,9 +163,9 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { const codeChallenge = decodeURIComponent(req.query.code_challenge); const codeChallengeMethod = req.query.code_challenge_method; //this should equal to S256 try { - if (req.query.client_metadata) { + if (client_metadata) { const clientMetadata = JSON.parse( - decodeURIComponent(req.query.client_metadata) + decodeURIComponent(client_metadata) ); } else { console.log("client_metadata was missing"); @@ -183,7 +188,13 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { errors.push("authorizationDetails missing code_challenge"); try { - authorizationDetails = decodeURIComponent(authorizationDetails); + if (authorizationDetails) { + authorizationDetails = JSON.parse( + decodeURIComponent(authorizationDetails) + ); + } else { + console.log("authorization_details was missing"); + } if (authorizationDetails.length > 0) { authorizationDetails.forEach((item) => { let cred = fetchVCTorCredentialConfigId(item); @@ -231,6 +242,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { const codeSessions = getAuthCodeSessions(); if (codeSessions.sessions.indexOf(issuerState) >= 0) { codeSessions.requests.push({ + redirectUri: redirectUri, challenge: codeChallenge, method: codeChallengeMethod, sessionId: authorizationCode, @@ -305,7 +317,7 @@ codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { __dirname, "..", "data", - "presentation_definition.json" + "presentation_definition.json" ))); // Read and parse the JSON file @@ -370,7 +382,7 @@ codeFlowRouterSDJWT.post("/direct_post_vci", async (req, res) => { codeSessions.results, codeSessions.requests ); - const redirectUrl = `${redirectUri}?code=${authorizationCode}&state=${state}`; + const redirectUrl = `${codeSessions.requests.redirectUri}?code=${authorizationCode}&state=${state}`; return res.redirect(302, redirectUrl); } else { return res.sendStatus(500); From 0db3edf8069c4c875ee543ba80288f0f6da8b3bd Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Mon, 7 Oct 2024 11:36:50 +0300 Subject: [PATCH 10/37] clean up vp requests --- data/issuer-config.json | 49 ++++++++++++++++++++++++- data/presentation_definition_sdjwt.json | 37 +++++++++++++++++++ data/verifier-config copy.json | 35 ++++++++++++++++++ data/verifier-config.json | 44 +++------------------- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 4 +- routes/preAuthSDjwRoutes.js | 12 ++++-- routes/verifierRoutes.js | 26 +++++++++---- 8 files changed, 155 insertions(+), 54 deletions(-) create mode 100644 data/presentation_definition_sdjwt.json create mode 100644 data/verifier-config copy.json diff --git a/data/issuer-config.json b/data/issuer-config.json index ac824a7..06eef9c 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -4,7 +4,7 @@ "credential_endpoint": "https://server.example.com/credential", "deferred_credential_endpoint": "https://server.example.com/credential_deferred", "notification_endpoint": "https://server.example.com/notification", - "batch_credential_endpoint":"https://server.example.com/batch", + "batch_credential_endpoint": "https://server.example.com/batch", "display": [ { "name": "Issuer", @@ -60,6 +60,53 @@ "proof_signing_alg_values_supported": ["ES256"] } } + }, + "VerifiablePortableDocumentA2SDJWT": { + "scope": "VerifiablePortableDocumentA2SDJWT", + "claims": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-GB" + }, + { + "name": "Vorname", + "locale": "de-DE" + } + ] + }, + "last_name": { + "display": [ + { + "name": "Surname", + "locale": "en-GB" + }, + { + "name": "Nachname", + "locale": "de-DE" + } + ] + } + }, + "cryptographic_binding_methods_supported": ["jwk"], + "display": [ + { + "name": "SD-JWT Portable Document A2", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "credential_signing_alg_values_supported": ["ES256"], + "format": "vc+sd-jwt", + + "vct": "VerifiablePortableDocumentA2SDJWT", + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": ["ES256"] + } + } } } } diff --git a/data/presentation_definition_sdjwt.json b/data/presentation_definition_sdjwt.json new file mode 100644 index 0000000..3862dc2 --- /dev/null +++ b/data/presentation_definition_sdjwt.json @@ -0,0 +1,37 @@ +{ + "id": "d49ee616-0e8d-4698-aff5-2a8a2362652d", + "name": "id-card-proof", + "format": { + "vc+sd-jwt": { + "alg": ["ES256", "ES384"] + } + }, + "input_descriptors": [ + { + "id": "abd4acb1-1dcb-41ad-8596-ceb1401a69c7", + "format": { + "vc+sd-jwt": { + "alg": ["ES256", "ES384"] + } + }, + "constraints": { + "fields": [ + { + "path": ["$.given_name"] + }, + { + "path": ["$.last_name"] + }, + { + "path": ["$.vct"], + "filter": { + "type": "string", + "const": "VerifiablePortableDocumentA2SDJWT" + } + } + ], + "limit_disclosure": "required" + } + } + ] +} diff --git a/data/verifier-config copy.json b/data/verifier-config copy.json new file mode 100644 index 0000000..a8c689d --- /dev/null +++ b/data/verifier-config copy.json @@ -0,0 +1,35 @@ +{ + "client_id": "https://dss.aegean.gr", + "client_name": "UAegean EWC Verifier", + "vp_formats_supported": [ + "jwt_vc_json", + "ldp_vc" + ], + "vp_token_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post" + ], + "scopes_supported": [ + "openid", + "profile", + "vc_present" + ], + "response_types_supported": [ + "vp_token" + ], + "grant_types_supported": [ + "authorization_code" + ], + "id_token_signing_alg_values_supported": [ + "RS256", + "ES256" + ], + "vp_token_signing_alg_values_supported": [ + "RS256", + "ES256" + ], + "subject_types_supported": [ + "pairwise" + ] + } + \ No newline at end of file diff --git a/data/verifier-config.json b/data/verifier-config.json index a4f57df..3de0382 100644 --- a/data/verifier-config.json +++ b/data/verifier-config.json @@ -1,39 +1,7 @@ { - "client_id": "https://dss.aegean.gr", - "client_name": "UAegean EWC Verifier", - "redirect_uris": [ - "https://example-verifier.com/callback" - ], - "vp_formats_supported": [ - "jwt_vc_json", - "ldp_vc" - ], - "vp_token_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post" - ], - "presentation_definition_uri": "https://example-verifier.com/presentation-definitions", - "scopes_supported": [ - "openid", - "profile", - "vc_present" - ], - "response_types_supported": [ - "vp_token" - ], - "grant_types_supported": [ - "authorization_code" - ], - "id_token_signing_alg_values_supported": [ - "RS256", - "ES256" - ], - "vp_token_signing_alg_values_supported": [ - "RS256", - "ES256" - ], - "subject_types_supported": [ - "pairwise" - ] - } - \ No newline at end of file + "client_name": "UAegean EWC Verifier", + "logo_uri": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "location": "Greece", + "cover_uri": "string", + "description": "EWC pilot case verification" +} diff --git a/package.json b/package.json index 9d4c05d..b9a6f9d 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://9722-2a02-587-8710-c200-390f-19d1-d8ee-3fd3.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://e645-2a02-587-8710-c200-1e97-1cb8-515a-ce8c.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 17ff93f..62efc75 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -347,8 +347,8 @@ codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { clientMetadata ); - console.log("Dynamic VP request ") - console.log(JSON.stringify(vpRequestJWT, null, 2)); + // console.log("Dynamic VP request ") + // console.log(JSON.stringify(vpRequestJWT, null, 2)); res.send(vpRequestJWT); }); diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 1c32686..4ec8ff9 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -83,7 +83,7 @@ router.get(["/offer-tx-code"], async (req, res) => { router.get(["/credential-offer-tx-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], + credential_configuration_ids: ["VerifiablePortableDocumentA2SDJWT"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, @@ -130,7 +130,7 @@ router.get(["/offer-no-code"], async (req, res) => { router.get(["/credential-offer-no-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], + credential_configuration_ids: ["VerifiablePortableDocumentA2SDJWT"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, @@ -257,6 +257,7 @@ router.post("/credential", async (req, res) => { //TODO valiate bearer header let decodedWithHeader; let decodedHeaderSubjectDID; + let vct = requestBody.vct if (requestBody.proof && requestBody.proof.jwt) { // console.log(requestBody.proof.jwt) decodedWithHeader = jwt.decode(requestBody.proof.jwt, { complete: true }); @@ -690,6 +691,8 @@ router.post("/credential", async (req, res) => { }; } } else { + + let credType = requestedCredentials[0] //VerifiablePortableDocumentA2 or VerifiablePortableDocumentA1 //sign as jwt payload = { iss: serverURL, @@ -715,7 +718,7 @@ router.post("/credential", async (req, res) => { Math.floor(Date.now() / 1000) * 1000 ).toISOString(), issuer: serverURL, - type: ["VerifiablePortableDocumentA2"], + type: [credType], validFrom: new Date( Math.floor(Date.now() / 1000) * 1000 ).toISOString(), @@ -755,6 +758,7 @@ router.post("/credential", async (req, res) => { if (format === "vc+sd-jwt") { // console.log("Token:", token); // console.log("Request Body:", requestBody); + let credType = vct // VerifiablePortableDocumentA1SDJWT or VerifiablePortableDocumentA2SDJWT const { signer, verifier } = await createSignerVerifier( pemToJWK(privateKey, "private"), pemToJWK(publicKeyPem, "public") @@ -780,7 +784,7 @@ router.post("/credential", async (req, res) => { { iss: serverURL, iat: Math.floor(Date.now() / 1000), - vct: "VerifiablePortableDocumentA1SDJWT", + vct: credType, ...claims, cnf: cnf, }, diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index 6748809..37b755f 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -6,6 +6,7 @@ import { generateNonce, decryptJWE, buildVpRequestJSON, + buildVpRequestJWT, } from "../utils/cryptoUtils.js"; import { decodeSdJwt, getClaims } from "@sd-jwt/decode"; import { digest } from "@sd-jwt/crypto-nodejs"; @@ -23,7 +24,7 @@ const privateKey = fs.readFileSync("./private-key.pem", "utf-8"); const publicKeyPem = fs.readFileSync("./public-key.pem", "utf-8"); const presentation_definition_sdJwt = JSON.parse( - fs.readFileSync("./data/presentation_definition.json", "utf-8") + fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") ); const presentation_definition_jwt = JSON.parse( @@ -119,18 +120,27 @@ verifierRouter.get("/vpRequest/:id", async (req, res) => { claims: null, }); - let jwtToken = buildVpRequestJSON( - stateParam, - nonce, + // let jwtToken = buildVpRequestJSON( + // stateParam, + // nonce, + // clientId, + // response_uri, + // presentation_definition_sdJwt, + // jwks, + // serverURL, + // privateKey + // ); + let client_id_scheme = "redirect_uri"; + let jwtToken = buildVpRequestJWT( clientId, response_uri, presentation_definition_sdJwt, - jwks, - serverURL, - privateKey + privateKey, + client_id_scheme, + clientMetadata ); - console.log("VP request ") + console.log("VP request "); console.log(JSON.stringify(jwtToken, null, 2)); res.type("text/plain").send(jwtToken); From ca3d199f18a8508b917fe2951a43366e65df2d01 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Mon, 7 Oct 2024 11:36:50 +0300 Subject: [PATCH 11/37] clean up vp requests --- data/issuer-config.json | 49 ++++++++++++++++++++++++- data/presentation_definition_sdjwt.json | 37 +++++++++++++++++++ data/verifier-config copy.json | 35 ++++++++++++++++++ data/verifier-config.json | 44 +++------------------- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 4 +- routes/preAuthSDjwRoutes.js | 12 ++++-- routes/verifierRoutes.js | 26 +++++++++---- 8 files changed, 155 insertions(+), 54 deletions(-) create mode 100644 data/presentation_definition_sdjwt.json create mode 100644 data/verifier-config copy.json diff --git a/data/issuer-config.json b/data/issuer-config.json index ac824a7..06eef9c 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -4,7 +4,7 @@ "credential_endpoint": "https://server.example.com/credential", "deferred_credential_endpoint": "https://server.example.com/credential_deferred", "notification_endpoint": "https://server.example.com/notification", - "batch_credential_endpoint":"https://server.example.com/batch", + "batch_credential_endpoint": "https://server.example.com/batch", "display": [ { "name": "Issuer", @@ -60,6 +60,53 @@ "proof_signing_alg_values_supported": ["ES256"] } } + }, + "VerifiablePortableDocumentA2SDJWT": { + "scope": "VerifiablePortableDocumentA2SDJWT", + "claims": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-GB" + }, + { + "name": "Vorname", + "locale": "de-DE" + } + ] + }, + "last_name": { + "display": [ + { + "name": "Surname", + "locale": "en-GB" + }, + { + "name": "Nachname", + "locale": "de-DE" + } + ] + } + }, + "cryptographic_binding_methods_supported": ["jwk"], + "display": [ + { + "name": "SD-JWT Portable Document A2", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "credential_signing_alg_values_supported": ["ES256"], + "format": "vc+sd-jwt", + + "vct": "VerifiablePortableDocumentA2SDJWT", + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": ["ES256"] + } + } } } } diff --git a/data/presentation_definition_sdjwt.json b/data/presentation_definition_sdjwt.json new file mode 100644 index 0000000..3862dc2 --- /dev/null +++ b/data/presentation_definition_sdjwt.json @@ -0,0 +1,37 @@ +{ + "id": "d49ee616-0e8d-4698-aff5-2a8a2362652d", + "name": "id-card-proof", + "format": { + "vc+sd-jwt": { + "alg": ["ES256", "ES384"] + } + }, + "input_descriptors": [ + { + "id": "abd4acb1-1dcb-41ad-8596-ceb1401a69c7", + "format": { + "vc+sd-jwt": { + "alg": ["ES256", "ES384"] + } + }, + "constraints": { + "fields": [ + { + "path": ["$.given_name"] + }, + { + "path": ["$.last_name"] + }, + { + "path": ["$.vct"], + "filter": { + "type": "string", + "const": "VerifiablePortableDocumentA2SDJWT" + } + } + ], + "limit_disclosure": "required" + } + } + ] +} diff --git a/data/verifier-config copy.json b/data/verifier-config copy.json new file mode 100644 index 0000000..a8c689d --- /dev/null +++ b/data/verifier-config copy.json @@ -0,0 +1,35 @@ +{ + "client_id": "https://dss.aegean.gr", + "client_name": "UAegean EWC Verifier", + "vp_formats_supported": [ + "jwt_vc_json", + "ldp_vc" + ], + "vp_token_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post" + ], + "scopes_supported": [ + "openid", + "profile", + "vc_present" + ], + "response_types_supported": [ + "vp_token" + ], + "grant_types_supported": [ + "authorization_code" + ], + "id_token_signing_alg_values_supported": [ + "RS256", + "ES256" + ], + "vp_token_signing_alg_values_supported": [ + "RS256", + "ES256" + ], + "subject_types_supported": [ + "pairwise" + ] + } + \ No newline at end of file diff --git a/data/verifier-config.json b/data/verifier-config.json index a4f57df..3de0382 100644 --- a/data/verifier-config.json +++ b/data/verifier-config.json @@ -1,39 +1,7 @@ { - "client_id": "https://dss.aegean.gr", - "client_name": "UAegean EWC Verifier", - "redirect_uris": [ - "https://example-verifier.com/callback" - ], - "vp_formats_supported": [ - "jwt_vc_json", - "ldp_vc" - ], - "vp_token_endpoint_auth_methods_supported": [ - "client_secret_basic", - "client_secret_post" - ], - "presentation_definition_uri": "https://example-verifier.com/presentation-definitions", - "scopes_supported": [ - "openid", - "profile", - "vc_present" - ], - "response_types_supported": [ - "vp_token" - ], - "grant_types_supported": [ - "authorization_code" - ], - "id_token_signing_alg_values_supported": [ - "RS256", - "ES256" - ], - "vp_token_signing_alg_values_supported": [ - "RS256", - "ES256" - ], - "subject_types_supported": [ - "pairwise" - ] - } - \ No newline at end of file + "client_name": "UAegean EWC Verifier", + "logo_uri": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "location": "Greece", + "cover_uri": "string", + "description": "EWC pilot case verification" +} diff --git a/package.json b/package.json index 9d4c05d..b9a6f9d 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://9722-2a02-587-8710-c200-390f-19d1-d8ee-3fd3.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://e645-2a02-587-8710-c200-1e97-1cb8-515a-ce8c.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 17ff93f..62efc75 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -347,8 +347,8 @@ codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { clientMetadata ); - console.log("Dynamic VP request ") - console.log(JSON.stringify(vpRequestJWT, null, 2)); + // console.log("Dynamic VP request ") + // console.log(JSON.stringify(vpRequestJWT, null, 2)); res.send(vpRequestJWT); }); diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 1c32686..4ec8ff9 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -83,7 +83,7 @@ router.get(["/offer-tx-code"], async (req, res) => { router.get(["/credential-offer-tx-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], + credential_configuration_ids: ["VerifiablePortableDocumentA2SDJWT"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, @@ -130,7 +130,7 @@ router.get(["/offer-no-code"], async (req, res) => { router.get(["/credential-offer-no-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], + credential_configuration_ids: ["VerifiablePortableDocumentA2SDJWT"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, @@ -257,6 +257,7 @@ router.post("/credential", async (req, res) => { //TODO valiate bearer header let decodedWithHeader; let decodedHeaderSubjectDID; + let vct = requestBody.vct if (requestBody.proof && requestBody.proof.jwt) { // console.log(requestBody.proof.jwt) decodedWithHeader = jwt.decode(requestBody.proof.jwt, { complete: true }); @@ -690,6 +691,8 @@ router.post("/credential", async (req, res) => { }; } } else { + + let credType = requestedCredentials[0] //VerifiablePortableDocumentA2 or VerifiablePortableDocumentA1 //sign as jwt payload = { iss: serverURL, @@ -715,7 +718,7 @@ router.post("/credential", async (req, res) => { Math.floor(Date.now() / 1000) * 1000 ).toISOString(), issuer: serverURL, - type: ["VerifiablePortableDocumentA2"], + type: [credType], validFrom: new Date( Math.floor(Date.now() / 1000) * 1000 ).toISOString(), @@ -755,6 +758,7 @@ router.post("/credential", async (req, res) => { if (format === "vc+sd-jwt") { // console.log("Token:", token); // console.log("Request Body:", requestBody); + let credType = vct // VerifiablePortableDocumentA1SDJWT or VerifiablePortableDocumentA2SDJWT const { signer, verifier } = await createSignerVerifier( pemToJWK(privateKey, "private"), pemToJWK(publicKeyPem, "public") @@ -780,7 +784,7 @@ router.post("/credential", async (req, res) => { { iss: serverURL, iat: Math.floor(Date.now() / 1000), - vct: "VerifiablePortableDocumentA1SDJWT", + vct: credType, ...claims, cnf: cnf, }, diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index 6748809..37b755f 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -6,6 +6,7 @@ import { generateNonce, decryptJWE, buildVpRequestJSON, + buildVpRequestJWT, } from "../utils/cryptoUtils.js"; import { decodeSdJwt, getClaims } from "@sd-jwt/decode"; import { digest } from "@sd-jwt/crypto-nodejs"; @@ -23,7 +24,7 @@ const privateKey = fs.readFileSync("./private-key.pem", "utf-8"); const publicKeyPem = fs.readFileSync("./public-key.pem", "utf-8"); const presentation_definition_sdJwt = JSON.parse( - fs.readFileSync("./data/presentation_definition.json", "utf-8") + fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") ); const presentation_definition_jwt = JSON.parse( @@ -119,18 +120,27 @@ verifierRouter.get("/vpRequest/:id", async (req, res) => { claims: null, }); - let jwtToken = buildVpRequestJSON( - stateParam, - nonce, + // let jwtToken = buildVpRequestJSON( + // stateParam, + // nonce, + // clientId, + // response_uri, + // presentation_definition_sdJwt, + // jwks, + // serverURL, + // privateKey + // ); + let client_id_scheme = "redirect_uri"; + let jwtToken = buildVpRequestJWT( clientId, response_uri, presentation_definition_sdJwt, - jwks, - serverURL, - privateKey + privateKey, + client_id_scheme, + clientMetadata ); - console.log("VP request ") + console.log("VP request "); console.log(JSON.stringify(jwtToken, null, 2)); res.type("text/plain").send(jwtToken); From 9c27f9b7b66adab35aec4c78e76518261968b40e Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Mon, 7 Oct 2024 18:30:03 +0300 Subject: [PATCH 12/37] bug for dynamic linking vp redirect uri --- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 33 ++++++++++++--------------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index b9a6f9d..2dd5248 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://e645-2a02-587-8710-c200-1e97-1cb8-515a-ce8c.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://7777-2a02-587-8710-c200-dfe5-692-8f85-d14b.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 62efc75..c58bbef 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -78,7 +78,7 @@ codeFlowRouterSDJWT.post("/par", async (req, res) => { const responseType = req.body.response_type; const issuerState = decodeURIComponent(req.body.issuer_state); // This can be associated with the ITB session let authorizationDetails = req.body.authorization_details; - const clientMetadata = req.body.client_metadata + const clientMetadata = req.body.client_metadata; let requestURI = "urn:aegean.gr:" + uuidv4(); let parRequests = getPushedAuthorizationRequests(); @@ -95,7 +95,7 @@ codeFlowRouterSDJWT.post("/par", async (req, res) => { responseType: responseType, issuerState: issuerState, authorizationDetails: authorizationDetails, - clientMetadata: clientMetadata + clientMetadata: clientMetadata, }); res.json({ @@ -151,7 +151,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { } else { console.log( "ERROR: request_uri present in authorization endpoint, but no par request cached for request_uri" + - request_uri + request_uri ); } } @@ -164,9 +164,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { const codeChallengeMethod = req.query.code_challenge_method; //this should equal to S256 try { if (client_metadata) { - const clientMetadata = JSON.parse( - decodeURIComponent(client_metadata) - ); + const clientMetadata = JSON.parse(decodeURIComponent(client_metadata)); } else { console.log("client_metadata was missing"); } @@ -291,15 +289,9 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { nonce, request_uri: The authorisation server’s private key signed the request. */ - const redirectUrl = `${redirectUri}? - state=${state} - &client_id=${client_id} - &redirect_uri=${serverURL}/direct_post_vci - &response_type=id_token - &response_mode=direct_post - &scope=openid - &nonce=${nonce} - &request_uri=${serverURL}/request_uri_dynamic`; + const vpRedirectURI="openid4vp://" + console.log(redirect_uri + " but i will request the vp based on " + vpRedirectURI) + const redirectUrl = `${vpRedirectURI}?state=${state}&client_id=${client_id}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${serverURL}/request_uri_dynamic`; return res.redirect(302, redirectUrl); } }); @@ -313,12 +305,11 @@ codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { const __dirname = path.dirname(__filename); // Construct the absolute path to verifier-config.json const configPath = path.join(__dirname, "..", "data", "verifier-config.json"); - const presentation_definition_sdJwt = JSON.parse(fs.readFileSync(path.join( - __dirname, - "..", - "data", - "presentation_definition.json" - ))); + const presentation_definition_sdJwt = JSON.parse( + fs.readFileSync( + path.join(__dirname, "..", "data", "presentation_definition_sdjwt.json") + ) + ); // Read and parse the JSON file // const clientMetadata = JSON.parse(fs.readFileSync(configPath, "utf-8")); From 2caa8627526251e9997792192a7addbaf84de5c3 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Wed, 9 Oct 2024 15:02:53 +0300 Subject: [PATCH 13/37] auth request by value --- data/oauth-config.json | 3 +- package-lock.json | 4 +- package.json | 3 +- routes/codeFlowSdJwtRoutes.js | 9 +- routes/preAuthSDjwRoutes.js | 2 +- routes/verifierRoutes.js | 165 ++++++++++++++++++++++------------ utils/cryptoUtils.js | 6 +- utils/tokenUtils.js | 30 +++++++ 8 files changed, 157 insertions(+), 65 deletions(-) diff --git a/data/oauth-config.json b/data/oauth-config.json index 6036f0e..2708aad 100644 --- a/data/oauth-config.json +++ b/data/oauth-config.json @@ -32,7 +32,8 @@ "subject_syntax_types_supported": [ "did:key:jwk_jcs-pub", "did:ebsi:v1", - "did:ebsi:v2" + "did:ebsi:v2", + "did:jwk" ], "subject_trust_frameworks_supported": ["ebsi", "ewc-issuer-trust-list"], "id_token_types_supported": [ diff --git a/package-lock.json b/package-lock.json index 4f745c5..f1bd4e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@sd-jwt/core": "^0.6.1", "@sd-jwt/crypto-nodejs": "^0.6.1", "@sd-jwt/sd-jwt-vc": "^0.6.1", + "base64url": "^3.0.1", "express": "^4.18.3", "express-openapi-validator": "^5.3.7", "image-data-uri": "^2.0.1", @@ -400,7 +401,8 @@ }, "node_modules/base64url": { "version": "3.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", "engines": { "node": ">=6.0.0" } diff --git a/package.json b/package.json index 2dd5248..7a94a4d 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://7777-2a02-587-8710-c200-dfe5-692-8f85-d14b.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://4a3a-2a02-587-8710-c200-ab47-cbd6-6894-33d4.ngrok-free.app node server.js" }, "author": "", "license": "ISC", @@ -15,6 +15,7 @@ "@sd-jwt/core": "^0.6.1", "@sd-jwt/crypto-nodejs": "^0.6.1", "@sd-jwt/sd-jwt-vc": "^0.6.1", + "base64url": "^3.0.1", "express": "^4.18.3", "express-openapi-validator": "^5.3.7", "image-data-uri": "^2.0.1", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index c58bbef..c9d7409 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -9,6 +9,7 @@ import { getSessionsAuthorizationDetail, getAuthCodeAuthorizationDetail, } from "../services/cacheService.js"; +import { buildVPbyValue } from "../utils/tokenUtils.js"; import qr from "qr-image"; import imageDataURI from "image-data-uri"; @@ -291,7 +292,13 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { */ const vpRedirectURI="openid4vp://" console.log(redirect_uri + " but i will request the vp based on " + vpRedirectURI) - const redirectUrl = `${vpRedirectURI}?state=${state}&client_id=${client_id}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${serverURL}/request_uri_dynamic`; + // changed this to support auth request by value not reference + //const redirectUrl = `${vpRedirectURI}?state=${state}&client_id=${client_id}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${serverURL}/request_uri_dynamic`; + const response_url = serverURL+"/request_uri_dynamic" + const presentation_definition_uri = + serverURL + "/presentation-definition/itbsdjwt"; + const client_metadata_uri = serverURL + "/client-metadata"; + const redirectUrl = buildVPbyValue(client_id,presentation_definition_uri,"redirect_uri",client_metadata_uri,response_url) return res.redirect(302, redirectUrl); } }); diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 4ec8ff9..7630635 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -869,7 +869,7 @@ function checkIfExistsIssuanceStatus( // } // }); // } - if (index >= 0) { + if (index >= 0 && sessionResults[index]) { let status = sessionResults[index].status; console.log(`sending status ${status} for session ${sessionId}`); console.log(`new sessions`); diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index 37b755f..08b7801 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -8,6 +8,8 @@ import { buildVpRequestJSON, buildVpRequestJWT, } from "../utils/cryptoUtils.js"; + +import {buildVPbyValue} from "../utils/tokenUtils.js" import { decodeSdJwt, getClaims } from "@sd-jwt/decode"; import { digest } from "@sd-jwt/crypto-nodejs"; import qr from "qr-image"; @@ -51,6 +53,10 @@ const presentation_definition_ferryboardingpass = JSON.parse( ) ); +const client_metadata = JSON.parse( + fs.readFileSync("./data/verifier-config.json", "utf-8") +); + const presentation_definition_alliance_and_education_Id = JSON.parse( fs.readFileSync( "./data/presentation_definition_alliance_and_education_Id.json", @@ -73,16 +79,30 @@ verifierRouter.get("/generateVPRequest", async (req, res) => { const stateParam = req.query.id ? req.query.id : uuidv4(); const nonce = generateNonce(16); - let request_uri = serverURL + "/vpRequest/" + stateParam; - const response_uri = serverURL + "/direct_post"; //not used + // The Verifier may send an Authorization Request as Request Object by value + // or by reference as defined in JWT-Secured Authorization Request (JAR) + // The Verifier articulates requirements of the Credential(s) that + // are requested using presentation_definition and presentation_definition_uri - const vpRequest = buildVP( - serverURL, - response_uri, - request_uri, - stateParam, - nonce, - encodeURIComponent(JSON.stringify(presentation_definition_sdJwt)) + const uuid = req.params.id ? req.params.id : uuidv4(); + //url.searchParams.get("presentation_definition"); + const response_uri = serverURL + "/direct_post" + "/" + uuid; + const presentation_definition_uri = + serverURL + "/presentation-definition/itbsdjwt"; + const client_metadata_uri = serverURL + "/client-metadata"; + const clientId = serverURL + "/direct_post" + "/" + uuid; + sessions.push(uuid); + verificationSessions.push({ + uuid: uuid, + status: "pending", + claims: null, + }); + const vpRequest = buildVPbyValue( + clientId, + presentation_definition_uri, + "redirect_uri", + client_metadata_uri, + response_uri ); let code = qr.image(vpRequest, { @@ -102,48 +122,44 @@ verifierRouter.get("/generateVPRequest", async (req, res) => { // res.json({ vpRequest: vpRequest }); }); -verifierRouter.get("/vpRequest/:id", async (req, res) => { - console.log("VPRequest called Will send JWT"); - // console.log(jwtToken); - const uuid = req.params.id ? req.params.id : uuidv4(); +//***********PRESENTATION DEFINITON endpoint to support presentation_definition_uri Parameter */ +verifierRouter.get("/presentation-definition/:type", async (req, res) => { + const { type } = req.params; + const presentationDefinitions = { + 1: presentation_definition_jwt, + jwt: presentation_definition_jwt, + 2: presentation_definition_sdJwt, + itbsdjwt: presentation_definition_sdJwt, + 3: presentation_definition_pid, + pid: presentation_definition_pid, + 4: presentation_definition_epass, + epass: presentation_definition_epass, + 5: presentation_definition_alliance_and_education_Id, + eduId: presentation_definition_alliance_and_education_Id, + 6: presentation_definition_ferryboardingpass, // Changed from '5' to '6' to avoid duplication + ferryboarding: presentation_definition_ferryboardingpass, + }; - //url.searchParams.get("presentation_definition"); - const stateParam = uuidv4(); - const nonce = generateNonce(16); + // Retrieve the appropriate presentation definition based on the type + const selectedDefinition = presentationDefinitions[type]; - const response_uri = serverURL + "/direct_post" + "/" + uuid; - let clientId = serverURL + "/direct_post" + "/" + uuid; - sessions.push(uuid); - verificationSessions.push({ - uuid: uuid, - status: "pending", - claims: null, - }); - - // let jwtToken = buildVpRequestJSON( - // stateParam, - // nonce, - // clientId, - // response_uri, - // presentation_definition_sdJwt, - // jwks, - // serverURL, - // privateKey - // ); - let client_id_scheme = "redirect_uri"; - let jwtToken = buildVpRequestJWT( - clientId, - response_uri, - presentation_definition_sdJwt, - privateKey, - client_id_scheme, - clientMetadata - ); + if (selectedDefinition) { + res.type("application/json").send(selectedDefinition); + } else { + // Log the error for debugging purposes (optional) + console.error(`No presentation definition found for type: ${type}`); - console.log("VP request "); - console.log(JSON.stringify(jwtToken, null, 2)); + // Send a 500 Internal Server Error response + res.status(500).json({ + error: "Internal Server Error", + message: `No presentation definition found for type: ${type}`, + }); + } +}); - res.type("text/plain").send(jwtToken); +// CLIENT VERIFIER METADATA +verifierRouter.get("/client-metadata", async (req, res) => { + res.type("application/json").send(clientMetadata); }); verifierRouter.post("/direct_post/:id", async (req, res) => { @@ -178,6 +194,50 @@ verifierRouter.post("/direct_post/:id", async (req, res) => { } }); +// this endpoint returns the autthorization requqest object (by reference), and is to be set on the request_uri +// however, when combined with client_id_scheme=redirect_uri then, the Authorization Request MUST NOT be signed, +// this causes incompatibilities with JAR, which is the expected result form this endpoint: +// The Verifier may send an Authorization Request as Request Object by value or by reference +// as defined in JWT-Secured Authorization Request (JAR) +// as a result this endpoint will not be used and we will only use request object by Value +/* + ******************DEPRECATED****************************************************************************** + */ +verifierRouter.get("/vpRequest/:id", async (req, res) => { + console.log("VPRequest called Will send JWT"); + // console.log(jwtToken); + const uuid = req.params.id ? req.params.id : uuidv4(); + + //url.searchParams.get("presentation_definition"); + const stateParam = uuidv4(); + const nonce = generateNonce(16); + + const response_uri = serverURL + "/direct_post" + "/" + uuid; + let clientId = serverURL + "/direct_post" + "/" + uuid; + sessions.push(uuid); + verificationSessions.push({ + uuid: uuid, + status: "pending", + claims: null, + }); + + let client_id_scheme = "redirect_uri"; + let jwtToken = buildVpRequestJWT( + clientId, + response_uri, + presentation_definition_sdJwt, + privateKey, + client_id_scheme, + clientMetadata + ); + + console.log("VP request "); + //console.log(JSON.stringify(jwtToken, null, 2)); + console.log(jwtToken); + + res.type("text/plain").send(jwtToken); +}); + // ******************************************************* verifierRouter.get("/generateVPRequest-jwt", async (req, res) => { @@ -250,15 +310,6 @@ verifierRouter.get("/vpRequestJwt/:id", async (req, res) => { res.json(vpRequest); }); -verifierRouter.get("/presentation-definition/:type", async (req, res) => { - const { type } = req.params; - if (type == 1) { - res.type("application/json").send(presentation_definition_jwt); - } - console.log("ERROR getting presentatiton-definition type"); - res.status(500); -}); - // *******************PILOT USE CASES ****************************** verifierRouter.get("/vp-request/:type", async (req, res) => { const { type } = req.params; @@ -491,6 +542,8 @@ function buildVP( return result; } + + async function flattenCredentialsToClaims(credentials) { let claimsResult = {}; credentials.forEach((credentialJwt) => { diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index b9f02c2..e9e9b8b 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -1,6 +1,7 @@ import crypto from "crypto"; import jwt from "jsonwebtoken"; import * as jose from "jose"; +import base64url from "base64url"; export function pemToJWK(pem, keyType) { let key; @@ -137,10 +138,7 @@ export function buildVpRequestJWT( return token; } else { // Do NOT sign the JWT for "redirect_uri" scheme - // Depending on your application's requirements, you might: - // a) Return the payload as a plain object - // b) Serialize it differently - // Here, we'll return the payload without signing + return jwtPayload; } } diff --git a/utils/tokenUtils.js b/utils/tokenUtils.js index 26ca835..1e9cef4 100644 --- a/utils/tokenUtils.js +++ b/utils/tokenUtils.js @@ -44,3 +44,33 @@ export function buildIdToken(issuerURL, privateKey) { // console.log("Generated ID Token:", idToken); return idToken; } + + +export function buildVPbyValue( + client_id, + presentation_definition_uri, + client_id_scheme = "redirect_uri", + client_metadata_uri, + redirect_uri +) { + if (client_id_scheme == "redirect_uri") { + redirect_uri = client_id; + } + + let result = + "openid4vp://?client_id=" + + encodeURIComponent(client_id) + + "&response_type=vp_token" + + "&response_mode=direct_post"+ + "&response_uri=" + + encodeURIComponent(redirect_uri) + + "&presentation_definition_uri=" + + encodeURIComponent(presentation_definition_uri) + + "&client_id_scheme=" + + client_id_scheme + + "&client_metadata_uri=" + + encodeURIComponent(client_metadata_uri) + + "&nonce=n0S6_WzA2Mj" + + "&state=af0ifjsldkj"; + return result; +} \ No newline at end of file From 3b14d55bbc58df7420261c2dfa33571cce8a4d64 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Tue, 15 Oct 2024 13:08:26 +0300 Subject: [PATCH 14/37] ngrok endpoing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a94a4d..ba395af 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://4a3a-2a02-587-8710-c200-ab47-cbd6-6894-33d4.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://159c-2a02-587-8710-c200-f16f-3054-7426-ff3a.ngrok-free.app node server.js" }, "author": "", "license": "ISC", From 2616c80e7016628bec6abd969b618f186152db4a Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Tue, 15 Oct 2024 15:25:48 +0300 Subject: [PATCH 15/37] refactoring --- data/issuer-config.json | 542 +++++++++++++++++++++++++++++ package.json | 2 +- routes/preAuthSDjwRoutes.js | 676 ++++++++---------------------------- utils/credPayloadUtil.js | 448 ++++++++++++++++++++++++ 4 files changed, 1131 insertions(+), 537 deletions(-) create mode 100644 utils/credPayloadUtil.js diff --git a/data/issuer-config.json b/data/issuer-config.json index 06eef9c..fef5ebc 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -107,6 +107,548 @@ "proof_signing_alg_values_supported": ["ES256"] } } + }, + + "VerifiablePIDSDJWT": { + "scope": "PID", + "claims": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-GB" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-GB" + } + ] + }, + "birth_date": { + "display": [ + { + "name": "Birth Date", + "locale": "en-GB" + } + ] + }, + "age_over_18": { + "display": [ + { + "name": "Age Over 18", + "locale": "en-GB" + } + ] + }, + "issuance_date": { + "display": [ + { + "name": "Issuance Date", + "locale": "en-GB" + } + ] + }, + "expiry_date": { + "display": [ + { + "name": "Expiry Date", + "locale": "en-GB" + } + ] + }, + "issuing_authority": { + "display": [ + { + "name": "Issuing Authority", + "locale": "en-GB" + } + ] + }, + "issuing_country": { + "display": [ + { + "name": "Issuing Country", + "locale": "en-GB" + } + ] + } + }, + "cryptographic_binding_methods_supported": ["jwk"], + "display": [ + { + "name": "PID", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF", + "logo": { + "url": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "alt_text": "UAegean" + } + } + ], + "credential_signing_alg_values_supported": ["ES256"], + "format": "vc+sd-jwt", + "vct": "VerifiablePIDSDJWT", + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": ["ES256"] + } + } + }, + + "VerifiableePassportCredentialSDJWT": { + "scope": "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" + } + ] + } + } + } + } + } + }, + "cryptographic_binding_methods_supported": ["jwk"], + "display": [ + { + "name": "ePassportCredential", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF", + "logo": { + "url": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "alt_text": "UAegean" + } + } + ], + "credential_signing_alg_values_supported": ["ES256"], + "format": "vc+sd-jwt", + "vct": "VerifiableePassportCredentialSDJWT", + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": ["ES256"] + } + } + }, + + "VerifiableStudentIDSDJWT": { + "scope": "StudentID", + "claims": { + "identifier": { + "display": [ + { + "name": "Identifier", + "locale": "en-GB" + } + ] + }, + "schacPersonalUniqueCode": { + "display": [ + { + "name": "SCHAC Personal Unique Code", + "locale": "en-GB" + } + ] + }, + "schacPersonalUniqueID": { + "display": [ + { + "name": "SCHAC Personal Unique ID", + "locale": "en-GB" + } + ] + }, + "schacHomeOrganization": { + "display": [ + { + "name": "SCHAC Home Organization", + "locale": "en-GB" + } + ] + }, + "familyName": { + "display": [ + { + "name": "Family Name", + "locale": "en-GB" + } + ] + }, + "firstName": { + "display": [ + { + "name": "First Name", + "locale": "en-GB" + } + ] + }, + "displayName": { + "display": [ + { + "name": "Display Name", + "locale": "en-GB" + } + ] + }, + "dateOfBirth": { + "display": [ + { + "name": "Date of Birth", + "locale": "en-GB" + } + ] + }, + "commonName": { + "display": [ + { + "name": "Common Name", + "locale": "en-GB" + } + ] + }, + "mail": { + "display": [ + { + "name": "Email", + "locale": "en-GB" + } + ] + }, + "eduPersonPrincipalName": { + "display": [ + { + "name": "eduPerson Principal Name", + "locale": "en-GB" + } + ] + }, + "eduPersonPrimaryAffiliation": { + "display": [ + { + "name": "eduPerson Primary Affiliation", + "locale": "en-GB" + } + ] + }, + "eduPersonAffiliation": { + "display": [ + { + "name": "eduPerson Affiliation", + "locale": "en-GB" + } + ] + }, + "eduPersonScopedAffiliation": { + "display": [ + { + "name": "eduPerson Scoped Affiliation", + "locale": "en-GB" + } + ] + }, + "eduPersonAssurance": { + "display": [ + { + "name": "eduPerson Assurance", + "locale": "en-GB" + } + ] + } + }, + "cryptographic_binding_methods_supported": ["jwk"], + "display": [ + { + "name": "StudentID", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF", + "logo": { + "url": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "alt_text": "UAegean" + } + } + ], + "credential_signing_alg_values_supported": ["ES256"], + "format": "vc+sd-jwt", + "vct": "VerifiableStudentIDSDJWT", + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": ["ES256"] + } + } + }, + + "VerifiableFerryBoardingPassCredentialSDJWT": { + "scope": "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" + } + ] + } + }, + "cryptographic_binding_methods_supported": ["jwk"], + "display": [ + { + "name": "Ferry Boarding Pass", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF", + "logo": { + "url": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "alt_text": "UAegean" + } + } + ], + "credential_signing_alg_values_supported": ["ES256"], + "format": "vc+sd-jwt", + "vct": "VerifiableFerryBoardingPassCredentialSDJWT", + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": ["ES256"] + } + } } } } diff --git a/package.json b/package.json index ba395af..3ae1630 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://159c-2a02-587-8710-c200-f16f-3054-7426-ff3a.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://9a42-2a02-587-8710-c200-f16f-3054-7426-ff3a.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 7630635..42b543f 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -35,6 +35,19 @@ import qr from "qr-image"; import imageDataURI from "image-data-uri"; import { streamToBuffer } from "@jorgeferrero/stream-to-buffer"; import { request } from "http"; +import { + createPIDPayload, + createStudentIDPayload, + createAllianceIDPayload, + createFerryBoardingPassPayload, + getPIDSDJWTData, + getStudentIDSDJWTData, + getAllianceIDSDJWTData, + getFerryBoardingPassSDJWTData, + getGenericSDJWTData, + getEPassportSDJWTData, + createEPassportPayload, +} from "../utils/credPayloadUtil.js"; const router = express.Router(); @@ -54,14 +67,18 @@ console.log(privateKey); /* Generates a VCI request with pre-authorised flow with a transaction code */ -router.get(["/offer-tx-code"], async (req, res) => { +router.get(["/offer-tx-code/:type"], async (req, res) => { const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); + const credentialType = req.params.type + ? req.params.type + : "VerifiablePortableDocumentA2SDJWT"; + 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-tx-code/${uuid}`; //OfferUUID + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-tx-code/${uuid}?type=${credentialType}`; //OfferUUID let code = qr.image(credentialOffer, { type: "png", ec_level: "H", @@ -81,9 +98,13 @@ router.get(["/offer-tx-code"], async (req, res) => { * pre-authorised flow with a transaction code, credential offer */ router.get(["/credential-offer-tx-code/:id"], (req, res) => { + const credentialType = req.query.type + ? req.query.type + : "VerifiablePortableDocumentA2SDJWT"; + console.log(credentialType); res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA2SDJWT"], + credential_configuration_ids: [credentialType], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, @@ -101,14 +122,17 @@ router.get(["/credential-offer-tx-code/:id"], (req, res) => { /** * pre-authorised flow without a transaction code request */ -router.get(["/offer-no-code"], async (req, res) => { +router.get(["/offer-no-code/:type"], async (req, res) => { const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); + const credentialType = req.params.type + ? req.params.type + : "VerifiablePortableDocumentA2SDJWT"; 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-no-code/${uuid}`; //OfferUUID + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-no-code/${uuid}?type=${credentialType}`; //OfferUUID let code = qr.image(credentialOffer, { type: "png", ec_level: "H", @@ -128,9 +152,12 @@ router.get(["/offer-no-code"], async (req, res) => { * pre-authorised flow no transaction code request endpoint */ router.get(["/credential-offer-no-code/:id"], (req, res) => { + const credentialType = req.query.type + ? req.query.type + : "VerifiablePortableDocumentA2SDJWT"; res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA2SDJWT"], + credential_configuration_ids: [credentialType], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { "pre-authorized_code": req.params.id, @@ -245,550 +272,105 @@ router.post("/token_endpoint", async (req, res) => { // ***************************************************************** router.post("/credential", async (req, res) => { - // console.log("7 ROUTE /credential CALLED!!!!!!"); const authHeader = req.headers["authorization"]; const token = authHeader && authHeader.split(" ")[1]; // Split "Bearer" and the token - // Accessing the body data const requestBody = req.body; const format = requestBody.format; - let requestedCredentials = requestBody.credential_definition + const requestedCredentials = requestBody.credential_definition ? requestBody.credential_definition.type - : null; //removed requestBody.types to conform to RFC001 - //TODO valiate bearer header - let decodedWithHeader; - let decodedHeaderSubjectDID; - let vct = requestBody.vct - if (requestBody.proof && requestBody.proof.jwt) { - // console.log(requestBody.proof.jwt) - decodedWithHeader = jwt.decode(requestBody.proof.jwt, { complete: true }); - let holderJWKS = decodedWithHeader.header.jwk; - // console.log(decodedWithHeader.payload.iss); - decodedHeaderSubjectDID = decodedWithHeader.payload.iss; - - // console.log(credential); - if (format === "jwt_vc_json") { - let payload = {}; - if (requestedCredentials != null && requestedCredentials[0] === "PID") { - //get persona if existing from accessToken - const preSessions = getPreCodeSessions(); - let persona = getPersonaFromAccessToken( - token, - preSessions.personas, - preSessions.accessTokens - ); - - // Get the persona-specific credentialSubject - const credentialSubject = getCredentialSubjectForPersona( - persona, - decodedHeaderSubjectDID - ); + : null; + const decodedHeaderSubjectDID = + requestBody.proof && requestBody.proof.jwt + ? jwt.decode(requestBody.proof.jwt, { complete: true }).payload.iss + : null; - payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 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: credentialSubject, - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 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"], - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://europa.eu/2018/credentials/eudi/pid/v1", - ], - issuer: serverURL, - validFrom: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - }, - // 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"], - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schemas.prod.digitalcredentials.iata.org/contexts/iata_credential.jsonld", - ], - issuer: serverURL, - validFrom: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - }, - }; - } else { - if ( - (requestedCredentials != null && - requestedCredentials[0] === "EducationalID") || - requestedCredentials[0] === "StudentID" - ) { - const preSessions = getPreCodeSessions(); - let persona = getPersonaFromAccessToken( - token, - preSessions.personas, - preSessions.accessTokens - ); - - payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - iat: Math.floor(Date.now() / 1000), // Token issued at time - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Token expiration time (1 hour from now) - jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", - vc: { - type: ["StudentID"], //["EducationalID"], - "@context": ["https://www.w3.org/2018/credentials/v1"], - issuer: serverURL, - credentialSubject: { - id: decodedHeaderSubjectDID || "", - identifier: "john.doe@university.edu", - schacPersonalUniqueCode: [ - "urn:schac:personalUniqueCode:int:esi:university.edu:12345", - ], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Doe", - firstName: "John", - displayName: "John Doe", - dateOfBirth: "1990-01-01", - commonName: "Johnathan Doe", - mail: "john.doe@university.edu", - eduPersonPrincipalName: "john.doe@university.edu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@university.edu"], - eduPersonAssurance: [ - "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", - ], - }, - issuanceDate: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 - ).toISOString(), - validFrom: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - }, - }; - - if (persona === "1") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", - identifier: "mario.conti@ewc.eu", - schacPersonalUniqueCode: [ - "urn:schac:personalUniqueCode:int:esi:university.edu:12345", - ], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Conti", - firstName: "Mario", - displayName: "Mario Conti", - dateOfBirth: "1990-01-01", - commonName: "Mario Contri", - mail: "mario.conti@ewc.eu", - eduPersonPrincipalName: "mario.conti@ewc.eu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@ewc.eu"], - eduPersonAssurance: [ - "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", - ], - }; - } else if (persona === "2") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", - identifier: "hannah@ewc.eu", - schacPersonalUniqueCode: [ - "urn:schac:personalUniqueCode:int:esi:university.edu:12345", - ], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Matkalainen", - firstName: "Hannah", - displayName: "Hannah Matkalainen", - dateOfBirth: "1990-01-01", - commonName: "Hannah Matkalainen", - mail: "hannah@ewc.eu", - eduPersonPrincipalName: "hannah@ewc.eu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@ewc.eu"], - eduPersonAssurance: [ - "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", - ], - }; - } else if (persona === "3") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", - identifier: "felix@ewc.eu", - schacPersonalUniqueCode: [ - "urn:schac:personalUniqueCode:int:esi:university.edu:12345", - ], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Fischer", - firstName: "Felix", - displayName: "Felix Fischer", - dateOfBirth: "1990-01-01", - commonName: "Felix Fischer", - mail: "felix@ewc.eu", - eduPersonPrincipalName: "felix@ewc.eu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@ewc.eu"], - eduPersonAssurance: [ - "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", - ], - }; - } - } else { - if ( - requestedCredentials != null && - requestedCredentials[0] === "allianceIDCredential" - ) { - payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - iat: Math.floor(Date.now() / 1000), // Token issued at time - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Token expiration time (1 hour from now) - jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", - vc: { - type: ["allianceIDCredential"], - "@context": ["https://www.w3.org/2018/credentials/v1"], - issuer: serverURL, - credentialSubject: { - id: decodedHeaderSubjectDID, // Replace with the actual subject DID - identifier: { - schemeID: "European Student Identifier", - value: - "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", - id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", - }, - }, - issuanceDate: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 - ).toISOString(), - validFrom: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - }, - }; - } else if ( - requestedCredentials != null && - requestedCredentials[0] === "ferryBoardingPassCredential" - ) { - const preSessions = getPreCodeSessions(); - let persona = getPersonaFromAccessToken( - token, - preSessions.personas, - preSessions.accessTokens - ); - - payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - iat: Math.floor(Date.now() / 1000), // Token issued at time - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 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, - issuanceDate: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - expirationDate: new Date( - (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 - ).toISOString(), - validFrom: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - 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", - }, - }, - }; - - if (persona === "1") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID - identifier: "Mario Conti", - ticketQR: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", - ticketNumber: "3022", - ticketLet: "Y", - lastName: "Conti", - firstName: "Mario", - seatType: "Economy", - seatNumber: "12A", - departureDate: "2024-08-17", - departureTime: "13:07:34", - arrivalDate: "2024-08-17", - arrivalTime: "15:30:00", - arrivalPort: "MYKONOS TEST", - vesselDescription: "MYKONOS TEST", - }; - } else if (persona === "2") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID - identifier: "Hannah Matkalainen", - ticketQR: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", - ticketNumber: "3022", - ticketLet: "Y", - lastName: "Matkalainen", - firstName: "Hannah", - seatType: "Economy", - seatNumber: "12A", - departureDate: "2024-08-17", - departureTime: "13:07:34", - arrivalDate: "2024-08-17", - arrivalTime: "15:30:00", - arrivalPort: "MYKONOS TEST", - vesselDescription: "MYKONOS TEST", - }; - } else if (persona === "3") { - payload.vc.credentialSubject = { - id: decodedHeaderSubjectDID || "", // Replace with the actual subject DID - identifier: "Felix Fischer", - ticketQR: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHkAAAB5AQAAAAA+SX7VAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABlUlEQVRIDe2YMUtCQRCFZxQVhQo2hdwV2NhZ+ggLFzcBrcNLYktjb2AiBBkDEtFiFE3d7WVo+BR+BX+gtEop3Rjb7ndmxmh8HTIz5TdvzZk5MhGoUZ1Hnz1swr3/ehyHt6nuKPexG5XQPeDqcgrBXT3wGso+ULuLoDr/owxPI27WqZLPKpFvH2FqESkBu8WqwCrCL3r6Ne3Slo6I0A9H/UUXH5KoJwUbPFLU5CJqsZubAiZwVLja4YpAyyKcijulVDrwjeMaLs5CmlgHds1GZc9K8T/AXTDwLB0yzhFb2CHavBtL68YRuBN3le6HB54Rz6MPGoNx7N2e3ws+b9YL2scQY8jI9iA9gN0FbgeEQLzUXwl90kpAmNyDe4SjH5itwbPfNRi7w0Ogl8KiB1QWUDZc0h34sFjDwrIs+w6GCSnNhWbzP9FAvVd8BzCgbChAkd4VnLQZX9VaQd9gM0b9D/UZoTQAAAABJRU5ErkJggg==", - ticketNumber: "3022", - ticketLet: "Y", - lastName: "Fischer", - firstName: "Felix", - seatType: "Economy", - seatNumber: "12A", - departureDate: "2024-08-17", - departureTime: "13:07:34", - arrivalDate: "2024-08-17", - arrivalTime: "15:30:00", - arrivalPort: "MYKONOS TEST", - vesselDescription: "MYKONOS TEST", - }; - } - } else { - - let credType = requestedCredentials[0] //VerifiablePortableDocumentA2 or VerifiablePortableDocumentA1 - //sign as jwt - payload = { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 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 * 24 * 30) * 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: [credType], - validFrom: new Date( - Math.floor(Date.now() / 1000) * 1000 - ).toISOString(), - }, - // Optional claims - }; - } - } - } - } + if (!requestBody.proof || !requestBody.proof.jwt) { + console.log("NO keybinding info found!!!"); + return res.status(400).json({ error: "No proof information found" }); + } - const signOptions = { - algorithm: "ES256", // Specify the signing algorithm - }; - - // Define additional JWT header fields - const additionalHeaders = { - kid: "aegean#authentication-key", - typ: "JWT", - }; - // Sign the token - const idtoken = jwt.sign(payload, privateKey, { - ...signOptions, - header: additionalHeaders, // Include additional headers separately + let payload = {}; + + if (format === "jwt_vc_json") { + if (requestedCredentials && requestedCredentials[0] === "PID") { + payload = createPIDPayload(token, serverURL, decodedHeaderSubjectDID); + } else if ( + requestedCredentials && + requestedCredentials[0] === "ePassportCredential" + ) { + payload = createEPassportPayload(serverURL, decodedHeaderSubjectDID); + } else if ( + requestedCredentials && + requestedCredentials[0] === "StudentID" + ) { + payload = createStudentIDPayload(serverURL, decodedHeaderSubjectDID); + } else if ( + requestedCredentials && + requestedCredentials[0] === "ferryBoardingPassCredential" + ) { + payload = createEPassportPayload(serverURL, decodedHeaderSubjectDID); + } + // Handle other credentials similarly... + } else if (format === "vc+sd-jwt") { + let vct = requestBody.vct; + let decodedHeaderSubjectDID; + if (requestBody.proof && requestBody.proof.jwt) { + // console.log(requestBody.proof.jwt) + let decodedWithHeader = jwt.decode(requestBody.proof.jwt, { + complete: true, + }); + let holderJWKS = decodedWithHeader.header; + // console.log("Token:", token); + // console.log("Request Body:", requestBody); + let credType = vct; // VerifiablePortableDocumentA1SDJWT or VerifiablePortableDocumentA2SDJWT + const { signer, verifier } = await createSignerVerifier( + pemToJWK(privateKey, "private"), + pemToJWK(publicKeyPem, "public") + ); + const sdjwt = new SDJwtVcInstance({ + signer, + verifier, + signAlg: "ES256", + hasher: digest, + hashAlg: "SHA-256", + saltGenerator: generateSalt, }); - // console.log(idtoken); + // const claims = { + // given_name: "John", + // last_name: "Doe", + // }; + + // const disclosureFrame = { + // _sd: ["given_name", "last_name"], + // }; + let credPayload = {}; + try { + + if (credType === "VerifiablePIDSDJWT") { + credPayload = getPIDSDJWTData(decodedHeaderSubjectDID); + } else if (credType === "VerifiableePassportCredentialSDJWT") { + credPayload = getEPassportSDJWTData(decodedHeaderSubjectDID); + } else if (credType === "VerifiableStudentIDSDJWT") { + credPayload = getStudentIDSDJWTData(decodedHeaderSubjectDID); + } else if (credType === "ferryBoardingPassCredential") { + credPayload = VerifiableFerryBoardingPassCredentialSDJWT(decodedHeaderSubjectDID); + } - /* jwt format */ - res.json({ - format: "jwt_vc_json", - credential: idtoken, - c_nonce: generateNonce(), - c_nonce_expires_in: 86400, - }); - } else { - if (format === "vc+sd-jwt") { - // console.log("Token:", token); - // console.log("Request Body:", requestBody); - let credType = vct // VerifiablePortableDocumentA1SDJWT or VerifiablePortableDocumentA2SDJWT - const { signer, verifier } = await createSignerVerifier( - pemToJWK(privateKey, "private"), - pemToJWK(publicKeyPem, "public") - ); - const sdjwt = new SDJwtVcInstance({ - signer, - verifier, - signAlg: "ES256", - hasher: digest, - hashAlg: "SHA-256", - saltGenerator: generateSalt, - }); - const claims = { - given_name: "John", - last_name: "Doe", - }; const cnf = { jwk: holderJWKS }; + console.log(credType); + console.log(credPayload.claims); + console.log(credPayload.disclosureFrame); - const disclosureFrame = { - _sd: ["given_name", "last_name"], - }; const credential = await sdjwt.issue( { iss: serverURL, iat: Math.floor(Date.now() / 1000), vct: credType, - ...claims, + ...credPayload.claims, cnf: cnf, }, - disclosureFrame + credPayload.disclosureFrame ); res.json({ @@ -797,16 +379,38 @@ router.post("/credential", async (req, res) => { c_nonce: generateNonce(), c_nonce_expires_in: 86400, }); - } else { - console.log("UNSUPPORTED FORMAT!"); - console.log(format); + } catch (error) { + console.log(error); } + } else { + console.log( + "requestBody.proof && requestBody.proof.jwt not found", + requestBody + ); + return res.status(400).json({ error: "proof not found" }); } + + // } else { - console.log("NO keybinding info found!!!"); - console.log(requestBody.proof); + console.log("UNSUPPORTED FORMAT:", format); + return res.status(400).json({ error: "Unsupported format" }); } + + const signOptions = { algorithm: "ES256" }; + const additionalHeaders = { kid: "aegean#authentication-key", typ: "JWT" }; + const idtoken = jwt.sign(payload, privateKey, { + ...signOptions, + header: additionalHeaders, + }); + + res.json({ + format: "jwt_vc_json", + credential: idtoken, + c_nonce: generateNonce(), + c_nonce_expires_in: 86400, + }); }); + //issuerConfig.credential_endpoint = serverURL + "/credential"; //ITB diff --git a/utils/credPayloadUtil.js b/utils/credPayloadUtil.js new file mode 100644 index 0000000..6474905 --- /dev/null +++ b/utils/credPayloadUtil.js @@ -0,0 +1,448 @@ +// Helper functions to create payloads for different credential types +export const createPIDPayload = (token, serverURL, decodedHeaderSubjectDID) => { + const preSessions = getPreCodeSessions(); + const persona = getPersonaFromAccessToken( + token, + preSessions.personas, + preSessions.accessTokens + ); + const credentialSubject = getCredentialSubjectForPersona( + persona, + decodedHeaderSubjectDID + ); + + return { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // Token expiration time (30 days) + iat: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + credentialSubject: credentialSubject, + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 + ).toISOString(), + id: decodedHeaderSubjectDID, + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + issuer: serverURL, + type: ["PID"], + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://europa.eu/2018/credentials/eudi/pid/v1", + ], + validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + }, + }; +}; + +export const createEPassportPayload = (serverURL, decodedHeaderSubjectDID) => { + return { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 60 * 60, // Token expiration time (1 hour) + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + credentialSubject: { + id: decodedHeaderSubjectDID || "", + 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, + }, + }, + }, + }, + }, + type: ["ePassportCredential"], + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schemas.prod.digitalcredentials.iata.org/contexts/iata_credential.jsonld", + ], + issuer: serverURL, + validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + }, + }; +}; + + +export const createStudentIDPayload = (token, serverURL, decodedHeaderSubjectDID) => { + const preSessions = getPreCodeSessions(); + const persona = getPersonaFromAccessToken(token, preSessions.personas, preSessions.accessTokens); + + let credentialSubject = { + id: decodedHeaderSubjectDID || "", + identifier: "john.doe@university.edu", + schacPersonalUniqueCode: ["urn:schac:personalUniqueCode:int:esi:university.edu:12345"], + schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", + schacHomeOrganization: "university.edu", + familyName: "Doe", + firstName: "John", + displayName: "John Doe", + dateOfBirth: "1990-01-01", + commonName: "Johnathan Doe", + mail: "john.doe@university.edu", + eduPersonPrincipalName: "john.doe@university.edu", + eduPersonPrimaryAffiliation: "student", + eduPersonAffiliation: ["member", "student"], + eduPersonScopedAffiliation: ["student@university.edu"], + eduPersonAssurance: ["https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0"] + }; + + // Handle different persona data if available + if (persona === "1") { + credentialSubject = { + ...credentialSubject, + identifier: "mario.conti@ewc.eu", + familyName: "Conti", + firstName: "Mario", + displayName: "Mario Conti", + commonName: "Mario Conti", + mail: "mario.conti@ewc.eu" + }; + } else if (persona === "2") { + credentialSubject = { + ...credentialSubject, + identifier: "hannah@ewc.eu", + familyName: "Matkalainen", + firstName: "Hannah", + displayName: "Hannah Matkalainen", + commonName: "Hannah Matkalainen", + mail: "hannah@ewc.eu" + }; + } else if (persona === "3") { + credentialSubject = { + ...credentialSubject, + identifier: "felix@ewc.eu", + familyName: "Fischer", + firstName: "Felix", + displayName: "Felix Fischer", + commonName: "Felix Fischer", + mail: "felix@ewc.eu" + }; + } + + return { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, + iat: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + type: ["StudentID"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + issuer: serverURL, + credentialSubject: credentialSubject, + issuanceDate: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + expirationDate: new Date((Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000).toISOString(), + validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString() + } + }; +}; + + +export const createAllianceIDPayload = (serverURL, decodedHeaderSubjectDID) => { + return { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, + iat: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + type: ["allianceIDCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + issuer: serverURL, + credentialSubject: { + id: decodedHeaderSubjectDID || "", + identifier: { + schemeID: "European Student Identifier", + value: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", + id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ" + } + }, + issuanceDate: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + expirationDate: new Date((Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000).toISOString(), + validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString() + } + }; + }; + + export const createFerryBoardingPassPayload = (token, serverURL, decodedHeaderSubjectDID) => { + const preSessions = getPreCodeSessions(); + const persona = getPersonaFromAccessToken(token, preSessions.personas, preSessions.accessTokens); + + let credentialSubject = { + id: decodedHeaderSubjectDID || "", + identifier: "John Doe", + ticketQR: "data:image/png;base64,someBase64EncodedQR", + 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" + }; + + if (persona === "1") { + credentialSubject = { + ...credentialSubject, + identifier: "Mario Conti", + lastName: "Conti", + firstName: "Mario", + ticketNumber: "3022", + arrivalPort: "Mykonos", + vesselDescription: "Ferry to Mykonos" + }; + } else if (persona === "2") { + credentialSubject = { + ...credentialSubject, + identifier: "Hannah Matkalainen", + lastName: "Matkalainen", + firstName: "Hannah", + ticketNumber: "3022", + arrivalPort: "Santorini", + vesselDescription: "Ferry to Santorini" + }; + } else if (persona === "3") { + credentialSubject = { + ...credentialSubject, + identifier: "Felix Fischer", + lastName: "Fischer", + firstName: "Felix", + ticketNumber: "3022", + arrivalPort: "Crete", + vesselDescription: "Ferry to Crete" + }; + } + + return { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, + iat: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + type: ["VerifiableCredential", "ferryBoardingPassCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + issuer: serverURL, + credentialSubject: credentialSubject, + issuanceDate: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + expirationDate: new Date((Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000).toISOString(), + validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString() + } + }; + }; + + +// SD-JWT HELPERS + + export const getPIDSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + given_name: "John", + family_name: "Doe", + birth_date: "1990-01-01", + age_over_18: true, + }; + + const disclosureFrame = { + _sd: ["id", "given_name", "family_name", "birth_date", "age_over_18"] + }; + + return { claims, disclosureFrame }; + }; + + + export const getStudentIDSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + identifier: "john.doe@university.edu", + schacPersonalUniqueCode: ["urn:schac:personalUniqueCode:int:esi:university.edu:12345"], + schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", + schacHomeOrganization: "university.edu", + familyName: "Doe", + firstName: "John", + displayName: "John Doe", + dateOfBirth: "1990-01-01", + commonName: "Johnathan Doe", + mail: "john.doe@university.edu", + eduPersonPrincipalName: "john.doe@university.edu", + eduPersonPrimaryAffiliation: "student", + eduPersonAffiliation: ["member", "student"], + eduPersonScopedAffiliation: ["student@university.edu"], + eduPersonAssurance: ["https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0"] + }; + + const disclosureFrame = { + _sd: [ + "id", "identifier", "schacPersonalUniqueCode", "schacPersonalUniqueID", "schacHomeOrganization", + "familyName", "firstName", "displayName", "dateOfBirth", "commonName", "mail", "eduPersonPrincipalName", + "eduPersonPrimaryAffiliation", "eduPersonAffiliation", "eduPersonScopedAffiliation", "eduPersonAssurance" + ] + }; + + return { claims, disclosureFrame }; + }; + +export const getAllianceIDSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + identifier: { + schemeID: "European Student Identifier", + value: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", + id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ" + } + }; + + const disclosureFrame = { + _sd: ["id", "identifier.schemeID", "identifier.value", "identifier.id"] + }; + + return { claims, disclosureFrame }; + }; + + + export const getFerryBoardingPassSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + identifier: "John Doe", + ticketQR: "data:image/png;base64,someBase64EncodedQR", + 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" + }; + + const disclosureFrame = { + _sd: [ + "id", "identifier", "ticketQR", "ticketNumber", "ticketLet", "lastName", "firstName", + "seatType", "seatNumber", "departureDate", "departureTime", "arrivalDate", "arrivalTime", + "arrivalPort", "vesselDescription" + ] + }; + + return { claims, disclosureFrame }; + }; + + export const getGenericSDJWTData = (decodedHeaderSubjectDID, credType) => { + const claims = { + id: decodedHeaderSubjectDID || "", + given_name: "John", + last_name: "Doe" + }; + + const disclosureFrame = { + _sd: ["id", "given_name", "last_name"] + }; + + return { claims, disclosureFrame }; + }; + + + export const getEPassportSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + 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" + } + } + }; + + const disclosureFrame = { + _sd: ["id", "electronicPassport.dataGroup1.birthdate", "electronicPassport.dataGroup1.docTypeCode", + "electronicPassport.dataGroup1.expiryDate", "electronicPassport.dataGroup1.genderCode", + "electronicPassport.dataGroup1.holdersName", "electronicPassport.dataGroup1.issuerCode", + "electronicPassport.dataGroup1.natlText", "electronicPassport.dataGroup1.passportNumberIdentifier"] + }; + + return { claims, disclosureFrame }; + }; + \ No newline at end of file From 54e321dc81184f962b67d17390a6b82a1bd483da Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Tue, 15 Oct 2024 15:52:23 +0300 Subject: [PATCH 16/37] refactor --- routes/preAuthSDjwRoutes.js | 48 ++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 42b543f..4757fb3 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -310,7 +310,20 @@ router.post("/credential", async (req, res) => { ) { payload = createEPassportPayload(serverURL, decodedHeaderSubjectDID); } - // Handle other credentials similarly... + + const signOptions = { algorithm: "ES256" }; + const additionalHeaders = { kid: "aegean#authentication-key", typ: "JWT" }; + const idtoken = jwt.sign(payload, privateKey, { + ...signOptions, + header: additionalHeaders, + }); + + res.json({ + format: "jwt_vc_json", + credential: idtoken, + c_nonce: generateNonce(), + c_nonce_expires_in: 86400, + }); } else if (format === "vc+sd-jwt") { let vct = requestBody.vct; let decodedHeaderSubjectDID; @@ -336,17 +349,8 @@ router.post("/credential", async (req, res) => { saltGenerator: generateSalt, }); - // const claims = { - // given_name: "John", - // last_name: "Doe", - // }; - - // const disclosureFrame = { - // _sd: ["given_name", "last_name"], - // }; let credPayload = {}; try { - if (credType === "VerifiablePIDSDJWT") { credPayload = getPIDSDJWTData(decodedHeaderSubjectDID); } else if (credType === "VerifiableePassportCredentialSDJWT") { @@ -354,13 +358,15 @@ router.post("/credential", async (req, res) => { } else if (credType === "VerifiableStudentIDSDJWT") { credPayload = getStudentIDSDJWTData(decodedHeaderSubjectDID); } else if (credType === "ferryBoardingPassCredential") { - credPayload = VerifiableFerryBoardingPassCredentialSDJWT(decodedHeaderSubjectDID); + credPayload = VerifiableFerryBoardingPassCredentialSDJWT( + decodedHeaderSubjectDID + ); } const cnf = { jwk: holderJWKS }; - console.log(credType); - console.log(credPayload.claims); - console.log(credPayload.disclosureFrame); + // console.log(credType); + // console.log(credPayload.claims); + // console.log(credPayload.disclosureFrame); const credential = await sdjwt.issue( { @@ -395,20 +401,6 @@ router.post("/credential", async (req, res) => { console.log("UNSUPPORTED FORMAT:", format); return res.status(400).json({ error: "Unsupported format" }); } - - const signOptions = { algorithm: "ES256" }; - const additionalHeaders = { kid: "aegean#authentication-key", typ: "JWT" }; - const idtoken = jwt.sign(payload, privateKey, { - ...signOptions, - header: additionalHeaders, - }); - - res.json({ - format: "jwt_vc_json", - credential: idtoken, - c_nonce: generateNonce(), - c_nonce_expires_in: 86400, - }); }); //issuerConfig.credential_endpoint = serverURL + "/credential"; From ce8d800213c7ad33e8dd4460473c0b18d51d1e64 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Tue, 15 Oct 2024 16:22:27 +0300 Subject: [PATCH 17/37] minor changes for ITB --- routes/preAuthSDjwRoutes.js | 15 ++++++++------- utils/credPayloadUtil.js | 4 ++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 4757fb3..13f16e8 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -67,10 +67,10 @@ console.log(privateKey); /* Generates a VCI request with pre-authorised flow with a transaction code */ -router.get(["/offer-tx-code/:type"], async (req, res) => { +router.get(["/offer-tx-code"], async (req, res) => { const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); - const credentialType = req.params.type - ? req.params.type + const credentialType = req.query.credentialType + ? req.query.credentialType : "VerifiablePortableDocumentA2SDJWT"; const preSessions = getPreCodeSessions(); @@ -122,11 +122,12 @@ router.get(["/credential-offer-tx-code/:id"], (req, res) => { /** * pre-authorised flow without a transaction code request */ -router.get(["/offer-no-code/:type"], async (req, res) => { +router.get(["/offer-no-code"], async (req, res) => { const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); - const credentialType = req.params.type - ? req.params.type - : "VerifiablePortableDocumentA2SDJWT"; + const credentialType = req.query.credentialType + ? req.query.credentialType + : "VerifiablePortableDocumentA2SDJWT"; + const preSessions = getPreCodeSessions(); if (preSessions.sessions.indexOf(uuid) < 0) { preSessions.sessions.push(uuid); diff --git a/utils/credPayloadUtil.js b/utils/credPayloadUtil.js index 6474905..07dc64d 100644 --- a/utils/credPayloadUtil.js +++ b/utils/credPayloadUtil.js @@ -1,4 +1,8 @@ +import {getCredentialSubjectForPersona} from "./personasUtils.js" + // Helper functions to create payloads for different credential types + + export const createPIDPayload = (token, serverURL, decodedHeaderSubjectDID) => { const preSessions = getPreCodeSessions(); const persona = getPersonaFromAccessToken( From 3168b43b51433321525f6eeda82dd1dee9c55d68 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Wed, 16 Oct 2024 10:18:01 +0300 Subject: [PATCH 18/37] x509 --- clientIdKeys/client_cert.cnf | 21 +++++++++++++++++ clientIdKeys/client_private.key | 28 ++++++++++++++++++++++ utils/cryptoUtils.js | 16 +++++++++++++ utils/tokenUtils.js | 41 ++++++++++++++++++++++++++++----- 4 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 clientIdKeys/client_cert.cnf create mode 100644 clientIdKeys/client_private.key diff --git a/clientIdKeys/client_cert.cnf b/clientIdKeys/client_cert.cnf new file mode 100644 index 0000000..fd77141 --- /dev/null +++ b/clientIdKeys/client_cert.cnf @@ -0,0 +1,21 @@ +[ req ] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_ext + +[ dn ] +C = EU +ST = GREECE +L = GREECE +O = UAegean +OU = IT Department +CN = dss.aegean.gr + +[ req_ext ] +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = dss.aegean.gr +DNS.2 = www.dss.aegean.gr diff --git a/clientIdKeys/client_private.key b/clientIdKeys/client_private.key new file mode 100644 index 0000000..89442f0 --- /dev/null +++ b/clientIdKeys/client_private.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDxisIDLK+rq9GB +9JyJrq+z1cGg9ANR6uNwy/JdQQuc8Le3zK1y0A+pGkQS4skDT4OOflUh5J4R21eA +KQe8OP2lA2qqbkk9g8axvVTIAo10U5rMpgr+3TA/ykHs9M6y3rodiOvAz8RPJvqF +V5u90rM1gpuqbxUj+hlC0HCrEaJiPvvb0O8HC+yr6Yoe0g9uFkmeSCoTecgz0/EL +p4NqNvaXA9Cqze6j645mKon0fTxfM0AY7bAlQG5Yy1qNJMFHnixsS4zxFI/m9//7 +huKnpGC1BZZHibxOe9jVvuaXS0hpRCYJDCgvrFCeaQn3/+PHGjFCF3DJAp2mWh+N +t1SBAHclAgMBAAECggEABLiinC9Ga8Oa/8BJQwOwGYLHGx9J5VMDCRRjc2EvGe02 +mhgdF85rT/HcfMYECdWHC8T6YwQ19sFjz3R92zb99ji/3/7x3Yry2kyKGLK8ExcD +QpbIRj0KkWShr0EFvUdPOf6QJR+qhaWMZgjVWXuB1zzrUASF1EMyNTUfzqg0lVj+ +GGl7RYMgfk0FMPUDJ1xx5CDFzJxlOOwovYM1UR0skav9EVU+v96CemUM29WR01Vk +zyuzzForhi/J7CaLOYZHgUJN9qr1lo9YHn1N0kAbz6sNIoGc5uquLlddg6VvAAKz +XUKm4HjxSqCENdCe1K2h8cBvHdwNlWLgH9kK1MQ2QQKBgQD8W5H87X06NKGJIIg6 +zk24VVTbz0Hs8718UgL2YqzXl32LbQIgFgBewVa3HSH9pA+UJ1Fyn3uOoMx3+TXL +5fMCP2fcPqvDtb2Lw4qpVTn1/MDTBb3REfZFAWFPQ0AT2JaDxEKmYHnzpQzao/Bq +u7is6ToeODz1SwfUKCp/N3MulQKBgQD1Bzmc97RERDfrA9flvHD8SWrTr37Vl1ff +TryQ9kE9zg03lnbbUlA82keusUzsnVGbcZuloVqU5+VIgqdW5EQKF755i6eygi26 +Zao5z4guaVuVlMg9d1v9iunpD+hNqB6rq03GuwxwpRB6APWQXdLnZJRn49+MfZpW +Cy/4TFdSUQKBgHROy+kTl9zEtxKZUNAxlzZ29aZdTpgj+lgbAEyIS/sNUgp5kjox +ibgiSZIZtjnNbHSu7mXyxSKGT9aXoEi6UZbenEKxC78muxZ4aCcFJbKO7FwV0FdU +fPlu2meKqjS7ZRuTz9gYrPwzPMpkNoEqxcHDuEqHRukSzKfmXzfE2axFAoGAQhkL +2qMbdtrsNrKXt/qIhBYOAkKyNU4ZPz0PMw+q89A00oz9TRa8XF4Jvj9H4ABkNkmE +Uj7fn5Ga0V+lYpQ7InnbQRCCUW46hbDNJcEtHFABi61SAvrCzvd9OAi5gtFaTLbu +STDGH1D3y+GcXLBip1cDja7z7AMp8hIcg5+cYrECgYEA1N5XiP+LRCDVJbpkStjO +ZlYhmzbtxaBj/68sXQwuIyR1qce48z5RQhohUCqmtro69H4FB+22yc+ApOhrLszK +sgb8MRYdj00M2cv0KYUSfQ5HfrzDWyMw6Offzi3xbqaiOsN1XtpF2JzzP/XaWOOK +QwDvAMlNNf6PmgSBLZpw3EQ= +-----END PRIVATE KEY----- diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index e9e9b8b..4a9f6e4 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -101,6 +101,10 @@ export function buildVpRequestJSON( return jwtPayload; } + + + + export function buildVpRequestJWT( client_id, response_uri, @@ -143,6 +147,18 @@ export function buildVpRequestJWT( } } + + + + + + + + + + + + export async function decryptJWE(jweToken, privateKeyPEM) { try { const privateKey = crypto.createPrivateKey(privateKeyPEM); diff --git a/utils/tokenUtils.js b/utils/tokenUtils.js index 1e9cef4..9d175f3 100644 --- a/utils/tokenUtils.js +++ b/utils/tokenUtils.js @@ -1,6 +1,7 @@ import fs from "fs"; import jwt from "jsonwebtoken"; import crypto from "crypto"; +import { error } from "console"; export function buildAccessToken(issuerURL, privateKey) { const payload = { @@ -14,7 +15,7 @@ export function buildAccessToken(issuerURL, privateKey) { // Sign the JWT const token = jwt.sign(payload, privateKey, { algorithm: "ES256" }); -// console.log(token); + // console.log(token); return token; } @@ -41,11 +42,10 @@ export function buildIdToken(issuerURL, privateKey) { // You can add a "kid" (key ID) here if your private key has one }); -// console.log("Generated ID Token:", idToken); + // console.log("Generated ID Token:", idToken); return idToken; } - export function buildVPbyValue( client_id, presentation_definition_uri, @@ -61,8 +61,8 @@ export function buildVPbyValue( "openid4vp://?client_id=" + encodeURIComponent(client_id) + "&response_type=vp_token" + - "&response_mode=direct_post"+ - "&response_uri=" + + "&response_mode=direct_post" + + "&response_uri=" + encodeURIComponent(redirect_uri) + "&presentation_definition_uri=" + encodeURIComponent(presentation_definition_uri) + @@ -73,4 +73,33 @@ export function buildVPbyValue( "&nonce=n0S6_WzA2Mj" + "&state=af0ifjsldkj"; return result; -} \ No newline at end of file +} + +export function buildVPbyReference( + client_id, + presentation_definition_uri, + client_id_scheme = "redirect_uri", + client_metadata_uri, + redirect_uri +) { + if (client_id_scheme == "redirect_uri") { + throw error("redirect_uri is not supportted for VP by reference"); + } else { + let result = + "openid4vp://?client_id=" + + encodeURIComponent(client_id) + + "&response_type=vp_token" + + "&response_mode=direct_post" + + "&response_uri=" + + encodeURIComponent(redirect_uri) + + "&presentation_definition_uri=" + + encodeURIComponent(presentation_definition_uri) + + "&client_id_scheme=" + + client_id_scheme + + "&client_metadata_uri=" + + encodeURIComponent(client_metadata_uri) + + "&nonce=n0S6_WzA2Mj" + + "&state=af0ifjsldkj"; + return result; + } +} From 5c374f17ac2eda65aa49d7be630ee734091494d2 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Wed, 16 Oct 2024 11:27:36 +0300 Subject: [PATCH 19/37] corrected bug with client_id for dynamic vp request --- clientIdKeys/client_certificate.crt | 21 +++++++++++++++++++++ clientIdKeys/client_certificate.der | Bin 0 -> 877 bytes clientIdKeys/client_certificate_base64.txt | 1 + package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 9 +++++++-- 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 clientIdKeys/client_certificate.crt create mode 100644 clientIdKeys/client_certificate.der create mode 100644 clientIdKeys/client_certificate_base64.txt diff --git a/clientIdKeys/client_certificate.crt b/clientIdKeys/client_certificate.crt new file mode 100644 index 0000000..47fb843 --- /dev/null +++ b/clientIdKeys/client_certificate.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDaTCCAlECFBagdVsnDy9Af/Df4n3bvO3yxLWdMA0GCSqGSIb3DQEBCwUAMHEx +CzAJBgNVBAYTAkVVMQ8wDQYDVQQIDAZHUkVFQ0UxDzANBgNVBAcMBkdSRUVDRTEQ +MA4GA1UECgwHVUFlZ2VhbjEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEWMBQGA1UE +AwwNZHNzLmFlZ2Vhbi5ncjAeFw0yNDEwMTYwNzE4MzJaFw0yNTEwMTYwNzE4MzJa +MHExCzAJBgNVBAYTAkVVMQ8wDQYDVQQIDAZHUkVFQ0UxDzANBgNVBAcMBkdSRUVD +RTEQMA4GA1UECgwHVUFlZ2VhbjEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEWMBQG +A1UEAwwNZHNzLmFlZ2Vhbi5ncjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAPGKwgMsr6ur0YH0nImur7PVwaD0A1Hq43DL8l1BC5zwt7fMrXLQD6kaRBLi +yQNPg45+VSHknhHbV4ApB7w4/aUDaqpuST2DxrG9VMgCjXRTmsymCv7dMD/KQez0 +zrLeuh2I68DPxE8m+oVXm73SszWCm6pvFSP6GULQcKsRomI++9vQ7wcL7Kvpih7S +D24WSZ5IKhN5yDPT8Qung2o29pcD0KrN7qPrjmYqifR9PF8zQBjtsCVAbljLWo0k +wUeeLGxLjPEUj+b3//uG4qekYLUFlkeJvE572NW+5pdLSGlEJgkMKC+sUJ5pCff/ +48caMUIXcMkCnaZaH423VIEAdyUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAB8I/ +rM5nScy+CXf9s2VC5QFPy4bw6DDyih4JZj2VmO2PbZToOhtZ8FwE2glIikoqVQ5n +sY7B7mij5dW6zgnnqYJ1Lquw8M5xLo78xMDjatm52I3mvxvuLS4D7hiJE1ej0QAa +cCM2fkwuWGwTAyZlhWx69znccazxliMfjz3LqdyFjZELgmOnzoO1HwOI+2NrDbI4 +D1UJOongotoHn8jKkqrKpYDiccBp7z2vuD4TlgyDFA28r1LlzOlLMGaXHcGZ0Ijd +8ODEP1U4nWjdVa0FHQHiRFPIRHSqCHqwS/YYM7uD4f1My0whCmRGrbsNmLYRlF2D +HrMhh1RzYDRnGi58Lg== +-----END CERTIFICATE----- diff --git a/clientIdKeys/client_certificate.der b/clientIdKeys/client_certificate.der new file mode 100644 index 0000000000000000000000000000000000000000..83ca6d7e9fbfdfd43115b65a94855da953c2aaf1 GIT binary patch literal 877 zcmXqLV$L*ZVhUsu5nE6itd#AN85K^Mn-N{27^LF zZUas>=1>+kVJ6p5Lw=wg);=V229`81R8qaPhE*I;N(lCgvH68Hj*H zxOsRzLlj(63lfV;a#Qn4VB*X?yeY-SdWm4gdg(<5a^k#3CWZ!vW(MYl7RE+VK&~kg zcOab9#HfTEFpR7W%uS5^3(+0+dT_xP=D=5v z3r>HEb>yD&Vf*$oYl|-Muat5TdUTT6zqzk2RPo6?!Q0^tn(TWl{w`(CT9xN%+k9-} z-jEYay(Pi3&Mf2lch|uFl;fK(=QiEjCEM}(!1*KoYQI{;XYakV*|cf)s(ex9Uy@E2 z3RVj)O0xTX`@(y6?l-GncFA4h&lB^U=bq zUutdQjU6Q3Zcuf|i#Q$Ct8&nNo=%Q;&qtB|XW#$-ZhN$RNy1jvY3`kS{HkwU-S=#| zw@0Rn8Yhp2{+fV!nVjGMKRzyH=ps!Ys`42;Mj1WY=>5MpFt zKV-k=T)O9(eVpZgH>Wy1W%NJY_Th!Wr!G0pG~1~&-uCBCd0{0T`5}ho7NFF)73Ri3%ycXBrGCJX*h zPOHubi*B*cKXGc(s#8lF9u*$Qd~dschn?^=o@No=J?n#>o_XnQkTzZR;LHmhcRxHh zVjpTTH{)*TT2@)cM=rr9TuN4PRBiD7CSkn0`Qcxm(>{t^DQ;_b^Ul~NI3>1OZnI*0 NNO6Kmx|Cjx9so)ZO*jAm literal 0 HcmV?d00001 diff --git a/clientIdKeys/client_certificate_base64.txt b/clientIdKeys/client_certificate_base64.txt new file mode 100644 index 0000000..3956d36 --- /dev/null +++ b/clientIdKeys/client_certificate_base64.txt @@ -0,0 +1 @@ +MIIDaTCCAlECFBagdVsnDy9Af/Df4n3bvO3yxLWdMA0GCSqGSIb3DQEBCwUAMHExCzAJBgNVBAYTAkVVMQ8wDQYDVQQIDAZHUkVFQ0UxDzANBgNVBAcMBkdSRUVDRTEQMA4GA1UECgwHVUFlZ2VhbjEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEWMBQGA1UEAwwNZHNzLmFlZ2Vhbi5ncjAeFw0yNDEwMTYwNzE4MzJaFw0yNTEwMTYwNzE4MzJaMHExCzAJBgNVBAYTAkVVMQ8wDQYDVQQIDAZHUkVFQ0UxDzANBgNVBAcMBkdSRUVDRTEQMA4GA1UECgwHVUFlZ2VhbjEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEWMBQGA1UEAwwNZHNzLmFlZ2Vhbi5ncjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPGKwgMsr6ur0YH0nImur7PVwaD0A1Hq43DL8l1BC5zwt7fMrXLQD6kaRBLiyQNPg45+VSHknhHbV4ApB7w4/aUDaqpuST2DxrG9VMgCjXRTmsymCv7dMD/KQez0zrLeuh2I68DPxE8m+oVXm73SszWCm6pvFSP6GULQcKsRomI++9vQ7wcL7Kvpih7SD24WSZ5IKhN5yDPT8Qung2o29pcD0KrN7qPrjmYqifR9PF8zQBjtsCVAbljLWo0kwUeeLGxLjPEUj+b3//uG4qekYLUFlkeJvE572NW+5pdLSGlEJgkMKC+sUJ5pCff/48caMUIXcMkCnaZaH423VIEAdyUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAB8I/rM5nScy+CXf9s2VC5QFPy4bw6DDyih4JZj2VmO2PbZToOhtZ8FwE2glIikoqVQ5nsY7B7mij5dW6zgnnqYJ1Lquw8M5xLo78xMDjatm52I3mvxvuLS4D7hiJE1ej0QAacCM2fkwuWGwTAyZlhWx69znccazxliMfjz3LqdyFjZELgmOnzoO1HwOI+2NrDbI4D1UJOongotoHn8jKkqrKpYDiccBp7z2vuD4TlgyDFA28r1LlzOlLMGaXHcGZ0Ijd8ODEP1U4nWjdVa0FHQHiRFPIRHSqCHqwS/YYM7uD4f1My0whCmRGrbsNmLYRlF2DHrMhh1RzYDRnGi58Lg== \ No newline at end of file diff --git a/package.json b/package.json index 3ae1630..59a1b26 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://9a42-2a02-587-8710-c200-f16f-3054-7426-ff3a.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://f5ac-2a02-587-8710-c200-a156-e1c4-5eed-c02b.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index c9d7409..b887e7f 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -294,11 +294,16 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { console.log(redirect_uri + " but i will request the vp based on " + vpRedirectURI) // changed this to support auth request by value not reference //const redirectUrl = `${vpRedirectURI}?state=${state}&client_id=${client_id}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${serverURL}/request_uri_dynamic`; - const response_url = serverURL+"/request_uri_dynamic" + const response_uri = serverURL + "/direct_post_vci" + "/" + uuid; const presentation_definition_uri = serverURL + "/presentation-definition/itbsdjwt"; const client_metadata_uri = serverURL + "/client-metadata"; - const redirectUrl = buildVPbyValue(client_id,presentation_definition_uri,"redirect_uri",client_metadata_uri,response_url) + + //response_uri + //const redirectUrl = buildVPbyValue(client_id,presentation_definition_uri,"redirect_uri",client_metadata_uri,response_uri) + // client_id_scheme is set to redirect_uri in OIDC4VP v20, the client_id becomes the redirect_uri + const redirectUrl = buildVPbyValue(response_uri,presentation_definition_uri,"redirect_uri",client_metadata_uri,response_uri) + console.log("redirectUrl", redirectUrl) return res.redirect(302, redirectUrl); } }); From a03f0f4ab15af2ebf710f9fe93649e195c045efe Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Wed, 16 Oct 2024 12:55:58 +0300 Subject: [PATCH 20/37] updattes --- routes/verifierRoutes.js | 60 +++++++++- utils/cryptoUtils.js | 112 ++++++++++-------- utils/tokenUtils.js | 2 +- {clientIdKeys => x509}/client_cert.cnf | 0 {clientIdKeys => x509}/client_certificate.crt | 0 {clientIdKeys => x509}/client_certificate.der | Bin .../client_certificate_base64.txt | 0 {clientIdKeys => x509}/client_private.key | 0 x509/client_private_pkcs8.key | 28 +++++ 9 files changed, 149 insertions(+), 53 deletions(-) rename {clientIdKeys => x509}/client_cert.cnf (100%) rename {clientIdKeys => x509}/client_certificate.crt (100%) rename {clientIdKeys => x509}/client_certificate.der (100%) rename {clientIdKeys => x509}/client_certificate_base64.txt (100%) rename {clientIdKeys => x509}/client_private.key (100%) create mode 100644 x509/client_private_pkcs8.key diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index 08b7801..b8eeef1 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -9,7 +9,7 @@ import { buildVpRequestJWT, } from "../utils/cryptoUtils.js"; -import {buildVPbyValue} from "../utils/tokenUtils.js" +import { buildVPbyValue } from "../utils/tokenUtils.js"; import { decodeSdJwt, getClaims } from "@sd-jwt/decode"; import { digest } from "@sd-jwt/crypto-nodejs"; import qr from "qr-image"; @@ -75,6 +75,10 @@ let sessions = []; let sessionHistory = new TimedArray(30000); //cache data for 30sec let verificationResultsHistory = new TimedArray(30000); //cache data for 30sec +/* ******************************************************* + CLIENT_ID_SCHEME_REDIRECT_URI + +*********************************************************** */ verifierRouter.get("/generateVPRequest", async (req, res) => { const stateParam = req.query.id ? req.query.id : uuidv4(); const nonce = generateNonce(16); @@ -162,6 +166,58 @@ verifierRouter.get("/client-metadata", async (req, res) => { res.type("application/json").send(clientMetadata); }); +/* ******************************************************* + CLIENT_ID_SCHEME x509_dns_san +*********************************************************** */ +verifierRouter.get("/generateVPRequestx509", async (req, res) => { + const stateParam = req.query.id ? req.query.id : uuidv4(); + const nonce = generateNonce(16); + + const uuid = req.params.id ? req.params.id : uuidv4(); + const response_uri = serverURL + "/direct_post" + "/" + uuid; + + + const client_metadata = { + client_name: "UAegean EWC Verifier", + logo_uri: "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + location: "Greece", + cover_uri: "string", + description: "EWC pilot case verification", + }; + + const clientId = "dss.aegean.gr"; + sessions.push(uuid); + verificationSessions.push({ + uuid: uuid, + status: "pending", + claims: null, + }); + + const vpRequest = await buildVpRequestJWT( + clientId, + response_uri, + presentation_definition_sdJwt, + "", + "x509_san_dns", + client_metadata + ); + + let code = qr.image(vpRequest, { + type: "png", + ec_level: "M", + size: 20, + margin: 10, + }); + let mediaType = "PNG"; + let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); + res.json({ + qr: encodedQR, + deepLink: vpRequest, + sessionId: stateParam, + }); + +}); + verifierRouter.post("/direct_post/:id", async (req, res) => { console.log("direct_post VP is below!"); const sessionId = req.params.id; @@ -542,8 +598,6 @@ function buildVP( return result; } - - async function flattenCredentialsToClaims(credentials) { let claimsResult = {}; credentials.forEach((credentialJwt) => { diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index 4a9f6e4..c5b7b48 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -2,6 +2,8 @@ import crypto from "crypto"; import jwt from "jsonwebtoken"; import * as jose from "jose"; import base64url from "base64url"; +import { error } from "console"; +import fs from "fs"; export function pemToJWK(pem, keyType) { let key; @@ -101,64 +103,76 @@ export function buildVpRequestJSON( return jwtPayload; } - - - - -export function buildVpRequestJWT( +export async function buildVpRequestJWT( client_id, - response_uri, + redirect_uri, presentation_definition, - privateKey, + privateKey="", client_id_scheme = "redirect_uri", // Default to "redirect_uri" client_metadata = {} // Default to an empty object ) { - // Construct the JWT payload - let jwtPayload = { - response_type: "vp_token", - client_id: client_id, - client_id_scheme: client_id_scheme, - presentation_definition: presentation_definition, - redirect_uri: response_uri, - nonce: "n-0S6_WzA2Mj", - state: "af0ifjsldkj", - client_metadata: client_metadata, // - }; - - // Define the JWT header - const header = { - alg: "ES256", - kid: `aegean#authentication-key`, // Ensure this kid is resolvable from the did.json endpoint - }; - - // Conditional signing based on client_id_scheme - if (client_id_scheme !== "redirect_uri") { - // Sign the JWT as per the scheme's requirements - const token = jwt.sign(jwtPayload, privateKey, { - algorithm: "ES256", - noTimestamp: true, // Retain if necessary - header, - }); - return token; + if (client_id_scheme === "x509_san_dns") { + privateKey = fs.readFileSync("./x509/client_private_pkcs8.key", "utf8"); + const certificate = fs.readFileSync( + "./x509/client_certificate.crt", + "utf8" + ); + // Convert certificate to Base64 without headers + const certBase64 = certificate + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace(/\s+/g, ""); + + // Construct the JWT payload + let jwtPayload = { + response_type: "vp_token", + response_mode: "direct_post", + client_id: client_id, // this should match the dns record in the certificate (dss.aegean.gr) + client_id_scheme: client_id_scheme, + presentation_definition: presentation_definition, + redirect_uri: redirect_uri, + nonce: "n-0S6_WzA2Mj", + state: "af0ifjsldkj", + client_metadata: client_metadata, // + }; + + // Define the JWT header + // const header = { + // alg: "ES256", + // kid: `aegean#authentication-key`, // Ensure this kid is resolvable from the did.json endpoint + // }; + const header = { + alg: "RS256", + typ: "JWT", + x5c: [certBase64], + }; + + const jwt = await new jose.SignJWT(jwtPayload) + .setProtectedHeader(header) + .sign(await jose.importPKCS8(privateKey, "RS256")); + + return jwt + + // Conditional signing based on client_id_scheme + + //} + // else if (client_id_scheme !== "redirect_uri") { + // // Do NOT sign the JWT for "redirect_uri" scheme + // // Sign the JWT as per the scheme's requirements + // const token = jwt.sign(jwtPayload, privateKey, { + // algorithm: "ES256", + // noTimestamp: true, // Retain if necessary + // header, + // }); + // return token; + + // return jwtPayload; + // } } else { - // Do NOT sign the JWT for "redirect_uri" scheme - - return jwtPayload; + throw new Error("not supported client_id_scheme:" + client_id_scheme); } } - - - - - - - - - - - - export async function decryptJWE(jweToken, privateKeyPEM) { try { const privateKey = crypto.createPrivateKey(privateKeyPEM); diff --git a/utils/tokenUtils.js b/utils/tokenUtils.js index 9d175f3..b2f1048 100644 --- a/utils/tokenUtils.js +++ b/utils/tokenUtils.js @@ -83,7 +83,7 @@ export function buildVPbyReference( redirect_uri ) { if (client_id_scheme == "redirect_uri") { - throw error("redirect_uri is not supportted for VP by reference"); + throw new Error("redirect_uri is not supportted for VP by reference"); } else { let result = "openid4vp://?client_id=" + diff --git a/clientIdKeys/client_cert.cnf b/x509/client_cert.cnf similarity index 100% rename from clientIdKeys/client_cert.cnf rename to x509/client_cert.cnf diff --git a/clientIdKeys/client_certificate.crt b/x509/client_certificate.crt similarity index 100% rename from clientIdKeys/client_certificate.crt rename to x509/client_certificate.crt diff --git a/clientIdKeys/client_certificate.der b/x509/client_certificate.der similarity index 100% rename from clientIdKeys/client_certificate.der rename to x509/client_certificate.der diff --git a/clientIdKeys/client_certificate_base64.txt b/x509/client_certificate_base64.txt similarity index 100% rename from clientIdKeys/client_certificate_base64.txt rename to x509/client_certificate_base64.txt diff --git a/clientIdKeys/client_private.key b/x509/client_private.key similarity index 100% rename from clientIdKeys/client_private.key rename to x509/client_private.key diff --git a/x509/client_private_pkcs8.key b/x509/client_private_pkcs8.key new file mode 100644 index 0000000..89442f0 --- /dev/null +++ b/x509/client_private_pkcs8.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDxisIDLK+rq9GB +9JyJrq+z1cGg9ANR6uNwy/JdQQuc8Le3zK1y0A+pGkQS4skDT4OOflUh5J4R21eA +KQe8OP2lA2qqbkk9g8axvVTIAo10U5rMpgr+3TA/ykHs9M6y3rodiOvAz8RPJvqF +V5u90rM1gpuqbxUj+hlC0HCrEaJiPvvb0O8HC+yr6Yoe0g9uFkmeSCoTecgz0/EL +p4NqNvaXA9Cqze6j645mKon0fTxfM0AY7bAlQG5Yy1qNJMFHnixsS4zxFI/m9//7 +huKnpGC1BZZHibxOe9jVvuaXS0hpRCYJDCgvrFCeaQn3/+PHGjFCF3DJAp2mWh+N +t1SBAHclAgMBAAECggEABLiinC9Ga8Oa/8BJQwOwGYLHGx9J5VMDCRRjc2EvGe02 +mhgdF85rT/HcfMYECdWHC8T6YwQ19sFjz3R92zb99ji/3/7x3Yry2kyKGLK8ExcD +QpbIRj0KkWShr0EFvUdPOf6QJR+qhaWMZgjVWXuB1zzrUASF1EMyNTUfzqg0lVj+ +GGl7RYMgfk0FMPUDJ1xx5CDFzJxlOOwovYM1UR0skav9EVU+v96CemUM29WR01Vk +zyuzzForhi/J7CaLOYZHgUJN9qr1lo9YHn1N0kAbz6sNIoGc5uquLlddg6VvAAKz +XUKm4HjxSqCENdCe1K2h8cBvHdwNlWLgH9kK1MQ2QQKBgQD8W5H87X06NKGJIIg6 +zk24VVTbz0Hs8718UgL2YqzXl32LbQIgFgBewVa3HSH9pA+UJ1Fyn3uOoMx3+TXL +5fMCP2fcPqvDtb2Lw4qpVTn1/MDTBb3REfZFAWFPQ0AT2JaDxEKmYHnzpQzao/Bq +u7is6ToeODz1SwfUKCp/N3MulQKBgQD1Bzmc97RERDfrA9flvHD8SWrTr37Vl1ff +TryQ9kE9zg03lnbbUlA82keusUzsnVGbcZuloVqU5+VIgqdW5EQKF755i6eygi26 +Zao5z4guaVuVlMg9d1v9iunpD+hNqB6rq03GuwxwpRB6APWQXdLnZJRn49+MfZpW +Cy/4TFdSUQKBgHROy+kTl9zEtxKZUNAxlzZ29aZdTpgj+lgbAEyIS/sNUgp5kjox +ibgiSZIZtjnNbHSu7mXyxSKGT9aXoEi6UZbenEKxC78muxZ4aCcFJbKO7FwV0FdU +fPlu2meKqjS7ZRuTz9gYrPwzPMpkNoEqxcHDuEqHRukSzKfmXzfE2axFAoGAQhkL +2qMbdtrsNrKXt/qIhBYOAkKyNU4ZPz0PMw+q89A00oz9TRa8XF4Jvj9H4ABkNkmE +Uj7fn5Ga0V+lYpQ7InnbQRCCUW46hbDNJcEtHFABi61SAvrCzvd9OAi5gtFaTLbu +STDGH1D3y+GcXLBip1cDja7z7AMp8hIcg5+cYrECgYEA1N5XiP+LRCDVJbpkStjO +ZlYhmzbtxaBj/68sXQwuIyR1qce48z5RQhohUCqmtro69H4FB+22yc+ApOhrLszK +sgb8MRYdj00M2cv0KYUSfQ5HfrzDWyMw6Offzi3xbqaiOsN1XtpF2JzzP/XaWOOK +QwDvAMlNNf6PmgSBLZpw3EQ= +-----END PRIVATE KEY----- From 22c97e36a0305ade6653ffed69df8034c7fa9d94 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Wed, 16 Oct 2024 15:30:22 +0300 Subject: [PATCH 21/37] x509 vp request completed --- routes/verifierRoutes.js | 52 +++++++++++++++++++---------- utils/cryptoUtils.js | 2 +- x509/client_certificate.crt | 40 +++++++++++----------- x509/client_certificate.der | Bin 877 -> 0 bytes x509/client_certificate_base64.txt | 1 - x509/client_csr.csr | 18 ++++++++++ 6 files changed, 75 insertions(+), 38 deletions(-) delete mode 100644 x509/client_certificate.der delete mode 100644 x509/client_certificate_base64.txt create mode 100644 x509/client_csr.csr diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index b8eeef1..d06489f 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -170,12 +170,38 @@ verifierRouter.get("/client-metadata", async (req, res) => { CLIENT_ID_SCHEME x509_dns_san *********************************************************** */ verifierRouter.get("/generateVPRequestx509", async (req, res) => { - const stateParam = req.query.id ? req.query.id : uuidv4(); + const uuid = req.params.uuid ? req.params.uuid : uuidv4(); + + let client_id = "dss.aegean.gr"; + let request_uri = `${serverURL}/x509VPrequest/${uuid}`; + let vpRequest = + "openid4vp://?client_id=" + + encodeURIComponent(client_id) + + "&request_uri=" + + encodeURIComponent(request_uri); + + let code = qr.image(vpRequest, { + type: "png", + ec_level: "M", + size: 20, + margin: 10, + }); + let mediaType = "PNG"; + let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); + res.json({ + qr: encodedQR, + deepLink: vpRequest, + sessionId: uuid, + }); +}); + +verifierRouter.get("/x509VPrequest/:id", async (req, res) => { + //TODO pass state and nonce to the jwt request + const stateParam = req.params.id; //req.query.id ? req.query.id : uuidv4(); const nonce = generateNonce(16); const uuid = req.params.id ? req.params.id : uuidv4(); const response_uri = serverURL + "/direct_post" + "/" + uuid; - const client_metadata = { client_name: "UAegean EWC Verifier", @@ -193,7 +219,7 @@ verifierRouter.get("/generateVPRequestx509", async (req, res) => { claims: null, }); - const vpRequest = await buildVpRequestJWT( + let signedVPJWT = await buildVpRequestJWT( clientId, response_uri, presentation_definition_sdJwt, @@ -202,22 +228,14 @@ verifierRouter.get("/generateVPRequestx509", async (req, res) => { client_metadata ); - let code = qr.image(vpRequest, { - type: "png", - ec_level: "M", - size: 20, - margin: 10, - }); - let mediaType = "PNG"; - let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); - res.json({ - qr: encodedQR, - deepLink: vpRequest, - sessionId: stateParam, - }); - + console.log(signedVPJWT) + res.type("text/plain").send(signedVPJWT); }); +/* ******************************************** + RESPONSES +*******************************************/ + verifierRouter.post("/direct_post/:id", async (req, res) => { console.log("direct_post VP is below!"); const sessionId = req.params.id; diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index c5b7b48..f17c166 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -130,7 +130,7 @@ export async function buildVpRequestJWT( client_id: client_id, // this should match the dns record in the certificate (dss.aegean.gr) client_id_scheme: client_id_scheme, presentation_definition: presentation_definition, - redirect_uri: redirect_uri, + response_uri: redirect_uri, nonce: "n-0S6_WzA2Mj", state: "af0ifjsldkj", client_metadata: client_metadata, // diff --git a/x509/client_certificate.crt b/x509/client_certificate.crt index 47fb843..b4c9645 100644 --- a/x509/client_certificate.crt +++ b/x509/client_certificate.crt @@ -1,21 +1,23 @@ -----BEGIN CERTIFICATE----- -MIIDaTCCAlECFBagdVsnDy9Af/Df4n3bvO3yxLWdMA0GCSqGSIb3DQEBCwUAMHEx -CzAJBgNVBAYTAkVVMQ8wDQYDVQQIDAZHUkVFQ0UxDzANBgNVBAcMBkdSRUVDRTEQ -MA4GA1UECgwHVUFlZ2VhbjEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEWMBQGA1UE -AwwNZHNzLmFlZ2Vhbi5ncjAeFw0yNDEwMTYwNzE4MzJaFw0yNTEwMTYwNzE4MzJa -MHExCzAJBgNVBAYTAkVVMQ8wDQYDVQQIDAZHUkVFQ0UxDzANBgNVBAcMBkdSRUVD -RTEQMA4GA1UECgwHVUFlZ2VhbjEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEWMBQG -A1UEAwwNZHNzLmFlZ2Vhbi5ncjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAPGKwgMsr6ur0YH0nImur7PVwaD0A1Hq43DL8l1BC5zwt7fMrXLQD6kaRBLi -yQNPg45+VSHknhHbV4ApB7w4/aUDaqpuST2DxrG9VMgCjXRTmsymCv7dMD/KQez0 -zrLeuh2I68DPxE8m+oVXm73SszWCm6pvFSP6GULQcKsRomI++9vQ7wcL7Kvpih7S -D24WSZ5IKhN5yDPT8Qung2o29pcD0KrN7qPrjmYqifR9PF8zQBjtsCVAbljLWo0k -wUeeLGxLjPEUj+b3//uG4qekYLUFlkeJvE572NW+5pdLSGlEJgkMKC+sUJ5pCff/ -48caMUIXcMkCnaZaH423VIEAdyUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAB8I/ -rM5nScy+CXf9s2VC5QFPy4bw6DDyih4JZj2VmO2PbZToOhtZ8FwE2glIikoqVQ5n -sY7B7mij5dW6zgnnqYJ1Lquw8M5xLo78xMDjatm52I3mvxvuLS4D7hiJE1ej0QAa -cCM2fkwuWGwTAyZlhWx69znccazxliMfjz3LqdyFjZELgmOnzoO1HwOI+2NrDbI4 -D1UJOongotoHn8jKkqrKpYDiccBp7z2vuD4TlgyDFA28r1LlzOlLMGaXHcGZ0Ijd -8ODEP1U4nWjdVa0FHQHiRFPIRHSqCHqwS/YYM7uD4f1My0whCmRGrbsNmLYRlF2D -HrMhh1RzYDRnGi58Lg== +MIIDvjCCAqagAwIBAgIUcw2rW9UgjWB9tCYy6PGb8uF3GsUwDQYJKoZIhvcNAQEL +BQAwcTELMAkGA1UEBhMCRVUxDzANBgNVBAgMBkdSRUVDRTEPMA0GA1UEBwwGR1JF +RUNFMRAwDgYDVQQKDAdVQWVnZWFuMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRYw +FAYDVQQDDA1kc3MuYWVnZWFuLmdyMB4XDTI0MTAxNjEyMjYzNloXDTI1MTAxNjEy +MjYzNlowcTELMAkGA1UEBhMCRVUxDzANBgNVBAgMBkdSRUVDRTEPMA0GA1UEBwwG +R1JFRUNFMRAwDgYDVQQKDAdVQWVnZWFuMRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50 +MRYwFAYDVQQDDA1kc3MuYWVnZWFuLmdyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA8YrCAyyvq6vRgfScia6vs9XBoPQDUerjcMvyXUELnPC3t8ytctAP +qRpEEuLJA0+Djn5VIeSeEdtXgCkHvDj9pQNqqm5JPYPGsb1UyAKNdFOazKYK/t0w +P8pB7PTOst66HYjrwM/ETyb6hVebvdKzNYKbqm8VI/oZQtBwqxGiYj7729DvBwvs +q+mKHtIPbhZJnkgqE3nIM9PxC6eDajb2lwPQqs3uo+uOZiqJ9H08XzNAGO2wJUBu +WMtajSTBR54sbEuM8RSP5vf/+4bip6RgtQWWR4m8TnvY1b7ml0tIaUQmCQwoL6xQ +nmkJ9//jxxoxQhdwyQKdplofjbdUgQB3JQIDAQABo04wTDArBgNVHREEJDAigg1k +c3MuYWVnZWFuLmdyghF3d3cuZHNzLmFlZ2Vhbi5ncjAdBgNVHQ4EFgQUT085TK8k +rLZUVcL4SH2l6LEwg3wwDQYJKoZIhvcNAQELBQADggEBAB3Os4XxaxdG8Q2eAccp +KoDjICjRxgTQ6UFkSB21GOF5S/CtHKOcMhYtWDOd4GeQY5LrCidqcT7Oxq0qfWwy +8jqHcGSSyRqtZ4LzXyzKmm/sl0lZf0h2JrWSxkP8qcpu4vmH1JzOs1yhPPKnZGS4 +vJRVTB6JbRxFYHP5uYkQ56GguBAFQ0V0zZ0MPeduFmJL/IC2EEJTCb58BOht+kaV +HhGQPGD5Nw4hmO/Hi+WfyxJGGgRtkpIlDT7nkPIT+rLolcQHYP63qGUl6QmqILPo +1Wf3Wf7ajsBCfYm2EKqCpdSSwrtLX/qJ6RzKyubpSUx2KLbqy7fulkA6uT71QuSm +IXI= -----END CERTIFICATE----- diff --git a/x509/client_certificate.der b/x509/client_certificate.der deleted file mode 100644 index 83ca6d7e9fbfdfd43115b65a94855da953c2aaf1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 877 zcmXqLV$L*ZVhUsu5nE6itd#AN85K^Mn-N{27^LF zZUas>=1>+kVJ6p5Lw=wg);=V229`81R8qaPhE*I;N(lCgvH68Hj*H zxOsRzLlj(63lfV;a#Qn4VB*X?yeY-SdWm4gdg(<5a^k#3CWZ!vW(MYl7RE+VK&~kg zcOab9#HfTEFpR7W%uS5^3(+0+dT_xP=D=5v z3r>HEb>yD&Vf*$oYl|-Muat5TdUTT6zqzk2RPo6?!Q0^tn(TWl{w`(CT9xN%+k9-} z-jEYay(Pi3&Mf2lch|uFl;fK(=QiEjCEM}(!1*KoYQI{;XYakV*|cf)s(ex9Uy@E2 z3RVj)O0xTX`@(y6?l-GncFA4h&lB^U=bq zUutdQjU6Q3Zcuf|i#Q$Ct8&nNo=%Q;&qtB|XW#$-ZhN$RNy1jvY3`kS{HkwU-S=#| zw@0Rn8Yhp2{+fV!nVjGMKRzyH=ps!Ys`42;Mj1WY=>5MpFt zKV-k=T)O9(eVpZgH>Wy1W%NJY_Th!Wr!G0pG~1~&-uCBCd0{0T`5}ho7NFF)73Ri3%ycXBrGCJX*h zPOHubi*B*cKXGc(s#8lF9u*$Qd~dschn?^=o@No=J?n#>o_XnQkTzZR;LHmhcRxHh zVjpTTH{)*TT2@)cM=rr9TuN4PRBiD7CSkn0`Qcxm(>{t^DQ;_b^Ul~NI3>1OZnI*0 NNO6Kmx|Cjx9so)ZO*jAm diff --git a/x509/client_certificate_base64.txt b/x509/client_certificate_base64.txt deleted file mode 100644 index 3956d36..0000000 --- a/x509/client_certificate_base64.txt +++ /dev/null @@ -1 +0,0 @@ -MIIDaTCCAlECFBagdVsnDy9Af/Df4n3bvO3yxLWdMA0GCSqGSIb3DQEBCwUAMHExCzAJBgNVBAYTAkVVMQ8wDQYDVQQIDAZHUkVFQ0UxDzANBgNVBAcMBkdSRUVDRTEQMA4GA1UECgwHVUFlZ2VhbjEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEWMBQGA1UEAwwNZHNzLmFlZ2Vhbi5ncjAeFw0yNDEwMTYwNzE4MzJaFw0yNTEwMTYwNzE4MzJaMHExCzAJBgNVBAYTAkVVMQ8wDQYDVQQIDAZHUkVFQ0UxDzANBgNVBAcMBkdSRUVDRTEQMA4GA1UECgwHVUFlZ2VhbjEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDEWMBQGA1UEAwwNZHNzLmFlZ2Vhbi5ncjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPGKwgMsr6ur0YH0nImur7PVwaD0A1Hq43DL8l1BC5zwt7fMrXLQD6kaRBLiyQNPg45+VSHknhHbV4ApB7w4/aUDaqpuST2DxrG9VMgCjXRTmsymCv7dMD/KQez0zrLeuh2I68DPxE8m+oVXm73SszWCm6pvFSP6GULQcKsRomI++9vQ7wcL7Kvpih7SD24WSZ5IKhN5yDPT8Qung2o29pcD0KrN7qPrjmYqifR9PF8zQBjtsCVAbljLWo0kwUeeLGxLjPEUj+b3//uG4qekYLUFlkeJvE572NW+5pdLSGlEJgkMKC+sUJ5pCff/48caMUIXcMkCnaZaH423VIEAdyUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAB8I/rM5nScy+CXf9s2VC5QFPy4bw6DDyih4JZj2VmO2PbZToOhtZ8FwE2glIikoqVQ5nsY7B7mij5dW6zgnnqYJ1Lquw8M5xLo78xMDjatm52I3mvxvuLS4D7hiJE1ej0QAacCM2fkwuWGwTAyZlhWx69znccazxliMfjz3LqdyFjZELgmOnzoO1HwOI+2NrDbI4D1UJOongotoHn8jKkqrKpYDiccBp7z2vuD4TlgyDFA28r1LlzOlLMGaXHcGZ0Ijd8ODEP1U4nWjdVa0FHQHiRFPIRHSqCHqwS/YYM7uD4f1My0whCmRGrbsNmLYRlF2DHrMhh1RzYDRnGi58Lg== \ No newline at end of file diff --git a/x509/client_csr.csr b/x509/client_csr.csr new file mode 100644 index 0000000..c7a2155 --- /dev/null +++ b/x509/client_csr.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC9DCCAdwCAQAwcTELMAkGA1UEBhMCRVUxDzANBgNVBAgMBkdSRUVDRTEPMA0G +A1UEBwwGR1JFRUNFMRAwDgYDVQQKDAdVQWVnZWFuMRYwFAYDVQQLDA1JVCBEZXBh +cnRtZW50MRYwFAYDVQQDDA1kc3MuYWVnZWFuLmdyMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA8YrCAyyvq6vRgfScia6vs9XBoPQDUerjcMvyXUELnPC3 +t8ytctAPqRpEEuLJA0+Djn5VIeSeEdtXgCkHvDj9pQNqqm5JPYPGsb1UyAKNdFOa +zKYK/t0wP8pB7PTOst66HYjrwM/ETyb6hVebvdKzNYKbqm8VI/oZQtBwqxGiYj77 +29DvBwvsq+mKHtIPbhZJnkgqE3nIM9PxC6eDajb2lwPQqs3uo+uOZiqJ9H08XzNA +GO2wJUBuWMtajSTBR54sbEuM8RSP5vf/+4bip6RgtQWWR4m8TnvY1b7ml0tIaUQm +CQwoL6xQnmkJ9//jxxoxQhdwyQKdplofjbdUgQB3JQIDAQABoD4wPAYJKoZIhvcN +AQkOMS8wLTArBgNVHREEJDAigg1kc3MuYWVnZWFuLmdyghF3d3cuZHNzLmFlZ2Vh +bi5ncjANBgkqhkiG9w0BAQsFAAOCAQEA4IIR0C9mx7BVxBpsHQ92QVp0qBWg7Qat +SjNMHmy8/RT+kDg/mPA1si2lq0wR8q9MEar9wIlafm4O37XTvs+3bC8HRFnmbNc7 +pj6OEQw3wL5GSFD3CX3yUTOnbgT/3HYjCc6QhIW6ApUCwowi5QoMZ6GNaIKp6603 +iRCEceNi0wTTm/C4HBW+zIvyyw37DtS8cANITo7069gtGm5veHzG1i07FvAdIrb8 +3qJeyZOlqF0cVROpriba2sBPwqsivfEDd3thE8EiQofbc5qA20+DesbcJV2vjK02 ++pr9RhPq75h8G+JraHlYAxyhxur6dMxtYDYx8CQtqmce5JS+RzFzWw== +-----END CERTIFICATE REQUEST----- From 5a74d4a573c67586cecf769f7dab43b7d0dbd83d Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Wed, 16 Oct 2024 20:20:26 +0300 Subject: [PATCH 22/37] x509 dynamic vp request completed for code flfow --- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 267 ++++++++++++++++++++++++---------- routes/verifierRoutes.js | 2 +- 3 files changed, 196 insertions(+), 75 deletions(-) diff --git a/package.json b/package.json index 59a1b26..2524bb9 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://f5ac-2a02-587-8710-c200-a156-e1c4-5eed-c02b.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://dfbc-2a02-587-8710-c200-a156-e1c4-5eed-c02b.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index b887e7f..e1bc0fc 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -28,11 +28,19 @@ const privateKey = fs.readFileSync("./private-key.pem", "utf-8"); codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); const codeSessions = getAuthCodeSessions(); + const credentialType = req.query.credentialType + ? req.query.credentialType + : "VerifiablePortableDocumentA2SDJWT"; + + const client_id_scheme = req.query.client_id_scheme + ? req.query.client_id_scheme + : "redirect_uri"; + if (codeSessions.sessions.indexOf(uuid) < 0) { codeSessions.sessions.push(uuid); // codeSessions.results.push({ sessionId: uuid, status: "pending" }); } - let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-code-sd-jwt/${uuid}`; + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-code-sd-jwt/${uuid}?scheme=${client_id_scheme}&type=${credentialType}`; let code = qr.image(credentialOffer, { type: "png", @@ -50,13 +58,30 @@ codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { }); // auth code-flow request + +// with dynamic cred request and client_id_scheme == redirect_uri codeFlowRouterSDJWT.get(["/credential-offer-code-sd-jwt/:id"], (req, res) => { + const credentialType = req.query.credentialType + ? req.query.credentialType + : "VerifiablePortableDocumentA1SDJWT"; + + console.log(req.query); + console.log(req.query.client_id_scheme); + const client_id_scheme = req.query.scheme ? req.query.scheme : "redirect_uri"; + + /* + To support multiple client_id_schemas, this param client_id_scheme + will be passed into the session of the issuer and fetched in the + authorize endpoint to decide what schema to use + */ + const issuer_state = `${req.params.id}|${client_id_scheme}`; // using "|" as a delimiter + res.json({ credential_issuer: serverURL, - credential_configuration_ids: ["VerifiablePortableDocumentA1SDJWT"], + credential_configuration_ids: [credentialType], grants: { authorization_code: { - issuer_state: req.params.id, + issuer_state: issuer_state, }, }, }); @@ -232,11 +257,15 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { if (response_type !== "code") { errors.push("Invalid response_type"); } - // removed due to https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#name-using-scope-parameter-to-re - // if (!scope.includes("openid")) { - // errors.push("Invalid scope"); - // } + // *************************************************************************** + // retrive cleaned ITB session and also get (if specified) the client_id_scheme + // *************************************************************************** + + const [originalUuid, client_id_scheme] = issuerState.split("|"); + issuerState = originalUuid; + + // ITB Sessions const authorizationCode = null; //"SplxlOBeZQQYbYS6WxSbIA"; const codeSessions = getAuthCodeSessions(); if (codeSessions.sessions.indexOf(issuerState) >= 0) { @@ -281,90 +310,176 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { /* //5.1.5. Dynamic Credential Request https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-12.html#name-successful-authorization-requesttt The credential issuer can optionally request additional details to authenticate the client e.g. DID authentication. In this case, the authorisation response will contain a response_mode parameter with the value direct_post. A sample response is as given: - - client_id, Decentralised identifier - redirect_uri, For redirection of the response - response_type ( if the issuer requests DID authentication.), - response_mode (The value must be direct_post) - scope, The value must be openid - nonce, - request_uri: The authorisation server’s private key signed the request. */ - const vpRedirectURI="openid4vp://" - console.log(redirect_uri + " but i will request the vp based on " + vpRedirectURI) - // changed this to support auth request by value not reference - //const redirectUrl = `${vpRedirectURI}?state=${state}&client_id=${client_id}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${serverURL}/request_uri_dynamic`; - const response_uri = serverURL + "/direct_post_vci" + "/" + uuid; - const presentation_definition_uri = - serverURL + "/presentation-definition/itbsdjwt"; - const client_metadata_uri = serverURL + "/client-metadata"; - - //response_uri - //const redirectUrl = buildVPbyValue(client_id,presentation_definition_uri,"redirect_uri",client_metadata_uri,response_uri) - // client_id_scheme is set to redirect_uri in OIDC4VP v20, the client_id becomes the redirect_uri - const redirectUrl = buildVPbyValue(response_uri,presentation_definition_uri,"redirect_uri",client_metadata_uri,response_uri) - console.log("redirectUrl", redirectUrl) - return res.redirect(302, redirectUrl); + + if (client_id_scheme == "redirect_uri") { + const vpRedirectURI = "openid4vp://"; + console.log( + redirect_uri + " but i will request the vp based on " + vpRedirectURI + ); + // changed this to support auth request by value not reference + //const redirectUrl = `${vpRedirectURI}?state=${state}&client_id=${client_id}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${serverURL}/request_uri_dynamic`; + const response_uri = serverURL + "/direct_post_vci" + "/" + issuerState; + const presentation_definition_uri = + serverURL + "/presentation-definition/itbsdjwt"; + const client_metadata_uri = serverURL + "/client-metadata"; + + //response_uri + //const redirectUrl = buildVPbyValue(client_id,presentation_definition_uri,"redirect_uri",client_metadata_uri,response_uri) + // client_id_scheme is set to redirect_uri in OIDC4VP v20, the client_id becomes the redirect_uri + const redirectUrl = buildVPbyValue( + response_uri, + presentation_definition_uri, + "redirect_uri", + client_metadata_uri, + response_uri + ); + // console.log("redirectUrl", redirectUrl); + return res.redirect(302, redirectUrl); + } else if (client_id_scheme == "x509_san_dns") { + //TODO + // let client_id = "dss.aegean.gr"; + // let request_uri = `${serverURL}/x509VPrequest/${issuerState}`; + // // let vpRequest_url = + // // "openid4vp://?client_id=" + + // // encodeURIComponent(client_id) + + // // "&request_uri=" + + // // encodeURIComponent(request_uri); + // const redirectUrl = `openid4vp://?state=${issuerState}&client_id=${client_id}&response_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${request_uri}`; + + // return res.redirect(302, redirectUrl); + // const stateParam = issuerState + // const nonce = generateNonce(16); + + // const uuid = issuerState + // const response_uri = serverURL + "/direct_post" + "/" + uuid; + + // const client_metadata = { + // client_name: "UAegean EWC Verifier", + // logo_uri: + // "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + // location: "Greece", + // cover_uri: "string", + // description: "EWC pilot case verification", + // }; + + // const clientId = "dss.aegean.gr"; + // const presentation_definition_sdJwt = JSON.parse( + // fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") + // ); + + // let signedVPJWT = await buildVpRequestJWT( + // clientId, + // response_uri, + // presentation_definition_sdJwt, + // "", + // "x509_san_dns", + // client_metadata + // ); + // res.type("text/plain").send(signedVPJWT); + let request_uri = `${serverURL}/x509VPrequest_dynamic/${issuerState}`; + const clientId = "dss.aegean.gr"; + let vpRequest = + "openid4vp://?client_id=" + + encodeURIComponent(clientId) + + "&request_uri=" + + encodeURIComponent(request_uri); + + return res.redirect(302, vpRequest); + } } }); -codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { - let uuid = uuidv4(); - let client_id = serverURL + "/direct_post" + "/" + uuid; - const response_uri = serverURL + "/direct_post" + "/" + uuid; - - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - // Construct the absolute path to verifier-config.json - const configPath = path.join(__dirname, "..", "data", "verifier-config.json"); - const presentation_definition_sdJwt = JSON.parse( - fs.readFileSync( - path.join(__dirname, "..", "data", "presentation_definition_sdjwt.json") - ) - ); +// Dynamic VP request by reference endpoint +codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { + //TODO pass state and nonce to the jwt request - // Read and parse the JSON file - // const clientMetadata = JSON.parse(fs.readFileSync(configPath, "utf-8")); - - // clientMetadata.presentation_definition_uri = - // serverURL + "/presentation-definition/" + uuid; - // clientMetadata.redirect_uris = [response_uri]; - // clientMetadata.client_id = client_id; - const clientMetadata = { - vp_formats: { - jwt_vp: { - alg: ["EdDSA", "ES256K"], - }, - ldp_vp: { - proof_type: ["Ed25519Signature2018"], - }, - }, + const uuid = req.params.id ? req.params.id : uuidv4(); + const response_uri = serverURL + "/direct_post_vci/"+uuid; + + const client_metadata = { + client_name: "UAegean EWC Verifier", + logo_uri: "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + location: "Greece", + cover_uri: "string", + description: "EWC pilot case verification", }; + const presentation_definition_sdJwt = JSON.parse( + fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") + ); - const vpRequestJWT = buildVpRequestJWT( - client_id, + const clientId = "dss.aegean.gr"; + let signedVPJWT = await buildVpRequestJWT( + clientId, response_uri, presentation_definition_sdJwt, - privateKey, - "redirect_uri", - clientMetadata + "", + "x509_san_dns", + client_metadata ); - // console.log("Dynamic VP request ") - // console.log(JSON.stringify(vpRequestJWT, null, 2)); - - res.send(vpRequestJWT); + console.log(signedVPJWT) + res.type("text/plain").send(signedVPJWT); }); +// codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { +// let uuid = uuidv4(); +// let client_id = serverURL + "/direct_post" + "/" + uuid; +// const response_uri = serverURL + "/direct_post" + "/" + uuid; + +// const __filename = fileURLToPath(import.meta.url); +// const __dirname = path.dirname(__filename); +// // Construct the absolute path to verifier-config.json +// const configPath = path.join(__dirname, "..", "data", "verifier-config.json"); +// const presentation_definition_sdJwt = JSON.parse( +// fs.readFileSync( +// path.join(__dirname, "..", "data", "presentation_definition_sdjwt.json") +// ) +// ); + +// // Read and parse the JSON file +// // const clientMetadata = JSON.parse(fs.readFileSync(configPath, "utf-8")); + +// // clientMetadata.presentation_definition_uri = +// // serverURL + "/presentation-definition/" + uuid; +// // clientMetadata.redirect_uris = [response_uri]; +// // clientMetadata.client_id = client_id; +// const clientMetadata = { +// vp_formats: { +// jwt_vp: { +// alg: ["EdDSA", "ES256K"], +// }, +// ldp_vp: { +// proof_type: ["Ed25519Signature2018"], +// }, +// }, +// }; + +// const vpRequestJWT = buildVpRequestJWT( +// client_id, +// response_uri, +// presentation_definition_sdJwt, +// privateKey, +// "redirect_uri", +// clientMetadata +// ); + +// // console.log("Dynamic VP request ") +// // console.log(JSON.stringify(vpRequestJWT, null, 2)); + +// res.send(vpRequestJWT); +// }); + /* presentation by the wallet during an Issuance part of the Dynamic Credential Request */ -codeFlowRouterSDJWT.post("/direct_post_vci", async (req, res) => { +codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { console.log("direct_post VP for VCI is below!"); let state = req.body["state"]; - let jwt = req.body["id_token"]; + let jwt = req.body["vp_token"]; console.log("direct_post_vci received jwt is::"); console.log(jwt); + const uuid = req.params.id // const authorizatiton_details = getSessionsAuthorizationDetail().get(state); @@ -385,8 +500,14 @@ codeFlowRouterSDJWT.post("/direct_post_vci", async (req, res) => { codeSessions.results, codeSessions.requests ); - const redirectUrl = `${codeSessions.requests.redirectUri}?code=${authorizationCode}&state=${state}`; - return res.redirect(302, redirectUrl); + let sessionIndex = codeSessions.sessions.indexOf(uuid) + if(sessionIndex >= 0){ + const redirectUrl = `${codeSessions.requests[sessionIndex].redirectUri}?code=${authorizationCode}&state=${state}`; + return res.redirect(302, redirectUrl); + }else{ + return res.sendStatus(500); + } + } else { return res.sendStatus(500); } diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index d06489f..ce0fe58 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -244,7 +244,7 @@ verifierRouter.post("/direct_post/:id", async (req, res) => { if (sdjwt) { let presentationSubmission = req.body["presentation_submission"]; let state = req.body["state"]; - console.log(state); + // console.log(state); // console.log(response); const decodedSdJwt = await decodeSdJwt(sdjwt, digest); const claims = await getClaims( From 39ce95fd2db6e1b030dbb19e1a9e16a000848429 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 17 Oct 2024 11:05:50 +0300 Subject: [PATCH 23/37] testing of dynamic cred request --- data/presentation_definition_sdjwt.json | 2 +- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/presentation_definition_sdjwt.json b/data/presentation_definition_sdjwt.json index 3862dc2..d17c69e 100644 --- a/data/presentation_definition_sdjwt.json +++ b/data/presentation_definition_sdjwt.json @@ -26,7 +26,7 @@ "path": ["$.vct"], "filter": { "type": "string", - "const": "VerifiablePortableDocumentA2SDJWT" + "const": "VerifiablePortableDocumentA1SDJWT" } } ], diff --git a/package.json b/package.json index 2524bb9..cd95fb8 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://dfbc-2a02-587-8710-c200-a156-e1c4-5eed-c02b.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://abb5-2a02-587-8710-c200-df4c-297c-da50-b930.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index e1bc0fc..bfcb038 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -63,7 +63,7 @@ codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { codeFlowRouterSDJWT.get(["/credential-offer-code-sd-jwt/:id"], (req, res) => { const credentialType = req.query.credentialType ? req.query.credentialType - : "VerifiablePortableDocumentA1SDJWT"; + : "VerifiablePortableDocumentA2SDJWT"; console.log(req.query); console.log(req.query.client_id_scheme); From 90f6a80f12cbc2c8b45b816d9fdbd6002ab79d90 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 17 Oct 2024 13:50:32 +0300 Subject: [PATCH 24/37] corrected cred issuance --- data/issuer-config.json | 4 +- didjwks/did_private.key | 5 ++ didjwks/did_public.pem | 4 ++ package-lock.json | 104 ++++++++++++++++++++++++++++++---- package.json | 4 +- routes/codeFlowSdJwtRoutes.js | 9 ++- routes/didweb.js | 40 +++++++++++++ routes/preAuthSDjwRoutes.js | 8 +++ server.js | 3 + utils/didjwks.js | 16 ++++++ 10 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 didjwks/did_private.key create mode 100644 didjwks/did_public.pem create mode 100644 routes/didweb.js create mode 100644 utils/didjwks.js diff --git a/data/issuer-config.json b/data/issuer-config.json index fef5ebc..aadea02 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -7,8 +7,8 @@ "batch_credential_endpoint": "https://server.example.com/batch", "display": [ { - "name": "Issuer", - "location": "Belgium", + "name": "UAegean Test Issuer", + "location": "Greece", "locale": "en-GB", "description": "For queries about how we manage your data please contact the Data Protection Officer." } diff --git a/didjwks/did_private.key b/didjwks/did_private.key new file mode 100644 index 0000000..e5b0532 --- /dev/null +++ b/didjwks/did_private.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDfw3MPQvb1xZQSca9QrZvJXTpGquu+bmVT1+/eeGh8boAoGCCqGSM49 +AwEHoUQDQgAEijVgOGHvwHSeV1Z2iLF9pQLQAw7KcHF3VIjThhvVtBRJ8VKFYBQY +Sc3HbhXZvkbWOuEk0eYzC2BE4E6L04oktw== +-----END EC PRIVATE KEY----- diff --git a/didjwks/did_public.pem b/didjwks/did_public.pem new file mode 100644 index 0000000..c004486 --- /dev/null +++ b/didjwks/did_public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEijVgOGHvwHSeV1Z2iLF9pQLQAw7K +cHF3VIjThhvVtBRJ8VKFYBQYSc3HbhXZvkbWOuEk0eYzC2BE4E6L04oktw== +-----END PUBLIC KEY----- diff --git a/package-lock.json b/package-lock.json index f1bd4e2..9d08ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "dependencies": { "@jorgeferrero/stream-to-buffer": "^2.0.6", "@sd-jwt/core": "^0.6.1", - "@sd-jwt/crypto-nodejs": "^0.6.1", - "@sd-jwt/sd-jwt-vc": "^0.6.1", + "@sd-jwt/crypto-nodejs": "^0.7.2", + "@sd-jwt/sd-jwt-vc": "^0.7.2", "base64url": "^3.0.1", "express": "^4.18.3", "express-openapi-validator": "^5.3.7", @@ -66,11 +66,11 @@ } }, "node_modules/@sd-jwt/crypto-nodejs": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@sd-jwt/crypto-nodejs/-/crypto-nodejs-0.6.1.tgz", - "integrity": "sha512-dkzZYNPlswCCnn4PfR/+GFeo/tI7QEZv4vqzpAp8/7v62Cpi/VjdyMmW5+SlHg3sTw82eL/B9aFTcFlnbQuvPA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/crypto-nodejs/-/crypto-nodejs-0.7.2.tgz", + "integrity": "sha512-7DHy1WBHwvXseiX+U7XA6jX4dX4Ins3Nxd12JhBSm+FJfIwU97FU/H0KlF6lLyi4a4nbY/O6U9wJjYI1PxA9sQ==", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@sd-jwt/decode": { @@ -85,6 +85,27 @@ "node": ">=16" } }, + "node_modules/@sd-jwt/jwt-status-list": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/jwt-status-list/-/jwt-status-list-0.7.2.tgz", + "integrity": "sha512-o/Mg/Zg21poFsPXuxtPD9sdXq2b/0L+rb9gxU2k1rp1aT+DWmqD0k8v0Ttr2tlMc8l1xXQNA8FLXbL1AdLRmbQ==", + "dependencies": { + "@sd-jwt/types": "0.7.2", + "base64url": "^3.0.1", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/jwt-status-list/node_modules/@sd-jwt/types": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.7.2.tgz", + "integrity": "sha512-1NRKowiW0ZiB9SGLApLPBH4Xk8gDQJ+nA9NdZ+uy6MmJKLEwjuJxO7yTvRIv/jX/0/Ebh339S7Kq4RD2AiFuRg==", + "engines": { + "node": ">=18" + } + }, "node_modules/@sd-jwt/present": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@sd-jwt/present/-/present-0.6.1.tgz", @@ -99,14 +120,75 @@ } }, "node_modules/@sd-jwt/sd-jwt-vc": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@sd-jwt/sd-jwt-vc/-/sd-jwt-vc-0.6.1.tgz", - "integrity": "sha512-eF7NAFvedBCx+vrw4TVY3evUz5rAG8/FtB/CUudYEigKcpanLgfuNGhk93D45k+lLDG0b24w+qorqbpLZzHA2g==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/sd-jwt-vc/-/sd-jwt-vc-0.7.2.tgz", + "integrity": "sha512-rryYmnoJHRSNqHcrs0Atta+bfJzU2yT7mYumR2D4lTfxJKWZd0OHHFq57uZSEm/wXPI6uytUJXYbEboCqLUAtw==", "dependencies": { - "@sd-jwt/core": "0.6.1" + "@sd-jwt/core": "0.7.2", + "@sd-jwt/jwt-status-list": "0.7.2", + "@sd-jwt/utils": "0.7.2" }, "engines": { - "node": ">=16" + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/@sd-jwt/core": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/core/-/core-0.7.2.tgz", + "integrity": "sha512-vix1GplUFc1A9H42r/yXkg7cKYthggyqZEwlFdsBbn4xdZNE+AHVF4N7kPa1pPxipwN3UIHd4XnQ5MJV15mhsQ==", + "dependencies": { + "@sd-jwt/decode": "0.7.2", + "@sd-jwt/present": "0.7.2", + "@sd-jwt/types": "0.7.2", + "@sd-jwt/utils": "0.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/@sd-jwt/decode": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/decode/-/decode-0.7.2.tgz", + "integrity": "sha512-dan2LSvK63SKwb62031G4r7TE4TaiI0EK1KbPXqS+LCXNkNDUHqhtYp9uOpj+grXceCsMtMa2f8VnUfsjmwHHg==", + "dependencies": { + "@sd-jwt/types": "0.7.2", + "@sd-jwt/utils": "0.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/@sd-jwt/present": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/present/-/present-0.7.2.tgz", + "integrity": "sha512-mQV85u2+mLLy2VZ9Wx2zpaB6yTDnbhCfWkP7eeCrzJQHBKAAHko8GrylEFmLKewFIcajS/r4lT/zHOsCkp5pZw==", + "dependencies": { + "@sd-jwt/decode": "0.7.2", + "@sd-jwt/types": "0.7.2", + "@sd-jwt/utils": "0.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/@sd-jwt/types": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/types/-/types-0.7.2.tgz", + "integrity": "sha512-1NRKowiW0ZiB9SGLApLPBH4Xk8gDQJ+nA9NdZ+uy6MmJKLEwjuJxO7yTvRIv/jX/0/Ebh339S7Kq4RD2AiFuRg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sd-jwt/sd-jwt-vc/node_modules/@sd-jwt/utils": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sd-jwt/utils/-/utils-0.7.2.tgz", + "integrity": "sha512-aMPY7uHRMgyI5PlDvEiIc+eBFGC1EM8OCQRiEjJ8HGN0pajWMYj0qwSw7pS90A49/DsYU1a5Zpvb7nyjgGH0Yg==", + "dependencies": { + "@sd-jwt/types": "0.7.2", + "js-base64": "^3.7.6" + }, + "engines": { + "node": ">=18" } }, "node_modules/@sd-jwt/types": { diff --git a/package.json b/package.json index cd95fb8..cfacace 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "dependencies": { "@jorgeferrero/stream-to-buffer": "^2.0.6", "@sd-jwt/core": "^0.6.1", - "@sd-jwt/crypto-nodejs": "^0.6.1", - "@sd-jwt/sd-jwt-vc": "^0.6.1", + "@sd-jwt/crypto-nodejs": "^0.7.2", + "@sd-jwt/sd-jwt-vc": "^0.7.2", "base64url": "^3.0.1", "express": "^4.18.3", "express-openapi-validator": "^5.3.7", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index bfcb038..e2a96b1 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -313,10 +313,11 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { */ if (client_id_scheme == "redirect_uri") { + console.log("client_id_scheme redirect_uri") const vpRedirectURI = "openid4vp://"; - console.log( - redirect_uri + " but i will request the vp based on " + vpRedirectURI - ); + // console.log( + // redirect_uri + " but i will request the vp based on " + vpRedirectURI + // ); // changed this to support auth request by value not reference //const redirectUrl = `${vpRedirectURI}?state=${state}&client_id=${client_id}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${serverURL}/request_uri_dynamic`; const response_uri = serverURL + "/direct_post_vci" + "/" + issuerState; @@ -337,6 +338,8 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { // console.log("redirectUrl", redirectUrl); return res.redirect(302, redirectUrl); } else if (client_id_scheme == "x509_san_dns") { + console.log("client_id_scheme x509_san_dns") + //TODO // let client_id = "dss.aegean.gr"; // let request_uri = `${serverURL}/x509VPrequest/${issuerState}`; diff --git a/routes/didweb.js b/routes/didweb.js new file mode 100644 index 0000000..3d179f9 --- /dev/null +++ b/routes/didweb.js @@ -0,0 +1,40 @@ +import express from "express"; +import fs from "fs"; +import { v4 as uuidv4 } from "uuid"; +import { convertPemToJwk } from "../utils/didjwks.js"; + +const didWebRouter = express.Router(); +const serverURL = process.env.SERVER_URL || "http://localhost:3000"; +const proxyPath = process.env.PROXY_PATH || null; + +didWebRouter.get(["/.well-known/did.json", "/.well-known/jwks.json"], async (req, res) => { + let jwks = await convertPemToJwk(); + // console.log(jwks); + let contorller = serverURL; + if (proxyPath) { + contorller = serverURL + ":" + proxyPath; + } + + let didDoc = { + "@context": "https://www.w3.org/ns/did/v1", + id: `${contorller}`, + verificationMethod: [ + { + id: `${contorller}#keys-1`, //"did:web:example.com#keys-1", + type: "JsonWebKey2020", + controller: `${contorller}`, + publicKeyJwk: jwks, + }, + ], + authentication: [`${contorller}#keys-1`], + + didResolutionMetadata: { + contentType: "application/did+json", + }, + didDocumentMetadata: {}, + }; + + res.json(didDoc); +}); + +export default didWebRouter; diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 13f16e8..b1d993f 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -362,6 +362,14 @@ router.post("/credential", async (req, res) => { credPayload = VerifiableFerryBoardingPassCredentialSDJWT( decodedHeaderSubjectDID ); + }else if (credType === "VerifiablePortableDocumentA1SDJWT") { + credPayload = getGenericSDJWTData( + decodedHeaderSubjectDID + ); + }else if (credType === "VerifiablePortableDocumentA2SDJWT") { + credPayload = getGenericSDJWTData( + decodedHeaderSubjectDID + ); } const cnf = { jwk: holderJWKS }; diff --git a/server.js b/server.js index ea6b704..d803744 100644 --- a/server.js +++ b/server.js @@ -8,8 +8,10 @@ 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 didWebRouter from "./routes/didweb.js" import educationalRouter from "./routes/educationalRoutes.js"; import bodyParser from "body-parser"; // Body parser middleware + import * as OpenApiValidator from "express-openapi-validator"; import path from "path"; @@ -50,6 +52,7 @@ app.use("/", pidRouter); app.use("/", passportRouter); app.use("/", educationalRouter); app.use("/", boardingPassRouter); +app.use("/", didWebRouter); // Error handler for validation errors app.use((err, req, res, next) => { diff --git a/utils/didjwks.js b/utils/didjwks.js new file mode 100644 index 0000000..8c564df --- /dev/null +++ b/utils/didjwks.js @@ -0,0 +1,16 @@ +import crypto from "crypto"; +import jwt from "jsonwebtoken"; +import { importSPKI, exportJWK } from "jose"; +import base64url from "base64url"; +import { error } from "console"; +import fs from "fs"; + + + +export async function convertPemToJwk() { + const spki = fs.readFileSync('./didjwks/did_public.pem', 'utf8'); + const key = await importSPKI(spki, 'ES256'); + const jwk = await exportJWK(key); + return jwk +// console.log(JSON.stringify(jwk, null, 2)); +} \ No newline at end of file From 074170132f544a309f5346584b7ed2729e546737 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Fri, 18 Oct 2024 12:44:48 +0300 Subject: [PATCH 25/37] did:jwks support --- didjwks/did_private_pkcs8.key | 5 +++ package.json | 2 +- routes/didweb.js | 33 ++++++++++++--- routes/preAuthSDjwRoutes.js | 29 ++++++++----- routes/verifierRoutes.js | 80 ++++++++++++++++++++++++++++++++--- utils/cryptoUtils.js | 57 ++++++++++++++++++++++--- 6 files changed, 177 insertions(+), 29 deletions(-) create mode 100644 didjwks/did_private_pkcs8.key diff --git a/didjwks/did_private_pkcs8.key b/didjwks/did_private_pkcs8.key new file mode 100644 index 0000000..9195105 --- /dev/null +++ b/didjwks/did_private_pkcs8.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgN/Dcw9C9vXFlBJxr +1Ctm8ldOkaq675uZVPX7954aHxuhRANCAASKNWA4Ye/AdJ5XVnaIsX2lAtADDspw +cXdUiNOGG9W0FEnxUoVgFBhJzcduFdm+RtY64STR5jMLYETgTovTiiS3 +-----END PRIVATE KEY----- diff --git a/package.json b/package.json index cfacace..877f9d6 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://abb5-2a02-587-8710-c200-df4c-297c-da50-b930.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://477f-2a02-587-8710-c200-6dc2-5917-be51-eb3e.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/didweb.js b/routes/didweb.js index 3d179f9..24c907a 100644 --- a/routes/didweb.js +++ b/routes/didweb.js @@ -7,20 +7,23 @@ const didWebRouter = express.Router(); const serverURL = process.env.SERVER_URL || "http://localhost:3000"; const proxyPath = process.env.PROXY_PATH || null; -didWebRouter.get(["/.well-known/did.json", "/.well-known/jwks.json"], async (req, res) => { +didWebRouter.get(["/.well-known/did.json"], async (req, res) => { let jwks = await convertPemToJwk(); // console.log(jwks); let contorller = serverURL; + let serviceURL = serverURL if (proxyPath) { contorller = serverURL + ":" + proxyPath; + serviceURL = serverURL + "/" + proxyPath; } + let didDoc = { "@context": "https://www.w3.org/ns/did/v1", - id: `${contorller}`, + id: `did:web:${contorller}`, verificationMethod: [ { - id: `${contorller}#keys-1`, //"did:web:example.com#keys-1", + id: `did:web:${contorller}#keys-1`, //"did:web:example.com#keys-1", type: "JsonWebKey2020", controller: `${contorller}`, publicKeyJwk: jwks, @@ -28,13 +31,29 @@ didWebRouter.get(["/.well-known/did.json", "/.well-known/jwks.json"], async (req ], authentication: [`${contorller}#keys-1`], - didResolutionMetadata: { - contentType: "application/did+json", - }, - didDocumentMetadata: {}, + service: [ + { + id: `did:web:${contorller}#jwks`, + type: "JsonWebKey2020", + serviceEndpoint: `${serviceURL}/.well-known/jwks.json`, + }, + ], }; res.json(didDoc); }); +didWebRouter.get(["/.well-known/jwks.json"], async (req, res) => { + let contorller = serverURL; + if (proxyPath) { + contorller = serverURL + ":" + proxyPath; + } + let jwks = await convertPemToJwk(); + let result = { + keys: [{ ...jwks, use: "sig", kid: `${contorller}#keys-1` }], + }; + + res.json(result); +}); + export default didWebRouter; diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index b1d993f..f08d574 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -125,9 +125,9 @@ router.get(["/credential-offer-tx-code/:id"], (req, res) => { router.get(["/offer-no-code"], async (req, res) => { const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); const credentialType = req.query.credentialType - ? req.query.credentialType - : "VerifiablePortableDocumentA2SDJWT"; - + ? req.query.credentialType + : "VerifiablePortableDocumentA2SDJWT"; + const preSessions = getPreCodeSessions(); if (preSessions.sessions.indexOf(uuid) < 0) { preSessions.sessions.push(uuid); @@ -293,6 +293,7 @@ router.post("/credential", async (req, res) => { let payload = {}; if (format === "jwt_vc_json") { + console.log("jwt ", requestedCredentials); if (requestedCredentials && requestedCredentials[0] === "PID") { payload = createPIDPayload(token, serverURL, decodedHeaderSubjectDID); } else if ( @@ -327,6 +328,8 @@ router.post("/credential", async (req, res) => { }); } else if (format === "vc+sd-jwt") { let vct = requestBody.vct; + console.log("vc+sd-jwt ", vct); + let decodedHeaderSubjectDID; if (requestBody.proof && requestBody.proof.jwt) { // console.log(requestBody.proof.jwt) @@ -362,14 +365,10 @@ router.post("/credential", async (req, res) => { credPayload = VerifiableFerryBoardingPassCredentialSDJWT( decodedHeaderSubjectDID ); - }else if (credType === "VerifiablePortableDocumentA1SDJWT") { - credPayload = getGenericSDJWTData( - decodedHeaderSubjectDID - ); - }else if (credType === "VerifiablePortableDocumentA2SDJWT") { - credPayload = getGenericSDJWTData( - decodedHeaderSubjectDID - ); + } else if (credType === "VerifiablePortableDocumentA1SDJWT") { + credPayload = getGenericSDJWTData(decodedHeaderSubjectDID); + } else if (credType === "VerifiablePortableDocumentA2SDJWT") { + credPayload = getGenericSDJWTData(decodedHeaderSubjectDID); } const cnf = { jwk: holderJWKS }; @@ -387,6 +386,14 @@ router.post("/credential", async (req, res) => { }, credPayload.disclosureFrame ); + + console.log("sending credential"); + console.log({ + format: "vc+sd-jwt", + credential: credential, + c_nonce: generateNonce(), + c_nonce_expires_in: 86400, + }); res.json({ format: "vc+sd-jwt", diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index ce0fe58..2baaa97 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -21,6 +21,7 @@ import TimedArray from "../utils/timedArray.js"; const verifierRouter = express.Router(); const serverURL = process.env.SERVER_URL || "http://localhost:3000"; +const proxyPath = process.env.PROXY_PATH || null; const privateKey = fs.readFileSync("./private-key.pem", "utf-8"); const publicKeyPem = fs.readFileSync("./public-key.pem", "utf-8"); @@ -196,10 +197,6 @@ verifierRouter.get("/generateVPRequestx509", async (req, res) => { }); verifierRouter.get("/x509VPrequest/:id", async (req, res) => { - //TODO pass state and nonce to the jwt request - const stateParam = req.params.id; //req.query.id ? req.query.id : uuidv4(); - const nonce = generateNonce(16); - const uuid = req.params.id ? req.params.id : uuidv4(); const response_uri = serverURL + "/direct_post" + "/" + uuid; @@ -228,7 +225,80 @@ verifierRouter.get("/x509VPrequest/:id", async (req, res) => { client_metadata ); - console.log(signedVPJWT) + console.log(signedVPJWT); + res.type("text/plain").send(signedVPJWT); +}); + +/* ******************************************************* + CLIENT_ID_SCHEME did:jwks +*********************************************************** */ +verifierRouter.get("/generateVPRequestDidjwks", async (req, res) => { + const uuid = req.params.uuid ? req.params.uuid : uuidv4(); + + let contorller = serverURL; + if (proxyPath) { + contorller = serverURL + ":" + proxyPath; + } + const client_id = `did:web:${contorller}`; + let request_uri = `${serverURL}/didjwks/${uuid}`; + let vpRequest = + "openid4vp://?client_id=" + + encodeURIComponent(client_id) + + "&request_uri=" + + encodeURIComponent(request_uri); + + let code = qr.image(vpRequest, { + type: "png", + ec_level: "M", + size: 20, + margin: 10, + }); + let mediaType = "PNG"; + let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); + res.json({ + qr: encodedQR, + deepLink: vpRequest, + sessionId: uuid, + }); +}); + +verifierRouter.get("/didjwks/:id", async (req, res) => { + const uuid = req.params.id ? req.params.id : uuidv4(); + const response_uri = serverURL + "/direct_post" + "/" + uuid; + + const client_metadata = { + client_name: "UAegean EWC Verifier", + logo_uri: "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + location: "Greece", + cover_uri: "string", + description: "EWC pilot case verification", + }; + + let privateKeyPem = fs.readFileSync("./didjwks/did_private_pkcs8.key", "utf8"); + + let contorller = serverURL; + if (proxyPath) { + contorller = serverURL + ":" + proxyPath; + } + const clientId = `did:web:${contorller}`; + sessions.push(uuid); + verificationSessions.push({ + uuid: uuid, + status: "pending", + claims: null, + }); + + let signedVPJWT = await buildVpRequestJWT( + clientId, + response_uri, + presentation_definition_sdJwt, + privateKeyPem, + "did:jwks", + client_metadata, + `did:web:${serverURL}#keys-1` + ); + + console.log(signedVPJWT); res.type("text/plain").send(signedVPJWT); }); diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index f17c166..e385542 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -107,10 +107,14 @@ export async function buildVpRequestJWT( client_id, redirect_uri, presentation_definition, - privateKey="", + privateKey = "", client_id_scheme = "redirect_uri", // Default to "redirect_uri" - client_metadata = {} // Default to an empty object + client_metadata = {}, + kid =null // Default to an empty object ) { + const nonce = generateNonce(16); + const state = generateNonce(16); + if (client_id_scheme === "x509_san_dns") { privateKey = fs.readFileSync("./x509/client_private_pkcs8.key", "utf8"); const certificate = fs.readFileSync( @@ -131,8 +135,8 @@ export async function buildVpRequestJWT( client_id_scheme: client_id_scheme, presentation_definition: presentation_definition, response_uri: redirect_uri, - nonce: "n-0S6_WzA2Mj", - state: "af0ifjsldkj", + nonce: nonce, + state: state, client_metadata: client_metadata, // }; @@ -151,7 +155,7 @@ export async function buildVpRequestJWT( .setProtectedHeader(header) .sign(await jose.importPKCS8(privateKey, "RS256")); - return jwt + return jwt; // Conditional signing based on client_id_scheme @@ -168,6 +172,49 @@ export async function buildVpRequestJWT( // return jwtPayload; // } + } else if (client_id_scheme === "did:jwks") { + + const signingKey = { + kty: "EC", + x: "ijVgOGHvwHSeV1Z2iLF9pQLQAw7KcHF3VIjThhvVtBQ", + y: "SfFShWAUGEnNx24V2b5G1jrhJNHmMwtgROBOi9OKJLc", + crv: "P-256", + use: "sig", + kid: kid, + }; + + // Convert the private key to a KeyLike object + const privateKeyObj = await jose.importPKCS8( + privateKey, + signingKey.alg || "ES256" + ); + + const jwtPayload = { + response_type: "vp_token", + response_mode: "direct_post", + client_id: client_id, // DID the did of the verifier!!!!!! + client_id_scheme: client_id_scheme, + presentation_definition: presentation_definition, + redirect_uri: redirect_uri, + nonce: nonce, + state: state, + client_metadata: client_metadata, + }; + + // JWT header + const header = { + alg: signingKey.alg || "ES256", + typ: "JWT", + kid: kid, + }; + + const jwt = await new jose.SignJWT(jwtPayload) + .setProtectedHeader(header) + .sign(privateKeyObj); + + return jwt; + + // Conditional signing based on client_id_scheme } else { throw new Error("not supported client_id_scheme:" + client_id_scheme); } From 7313dffee74d3a04363da42ff7755c347d4bf191 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Fri, 18 Oct 2024 16:41:11 +0300 Subject: [PATCH 26/37] fix cnf in issuance --- routes/preAuthSDjwRoutes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index f08d574..d2b98b6 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -371,7 +371,7 @@ router.post("/credential", async (req, res) => { credPayload = getGenericSDJWTData(decodedHeaderSubjectDID); } - const cnf = { jwk: holderJWKS }; + const cnf = { jwk: holderJWKS.jwk }; // console.log(credType); // console.log(credPayload.claims); // console.log(credPayload.disclosureFrame); From 17f73d0066a35b591aaec1596ed843e43223df5d Mon Sep 17 00:00:00 2001 From: George J Padayatti Date: Fri, 18 Oct 2024 21:06:53 +0530 Subject: [PATCH 27/37] Fix #24: Credential offer URI must be URI encoded if containing query params Signed-off-by: George J Padayatti --- routes/codeFlowSdJwtRoutes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index bfcb038..07e4d45 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -40,7 +40,9 @@ codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { codeSessions.sessions.push(uuid); // codeSessions.results.push({ sessionId: uuid, status: "pending" }); } - let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-code-sd-jwt/${uuid}?scheme=${client_id_scheme}&type=${credentialType}`; + + let encodedCredentialOfferUri = encodeURIComponent(`${serverURL}/credential-offer-code-sd-jwt/${uuid}?scheme=${client_id_scheme}&credentialType=${credentialType}`) + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${encodedCredentialOfferUri}`; let code = qr.image(credentialOffer, { type: "png", From 34a4399240752b038a77fecf5a971d1eab6bd81f Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Fri, 18 Oct 2024 18:56:01 +0300 Subject: [PATCH 28/37] missing fields in PID credential issuance --- utils/credPayloadUtil.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/utils/credPayloadUtil.js b/utils/credPayloadUtil.js index 07dc64d..31a52d5 100644 --- a/utils/credPayloadUtil.js +++ b/utils/credPayloadUtil.js @@ -305,16 +305,22 @@ export const createAllianceIDPayload = (serverURL, decodedHeaderSubjectDID) => { // SD-JWT HELPERS export const getPIDSDJWTData = (decodedHeaderSubjectDID) => { + const currentTimestamp = new Date().getTime(); + const expTimestamp = currentDate.setFullYear(currentDate.getFullYear() + 1) const claims = { id: decodedHeaderSubjectDID || "", given_name: "John", family_name: "Doe", birth_date: "1990-01-01", age_over_18: true, + issuance_date: currentTimestamp, + expiry_date: expTimestamp.getTime(), + issuing_authority: "UAegean Test Issuer", + issuing_country: "Greece", }; const disclosureFrame = { - _sd: ["id", "given_name", "family_name", "birth_date", "age_over_18"] + _sd: ["id", "given_name", "family_name", "birth_date", "age_over_18", "expiry_date","issuance_date","issuing_authority","issuing_country"] }; return { claims, disclosureFrame }; From 8523f41f68f6e488dc65c20391e3ae1aee5b6d9b Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Tue, 22 Oct 2024 11:37:00 +0300 Subject: [PATCH 29/37] bug in ITB sessions --- package.json | 2 +- routes/codeFlowJwtRoutes.js | 18 ++++++++++++++++++ routes/codeFlowSdJwtRoutes.js | 11 +++++++---- routes/preAuthSDjwRoutes.js | 9 ++++++++- routes/verifierRoutes.js | 23 +++++++++++++---------- utils/credPayloadUtil.js | 3 ++- 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 877f9d6..02e1a68 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://477f-2a02-587-8710-c200-6dc2-5917-be51-eb3e.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://c1a4-2a02-587-8710-c200-e22b-28d1-dd73-c2db.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowJwtRoutes.js b/routes/codeFlowJwtRoutes.js index 817ee9d..2a987e9 100644 --- a/routes/codeFlowJwtRoutes.js +++ b/routes/codeFlowJwtRoutes.js @@ -77,4 +77,22 @@ export function updateIssuerStateWithAuthCode( } } +export function updateIssuerStateWithAuthCodeAfterVP( + code, + issuerState, + issuerSessions, + codeFlowRequestsResults, + codeFlowRequests +) { + let index = issuerSessions.indexOf(issuerState); + if (index >= 0) { + codeFlowRequestsResults[index].sessionId = code; + codeFlowRequests[index].sessionId = code; + } else { + console.log("could not find session "+issuerSessions +"issuer state will not be updated with code "+code); + } +} + + + export default codeFlowRouter; diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 28c85fe..0e0230c 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -15,7 +15,7 @@ import qr from "qr-image"; import imageDataURI from "image-data-uri"; import { streamToBuffer } from "@jorgeferrero/stream-to-buffer"; import { generateNonce, buildVpRequestJWT } from "../utils/cryptoUtils.js"; -import { updateIssuerStateWithAuthCode } from "./codeFlowJwtRoutes.js"; +import { updateIssuerStateWithAuthCode, updateIssuerStateWithAuthCodeAfterVP } from "./codeFlowJwtRoutes.js"; const codeFlowRouterSDJWT = express.Router(); @@ -498,10 +498,12 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { authorizatiton_details ); - updateIssuerStateWithAuthCode( + // THE WALLET SENDS A DIFFERENT SATE (THAT IS THE STATE OF THE VP NOT THE VCI) + // SO A DIFFERENT UPDATE IS REQUIRED HERE + updateIssuerStateWithAuthCodeAfterVP( authorizationCode, - state, - codeSessions.walletSessions, + uuid, + codeSessions.sessions, codeSessions.results, codeSessions.requests ); @@ -510,6 +512,7 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { const redirectUrl = `${codeSessions.requests[sessionIndex].redirectUri}?code=${authorizationCode}&state=${state}`; return res.redirect(302, redirectUrl); }else{ + console.log("issuance session not found " + uuid); return res.sendStatus(500); } diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index d2b98b6..9d55769 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -215,6 +215,13 @@ router.post("/token_endpoint", async (req, res) => { } else { if (grantType == "authorization_code") { const codeSessions = getAuthCodeSessions(); + const index = codeSessions.results.findIndex( + (result) => result.sessionId === code + ); + if (index > 0) { + codeSessions.results[index].status = "success"; + } + //TODO if PKCE validattiton fails the flow should validatePKCE( codeSessions.requests, code, @@ -386,7 +393,7 @@ router.post("/credential", async (req, res) => { }, credPayload.disclosureFrame ); - + console.log("sending credential"); console.log({ format: "vc+sd-jwt", diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index 2baaa97..e0ef9b8 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -81,7 +81,7 @@ let verificationResultsHistory = new TimedArray(30000); //cache data for 30sec *********************************************************** */ verifierRouter.get("/generateVPRequest", async (req, res) => { - const stateParam = req.query.id ? req.query.id : uuidv4(); + const stateParam = req.query.sessionId ? req.query.sessionId : uuidv4(); const nonce = generateNonce(16); // The Verifier may send an Authorization Request as Request Object by value @@ -89,16 +89,16 @@ verifierRouter.get("/generateVPRequest", async (req, res) => { // The Verifier articulates requirements of the Credential(s) that // are requested using presentation_definition and presentation_definition_uri - const uuid = req.params.id ? req.params.id : uuidv4(); + // const uuid = req.params.id ? req.params.id : uuidv4(); //url.searchParams.get("presentation_definition"); - const response_uri = serverURL + "/direct_post" + "/" + uuid; + const response_uri = serverURL + "/direct_post" + "/" + stateParam; const presentation_definition_uri = serverURL + "/presentation-definition/itbsdjwt"; const client_metadata_uri = serverURL + "/client-metadata"; - const clientId = serverURL + "/direct_post" + "/" + uuid; - sessions.push(uuid); + const clientId = serverURL + "/direct_post" + "/" + stateParam; + sessions.push(stateParam); verificationSessions.push({ - uuid: uuid, + uuid: stateParam, status: "pending", claims: null, }); @@ -127,7 +127,7 @@ verifierRouter.get("/generateVPRequest", async (req, res) => { // res.json({ vpRequest: vpRequest }); }); -//***********PRESENTATION DEFINITON endpoint to support presentation_definition_uri Parameter */ +//PRESENTATION DEFINITON endpoint to support presentation_definition_uri Parameter */ verifierRouter.get("/presentation-definition/:type", async (req, res) => { const { type } = req.params; const presentationDefinitions = { @@ -171,7 +171,7 @@ verifierRouter.get("/client-metadata", async (req, res) => { CLIENT_ID_SCHEME x509_dns_san *********************************************************** */ verifierRouter.get("/generateVPRequestx509", async (req, res) => { - const uuid = req.params.uuid ? req.params.uuid : uuidv4(); + const uuid = req.params.sessionId ? req.params.sessionId : uuidv4(); let client_id = "dss.aegean.gr"; let request_uri = `${serverURL}/x509VPrequest/${uuid}`; @@ -233,7 +233,7 @@ verifierRouter.get("/x509VPrequest/:id", async (req, res) => { CLIENT_ID_SCHEME did:jwks *********************************************************** */ verifierRouter.get("/generateVPRequestDidjwks", async (req, res) => { - const uuid = req.params.uuid ? req.params.uuid : uuidv4(); + const uuid = req.params.sessionId ? req.params.sessionId : uuidv4(); let contorller = serverURL; if (proxyPath) { @@ -274,7 +274,10 @@ verifierRouter.get("/didjwks/:id", async (req, res) => { description: "EWC pilot case verification", }; - let privateKeyPem = fs.readFileSync("./didjwks/did_private_pkcs8.key", "utf8"); + let privateKeyPem = fs.readFileSync( + "./didjwks/did_private_pkcs8.key", + "utf8" + ); let contorller = serverURL; if (proxyPath) { diff --git a/utils/credPayloadUtil.js b/utils/credPayloadUtil.js index 31a52d5..7ef84b1 100644 --- a/utils/credPayloadUtil.js +++ b/utils/credPayloadUtil.js @@ -306,6 +306,7 @@ export const createAllianceIDPayload = (serverURL, decodedHeaderSubjectDID) => { export const getPIDSDJWTData = (decodedHeaderSubjectDID) => { const currentTimestamp = new Date().getTime(); + const currentDate = new Date() const expTimestamp = currentDate.setFullYear(currentDate.getFullYear() + 1) const claims = { id: decodedHeaderSubjectDID || "", @@ -314,7 +315,7 @@ export const createAllianceIDPayload = (serverURL, decodedHeaderSubjectDID) => { birth_date: "1990-01-01", age_over_18: true, issuance_date: currentTimestamp, - expiry_date: expTimestamp.getTime(), + expiry_date: expTimestamp,//expTimestamp.getTime(), issuing_authority: "UAegean Test Issuer", issuing_country: "Greece", }; From e16ab11f98cf3ce9efe08ca63e4a9b1680d52544 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Tue, 22 Oct 2024 20:20:12 +0300 Subject: [PATCH 30/37] did:jwks --- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 86 +++++++++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 02e1a68..68033a5 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://c1a4-2a02-587-8710-c200-e22b-28d1-dd73-c2db.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://4f38-2a02-587-8710-c200-4005-29fc-1d2a-93fe.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 0e0230c..ecffff8 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -15,11 +15,15 @@ import qr from "qr-image"; import imageDataURI from "image-data-uri"; import { streamToBuffer } from "@jorgeferrero/stream-to-buffer"; import { generateNonce, buildVpRequestJWT } from "../utils/cryptoUtils.js"; -import { updateIssuerStateWithAuthCode, updateIssuerStateWithAuthCodeAfterVP } from "./codeFlowJwtRoutes.js"; +import { + updateIssuerStateWithAuthCode, + updateIssuerStateWithAuthCodeAfterVP, +} from "./codeFlowJwtRoutes.js"; const codeFlowRouterSDJWT = express.Router(); const serverURL = process.env.SERVER_URL || "http://localhost:3000"; +const proxyPath = process.env.PROXY_PATH || null; const privateKey = fs.readFileSync("./private-key.pem", "utf-8"); // ****************************************************************** @@ -41,7 +45,9 @@ codeFlowRouterSDJWT.get(["/offer-code-sd-jwt"], async (req, res) => { // codeSessions.results.push({ sessionId: uuid, status: "pending" }); } - let encodedCredentialOfferUri = encodeURIComponent(`${serverURL}/credential-offer-code-sd-jwt/${uuid}?scheme=${client_id_scheme}&credentialType=${credentialType}`) + let encodedCredentialOfferUri = encodeURIComponent( + `${serverURL}/credential-offer-code-sd-jwt/${uuid}?scheme=${client_id_scheme}&credentialType=${credentialType}` + ); let credentialOffer = `openid-credential-offer://?credential_offer_uri=${encodedCredentialOfferUri}`; let code = qr.image(credentialOffer, { @@ -315,7 +321,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { */ if (client_id_scheme == "redirect_uri") { - console.log("client_id_scheme redirect_uri") + console.log("client_id_scheme redirect_uri"); const vpRedirectURI = "openid4vp://"; // console.log( // redirect_uri + " but i will request the vp based on " + vpRedirectURI @@ -340,7 +346,7 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { // console.log("redirectUrl", redirectUrl); return res.redirect(302, redirectUrl); } else if (client_id_scheme == "x509_san_dns") { - console.log("client_id_scheme x509_san_dns") + console.log("client_id_scheme x509_san_dns"); //TODO // let client_id = "dss.aegean.gr"; @@ -390,7 +396,18 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { "&request_uri=" + encodeURIComponent(request_uri); - return res.redirect(302, vpRequest); + return res.redirect(302, vpRequest); + } else if (client_id_scheme == "did:jwk") { + console.log("client_id_scheme did:jwk"); + let request_uri = `${serverURL}/didJwksVPrequest_dynamic/${issuerState}`; + const clientId = "dss.aegean.gr"; + let vpRequest = + "openid4vp://?client_id=" + + encodeURIComponent(clientId) + + "&request_uri=" + + encodeURIComponent(request_uri); + + return res.redirect(302, vpRequest); } } }); @@ -400,7 +417,7 @@ codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { //TODO pass state and nonce to the jwt request const uuid = req.params.id ? req.params.id : uuidv4(); - const response_uri = serverURL + "/direct_post_vci/"+uuid; + const response_uri = serverURL + "/direct_post_vci/" + uuid; const client_metadata = { client_name: "UAegean EWC Verifier", @@ -409,9 +426,9 @@ codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { cover_uri: "string", description: "EWC pilot case verification", }; - const presentation_definition_sdJwt = JSON.parse( - fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") - ); + const presentation_definition_sdJwt = JSON.parse( + fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") + ); const clientId = "dss.aegean.gr"; let signedVPJWT = await buildVpRequestJWT( @@ -423,7 +440,47 @@ codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { client_metadata ); - console.log(signedVPJWT) + console.log(signedVPJWT); + res.type("text/plain").send(signedVPJWT); +}); + +codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { + //TODO build vp request appropriately using did:jwks + + const uuid = req.params.id ? req.params.id : uuidv4(); + const response_uri = serverURL + "/direct_post" + "/" + uuid; + + const client_metadata = { + client_name: "UAegean EWC Verifier", + logo_uri: "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + location: "Greece", + cover_uri: "string", + description: "EWC pilot case verification", + }; + + const privateKeyPem = fs.readFileSync( + "./didjwks/did_private_pkcs8.key", + "utf8" + ); + + let contorller = serverURL; + if (proxyPath) { + contorller = serverURL + ":" + proxyPath; + } + const clientId = `did:web:${contorller}`; + const presentation_definition_sdJwt = JSON.parse( + fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") + ); + + let signedVPJWT = await buildVpRequestJWT( + clientId, + response_uri, + presentation_definition_sdJwt, + privateKeyPem, + "did:jwks", + client_metadata, + `did:web:${serverURL}#keys-1` + ); res.type("text/plain").send(signedVPJWT); }); @@ -484,7 +541,7 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { let jwt = req.body["vp_token"]; console.log("direct_post_vci received jwt is::"); console.log(jwt); - const uuid = req.params.id + const uuid = req.params.id; // const authorizatiton_details = getSessionsAuthorizationDetail().get(state); @@ -507,15 +564,14 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { codeSessions.results, codeSessions.requests ); - let sessionIndex = codeSessions.sessions.indexOf(uuid) - if(sessionIndex >= 0){ + let sessionIndex = codeSessions.sessions.indexOf(uuid); + if (sessionIndex >= 0) { const redirectUrl = `${codeSessions.requests[sessionIndex].redirectUri}?code=${authorizationCode}&state=${state}`; return res.redirect(302, redirectUrl); - }else{ + } else { console.log("issuance session not found " + uuid); return res.sendStatus(500); } - } else { return res.sendStatus(500); } From b90eb070295029cef6974199843955e48ba97d38 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Wed, 23 Oct 2024 13:58:18 +0300 Subject: [PATCH 31/37] bug with ITB session --- routes/codeFlowJwtRoutes.js | 13 ++++- routes/codeFlowSdJwtRoutes.js | 96 ++--------------------------------- routes/preAuthSDjwRoutes.js | 11 +++- routes/verifierRoutes.js | 25 ++++++++- 4 files changed, 46 insertions(+), 99 deletions(-) diff --git a/routes/codeFlowJwtRoutes.js b/routes/codeFlowJwtRoutes.js index 2a987e9..ed172dc 100644 --- a/routes/codeFlowJwtRoutes.js +++ b/routes/codeFlowJwtRoutes.js @@ -86,8 +86,17 @@ export function updateIssuerStateWithAuthCodeAfterVP( ) { let index = issuerSessions.indexOf(issuerState); if (index >= 0) { - codeFlowRequestsResults[index].sessionId = code; - codeFlowRequests[index].sessionId = code; + console.log("codeFlowRequestResults of " + index) + console.log(codeFlowRequestsResults[index]) + console.log(codeFlowRequests[index]) + + codeFlowRequestsResults[index]["sessionId"] = code; + codeFlowRequests[index]["sessionId"] = code; + + console.log("UPDATED codeFlowRequestResults of " + index) + console.log(codeFlowRequestsResults[index]) + console.log(codeFlowRequests[index]) + } else { console.log("could not find session "+issuerSessions +"issuer state will not be updated with code "+code); } diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index ecffff8..8036c9c 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -347,47 +347,6 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { return res.redirect(302, redirectUrl); } else if (client_id_scheme == "x509_san_dns") { console.log("client_id_scheme x509_san_dns"); - - //TODO - // let client_id = "dss.aegean.gr"; - // let request_uri = `${serverURL}/x509VPrequest/${issuerState}`; - // // let vpRequest_url = - // // "openid4vp://?client_id=" + - // // encodeURIComponent(client_id) + - // // "&request_uri=" + - // // encodeURIComponent(request_uri); - // const redirectUrl = `openid4vp://?state=${issuerState}&client_id=${client_id}&response_uri=${serverURL}/direct_post_vci&response_type=id_token&response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=${request_uri}`; - - // return res.redirect(302, redirectUrl); - // const stateParam = issuerState - // const nonce = generateNonce(16); - - // const uuid = issuerState - // const response_uri = serverURL + "/direct_post" + "/" + uuid; - - // const client_metadata = { - // client_name: "UAegean EWC Verifier", - // logo_uri: - // "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", - // location: "Greece", - // cover_uri: "string", - // description: "EWC pilot case verification", - // }; - - // const clientId = "dss.aegean.gr"; - // const presentation_definition_sdJwt = JSON.parse( - // fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") - // ); - - // let signedVPJWT = await buildVpRequestJWT( - // clientId, - // response_uri, - // presentation_definition_sdJwt, - // "", - // "x509_san_dns", - // client_metadata - // ); - // res.type("text/plain").send(signedVPJWT); let request_uri = `${serverURL}/x509VPrequest_dynamic/${issuerState}`; const clientId = "dss.aegean.gr"; let vpRequest = @@ -414,8 +373,6 @@ codeFlowRouterSDJWT.get("/authorize", async (req, res) => { // Dynamic VP request by reference endpoint codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { - //TODO pass state and nonce to the jwt request - const uuid = req.params.id ? req.params.id : uuidv4(); const response_uri = serverURL + "/direct_post_vci/" + uuid; @@ -445,10 +402,10 @@ codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { }); codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { - //TODO build vp request appropriately using did:jwks - const uuid = req.params.id ? req.params.id : uuidv4(); - const response_uri = serverURL + "/direct_post" + "/" + uuid; + const response_uri = serverURL + "/direct_post_vci/" + uuid; + + const client_metadata = { client_name: "UAegean EWC Verifier", @@ -484,53 +441,6 @@ codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { res.type("text/plain").send(signedVPJWT); }); -// codeFlowRouterSDJWT.get("/request_uri_dynamic", async (req, res) => { -// let uuid = uuidv4(); -// let client_id = serverURL + "/direct_post" + "/" + uuid; -// const response_uri = serverURL + "/direct_post" + "/" + uuid; - -// const __filename = fileURLToPath(import.meta.url); -// const __dirname = path.dirname(__filename); -// // Construct the absolute path to verifier-config.json -// const configPath = path.join(__dirname, "..", "data", "verifier-config.json"); -// const presentation_definition_sdJwt = JSON.parse( -// fs.readFileSync( -// path.join(__dirname, "..", "data", "presentation_definition_sdjwt.json") -// ) -// ); - -// // Read and parse the JSON file -// // const clientMetadata = JSON.parse(fs.readFileSync(configPath, "utf-8")); - -// // clientMetadata.presentation_definition_uri = -// // serverURL + "/presentation-definition/" + uuid; -// // clientMetadata.redirect_uris = [response_uri]; -// // clientMetadata.client_id = client_id; -// const clientMetadata = { -// vp_formats: { -// jwt_vp: { -// alg: ["EdDSA", "ES256K"], -// }, -// ldp_vp: { -// proof_type: ["Ed25519Signature2018"], -// }, -// }, -// }; - -// const vpRequestJWT = buildVpRequestJWT( -// client_id, -// response_uri, -// presentation_definition_sdJwt, -// privateKey, -// "redirect_uri", -// clientMetadata -// ); - -// // console.log("Dynamic VP request ") -// // console.log(JSON.stringify(vpRequestJWT, null, 2)); - -// res.send(vpRequestJWT); -// }); /* presentation by the wallet during an Issuance part of the Dynamic Credential Request diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 9d55769..0c14710 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -215,12 +215,19 @@ router.post("/token_endpoint", async (req, res) => { } else { if (grantType == "authorization_code") { const codeSessions = getAuthCodeSessions(); + console.log("codeSessions ==> grantType == authorization_code") + console.log(codeSessions) + console.log(code) const index = codeSessions.results.findIndex( (result) => result.sessionId === code ); - if (index > 0) { - codeSessions.results[index].status = "success"; + console.log(index) + if (index >= 0) { + codeSessions.results[index]["status"] = "success"; } + console.log("Updatetd Code sessions") + console.log(codeSessions) + //TODO if PKCE validattiton fails the flow should validatePKCE( codeSessions.requests, diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index e0ef9b8..03add44 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -171,7 +171,7 @@ verifierRouter.get("/client-metadata", async (req, res) => { CLIENT_ID_SCHEME x509_dns_san *********************************************************** */ verifierRouter.get("/generateVPRequestx509", async (req, res) => { - const uuid = req.params.sessionId ? req.params.sessionId : uuidv4(); + const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); let client_id = "dss.aegean.gr"; let request_uri = `${serverURL}/x509VPrequest/${uuid}`; @@ -181,6 +181,16 @@ verifierRouter.get("/generateVPRequestx509", async (req, res) => { "&request_uri=" + encodeURIComponent(request_uri); + console.log(`pushing to sessions ${uuid}`) + sessions.push(uuid); + verificationSessions.push({ + uuid: uuid, + status: "pending", + claims: null, + }); + console.log(`verification sessions`) + console.log(verificationSessions) + let code = qr.image(vpRequest, { type: "png", ec_level: "M", @@ -233,7 +243,7 @@ verifierRouter.get("/x509VPrequest/:id", async (req, res) => { CLIENT_ID_SCHEME did:jwks *********************************************************** */ verifierRouter.get("/generateVPRequestDidjwks", async (req, res) => { - const uuid = req.params.sessionId ? req.params.sessionId : uuidv4(); + const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); let contorller = serverURL; if (proxyPath) { @@ -247,6 +257,17 @@ verifierRouter.get("/generateVPRequestDidjwks", async (req, res) => { "&request_uri=" + encodeURIComponent(request_uri); + console.log(`pushing to sessions ${uuid}`) + + sessions.push(uuid); + verificationSessions.push({ + uuid: uuid, + status: "pending", + claims: null, + }); + console.log(`verification sessions`) + console.log(verificationSessions) + let code = qr.image(vpRequest, { type: "png", ec_level: "M", From 526fe7a8d2055d13acb88c5242d98a07b99e6603 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 24 Oct 2024 11:19:41 +0300 Subject: [PATCH 32/37] bug with vp direct_post endpoint with redirect uri --- package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 68033a5..ca6ab78 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://4f38-2a02-587-8710-c200-4005-29fc-1d2a-93fe.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://db65-2a02-587-8704-c700-9303-e0f5-b98d-e1a2.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 8036c9c..93040fc 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -405,8 +405,6 @@ codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { const uuid = req.params.id ? req.params.id : uuidv4(); const response_uri = serverURL + "/direct_post_vci/" + uuid; - - const client_metadata = { client_name: "UAegean EWC Verifier", logo_uri: "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", @@ -441,7 +439,6 @@ codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { res.type("text/plain").send(signedVPJWT); }); - /* presentation by the wallet during an Issuance part of the Dynamic Credential Request */ @@ -475,9 +472,11 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { codeSessions.requests ); let sessionIndex = codeSessions.sessions.indexOf(uuid); + let issuanceState = codeSessions.results[sessionIndex].state if (sessionIndex >= 0) { - const redirectUrl = `${codeSessions.requests[sessionIndex].redirectUri}?code=${authorizationCode}&state=${state}`; - return res.redirect(302, redirectUrl); + const redirectUrl = `${codeSessions.requests[sessionIndex].redirectUri}?code=${authorizationCode}&state=${issuanceState}`; + // return //res.redirect(302, redirectUrl); + return res.send({ redirect_uri: redirectUrl }); } else { console.log("issuance session not found " + uuid); return res.sendStatus(500); From 0f96e48e78b065a134a085114563130781b8202c Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 24 Oct 2024 13:21:39 +0300 Subject: [PATCH 33/37] autht code debugging --- package.json | 2 +- routes/codeFlowJwtRoutes.js | 12 ++++++------ routes/codeFlowSdJwtRoutes.js | 37 ++++++++++++++++++++++++----------- routes/preAuthSDjwRoutes.js | 2 +- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index ca6ab78..b9dd23a 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://db65-2a02-587-8704-c700-9303-e0f5-b98d-e1a2.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://1479-2a02-587-8704-c700-9303-e0f5-b98d-e1a2.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowJwtRoutes.js b/routes/codeFlowJwtRoutes.js index ed172dc..01e1fa8 100644 --- a/routes/codeFlowJwtRoutes.js +++ b/routes/codeFlowJwtRoutes.js @@ -86,16 +86,16 @@ export function updateIssuerStateWithAuthCodeAfterVP( ) { let index = issuerSessions.indexOf(issuerState); if (index >= 0) { - console.log("codeFlowRequestResults of " + index) - console.log(codeFlowRequestsResults[index]) - console.log(codeFlowRequests[index]) + // console.log("codeFlowRequestResults of " + index) + // console.log(codeFlowRequestsResults[index]) + // console.log(codeFlowRequests[index]) codeFlowRequestsResults[index]["sessionId"] = code; codeFlowRequests[index]["sessionId"] = code; - console.log("UPDATED codeFlowRequestResults of " + index) - console.log(codeFlowRequestsResults[index]) - console.log(codeFlowRequests[index]) + // console.log("UPDATED codeFlowRequestResults of " + index) + // console.log(codeFlowRequestsResults[index]) + // console.log(codeFlowRequests[index]) } else { console.log("could not find session "+issuerSessions +"issuer state will not be updated with code "+code); diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 93040fc..c255ea2 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -462,21 +462,36 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { authorizatiton_details ); - // THE WALLET SENDS A DIFFERENT SATE (THAT IS THE STATE OF THE VP NOT THE VCI) - // SO A DIFFERENT UPDATE IS REQUIRED HERE - updateIssuerStateWithAuthCodeAfterVP( - authorizationCode, - uuid, - codeSessions.sessions, - codeSessions.results, - codeSessions.requests - ); + // // THE WALLET SENDS A DIFFERENT SATE (THAT IS THE STATE OF THE VP NOT THE VCI) + // // SO A DIFFERENT UPDATE IS REQUIRED HERE + // updateIssuerStateWithAuthCodeAfterVP( + // authorizationCode, + // uuid, + // codeSessions.sessions, + // codeSessions.results, + // codeSessions.requests + // ); + let sessionIndex = codeSessions.sessions.indexOf(uuid); - let issuanceState = codeSessions.results[sessionIndex].state + let issuanceState = codeSessions.results[sessionIndex].state; if (sessionIndex >= 0) { + // updateIssuerStateWithAuthCode( + // authorizationCode, + // uuid, + // codeSessions.results, + // codeSessions.requests + // ); + updateIssuerStateWithAuthCodeAfterVP( + authorizationCode, + uuid, + codeSessions.sessions, + codeSessions.results, + codeSessions.requests + ); + const redirectUrl = `${codeSessions.requests[sessionIndex].redirectUri}?code=${authorizationCode}&state=${issuanceState}`; // return //res.redirect(302, redirectUrl); - return res.send({ redirect_uri: redirectUrl }); + return res.send({redirect_uri: redirectUrl }); } else { console.log("issuance session not found " + uuid); return res.sendStatus(500); diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 0c14710..1cb34b3 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -215,7 +215,7 @@ router.post("/token_endpoint", async (req, res) => { } else { if (grantType == "authorization_code") { const codeSessions = getAuthCodeSessions(); - console.log("codeSessions ==> grantType == authorization_code") + console.log("codeSessions ==> grantType == authorization_code") console.log(codeSessions) console.log(code) const index = codeSessions.results.findIndex( From 8f867ce0061209d09bd46511632d5b9a2321d566 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Fri, 25 Oct 2024 15:57:13 +0300 Subject: [PATCH 34/37] fixed metadata for issuer and oauth --- data/issuer-config.json | 8 ++++---- data/oauth-config.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/issuer-config.json b/data/issuer-config.json index aadea02..07a67c5 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -185,7 +185,7 @@ "background_color": "#12107c", "text_color": "#FFFFFF", "logo": { - "url": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "uri": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", "alt_text": "UAegean" } } @@ -350,7 +350,7 @@ "background_color": "#12107c", "text_color": "#FFFFFF", "logo": { - "url": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "uri": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", "alt_text": "UAegean" } } @@ -497,7 +497,7 @@ "background_color": "#12107c", "text_color": "#FFFFFF", "logo": { - "url": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "uri": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", "alt_text": "UAegean" } } @@ -636,7 +636,7 @@ "background_color": "#12107c", "text_color": "#FFFFFF", "logo": { - "url": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", + "uri": "https://studyingreece.edu.gr/wp-content/uploads/2023/03/25.png", "alt_text": "UAegean" } } diff --git a/data/oauth-config.json b/data/oauth-config.json index 2708aad..eaa1e92 100644 --- a/data/oauth-config.json +++ b/data/oauth-config.json @@ -8,7 +8,7 @@ "scopes_supported": ["openid"], "response_types_supported": ["code", "vp_token", "id_token"], "response_modes_supported": ["query"], - "grant_types_supported": ["authorization_code"], + "grant_types_supported": ["authorization_code","urn:ietf:params:oauth:grant-type:pre-authorized_code"], "subject_types_supported": ["public", "pairwise"], "id_token_signing_alg_values_supported": ["ES256"], From b5f201a2e7e29172b0168293e26e2187b7da36ca Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 7 Nov 2024 10:23:28 +0200 Subject: [PATCH 35/37] changed txt for sha-256 --- data/issuer-config.json | 643 +++++++++++++++++++++++ package.json | 2 +- routes/codeFlowSdJwtRoutes.js | 9 +- routes/didweb.js | 11 +- routes/preAuthSDjwRoutes.js | 10 +- routes/verifierRoutes.js | 6 +- utils/credPayloadUtil.js | 961 +++++++++++++++++++++++----------- utils/cryptoUtils.js | 2 +- 8 files changed, 1319 insertions(+), 325 deletions(-) diff --git a/data/issuer-config.json b/data/issuer-config.json index 07a67c5..7ca88b4 100644 --- a/data/issuer-config.json +++ b/data/issuer-config.json @@ -649,6 +649,649 @@ "proof_signing_alg_values_supported": ["ES256"] } } + }, + + "VerifiableReceipt": { + "scope": "vReceipt", + "claims": { + "MonetaryTotal.lineExtensionAmount": { + "display": [ + { + "name": "Line Extension Amount", + "locale": "en-GB" + } + ] + }, + "MonetaryTotal.taxInclusiveAmount": { + "display": [ + { + "name": "Tax Inclusive Amount", + "locale": "en-GB" + } + ] + }, + "MonetaryTotal.payableAmount": { + "display": [ + { + "name": "Payable Amount", + "locale": "en-GB" + } + ] + }, + "TaxTotal.taxSubtotal_": { + "display": [ + { + "name": "Tax Subtotal", + "locale": "en-GB" + } + ] + }, + "TaxTotal.taxAmount": { + "display": [ + { + "name": "Tax Amount", + "locale": "en-GB" + } + ] + }, + "Address.streetName": { + "display": [ + { + "name": "Street Name", + "locale": "en-GB" + } + ] + }, + "Address.cityName": { + "display": [ + { + "name": "City Name", + "locale": "en-GB" + } + ] + }, + "Address.postcode": { + "display": [ + { + "name": "Postcode", + "locale": "en-GB" + } + ] + }, + "Address.countryIdentifier": { + "display": [ + { + "name": "Country", + "locale": "en-GB" + } + ] + }, + "TaxCategory.taxScheme_": { + "display": [ + { + "name": "Tax Scheme", + "locale": "en-GB" + } + ] + }, + "ItemProperty.itemPropertyName": { + "display": [ + { + "name": "Item Property Name", + "locale": "en-GB" + } + ] + }, + "ItemProperty.value": { + "display": [ + { + "name": "Item Property Value", + "locale": "en-GB" + } + ] + }, + "TaxScheme.taxSchemeName": { + "display": [ + { + "name": "Tax Scheme Name", + "locale": "en-GB" + } + ] + }, + "AllowanceCharge.amount": { + "display": [ + { + "name": "Allowance Charge Amount", + "locale": "en-GB" + } + ] + }, + "AllowanceCharge.allowanceChargeReason": { + "display": [ + { + "name": "Allowance Charge Reason", + "locale": "en-GB" + } + ] + }, + "PartyName.name": { + "display": [ + { + "name": "Party Name", + "locale": "en-GB" + } + ] + }, + "PaymentMeans.cardAccount_": { + "display": [ + { + "name": "Card Account", + "locale": "en-GB" + } + ] + }, + "PaymentMeans.paymentMeansCode": { + "display": [ + { + "name": "Payment Means Code", + "locale": "en-GB" + } + ] + }, + "PartyIdentification.iD": { + "display": [ + { + "name": "Party Identification ID", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.paymentMeans_": { + "display": [ + { + "name": "Payment Means", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.note": { + "display": [ + { + "name": "Note", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.delivery_": { + "display": [ + { + "name": "Delivery", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.taxIncludedIndicator": { + "display": [ + { + "name": "Tax Included Indicator", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.taxTotal_": { + "display": [ + { + "name": "Tax Total", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.accountingCustomerParty_": { + "display": [ + { + "name": "Accounting Customer Party", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.documentCurrencyCode": { + "display": [ + { + "name": "Document Currency Code", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.payment_": { + "display": [ + { + "name": "Payment", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.sellerSupplierParty": { + "display": [ + { + "name": "Seller Supplier Party", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.legalMonetaryTotal": { + "display": [ + { + "name": "Legal Monetary Total", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.salesDocumentReference": { + "display": [ + { + "name": "Sales Document Reference", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.iD": { + "display": [ + { + "name": "Purchase Receipt ID", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.issueDate": { + "display": [ + { + "name": "Issue Date", + "locale": "en-GB" + } + ] + }, + "PurchaseReceipt.purchaseReceiptLine-1": { + "display": [ + { + "name": "Purchase Receipt Line", + "locale": "en-GB" + } + ] + }, + "TaxSubtotal.taxableAmount": { + "display": [ + { + "name": "Taxable Amount", + "locale": "en-GB" + } + ] + }, + "TaxSubtotal.taxSubtotalTaxAmount": { + "display": [ + { + "name": "Tax Subtotal Amount", + "locale": "en-GB" + } + ] + }, + "TaxSubtotal.taxCategory_": { + "display": [ + { + "name": "Tax Category", + "locale": "en-GB" + } + ] + }, + "TaxSubtotal.percent": { + "display": [ + { + "name": "Tax Percent", + "locale": "en-GB" + } + ] + }, + "Item.commodityClassification_": { + "display": [ + { + "name": "Commodity Classification", + "locale": "en-GB" + } + ] + }, + "Item.itemInstance_": { + "display": [ + { + "name": "Item Instance", + "locale": "en-GB" + } + ] + }, + "Item.additionalItemProperty": { + "display": [ + { + "name": "Additional Item Property", + "locale": "en-GB" + } + ] + }, + "Payment.authorizationID": { + "display": [ + { + "name": "Authorization ID", + "locale": "en-GB" + } + ] + }, + "Payment.paidAmount": { + "display": [ + { + "name": "Paid Amount", + "locale": "en-GB" + } + ] + }, + "Payment.transactionID": { + "display": [ + { + "name": "Transaction ID", + "locale": "en-GB" + } + ] + }, + "SupplierParty.party_": { + "display": [ + { + "name": "Supplier Party", + "locale": "en-GB" + } + ] + }, + "SupplierParty.supplierPartyID": { + "display": [ + { + "name": "Supplier Party ID", + "locale": "en-GB" + } + ] + }, + "Party.partyIdentification_.iD": { + "display": [ + { + "name": "Party Identification ID", + "locale": "en-GB" + } + ] + }, + "Party.partyName_.name": { + "display": [ + { + "name": "Party Name", + "locale": "en-GB" + } + ] + }, + "Party.postalAddress_.streetName": { + "display": [ + { + "name": "Street Name", + "locale": "en-GB" + } + ] + }, + "Party.postalAddress_.cityName": { + "display": [ + { + "name": "City Name", + "locale": "en-GB" + } + ] + }, + "Party.postalAddress_.postcode": { + "display": [ + { + "name": "Postcode", + "locale": "en-GB" + } + ] + }, + "Party.postalAddress_.countryIdentifier": { + "display": [ + { + "name": "Country", + "locale": "en-GB" + } + ] + }, + "CommodityClassification.itemClassificationCode": { + "display": [ + { + "name": "Item Classification Code", + "locale": "en-GB" + } + ] + }, + "CardAccount.networkID": { + "display": [ + { + "name": "Network ID", + "locale": "en-GB" + } + ] + }, + "CardAccount.accountNumberID": { + "display": [ + { + "name": "Account Number ID", + "locale": "en-GB" + } + ] + }, + "CustomerParty.party_.partyIdentification_.iD": { + "display": [ + { + "name": "Customer Party Identification ID", + "locale": "en-GB" + } + ] + }, + "CustomerParty.party_.partyName_.name": { + "display": [ + { + "name": "Customer Party Name", + "locale": "en-GB" + } + ] + }, + "CustomerParty.party_.postalAddress_.streetName": { + "display": [ + { + "name": "Customer Party Street Name", + "locale": "en-GB" + } + ] + }, + "CustomerParty.party_.postalAddress_.cityName": { + "display": [ + { + "name": "Customer Party City Name", + "locale": "en-GB" + } + ] + }, + "CustomerParty.party_.postalAddress_.postcode": { + "display": [ + { + "name": "Customer Party Postcode", + "locale": "en-GB" + } + ] + }, + "CustomerParty.party_.postalAddress_.countryIdentifier": { + "display": [ + { + "name": "Customer Party Country", + "locale": "en-GB" + } + ] + }, + "PurchaseReceiptLine.item_.commodityClassification_.itemClassificationCode": { + "display": [ + { + "name": "Purchase Receipt Line Item Classification Code", + "locale": "en-GB" + } + ] + }, + "PurchaseReceiptLine.item_.itemInstance_.additionalItemProperty.itemPropertyName": { + "display": [ + { + "name": "Purchase Receipt Line Item Property Name", + "locale": "en-GB" + } + ] + }, + "PurchaseReceiptLine.item_.itemInstance_.additionalItemProperty.value": { + "display": [ + { + "name": "Purchase Receipt Line Item Property Value", + "locale": "en-GB" + } + ] + }, + "PurchaseReceiptLine.quantity": { + "display": [ + { + "name": "Purchase Receipt Line Quantity", + "locale": "en-GB" + } + ] + }, + "PurchaseReceiptLine.allowanceCharge_.amount": { + "display": [ + { + "name": "Purchase Receipt Line Allowance Charge Amount", + "locale": "en-GB" + } + ] + }, + "PurchaseReceiptLine.allowanceCharge_.allowanceChargeReason": { + "display": [ + { + "name": "Purchase Receipt Line Allowance Charge Reason", + "locale": "en-GB" + } + ] + }, + "PurchaseReceiptLine.taxInclusiveLineExtentionAmount": { + "display": [ + { + "name": "Tax Inclusive Line Extension Amount", + "locale": "en-GB" + } + ] + }, + "PurchaseReceiptLine.iD": { + "display": [ + { + "name": "Purchase Receipt Line ID", + "locale": "en-GB" + } + ] + }, + "Delivery.actualDeliveryDate": { + "display": [ + { + "name": "Actual Delivery Date", + "locale": "en-GB" + } + ] + }, + "Delivery.deliveryAddress.streetName": { + "display": [ + { + "name": "Delivery Street Name", + "locale": "en-GB" + } + ] + }, + "Delivery.deliveryAddress.cityName": { + "display": [ + { + "name": "Delivery City Name", + "locale": "en-GB" + } + ] + }, + "Delivery.deliveryAddress.postcode": { + "display": [ + { + "name": "Delivery Postcode", + "locale": "en-GB" + } + ] + }, + "Delivery.deliveryAddress.countryIdentifier": { + "display": [ + { + "name": "Delivery Country", + "locale": "en-GB" + } + ] + }, + "Delivery.actualDeliveryTime": { + "display": [ + { + "name": "Actual Delivery Time", + "locale": "en-GB" + } + ] + }, + "DocumentReference.iD": { + "display": [ + { + "name": "Document Reference ID", + "locale": "en-GB" + } + ] + }, + "ItemInstance.additionalItemProperty.itemPropertyName": { + "display": [ + { + "name": "Item Instance Property Name", + "locale": "en-GB" + } + ] + }, + "ItemInstance.additionalItemProperty.value": { + "display": [ + { + "name": "Item Instance Property Value", + "locale": "en-GB" + } + ] + } + }, + "cryptographic_binding_methods_supported": ["jwk"], + "display": [ + { + "name": "vReceipt", + "locale": "en-GB", + "background_color": "#12107c", + "text_color": "#FFFFFF", + "logo": { + "uri": "https://yourdomain.com/logo.png", + "alt_text": "vReceipt Logo" + } + } + ], + "credential_signing_alg_values_supported": ["ES256"], + "format": "vc+sd-jwt", + "vct": "VerifiablevReceiptSDJWT", + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": ["ES256"] + } + } } } } diff --git a/package.json b/package.json index b9dd23a..0f05b1c 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://1479-2a02-587-8704-c700-9303-e0f5-b98d-e1a2.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://cef1-2a02-587-8704-c700-f7b8-b2c7-d0e1-cf37.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index c255ea2..da5562b 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -420,8 +420,9 @@ codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { let contorller = serverURL; if (proxyPath) { - contorller = serverURL + ":" + proxyPath; + contorller = serverURL.replace("/"+proxyPath,"") + ":" + proxyPath; } + contorller = contorller.replace("https://","") const clientId = `did:web:${contorller}`; const presentation_definition_sdJwt = JSON.parse( fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") @@ -434,7 +435,7 @@ codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { privateKeyPem, "did:jwks", client_metadata, - `did:web:${serverURL}#keys-1` + `did:web:${contorller}#keys-1` ); res.type("text/plain").send(signedVPJWT); }); @@ -473,8 +474,10 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { // ); let sessionIndex = codeSessions.sessions.indexOf(uuid); - let issuanceState = codeSessions.results[sessionIndex].state; + if (sessionIndex >= 0) { + let issuanceState = codeSessions.results[sessionIndex].state; + // updateIssuerStateWithAuthCode( // authorizationCode, // uuid, diff --git a/routes/didweb.js b/routes/didweb.js index 24c907a..4148b67 100644 --- a/routes/didweb.js +++ b/routes/didweb.js @@ -7,17 +7,17 @@ const didWebRouter = express.Router(); const serverURL = process.env.SERVER_URL || "http://localhost:3000"; const proxyPath = process.env.PROXY_PATH || null; -didWebRouter.get(["/.well-known/did.json"], async (req, res) => { +didWebRouter.get(["/.well-known/did.json","/did.json"], async (req, res) => { let jwks = await convertPemToJwk(); // console.log(jwks); let contorller = serverURL; let serviceURL = serverURL if (proxyPath) { - contorller = serverURL + ":" + proxyPath; - serviceURL = serverURL + "/" + proxyPath; + contorller = serverURL.replace("/"+proxyPath,"") + ":" + proxyPath; + serviceURL = serverURL //+ "/" + proxyPath; } - + contorller = contorller.replace("https://","") let didDoc = { "@context": "https://www.w3.org/ns/did/v1", id: `did:web:${contorller}`, @@ -46,8 +46,9 @@ didWebRouter.get(["/.well-known/did.json"], async (req, res) => { didWebRouter.get(["/.well-known/jwks.json"], async (req, res) => { let contorller = serverURL; if (proxyPath) { - contorller = serverURL + ":" + proxyPath; + contorller = serverURL.replace("/"+proxyPath,"") + ":" + proxyPath; } + contorller = contorller.replace("https://","") let jwks = await convertPemToJwk(); let result = { keys: [{ ...jwks, use: "sig", kid: `${contorller}#keys-1` }], diff --git a/routes/preAuthSDjwRoutes.js b/routes/preAuthSDjwRoutes.js index 1cb34b3..ff2a497 100644 --- a/routes/preAuthSDjwRoutes.js +++ b/routes/preAuthSDjwRoutes.js @@ -47,6 +47,7 @@ import { getGenericSDJWTData, getEPassportSDJWTData, createEPassportPayload, + getVReceiptSDJWTData } from "../utils/credPayloadUtil.js"; const router = express.Router(); @@ -363,7 +364,7 @@ router.post("/credential", async (req, res) => { verifier, signAlg: "ES256", hasher: digest, - hashAlg: "SHA-256", + hashAlg: "sha-256", saltGenerator: generateSalt, }); @@ -381,13 +382,16 @@ router.post("/credential", async (req, res) => { ); } else if (credType === "VerifiablePortableDocumentA1SDJWT") { credPayload = getGenericSDJWTData(decodedHeaderSubjectDID); - } else if (credType === "VerifiablePortableDocumentA2SDJWT") { + } else if (credType === "VerifiablevReceiptSDJWT") { + credPayload = getVReceiptSDJWTData(decodedHeaderSubjectDID); + } + else if (credType === "VerifiablePortableDocumentA2SDJWT") { credPayload = getGenericSDJWTData(decodedHeaderSubjectDID); } const cnf = { jwk: holderJWKS.jwk }; // console.log(credType); - // console.log(credPayload.claims); + console.log(credPayload.claims); // console.log(credPayload.disclosureFrame); const credential = await sdjwt.issue( diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index 03add44..f64f143 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -247,7 +247,7 @@ verifierRouter.get("/generateVPRequestDidjwks", async (req, res) => { let contorller = serverURL; if (proxyPath) { - contorller = serverURL + ":" + proxyPath; + contorller = serverURL.replace("/"+proxyPath,"") + ":" + proxyPath; } const client_id = `did:web:${contorller}`; let request_uri = `${serverURL}/didjwks/${uuid}`; @@ -302,7 +302,7 @@ verifierRouter.get("/didjwks/:id", async (req, res) => { let contorller = serverURL; if (proxyPath) { - contorller = serverURL + ":" + proxyPath; + contorller = serverURL.replace("/"+proxyPath,"") + ":" + proxyPath; } const clientId = `did:web:${contorller}`; sessions.push(uuid); @@ -319,7 +319,7 @@ verifierRouter.get("/didjwks/:id", async (req, res) => { privateKeyPem, "did:jwks", client_metadata, - `did:web:${serverURL}#keys-1` + `did:web:${contorller}#keys-1` ); console.log(signedVPJWT); diff --git a/utils/credPayloadUtil.js b/utils/credPayloadUtil.js index 7ef84b1..ee58d68 100644 --- a/utils/credPayloadUtil.js +++ b/utils/credPayloadUtil.js @@ -1,8 +1,7 @@ -import {getCredentialSubjectForPersona} from "./personasUtils.js" +import { getCredentialSubjectForPersona } from "./personasUtils.js"; // Helper functions to create payloads for different credential types - export const createPIDPayload = (token, serverURL, decodedHeaderSubjectDID) => { const preSessions = getPreCodeSessions(); const persona = getPersonaFromAccessToken( @@ -74,7 +73,7 @@ export const createEPassportPayload = (serverURL, decodedHeaderSubjectDID) => { contentInfo: { versionNumber: 1, signatureInfo: { - digestHashAlgorithmIdentifier: "SHA-256", + digestHashAlgorithmIdentifier: "sha-256", signatureAlgorithmIdentifier: "RS256", signatureCertificateText: "someCertificateText", signatureDigestResultBinaryObject: "someDigestResultUri", @@ -109,7 +108,7 @@ export const createEPassportPayload = (serverURL, decodedHeaderSubjectDID) => { dataGroupHash: [ { dataGroupNumber: 1, valueBinaryObject: "someHashUri" }, ], - digestHashAlgorithmIdentifier: "SHA-256", + digestHashAlgorithmIdentifier: "sha-256", versionNumber: 1, }, }, @@ -127,333 +126,677 @@ export const createEPassportPayload = (serverURL, decodedHeaderSubjectDID) => { }; }; +export const createStudentIDPayload = ( + token, + serverURL, + decodedHeaderSubjectDID +) => { + const preSessions = getPreCodeSessions(); + const persona = getPersonaFromAccessToken( + token, + preSessions.personas, + preSessions.accessTokens + ); + + let credentialSubject = { + id: decodedHeaderSubjectDID || "", + identifier: "john.doe@university.edu", + schacPersonalUniqueCode: [ + "urn:schac:personalUniqueCode:int:esi:university.edu:12345", + ], + schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", + schacHomeOrganization: "university.edu", + familyName: "Doe", + firstName: "John", + displayName: "John Doe", + dateOfBirth: "1990-01-01", + commonName: "Johnathan Doe", + mail: "john.doe@university.edu", + eduPersonPrincipalName: "john.doe@university.edu", + eduPersonPrimaryAffiliation: "student", + eduPersonAffiliation: ["member", "student"], + eduPersonScopedAffiliation: ["student@university.edu"], + eduPersonAssurance: [ + "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", + ], + }; -export const createStudentIDPayload = (token, serverURL, decodedHeaderSubjectDID) => { - const preSessions = getPreCodeSessions(); - const persona = getPersonaFromAccessToken(token, preSessions.personas, preSessions.accessTokens); - - let credentialSubject = { - id: decodedHeaderSubjectDID || "", - identifier: "john.doe@university.edu", - schacPersonalUniqueCode: ["urn:schac:personalUniqueCode:int:esi:university.edu:12345"], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Doe", - firstName: "John", - displayName: "John Doe", - dateOfBirth: "1990-01-01", - commonName: "Johnathan Doe", - mail: "john.doe@university.edu", - eduPersonPrincipalName: "john.doe@university.edu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@university.edu"], - eduPersonAssurance: ["https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0"] + // Handle different persona data if available + if (persona === "1") { + credentialSubject = { + ...credentialSubject, + identifier: "mario.conti@ewc.eu", + familyName: "Conti", + firstName: "Mario", + displayName: "Mario Conti", + commonName: "Mario Conti", + mail: "mario.conti@ewc.eu", }; - - // Handle different persona data if available - if (persona === "1") { - credentialSubject = { - ...credentialSubject, - identifier: "mario.conti@ewc.eu", - familyName: "Conti", - firstName: "Mario", - displayName: "Mario Conti", - commonName: "Mario Conti", - mail: "mario.conti@ewc.eu" - }; - } else if (persona === "2") { - credentialSubject = { - ...credentialSubject, - identifier: "hannah@ewc.eu", - familyName: "Matkalainen", - firstName: "Hannah", - displayName: "Hannah Matkalainen", - commonName: "Hannah Matkalainen", - mail: "hannah@ewc.eu" - }; - } else if (persona === "3") { - credentialSubject = { - ...credentialSubject, - identifier: "felix@ewc.eu", - familyName: "Fischer", - firstName: "Felix", - displayName: "Felix Fischer", - commonName: "Felix Fischer", - mail: "felix@ewc.eu" - }; - } - - return { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, - iat: Math.floor(Date.now() / 1000), - jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", - vc: { - type: ["StudentID"], - "@context": ["https://www.w3.org/2018/credentials/v1"], - issuer: serverURL, - credentialSubject: credentialSubject, - issuanceDate: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), - expirationDate: new Date((Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000).toISOString(), - validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString() - } + } else if (persona === "2") { + credentialSubject = { + ...credentialSubject, + identifier: "hannah@ewc.eu", + familyName: "Matkalainen", + firstName: "Hannah", + displayName: "Hannah Matkalainen", + commonName: "Hannah Matkalainen", + mail: "hannah@ewc.eu", }; -}; + } else if (persona === "3") { + credentialSubject = { + ...credentialSubject, + identifier: "felix@ewc.eu", + familyName: "Fischer", + firstName: "Felix", + displayName: "Felix Fischer", + commonName: "Felix Fischer", + mail: "felix@ewc.eu", + }; + } + return { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, + iat: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + type: ["StudentID"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + issuer: serverURL, + credentialSubject: credentialSubject, + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 + ).toISOString(), + validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + }, + }; +}; export const createAllianceIDPayload = (serverURL, decodedHeaderSubjectDID) => { - return { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, - iat: Math.floor(Date.now() / 1000), - jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", - vc: { - type: ["allianceIDCredential"], - "@context": ["https://www.w3.org/2018/credentials/v1"], - issuer: serverURL, - credentialSubject: { - id: decodedHeaderSubjectDID || "", - identifier: { - schemeID: "European Student Identifier", - value: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", - id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ" - } + return { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, + iat: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + type: ["allianceIDCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + issuer: serverURL, + credentialSubject: { + id: decodedHeaderSubjectDID || "", + identifier: { + schemeID: "European Student Identifier", + value: + "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", + id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", }, - issuanceDate: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), - expirationDate: new Date((Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000).toISOString(), - validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString() - } - }; + }, + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 + ).toISOString(), + validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + }, + }; +}; + +export const createFerryBoardingPassPayload = ( + token, + serverURL, + decodedHeaderSubjectDID +) => { + const preSessions = getPreCodeSessions(); + const persona = getPersonaFromAccessToken( + token, + preSessions.personas, + preSessions.accessTokens + ); + + let credentialSubject = { + id: decodedHeaderSubjectDID || "", + identifier: "John Doe", + ticketQR: "data:image/png;base64,someBase64EncodedQR", + 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", }; - export const createFerryBoardingPassPayload = (token, serverURL, decodedHeaderSubjectDID) => { - const preSessions = getPreCodeSessions(); - const persona = getPersonaFromAccessToken(token, preSessions.personas, preSessions.accessTokens); - - let credentialSubject = { - id: decodedHeaderSubjectDID || "", - identifier: "John Doe", - ticketQR: "data:image/png;base64,someBase64EncodedQR", - 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" + if (persona === "1") { + credentialSubject = { + ...credentialSubject, + identifier: "Mario Conti", + lastName: "Conti", + firstName: "Mario", + ticketNumber: "3022", + arrivalPort: "Mykonos", + vesselDescription: "Ferry to Mykonos", + }; + } else if (persona === "2") { + credentialSubject = { + ...credentialSubject, + identifier: "Hannah Matkalainen", + lastName: "Matkalainen", + firstName: "Hannah", + ticketNumber: "3022", + arrivalPort: "Santorini", + vesselDescription: "Ferry to Santorini", }; - - if (persona === "1") { - credentialSubject = { - ...credentialSubject, - identifier: "Mario Conti", - lastName: "Conti", - firstName: "Mario", - ticketNumber: "3022", - arrivalPort: "Mykonos", - vesselDescription: "Ferry to Mykonos" - }; - } else if (persona === "2") { - credentialSubject = { - ...credentialSubject, - identifier: "Hannah Matkalainen", - lastName: "Matkalainen", - firstName: "Hannah", - ticketNumber: "3022", - arrivalPort: "Santorini", - vesselDescription: "Ferry to Santorini" - }; - } else if (persona === "3") { - credentialSubject = { - ...credentialSubject, - identifier: "Felix Fischer", - lastName: "Fischer", - firstName: "Felix", - ticketNumber: "3022", - arrivalPort: "Crete", - vesselDescription: "Ferry to Crete" - }; - } - - return { - iss: serverURL, - sub: decodedHeaderSubjectDID || "", - exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, - iat: Math.floor(Date.now() / 1000), - jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", - vc: { - type: ["VerifiableCredential", "ferryBoardingPassCredential"], - "@context": ["https://www.w3.org/2018/credentials/v1"], - issuer: serverURL, - credentialSubject: credentialSubject, - issuanceDate: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), - expirationDate: new Date((Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000).toISOString(), - validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString() - } + } else if (persona === "3") { + credentialSubject = { + ...credentialSubject, + identifier: "Felix Fischer", + lastName: "Fischer", + firstName: "Felix", + ticketNumber: "3022", + arrivalPort: "Crete", + vesselDescription: "Ferry to Crete", }; + } + + return { + iss: serverURL, + sub: decodedHeaderSubjectDID || "", + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, + iat: Math.floor(Date.now() / 1000), + jti: "urn:did:1904a925-38bd-4eda-b682-4b5e3ca9d4bc", + vc: { + type: ["VerifiableCredential", "ferryBoardingPassCredential"], + "@context": ["https://www.w3.org/2018/credentials/v1"], + issuer: serverURL, + credentialSubject: credentialSubject, + issuanceDate: new Date( + Math.floor(Date.now() / 1000) * 1000 + ).toISOString(), + expirationDate: new Date( + (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30) * 1000 + ).toISOString(), + validFrom: new Date(Math.floor(Date.now() / 1000) * 1000).toISOString(), + }, }; - +}; // SD-JWT HELPERS - export const getPIDSDJWTData = (decodedHeaderSubjectDID) => { +export const getPIDSDJWTData = (decodedHeaderSubjectDID) => { const currentTimestamp = new Date().getTime(); - const currentDate = new Date() - const expTimestamp = currentDate.setFullYear(currentDate.getFullYear() + 1) - const claims = { - id: decodedHeaderSubjectDID || "", - given_name: "John", - family_name: "Doe", - birth_date: "1990-01-01", - age_over_18: true, - issuance_date: currentTimestamp, - expiry_date: expTimestamp,//expTimestamp.getTime(), - issuing_authority: "UAegean Test Issuer", - issuing_country: "Greece", - }; - - const disclosureFrame = { - _sd: ["id", "given_name", "family_name", "birth_date", "age_over_18", "expiry_date","issuance_date","issuing_authority","issuing_country"] - }; - - return { claims, disclosureFrame }; + const currentDate = new Date(); + const expTimestamp = currentDate.setFullYear(currentDate.getFullYear() + 1); + const claims = { + id: decodedHeaderSubjectDID || "", + given_name: "John", + family_name: "Doe", + birth_date: "1990-01-01", + age_over_18: true, + issuance_date: currentTimestamp, + expiry_date: expTimestamp, //expTimestamp.getTime(), + issuing_authority: "UAegean Test Issuer", + issuing_country: "Greece", }; - - - export const getStudentIDSDJWTData = (decodedHeaderSubjectDID) => { - const claims = { - id: decodedHeaderSubjectDID || "", - identifier: "john.doe@university.edu", - schacPersonalUniqueCode: ["urn:schac:personalUniqueCode:int:esi:university.edu:12345"], - schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", - schacHomeOrganization: "university.edu", - familyName: "Doe", - firstName: "John", - displayName: "John Doe", - dateOfBirth: "1990-01-01", - commonName: "Johnathan Doe", - mail: "john.doe@university.edu", - eduPersonPrincipalName: "john.doe@university.edu", - eduPersonPrimaryAffiliation: "student", - eduPersonAffiliation: ["member", "student"], - eduPersonScopedAffiliation: ["student@university.edu"], - eduPersonAssurance: ["https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0"] - }; - - const disclosureFrame = { - _sd: [ - "id", "identifier", "schacPersonalUniqueCode", "schacPersonalUniqueID", "schacHomeOrganization", - "familyName", "firstName", "displayName", "dateOfBirth", "commonName", "mail", "eduPersonPrincipalName", - "eduPersonPrimaryAffiliation", "eduPersonAffiliation", "eduPersonScopedAffiliation", "eduPersonAssurance" - ] - }; - - return { claims, disclosureFrame }; + + const disclosureFrame = { + _sd: [ + "id", + "given_name", + "family_name", + "birth_date", + "age_over_18", + "expiry_date", + "issuance_date", + "issuing_authority", + "issuing_country", + ], }; + return { claims, disclosureFrame }; +}; + +export const getStudentIDSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + identifier: "john.doe@university.edu", + schacPersonalUniqueCode: [ + "urn:schac:personalUniqueCode:int:esi:university.edu:12345", + ], + schacPersonalUniqueID: "urn:schac:personalUniqueID:us:12345", + schacHomeOrganization: "university.edu", + familyName: "Doe", + firstName: "John", + displayName: "John Doe", + dateOfBirth: "1990-01-01", + commonName: "Johnathan Doe", + mail: "john.doe@university.edu", + eduPersonPrincipalName: "john.doe@university.edu", + eduPersonPrimaryAffiliation: "student", + eduPersonAffiliation: ["member", "student"], + eduPersonScopedAffiliation: ["student@university.edu"], + eduPersonAssurance: [ + "https://wiki.refeds.org/display/ASS/REFEDS+Assurance+Framework+ver+1.0", + ], + }; + + const disclosureFrame = { + _sd: [ + "id", + "identifier", + "schacPersonalUniqueCode", + "schacPersonalUniqueID", + "schacHomeOrganization", + "familyName", + "firstName", + "displayName", + "dateOfBirth", + "commonName", + "mail", + "eduPersonPrincipalName", + "eduPersonPrimaryAffiliation", + "eduPersonAffiliation", + "eduPersonScopedAffiliation", + "eduPersonAssurance", + ], + }; + + return { claims, disclosureFrame }; +}; + export const getAllianceIDSDJWTData = (decodedHeaderSubjectDID) => { - const claims = { - id: decodedHeaderSubjectDID || "", - identifier: { - schemeID: "European Student Identifier", - value: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", - id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ" - } - }; - - const disclosureFrame = { - _sd: ["id", "identifier.schemeID", "identifier.value", "identifier.id"] - }; - - return { claims, disclosureFrame }; + const claims = { + id: decodedHeaderSubjectDID || "", + identifier: { + schemeID: "European Student Identifier", + value: + "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", + id: "urn:schac:europeanUniversityAllianceCode:int:euai:ERUA:universityXYZ", + }, }; - - - export const getFerryBoardingPassSDJWTData = (decodedHeaderSubjectDID) => { - const claims = { - id: decodedHeaderSubjectDID || "", - identifier: "John Doe", - ticketQR: "data:image/png;base64,someBase64EncodedQR", - 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" - }; - - const disclosureFrame = { - _sd: [ - "id", "identifier", "ticketQR", "ticketNumber", "ticketLet", "lastName", "firstName", - "seatType", "seatNumber", "departureDate", "departureTime", "arrivalDate", "arrivalTime", - "arrivalPort", "vesselDescription" - ] - }; - - return { claims, disclosureFrame }; + + const disclosureFrame = { + _sd: ["id", "identifier.schemeID", "identifier.value", "identifier.id"], }; - - export const getGenericSDJWTData = (decodedHeaderSubjectDID, credType) => { - const claims = { - id: decodedHeaderSubjectDID || "", - given_name: "John", - last_name: "Doe" - }; - - const disclosureFrame = { - _sd: ["id", "given_name", "last_name"] - }; - - return { claims, disclosureFrame }; + + return { claims, disclosureFrame }; +}; + +export const getFerryBoardingPassSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + identifier: "John Doe", + ticketQR: "data:image/png;base64,someBase64EncodedQR", + 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", }; - - - export const getEPassportSDJWTData = (decodedHeaderSubjectDID) => { - const claims = { - id: decodedHeaderSubjectDID || "", - 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" - } + + const disclosureFrame = { + _sd: [ + "id", + "identifier", + "ticketQR", + "ticketNumber", + "ticketLet", + "lastName", + "firstName", + "seatType", + "seatNumber", + "departureDate", + "departureTime", + "arrivalDate", + "arrivalTime", + "arrivalPort", + "vesselDescription", + ], + }; + + return { claims, disclosureFrame }; +}; + +export const getGenericSDJWTData = (decodedHeaderSubjectDID, credType) => { + const claims = { + id: decodedHeaderSubjectDID || "", + given_name: "John", + last_name: "Doe", + }; + + const disclosureFrame = { + _sd: ["id", "given_name", "last_name"], + }; + + return { claims, disclosureFrame }; +}; + +export const getEPassportSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + 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" - } - } - }; - - const disclosureFrame = { - _sd: ["id", "electronicPassport.dataGroup1.birthdate", "electronicPassport.dataGroup1.docTypeCode", - "electronicPassport.dataGroup1.expiryDate", "electronicPassport.dataGroup1.genderCode", - "electronicPassport.dataGroup1.holdersName", "electronicPassport.dataGroup1.issuerCode", - "electronicPassport.dataGroup1.natlText", "electronicPassport.dataGroup1.passportNumberIdentifier"] - }; - - return { claims, disclosureFrame }; + }, + dataGroup2EncodedFaceBiometrics: { + faceBiometricDataEncodedPicture: "someBiometricUri", + }, + }, + }; + + const disclosureFrame = { + _sd: [ + "id", + "electronicPassport.dataGroup1.birthdate", + "electronicPassport.dataGroup1.docTypeCode", + "electronicPassport.dataGroup1.expiryDate", + "electronicPassport.dataGroup1.genderCode", + "electronicPassport.dataGroup1.holdersName", + "electronicPassport.dataGroup1.issuerCode", + "electronicPassport.dataGroup1.natlText", + "electronicPassport.dataGroup1.passportNumberIdentifier", + ], + }; + + return { claims, disclosureFrame }; +}; + +export const getVReceiptSDJWTData = (decodedHeaderSubjectDID) => { + const claims = { + id: decodedHeaderSubjectDID || "", + + // Monetary Total + "MonetaryTotal.lineExtensionAmount": 150.75, + "MonetaryTotal.taxInclusiveAmount": 180.9, + "MonetaryTotal.payableAmount": 180.9, + + // Tax Total + "TaxTotal.taxSubtotal_": { + taxableAmount: 150.75, + taxSubtotalTaxAmount: 30.15, + taxCategory_: "VAT", + percent: 20, + }, + "TaxTotal.taxAmount": 30.15, + + // Address + "Address.streetName": "123 Main Street", + "Address.cityName": "Sample City", + "Address.postcode": "SC12345", + "Address.countryIdentifier": "GB", + + // Tax Category + "TaxCategory.taxScheme_": "VAT Scheme", + + // Item Property + "ItemProperty.itemPropertyName": "Color", + "ItemProperty.value": "Red", + + // Tax Scheme + "TaxScheme.taxSchemeName": "Standard VAT", + + // Allowance Charge + "AllowanceCharge.amount": 10.0, + "AllowanceCharge.allowanceChargeReason": "Discount Applied", + + // Party Name + "PartyName.name": "Sample Seller Ltd.", + + // Payment Means + "PaymentMeans.cardAccount_": { + networkID: "VISA", + accountNumberID: "411111******1111", + }, + "PaymentMeans.paymentMeansCode": "PM01", + + // Party Identification + "PartyIdentification.iD": "PID123456789", + + // Purchase Receipt + "PurchaseReceipt.paymentMeans_": "Credit Card", + "PurchaseReceipt.note": "Thank you for your purchase!", + "PurchaseReceipt.delivery_": "Standard Shipping", + "PurchaseReceipt.taxIncludedIndicator": true, + "PurchaseReceipt.taxTotal_": 30.15, + "PurchaseReceipt.accountingCustomerParty_": "Customer Account", + "PurchaseReceipt.documentCurrencyCode": "GBP", + "PurchaseReceipt.payment_": "Paid", + "PurchaseReceipt.sellerSupplierParty": "Sample Seller Ltd.", + "PurchaseReceipt.legalMonetaryTotal": 180.9, + "PurchaseReceipt.salesDocumentReference": "SDR123456", + "PurchaseReceipt.iD": "PR123456789", + "PurchaseReceipt.issueDate": "2024-04-27", + "PurchaseReceipt.purchaseReceiptLine-1": "Line1", + + // Tax Subtotal + "TaxSubtotal.taxableAmount": 150.75, + "TaxSubtotal.taxSubtotalTaxAmount": 30.15, + "TaxSubtotal.taxCategory_": "VAT", + "TaxSubtotal.percent": 20, + + // Item + "Item.commodityClassification_": "CC123", + "Item.itemInstance_": "Instance1", + "Item.additionalItemProperty": "Property1", + + // Payment + "Payment.authorizationID": "AUTH123456789", + "Payment.paidAmount": 180.9, + "Payment.transactionID": "TXN123456789", + + // Supplier Party + "SupplierParty.party_": "SupplierParty1", + "SupplierParty.supplierPartyID": "SPID123456", + + // Party + "Party.partyIdentification_.iD": "PID987654321", + "Party.partyName_.name": "Sample Buyer Ltd.", + "Party.postalAddress_.streetName": "456 Another St.", + "Party.postalAddress_.cityName": "Another City", + "Party.postalAddress_.postcode": "AC54321", + "Party.postalAddress_.countryIdentifier": "GB", + + // Commodity Classification + "CommodityClassification.itemClassificationCode": "ICC12345", + + // Card Account + "CardAccount.networkID": "MASTERCARD", + "CardAccount.accountNumberID": "550000******0004", + + // Customer Party + "CustomerParty.party_.partyIdentification_.iD": "CPID123456789", + "CustomerParty.party_.partyName_.name": "Customer Name Ltd.", + "CustomerParty.party_.postalAddress_.streetName": "789 Customer Ave.", + "CustomerParty.party_.postalAddress_.cityName": "Customer City", + "CustomerParty.party_.postalAddress_.postcode": "CC67890", + "CustomerParty.party_.postalAddress_.countryIdentifier": "GB", + + // Purchase Receipt Line + "PurchaseReceiptLine.item_.commodityClassification_.itemClassificationCode": + "ICC67890", + "PurchaseReceiptLine.item_.itemInstance_.additionalItemProperty.itemPropertyName": + "Size", + "PurchaseReceiptLine.item_.itemInstance_.additionalItemProperty.value": + "Medium", + "PurchaseReceiptLine.quantity": 2, + "PurchaseReceiptLine.allowanceCharge_.amount": 5.0, + "PurchaseReceiptLine.allowanceCharge_.allowanceChargeReason": "Promotion", + "PurchaseReceiptLine.taxInclusiveLineExtentionAmount": 80.45, + "PurchaseReceiptLine.iD": "PRL123456789", + + // Delivery + "Delivery.actualDeliveryDate": "2024-04-28", + "Delivery.deliveryAddress.streetName": "321 Delivery Rd.", + "Delivery.deliveryAddress.cityName": "Delivery City", + "Delivery.deliveryAddress.postcode": "DC12345", + "Delivery.deliveryAddress.countryIdentifier": "GB", + "Delivery.actualDeliveryTime": "14:30", + + // Document Reference + "DocumentReference.iD": "DR123456789", + + // Item Instance + "ItemInstance.additionalItemProperty.itemPropertyName": "Warranty", + "ItemInstance.additionalItemProperty.value": "2 Years", + }; + + const disclosureFrame = { + _sd: [ + "id", + + // Monetary Total + "MonetaryTotal.lineExtensionAmount", + "MonetaryTotal.taxInclusiveAmount", + "MonetaryTotal.payableAmount", + + // Tax Total + "TaxTotal.taxSubtotal_", + "TaxTotal.taxAmount", + + // Address + "Address.streetName", + "Address.cityName", + "Address.postcode", + "Address.countryIdentifier", + + // Tax Category + "TaxCategory.taxScheme_", + + // Item Property + "ItemProperty.itemPropertyName", + "ItemProperty.value", + + // Tax Scheme + "TaxScheme.taxSchemeName", + + // Allowance Charge + "AllowanceCharge.amount", + "AllowanceCharge.allowanceChargeReason", + + // Party Name + "PartyName.name", + + // Payment Means + "PaymentMeans.cardAccount_", + "PaymentMeans.paymentMeansCode", + + // Party Identification + "PartyIdentification.iD", + + // Purchase Receipt + "PurchaseReceipt.paymentMeans_", + "PurchaseReceipt.note", + "PurchaseReceipt.delivery_", + "PurchaseReceipt.taxIncludedIndicator", + "PurchaseReceipt.taxTotal_", + "PurchaseReceipt.accountingCustomerParty_", + "PurchaseReceipt.documentCurrencyCode", + "PurchaseReceipt.payment_", + "PurchaseReceipt.sellerSupplierParty", + "PurchaseReceipt.legalMonetaryTotal", + "PurchaseReceipt.salesDocumentReference", + "PurchaseReceipt.iD", + "PurchaseReceipt.issueDate", + "PurchaseReceipt.purchaseReceiptLine-1", + + // Tax Subtotal + "TaxSubtotal.taxableAmount", + "TaxSubtotal.taxSubtotalTaxAmount", + "TaxSubtotal.taxCategory_", + "TaxSubtotal.percent", + + // Item + "Item.commodityClassification_", + "Item.itemInstance_", + "Item.additionalItemProperty", + + // Payment + "Payment.authorizationID", + "Payment.paidAmount", + "Payment.transactionID", + + // Supplier Party + "SupplierParty.party_", + "SupplierParty.supplierPartyID", + + // Party + "Party.partyIdentification_.iD", + "Party.partyName_.name", + "Party.postalAddress_.streetName", + "Party.postalAddress_.cityName", + "Party.postalAddress_.postcode", + "Party.postalAddress_.countryIdentifier", + + // Commodity Classification + "CommodityClassification.itemClassificationCode", + + // Card Account + "CardAccount.networkID", + "CardAccount.accountNumberID", + + // Customer Party + "CustomerParty.party_.partyIdentification_.iD", + "CustomerParty.party_.partyName_.name", + "CustomerParty.party_.postalAddress_.streetName", + "CustomerParty.party_.postalAddress_.cityName", + "CustomerParty.party_.postalAddress_.postcode", + "CustomerParty.party_.postalAddress_.countryIdentifier", + + // Purchase Receipt Line + "PurchaseReceiptLine.item_.commodityClassification_.itemClassificationCode", + "PurchaseReceiptLine.item_.itemInstance_.additionalItemProperty.itemPropertyName", + "PurchaseReceiptLine.item_.itemInstance_.additionalItemProperty.value", + "PurchaseReceiptLine.quantity", + "PurchaseReceiptLine.allowanceCharge_.amount", + "PurchaseReceiptLine.allowanceCharge_.allowanceChargeReason", + "PurchaseReceiptLine.taxInclusiveLineExtentionAmount", + "PurchaseReceiptLine.iD", + + // Delivery + "Delivery.actualDeliveryDate", + "Delivery.deliveryAddress.streetName", + "Delivery.deliveryAddress.cityName", + "Delivery.deliveryAddress.postcode", + "Delivery.deliveryAddress.countryIdentifier", + "Delivery.actualDeliveryTime", + + // Document Reference + "DocumentReference.iD", + + // Item Instance + "ItemInstance.additionalItemProperty.itemPropertyName", + "ItemInstance.additionalItemProperty.value", + ], }; - \ No newline at end of file + + return { claims, disclosureFrame }; +}; diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index e385542..3d315b9 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -245,7 +245,7 @@ export async function base64UrlEncodeSha256(codeVerifier) { const data = encoder.encode(codeVerifier); // Calculate the SHA-256 hash of the ArrayBuffer - const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashBuffer = await crypto.subtle.digest("sha-256", data); // Convert the ArrayBuffer to a Uint8Array const hashArray = new Uint8Array(hashBuffer); From 62240f45d8ea1f49dc99d46bcdef9555293eee22 Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 7 Nov 2024 10:45:26 +0200 Subject: [PATCH 36/37] added iss and aud to request oobjects --- routes/codeFlowSdJwtRoutes.js | 5 +++-- routes/verifierRoutes.js | 12 +++++++++--- utils/cryptoUtils.js | 9 ++++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index da5562b..9561296 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -394,7 +394,7 @@ codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { presentation_definition_sdJwt, "", "x509_san_dns", - client_metadata + client_metadata, null, serverURL ); console.log(signedVPJWT); @@ -435,7 +435,8 @@ codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { privateKeyPem, "did:jwks", client_metadata, - `did:web:${contorller}#keys-1` + `did:web:${contorller}#keys-1`, + serverURL ); res.type("text/plain").send(signedVPJWT); }); diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index f64f143..f981ca1 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -216,6 +216,7 @@ verifierRouter.get("/x509VPrequest/:id", async (req, res) => { location: "Greece", cover_uri: "string", description: "EWC pilot case verification", + vp_formats: }; const clientId = "dss.aegean.gr"; @@ -232,7 +233,9 @@ verifierRouter.get("/x509VPrequest/:id", async (req, res) => { presentation_definition_sdJwt, "", "x509_san_dns", - client_metadata + client_metadata, + null, + serverURL ); console.log(signedVPJWT); @@ -319,7 +322,8 @@ verifierRouter.get("/didjwks/:id", async (req, res) => { privateKeyPem, "did:jwks", client_metadata, - `did:web:${contorller}#keys-1` + `did:web:${contorller}#keys-1`, + serverURL ); console.log(signedVPJWT); @@ -396,7 +400,9 @@ verifierRouter.get("/vpRequest/:id", async (req, res) => { presentation_definition_sdJwt, privateKey, client_id_scheme, - clientMetadata + clientMetadata, + null, + serverURL ); console.log("VP request "); diff --git a/utils/cryptoUtils.js b/utils/cryptoUtils.js index 3d315b9..d257355 100644 --- a/utils/cryptoUtils.js +++ b/utils/cryptoUtils.js @@ -110,7 +110,8 @@ export async function buildVpRequestJWT( privateKey = "", client_id_scheme = "redirect_uri", // Default to "redirect_uri" client_metadata = {}, - kid =null // Default to an empty object + kid =null, // Default to an empty object, + serverURL ) { const nonce = generateNonce(16); const state = generateNonce(16); @@ -127,6 +128,10 @@ export async function buildVpRequestJWT( .replace("-----END CERTIFICATE-----", "") .replace(/\s+/g, ""); + // The result is a JWS-signed JWT [RFC7519]. If signed, the Authorization Request Object SHOULD contain + // the Claims iss (issuer) and aud (audience) as members with their semantics being the same as defined in the JWT [RFC7519] specification. + // The value of aud should be the value of the authorization server (AS) issuer, as defined in RFC 8414 [RFC8414] + // Construct the JWT payload let jwtPayload = { response_type: "vp_token", @@ -138,6 +143,8 @@ export async function buildVpRequestJWT( nonce: nonce, state: state, client_metadata: client_metadata, // + iss: serverURL, + aud: serverURL }; // Define the JWT header From 2a20f5f1fd4ea8363588f16dbfe5e35400bdc0ea Mon Sep 17 00:00:00 2001 From: Nikos Triantafyllou Date: Thu, 7 Nov 2024 13:27:33 +0200 Subject: [PATCH 37/37] updated vp request with iss/aud and also vp_format in client metadata --- routes/codeFlowSdJwtRoutes.js | 22 +++++++++++++++++----- routes/verifierRoutes.js | 17 ++++++++++++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/routes/codeFlowSdJwtRoutes.js b/routes/codeFlowSdJwtRoutes.js index 9561296..eef92f7 100644 --- a/routes/codeFlowSdJwtRoutes.js +++ b/routes/codeFlowSdJwtRoutes.js @@ -382,6 +382,11 @@ codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { location: "Greece", cover_uri: "string", description: "EWC pilot case verification", + vp_formats: { + "vc+sd-jwt": { + alg: ["ES256", "ES384"], + }, + }, }; const presentation_definition_sdJwt = JSON.parse( fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") @@ -394,7 +399,9 @@ codeFlowRouterSDJWT.get("/x509VPrequest_dynamic/:id", async (req, res) => { presentation_definition_sdJwt, "", "x509_san_dns", - client_metadata, null, serverURL + client_metadata, + null, + serverURL ); console.log(signedVPJWT); @@ -411,6 +418,11 @@ codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { location: "Greece", cover_uri: "string", description: "EWC pilot case verification", + vp_formats: { + "vc+sd-jwt": { + alg: ["ES256", "ES384"], + }, + }, }; const privateKeyPem = fs.readFileSync( @@ -420,9 +432,9 @@ codeFlowRouterSDJWT.get("/didJwksVPrequest_dynamic/:id", async (req, res) => { let contorller = serverURL; if (proxyPath) { - contorller = serverURL.replace("/"+proxyPath,"") + ":" + proxyPath; + contorller = serverURL.replace("/" + proxyPath, "") + ":" + proxyPath; } - contorller = contorller.replace("https://","") + contorller = contorller.replace("https://", ""); const clientId = `did:web:${contorller}`; const presentation_definition_sdJwt = JSON.parse( fs.readFileSync("./data/presentation_definition_sdjwt.json", "utf-8") @@ -475,7 +487,7 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { // ); let sessionIndex = codeSessions.sessions.indexOf(uuid); - + if (sessionIndex >= 0) { let issuanceState = codeSessions.results[sessionIndex].state; @@ -495,7 +507,7 @@ codeFlowRouterSDJWT.post("/direct_post_vci/:id", async (req, res) => { const redirectUrl = `${codeSessions.requests[sessionIndex].redirectUri}?code=${authorizationCode}&state=${issuanceState}`; // return //res.redirect(302, redirectUrl); - return res.send({redirect_uri: redirectUrl }); + return res.send({ redirect_uri: redirectUrl }); } else { console.log("issuance session not found " + uuid); return res.sendStatus(500); diff --git a/routes/verifierRoutes.js b/routes/verifierRoutes.js index f981ca1..dcdae87 100644 --- a/routes/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -216,7 +216,14 @@ verifierRouter.get("/x509VPrequest/:id", async (req, res) => { location: "Greece", cover_uri: "string", description: "EWC pilot case verification", - vp_formats: + vp_formats: { + jwt_vp: { + alg: ["EdDSA", "ES256K"], + }, + ldp_vp: { + proof_type: ["Ed25519Signature2018"], + }, + } }; const clientId = "dss.aegean.gr"; @@ -296,6 +303,14 @@ verifierRouter.get("/didjwks/:id", async (req, res) => { location: "Greece", cover_uri: "string", description: "EWC pilot case verification", + vp_formats: { + jwt_vp: { + alg: ["EdDSA", "ES256K"], + }, + ldp_vp: { + proof_type: ["Ed25519Signature2018"], + }, + } }; let privateKeyPem = fs.readFileSync(