Skip to content

Instantly share code, notes, and snippets.

@apowers313
Last active June 11, 2023 07:06
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save apowers313/09e274e52807044067eca346c8aa40b0 to your computer and use it in GitHub Desktop.
Save apowers313/09e274e52807044067eca346c8aa40b0 to your computer and use it in GitHub Desktop.
UAF TLV Decoder Example
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
// Use a lookup table to find the index.
var lookup = new Uint8Array(256);
for (var i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
// stolen from:
// https://github.com/niklasvh/base64-arraybuffer/blob/master/lib/base64-arraybuffer.js
// modified to base64url by Yuriy :)
function decode(base64) {
var bufferLength = base64.length * 0.75,
len = base64.length,
i, p = 0,
encoded1, encoded2, encoded3, encoded4;
if (base64[base64.length - 1] === "=") {
bufferLength--;
if (base64[base64.length - 2] === "=") {
bufferLength--;
}
}
var arraybuffer = new ArrayBuffer(bufferLength),
bytes = new Uint8Array(arraybuffer);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)];
encoded2 = lookup[base64.charCodeAt(i + 1)];
encoded3 = lookup[base64.charCodeAt(i + 2)];
encoded4 = lookup[base64.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return arraybuffer;
}
function printHex(msg, buf) {
// if the buffer was a TypedArray (e.g. Uint8Array), grab its buffer and use that
if (ArrayBuffer.isView(buf) && buf.buffer instanceof ArrayBuffer) {
buf = buf.buffer;
}
// check the arguments
if ((typeof msg != "string") ||
(typeof buf != "object")) {
console.log("Bad args to printHex");
return;
}
if (!(buf instanceof ArrayBuffer)) {
console.log("Attempted printHex with non-ArrayBuffer:", buf);
return;
}
// print the buffer as a 16 byte long hex string
var arr = new Uint8Array(buf);
var len = buf.byteLength;
var i, str = "";
console.log(msg);
for (i = 0; i < len; i++) {
var hexch = arr[i].toString(16);
hexch = (hexch.length == 1) ? ("0" + hexch) : hexch;
str += hexch.toUpperCase() + " ";
if (i && !((i + 1) % 16)) {
console.log(str);
str = "";
}
}
// print the remaining bytes
if ((i) % 16) {
console.log(str);
}
}
var uafThingy = [{
"assertions": [{
"assertionScheme": "UAFV1TLV",
"assertion": "AT4TBAM-ywALLgkAODA4NiM1MDE2Di4HAAABAQIAAQEKLiAAyhcSKymHB0XKDW24w-JwL6Bi5VhIeXAaxwLsnKspLI8JLiAA6_Iy-fYu6ZGzXPMcrNUcAGQPQVrljGsgi0aTNvNFGNUNLggADAAAAA8AAAAMLlsAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbe5AVrIzVoXZnEv9xfc_O-SiyA3qIoepR98IWImcpSCAhELgIMf9Td3H_uQFgAjsAHGGMSIXzp5DnhtCqKrk7Qc-QAMGLkcAMEUCIQDbmSd3w1smcsOnShtl-ygCWbfwogkuT_ExoEMDpSD4kwIgelhGj9Vzmk-G8G7Yy2wizbclirt5uy_KdwAXV0oopUgFLvECMIIC7TCCAdWgAwIBAgICENQwDQYJKoZIhvcNAQELBQAwVDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExDjAMBgNVBAoMBUludGVsMSAwHgYDVQQDDBdQYXltZW50IFByb2plY3QgVGVzdCBDQTAeFw0xNjAyMDgxNzQ3MjVaFw0xODAyMDcxNzQ3MjVaMGwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExDjAMBgNVBAoMBUludGVsMQwwCgYDVQQLDANTU0cxHDAaBgNVBAMME0FBRSBUZXN0IEVDRFNBIENlcnQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT0mupJAfo2ZB6GEppbhllAvn-f4g2wWc8ujNOUyayMyEBDSP-iGUZGFIgYfISwCC1JxilXN509S7O0cX95TzCeo3wwejAJBgNVHRMEAjAAMB0GA1UdDgQWBBTO6UVCsHHm_3cewtekuatVFM38rjAfBgNVHSMEGDAWgBSKY1RPVFVXtk3d8JjvFGkeN8KL7jAOBgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMA0GCSqGSIb3DQEBCwUAA4IBAQAwfGfyW2EQ1xGHbXln_PYNKiR9UMikMJ_NEiM_D3dNFv7wOxWbryHT5AVnOKqis_HSMBf2aROWE5NgZpPT3r8RAaxhbWkHmJG50iQZBpj9L3m6XHzmXy-tDPBABuQw8q6x6VnoEFuGljRgRN0Jt6Cx0sSYXnjY1D5GJ0neQXelFUdB6FA58S-jig_kUXI4kAJ0kXFpfU7pVV6l84SHfaBfMcYsp9rxxsk3_MEVa8OyJeIQJQ3r02np4DXhVsih0alzYhkSAliOSifJCpjkTukzi5SiWCY-7xuY9WznbuT6zwAQS8MZaEzzqiGxTz1ZHEZyxDAXhEZwaf7i-tMjM6JL"
}],
"header": {
"upv": {
"major": 1,
"minor": 0
},
"op": "Reg",
"appID": "https://192.168.1.103:8080"
},
"fcParams": "eyJhcHBJRCI6Imh0dHBzOi8vMTkyLjE2OC4xLjEwMzo4MDgwIiwiY2hhbGxlbmdlIjoiSVI5Y2VsT29VdjFCdUVzVGhkd05WUXYiLCJmYWNldElEIjoiaHR0cHM6Ly8xOTIuMTY4LjEuMTAzOjgwODAiLCJjaGFubmVsQmluZGluZyI6e319"
}];
// var thing2 = [{
// "assertions": [{
// "assertion": "AT5IAQM-_AALLgkARDQwOSMwOTAxDi4HAAEAAQEAAAEKLiAA-PuqiaEkPiZYMWltcBEaQjJ2sn5ozCfoHNnzlZRAvngJLiQARDYxMkE5QjYtQjU5OS00MzUzLTg5QzItRjMwMjQzOTUzRDhFDS4IAAAAAAAAAAAADC5BAATOEmN5XMKnxzyZ9pe4QwMAdHHp9TnTqeDgvsWDakirV5PEttcN5UGycBDv9ZbLApElgRPktVV6r_jfkmTseLwfEj5DABMuGwBjb20uZGFvbi5zZGsuZXh0ZW5zaW9uc0hhc2gULiAAbjQLnP-zepicpUTmu3gKLHiQHT-zNzh2hRGjBhevoB0IPkQABi5AAIW9Tw-vcb8vau8i0Zd1HTQ-CcghdY73DJQ9jQCzceuTe8IXyOjBZeyEhrOLkFPZ9OhEZbJRmUy-4kZ4cH31Vb0=",
// "assertionScheme": "UAFV1TLV"
// }],
// "header": {
// "op": "Reg",
// "appID": "https://api-rp.identityx-cloud.com/jbtest2/facets",
// "upv": {
// "major": 1,
// "minor": 0
// }
// },
// "fcParams": "eyJmYWNldElEIjoiaW9zOmJ1bmRsZS1pZDpjb20uZGFvbi5EYW9uRmlkb1NhbXBsZVJwQXBwIiwiY2hhbm5lbEJpbmRpbmciOnt9LCJhcHBJRCI6Imh0dHBzOi8vYXBpLXJwLmlkZW50aXR5eC1jbG91ZC5jb20vamJ0ZXN0Mi9mYWNldHMiLCJjaGFsbGVuZ2UiOiJJaVNtYVAybFJqOWtKeVphNVFzMFh3In0="
// }];
var authAlgList = new Map([
[0x01, 'UAF_ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW'],
[0x02, 'UAF_ALG_SIGN_SECP256R1_ECDSA_SHA256_DER'],
[0x03, 'UAF_ALG_SIGN_RSASSA_PSS_SHA256_RAW'],
[0x04, 'UAF_ALG_SIGN_RSASSA_PSS_SHA256_DER'],
[0x05, 'UAF_ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW'],
[0x06, 'UAF_ALG_SIGN_SECP256K1_ECDSA_SHA256_DER'],
]);
var pkFormatList = new Map([
[0x100, 'UAF_ALG_KEY_ECC_X962_RAW'],
[0x101, 'UAF_ALG_KEY_ECC_X962_DER'],
[0x102, 'UAF_ALG_KEY_RSA_2048_PSS_RAW'],
[0x103, 'UAF_ALG_KEY_RSA_2048_PSS_DER'],
]);
// Tag values are defined at:
// https://fidoalliance.org/specs/fido-uaf-v1.0-ps-20141208/fido-uaf-reg-v1.0-ps-20141208.html#predefined-tags
var tagList = new Map([
[0x3E01, "TAG_UAFV1_REG_ASSERTION"],
[0x3E02, "TAG_UAFV1_AUTH_ASSERTION"],
[0x3E03, "TAG_UAFV1_KRD"],
[0x3E04, "TAG_UAFV1_SIGNED_DATA"],
[0x2E05, "TAG_ATTESTATION_CERT"],
[0x2E06, "TAG_SIGNATURE"],
[0x3E07, "TAG_ATTESTATION_BASIC_FULL"],
[0x3E08, "TAG_ATTESTATION_BASIC_SURROGATE"],
[0x2E09, "TAG_KEYID"],
[0x2E0A, "TAG_FINAL_CHALLENGE"],
[0x2E0B, "TAG_AAID"],
[0x2E0C, "TAG_PUB_KEY"],
[0x2E0D, "TAG_COUNTERS"],
[0x2E0E, "TAG_ASSERTION_INFO"],
[0x2E0F, "TAG_AUTHENTICATOR_NONCE"],
[0x2E10, "TAG_TRANSACTION_CONTENT_HASH"],
[0x3E11, "TAG_EXTENSION1"],
[0x3E12, "TAG_EXTENSION2"],
[0x2E13, "TAG_EXTENSION_ID"],
[0x2E14, "TAG_EXTENSION_DATA"],
["TAG_UAFV1_REG_ASSERTION", 0x3E01],
["TAG_UAFV1_AUTH_ASSERTION", 0x3E02],
["TAG_UAFV1_KRD", 0x3E03],
["TAG_UAFV1_SIGNED_DATA", 0x3E04],
["TAG_ATTESTATION_CERT", 0x2E05],
["TAG_SIGNATURE", 0x2E06],
["TAG_ATTESTATION_BASIC_FULL", 0x3E07],
["TAG_ATTESTATION_BASIC_SURROGATE", 0x3E08],
["TAG_KEYID", 0x2E09],
["TAG_FINAL_CHALLENGE", 0x2E0A],
["TAG_AAID", 0x2E0B],
["TAG_PUB_KEY", 0x2E0C],
["TAG_COUNTERS", 0x2E0D],
["TAG_ASSERTION_INFO", 0x2E0E],
["TAG_AUTHENTICATOR_NONCE", 0x2E0F],
["TAG_TRANSACTION_CONTENT_HASH", 0x2E10],
["TAG_EXTENSION1", 0x3E11],
["TAG_EXTENSION2", 0x3E12],
["TAG_EXTENSION_ID", 0x2E13],
["TAG_EXTENSION_DATA", 0x2E14],
]);
function tagToString(tag) {
return tagList.get(tag);
}
function printTag(msg, tag) {
console.log(`${msg}: 0x${tag.toString(16).toUpperCase()} (${tagToString (tag)})`);
}
// decode the TLV assertion to an ArrayBuffer
var bufThing = decode(uafThingy[0].assertions[0].assertion);
console.log(`Got assertion, ${bufThing.byteLength} bytes`);
// printHex ("decoded", buf);
// Decoding registration assertion, based on:
// https://fidoalliance.org/specs/fido-uaf-v1.0-ps-20141208/fido-uaf-authnr-cmds-v1.0-ps-20141208.html#tag_uafv1_reg_assertion
parseRegAssn (bufThing);
function parseRegAssn(buf) {
var view = new DataView(buf);
var offset = 0;
// assertion type = TAG_UAFV1_REG_ASSERTION (2)
var assnType = view.getUint16(offset, true);
printTag("Assertion Type", assnType);
offset += 2;
// Length of the structure (2)
var assnLen = view.getUint16(offset, true);
console.log("Assertion Length:", assnLen);
offset += 2;
// TAG_UAFV1_KRD (2)
var krd = view.getUint16(offset, true);
printTag("KRD", krd);
offset += 2;
// Length of AAID (2)
var krdLen = view.getUint16(offset, true);
console.log("KRD Length:", krdLen);
offset += 2;
// TAG_AAID (2)
var aaidTag = view.getUint16(offset, true);
printTag("AAID Tag", aaidTag);
offset += 2;
// AAID len (2)
var aaidLen = view.getUint16(offset, true);
console.log("AAID Length:", aaidLen);
offset += 2;
// AAID (n)
var aaid = view.buffer.slice (offset, offset + aaidLen);
printHex ("AAID:", aaid);
offset += aaidLen;
// TAG_ASSERTION_INFO (2)
var assnInfo = view.getUint16(offset, true);
printTag("Assertion Info", assnInfo);
offset += 2;
// Length of Assertion Information (2)
var assnInfoLen = view.getUint16(offset, true);
console.log("Assn Info Length:", assnInfoLen);
offset += 2;
// Vendor assigned authenticator version (2)
var vendorAuthnVersion = view.getUint16(offset, true);
console.log("Authenticator Version: 0x" + vendorAuthnVersion.toString(16).toUpperCase());
offset += 2;
// For Registration this must be 0x01 indicating that the user has explicitly verified the action (1)
var verifiedAction = view.getUint8(offset);
console.log("Verified Action: 0x" + verifiedAction.toString(16).toUpperCase());
offset += 1;
// Signature Algorithm and Encoding of the attestation signature (2)
var sigAlg = view.getUint16(offset, true);
console.log(`Signature Algorithm: 0x${sigAlg.toString(16).toUpperCase()} (${authAlgList.get(sigAlg)})`); // TODO: translate
offset += 2;
// Public Key algorithm and encoding of the newly generated UAuth.pub key (2)
var pkAlg = view.getUint16(offset, true);
console.log(`Public Key Algorithm: 0x${pkAlg.toString(16).toUpperCase()} (${pkFormatList.get(pkAlg)})`); // TODO: translate
offset += 2;
// TAG_FINAL_CHALLENGE (2)
var fcTag = view.getUint16(offset, true);
printTag("Final Challenge", fcTag);
offset += 2;
// Final Challenge length (2)
var fcLen = view.getUint16(offset, true);
console.log("Final Challenge Length:", fcLen);
offset += 2;
// (binary value of) Final Challenge provided in the Command (n)
var fc = view.buffer.slice (offset, offset + fcLen);
printHex ("Final Challenge:", fc);
offset += fcLen;
// TAG_KEYID (2)
var keyidTag = view.getUint16(offset, true);
printTag("Key ID Tag", keyidTag);
offset += 2;
// Length of KeyID (2)
var keyidLen = view.getUint16(offset, true);
console.log("Key ID Length:", keyidLen);
offset += 2;
// (binary value of) KeyID generated by Authenticator (n)
var keyid = view.buffer.slice (offset, offset + keyidLen);
printHex ("Key ID:", keyid);
offset += keyidLen;
// TAG_COUNTERS (2)
var countersTag = view.getUint16(offset, true);
printTag("Counters Tag", countersTag);
offset += 2;
// Length of Counters (2)
var countersLen = view.getUint16(offset, true);
console.log("Counters Length:", countersLen);
offset += 2;
// Signature Counter (4)
var sigCounter = view.getUint32(offset, true);
console.log("Signature Counter: 0x" + sigCounter.toString(16).toUpperCase());
offset += 4;
// Registration Counter (4)
var regCounter = view.getUint32(offset, true);
console.log("Registration Counter: 0x" + regCounter.toString(16).toUpperCase());
offset += 4;
// TAG_PUB_KEY (2)
var pkTag = view.getUint16(offset, true);
printTag("Public Key Tag", pkTag);
offset += 2;
// Length of UAuth.pub (2)
var pkLen = view.getUint16(offset, true);
console.log("Public Key Length:", pkLen);
offset += 2;
// User authentication public key (UAuth.pub) newly generated by authenticator (n)
var pk = view.buffer.slice (offset, offset + pkLen);
printHex ("Key ID:", pk);
offset += pkLen;
// TAG_ATTESTATION_BASIC_FULL (2) (choice 1)
var attestationTag = view.getUint16(offset, true);
printTag("Attestation Tag", attestationTag);
offset += 2;
// Length of structure (2)
var attestationLen = view.getUint16(offset, true);
console.log("Attestation Length:", attestationLen);
offset += 2;
var attestation = view.buffer.slice (offset, offset + attestationLen);
if (attestationTag === tagList.get("TAG_ATTESTATION_BASIC_FULL")) {
parseBasicFullAttestation (attestation);
} else if (attestationTag === tagList.get("TAG_ATTESTATION_BASIC_SURROGATE")) {
parseSurrogateAttestation (attestation);
}
}
function parseBasicFullAttestation (buf) {
var view = new DataView(buf);
var offset = 0;
// TAG_ATTESTATION_BASIC_FULL (2) (choice 1) -- PARSED ABOVE
// Length of structure (2) -- PARSED ABOVE
// TAG_SIGNATURE (2)
var sigTag = view.getUint16(offset, true);
printTag("Signature Tag", sigTag);
offset += 2;
// Length of signature (2)
var sigLen = view.getUint16(offset, true);
console.log("Signature Length:", sigLen);
offset += 2;
// Signature calculated with Basic Attestation Private Key over TAG_UAFV1_KRD content (n)
var signature = view.buffer.slice (offset, offset + sigLen);
printHex ("Signature:", signature);
offset += sigLen;
// TAG_ATTESTATION_CERT (multiple occurrences possible) (2)
var attestationCertTag = view.getUint16(offset, true);
printTag("Attestation Certificate Tag", attestationCertTag);
offset += 2;
// Length of Attestation Cert (2)
var attCertLen = view.getUint16(offset, true);
console.log("Attestation Certificate Length:", attCertLen);
offset += 2;
// Single X.509 DER-encoded [ITU-X690-2008] Attestation Certificate or a single certificate from the attestation certificate chain (n)
var cert = view.buffer.slice (offset, offset + attCertLen);
printHex ("Cert:", cert);
offset += attCertLen;
}
function parseSurrogateAttestation (buf) {
var view = new DataView(buf);
var offset = 0;
throw new TypeError ("parseSurrogateAttestation not tested -- remove this at your own peril");
// TAG_ATTESTATION_BASIC_SURROGATE (2) (choice 2) -- PARSED ABOVE
// Length of structure (2) -- PARSED ABOVE
// TAG_SIGNATURE (2)
var sigTag = view.getUint16(offset, true);
printTag("Signature Tag", sigTag);
offset += 2;
// Length of signature (2)
var sigLen = view.getUint16(offset, true);
console.log("Signature Length:", sigLen);
offset += 2;
// Signature calculated with newly generated UAuth.priv key over TAG_UAFV1_KRD content (n)
var signature = view.buffer.slice (offset, offset + sigLen);
printHex ("Signature:", signature);
offset += sigLen;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment