diff --git a/bruno/collections/Rafiki/HSM Emulator/1- Customer ASE - Generate ZMK.bru b/bruno/collections/Rafiki/HSM Emulator/1- Customer ASE - Generate ZMK.bru new file mode 100644 index 0000000000..4050c6d6fb --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/1- Customer ASE - Generate ZMK.bru @@ -0,0 +1,36 @@ +meta { + name: 1: Customer ASE - Generate ZMK + type: http + seq: 1 +} + +post { + url: {{hsmEmulatorHost}}/hsm/ase-customer/generate-zmk + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + {} +} + +script:post-response { + const body = res.getBody(); + + if (body?.component1) bru.setEnvVar("zmk_component1", body.component1); + if (body?.component2) bru.setEnvVar("zmk_component2", body.component2); + if (body?.component3) bru.setEnvVar("zmk_component3", body.component3); + if (body?.kcv) bru.setEnvVar("zmk_kcv", body.kcv); + if (body?.tr31Block) bru.setEnvVar("zmk_tr31_lmk_ilf", body.tr31Block); + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/1- Merchant ASE - Generate ZMK.bru b/bruno/collections/Rafiki/HSM Emulator/1- Merchant ASE - Generate ZMK.bru new file mode 100644 index 0000000000..5b83fb786c --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/1- Merchant ASE - Generate ZMK.bru @@ -0,0 +1,36 @@ +meta { + name: 1: Merchant ASE - Generate ZMK + type: http + seq: 5 +} + +post { + url: {{hsmEmulatorHost}}/hsm/ase-merchant/generate-zmk + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + {} +} + +script:post-response { + const body = res.getBody(); + + if (body?.component1) bru.setEnvVar("zmk_component1", body.component1); + if (body?.component2) bru.setEnvVar("zmk_component2", body.component2); + if (body?.component3) bru.setEnvVar("zmk_component3", body.component3); + if (body?.kcv) bru.setEnvVar("zmk_kcv", body.kcv); + if (body?.tr31Block) bru.setEnvVar("zmk_tr31_lmk_ilf", body.tr31Block); + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/2- Austria Card - Import ZMK.bru b/bruno/collections/Rafiki/HSM Emulator/2- Austria Card - Import ZMK.bru new file mode 100644 index 0000000000..6a9b0c76ce --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/2- Austria Card - Import ZMK.bru @@ -0,0 +1,37 @@ +meta { + name: 2: Austria Card - Import ZMK + type: http + seq: 2 +} + +post { + url: {{hsmEmulatorHost}}/hsm/austria-card/import-zmk + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "component1": "{{zmk_component1}}", + "component2": "{{zmk_component2}}", + "component3": "{{zmk_component3}}", + "kcv": "{{zmk_kcv}}" + } +} + +script:post-response { + const body = res.getBody(); + + if (body?.tr31Block) bru.setEnvVar("zmk_tr31_lmk_austria_card", body.tr31Block); + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/2- KaiOS - Import ZMK.bru b/bruno/collections/Rafiki/HSM Emulator/2- KaiOS - Import ZMK.bru new file mode 100644 index 0000000000..3904411867 --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/2- KaiOS - Import ZMK.bru @@ -0,0 +1,37 @@ +meta { + name: 2: KaiOS - Import ZMK + type: http + seq: 6 +} + +post { + url: {{hsmEmulatorHost}}/hsm/kai-os/import-zmk + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "component1": "{{zmk_component1}}", + "component2": "{{zmk_component2}}", + "component3": "{{zmk_component3}}", + "kcv": "{{zmk_kcv}}" + } +} + +script:post-response { + const body = res.getBody(); + + if (body?.tr31Block) bru.setEnvVar("zmk_tr31_lmk_kai", body.tr31Block); + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/3- Customer ASE - Generate Card Keypair.bru b/bruno/collections/Rafiki/HSM Emulator/3- Customer ASE - Generate Card Keypair.bru new file mode 100644 index 0000000000..18d495afd7 --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/3- Customer ASE - Generate Card Keypair.bru @@ -0,0 +1,38 @@ +meta { + name: 3: Customer ASE - Generate Card Keypair + type: http + seq: 3 +} + +post { + url: {{hsmEmulatorHost}}/hsm/ase-customer/generate-card-key + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "tr31ZmkUnderLmk": "{{zmk_tr31_lmk_ilf}}" + } +} + +script:post-response { + const body = res.getBody(); + + if (body?.tr31CardKeyUnderLmk) bru.setEnvVar("cardkey_tr31_lmk_ilf", body.tr31CardKeyUnderLmk); + if (body?.tr31CardKeyUnderZmk) bru.setEnvVar("cardkey_tr31_zmk", body.tr31CardKeyUnderZmk); + if (body?.publicKey) bru.setEnvVar("cardkey_public", body.publicKey); + if (body?.kcv) bru.setEnvVar("cardkey_kcv", body.kcv); + + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/3- KaiOS - Generate TMK.bru b/bruno/collections/Rafiki/HSM Emulator/3- KaiOS - Generate TMK.bru new file mode 100644 index 0000000000..38177debb2 --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/3- KaiOS - Generate TMK.bru @@ -0,0 +1,38 @@ +meta { + name: 3: KaiOS - Generate TMK + type: http + seq: 7 +} + +post { + url: {{hsmEmulatorHost}}/hsm/kai-os/generate-tmk + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "terminalSerial": "KAISN0000111", + "zmkUnderLmk": "{{zmk_tr31_lmk_kai}}" + } +} + +script:post-response { + const body = res.getBody(); + + if (body?.tr31TmkUnderLmk) bru.setEnvVar("tmk_tr31_lmk_kai", body.tr31TmkUnderLmk); + if (body?.tr31TmkUnderZmk) bru.setEnvVar("tmk_tr31_zmk", body.tr31TmkUnderZmk); + if (body?.terminalSerial) bru.setEnvVar("tmk_serial", body.terminalSerial); + if (body?.kcv) bru.setEnvVar("tmk_kcv", body.kcv); + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/4- Austria Card - Import Card PrivateKey.bru b/bruno/collections/Rafiki/HSM Emulator/4- Austria Card - Import Card PrivateKey.bru new file mode 100644 index 0000000000..c9f12b4d6b --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/4- Austria Card - Import Card PrivateKey.bru @@ -0,0 +1,36 @@ +meta { + name: 4: Austria Card - Import Card PrivateKey + type: http + seq: 4 +} + +post { + url: {{hsmEmulatorHost}}/hsm/austria-card/import-card-key + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "tr31ZmkUnderLmk": "{{zmk_tr31_lmk_austria_card}}", + "tr31CardKeyUnderZmk": "{{cardkey_tr31_zmk}}", + "kcv": "{{cardkey_kcv}}" + } +} + +script:post-response { + const body = res.getBody(); + + if (body?.tr31CardKeyUnderLmk) bru.setEnvVar("cardkey_tr31_lmk_austria_card", body.tr31CardKeyUnderLmk); + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/4- Merchant ASE - Import TMK.bru b/bruno/collections/Rafiki/HSM Emulator/4- Merchant ASE - Import TMK.bru new file mode 100644 index 0000000000..0da63c5703 --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/4- Merchant ASE - Import TMK.bru @@ -0,0 +1,37 @@ +meta { + name: 4: Merchant ASE - Import TMK + type: http + seq: 8 +} + +post { + url: {{hsmEmulatorHost}}/hsm/ase-merchant/import-tmk + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "tr31TmkUnderZmk": "{{tmk_tr31_zmk}}", + "tr31ZmkUnderLmk": "{{zmk_tr31_lmk_ilf}}", + "kcv": "{{tmk_kcv}}" + } +} + +script:post-response { + const body = res.getBody(); + + if (body?.tr31TmkUnderLmk) bru.setEnvVar("tmk_tr31_lmk_ilf", body.tr31TmkUnderLmk); + + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/5- Merchant ASE - Generate BDK.bru b/bruno/collections/Rafiki/HSM Emulator/5- Merchant ASE - Generate BDK.bru new file mode 100644 index 0000000000..083253ea5c --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/5- Merchant ASE - Generate BDK.bru @@ -0,0 +1,36 @@ +meta { + name: 5: Merchant ASE - Generate BDK + type: http + seq: 9 +} + +post { + url: {{hsmEmulatorHost}}/hsm/ase-merchant/generate-bdk + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "zmkUnderLmk": "{{zmk_tr31_lmk_ilf}}" + } +} + +script:post-response { + const body = res.getBody(); + + if (body?.tr31BdkUnderLmk) bru.setEnvVar("bdk_tr31_lmk_ilf", body.tr31BdkUnderLmk); + if (body?.tr31BdkUnderZmk) bru.setEnvVar("bdk_tr31_zmk", body.tr31BdkUnderZmk); + + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/HSM Emulator/6- Merchant ASE - Derive IPEK.bru b/bruno/collections/Rafiki/HSM Emulator/6- Merchant ASE - Derive IPEK.bru new file mode 100644 index 0000000000..9bb3534e69 --- /dev/null +++ b/bruno/collections/Rafiki/HSM Emulator/6- Merchant ASE - Derive IPEK.bru @@ -0,0 +1,39 @@ +meta { + name: 6: Merchant ASE - Derive IPEK + type: http + seq: 10 +} + +post { + url: {{hsmEmulatorHost}}/hsm/ase-merchant/derive-ipek + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "tr31BdkUnderLmk": "{{bdk_tr31_lmk_ilf}}", + "tr31TmkUnderLmk": "{{tmk_tr31_lmk_ilf}}", + "ksnHex": "ffff9876543210e00000" + } +} + +script:post-response { + const body = res.getBody(); + + if (body?.tr31IpekUnderLmk) bru.setEnvVar("ipek_tr31_lmk_ilf", body.tr31IpekUnderLmk); + if (body?.tr31IpekUnderTmk) bru.setEnvVar("ipek_tr31_tmk", body.tr31IpekUnderTmk); + if (body?.kcv) bru.setEnvVar("ipek_kcv", body.kcv); + + +} + +tests { + test("Status code is 200", function() { + expect(res.getStatus()).to.equal(200); + }); +} diff --git a/bruno/collections/Rafiki/environments/Local Playground.bru b/bruno/collections/Rafiki/environments/Local Playground.bru index b8bb2d3edc..f90bbea549 100644 --- a/bruno/collections/Rafiki/environments/Local Playground.bru +++ b/bruno/collections/Rafiki/environments/Local Playground.bru @@ -30,6 +30,7 @@ vars { assetIdTigerBeetle: 1 assetCode: USD assetScale: 2 + hsmEmulatorHost: http://localhost:5002 senderTenantId: 438fa74a-fa7d-4317-9ced-dde32ece1787 RafikiGraphqlHostTenantId: 438fa74a-fa7d-4317-9ced-dde32ece1787 senderOpenPaymentsPort: 3000 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e6cd4fa82..2c462e12c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -749,6 +749,28 @@ importers: specifier: ^6.7.6 version: 6.7.6 + test/hsm-emulator: + dependencies: + fastify: + specifier: ^5.2.1 + version: 5.4.0 + pino: + specifier: ^9.6.0 + version: 9.7.0 + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.14.15 + '@types/pg': + specifier: ^8.11.11 + version: 8.15.4 + tsx: + specifier: ^4.19.3 + version: 4.20.3 + typescript: + specifier: ^5.0.0 + version: 5.8.3 + test/integration: devDependencies: '@interledger/open-payments': @@ -3976,11 +3998,46 @@ packages: resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + /@fastify/ajv-compiler@4.0.2: + resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.3 + dev: false + /@fastify/busboy@2.1.1: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} dev: true + /@fastify/error@4.2.0: + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + dev: false + + /@fastify/fast-json-stringify-compiler@5.0.3: + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + dependencies: + fast-json-stringify: 6.0.1 + dev: false + + /@fastify/forwarded@3.0.0: + resolution: {integrity: sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==} + dev: false + + /@fastify/merge-json-schemas@0.2.1: + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + dependencies: + dequal: 2.0.3 + dev: false + + /@fastify/proxy-addr@5.0.0: + resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} + dependencies: + '@fastify/forwarded': 3.0.0 + ipaddr.js: 2.2.0 + dev: false + /@graphql-codegen/add@5.0.3(graphql@16.11.0): resolution: {integrity: sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA==} peerDependencies: @@ -8236,9 +8293,16 @@ packages: /@types/pg-pool@2.0.4: resolution: {integrity: sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==} dependencies: - '@types/pg': 8.6.1 + '@types/pg': 8.15.4 dev: false + /@types/pg@8.15.4: + resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==} + dependencies: + '@types/node': 20.14.15 + pg-protocol: 1.6.0 + pg-types: 2.2.0 + /@types/pg@8.6.1: resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} dependencies: @@ -8585,7 +8649,7 @@ packages: debug: 4.4.0(supports-color@9.4.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.3 + semver: 7.7.2 tsutils: 3.21.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -8606,7 +8670,7 @@ packages: debug: 4.4.0(supports-color@9.4.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.3 + semver: 7.7.2 tsutils: 3.21.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -8649,7 +8713,7 @@ packages: '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.8.3) eslint: 8.57.1 eslint-scope: 5.1.1 - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color - typescript @@ -8669,7 +8733,7 @@ packages: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3) eslint: 8.57.1 eslint-scope: 5.1.1 - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color - typescript @@ -9050,6 +9114,10 @@ packages: dependencies: event-target-shim: 5.0.1 + /abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -9138,6 +9206,17 @@ packages: dependencies: ajv: 8.17.1 + /ajv-formats@3.0.1(ajv@8.17.1): + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.17.1 + dev: false + /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -9765,6 +9844,13 @@ packages: dependencies: possible-typed-array-names: 1.0.0 + /avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + dev: false + /axe-core@4.10.2: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} @@ -12752,7 +12838,6 @@ packages: /fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - dev: true /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -12787,6 +12872,17 @@ packages: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true + /fast-json-stringify@6.0.1: + resolution: {integrity: sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==} + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.3 + json-schema-ref-resolver: 2.0.1 + rfdc: 1.4.1 + dev: false + /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: true @@ -12795,7 +12891,6 @@ packages: resolution: {integrity: sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==} dependencies: fast-decode-uri-component: 1.0.1 - dev: true /fast-redact@3.1.2: resolution: {integrity: sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==} @@ -12818,11 +12913,37 @@ packages: engines: {node: '>= 4.9.1'} dev: true + /fastify@5.4.0: + resolution: {integrity: sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw==} + dependencies: + '@fastify/ajv-compiler': 4.0.2 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.0.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.0.1 + find-my-way: 9.3.0 + light-my-request: 6.6.0 + pino: 9.7.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.0.0 + semver: 7.7.2 + toad-cache: 3.7.0 + dev: false + /fastq@1.13.0: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} dependencies: reusify: 1.0.4 + /fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + dependencies: + reusify: 1.0.4 + dev: false + /fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} dependencies: @@ -12913,6 +13034,15 @@ packages: pkg-dir: 7.0.0 dev: true + /find-my-way@9.3.0: + resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==} + engines: {node: '>=20'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.1 + safe-regex2: 5.0.0 + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -13223,6 +13353,12 @@ packages: get-intrinsic: 1.2.4 dev: true + /get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /get-tsconfig@4.5.0: resolution: {integrity: sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==} dev: true @@ -14154,6 +14290,11 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + /ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + dev: false + /iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} dev: false @@ -14666,7 +14807,7 @@ packages: '@babel/parser': 7.26.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.0 - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color dev: true @@ -15103,7 +15244,7 @@ packages: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.3 + semver: 7.7.2 transitivePeerDependencies: - supports-color dev: true @@ -15243,6 +15384,12 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /json-schema-ref-resolver@2.0.1: + resolution: {integrity: sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==} + dependencies: + dequal: 2.0.3 + dev: false + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true @@ -15533,6 +15680,14 @@ packages: type-check: 0.4.0 dev: true + /light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + dependencies: + cookie: 1.0.2 + process-warning: 4.0.1 + set-cookie-parser: 2.7.1 + dev: false + /lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -17312,7 +17467,7 @@ packages: resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - semver: 7.6.3 + semver: 7.7.2 dev: true /npm-normalize-package-bin@3.0.1: @@ -17326,7 +17481,7 @@ packages: dependencies: hosted-git-info: 6.1.1 proc-log: 3.0.0 - semver: 7.6.3 + semver: 7.7.2 validate-npm-package-name: 5.0.0 dev: true @@ -17337,7 +17492,7 @@ packages: npm-install-checks: 6.3.0 npm-normalize-package-bin: 3.0.1 npm-package-arg: 10.1.0 - semver: 7.6.3 + semver: 7.7.2 dev: true /npm-run-all2@6.2.6: @@ -17958,7 +18113,6 @@ packages: /pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - dev: false /pg-pool@3.6.1(pg@8.11.3): resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} @@ -17970,7 +18124,6 @@ packages: /pg-protocol@1.6.0: resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} - dev: false /pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -17981,7 +18134,6 @@ packages: postgres-bytea: 1.0.0 postgres-date: 1.0.7 postgres-interval: 1.2.0 - dev: false /pg@8.11.3: resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} @@ -18038,6 +18190,12 @@ packages: readable-stream: 4.1.0 split2: 4.1.0 + /pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + dependencies: + split2: 4.1.0 + dev: false + /pino-pretty@11.0.0: resolution: {integrity: sha512-YFJZqw59mHIY72wBnBs7XhLGG6qpJMa4pEQTRgEPEbjIYbng2LXEZZF1DoyDg9CfejEy8uZCyzpcBXXG0oOCwQ==} hasBin: true @@ -18060,6 +18218,10 @@ packages: /pino-std-serializers@6.0.0: resolution: {integrity: sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==} + /pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + dev: false + /pino@8.19.0: resolution: {integrity: sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==} hasBin: true @@ -18076,6 +18238,23 @@ packages: sonic-boom: 3.7.0 thread-stream: 2.1.0 + /pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.1.2 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.3.1 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + dev: false + /pirates@4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} @@ -18287,24 +18466,20 @@ packages: /postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} - dev: false /postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} - dev: false /postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} - dev: false /postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} dependencies: xtend: 4.0.2 - dev: false /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -18375,6 +18550,14 @@ packages: /process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + /process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + dev: false + + /process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + dev: false + /promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -19156,6 +19339,10 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve.exports@2.0.2: resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} engines: {node: '>=10'} @@ -19202,6 +19389,11 @@ packages: resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} dev: false + /ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + dev: false + /retext-latin@4.0.0: resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} dependencies: @@ -19253,6 +19445,10 @@ packages: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} dev: true + /rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + dev: false + /rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} hasBin: true @@ -19420,6 +19616,12 @@ packages: is-regex: 1.2.1 dev: true + /safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + dependencies: + ret: 0.5.0 + dev: false + /safe-stable-stringify@2.3.1: resolution: {integrity: sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==} engines: {node: '>=10'} @@ -19466,6 +19668,10 @@ packages: /secure-json-parse@2.5.0: resolution: {integrity: sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==} + /secure-json-parse@4.0.0: + resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} + dev: false + /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -19852,6 +20058,12 @@ packages: dependencies: atomic-sleep: 1.0.0 + /sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + dependencies: + atomic-sleep: 1.0.0 + dev: false + /source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -20560,6 +20772,12 @@ packages: dependencies: real-require: 0.2.0 + /thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + dependencies: + real-require: 0.2.0 + dev: false + /through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: @@ -20643,6 +20861,11 @@ packages: dependencies: is-number: 7.0.0 + /toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + dev: false + /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -20833,6 +21056,17 @@ packages: typescript: 5.8.3 dev: true + /tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.25.2 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /turbo-stream@2.4.0: resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} diff --git a/test/hsm-emulator/README.md b/test/hsm-emulator/README.md new file mode 100644 index 0000000000..b80f312682 --- /dev/null +++ b/test/hsm-emulator/README.md @@ -0,0 +1,164 @@ +# HSM Emulator + +The `hsm-emulator` is a lightweight emulator that simulates the behavior of a Hardware Security Module (HSM) for +development and testing purposes. + +Please see: https://docs.google.com/document/d/12GGzU-HC9xAccj9t7EzLkrPEaSGZWCtMhGpjcwWBmG8/edit?tab=t.0 + +## Overview + +The HSM Emulator provides cryptographic operations such as key management between card and processor parties, enabling +developers to validate HSM-integrated workflows without requiring access to a physical HSM. + +## Parties + +The parties involved in the HSM emulator include: + +- **Customer ASE** - The Account Servicing Entity (ASE) who manages the card for the customer (issuer) +- **Merchant ASE** - The ASE who manages the POS device (acquirer) +- **KaiOS** - The POS manufacturer +- **Austria Card** - The card personalization / printer + +## Build HSM Emulator: + +The project is built as part of the Rafiki project. + +## Start HSM Emulator + +```shell +# Run (port 5002 default): +pnpm dev +``` + +## Key Management between Parties + +The steps and digrams below illustrate the cryptographic keys and their relationship with one another and parties. + +### 1.1. ZMK - Zone Master Key + +The ZMK may be generated by either of two or more parties. One party is responsible for a ZMK key generation, while the +other party is responsible for importing the ZMK. It is strongly advised that the ZMK be generated/imported using an HSM. +The KCV is used to verify integrity during the exchange. + +### 1.1.1 ZMK - Customer ASE (Issuer) + +The ZMK generated between Customer ASE and Austria Card. + +```mermaid +--- +title: ZMK Exchange - Card +--- +erDiagram + "Customer ASE 🏦" ||--}| "ZMK πŸ”‘" : "generates key" + "Austria Card πŸ’³" ||--|| "ZMK πŸ”‘" : "imports key (3x clear components)" +``` + +### 1.1.2 ZMK - Merchant ASE (Acquirer) + +The ZMK generated between Merchant ASE and KaiOS. + +```mermaid +--- +title: ZMK Exchange - POS +--- +erDiagram + "Merchant ASE 🏦" ||--}| "ZMK πŸ”‘" : "generates key" + "KaiOS πŸ“±" ||--|| "ZMK πŸ”‘" : "imports key (3x clear components)" +``` + +### 1.2. TMK - Terminal Master Key + +The Terminal Master Key (TMK) is generated by the terminal POS manufacturer and is unique to each terminal. +The TMK is securely transferred to the Merchant ASE encrypted under the Zone Master Key (ZMK) using the TR-31 key block format. +The TMK facilitates the secure delivery of session keys between the ASE and the terminal, +ensuring encrypted communication and key management. + +```mermaid +--- +title: TMK Exchange +--- +erDiagram + "KaiOS πŸ“±" ||--}| "TMK πŸ”‘" : generates + "Merchant ASE 🏦" ||--}| "TMK πŸ”‘" : "imports (under ZMK)" +``` + +### 1.3. PIN/SRED BDK and IPEK - Base Derivation Key and Initial PIN Encryption Keys + +The Base Derivation Keys (BDKs), generated by the Merchant’s ASE, are typically fixed per terminal manufacturer and model. +From each BDK, Initial PIN Encryption Keys (IPEKs) are derived and securely loaded onto the terminal. +These IPEKs are encrypted under the Terminal Master Key (TMK) using the TR-31 key block format, ensuring secure key delivery. +The distribution can occur either through direct injection or remotely via an over-the-air Remote Key Injection (RKI) process. + +The PIN and SRED IPEK's will not share a BDK (See PCI-PIN Security Requirement ref #18-3). + +```mermaid +--- +title: PIN/SRED Key Exchange and IPEK +--- +erDiagram + "Merchant ASE 🏦" ||--|| "SRED / PIN BDK πŸ”‘" : "generates" + "SRED / PIN BDK πŸ”‘" ||--}| "IPEK πŸ”‘" : "generates derived IPEK (based on BDK)" + "Terminal πŸ“±" ||--|| "IPEK πŸ”‘" : "imports (under TMK)" +``` + +### 2.1 Card Key - Card Asymmetric Keys + +The Card Key Pair is generated by the Account Serving Entity (ASE), which also acts as the issuer. +A unique key pair is created for each card. +The public key from this pair is securely linked to a corresponding wallet address. + +```mermaid +--- +title: Card Key Generation and Issuing +--- +erDiagram + "Customer ASE 🏦" ||--}| "Card KeyPair πŸ”" : "generates" + "Card KeyPair πŸ”" ||--}| "Private Key πŸ”‘" : "has" + "Card KeyPair πŸ”" ||--}| "Public Key πŸ”“" : "has" + "Public Key πŸ”“" |{--}| "Wallet Address πŸ“‡" : "store against wallet address πŸ’½" + + "Austria Card πŸ’³" ||--}| "Private Key πŸ”‘" : "imports (under ZMK)" + + "Private Key πŸ”‘" ||--|| "Card πŸ’³" : "loaded onto (securely during issuing)" +``` + +### 2.2 POS Key - POS Asymmetric Keys + +The POS Key Pair is generated by the merchant Account Serving Entity (ASE), which acts as the card acceptor / merchant. +A unique key pair is created for each POS terminal. The generation would take place when the POS terminal is linked to the merchant ASE. +The public key from this pair is securely linked to a corresponding merchant wallet address. + +```mermaid +--- +title: POS Key Generation and Registration +--- +erDiagram + "Merchant ASE 🏦" ||--}| "POS KeyPair πŸ”" : "generates" + "POS KeyPair πŸ”" ||--}| "Private Key πŸ”‘" : "has" + "POS KeyPair πŸ”" ||--}| "Public Key πŸ”“" : "has" + "Public Key πŸ”“" |{--}| "Merchant Wallet Address DB πŸ“‡" : "store against merchant wallet address πŸ’½" + + "Terminal πŸ“±" ||--}| "Private Key πŸ”‘" : "imports (under TMK)" + + "Private Key πŸ”‘" ||--|| "Terminal πŸ“±" : "loaded onto (securely during registration)" +``` + +## Glossary of Terms + +Terms of definition related to ASE, Card issuer and terminal manufacturers. + +| Term | Description | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| HSM | Hardware Security Module. Physical device that provides secure key storage and cryptographic processing. It is designed to protect sensitive data and perform secure operations. | +| LMK | Local Master Key. Top-level encryption key used to secure and manage other keys within the HSM. It plays a central role in the HSM’s key hierarchy. | +| ZMK | Zone Master Key. Cryptographic key used to securely exchange other encryption keys between two systems or organizations β€” typically between two HSMs (Hardware Security Modules) that are part of different cryptographic zones. | +| TMK | Terminal Master Key. Used to secure the transmission of working keys (like PIN, SRED encryption keys or MAC keys) between a terminal and the HSM. | +| BDK | Base Derivation Key, is the root key from which a unique IPEK (Initial PIN Encryption Key) is derived for each device or terminal. The IPEK, in turn, is used to derive session keys for each transaction, ensuring that no two transactions share the same encryption key. | +| IPEK | Initial PIN Encryption Key, is used to derive session keys for each transaction, ensuring that no two transactions share the same encryption key. | +| KSN | Key Serial Number. A unique identifier for each device, used in key derivation. | +| Session Key | Derived from the IPEK for encrypting a single transaction. | +| DUKPT | DUKPT stands for Derived Unique Key Per Transaction. It’s a key management scheme used primarily in payment systems (e.g. POS terminals) to ensure each transaction is encrypted with a unique key, dramatically reducing the risk of compromise. | +| KCV | Key Check Value. Short cryptographic value derived from a key, used to verify that the key has been correctly transferred or entered without revealing the key itself. | +| POS | Point of Service device (also referred to as the terminal). | +| TR-31 | A TR-31 key block is a standardized format used to securely exchange cryptographic keys between systems, especially in financial environments involving HSMs (Hardware Security Modules). It was defined by the ANSI X9.24-1 standard. | +| PGP | Pretty Good Privacy (PGP) is an encryption program that provides cryptographic privacy and authentication for data communication, primarily used for securing emails and files. It combines public-key and symmetric-key cryptography to ensure that messages remain confidential and can be verified for authenticity. | diff --git a/test/hsm-emulator/package.json b/test/hsm-emulator/package.json new file mode 100644 index 0000000000..ee7f7073bb --- /dev/null +++ b/test/hsm-emulator/package.json @@ -0,0 +1,25 @@ +{ + "name": "hsm-emulator", + "description": "A lightweight emulator that simulates the behavior of a Hardware Security Module (HSM) for development and testing purposes. It provides cryptographic operations such as key management, digital signing, and verification over HTTP, enabling developers to validate HSM-integrated workflows without requiring access to a physical HSM.", + "private": true, + "version": "1.0.0", + "scripts": { + "dev": "tsx src/index.ts", + "test": "node --import tsx --test test/*.test.ts", + "build": "pnpm clean && tsc --build tsconfig.json", + "clean": "rm -rf dist" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "fastify": "^5.2.1", + "pino": "^9.6.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/pg": "^8.11.11", + "typescript": "^5.0.0", + "tsx": "^4.19.3" + } +} diff --git a/test/hsm-emulator/src/app.ts b/test/hsm-emulator/src/app.ts new file mode 100644 index 0000000000..0f90bdc356 --- /dev/null +++ b/test/hsm-emulator/src/app.ts @@ -0,0 +1,272 @@ +import fastify from 'fastify' +import logger from './logger' +import { + AES_AUSTRIA_CARD_LMK_HEX, + AES_CUSTOMER_ASE_LMK_HEX, + AES_MERCHANT_ASE_LMK_HEX, + AES_KAI_LMK_HEX, + createTR31KeyBlockUnder, + importTMK, + generate3DESKeyFromComponents, + generateTMK, + generateBDK, + deriveIPEK, + import3DESKeyFromComponents, + KeyUsage, + Tr31Intent, + generateCardKey, + importCardKey +} from './card-hsm' + +export function createApp(port: number) { + const app = fastify() + + app.post( + '/hsm/ase-customer/generate-zmk', + async function handler(ffReq, ffReply) { + const genCleanZmkKey = generate3DESKeyFromComponents() + const { iv, tr31Block } = createTR31KeyBlockUnder( + Tr31Intent.LMK, + AES_CUSTOMER_ASE_LMK_HEX, + KeyUsage.ZMK, + 'T', + genCleanZmkKey.finalKeyBuffer + ) + + logger.info( + `Generated ZMK (Issuer): ${tr31Block.toString('ascii')} with KCV: ${genCleanZmkKey.kcv}` + ) + + ffReply.code(200).send({ + iv: iv.toString('hex'), + tr31Block: tr31Block.toString('ascii'), // Only to be used with ILF HSM + component1: genCleanZmkKey.component1, // DANGER! Sent to custodian 1. + component2: genCleanZmkKey.component2, // DANGER! Sent to custodian 2. + component3: genCleanZmkKey.component3, // DANGER! Sent to custodian 3. + finalKey: genCleanZmkKey.finalKey, // FATAL! Never in the clear. XOR of key elements. + kcv: genCleanZmkKey.kcv // This allows all parties to verify the integrity of the key. + }) + } + ) + + app.post( + '/hsm/ase-merchant/generate-zmk', + async function handler(ffReq, ffReply) { + const genCleanZmkKey = generate3DESKeyFromComponents() + const { iv, tr31Block } = createTR31KeyBlockUnder( + Tr31Intent.LMK, + AES_MERCHANT_ASE_LMK_HEX, + KeyUsage.ZMK, + 'T', + genCleanZmkKey.finalKeyBuffer + ) + + logger.info( + `Generated ZMK (Acquirer): ${tr31Block.toString('ascii')} with KCV: ${genCleanZmkKey.kcv}` + ) + + ffReply.code(200).send({ + iv: iv.toString('hex'), + tr31Block: tr31Block.toString('ascii'), // Only to be used with ILF HSM + component1: genCleanZmkKey.component1, // DANGER! Sent to custodian 1. + component2: genCleanZmkKey.component2, // DANGER! Sent to custodian 2. + component3: genCleanZmkKey.component3, // DANGER! Sent to custodian 3. + finalKey: genCleanZmkKey.finalKey, // FATAL! Never in the clear. XOR of key elements. + kcv: genCleanZmkKey.kcv // This allows all parties to verify the integrity of the key. + }) + } + ) + + app.post( + '/hsm/ase-merchant/generate-bdk', + async function handler(ffReq, ffReply) { + const requestBody = JSON.parse(JSON.stringify(ffReq.body)) + const { zmkUnderLmk } = requestBody + + const genBdkKey = generateBDK(AES_MERCHANT_ASE_LMK_HEX, zmkUnderLmk) + + logger.info( + `ILF generated BDK (Acquirer) '${genBdkKey.tr31BdkUnderZmk}|${genBdkKey.tr31BdkUnderZmk}' with KCV: ${genBdkKey.kcv}` + ) + + ffReply.code(200).send({ + tr31BdkUnderLmk: genBdkKey.tr31BdkUnderLmk, + tr31BdkUnderZmk: genBdkKey.tr31BdkUnderZmk, + kcv: genBdkKey.kcv + }) + } + ) + + app.post( + '/hsm/ase-merchant/derive-ipek', + async function handler(ffReq, ffReply) { + const requestBody = JSON.parse(JSON.stringify(ffReq.body)) + const { tr31BdkUnderLmk, tr31TmkUnderLmk, ksnHex } = requestBody + const ipek = deriveIPEK( + AES_MERCHANT_ASE_LMK_HEX, + tr31BdkUnderLmk, + tr31TmkUnderLmk, + ksnHex + ) + logger.info( + `ILF generated IPEK (Acquirer) '${ipek.tr31IpekUnderLmk}|${ipek.tr31IpekUnderTmk}' with KCV: ${ipek.kcv}` + ) + + ffReply.code(200).send({ + tr31IpekUnderLmk: ipek.tr31IpekUnderLmk, + tr31IpekUnderTmk: ipek.tr31IpekUnderTmk, + kcv: ipek.kcv + }) + } + ) + + app.post('/hsm/kai-os/import-zmk', async function handler(ffReq, ffReply) { + const requestBody = JSON.parse(JSON.stringify(ffReq.body)) + const { component1, component2, component3, kcv } = requestBody + + const zmkKey = import3DESKeyFromComponents( + AES_KAI_LMK_HEX, + KeyUsage.ZMK, + component1, + component2, + component3, + kcv + ) + + logger.info( + `KaiOS imported ZMK (Terminal Manufacturer) '${zmkKey.tr31KeyBlock}' with KCV: ${zmkKey.kcv}` + ) + + ffReply.code(200).send({ + iv: zmkKey.iv.toString('hex'), + tr31Block: zmkKey.tr31KeyBlock, // Only to be used with KAI HSM + kcv // This allows all parties to verify the integrity of the key. + }) + }) + + app.post( + '/hsm/austria-card/import-zmk', + async function handler(ffReq, ffReply) { + const requestBody = JSON.parse(JSON.stringify(ffReq.body)) + const { component1, component2, component3, kcv } = requestBody + + const zmkKey = import3DESKeyFromComponents( + AES_AUSTRIA_CARD_LMK_HEX, + KeyUsage.ZMK, + component1, + component2, + component3, + kcv + ) + + logger.info( + `AustriaCard imported ZMK (Card Personalization) '${zmkKey.tr31KeyBlock}' with KCV: ${zmkKey.kcv}` + ) + + ffReply.code(200).send({ + iv: zmkKey.iv.toString('hex'), + tr31Block: zmkKey.tr31KeyBlock, // Only to be used with KAI HSM + kcv // This allows all parties to verify the integrity of the key. + }) + } + ) + + app.post('/hsm/kai-os/generate-tmk', async function handler(ffReq, ffReply) { + const requestBody = JSON.parse(JSON.stringify(ffReq.body)) + const { zmkUnderLmk, terminalSerial } = requestBody + + const genTmkKey = generateTMK(AES_KAI_LMK_HEX, zmkUnderLmk) + + logger.info( + `KaiOS generated TMK (Terminal Manufacturer) '${genTmkKey.tr31TmkUnderLmk}|${genTmkKey.tr31TmkUnderZmk}' with KCV: ${genTmkKey.kcv}` + ) + + ffReply.code(200).send({ + tr31TmkUnderLmk: genTmkKey.tr31TmkUnderLmk, + tr31TmkUnderZmk: genTmkKey.tr31TmkUnderZmk, + terminalSerial, + kcv: genTmkKey.kcv + }) + }) + + app.post( + '/hsm/ase-merchant/import-tmk', + async function handler(ffReq, ffReply) { + const requestBody = JSON.parse(JSON.stringify(ffReq.body)) + const { tr31ZmkUnderLmk, tr31TmkUnderZmk, kcv } = requestBody + + const tr31TmkUnderLmk = importTMK( + AES_MERCHANT_ASE_LMK_HEX, + tr31ZmkUnderLmk, + tr31TmkUnderZmk, + kcv + ) + + logger.info( + `ILF imported TMK (Merchant) '${tr31TmkUnderLmk.tr31TmkUnderLmk}' with KCV: ${tr31TmkUnderLmk.kcv}` + ) + + ffReply.code(200).send({ + tr31TmkUnderLmk: tr31TmkUnderLmk.tr31TmkUnderLmk, + kcv: tr31TmkUnderLmk.kcv + }) + } + ) + + // At this point, we have a mechanism for transporting keys to the card and terminal via TMK. + // We are now able to issue SRED and PIN BDK keys. + + app.post( + '/hsm/ase-customer/generate-card-key', + async function handler(ffReq, ffReply) { + const requestBody = JSON.parse(JSON.stringify(ffReq.body)) + const { tr31ZmkUnderLmk } = requestBody + const genCardKey = generateCardKey( + AES_CUSTOMER_ASE_LMK_HEX, + tr31ZmkUnderLmk + ) + + logger.info( + `ILF generated Card Key-Pair (Issuer) '${genCardKey.tr31CardKeyUnderLmk}|${genCardKey.tr31CardKeyUnderZmk}' with KCV: ${genCardKey.kcv}` + ) + + ffReply.code(200).send({ + tr31CardKeyUnderLmk: genCardKey.tr31CardKeyUnderLmk, + tr31CardKeyUnderZmk: genCardKey.tr31CardKeyUnderZmk, + publicKey: genCardKey.publicKey, + kcv: genCardKey.kcv + }) + } + ) + + app.post( + '/hsm/austria-card/import-card-key', + async function handler(ffReq, ffReply) { + const requestBody = JSON.parse(JSON.stringify(ffReq.body)) + const { tr31ZmkUnderLmk, tr31CardKeyUnderZmk, kcv } = requestBody + + const tr31CardKeyUnderLmk = importCardKey( + AES_AUSTRIA_CARD_LMK_HEX, + tr31ZmkUnderLmk, + tr31CardKeyUnderZmk, + kcv + ) + + logger.info( + `ILF imported CardKey (Card Personalization) '${tr31CardKeyUnderLmk.tr31CardKeyUnderLmk}' with KCV: ${tr31CardKeyUnderLmk.kcv}` + ) + + ffReply.code(200).send({ + tr31CardKeyUnderLmk: tr31CardKeyUnderLmk.tr31CardKeyUnderLmk, + kcv: tr31CardKeyUnderLmk.kcv + }) + } + ) + + return async () => { + await app.listen({ port, host: '0.0.0.0' }) + logger.info( + `πŸ—ƒ -> πŸ”‘ <-πŸ—ƒ 'Rafiki-HSM-Emulator' Listening on port '${port}'` + ) + } +} diff --git a/test/hsm-emulator/src/card-hsm.ts b/test/hsm-emulator/src/card-hsm.ts new file mode 100644 index 0000000000..08463cbbf6 --- /dev/null +++ b/test/hsm-emulator/src/card-hsm.ts @@ -0,0 +1,678 @@ +import { + randomBytes, + createCipheriv, + createDecipheriv, + createHash, + generateKeyPairSync +} from 'crypto' +import logger from './logger' + +/** + * The AES Local Master Key for the Customer ASE HSM. + */ +const AES_CUSTOMER_ASE_LMK_HEX = + '00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff' + +/** + * The AES Local Master Key for the Merchant ASE HSM. + */ +const AES_MERCHANT_ASE_LMK_HEX = + '01112233445566778899aabbccddeeff00112233445566778899aabbccddeeff' +/** + * The AES Local Master Key for the KaiOS HSM. + */ +const AES_KAI_LMK_HEX = + 'ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100' +/** + * The AES Local Master Key for the Austria Card HSM. + */ +const AES_AUSTRIA_CARD_LMK_HEX = + '11112233445566778899aabbccddeeff00112233446666778899aabbccddeeff' + +enum KeyUsage { + ZMK, + TMK, + DEK, + BDK, + IPK +} + +enum Tr31Intent { + LMK, + ZMK, + TMK +} + +// XOR two buffers +function xorBuffers(buf1: Buffer, buf2: Buffer): Buffer { + if (buf1.length !== buf2.length) { + throw new Error('Buffers must be the same length for XOR') + } + const result = Buffer.alloc(buf1.length) + for (let i = 0; i < buf1.length; i++) { + result[i] = buf1[i] ^ buf2[i] + } + return result +} + +// Generate 3DES key from 3 components (each 24 bytes) +function generate3DESKeyFromComponents(): { + component1: string + component2: string + component3: string + finalKey: string + finalKeyBuffer: Buffer + kcv: string +} { + const length = 24 // 3DES key length (3 x 8 bytes = 24 bytes) + + const keyComponent1 = randomBytes(length) + const keyComponent2 = randomBytes(length) + const keyComponent3 = randomBytes(length) + + // Combine via XOR + const tempXor = xorBuffers(keyComponent1, keyComponent2) + const finalKey = xorBuffers(tempXor, keyComponent3) + + logger.info('Key Component 1:', keyComponent1.toString('hex')) + logger.info('Key Component 2:', keyComponent2.toString('hex')) + logger.info('Key Component 3:', keyComponent3.toString('hex')) + logger.info('Final 3DES Key :', finalKey.toString('hex')) + + return { + component1: keyComponent1.toString('hex').toUpperCase(), + component2: keyComponent2.toString('hex').toUpperCase(), + component3: keyComponent3.toString('hex').toUpperCase(), + finalKey: finalKey.toString('hex').toUpperCase(), + finalKeyBuffer: finalKey, + kcv: obtainKCVFrom3DESKey(finalKey) + } +} + +function import3DESKeyFromComponents( + kekHex: string, + keyUsage: KeyUsage, + component1: string, + component2: string, + component3: string, + kcv?: string +): { + finalKey: string + finalKeyBuffer: Buffer + tr31KeyBlock: string + iv: Buffer + kcv: string +} { + const keyComponent1 = Buffer.from(component1, 'hex') + const keyComponent2 = Buffer.from(component2, 'hex') + const keyComponent3 = Buffer.from(component3, 'hex') + + // Combine via XOR + const tempXor = xorBuffers(keyComponent1, keyComponent2) + const finalKey = xorBuffers(tempXor, keyComponent3) + const finalKeyKcv = obtainKCVFrom3DESKey(finalKey) + if (kcv && kcv !== finalKeyKcv) { + throw new Error(`Expected KCV '${kcv}' but got '${finalKeyKcv}' instead.`) + } + + const { iv, tr31Block } = createTR31KeyBlockUnder( + Tr31Intent.LMK, + kekHex, + keyUsage, + 'T', //T for TDEA, A for AES + finalKey + ) + + return { + finalKey: finalKey.toString('hex').toUpperCase(), + finalKeyBuffer: finalKey, + tr31KeyBlock: tr31Block.toString('ascii').toUpperCase(), + iv, + kcv: finalKeyKcv + } +} + +function generateTMK( + lmk: string, + tr31ZmkUnderLmk: string +): { + tr31TmkUnderLmk: string + tr31TmkUnderZmk: string + kcv: string +} { + const { clearKeyHex } = extractClearKeyFromTR31KeyBlock( + Tr31Intent.LMK, + lmk, + tr31ZmkUnderLmk + ) + + const tmkRaw = randomBytes(24) + const tmkKCV = obtainKCVFrom3DESKey(tmkRaw) + + const tr31TmkUnderLmk = createTR31KeyBlockUnder( + Tr31Intent.LMK, + lmk, + KeyUsage.TMK, + 'T', + tmkRaw + ) + const tr31TmkUnderZmk = createTR31KeyBlockUnder( + Tr31Intent.ZMK, + clearKeyHex, //ZMK + KeyUsage.TMK, + 'T', + tmkRaw + ) + + return { + tr31TmkUnderLmk: tr31TmkUnderLmk.tr31Block.toString('ascii'), + tr31TmkUnderZmk: tr31TmkUnderZmk.tr31Block.toString('ascii'), + kcv: tmkKCV + } +} + +function generateBDK( + lmk: string, + tr31ZmkUnderLmk: string +): { + tr31BdkUnderLmk: string + tr31BdkUnderZmk: string + kcv: string +} { + const { clearKeyHex } = extractClearKeyFromTR31KeyBlock( + Tr31Intent.LMK, + lmk, + tr31ZmkUnderLmk + ) + + const bdkRaw = randomBytes(24) + const bdkKCV = obtainKCVFrom3DESKey(bdkRaw) + + const tr31BdkUnderLmk = createTR31KeyBlockUnder( + Tr31Intent.LMK, + lmk, + KeyUsage.BDK, + 'T', + bdkRaw + ) + const tr31BdkUnderZmk = createTR31KeyBlockUnder( + Tr31Intent.ZMK, + clearKeyHex, //ZMK + KeyUsage.BDK, + 'T', + bdkRaw + ) + + return { + tr31BdkUnderLmk: tr31BdkUnderLmk.tr31Block.toString('ascii'), + tr31BdkUnderZmk: tr31BdkUnderZmk.tr31Block.toString('ascii'), + kcv: bdkKCV + } +} + +function deriveIPEK( + lmkHex: string, + tr31BdkUnderLmk: string, + tr31TmkUnderLmk: string, + ksnHex: string +): { + tr31IpekUnderLmk: string + tr31IpekUnderTmk: string + kcv: string +} { + // 1. Obtain the clear BDK key from the LMK: + const clearBdkKey = extractClearKeyFromTR31KeyBlock( + Tr31Intent.LMK, + lmkHex, + tr31BdkUnderLmk + ) + + const ksn = Buffer.from(ksnHex, 'hex') + const bdk = clearBdkKey.clearKey + if (bdk.length !== 24 || ksn.length !== 10) { + throw new Error('BDK must be 24 bytes and KSN must be 10 bytes') + } + + // Step 1: Mask the KSN (clear rightmost 21 bits) + const ksnMasked = Buffer.from(ksn) + ksnMasked[7] &= 0xe0 + ksnMasked[8] = 0x00 + ksnMasked[9] = 0x00 + + // Step 2: Encrypt ksnMasked with original BDK (Key 1) + const cipher1 = createCipheriv('des-ede3', bdk, null) + let left = cipher1.update(ksnMasked.subarray(0, 8)) + left = Buffer.concat([left, cipher1.final()]) + + // Step 3: XOR BDK with mask + const mask = Buffer.from( + 'C0C0C0C000000000C0C0C0C000000000C0C0C0C000000000', + 'hex' + ) + const bdkMasked = xorBuffers(bdk, mask) + + // Step 4: Encrypt ksnMasked with masked BDK (Key 2) + const cipher2 = createCipheriv('des-ede3', bdkMasked, null) + let right = cipher2.update(ksnMasked.subarray(0, 8)) + right = Buffer.concat([right, cipher2.final()]) + + // Step 5: IPEK is 16-byte concat of both halves: + const ipekClear = Buffer.concat([left, right]) + const tr31IpekUnderLmk = createTR31KeyBlockUnder( + Tr31Intent.LMK, + lmkHex, + KeyUsage.IPK, + 'T', + ipekClear + ) + + const clearTmkKey = extractClearKeyFromTR31KeyBlock( + Tr31Intent.LMK, + lmkHex, + tr31TmkUnderLmk + ) + + const tr31IpekUnderTmk = createTR31KeyBlockUnder( + Tr31Intent.TMK, + clearTmkKey.clearKeyHex, // TMK + KeyUsage.IPK, + 'T', + ipekClear + ) + return { + tr31IpekUnderLmk: tr31IpekUnderLmk.tr31Block.toString('ascii'), + tr31IpekUnderTmk: tr31IpekUnderTmk.tr31Block.toString('ascii'), + kcv: tr31IpekUnderTmk.kcv + } +} + +function importTMK( + lmkHex: string, + tr31ZmkUnderLmk: string, + tr31TmkUnderZmk: string, + kcv?: string +): { + tr31TmkUnderLmk: string + kcv: string +} { + // 1. Obtain the clear ZMK key from the ZMK under LMK: + const clearZmkKey = extractClearKeyFromTR31KeyBlock( + Tr31Intent.LMK, + lmkHex, + tr31ZmkUnderLmk + ) + + // 2. Obtain the clear TMK key: + const clearTmkKey = extractClearKeyFromTR31KeyBlock( + Tr31Intent.ZMK, + clearZmkKey.clearKeyHex, + tr31TmkUnderZmk + ) + if (kcv && kcv !== clearTmkKey.kcv) { + throw new Error( + `Expected KCV '${kcv}' but got '${clearTmkKey.kcv}' instead.` + ) + } + + const tmkUnderLmk = createTR31KeyBlockUnder( + Tr31Intent.LMK, + lmkHex, + KeyUsage.TMK, + 'T', + clearTmkKey.clearKey + ) + + return { + kcv: clearTmkKey.kcv, + tr31TmkUnderLmk: tmkUnderLmk.tr31Block.toString('ascii') + } +} + +function importCardKey( + lmkHex: string, + tr31ZmkUnderLmk: string, + tr31CardKeyUnderZmk: string, + kcv?: string +): { + tr31CardKeyUnderLmk: string + kcv: string +} { + // 1. Obtain the clear ZMK key from the ZMK under LMK: + const clearZmkKey = extractClearKeyFromTR31KeyBlock( + Tr31Intent.LMK, + lmkHex, + tr31ZmkUnderLmk + ) + // 2. Obtain the clear Card key: + const clearCardKey = extractClearKeyFromTR31KeyBlock( + Tr31Intent.ZMK, + clearZmkKey.clearKeyHex, + tr31CardKeyUnderZmk + ) + if (kcv && kcv !== clearCardKey.kcv) { + throw new Error( + `Expected KCV '${kcv}' but got '${clearCardKey.kcv}' instead.` + ) + } + + const cardKeyUnderLmk = createTR31KeyBlockUnder( + Tr31Intent.LMK, + lmkHex, + KeyUsage.DEK, + 'T', + clearCardKey.clearKey + ) + + return { + kcv: clearCardKey.kcv, + tr31CardKeyUnderLmk: cardKeyUnderLmk.tr31Block.toString('ascii') + } +} + +function generateCardKey( + lmk: string, + tr31ZmkUnderLmk: string +): { + tr31CardKeyUnderLmk: string + tr31CardKeyUnderZmk: string + publicKey: string + kcv: string +} { + // EllipticCurve: prime256v1 + const { publicKey, privateKey } = generateKeyPairSync('ec', { + //modulusLength: keySize, // Key size in bits (RSA/DSA) + namedCurve: 'prime256v1', + publicKeyEncoding: { + type: 'spki', // Recommended for public keys + format: 'pem' //pem for string, der for buffer. + }, + privateKeyEncoding: { + type: 'pkcs8', // Recommended for private keys + format: 'der' + // We do not want encryption, as we make use of AES already. + //cipher: 'aes-256-cbc', // Optional encryption + //passphrase // Optional passphrase + } + }) + + logger.info( + `Private key size is ${privateKey.length} bytes, and public key size is ${publicKey.length} bytes` + ) + + const tr31CardKeyUnderLmk = createTR31KeyBlockUnder( + Tr31Intent.LMK, + lmk, + KeyUsage.DEK, + 'T', + privateKey + ) + + const clearPvtKcv = sha512Last3Bytes(privateKey) + const clearZmkKeyHex = extractClearKeyFromTR31KeyBlock( + Tr31Intent.LMK, + lmk, + tr31ZmkUnderLmk + ).clearKeyHex + const tr31CardKeyUnderZmk = createTR31KeyBlockUnder( + Tr31Intent.ZMK, + clearZmkKeyHex, //ZMK + KeyUsage.DEK, + 'T', + privateKey + ) + + if (tr31CardKeyUnderZmk.kcv !== clearPvtKcv) { + throw new Error( + `PvtKey under ZMK failure. Expected KCV '${clearPvtKcv}' but got '${tr31CardKeyUnderZmk.kcv}'.` + ) + } + + const tr31CardKeyUnderLmkAscii = + tr31CardKeyUnderLmk.tr31Block.toString('ascii') + const pvtKey = extractClearKeyFromTR31KeyBlock( + Tr31Intent.LMK, + lmk, + tr31CardKeyUnderLmkAscii + ) + + logger.debug(`Private key [BACK] size is ${pvtKey.clearKey.length} bytes.`) + + return { + tr31CardKeyUnderLmk: tr31CardKeyUnderLmk.tr31Block.toString('ascii'), + tr31CardKeyUnderZmk: tr31CardKeyUnderZmk.tr31Block.toString('ascii'), + publicKey, + kcv: clearPvtKcv + } +} + +function obtainKCVFrom3DESKey(key: Buffer | Uint8Array): string { + const data = Buffer.alloc(8, 0x00) // 8 bytes of zeros + const cipher = createCipheriv('des-ede3', key, null) + const encrypted = Buffer.concat([cipher.update(data), cipher.final()]) + return encrypted.subarray(0, 3).toString('hex').toUpperCase() // First 3 bytes +} + +function sha512Last3Bytes(input: Buffer | Uint8Array): string { + const hexSha = createHash('sha512').update(input).digest('hex') + return hexSha.slice(-6).toUpperCase() +} + +function encryptWithAES256( + plaintext: Buffer | Uint8Array, + aesKeyHex: string, + zeroIv: boolean = true +): { iv: Buffer; ciphertext: Buffer; ciphertextHex: string } { + if (aesKeyHex.length !== 64) + throw new Error('AES key must be 64 hex characters (256 bits / 32 bytes)') + + const key = Buffer.from(aesKeyHex, 'hex') + const iv = zeroIv ? Buffer.alloc(16) : randomBytes(16) // AES block size = 16 bytes + + const cipher = createCipheriv('aes-256-cbc', key, iv) + const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]) + + return { + iv, + ciphertext: encrypted, + ciphertextHex: encrypted.toString('hex') + } +} + +function encryptWith3DES( + plaintext: Buffer | Uint8Array, + keyHex: string, + zeroIv: boolean = true +): { iv: Buffer; ciphertext: Buffer; ciphertextHex: string } { + if (keyHex.length !== 48) + throw new Error('3DES key must be 48 hex characters (24 bytes)') + + const key = Buffer.from(keyHex, 'hex') + const iv = zeroIv ? Buffer.alloc(8) : randomBytes(8) // 3DES block size = 8 bytes + + const cipher = createCipheriv('des-ede3-cbc', key, iv) + const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]) + + return { + iv, + ciphertext: encrypted, + ciphertextHex: encrypted.toString('hex') + } +} + +function decryptWithAES256( + ciphertext: Buffer, + aesKeyHex: string, + iv: Buffer +): Buffer { + if (aesKeyHex.length !== 64) + throw new Error('AES key must be 64 hex characters (256 bits / 32 bytes)') + if (iv.length !== 16) throw new Error('IV must be 16 bytes (128 bits)') + + const key = Buffer.from(aesKeyHex, 'hex') + const decipher = createDecipheriv('aes-256-cbc', key, iv) + logger.info( + 'decipher.decryptWithAES256: ' + + ciphertext.length + + ' <-> ' + + key.length + + ' <-> ' + + iv.length + + ' | ' + + key + + ' | ' + + ciphertext + ) + return Buffer.concat([decipher.update(ciphertext), decipher.final()]) +} + +function decryptWith3DES( + ciphertext: Buffer, + keyHex: string, + iv: Buffer +): Buffer { + if (keyHex.length !== 48) + throw new Error('3DES key must be 48 hex characters (24 bytes)') + if (iv.length !== 8) throw new Error('IV must be 8 bytes (64 bits)') + + const key = Buffer.from(keyHex, 'hex') + const decipher = createDecipheriv('des-ede3-cbc', key, iv) + return Buffer.concat([decipher.update(ciphertext), decipher.final()]) +} + +function createTR31KeyBlockUnder( + intent: Tr31Intent, + kekHex: string, // 32-byte AES key (hex-encoded) + keyUsage: KeyUsage, // 3 chars, e.g., 'DEK' for Encryption Key + keyType: string, // 1 char, e.g., 'T' for TDEA, 'A' for AES + key: Buffer | Uint8Array, // Key material (e.g., 24 bytes for 3DES) + zeroIv: boolean = true +): { iv: Buffer; tr31Block: Buffer; kcv: string } { + if (intent == Tr31Intent.LMK && kekHex.length !== 64) + throw new Error( + `KEK (LMK) must be 64 hex chars (32 bytes), currently ${kekHex.length}` + ) + //AES + else if ( + (intent == Tr31Intent.ZMK || intent == Tr31Intent.TMK) && + kekHex.length !== 48 + ) + throw new Error( + `KEK (ZMK) must be 48 hex chars (24 bytes), currently ${kekHex.length}` + ) //3DES + + const kcv = isKeyUsageForNon3DS(keyUsage) + ? sha512Last3Bytes(key) + : obtainKCVFrom3DESKey(key) + + // for data, we convert to ASCII-HEX: + if (isKeyUsageForNon3DS(keyUsage)) key = Buffer.from(key.toString('hex')) + + // Encrypt using AES-256-CBC + const encryptedKey = + intent == Tr31Intent.LMK + ? encryptWithAES256(key, kekHex, zeroIv) + : encryptWith3DES(key, kekHex, zeroIv) + const encryptedKeyHex = encryptedKey.ciphertextHex.toUpperCase() + + // TR-31 Header – 16 bytes (simplified) + const version = intent == Tr31Intent.LMK ? 'S' : 'B' // 1 byte: Version ID + const reserved = '0' // 1 byte: Reserved + const usage = `${KeyUsage[keyUsage]}` // 3 bytes: Key Usage (e.g., 'ZMK' / 'DEK') + const algo = keyType // 1 byte: Key Algorithm ID ('T' for 3DES) + const exportFlag = 'N' // 1 byte: Export flag + const numComponents = '1' // 1 byte: Key components (assume 1) + const generationMethod = 'K' // 1 byte: Key generation method + const length = encryptedKeyHex.length.toString().padStart(4, '0') // 4 bytes: Key length + const reserved2 = '000000000' // 9 bytes reserved + + const headerStr = + version + + reserved + + usage + + algo + + exportFlag + + numComponents + + generationMethod + + length + + reserved2 + const header = Buffer.from(headerStr, 'ascii') // total 16 bytes + + // Combine header and key + const tr31Block = Buffer.concat([ + header, + Buffer.from(encryptedKeyHex), + Buffer.from(kcv) + ]) + + return { + iv: encryptedKey.iv, + tr31Block, + kcv + } +} + +function isKeyUsageForNon3DS(keyUsage: KeyUsage): boolean { + switch (keyUsage) { + case KeyUsage.DEK: + case KeyUsage.IPK: + return true + default: + return false + } +} + +function extractClearKeyFromTR31KeyBlock( + intent: Tr31Intent, + kekHex: string, + tr31: string +): { + clearKey: Buffer + clearKeyHex: string + kcv: string +} { + const keyLength = parseInt(tr31.substring(9, 13)) + const usage = KeyUsage[tr31.substring(2, 5) as keyof typeof KeyUsage] + const key = tr31.substring(22, 22 + keyLength) + const tr31EncKeyPortion = Buffer.from(key, 'hex') + let clearKey = + intent === Tr31Intent.LMK + ? decryptWithAES256(tr31EncKeyPortion, kekHex, Buffer.alloc(16)) + : decryptWith3DES(tr31EncKeyPortion, kekHex, Buffer.alloc(8)) + if (isKeyUsageForNon3DS(usage)) + clearKey = Buffer.from(clearKey.toString('ascii'), 'hex') + + const tr31Kcv = tr31.slice(-6) + const kcvComputed = isKeyUsageForNon3DS(usage) + ? sha512Last3Bytes(clearKey) + : obtainKCVFrom3DESKey(clearKey) + if (kcvComputed !== tr31Kcv) { + throw new Error( + `Expected KCV '${tr31Kcv}' but got '${kcvComputed}'. Please confirm correct KEK is used.` + ) + } + + return { + clearKey: clearKey, + clearKeyHex: clearKey.toString('hex'), + kcv: kcvComputed + } +} + +export { + generate3DESKeyFromComponents, + import3DESKeyFromComponents, + obtainKCVFrom3DESKey, + createTR31KeyBlockUnder, + generateTMK, + generateBDK, + deriveIPEK, + importTMK, + generateCardKey, + importCardKey, + AES_MERCHANT_ASE_LMK_HEX, + AES_CUSTOMER_ASE_LMK_HEX, + AES_KAI_LMK_HEX, + AES_AUSTRIA_CARD_LMK_HEX, + KeyUsage, + Tr31Intent +} diff --git a/test/hsm-emulator/src/index.ts b/test/hsm-emulator/src/index.ts new file mode 100644 index 0000000000..dd22df10f0 --- /dev/null +++ b/test/hsm-emulator/src/index.ts @@ -0,0 +1,13 @@ +import { createApp } from './app' +import logger from './logger' +;(async () => { + const start = createApp( + Number(process.env['HTTP_HSM_EMULATOR_API_PORT'] ?? 5002) + ) + await start() + + process.on('SIGINT', () => { + logger.info('Received SIGINT. Shutting down gracefully...') + process.exit(0) + }) +})() diff --git a/test/hsm-emulator/src/logger.ts b/test/hsm-emulator/src/logger.ts new file mode 100644 index 0000000000..072d15c3b9 --- /dev/null +++ b/test/hsm-emulator/src/logger.ts @@ -0,0 +1,6 @@ +import pino from 'pino' + +export default pino({ + level: process.env['LOG_LEVEL'] || 'info', + base: null +}) diff --git a/test/hsm-emulator/tsconfig.json b/test/hsm-emulator/tsconfig.json new file mode 100644 index 0000000000..bc4252bfc2 --- /dev/null +++ b/test/hsm-emulator/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "src/test/*"] +}