From 44efdb1baec422f26e529b68cb332bae0a12dec6 Mon Sep 17 00:00:00 2001 From: Robur Date: Thu, 7 Oct 2021 09:53:38 +0000 Subject: [PATCH] support U2F transport extension extraction from certificate webauthn-demo: use base64js for decoding display certificate pretty printed, and the transports --- bin/base64.js | 6 +----- bin/template.ml | 15 +++++++++------ bin/webauthn_demo.ml | 36 +++++++++++++++++------------------- src/webauthn.ml | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/webauthn.mli | 18 ++++++++++++++++++ 5 files changed, 89 insertions(+), 30 deletions(-) diff --git a/bin/base64.js b/bin/base64.js index 3ebe0a9..dff0e35 100644 --- a/bin/base64.js +++ b/bin/base64.js @@ -28,10 +28,6 @@ var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' function b64ToByteArray (b64) { var i, j, l, tmp, placeHolders, arr - if (b64.length % 4 > 0) { - throw new Error('Invalid string. Length must be a multiple of 4') - } - // the number of equal signs (place holders) // if there are two placeholders, than the two characters before it // represent one byte @@ -115,4 +111,4 @@ var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' exports.toByteArray = b64ToByteArray exports.fromByteArray = uint8ToBase64 -}(typeof exports === 'undefined' ? (this.base64js = {}) : exports)) \ No newline at end of file +}(typeof exports === 'undefined' ? (this.base64js = {}) : exports)) diff --git a/bin/template.ml b/bin/template.ml index 72a33a1..54bd158 100644 --- a/bin/template.ml +++ b/bin/template.ml @@ -11,6 +11,9 @@ let page s b = .replace(/\//g, "_") .replace(/=/g, ""); } + function bufferDecode(value) { + return base64js.toByteArray(value); + } %s|} s b @@ -43,8 +46,8 @@ let overview notes authenticated_as users = let register_view origin user = let script = Printf.sprintf {| function makePublicKey(challengeData, attestation) { - let challenge = Uint8Array.from(window.atob(challengeData.challenge), c=>c.charCodeAt(0)); - let user_id = Uint8Array.from(window.atob(challengeData.user.id), c=>c.charCodeAt(0)); + let challenge = bufferDecode(challengeData.challenge); + let user_id = bufferDecode(challengeData.user.id); return { challenge: challenge, rp: { @@ -64,7 +67,7 @@ let register_view origin user = ], attestation: attestation, excludeCredentials: challengeData.excludeCredentials.map(id => ({ type: "public-key", - id: Uint8Array.from(window.atob(id), c=>c.charCodeAt(0))})) + id: bufferDecode(id)})) }; } function do_register(username, attestation) { @@ -120,7 +123,7 @@ let register_view origin user = - + |} user in @@ -130,8 +133,8 @@ let authenticate_view challenge credentials user = let script = Printf.sprintf {| let request_options = { - challenge: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)), - allowCredentials: %s.map(x => { x.id = Uint8Array.from(window.atob(x.id), c=>c.charCodeAt(0)); return x }), + challenge: bufferDecode("%s"), + allowCredentials: %s.map(x => { x.id = bufferDecode(x.id); return x }), }; navigator.credentials.get({ publicKey: request_options }) .then(function (assertion) { diff --git a/bin/webauthn_demo.ml b/bin/webauthn_demo.ml index 943a130..663c820 100644 --- a/bin/webauthn_demo.ml +++ b/bin/webauthn_demo.ml @@ -132,34 +132,32 @@ let add_routes t = ignore (check_counter (credential_id, public_key) sign_count); Logs.info (fun m -> m "register %S user present %B user verified %B" username user_present user_verified); - match Dream.session "authenticated_as" req, Hashtbl.find_opt users userid with - | _, None -> + let registered other_keys = Logs.app (fun m -> m "registered %s: %S" username credential_id); - Hashtbl.replace users userid (username, [ (public_key, credential_id, certificate) ]); + Hashtbl.replace users userid (username, ((public_key, credential_id, certificate) :: other_keys)) ; Dream.invalidate_session req >>= fun () -> - let cert_string = - Option.fold ~none:"No certificate" - ~some:(fun c -> X509.Certificate.encode_pem c |> Cstruct.to_string) + let cert_pem, cert_string, transports = + Option.fold ~none:("No certificate", "No certificate", Ok []) + ~some:(fun c -> + X509.Certificate.encode_pem c |> Cstruct.to_string, + Fmt.to_to_string X509.Certificate.pp c, + Webauthn.transports_of_cert c) certificate in + let transports = match transports with + | Error `Msg m -> "error " ^ m + | Ok ts -> Fmt.str "%a" Fmt.(list ~sep:(any ", ") Webauthn.pp_transport) ts + in Flash_message.put_flash "" - (Printf.sprintf "Successfully registered as %s! [authenticate]
Certificate:
%s
" username userid cert_string) + (Printf.sprintf "Successfully registered as %s! [authenticate]
Certificate transports: %s
Certificate: %s
PEM Certificate:
%s
" username userid transports cert_string cert_pem) req; Dream.json "true" + in + match Dream.session "authenticated_as" req, Hashtbl.find_opt users userid with + | _, None -> registered [] | Some session_user, Some (username', keys) -> if String.equal username session_user && String.equal username username' then begin - Logs.app (fun m -> m "registered %s: %S" username credential_id); - Hashtbl.replace users userid (username, ((public_key, credential_id, certificate) :: keys)) ; - Dream.invalidate_session req >>= fun () -> - let cert_string = - Option.fold ~none:"No certificate" - ~some:(fun c -> X509.Certificate.encode_pem c |> Cstruct.to_string) - certificate - in - Flash_message.put_flash "" - (Printf.sprintf "Successfully registered as %s! [authenticate]
Certificate:
%s
" username userid cert_string) - req; - Dream.json "true" + registered keys end else (Logs.info (fun m -> m "session_user %s, user %s (user in users table %s)" session_user username username'); Dream.json ~status:`Forbidden "false") diff --git a/src/webauthn.ml b/src/webauthn.ml index 508d1b5..b4a6ab9 100644 --- a/src/webauthn.ml +++ b/src/webauthn.ml @@ -434,3 +434,47 @@ let authenticate t public_key response = client_extensions ; } in Ok (challenge, authentication) + +let fido_u2f_transport_oid = + Asn.OID.(base 1 3 <| 6 <| 1 <| 4 <| 1 <| 45724 <| 2 <| 1 <| 1) + +type transport = [ + | `Bluetooth_classic + | `Bluetooth_low_energy + | `Usb + | `Nfc + | `Usb_internal +] + +let pp_transport ppf = function + | `Bluetooth_classic -> Fmt.string ppf "Bluetooth classic" + | `Bluetooth_low_energy -> Fmt.string ppf "Bluetooth low energy" + | `Usb -> Fmt.string ppf "USB" + | `Nfc -> Fmt.string ppf "NFC" + | `Usb_internal -> Fmt.string ppf "USB internal" + +let transports = + let opts = [ + (0, `Bluetooth_classic); + (1, `Bluetooth_low_energy); + (2, `Usb); + (3, `Nfc); + (4, `Usb_internal); + ] in + Asn.S.bit_string_flags opts + +let decode_strict codec cs = + match Asn.decode codec cs with + | Ok (a, cs) -> + guard (Cstruct.length cs = 0) (`Msg "trailing bytes") >>= fun () -> + Ok a + | Error (`Parse msg) -> Error (`Msg msg) + +let decode_transport = + decode_strict (Asn.codec Asn.der transports) + +let transports_of_cert c = + Result.bind + (Option.to_result ~none:(`Msg "extension not present") + (X509.Extension.(find (Unsupported fido_u2f_transport_oid) (X509.Certificate.extensions c)))) + (fun (_, data) -> decode_transport data) diff --git a/src/webauthn.mli b/src/webauthn.mli index 56177bb..d35a17b 100644 --- a/src/webauthn.mli +++ b/src/webauthn.mli @@ -155,3 +155,21 @@ val authenticate_response_of_string : string -> browser. *) val authenticate : t -> Mirage_crypto_ec.P256.Dsa.pub -> authenticate_response -> (challenge * authentication, error) result + +(** The type of FIDO U2F transports. *) +type transport = [ + | `Bluetooth_classic + | `Bluetooth_low_energy + | `Usb + | `Nfc + | `Usb_internal +] + +(** [pp_transport ppf tranport] pretty-prints the [transport] on [ppf]. *) +val pp_transport : Format.formatter -> transport -> unit + +(** [transports_of_cert certficate] attempts to extract the FIDO U2F + authenticator transports extension (OID 1.3.6.1.4.1.45724.2.1.1) from the + [certificate]. *) +val transports_of_cert : X509.Certificate.t -> + (transport list, [`Msg of string]) result