diff --git a/.gitignore b/.gitignore index dee228b..03be28f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ docker-compose.yml *.production* *.tar .npmrc -dataset.json \ No newline at end of file +dataset.json +*.xlsx +tasks.json \ No newline at end of file diff --git a/dataset-reader/index.d.ts b/dataset-reader/index.d.ts new file mode 100644 index 0000000..3659e0e --- /dev/null +++ b/dataset-reader/index.d.ts @@ -0,0 +1,5 @@ +export function parsePidData(filePath: string): any; + +export function parseEhicData(filePath: string): any; + +export function parsePda1Data(filePath: string): any; diff --git a/dataset-reader/index.js b/dataset-reader/index.js new file mode 100644 index 0000000..18edda5 --- /dev/null +++ b/dataset-reader/index.js @@ -0,0 +1,113 @@ +const XLSX = require('xlsx'); +const fs = require('fs'); + + + +function parsePidData(filePath) { + const readOpts = { // <--- need these settings in readFile options + cellText: false, + cellDates: true, + type: 'buffer' + }; + const fileBuffer = fs.readFileSync(filePath); + + // Parse the workbook + const workbook = XLSX.read(fileBuffer, readOpts); + const sheetName = "PID" + // Get the first worksheet + const worksheet = workbook.Sheets[sheetName]; + + // Convert worksheet to JSON format + const data = XLSX.utils.sheet_to_json(worksheet, { + defval: undefined, + dateNF: 'd"/"m"/"yyyy' + // skipHidden: true, + // header: 0 + }); + + const headers = Object.values(data[0]).map((h) => h.trim()); + const ncols = Object.keys(headers); + + const result = data.slice(1).map(row => { + row = Object.values(row) + const obj = {}; + row.map((cell, index) => { + obj[headers[index]] = cell; // Assign key-value pairs + }); + + return obj; + }); + + return result; +} + +function parseEhicData(filePath) { + const readOpts = { // <--- need these settings in readFile options + cellText: false, + cellDates: true, + type: 'buffer' + }; + const fileBuffer = fs.readFileSync(filePath); + + // Parse the workbook + const workbook = XLSX.read(fileBuffer, readOpts); + const sheetName = "EHIC" + // Get the first worksheet + const worksheet = workbook.Sheets[sheetName]; + + // Convert worksheet to JSON format + const data = XLSX.utils.sheet_to_json(worksheet, { + defval: undefined, + dateNF: 'd"/"m"/"yyyy' + // skipHidden: true, + // header: 0 + }); + return data; +} + + +function parsePda1Data(filePath) { + const readOpts = { // <--- need these settings in readFile options + cellText: false, + cellDates: true, + type: 'buffer' + }; + const fileBuffer = fs.readFileSync(filePath); + + // Parse the workbook + const workbook = XLSX.read(fileBuffer, readOpts); + const sheetName = "PDA1" + // Get the first worksheet + const worksheet = workbook.Sheets[sheetName]; + + // Convert worksheet to JSON format + const data = XLSX.utils.sheet_to_json(worksheet, { + defval: undefined, + dateNF: 'd"/"m"/"yyyy' + // skipHidden: true, + // header: 0 + }); + + let headers = Object.values(data[0]).map((h) => h.trim()); + + const ncols = Object.keys(headers); + + + const result = data.slice(1).map(row => { + row = Object.values(row) + + const obj = {}; + row.forEach((cell, index) => { + obj[headers[index]] = cell; // Assign key-value pairs + }); + return obj; + }); + + return result; +} + +module.exports = { + parsePidData, + parseEhicData, + parsePda1Data +} \ No newline at end of file diff --git a/dataset-reader/package.json b/dataset-reader/package.json new file mode 100644 index 0000000..0ffadfc --- /dev/null +++ b/dataset-reader/package.json @@ -0,0 +1,9 @@ +{ + "name": "dataset-reader", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "xlsx": "^0.18.5" + } +} diff --git a/dataset-reader/yarn.lock b/dataset-reader/yarn.lock new file mode 100644 index 0000000..7392f31 --- /dev/null +++ b/dataset-reader/yarn.lock @@ -0,0 +1,61 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +adler-32@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2" + integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== + +cfb@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" + integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== + dependencies: + adler-32 "~1.3.0" + crc-32 "~1.2.0" + +codepage@~1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab" + integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== + +crc-32@~1.2.0, crc-32@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + dependencies: + frac "~1.1.2" + +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + +xlsx@^0.18.5: + version "0.18.5" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0" + integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ== + dependencies: + adler-32 "~1.3.0" + cfb "~1.2.1" + codepage "~1.15.0" + crc-32 "~1.2.1" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" diff --git a/wallet-enterprise-configurations/acme-verifier/Dockerfile b/wallet-enterprise-configurations/acme-verifier/Dockerfile index 18ef25f..b7dfef0 100755 --- a/wallet-enterprise-configurations/acme-verifier/Dockerfile +++ b/wallet-enterprise-configurations/acme-verifier/Dockerfile @@ -26,6 +26,7 @@ COPY --from=builder /app/package.json . COPY --from=builder /app/dist ./dist COPY --from=builder /app/public ./public COPY --from=builder /app/views/ ./views/ +COPY --from=builder /app/dataset-reader-v1.0.0.tgz ./dataset-reader-v1.0.0.tgz RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ diff --git a/wallet-enterprise-configurations/acme-verifier/src/configuration/verifier/VerifierConfigurationService.ts b/wallet-enterprise-configurations/acme-verifier/src/configuration/verifier/VerifierConfigurationService.ts index ba94314..0720099 100644 --- a/wallet-enterprise-configurations/acme-verifier/src/configuration/verifier/VerifierConfigurationService.ts +++ b/wallet-enterprise-configurations/acme-verifier/src/configuration/verifier/VerifierConfigurationService.ts @@ -30,23 +30,23 @@ const verifiableIdDescriptor = { } }, { - "name": "First Name", - "path": ['$.credentialSubject.firstName'], + "name": "Family Name", + "path": ['$.credentialSubject.family_name'], "filter": {} }, { - "name": "Family Name", - "path": ['$.credentialSubject.familyName'], + "name": "Given Name", + "path": ['$.credentialSubject.given_name'], "filter": {} }, { "name": "Personal Identifier", - "path": ['$.credentialSubject.personalIdentifier'], + "path": ['$.credentialSubject.personal_identifier'], "filter": {} }, { "name": "Birthdate", - "path": ['$.credentialSubject.birthdate'], + "path": ['$.credentialSubject.birth_date'], "filter": {} } ] @@ -67,18 +67,18 @@ const verifiableIdDescriptorWithFirstnameLastnameAndBirthdate = { } }, { - "name": "First Name", - "path": ['$.credentialSubject.firstName'], + "name": "Given Name", + "path": ['$.credentialSubject.given_name'], "filter": {} }, { "name": "Family Name", - "path": ['$.credentialSubject.familyName'], + "path": ['$.credentialSubject.family_name'], "filter": {} }, { "name": "Birthdate", - "path": ['$.credentialSubject.birthdate'], + "path": ['$.credentialSubject.birth_date'], "filter": {} } ] @@ -102,37 +102,37 @@ const europeanHealthInsuranceCardDescriptor = { }, { "name": "SSN", - "path": ['$.credentialSubject.socialSecurityIdentification.ssn'], + "path": ['$.credentialSubject.social_security_pin'], "filter": {} }, { "name": "Starting Date", - "path": ['$.credentialSubject.validityPeriod.startingDate'], + "path": ['$.validFrom'], "filter": {} }, { "name": "Ending Date", - "path": ['$.credentialSubject.validityPeriod.endingDate'], + "path": ['$.expirationDate'], "filter": {} }, { "name": "Document Id", - "path": ['$.credentialSubject.documentId'], + "path": ['$.credentialSubject.ehic_card_identification_number'], "filter": {} }, { "name": "Competent Institution Id", - "path": ['$.credentialSubject.competentInstitution.competentInstitutionId'], + "path": ['$.credentialSubject.ehic_institution_id'], "filter": {} }, { "name": "Competent Institution Name", - "path": ['$.credentialSubject.competentInstitution.competentInstitutionName'], + "path": ['$.credentialSubject.ehic_institution_name'], "filter": {} }, { "name": "Competent Institution Country Code", - "path": ['$.credentialSubject.competentInstitution.competentInstitutionCountryCode'], + "path": ['$.credentialSubject.ehic_institution_country_code'], "filter": {} } ] @@ -155,72 +155,72 @@ const Pda1Descriptor = { }, { "name": "SSN", - "path": ['$.credentialSubject.socialSecurityIdentification.ssn'], + "path": ['$.credentialSubject.social_security_pin'], "filter": {} }, { "name": "Starting Date", - "path": ['$.credentialSubject.decisionOnApplicableLegislation.validityPeriod.startingDate'], + "path": ['$.validFrom'], "filter": {} }, { "name": "Ending Date", - "path": ['$.credentialSubject.decisionOnApplicableLegislation.validityPeriod.endingDate'], + "path": ['$.expirationDate'], "filter": {} }, { "name": "Document Id", - "path": ['$.credentialSubject.documentId'], + "path": ['$.credentialSubject.pda1_document_id'], "filter": {} }, { "name": "Competent Institution Id", - "path": ['$.credentialSubject.competentInstitution.competentInstitutionId'], + "path": ['$.credentialSubject.pda1_institution_id'], "filter": {} }, { "name": "Competent Institution Name", - "path": ['$.credentialSubject.competentInstitution.competentInstitutionName'], + "path": ['$.credentialSubject.pda1_institution_name'], "filter": {} }, { "name": "Competent Institution Country Code", - "path": ['$.credentialSubject.competentInstitution.competentInstitutionCountryCode'], + "path": ['$.credentialSubject.pda1_institution_country_code'], "filter": {} }, { "name": "Name of Employer", - "path": ['$.credentialSubject.employer.name'], + "path": ['$.credentialSubject.pda1_name'], "filter": {} }, { "name": "Employer Id", - "path": ['$.credentialSubject.employer.employerId'], + "path": ['$.credentialSubject.pda1_employer_id'], "filter": {} }, { "name": "Place of Work Street", - "path": ['$.credentialSubject.placeOfWork.street'], + "path": ['$.credentialSubject.pda1_pow_employer_street'], "filter": {} }, { "name": "Place of Work Town", - "path": ['$.credentialSubject.placeOfWork.town'], + "path": ['$.credentialSubject.pda1_pow_employer_town'], "filter": {} }, { "name": "Place of Work Postal Code", - "path": ['$.credentialSubject.placeOfWork.postalCode'], + "path": ['$.credentialSubject.pda1_pow_employer_postal_code'], "filter": {} }, { "name": "Place of Work Country Code", - "path": ['$.credentialSubject.placeOfWork.countryCode'], + "path": ['$.credentialSubject.pda1_pow_employer_country_code'], "filter": {} }, { "name": "Member State Legislation", - "path": ['$.credentialSubject.decisionOnApplicableLegislation.decisionOnMSWhoseLegislationApplies.memberStateWhoseLegislationIsToBeApplied'], + "path": ['$.credentialSubject.pda1_member_state'], "filter": {} } ] @@ -229,7 +229,7 @@ const Pda1Descriptor = { - +// @ts-ignore const verifiableIdWithEuropeanHealthInsuranceCardPresentationDefinition = { "id": "VerifiableIdWithEuropeanHealthInsuranceCard", "title": "PID and EHIC", @@ -253,6 +253,7 @@ const verifiableIdWithEuropeanHealthInsuranceCardPresentationDefinition = { // ] // } +// @ts-ignore const verifiableIdWithPda1PresentationDefinition = { "id": "PIDWithPda1", "title": "PID and PDA1", @@ -266,7 +267,7 @@ const verifiableIdWithPda1PresentationDefinition = { } - +// @ts-ignore const customVerifiableIdSdJwtPresentationDefinition = { "id": "CustomPID", "title": "Custom PID", @@ -278,6 +279,7 @@ const customVerifiableIdSdJwtPresentationDefinition = { ] } +// @ts-ignore const customEHICSdJwtPresentationDefinition = { "id": "CustomEHIC", "title": "Custom EHIC", @@ -289,6 +291,7 @@ const customEHICSdJwtPresentationDefinition = { ] } +// @ts-ignore const customPDA1SdJwtPresentationDefinition = { "id": "CustomPDA1", "title": "Custom PDA1", @@ -300,6 +303,149 @@ const customPDA1SdJwtPresentationDefinition = { ] } + +const scenarioOnePidPart = { + "id": "PID", + "constraints": { + "fields": [ + { + "name": "Credential Type", + "path": [ '$.type' ], + "filter": { + "type": 'array', + "items": { type: 'string' }, + "contains": { const: 'VerifiableId' } + } + }, + { + "name": "Family Name", + "path": ['$.credentialSubject.family_name'], + "filter": {} + }, + { + "name": "Given Name", + "path": ['$.credentialSubject.given_name'], + "filter": {} + }, + { + "name": "Personal Identifier", + "path": ['$.credentialSubject.personal_identifier'], + "filter": {} + }, + { + "name": "Birthdate", + "path": ['$.credentialSubject.birth_date'], + "filter": {} + } + ] + } +} + +const scenarioOneEhicPart = { + "id": "EHIC", + "constraints": { + "fields": [ + { + "name": "Credential Type", + "path": [ '$.type' ], + "filter": { + "type": 'array', + "items": { type: 'string' }, + "contains": { const: 'EuropeanHealthInsuranceCard' } + } + }, + { + "name": "Starting Date", + "path": ['$.validFrom'], + "filter": {} + }, + { + "name": "Expiration Date", + "path": ['$.expirationDate'], + "filter": {} + }, + { + "name": "Competent Institution Country", + "path": ['$.credentialSubject.ehic_institution_country_code'], + "filter": {} + }, + ] + } +} + +const scenarioTwoPda1Part = { + "id": "PDA1", + "constraints": { + "fields": [ + { + "name": "Credential Type", + "path": [ '$.type' ], + "filter": { + "type": 'array', + "items": { type: 'string' }, + "contains": { const: 'PDA1Credential' } + } + }, + { + "name": "Starting Date", + "path": ['$.validFrom'], + "filter": {} + }, + { + "name": "Expiration Date", + "path": ['$.expirationDate'], + "filter": {} + }, + { + "name": "Place of work Street", + "path": ['$.credentialSubject.pda1_pow_employer_street'], + "filter": {} + }, + { + "name": "Place of work Town", + "path": ['$.credentialSubject.pda1_pow_employer_town'], + "filter": {} + }, + { + "name": "Place of work Employer Postal Code", + "path": ['$.credentialSubject.pda1_pow_employer_postal_code'], + "filter": {} + }, + { + "name": "Place of work Employer Country Code", + "path": ['$.credentialSubject.pda1_pow_employer_country_code'], + "filter": {} + }, + + ] + } +} + +const scenarioOnePresentationDefinition = { + "id": "ScenarioOne", + "title": "Apply for Masters studies", + "description": "In order to apply for Masters studies, you will be asked to present your PID credential and an active European Health Insurance Card from your home country (**only country will be requested**) ", + "selectable": false, + "format": { "vc+sd-jwt": { alg: ['ES256'] }, jwt_vc_json: { alg: ['ES256'] }, jwt_vp: { alg: ['ES256'] } }, + "input_descriptors": [ + scenarioOnePidPart, + scenarioOneEhicPart + ] +} + +const scenarioTwoPresentationDefinition = { + "id": "ScenarioTwo", + "title": "Work site permission", + "description": "In order to enter the work site, you will be asked to present your PID credential and an active PDA1 document (**data related to the place of work will be requested**)", + "selectable": false, + "format": { "vc+sd-jwt": { alg: ['ES256'] }, jwt_vc_json: { alg: ['ES256'] }, jwt_vp: { alg: ['ES256'] } }, + "input_descriptors": [ + scenarioOnePidPart, + scenarioTwoPda1Part, + ] +} + + @injectable() export class VerifierConfigurationService implements VerifierConfigurationInterface { @@ -307,8 +453,10 @@ export class VerifierConfigurationService implements VerifierConfigurationInterf getPresentationDefinitions(): PresentationDefinitionTypeWithFormat[] { return [ customVerifiableIdSdJwtPresentationDefinition, - customEHICSdJwtPresentationDefinition, - customPDA1SdJwtPresentationDefinition, + // customEHICSdJwtPresentationDefinition, + // customPDA1SdJwtPresentationDefinition, + scenarioOnePresentationDefinition, + scenarioTwoPresentationDefinition, verifiableIdWithEuropeanHealthInsuranceCardPresentationDefinition, verifiableIdWithPda1PresentationDefinition, // verifiableIdWithPda1WithEuropeanHealthInsuranceCardPresentationDefinition diff --git a/wallet-enterprise-configurations/ehic-issuer/Dockerfile b/wallet-enterprise-configurations/ehic-issuer/Dockerfile index 3b9fe35..bda3681 100755 --- a/wallet-enterprise-configurations/ehic-issuer/Dockerfile +++ b/wallet-enterprise-configurations/ehic-issuer/Dockerfile @@ -19,6 +19,7 @@ COPY --from=builder /app/package.json . COPY --from=builder /app/dist/ ./dist/ COPY --from=builder /app/public/ ./public/ COPY --from=builder /app/views/ ./views/ +COPY --from=builder /app/dataset-reader-v1.0.0.tgz ./dataset-reader-v1.0.0.tgz RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ diff --git a/wallet-enterprise-configurations/ehic-issuer/config/config.development.ts b/wallet-enterprise-configurations/ehic-issuer/config/config.development.ts index 44e7333..8d57681 100644 --- a/wallet-enterprise-configurations/ehic-issuer/config/config.development.ts +++ b/wallet-enterprise-configurations/ehic-issuer/config/config.development.ts @@ -9,6 +9,9 @@ export = { password: "root", dbname: "ehicissuer" }, + resourcesVaultService: { + url: "http://resources-vault:6555" + }, wwwalletURL: "http://localhost:3000/cb", crl: { url: "http://credential-status-list:9001", diff --git a/wallet-enterprise-configurations/ehic-issuer/keys/issuer.private.ecdh.json b/wallet-enterprise-configurations/ehic-issuer/keys/issuer.private.ecdh.json new file mode 100644 index 0000000..8917f81 --- /dev/null +++ b/wallet-enterprise-configurations/ehic-issuer/keys/issuer.private.ecdh.json @@ -0,0 +1 @@ +{"kty":"EC","x":"5Q_PzfqQf6MRGFjMZ1owZGtuzHCozN_KKC2RNgzacLs","y":"Coaxh8MYMw1WeQODdAmI5C0ZXwi7h0qB1914G341Kww","crv":"P-256","d":"TYSFecf3b2uK1ubH12ROTkNPgpbnOazJLIxmZEEW1mU"} \ No newline at end of file diff --git a/wallet-enterprise-configurations/ehic-issuer/keys/issuer.public.ecdh.json b/wallet-enterprise-configurations/ehic-issuer/keys/issuer.public.ecdh.json new file mode 100644 index 0000000..060f44c --- /dev/null +++ b/wallet-enterprise-configurations/ehic-issuer/keys/issuer.public.ecdh.json @@ -0,0 +1 @@ +{"kty":"EC","x":"5Q_PzfqQf6MRGFjMZ1owZGtuzHCozN_KKC2RNgzacLs","y":"Coaxh8MYMw1WeQODdAmI5C0ZXwi7h0qB1914G341Kww","crv":"P-256"} \ No newline at end of file diff --git a/wallet-enterprise-configurations/ehic-issuer/keys/vault.public.ecdh.json b/wallet-enterprise-configurations/ehic-issuer/keys/vault.public.ecdh.json new file mode 100644 index 0000000..f466d4e --- /dev/null +++ b/wallet-enterprise-configurations/ehic-issuer/keys/vault.public.ecdh.json @@ -0,0 +1 @@ +{"kty":"EC","x":"PiXfYPrIGa3SE6bINX9X20EoKCtO_r2dDKhOzoDGnuU","y":"9gxiOT-vNDLe0HLNJA6HpH5vlCuCknnBmXUZLB05Vgk","crv":"P-256"} \ No newline at end of file diff --git a/wallet-enterprise-configurations/ehic-issuer/src/configuration/CredentialIssuersConfigurationService.ts b/wallet-enterprise-configurations/ehic-issuer/src/configuration/CredentialIssuersConfigurationService.ts index 8612728..9004c13 100644 --- a/wallet-enterprise-configurations/ehic-issuer/src/configuration/CredentialIssuersConfigurationService.ts +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/CredentialIssuersConfigurationService.ts @@ -11,13 +11,13 @@ import fs from 'fs'; import { util } from "@cef-ebsi/key-did-resolver"; import { HasherAlgorithm, HasherAndAlgorithm, SdJwt, SignatureAndEncryptionAlgorithm, Signer } from "@sd-jwt/core"; import { KeyLike, createHash, randomBytes, sign } from "crypto"; -import { EHICSupportedCredentialSdJwt } from "./SupportedCredentialsConfiguration/EHICSupportedCredentialSdJwt"; +import { PickupCodeEHICSupportedCredentialSdJwt } from "./SupportedCredentialsConfiguration/PickupCodeEHICSupportedCredentialSdJwt"; const issuerKeySetFile = fs.readFileSync(path.join(__dirname, '../../../keys/issuer.key.json'), 'utf-8'); const issuerKeySet = KeyIdentifierKeySchema.parse(JSON.parse(issuerKeySetFile)); const issuerSigner: CredentialSigner = { - sign: async function (payload, headers, disclosureFrame) { + sign: async function (payload, headers, disclosureFrame, { nbf, exp }) { const key = await importJWK(issuerKeySet.keys['ES256']?.privateKeyJwk, 'ES256'); const signer: Signer = (input, header) => { @@ -44,20 +44,15 @@ const issuerSigner: CredentialSigner = { const kid = `${did}#${did.split(':')[2]}`; const issuanceDate = new Date(); - const expirationDate = (() => { - if (payload.vc.credentialSubject?.validityPeriod?.endingDate) { - const expirationDate = new Date(payload.vc.credentialSubject.validityPeriod.endingDate); - return expirationDate; - } - else { // if no expiration date found on credential subject, then set it to next year - const expirationDate = new Date(); - expirationDate.setFullYear(expirationDate.getFullYear() + 1); - return expirationDate; - } - })(); - payload.vc.expirationDate = expirationDate.toISOString(); - payload.exp = Math.floor(expirationDate.getTime() / 1000); + if (nbf) { + payload.nbf = nbf; + payload.vc.validFrom = new Date(nbf * 1000).toISOString(); + } + if (exp) { + payload.exp = exp; + payload.vc.expirationDate = new Date(exp * 1000).toISOString(); + } payload.vc.issuanceDate = issuanceDate.toISOString(); payload.iat = Math.floor(issuanceDate.getTime() / 1000); @@ -101,8 +96,8 @@ export class CredentialIssuersConfigurationService implements CredentialIssuersC // ehicIssuer.addSupportedCredential(new CTWalletSameInTimeSupportedCredential(ehicIssuer)); // ehicIssuer.addSupportedCredential(new CTWalletSameDeferredSupportedCredential(ehicIssuer)); // ehicIssuer.addSupportedCredential(new CTWalletSamePreAuthorisedSupportedCredential(ehicIssuer)); - ehicIssuer.addSupportedCredential(new EHICSupportedCredentialSdJwt(ehicIssuer)); - + // ehicIssuer.addSupportedCredential(new EHICSupportedCredentialSdJwt(ehicIssuer)); + ehicIssuer.addSupportedCredential(new PickupCodeEHICSupportedCredentialSdJwt(ehicIssuer)); // const ehicIssuer2 = new CredentialIssuer() // .setCredentialIssuerIdentifier(config.url + "/vid") // .setWalletId("conformant") diff --git a/wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/EHICSupportedCredentialSdJwt.ts b/wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/EHICSupportedCredentialSdJwt.ts index 546b433..001492e 100644 --- a/wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/EHICSupportedCredentialSdJwt.ts +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/EHICSupportedCredentialSdJwt.ts @@ -27,7 +27,7 @@ export class EHICSupportedCredentialSdJwt implements SupportedCredentialProtocol return VerifiableCredentialFormat.VC_SD_JWT; } getTypes(): string[] { - return ["VerifiableCredential", "VerifiableAttestation", "EuropeanHealthInsuranceCard", this.getId()]; + return ["VerifiableCredential", "VerifiableAttestation", "EuropeanHealthInsuranceCard", "NoPickupCode", this.getId()]; } getDisplay(): Display { return { @@ -121,7 +121,7 @@ export class EHICSupportedCredentialSdJwt implements SupportedCredentialProtocol const { jws } = await this.getCredentialIssuerConfig().getCredentialSigner() .sign({ vc: payload - }, {}, disclosureFrame); + }, {}, disclosureFrame, {}); const response = { format: this.getFormat(), credential: jws diff --git a/wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/PickupCodeEHICSupportedCredentialSdJwt.ts b/wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/PickupCodeEHICSupportedCredentialSdJwt.ts new file mode 100644 index 0000000..901c117 --- /dev/null +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/PickupCodeEHICSupportedCredentialSdJwt.ts @@ -0,0 +1,238 @@ +import config from "../../../config"; +import { VerifiableCredentialFormat, Display, CredentialSupportedJwtVcJson } from "../../types/oid4vci"; +import { CredentialIssuer } from "../../lib/CredentialIssuerConfig/CredentialIssuer"; +import { SupportedCredentialProtocol } from "../../lib/CredentialIssuerConfig/SupportedCredentialProtocol"; +import { AuthorizationServerState } from "../../entities/AuthorizationServerState.entity"; +import { CredentialView } from "../../authorization/types"; +import crypto, { randomUUID } from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; +import { compactDecrypt, calculateJwkThumbprint, CompactEncrypt } from 'jose'; +import { CredentialStatusList } from "../../lib/CredentialStatus"; +const currentWorkingDirectory = __dirname + "/../../../../"; + +var publicKeyFilePath; +var publicKeyContent; + +publicKeyFilePath = path.resolve(currentWorkingDirectory, 'keys', 'issuer.public.ecdh.json'); +publicKeyContent = fs.readFileSync(publicKeyFilePath, 'utf8'); +const credentialIssuerPublicKeyJWK = JSON.parse(publicKeyContent) as crypto.JsonWebKey; +// const credentialIssuerPublicKey = crypto.createPublicKey({ key: credentialIssuerPublicKeyJWK, format: 'jwk' }); + + +publicKeyFilePath = path.resolve(currentWorkingDirectory, 'keys', 'vault.public.ecdh.json'); +publicKeyContent = fs.readFileSync(publicKeyFilePath, 'utf8'); +const vaultPublicKeyJWK = JSON.parse(publicKeyContent) as crypto.JsonWebKey; +const vaultPublicKey = crypto.createPublicKey({ key: vaultPublicKeyJWK, format: 'jwk' }); + + +var privateKeyFilePath; +var privateKeyContent; + +privateKeyFilePath = path.resolve(currentWorkingDirectory, 'keys', 'issuer.private.ecdh.json'); +privateKeyContent = fs.readFileSync(privateKeyFilePath, 'utf8'); +const credentialIssuerPrivateKeyJWK = JSON.parse(privateKeyContent) as crypto.JsonWebKey; +const credentialIssuerPrivateKey = crypto.createPrivateKey({ key: credentialIssuerPrivateKeyJWK, format: 'jwk' }); + + + +export class PickupCodeEHICSupportedCredentialSdJwt implements SupportedCredentialProtocol { + + + constructor(private credentialIssuerConfig: CredentialIssuer) { } + + getCredentialIssuerConfig(): CredentialIssuer { + return this.credentialIssuerConfig; + } + getId(): string { + return "urn:credential:ehic" + } + getFormat(): VerifiableCredentialFormat { + return VerifiableCredentialFormat.VC_SD_JWT; + } + getTypes(): string[] { + return ["VerifiableCredential", "VerifiableAttestation", "EuropeanHealthInsuranceCard", this.getId()]; + } + getDisplay(): Display { + return { + name: "EHIC Card", + logo: { url: config.url + "/images/ehicCard.png" }, + background_color: "#4CC3DD" + } + } + + + async getProfile(_userSession: AuthorizationServerState): Promise { + return null; + } + + async generateCredentialResponse(userSession: AuthorizationServerState, holderDID: string): Promise<{ format: VerifiableCredentialFormat; credential: any; }> { + if (!userSession.issuer_state || userSession.issuer_state == "null") { + throw new Error("issuer_state was not found user session"); + } + + console.log('type of issuer state ', typeof userSession.issuer_state); + if (!userSession.personalIdentifier) { + throw new Error("Cannot generate credential: personalIdentifier is missing"); + } + + const { issuer_state } = userSession; + console.log("issuer state = ", userSession.issuer_state); + let { plaintext } = await compactDecrypt(issuer_state, credentialIssuerPrivateKey); + const { + iss, + exp, + jti, // is the collection id + aud, + sub, // authorized identities to receive this specific credential, + nonce, + } = JSON.parse(new TextDecoder().decode(plaintext)) as { iss: string, exp: number, jti: string, aud: string, sub: string[], nonce: string }; + + + console.log("Issuer state attributes: ", { + iss, + exp, + jti, // is the collection id + aud, + sub, // authorized identities to receive this specific credential + nonce, + }) + const expectedIssuer = await calculateJwkThumbprint(vaultPublicKeyJWK); + if (!iss || iss !== expectedIssuer) { + throw new Error(`'iss' is missing from issuer_state or expected value '${expectedIssuer}'`); + } + + const expectedAudience = await calculateJwkThumbprint(credentialIssuerPublicKeyJWK); + if (!aud || aud !== expectedAudience) { + throw new Error(`'aud' is missing from issuer_state or expected value for '${expectedAudience}'`); + } + + if (exp && Math.floor(Date.now() / 1000) > exp) { + console.log("Exp cmp = ", Math.floor(Date.now() / 1000) > exp) + throw new Error(`'exp' is missing from issuer_state or the issuer_state is expired`); + } + + console.log("User session = ", userSession) + if (!sub || !sub.includes(userSession.personalIdentifier)) { + console.log(`Personal identifier ${userSession.personalIdentifier} is not authorized to receive this credential`); + throw new Error(`Personal identifier ${userSession.personalIdentifier} is not authorized to receive this credential`); + } + + const collection_id = jti; + + const jwePayload = { + iss: await calculateJwkThumbprint(credentialIssuerPublicKeyJWK), + exp: (Math.floor(Date.now() / 1000)) + 60*5, // expires in 5 minutes, + jti: collection_id, + aud: await calculateJwkThumbprint(vaultPublicKeyJWK), + sub: userSession.personalIdentifier, + nonce: nonce, + }; + + const fetchRequestToken = await new CompactEncrypt(new TextEncoder().encode(JSON.stringify(jwePayload))) + .setProtectedHeader({ + alg: 'ECDH-ES+A256KW', // Elliptic Curve Diffie-Hellman Ephemeral Static with AES Key Wrap using 256-bit key + enc: 'A256GCM', // AES GCM using 256-bit key + epk: { + kty: 'EC', // Elliptic Curve Key Type + crv: 'P-256' // Curve name + // you can add other parameters as needed, like 'x' and 'y' for specific key pairs + } + }) + .encrypt(vaultPublicKey); + + let fetchResponse = null; + try { + fetchResponse = await axios.post('http://resources-vault:6555/fetch', { + fetch_request_token: fetchRequestToken + }); + } + catch(err) { + console.log(err) + console.error('Failed fetch request') + throw new Error("Failed fetch request"); + } + if (fetchResponse == null || !fetchResponse.data.claims) { + console.error("'claims' is missing from resources vault fetch response"); + throw new Error("'claims' is missing from resources vault fetch response"); + } + + const { claims } = fetchResponse.data; + console.log("Claims = ", claims) + + + // use the dataset to retrieve only the username based on personalIdentifier + const payload = { + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": this.getTypes(), + "id": `urn:ehic:${randomUUID()}`, + "name": "EHIC ID Card", // https://www.w3.org/TR/vc-data-model-2.0/#names-and-descriptions + "description": "This credential is issued by the National EHIC ID credential issuer and it can be used for authentication purposes", + "credentialSubject": { + social_security_pin: String(claims.social_security_pin), + ehic_card_identification_number: String(claims.ehic_card_identification_number), + ehic_institution_id: String(claims.ehic_institution_id), + ehic_institution_name: claims.ehic_institution_name, + ehic_institution_country_code: claims.ehic_institution_country_code, + pid_id: undefined, + "id": holderDID, + }, + "credentialStatus": { + "id": `${config.crl.url}#${(await CredentialStatusList.insert(userSession.familyName ?? "", userSession.personalIdentifier)).id}`, + "type": "CertificateRevocationList" + }, + "credentialBranding": { + "image": { + "url": config.url + "/images/ehicCard.png" + }, + "backgroundColor": "#8ebeeb", + "textColor": "#ffffff" + }, + }; + + console.log("payload = ", payload) + const disclosureFrame = { + vc: { + credentialSubject: { + // familyName: true, + // firstName: true, + // birthdate: true, + social_security_pin: true, + ehic_card_identification_number: true, + ehic_institution_id: true, + ehic_institution_name: true, + ehic_institution_country_code: true, + } + } + } + + console.log("Claims = ", claims) + const { ehic_start_date, ehic_end_date } = claims; + const { jws } = await this.getCredentialIssuerConfig().getCredentialSigner() + .sign({ + vc: payload + }, {}, disclosureFrame, { + nbf: Math.floor(new Date(ehic_start_date).getTime() / 1000), + exp: Math.floor(new Date(ehic_end_date).getTime() / 1000) + }); + const response = { + format: this.getFormat(), + credential: jws + }; + + return response; + } + + exportCredentialSupportedObject(): CredentialSupportedJwtVcJson { + return { + id: this.getId(), + format: this.getFormat(), + display: [this.getDisplay()], + types: this.getTypes(), + cryptographic_binding_methods_supported: ["ES256"] + } + } + +} + diff --git a/wallet-enterprise-configurations/ehic-issuer/src/configuration/authentication/VIDAuthenticationComponent.ts b/wallet-enterprise-configurations/ehic-issuer/src/configuration/authentication/VIDAuthenticationComponent.ts index a0806c8..944d181 100644 --- a/wallet-enterprise-configurations/ehic-issuer/src/configuration/authentication/VIDAuthenticationComponent.ts +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/authentication/VIDAuthenticationComponent.ts @@ -64,10 +64,8 @@ export class VIDAuthenticationComponent extends AuthenticationComponent { const [_credentialHeader, credentialPayload, _sig] = credential.split('.'); const parsedCredPayload = JSON.parse(base64url.decode(credentialPayload)) as any; - - const { validityPeriod: { startingDate, endingDate }} = parsedCredPayload.vc.credentialSubject; - if (new Date(startingDate) > new Date() || new Date() > new Date(endingDate)) { + if (new Date(parsedCredPayload.exp * 1000) < new Date()) { return { valid: false }; } @@ -98,21 +96,24 @@ export class VIDAuthenticationComponent extends AuthenticationComponent { const { valid } = await this.checkForInvalidCredentials(queryRes!.raw_presentation as string); if (!valid) { - return await this.redirectToFailurePage(req, res, "Credential is not valid"); + return await this.redirectToFailurePage(req, res, "Credential is not valid", authorizationServerState.redirect_uri != undefined ? new URL(authorizationServerState.redirect_uri).origin : null); } - const personalIdentifier = queryRes.claims["PID"].filter((claim) => claim.name == 'personalIdentifier')[0].value ?? null; + const personalIdentifier = queryRes.claims["PID"].filter((claim) => claim.name == 'personal_identifier')[0].value ?? null; + const familyName = queryRes.claims["PID"].filter((claim) => claim.name == 'family_name')[0].value ?? null; if (!personalIdentifier) { return; } authorizationServerState.personalIdentifier = personalIdentifier; - + authorizationServerState.familyName = familyName; req.session.authenticationChain.vidAuthenticationComponent = { - personalIdentifier: personalIdentifier + personalIdentifier: personalIdentifier, + familyName: familyName, }; console.log("Personal identifier = ", personalIdentifier) req.authorizationServerState.personalIdentifier = personalIdentifier; + req.authorizationServerState.familyName = familyName; await AppDataSource.getRepository(AuthorizationServerState).save(authorizationServerState); return res.redirect(this.protectedEndpoint); @@ -120,11 +121,12 @@ export class VIDAuthenticationComponent extends AuthenticationComponent { } - private async redirectToFailurePage(_req: Request, res: Response, msg: string) { + private async redirectToFailurePage(_req: Request, res: Response, msg: string, redirectUrl: string | null) { res.render('error', { code: 100, msg: msg, locale: locale, + redirect: redirectUrl }) } diff --git a/wallet-enterprise-configurations/ehic-issuer/src/configuration/authentication/authenticationChain.ts b/wallet-enterprise-configurations/ehic-issuer/src/configuration/authentication/authenticationChain.ts index c6f72be..f82aeb6 100644 --- a/wallet-enterprise-configurations/ehic-issuer/src/configuration/authentication/authenticationChain.ts +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/authentication/authenticationChain.ts @@ -1,6 +1,5 @@ import { CONSENT_ENTRYPOINT, VERIFIER_PANEL_ENTRYPOINT } from "../../authorization/constants"; import { AuthenticationChainBuilder } from "../../authentication/AuthenticationComponent"; -import { LocalAuthenticationComponent } from "./LocalAuthenticationComponent"; import { VerifierAuthenticationComponent } from "./VerifierAuthenticationComponent"; import { IssuerSelectionComponent } from "./IssuerSelectionComponent"; import { AuthenticationMethodSelectionComponent } from "./AuthenticationMethodSelectionComponent"; @@ -10,7 +9,7 @@ import { VIDAuthenticationComponent } from "./VIDAuthenticationComponent"; export const authChain = new AuthenticationChainBuilder() .addAuthenticationComponent(new AuthenticationMethodSelectionComponent("auth-method", CONSENT_ENTRYPOINT)) .addAuthenticationComponent(new VIDAuthenticationComponent("vid-authentication", CONSENT_ENTRYPOINT)) - .addAuthenticationComponent(new LocalAuthenticationComponent("1-local", CONSENT_ENTRYPOINT)) + // .addAuthenticationComponent(new LocalAuthenticationComponent("1-local", CONSENT_ENTRYPOINT)) .addAuthenticationComponent(new IssuerSelectionComponent("2-issuer-selection", CONSENT_ENTRYPOINT)) .build(); diff --git a/wallet-enterprise-configurations/ehic-issuer/src/configuration/consent/consent.config.ts b/wallet-enterprise-configurations/ehic-issuer/src/configuration/consent/consent.config.ts index 2ed6be7..11f4ad6 100644 --- a/wallet-enterprise-configurations/ehic-issuer/src/configuration/consent/consent.config.ts +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/consent/consent.config.ts @@ -5,5 +5,5 @@ * * For EBSI conformance tests, it should be set to true */ -export const SKIP_CONSENT = false; +export const SKIP_CONSENT = true; export const REQUIRE_PIN = true; \ No newline at end of file diff --git a/wallet-enterprise-configurations/ehic-issuer/src/configuration/locale.ts b/wallet-enterprise-configurations/ehic-issuer/src/configuration/locale.ts index 44026ff..271f12d 100644 --- a/wallet-enterprise-configurations/ehic-issuer/src/configuration/locale.ts +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/locale.ts @@ -22,7 +22,7 @@ const locale = { }, index: { header: "DC4EU EHIC Issuer", - phrase: "I want to receive my eEHIC", + phrase: "eEHIC", proceed: "Proceed", heading: "DC4EU EHIC Issuer", paragraph: "This is a portal where citizens can receive their digital EHIC Card in their wallet. To proceed you must first have an EUDI wallet from a provider of your choice. This Verifiable eEHIC will contain the Social Security Number as your personal identifier for social security.", diff --git a/wallet-enterprise-configurations/ehic-issuer/src/configuration/verifier/VerifierConfigurationService.ts b/wallet-enterprise-configurations/ehic-issuer/src/configuration/verifier/VerifierConfigurationService.ts index e5da60b..0abba99 100644 --- a/wallet-enterprise-configurations/ehic-issuer/src/configuration/verifier/VerifierConfigurationService.ts +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/verifier/VerifierConfigurationService.ts @@ -27,7 +27,13 @@ export class VerifierConfigurationService implements VerifierConfigurationInterf "fields": [ { "path": [ - "$.credentialSubject.personalIdentifier" + "$.credentialSubject.personal_identifier" + ], + "filter": {} + }, + { + "path": [ + "$.credentialSubject.family_name" ], "filter": {} }, @@ -45,7 +51,7 @@ export class VerifierConfigurationService implements VerifierConfigurationInterf } ] } - ] + ] as any[]; } getConfiguration(): OpenidForPresentationsConfiguration { diff --git a/wallet-enterprise-configurations/ehic-issuer/views/index.pug b/wallet-enterprise-configurations/ehic-issuer/views/index.pug index 1a622be..57d7c5f 100644 --- a/wallet-enterprise-configurations/ehic-issuer/views/index.pug +++ b/wallet-enterprise-configurations/ehic-issuer/views/index.pug @@ -9,7 +9,7 @@ block layout-content img(src="/images/ehicCard.png", alt="Descriptive text", class="index-card") p.title #{locale.index.phrase} p.text-area #{locale.index.subtitle} - button.Btn.Large.Landing(id="mainBtn" type="submit") #{locale.index.proceed} + //- button.Btn.Large.Landing(id="mainBtn" type="submit") #{locale.index.proceed} input(type="hidden" id="initiate_pre_authorized" name="initiate_pre_authorized" value="true") //- div.item.VerifierSection //- .ItemLayout diff --git a/wallet-enterprise-configurations/pda1-issuer/Dockerfile b/wallet-enterprise-configurations/pda1-issuer/Dockerfile index f73f1ce..a36a202 100755 --- a/wallet-enterprise-configurations/pda1-issuer/Dockerfile +++ b/wallet-enterprise-configurations/pda1-issuer/Dockerfile @@ -20,6 +20,7 @@ COPY --from=builder /app/package.json . COPY --from=builder /app/dist/ ./dist/ COPY --from=builder /app/public/ ./public/ COPY --from=builder /app/views/ ./views/ +COPY --from=builder /app/dataset-reader-v1.0.0.tgz ./dataset-reader-v1.0.0.tgz RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ yarn cache clean && yarn install --production diff --git a/wallet-enterprise-configurations/pda1-issuer/src/configuration/CredentialIssuersConfigurationService.ts b/wallet-enterprise-configurations/pda1-issuer/src/configuration/CredentialIssuersConfigurationService.ts index 06866b2..ba07fdc 100644 --- a/wallet-enterprise-configurations/pda1-issuer/src/configuration/CredentialIssuersConfigurationService.ts +++ b/wallet-enterprise-configurations/pda1-issuer/src/configuration/CredentialIssuersConfigurationService.ts @@ -17,7 +17,7 @@ const issuerKeySetFile = fs.readFileSync(path.join(__dirname, '../../../keys/iss const issuerKeySet = KeyIdentifierKeySchema.parse(JSON.parse(issuerKeySetFile)); const issuerSigner: CredentialSigner = { - sign: async function (payload, headers, disclosureFrame) { + sign: async function (payload, headers, disclosureFrame, { nbf, exp }) { const key = await importJWK(issuerKeySet.keys['ES256']?.privateKeyJwk, 'ES256'); const signer: Signer = (input, header) => { @@ -44,20 +44,17 @@ const issuerSigner: CredentialSigner = { const kid = `${did}#${did.split(':')[2]}`; const issuanceDate = new Date(); - const expirationDate = (() => { - if (payload.vc.credentialSubject?.decisionOnApplicableLegislation?.validityPeriod?.endingDate) { - const expirationDate = new Date(payload.vc.credentialSubject.decisionOnApplicableLegislation.validityPeriod.endingDate); - return expirationDate; - } - else { // if no expiration date found on credential subject, then set it to next year - const expirationDate = new Date(); - expirationDate.setFullYear(expirationDate.getFullYear() + 1); - return expirationDate; - } - })(); - payload.vc.expirationDate = expirationDate.toISOString(); - payload.exp = Math.floor(expirationDate.getTime() / 1000); + + + if (nbf) { + payload.nbf = nbf; + payload.vc.validFrom = new Date(nbf * 1000).toISOString(); + } + if (exp) { + payload.exp = exp; + payload.vc.expirationDate = new Date(exp * 1000).toISOString(); + } payload.vc.issuanceDate = issuanceDate.toISOString(); payload.iat = Math.floor(issuanceDate.getTime() / 1000); diff --git a/wallet-enterprise-configurations/pda1-issuer/src/configuration/SupportedCredentialsConfiguration/PDA1SupportedCredentialSdJwt.ts b/wallet-enterprise-configurations/pda1-issuer/src/configuration/SupportedCredentialsConfiguration/PDA1SupportedCredentialSdJwt.ts index 8712131..ff6af24 100644 --- a/wallet-enterprise-configurations/pda1-issuer/src/configuration/SupportedCredentialsConfiguration/PDA1SupportedCredentialSdJwt.ts +++ b/wallet-enterprise-configurations/pda1-issuer/src/configuration/SupportedCredentialsConfiguration/PDA1SupportedCredentialSdJwt.ts @@ -35,11 +35,6 @@ privateKeyContent = fs.readFileSync(privateKeyFilePath, 'utf8'); const credentialIssuerPrivateKeyJWK = JSON.parse(privateKeyContent) as crypto.JsonWebKey; const credentialIssuerPrivateKey = crypto.createPrivateKey({ key: credentialIssuerPrivateKeyJWK, format: 'jwk' }); - - -const dataset = JSON.parse(fs.readFileSync('/datasets/dataset.json', 'utf-8').toString()) as any; - - export class PDA1SupportedCredentialSdJwt implements SupportedCredentialProtocol { @@ -75,6 +70,8 @@ export class PDA1SupportedCredentialSdJwt implements SupportedCredentialProtocol throw new Error("issuer_state was not found user session"); } + console.log("Family name = ", userSession.familyName) + console.log('type of issuer state ', typeof userSession.issuer_state); if (!userSession.personalIdentifier) { throw new Error("Cannot generate credential: personalIdentifier is missing"); @@ -126,7 +123,7 @@ export class PDA1SupportedCredentialSdJwt implements SupportedCredentialProtocol const jwePayload = { iss: await calculateJwkThumbprint(credentialIssuerPublicKeyJWK), - exp: Date.now() + 60*5, // expires in 5 minutes, + exp: (Math.floor(Date.now() / 1000)) + 60*5, // expires in 5 minutes, jti: collection_id, aud: await calculateJwkThumbprint(vaultPublicKeyJWK), sub: userSession.personalIdentifier, @@ -166,7 +163,6 @@ export class PDA1SupportedCredentialSdJwt implements SupportedCredentialProtocol // use the dataset to retrieve only the username based on personalIdentifier - const username = dataset.users.filter((u: any) => u.authentication.personalIdentifier == userSession.personalIdentifier)[0].authentication.username; const payload = { "@context": ["https://www.w3.org/2018/credentials/v1"], "type": this.getTypes(), @@ -175,10 +171,14 @@ export class PDA1SupportedCredentialSdJwt implements SupportedCredentialProtocol "description": "This credential is issued by the National PDA1 credential issuer", "credentialSubject": { ...claims, + pid_id: undefined, // hide this field + pda1_expiry_date: undefined, // hide this field + pda1_starting_date: undefined, // hide this field + pda1_ending_date: undefined, // hide this field "id": holderDID, }, "credentialStatus": { - "id": `${config.crl.url}#${(await CredentialStatusList.insert(username, claims.personalIdentifier)).id}`, + "id": `${config.crl.url}#${(await CredentialStatusList.insert(userSession.familyName ?? "", claims.pid_id)).id}`, "type": "CertificateRevocationList" }, "credentialBranding": { @@ -194,52 +194,50 @@ export class PDA1SupportedCredentialSdJwt implements SupportedCredentialProtocol const disclosureFrame = { vc: { credentialSubject: { - personalIdentifier: true, - socialSecurityIdentification: { - ssn: true - }, + social_security_pin: true, nationality: true, - employer: { - employmentType: true, - name: true, - employerId: true, - typeOfId: true - }, - decisionOnApplicableLegislation: { - validityPeriod: { - startingDate: true, - endingDate: true - } - }, - address: { - street: true, - town: true, - postalCode: true, - countryCode: true - }, - placeOfWork: { - companyName: true, - flagBaseHomeState: true, - companyId: true, - typeOfId: true, - street: true, - town: true, - postalCode: true, - countryCode: true - }, - documentId: true, - competentInstitution: { - competentInstitutionId: true, - competentInstitutionName: true, - competentInstitutionCountryCode: true - }, + type_of_employment: true, + pda1_name: true, + pda1_employer_id: true, + pda1_type_of_id: true, + pda1_employer_street: true, + pda1_employer_town: true, + pda1_employer_postal_code: true, + pda1_employer_country_code: true, + + pda1_pow_country_code: true, + pda1_pow_company_name: true, + pda1_pow_company_id: true, + pda1_pow_type_of_id: true, + pda1_pow_employer_country_code: true, + pda1_flag_base_home_state: true, + pda1_pow_employer_street: true, + pda1_pow_employer_town: true, + pda1_pow_employer_postal_code: true, + pda1_member_state: true, + pda1_transitional_rules: true, + + pda1_status_confirmation: true, + + pda1_document_id: true, + + pda1_institution_id: true, + pda1_institution_name: true, + pda1_institution_country_code: true, + } } } + + const { pda1_starting_date, pda1_ending_date } = claims; + const { jws } = await this.getCredentialIssuerConfig().getCredentialSigner() .sign({ vc: payload - }, {}, disclosureFrame); + }, {}, disclosureFrame, { + nbf: Math.floor(new Date(pda1_starting_date).getTime() / 1000), + exp: Math.floor(new Date(pda1_ending_date).getTime() / 1000) + }); const response = { format: this.getFormat(), credential: jws diff --git a/wallet-enterprise-configurations/pda1-issuer/src/configuration/authentication/VIDAuthenticationComponent.ts b/wallet-enterprise-configurations/pda1-issuer/src/configuration/authentication/VIDAuthenticationComponent.ts index 161bf7e..75670d0 100644 --- a/wallet-enterprise-configurations/pda1-issuer/src/configuration/authentication/VIDAuthenticationComponent.ts +++ b/wallet-enterprise-configurations/pda1-issuer/src/configuration/authentication/VIDAuthenticationComponent.ts @@ -64,10 +64,8 @@ export class VIDAuthenticationComponent extends AuthenticationComponent { const [_credentialHeader, credentialPayload, _sig] = credential.split('.'); const parsedCredPayload = JSON.parse(base64url.decode(credentialPayload)) as any; - - const { validityPeriod: { startingDate, endingDate }} = parsedCredPayload.vc.credentialSubject; - - if (new Date(startingDate) > new Date() || new Date() > new Date(endingDate)) { + console.log("Parsed payload = ", parsedCredPayload) + if (new Date(parsedCredPayload.exp * 1000) < new Date()) { return { valid: false }; } @@ -81,6 +79,7 @@ export class VIDAuthenticationComponent extends AuthenticationComponent { .createQueryBuilder("vp") .where("vp.state = :state_id", { state_id: state }) .getOne(); + if (!queryRes) { return; } @@ -92,37 +91,49 @@ export class VIDAuthenticationComponent extends AuthenticationComponent { .getOne(); if (!authorizationServerState || !vp_token || !queryRes.claims || !queryRes.claims["PID"]) { + console.error("authorizationServerState or vp_token or claims or claims['PID'] is not defined") return; } + console.log("Checking validity") const { valid } = await this.checkForInvalidCredentials(queryRes!.raw_presentation as string); if (!valid) { - return await this.redirectToFailurePage(req, res, "Credential is not valid"); + // provide wallet redirect url + return await this.redirectToFailurePage(req, res, "Credential is not valid", authorizationServerState.redirect_uri != undefined ? new URL(authorizationServerState.redirect_uri).origin : null); } - const personalIdentifier = queryRes.claims["PID"].filter((claim) => claim.name == 'personalIdentifier')[0].value ?? null; - + + const personalIdentifier = queryRes.claims["PID"].filter((claim) => claim.name == 'personal_identifier')[0].value ?? null; + + const familyName = queryRes.claims["PID"].filter((claim) => claim.name == 'family_name')[0].value ?? null; + console.log("Parsed family name = ", familyName) + if (!personalIdentifier) { + console.error("personal identifier could not be extracted") return; } authorizationServerState.personalIdentifier = personalIdentifier; + authorizationServerState.familyName = familyName; req.session.authenticationChain.vidAuthenticationComponent = { - personalIdentifier: personalIdentifier + personalIdentifier: personalIdentifier, + familyName: familyName, }; console.log("Personal identifier = ", personalIdentifier) req.authorizationServerState.personalIdentifier = personalIdentifier; + req.authorizationServerState.familyName = familyName; await AppDataSource.getRepository(AuthorizationServerState).save(authorizationServerState); return res.redirect(this.protectedEndpoint); } - private async redirectToFailurePage(_req: Request, res: Response, msg: string) { + private async redirectToFailurePage(_req: Request, res: Response, msg: string, redirectUrl: string | null) { res.render('error', { code: 100, msg: msg, locale: locale, + redirect: redirectUrl }) } diff --git a/wallet-enterprise-configurations/pda1-issuer/src/configuration/authentication/authenticationChain.ts b/wallet-enterprise-configurations/pda1-issuer/src/configuration/authentication/authenticationChain.ts index 13728b5..acb1274 100644 --- a/wallet-enterprise-configurations/pda1-issuer/src/configuration/authentication/authenticationChain.ts +++ b/wallet-enterprise-configurations/pda1-issuer/src/configuration/authentication/authenticationChain.ts @@ -1,6 +1,5 @@ import { CONSENT_ENTRYPOINT, VERIFIER_PANEL_ENTRYPOINT } from "../../authorization/constants"; import { AuthenticationChainBuilder } from "../../authentication/AuthenticationComponent"; -import { LocalAuthenticationComponent } from "./LocalAuthenticationComponent"; import { VerifierAuthenticationComponent } from "./VerifierAuthenticationComponent"; import { IssuerSelectionComponent } from "./IssuerSelectionComponent"; import { AuthenticationMethodSelectionComponent } from "./AuthenticationMethodSelectionComponent"; @@ -10,7 +9,7 @@ import { RecipientValidationComponent } from './RecipientValidationComponent'; export const authChain = new AuthenticationChainBuilder() .addAuthenticationComponent(new AuthenticationMethodSelectionComponent("auth-method", CONSENT_ENTRYPOINT)) .addAuthenticationComponent(new VIDAuthenticationComponent("vid-authentication", CONSENT_ENTRYPOINT)) - .addAuthenticationComponent(new LocalAuthenticationComponent("1-local", CONSENT_ENTRYPOINT)) + // .addAuthenticationComponent(new LocalAuthenticationComponent("1-local", CONSENT_ENTRYPOINT)) .addAuthenticationComponent(new RecipientValidationComponent("3-recipient-validation", CONSENT_ENTRYPOINT)) .addAuthenticationComponent(new IssuerSelectionComponent("2-issuer-selection", CONSENT_ENTRYPOINT)) .build(); diff --git a/wallet-enterprise-configurations/pda1-issuer/src/configuration/verifier/VerifierConfigurationService.ts b/wallet-enterprise-configurations/pda1-issuer/src/configuration/verifier/VerifierConfigurationService.ts index bfe3788..9a7fffa 100644 --- a/wallet-enterprise-configurations/pda1-issuer/src/configuration/verifier/VerifierConfigurationService.ts +++ b/wallet-enterprise-configurations/pda1-issuer/src/configuration/verifier/VerifierConfigurationService.ts @@ -27,7 +27,13 @@ export class VerifierConfigurationService implements VerifierConfigurationInterf "fields": [ { "path": [ - "$.credentialSubject.personalIdentifier" + "$.credentialSubject.personal_identifier" + ], + "filter": {} + }, + { + "path": [ + "$.credentialSubject.family_name" ], "filter": {} }, @@ -45,7 +51,7 @@ export class VerifierConfigurationService implements VerifierConfigurationInterf } ] } - ] + ] as any[] } getConfiguration(): OpenidForPresentationsConfiguration { diff --git a/wallet-enterprise-configurations/vid-issuer/Dockerfile b/wallet-enterprise-configurations/vid-issuer/Dockerfile index b5c1d59..547d28e 100755 --- a/wallet-enterprise-configurations/vid-issuer/Dockerfile +++ b/wallet-enterprise-configurations/vid-issuer/Dockerfile @@ -20,6 +20,7 @@ COPY --from=builder /app/package.json . COPY --from=builder /app/dist/ ./dist/ COPY --from=builder /app/public/ ./public/ COPY --from=builder /app/views/ ./views/ +COPY --from=builder /app/dataset-reader-v1.0.0.tgz ./dataset-reader-v1.0.0.tgz RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ yarn cache clean && yarn install --production diff --git a/wallet-enterprise-configurations/vid-issuer/src/configuration/CredentialIssuersConfigurationService.ts b/wallet-enterprise-configurations/vid-issuer/src/configuration/CredentialIssuersConfigurationService.ts index 1d1d2a8..aa91b8e 100644 --- a/wallet-enterprise-configurations/vid-issuer/src/configuration/CredentialIssuersConfigurationService.ts +++ b/wallet-enterprise-configurations/vid-issuer/src/configuration/CredentialIssuersConfigurationService.ts @@ -18,7 +18,7 @@ const issuerKeySet = KeyIdentifierKeySchema.parse(JSON.parse(issuerKeySetFile)); const issuerSigner: CredentialSigner = { - sign: async function (payload, headers, disclosureFrame) { + sign: async function (payload, headers, disclosureFrame, { exp }) { const key = await importJWK(issuerKeySet.keys['ES256']?.privateKeyJwk, 'ES256'); const signer: Signer = (input, header) => { @@ -45,20 +45,11 @@ const issuerSigner: CredentialSigner = { const kid = `${did}#${did.split(':')[2]}`; const issuanceDate = new Date(); - const expirationDate = (() => { - if (payload.vc.credentialSubject?.validityPeriod?.endingDate) { - const expirationDate = new Date(payload.vc.credentialSubject.validityPeriod.endingDate); - return expirationDate; - } - else { // if no expiration date found on credential subject, then set it to next year - const expirationDate = new Date(); - expirationDate.setFullYear(expirationDate.getFullYear() + 1); - return expirationDate; - } - })(); - payload.vc.expirationDate = expirationDate.toISOString(); - payload.exp = Math.floor(expirationDate.getTime() / 1000); + if (exp) { + payload.exp = exp; + payload.vc.expirationDate = new Date(exp * 1000).toISOString(); + } payload.vc.issuanceDate = issuanceDate.toISOString(); payload.iat = Math.floor(issuanceDate.getTime() / 1000); diff --git a/wallet-enterprise-configurations/vid-issuer/src/configuration/SupportedCredentialsConfiguration/VIDSupportedCredentialSdJwt.ts b/wallet-enterprise-configurations/vid-issuer/src/configuration/SupportedCredentialsConfiguration/VIDSupportedCredentialSdJwt.ts index 225fd35..51dfe4d 100644 --- a/wallet-enterprise-configurations/vid-issuer/src/configuration/SupportedCredentialsConfiguration/VIDSupportedCredentialSdJwt.ts +++ b/wallet-enterprise-configurations/vid-issuer/src/configuration/SupportedCredentialsConfiguration/VIDSupportedCredentialSdJwt.ts @@ -6,16 +6,13 @@ import { SupportedCredentialProtocol } from "../../lib/CredentialIssuerConfig/Su import { AuthorizationServerState } from "../../entities/AuthorizationServerState.entity"; import { CredentialView } from "../../authorization/types"; import { randomUUID } from "node:crypto"; -import fs from 'fs'; import { CredentialStatusList } from "../../lib/CredentialStatus"; +import { parsePidData } from 'dataset-reader'; export class VIDSupportedCredentialSdJwt implements SupportedCredentialProtocol { - dataset: any; - constructor(private credentialIssuerConfig: CredentialIssuer) { - this.dataset = JSON.parse(fs.readFileSync('/datasets/dataset.json', 'utf-8').toString()) as any; - } + constructor(private credentialIssuerConfig: CredentialIssuer) { } getCredentialIssuerConfig(): CredentialIssuer { return this.credentialIssuerConfig; @@ -43,16 +40,16 @@ export class VIDSupportedCredentialSdJwt implements SupportedCredentialProtocol return null; } - this.dataset = JSON.parse(fs.readFileSync('/datasets/dataset.json', 'utf-8').toString()) as any; - const vids = this.dataset.users.filter((user: any) => user.authentication.personalIdentifier == userSession.personalIdentifier); + const dataset = parsePidData("/datasets/dataset.xlsx"); + const vids = dataset.filter((user: any) => user.pid_id == userSession.personalIdentifier); const credentialViews: CredentialView[] = vids .map((vid: any) => { const rows: CategorizedRawCredentialViewRow[] = [ - { name: "Family Name", value: vid.claims.familyName }, - { name: "First Name", value: vid.claims.firstName }, - { name: "Personal Identifier", value: vid.claims.personalIdentifier }, - { name: "Date of Birth", value: vid.claims.birthdate }, - { name: "Expiration Date", value: vid.claims.validityPeriod.endingDate }, + { name: "Family Name", value: vid.family_name }, + { name: "Given Name", value: vid.given_name }, + { name: "Personal Identifier", value: vid.pid_id }, + { name: "Date of Birth", value: (vid.birth_date as Date).toDateString() }, + { name: "Expiration Date", value: new Date(vid.pid_expiry_date).toDateString() }, ]; const rowsObject: CategorizedRawCredentialView = { rows }; @@ -72,9 +69,8 @@ export class VIDSupportedCredentialSdJwt implements SupportedCredentialProtocol } - this.dataset = JSON.parse(fs.readFileSync('/datasets/dataset.json', 'utf-8').toString()) as any; - const { claims, authentication } = this.dataset.users.filter((user: any) => user.authentication.personalIdentifier == userSession.personalIdentifier)[0]; - console.log("Vid claims = ", claims) + const dataset = parsePidData("/datasets/dataset.xlsx"); + const data = dataset.filter((user: any) => user.pid_id == userSession.personalIdentifier)[0]; const payload = { "@context": ["https://www.w3.org/2018/credentials/v1"], "type": this.getTypes(), @@ -82,11 +78,14 @@ export class VIDSupportedCredentialSdJwt implements SupportedCredentialProtocol "name": "PID", // https://www.w3.org/TR/vc-data-model-2.0/#names-and-descriptions "description": "This credential is issued by the National PID credential issuer and it can be used for authentication purposes", "credentialSubject": { - ...claims, + family_name: data.family_name, + given_name: data.given_name, + birth_date: data.birth_date, + personal_identifier: String(data.pid_id), "id": holderDID, }, "credentialStatus": { - "id": `${config.crl.url}#${(await CredentialStatusList.insert(authentication.username, claims.personalIdentifier)).id}`, + "id": `${config.crl.url}#${(await CredentialStatusList.insert(data.User, data.pid_id)).id}`, "type": "CertificateRevocationList" }, "credentialBranding": { @@ -105,17 +104,17 @@ export class VIDSupportedCredentialSdJwt implements SupportedCredentialProtocol const disclosureFrame = { vc: { credentialSubject: { - familyName: true, - firstName: true, - birthdate: true, - personalIdentifier: true, + family_name: true, + given_name: true, + birth_date: true, + personal_identifier: true, } } } const { jws } = await this.getCredentialIssuerConfig().getCredentialSigner() .sign({ vc: payload - }, {}, disclosureFrame); + }, {}, disclosureFrame, { exp: Math.floor(new Date(data.pid_expiry_date).getTime() / 1000) }); const response = { format: this.getFormat(), credential: jws diff --git a/wallet-enterprise-configurations/vid-issuer/src/configuration/authentication/LocalAuthenticationComponent.ts b/wallet-enterprise-configurations/vid-issuer/src/configuration/authentication/LocalAuthenticationComponent.ts index 1836e19..526b207 100644 --- a/wallet-enterprise-configurations/vid-issuer/src/configuration/authentication/LocalAuthenticationComponent.ts +++ b/wallet-enterprise-configurations/vid-issuer/src/configuration/authentication/LocalAuthenticationComponent.ts @@ -6,19 +6,23 @@ import AppDataSource from "../../AppDataSource"; import { AuthorizationServerState } from "../../entities/AuthorizationServerState.entity"; import locale from "../locale"; import { UserAuthenticationMethod } from "../../types/UserAuthenticationMethod.enum"; -import fs from 'fs'; +import { parsePidData } from 'dataset-reader'; export class LocalAuthenticationComponent extends AuthenticationComponent { constructor( override identifier: string, override protectedEndpoint: string, - private users = [] + private users: any[] = [] ) { - super(identifier, protectedEndpoint) - const dataset = JSON.parse(fs.readFileSync('/datasets/dataset.json', 'utf-8').toString()) as any; - console.dir(dataset, { depth: null }) - this.users = dataset.users; + super(identifier, protectedEndpoint); + const data = parsePidData("/datasets/dataset.xlsx"); + console.log("Raw pid_expiry_date = ", data[0].pid_expiry_date) + console.log("Converted to unix timestamp = ", Math.floor(new Date(data[0].pid_expiry_date).getTime() / 1000) ) + console.log("Birthdate = ", data[0].birth_date) + console.log("Columns of first row"); + console.log(Object.keys(data[0])); + this.users = data as any[]; } public override async authenticate( @@ -55,10 +59,10 @@ export class LocalAuthenticationComponent extends AuthenticationComponent { return false; } const username = req.session.authenticationChain.localAuthenticationComponent.username; - if (!username || this.users.filter((u: any) => u.authentication.username == username).length != 1) return false; + if (!username || this.users.filter((u: any) => u.User == username).length != 1) return false; - const usersFound = this.users.filter((u: any) => u.authentication.username == username) as any; - req.authorizationServerState.personalIdentifier = usersFound[0].authentication.personalIdentifier; + const usersFound = this.users.filter((u: any) => u.User == username) as any; + req.authorizationServerState.personalIdentifier = usersFound[0].pid_id; await AppDataSource.getRepository(AuthorizationServerState).save(req.authorizationServerState); return true; } @@ -82,7 +86,7 @@ export class LocalAuthenticationComponent extends AuthenticationComponent { private async handleLoginSubmission(req: Request, res: Response): Promise { const { username, password } = req.body; - const usersFound = this.users.filter((u: any) => u.authentication.username == username && u.authentication.password == password); + const usersFound = this.users.filter((u: any) => u.User == username && u.Password == password); if (usersFound.length == 1) { // sign a token and send it to the client @@ -90,7 +94,7 @@ export class LocalAuthenticationComponent extends AuthenticationComponent { username: username }; - req.authorizationServerState.personalIdentifier = (usersFound[0] as any).authentication.personalIdentifier; + req.authorizationServerState.personalIdentifier = (usersFound[0] as any).pid_id; await AppDataSource.getRepository(AuthorizationServerState).save(req.authorizationServerState); return res.redirect(this.protectedEndpoint); }