From a67f7cb7c8ef8df20a00c748e1b624aad2d9487a Mon Sep 17 00:00:00 2001 From: Fabian-Lars Date: Wed, 29 Nov 2023 12:56:26 +0100 Subject: [PATCH] chore(authenticator): Inline u2f crate (#780) * chore(authenticator): Inline u2f crate * update chrono * update rustix * clippy --- Cargo.lock | 87 +++----- plugins/authenticator/Cargo.toml | 4 +- plugins/authenticator/src/error.rs | 2 +- plugins/authenticator/src/lib.rs | 1 + plugins/authenticator/src/u2f.rs | 6 +- plugins/authenticator/src/u2f_crate/LICENSE | 8 + .../src/u2f_crate/authorization.rs | 65 ++++++ plugins/authenticator/src/u2f_crate/crypto.rs | 156 ++++++++++++++ .../authenticator/src/u2f_crate/messages.rs | 54 +++++ plugins/authenticator/src/u2f_crate/mod.rs | 12 ++ .../authenticator/src/u2f_crate/protocol.rs | 191 ++++++++++++++++++ .../authenticator/src/u2f_crate/register.rs | 101 +++++++++ .../authenticator/src/u2f_crate/u2ferror.rs | 39 ++++ plugins/authenticator/src/u2f_crate/util.rs | 66 ++++++ .../src/platform_impl/windows.rs | 2 +- 15 files changed, 725 insertions(+), 69 deletions(-) create mode 100644 plugins/authenticator/src/u2f_crate/LICENSE create mode 100644 plugins/authenticator/src/u2f_crate/authorization.rs create mode 100644 plugins/authenticator/src/u2f_crate/crypto.rs create mode 100644 plugins/authenticator/src/u2f_crate/messages.rs create mode 100644 plugins/authenticator/src/u2f_crate/mod.rs create mode 100644 plugins/authenticator/src/u2f_crate/protocol.rs create mode 100644 plugins/authenticator/src/u2f_crate/register.rs create mode 100644 plugins/authenticator/src/u2f_crate/u2ferror.rs create mode 100644 plugins/authenticator/src/u2f_crate/util.rs diff --git a/Cargo.lock b/Cargo.lock index 622988cf..4b13ca10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,7 +219,7 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.25", + "rustix 0.37.27", "slab", "socket2", "waker-fn", @@ -247,7 +247,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 0.37.25", + "rustix 0.37.27", "signal-hook", "windows-sys 0.48.0", ] @@ -368,12 +368,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - [[package]] name = "base64" version = "0.13.1" @@ -665,18 +659,17 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.1", ] [[package]] @@ -2233,7 +2226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.7", + "rustix 0.38.8", "windows-sys 0.48.0", ] @@ -2523,7 +2516,7 @@ dependencies = [ "dirs-next", "objc-foundation", "objc_id", - "time 0.3.20", + "time", ] [[package]] @@ -3296,7 +3289,7 @@ dependencies = [ "line-wrap", "quick-xml 0.29.0", "serde", - "time 0.3.20", + "time", ] [[package]] @@ -3751,9 +3744,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.25" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", @@ -3765,9 +3758,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.7" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ "bitflags 2.3.3", "errno", @@ -4005,7 +3998,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time 0.3.20", + "time", ] [[package]] @@ -4296,7 +4289,7 @@ dependencies = [ "smallvec", "sqlformat", "thiserror", - "time 0.3.20", + "time", "tokio", "tokio-stream", "tracing", @@ -4381,7 +4374,7 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", - "time 0.3.20", + "time", "tracing", "whoami", ] @@ -4421,7 +4414,7 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", - "time 0.3.20", + "time", "tracing", "whoami", ] @@ -4444,7 +4437,7 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", - "time 0.3.20", + "time", "tracing", "url", ] @@ -4807,7 +4800,7 @@ dependencies = [ "sha2 0.10.7", "tauri-utils", "thiserror", - "time 0.3.20", + "time", "uuid", "walkdir", ] @@ -4832,9 +4825,12 @@ version = "0.0.0" dependencies = [ "authenticator", "base64 0.21.2", + "byteorder", + "bytes 0.4.12", "chrono", "log", "once_cell", + "openssl", "rand 0.8.5", "rusty-fork", "serde", @@ -4842,7 +4838,6 @@ dependencies = [ "sha2 0.10.7", "tauri", "thiserror", - "u2f", ] [[package]] @@ -4905,7 +4900,7 @@ dependencies = [ "serde_json", "serde_repr", "tauri", - "time 0.3.20", + "time", ] [[package]] @@ -4957,7 +4952,7 @@ dependencies = [ "sqlx", "tauri", "thiserror", - "time 0.3.20", + "time", "tokio", ] @@ -5135,7 +5130,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.7", + "rustix 0.38.8", "windows-sys 0.48.0", ] @@ -5186,17 +5181,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.20" @@ -5472,23 +5456,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "u2f" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2f285392366190c4d46823458f4543ac0f35174759c78e80c5baa39e1f7aa4f" -dependencies = [ - "base64 0.11.0", - "byteorder", - "bytes 0.4.12", - "chrono", - "openssl", - "serde", - "serde_derive", - "serde_json", - "time 0.1.45", -] - [[package]] name = "uds_windows" version = "1.0.2" @@ -5677,12 +5644,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/plugins/authenticator/Cargo.toml b/plugins/authenticator/Cargo.toml index 007e63f4..adaab2fc 100644 --- a/plugins/authenticator/Cargo.toml +++ b/plugins/authenticator/Cargo.toml @@ -19,8 +19,10 @@ authenticator = "0.3.1" once_cell = "1" sha2 = "0.10" base64 = "0.21" -u2f = "0.2" chrono = "0.4" +bytes = "0.4" +byteorder = "1.3" +openssl = "0.10" [dev-dependencies] rand = "0.8" diff --git a/plugins/authenticator/src/error.rs b/plugins/authenticator/src/error.rs index 87a393d2..db731e8f 100644 --- a/plugins/authenticator/src/error.rs +++ b/plugins/authenticator/src/error.rs @@ -7,7 +7,7 @@ pub enum Error { #[error(transparent)] JSON(#[from] serde_json::Error), #[error(transparent)] - U2F(#[from] u2f::u2ferror::U2fError), + U2F(#[from] crate::u2f_crate::u2ferror::U2fError), #[error(transparent)] Auth(#[from] authenticator::errors::AuthenticatorError), } diff --git a/plugins/authenticator/src/lib.rs b/plugins/authenticator/src/lib.rs index ef889e21..36ed0228 100644 --- a/plugins/authenticator/src/lib.rs +++ b/plugins/authenticator/src/lib.rs @@ -5,6 +5,7 @@ mod auth; mod error; mod u2f; +mod u2f_crate; use tauri::{ plugin::{Builder as PluginBuilder, TauriPlugin}, diff --git a/plugins/authenticator/src/u2f.rs b/plugins/authenticator/src/u2f.rs index e8bd5de9..8a443b16 100644 --- a/plugins/authenticator/src/u2f.rs +++ b/plugins/authenticator/src/u2f.rs @@ -2,13 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use crate::u2f_crate::messages::*; +use crate::u2f_crate::protocol::*; +use crate::u2f_crate::register::*; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use chrono::prelude::*; use serde::Serialize; use std::convert::Into; -use u2f::messages::*; -use u2f::protocol::*; -use u2f::register::*; static VERSION: &str = "U2F_V2"; diff --git a/plugins/authenticator/src/u2f_crate/LICENSE b/plugins/authenticator/src/u2f_crate/LICENSE new file mode 100644 index 00000000..d26d5f6c --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/LICENSE @@ -0,0 +1,8 @@ +Copyright (c) 2017 + +Licensed under either of + + * Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) + * MIT license (http://opensource.org/licenses/MIT) + +at your option. \ No newline at end of file diff --git a/plugins/authenticator/src/u2f_crate/authorization.rs b/plugins/authenticator/src/u2f_crate/authorization.rs new file mode 100644 index 00000000..611ab832 --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/authorization.rs @@ -0,0 +1,65 @@ +// Copyright 2021 Flavio Oliveira +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use bytes::{Buf, BufMut}; +use openssl::sha::sha256; +use serde::Serialize; +use std::io::Cursor; + +use crate::u2f_crate::u2ferror::U2fError; + +/// The `Result` type used in this crate. +type Result = ::std::result::Result; + +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Authorization { + pub counter: u32, + pub user_presence: bool, +} + +pub fn parse_sign_response( + app_id: String, + client_data: Vec, + public_key: Vec, + sign_data: Vec, +) -> Result { + if sign_data.len() <= 5 { + return Err(U2fError::InvalidSignatureData); + } + + let user_presence_flag = &sign_data[0]; + let counter = &sign_data[1..=4]; + let signature = &sign_data[5..]; + + // Let's build the msg to verify the signature + let app_id_hash = sha256(&app_id.into_bytes()); + let client_data_hash = sha256(&client_data[..]); + + let mut msg = vec![]; + msg.put(app_id_hash.as_ref()); + msg.put(*user_presence_flag); + msg.put(counter); + msg.put(client_data_hash.as_ref()); + + let public_key = super::crypto::NISTP256Key::from_bytes(&public_key)?; + + // The signature is to be verified by the relying party using the public key obtained during registration. + let verified = public_key.verify_signature(signature, msg.as_ref())?; + if !verified { + return Err(U2fError::BadSignature); + } + + let authorization = Authorization { + counter: get_counter(counter), + user_presence: true, + }; + + Ok(authorization) +} + +fn get_counter(counter: &[u8]) -> u32 { + let mut buf = Cursor::new(counter); + buf.get_u32_be() +} diff --git a/plugins/authenticator/src/u2f_crate/crypto.rs b/plugins/authenticator/src/u2f_crate/crypto.rs new file mode 100644 index 00000000..32379805 --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/crypto.rs @@ -0,0 +1,156 @@ +// Copyright 2021 Flavio Oliveira +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Cryptographic operation wrapper for Webauthn. This module exists to +//! allow ease of auditing, safe operation wrappers for the webauthn library, +//! and cryptographic provider abstraction. This module currently uses OpenSSL +//! as the cryptographic primitive provider. + +// Source can be found here: https://github.com/Firstyear/webauthn-rs/blob/master/src/crypto.rs + +#![allow(non_camel_case_types)] + +use openssl::{bn, ec, hash, nid, sign, x509}; +use std::convert::TryFrom; + +// use super::constants::*; +use crate::u2f_crate::u2ferror::U2fError; +use openssl::pkey::Public; + +// use super::proto::*; + +// Why OpenSSL over another rust crate? +// - Well, the openssl crate allows us to reconstruct a public key from the +// x/y group coords, where most others want a pkcs formatted structure. As +// a result, it's easiest to use openssl as it gives us exactly what we need +// for these operations, and despite it's many challenges as a library, it +// has resources and investment into it's maintenance, so we can a least +// assert a higher level of confidence in it that . + +// Object({Integer(-3): Bytes([48, 185, 178, 204, 113, 186, 105, 138, 190, 33, 160, 46, 131, 253, 100, 177, 91, 243, 126, 128, 245, 119, 209, 59, 186, 41, 215, 196, 24, 222, 46, 102]), Integer(-2): Bytes([158, 212, 171, 234, 165, 197, 86, 55, 141, 122, 253, 6, 92, 242, 242, 114, 158, 221, 238, 163, 127, 214, 120, 157, 145, 226, 232, 250, 144, 150, 218, 138]), Integer(-1): U64(1), Integer(1): U64(2), Integer(3): I64(-7)}) +// + +/// An X509PublicKey. This is what is otherwise known as a public certificate +/// which comprises a public key and other signed metadata related to the issuer +/// of the key. +pub struct X509PublicKey { + pubk: x509::X509, +} + +impl std::fmt::Debug for X509PublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "X509PublicKey") + } +} + +impl TryFrom<&[u8]> for X509PublicKey { + type Error = U2fError; + + // Must be DER bytes. If you have PEM, base64decode first! + fn try_from(d: &[u8]) -> Result { + let pubk = x509::X509::from_der(d)?; + Ok(X509PublicKey { pubk }) + } +} + +impl X509PublicKey { + pub(crate) fn common_name(&self) -> Option { + let cert = &self.pubk; + + let subject = cert.subject_name(); + let common = subject + .entries_by_nid(openssl::nid::Nid::COMMONNAME) + .next() + .map(|b| b.data().as_slice()); + + if let Some(common) = common { + std::str::from_utf8(common).ok().map(|s| s.to_string()) + } else { + None + } + } + + pub(crate) fn is_secp256r1(&self) -> Result { + // Can we get the public key? + let pk = self.pubk.public_key()?; + + let ec_key = pk.ec_key()?; + + ec_key.check_key()?; + + let ec_grpref = ec_key.group(); + + let ec_curve = ec_grpref.curve_name().ok_or(U2fError::OpenSSLNoCurveName)?; + + Ok(ec_curve == nid::Nid::X9_62_PRIME256V1) + } + + pub(crate) fn verify_signature( + &self, + signature: &[u8], + verification_data: &[u8], + ) -> Result { + let pkey = self.pubk.public_key()?; + + // TODO: Should this determine the hash type from the x509 cert? Or other? + let mut verifier = sign::Verifier::new(hash::MessageDigest::sha256(), &pkey)?; + verifier.update(verification_data)?; + Ok(verifier.verify(signature)?) + } +} + +pub struct NISTP256Key { + /// The key's public X coordinate. + pub x: [u8; 32], + /// The key's public Y coordinate. + pub y: [u8; 32], +} + +impl NISTP256Key { + pub fn from_bytes(public_key_bytes: &[u8]) -> Result { + if public_key_bytes.len() != 65 { + return Err(U2fError::InvalidPublicKey); + } + + if public_key_bytes[0] != 0x04 { + return Err(U2fError::InvalidPublicKey); + } + + let mut x: [u8; 32] = Default::default(); + x.copy_from_slice(&public_key_bytes[1..=32]); + + let mut y: [u8; 32] = Default::default(); + y.copy_from_slice(&public_key_bytes[33..=64]); + + Ok(NISTP256Key { x, y }) + } + + fn get_key(&self) -> Result, U2fError> { + let ec_group = ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)?; + + let xbn = bn::BigNum::from_slice(&self.x)?; + let ybn = bn::BigNum::from_slice(&self.y)?; + + let ec_key = openssl::ec::EcKey::from_public_key_affine_coordinates(&ec_group, &xbn, &ybn)?; + + // Validate the key is sound. IIRC this actually checks the values + // are correctly on the curve as specified + ec_key.check_key()?; + + Ok(ec_key) + } + + pub fn verify_signature( + &self, + signature: &[u8], + verification_data: &[u8], + ) -> Result { + let pkey = self.get_key()?; + + let signature = openssl::ecdsa::EcdsaSig::from_der(signature)?; + let hash = openssl::sha::sha256(verification_data); + + Ok(signature.verify(hash.as_ref(), &pkey)?) + } +} diff --git a/plugins/authenticator/src/u2f_crate/messages.rs b/plugins/authenticator/src/u2f_crate/messages.rs new file mode 100644 index 00000000..6146c83b --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/messages.rs @@ -0,0 +1,54 @@ +// Copyright 2021 Flavio Oliveira +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// As defined by FIDO U2F Javascript API. +// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-javascript-api.html#registration + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct U2fRegisterRequest { + pub app_id: String, + pub register_requests: Vec, + pub registered_keys: Vec, +} + +#[derive(Serialize)] +pub struct RegisterRequest { + pub version: String, + pub challenge: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RegisteredKey { + pub version: String, + pub key_handle: Option, + pub app_id: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegisterResponse { + pub registration_data: String, + pub version: String, + pub client_data: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct U2fSignRequest { + pub app_id: String, + pub challenge: String, + pub registered_keys: Vec, +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignResponse { + pub key_handle: String, + pub signature_data: String, + pub client_data: String, +} diff --git a/plugins/authenticator/src/u2f_crate/mod.rs b/plugins/authenticator/src/u2f_crate/mod.rs new file mode 100644 index 00000000..0aaebf6f --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2021 Flavio Oliveira +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod util; + +pub mod authorization; +mod crypto; +pub mod messages; +pub mod protocol; +pub mod register; +pub mod u2ferror; diff --git a/plugins/authenticator/src/u2f_crate/protocol.rs b/plugins/authenticator/src/u2f_crate/protocol.rs new file mode 100644 index 00000000..fc7343ea --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/protocol.rs @@ -0,0 +1,191 @@ +// Copyright 2021 Flavio Oliveira +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::u2f_crate::authorization::*; +use crate::u2f_crate::messages::*; +use crate::u2f_crate::register::*; +use crate::u2f_crate::u2ferror::U2fError; +use crate::u2f_crate::util::*; + +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use chrono::prelude::*; +use chrono::Duration; +use serde::{Deserialize, Serialize}; + +type Result = ::std::result::Result; + +#[derive(Clone)] +pub struct U2f { + app_id: String, +} + +#[derive(Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Challenge { + pub app_id: String, + pub challenge: String, + pub timestamp: String, +} + +impl Challenge { + // Not used in this plugin. + #[allow(dead_code)] + pub fn new() -> Self { + Challenge { + app_id: String::new(), + challenge: String::new(), + timestamp: String::new(), + } + } +} + +impl U2f { + // The app ID is a string used to uniquely identify an U2F app + pub fn new(app_id: String) -> Self { + U2f { app_id } + } + + // Not used in this plugin. + #[allow(dead_code)] + pub fn generate_challenge(&self) -> Result { + let utc: DateTime = Utc::now(); + + let challenge_bytes = generate_challenge(32)?; + let challenge = Challenge { + challenge: URL_SAFE_NO_PAD.encode(challenge_bytes), + timestamp: format!("{:?}", utc), + app_id: self.app_id.clone(), + }; + + Ok(challenge.clone()) + } + + // Not used in this plugin. + #[allow(dead_code)] + pub fn request( + &self, + challenge: Challenge, + registrations: Vec, + ) -> Result { + let u2f_request = U2fRegisterRequest { + app_id: self.app_id.clone(), + register_requests: self.register_request(challenge), + registered_keys: self.registered_keys(registrations), + }; + + Ok(u2f_request) + } + + fn register_request(&self, challenge: Challenge) -> Vec { + let mut requests: Vec = vec![]; + + let request = RegisterRequest { + version: U2F_V2.into(), + challenge: challenge.challenge, + }; + requests.push(request); + + requests + } + + pub fn register_response( + &self, + challenge: Challenge, + response: RegisterResponse, + ) -> Result { + if expiration(challenge.timestamp) > Duration::seconds(300) { + return Err(U2fError::ChallengeExpired); + } + + let registration_data: Vec = URL_SAFE_NO_PAD + .decode(&response.registration_data[..]) + .unwrap(); + let client_data: Vec = URL_SAFE_NO_PAD.decode(&response.client_data[..]).unwrap(); + + parse_registration(challenge.app_id, client_data, registration_data) + } + + fn registered_keys(&self, registrations: Vec) -> Vec { + let mut keys: Vec = vec![]; + + for registration in registrations { + keys.push(get_registered_key( + self.app_id.clone(), + registration.key_handle, + )); + } + + keys + } + + // Not used in this plugin. + #[allow(dead_code)] + pub fn sign_request( + &self, + challenge: Challenge, + registrations: Vec, + ) -> U2fSignRequest { + let mut keys: Vec = vec![]; + + for registration in registrations { + keys.push(get_registered_key( + self.app_id.clone(), + registration.key_handle, + )); + } + + let signed_request = U2fSignRequest { + app_id: self.app_id.clone(), + challenge: URL_SAFE_NO_PAD.encode(challenge.challenge.as_bytes()), + registered_keys: keys, + }; + + signed_request + } + + pub fn sign_response( + &self, + challenge: Challenge, + reg: Registration, + sign_resp: SignResponse, + counter: u32, + ) -> Result { + if expiration(challenge.timestamp) > Duration::seconds(300) { + return Err(U2fError::ChallengeExpired); + } + + if sign_resp.key_handle != get_encoded(®.key_handle[..]) { + return Err(U2fError::WrongKeyHandler); + } + + let client_data: Vec = URL_SAFE_NO_PAD + .decode(&sign_resp.client_data[..]) + .map_err(|_e| U2fError::InvalidClientData)?; + let sign_data: Vec = URL_SAFE_NO_PAD + .decode(&sign_resp.signature_data[..]) + .map_err(|_e| U2fError::InvalidSignatureData)?; + + let public_key = reg.pub_key; + + let auth = parse_sign_response( + self.app_id.clone(), + client_data.clone(), + public_key, + sign_data.clone(), + ); + + match auth { + Ok(ref res) => { + // CounterTooLow is raised when the counter value received from the device is + // lower than last stored counter value. + if res.counter < counter { + Err(U2fError::CounterTooLow) + } else { + Ok(res.counter) + } + } + Err(e) => Err(e), + } + } +} diff --git a/plugins/authenticator/src/u2f_crate/register.rs b/plugins/authenticator/src/u2f_crate/register.rs new file mode 100644 index 00000000..3717b003 --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/register.rs @@ -0,0 +1,101 @@ +// Copyright 2021 Flavio Oliveira +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use byteorder::{BigEndian, ByteOrder}; +use bytes::{BufMut, Bytes}; +use openssl::sha::sha256; +use serde::Serialize; + +use crate::u2f_crate::messages::RegisteredKey; +use crate::u2f_crate::u2ferror::U2fError; +use crate::u2f_crate::util::*; +use std::convert::TryFrom; + +/// The `Result` type used in this crate. +type Result = ::std::result::Result; + +// Single enrolment or pairing between an application and a token. +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Registration { + pub key_handle: Vec, + pub pub_key: Vec, + + // AttestationCert can be null for Authenticate requests. + pub attestation_cert: Option>, + pub device_name: Option, +} + +pub fn parse_registration( + app_id: String, + client_data: Vec, + registration_data: Vec, +) -> Result { + let reserved_byte = registration_data[0]; + if reserved_byte != 0x05 { + return Err(U2fError::InvalidReservedByte); + } + + let mut mem = Bytes::from(registration_data); + + //Start parsing ... advance the reserved byte. + let _ = mem.split_to(1); + + // P-256 NIST elliptic curve + let public_key = mem.split_to(65); + + // Key Handle + let key_handle_size = mem.split_to(1); + let key_len = BigEndian::read_uint(&key_handle_size[..], 1); + let key_handle = mem.split_to(key_len as usize); + + // The certificate length needs to be inferred by parsing. + let cert_len = asn_length(mem.clone()).unwrap(); + let attestation_certificate = mem.split_to(cert_len); + + // Remaining data corresponds to the signature + let signature = mem; + + // Let's build the msg to verify the signature + let app_id_hash = sha256(&app_id.into_bytes()); + let client_data_hash = sha256(&client_data[..]); + + let mut msg = vec![0x00]; // A byte reserved for future use [1 byte] with the value 0x00 + msg.put(app_id_hash.as_ref()); + msg.put(client_data_hash.as_ref()); + msg.put(key_handle.clone()); + msg.put(public_key.clone()); + + // The signature is to be verified by the relying party using the public key certified + // in the attestation certificate. + let cerificate_public_key = + super::crypto::X509PublicKey::try_from(&attestation_certificate[..])?; + + if !(cerificate_public_key.is_secp256r1()?) { + return Err(U2fError::BadCertificate); + } + + let verified = cerificate_public_key.verify_signature(&signature[..], &msg[..])?; + + if !verified { + return Err(U2fError::BadCertificate); + } + + let registration = Registration { + key_handle: key_handle[..].to_vec(), + pub_key: public_key[..].to_vec(), + attestation_cert: Some(attestation_certificate[..].to_vec()), + device_name: cerificate_public_key.common_name(), + }; + + Ok(registration) +} + +pub fn get_registered_key(app_id: String, key_handle: Vec) -> RegisteredKey { + RegisteredKey { + app_id, + version: U2F_V2.into(), + key_handle: Some(get_encoded(key_handle.as_slice())), + } +} diff --git a/plugins/authenticator/src/u2f_crate/u2ferror.rs b/plugins/authenticator/src/u2f_crate/u2ferror.rs new file mode 100644 index 00000000..9ae8fa92 --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/u2ferror.rs @@ -0,0 +1,39 @@ +// Copyright 2021 Flavio Oliveira +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum U2fError { + #[error("ASM1 Decoder error")] + Asm1DecoderError, + #[error("Not able to verify signature")] + BadSignature, + #[error("Not able to generate random bytes")] + RandomSecureBytesError, + #[error("Invalid Reserved Byte")] + InvalidReservedByte, + #[error("Challenge Expired")] + ChallengeExpired, + #[error("Wrong Key Handler")] + WrongKeyHandler, + #[error("Invalid Client Data")] + InvalidClientData, + #[error("Invalid Signature Data")] + InvalidSignatureData, + #[error("Invalid User Presence Byte")] + InvalidUserPresenceByte, + #[error("Failed to parse certificate")] + BadCertificate, + #[error("Not Trusted Anchor")] + NotTrustedAnchor, + #[error("Counter too low")] + CounterTooLow, + #[error("Invalid public key")] + OpenSSLNoCurveName, + #[error("OpenSSL no curve name")] + InvalidPublicKey, + #[error(transparent)] + OpenSSLError(#[from] openssl::error::ErrorStack), +} diff --git a/plugins/authenticator/src/u2f_crate/util.rs b/plugins/authenticator/src/u2f_crate/util.rs new file mode 100644 index 00000000..cba58d4d --- /dev/null +++ b/plugins/authenticator/src/u2f_crate/util.rs @@ -0,0 +1,66 @@ +// Copyright 2021 Flavio Oliveira +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::u2f_crate::u2ferror::U2fError; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use bytes::Bytes; +use chrono::prelude::*; +use chrono::Duration; +use openssl::rand; + +/// The `Result` type used in this crate. +type Result = ::std::result::Result; + +pub const U2F_V2: &str = "U2F_V2"; + +// Generates a challenge from a secure, random source. +pub fn generate_challenge(size: usize) -> Result> { + let mut bytes: Vec = vec![0; size]; + rand::rand_bytes(&mut bytes).map_err(|_e| U2fError::RandomSecureBytesError)?; + Ok(bytes) +} + +pub fn expiration(timestamp: String) -> Duration { + let now: DateTime = Utc::now(); + + let ts = timestamp.parse::>(); + + now.signed_duration_since(ts.unwrap()) +} + +// Decode initial bytes of buffer as ASN and return the length of the encoded structure. +// http://en.wikipedia.org/wiki/X.690 +pub fn asn_length(mem: Bytes) -> Result { + let buffer: &[u8] = &mem[..]; + + if mem.len() < 2 || buffer[0] != 0x30 { + // Type + return Err(U2fError::Asm1DecoderError); + } + + let len = buffer[1]; // Len + if len & 0x80 == 0 { + return Ok((len & 0x7f) as usize); + } + + let numbem_of_bytes = len & 0x7f; + if numbem_of_bytes == 0 { + return Err(U2fError::Asm1DecoderError); + } + + let mut length: usize = 0; + for num in 0..numbem_of_bytes { + length = length * 0x100 + (buffer[(2 + num) as usize] as usize); + } + + length += numbem_of_bytes as usize; + + Ok(length + 2) // Add the 2 initial bytes: type and length. +} + +pub fn get_encoded(data: &[u8]) -> String { + let encoded: String = URL_SAFE_NO_PAD.encode(data); + + encoded.trim_end_matches('=').to_string() +} diff --git a/plugins/single-instance/src/platform_impl/windows.rs b/plugins/single-instance/src/platform_impl/windows.rs index 5919d3e1..63e95645 100644 --- a/plugins/single-instance/src/platform_impl/windows.rs +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -115,7 +115,7 @@ unsafe extern "system" fn single_instance_window_proc( let data = CStr::from_ptr((*cds_ptr).lpData as _).to_string_lossy(); let mut s = data.split('|'); let cwd = s.next().unwrap(); - let args = s.into_iter().map(|s| s.to_string()).collect(); + let args = s.map(|s| s.to_string()).collect(); callback(app_handle, args, cwd.to_string()); } 1