support U2F transport extension extraction from certificate

webauthn-demo: use base64js for decoding
display certificate pretty printed, and the transports
This commit is contained in:
Robur 2021-10-07 09:53:38 +00:00
parent 89703fe795
commit 44efdb1bae
5 changed files with 89 additions and 30 deletions

View file

@ -28,10 +28,6 @@ var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
function b64ToByteArray (b64) { function b64ToByteArray (b64) {
var i, j, l, tmp, placeHolders, arr 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) // the number of equal signs (place holders)
// if there are two placeholders, than the two characters before it // if there are two placeholders, than the two characters before it
// represent one byte // represent one byte

View file

@ -11,6 +11,9 @@ let page s b =
.replace(/\//g, "_") .replace(/\//g, "_")
.replace(/=/g, ""); .replace(/=/g, "");
} }
function bufferDecode(value) {
return base64js.toByteArray(value);
}
</script> </script>
<script>%s</script> <script>%s</script>
</head><body>%s</body></html>|} s b </head><body>%s</body></html>|} s b
@ -43,8 +46,8 @@ let overview notes authenticated_as users =
let register_view origin user = let register_view origin user =
let script = Printf.sprintf {| let script = Printf.sprintf {|
function makePublicKey(challengeData, attestation) { function makePublicKey(challengeData, attestation) {
let challenge = Uint8Array.from(window.atob(challengeData.challenge), c=>c.charCodeAt(0)); let challenge = bufferDecode(challengeData.challenge);
let user_id = Uint8Array.from(window.atob(challengeData.user.id), c=>c.charCodeAt(0)); let user_id = bufferDecode(challengeData.user.id);
return { return {
challenge: challenge, challenge: challenge,
rp: { rp: {
@ -64,7 +67,7 @@ let register_view origin user =
], ],
attestation: attestation, attestation: attestation,
excludeCredentials: challengeData.excludeCredentials.map(id => ({ type: "public-key", 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) { function do_register(username, attestation) {
@ -120,7 +123,7 @@ let register_view origin user =
<option value="indirect">indirect</option> <option value="indirect">indirect</option>
<option value="none">none</option> <option value="none">none</option>
</select> </select>
<button id="button" type="button" onclick="doit()">Register</button> <button id="button" type="button" onmousedown="doit()">Register</button>
</form> </form>
|} user |} user
in in
@ -130,8 +133,8 @@ let authenticate_view challenge credentials user =
let script = let script =
Printf.sprintf {| Printf.sprintf {|
let request_options = { let request_options = {
challenge: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)), challenge: bufferDecode("%s"),
allowCredentials: %s.map(x => { x.id = Uint8Array.from(window.atob(x.id), c=>c.charCodeAt(0)); return x }), allowCredentials: %s.map(x => { x.id = bufferDecode(x.id); return x }),
}; };
navigator.credentials.get({ publicKey: request_options }) navigator.credentials.get({ publicKey: request_options })
.then(function (assertion) { .then(function (assertion) {

View file

@ -132,34 +132,32 @@ let add_routes t =
ignore (check_counter (credential_id, public_key) sign_count); ignore (check_counter (credential_id, public_key) sign_count);
Logs.info (fun m -> m "register %S user present %B user verified %B" Logs.info (fun m -> m "register %S user present %B user verified %B"
username user_present user_verified); username user_present user_verified);
match Dream.session "authenticated_as" req, Hashtbl.find_opt users userid with let registered other_keys =
| _, None ->
Logs.app (fun m -> m "registered %s: %S" username credential_id); 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 () -> Dream.invalidate_session req >>= fun () ->
let cert_string = let cert_pem, cert_string, transports =
Option.fold ~none:"No certificate" Option.fold ~none:("No certificate", "No certificate", Ok [])
~some:(fun c -> X509.Certificate.encode_pem c |> Cstruct.to_string) ~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 certificate
in 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 "" Flash_message.put_flash ""
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a><br/>Certificate:<br/><pre>%s</pre>" username userid cert_string) (Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a><br/>Certificate transports: %s<br/>Certificate: %s<br/>PEM Certificate:<br/><pre>%s</pre>" username userid transports cert_string cert_pem)
req; req;
Dream.json "true" Dream.json "true"
in
match Dream.session "authenticated_as" req, Hashtbl.find_opt users userid with
| _, None -> registered []
| Some session_user, Some (username', keys) -> | Some session_user, Some (username', keys) ->
if String.equal username session_user && String.equal username username' then begin if String.equal username session_user && String.equal username username' then begin
Logs.app (fun m -> m "registered %s: %S" username credential_id); registered keys
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! <a href=\"/authenticate/%s\">[authenticate]</a><br/>Certificate:<br/><pre>%s</pre>" username userid cert_string)
req;
Dream.json "true"
end else end else
(Logs.info (fun m -> m "session_user %s, user %s (user in users table %s)" session_user username username'); (Logs.info (fun m -> m "session_user %s, user %s (user in users table %s)" session_user username username');
Dream.json ~status:`Forbidden "false") Dream.json ~status:`Forbidden "false")

View file

@ -434,3 +434,47 @@ let authenticate t public_key response =
client_extensions ; client_extensions ;
} in } in
Ok (challenge, authentication) 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)

View file

@ -155,3 +155,21 @@ val authenticate_response_of_string : string ->
browser. *) browser. *)
val authenticate : t -> Mirage_crypto_ec.P256.Dsa.pub -> authenticate_response -> val authenticate : t -> Mirage_crypto_ec.P256.Dsa.pub -> authenticate_response ->
(challenge * authentication, error) result (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