Fix AES DUKPT derivation data format; add X9.24-3 test vectors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
J8k3 2026-05-18 23:02:01 -04:00
parent e864259c1e
commit b724fc4b8c
2 changed files with 144 additions and 21 deletions

View File

@ -11,7 +11,7 @@ import { toHexFast } from "../lib/Hex.mjs";
// ── X9.24-3 key usage indicators (bytes 2-3 of derivation data) ─────────────── // ── X9.24-3 key usage indicators (bytes 2-3 of derivation data) ───────────────
const KEY_USAGE = { 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) Intermediate: 0x0000, // internal binary-tree node (not user-visible)
"PIN Encryption": 0x1000, "PIN Encryption": 0x1000,
"MAC Generation": 0x2000, // sender / request direction "MAC Generation": 0x2000, // sender / request direction
@ -165,15 +165,42 @@ function aesCmac(key16, message) {
// ── X9.24-3 AES-128 DUKPT derivation ───────────────────────────────────────── // ── 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: * Layout:
* [0-1] version = 0x0001 * [0] version = 0x01
* [1] key size class = 0x01 (AES-128)
* [2-3] key usage indicator * [2-3] key usage indicator
* [4-5] algorithm = 0x0002 (AES-128) * [4-5] algorithm = 0x0002 (AES-128)
* [6-7] key length = 0x0080 (128 bits) * [6-7] key length = 0x0080 (128 bits)
* [8-15] IKI (8 bytes, from KSN bytes 0-7) * [8-11] last 4 bytes of IKI (IKI[4..7])
* [16-19] counter register (4 bytes) * [12-15] counter register (4 bytes, big-endian)
* *
* @param {number} usage * @param {number} usage
* @param {Uint8Array} iki8 * @param {Uint8Array} iki8
@ -181,16 +208,17 @@ function aesCmac(key16, message) {
* @returns {Uint8Array} * @returns {Uint8Array}
*/ */
function derivationData(usage, iki8, counterReg) { function derivationData(usage, iki8, counterReg) {
const d = new Uint8Array(20); const d = new Uint8Array(16);
d[0] = 0x00; d[1] = 0x01; d[0] = 0x01; d[1] = 0x01;
d[2] = (usage >> 8) & 0xFF; d[3] = usage & 0xFF; d[2] = (usage >> 8) & 0xFF; d[3] = usage & 0xFF;
d[4] = (ALGO_CODE >> 8) & 0xFF; d[5] = ALGO_CODE & 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[6] = (KEY_LEN_VAL >> 8) & 0xFF; d[7] = KEY_LEN_VAL & 0xFF;
d.set(iki8, 8); // last 4 bytes of 8-byte IKI
d[16] = (counterReg >>> 24) & 0xFF; d[8] = iki8[4]; d[9] = iki8[5]; d[10] = iki8[6]; d[11] = iki8[7];
d[17] = (counterReg >>> 16) & 0xFF; d[12] = (counterReg >>> 24) & 0xFF;
d[18] = (counterReg >>> 8) & 0xFF; d[13] = (counterReg >>> 16) & 0xFF;
d[19] = counterReg & 0xFF; d[14] = (counterReg >>> 8) & 0xFF;
d[15] = counterReg & 0xFF;
return d; return d;
} }
@ -202,7 +230,7 @@ function derivationData(usage, iki8, counterReg) {
* @returns {Uint8Array} * @returns {Uint8Array}
*/ */
function deriveIK(bdk16, iki8) { 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. * 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} txKey16
* @param {Uint8Array} iki8 * @param {Uint8Array} iki8
@ -243,7 +272,7 @@ function deriveTransactionKey(ik16, iki8, counter) {
* @returns {Uint8Array} * @returns {Uint8Array}
*/ */
function deriveWorkingKey(txKey16, iki8, counter, purposeName) { 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 ─────────────────────────────────────────────────────────── // ── Operation class ───────────────────────────────────────────────────────────
@ -269,15 +298,17 @@ class DeriveDUKPTAESKey extends Operation {
"The <b>KSN</b> is 12 bytes: 8-byte Initial Key Identifier (IKI) + 4-byte transaction counter.", "The <b>KSN</b> 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).", "Only the low 21 bits of the counter are used for derivation (max 2,097,151 transactions per IK).",
"<br><br>", "<br><br>",
"<b>Derivation data format (X9.24-3, 20 bytes):</b>", "<b>Derivation data format (X9.24-3, 16 bytes — working keys):</b>",
"<pre>", "<pre>",
"[0-1] version = 0x0001\n", "[0] version = 0x01\n",
"[1] key size class = 0x01 (AES-128)\n",
"[2-3] key usage indicator\n", "[2-3] key usage indicator\n",
"[4-5] algorithm = 0x0002 (AES-128)\n", "[4-5] algorithm = 0x0002 (AES-128)\n",
"[6-7] key length = 0x0080 (128 bits)\n", "[6-7] key length = 0x0080 (128 bits)\n",
"[8-15] IKI (8 bytes from KSN)\n", "[8-11] last 4 bytes of IKI\n",
"[16-19] counter register (4 bytes)\n", "[12-15] transaction counter (4 bytes, full 32-bit value)\n",
"</pre>", "</pre>",
"IK derivation uses a separate 16-byte block with full 8-byte IKI at [8-15] and usage 0x8001.",
"<b>Key usage codes:</b> PIN Encryption=0x1000, MAC Generation=0x2000, ", "<b>Key usage codes:</b> PIN Encryption=0x1000, MAC Generation=0x2000, ",
"MAC Verification=0x2001, MAC Both Ways=0x2002, ", "MAC Verification=0x2001, MAC Both Ways=0x2002, ",
"Data Encryption=0x3000, Data Decryption=0x3001, Data Both Ways=0x3002.", "Data Encryption=0x3000, Data Decryption=0x3001, Data Both Ways=0x3002.",

View File

@ -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", name: "DUKPT Derive TDES Key: known IPEK vector",
input: "0123456789ABCDEFFEDCBA9876543210", input: "0123456789ABCDEFFEDCBA9876543210",