From b724fc4b8cd41fc4f80ce149d6c1f3bcb274a205 Mon Sep 17 00:00:00 2001 From: J8k3 Date: Mon, 18 May 2026 23:02:01 -0400 Subject: [PATCH] Fix AES DUKPT derivation data format; add X9.24-3 test vectors Co-Authored-By: Claude Sonnet 4.6 --- src/core/operations/DeriveDUKPTAESKey.mjs | 73 ++++++++++++------ tests/operations/tests/Payment.mjs | 92 +++++++++++++++++++++++ 2 files changed, 144 insertions(+), 21 deletions(-) diff --git a/src/core/operations/DeriveDUKPTAESKey.mjs b/src/core/operations/DeriveDUKPTAESKey.mjs index c061cd7e..0d2d8217 100644 --- a/src/core/operations/DeriveDUKPTAESKey.mjs +++ b/src/core/operations/DeriveDUKPTAESKey.mjs @@ -11,7 +11,7 @@ import { toHexFast } from "../lib/Hex.mjs"; // ── X9.24-3 key usage indicators (bytes 2-3 of derivation data) ─────────────── const KEY_USAGE = { - "IK Derivation": 0x8000, // BDK → device Initial Key + "IK Derivation": 0x8001, // BDK → device Initial Key (X9.24-3 §6.3.1) Intermediate: 0x0000, // internal binary-tree node (not user-visible) "PIN Encryption": 0x1000, "MAC Generation": 0x2000, // sender / request direction @@ -165,15 +165,42 @@ function aesCmac(key16, message) { // ── X9.24-3 AES-128 DUKPT derivation ───────────────────────────────────────── /** - * Builds the 20-byte derivation data block (ANSI X9.24-3-2017). + * Builds the 16-byte IK derivation data block (ANSI X9.24-3-2017 §6.3.1). + * + * Layout (IK derivation only — uses full 8-byte IKI, no counter field): + * [0] version = 0x01 + * [1] key size class = 0x01 (AES-128) + * [2-3] key usage = 0x8001 (IK Derivation) + * [4-5] algorithm = 0x0002 (AES-128) + * [6-7] key length = 0x0080 (128 bits) + * [8-15] IKI (full 8 bytes) + * + * @param {Uint8Array} iki8 + * @returns {Uint8Array} + */ +function ikDerivationData(iki8) { + const d = new Uint8Array(16); + d[0] = 0x01; d[1] = 0x01; + d[2] = (KEY_USAGE["IK Derivation"] >> 8) & 0xFF; + d[3] = KEY_USAGE["IK Derivation"] & 0xFF; + d[4] = (ALGO_CODE >> 8) & 0xFF; d[5] = ALGO_CODE & 0xFF; + d[6] = (KEY_LEN_VAL >> 8) & 0xFF; d[7] = KEY_LEN_VAL & 0xFF; + d.set(iki8, 8); + return d; +} + +/** + * Builds the 16-byte derivation data block for intermediate-node and working-key + * derivation (ANSI X9.24-3-2017 §6.3.2 / §6.3.3). * * Layout: - * [0-1] version = 0x0001 - * [2-3] key usage indicator - * [4-5] algorithm = 0x0002 (AES-128) - * [6-7] key length = 0x0080 (128 bits) - * [8-15] IKI (8 bytes, from KSN bytes 0-7) - * [16-19] counter register (4 bytes) + * [0] version = 0x01 + * [1] key size class = 0x01 (AES-128) + * [2-3] key usage indicator + * [4-5] algorithm = 0x0002 (AES-128) + * [6-7] key length = 0x0080 (128 bits) + * [8-11] last 4 bytes of IKI (IKI[4..7]) + * [12-15] counter register (4 bytes, big-endian) * * @param {number} usage * @param {Uint8Array} iki8 @@ -181,16 +208,17 @@ function aesCmac(key16, message) { * @returns {Uint8Array} */ function derivationData(usage, iki8, counterReg) { - const d = new Uint8Array(20); - d[0] = 0x00; d[1] = 0x01; + const d = new Uint8Array(16); + d[0] = 0x01; d[1] = 0x01; d[2] = (usage >> 8) & 0xFF; d[3] = usage & 0xFF; d[4] = (ALGO_CODE >> 8) & 0xFF; d[5] = ALGO_CODE & 0xFF; d[6] = (KEY_LEN_VAL >> 8) & 0xFF; d[7] = KEY_LEN_VAL & 0xFF; - d.set(iki8, 8); - d[16] = (counterReg >>> 24) & 0xFF; - d[17] = (counterReg >>> 16) & 0xFF; - d[18] = (counterReg >>> 8) & 0xFF; - d[19] = counterReg & 0xFF; + // last 4 bytes of 8-byte IKI + d[8] = iki8[4]; d[9] = iki8[5]; d[10] = iki8[6]; d[11] = iki8[7]; + d[12] = (counterReg >>> 24) & 0xFF; + d[13] = (counterReg >>> 16) & 0xFF; + d[14] = (counterReg >>> 8) & 0xFF; + d[15] = counterReg & 0xFF; return d; } @@ -202,7 +230,7 @@ function derivationData(usage, iki8, counterReg) { * @returns {Uint8Array} */ function deriveIK(bdk16, iki8) { - return aesCmac(bdk16, derivationData(KEY_USAGE["IK Derivation"], iki8, 0)); + return aesCmac(bdk16, ikDerivationData(iki8)); } /** @@ -235,6 +263,7 @@ function deriveTransactionKey(ik16, iki8, counter) { /** * Derives a purpose-specific working key from the transaction key. + * Uses the full 32-bit counter in the derivation data (not the 21-bit tree counter). * * @param {Uint8Array} txKey16 * @param {Uint8Array} iki8 @@ -243,7 +272,7 @@ function deriveTransactionKey(ik16, iki8, counter) { * @returns {Uint8Array} */ function deriveWorkingKey(txKey16, iki8, counter, purposeName) { - return aesCmac(txKey16, derivationData(KEY_USAGE[purposeName], iki8, counter & 0x1FFFFF)); + return aesCmac(txKey16, derivationData(KEY_USAGE[purposeName], iki8, counter)); } // ── Operation class ─────────────────────────────────────────────────────────── @@ -269,15 +298,17 @@ class DeriveDUKPTAESKey extends Operation { "The KSN is 12 bytes: 8-byte Initial Key Identifier (IKI) + 4-byte transaction counter.", "Only the low 21 bits of the counter are used for derivation (max 2,097,151 transactions per IK).", "

", - "Derivation data format (X9.24-3, 20 bytes):", + "Derivation data format (X9.24-3, 16 bytes — working keys):", "
",
-            "[0-1]  version        = 0x0001\n",
+            "[0]    version        = 0x01\n",
+            "[1]    key size class = 0x01 (AES-128)\n",
             "[2-3]  key usage indicator\n",
             "[4-5]  algorithm      = 0x0002 (AES-128)\n",
             "[6-7]  key length     = 0x0080 (128 bits)\n",
-            "[8-15] IKI            (8 bytes from KSN)\n",
-            "[16-19] counter register (4 bytes)\n",
+            "[8-11] last 4 bytes of IKI\n",
+            "[12-15] transaction counter (4 bytes, full 32-bit value)\n",
             "
", + "IK derivation uses a separate 16-byte block with full 8-byte IKI at [8-15] and usage 0x8001.", "Key usage codes: PIN Encryption=0x1000, MAC Generation=0x2000, ", "MAC Verification=0x2001, MAC Both Ways=0x2002, ", "Data Encryption=0x3000, Data Decryption=0x3001, Data Both Ways=0x3002.", diff --git a/tests/operations/tests/Payment.mjs b/tests/operations/tests/Payment.mjs index a2abcfb6..0307944f 100644 --- a/tests/operations/tests/Payment.mjs +++ b/tests/operations/tests/Payment.mjs @@ -315,6 +315,98 @@ TestRegister.addTests([ } ] }, + { + // ── DUKPT Derive AES Key — ANSI X9.24-3-2017 official test vectors ─────── + // BDK-128: FEDCBA9876543210F1F1F1F1F1F1F1F1 + // KSN: 1234567890123456 (IKI) + counter + // Source: https://x9.org/standards/x9-24-part-3-test-vectors/ + name: "DUKPT Derive AES Key: IK from BDK (X9.24-3 §6.3.1)", + input: "FEDCBA9876543210F1F1F1F1F1F1F1F1", + expectedOutput: "1273671EA26AC29AFA4D1084127652A1", + recipeConfig: [ + { + op: "DUKPT Derive AES Key", + args: ["BDK", "Initial Key (IK)", "123456789012345600000001", "PIN Encryption", false] + } + ] + }, + { + name: "DUKPT Derive AES Key: PIN Encryption key, counter 1 (X9.24-3 §6.3.3)", + input: "FEDCBA9876543210F1F1F1F1F1F1F1F1", + expectedOutput: "AF8CB133A78F8DC2D1359F18527593FB", + recipeConfig: [ + { + op: "DUKPT Derive AES Key", + args: ["BDK", "Working Key", "123456789012345600000001", "PIN Encryption", false] + } + ] + }, + { + name: "DUKPT Derive AES Key: MAC Generation key, counter 1", + input: "FEDCBA9876543210F1F1F1F1F1F1F1F1", + expectedOutput: "A2DC23DE6FDE0824A2BC321E08E4B8B7", + recipeConfig: [ + { + op: "DUKPT Derive AES Key", + args: ["BDK", "Working Key", "123456789012345600000001", "MAC Generation", false] + } + ] + }, + { + name: "DUKPT Derive AES Key: Data Encryption key, counter 1", + input: "FEDCBA9876543210F1F1F1F1F1F1F1F1", + expectedOutput: "A35C412EFD41FDB98B69797C02DCD08F", + recipeConfig: [ + { + op: "DUKPT Derive AES Key", + args: ["BDK", "Working Key", "123456789012345600000001", "Data Encryption", false] + } + ] + }, + { + name: "DUKPT Derive AES Key: PIN Encryption key, counter 8", + input: "FEDCBA9876543210F1F1F1F1F1F1F1F1", + expectedOutput: "4D9DF3FBEE3448FC3E676D04320A90F5", + recipeConfig: [ + { + op: "DUKPT Derive AES Key", + args: ["BDK", "Working Key", "123456789012345600000008", "PIN Encryption", false] + } + ] + }, + { + name: "DUKPT Derive AES Key: PIN Encryption key, counter 131072 (0x20000, first skipped-bit counter)", + input: "FEDCBA9876543210F1F1F1F1F1F1F1F1", + expectedOutput: "AB828BE7B58C7EC5D5ED0D5D320A0C9D", + recipeConfig: [ + { + op: "DUKPT Derive AES Key", + args: ["BDK", "Working Key", "123456789012345600020000", "PIN Encryption", false] + } + ] + }, + { + name: "DUKPT Derive AES Key: PIN Encryption key, counter 8675309 (0x845FED, midrange)", + input: "FEDCBA9876543210F1F1F1F1F1F1F1F1", + expectedOutput: "D1DDA386AA4A556AF0119FDCB5D132C6", + recipeConfig: [ + { + op: "DUKPT Derive AES Key", + args: ["BDK", "Working Key", "1234567890123456 00845FED", "PIN Encryption", false] + } + ] + }, + { + name: "DUKPT Derive AES Key: working key from IK input, counter 1 PIN Encryption", + input: "1273671EA26AC29AFA4D1084127652A1", + expectedOutput: "AF8CB133A78F8DC2D1359F18527593FB", + recipeConfig: [ + { + op: "DUKPT Derive AES Key", + args: ["Initial Key (IK)", "Working Key", "123456789012345600000001", "PIN Encryption", false] + } + ] + }, { name: "DUKPT Derive TDES Key: known IPEK vector", input: "0123456789ABCDEFFEDCBA9876543210",