Broaden ParseTR34B9Envelope to TR-34 key transport: message type table, error codes, ASN.1 envelope peek
This commit is contained in:
parent
cf3023bc82
commit
dd7a26aa1f
@ -6,58 +6,118 @@
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
|
||||
/**
|
||||
* Parses an ASN.1 TLV length field at the given offset.
|
||||
*
|
||||
* @param {Uint8Array} bytes
|
||||
* @param {number} offset
|
||||
* @returns {{headerLength: number, valueLength: number}}
|
||||
*/
|
||||
// ── TR-34 message type table ──────────────────────────────────────────────────
|
||||
|
||||
const TR34_MESSAGE_TYPES = {
|
||||
B0: "GetCredential — KRD requests KDH credentials",
|
||||
B1: "KeyCertificate — KDH distributes its certificate",
|
||||
B2: "GetData — KDH requests data from KRD",
|
||||
B3: "ReceiveCert — KRD acknowledges certificate receipt",
|
||||
B4: "BindBegin — KDH initiates key transport binding",
|
||||
B5: "BindRequest — KRD sends public key / OWHF to KDH",
|
||||
B6: "BindResponse — KDH sends certificate binding confirmation",
|
||||
B8: "KeyToken — encrypted key transport object (CMS EnvelopedData)",
|
||||
B9: "BindResponse — final key delivery; contains CMS EnvelopedData + signature",
|
||||
};
|
||||
|
||||
const TR34_ERROR_CODES = {
|
||||
"00": "Success",
|
||||
"01": "General failure",
|
||||
"02": "Invalid message format",
|
||||
"03": "Unsupported message type",
|
||||
"04": "Certificate not found",
|
||||
"05": "Invalid signature",
|
||||
"06": "Decryption failure",
|
||||
"07": "Invalid key usage",
|
||||
"08": "KDH not authorized",
|
||||
"09": "KRD not authorized",
|
||||
"10": "Message replay detected",
|
||||
"11": "Key already loaded",
|
||||
"12": "Invalid random number",
|
||||
"FF": "Unspecified error",
|
||||
};
|
||||
|
||||
// ── ASN.1 helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
function parseAsnLength(bytes, offset) {
|
||||
if (offset + 2 > bytes.length) {
|
||||
if (offset + 2 > bytes.length)
|
||||
throw new OperationError("Insufficient ASN.1 data.");
|
||||
}
|
||||
|
||||
const first = bytes[offset + 1];
|
||||
if ((first & 0x80) === 0) {
|
||||
if ((first & 0x80) === 0)
|
||||
return { headerLength: 2, valueLength: first };
|
||||
}
|
||||
|
||||
const lengthOfLength = first & 0x7f;
|
||||
if (offset + 2 + lengthOfLength > bytes.length) {
|
||||
if (offset + 2 + lengthOfLength > bytes.length)
|
||||
throw new OperationError("Invalid ASN.1 length field.");
|
||||
}
|
||||
|
||||
let valueLength = 0;
|
||||
for (let i = 0; i < lengthOfLength; i++) {
|
||||
for (let i = 0; i < lengthOfLength; i++)
|
||||
valueLength = (valueLength << 8) | bytes[offset + 2 + i];
|
||||
}
|
||||
|
||||
return { headerLength: 2 + lengthOfLength, valueLength };
|
||||
}
|
||||
|
||||
/** Best-effort parse of the outermost ASN.1 SEQUENCE tag in a byte array. */
|
||||
function peekAsnSequence(bytes) {
|
||||
if (!bytes || bytes.length < 2) return null;
|
||||
if (bytes[0] !== 0x30) return null; // not SEQUENCE
|
||||
try {
|
||||
const meta = parseAsnLength(bytes, 0);
|
||||
return {
|
||||
tag: "0x30 (SEQUENCE)",
|
||||
headerBytes: meta.headerLength,
|
||||
valueLength: meta.valueLength,
|
||||
totalExpected: meta.headerLength + meta.valueLength,
|
||||
complete: (meta.headerLength + meta.valueLength) === bytes.length,
|
||||
};
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function hexStr(bytes) {
|
||||
return Array.from(bytes, b => b.toString(16).padStart(2, "0").toUpperCase()).join("");
|
||||
}
|
||||
|
||||
// ── Operation ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse TR-34 B9 envelope operation
|
||||
* Parse TR-34 key transport message operation.
|
||||
*/
|
||||
class ParseTR34B9Envelope extends Operation {
|
||||
|
||||
/**
|
||||
* ParseTR34B9Envelope constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Parse TR-34 B9 envelope";
|
||||
this.name = "Parse TR-34 key transport";
|
||||
this.module = "Payment";
|
||||
this.description = "Paste the full B9 response frame into the input field as hex.<br><br><b>Input:</b> complete TR-34 B9 response encoded as hex, including the leading length field.<br><br>This operation splits the response into header, response code, authentication data, KCV, envelope data, signature length, signature, and any trailing bytes.";
|
||||
this.inlineHelp = "<strong>Input:</strong> full B9 response frame as hex, including the 2-byte length field.<br><strong>Args:</strong> none.";
|
||||
this.description = [
|
||||
"Parses a <b>TR-34</b> key transport message frame (hex input) and decodes each section.",
|
||||
"<br><br>",
|
||||
"<b>Input:</b> Complete TR-34 message frame encoded as hex, including the 2-byte length prefix.",
|
||||
"<br><br>",
|
||||
"TR-34 defines a family of messages (B0–B9) for transporting symmetric keys using RSA.",
|
||||
" This operation auto-detects the message type and labels each field accordingly.",
|
||||
" The <b>Envelope Data</b> section is a CMS <code>EnvelopedData</code> (ASN.1 SEQUENCE) —",
|
||||
" the outer tag and length are shown; full CMS parsing is not performed here.",
|
||||
"<br><br>",
|
||||
"<b>Key transport flow:</b> KDH (Key Distribution Host) wraps a symmetric key under the",
|
||||
" KRD's RSA public key and signs the result. KRD verifies the signature, then decrypts.",
|
||||
"<br><br>",
|
||||
"<b>References:</b> ANS X9.143, ANSI TR-34, PCI PIN v3.1 Req 18-4.",
|
||||
].join("");
|
||||
|
||||
this.inlineHelp = "<strong>Input:</strong> full TR-34 message frame as hex, including the 2-byte length prefix.";
|
||||
|
||||
this.testDataSamples = [
|
||||
{
|
||||
name: "Synthetic B9 parser sample",
|
||||
input: "001730303030423930303100112233300030303034AABBCCDD",
|
||||
args: []
|
||||
}
|
||||
args: [],
|
||||
},
|
||||
];
|
||||
|
||||
this.infoURL = "https://en.wikipedia.org/wiki/Key_block";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
@ -70,69 +130,91 @@ class ParseTR34B9Envelope extends Operation {
|
||||
*/
|
||||
run(input) {
|
||||
const hex = (input || "").replace(/\s+/g, "");
|
||||
if (!hex.length) {
|
||||
throw new OperationError("No input.");
|
||||
}
|
||||
if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) {
|
||||
|
||||
if (!hex.length) throw new OperationError("No input.");
|
||||
if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
||||
throw new OperationError("Input must be hex.");
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(hex.match(/.{2}/g).map(h => parseInt(h, 16)));
|
||||
if (bytes.length < 12) {
|
||||
throw new OperationError("Input too short.");
|
||||
}
|
||||
if (bytes.length < 12) throw new OperationError("Input too short for a TR-34 frame.");
|
||||
|
||||
const notes = [];
|
||||
|
||||
// ── Outer frame ─────────────────────────────────────────────────────
|
||||
const declaredLength = (bytes[0] << 8) | bytes[1];
|
||||
let offset = 2;
|
||||
|
||||
const header = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
||||
offset += 4;
|
||||
const header = String.fromCharCode(...bytes.slice(offset, offset + 4)); offset += 4;
|
||||
const responseType = String.fromCharCode(...bytes.slice(offset, offset + 2)); offset += 2;
|
||||
const errorCode = String.fromCharCode(...bytes.slice(offset, offset + 2)); offset += 2;
|
||||
|
||||
const responseType = String.fromCharCode(...bytes.slice(offset, offset + 2));
|
||||
offset += 2;
|
||||
const msgDesc = TR34_MESSAGE_TYPES[responseType] || "Unknown message type";
|
||||
const errDesc = TR34_ERROR_CODES[errorCode] || "Unknown error code";
|
||||
|
||||
const errorCode = String.fromCharCode(...bytes.slice(offset, offset + 2));
|
||||
offset += 2;
|
||||
if (errorCode !== "00") {
|
||||
notes.push(`Non-zero error code: ${errorCode} — ${errDesc}`);
|
||||
}
|
||||
|
||||
// ── Authentication data (ASN.1 variable length) ──────────────────────
|
||||
const authLenMeta = parseAsnLength(bytes, offset);
|
||||
const authTotalLen = authLenMeta.headerLength + authLenMeta.valueLength;
|
||||
const authData = bytes.slice(offset, offset + authTotalLen);
|
||||
const authAsn = peekAsnSequence(authData);
|
||||
offset += authTotalLen;
|
||||
|
||||
const kcv = bytes.slice(offset, offset + 3);
|
||||
offset += 3;
|
||||
// ── KCV (3 bytes) ────────────────────────────────────────────────────
|
||||
const kcv = bytes.slice(offset, offset + 3); offset += 3;
|
||||
|
||||
// ── Envelope data (CMS EnvelopedData, ASN.1 variable length) ─────────
|
||||
const envLenMeta = parseAsnLength(bytes, offset);
|
||||
const envTotalLen = envLenMeta.headerLength + envLenMeta.valueLength;
|
||||
const envelopeData = bytes.slice(offset, offset + envTotalLen);
|
||||
const envAsn = peekAsnSequence(envelopeData);
|
||||
offset += envTotalLen;
|
||||
|
||||
const signatureLengthAscii = String.fromCharCode(...bytes.slice(offset, offset + 4));
|
||||
offset += 4;
|
||||
const signatureLength = parseInt(signatureLengthAscii, 10);
|
||||
const signature = Number.isFinite(signatureLength) ? bytes.slice(offset, offset + signatureLength) : new Uint8Array();
|
||||
if (Number.isFinite(signatureLength)) {
|
||||
offset += signatureLength;
|
||||
}
|
||||
// ── Signature length (4-byte ASCII decimal) ──────────────────────────
|
||||
const sigLenAscii = String.fromCharCode(...bytes.slice(offset, offset + 4)); offset += 4;
|
||||
const sigLength = parseInt(sigLenAscii, 10);
|
||||
const signature = Number.isFinite(sigLength) ? bytes.slice(offset, offset + sigLength) : new Uint8Array();
|
||||
if (Number.isFinite(sigLength)) offset += sigLength;
|
||||
|
||||
// ── Build output ─────────────────────────────────────────────────────
|
||||
const out = {
|
||||
declaredLength,
|
||||
actualLengthExcludingLengthField: bytes.length - 2,
|
||||
header,
|
||||
responseType,
|
||||
messageType: responseType,
|
||||
messageDescription: msgDesc,
|
||||
errorCode,
|
||||
authDataHex: Buffer.from(authData).toString("hex").toUpperCase(),
|
||||
kcvHex: Buffer.from(kcv).toString("hex").toUpperCase(),
|
||||
envelopeDataHex: Buffer.from(envelopeData).toString("hex").toUpperCase(),
|
||||
signatureLengthAscii,
|
||||
signatureLength: Number.isFinite(signatureLength) ? signatureLength : null,
|
||||
signatureHex: Buffer.from(signature).toString("hex").toUpperCase(),
|
||||
trailingHex: Buffer.from(bytes.slice(offset)).toString("hex").toUpperCase()
|
||||
errorDescription: errDesc,
|
||||
authData: {
|
||||
hex: hexStr(authData),
|
||||
byteCount: authData.length,
|
||||
asnOuter: authAsn,
|
||||
},
|
||||
kcvHex: hexStr(kcv),
|
||||
envelopeData: {
|
||||
hex: hexStr(envelopeData),
|
||||
byteCount: envelopeData.length,
|
||||
description: "CMS EnvelopedData — wrapped symmetric key (decrypt with KRD private RSA key)",
|
||||
asnOuter: envAsn,
|
||||
},
|
||||
signatureLengthAscii: sigLenAscii,
|
||||
signatureLength: Number.isFinite(sigLength) ? sigLength : null,
|
||||
signatureHex: hexStr(signature),
|
||||
trailingHex: hexStr(bytes.slice(offset)),
|
||||
notes,
|
||||
};
|
||||
|
||||
if (out.declaredLength !== bytes.length - 2) {
|
||||
out.notes.push(
|
||||
`Declared length ${out.declaredLength} does not match ` +
|
||||
`actual payload length ${bytes.length - 2}.`
|
||||
);
|
||||
}
|
||||
|
||||
return JSON.stringify(out, null, 4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ParseTR34B9Envelope;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user