Skip to content

Commit

Permalink
implemented pick up code for ehic issuer
Browse files Browse the repository at this point in the history
  • Loading branch information
kkmanos committed Sep 6, 2024
1 parent 021d713 commit 766254f
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"kty":"EC","x":"5Q_PzfqQf6MRGFjMZ1owZGtuzHCozN_KKC2RNgzacLs","y":"Coaxh8MYMw1WeQODdAmI5C0ZXwi7h0qB1914G341Kww","crv":"P-256","d":"TYSFecf3b2uK1ubH12ROTkNPgpbnOazJLIxmZEEW1mU"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"kty":"EC","x":"5Q_PzfqQf6MRGFjMZ1owZGtuzHCozN_KKC2RNgzacLs","y":"Coaxh8MYMw1WeQODdAmI5C0ZXwi7h0qB1914G341Kww","crv":"P-256"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"kty":"EC","x":"PiXfYPrIGa3SE6bINX9X20EoKCtO_r2dDKhOzoDGnuU","y":"9gxiOT-vNDLe0HLNJA6HpH5vlCuCknnBmXUZLB05Vgk","crv":"P-256"}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CredentialView | null> {
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"]
}
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 766254f

Please sign in to comment.