Last active
June 11, 2023 07:06
-
-
Save apowers313/09e274e52807044067eca346c8aa40b0 to your computer and use it in GitHub Desktop.
UAF TLV Decoder Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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