From 766254fa005c99cdc9493b651d3db7939c3f9193 Mon Sep 17 00:00:00 2001 From: emmanouil koukoularis Date: Fri, 6 Sep 2024 17:47:22 +0300 Subject: [PATCH] implemented pick up code for ehic issuer --- .../ehic-issuer/config/config.development.ts | 3 + .../ehic-issuer/keys/issuer.private.ecdh.json | 1 + .../ehic-issuer/keys/issuer.public.ecdh.json | 1 + .../ehic-issuer/keys/vault.public.ecdh.json | 1 + .../CredentialIssuersConfigurationService.ts | 6 +- .../EHICSupportedCredentialSdJwt.ts | 2 +- .../PickupCodeEHICSupportedCredentialSdJwt.ts | 240 ++++++++++++++++++ .../configuration/consent/consent.config.ts | 2 +- .../PDA1SupportedCredentialSdJwt.ts | 2 +- 9 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 wallet-enterprise-configurations/ehic-issuer/keys/issuer.private.ecdh.json create mode 100644 wallet-enterprise-configurations/ehic-issuer/keys/issuer.public.ecdh.json create mode 100644 wallet-enterprise-configurations/ehic-issuer/keys/vault.public.ecdh.json create mode 100644 wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/PickupCodeEHICSupportedCredentialSdJwt.ts 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..519f717 100644 --- a/wallet-enterprise-configurations/ehic-issuer/src/configuration/CredentialIssuersConfigurationService.ts +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/CredentialIssuersConfigurationService.ts @@ -11,7 +11,7 @@ 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)); @@ -101,8 +101,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..071c017 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 { 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..438085c --- /dev/null +++ b/wallet-enterprise-configurations/ehic-issuer/src/configuration/SupportedCredentialsConfiguration/PickupCodeEHICSupportedCredentialSdJwt.ts @@ -0,0 +1,240 @@ +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' }); + + + +const dataset = JSON.parse(fs.readFileSync('/datasets/dataset.json', 'utf-8').toString()) as any; + + +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 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(), + "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": { + ...claims, + "id": holderDID, + }, + "credentialStatus": { + "id": `${config.crl.url}#${(await CredentialStatusList.insert(username, claims.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, + personalIdentifier: true, + socialSecurityIdentification: { + ssn: true + }, + validityPeriod: { + startingDate: true, + endingDate: true + }, + documentId: true, + competentInstitution: { + competentInstitutionId: true, + competentInstitutionName: true, + competentInstitutionCountryCode: true + }, + } + } + } + const { jws } = await this.getCredentialIssuerConfig().getCredentialSigner() + .sign({ + vc: payload + }, {}, disclosureFrame); + 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/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/pda1-issuer/src/configuration/SupportedCredentialsConfiguration/PDA1SupportedCredentialSdJwt.ts b/wallet-enterprise-configurations/pda1-issuer/src/configuration/SupportedCredentialsConfiguration/PDA1SupportedCredentialSdJwt.ts index 8712131..92e0eba 100644 --- a/wallet-enterprise-configurations/pda1-issuer/src/configuration/SupportedCredentialsConfiguration/PDA1SupportedCredentialSdJwt.ts +++ b/wallet-enterprise-configurations/pda1-issuer/src/configuration/SupportedCredentialsConfiguration/PDA1SupportedCredentialSdJwt.ts @@ -126,7 +126,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,