Attestation type, rework session variables
* Allow the user to select attestation type * Rework session variables and challenge tracking
This commit is contained in:
parent
a5af696428
commit
ddebe8b804
2 changed files with 71 additions and 73 deletions
|
@ -42,7 +42,7 @@ 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) {
|
function makePublicKey(challengeData, attestation) {
|
||||||
let challenge = Uint8Array.from(window.atob(challengeData.challenge), c=>c.charCodeAt(0));
|
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 user_id = Uint8Array.from(window.atob(challengeData.user.id), c=>c.charCodeAt(0));
|
||||||
return {
|
return {
|
||||||
|
@ -62,16 +62,16 @@ let register_view origin user =
|
||||||
alg: -7
|
alg: -7
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
attestation: "direct",
|
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: Uint8Array.from(window.atob(id), c=>c.charCodeAt(0))}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function do_register(username) {
|
function do_register(username, attestation) {
|
||||||
fetch("/registration-challenge/"+username)
|
fetch("/registration-challenge/"+username)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(function (challengeData) {
|
.then(function (challengeData) {
|
||||||
let publicKey = makePublicKey(challengeData);
|
let publicKey = makePublicKey(challengeData, attestation);
|
||||||
navigator.credentials.create({ publicKey })
|
navigator.credentials.create({ publicKey })
|
||||||
.then(function (credential) {
|
.then(function (credential) {
|
||||||
let response = credential.response;
|
let response = credential.response;
|
||||||
|
@ -92,7 +92,7 @@ let register_view origin user =
|
||||||
|
|
||||||
let headers = {'Content-type': "application/json; charset=utf-8"};
|
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)
|
fetch(request)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (!response.ok && response.status != 403) {
|
if (!response.ok && response.status != 403) {
|
||||||
|
@ -111,7 +111,8 @@ let register_view origin user =
|
||||||
}
|
}
|
||||||
function doit() {
|
function doit() {
|
||||||
let username = document.getElementById("username").value;
|
let username = document.getElementById("username").value;
|
||||||
return do_register(username);
|
let attestation = document.getElementById("attestation").value;
|
||||||
|
return do_register(username, attestation);
|
||||||
}
|
}
|
||||||
|} origin
|
|} origin
|
||||||
and body =
|
and body =
|
||||||
|
@ -119,6 +120,11 @@ let register_view origin user =
|
||||||
<p>Welcome.</p>
|
<p>Welcome.</p>
|
||||||
<form method="post" id="form" onsubmit="return false;">
|
<form method="post" id="form" onsubmit="return false;">
|
||||||
<label for="username" >Desired username</label><input name="username" id="username" value="%s"/>
|
<label for="username" >Desired username</label><input name="username" id="username" value="%s"/>
|
||||||
|
<label for="attestation">Attestation type</label><select name="attestation" id="attestation">
|
||||||
|
<option value="direct">direct</option>
|
||||||
|
<option value="indirect">indirect</option>
|
||||||
|
<option value="none">none</option>
|
||||||
|
</select>
|
||||||
<button id="button" type="button" onclick="doit()">Register</button>
|
<button id="button" type="button" onclick="doit()">Register</button>
|
||||||
</form>
|
</form>
|
||||||
|} user
|
|} user
|
||||||
|
@ -155,8 +161,8 @@ let authenticate_view challenge credentials user =
|
||||||
});
|
});
|
||||||
|
|
||||||
let headers = {'Content-type': "application/json; charset=utf-8"};
|
let headers = {'Content-type': "application/json; charset=utf-8"};
|
||||||
|
let username = window.location.pathname.substring("/authenticate/".length);
|
||||||
let request = new Request('/authenticate_finish', { method: 'POST', body: body, headers: headers } );
|
let request = new Request('/authenticate_finish/'+username, { method: 'POST', body: body, headers: headers } );
|
||||||
fetch(request)
|
fetch(request)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
|
@ -22,7 +22,9 @@ let check_counter kh_pub counter =
|
||||||
then KhPubHashtbl.replace counters kh_pub counter;
|
then KhPubHashtbl.replace counters kh_pub counter;
|
||||||
r
|
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
|
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)
|
let challenge = Cstruct.to_string (Mirage_crypto_rng.generate 16)
|
||||||
(* [userid] should be a random value *)
|
(* [userid] should be a random value *)
|
||||||
and userid = Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet user in
|
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
|
let credentials = match Hashtbl.find_opt users user with
|
||||||
| None -> []
|
| None -> []
|
||||||
| Some credentials -> List.map (fun (_, cid, _) -> cid) credentials
|
| Some credentials -> List.map (fun (_, cid, _) -> cid) credentials
|
||||||
|
@ -68,18 +70,19 @@ let add_routes t =
|
||||||
]
|
]
|
||||||
in
|
in
|
||||||
Logs.info (fun m -> m "produced challenge for user %s: %s" user challenge_b64);
|
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)
|
Dream.json (Yojson.Safe.to_string json)
|
||||||
in
|
in
|
||||||
|
|
||||||
let register_finish req =
|
let register_finish req =
|
||||||
|
let user = Dream.param "user" req in
|
||||||
Dream.body req >>= fun body ->
|
Dream.body req >>= fun body ->
|
||||||
Logs.info (fun m -> m "received body: %s" 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 ->
|
| None ->
|
||||||
Logs.warn (fun m -> m "no challenge found");
|
Logs.warn (fun m -> m "no challenge found");
|
||||||
Dream.respond ~status:`Bad_Request "Bad request."
|
Dream.respond ~status:`Bad_Request "Bad request."
|
||||||
| Some challenge ->
|
| Some challenge ->
|
||||||
|
Hashtbl.remove registration_challenges user;
|
||||||
match Webauthn.register_response t challenge body with
|
match Webauthn.register_response t challenge body with
|
||||||
| Error e ->
|
| Error e ->
|
||||||
Logs.warn (fun m -> m "error %a" Webauthn.pp_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) ->
|
user_verified, sig_count, _authenticator_extensions, attestation_cert) ->
|
||||||
ignore (check_counter (credential_id, pubkey) sig_count);
|
ignore (check_counter (credential_id, pubkey) sig_count);
|
||||||
Logs.info (fun m -> m "user present %B user verified %B" user_present user_verified);
|
Logs.info (fun m -> m "user present %B user verified %B" user_present user_verified);
|
||||||
match Hashtbl.find_opt challenges challenge with
|
Logs.app (fun m -> m "challenge for user %S" user);
|
||||||
| None ->
|
match Dream.session "authenticated_as" req, Hashtbl.find_opt users user with
|
||||||
Logs.warn (fun m -> m "challenge not registered");
|
| _, None ->
|
||||||
Dream.respond ~status:`Internal_Server_Error
|
Logs.app (fun m -> m "registered %s: %S" user credential_id);
|
||||||
"Internal server error: couldn't find user for challenge"
|
Hashtbl.replace users user [ (pubkey, credential_id, attestation_cert) ];
|
||||||
| Some user ->
|
Dream.invalidate_session req >>= fun () ->
|
||||||
Logs.app (fun m -> m "challenge for user %S" user);
|
Flash_message.put_flash ""
|
||||||
Hashtbl.remove challenges challenge;
|
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user)
|
||||||
match Dream.session "authenticated_as" req, Hashtbl.find_opt users user with
|
req;
|
||||||
| _, None ->
|
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);
|
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 () ->
|
Dream.invalidate_session req >>= fun () ->
|
||||||
Flash_message.put_flash ""
|
Flash_message.put_flash ""
|
||||||
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user)
|
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user)
|
||||||
req;
|
req;
|
||||||
Dream.json "true"
|
Dream.json "true"
|
||||||
| Some session_user, Some keys ->
|
end else
|
||||||
Logs.app (fun m -> m "user %S session_user %S" user session_user);
|
(Logs.info (fun m -> m "session_user %s, user %s" session_user user);
|
||||||
if String.equal user session_user then begin
|
Dream.json ~status:`Forbidden "false")
|
||||||
Logs.app (fun m -> m "registered %s: %S" user credential_id);
|
| None, Some _keys ->
|
||||||
Hashtbl.replace users user ((pubkey, credential_id, attestation_cert) :: keys) ;
|
Logs.app (fun m -> m "no session user");
|
||||||
Dream.invalidate_session req >>= fun () ->
|
Dream.json ~status:`Forbidden "false"
|
||||||
Flash_message.put_flash ""
|
|
||||||
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" 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"
|
|
||||||
in
|
in
|
||||||
|
|
||||||
let authenticate req =
|
let authenticate req =
|
||||||
|
@ -134,48 +130,44 @@ let add_routes t =
|
||||||
| Some keys ->
|
| Some keys ->
|
||||||
let credentials = List.map (fun (_, c, _) -> Base64.encode_string c) keys in
|
let credentials = List.map (fun (_, c, _) -> Base64.encode_string c) keys in
|
||||||
let challenge = Cstruct.to_string (Mirage_crypto_rng.generate 16) in
|
let challenge = Cstruct.to_string (Mirage_crypto_rng.generate 16) in
|
||||||
Dream.put_session "challenge" challenge req >>= fun () ->
|
Hashtbl.replace authentication_challenges user challenge;
|
||||||
Dream.put_session "challenge_user" user req >>= fun () ->
|
|
||||||
Dream.html (Template.authenticate_view (Base64.encode_string challenge) credentials user)
|
Dream.html (Template.authenticate_view (Base64.encode_string challenge) credentials user)
|
||||||
in
|
in
|
||||||
|
|
||||||
let authenticate_finish req =
|
let authenticate_finish req =
|
||||||
|
let user = Dream.param "user" req in
|
||||||
Dream.body req >>= fun body ->
|
Dream.body req >>= fun body ->
|
||||||
Logs.info (fun m -> m "received body: %s" 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."
|
| None -> Dream.respond ~status:`Internal_Server_Error "Internal server error."
|
||||||
| Some user ->
|
| Some challenge ->
|
||||||
match Dream.session "challenge" req with
|
Hashtbl.remove authentication_challenges user;
|
||||||
|
match Hashtbl.find_opt users user with
|
||||||
| None ->
|
| 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."
|
Dream.respond ~status:`Bad_Request "Bad request."
|
||||||
| Some challenge ->
|
| Some keys ->
|
||||||
match Hashtbl.find_opt users user with
|
let cid_keys = List.map (fun (key, credential_id, _) -> credential_id, key) keys in
|
||||||
| None ->
|
match Webauthn.authentication_response t cid_keys challenge body with
|
||||||
Logs.warn (fun m -> m "no user found, using empty");
|
| Ok (credential, _client_extensions, _user_present, _user_verified, counter, _authenticator_extensions) ->
|
||||||
Dream.respond ~status:`Bad_Request "Bad request."
|
if check_counter credential counter
|
||||||
| Some keys ->
|
then begin
|
||||||
let cid_keys = List.map (fun (key, credential_id, _) -> credential_id, key) keys in
|
Flash_message.put_flash "" "Successfully authenticated" req;
|
||||||
match Webauthn.authentication_response t cid_keys challenge body with
|
Dream.put_session "user" user req >>= fun () ->
|
||||||
| Ok (credential, _client_extensions, _user_present, _user_verified, counter, _authenticator_extensions) ->
|
Dream.put_session "authenticated_as" user req >>= fun () ->
|
||||||
if check_counter credential counter
|
Dream.json "true"
|
||||||
then begin
|
end else begin
|
||||||
Flash_message.put_flash "" "Successfully authenticated" req;
|
Logs.warn (fun m -> m "credential %S for user %S: counter not strictly increasing! \
|
||||||
Dream.put_session "user" user req >>= fun () ->
|
Got %ld, expected >%ld. webauthn device compromised?"
|
||||||
Dream.put_session "authenticated_as" user req >>= fun () ->
|
(fst credential) user counter (KhPubHashtbl.find counters credential));
|
||||||
Dream.json "true"
|
Flash_message.put_flash "" "Authentication failure: key compromised?" req;
|
||||||
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"
|
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
|
in
|
||||||
|
|
||||||
let logout req =
|
let logout req =
|
||||||
|
@ -192,9 +184,9 @@ let add_routes t =
|
||||||
Dream.get "/" main;
|
Dream.get "/" main;
|
||||||
Dream.get "/register" register;
|
Dream.get "/register" register;
|
||||||
Dream.get "/registration-challenge/:user" registration_challenge;
|
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.get "/authenticate/:user" authenticate;
|
||||||
Dream.post "/authenticate_finish" authenticate_finish;
|
Dream.post "/authenticate_finish/:user" authenticate_finish;
|
||||||
Dream.post "/logout" logout;
|
Dream.post "/logout" logout;
|
||||||
Dream.get "/static/base64.js" base64;
|
Dream.get "/static/base64.js" base64;
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue