зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1666701 - Upgrade Authenticator-rs to v0.3.1 r=kjacobs,keeler
This patch fixes a failure to compile on OpenBSD, and also includes the new (and not yet used by Gecko) WebDriver implementation, and its associated error-code upgrades. This has a lot of new packages added into the cargo-checksum, but they were already used by Gecko, and thus don't change the gecko-wide Cargo.{lock,toml} files. Differential Revision: https://phabricator.services.mozilla.com/D92784
This commit is contained in:
Родитель
0563d3a60a
Коммит
429b38902b
|
@ -189,9 +189,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "authenticator"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1705a61db5ea860a40b54f649ee82ece942aef6bb91c6e86994e3b7d96597ab"
|
||||
checksum = "08cee7a0952628fde958e149507c2bb321ab4fccfafd225da0b20adc956ef88a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -13,16 +13,25 @@
|
|||
[package]
|
||||
edition = "2018"
|
||||
name = "authenticator"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
authors = ["J.C. Jones <jc@mozilla.com>", "Tim Taubert <ttaubert@mozilla.com>", "Kyle Machulis <kyle@nonpolynomial.com>"]
|
||||
description = "Library for interacting with CTAP1/2 security keys for Web Authentication. Used by Firefox."
|
||||
keywords = ["ctap2", "u2f", "fido", "webauthn"]
|
||||
categories = ["cryptography", "hardware-support", "os"]
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/mozilla/authenticator-rs/"
|
||||
[dependencies.base64]
|
||||
version = "^0.10"
|
||||
optional = true
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.bytes]
|
||||
version = "0.5"
|
||||
features = ["serde"]
|
||||
optional = true
|
||||
|
||||
[dependencies.libc]
|
||||
version = "0.2"
|
||||
|
||||
|
@ -34,6 +43,24 @@ version = "0.7"
|
|||
|
||||
[dependencies.runloop]
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "0.2"
|
||||
features = ["macros"]
|
||||
optional = true
|
||||
|
||||
[dependencies.warp]
|
||||
version = "0.2.4"
|
||||
optional = true
|
||||
[dev-dependencies.assert_matches]
|
||||
version = "1.2"
|
||||
|
||||
|
@ -54,6 +81,7 @@ optional = true
|
|||
|
||||
[features]
|
||||
binding-recompile = ["bindgen"]
|
||||
webdriver = ["base64", "bytes", "warp", "tokio", "serde", "serde_json"]
|
||||
[target."cfg(target_os = \"freebsd\")".dependencies.devd-rs]
|
||||
version = "0.3"
|
||||
[target."cfg(target_os = \"linux\")".dependencies.libudev]
|
||||
|
|
|
@ -40,6 +40,15 @@ fn main() {
|
|||
|
||||
let mut opts = Options::new();
|
||||
opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
|
||||
#[cfg(feature = "webdriver")]
|
||||
opts.optflag("w", "webdriver", "enable WebDriver virtual bus");
|
||||
|
||||
opts.optflag("h", "help", "print this help menu").optopt(
|
||||
"t",
|
||||
"timeout",
|
||||
"timeout in seconds",
|
||||
"SEC",
|
||||
);
|
||||
|
||||
opts.optflag("h", "help", "print this help menu");
|
||||
let matches = match opts.parse(&args[1..]) {
|
||||
|
@ -58,6 +67,25 @@ fn main() {
|
|||
manager.add_u2f_usb_hid_platform_transports();
|
||||
}
|
||||
|
||||
#[cfg(feature = "webdriver")]
|
||||
{
|
||||
if matches.opt_present("webdriver") {
|
||||
manager.add_webdriver_virtual_bus();
|
||||
}
|
||||
}
|
||||
|
||||
let timeout_ms = match matches.opt_get_default::<u64>("timeout", 15) {
|
||||
Ok(timeout_s) => {
|
||||
println!("Using {}s as the timeout", &timeout_s);
|
||||
timeout_s * 1_000
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("Asking a security key to register now...");
|
||||
let challenge_str = format!(
|
||||
"{}{}",
|
||||
|
@ -101,7 +129,7 @@ fn main() {
|
|||
manager
|
||||
.register(
|
||||
flags,
|
||||
60_000 * 5,
|
||||
timeout_ms,
|
||||
chall_bytes.clone(),
|
||||
app_bytes.clone(),
|
||||
vec![],
|
||||
|
@ -133,7 +161,7 @@ fn main() {
|
|||
|
||||
if let Err(e) = manager.sign(
|
||||
flags,
|
||||
15_000,
|
||||
timeout_ms,
|
||||
chall_bytes,
|
||||
vec![app_bytes],
|
||||
vec![key_handle],
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
comment_width = 200
|
||||
wrap_comments = true
|
||||
edition = "2018"
|
||||
|
|
|
@ -84,6 +84,17 @@ impl AuthenticatorService {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webdriver")]
|
||||
pub fn add_webdriver_virtual_bus(&mut self) {
|
||||
match crate::virtualdevices::webdriver::VirtualManager::new() {
|
||||
Ok(token) => {
|
||||
println!("WebDriver ready, listening at {}", &token.url());
|
||||
self.add_transport(Box::new(token));
|
||||
}
|
||||
Err(e) => error!("Could not add WebDriver virtual bus: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&mut self,
|
||||
flags: crate::RegisterFlags,
|
||||
|
@ -125,7 +136,7 @@ impl AuthenticatorService {
|
|||
);
|
||||
|
||||
transport_mutex.lock().unwrap().register(
|
||||
flags.clone(),
|
||||
flags,
|
||||
timeout,
|
||||
challenge.clone(),
|
||||
application.clone(),
|
||||
|
@ -178,7 +189,7 @@ impl AuthenticatorService {
|
|||
transports_to_cancel.remove(idx);
|
||||
|
||||
transport_mutex.lock().unwrap().sign(
|
||||
flags.clone(),
|
||||
flags,
|
||||
timeout,
|
||||
challenge.clone(),
|
||||
app_ids.clone(),
|
||||
|
@ -533,7 +544,7 @@ mod tests {
|
|||
mk_challenge(),
|
||||
mk_appid(),
|
||||
vec![],
|
||||
status_tx.clone(),
|
||||
status_tx,
|
||||
callback.clone(),
|
||||
)
|
||||
.is_ok());
|
||||
|
@ -605,7 +616,7 @@ mod tests {
|
|||
mk_challenge(),
|
||||
mk_appid(),
|
||||
vec![],
|
||||
status_tx.clone(),
|
||||
status_tx,
|
||||
callback.clone(),
|
||||
)
|
||||
.is_ok());
|
||||
|
|
|
@ -75,6 +75,7 @@ pub use crate::capi::*;
|
|||
|
||||
pub mod errors;
|
||||
pub mod statecallback;
|
||||
mod virtualdevices;
|
||||
|
||||
// Keep this in sync with the constants in u2fhid-capi.h.
|
||||
bitflags! {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use libc::{c_int, c_short, c_ulong};
|
||||
use libudev;
|
||||
use libudev::EventType;
|
||||
use runloop::RunLoop;
|
||||
use std::collections::HashMap;
|
||||
|
|
|
@ -140,9 +140,7 @@ impl AuthenticatorTransport for U2FManager {
|
|||
status,
|
||||
callback,
|
||||
};
|
||||
self.tx
|
||||
.send(action)
|
||||
.map_err(|e| AuthenticatorError::from(e))
|
||||
Ok(self.tx.send(action)?)
|
||||
}
|
||||
|
||||
fn sign(
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
extern crate libc;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::io::{Read, Result, Write};
|
||||
use std::mem;
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ pub struct StateCallback<T> {
|
|||
}
|
||||
|
||||
impl<T> StateCallback<T> {
|
||||
// This is used for the Condvar, which requires this kind of construction
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
pub fn new(cb: Box<dyn Fn(T) + Send>) -> Self {
|
||||
Self {
|
||||
callback: Arc::new(Mutex::new(Some(cb))),
|
||||
|
@ -134,11 +136,13 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::redundant_clone)]
|
||||
fn test_statecallback_observer_unclonable() {
|
||||
let mut sc = StateCallback::<()>::new(Box::new(move |_| {}));
|
||||
sc.add_uncloneable_observer(Box::new(move || {}));
|
||||
|
||||
assert!(sc.observer.lock().unwrap().is_some());
|
||||
// This is deliberate, to force an extra clone
|
||||
assert!(sc.clone().observer.lock().unwrap().is_none());
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,6 @@ fn send_status(status_mutex: &Mutex<Sender<crate::StatusUpdate>>, msg: crate::St
|
|||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't obtain status mutex: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ use std::{cmp, fmt, io, str};
|
|||
use crate::consts::*;
|
||||
use crate::util::io_err;
|
||||
|
||||
use log;
|
||||
|
||||
pub fn to_hex(data: &[u8], joiner: &str) -> String {
|
||||
let parts: Vec<String> = data.iter().map(|byte| format!("{:02x}", byte)).collect();
|
||||
parts.join(joiner)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/* 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/. */
|
||||
|
||||
#[cfg(feature = "webdriver")]
|
||||
pub mod webdriver;
|
||||
|
||||
pub mod software_u2f;
|
|
@ -0,0 +1,58 @@
|
|||
/* 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/. */
|
||||
|
||||
pub struct SoftwareU2FToken {}
|
||||
|
||||
// This is simply for platforms that aren't using the U2F Token, usually for builds
|
||||
// without --feature webdriver
|
||||
#[allow(dead_code)]
|
||||
|
||||
impl SoftwareU2FToken {
|
||||
pub fn new() -> SoftwareU2FToken {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&self,
|
||||
_flags: crate::RegisterFlags,
|
||||
_timeout: u64,
|
||||
_challenge: Vec<u8>,
|
||||
_application: crate::AppId,
|
||||
_key_handles: Vec<crate::KeyHandle>,
|
||||
) -> crate::Result<crate::RegisterResult> {
|
||||
Ok((vec![0u8; 16], self.dev_info()))
|
||||
}
|
||||
|
||||
/// The implementation of this method must return quickly and should
|
||||
/// report its status via the status and callback methods
|
||||
pub fn sign(
|
||||
&self,
|
||||
_flags: crate::SignFlags,
|
||||
_timeout: u64,
|
||||
_challenge: Vec<u8>,
|
||||
_app_ids: Vec<crate::AppId>,
|
||||
_key_handles: Vec<crate::KeyHandle>,
|
||||
) -> crate::Result<crate::SignResult> {
|
||||
Ok((vec![0u8; 0], vec![0u8; 0], vec![0u8; 0], self.dev_info()))
|
||||
}
|
||||
|
||||
pub fn dev_info(&self) -> crate::u2ftypes::U2FDeviceInfo {
|
||||
crate::u2ftypes::U2FDeviceInfo {
|
||||
vendor_name: b"Mozilla".to_vec(),
|
||||
device_name: b"Authenticator Webdriver Token".to_vec(),
|
||||
version_interface: 0,
|
||||
version_major: 1,
|
||||
version_minor: 2,
|
||||
version_build: 3,
|
||||
cap_flags: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Tests
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
|
@ -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;
|
140
third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs
поставляемый
Normal file
140
third_party/rust/authenticator/src/virtualdevices/webdriver/testtoken.rs
поставляемый
Normal file
|
@ -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,
|
||||
))
|
||||
}
|
||||
}
|
157
third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs
поставляемый
Normal file
157
third_party/rust/authenticator/src/virtualdevices/webdriver/virtualmanager.rs
поставляемый
Normal file
|
@ -0,0 +1,157 @@
|
|||
/* 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;
|
||||
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,
|
||||
_flags: crate::RegisterFlags,
|
||||
timeout: u64,
|
||||
_challenge: Vec<u8>,
|
||||
_application: crate::AppId,
|
||||
_key_handles: Vec<crate::KeyHandle>,
|
||||
_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,
|
||||
_flags: crate::SignFlags,
|
||||
timeout: u64,
|
||||
_challenge: Vec<u8>,
|
||||
_app_ids: Vec<crate::AppId>,
|
||||
_key_handles: Vec<crate::KeyHandle>,
|
||||
_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(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,965 @@
|
|||
/* 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 bytes::Buf;
|
||||
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: &bytes::Bytes) {
|
||||
assert_eq!(String::from_utf8_lossy(body.bytes()), 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().bytes())
|
||||
.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().bytes()),
|
||||
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().bytes()),
|
||||
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(¶m_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(¶m_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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
requests>=2.23.0
|
||||
rich>=3.0
|
|
@ -0,0 +1,207 @@
|
|||
# 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/.
|
||||
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import requests
|
||||
|
||||
console = Console()
|
||||
log = logging.getLogger("webdriver-driver")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(help="sub-command help")
|
||||
|
||||
parser.add_argument(
|
||||
"--verbose", "-v", help="Be more verbose", action="count", default=0
|
||||
)
|
||||
parser.add_argument(
|
||||
"--url",
|
||||
default="http://localhost:8080/webauthn/authenticator",
|
||||
help="webdriver url",
|
||||
)
|
||||
|
||||
|
||||
def device_add(args):
|
||||
data = {
|
||||
"protocol": args.protocol,
|
||||
"transport": args.transport,
|
||||
"hasResidentKey": args.residentkey in ["true", "yes"],
|
||||
"isUserConsenting": args.consent in ["true", "yes"],
|
||||
"hasUserVerification": args.uv in ["available", "verified"],
|
||||
"isUserVerified": args.uv in ["verified"],
|
||||
}
|
||||
console.print("Adding new device: ", data)
|
||||
rsp = requests.post(args.url, json=data)
|
||||
console.print(rsp)
|
||||
console.print(rsp.json())
|
||||
|
||||
|
||||
parser_add = subparsers.add_parser("add", help="Add a device")
|
||||
parser_add.set_defaults(func=device_add)
|
||||
parser_add.add_argument(
|
||||
"--consent",
|
||||
choices=["yes", "no", "true", "false"],
|
||||
default="true",
|
||||
help="consent automatically",
|
||||
)
|
||||
parser_add.add_argument(
|
||||
"--residentkey",
|
||||
choices=["yes", "no", "true", "false"],
|
||||
default="no",
|
||||
help="indicate a resident key",
|
||||
)
|
||||
parser_add.add_argument(
|
||||
"--uv",
|
||||
choices=["no", "available", "verified"],
|
||||
default="no",
|
||||
help="indicate user verification",
|
||||
)
|
||||
parser_add.add_argument(
|
||||
"--protocol", choices=["ctap1/u2f", "ctap2"], default="ctap1/u2f", help="protocol"
|
||||
)
|
||||
parser_add.add_argument("--transport", default="usb", help="transport type(s)")
|
||||
|
||||
|
||||
def device_delete(args):
|
||||
rsp = requests.delete(f"{args.url}/{args.id}")
|
||||
console.print(rsp)
|
||||
console.print(rsp.json())
|
||||
|
||||
|
||||
parser_delete = subparsers.add_parser("delete", help="Delete a device")
|
||||
parser_delete.set_defaults(func=device_delete)
|
||||
parser_delete.add_argument("id", type=int, help="device ID to delete")
|
||||
|
||||
|
||||
def device_view(args):
|
||||
rsp = requests.get(f"{args.url}/{args.id}")
|
||||
console.print(rsp)
|
||||
console.print(rsp.json())
|
||||
|
||||
|
||||
parser_view = subparsers.add_parser("view", help="View data about a device")
|
||||
parser_view.set_defaults(func=device_view)
|
||||
parser_view.add_argument("id", type=int, help="device ID to view")
|
||||
|
||||
|
||||
def device_update_uv(args):
|
||||
data = {"isUserVerified": args.uv in ["verified", "yes"]}
|
||||
rsp = requests.post(f"{args.url}/{args.id}/uv", json=data)
|
||||
console.print(rsp)
|
||||
console.print(rsp.json())
|
||||
|
||||
|
||||
parser_update_uv = subparsers.add_parser(
|
||||
"update-uv", help="Update the User Verified setting"
|
||||
)
|
||||
parser_update_uv.set_defaults(func=device_update_uv)
|
||||
parser_update_uv.add_argument("id", type=int, help="device ID to update")
|
||||
parser_update_uv.add_argument(
|
||||
"uv",
|
||||
choices=["no", "yes", "verified"],
|
||||
default="no",
|
||||
help="indicate user verification",
|
||||
)
|
||||
|
||||
|
||||
def credential_add(args):
|
||||
data = {
|
||||
"credentialId": args.credentialId,
|
||||
"isResidentCredential": args.isResidentCredential in ["true", "yes"],
|
||||
"rpId": args.rpId,
|
||||
"privateKey": args.privateKey,
|
||||
"signCount": args.signCount,
|
||||
}
|
||||
if args.userHandle:
|
||||
data["userHandle"] = (args.userHandle,)
|
||||
|
||||
console.print(f"Adding new credential to device {args.id}: ", data)
|
||||
rsp = requests.post(f"{args.url}/{args.id}/credential", json=data)
|
||||
console.print(rsp)
|
||||
console.print(rsp.json())
|
||||
|
||||
|
||||
parser_credential_add = subparsers.add_parser("addcred", help="Add a credential")
|
||||
parser_credential_add.set_defaults(func=credential_add)
|
||||
parser_credential_add.add_argument(
|
||||
"--id", required=True, type=int, help="device ID to use"
|
||||
)
|
||||
parser_credential_add.add_argument(
|
||||
"--credentialId", required=True, help="base64url-encoded credential ID"
|
||||
)
|
||||
parser_credential_add.add_argument(
|
||||
"--isResidentCredential",
|
||||
choices=["yes", "no", "true", "false"],
|
||||
default="no",
|
||||
help="indicate a resident key",
|
||||
)
|
||||
parser_credential_add.add_argument("--rpId", required=True, help="RP id (hostname)")
|
||||
parser_credential_add.add_argument(
|
||||
"--privateKey", required=True, help="base64url-encoded private key per RFC 5958"
|
||||
)
|
||||
parser_credential_add.add_argument("--userHandle", help="base64url-encoded user handle")
|
||||
parser_credential_add.add_argument(
|
||||
"--signCount", default=0, type=int, help="initial signature counter"
|
||||
)
|
||||
|
||||
|
||||
def credentials_get(args):
|
||||
rsp = requests.get(f"{args.url}/{args.id}/credentials")
|
||||
console.print(rsp)
|
||||
console.print(rsp.json())
|
||||
|
||||
|
||||
parser_credentials_get = subparsers.add_parser("getcreds", help="Get credentials")
|
||||
parser_credentials_get.set_defaults(func=credentials_get)
|
||||
parser_credentials_get.add_argument("id", type=int, help="device ID to query")
|
||||
|
||||
|
||||
def credential_delete(args):
|
||||
rsp = requests.delete(f"{args.url}/{args.id}/credentials/{args.credentialId}")
|
||||
console.print(rsp)
|
||||
console.print(rsp.json())
|
||||
|
||||
|
||||
parser_credentials_get = subparsers.add_parser("delcred", help="Delete a credential")
|
||||
parser_credentials_get.set_defaults(func=credential_delete)
|
||||
parser_credentials_get.add_argument("id", type=int, help="device ID to affect")
|
||||
parser_credentials_get.add_argument(
|
||||
"credentialId", help="base64url-encoded credential ID"
|
||||
)
|
||||
|
||||
|
||||
def credentials_clear(args):
|
||||
rsp = requests.delete(f"{args.url}/{args.id}/credentials")
|
||||
console.print(rsp)
|
||||
console.print(rsp.json())
|
||||
|
||||
|
||||
parser_credentials_get = subparsers.add_parser(
|
||||
"clearcreds", help="Clear all credentials for a device"
|
||||
)
|
||||
parser_credentials_get.set_defaults(func=credentials_clear)
|
||||
parser_credentials_get.add_argument("id", type=int, help="device ID to affect")
|
||||
|
||||
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
loglevel = logging.INFO
|
||||
if args.verbose > 0:
|
||||
loglevel = logging.DEBUG
|
||||
logging.basicConfig(
|
||||
level=loglevel, format="%(message)s", datefmt="[%X]", handlers=[RichHandler()]
|
||||
)
|
||||
|
||||
try:
|
||||
args.func(args)
|
||||
except requests.exceptions.ConnectionError as ce:
|
||||
log.error(f"Connection refused to {args.url}: {ce}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -25,7 +25,7 @@ cubeb-sys = { version = "0.7", optional = true, features=["gecko-in-tree"] }
|
|||
encoding_glue = { path = "../../../../intl/encoding_glue" }
|
||||
audioipc-client = { path = "../../../../media/audioipc/client", optional = true }
|
||||
audioipc-server = { path = "../../../../media/audioipc/server", optional = true }
|
||||
authenticator = "0.3.0"
|
||||
authenticator = "0.3.1"
|
||||
gkrust_utils = { path = "../../../../xpcom/rust/gkrust_utils" }
|
||||
gecko_logger = { path = "../../../../xpcom/rust/gecko_logger" }
|
||||
rsdparsa_capi = { path = "../../../../dom/media/webrtc/sdp/rsdparsa_capi" }
|
||||
|
|
Загрузка…
Ссылка в новой задаче