-- started based on https://gist.github.com/z4yx/218116240e2759759b239d16fed787ca cbor = Dissector.get("cbor") iso7816 = Dissector.get("iso7816") ctaphid_proto = Proto("CTAPHID","FIDO Client to Authenticator Protocol over USB HID") ctaphidfield_cid = ProtoField.uint32("ctaphid.cid", "Channel ID", base.HEX) ctaphidfield_cmd = ProtoField.uint8("ctaphid.cmd", "Command", base.HEX) ctaphidfield_bcnt = ProtoField.uint16("ctaphid.bcnt", "Payload Length", base.DEC_HEX) ctaphidfield_seq = ProtoField.uint8("ctaphid.seq", "Packet Sequence", base.HEX) ctaphidfield_data = ProtoField.bytes("ctaphid.data", "Data") ctaphid_proto.fields = { ctaphidfield_cid, ctaphidfield_cmd, ctaphidfield_bcnt, ctaphidfield_seq, ctaphidfield_data } u2f_proto = Proto("u2f","FIDO CTAP1/U2F Protocol") u2ffield_cla = ProtoField.uint8("u2f.request.cla", "Class", base.HEX) u2ffield_ins = ProtoField.uint8("u2f.request.ins", "U2F command code", base.HEX) u2ffield_p1 = ProtoField.uint8("u2f.request.p1", "U2F command parameter 1", base.HEX) u2ffield_p2 = ProtoField.uint8("u2f.request.p2", "U2F command parameter 2", base.HEX) u2ffield_reqlen = ProtoField.uint24("u2f.request.lc", "U2F request data length", base.HEX) u2ffield_expreslen = ProtoField.uint24("u2f.request.le", "U2F expected response data length", base.HEX) u2ffield_reqdata = ProtoField.bytes("u2f.request.data", "U2F request data") u2ffield_status = ProtoField.uint16("u2f.response.status", "U2F response status", base.HEX) u2ffield_respdata = ProtoField.bytes("u2f.response.data", "U2F response data") u2f_proto.fields = { u2ffield_cla, u2ffield_ins, u2ffield_p1, u2ffield_p2, u2ffield_reqlen, u2ffield_reqdata, u2ffield_expreslen, u2ffield_status, u2ffield_respdata } -- Field Extractor field_usb_bus = Field.new("usb.bus_id") field_usb_device = Field.new("usb.device_address") field_usb_endpoint = Field.new("usb.endpoint_address") field_usb_endpointdir = Field.new("usb.endpoint_address.direction") field_usb_datalen = Field.new("usb.data_len") field_iso7816_ins = Field.new("iso7816.apdu.ins") field_iso7816_p1 = Field.new("iso7816.apdu.p1") field_iso7816_p2 = Field.new("iso7816.apdu.p2") field_iso7816_sw1 = Field.new("iso7816.apdu.sw1") field_iso7816_sw2 = Field.new("iso7816.apdu.sw2") field_iso7816_lc = Field.new("iso7816.apdu.lc") field_iso7816_le = Field.new("iso7816.apdu.le") field_iso7816_data = Field.new("iso7816.application_data") CTAPHID_COMMANDS = { CTAPHID_MSG = 0x03, CTAPHID_CBOR = 0x10, CTAPHID_INIT = 0x06, CTAPHID_PING = 0x01, CTAPHID_CANCEL = 0x11, CTAPHID_ERROR = 0x3F, CTAPHID_KEEPALIVE = 0x3B, CTAPHID_WINK = 0x08, CTAPHID_LOCK = 0x04, CTAPHID_VENDOR_FIRST = 0x40, CTAPHID_VENDOR_LAST = 0x7F } CTAPHID_COMMAND_STRINGS = { [0x03] = 'CTAPHID_MSG', [0x10] = 'CTAPHID_CBOR', [0x06] = 'CTAPHID_INIT', [0x01] = 'CTAPHID_PING', [0x11] = 'CTAPHID_CANCEL', [0x3F] = 'CTAPHID_ERROR', [0x3B] = 'CTAPHID_KEEPALIVE', [0x08] = 'CTAPHID_WINK', [0x04] = 'CTAPHID_LOCK', [0x40] = 'VENDOR_FIRST', [0x7F] = 'VENDOR_LAST', } U2F_INS_STRINGS = { [0x01] = 'U2F_REGISTER', [0x02] = 'U2F_AUTHENTICATE', [0x03] = 'U2F_VERSION', [0x40] = 'VENDOR_FIRST', [0xBF] = 'VENDOR_LAST' } U2F_P1_STRINGS = { [0x03] = 'ENFORCE-USER-PRESENCE-AND-SIGN', [0x07] = 'CHECK-ONLY', [0x08] = 'DONT-ENFORCE-USER-PRESENCE-AND-SIGN', } U2F_STATUS_STRINGS = { [0x9000] = 'SW_NO_ERROR', [0x6985] = 'SW_CONDITIONS_NOT_SATISFIED', [0x6A80] = 'SW_WRONG_DATA', [0x6700] = 'SW_WRONG_LENGTH', [0x6E00] = 'SW_CLA_NOT_SUPPORTED', [0x6D00] = 'SW_INS_NOT_SUPPORTED' } CTAP_COMMAND_CODE = { [0x01]='authenticatorMakeCredential', [0x02]='authenticatorGetAssertion', [0x04]='authenticatorGetInfo', [0x06]='authenticatorClientPIN', [0x07]='authenticatorReset', [0x08]='authenticatorGetNextAssertion', [0x40]='authenticatorVendorFirst', [0xBF]='authenticatorVendorLast' } CTAP_RESPONSE_CODE = { [0x00]='CTAP1_ERR_SUCCESS', [0x01]='CTAP1_ERR_INVALID_COMMAND', [0x02]='CTAP1_ERR_INVALID_PARAMETER', [0x03]='CTAP1_ERR_INVALID_LENGTH', [0x04]='CTAP1_ERR_INVALID_SEQ', [0x05]='CTAP1_ERR_TIMEOUT', [0x06]='CTAP1_ERR_CHANNEL_BUSY', [0x0A]='CTAP1_ERR_LOCK_REQUIRED', [0x0B]='CTAP1_ERR_INVALID_CHANNEL', [0x11]='CTAP2_ERR_CBOR_UNEXPECTED_TYPE', [0x12]='CTAP2_ERR_INVALID_CBOR', [0x14]='CTAP2_ERR_MISSING_PARAMETER', [0x15]='CTAP2_ERR_LIMIT_EXCEEDED', [0x16]='CTAP2_ERR_UNSUPPORTED_EXTENSION', [0x19]='CTAP2_ERR_CREDENTIAL_EXCLUDED', [0x21]='CTAP2_ERR_PROCESSING', [0x22]='CTAP2_ERR_INVALID_CREDENTIAL', [0x23]='CTAP2_ERR_USER_ACTION_PENDING', [0x24]='CTAP2_ERR_OPERATION_PENDING', [0x25]='CTAP2_ERR_NO_OPERATIONS', [0x26]='CTAP2_ERR_UNSUPPORTED_ALGORITHM', [0x27]='CTAP2_ERR_OPERATION_DENIED', [0x28]='CTAP2_ERR_KEY_STORE_FULL', [0x29]='CTAP2_ERR_NOT_BUSY', [0x2A]='CTAP2_ERR_NO_OPERATION_PENDING', [0x2B]='CTAP2_ERR_UNSUPPORTED_OPTION', [0x2C]='CTAP2_ERR_INVALID_OPTION', [0x2D]='CTAP2_ERR_KEEPALIVE_CANCEL', [0x2E]='CTAP2_ERR_NO_CREDENTIALS', [0x2F]='CTAP2_ERR_USER_ACTION_TIMEOUT', [0x30]='CTAP2_ERR_NOT_ALLOWED', [0x31]='CTAP2_ERR_PIN_INVALID', [0x32]='CTAP2_ERR_PIN_BLOCKED', [0x33]='CTAP2_ERR_PIN_AUTH_INVALID', [0x34]='CTAP2_ERR_PIN_AUTH_BLOCKED', [0x35]='CTAP2_ERR_PIN_NOT_SET', [0x36]='CTAP2_ERR_PIN_REQUIRED', [0x37]='CTAP2_ERR_PIN_POLICY_VIOLATION', [0x38]='CTAP2_ERR_PIN_TOKEN_EXPIRED', [0x39]='CTAP2_ERR_REQUEST_TOO_LARGE', [0x3A]='CTAP2_ERR_ACTION_TIMEOUT', [0x3B]='CTAP2_ERR_UP_REQUIRED', [0x7F]='CTAP1_ERR_OTHER', [0xDF]='CTAP2_ERR_SPEC_LAST', [0xE0]='CTAP2_ERR_EXTENSION_FIRST', [0xEF]='CTAP2_ERR_EXTENSION_LAST', [0xF0]='CTAP2_ERR_VENDOR_FIRST', [0xFF]='CTAP2_ERR_VENDOR_LAST' } function dissect_ctaphid_payload(cmd, buffer, pinfo, tree) if buffer:len() == 0 then return end -- && usb.function == 0x0008 && select correct endpoint/etc. if cmd == CTAPHID_COMMANDS.CTAPHID_MSG then local isotree = tree:add("iso") iso7816:call(buffer, pinfo, isotree) isotree.hidden = true -- print(field_iso7816_ins().value, field_iso7816_p1().value, field_iso7816_p2().value) Dissector.get("u2f"):call(buffer, pinfo, tree) -- pinfo.cols.protocol = u2f_proto.name -- local subtree = tree:add(ctaphid_proto,buffer(),"CTAP1/U2F") -- local is_request = (field_usb_endpointdir().value == 0) -- print(field_usb_endpointdir().value) -- print(is_request) -- print(Dissector.get("u2f")) -- if is_request then -- this is a request -- local u2f_command = buffer(1,1):uint() -- subtree:append_text(" Request") -- pinfo.cols.info = "U2F Request (" .. u2f_command_label(u2f_command, true) .. ")" -- subtree:add(u2ffield_cla, buffer(0,1)) -- subtree:add(u2ffield_ins, buffer(1,1), u2f_command, "Command: " .. u2f_command_label(u2f_command)) -- subtree:add(u2ffield_p1, buffer(2,1)) -- subtree:add(u2ffield_p2, buffer(3,1)) -- local request_length = buffer(4,3):uint() -- subtree:add(u2ffield_reqlen, buffer(4,3)) -- subtree:add(u2ffield_reqdata, buffer(7, request_length)) -- else -- response -- local u2f_status = buffer(buffer:len()-2,2):uint() -- subtree:append_text(" Response") -- pinfo.cols.info = "U2F Response (" .. u2f_status_label(u2f_status, true) .. ")" -- subtree:add(u2ffield_status, u2f_status, u2f_status, "Status: " .. u2f_status_label(u2f_status)) -- if buffer:len() > 2 then -- subtree:add(u2ffield_respdata, buffer(0, buffer:len()-2)) -- end -- end elseif cmd == CTAPHID_COMMANDS.CTAPHID_CBOR then local subtree = tree:add(buffer(0),"FIDO2 Payload") local ctap_cmd = buffer(0,1):uint() local text = nil if is_request then text = CTAP_COMMAND_CODE[ctap_cmd] else text = CTAP_RESPONSE_CODE[ctap_cmd] end pinfo.cols.protocol = "CTAP " .. text subtree:add(buffer(0,1),string.format('CTAP CMD/Status: %s (0x%02x)', text, ctap_cmd)) if buffer(1):len() > 0 then cbor:call(buffer(1):tvb(), pinfo, subtree) end elseif cmd == CTAPHID_COMMANDS.CTAPHID_INIT then elseif cmd == CTAPHID_COMMANDS.CTAPHID_PING then elseif cmd == CTAPHID_COMMANDS.CTAPHID_CANCEL then elseif cmd == CTAPHID_COMMANDS.CTAPHID_ERROR then elseif cmd == CTAPHID_COMMANDS.CTAPHID_KEEPALIVE then elseif cmd == CTAPHID_COMMANDS.CTAPHID_WINK then elseif cmd == CTAPHID_COMMANDS.CTAPHID_LOCK then elseif cmd >= CTAPHID_COMMANDS.CTAPHID_VENDOR_FIRST and cmd <= CTAPHID_COMMANDS.CTAPHID_VENDOR_LAST then else tree:add(ctaphidfield_data, buffer(0)):prepend_text("Unknown payload ") end end function u2f_command_label(cmd, abbrev) if abbrev ~= true then abbrev = false end local command_string = U2F_INS_STRINGS[cmd] if command_string ~= nil and not abbrev then command_string = command_string .. string.format(" (0x%02x)", cmd) elseif command_string == nil then command_string = string.format("0x%02x", cmd) end return command_string end function u2f_p1_label(p1, abbrev) if abbrev ~= true then abbrev = false end local p1_string = U2F_P1_STRINGS[p1] if p1_string ~= nil and not abbrev then p1_string = p1_string .. string.format(" (0x%02x)", p1) elseif p1_string == nil then p1_string = string.format("0x%02x", p1) end return p1_string end function u2f_status_label(status, abbrev) if abbrev ~= true then abbrev = false end local status_string = U2F_STATUS_STRINGS[status] if status_string ~= nil and not abbrev then status_string = status_string .. string.format(" (0x%02x)", status) elseif status_string == nil then status_string = string.format("0x%02x", status) end return status_string end function ctaphid_command_label(cmd) local command_string = CTAPHID_COMMAND_STRINGS[cmd] if command_string ~= nil then command_string = command_string .. string.format(" (0x%02x)", cmd) else command_string = string.format("0x%02x", cmd) if cmd >= CTAPHID_COMMANDS.CTAPHID_VENDOR_FIRST and cmd <= CTAPHID_COMMANDS.CTAPHID_VENDOR_LAST then command_string = command_string .. " [Vendor specific]" end end return command_string end function channel_state_key(channel_id) local key = Struct.pack(">I2I2I1", field_usb_bus().value, field_usb_device().value, field_usb_endpoint().value) .. channel_id:bytes():raw() return Struct.tohex(key) end packet_state = {} -- { packet_number => { cmd = uint, buffer = bytearray, complete = bool } } channel_state = {} -- { channel_state_key => { cmd = uint, payload_length = uint, buffer = bytearray } } function dump(o) if type(o) == 'table' then local s = '{ ' for k,v in pairs(o) do if type(k) ~= 'number' then k = '"'..k..'"' end s = s .. '['..k..'] = ' .. dump(v) .. ',' end return s .. '} ' else return tostring(o) end end function u2f_proto.dissector(buffer,pinfo,tree) if buffer:len() == 0 then return end -- && usb.function == 0x0008 && select correct endpoint/etc. print("u2f_before", pinfo.curr_proto) pinfo.cols.protocol = u2f_proto.name -- FIXME why can't I filter against this? print("u2f_after", pinfo.curr_proto) local subtree = tree:add(ctaphid_proto,buffer(),"CTAP1/U2F") local is_request = (field_usb_endpointdir().value == 0) if is_request then -- this is a request local u2f_command = buffer(1,1):uint() local p1_value = buffer(2,1):uint() subtree:append_text(" Request") if u2f_command == 0x2 then pinfo.cols.info = "U2F Request (" .. u2f_command_label(u2f_command, true) .. ", " .. u2f_p1_label(p1_value, true) .. ")" else pinfo.cols.info = "U2F Request (" .. u2f_command_label(u2f_command, true) .. ")" end subtree:add(u2ffield_cla, buffer(0,1)) subtree:add(u2ffield_ins, buffer(1,1), u2f_command, "Command: " .. u2f_command_label(u2f_command)) if u2f_command == 0x2 then subtree:add(u2ffield_p1, buffer(2,1), p1_value, "U2F command parameter 1: " .. u2f_p1_label(p1_value)) else subtree:add(u2ffield_p1, buffer(2,1)) end subtree:add(u2ffield_p2, buffer(3,1)) local request_length = buffer(4,3):uint() subtree:add(u2ffield_reqlen, buffer(4,3)) subtree:add(u2ffield_reqdata, buffer(7, request_length)) if buffer:len() > (request_length + 7) then subtree:add(u2ffield_expreslen, buffer(7 + request_length, buffer:len() - (request_length + 7))) end else -- response local u2f_status = buffer(buffer:len()-2,2):uint() subtree:append_text(" Response") pinfo.cols.info = "U2F Response (" .. u2f_status_label(u2f_status, true) .. ")" subtree:add(u2ffield_status, u2f_status, u2f_status, "Status: " .. u2f_status_label(u2f_status)) if buffer:len() > 2 then subtree:add(u2ffield_respdata, buffer(0, buffer:len()-2)) end end return true end function ctaphid_proto.init() packet_state = {} channel_state = {} end function ctaphid_proto.dissector(buffer,pinfo,tree) if buffer:len() == 0 then return end -- && usb.function == 0x0008 && select correct endpoint/etc. print("hid_before", pinfo.curr_proto) pinfo.cols.protocol = ctaphid_proto.name print("hid_after", pinfo.curr_proto) local channel_id = buffer(0,4) local payload = nil local cmd_or_seq = buffer(4,1):uint() local is_init_packet = (bit.band(cmd_or_seq, 0x80) == 0x80) local cmd = nil local payload_length = nil local sequence = nil -- extract relevant fields for each packet type if is_init_packet then cmd = bit.band(cmd_or_seq, 0x7f) -- ignore first bit of command field on initialization packets payload_length = buffer(5,2):uint() payload = buffer(7) else sequence = cmd_or_seq payload = buffer(5) end -- keep track of state across packets to combine segmented packets local pstate = packet_state[pinfo.number] local cstate = nil if pstate == nil then pstate = {} cstate = channel_state[channel_state_key(channel_id)] if cstate == nil then assert(is_init_packet) cstate = {} cstate.buffer = payload:bytes() cstate.cmd = cmd cstate.payload_length = payload_length channel_state[channel_state_key(channel_id)] = cstate else cstate.buffer:append(payload:bytes()) --buffer = ByteArray.tvb(cstate.buffer, "Command") -- create new tvb for packet end if cstate.payload_length > cstate.buffer:len() then -- packet incomplete pstate.complete = false pstate.cmd = cstate.cmd else cstate.buffer:set_size(cstate.payload_length) -- usbpcap always returns full packets so we need to truncate them pstate.complete = true pstate.cmd = cstate.cmd pstate.buffer = cstate.buffer channel_state[channel_state_key(channel_id)] = nil end packet_state[pinfo.number] = pstate end -- generate CTAPHID subtree local subtree = tree:add(ctaphid_proto,buffer()) if is_init_packet then local packet_text = "CTAPHID Initialization Packet" pinfo.cols.info = packet_text subtree:set_text(packet_text) subtree:add(ctaphidfield_cid, channel_id) subtree:add(ctaphidfield_cmd, buffer(4,1), cmd, "Command: " .. ctaphid_command_label(cmd)) subtree:add(ctaphidfield_bcnt, buffer(5,2)) subtree:add(ctaphidfield_data, payload) else local packet_text ="CTAPHID Continuation Packet" pinfo.cols.info = packet_text subtree:set_text(packet_text) subtree:add(ctaphidfield_cid, channel_id) subtree:add("Command: " .. ctaphid_command_label(pstate.cmd)):set_generated(true) subtree:add(ctaphidfield_seq, buffer(4,1)) subtree:add(ctaphidfield_data, payload) end if pstate.complete then dissect_ctaphid_payload(pstate.cmd, pstate.buffer:tvb("CTAPHID data"), pinfo, tree) end return end usb_table = DissectorTable.get("usb.product") usb_table:add(0x10500407,ctaphid_proto) -- VID/PID of Yubikey usb_table:add(0x096e0858,ctaphid_proto) -- VID/PID of Feitian key usb_table:add_for_decode_as(u2f_proto)