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:
parent
89703fe795
commit
44efdb1bae
5 changed files with 89 additions and 30 deletions
|
@ -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))
|
||||
}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
|
||||
|
|
|
@ -11,6 +11,9 @@ let page s b =
|
|||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
function bufferDecode(value) {
|
||||
return base64js.toByteArray(value);
|
||||
}
|
||||
</script>
|
||||
<script>%s</script>
|
||||
</head><body>%s</body></html>|} 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 =
|
|||
<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" onmousedown="doit()">Register</button>
|
||||
</form>
|
||||
|} 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) {
|
||||
|
|
|
@ -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! <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;
|
||||
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! <a href=\"/authenticate/%s\">[authenticate]</a><br/>Certificate:<br/><pre>%s</pre>" 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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue