Skip to content

Instantly share code, notes, and snippets.

@joelonsql
Last active January 13, 2024 04:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joelonsql/28967ea50f93735de0381ebcfee0392a to your computer and use it in GitHub Desktop.
Save joelonsql/28967ea50f93735de0381ebcfee0392a to your computer and use it in GitHub Desktop.
Decode WebAuthn CBOR Using PostgreSQL Recursive CTE
CREATE OR REPLACE FUNCTION decode_cbor(cbor bytea)
RETURNS TABLE (
item integer,
map_item_count integer,
text_string text,
bytes bytea
)
LANGUAGE sql
AS $$
/*
Only a few major types implemented partially,
the absolute minimum required to decode the
WebAuthn "attestationObject"
into a "ctapMakeCredResp" object.
https://github.com/fido-alliance/webauthn-demo/blob/master/utils.js#L200
let ctapMakeCredResp = cbor.decodeAllSync(attestationBuffer)[0];
*/
WITH RECURSIVE x AS (
SELECT
decode_cbor.cbor,
0::integer AS item,
NULL::integer AS map_item_count,
NULL::text COLLATE "default" AS text_string,
NULL::bytea AS bytes
UNION ALL
SELECT
substring(x.cbor,byte_offset) AS cbor,
item+1 AS item,
CASE WHEN major_type_value = 5 AND additional_type_value <= 23
THEN additional_type_value
END AS map_item_count,
CASE WHEN major_type_value = 3 AND additional_type_value <= 23
THEN convert_from(substring(x.cbor,2,additional_type_value),'utf8')
END::text COLLATE "default" AS text_string,
CASE WHEN major_type_value = 2 AND additional_type_value = 24
THEN substring(x.cbor,3,get_byte(x.cbor,1))
END AS bytes
FROM x
JOIN LATERAL (VALUES(
(get_byte(x.cbor,0)>>5)&'111'::bit(3)::integer,
get_byte(x.cbor,0)&'11111'::bit(5)::integer
)) AS data_item_header(major_type_value,additional_type_value) ON TRUE
JOIN LATERAL (VALUES(CASE
WHEN major_type_value IN (2,3) AND additional_type_value <= 23 THEN 2+additional_type_value
WHEN major_type_value = 5 THEN 2
WHEN major_type_value = 2 AND additional_type_value = 24 THEN 3+get_byte(x.cbor,1)
END)) AS next_item(byte_offset) ON TRUE
WHERE length(x.cbor) > 0
)
SELECT
item,
map_item_count,
text_string,
bytes
FROM x
WHERE item > 0
ORDER BY item
$$;
CREATE OR REPLACE FUNCTION ctapMakeCredResp(attestationObject bytea)
RETURNS jsonb
LANGUAGE sql
AS $$
WITH items AS (
SELECT * FROM decode_cbor(attestationObject)
)
SELECT jsonb_object_agg(
keys.text_string,
COALESCE(values.text_string,encode(values.bytes,'base64'))
)
FROM items AS map
JOIN generate_series(1,map.map_item_count) AS map_index ON TRUE
JOIN items AS keys ON keys.item = map.item+map_index*2-1
JOIN items AS values ON values.item = map.item+map_index*2
WHERE map.map_item_count > 0
$$;
SELECT jsonb_pretty(ctapMakeCredResp('\xa363666d74646e6f6e656761747453746d74a068617574684461746158be49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763455fc81524adce000235bcc60a648b0b25f1f05503003a01135c2d9aadb3a18061e400654075dba7e3c6549eb8658a1ce3d051955971bbd1faa840a59ae65db032d6fae0c3892bc532cc35985013b4b350a5010203262001215820ad1d5633b0ccbdaab0efb55c999543d1850aaf350d30434c4765f099b5f1b87e2258206cdf70a6ee3193d94351c2aea0179f320d7ede464aec1e43a386ee9eb0628182'::bytea));
/*
{
"fmt": "none",
"attStmt": null,
"authData": "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NFX8gVJK3OAAI1vMYKZIsLJfHwVQMAOgET\nXC2arbOhgGHkAGVAddun48ZUnrhlihzj0FGVWXG70fqoQKWa5l2wMtb64MOJK8UyzDWYUBO0s1Cl\nAQIDJiABIVggrR1WM7DMvaqw77VcmZVD0YUKrzUNMENMR2XwmbXxuH4iWCBs33Cm7jGT2UNRwq6g\nF58yDX7eRkrsHkOjhu6esGKBgg=="
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment