diff --git a/bin/template.ml b/bin/template.ml index 5d9250d..966e23c 100644 --- a/bin/template.ml +++ b/bin/template.ml @@ -42,7 +42,7 @@ let overview notes authenticated_as users = let register_view origin user = let script = Printf.sprintf {| - function makePublicKey(challengeData) { + 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)); return { @@ -62,16 +62,16 @@ let register_view origin user = alg: -7 } ], - attestation: "direct", + attestation: attestation, excludeCredentials: challengeData.excludeCredentials.map(id => ({ type: "public-key", id: Uint8Array.from(window.atob(id), c=>c.charCodeAt(0))})) }; } - function do_register(username) { + function do_register(username, attestation) { fetch("/registration-challenge/"+username) .then(response => response.json()) .then(function (challengeData) { - let publicKey = makePublicKey(challengeData); + let publicKey = makePublicKey(challengeData, attestation); navigator.credentials.create({ publicKey }) .then(function (credential) { let response = credential.response; @@ -92,7 +92,7 @@ let register_view origin user = let headers = {'Content-type': "application/json; charset=utf-8"}; - let request = new Request('/register_finish', { method: 'POST', body: body, headers: headers } ); + let request = new Request('/register_finish/'+username, { method: 'POST', body: body, headers: headers } ); fetch(request) .then(function (response) { if (!response.ok && response.status != 403) { @@ -111,7 +111,8 @@ let register_view origin user = } function doit() { let username = document.getElementById("username").value; - return do_register(username); + let attestation = document.getElementById("attestation").value; + return do_register(username, attestation); } |} origin and body = @@ -119,6 +120,11 @@ let register_view origin user =

Welcome.

+
|} user @@ -155,8 +161,8 @@ let authenticate_view challenge credentials user = }); let headers = {'Content-type': "application/json; charset=utf-8"}; - - let request = new Request('/authenticate_finish', { method: 'POST', body: body, headers: headers } ); + let username = window.location.pathname.substring("/authenticate/".length); + let request = new Request('/authenticate_finish/'+username, { method: 'POST', body: body, headers: headers } ); fetch(request) .then(function (response) { if (!response.ok) { diff --git a/bin/webauthn_demo.ml b/bin/webauthn_demo.ml index 6e2ff99..f9a5d7b 100644 --- a/bin/webauthn_demo.ml +++ b/bin/webauthn_demo.ml @@ -22,7 +22,9 @@ let check_counter kh_pub counter = then KhPubHashtbl.replace counters kh_pub counter; r -let challenges : (string, string) Hashtbl.t = Hashtbl.create 7 +let registration_challenges : (string, string) Hashtbl.t = Hashtbl.create 7 + +let authentication_challenges : (string, string) Hashtbl.t = Hashtbl.create 7 let to_string err = Format.asprintf "%a" Webauthn.pp_error err @@ -51,7 +53,7 @@ let add_routes t = let challenge = Cstruct.to_string (Mirage_crypto_rng.generate 16) (* [userid] should be a random value *) and userid = Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet user in - Hashtbl.replace challenges challenge user; + Hashtbl.replace registration_challenges user challenge; let credentials = match Hashtbl.find_opt users user with | None -> [] | Some credentials -> List.map (fun (_, cid, _) -> cid) credentials @@ -68,18 +70,19 @@ let add_routes t = ] in Logs.info (fun m -> m "produced challenge for user %s: %s" user challenge_b64); - Dream.put_session "challenge" challenge req >>= fun () -> Dream.json (Yojson.Safe.to_string json) in let register_finish req = + let user = Dream.param "user" req in Dream.body req >>= fun body -> Logs.info (fun m -> m "received body: %s" body); - match Dream.session "challenge" req with + match Hashtbl.find_opt registration_challenges user with | None -> Logs.warn (fun m -> m "no challenge found"); Dream.respond ~status:`Bad_Request "Bad request." | Some challenge -> + Hashtbl.remove registration_challenges user; match Webauthn.register_response t challenge body with | Error e -> Logs.warn (fun m -> m "error %a" Webauthn.pp_error e); @@ -90,39 +93,32 @@ let add_routes t = user_verified, sig_count, _authenticator_extensions, attestation_cert) -> ignore (check_counter (credential_id, pubkey) sig_count); Logs.info (fun m -> m "user present %B user verified %B" user_present user_verified); - match Hashtbl.find_opt challenges challenge with - | None -> - Logs.warn (fun m -> m "challenge not registered"); - Dream.respond ~status:`Internal_Server_Error - "Internal server error: couldn't find user for challenge" - | Some user -> - Logs.app (fun m -> m "challenge for user %S" user); - Hashtbl.remove challenges challenge; - match Dream.session "authenticated_as" req, Hashtbl.find_opt users user with - | _, None -> + Logs.app (fun m -> m "challenge for user %S" user); + match Dream.session "authenticated_as" req, Hashtbl.find_opt users user with + | _, None -> + Logs.app (fun m -> m "registered %s: %S" user credential_id); + Hashtbl.replace users user [ (pubkey, credential_id, attestation_cert) ]; + Dream.invalidate_session req >>= fun () -> + Flash_message.put_flash "" + (Printf.sprintf "Successfully registered as %s! [authenticate]" user user) + req; + Dream.json "true" + | Some session_user, Some keys -> + Logs.app (fun m -> m "user %S session_user %S" user session_user); + if String.equal user session_user then begin Logs.app (fun m -> m "registered %s: %S" user credential_id); - Hashtbl.replace users user [ (pubkey, credential_id, attestation_cert) ]; + Hashtbl.replace users user ((pubkey, credential_id, attestation_cert) :: keys) ; Dream.invalidate_session req >>= fun () -> Flash_message.put_flash "" (Printf.sprintf "Successfully registered as %s! [authenticate]" user user) req; Dream.json "true" - | Some session_user, Some keys -> - Logs.app (fun m -> m "user %S session_user %S" user session_user); - if String.equal user session_user then begin - Logs.app (fun m -> m "registered %s: %S" user credential_id); - Hashtbl.replace users user ((pubkey, credential_id, attestation_cert) :: keys) ; - Dream.invalidate_session req >>= fun () -> - Flash_message.put_flash "" - (Printf.sprintf "Successfully registered as %s! [authenticate]" user user) - req; - Dream.json "true" - end else - (Logs.info (fun m -> m "session_user %s, user %s" session_user user); - Dream.json ~status:`Forbidden "false") - | None, Some _keys -> - Logs.app (fun m -> m "no session user"); - Dream.json ~status:`Forbidden "false" + end else + (Logs.info (fun m -> m "session_user %s, user %s" session_user user); + Dream.json ~status:`Forbidden "false") + | None, Some _keys -> + Logs.app (fun m -> m "no session user"); + Dream.json ~status:`Forbidden "false" in let authenticate req = @@ -134,48 +130,44 @@ let add_routes t = | Some keys -> let credentials = List.map (fun (_, c, _) -> Base64.encode_string c) keys in let challenge = Cstruct.to_string (Mirage_crypto_rng.generate 16) in - Dream.put_session "challenge" challenge req >>= fun () -> - Dream.put_session "challenge_user" user req >>= fun () -> + Hashtbl.replace authentication_challenges user challenge; Dream.html (Template.authenticate_view (Base64.encode_string challenge) credentials user) in let authenticate_finish req = + let user = Dream.param "user" req in Dream.body req >>= fun body -> Logs.info (fun m -> m "received body: %s" body); - match Dream.session "challenge_user" req with + match Hashtbl.find_opt authentication_challenges user with | None -> Dream.respond ~status:`Internal_Server_Error "Internal server error." - | Some user -> - match Dream.session "challenge" req with + | Some challenge -> + Hashtbl.remove authentication_challenges user; + match Hashtbl.find_opt users user with | None -> - Logs.warn (fun m -> m "no challenge found"); + Logs.warn (fun m -> m "no user found, using empty"); Dream.respond ~status:`Bad_Request "Bad request." - | Some challenge -> - match Hashtbl.find_opt users user with - | None -> - Logs.warn (fun m -> m "no user found, using empty"); - Dream.respond ~status:`Bad_Request "Bad request." - | Some keys -> - let cid_keys = List.map (fun (key, credential_id, _) -> credential_id, key) keys in - match Webauthn.authentication_response t cid_keys challenge body with - | Ok (credential, _client_extensions, _user_present, _user_verified, counter, _authenticator_extensions) -> - if check_counter credential counter - then begin - Flash_message.put_flash "" "Successfully authenticated" req; - Dream.put_session "user" user req >>= fun () -> - Dream.put_session "authenticated_as" user req >>= fun () -> - Dream.json "true" - end else begin - Logs.warn (fun m -> m "credential %S for user %S: counter not strictly increasing! \ - Got %ld, expected >%ld. webauthn device compromised?" - (fst credential) user counter (KhPubHashtbl.find counters credential)); - Flash_message.put_flash "" "Authentication failure: key compromised?" req; - Dream.json "false" - end - | Error e -> - Logs.warn (fun m -> m "error %a" Webauthn.pp_error e); - let err = to_string e in - Flash_message.put_flash "" ("Authentication failure: " ^ err) req; + | Some keys -> + let cid_keys = List.map (fun (key, credential_id, _) -> credential_id, key) keys in + match Webauthn.authentication_response t cid_keys challenge body with + | Ok (credential, _client_extensions, _user_present, _user_verified, counter, _authenticator_extensions) -> + if check_counter credential counter + then begin + Flash_message.put_flash "" "Successfully authenticated" req; + Dream.put_session "user" user req >>= fun () -> + Dream.put_session "authenticated_as" user req >>= fun () -> + Dream.json "true" + end else begin + Logs.warn (fun m -> m "credential %S for user %S: counter not strictly increasing! \ + Got %ld, expected >%ld. webauthn device compromised?" + (fst credential) user counter (KhPubHashtbl.find counters credential)); + Flash_message.put_flash "" "Authentication failure: key compromised?" req; Dream.json "false" + end + | Error e -> + Logs.warn (fun m -> m "error %a" Webauthn.pp_error e); + let err = to_string e in + Flash_message.put_flash "" ("Authentication failure: " ^ err) req; + Dream.json "false" in let logout req = @@ -192,9 +184,9 @@ let add_routes t = Dream.get "/" main; Dream.get "/register" register; Dream.get "/registration-challenge/:user" registration_challenge; - Dream.post "/register_finish" register_finish; + Dream.post "/register_finish/:user" register_finish; Dream.get "/authenticate/:user" authenticate; - Dream.post "/authenticate_finish" authenticate_finish; + Dream.post "/authenticate_finish/:user" authenticate_finish; Dream.post "/logout" logout; Dream.get "/static/base64.js" base64; ]