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:
parent
e864259c1e
commit
b724fc4b8c
@ -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
|
||||
* [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-15] IKI (8 bytes, from KSN bytes 0-7)
|
||||
* [16-19] counter register (4 bytes)
|
||||
* [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 <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).",
|
||||
"<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>",
|
||||
"[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",
|
||||
"</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, ",
|
||||
"MAC Verification=0x2001, MAC Both Ways=0x2002, ",
|
||||
"Data Encryption=0x3000, Data Decryption=0x3001, Data Both Ways=0x3002.",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user