WIP
This commit is contained in:
commit
55bb364b72
11 changed files with 615 additions and 0 deletions
118
bin/base64.js
Normal file
118
bin/base64.js
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||||
|
|
||||||
|
;(function (exports) {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var Arr = (typeof Uint8Array !== 'undefined')
|
||||||
|
? Uint8Array
|
||||||
|
: Array
|
||||||
|
|
||||||
|
var PLUS = '+'.charCodeAt(0)
|
||||||
|
var SLASH = '/'.charCodeAt(0)
|
||||||
|
var NUMBER = '0'.charCodeAt(0)
|
||||||
|
var LOWER = 'a'.charCodeAt(0)
|
||||||
|
var UPPER = 'A'.charCodeAt(0)
|
||||||
|
var PLUS_URL_SAFE = '-'.charCodeAt(0)
|
||||||
|
var SLASH_URL_SAFE = '_'.charCodeAt(0)
|
||||||
|
|
||||||
|
function decode (elt) {
|
||||||
|
var code = elt.charCodeAt(0)
|
||||||
|
if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
|
||||||
|
if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
|
||||||
|
if (code < NUMBER) return -1 // no match
|
||||||
|
if (code < NUMBER + 10) return code - NUMBER + 26 + 26
|
||||||
|
if (code < UPPER + 26) return code - UPPER
|
||||||
|
if (code < LOWER + 26) return code - LOWER + 26
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// if there is only one, then the three characters before it represent 2 bytes
|
||||||
|
// this is just a cheap hack to not do indexOf twice
|
||||||
|
var len = b64.length
|
||||||
|
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
|
||||||
|
|
||||||
|
// base64 is 4/3 + up to two characters of the original data
|
||||||
|
arr = new Arr(b64.length * 3 / 4 - placeHolders)
|
||||||
|
|
||||||
|
// if there are placeholders, only get up to the last complete 4 chars
|
||||||
|
l = placeHolders > 0 ? b64.length - 4 : b64.length
|
||||||
|
|
||||||
|
var L = 0
|
||||||
|
|
||||||
|
function push (v) {
|
||||||
|
arr[L++] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0, j = 0; i < l; i += 4, j += 3) {
|
||||||
|
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
|
||||||
|
push((tmp & 0xFF0000) >> 16)
|
||||||
|
push((tmp & 0xFF00) >> 8)
|
||||||
|
push(tmp & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeHolders === 2) {
|
||||||
|
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
|
||||||
|
push(tmp & 0xFF)
|
||||||
|
} else if (placeHolders === 1) {
|
||||||
|
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
|
||||||
|
push((tmp >> 8) & 0xFF)
|
||||||
|
push(tmp & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
function uint8ToBase64 (uint8) {
|
||||||
|
var i
|
||||||
|
var extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
|
||||||
|
var output = ''
|
||||||
|
var temp, length
|
||||||
|
|
||||||
|
function encode (num) {
|
||||||
|
return lookup.charAt(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
function tripletToBase64 (num) {
|
||||||
|
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through the array every three bytes, we'll deal with trailing stuff later
|
||||||
|
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
|
||||||
|
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
|
||||||
|
output += tripletToBase64(temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad the end with zeros, but make sure to not forget the extra bytes
|
||||||
|
switch (extraBytes) {
|
||||||
|
case 1:
|
||||||
|
temp = uint8[uint8.length - 1]
|
||||||
|
output += encode(temp >> 2)
|
||||||
|
output += encode((temp << 4) & 0x3F)
|
||||||
|
output += '=='
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
|
||||||
|
output += encode(temp >> 10)
|
||||||
|
output += encode((temp >> 4) & 0x3F)
|
||||||
|
output += encode((temp << 2) & 0x3F)
|
||||||
|
output += '='
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.toByteArray = b64ToByteArray
|
||||||
|
exports.fromByteArray = uint8ToBase64
|
||||||
|
}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
|
8
bin/dune
Normal file
8
bin/dune
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
(executable
|
||||||
|
(public_name webauthn_demo)
|
||||||
|
(name webauthn_demo)
|
||||||
|
(modules webauthn_demo template)
|
||||||
|
(preprocessor_deps base64.js)
|
||||||
|
(preprocess (pps ppx_blob))
|
||||||
|
(libraries webauthn dream cmdliner logs.cli lwt flash_message)
|
||||||
|
(optional))
|
140
bin/template.ml
Normal file
140
bin/template.ml
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
let page s b =
|
||||||
|
Printf.sprintf {|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WebAuthn Demo</title>
|
||||||
|
<script type="text/javascript" src="/static/base64.js"></script>
|
||||||
|
<script>
|
||||||
|
function bufferEncode(value) {
|
||||||
|
return base64js.fromByteArray(value)
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=/g, "");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script>%s</script>
|
||||||
|
</head><body>%s</body></html>|} s b
|
||||||
|
|
||||||
|
let overview notes authenticated_as users =
|
||||||
|
let authenticated_as =
|
||||||
|
match authenticated_as with
|
||||||
|
| None -> "<h2>Not authenticated</h2>"
|
||||||
|
| Some user -> Printf.sprintf {|<h2>Authenticated as %s</h2>
|
||||||
|
<form action="/logout" method="post"><input type="submit" value="Log out"/></form>
|
||||||
|
|} user
|
||||||
|
and links =
|
||||||
|
{|<h2>Register</h2><ul>
|
||||||
|
<li><a href="/register">register</a></li>
|
||||||
|
</ul>
|
||||||
|
|}
|
||||||
|
and users =
|
||||||
|
String.concat ""
|
||||||
|
("<h2>Users</h2><ul>" ::
|
||||||
|
Hashtbl.fold (fun name keys acc ->
|
||||||
|
let handles = List.map (fun (_, h, _) -> h) keys in
|
||||||
|
(Printf.sprintf "<li>%s [<a href=/authenticate/%s>authenticate</a>] (%s)</li>" name name (String.concat ", " handles)) :: acc)
|
||||||
|
users [] @ [ "</ul>" ])
|
||||||
|
in
|
||||||
|
page "" (String.concat "" (notes @ [authenticated_as;links;users]))
|
||||||
|
|
||||||
|
let register_view user challenge userid =
|
||||||
|
let script = Printf.sprintf {|
|
||||||
|
var publicKey = {
|
||||||
|
challenge: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)),
|
||||||
|
rp: {
|
||||||
|
id: "webauthn-demo.robur.coop",
|
||||||
|
name: "WebAuthn Demo from robur.coop"
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
id: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)),
|
||||||
|
displayName: "%s",
|
||||||
|
name: "%s"
|
||||||
|
},
|
||||||
|
pubKeyCredParams: [
|
||||||
|
{
|
||||||
|
type: "public-key",
|
||||||
|
alg: -7
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
navigator.credentials.create({ publicKey })
|
||||||
|
.then(function (credential) {
|
||||||
|
// send attestation response and client extensions
|
||||||
|
// to the server to proceed with the registration
|
||||||
|
// of the credential
|
||||||
|
console.log(credential);
|
||||||
|
// Move data into Arrays incase it is super long
|
||||||
|
let response = credential.response;
|
||||||
|
let attestationObject = new Uint8Array(response.attestationObject);
|
||||||
|
let clientDataJSON = new Uint8Array(response.clientDataJSON);
|
||||||
|
let rawId = new Uint8Array(credential.rawId);
|
||||||
|
|
||||||
|
var body =
|
||||||
|
JSON.stringify({
|
||||||
|
id: credential.id,
|
||||||
|
rawId: bufferEncode(rawId),
|
||||||
|
type: credential.type,
|
||||||
|
response: {
|
||||||
|
attestationObject: bufferEncode(attestationObject),
|
||||||
|
clientDataJSON: bufferEncode(clientDataJSON),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(body);
|
||||||
|
|
||||||
|
let headers = {'Content-type': "application/json; charset=utf-8"};
|
||||||
|
|
||||||
|
let request = new Request('/register_finish', { method: 'POST', body: body, headers: headers } );
|
||||||
|
fetch(request)
|
||||||
|
.then(function (response) {
|
||||||
|
console.log(response);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log("bad response: " + response.status);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|} challenge userid user user
|
||||||
|
and body =
|
||||||
|
Printf.sprintf {|
|
||||||
|
<p>Welcome %s.</p>
|
||||||
|
|} user
|
||||||
|
in
|
||||||
|
page script body
|
||||||
|
|
||||||
|
let authenticate_view data user =
|
||||||
|
let script =
|
||||||
|
Printf.sprintf {|
|
||||||
|
var request = JSON.parse('%s');
|
||||||
|
setTimeout(function() {
|
||||||
|
u2f.sign(
|
||||||
|
request.appId,
|
||||||
|
request.challenge,
|
||||||
|
request.registeredKeys,
|
||||||
|
function(data) {
|
||||||
|
if(data.errorCode) {
|
||||||
|
switch (data.errorCode) {
|
||||||
|
case 4:
|
||||||
|
alert("This device is not registered for this account.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
alert("U2F failed with error code: " + data.errorCode);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
document.getElementById('token').value = JSON.stringify(data);
|
||||||
|
document.getElementById('form').submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
|
|} data
|
||||||
|
and body =
|
||||||
|
Printf.sprintf {|
|
||||||
|
<p>Touch your U2F token to authenticate as %S.</p>
|
||||||
|
<form method="POST" action="/authenticate_finish" id="form">
|
||||||
|
<input type="hidden" name="token" id="token"/>
|
||||||
|
</form>
|
||||||
|
|} user
|
||||||
|
in
|
||||||
|
page script body
|
213
bin/webauthn_demo.ml
Normal file
213
bin/webauthn_demo.ml
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
open Lwt.Infix
|
||||||
|
|
||||||
|
let users = Hashtbl.create 7
|
||||||
|
|
||||||
|
module KhPubHashtbl = Hashtbl.Make(struct
|
||||||
|
type t = Webauthn.key_handle * Mirage_crypto_ec.P256.Dsa.pub
|
||||||
|
let cs_of_pub = Mirage_crypto_ec.P256.Dsa.pub_to_cstruct
|
||||||
|
let equal (kh, pub) (kh', pub') =
|
||||||
|
String.equal kh kh' && Cstruct.equal (cs_of_pub pub) (cs_of_pub pub')
|
||||||
|
let hash (kh, pub) = Hashtbl.hash (kh, Cstruct.to_string (cs_of_pub pub ))
|
||||||
|
end)
|
||||||
|
|
||||||
|
let counters = KhPubHashtbl.create 7
|
||||||
|
|
||||||
|
let check_counter kh_pub counter =
|
||||||
|
let r =
|
||||||
|
match KhPubHashtbl.find_opt counters kh_pub with
|
||||||
|
| Some counter' -> Int32.unsigned_compare counter counter' > 0
|
||||||
|
| None -> true
|
||||||
|
in
|
||||||
|
if r
|
||||||
|
then KhPubHashtbl.replace counters kh_pub counter;
|
||||||
|
r
|
||||||
|
|
||||||
|
let retrieve_form request =
|
||||||
|
Dream.body request >|= fun body ->
|
||||||
|
let form = Dream__pure.Formats.from_form_urlencoded body in
|
||||||
|
List.stable_sort (fun (key, _) (key', _) -> String.compare key key') form
|
||||||
|
|
||||||
|
let to_string err = Format.asprintf "%a" Webauthn.pp_error err
|
||||||
|
|
||||||
|
let gen_data ?(pad = false) ?alphabet length =
|
||||||
|
Base64.encode_string ~pad ?alphabet
|
||||||
|
(Cstruct.to_string (Mirage_crypto_rng.generate length))
|
||||||
|
|
||||||
|
|
||||||
|
let add_routes t =
|
||||||
|
let main req =
|
||||||
|
let authenticated_as = Dream.session "authenticated_as" req in
|
||||||
|
let flash = Flash_message.get_flash req |> List.map snd in
|
||||||
|
Dream.html (Template.overview flash authenticated_as users)
|
||||||
|
in
|
||||||
|
|
||||||
|
let register req =
|
||||||
|
let user =
|
||||||
|
match Dream.session "authenticated_as" req with
|
||||||
|
| None -> gen_data ~alphabet:Base64.uri_safe_alphabet 8
|
||||||
|
| Some username -> username
|
||||||
|
in
|
||||||
|
let _key_handles = match Hashtbl.find_opt users user with
|
||||||
|
| None -> []
|
||||||
|
| Some keys -> List.map (fun (_, kh, _) -> kh) keys
|
||||||
|
in
|
||||||
|
(* let challenge, rr = Webauthn.register_request ~key_handles t in *)
|
||||||
|
let challenge = gen_data ~pad:true 16
|
||||||
|
and userid = gen_data ~pad:true 16
|
||||||
|
in
|
||||||
|
Dream.put_session "challenge" challenge req >>= fun () ->
|
||||||
|
Dream.html (Template.register_view user challenge userid)
|
||||||
|
in
|
||||||
|
|
||||||
|
let register_finish req =
|
||||||
|
Dream.body req >>= fun body ->
|
||||||
|
Logs.info (fun m -> m "received body: %s" body);
|
||||||
|
(* let token = List.assoc "token" data in
|
||||||
|
let user = List.assoc "username" data in *)
|
||||||
|
let token = "a" and user = "b" in
|
||||||
|
match Dream.session "challenge" req with
|
||||||
|
| None ->
|
||||||
|
Logs.warn (fun m -> m "no challenge found");
|
||||||
|
Dream.respond ~status:`Bad_Request "Bad request."
|
||||||
|
| Some challenge ->
|
||||||
|
match Webauthn.register_response t challenge token with
|
||||||
|
| Error e ->
|
||||||
|
Logs.warn (fun m -> m "error %a" Webauthn.pp_error e);
|
||||||
|
let err = to_string e in
|
||||||
|
Flash_message.put_flash "" ("Registration failed " ^ err) req;
|
||||||
|
Dream.redirect req "/"
|
||||||
|
| Ok (key, kh, cert) ->
|
||||||
|
match Dream.session "authenticated_as" req, Hashtbl.find_opt users user with
|
||||||
|
| _, None ->
|
||||||
|
Logs.app (fun m -> m "registered %s" user);
|
||||||
|
Hashtbl.replace users user [ (key, kh, cert) ];
|
||||||
|
Dream.invalidate_session req >>= fun () ->
|
||||||
|
Flash_message.put_flash ""
|
||||||
|
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user)
|
||||||
|
req;
|
||||||
|
Dream.redirect req "/"
|
||||||
|
| Some session_user, Some keys ->
|
||||||
|
if String.equal user session_user then begin
|
||||||
|
Logs.app (fun m -> m "registered %s" user);
|
||||||
|
Hashtbl.replace users user ((key, kh, cert) :: keys) ;
|
||||||
|
Dream.invalidate_session req >>= fun () ->
|
||||||
|
Flash_message.put_flash ""
|
||||||
|
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user)
|
||||||
|
req;
|
||||||
|
Dream.redirect req "/"
|
||||||
|
end else
|
||||||
|
Dream.respond ~status:`Forbidden "Forbidden."
|
||||||
|
| None, Some _keys ->
|
||||||
|
Dream.respond ~status:`Forbidden "Forbidden."
|
||||||
|
in
|
||||||
|
|
||||||
|
let authenticate req =
|
||||||
|
let user = Dream.param "user" req in
|
||||||
|
match Hashtbl.find_opt users user with
|
||||||
|
| None ->
|
||||||
|
Logs.warn (fun m -> m "no user found");
|
||||||
|
Dream.respond ~status:`Bad_Request "Bad request."
|
||||||
|
| Some keys ->
|
||||||
|
let khs = List.map (fun (_, kh, _) -> kh) keys in
|
||||||
|
let challenge, ar = Webauthn.authentication_request t khs in
|
||||||
|
Dream.put_session "challenge" challenge req >>= fun () ->
|
||||||
|
Dream.put_session "challenge_user" user req >>= fun () ->
|
||||||
|
Dream.html (Template.authenticate_view ar user)
|
||||||
|
in
|
||||||
|
|
||||||
|
let authenticate_finish req =
|
||||||
|
retrieve_form req >>= fun data ->
|
||||||
|
match Dream.session "challenge_user" req with
|
||||||
|
| None -> Dream.respond ~status:`Internal_Server_Error "Internal server error."
|
||||||
|
| Some user ->
|
||||||
|
match Dream.session "challenge" req with
|
||||||
|
| None ->
|
||||||
|
Logs.warn (fun m -> m "no challenge found");
|
||||||
|
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 kh_keys = List.map (fun (key, kh, _) -> kh, key) keys in
|
||||||
|
let token = List.assoc "token" data in
|
||||||
|
match Webauthn.authentication_response t kh_keys challenge token with
|
||||||
|
| Ok (key_handle_pubkey, _user_present, counter) ->
|
||||||
|
if check_counter key_handle_pubkey 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.redirect req "/"
|
||||||
|
end else begin
|
||||||
|
Logs.warn (fun m -> m "key handle %S for user %S: counter not strictly increasing! \
|
||||||
|
Got %ld, expected >%ld. webauthn device compromised?"
|
||||||
|
(fst key_handle_pubkey) user counter (KhPubHashtbl.find counters key_handle_pubkey));
|
||||||
|
Flash_message.put_flash "" "Authentication failure: key compromised?" req;
|
||||||
|
Dream.redirect req "/"
|
||||||
|
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.redirect req "/"
|
||||||
|
in
|
||||||
|
|
||||||
|
let logout req =
|
||||||
|
Dream.invalidate_session req >>= fun () ->
|
||||||
|
Dream.redirect req "/"
|
||||||
|
in
|
||||||
|
|
||||||
|
let base64 _req =
|
||||||
|
Dream.respond ~headers:[("Content-type", "application/javascript")]
|
||||||
|
[%blob "base64.js"]
|
||||||
|
in
|
||||||
|
|
||||||
|
Dream.router [
|
||||||
|
Dream.get "/" main;
|
||||||
|
Dream.get "/register" register;
|
||||||
|
Dream.post "/register_finish" register_finish;
|
||||||
|
Dream.get "/authenticate/:user" authenticate;
|
||||||
|
Dream.post "/authenticate_finish" authenticate_finish;
|
||||||
|
Dream.post "/logout" logout;
|
||||||
|
Dream.get "/static/base64.js" base64;
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
let setup_app level port host application_id https =
|
||||||
|
let webauthn = Webauthn.create application_id in
|
||||||
|
let level = match level with None -> None | Some Logs.Debug -> Some `Debug | Some Info -> Some `Info | Some Warning -> Some `Warning | Some Error -> Some `Error | Some App -> None in
|
||||||
|
Dream.initialize_log ?level ();
|
||||||
|
Dream.run ~port ~interface:host ~https
|
||||||
|
@@ Dream.logger
|
||||||
|
@@ Dream.memory_sessions
|
||||||
|
@@ Flash_message.flash_messages
|
||||||
|
@@ add_routes webauthn
|
||||||
|
@@ Dream.not_found
|
||||||
|
|
||||||
|
open Cmdliner
|
||||||
|
|
||||||
|
let port =
|
||||||
|
let doc = "port" in
|
||||||
|
Arg.(value & opt int 5000 & info [ "p"; "port" ] ~doc)
|
||||||
|
|
||||||
|
let host =
|
||||||
|
let doc = "host" in
|
||||||
|
Arg.(value & opt string "0.0.0.0" & info [ "h"; "host" ] ~doc)
|
||||||
|
|
||||||
|
let application_id =
|
||||||
|
let doc = "the webauthn application id - usually protocol://host(:port)" in
|
||||||
|
Arg.(value & opt string "https://webauthn-demo.robur.coop" & info [ "application-id" ] ~doc)
|
||||||
|
|
||||||
|
let tls =
|
||||||
|
let doc = "tls" in
|
||||||
|
Arg.(value & flag & info [ "tls" ] ~doc)
|
||||||
|
|
||||||
|
let () =
|
||||||
|
let term = Term.(pure setup_app $ Logs_cli.level () $ port $ host $ application_id $ tls) in
|
||||||
|
let info = Term.info "Webauthn app" ~doc:"Webauthn app" ~man:[] in
|
||||||
|
match Term.eval (term, info) with
|
||||||
|
| `Ok () -> exit 0
|
||||||
|
| `Error _ -> exit 1
|
||||||
|
| _ -> exit 0
|
2
dune-project
Normal file
2
dune-project
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
(lang dune 2.7)
|
||||||
|
(name webauthn)
|
3
flash_message/dune
Normal file
3
flash_message/dune
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
(library
|
||||||
|
(name flash_message)
|
||||||
|
(libraries dream))
|
51
flash_message/flash_message.ml
Normal file
51
flash_message/flash_message.ml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
open Lwt.Syntax
|
||||||
|
|
||||||
|
let five_minutes = 5. *. 60.
|
||||||
|
|
||||||
|
|
||||||
|
let storage = Dream.new_local ~name:"dream.flash_message" ()
|
||||||
|
|
||||||
|
|
||||||
|
let flash_cookie = "dream.flash_message"
|
||||||
|
|
||||||
|
|
||||||
|
let flash_messages inner_handler request =
|
||||||
|
let outbox = ref [] in
|
||||||
|
let request = Dream.with_local storage outbox request in
|
||||||
|
let* response = inner_handler request in
|
||||||
|
Lwt.return(
|
||||||
|
let entries = List.rev !outbox in
|
||||||
|
let content = List.fold_right (fun (x,y) a -> `String x :: `String y :: a) entries [] in
|
||||||
|
let value = `List content |> Yojson.Basic.to_string in
|
||||||
|
Dream.set_cookie flash_cookie value request response ~max_age:five_minutes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
let (|>?) =
|
||||||
|
Option.bind
|
||||||
|
|
||||||
|
|
||||||
|
let get_flash request =
|
||||||
|
let rec group x = match x with
|
||||||
|
| x1::x2::rest -> (x1, x2) :: group rest
|
||||||
|
| _ -> []
|
||||||
|
in
|
||||||
|
let unpack u = match u with
|
||||||
|
| `String x -> x
|
||||||
|
| _ -> failwith "Bad flash message content" in
|
||||||
|
let x = Dream.cookie flash_cookie request
|
||||||
|
|>? fun value ->
|
||||||
|
match Yojson.Basic.from_string value with
|
||||||
|
| `List y -> Some (group @@ List.map unpack y)
|
||||||
|
| _ -> None
|
||||||
|
in Option.value x ~default:[]
|
||||||
|
|
||||||
|
|
||||||
|
let put_flash category message request =
|
||||||
|
let outbox = match Dream.local storage request with
|
||||||
|
| Some outbox -> outbox
|
||||||
|
| None ->
|
||||||
|
let message = "Missing flash message middleware" in
|
||||||
|
Logs.err (fun log -> log "%s" message);
|
||||||
|
failwith message in
|
||||||
|
outbox := (category, message) :: !outbox
|
6
src/dune
Normal file
6
src/dune
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
(library
|
||||||
|
(name webauthn)
|
||||||
|
(public_name webauthn)
|
||||||
|
(preprocess
|
||||||
|
(pps ppx_deriving_yojson))
|
||||||
|
(libraries mirage-crypto-rng yojson mirage-crypto-ec x509 base64))
|
48
src/webauthn.ml
Normal file
48
src/webauthn.ml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
type key_handle = string
|
||||||
|
|
||||||
|
type error = [
|
||||||
|
`None
|
||||||
|
]
|
||||||
|
|
||||||
|
let pp_error _ppf _e = ()
|
||||||
|
|
||||||
|
type t = unit
|
||||||
|
|
||||||
|
type challenge = string
|
||||||
|
|
||||||
|
type typ = Public_key [@name "public-key"]
|
||||||
|
[@@deriving yojson]
|
||||||
|
|
||||||
|
type response_raw = {
|
||||||
|
attestation_object : string [@key "attestationObject"];
|
||||||
|
client_data_json : string [@key "clientDataJSON"];
|
||||||
|
} [@@deriving of_yojson]
|
||||||
|
|
||||||
|
type attestation_raw = {
|
||||||
|
id : string;
|
||||||
|
raw_id : string [@key "rawId"];
|
||||||
|
typ : typ [@key "type"];
|
||||||
|
response : response_raw;
|
||||||
|
} [@@deriving of_yojson]
|
||||||
|
|
||||||
|
let b64_enc = Base64.(encode_string ~pad:false ~alphabet:uri_safe_alphabet)
|
||||||
|
|
||||||
|
let lift_err f = function Ok _ as a -> a | Error x -> Error (f x)
|
||||||
|
|
||||||
|
let b64_dec thing s =
|
||||||
|
lift_err
|
||||||
|
(function `Msg m -> `Base64_decoding (thing, m, s))
|
||||||
|
Base64.(decode ~pad:false ~alphabet:uri_safe_alphabet s)
|
||||||
|
|
||||||
|
let _ = ignore b64_enc; ignore b64_dec
|
||||||
|
|
||||||
|
let create _app = ()
|
||||||
|
|
||||||
|
let register_request ?key_handles:_ _t = "foo", "bar"
|
||||||
|
|
||||||
|
let register_response _t _challenge _data = Error `None
|
||||||
|
|
||||||
|
let authentication_request _t _handles = "foo", "bar"
|
||||||
|
|
||||||
|
let authentication_response _t _handles _challenges _data =
|
||||||
|
Error `None
|
26
src/webauthn.mli
Normal file
26
src/webauthn.mli
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
type key_handle = string
|
||||||
|
|
||||||
|
type error
|
||||||
|
|
||||||
|
val pp_error : Format.formatter -> error -> unit
|
||||||
|
|
||||||
|
type t
|
||||||
|
|
||||||
|
val create : string -> t
|
||||||
|
|
||||||
|
type challenge = string
|
||||||
|
|
||||||
|
val register_request : ?key_handles:key_handle list -> t -> challenge * string
|
||||||
|
|
||||||
|
val register_response : t -> challenge -> string ->
|
||||||
|
(Mirage_crypto_ec.P256.Dsa.pub * key_handle * X509.Certificate.t,
|
||||||
|
error) result
|
||||||
|
|
||||||
|
val authentication_request : t -> key_handle list ->
|
||||||
|
challenge * string
|
||||||
|
|
||||||
|
val authentication_response : t ->
|
||||||
|
(key_handle * Mirage_crypto_ec.P256.Dsa.pub) list ->
|
||||||
|
challenge -> string ->
|
||||||
|
((key_handle * Mirage_crypto_ec.P256.Dsa.pub) * bool * int32, error) result
|
||||||
|
|
0
webauthn.opam
Normal file
0
webauthn.opam
Normal file
Loading…
Reference in a new issue