summaryrefslogtreecommitdiffstats
path: root/third_party/rust/authenticator/src/virtualdevices/webdriver
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/authenticator/src/virtualdevices/webdriver
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/authenticator/src/virtualdevices/webdriver')
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs9
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs140
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs151
-rw-r--r--third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs964
4 files changed, 1264 insertions, 0 deletions
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs
new file mode 100644
index 0000000000..b1ef27d813
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/mod.rs
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+mod testtoken;
+mod virtualmanager;
+mod web_api;
+
+pub use virtualmanager::VirtualManager;
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs
new file mode 100644
index 0000000000..9bf60bbaf5
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::errors;
+use crate::virtualdevices::software_u2f::SoftwareU2FToken;
+use crate::{RegisterFlags, RegisterResult, SignFlags, SignResult};
+
+pub enum TestWireProtocol {
+ CTAP1,
+ CTAP2,
+}
+
+impl TestWireProtocol {
+ pub fn to_webdriver_string(&self) -> String {
+ match self {
+ TestWireProtocol::CTAP1 => "ctap1/u2f".to_string(),
+ TestWireProtocol::CTAP2 => "ctap2".to_string(),
+ }
+ }
+}
+
+pub struct TestTokenCredential {
+ pub credential: Vec<u8>,
+ pub privkey: Vec<u8>,
+ pub user_handle: Vec<u8>,
+ pub sign_count: u64,
+ pub is_resident_credential: bool,
+ pub rp_id: String,
+}
+
+pub struct TestToken {
+ pub id: u64,
+ pub protocol: TestWireProtocol,
+ pub transport: String,
+ pub is_user_consenting: bool,
+ pub has_user_verification: bool,
+ pub is_user_verified: bool,
+ pub has_resident_key: bool,
+ pub u2f_impl: Option<SoftwareU2FToken>,
+ pub credentials: Vec<TestTokenCredential>,
+}
+
+impl TestToken {
+ pub fn new(
+ id: u64,
+ protocol: TestWireProtocol,
+ transport: String,
+ is_user_consenting: bool,
+ has_user_verification: bool,
+ is_user_verified: bool,
+ has_resident_key: bool,
+ ) -> TestToken {
+ match protocol {
+ TestWireProtocol::CTAP1 => Self {
+ id,
+ protocol,
+ transport,
+ is_user_consenting,
+ has_user_verification,
+ is_user_verified,
+ has_resident_key,
+ u2f_impl: Some(SoftwareU2FToken::new()),
+ credentials: Vec::new(),
+ },
+ _ => unreachable!(),
+ }
+ }
+
+ pub fn insert_credential(
+ &mut self,
+ credential: &[u8],
+ privkey: &[u8],
+ rp_id: String,
+ is_resident_credential: bool,
+ user_handle: &[u8],
+ sign_count: u64,
+ ) {
+ let c = TestTokenCredential {
+ credential: credential.to_vec(),
+ privkey: privkey.to_vec(),
+ rp_id,
+ is_resident_credential,
+ user_handle: user_handle.to_vec(),
+ sign_count,
+ };
+
+ match self
+ .credentials
+ .binary_search_by_key(&credential, |probe| &probe.credential)
+ {
+ Ok(_) => {}
+ Err(idx) => self.credentials.insert(idx, c),
+ }
+ }
+
+ pub fn delete_credential(&mut self, credential: &[u8]) -> bool {
+ debug!("Asking to delete credential",);
+ if let Ok(idx) = self
+ .credentials
+ .binary_search_by_key(&credential, |probe| &probe.credential)
+ {
+ debug!("Asking to delete credential from idx {}", idx);
+ self.credentials.remove(idx);
+ return true;
+ }
+
+ false
+ }
+
+ pub fn register(&self) -> crate::Result<RegisterResult> {
+ if self.u2f_impl.is_some() {
+ return self.u2f_impl.as_ref().unwrap().register(
+ RegisterFlags::empty(),
+ 10_000,
+ vec![0; 32],
+ vec![0; 32],
+ vec![],
+ );
+ }
+ Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ))
+ }
+
+ pub fn sign(&self) -> crate::Result<SignResult> {
+ if self.u2f_impl.is_some() {
+ return self.u2f_impl.as_ref().unwrap().sign(
+ SignFlags::empty(),
+ 10_000,
+ vec![0; 32],
+ vec![vec![0; 32]],
+ vec![],
+ );
+ }
+ Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ))
+ }
+}
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs
new file mode 100644
index 0000000000..31e5d09e3a
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use runloop::RunLoop;
+use std::net::{IpAddr, Ipv4Addr, SocketAddr};
+use std::ops::Deref;
+use std::sync::mpsc::Sender;
+use std::sync::{Arc, Mutex};
+use std::vec;
+use std::{io, string, thread};
+
+use crate::authenticatorservice::{AuthenticatorTransport, RegisterArgs, SignArgs};
+use crate::errors;
+use crate::statecallback::StateCallback;
+use crate::virtualdevices::webdriver::{testtoken, web_api};
+
+pub struct VirtualManagerState {
+ pub authenticator_counter: u64,
+ pub tokens: vec::Vec<testtoken::TestToken>,
+}
+
+impl VirtualManagerState {
+ pub fn new() -> Arc<Mutex<VirtualManagerState>> {
+ Arc::new(Mutex::new(VirtualManagerState {
+ authenticator_counter: 0,
+ tokens: vec![],
+ }))
+ }
+}
+
+pub struct VirtualManager {
+ addr: SocketAddr,
+ state: Arc<Mutex<VirtualManagerState>>,
+ rloop: Option<RunLoop>,
+}
+
+impl VirtualManager {
+ pub fn new() -> io::Result<Self> {
+ let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080);
+ let state = VirtualManagerState::new();
+ let stateclone = state.clone();
+
+ let builder = thread::Builder::new().name("WebDriver Command Server".into());
+ builder.spawn(move || {
+ web_api::serve(stateclone, addr);
+ })?;
+
+ Ok(Self {
+ addr,
+ state,
+ rloop: None,
+ })
+ }
+
+ pub fn url(&self) -> string::String {
+ format!("http://{}/webauthn/authenticator", &self.addr)
+ }
+}
+
+impl AuthenticatorTransport for VirtualManager {
+ fn register(
+ &mut self,
+ timeout: u64,
+ _ctap_args: RegisterArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::RegisterResult>>,
+ ) -> crate::Result<()> {
+ if self.rloop.is_some() {
+ error!("WebDriver state error, prior operation never cancelled.");
+ return Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ));
+ }
+
+ let state = self.state.clone();
+ let rloop = try_or!(
+ RunLoop::new_with_timeout(
+ move |alive| {
+ while alive() {
+ let state_obj = state.lock().unwrap();
+
+ for token in state_obj.tokens.deref() {
+ if token.is_user_consenting {
+ let register_result = token.register();
+ thread::spawn(move || {
+ callback.call(register_result);
+ });
+ return;
+ }
+ }
+ }
+ },
+ timeout
+ ),
+ |_| Err(errors::AuthenticatorError::Platform)
+ );
+
+ self.rloop = Some(rloop);
+ Ok(())
+ }
+
+ fn sign(
+ &mut self,
+ timeout: u64,
+ _ctap_args: SignArgs,
+ _status: Sender<crate::StatusUpdate>,
+ callback: StateCallback<crate::Result<crate::SignResult>>,
+ ) -> crate::Result<()> {
+ if self.rloop.is_some() {
+ error!("WebDriver state error, prior operation never cancelled.");
+ return Err(errors::AuthenticatorError::U2FToken(
+ errors::U2FTokenError::Unknown,
+ ));
+ }
+
+ let state = self.state.clone();
+ let rloop = try_or!(
+ RunLoop::new_with_timeout(
+ move |alive| {
+ while alive() {
+ let state_obj = state.lock().unwrap();
+
+ for token in state_obj.tokens.deref() {
+ if token.is_user_consenting {
+ let sign_result = token.sign();
+ thread::spawn(move || {
+ callback.call(sign_result);
+ });
+ return;
+ }
+ }
+ }
+ },
+ timeout
+ ),
+ |_| Err(errors::AuthenticatorError::Platform)
+ );
+
+ self.rloop = Some(rloop);
+ Ok(())
+ }
+
+ fn cancel(&mut self) -> crate::Result<()> {
+ if let Some(r) = self.rloop.take() {
+ debug!("WebDriver operation cancelled.");
+ r.cancel();
+ }
+ Ok(())
+ }
+}
diff --git a/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs
new file mode 100644
index 0000000000..07bfc9f612
--- /dev/null
+++ b/third_party/rust/authenticator/src/virtualdevices/webdriver/web_api.rs
@@ -0,0 +1,964 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use serde::{Deserialize, Serialize};
+use std::net::SocketAddr;
+use std::string;
+use std::sync::{Arc, Mutex};
+use warp::Filter;
+
+use crate::virtualdevices::webdriver::{testtoken, virtualmanager::VirtualManagerState};
+
+fn default_as_false() -> bool {
+ false
+}
+fn default_as_true() -> bool {
+ false
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct AuthenticatorConfiguration {
+ protocol: string::String,
+ transport: string::String,
+ #[serde(rename = "hasResidentKey")]
+ #[serde(default = "default_as_false")]
+ has_resident_key: bool,
+ #[serde(rename = "hasUserVerification")]
+ #[serde(default = "default_as_false")]
+ has_user_verification: bool,
+ #[serde(rename = "isUserConsenting")]
+ #[serde(default = "default_as_true")]
+ is_user_consenting: bool,
+ #[serde(rename = "isUserVerified")]
+ #[serde(default = "default_as_false")]
+ is_user_verified: bool,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct CredentialParameters {
+ #[serde(rename = "credentialId")]
+ credential_id: String,
+ #[serde(rename = "isResidentCredential")]
+ is_resident_credential: bool,
+ #[serde(rename = "rpId")]
+ rp_id: String,
+ #[serde(rename = "privateKey")]
+ private_key: String,
+ #[serde(rename = "userHandle")]
+ #[serde(default)]
+ user_handle: String,
+ #[serde(rename = "signCount")]
+ sign_count: u64,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq)]
+pub struct UserVerificationParameters {
+ #[serde(rename = "isUserVerified")]
+ is_user_verified: bool,
+}
+
+impl CredentialParameters {
+ fn new_from_test_token_credential(tc: &testtoken::TestTokenCredential) -> CredentialParameters {
+ let credential_id = base64::encode_config(&tc.credential, base64::URL_SAFE);
+
+ let private_key = base64::encode_config(&tc.privkey, base64::URL_SAFE);
+
+ let user_handle = base64::encode_config(&tc.user_handle, base64::URL_SAFE);
+
+ CredentialParameters {
+ credential_id,
+ is_resident_credential: tc.is_resident_credential,
+ rp_id: tc.rp_id.clone(),
+ private_key,
+ user_handle,
+ sign_count: tc.sign_count,
+ }
+ }
+}
+
+fn with_state(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = (Arc<Mutex<VirtualManagerState>>,), Error = std::convert::Infallible> + Clone
+{
+ warp::any().map(move || state.clone())
+}
+
+fn authenticator_add(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_add)
+}
+
+fn authenticator_delete(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64)
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_delete)
+}
+
+fn authenticator_set_uv(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "uv")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_set_uv)
+}
+
+// This is not part of the specification, but it's useful for debugging
+fn authenticator_get(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64)
+ .and(warp::get())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_get)
+}
+
+fn authenticator_credential_add(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credential")
+ .and(warp::post())
+ .and(warp::body::json())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credential_add)
+}
+
+fn authenticator_credential_delete(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials" / String)
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credential_delete)
+}
+
+fn authenticator_credentials_get(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials")
+ .and(warp::get())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credentials_get)
+}
+
+fn authenticator_credentials_clear(
+ state: Arc<Mutex<VirtualManagerState>>,
+) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+ warp::path!("webauthn" / "authenticator" / u64 / "credentials")
+ .and(warp::delete())
+ .and(with_state(state))
+ .and_then(handlers::authenticator_credentials_clear)
+}
+
+mod handlers {
+ use super::{CredentialParameters, UserVerificationParameters};
+ use crate::virtualdevices::webdriver::{
+ testtoken, virtualmanager::VirtualManagerState, web_api::AuthenticatorConfiguration,
+ };
+ use serde::Serialize;
+ use std::convert::Infallible;
+ use std::ops::DerefMut;
+ use std::sync::{Arc, Mutex};
+ use std::vec;
+ use warp::http::{uri, StatusCode};
+
+ #[derive(Serialize)]
+ struct JsonSuccess {}
+
+ impl JsonSuccess {
+ pub fn blank() -> JsonSuccess {
+ JsonSuccess {}
+ }
+ }
+
+ #[derive(Serialize)]
+ struct JsonError {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ line: Option<u32>,
+ error: String,
+ details: String,
+ }
+
+ impl JsonError {
+ pub fn new(error: &str, line: u32, details: &str) -> JsonError {
+ JsonError {
+ details: details.to_string(),
+ error: error.to_string(),
+ line: Some(line),
+ }
+ }
+ pub fn from_status_code(code: StatusCode) -> JsonError {
+ JsonError {
+ details: code.canonical_reason().unwrap().to_string(),
+ line: None,
+ error: "".to_string(),
+ }
+ }
+ pub fn from_error(error: &str) -> JsonError {
+ JsonError {
+ details: "".to_string(),
+ error: error.to_string(),
+ line: None,
+ }
+ }
+ }
+
+ macro_rules! reply_error {
+ ($status:expr) => {
+ warp::reply::with_status(
+ warp::reply::json(&JsonError::from_status_code($status)),
+ $status,
+ )
+ };
+ }
+
+ macro_rules! try_json {
+ ($val:expr, $status:expr) => {
+ match $val {
+ Ok(v) => v,
+ Err(e) => {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonError::new(
+ $status.canonical_reason().unwrap(),
+ line!(),
+ &e.to_string(),
+ )),
+ $status,
+ ));
+ }
+ }
+ };
+ }
+
+ pub fn validate_rp_id(rp_id: &str) -> crate::Result<()> {
+ if let Ok(uri) = rp_id.parse::<uri::Uri>().map_err(|_| {
+ crate::errors::AuthenticatorError::U2FToken(crate::errors::U2FTokenError::Unknown)
+ }) {
+ if uri.scheme().is_none()
+ && uri.path_and_query().is_none()
+ && uri.port().is_none()
+ && uri.host().is_some()
+ && uri.authority().unwrap() == uri.host().unwrap()
+ // Don't try too hard to ensure it's a valid domain, just
+ // ensure there's a label delim in there somewhere
+ && uri.host().unwrap().find('.').is_some()
+ {
+ return Ok(());
+ }
+ }
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ }
+
+ pub async fn authenticator_add(
+ auth: AuthenticatorConfiguration,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let protocol = match auth.protocol.as_str() {
+ "ctap1/u2f" => testtoken::TestWireProtocol::CTAP1,
+ "ctap2" => testtoken::TestWireProtocol::CTAP2,
+ _ => {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonError::from_error(
+ format!("unknown protocol: {}", auth.protocol).as_str(),
+ )),
+ StatusCode::BAD_REQUEST,
+ ))
+ }
+ };
+
+ let mut state_lock = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ let mut state_obj = state_lock.deref_mut();
+ state_obj.authenticator_counter += 1;
+
+ let tt = testtoken::TestToken::new(
+ state_obj.authenticator_counter,
+ protocol,
+ auth.transport,
+ auth.is_user_consenting,
+ auth.has_user_verification,
+ auth.is_user_verified,
+ auth.has_resident_key,
+ );
+
+ match state_obj
+ .tokens
+ .binary_search_by_key(&state_obj.authenticator_counter, |probe| probe.id)
+ {
+ Ok(_) => panic!("unexpected repeat of authenticator_id"),
+ Err(idx) => state_obj.tokens.insert(idx, tt),
+ }
+
+ #[derive(Serialize)]
+ struct AddResult {
+ #[serde(rename = "authenticatorId")]
+ authenticator_id: u64,
+ }
+
+ Ok(warp::reply::with_status(
+ warp::reply::json(&AddResult {
+ authenticator_id: state_obj.authenticator_counter,
+ }),
+ StatusCode::CREATED,
+ ))
+ }
+
+ pub async fn authenticator_delete(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ match state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ Ok(idx) => state_obj.tokens.remove(idx),
+ Err(_) => {
+ return Ok(reply_error!(StatusCode::NOT_FOUND));
+ }
+ };
+
+ Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ))
+ }
+
+ pub async fn authenticator_get(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ let data = AuthenticatorConfiguration {
+ protocol: tt.protocol.to_webdriver_string(),
+ transport: tt.transport.clone(),
+ has_resident_key: tt.has_resident_key,
+ has_user_verification: tt.has_user_verification,
+ is_user_consenting: tt.is_user_consenting,
+ is_user_verified: tt.is_user_verified,
+ };
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&data),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_set_uv(
+ id: u64,
+ uv: UserVerificationParameters,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ tt.is_user_verified = uv.is_user_verified;
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credential_add(
+ id: u64,
+ auth: CredentialParameters,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let credential = try_json!(
+ base64::decode_config(&auth.credential_id, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let privkey = try_json!(
+ base64::decode_config(&auth.private_key, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let userhandle = try_json!(
+ base64::decode_config(&auth.user_handle, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ try_json!(validate_rp_id(&auth.rp_id), StatusCode::BAD_REQUEST);
+
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ tt.insert_credential(
+ &credential,
+ &privkey,
+ auth.rp_id,
+ auth.is_resident_credential,
+ &userhandle,
+ auth.sign_count,
+ );
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::CREATED,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credential_delete(
+ id: u64,
+ credential_id: String,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let credential = try_json!(
+ base64::decode_config(&credential_id, base64::URL_SAFE),
+ StatusCode::BAD_REQUEST
+ );
+
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ debug!("Asking to delete {}", &credential_id);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ debug!("Asking to delete from token {}", tt.id);
+ if tt.delete_credential(&credential) {
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credentials_get(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+ let mut creds: vec::Vec<CredentialParameters> = vec![];
+ for ttc in &tt.credentials {
+ creds.push(CredentialParameters::new_from_test_token_credential(ttc));
+ }
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&creds),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+
+ pub async fn authenticator_credentials_clear(
+ id: u64,
+ state: Arc<Mutex<VirtualManagerState>>,
+ ) -> Result<impl warp::Reply, Infallible> {
+ let mut state_obj = try_json!(state.lock(), StatusCode::INTERNAL_SERVER_ERROR);
+ if let Ok(idx) = state_obj.tokens.binary_search_by_key(&id, |probe| probe.id) {
+ let tt = &mut state_obj.tokens[idx];
+
+ tt.credentials.clear();
+
+ return Ok(warp::reply::with_status(
+ warp::reply::json(&JsonSuccess::blank()),
+ StatusCode::OK,
+ ));
+ }
+
+ Ok(reply_error!(StatusCode::NOT_FOUND))
+ }
+}
+
+#[tokio::main]
+pub async fn serve(state: Arc<Mutex<VirtualManagerState>>, addr: SocketAddr) {
+ let routes = authenticator_add(state.clone())
+ .or(authenticator_delete(state.clone()))
+ .or(authenticator_get(state.clone()))
+ .or(authenticator_set_uv(state.clone()))
+ .or(authenticator_credential_add(state.clone()))
+ .or(authenticator_credential_delete(state.clone()))
+ .or(authenticator_credentials_get(state.clone()))
+ .or(authenticator_credentials_clear(state.clone()));
+
+ warp::serve(routes).run(addr).await;
+}
+
+#[cfg(test)]
+mod tests {
+ use super::handlers::validate_rp_id;
+ use super::testtoken::*;
+ use super::*;
+ use crate::virtualdevices::webdriver::virtualmanager::VirtualManagerState;
+ use std::sync::{Arc, Mutex};
+ use warp::http::StatusCode;
+
+ fn init() {
+ let _ = env_logger::builder().is_test(true).try_init();
+ }
+
+ #[test]
+ fn test_validate_rp_id() {
+ init();
+
+ assert_matches!(
+ validate_rp_id(&String::from("http://example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("https://example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com:443")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com/path")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("example.com:443/path")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("user:pass@example.com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(
+ validate_rp_id(&String::from("com")),
+ Err(crate::errors::AuthenticatorError::U2FToken(
+ crate::errors::U2FTokenError::Unknown,
+ ))
+ );
+ assert_matches!(validate_rp_id(&String::from("example.com")), Ok(()));
+ }
+
+ fn mk_state_with_token_list(ids: &[u64]) -> Arc<Mutex<VirtualManagerState>> {
+ let state = VirtualManagerState::new();
+
+ {
+ let mut state_obj = state.lock().unwrap();
+ for id in ids {
+ state_obj.tokens.push(TestToken::new(
+ *id,
+ TestWireProtocol::CTAP1,
+ "internal".to_string(),
+ true,
+ true,
+ true,
+ true,
+ ));
+ }
+
+ state_obj.tokens.sort_by_key(|probe| probe.id)
+ }
+
+ state
+ }
+
+ fn assert_success_rsp_blank(body: &warp::hyper::body::Bytes) {
+ assert_eq!(String::from_utf8_lossy(&body), r#"{}"#)
+ }
+
+ fn assert_creds_equals_test_token_params(
+ a: &[CredentialParameters],
+ b: &[TestTokenCredential],
+ ) {
+ assert_eq!(a.len(), b.len());
+
+ for (i, j) in a.iter().zip(b.iter()) {
+ assert_eq!(
+ i.credential_id,
+ base64::encode_config(&j.credential, base64::URL_SAFE)
+ );
+ assert_eq!(
+ i.user_handle,
+ base64::encode_config(&j.user_handle, base64::URL_SAFE)
+ );
+ assert_eq!(
+ i.private_key,
+ base64::encode_config(&j.privkey, base64::URL_SAFE)
+ );
+ assert_eq!(i.rp_id, j.rp_id);
+ assert_eq!(i.sign_count, j.sign_count);
+ assert_eq!(i.is_resident_credential, j.is_resident_credential);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_add() {
+ init();
+ let filter = authenticator_add(mk_state_with_token_list(&[]));
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ let valid_add = AuthenticatorConfiguration {
+ protocol: "ctap1/u2f".to_string(),
+ transport: "usb".to_string(),
+ has_resident_key: false,
+ has_user_verification: false,
+ is_user_consenting: false,
+ is_user_verified: false,
+ };
+
+ {
+ let mut invalid = valid_add.clone();
+ invalid.protocol = "unknown".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ assert!(String::from_utf8_lossy(&res.body())
+ .contains(&String::from("unknown protocol: unknown")));
+ }
+
+ {
+ let mut unknown = valid_add.clone();
+ unknown.transport = "unknown".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&unknown)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_eq!(
+ String::from_utf8_lossy(&res.body()),
+ r#"{"authenticatorId":1}"#
+ )
+ }
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator")
+ .json(&valid_add)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_eq!(
+ String::from_utf8_lossy(&res.body()),
+ r#"{"authenticatorId":2}"#
+ )
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_delete() {
+ init();
+ let filter = authenticator_delete(mk_state_with_token_list(&[32]));
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/3")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/32")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/42")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_change_uv() {
+ init();
+ let state = mk_state_with_token_list(&[1]);
+ let filter = authenticator_set_uv(state.clone());
+
+ {
+ let state_obj = state.lock().unwrap();
+ assert_eq!(true, state_obj.tokens[0].is_user_verified);
+ }
+
+ {
+ // Empty POST is bad
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ // Unexpected POST structure is bad
+ #[derive(Serialize)]
+ struct Unexpected {
+ id: u64,
+ }
+ let unexpected = Unexpected { id: 4 };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&unexpected)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let param_false = UserVerificationParameters {
+ is_user_verified: false,
+ };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&param_false)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(false, state_obj.tokens[0].is_user_verified);
+ }
+
+ {
+ let param_false = UserVerificationParameters {
+ is_user_verified: true,
+ };
+
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/uv")
+ .json(&param_false)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(true, state_obj.tokens[0].is_user_verified);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_authenticator_credentials() {
+ init();
+ let state = mk_state_with_token_list(&[1]);
+ let filter = authenticator_credential_add(state.clone())
+ .or(authenticator_credential_delete(state.clone()))
+ .or(authenticator_credentials_get(state.clone()))
+ .or(authenticator_credentials_clear(state.clone()));
+
+ let valid_add_credential = CredentialParameters {
+ credential_id: r"c3VwZXIgcmVhZGVy".to_string(),
+ is_resident_credential: true,
+ rp_id: "valid.rpid".to_string(),
+ private_key: base64::encode_config(b"hello internet~", base64::URL_SAFE),
+ user_handle: base64::encode_config(b"hello internet~", base64::URL_SAFE),
+ sign_count: 0,
+ };
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.credential_id = "!@#$ invalid base64".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.rp_id = "example".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+ }
+
+ {
+ let mut invalid = valid_add_credential.clone();
+ invalid.rp_id = "https://example.com".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&invalid)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_client_error());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(0, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let mut no_user_handle = valid_add_credential.clone();
+ no_user_handle.user_handle = "".to_string();
+ no_user_handle.credential_id = "YQo=".to_string();
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&no_user_handle)
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(1, state_obj.tokens[0].credentials.len());
+ let c = &state_obj.tokens[0].credentials[0];
+ assert!(c.user_handle.is_empty());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&valid_add_credential)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), StatusCode::CREATED);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(2, state_obj.tokens[0].credentials.len());
+ let c = &state_obj.tokens[0].credentials[1];
+ assert!(!c.user_handle.is_empty());
+ }
+
+ {
+ // Duplicate, should still be two credentials
+ let res = warp::test::request()
+ .method("POST")
+ .path("/webauthn/authenticator/1/credential")
+ .json(&valid_add_credential)
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), StatusCode::CREATED);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(2, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("GET")
+ .path("/webauthn/authenticator/1/credentials")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ let (_, body) = res.into_parts();
+ let cred = serde_json::de::from_slice::<Vec<CredentialParameters>>(&body).unwrap();
+
+ let state_obj = state.lock().unwrap();
+ assert_creds_equals_test_token_params(&cred, &state_obj.tokens[0].credentials);
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials/YmxhbmsK")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 404);
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials/c3VwZXIgcmVhZGVy")
+ .reply(&filter)
+ .await;
+ assert_eq!(res.status(), 200);
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(1, state_obj.tokens[0].credentials.len());
+ }
+
+ {
+ let res = warp::test::request()
+ .method("DELETE")
+ .path("/webauthn/authenticator/1/credentials")
+ .reply(&filter)
+ .await;
+ assert!(res.status().is_success());
+ assert_success_rsp_blank(res.body());
+
+ let state_obj = state.lock().unwrap();
+ assert_eq!(0, state_obj.tokens[0].credentials.len());
+ }
+ }
+}