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) {
|
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
|
||||||
|
@ -115,4 +111,4 @@ var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||||
|
|
||||||
exports.toByteArray = b64ToByteArray
|
exports.toByteArray = b64ToByteArray
|
||||||
exports.fromByteArray = uint8ToBase64
|
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, "_")
|
||||||
.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) {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue