зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1530373 - vendor the latest version of authenticator. r=dveditz,supply-chain-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D163864
This commit is contained in:
Родитель
d7afdc2f9a
Коммит
4832b7d0f1
|
@ -378,18 +378,29 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "authenticator"
|
||||
version = "0.3.1"
|
||||
version = "0.4.0-alpha.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08cee7a0952628fde958e149507c2bb321ab4fccfafd225da0b20adc956ef88a"
|
||||
checksum = "671c5d49eab8c93b8aea310cef8a7fd0846eb9417e3c31e4f4d6ec7012aae842"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"core-foundation",
|
||||
"devd-rs",
|
||||
"libc",
|
||||
"libudev",
|
||||
"log",
|
||||
"rand 0.7.999",
|
||||
"memoffset 0.6.5",
|
||||
"nom 7.1.1",
|
||||
"nss-gk-api",
|
||||
"pkcs11-bindings",
|
||||
"rand 0.8.5",
|
||||
"runloop",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_cbor",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -2461,6 +2472,12 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.999"
|
||||
|
@ -3770,6 +3787,21 @@ dependencies = [
|
|||
"nsstring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nss-gk-api"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a689b62ba71fda80458a77b6ace9371d6e6a5473300901383ebd101659b3352"
|
||||
dependencies = [
|
||||
"bindgen 0.63.0",
|
||||
"mozbuild",
|
||||
"once_cell",
|
||||
"pkcs11-bindings",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nss_build_common"
|
||||
version = "0.1.0"
|
||||
|
@ -4763,6 +4795,16 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_cbor"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
|
||||
dependencies = [
|
||||
"half",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.144"
|
||||
|
|
|
@ -243,10 +243,6 @@ criteria = "safe-to-deploy"
|
|||
version = "0.26.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.authenticator]]
|
||||
version = "0.3.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.base64]]
|
||||
version = "0.13.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -3,26 +3,39 @@
|
|||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you believe there's an error in this file please file an
|
||||
# issue against the rust-lang/cargo repository. If you're
|
||||
# editing this file be aware that the upstream Cargo.toml
|
||||
# will likely look very different (and much more reasonable)
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "authenticator"
|
||||
version = "0.3.1"
|
||||
authors = ["J.C. Jones <jc@mozilla.com>", "Tim Taubert <ttaubert@mozilla.com>", "Kyle Machulis <kyle@nonpolynomial.com>"]
|
||||
version = "0.4.0-alpha.3"
|
||||
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"]
|
||||
readme = "README.md"
|
||||
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
|
||||
version = "^0.13"
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "1.0"
|
||||
|
@ -32,14 +45,42 @@ version = "0.5"
|
|||
features = ["serde"]
|
||||
optional = true
|
||||
|
||||
[dependencies.cfg-if]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.libc]
|
||||
version = "0.2"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.nom]
|
||||
version = "^7.1.1"
|
||||
features = ["std"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.nss-gk-api]
|
||||
version = "0.2.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.openssl]
|
||||
version = "0.10"
|
||||
optional = true
|
||||
|
||||
[dependencies.openssl-sys]
|
||||
version = "0.9"
|
||||
optional = true
|
||||
|
||||
[dependencies.pkcs11-bindings]
|
||||
version = "0.1.4"
|
||||
optional = true
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.7"
|
||||
version = "0.8"
|
||||
|
||||
[dependencies.ring]
|
||||
version = "0.16"
|
||||
optional = true
|
||||
|
||||
[dependencies.runloop]
|
||||
version = "0.1.0"
|
||||
|
@ -47,50 +88,89 @@ version = "0.1.0"
|
|||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
|
||||
[dependencies.serde_bytes]
|
||||
version = "0.11"
|
||||
|
||||
[dependencies.serde_cbor]
|
||||
version = "0.11"
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.sha2]
|
||||
version = "^0.10.0"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "0.2"
|
||||
features = ["macros"]
|
||||
version = "1.17"
|
||||
features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
]
|
||||
optional = true
|
||||
|
||||
[dependencies.warp]
|
||||
version = "0.2.4"
|
||||
version = "0.3.2"
|
||||
optional = true
|
||||
|
||||
[dev-dependencies.assert_matches]
|
||||
version = "1.2"
|
||||
|
||||
[dev-dependencies.base64]
|
||||
version = "^0.10"
|
||||
|
||||
[dev-dependencies.env_logger]
|
||||
version = "^0.6"
|
||||
|
||||
[dev-dependencies.getopts]
|
||||
version = "^0.2"
|
||||
|
||||
[dev-dependencies.sha2]
|
||||
version = "^0.8.2"
|
||||
[dev-dependencies.rpassword]
|
||||
version = "5.0"
|
||||
|
||||
[build-dependencies.bindgen]
|
||||
version = "^0.51"
|
||||
version = "^0.58.1"
|
||||
optional = true
|
||||
|
||||
[features]
|
||||
binding-recompile = ["bindgen"]
|
||||
webdriver = ["base64", "bytes", "warp", "tokio", "serde", "serde_json"]
|
||||
crypto_dummy = []
|
||||
crypto_nss = [
|
||||
"nss-gk-api",
|
||||
"pkcs11-bindings",
|
||||
]
|
||||
crypto_openssl = [
|
||||
"openssl",
|
||||
"openssl-sys",
|
||||
]
|
||||
crypto_ring = ["ring"]
|
||||
default = ["crypto_nss"]
|
||||
gecko = ["nss-gk-api/gecko"]
|
||||
webdriver = [
|
||||
"bytes",
|
||||
"warp",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[target."cfg(target_os = \"freebsd\")".dependencies.devd-rs]
|
||||
version = "0.3"
|
||||
|
||||
[target."cfg(target_os = \"linux\")".dependencies.libudev]
|
||||
version = "^0.2"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies.core-foundation]
|
||||
version = "0.9"
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies.memoffset]
|
||||
version = "0.6"
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies.winapi]
|
||||
version = "^0.3"
|
||||
features = ["handleapi", "hidclass", "hidpi", "hidusage", "setupapi"]
|
||||
features = [
|
||||
"handleapi",
|
||||
"hidclass",
|
||||
"hidpi",
|
||||
"hidusage",
|
||||
"setupapi",
|
||||
]
|
||||
|
||||
[badges.maintenance]
|
||||
status = "actively-developed"
|
||||
|
||||
|
|
|
@ -45,6 +45,8 @@ fn main() {
|
|||
"ioctl_aarch64be.rs"
|
||||
} else if cfg!(all(target_arch = "s390x", target_endian = "big")) {
|
||||
"ioctl_s390xbe.rs"
|
||||
} else if cfg!(all(target_arch = "riscv64", target_endian = "little")) {
|
||||
"ioctl_riscv64.rs"
|
||||
} else {
|
||||
panic!("architecture not supported");
|
||||
};
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
/* 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 authenticator::{
|
||||
authenticatorservice::{AuthenticatorService, CtapVersion, RegisterArgsCtap1, SignArgsCtap1},
|
||||
statecallback::StateCallback,
|
||||
AuthenticatorTransports, KeyHandle, RegisterFlags, RegisterResult, SignFlags, SignResult,
|
||||
StatusUpdate,
|
||||
};
|
||||
use getopts::Options;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::mpsc::{channel, RecvError};
|
||||
use std::{env, io, thread};
|
||||
|
||||
fn u2f_get_key_handle_from_register_response(register_response: &[u8]) -> io::Result<Vec<u8>> {
|
||||
if register_response[0] != 0x05 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Reserved byte not set correctly",
|
||||
));
|
||||
}
|
||||
|
||||
let key_handle_len = register_response[66] as usize;
|
||||
let mut public_key = register_response.to_owned();
|
||||
let mut key_handle = public_key.split_off(67);
|
||||
let _attestation = key_handle.split_off(key_handle_len);
|
||||
|
||||
Ok(key_handle)
|
||||
}
|
||||
|
||||
fn print_usage(program: &str, opts: Options) {
|
||||
let brief = format!("Usage: {} [options]", program);
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let program = args[0].clone();
|
||||
|
||||
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..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!("{}", f.to_string()),
|
||||
};
|
||||
if matches.opt_present("help") {
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut manager = AuthenticatorService::new(CtapVersion::CTAP2)
|
||||
.expect("The auth service should initialize safely");
|
||||
|
||||
if !matches.opt_present("no-u2f-usb-hid") {
|
||||
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!(
|
||||
"{}{}",
|
||||
r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
|
||||
r#" "version": "U2F_V2", "appId": "http://demo.yubico.com"}"#
|
||||
);
|
||||
let mut challenge = Sha256::new();
|
||||
challenge.update(challenge_str.as_bytes());
|
||||
let chall_bytes = challenge.finalize().to_vec();
|
||||
|
||||
let mut application = Sha256::new();
|
||||
application.update(b"http://demo.yubico.com");
|
||||
let app_bytes = application.finalize().to_vec();
|
||||
|
||||
let flags = RegisterFlags::empty();
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
thread::spawn(move || loop {
|
||||
match status_rx.recv() {
|
||||
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
|
||||
println!("STATUS: device available: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
|
||||
println!("STATUS: device unavailable: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::Success { dev_info }) => {
|
||||
println!("STATUS: success using device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::PinError(..))
|
||||
| Ok(StatusUpdate::SelectDeviceNotice)
|
||||
| Ok(StatusUpdate::DeviceSelected(..)) => {
|
||||
panic!("STATUS: This can't happen for CTAP1!");
|
||||
}
|
||||
Err(RecvError) => {
|
||||
println!("STATUS: end");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let (register_tx, register_rx) = channel();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
register_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
let ctap_args = RegisterArgsCtap1 {
|
||||
flags,
|
||||
challenge: chall_bytes.clone(),
|
||||
application: app_bytes.clone(),
|
||||
key_handles: vec![],
|
||||
};
|
||||
manager
|
||||
.register(timeout_ms, ctap_args.into(), status_tx.clone(), callback)
|
||||
.expect("Couldn't register");
|
||||
|
||||
let register_result = register_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
let (register_data, device_info) = match register_result {
|
||||
Ok(RegisterResult::CTAP1(r, d)) => (r, d),
|
||||
Ok(RegisterResult::CTAP2(..)) => panic!("Did not request CTAP2, but got CTAP2 results!"),
|
||||
Err(e) => panic!("Registration failed {:?}", e),
|
||||
};
|
||||
|
||||
println!("Register result: {}", base64::encode(®ister_data));
|
||||
println!("Device info: {}", &device_info);
|
||||
println!("");
|
||||
println!("*********************************************************************");
|
||||
println!("Asking a security key to sign now, with the data from the register...");
|
||||
println!("*********************************************************************");
|
||||
|
||||
let credential = u2f_get_key_handle_from_register_response(®ister_data).unwrap();
|
||||
let key_handle = KeyHandle {
|
||||
credential,
|
||||
transports: AuthenticatorTransports::empty(),
|
||||
};
|
||||
|
||||
let flags = SignFlags::empty();
|
||||
let (sign_tx, sign_rx) = channel();
|
||||
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
sign_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
if let Err(e) = manager.sign(
|
||||
timeout_ms,
|
||||
SignArgsCtap1 {
|
||||
flags,
|
||||
challenge: chall_bytes,
|
||||
app_ids: vec![app_bytes],
|
||||
key_handles: vec![key_handle],
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
callback,
|
||||
) {
|
||||
panic!("Couldn't register: {:?}", e);
|
||||
}
|
||||
|
||||
let sign_result = sign_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
if let SignResult::CTAP1(_, handle_used, sign_data, device_info) =
|
||||
sign_result.expect("Sign failed")
|
||||
{
|
||||
println!("Sign result: {}", base64::encode(&sign_data));
|
||||
println!("Key handle used: {}", base64::encode(&handle_used));
|
||||
println!("Device info: {}", &device_info);
|
||||
println!("Done.");
|
||||
} else {
|
||||
panic!("Expected CTAP version 1 for sign result!");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
/* 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 authenticator::{
|
||||
authenticatorservice::{
|
||||
AuthenticatorService, CtapVersion, GetAssertionExtensions, GetAssertionOptions,
|
||||
HmacSecretExtension, MakeCredentialsExtensions, MakeCredentialsOptions, RegisterArgsCtap2,
|
||||
SignArgsCtap2,
|
||||
},
|
||||
ctap2::server::{
|
||||
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
|
||||
},
|
||||
errors::PinError,
|
||||
statecallback::StateCallback,
|
||||
COSEAlgorithm, Pin, RegisterResult, SignResult, StatusUpdate,
|
||||
};
|
||||
use getopts::Options;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::mpsc::{channel, RecvError};
|
||||
use std::{env, thread};
|
||||
|
||||
fn print_usage(program: &str, opts: Options) {
|
||||
let brief = format!("Usage: {} [options]", program);
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let program = args[0].clone();
|
||||
|
||||
let mut opts = Options::new();
|
||||
opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
|
||||
opts.optflag("h", "help", "print this help menu").optopt(
|
||||
"t",
|
||||
"timeout",
|
||||
"timeout in seconds",
|
||||
"SEC",
|
||||
);
|
||||
opts.optflag("s", "hmac_secret", "With hmac-secret");
|
||||
opts.optflag("h", "help", "print this help menu");
|
||||
let matches = match opts.parse(&args[1..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!("{}", f.to_string()),
|
||||
};
|
||||
if matches.opt_present("help") {
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut manager = AuthenticatorService::new(CtapVersion::CTAP2)
|
||||
.expect("The auth service should initialize safely");
|
||||
|
||||
if !matches.opt_present("no-u2f-usb-hid") {
|
||||
manager.add_u2f_usb_hid_platform_transports();
|
||||
}
|
||||
|
||||
let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
|
||||
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!(
|
||||
"{}{}",
|
||||
r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
|
||||
r#" "version": "U2F_V2", "appId": "http://example.com"}"#
|
||||
);
|
||||
let mut challenge = Sha256::new();
|
||||
challenge.update(challenge_str.as_bytes());
|
||||
let chall_bytes = challenge.finalize().to_vec();
|
||||
|
||||
// TODO(MS): Needs to be added to RegisterArgsCtap2
|
||||
// let flags = RegisterFlags::empty();
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
thread::spawn(move || loop {
|
||||
match status_rx.recv() {
|
||||
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
|
||||
println!("STATUS: device available: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
|
||||
println!("STATUS: device unavailable: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::Success { dev_info }) => {
|
||||
println!("STATUS: success using device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::SelectDeviceNotice) => {
|
||||
println!("STATUS: Please select a device by touching one of them.");
|
||||
}
|
||||
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
|
||||
println!("STATUS: Continuing with device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::PinError(error, sender)) => match error {
|
||||
PinError::PinRequired => {
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::InvalidPin(attempts) => {
|
||||
println!(
|
||||
"Wrong PIN! {}",
|
||||
attempts.map_or(format!("Try again."), |a| format!(
|
||||
"You have {} attempts left.",
|
||||
a
|
||||
))
|
||||
);
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::PinAuthBlocked => {
|
||||
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
|
||||
}
|
||||
PinError::PinBlocked => {
|
||||
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
|
||||
}
|
||||
e => {
|
||||
panic!("Unexpected error: {:?}", e)
|
||||
}
|
||||
},
|
||||
Err(RecvError) => {
|
||||
println!("STATUS: end");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let user = User {
|
||||
id: "user_id".as_bytes().to_vec(),
|
||||
icon: None,
|
||||
name: Some("A. User".to_string()),
|
||||
display_name: None,
|
||||
};
|
||||
let origin = "https://example.com".to_string();
|
||||
let ctap_args = RegisterArgsCtap2 {
|
||||
challenge: chall_bytes.clone(),
|
||||
relying_party: RelyingParty {
|
||||
id: "example.com".to_string(),
|
||||
name: None,
|
||||
icon: None,
|
||||
},
|
||||
origin: origin.clone(),
|
||||
user: user.clone(),
|
||||
pub_cred_params: vec![
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::ES256,
|
||||
},
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::RS256,
|
||||
},
|
||||
],
|
||||
exclude_list: vec![PublicKeyCredentialDescriptor {
|
||||
id: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
|
||||
0x1c, 0x1d, 0x1e, 0x1f,
|
||||
],
|
||||
transports: vec![Transport::USB, Transport::NFC],
|
||||
}],
|
||||
options: MakeCredentialsOptions {
|
||||
resident_key: None,
|
||||
user_verification: None,
|
||||
},
|
||||
extensions: MakeCredentialsExtensions {
|
||||
hmac_secret: if matches.opt_present("hmac_secret") {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
pin: None,
|
||||
};
|
||||
|
||||
let attestation_object;
|
||||
let client_data;
|
||||
loop {
|
||||
let (register_tx, register_rx) = channel();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
register_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
if let Err(e) = manager.register(
|
||||
timeout_ms,
|
||||
ctap_args.clone().into(),
|
||||
status_tx.clone(),
|
||||
callback,
|
||||
) {
|
||||
panic!("Couldn't register: {:?}", e);
|
||||
};
|
||||
|
||||
let register_result = register_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
match register_result {
|
||||
Ok(RegisterResult::CTAP1(_, _)) => panic!("Requested CTAP2, but got CTAP1 results!"),
|
||||
Ok(RegisterResult::CTAP2(a, c)) => {
|
||||
println!("Ok!");
|
||||
attestation_object = a;
|
||||
client_data = c;
|
||||
break;
|
||||
}
|
||||
Err(e) => panic!("Registration failed: {:?}", e),
|
||||
};
|
||||
}
|
||||
|
||||
println!("Register result: {:?}", &attestation_object);
|
||||
println!("Collected client data: {:?}", &client_data);
|
||||
|
||||
println!("");
|
||||
println!("*********************************************************************");
|
||||
println!("Asking a security key to sign now, with the data from the register...");
|
||||
println!("*********************************************************************");
|
||||
|
||||
let allow_list;
|
||||
if let Some(cred_data) = attestation_object.auth_data.credential_data {
|
||||
allow_list = vec![PublicKeyCredentialDescriptor {
|
||||
id: cred_data.credential_id.clone(),
|
||||
transports: vec![Transport::USB],
|
||||
}];
|
||||
} else {
|
||||
allow_list = Vec::new();
|
||||
}
|
||||
|
||||
let ctap_args = SignArgsCtap2 {
|
||||
challenge: chall_bytes,
|
||||
origin,
|
||||
relying_party_id: "example.com".to_string(),
|
||||
allow_list,
|
||||
options: GetAssertionOptions::default(),
|
||||
extensions: GetAssertionExtensions {
|
||||
hmac_secret: if matches.opt_present("hmac_secret") {
|
||||
Some(HmacSecretExtension::new(
|
||||
vec![
|
||||
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0x10, 0x11, 0x12, 0x13,
|
||||
0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25,
|
||||
0x26, 0x27, 0x28, 0x29, 0x30, 0x31, 0x32, 0x33, 0x34,
|
||||
],
|
||||
None,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
pin: None,
|
||||
};
|
||||
|
||||
loop {
|
||||
let (sign_tx, sign_rx) = channel();
|
||||
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
sign_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
if let Err(e) = manager.sign(
|
||||
timeout_ms,
|
||||
ctap_args.clone().into(),
|
||||
status_tx.clone(),
|
||||
callback,
|
||||
) {
|
||||
panic!("Couldn't sign: {:?}", e);
|
||||
}
|
||||
|
||||
let sign_result = sign_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
|
||||
match sign_result {
|
||||
Ok(SignResult::CTAP1(..)) => panic!("Requested CTAP2, but got CTAP1 sign results!"),
|
||||
Ok(SignResult::CTAP2(assertion_object, _client_data)) => {
|
||||
println!("Assertion Object: {:?}", assertion_object);
|
||||
println!("Done.");
|
||||
break;
|
||||
}
|
||||
Err(e) => panic!("Signing failed: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
/* 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 authenticator::{
|
||||
authenticatorservice::{
|
||||
AuthenticatorService, CtapVersion, GetAssertionOptions, MakeCredentialsOptions,
|
||||
RegisterArgsCtap2, SignArgsCtap2,
|
||||
},
|
||||
ctap2::server::{
|
||||
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
|
||||
},
|
||||
errors::PinError,
|
||||
statecallback::StateCallback,
|
||||
COSEAlgorithm, Pin, RegisterResult, SignResult, StatusUpdate,
|
||||
};
|
||||
use getopts::Options;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::mpsc::{channel, RecvError};
|
||||
use std::{env, thread};
|
||||
|
||||
fn print_usage(program: &str, opts: Options) {
|
||||
println!("------------------------------------------------------------------------");
|
||||
println!("This program registers 3x the same origin with different users and");
|
||||
println!("requests 'discoverable credentials' for them.");
|
||||
println!("After that, we try to log in to that origin and list all credentials found.");
|
||||
println!("------------------------------------------------------------------------");
|
||||
let brief = format!("Usage: {} [options]", program);
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
fn register_user(manager: &mut AuthenticatorService, username: &str, timeout_ms: u64) {
|
||||
println!("");
|
||||
println!("*********************************************************************");
|
||||
println!(
|
||||
"Asking a security key to register now with user: {}",
|
||||
username
|
||||
);
|
||||
println!("*********************************************************************");
|
||||
|
||||
println!("Asking a security key to register now...");
|
||||
let challenge_str = format!(
|
||||
"{}{}{}{}",
|
||||
r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
|
||||
r#" "version": "U2F_V2", "appId": "http://example.com", "username": ""#,
|
||||
username,
|
||||
r#""}"#
|
||||
);
|
||||
let mut challenge = Sha256::new();
|
||||
challenge.update(challenge_str.as_bytes());
|
||||
let chall_bytes = challenge.finalize().to_vec();
|
||||
|
||||
// TODO(MS): Needs to be added to RegisterArgsCtap2
|
||||
// let flags = RegisterFlags::empty();
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
thread::spawn(move || loop {
|
||||
match status_rx.recv() {
|
||||
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
|
||||
println!("STATUS: device available: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
|
||||
println!("STATUS: device unavailable: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::Success { dev_info }) => {
|
||||
println!("STATUS: success using device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::SelectDeviceNotice) => {
|
||||
println!("STATUS: Please select a device by touching one of them.");
|
||||
}
|
||||
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
|
||||
println!("STATUS: Continuing with device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::PinError(error, sender)) => match error {
|
||||
PinError::PinRequired => {
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::InvalidPin(attempts) => {
|
||||
println!(
|
||||
"Wrong PIN! {}",
|
||||
attempts.map_or(format!("Try again."), |a| format!(
|
||||
"You have {} attempts left.",
|
||||
a
|
||||
))
|
||||
);
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::PinAuthBlocked => {
|
||||
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
|
||||
}
|
||||
PinError::PinBlocked => {
|
||||
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
|
||||
}
|
||||
e => {
|
||||
panic!("Unexpected error: {:?}", e)
|
||||
}
|
||||
},
|
||||
Err(RecvError) => {
|
||||
println!("STATUS: end");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let user = User {
|
||||
id: username.as_bytes().to_vec(),
|
||||
icon: None,
|
||||
name: Some(username.to_string()),
|
||||
display_name: None,
|
||||
};
|
||||
let origin = "https://example.com".to_string();
|
||||
let ctap_args = RegisterArgsCtap2 {
|
||||
challenge: chall_bytes.clone(),
|
||||
relying_party: RelyingParty {
|
||||
id: "example.com".to_string(),
|
||||
name: None,
|
||||
icon: None,
|
||||
},
|
||||
origin: origin.clone(),
|
||||
user: user.clone(),
|
||||
pub_cred_params: vec![
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::ES256,
|
||||
},
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::RS256,
|
||||
},
|
||||
],
|
||||
exclude_list: vec![PublicKeyCredentialDescriptor {
|
||||
id: vec![],
|
||||
transports: vec![Transport::USB, Transport::NFC],
|
||||
}],
|
||||
options: MakeCredentialsOptions {
|
||||
resident_key: Some(true),
|
||||
user_verification: Some(true),
|
||||
},
|
||||
extensions: Default::default(),
|
||||
pin: None,
|
||||
};
|
||||
|
||||
let attestation_object;
|
||||
let client_data;
|
||||
loop {
|
||||
let (register_tx, register_rx) = channel();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
register_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
if let Err(e) = manager.register(
|
||||
timeout_ms,
|
||||
ctap_args.clone().into(),
|
||||
status_tx.clone(),
|
||||
callback,
|
||||
) {
|
||||
panic!("Couldn't register: {:?}", e);
|
||||
};
|
||||
|
||||
let register_result = register_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
match register_result {
|
||||
Ok(RegisterResult::CTAP1(_, _)) => panic!("Requested CTAP2, but got CTAP1 results!"),
|
||||
Ok(RegisterResult::CTAP2(a, c)) => {
|
||||
println!("Ok!");
|
||||
attestation_object = a;
|
||||
client_data = c;
|
||||
break;
|
||||
}
|
||||
Err(e) => panic!("Registration failed: {:?}", e),
|
||||
};
|
||||
}
|
||||
|
||||
println!("Register result: {:?}", &attestation_object);
|
||||
println!("Collected client data: {:?}", &client_data);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let program = args[0].clone();
|
||||
|
||||
let mut opts = Options::new();
|
||||
opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
|
||||
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..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!("{}", f.to_string()),
|
||||
};
|
||||
if matches.opt_present("help") {
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut manager = AuthenticatorService::new(CtapVersion::CTAP2)
|
||||
.expect("The auth service should initialize safely");
|
||||
|
||||
if !matches.opt_present("no-u2f-usb-hid") {
|
||||
manager.add_u2f_usb_hid_platform_transports();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
for username in vec!["A. User", "A. Nother", "Dr. Who"] {
|
||||
register_user(&mut manager, username, timeout_ms)
|
||||
}
|
||||
|
||||
println!("");
|
||||
println!("*********************************************************************");
|
||||
println!("Asking a security key to sign now, with the data from the register...");
|
||||
println!("*********************************************************************");
|
||||
|
||||
// Discovering creds:
|
||||
let allow_list = Vec::new();
|
||||
let origin = "https://example.com".to_string();
|
||||
let challenge_str = format!(
|
||||
"{}{}",
|
||||
r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
|
||||
r#" "version": "U2F_V2", "appId": "http://example.com" "#,
|
||||
);
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
thread::spawn(move || loop {
|
||||
match status_rx.recv() {
|
||||
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
|
||||
println!("STATUS: device available: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
|
||||
println!("STATUS: device unavailable: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::Success { dev_info }) => {
|
||||
println!("STATUS: success using device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::SelectDeviceNotice) => {
|
||||
println!("STATUS: Please select a device by touching one of them.");
|
||||
}
|
||||
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
|
||||
println!("STATUS: Continuing with device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::PinError(error, sender)) => match error {
|
||||
PinError::PinRequired => {
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::InvalidPin(attempts) => {
|
||||
println!(
|
||||
"Wrong PIN! {}",
|
||||
attempts.map_or(format!("Try again."), |a| format!(
|
||||
"You have {} attempts left.",
|
||||
a
|
||||
))
|
||||
);
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::PinAuthBlocked => {
|
||||
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
|
||||
}
|
||||
PinError::PinBlocked => {
|
||||
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
|
||||
}
|
||||
e => {
|
||||
panic!("Unexpected error: {:?}", e)
|
||||
}
|
||||
},
|
||||
Err(RecvError) => {
|
||||
println!("STATUS: end");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut challenge = Sha256::new();
|
||||
challenge.update(challenge_str.as_bytes());
|
||||
let chall_bytes = challenge.finalize().to_vec();
|
||||
let ctap_args = SignArgsCtap2 {
|
||||
challenge: chall_bytes,
|
||||
origin,
|
||||
relying_party_id: "example.com".to_string(),
|
||||
allow_list,
|
||||
options: GetAssertionOptions::default(),
|
||||
extensions: Default::default(),
|
||||
pin: None,
|
||||
};
|
||||
|
||||
loop {
|
||||
let (sign_tx, sign_rx) = channel();
|
||||
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
sign_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
if let Err(e) = manager.sign(
|
||||
timeout_ms,
|
||||
ctap_args.clone().into(),
|
||||
status_tx.clone(),
|
||||
callback,
|
||||
) {
|
||||
panic!("Couldn't sign: {:?}", e);
|
||||
}
|
||||
|
||||
let sign_result = sign_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
|
||||
match sign_result {
|
||||
Ok(SignResult::CTAP1(..)) => panic!("Requested CTAP2, but got CTAP1 sign results!"),
|
||||
Ok(SignResult::CTAP2(assertion_object, _client_data)) => {
|
||||
println!("Assertion Object: {:?}", assertion_object);
|
||||
println!("-----------------------------------------------------------------");
|
||||
println!("Found credentials:");
|
||||
println!(
|
||||
"{:?}",
|
||||
assertion_object
|
||||
.0
|
||||
.iter()
|
||||
.map(|x| x.user.clone().unwrap().name.unwrap()) // Unwrapping here, as these shouldn't fail
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
println!("-----------------------------------------------------------------");
|
||||
println!("Done.");
|
||||
break;
|
||||
}
|
||||
Err(e) => panic!("Signing failed: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,8 +3,10 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use authenticator::{
|
||||
authenticatorservice::AuthenticatorService, statecallback::StateCallback,
|
||||
AuthenticatorTransports, KeyHandle, RegisterFlags, SignFlags, StatusUpdate,
|
||||
authenticatorservice::{AuthenticatorService, CtapVersion, RegisterArgsCtap1, SignArgsCtap1},
|
||||
statecallback::StateCallback,
|
||||
AuthenticatorTransports, KeyHandle, RegisterFlags, RegisterResult, SignFlags, SignResult,
|
||||
StatusUpdate,
|
||||
};
|
||||
use getopts::Options;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
@ -53,15 +55,15 @@ fn main() {
|
|||
opts.optflag("h", "help", "print this help menu");
|
||||
let matches = match opts.parse(&args[1..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!(f.to_string()),
|
||||
Err(f) => panic!("{}", f.to_string()),
|
||||
};
|
||||
if matches.opt_present("help") {
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut manager =
|
||||
AuthenticatorService::new().expect("The auth service should initialize safely");
|
||||
let mut manager = AuthenticatorService::new(CtapVersion::CTAP1)
|
||||
.expect("The auth service should initialize safely");
|
||||
|
||||
if !matches.opt_present("no-u2f-usb-hid") {
|
||||
manager.add_u2f_usb_hid_platform_transports();
|
||||
|
@ -92,13 +94,13 @@ fn main() {
|
|||
r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
|
||||
r#" "version": "U2F_V2", "appId": "http://demo.yubico.com"}"#
|
||||
);
|
||||
let mut challenge = Sha256::default();
|
||||
challenge.input(challenge_str.as_bytes());
|
||||
let chall_bytes = challenge.result().to_vec();
|
||||
let mut challenge = Sha256::new();
|
||||
challenge.update(challenge_str.as_bytes());
|
||||
let chall_bytes = challenge.finalize().to_vec();
|
||||
|
||||
let mut application = Sha256::default();
|
||||
application.input(b"http://demo.yubico.com");
|
||||
let app_bytes = application.result().to_vec();
|
||||
let mut application = Sha256::new();
|
||||
application.update(b"http://demo.yubico.com");
|
||||
let app_bytes = application.finalize().to_vec();
|
||||
|
||||
let flags = RegisterFlags::empty();
|
||||
|
||||
|
@ -114,6 +116,11 @@ fn main() {
|
|||
Ok(StatusUpdate::Success { dev_info }) => {
|
||||
println!("STATUS: success using device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::PinError(..))
|
||||
| Ok(StatusUpdate::SelectDeviceNotice)
|
||||
| Ok(StatusUpdate::DeviceSelected(..)) => {
|
||||
panic!("STATUS: This can't happen for CTAP1!");
|
||||
}
|
||||
Err(RecvError) => {
|
||||
println!("STATUS: end");
|
||||
return;
|
||||
|
@ -126,22 +133,24 @@ fn main() {
|
|||
register_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
let ctap_args = RegisterArgsCtap1 {
|
||||
flags,
|
||||
challenge: chall_bytes.clone(),
|
||||
application: app_bytes.clone(),
|
||||
key_handles: vec![],
|
||||
};
|
||||
manager
|
||||
.register(
|
||||
flags,
|
||||
timeout_ms,
|
||||
chall_bytes.clone(),
|
||||
app_bytes.clone(),
|
||||
vec![],
|
||||
status_tx.clone(),
|
||||
callback,
|
||||
)
|
||||
.register(timeout_ms, ctap_args.into(), status_tx.clone(), callback)
|
||||
.expect("Couldn't register");
|
||||
|
||||
let register_result = register_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
let (register_data, device_info) = register_result.expect("Registration failed");
|
||||
let (register_data, device_info) = match register_result {
|
||||
Ok(RegisterResult::CTAP1(r, d)) => (r, d),
|
||||
Ok(RegisterResult::CTAP2(..)) => panic!("Did not request CTAP2, but got CTAP2 results!"),
|
||||
Err(_) => panic!("Registration failed"),
|
||||
};
|
||||
|
||||
println!("Register result: {}", base64::encode(®ister_data));
|
||||
println!("Device info: {}", &device_info);
|
||||
|
@ -160,11 +169,14 @@ fn main() {
|
|||
}));
|
||||
|
||||
if let Err(e) = manager.sign(
|
||||
flags,
|
||||
timeout_ms,
|
||||
chall_bytes,
|
||||
vec![app_bytes],
|
||||
vec![key_handle],
|
||||
SignArgsCtap1 {
|
||||
flags,
|
||||
challenge: chall_bytes,
|
||||
app_ids: vec![app_bytes],
|
||||
key_handles: vec![key_handle],
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
callback,
|
||||
) {
|
||||
|
@ -174,10 +186,14 @@ fn main() {
|
|||
let sign_result = sign_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
let (_, handle_used, sign_data, device_info) = sign_result.expect("Sign failed");
|
||||
|
||||
println!("Sign result: {}", base64::encode(&sign_data));
|
||||
println!("Key handle used: {}", base64::encode(&handle_used));
|
||||
println!("Device info: {}", &device_info);
|
||||
println!("Done.");
|
||||
if let SignResult::CTAP1(_, handle_used, sign_data, device_info) =
|
||||
sign_result.expect("Sign failed")
|
||||
{
|
||||
println!("Sign result: {}", base64::encode(&sign_data));
|
||||
println!("Key handle used: {}", base64::encode(&handle_used));
|
||||
println!("Device info: {}", &device_info);
|
||||
println!("Done.");
|
||||
} else {
|
||||
panic!("Expected CTAP version 1 for sign result!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/* 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 authenticator::{
|
||||
authenticatorservice::{AuthenticatorService, CtapVersion},
|
||||
ctap2::commands::StatusCode,
|
||||
errors::{AuthenticatorError, CommandError, HIDError},
|
||||
statecallback::StateCallback,
|
||||
StatusUpdate,
|
||||
};
|
||||
use getopts::Options;
|
||||
use std::env;
|
||||
use std::sync::mpsc::{channel, RecvError};
|
||||
|
||||
fn print_usage(program: &str, opts: Options) {
|
||||
let brief = format!("Usage: {} [options]", program);
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let program = args[0].clone();
|
||||
|
||||
let mut opts = Options::new();
|
||||
opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
|
||||
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..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!("{}", f.to_string()),
|
||||
};
|
||||
if matches.opt_present("help") {
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut manager = AuthenticatorService::new(CtapVersion::CTAP2)
|
||||
.expect("The auth service should initialize safely");
|
||||
|
||||
if !matches.opt_present("no-u2f-usb-hid") {
|
||||
manager.add_u2f_usb_hid_platform_transports();
|
||||
}
|
||||
|
||||
let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
|
||||
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!(
|
||||
"NOTE: Please unplug all devices, type in 'yes' and plug in the device that should be reset."
|
||||
);
|
||||
loop {
|
||||
let mut s = String::new();
|
||||
println!("ATTENTION: Resetting a device will wipe all credentials! Do you wish to continue? [yes/N]");
|
||||
std::io::stdin()
|
||||
.read_line(&mut s)
|
||||
.expect("Did not enter a correct string");
|
||||
let trimmed = s.trim();
|
||||
if trimmed.is_empty() || trimmed == "N" || trimmed == "n" {
|
||||
println!("Exiting without reset.");
|
||||
return;
|
||||
}
|
||||
if trimmed == "y" {
|
||||
println!("Please type in the whole word 'yes'");
|
||||
continue;
|
||||
}
|
||||
if trimmed == "yes" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
let (reset_tx, reset_rx) = channel();
|
||||
let rs_tx = reset_tx.clone();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
let _ = rs_tx.send(rv);
|
||||
}));
|
||||
|
||||
if let Err(e) = manager.reset(timeout_ms, status_tx.clone(), callback) {
|
||||
panic!("Couldn't register: {:?}", e);
|
||||
};
|
||||
|
||||
loop {
|
||||
match status_rx.recv() {
|
||||
Ok(StatusUpdate::SelectDeviceNotice) => {
|
||||
println!("ERROR: Please unplug all other tokens that should not be reset!");
|
||||
// Needed to give the tokens enough time to start blinking
|
||||
// otherwise we may cancel pre-maturely and this binary will hang
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
manager.cancel().unwrap();
|
||||
return;
|
||||
}
|
||||
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
|
||||
println!("STATUS: Continuing with device: {}", dev_info);
|
||||
break;
|
||||
}
|
||||
Ok(StatusUpdate::PinError(..)) => panic!("Reset should never ask for a PIN!"),
|
||||
Ok(_) => { /* Ignore all other updates */ }
|
||||
Err(RecvError) => {
|
||||
println!("RecvError");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reset_result = reset_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
match reset_result {
|
||||
Ok(()) => {
|
||||
println!("Token successfully reset!");
|
||||
}
|
||||
Err(AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::NotAllowed,
|
||||
_,
|
||||
)))) => {
|
||||
println!("Resetting is only allowed within the first 10 seconds after powering up.");
|
||||
println!("Please unplug your device, plug it back in and try again.");
|
||||
}
|
||||
Err(e) => panic!("Reset failed: {:?}", e),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
/* 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 authenticator::{
|
||||
authenticatorservice::{AuthenticatorService, CtapVersion},
|
||||
statecallback::StateCallback,
|
||||
Pin, PinError, StatusUpdate,
|
||||
};
|
||||
use getopts::Options;
|
||||
use std::sync::mpsc::{channel, RecvError};
|
||||
use std::{env, thread};
|
||||
|
||||
fn print_usage(program: &str, opts: Options) {
|
||||
let brief = format!("Usage: {} [options]", program);
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let program = args[0].clone();
|
||||
|
||||
let mut opts = Options::new();
|
||||
opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms");
|
||||
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..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!("{}", f.to_string()),
|
||||
};
|
||||
if matches.opt_present("help") {
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut manager = AuthenticatorService::new(CtapVersion::CTAP2)
|
||||
.expect("The auth service should initialize safely");
|
||||
|
||||
if !matches.opt_present("no-u2f-usb-hid") {
|
||||
manager.add_u2f_usb_hid_platform_transports();
|
||||
}
|
||||
|
||||
let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
|
||||
Ok(timeout_s) => {
|
||||
println!("Using {}s as the timeout", &timeout_s);
|
||||
timeout_s * 1_000
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let new_pin = rpassword::prompt_password_stderr("Enter new PIN: ").expect("Failed to read PIN");
|
||||
let repeat_new_pin =
|
||||
rpassword::prompt_password_stderr("Enter it again: ").expect("Failed to read PIN");
|
||||
if new_pin != repeat_new_pin {
|
||||
println!("PINs did not match!");
|
||||
return;
|
||||
}
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
thread::spawn(move || loop {
|
||||
match status_rx.recv() {
|
||||
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
|
||||
println!("STATUS: device available: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
|
||||
println!("STATUS: device unavailable: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::Success { dev_info }) => {
|
||||
println!("STATUS: success using device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::SelectDeviceNotice) => {
|
||||
println!("STATUS: Please select a device by touching one of them.");
|
||||
}
|
||||
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
|
||||
println!("STATUS: Continuing with device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::PinError(error, sender)) => match error {
|
||||
PinError::PinRequired => {
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter current PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::InvalidPin(attempts) => {
|
||||
println!(
|
||||
"Wrong PIN! {}",
|
||||
attempts.map_or(format!("Try again."), |a| format!(
|
||||
"You have {} attempts left.",
|
||||
a
|
||||
))
|
||||
);
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter current PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::PinAuthBlocked => {
|
||||
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
|
||||
}
|
||||
PinError::PinBlocked => {
|
||||
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
|
||||
}
|
||||
e => {
|
||||
panic!("Unexpected error: {:?}", e)
|
||||
}
|
||||
},
|
||||
Err(RecvError) => {
|
||||
println!("STATUS: end");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let (reset_tx, reset_rx) = channel();
|
||||
let rs_tx = reset_tx.clone();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
let _ = rs_tx.send(rv);
|
||||
}));
|
||||
|
||||
if let Err(e) = manager.set_pin(timeout_ms, Pin::new(&new_pin), status_tx.clone(), callback) {
|
||||
panic!("Couldn't call set_pin: {:?}", e);
|
||||
};
|
||||
|
||||
let reset_result = reset_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
match reset_result {
|
||||
Ok(()) => {
|
||||
println!("PIN successfully set!");
|
||||
}
|
||||
Err(e) => panic!("Setting PIN failed: {:?}", e),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
/* 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 authenticator::{
|
||||
authenticatorservice::{
|
||||
AuthenticatorService, CtapVersion, MakeCredentialsOptions, RegisterArgsCtap2,
|
||||
},
|
||||
ctap2::commands::StatusCode,
|
||||
ctap2::server::{
|
||||
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User,
|
||||
},
|
||||
errors::{AuthenticatorError, CommandError, HIDError, PinError},
|
||||
statecallback::StateCallback,
|
||||
COSEAlgorithm, Pin, RegisterResult, StatusUpdate,
|
||||
};
|
||||
|
||||
use getopts::Options;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::mpsc::{channel, RecvError};
|
||||
use std::{env, thread};
|
||||
|
||||
fn print_usage(program: &str, opts: Options) {
|
||||
let brief = format!("Usage: {} [options]", program);
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let program = args[0].clone();
|
||||
|
||||
let mut opts = Options::new();
|
||||
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..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!("{}", f.to_string()),
|
||||
};
|
||||
if matches.opt_present("help") {
|
||||
print_usage(&program, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut manager = AuthenticatorService::new(CtapVersion::CTAP2)
|
||||
.expect("The auth service should initialize safely");
|
||||
|
||||
manager.add_u2f_usb_hid_platform_transports();
|
||||
|
||||
let timeout_ms = match matches.opt_get_default::<u64>("timeout", 25) {
|
||||
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!(
|
||||
"{}{}",
|
||||
r#"{"challenge": "1vQ9mxionq0ngCnjD-wTsv1zUSrGRtFqG2xP09SbZ70","#,
|
||||
r#" "version": "U2F_V2", "appId": "http://example.com"}"#
|
||||
);
|
||||
let mut challenge = Sha256::new();
|
||||
challenge.update(challenge_str.as_bytes());
|
||||
let chall_bytes = challenge.finalize().to_vec();
|
||||
|
||||
// TODO(MS): Needs to be added to RegisterArgsCtap2
|
||||
// let flags = RegisterFlags::empty();
|
||||
|
||||
let (status_tx, status_rx) = channel::<StatusUpdate>();
|
||||
thread::spawn(move || loop {
|
||||
match status_rx.recv() {
|
||||
Ok(StatusUpdate::DeviceAvailable { dev_info }) => {
|
||||
println!("STATUS: device available: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::DeviceUnavailable { dev_info }) => {
|
||||
println!("STATUS: device unavailable: {}", dev_info)
|
||||
}
|
||||
Ok(StatusUpdate::Success { dev_info }) => {
|
||||
println!("STATUS: success using device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::SelectDeviceNotice) => {
|
||||
println!("STATUS: Please select a device by touching one of them.");
|
||||
}
|
||||
Ok(StatusUpdate::DeviceSelected(dev_info)) => {
|
||||
println!("STATUS: Continuing with device: {}", dev_info);
|
||||
}
|
||||
Ok(StatusUpdate::PinError(error, sender)) => match error {
|
||||
PinError::PinRequired => {
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::InvalidPin(attempts) => {
|
||||
println!(
|
||||
"Wrong PIN! {}",
|
||||
attempts.map_or(format!("Try again."), |a| format!(
|
||||
"You have {} attempts left.",
|
||||
a
|
||||
))
|
||||
);
|
||||
let raw_pin = rpassword::prompt_password_stderr("Enter PIN: ")
|
||||
.expect("Failed to read PIN");
|
||||
sender.send(Pin::new(&raw_pin)).expect("Failed to send PIN");
|
||||
continue;
|
||||
}
|
||||
PinError::PinAuthBlocked => {
|
||||
panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.")
|
||||
}
|
||||
PinError::PinBlocked => {
|
||||
panic!("Too many failed attempts. Your device has been blocked. Reset it.")
|
||||
}
|
||||
e => {
|
||||
panic!("Unexpected error: {:?}", e)
|
||||
}
|
||||
},
|
||||
Err(RecvError) => {
|
||||
println!("STATUS: end");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let user = User {
|
||||
id: "user_id".as_bytes().to_vec(),
|
||||
icon: None,
|
||||
name: Some("A. User".to_string()),
|
||||
display_name: None,
|
||||
};
|
||||
let origin = "https://example.com".to_string();
|
||||
let mut ctap_args = RegisterArgsCtap2 {
|
||||
challenge: chall_bytes.clone(),
|
||||
relying_party: RelyingParty {
|
||||
id: "example.com".to_string(),
|
||||
name: None,
|
||||
icon: None,
|
||||
},
|
||||
origin: origin.clone(),
|
||||
user: user.clone(),
|
||||
pub_cred_params: vec![
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::ES256,
|
||||
},
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::RS256,
|
||||
},
|
||||
],
|
||||
exclude_list: vec![],
|
||||
options: MakeCredentialsOptions {
|
||||
resident_key: None,
|
||||
user_verification: None,
|
||||
},
|
||||
extensions: Default::default(),
|
||||
pin: None,
|
||||
};
|
||||
|
||||
loop {
|
||||
let (register_tx, register_rx) = channel();
|
||||
let callback = StateCallback::new(Box::new(move |rv| {
|
||||
register_tx.send(rv).unwrap();
|
||||
}));
|
||||
|
||||
if let Err(e) = manager.register(
|
||||
timeout_ms,
|
||||
ctap_args.clone().into(),
|
||||
status_tx.clone(),
|
||||
callback,
|
||||
) {
|
||||
panic!("Couldn't register: {:?}", e);
|
||||
};
|
||||
|
||||
let register_result = register_rx
|
||||
.recv()
|
||||
.expect("Problem receiving, unable to continue");
|
||||
match register_result {
|
||||
Ok(RegisterResult::CTAP1(_, _)) => panic!("Requested CTAP2, but got CTAP1 results!"),
|
||||
Ok(RegisterResult::CTAP2(a, _c)) => {
|
||||
println!("Ok!");
|
||||
println!("Registering again with the key_handle we just got back. This should result in a 'already registered' error.");
|
||||
let registered_key_handle =
|
||||
a.auth_data.credential_data.unwrap().credential_id.clone();
|
||||
ctap_args.exclude_list = vec![PublicKeyCredentialDescriptor {
|
||||
id: registered_key_handle.clone(),
|
||||
transports: vec![Transport::USB],
|
||||
}];
|
||||
continue;
|
||||
}
|
||||
Err(AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::CredentialExcluded,
|
||||
None,
|
||||
)))) => {
|
||||
println!("Got an 'already registered' error. Quitting.");
|
||||
break;
|
||||
}
|
||||
Err(e) => panic!("Registration failed: {:?}", e),
|
||||
};
|
||||
}
|
||||
println!("Done")
|
||||
}
|
|
@ -2,22 +2,118 @@
|
|||
* 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::consts::PARAMETER_SIZE;
|
||||
use crate::ctap2::commands::client_pin::Pin;
|
||||
pub use crate::ctap2::commands::get_assertion::{
|
||||
GetAssertionExtensions, GetAssertionOptions, HmacSecretExtension,
|
||||
};
|
||||
pub use crate::ctap2::commands::make_credentials::{
|
||||
MakeCredentialsExtensions, MakeCredentialsOptions,
|
||||
};
|
||||
use crate::ctap2::server::{
|
||||
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User,
|
||||
};
|
||||
use crate::errors::*;
|
||||
use crate::manager::Manager;
|
||||
use crate::statecallback::StateCallback;
|
||||
use std::sync::{mpsc::Sender, Arc, Mutex};
|
||||
|
||||
use crate::consts::PARAMETER_SIZE;
|
||||
use crate::errors::*;
|
||||
use crate::statecallback::StateCallback;
|
||||
// TODO(MS): Once U2FManager gets completely removed, this can be removed as well
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CtapVersion {
|
||||
CTAP1,
|
||||
CTAP2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RegisterArgsCtap1 {
|
||||
pub flags: crate::RegisterFlags,
|
||||
pub challenge: Vec<u8>,
|
||||
pub application: crate::AppId,
|
||||
pub key_handles: Vec<crate::KeyHandle>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RegisterArgsCtap2 {
|
||||
pub challenge: Vec<u8>,
|
||||
pub relying_party: RelyingParty,
|
||||
pub origin: String,
|
||||
pub user: User,
|
||||
pub pub_cred_params: Vec<PublicKeyCredentialParameters>,
|
||||
pub exclude_list: Vec<PublicKeyCredentialDescriptor>,
|
||||
pub options: MakeCredentialsOptions,
|
||||
pub extensions: MakeCredentialsExtensions,
|
||||
pub pin: Option<Pin>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RegisterArgs {
|
||||
CTAP1(RegisterArgsCtap1),
|
||||
CTAP2(RegisterArgsCtap2),
|
||||
}
|
||||
|
||||
impl From<RegisterArgsCtap1> for RegisterArgs {
|
||||
fn from(args: RegisterArgsCtap1) -> Self {
|
||||
RegisterArgs::CTAP1(args)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RegisterArgsCtap2> for RegisterArgs {
|
||||
fn from(args: RegisterArgsCtap2) -> Self {
|
||||
RegisterArgs::CTAP2(args)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SignArgsCtap1 {
|
||||
pub flags: crate::SignFlags,
|
||||
pub challenge: Vec<u8>,
|
||||
pub app_ids: Vec<crate::AppId>,
|
||||
pub key_handles: Vec<crate::KeyHandle>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SignArgsCtap2 {
|
||||
pub challenge: Vec<u8>,
|
||||
pub origin: String,
|
||||
pub relying_party_id: String,
|
||||
pub allow_list: Vec<PublicKeyCredentialDescriptor>,
|
||||
pub options: GetAssertionOptions,
|
||||
pub extensions: GetAssertionExtensions,
|
||||
pub pin: Option<Pin>,
|
||||
// Todo: Extensions
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SignArgs {
|
||||
CTAP1(SignArgsCtap1),
|
||||
CTAP2(SignArgsCtap2),
|
||||
}
|
||||
|
||||
impl From<SignArgsCtap1> for SignArgs {
|
||||
fn from(args: SignArgsCtap1) -> Self {
|
||||
SignArgs::CTAP1(args)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignArgsCtap2> for SignArgs {
|
||||
fn from(args: SignArgsCtap2) -> Self {
|
||||
SignArgs::CTAP2(args)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AssertionExtensions {
|
||||
pub hmac_secret: Option<HmacSecretExtension>,
|
||||
}
|
||||
|
||||
pub trait AuthenticatorTransport {
|
||||
/// The implementation of this method must return quickly and should
|
||||
/// report its status via the status and callback methods
|
||||
fn register(
|
||||
&mut self,
|
||||
flags: crate::RegisterFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: crate::AppId,
|
||||
key_handles: Vec<crate::KeyHandle>,
|
||||
ctap_args: RegisterArgs,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
) -> crate::Result<()>;
|
||||
|
@ -26,20 +122,31 @@ pub trait AuthenticatorTransport {
|
|||
/// report its status via the status and callback methods
|
||||
fn sign(
|
||||
&mut self,
|
||||
flags: crate::SignFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
app_ids: Vec<crate::AppId>,
|
||||
key_handles: Vec<crate::KeyHandle>,
|
||||
ctap_args: SignArgs,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
) -> crate::Result<()>;
|
||||
|
||||
fn cancel(&mut self) -> crate::Result<()>;
|
||||
fn reset(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()>;
|
||||
fn set_pin(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
new_pin: Pin,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
pub struct AuthenticatorService {
|
||||
transports: Vec<Arc<Mutex<Box<dyn AuthenticatorTransport + Send>>>>,
|
||||
ctap_version: CtapVersion,
|
||||
}
|
||||
|
||||
fn clone_and_configure_cancellation_callback<T>(
|
||||
|
@ -62,9 +169,10 @@ fn clone_and_configure_cancellation_callback<T>(
|
|||
}
|
||||
|
||||
impl AuthenticatorService {
|
||||
pub fn new() -> crate::Result<Self> {
|
||||
pub fn new(ctap_version: CtapVersion) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
transports: Vec::new(),
|
||||
ctap_version,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -78,9 +186,16 @@ impl AuthenticatorService {
|
|||
}
|
||||
|
||||
pub fn add_u2f_usb_hid_platform_transports(&mut self) {
|
||||
match crate::U2FManager::new() {
|
||||
Ok(token) => self.add_transport(Box::new(token)),
|
||||
Err(e) => error!("Could not add U2F HID transport: {}", e),
|
||||
if self.ctap_version == CtapVersion::CTAP1 {
|
||||
match crate::U2FManager::new() {
|
||||
Ok(token) => self.add_transport(Box::new(token)),
|
||||
Err(e) => error!("Could not add U2F HID transport: {}", e),
|
||||
}
|
||||
} else {
|
||||
match Manager::new() {
|
||||
Ok(token) => self.add_transport(Box::new(token)),
|
||||
Err(e) => error!("Could not add CTAP2 HID transport: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,20 +212,30 @@ impl AuthenticatorService {
|
|||
|
||||
pub fn register(
|
||||
&mut self,
|
||||
flags: crate::RegisterFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: crate::AppId,
|
||||
key_handles: Vec<crate::KeyHandle>,
|
||||
ctap_args: RegisterArgs,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
) -> crate::Result<()> {
|
||||
if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
|
||||
match ctap_args {
|
||||
RegisterArgs::CTAP1(a) => self.register_ctap1(timeout, a, status, callback),
|
||||
RegisterArgs::CTAP2(a) => self.register_ctap2(timeout, a, status, callback),
|
||||
}
|
||||
}
|
||||
|
||||
fn register_ctap1(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
args: RegisterArgsCtap1,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
) -> crate::Result<()> {
|
||||
if args.challenge.len() != PARAMETER_SIZE || args.application.len() != PARAMETER_SIZE {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
|
||||
for key_handle in &key_handles {
|
||||
if key_handle.credential.len() > 256 {
|
||||
for key_handle in &args.key_handles {
|
||||
if key_handle.credential.len() >= 256 {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
}
|
||||
|
@ -136,11 +261,46 @@ impl AuthenticatorService {
|
|||
);
|
||||
|
||||
transport_mutex.lock().unwrap().register(
|
||||
flags,
|
||||
timeout,
|
||||
challenge.clone(),
|
||||
application.clone(),
|
||||
key_handles.clone(),
|
||||
RegisterArgs::CTAP1(args.clone()),
|
||||
status.clone(),
|
||||
clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_ctap2(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
args: RegisterArgsCtap2,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
) -> crate::Result<()> {
|
||||
let iterable_transports = self.transports.clone();
|
||||
if iterable_transports.is_empty() {
|
||||
return Err(AuthenticatorError::NoConfiguredTransports);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"register called with {} transports, iterable is {}",
|
||||
self.transports.len(),
|
||||
iterable_transports.len()
|
||||
);
|
||||
|
||||
for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
|
||||
let mut transports_to_cancel = iterable_transports.clone();
|
||||
transports_to_cancel.remove(idx);
|
||||
|
||||
debug!(
|
||||
"register transports_to_cancel {}",
|
||||
transports_to_cancel.len()
|
||||
);
|
||||
|
||||
transport_mutex.lock().unwrap().register(
|
||||
timeout,
|
||||
RegisterArgs::CTAP2(args.clone()),
|
||||
status.clone(),
|
||||
clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
|
||||
)?;
|
||||
|
@ -151,30 +311,40 @@ impl AuthenticatorService {
|
|||
|
||||
pub fn sign(
|
||||
&mut self,
|
||||
flags: crate::SignFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
app_ids: Vec<crate::AppId>,
|
||||
key_handles: Vec<crate::KeyHandle>,
|
||||
ctap_args: SignArgs,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
) -> crate::Result<()> {
|
||||
if challenge.len() != PARAMETER_SIZE {
|
||||
match ctap_args {
|
||||
SignArgs::CTAP1(a) => self.sign_ctap1(timeout, a, status, callback),
|
||||
SignArgs::CTAP2(a) => self.sign_ctap2(timeout, a, status, callback),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign_ctap1(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
args: SignArgsCtap1,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
) -> crate::Result<()> {
|
||||
if args.challenge.len() != PARAMETER_SIZE {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
|
||||
if app_ids.is_empty() {
|
||||
if args.app_ids.is_empty() {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
|
||||
for app_id in &app_ids {
|
||||
for app_id in &args.app_ids {
|
||||
if app_id.len() != PARAMETER_SIZE {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
}
|
||||
|
||||
for key_handle in &key_handles {
|
||||
if key_handle.credential.len() > 256 {
|
||||
for key_handle in &args.key_handles {
|
||||
if key_handle.credential.len() >= 256 {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
}
|
||||
|
@ -189,11 +359,35 @@ impl AuthenticatorService {
|
|||
transports_to_cancel.remove(idx);
|
||||
|
||||
transport_mutex.lock().unwrap().sign(
|
||||
flags,
|
||||
timeout,
|
||||
challenge.clone(),
|
||||
app_ids.clone(),
|
||||
key_handles.clone(),
|
||||
SignArgs::CTAP1(args.clone()),
|
||||
status.clone(),
|
||||
clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sign_ctap2(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
args: SignArgsCtap2,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
) -> crate::Result<()> {
|
||||
let iterable_transports = self.transports.clone();
|
||||
if iterable_transports.is_empty() {
|
||||
return Err(AuthenticatorError::NoConfiguredTransports);
|
||||
}
|
||||
|
||||
for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
|
||||
let mut transports_to_cancel = iterable_transports.clone();
|
||||
transports_to_cancel.remove(idx);
|
||||
|
||||
transport_mutex.lock().unwrap().sign(
|
||||
timeout,
|
||||
SignArgs::CTAP2(args.clone()),
|
||||
status.clone(),
|
||||
clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
|
||||
)?;
|
||||
|
@ -213,6 +407,74 @@ impl AuthenticatorService {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()> {
|
||||
let iterable_transports = self.transports.clone();
|
||||
if iterable_transports.is_empty() {
|
||||
return Err(AuthenticatorError::NoConfiguredTransports);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"reset called with {} transports, iterable is {}",
|
||||
self.transports.len(),
|
||||
iterable_transports.len()
|
||||
);
|
||||
|
||||
for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
|
||||
let mut transports_to_cancel = iterable_transports.clone();
|
||||
transports_to_cancel.remove(idx);
|
||||
|
||||
debug!("reset transports_to_cancel {}", transports_to_cancel.len());
|
||||
|
||||
transport_mutex.lock().unwrap().reset(
|
||||
timeout,
|
||||
status.clone(),
|
||||
clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_pin(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
new_pin: Pin,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()> {
|
||||
let iterable_transports = self.transports.clone();
|
||||
if iterable_transports.is_empty() {
|
||||
return Err(AuthenticatorError::NoConfiguredTransports);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"reset called with {} transports, iterable is {}",
|
||||
self.transports.len(),
|
||||
iterable_transports.len()
|
||||
);
|
||||
|
||||
for (idx, transport_mutex) in iterable_transports.iter().enumerate() {
|
||||
let mut transports_to_cancel = iterable_transports.clone();
|
||||
transports_to_cancel.remove(idx);
|
||||
|
||||
debug!("reset transports_to_cancel {}", transports_to_cancel.len());
|
||||
|
||||
transport_mutex.lock().unwrap().set_pin(
|
||||
timeout,
|
||||
new_pin.clone(),
|
||||
status.clone(),
|
||||
clone_and_configure_cancellation_callback(callback.clone(), transports_to_cancel),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
@ -221,10 +483,17 @@ impl AuthenticatorService {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AuthenticatorService, AuthenticatorTransport};
|
||||
use super::{
|
||||
AuthenticatorService, AuthenticatorTransport, CtapVersion, Pin,
|
||||
PublicKeyCredentialDescriptor, RegisterArgs, RegisterArgsCtap1, RegisterArgsCtap2,
|
||||
SignArgs, SignArgsCtap1, SignArgsCtap2, User,
|
||||
};
|
||||
use crate::consts::Capability;
|
||||
use crate::consts::PARAMETER_SIZE;
|
||||
use crate::ctap2::server::RelyingParty;
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::{AuthenticatorTransports, KeyHandle, RegisterFlags, SignFlags, StatusUpdate};
|
||||
use crate::{RegisterResult, SignResult};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::sync::Arc;
|
||||
|
@ -257,7 +526,7 @@ mod tests {
|
|||
version_major: 1,
|
||||
version_minor: 2,
|
||||
version_build: 3,
|
||||
cap_flags: 0,
|
||||
cap_flags: Capability::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -265,16 +534,13 @@ mod tests {
|
|||
impl AuthenticatorTransport for TestTransportDriver {
|
||||
fn register(
|
||||
&mut self,
|
||||
_flags: crate::RegisterFlags,
|
||||
_timeout: u64,
|
||||
_challenge: Vec<u8>,
|
||||
_application: crate::AppId,
|
||||
_key_handles: Vec<crate::KeyHandle>,
|
||||
_args: RegisterArgs,
|
||||
_status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
) -> crate::Result<()> {
|
||||
if self.consent {
|
||||
let rv = Ok((vec![0u8; 16], self.dev_info()));
|
||||
let rv = Ok(RegisterResult::CTAP1(vec![0u8; 16], self.dev_info()));
|
||||
thread::spawn(move || callback.call(rv));
|
||||
}
|
||||
Ok(())
|
||||
|
@ -282,16 +548,18 @@ mod tests {
|
|||
|
||||
fn sign(
|
||||
&mut self,
|
||||
_flags: crate::SignFlags,
|
||||
_timeout: u64,
|
||||
_challenge: Vec<u8>,
|
||||
_app_ids: Vec<crate::AppId>,
|
||||
_key_handles: Vec<crate::KeyHandle>,
|
||||
_ctap_args: SignArgs,
|
||||
_status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
) -> crate::Result<()> {
|
||||
if self.consent {
|
||||
let rv = Ok((vec![0u8; 0], vec![0u8; 0], vec![0u8; 0], self.dev_info()));
|
||||
let rv = Ok(SignResult::CTAP1(
|
||||
vec![0u8; 0],
|
||||
vec![0u8; 0],
|
||||
vec![0u8; 0],
|
||||
self.dev_info(),
|
||||
));
|
||||
thread::spawn(move || callback.call(rv));
|
||||
}
|
||||
Ok(())
|
||||
|
@ -307,6 +575,25 @@ mod tests {
|
|||
|_| Ok(()),
|
||||
)
|
||||
}
|
||||
|
||||
fn reset(
|
||||
&mut self,
|
||||
_timeout: u64,
|
||||
_status: Sender<crate::StatusUpdate>,
|
||||
_callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn set_pin(
|
||||
&mut self,
|
||||
_timeout: u64,
|
||||
_new_pin: Pin,
|
||||
_status: Sender<crate::StatusUpdate>,
|
||||
_callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
fn mk_key() -> KeyHandle {
|
||||
|
@ -329,16 +616,19 @@ mod tests {
|
|||
init();
|
||||
let (status_tx, _) = channel::<StatusUpdate>();
|
||||
|
||||
let mut s = AuthenticatorService::new().unwrap();
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
|
||||
s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
|
||||
|
||||
assert_matches!(
|
||||
s.register(
|
||||
RegisterFlags::empty(),
|
||||
1_000,
|
||||
vec![],
|
||||
mk_appid(),
|
||||
vec![mk_key()],
|
||||
RegisterArgsCtap1 {
|
||||
challenge: vec![],
|
||||
flags: RegisterFlags::empty(),
|
||||
application: mk_appid(),
|
||||
key_handles: vec![mk_key()],
|
||||
}
|
||||
.into(),
|
||||
status_tx.clone(),
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
|
@ -348,11 +638,14 @@ mod tests {
|
|||
|
||||
assert_matches!(
|
||||
s.sign(
|
||||
SignFlags::empty(),
|
||||
1_000,
|
||||
vec![],
|
||||
vec![mk_appid()],
|
||||
vec![mk_key()],
|
||||
SignArgsCtap1 {
|
||||
flags: SignFlags::empty(),
|
||||
challenge: vec![],
|
||||
app_ids: vec![mk_appid()],
|
||||
key_handles: vec![mk_key()]
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
|
@ -366,16 +659,19 @@ mod tests {
|
|||
init();
|
||||
let (status_tx, _) = channel::<StatusUpdate>();
|
||||
|
||||
let mut s = AuthenticatorService::new().unwrap();
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
|
||||
s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
|
||||
|
||||
assert_matches!(
|
||||
s.register(
|
||||
RegisterFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
vec![],
|
||||
vec![mk_key()],
|
||||
RegisterArgsCtap1 {
|
||||
challenge: mk_challenge(),
|
||||
flags: RegisterFlags::empty(),
|
||||
application: vec![],
|
||||
key_handles: vec![mk_key()],
|
||||
}
|
||||
.into(),
|
||||
status_tx.clone(),
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
|
@ -385,11 +681,14 @@ mod tests {
|
|||
|
||||
assert_matches!(
|
||||
s.sign(
|
||||
SignFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
vec![],
|
||||
vec![mk_key()],
|
||||
SignArgsCtap1 {
|
||||
flags: SignFlags::empty(),
|
||||
challenge: mk_challenge(),
|
||||
app_ids: vec![],
|
||||
key_handles: vec![mk_key()]
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
|
@ -406,16 +705,19 @@ mod tests {
|
|||
// This test yields OKs.
|
||||
let (status_tx, _) = channel::<StatusUpdate>();
|
||||
|
||||
let mut s = AuthenticatorService::new().unwrap();
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
|
||||
s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
|
||||
|
||||
assert_matches!(
|
||||
s.register(
|
||||
RegisterFlags::empty(),
|
||||
100,
|
||||
mk_challenge(),
|
||||
mk_appid(),
|
||||
vec![],
|
||||
RegisterArgsCtap1 {
|
||||
challenge: mk_challenge(),
|
||||
flags: RegisterFlags::empty(),
|
||||
application: mk_appid(),
|
||||
key_handles: vec![],
|
||||
}
|
||||
.into(),
|
||||
status_tx.clone(),
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
),
|
||||
|
@ -424,11 +726,14 @@ mod tests {
|
|||
|
||||
assert_matches!(
|
||||
s.sign(
|
||||
SignFlags::empty(),
|
||||
100,
|
||||
mk_challenge(),
|
||||
vec![mk_appid()],
|
||||
vec![],
|
||||
SignArgsCtap1 {
|
||||
flags: SignFlags::empty(),
|
||||
challenge: mk_challenge(),
|
||||
app_ids: vec![mk_appid()],
|
||||
key_handles: vec![]
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
),
|
||||
|
@ -446,16 +751,19 @@ mod tests {
|
|||
transports: AuthenticatorTransports::USB,
|
||||
};
|
||||
|
||||
let mut s = AuthenticatorService::new().unwrap();
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
|
||||
s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
|
||||
|
||||
assert_matches!(
|
||||
s.register(
|
||||
RegisterFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
mk_appid(),
|
||||
vec![large_key.clone()],
|
||||
RegisterArgsCtap1 {
|
||||
challenge: mk_challenge(),
|
||||
flags: RegisterFlags::empty(),
|
||||
application: mk_appid(),
|
||||
key_handles: vec![large_key.clone()],
|
||||
}
|
||||
.into(),
|
||||
status_tx.clone(),
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
|
@ -465,11 +773,14 @@ mod tests {
|
|||
|
||||
assert_matches!(
|
||||
s.sign(
|
||||
SignFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
vec![mk_appid()],
|
||||
vec![large_key],
|
||||
SignArgsCtap1 {
|
||||
flags: SignFlags::empty(),
|
||||
challenge: mk_challenge(),
|
||||
app_ids: vec![mk_appid()],
|
||||
key_handles: vec![large_key]
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
|
@ -478,19 +789,84 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_keys_ctap2() {
|
||||
init();
|
||||
let (status_tx, _) = channel::<StatusUpdate>();
|
||||
|
||||
let large_key = KeyHandle {
|
||||
credential: vec![0; 1000],
|
||||
transports: AuthenticatorTransports::USB,
|
||||
};
|
||||
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP2).unwrap();
|
||||
s.add_transport(Box::new(TestTransportDriver::new(true).unwrap()));
|
||||
|
||||
let ctap2_register_args = RegisterArgsCtap2 {
|
||||
challenge: mk_challenge(),
|
||||
relying_party: RelyingParty {
|
||||
id: "example.com".to_string(),
|
||||
name: None,
|
||||
icon: None,
|
||||
},
|
||||
origin: "example.com".to_string(),
|
||||
user: User {
|
||||
id: "user_id".as_bytes().to_vec(),
|
||||
icon: None,
|
||||
name: Some("A. User".to_string()),
|
||||
display_name: None,
|
||||
},
|
||||
pub_cred_params: vec![],
|
||||
exclude_list: vec![(&large_key).into()],
|
||||
options: Default::default(),
|
||||
extensions: Default::default(),
|
||||
pin: None,
|
||||
};
|
||||
|
||||
assert!(s
|
||||
.register(
|
||||
1_000,
|
||||
ctap2_register_args.into(),
|
||||
status_tx.clone(),
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
.is_ok(),);
|
||||
|
||||
let ctap2_sign_args = SignArgsCtap2 {
|
||||
challenge: mk_challenge(),
|
||||
origin: "example.com".to_string(),
|
||||
relying_party_id: "example.com".to_string(),
|
||||
allow_list: vec![(&large_key).into()],
|
||||
options: Default::default(),
|
||||
extensions: Default::default(),
|
||||
pin: None,
|
||||
};
|
||||
assert!(s
|
||||
.sign(
|
||||
1_000,
|
||||
ctap2_sign_args.into(),
|
||||
status_tx,
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
.is_ok(),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_transports() {
|
||||
init();
|
||||
let (status_tx, _) = channel::<StatusUpdate>();
|
||||
|
||||
let mut s = AuthenticatorService::new().unwrap();
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
|
||||
assert_matches!(
|
||||
s.register(
|
||||
RegisterFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
mk_appid(),
|
||||
vec![mk_key()],
|
||||
RegisterArgsCtap1 {
|
||||
challenge: mk_challenge(),
|
||||
flags: RegisterFlags::empty(),
|
||||
application: mk_appid(),
|
||||
key_handles: vec![mk_key()],
|
||||
}
|
||||
.into(),
|
||||
status_tx.clone(),
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
|
@ -500,11 +876,14 @@ mod tests {
|
|||
|
||||
assert_matches!(
|
||||
s.sign(
|
||||
SignFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
vec![mk_appid()],
|
||||
vec![mk_key()],
|
||||
SignArgsCtap1 {
|
||||
flags: SignFlags::empty(),
|
||||
challenge: mk_challenge(),
|
||||
app_ids: vec![mk_appid()],
|
||||
key_handles: vec![mk_key()]
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
StateCallback::new(Box::new(move |_rv| {})),
|
||||
)
|
||||
|
@ -523,7 +902,7 @@ mod tests {
|
|||
init();
|
||||
let (status_tx, _) = channel::<StatusUpdate>();
|
||||
|
||||
let mut s = AuthenticatorService::new().unwrap();
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
|
||||
let ttd_one = TestTransportDriver::new(true).unwrap();
|
||||
let ttd_two = TestTransportDriver::new(false).unwrap();
|
||||
let ttd_three = TestTransportDriver::new(false).unwrap();
|
||||
|
@ -539,11 +918,14 @@ mod tests {
|
|||
let callback = StateCallback::new(Box::new(move |_rv| {}));
|
||||
assert!(s
|
||||
.register(
|
||||
RegisterFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
mk_appid(),
|
||||
vec![],
|
||||
RegisterArgsCtap1 {
|
||||
challenge: mk_challenge(),
|
||||
flags: RegisterFlags::empty(),
|
||||
application: mk_appid(),
|
||||
key_handles: vec![],
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
callback.clone(),
|
||||
)
|
||||
|
@ -560,7 +942,7 @@ mod tests {
|
|||
init();
|
||||
let (status_tx, _) = channel::<StatusUpdate>();
|
||||
|
||||
let mut s = AuthenticatorService::new().unwrap();
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
|
||||
let ttd_one = TestTransportDriver::new(true).unwrap();
|
||||
let ttd_two = TestTransportDriver::new(false).unwrap();
|
||||
let ttd_three = TestTransportDriver::new(false).unwrap();
|
||||
|
@ -576,11 +958,14 @@ mod tests {
|
|||
let callback = StateCallback::new(Box::new(move |_rv| {}));
|
||||
assert!(s
|
||||
.sign(
|
||||
SignFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
vec![mk_appid()],
|
||||
vec![mk_key()],
|
||||
SignArgsCtap1 {
|
||||
flags: SignFlags::empty(),
|
||||
challenge: mk_challenge(),
|
||||
app_ids: vec![mk_appid()],
|
||||
key_handles: vec![mk_key()]
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
callback.clone(),
|
||||
)
|
||||
|
@ -597,7 +982,7 @@ mod tests {
|
|||
init();
|
||||
let (status_tx, _) = channel::<StatusUpdate>();
|
||||
|
||||
let mut s = AuthenticatorService::new().unwrap();
|
||||
let mut s = AuthenticatorService::new(CtapVersion::CTAP1).unwrap();
|
||||
// Let both of these race which one provides consent.
|
||||
let ttd_one = TestTransportDriver::new(true).unwrap();
|
||||
let ttd_two = TestTransportDriver::new(true).unwrap();
|
||||
|
@ -611,11 +996,14 @@ mod tests {
|
|||
let callback = StateCallback::new(Box::new(move |_rv| {}));
|
||||
assert!(s
|
||||
.register(
|
||||
RegisterFlags::empty(),
|
||||
1_000,
|
||||
mk_challenge(),
|
||||
mk_appid(),
|
||||
vec![],
|
||||
RegisterArgsCtap1 {
|
||||
challenge: mk_challenge(),
|
||||
flags: RegisterFlags::empty(),
|
||||
application: mk_appid(),
|
||||
key_handles: vec![],
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
callback.clone(),
|
||||
)
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
* 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::authenticatorservice::AuthenticatorService;
|
||||
use crate::authenticatorservice::{
|
||||
AuthenticatorService, CtapVersion, RegisterArgsCtap1, SignArgsCtap1,
|
||||
};
|
||||
use crate::errors;
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::{RegisterResult, SignResult};
|
||||
|
@ -46,7 +48,7 @@ unsafe fn from_raw(ptr: *const u8, len: usize) -> Vec<u8> {
|
|||
/// The handle returned by this method must be freed by the caller.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rust_u2f_mgr_new() -> *mut AuthenticatorService {
|
||||
if let Ok(mut mgr) = AuthenticatorService::new() {
|
||||
if let Ok(mut mgr) = AuthenticatorService::new(CtapVersion::CTAP1) {
|
||||
mgr.add_detected_transports();
|
||||
Box::into_raw(Box::new(mgr))
|
||||
} else {
|
||||
|
@ -61,7 +63,7 @@ pub extern "C" fn rust_u2f_mgr_new() -> *mut AuthenticatorService {
|
|||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_mgr_free(mgr: *mut AuthenticatorService) {
|
||||
if !mgr.is_null() {
|
||||
Box::from_raw(mgr);
|
||||
drop(Box::from_raw(mgr));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,7 +94,7 @@ pub unsafe extern "C" fn rust_u2f_app_ids_add(
|
|||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_app_ids_free(ids: *mut U2FAppIds) {
|
||||
if !ids.is_null() {
|
||||
Box::from_raw(ids);
|
||||
drop(Box::from_raw(ids));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +129,7 @@ pub unsafe extern "C" fn rust_u2f_khs_add(
|
|||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_khs_free(khs: *mut U2FKeyHandles) {
|
||||
if !khs.is_null() {
|
||||
Box::from_raw(khs);
|
||||
drop(Box::from_raw(khs));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,6 +149,22 @@ pub unsafe extern "C" fn rust_u2f_result_error(res: *const U2FResult) -> u8 {
|
|||
0 /* No error, the request succeeded. */
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method must be used before rust_u2f_resbuf_copy
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_resbuf_contains(res: *const U2FResult, bid: u8) -> bool {
|
||||
if res.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let U2FResult::Success(ref bufs) = *res {
|
||||
return bufs.contains_key(&bid);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method must be used before rust_u2f_resbuf_copy
|
||||
|
@ -201,7 +219,7 @@ pub unsafe extern "C" fn rust_u2f_resbuf_copy(
|
|||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_u2f_res_free(res: *mut U2FResult) {
|
||||
if !res.is_null() {
|
||||
Box::from_raw(res);
|
||||
drop(Box::from_raw(res));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,7 +267,7 @@ pub unsafe extern "C" fn rust_u2f_mgr_register(
|
|||
|
||||
let state_callback = StateCallback::<crate::Result<RegisterResult>>::new(Box::new(move |rv| {
|
||||
let result = match rv {
|
||||
Ok((registration, dev_info)) => {
|
||||
Ok(RegisterResult::CTAP1(registration, dev_info)) => {
|
||||
let mut bufs = HashMap::new();
|
||||
bufs.insert(RESBUF_ID_REGISTRATION, registration);
|
||||
bufs.insert(RESBUF_ID_VENDOR_NAME, dev_info.vendor_name);
|
||||
|
@ -259,21 +277,22 @@ pub unsafe extern "C" fn rust_u2f_mgr_register(
|
|||
bufs.insert(RESBUF_ID_FIRMWARE_BUILD, vec![dev_info.version_build]);
|
||||
U2FResult::Success(bufs)
|
||||
}
|
||||
Ok(RegisterResult::CTAP2(..)) => U2FResult::Error(
|
||||
errors::AuthenticatorError::VersionMismatch("rust_u2f_mgr_register", 1),
|
||||
),
|
||||
Err(e) => U2FResult::Error(e),
|
||||
};
|
||||
|
||||
callback(tid, Box::into_raw(Box::new(result)));
|
||||
}));
|
||||
|
||||
let res = (*mgr).register(
|
||||
let ctap_args = RegisterArgsCtap1 {
|
||||
flags,
|
||||
timeout,
|
||||
challenge,
|
||||
application,
|
||||
key_handles,
|
||||
status_tx,
|
||||
state_callback,
|
||||
);
|
||||
};
|
||||
|
||||
let res = (*mgr).register(timeout, ctap_args.into(), status_tx, state_callback);
|
||||
|
||||
if res.is_ok() {
|
||||
tid
|
||||
|
@ -329,7 +348,7 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign(
|
|||
let tid = new_tid();
|
||||
let state_callback = StateCallback::<crate::Result<SignResult>>::new(Box::new(move |rv| {
|
||||
let result = match rv {
|
||||
Ok((app_id, key_handle, signature, dev_info)) => {
|
||||
Ok(SignResult::CTAP1(app_id, key_handle, signature, dev_info)) => {
|
||||
let mut bufs = HashMap::new();
|
||||
bufs.insert(RESBUF_ID_KEYHANDLE, key_handle);
|
||||
bufs.insert(RESBUF_ID_SIGNATURE, signature);
|
||||
|
@ -341,6 +360,9 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign(
|
|||
bufs.insert(RESBUF_ID_FIRMWARE_BUILD, vec![dev_info.version_build]);
|
||||
U2FResult::Success(bufs)
|
||||
}
|
||||
Ok(SignResult::CTAP2(..)) => U2FResult::Error(
|
||||
errors::AuthenticatorError::VersionMismatch("rust_u2f_mgr_sign", 1),
|
||||
),
|
||||
Err(e) => U2FResult::Error(e),
|
||||
};
|
||||
|
||||
|
@ -348,11 +370,14 @@ pub unsafe extern "C" fn rust_u2f_mgr_sign(
|
|||
}));
|
||||
|
||||
let res = (*mgr).sign(
|
||||
flags,
|
||||
timeout,
|
||||
challenge,
|
||||
app_ids,
|
||||
key_handles,
|
||||
SignArgsCtap1 {
|
||||
flags,
|
||||
challenge,
|
||||
app_ids,
|
||||
key_handles,
|
||||
}
|
||||
.into(),
|
||||
status_tx,
|
||||
state_callback,
|
||||
);
|
||||
|
|
|
@ -5,8 +5,18 @@
|
|||
// Allow dead code in this module, since it's all packet consts anyways.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
pub const MAX_HID_RPT_SIZE: usize = 64;
|
||||
|
||||
/// Minimum size of the U2F Raw Message header (FIDO v1.x) in extended mode,
|
||||
/// including expected response length (L<sub>e</sub>).
|
||||
///
|
||||
/// Fields `CLA`, `INS`, `P1` and `P2` are 1 byte each, and L<sub>e</sub> is 3
|
||||
/// bytes. If there is a data payload, add 2 bytes (L<sub>c</sub> is 3 bytes,
|
||||
/// and L<sub>e</sub> is 2 bytes).
|
||||
pub const U2FAPDUHEADER_SIZE: usize = 7;
|
||||
|
||||
pub const CID_BROADCAST: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
|
||||
pub const TYPE_MASK: u8 = 0x80;
|
||||
pub const TYPE_INIT: u8 = 0x80;
|
||||
|
@ -30,13 +40,65 @@ pub const U2FHID_IF_VERSION: u32 = 2; // Current interface implementation versio
|
|||
pub const U2FHID_FRAME_TIMEOUT: u32 = 500; // Default frame timeout in ms
|
||||
pub const U2FHID_TRANS_TIMEOUT: u32 = 3000; // Default message timeout in ms
|
||||
|
||||
// U2FHID native commands
|
||||
pub const U2FHID_PING: u8 = TYPE_INIT | 0x01; // Echo data through local processor only
|
||||
pub const U2FHID_MSG: u8 = TYPE_INIT | 0x03; // Send U2F message frame
|
||||
pub const U2FHID_LOCK: u8 = TYPE_INIT | 0x04; // Send lock channel command
|
||||
pub const U2FHID_INIT: u8 = TYPE_INIT | 0x06; // Channel initialization
|
||||
pub const U2FHID_WINK: u8 = TYPE_INIT | 0x08; // Send device identification wink
|
||||
pub const U2FHID_ERROR: u8 = TYPE_INIT | 0x3f; // Error response
|
||||
// CTAPHID native commands
|
||||
const CTAPHID_PING: u8 = TYPE_INIT | 0x01; // Echo data through local processor only
|
||||
const CTAPHID_MSG: u8 = TYPE_INIT | 0x03; // Send U2F message frame
|
||||
const CTAPHID_LOCK: u8 = TYPE_INIT | 0x04; // Send lock channel command
|
||||
const CTAPHID_INIT: u8 = TYPE_INIT | 0x06; // Channel initialization
|
||||
const CTAPHID_WINK: u8 = TYPE_INIT | 0x08; // Send device identification wink
|
||||
const CTAPHID_CBOR: u8 = TYPE_INIT | 0x10; // Encapsulated CBOR encoded message
|
||||
const CTAPHID_CANCEL: u8 = TYPE_INIT | 0x11; // Cancel outstanding requests
|
||||
const CTAPHID_KEEPALIVE: u8 = TYPE_INIT | 0x3b; // Keepalive sent to authenticator every 100ms and whenever a status changes
|
||||
const CTAPHID_ERROR: u8 = TYPE_INIT | 0x3f; // Error response
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum HIDCmd {
|
||||
Ping,
|
||||
Msg,
|
||||
Lock,
|
||||
Init,
|
||||
Wink,
|
||||
Cbor,
|
||||
Cancel,
|
||||
Keepalive,
|
||||
Error,
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
impl Into<u8> for HIDCmd {
|
||||
fn into(self) -> u8 {
|
||||
match self {
|
||||
HIDCmd::Ping => CTAPHID_PING,
|
||||
HIDCmd::Msg => CTAPHID_MSG,
|
||||
HIDCmd::Lock => CTAPHID_LOCK,
|
||||
HIDCmd::Init => CTAPHID_INIT,
|
||||
HIDCmd::Wink => CTAPHID_WINK,
|
||||
HIDCmd::Cbor => CTAPHID_CBOR,
|
||||
HIDCmd::Cancel => CTAPHID_CANCEL,
|
||||
HIDCmd::Keepalive => CTAPHID_KEEPALIVE,
|
||||
HIDCmd::Error => CTAPHID_ERROR,
|
||||
HIDCmd::Unknown(v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for HIDCmd {
|
||||
fn from(v: u8) -> HIDCmd {
|
||||
match v {
|
||||
CTAPHID_PING => HIDCmd::Ping,
|
||||
CTAPHID_MSG => HIDCmd::Msg,
|
||||
CTAPHID_LOCK => HIDCmd::Lock,
|
||||
CTAPHID_INIT => HIDCmd::Init,
|
||||
CTAPHID_WINK => HIDCmd::Wink,
|
||||
CTAPHID_CBOR => HIDCmd::Cbor,
|
||||
CTAPHID_CANCEL => HIDCmd::Cancel,
|
||||
CTAPHID_KEEPALIVE => HIDCmd::Keepalive,
|
||||
CTAPHID_ERROR => HIDCmd::Error,
|
||||
v => HIDCmd::Unknown(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// U2FHID_MSG commands
|
||||
pub const U2F_VENDOR_FIRST: u8 = TYPE_INIT | 0x40; // First vendor defined command
|
||||
|
@ -57,8 +119,26 @@ pub const U2F_CHECK_IS_REGISTERED: u8 = 0x07; // Check if the key handle is regi
|
|||
|
||||
// U2FHID_INIT command defines
|
||||
pub const INIT_NONCE_SIZE: usize = 8; // Size of channel initialization challenge
|
||||
pub const CAPFLAG_WINK: u8 = 0x01; // Device supports WINK command
|
||||
pub const CAPFLAG_LOCK: u8 = 0x02; // Device supports LOCK command
|
||||
|
||||
bitflags! {
|
||||
#[derive(Serialize)]
|
||||
pub struct Capability: u8 {
|
||||
const WINK = 0x01;
|
||||
const LOCK = 0x02;
|
||||
const CBOR = 0x04;
|
||||
const NMSG = 0x08;
|
||||
}
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
pub fn has_fido1(self) -> bool {
|
||||
!self.contains(Capability::NMSG)
|
||||
}
|
||||
|
||||
pub fn has_fido2(self) -> bool {
|
||||
self.contains(Capability::CBOR)
|
||||
}
|
||||
}
|
||||
|
||||
// Low-level error codes. Return as negatives.
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
use super::{ByteBuf, COSEKey, ECDHSecret, ECDSACurve};
|
||||
use serde::Serialize;
|
||||
/*
|
||||
This is a dummy implementation for CI, to avoid having to install NSS or openSSL in the CI-pipeline
|
||||
*/
|
||||
|
||||
pub type Result<T> = std::result::Result<T, BackendError>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub enum BackendError {}
|
||||
|
||||
pub(crate) fn serialize_key(_curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
|
||||
// Copy from NSS
|
||||
let length = key[1..].len() / 2;
|
||||
let chunks: Vec<_> = key[1..].chunks_exact(length).collect();
|
||||
Ok((
|
||||
ByteBuf::from(chunks[0].to_vec()),
|
||||
ByteBuf::from(chunks[1].to_vec()),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn encapsulate(_key: &COSEKey) -> Result<ECDHSecret> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub(crate) fn encrypt(
|
||||
_key: &[u8],
|
||||
_plain_text: &[u8], /*PlainText*/
|
||||
) -> Result<Vec<u8> /*CypherText*/> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub(crate) fn decrypt(
|
||||
_key: &[u8],
|
||||
_cypher_text: &[u8], /*CypherText*/
|
||||
) -> Result<Vec<u8> /*PlainText*/> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub(crate) fn authenticate(_token: &[u8], _input: &[u8]) -> Result<Vec<u8>> {
|
||||
unimplemented!()
|
||||
}
|
|
@ -0,0 +1,924 @@
|
|||
/* 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::AuthenticatorError;
|
||||
use crate::{ctap2::commands::CommandError, transport::errors::HIDError};
|
||||
use serde::{
|
||||
de::{Error as SerdeError, MapAccess, Unexpected, Visitor},
|
||||
ser::SerializeMap,
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use serde_bytes::ByteBuf;
|
||||
use serde_cbor::Value;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "crypto_ring")] {
|
||||
#[path = "ring.rs"]
|
||||
pub mod imp;
|
||||
} else if #[cfg(feature = "crypto_openssl")] {
|
||||
#[path = "openssl.rs"]
|
||||
pub mod imp;
|
||||
} else if #[cfg(feature = "crypto_dummy")] {
|
||||
#[path = "dummy.rs"]
|
||||
pub mod imp;
|
||||
} else {
|
||||
#[path = "nss.rs"]
|
||||
pub mod imp;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use imp::{authenticate, decrypt, encapsulate, encrypt, serialize_key, BackendError};
|
||||
|
||||
/// An ECDSACurve identifier. You probably will never need to alter
|
||||
/// or use this value, as it is set inside the Credential for you.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ECDSACurve {
|
||||
// +---------+-------+----------+------------------------------------+
|
||||
// | Name | Value | Key Type | Description |
|
||||
// +---------+-------+----------+------------------------------------+
|
||||
// | P-256 | 1 | EC2 | NIST P-256 also known as secp256r1 |
|
||||
// | P-384 | 2 | EC2 | NIST P-384 also known as secp384r1 |
|
||||
// | P-521 | 3 | EC2 | NIST P-521 also known as secp521r1 |
|
||||
// | X25519 | 4 | OKP | X25519 for use w/ ECDH only |
|
||||
// | X448 | 5 | OKP | X448 for use w/ ECDH only |
|
||||
// | Ed25519 | 6 | OKP | Ed25519 for use w/ EdDSA only |
|
||||
// | Ed448 | 7 | OKP | Ed448 for use w/ EdDSA only |
|
||||
// +---------+-------+----------+------------------------------------+
|
||||
/// Identifies this curve as SECP256R1 (X9_62_PRIME256V1 in OpenSSL)
|
||||
SECP256R1 = 1,
|
||||
/// Identifies this curve as SECP384R1
|
||||
SECP384R1 = 2,
|
||||
/// Identifies this curve as SECP521R1
|
||||
SECP521R1 = 3,
|
||||
/// Identifieds this as OKP X25519 for use w/ ECDH only
|
||||
X25519 = 4,
|
||||
/// Identifieds this as OKP X448 for use w/ ECDH only
|
||||
X448 = 5,
|
||||
/// Identifieds this as OKP Ed25519 for use w/ EdDSA only
|
||||
Ed25519 = 6,
|
||||
/// Identifieds this as OKP Ed448 for use w/ EdDSA only
|
||||
Ed448 = 7,
|
||||
}
|
||||
|
||||
impl TryFrom<u64> for ECDSACurve {
|
||||
type Error = CryptoError;
|
||||
fn try_from(i: u64) -> Result<Self, Self::Error> {
|
||||
match i {
|
||||
1 => Ok(ECDSACurve::SECP256R1),
|
||||
2 => Ok(ECDSACurve::SECP384R1),
|
||||
3 => Ok(ECDSACurve::SECP521R1),
|
||||
4 => Ok(ECDSACurve::X25519),
|
||||
5 => Ok(ECDSACurve::X448),
|
||||
6 => Ok(ECDSACurve::Ed25519),
|
||||
7 => Ok(ECDSACurve::Ed448),
|
||||
_ => Err(CryptoError::UnknownKeyType),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A COSE signature algorithm, indicating the type of key and hash type
|
||||
/// that should be used.
|
||||
/// see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum COSEAlgorithm {
|
||||
// /// Identifies this key as ECDSA (recommended SECP256R1) with SHA256 hashing
|
||||
// //#[serde(alias = "ECDSA_SHA256")]
|
||||
// ES256 = -7, // recommends curve SECP256R1
|
||||
// /// Identifies this key as ECDSA (recommended SECP384R1) with SHA384 hashing
|
||||
// //#[serde(alias = "ECDSA_SHA384")]
|
||||
// ES384 = -35, // recommends curve SECP384R1
|
||||
// /// Identifies this key as ECDSA (recommended SECP521R1) with SHA512 hashing
|
||||
// //#[serde(alias = "ECDSA_SHA512")]
|
||||
// ES512 = -36, // recommends curve SECP521R1
|
||||
// /// Identifies this key as RS256 aka RSASSA-PKCS1-v1_5 w/ SHA-256
|
||||
// RS256 = -257,
|
||||
// /// Identifies this key as RS384 aka RSASSA-PKCS1-v1_5 w/ SHA-384
|
||||
// RS384 = -258,
|
||||
// /// Identifies this key as RS512 aka RSASSA-PKCS1-v1_5 w/ SHA-512
|
||||
// RS512 = -259,
|
||||
// /// Identifies this key as PS256 aka RSASSA-PSS w/ SHA-256
|
||||
// PS256 = -37,
|
||||
// /// Identifies this key as PS384 aka RSASSA-PSS w/ SHA-384
|
||||
// PS384 = -38,
|
||||
// /// Identifies this key as PS512 aka RSASSA-PSS w/ SHA-512
|
||||
// PS512 = -39,
|
||||
// /// Identifies this key as EdDSA (likely curve ed25519)
|
||||
// EDDSA = -8,
|
||||
// /// Identifies this as an INSECURE RS1 aka RSASSA-PKCS1-v1_5 using SHA-1. This is not
|
||||
// /// used by validators, but can exist in some windows hello tpm's
|
||||
// INSECURE_RS1 = -65535,
|
||||
INSECURE_RS1 = -65535, // RSASSA-PKCS1-v1_5 using SHA-1
|
||||
RS512 = -259, // RSASSA-PKCS1-v1_5 using SHA-512
|
||||
RS384 = -258, // RSASSA-PKCS1-v1_5 using SHA-384
|
||||
RS256 = -257, // RSASSA-PKCS1-v1_5 using SHA-256
|
||||
ES256K = -47, // ECDSA using secp256k1 curve and SHA-256
|
||||
HSS_LMS = -46, // HSS/LMS hash-based digital signature
|
||||
SHAKE256 = -45, // SHAKE-256 512-bit Hash Value
|
||||
SHA512 = -44, // SHA-2 512-bit Hash
|
||||
SHA384 = -43, // SHA-2 384-bit Hash
|
||||
RSAES_OAEP_SHA_512 = -42, // RSAES-OAEP w/ SHA-512
|
||||
RSAES_OAEP_SHA_256 = -41, // RSAES-OAEP w/ SHA-256
|
||||
RSAES_OAEP_RFC_8017_default = -40, // RSAES-OAEP w/ SHA-1
|
||||
PS512 = -39, // RSASSA-PSS w/ SHA-512
|
||||
PS384 = -38, // RSASSA-PSS w/ SHA-384
|
||||
PS256 = -37, // RSASSA-PSS w/ SHA-256
|
||||
ES512 = -36, // ECDSA w/ SHA-512
|
||||
ES384 = -35, // ECDSA w/ SHA-384
|
||||
ECDH_SS_A256KW = -34, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key
|
||||
ECDH_SS_A192KW = -33, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key
|
||||
ECDH_SS_A128KW = -32, // ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key
|
||||
ECDH_ES_A256KW = -31, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key
|
||||
ECDH_ES_A192KW = -30, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key
|
||||
ECDH_ES_A128KW = -29, // ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key
|
||||
ECDH_SS_HKDF512 = -28, // ECDH SS w/ HKDF - generate key directly
|
||||
ECDH_SS_HKDF256 = -27, // ECDH SS w/ HKDF - generate key directly
|
||||
ECDH_ES_HKDF512 = -26, // ECDH ES w/ HKDF - generate key directly
|
||||
ECDH_ES_HKDF256 = -25, // ECDH ES w/ HKDF - generate key directly
|
||||
SHAKE128 = -18, // SHAKE-128 256-bit Hash Value
|
||||
SHA512_256 = -17, // SHA-2 512-bit Hash truncated to 256-bits
|
||||
SHA256 = -16, // SHA-2 256-bit Hash
|
||||
SHA256_64 = -15, // SHA-2 256-bit Hash truncated to 64-bits
|
||||
SHA1 = -14, // SHA-1 Hash
|
||||
Direct_HKDF_AES256 = -13, // Shared secret w/ AES-MAC 256-bit key
|
||||
Direct_HKDF_AES128 = -12, // Shared secret w/ AES-MAC 128-bit key
|
||||
Direct_HKDF_SHA512 = -11, // Shared secret w/ HKDF and SHA-512
|
||||
Direct_HKDF_SHA256 = -10, // Shared secret w/ HKDF and SHA-256
|
||||
EDDSA = -8, // EdDSA
|
||||
ES256 = -7, // ECDSA w/ SHA-256
|
||||
Direct = -6, // Direct use of CEK
|
||||
A256KW = -5, // AES Key Wrap w/ 256-bit key
|
||||
A192KW = -4, // AES Key Wrap w/ 192-bit key
|
||||
A128KW = -3, // AES Key Wrap w/ 128-bit key
|
||||
A128GCM = 1, // AES-GCM mode w/ 128-bit key, 128-bit tag
|
||||
A192GCM = 2, // AES-GCM mode w/ 192-bit key, 128-bit tag
|
||||
A256GCM = 3, // AES-GCM mode w/ 256-bit key, 128-bit tag
|
||||
HMAC256_64 = 4, // HMAC w/ SHA-256 truncated to 64 bits
|
||||
HMAC256_256 = 5, // HMAC w/ SHA-256
|
||||
HMAC384_384 = 6, // HMAC w/ SHA-384
|
||||
HMAC512_512 = 7, // HMAC w/ SHA-512
|
||||
AES_CCM_16_64_128 = 10, // AES-CCM mode 128-bit key, 64-bit tag, 13-byte nonce
|
||||
AES_CCM_16_64_256 = 11, // AES-CCM mode 256-bit key, 64-bit tag, 13-byte nonce
|
||||
AES_CCM_64_64_128 = 12, // AES-CCM mode 128-bit key, 64-bit tag, 7-byte nonce
|
||||
AES_CCM_64_64_256 = 13, // AES-CCM mode 256-bit key, 64-bit tag, 7-byte nonce
|
||||
AES_MAC_128_64 = 14, // AES-MAC 128-bit key, 64-bit tag
|
||||
AES_MAC_256_64 = 15, // AES-MAC 256-bit key, 64-bit tag
|
||||
ChaCha20_Poly1305 = 24, // ChaCha20/Poly1305 w/ 256-bit key, 128-bit tag
|
||||
AES_MAC_128_128 = 25, // AES-MAC 128-bit key, 128-bit tag
|
||||
AES_MAC_256_128 = 26, // AES-MAC 256-bit key, 128-bit tag
|
||||
AES_CCM_16_128_128 = 30, // AES-CCM mode 128-bit key, 128-bit tag, 13-byte nonce
|
||||
AES_CCM_16_128_256 = 31, // AES-CCM mode 256-bit key, 128-bit tag, 13-byte nonce
|
||||
AES_CCM_64_128_128 = 32, // AES-CCM mode 128-bit key, 128-bit tag, 7-byte nonce
|
||||
AES_CCM_64_128_256 = 33, // AES-CCM mode 256-bit key, 128-bit tag, 7-byte nonce
|
||||
IV_GENERATION = 34, // For doing IV generation for symmetric algorithms.
|
||||
}
|
||||
|
||||
impl Serialize for COSEAlgorithm {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match *self {
|
||||
COSEAlgorithm::RS512 => serializer.serialize_i16(-259),
|
||||
COSEAlgorithm::RS384 => serializer.serialize_i16(-258),
|
||||
COSEAlgorithm::RS256 => serializer.serialize_i16(-257),
|
||||
COSEAlgorithm::ES256K => serializer.serialize_i8(-47),
|
||||
COSEAlgorithm::HSS_LMS => serializer.serialize_i8(-46),
|
||||
COSEAlgorithm::SHAKE256 => serializer.serialize_i8(-45),
|
||||
COSEAlgorithm::SHA512 => serializer.serialize_i8(-44),
|
||||
COSEAlgorithm::SHA384 => serializer.serialize_i8(-43),
|
||||
COSEAlgorithm::RSAES_OAEP_SHA_512 => serializer.serialize_i8(-42),
|
||||
COSEAlgorithm::RSAES_OAEP_SHA_256 => serializer.serialize_i8(-41),
|
||||
COSEAlgorithm::RSAES_OAEP_RFC_8017_default => serializer.serialize_i8(-40),
|
||||
COSEAlgorithm::PS512 => serializer.serialize_i8(-39),
|
||||
COSEAlgorithm::PS384 => serializer.serialize_i8(-38),
|
||||
COSEAlgorithm::PS256 => serializer.serialize_i8(-37),
|
||||
COSEAlgorithm::ES512 => serializer.serialize_i8(-36),
|
||||
COSEAlgorithm::ES384 => serializer.serialize_i8(-35),
|
||||
COSEAlgorithm::ECDH_SS_A256KW => serializer.serialize_i8(-34),
|
||||
COSEAlgorithm::ECDH_SS_A192KW => serializer.serialize_i8(-33),
|
||||
COSEAlgorithm::ECDH_SS_A128KW => serializer.serialize_i8(-32),
|
||||
COSEAlgorithm::ECDH_ES_A256KW => serializer.serialize_i8(-31),
|
||||
COSEAlgorithm::ECDH_ES_A192KW => serializer.serialize_i8(-30),
|
||||
COSEAlgorithm::ECDH_ES_A128KW => serializer.serialize_i8(-29),
|
||||
COSEAlgorithm::ECDH_SS_HKDF512 => serializer.serialize_i8(-28),
|
||||
COSEAlgorithm::ECDH_SS_HKDF256 => serializer.serialize_i8(-27),
|
||||
COSEAlgorithm::ECDH_ES_HKDF512 => serializer.serialize_i8(-26),
|
||||
COSEAlgorithm::ECDH_ES_HKDF256 => serializer.serialize_i8(-25),
|
||||
COSEAlgorithm::SHAKE128 => serializer.serialize_i8(-18),
|
||||
COSEAlgorithm::SHA512_256 => serializer.serialize_i8(-17),
|
||||
COSEAlgorithm::SHA256 => serializer.serialize_i8(-16),
|
||||
COSEAlgorithm::SHA256_64 => serializer.serialize_i8(-15),
|
||||
COSEAlgorithm::SHA1 => serializer.serialize_i8(-14),
|
||||
COSEAlgorithm::Direct_HKDF_AES256 => serializer.serialize_i8(-13),
|
||||
COSEAlgorithm::Direct_HKDF_AES128 => serializer.serialize_i8(-12),
|
||||
COSEAlgorithm::Direct_HKDF_SHA512 => serializer.serialize_i8(-11),
|
||||
COSEAlgorithm::Direct_HKDF_SHA256 => serializer.serialize_i8(-10),
|
||||
COSEAlgorithm::EDDSA => serializer.serialize_i8(-8),
|
||||
COSEAlgorithm::ES256 => serializer.serialize_i8(-7),
|
||||
COSEAlgorithm::Direct => serializer.serialize_i8(-6),
|
||||
COSEAlgorithm::A256KW => serializer.serialize_i8(-5),
|
||||
COSEAlgorithm::A192KW => serializer.serialize_i8(-4),
|
||||
COSEAlgorithm::A128KW => serializer.serialize_i8(-3),
|
||||
COSEAlgorithm::A128GCM => serializer.serialize_i8(1),
|
||||
COSEAlgorithm::A192GCM => serializer.serialize_i8(2),
|
||||
COSEAlgorithm::A256GCM => serializer.serialize_i8(3),
|
||||
COSEAlgorithm::HMAC256_64 => serializer.serialize_i8(4),
|
||||
COSEAlgorithm::HMAC256_256 => serializer.serialize_i8(5),
|
||||
COSEAlgorithm::HMAC384_384 => serializer.serialize_i8(6),
|
||||
COSEAlgorithm::HMAC512_512 => serializer.serialize_i8(7),
|
||||
COSEAlgorithm::AES_CCM_16_64_128 => serializer.serialize_i8(10),
|
||||
COSEAlgorithm::AES_CCM_16_64_256 => serializer.serialize_i8(11),
|
||||
COSEAlgorithm::AES_CCM_64_64_128 => serializer.serialize_i8(12),
|
||||
COSEAlgorithm::AES_CCM_64_64_256 => serializer.serialize_i8(13),
|
||||
COSEAlgorithm::AES_MAC_128_64 => serializer.serialize_i8(14),
|
||||
COSEAlgorithm::AES_MAC_256_64 => serializer.serialize_i8(15),
|
||||
COSEAlgorithm::ChaCha20_Poly1305 => serializer.serialize_i8(24),
|
||||
COSEAlgorithm::AES_MAC_128_128 => serializer.serialize_i8(25),
|
||||
COSEAlgorithm::AES_MAC_256_128 => serializer.serialize_i8(26),
|
||||
COSEAlgorithm::AES_CCM_16_128_128 => serializer.serialize_i8(30),
|
||||
COSEAlgorithm::AES_CCM_16_128_256 => serializer.serialize_i8(31),
|
||||
COSEAlgorithm::AES_CCM_64_128_128 => serializer.serialize_i8(32),
|
||||
COSEAlgorithm::AES_CCM_64_128_256 => serializer.serialize_i8(33),
|
||||
COSEAlgorithm::IV_GENERATION => serializer.serialize_i8(34),
|
||||
COSEAlgorithm::INSECURE_RS1 => serializer.serialize_i32(-65535),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for COSEAlgorithm {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct COSEAlgorithmVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for COSEAlgorithmVisitor {
|
||||
type Value = COSEAlgorithm;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a signed integer")
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: SerdeError,
|
||||
{
|
||||
COSEAlgorithm::try_from(v).map_err(|_| {
|
||||
SerdeError::invalid_value(Unexpected::Signed(v), &"valid COSEAlgorithm")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(COSEAlgorithmVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i64> for COSEAlgorithm {
|
||||
type Error = CryptoError;
|
||||
fn try_from(i: i64) -> Result<Self, Self::Error> {
|
||||
match i {
|
||||
-259 => Ok(COSEAlgorithm::RS512),
|
||||
-258 => Ok(COSEAlgorithm::RS384),
|
||||
-257 => Ok(COSEAlgorithm::RS256),
|
||||
-47 => Ok(COSEAlgorithm::ES256K),
|
||||
-46 => Ok(COSEAlgorithm::HSS_LMS),
|
||||
-45 => Ok(COSEAlgorithm::SHAKE256),
|
||||
-44 => Ok(COSEAlgorithm::SHA512),
|
||||
-43 => Ok(COSEAlgorithm::SHA384),
|
||||
-42 => Ok(COSEAlgorithm::RSAES_OAEP_SHA_512),
|
||||
-41 => Ok(COSEAlgorithm::RSAES_OAEP_SHA_256),
|
||||
-40 => Ok(COSEAlgorithm::RSAES_OAEP_RFC_8017_default),
|
||||
-39 => Ok(COSEAlgorithm::PS512),
|
||||
-38 => Ok(COSEAlgorithm::PS384),
|
||||
-37 => Ok(COSEAlgorithm::PS256),
|
||||
-36 => Ok(COSEAlgorithm::ES512),
|
||||
-35 => Ok(COSEAlgorithm::ES384),
|
||||
-34 => Ok(COSEAlgorithm::ECDH_SS_A256KW),
|
||||
-33 => Ok(COSEAlgorithm::ECDH_SS_A192KW),
|
||||
-32 => Ok(COSEAlgorithm::ECDH_SS_A128KW),
|
||||
-31 => Ok(COSEAlgorithm::ECDH_ES_A256KW),
|
||||
-30 => Ok(COSEAlgorithm::ECDH_ES_A192KW),
|
||||
-29 => Ok(COSEAlgorithm::ECDH_ES_A128KW),
|
||||
-28 => Ok(COSEAlgorithm::ECDH_SS_HKDF512),
|
||||
-27 => Ok(COSEAlgorithm::ECDH_SS_HKDF256),
|
||||
-26 => Ok(COSEAlgorithm::ECDH_ES_HKDF512),
|
||||
-25 => Ok(COSEAlgorithm::ECDH_ES_HKDF256),
|
||||
-18 => Ok(COSEAlgorithm::SHAKE128),
|
||||
-17 => Ok(COSEAlgorithm::SHA512_256),
|
||||
-16 => Ok(COSEAlgorithm::SHA256),
|
||||
-15 => Ok(COSEAlgorithm::SHA256_64),
|
||||
-14 => Ok(COSEAlgorithm::SHA1),
|
||||
-13 => Ok(COSEAlgorithm::Direct_HKDF_AES256),
|
||||
-12 => Ok(COSEAlgorithm::Direct_HKDF_AES128),
|
||||
-11 => Ok(COSEAlgorithm::Direct_HKDF_SHA512),
|
||||
-10 => Ok(COSEAlgorithm::Direct_HKDF_SHA256),
|
||||
-8 => Ok(COSEAlgorithm::EDDSA),
|
||||
-7 => Ok(COSEAlgorithm::ES256),
|
||||
-6 => Ok(COSEAlgorithm::Direct),
|
||||
-5 => Ok(COSEAlgorithm::A256KW),
|
||||
-4 => Ok(COSEAlgorithm::A192KW),
|
||||
-3 => Ok(COSEAlgorithm::A128KW),
|
||||
1 => Ok(COSEAlgorithm::A128GCM),
|
||||
2 => Ok(COSEAlgorithm::A192GCM),
|
||||
3 => Ok(COSEAlgorithm::A256GCM),
|
||||
4 => Ok(COSEAlgorithm::HMAC256_64),
|
||||
5 => Ok(COSEAlgorithm::HMAC256_256),
|
||||
6 => Ok(COSEAlgorithm::HMAC384_384),
|
||||
7 => Ok(COSEAlgorithm::HMAC512_512),
|
||||
10 => Ok(COSEAlgorithm::AES_CCM_16_64_128),
|
||||
11 => Ok(COSEAlgorithm::AES_CCM_16_64_256),
|
||||
12 => Ok(COSEAlgorithm::AES_CCM_64_64_128),
|
||||
13 => Ok(COSEAlgorithm::AES_CCM_64_64_256),
|
||||
14 => Ok(COSEAlgorithm::AES_MAC_128_64),
|
||||
15 => Ok(COSEAlgorithm::AES_MAC_256_64),
|
||||
24 => Ok(COSEAlgorithm::ChaCha20_Poly1305),
|
||||
25 => Ok(COSEAlgorithm::AES_MAC_128_128),
|
||||
26 => Ok(COSEAlgorithm::AES_MAC_256_128),
|
||||
30 => Ok(COSEAlgorithm::AES_CCM_16_128_128),
|
||||
31 => Ok(COSEAlgorithm::AES_CCM_16_128_256),
|
||||
32 => Ok(COSEAlgorithm::AES_CCM_64_128_128),
|
||||
33 => Ok(COSEAlgorithm::AES_CCM_64_128_256),
|
||||
34 => Ok(COSEAlgorithm::IV_GENERATION),
|
||||
-65535 => Ok(COSEAlgorithm::INSECURE_RS1),
|
||||
_ => Err(CryptoError::UnknownAlgorithm),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A COSE Elliptic Curve Public Key. This is generally the provided credential
|
||||
/// that an authenticator registers, and is used to authenticate the user.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct COSEEC2Key {
|
||||
/// The curve that this key references.
|
||||
pub curve: ECDSACurve,
|
||||
/// The key's public X coordinate.
|
||||
pub x: Vec<u8>,
|
||||
/// The key's public Y coordinate.
|
||||
pub y: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A Octet Key Pair (OKP).
|
||||
/// The other version uses only the x-coordinate as the y-coordinate is
|
||||
/// either to be recomputed or not needed for the key agreement operation ('OKP').
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct COSEOKPKey {
|
||||
/// The curve that this key references.
|
||||
pub curve: ECDSACurve,
|
||||
/// The key's public X coordinate.
|
||||
pub x: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A COSE RSA PublicKey. This is a provided credential from a registered
|
||||
/// authenticator.
|
||||
/// You will likely never need to interact with this value, as it is part of the Credential
|
||||
/// API.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct COSERSAKey {
|
||||
/// An RSA modulus
|
||||
pub n: Vec<u8>,
|
||||
/// An RSA exponent
|
||||
pub e: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A Octet Key Pair (OKP).
|
||||
/// The other version uses only the x-coordinate as the y-coordinate is
|
||||
/// either to be recomputed or not needed for the key agreement operation ('OKP').
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct COSESymmetricKey {
|
||||
/// The key
|
||||
pub key: Vec<u8>,
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc8152#section-13
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[repr(i64)]
|
||||
pub enum COSEKeyTypeId {
|
||||
// Reserved is invalid
|
||||
// Reserved = 0,
|
||||
/// Octet Key Pair
|
||||
OKP = 1,
|
||||
/// Elliptic Curve Keys w/ x- and y-coordinate
|
||||
EC2 = 2,
|
||||
/// RSA
|
||||
RSA = 3,
|
||||
/// Symmetric
|
||||
Symmetric = 4,
|
||||
}
|
||||
|
||||
impl TryFrom<u64> for COSEKeyTypeId {
|
||||
type Error = CryptoError;
|
||||
fn try_from(i: u64) -> Result<Self, Self::Error> {
|
||||
match i {
|
||||
1 => Ok(COSEKeyTypeId::OKP),
|
||||
2 => Ok(COSEKeyTypeId::EC2),
|
||||
3 => Ok(COSEKeyTypeId::RSA),
|
||||
4 => Ok(COSEKeyTypeId::Symmetric),
|
||||
_ => Err(CryptoError::UnknownKeyType),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of Key contained within a COSE value. You should never need
|
||||
/// to alter or change this type.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum COSEKeyType {
|
||||
// +-----------+-------+-----------------------------------------------+
|
||||
// | Name | Value | Description |
|
||||
// +-----------+-------+-----------------------------------------------+
|
||||
// | OKP | 1 | Octet Key Pair |
|
||||
// | EC2 | 2 | Elliptic Curve Keys w/ x- and y-coordinate |
|
||||
// | | | pair |
|
||||
// | Symmetric | 4 | Symmetric Keys |
|
||||
// | Reserved | 0 | This value is reserved |
|
||||
// +-----------+-------+-----------------------------------------------+
|
||||
// Reserved, // should always be invalid.
|
||||
/// Identifies this as an Elliptic Curve octet key pair
|
||||
OKP(COSEOKPKey), // Not used here
|
||||
/// Identifies this as an Elliptic Curve EC2 key
|
||||
EC2(COSEEC2Key),
|
||||
/// Identifies this as an RSA key
|
||||
RSA(COSERSAKey), // Not used here
|
||||
/// Identifies this as a Symmetric key
|
||||
Symmetric(COSESymmetricKey), // Not used here
|
||||
}
|
||||
|
||||
/// A COSE Key as provided by the Authenticator. You should never need
|
||||
/// to alter or change these values.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct COSEKey {
|
||||
/// COSE signature algorithm, indicating the type of key and hash type
|
||||
/// that should be used.
|
||||
pub alg: COSEAlgorithm,
|
||||
/// The public key
|
||||
pub key: COSEKeyType,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for COSEKey {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct COSEKeyVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for COSEKeyVisitor {
|
||||
type Value = COSEKey;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a map")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> std::result::Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut curve: Option<ECDSACurve> = None;
|
||||
let mut key_type: Option<COSEKeyTypeId> = None;
|
||||
let mut alg: Option<COSEAlgorithm> = None;
|
||||
let mut x: Option<Vec<u8>> = None;
|
||||
let mut y: Option<Vec<u8>> = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
trace!("cose key {:?}", key);
|
||||
match key {
|
||||
1 => {
|
||||
if key_type.is_some() {
|
||||
return Err(SerdeError::duplicate_field("key_type"));
|
||||
}
|
||||
let value: u64 = map.next_value()?;
|
||||
let val = COSEKeyTypeId::try_from(value).map_err(|_| {
|
||||
SerdeError::custom(format!("unsupported key_type {}", value))
|
||||
})?;
|
||||
key_type = Some(val);
|
||||
// key_type = Some(map.next_value()?);
|
||||
}
|
||||
-1 => {
|
||||
let key_type = key_type.ok_or(SerdeError::missing_field("key_type"))?;
|
||||
if key_type == COSEKeyTypeId::RSA {
|
||||
if y.is_some() {
|
||||
return Err(SerdeError::duplicate_field("y"));
|
||||
}
|
||||
let value: ByteBuf = map.next_value()?;
|
||||
y = Some(value.to_vec());
|
||||
} else {
|
||||
if curve.is_some() {
|
||||
return Err(SerdeError::duplicate_field("curve"));
|
||||
}
|
||||
let value: u64 = map.next_value()?;
|
||||
let val = ECDSACurve::try_from(value).map_err(|_| {
|
||||
SerdeError::custom(format!("unsupported curve {}", value))
|
||||
})?;
|
||||
curve = Some(val);
|
||||
// curve = Some(map.next_value()?);
|
||||
}
|
||||
}
|
||||
-2 => {
|
||||
if x.is_some() {
|
||||
return Err(SerdeError::duplicate_field("x"));
|
||||
}
|
||||
let value: ByteBuf = map.next_value()?;
|
||||
x = Some(value.to_vec());
|
||||
}
|
||||
-3 => {
|
||||
if y.is_some() {
|
||||
return Err(SerdeError::duplicate_field("y"));
|
||||
}
|
||||
let value: ByteBuf = map.next_value()?;
|
||||
y = Some(value.to_vec());
|
||||
}
|
||||
3 => {
|
||||
if alg.is_some() {
|
||||
return Err(SerdeError::duplicate_field("alg"));
|
||||
}
|
||||
let value: i64 = map.next_value()?;
|
||||
let val = COSEAlgorithm::try_from(value).map_err(|_| {
|
||||
SerdeError::custom(format!("unsupported algorithm {}", value))
|
||||
})?;
|
||||
alg = Some(val);
|
||||
// alg = map.next_value()?;
|
||||
}
|
||||
_ => {
|
||||
// This unknown field should raise an error, but
|
||||
// there is a couple of field I(baloo) do not understand
|
||||
// yet. I(baloo) chose to ignore silently the
|
||||
// error instead because of that
|
||||
let value: Value = map.next_value()?;
|
||||
trace!("cose unknown value {:?}:{:?}", key, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let key_type = key_type.ok_or(SerdeError::missing_field("key_type"))?;
|
||||
let x = x.ok_or(SerdeError::missing_field("x"))?;
|
||||
let alg = alg.ok_or(SerdeError::missing_field("alg"))?;
|
||||
|
||||
let res = match key_type {
|
||||
COSEKeyTypeId::OKP => {
|
||||
let curve = curve.ok_or(SerdeError::missing_field("curve"))?;
|
||||
COSEKeyType::OKP(COSEOKPKey { curve, x })
|
||||
}
|
||||
COSEKeyTypeId::EC2 => {
|
||||
let curve = curve.ok_or(SerdeError::missing_field("curve"))?;
|
||||
let y = y.ok_or(SerdeError::missing_field("y"))?;
|
||||
COSEKeyType::EC2(COSEEC2Key { curve, x, y })
|
||||
}
|
||||
COSEKeyTypeId::RSA => {
|
||||
let e = y.ok_or(SerdeError::missing_field("y"))?;
|
||||
COSEKeyType::RSA(COSERSAKey { e, n: x })
|
||||
}
|
||||
COSEKeyTypeId::Symmetric => COSEKeyType::Symmetric(COSESymmetricKey { key: x }),
|
||||
};
|
||||
Ok(COSEKey { alg, key: res })
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(COSEKeyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for COSEKey {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let map_len = match &self.key {
|
||||
COSEKeyType::OKP(_) => 3,
|
||||
COSEKeyType::EC2(_) => 5,
|
||||
COSEKeyType::RSA(_) => 4,
|
||||
COSEKeyType::Symmetric(_) => 3,
|
||||
};
|
||||
let mut map = serializer.serialize_map(Some(map_len))?;
|
||||
match &self.key {
|
||||
COSEKeyType::OKP(key) => {
|
||||
map.serialize_entry(&1, &COSEKeyTypeId::OKP)?;
|
||||
map.serialize_entry(&3, &self.alg)?;
|
||||
map.serialize_entry(&-1, &key.curve)?;
|
||||
map.serialize_entry(&-2, &key.x)?;
|
||||
}
|
||||
COSEKeyType::EC2(key) => {
|
||||
map.serialize_entry(&1, &(COSEKeyTypeId::EC2 as u8))?;
|
||||
map.serialize_entry(&3, &self.alg)?;
|
||||
map.serialize_entry(&-1, &(key.curve as u8))?;
|
||||
map.serialize_entry(&-2, &serde_bytes::Bytes::new(&key.x))?;
|
||||
map.serialize_entry(&-3, &serde_bytes::Bytes::new(&key.y))?;
|
||||
}
|
||||
COSEKeyType::RSA(key) => {
|
||||
map.serialize_entry(&1, &COSEKeyTypeId::RSA)?;
|
||||
map.serialize_entry(&3, &self.alg)?;
|
||||
map.serialize_entry(&-1, &key.n)?;
|
||||
map.serialize_entry(&-2, &key.e)?;
|
||||
}
|
||||
COSEKeyType::Symmetric(key) => {
|
||||
map.serialize_entry(&1, &COSEKeyTypeId::Symmetric)?;
|
||||
map.serialize_entry(&3, &self.alg)?;
|
||||
map.serialize_entry(&-1, &key.key)?;
|
||||
}
|
||||
}
|
||||
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can be returned from COSE functions.
|
||||
#[derive(Debug)]
|
||||
pub enum CryptoError {
|
||||
// DecodingFailure,
|
||||
// LibraryFailure,
|
||||
MalformedInput,
|
||||
// MissingHeader,
|
||||
// UnexpectedHeaderValue,
|
||||
// UnexpectedTag,
|
||||
// UnexpectedType,
|
||||
// Unimplemented,
|
||||
// VerificationFailed,
|
||||
// SigningFailed,
|
||||
// InvalidArgument,
|
||||
UnknownKeyType,
|
||||
UnknownSignatureScheme,
|
||||
UnknownAlgorithm,
|
||||
WrongSaltLength,
|
||||
Backend(BackendError),
|
||||
}
|
||||
|
||||
impl From<BackendError> for CryptoError {
|
||||
fn from(e: BackendError) -> Self {
|
||||
CryptoError::Backend(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CryptoError> for CommandError {
|
||||
fn from(e: CryptoError) -> Self {
|
||||
CommandError::Crypto(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CryptoError> for AuthenticatorError {
|
||||
fn from(e: CryptoError) -> Self {
|
||||
AuthenticatorError::HIDError(HIDError::Command(CommandError::Crypto(e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ECDHSecret {
|
||||
remote: COSEKey,
|
||||
my: COSEKey,
|
||||
shared_secret: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ECDHSecret {
|
||||
pub fn my_public_key(&self) -> &COSEKey {
|
||||
&self.my
|
||||
}
|
||||
|
||||
pub fn shared_secret(&self) -> &[u8] {
|
||||
&self.shared_secret
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ECDHSecret {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ECDHSecret(remote: {:?}, my: {:?})",
|
||||
self.remote,
|
||||
self.my_public_key()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct U2FRegisterAnswer<'a> {
|
||||
pub certificate: &'a [u8],
|
||||
pub signature: &'a [u8],
|
||||
}
|
||||
|
||||
// We will only return MalformedInput here
|
||||
pub fn parse_u2f_der_certificate(data: &[u8]) -> Result<U2FRegisterAnswer, CryptoError> {
|
||||
// So we don't panic below, when accessing individual bytes
|
||||
if data.len() < 4 {
|
||||
return Err(CryptoError::MalformedInput);
|
||||
}
|
||||
// Check if it is a SEQUENCE
|
||||
if data[0] != 0x30 {
|
||||
return Err(CryptoError::MalformedInput);
|
||||
}
|
||||
|
||||
// This algorithm is taken from mozilla-central/security/nss/lib/mozpkix/lib/pkixder.cpp
|
||||
// The short form of length is a single byte with the high order bit set
|
||||
// to zero. The long form of length is one byte with the high order bit
|
||||
// set, followed by N bytes, where N is encoded in the lowest 7 bits of
|
||||
// the first byte.
|
||||
let end = if (data[1] & 0x80) == 0 {
|
||||
2 + data[1] as usize
|
||||
} else if data[1] == 0x81 {
|
||||
// The next byte specifies the length
|
||||
|
||||
if data[2] < 128 {
|
||||
// Not shortest possible encoding
|
||||
// Forbidden by DER-format
|
||||
return Err(CryptoError::MalformedInput);
|
||||
}
|
||||
3 + data[2] as usize
|
||||
} else if data[1] == 0x82 {
|
||||
// The next 2 bytes specify the length
|
||||
let l = u16::from_be_bytes([data[2], data[3]]);
|
||||
if l < 256 {
|
||||
// Not shortest possible encoding
|
||||
// Forbidden by DER-format
|
||||
return Err(CryptoError::MalformedInput);
|
||||
}
|
||||
4 + l as usize
|
||||
} else {
|
||||
// We don't support lengths larger than 2^16 - 1.
|
||||
return Err(CryptoError::MalformedInput);
|
||||
};
|
||||
|
||||
if data.len() < end {
|
||||
return Err(CryptoError::MalformedInput);
|
||||
}
|
||||
|
||||
Ok(U2FRegisterAnswer {
|
||||
certificate: &data[0..end],
|
||||
signature: &data[end..],
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(feature = "crypto_dummy")))]
|
||||
mod test {
|
||||
use super::{
|
||||
authenticate, decrypt, encrypt, imp::parse_key, imp::test_encapsulate, serialize_key,
|
||||
COSEAlgorithm, COSEKey, ECDSACurve,
|
||||
};
|
||||
use crate::crypto::{COSEEC2Key, COSEKeyType};
|
||||
use crate::ctap2::commands::client_pin::Pin;
|
||||
use crate::util::decode_hex;
|
||||
use serde_cbor::de::from_slice;
|
||||
|
||||
#[test]
|
||||
fn test_serialize_key() {
|
||||
let x = [
|
||||
0xfc, 0x9e, 0xd3, 0x6f, 0x7c, 0x1a, 0xa9, 0x15, 0xce, 0x3e, 0xa1, 0x77, 0xf0, 0x75,
|
||||
0x67, 0xf0, 0x7f, 0x16, 0xf9, 0x47, 0x9d, 0x95, 0xad, 0x8e, 0xd4, 0x97, 0x1d, 0x33,
|
||||
0x05, 0xe3, 0x1a, 0x80,
|
||||
];
|
||||
let y = [
|
||||
0x50, 0xb7, 0x33, 0xaf, 0x8c, 0x0b, 0x0e, 0xe1, 0xda, 0x8d, 0xe0, 0xac, 0xf9, 0xd8,
|
||||
0xe1, 0x32, 0x82, 0xf0, 0x63, 0xb7, 0xb3, 0x0d, 0x73, 0xd4, 0xd3, 0x2c, 0x9a, 0xad,
|
||||
0x6d, 0xfa, 0x8b, 0x27,
|
||||
];
|
||||
let serialized_key = [
|
||||
0x04, 0xfc, 0x9e, 0xd3, 0x6f, 0x7c, 0x1a, 0xa9, 0x15, 0xce, 0x3e, 0xa1, 0x77, 0xf0,
|
||||
0x75, 0x67, 0xf0, 0x7f, 0x16, 0xf9, 0x47, 0x9d, 0x95, 0xad, 0x8e, 0xd4, 0x97, 0x1d,
|
||||
0x33, 0x05, 0xe3, 0x1a, 0x80, 0x50, 0xb7, 0x33, 0xaf, 0x8c, 0x0b, 0x0e, 0xe1, 0xda,
|
||||
0x8d, 0xe0, 0xac, 0xf9, 0xd8, 0xe1, 0x32, 0x82, 0xf0, 0x63, 0xb7, 0xb3, 0x0d, 0x73,
|
||||
0xd4, 0xd3, 0x2c, 0x9a, 0xad, 0x6d, 0xfa, 0x8b, 0x27,
|
||||
];
|
||||
|
||||
let (res_x, res_y) =
|
||||
serialize_key(ECDSACurve::SECP256R1, &serialized_key).expect("Failed to serialize key");
|
||||
assert_eq!(res_x, x);
|
||||
assert_eq!(res_y, y);
|
||||
|
||||
let res_key = parse_key(ECDSACurve::SECP256R1, &x, &y).expect("Failed to parse key");
|
||||
|
||||
assert_eq!(res_key, serialized_key)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_es256_serialize_key() {
|
||||
// Test values taken from https://github.com/Yubico/python-fido2/blob/master/test/test_cose.py
|
||||
let key_data = decode_hex("A5010203262001215820A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1225820FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C");
|
||||
let key: COSEKey = from_slice(&key_data).unwrap();
|
||||
assert_eq!(key.alg, COSEAlgorithm::ES256);
|
||||
if let COSEKeyType::EC2(ec2key) = &key.key {
|
||||
assert_eq!(ec2key.curve, ECDSACurve::SECP256R1);
|
||||
assert_eq!(
|
||||
ec2key.x,
|
||||
decode_hex("A5FD5CE1B1C458C530A54FA61B31BF6B04BE8B97AFDE54DD8CBB69275A8A1BE1")
|
||||
);
|
||||
assert_eq!(
|
||||
ec2key.y,
|
||||
decode_hex("FA3A3231DD9DEED9D1897BE5A6228C59501E4BCD12975D3DFF730F01278EA61C")
|
||||
);
|
||||
} else {
|
||||
panic!("Wrong key type!");
|
||||
}
|
||||
|
||||
let serialized = serde_cbor::to_vec(&key).expect("Failed to serialize key");
|
||||
assert_eq!(key_data, serialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(non_snake_case)]
|
||||
fn test_shared_secret() {
|
||||
// Test values taken from https://github.com/Yubico/python-fido2/blob/master/test/test_ctap2.py
|
||||
let EC_PRIV =
|
||||
decode_hex("7452E599FEE739D8A653F6A507343D12D382249108A651402520B72F24FE7684");
|
||||
let EC_PUB_X =
|
||||
decode_hex("44D78D7989B97E62EA993496C9EF6E8FD58B8B00715F9A89153DDD9C4657E47F");
|
||||
let EC_PUB_Y =
|
||||
decode_hex("EC802EE7D22BD4E100F12E48537EB4E7E96ED3A47A0A3BD5F5EEAB65001664F9");
|
||||
let DEV_PUB_X =
|
||||
decode_hex("0501D5BC78DA9252560A26CB08FCC60CBE0B6D3B8E1D1FCEE514FAC0AF675168");
|
||||
let DEV_PUB_Y =
|
||||
decode_hex("D551B3ED46F665731F95B4532939C25D91DB7EB844BD96D4ABD4083785F8DF47");
|
||||
let SHARED = decode_hex("c42a039d548100dfba521e487debcbbb8b66bb7496f8b1862a7a395ed83e1a1c");
|
||||
let TOKEN_ENC = decode_hex("7A9F98E31B77BE90F9C64D12E9635040");
|
||||
let TOKEN = decode_hex("aff12c6dcfbf9df52f7a09211e8865cd");
|
||||
let PIN_HASH_ENC = decode_hex("afe8327ce416da8ee3d057589c2ce1a9");
|
||||
|
||||
// let peer_key = parse_key(ECDSACurve::SECP256R1, &DEV_PUB_X, &DEV_PUB_Y).unwrap();
|
||||
let my_pub_key_data = parse_key(ECDSACurve::SECP256R1, &EC_PUB_X, &EC_PUB_Y).unwrap();
|
||||
|
||||
let peer_key = COSEEC2Key {
|
||||
curve: ECDSACurve::SECP256R1,
|
||||
x: DEV_PUB_X,
|
||||
y: DEV_PUB_Y,
|
||||
};
|
||||
|
||||
// let my_pub_key = COSEKey {
|
||||
// alg: COSEAlgorithm::ES256,
|
||||
// key: COSEKeyType::EC2(COSEEC2Key {
|
||||
// curve: ECDSACurve::SECP256R1,
|
||||
// x: EC_PUB_X,
|
||||
// y: EC_PUB_Y,
|
||||
// }),
|
||||
// };
|
||||
// We are using `test_encapsulate()` here, because we need a way to hand in the private key
|
||||
// which would be generated on the fly otherwise (ephemeral keys), to predict the outputs
|
||||
let shared_secret =
|
||||
test_encapsulate(&peer_key, COSEAlgorithm::ES256, &my_pub_key_data, &EC_PRIV).unwrap();
|
||||
assert_eq!(shared_secret.shared_secret, SHARED);
|
||||
|
||||
let token_enc = encrypt(&shared_secret.shared_secret(), &TOKEN).unwrap();
|
||||
assert_eq!(token_enc, TOKEN_ENC);
|
||||
|
||||
let token = decrypt(&shared_secret.shared_secret(), &TOKEN_ENC).unwrap();
|
||||
assert_eq!(token, TOKEN);
|
||||
|
||||
let pin = Pin::new("1234");
|
||||
let pin_hash_enc =
|
||||
encrypt(&shared_secret.shared_secret(), pin.for_pin_token().as_ref()).unwrap();
|
||||
assert_eq!(pin_hash_enc, PIN_HASH_ENC);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authenticate() {
|
||||
let key = "key";
|
||||
let message = "The quick brown fox jumps over the lazy dog";
|
||||
let expected =
|
||||
decode_hex("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8");
|
||||
|
||||
let result =
|
||||
authenticate(key.as_bytes(), message.as_bytes()).expect("Failed to authenticate");
|
||||
assert_eq!(result, expected);
|
||||
|
||||
let key = "The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog";
|
||||
let message = "message";
|
||||
let expected =
|
||||
decode_hex("5597b93a2843078cbb0c920ae41dfe20f1685e10c67e423c11ab91adfc319d12");
|
||||
|
||||
let result =
|
||||
authenticate(key.as_bytes(), message.as_bytes()).expect("Failed to authenticate");
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pin_encryption_and_hashing() {
|
||||
let pin = "1234";
|
||||
|
||||
let shared_secret = vec![
|
||||
0x82, 0xE3, 0xD8, 0x41, 0xE2, 0x5C, 0x5C, 0x13, 0x46, 0x2C, 0x12, 0x3C, 0xC3, 0xD3,
|
||||
0x98, 0x78, 0x65, 0xBA, 0x3D, 0x20, 0x46, 0x74, 0xFB, 0xED, 0xD4, 0x7E, 0xF5, 0xAB,
|
||||
0xAB, 0x8D, 0x13, 0x72,
|
||||
];
|
||||
let expected_new_pin_enc = vec![
|
||||
0x70, 0x66, 0x4B, 0xB5, 0x81, 0xE2, 0x57, 0x45, 0x1A, 0x3A, 0xB9, 0x1B, 0xF1, 0xAA,
|
||||
0xD8, 0xE4, 0x5F, 0x6C, 0xE9, 0xB5, 0xC3, 0xB0, 0xF3, 0x2B, 0x5E, 0xCD, 0x62, 0xD0,
|
||||
0xBA, 0x3B, 0x60, 0x5F, 0xD9, 0x18, 0x31, 0x66, 0xF6, 0xC5, 0xFA, 0xF3, 0xE4, 0xDA,
|
||||
0x24, 0x81, 0x50, 0x2C, 0xD0, 0xCE, 0xE0, 0x15, 0x8B, 0x35, 0x1F, 0xC3, 0x92, 0x08,
|
||||
0xA7, 0x7C, 0xB2, 0x74, 0x4B, 0xD4, 0x3C, 0xF9,
|
||||
];
|
||||
let expected_pin_auth = vec![
|
||||
0x8E, 0x7F, 0x01, 0x69, 0x97, 0xF3, 0xB0, 0xA2, 0x7B, 0xA4, 0x34, 0x7A, 0x0E, 0x49,
|
||||
0xFD, 0xF5,
|
||||
];
|
||||
|
||||
// Padding to 64 bytes
|
||||
let input: Vec<u8> = pin
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.chain(std::iter::repeat(&0x00))
|
||||
.take(64)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let new_pin_enc = encrypt(&shared_secret, &input).expect("Failed to encrypt pin");
|
||||
assert_eq!(new_pin_enc, expected_new_pin_enc);
|
||||
|
||||
let pin_auth = authenticate(&shared_secret, &new_pin_enc).expect("Failed to authenticate");
|
||||
assert_eq!(pin_auth[0..16], expected_pin_auth);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,543 @@
|
|||
use super::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, ECDHSecret, ECDSACurve};
|
||||
use nss_gk_api::p11::{
|
||||
PK11Origin, PK11_CreateContextBySymKey, PK11_Decrypt, PK11_DigestFinal, PK11_DigestOp,
|
||||
PK11_Encrypt, PK11_GenerateKeyPairWithOpFlags, PK11_HashBuf, PK11_ImportSymKey,
|
||||
PK11_PubDeriveWithKDF, PrivateKey, PublicKey, SECKEY_DecodeDERSubjectPublicKeyInfo,
|
||||
SECKEY_ExtractPublicKey, SECOidTag, Slot, SubjectPublicKeyInfo, AES_BLOCK_SIZE,
|
||||
PK11_ATTR_SESSION, SHA256_LENGTH,
|
||||
};
|
||||
use nss_gk_api::{Error as NSSError, IntoResult, SECItem, SECItemBorrowed, PR_FALSE};
|
||||
use pkcs11_bindings::{
|
||||
CKA_DERIVE, CKA_ENCRYPT, CKA_SIGN, CKD_NULL, CKF_DERIVE, CKM_AES_CBC, CKM_ECDH1_DERIVE,
|
||||
CKM_EC_KEY_PAIR_GEN, CKM_SHA256_HMAC, CKM_SHA512_HMAC,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_bytes::ByteBuf;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::num::TryFromIntError;
|
||||
use std::os::raw::c_uint;
|
||||
use std::ptr;
|
||||
|
||||
#[cfg(test)]
|
||||
use nss_gk_api::p11::{PK11_ImportDERPrivateKeyInfoAndReturnKey, SECKEY_ConvertToPublicKey};
|
||||
|
||||
/// Errors that can be returned from COSE functions.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub enum BackendError {
|
||||
NSSError(String),
|
||||
TryFromError,
|
||||
UnsupportedAlgorithm(COSEAlgorithm),
|
||||
UnsupportedCurve(ECDSACurve),
|
||||
UnsupportedKeyType,
|
||||
}
|
||||
|
||||
impl From<NSSError> for BackendError {
|
||||
fn from(e: NSSError) -> Self {
|
||||
BackendError::NSSError(format!("{}", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TryFromIntError> for BackendError {
|
||||
fn from(_: TryFromIntError) -> Self {
|
||||
BackendError::TryFromError
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, BackendError>;
|
||||
|
||||
// Object identifiers in DER tag-length-value form
|
||||
|
||||
const DER_OID_EC_PUBLIC_KEY_BYTES: &[u8] = &[
|
||||
0x06, 0x07,
|
||||
/* {iso(1) member-body(2) us(840) ansi-x962(10045) keyType(2) ecPublicKey(1)} */
|
||||
0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
|
||||
];
|
||||
|
||||
const DER_OID_P256_BYTES: &[u8] = &[
|
||||
0x06, 0x08,
|
||||
/* {iso(1) member-body(2) us(840) ansi-x962(10045) curves(3) prime(1) prime256v1(7)} */
|
||||
0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07,
|
||||
];
|
||||
const DER_OID_P384_BYTES: &[u8] = &[
|
||||
0x06, 0x05,
|
||||
/* {iso(1) identified-organization(3) certicom(132) curve(0) ansip384r1(34)} */
|
||||
0x2b, 0x81, 0x04, 0x00, 0x22,
|
||||
];
|
||||
const DER_OID_P521_BYTES: &[u8] = &[
|
||||
0x06, 0x05,
|
||||
/* {iso(1) identified-organization(3) certicom(132) curve(0) ansip521r1(35)} */
|
||||
0x2b, 0x81, 0x04, 0x00, 0x23,
|
||||
];
|
||||
|
||||
/* From CTAP2.1 spec:
|
||||
|
||||
initialize()
|
||||
|
||||
This is run by the platform when starting a series of transactions with a specific authenticator.
|
||||
encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error
|
||||
|
||||
Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret.
|
||||
encrypt(key, demPlaintext) → ciphertext
|
||||
|
||||
Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
|
||||
decrypt(key, ciphertext) → plaintext | error
|
||||
|
||||
Decrypts a ciphertext and returns the plaintext.
|
||||
authenticate(key, message) → signature
|
||||
|
||||
Computes a MAC of the given message.
|
||||
*/
|
||||
|
||||
// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface
|
||||
pub(crate) fn serialize_key(_curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
|
||||
// TODO(MS): I actually have NO idea how to do this with NSS
|
||||
let length = key[1..].len() / 2;
|
||||
let chunks: Vec<_> = key[1..].chunks_exact(length).collect();
|
||||
Ok((
|
||||
ByteBuf::from(chunks[0].to_vec()),
|
||||
ByteBuf::from(chunks[1].to_vec()),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_key(_curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> {
|
||||
if x.len() != y.len() {
|
||||
return Err(BackendError::NSSError(
|
||||
"EC coordinates not equally long".to_string(),
|
||||
));
|
||||
}
|
||||
let mut buf = Vec::with_capacity(2 * x.len() + 1);
|
||||
// The uncompressed point format is defined in Section 2.3.3 of "SEC 1: Elliptic Curve
|
||||
// Cryptography" https://www.secg.org/sec1-v2.pdf.
|
||||
buf.push(0x04);
|
||||
buf.extend_from_slice(x);
|
||||
buf.extend_from_slice(y);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn der_spki_from_cose(cose_key: &COSEKey) -> Result<Vec<u8>> {
|
||||
let ec2key = match cose_key.key {
|
||||
COSEKeyType::EC2(ref ec2key) => ec2key,
|
||||
_ => return Err(BackendError::UnsupportedKeyType),
|
||||
};
|
||||
|
||||
let (curve_oid, seq_len, alg_len, spk_len) = match ec2key.curve {
|
||||
ECDSACurve::SECP256R1 => (
|
||||
DER_OID_P256_BYTES,
|
||||
[0x59].as_slice(),
|
||||
[0x13].as_slice(),
|
||||
[0x42].as_slice(),
|
||||
),
|
||||
ECDSACurve::SECP384R1 => (
|
||||
DER_OID_P384_BYTES,
|
||||
[0x76].as_slice(),
|
||||
[0x10].as_slice(),
|
||||
[0x62].as_slice(),
|
||||
),
|
||||
ECDSACurve::SECP521R1 => (
|
||||
DER_OID_P521_BYTES,
|
||||
[0x81, 0x9b].as_slice(),
|
||||
[0x10].as_slice(),
|
||||
[0x8a, 0xdf].as_slice(),
|
||||
),
|
||||
x => return Err(BackendError::UnsupportedCurve(x)),
|
||||
};
|
||||
|
||||
let cose_key_sec1 = parse_key(ec2key.curve, &ec2key.x, &ec2key.y)?;
|
||||
|
||||
// [RFC 5280]
|
||||
let mut spki: Vec<u8> = vec![];
|
||||
// SubjectPublicKeyInfo
|
||||
spki.push(0x30);
|
||||
spki.extend_from_slice(seq_len);
|
||||
// AlgorithmIdentifier
|
||||
spki.push(0x30);
|
||||
spki.extend_from_slice(alg_len);
|
||||
// ObjectIdentifier
|
||||
spki.extend_from_slice(DER_OID_EC_PUBLIC_KEY_BYTES);
|
||||
// RFC 5480 ECParameters
|
||||
spki.extend_from_slice(curve_oid);
|
||||
// BIT STRING encoding uncompressed SEC1 public point
|
||||
spki.push(0x03);
|
||||
spki.extend_from_slice(spk_len);
|
||||
spki.push(0x0); // no trailing zeros
|
||||
spki.extend_from_slice(&cose_key_sec1);
|
||||
|
||||
Ok(spki)
|
||||
}
|
||||
|
||||
/// This is run by the platform when starting a series of transactions with a specific authenticator.
|
||||
//pub(crate) fn initialize() { }
|
||||
|
||||
/// Generates an encapsulation for the authenticator's public key and returns the message
|
||||
/// to transmit and the shared secret.
|
||||
///
|
||||
/// `peer_cose_key` is the authenticator's (peer's) public key.
|
||||
pub(crate) fn encapsulate(peer_cose_key: &COSEKey) -> Result<ECDHSecret> {
|
||||
nss_gk_api::init();
|
||||
// Generate an ephmeral keypair to do ECDH with the authenticator.
|
||||
// This is "platformKeyAgreementKey".
|
||||
let ec2key = match peer_cose_key.key {
|
||||
COSEKeyType::EC2(ref ec2key) => ec2key,
|
||||
_ => return Err(BackendError::UnsupportedKeyType),
|
||||
};
|
||||
|
||||
let mut oid = match ec2key.curve {
|
||||
ECDSACurve::SECP256R1 => SECItemBorrowed::wrap(DER_OID_P256_BYTES),
|
||||
ECDSACurve::SECP384R1 => SECItemBorrowed::wrap(DER_OID_P384_BYTES),
|
||||
ECDSACurve::SECP521R1 => SECItemBorrowed::wrap(DER_OID_P521_BYTES),
|
||||
x => return Err(BackendError::UnsupportedCurve(x)),
|
||||
};
|
||||
let oid_ptr: *mut SECItem = oid.as_mut();
|
||||
|
||||
let slot = Slot::internal()?;
|
||||
|
||||
let mut client_public_ptr = ptr::null_mut();
|
||||
|
||||
// We have to be careful with error handling between the `PK11_GenerateKeyPairWithOpFlags` and
|
||||
// `PublicKey::from_ptr` calls here, so I've wrapped them in the same unsafe block as a
|
||||
// warning. TODO(jms) Replace this once there is a safer alternative.
|
||||
// https://github.com/mozilla/nss-gk-api/issues/1
|
||||
let (client_private, client_public) = unsafe {
|
||||
let client_private =
|
||||
// Type of `param` argument depends on mechanism. For EC keygen it is
|
||||
// `SECKEYECParams *` which is a typedef for `SECItem *`.
|
||||
PK11_GenerateKeyPairWithOpFlags(
|
||||
*slot,
|
||||
CKM_EC_KEY_PAIR_GEN,
|
||||
oid_ptr.cast(),
|
||||
&mut client_public_ptr,
|
||||
PK11_ATTR_SESSION,
|
||||
CKF_DERIVE,
|
||||
CKF_DERIVE,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
.into_result()?;
|
||||
|
||||
let client_public = PublicKey::from_ptr(client_public_ptr)?;
|
||||
|
||||
(client_private, client_public)
|
||||
};
|
||||
|
||||
let peer_spki = der_spki_from_cose(peer_cose_key)?;
|
||||
let peer_public = nss_public_key_from_der_spki(&peer_spki)?;
|
||||
let shared_secret = encapsulate_helper(peer_public, client_private)?;
|
||||
|
||||
let client_cose_key = cose_key_from_nss_public(peer_cose_key.alg, ec2key.curve, client_public)?;
|
||||
|
||||
Ok(ECDHSecret {
|
||||
remote: COSEKey {
|
||||
alg: peer_cose_key.alg,
|
||||
key: COSEKeyType::EC2(ec2key.clone()),
|
||||
},
|
||||
my: client_cose_key,
|
||||
shared_secret,
|
||||
})
|
||||
}
|
||||
|
||||
fn nss_public_key_from_der_spki(spki: &[u8]) -> Result<PublicKey> {
|
||||
let mut spki_item = SECItemBorrowed::wrap(&spki);
|
||||
let spki_item_ptr: *mut SECItem = spki_item.as_mut();
|
||||
let nss_spki = unsafe {
|
||||
SubjectPublicKeyInfo::from_ptr(SECKEY_DecodeDERSubjectPublicKeyInfo(spki_item_ptr))?
|
||||
};
|
||||
let public_key = unsafe { PublicKey::from_ptr(SECKEY_ExtractPublicKey(*nss_spki))? };
|
||||
Ok(public_key)
|
||||
}
|
||||
|
||||
fn cose_key_from_nss_public(
|
||||
alg: COSEAlgorithm,
|
||||
curve: ECDSACurve,
|
||||
nss_public: PublicKey,
|
||||
) -> Result<COSEKey> {
|
||||
let public_data = nss_public.key_data()?;
|
||||
let (public_x, public_y) = serialize_key(curve, &public_data)?;
|
||||
Ok(COSEKey {
|
||||
alg,
|
||||
key: COSEKeyType::EC2(COSEEC2Key {
|
||||
curve,
|
||||
x: public_x.to_vec(),
|
||||
y: public_y.to_vec(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// `peer_public`: The authenticator's public key.
|
||||
/// `client_private`: Our ephemeral private key.
|
||||
fn encapsulate_helper(peer_public: PublicKey, client_private: PrivateKey) -> Result<Vec<u8>> {
|
||||
let ecdh_x_coord = unsafe {
|
||||
PK11_PubDeriveWithKDF(
|
||||
*client_private,
|
||||
*peer_public,
|
||||
PR_FALSE,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
CKM_ECDH1_DERIVE,
|
||||
CKM_SHA512_HMAC, // unused
|
||||
CKA_DERIVE, // unused
|
||||
0,
|
||||
CKD_NULL,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.into_result()?
|
||||
};
|
||||
let ecdh_x_coord_bytes = ecdh_x_coord.as_bytes()?;
|
||||
let mut shared_secret = [0u8; SHA256_LENGTH];
|
||||
unsafe {
|
||||
PK11_HashBuf(
|
||||
SECOidTag::SEC_OID_SHA256,
|
||||
shared_secret.as_mut_ptr(),
|
||||
ecdh_x_coord_bytes.as_ptr(),
|
||||
ecdh_x_coord_bytes.len() as i32,
|
||||
)
|
||||
};
|
||||
Ok(shared_secret.to_vec())
|
||||
}
|
||||
|
||||
/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext.
|
||||
/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
|
||||
pub(crate) fn encrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
|
||||
nss_gk_api::init();
|
||||
|
||||
if key.len() != 32 {
|
||||
return Err(BackendError::NSSError(
|
||||
"Invalid AES-256 key length".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// The input must be a multiple of the AES block size, 16
|
||||
if data.len() % AES_BLOCK_SIZE != 0 {
|
||||
return Err(BackendError::NSSError(
|
||||
"Input to encrypt is too long".to_string(),
|
||||
));
|
||||
}
|
||||
let in_len = c_uint::try_from(data.len())?;
|
||||
|
||||
let slot = Slot::internal()?;
|
||||
|
||||
let sym_key = unsafe {
|
||||
PK11_ImportSymKey(
|
||||
*slot,
|
||||
CKM_AES_CBC,
|
||||
PK11Origin::PK11_OriginUnwrap,
|
||||
CKA_ENCRYPT,
|
||||
SECItemBorrowed::wrap(key).as_mut(),
|
||||
ptr::null_mut(),
|
||||
)
|
||||
.into_result()?
|
||||
};
|
||||
|
||||
let iv = [0u8; AES_BLOCK_SIZE];
|
||||
let mut params = SECItemBorrowed::wrap(&iv);
|
||||
let params_ptr: *mut SECItem = params.as_mut();
|
||||
let mut out_len: c_uint = 0;
|
||||
let mut out = vec![0; in_len as usize];
|
||||
unsafe {
|
||||
PK11_Encrypt(
|
||||
*sym_key,
|
||||
CKM_AES_CBC,
|
||||
params_ptr,
|
||||
out.as_mut_ptr(),
|
||||
&mut out_len,
|
||||
in_len,
|
||||
data.as_ptr(),
|
||||
in_len,
|
||||
)
|
||||
.into_result()?
|
||||
}
|
||||
// CKM_AES_CBC should have output length equal to input length.
|
||||
debug_assert_eq!(out_len, in_len);
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Decrypts a ciphertext and returns the plaintext.
|
||||
pub(crate) fn decrypt(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
|
||||
nss_gk_api::init();
|
||||
let slot = Slot::internal()?;
|
||||
|
||||
if key.len() != 32 {
|
||||
return Err(BackendError::NSSError(
|
||||
"Invalid AES-256 key length".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// The input must be a multiple of the AES block size, 16
|
||||
if data.len() % AES_BLOCK_SIZE != 0 {
|
||||
return Err(BackendError::NSSError(
|
||||
"Invalid input to decrypt".to_string(),
|
||||
));
|
||||
}
|
||||
let in_len = c_uint::try_from(data.len())?;
|
||||
|
||||
let sym_key = unsafe {
|
||||
PK11_ImportSymKey(
|
||||
*slot,
|
||||
CKM_AES_CBC,
|
||||
PK11Origin::PK11_OriginUnwrap,
|
||||
CKA_ENCRYPT,
|
||||
SECItemBorrowed::wrap(key).as_mut(),
|
||||
ptr::null_mut(),
|
||||
)
|
||||
.into_result()?
|
||||
};
|
||||
|
||||
let iv = [0u8; AES_BLOCK_SIZE];
|
||||
let mut params = SECItemBorrowed::wrap(&iv);
|
||||
let params_ptr: *mut SECItem = params.as_mut();
|
||||
let mut out_len: c_uint = 0;
|
||||
let mut out = vec![0; in_len as usize];
|
||||
unsafe {
|
||||
PK11_Decrypt(
|
||||
*sym_key,
|
||||
CKM_AES_CBC,
|
||||
params_ptr,
|
||||
out.as_mut_ptr(),
|
||||
&mut out_len,
|
||||
in_len,
|
||||
data.as_ptr(),
|
||||
in_len,
|
||||
)
|
||||
.into_result()?
|
||||
}
|
||||
// CKM_AES_CBC should have output length equal to input length.
|
||||
debug_assert_eq!(out_len, in_len);
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Computes a MAC of the given message.
|
||||
pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> {
|
||||
nss_gk_api::init();
|
||||
let slot = Slot::internal()?;
|
||||
let sym_key = unsafe {
|
||||
PK11_ImportSymKey(
|
||||
*slot,
|
||||
CKM_SHA256_HMAC,
|
||||
PK11Origin::PK11_OriginUnwrap,
|
||||
CKA_SIGN,
|
||||
SECItemBorrowed::wrap(token).as_mut(),
|
||||
ptr::null_mut(),
|
||||
)
|
||||
.into_result()?
|
||||
};
|
||||
let param = SECItemBorrowed::make_empty();
|
||||
let context = unsafe {
|
||||
PK11_CreateContextBySymKey(CKM_SHA256_HMAC, CKA_SIGN, *sym_key, param.as_ref())
|
||||
.into_result()?
|
||||
};
|
||||
unsafe { PK11_DigestOp(*context, input.as_ptr(), input.len().try_into()?).into_result()? };
|
||||
let mut digest = vec![0u8; 32];
|
||||
let mut digest_len = 0u32;
|
||||
unsafe {
|
||||
PK11_DigestFinal(
|
||||
*context,
|
||||
digest.as_mut_ptr(),
|
||||
&mut digest_len,
|
||||
digest.len() as u32,
|
||||
)
|
||||
.into_result()?
|
||||
}
|
||||
assert_eq!(digest_len, 32);
|
||||
Ok(digest)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_encapsulate(
|
||||
peer_coseec2_key: &COSEEC2Key,
|
||||
alg: COSEAlgorithm,
|
||||
my_pub_key: &[u8],
|
||||
my_priv_key: &[u8],
|
||||
) -> Result<ECDHSecret> {
|
||||
nss_gk_api::init();
|
||||
|
||||
let peer_cose_key = COSEKey {
|
||||
alg: alg,
|
||||
key: COSEKeyType::EC2(peer_coseec2_key.clone()),
|
||||
};
|
||||
let spki = der_spki_from_cose(&peer_cose_key)?;
|
||||
let peer_public = nss_public_key_from_der_spki(&spki)?;
|
||||
|
||||
/* NSS has no mechanism to import a raw elliptic curve coordinate as a private key.
|
||||
* We need to encode it in a key storage format such as PKCS#8. To avoid a dependency
|
||||
* on an ASN.1 encoder for this test, we'll do it manually. */
|
||||
let pkcs8_private_key_info_version = &[0x02, 0x01, 0x00];
|
||||
let rfc5915_ec_private_key_version = &[0x02, 0x01, 0x01];
|
||||
|
||||
let (curve_oid, seq_len, alg_len, attr_len, ecpriv_len, param_len, spk_len) =
|
||||
match peer_coseec2_key.curve {
|
||||
ECDSACurve::SECP256R1 => (
|
||||
DER_OID_P256_BYTES,
|
||||
[0x81, 0x87].as_slice(),
|
||||
[0x13].as_slice(),
|
||||
[0x6d].as_slice(),
|
||||
[0x6b].as_slice(),
|
||||
[0x44].as_slice(),
|
||||
[0x42].as_slice(),
|
||||
),
|
||||
x => return Err(BackendError::UnsupportedCurve(x)),
|
||||
};
|
||||
|
||||
let priv_len = my_priv_key.len() as u8; // < 127
|
||||
|
||||
let mut pkcs8_priv: Vec<u8> = vec![];
|
||||
// RFC 5208 PrivateKeyInfo
|
||||
pkcs8_priv.push(0x30);
|
||||
pkcs8_priv.extend_from_slice(seq_len);
|
||||
// Integer (0)
|
||||
pkcs8_priv.extend_from_slice(pkcs8_private_key_info_version);
|
||||
// AlgorithmIdentifier
|
||||
pkcs8_priv.push(0x30);
|
||||
pkcs8_priv.extend_from_slice(alg_len);
|
||||
// ObjectIdentifier
|
||||
pkcs8_priv.extend_from_slice(DER_OID_EC_PUBLIC_KEY_BYTES);
|
||||
// RFC 5480 ECParameters
|
||||
pkcs8_priv.extend_from_slice(DER_OID_P256_BYTES);
|
||||
// Attributes
|
||||
pkcs8_priv.push(0x04);
|
||||
pkcs8_priv.extend_from_slice(attr_len);
|
||||
// RFC 5915 ECPrivateKey
|
||||
pkcs8_priv.push(0x30);
|
||||
pkcs8_priv.extend_from_slice(ecpriv_len);
|
||||
pkcs8_priv.extend_from_slice(rfc5915_ec_private_key_version);
|
||||
pkcs8_priv.push(0x04);
|
||||
pkcs8_priv.push(priv_len);
|
||||
pkcs8_priv.extend_from_slice(my_priv_key);
|
||||
pkcs8_priv.push(0xa1);
|
||||
pkcs8_priv.extend_from_slice(param_len);
|
||||
pkcs8_priv.push(0x03);
|
||||
pkcs8_priv.extend_from_slice(spk_len);
|
||||
pkcs8_priv.push(0x0);
|
||||
pkcs8_priv.extend_from_slice(&my_pub_key);
|
||||
|
||||
// Now we can import the private key.
|
||||
let slot = Slot::internal()?;
|
||||
let mut pkcs8_priv_item = SECItemBorrowed::wrap(&pkcs8_priv);
|
||||
let pkcs8_priv_item_ptr: *mut SECItem = pkcs8_priv_item.as_mut();
|
||||
let mut client_private_ptr = ptr::null_mut();
|
||||
unsafe {
|
||||
PK11_ImportDERPrivateKeyInfoAndReturnKey(
|
||||
*slot,
|
||||
pkcs8_priv_item_ptr,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
PR_FALSE,
|
||||
PR_FALSE,
|
||||
255, /* todo: expose KU_ flags in nss-gk-api */
|
||||
&mut client_private_ptr,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
let client_private = unsafe { PrivateKey::from_ptr(client_private_ptr) }?;
|
||||
let client_public = unsafe { PublicKey::from_ptr(SECKEY_ConvertToPublicKey(*client_private))? };
|
||||
let client_cose_key = cose_key_from_nss_public(alg, peer_coseec2_key.curve, client_public)?;
|
||||
|
||||
let shared_secret = encapsulate_helper(peer_public, client_private)?;
|
||||
|
||||
Ok(ECDHSecret {
|
||||
remote: peer_cose_key,
|
||||
my: client_cose_key,
|
||||
shared_secret,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
use super::{
|
||||
/*Signature,*/ COSEAlgorithm, COSEEC2Key, /*PlainText*/ COSEKey, COSEKeyType,
|
||||
/*CypherText,*/ ECDHSecret, ECDSACurve,
|
||||
};
|
||||
use openssl::bn::{BigNum, BigNumContext};
|
||||
use openssl::derive::Deriver;
|
||||
#[cfg(test)]
|
||||
use openssl::ec::PointConversionForm;
|
||||
use openssl::ec::{EcGroup, EcKey, EcPoint};
|
||||
use openssl::error::ErrorStack;
|
||||
use openssl::hash::{hash, MessageDigest};
|
||||
use openssl::nid::Nid;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::sign::{Signer, Verifier};
|
||||
use openssl::symm::{Cipher, Crypter, Mode};
|
||||
use openssl::x509::X509;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_bytes::ByteBuf;
|
||||
|
||||
fn openssl_string<S>(_: &ErrorStack, s: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_str("OpenSSLError")
|
||||
}
|
||||
|
||||
/// Errors that can be returned from COSE functions.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub enum BackendError {
|
||||
#[serde(serialize_with = "openssl_string")]
|
||||
OpenSSL(ErrorStack),
|
||||
UnsupportedCurve(ECDSACurve),
|
||||
UnsupportedKeyType,
|
||||
}
|
||||
|
||||
impl From<ErrorStack> for BackendError {
|
||||
fn from(e: ErrorStack) -> Self {
|
||||
BackendError::OpenSSL(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ErrorStack> for BackendError {
|
||||
fn from(e: &ErrorStack) -> Self {
|
||||
BackendError::OpenSSL(e.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, BackendError>;
|
||||
|
||||
/* From CTAP2.1 spec:
|
||||
|
||||
initialize()
|
||||
|
||||
This is run by the platform when starting a series of transactions with a specific authenticator.
|
||||
encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error
|
||||
|
||||
Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret.
|
||||
encrypt(key, demPlaintext) → ciphertext
|
||||
|
||||
Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
|
||||
decrypt(key, ciphertext) → plaintext | error
|
||||
|
||||
Decrypts a ciphertext and returns the plaintext.
|
||||
authenticate(key, message) → signature
|
||||
|
||||
Computes a MAC of the given message.
|
||||
*/
|
||||
|
||||
fn to_openssl_name(curve: ECDSACurve) -> Result<Nid> {
|
||||
match curve {
|
||||
ECDSACurve::SECP256R1 => Ok(Nid::X9_62_PRIME256V1),
|
||||
ECDSACurve::SECP384R1 => Ok(Nid::SECP384R1),
|
||||
ECDSACurve::SECP521R1 => Ok(Nid::SECP521R1),
|
||||
x => Err(BackendError::UnsupportedCurve(x)),
|
||||
}
|
||||
}
|
||||
|
||||
fn affine_coordinates(curve: ECDSACurve, bytes: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
|
||||
let name = to_openssl_name(curve)?;
|
||||
let group = EcGroup::from_curve_name(name)?;
|
||||
|
||||
let mut ctx = BigNumContext::new()?;
|
||||
let point = EcPoint::from_bytes(&group, bytes, &mut ctx)?;
|
||||
|
||||
let mut x = BigNum::new()?;
|
||||
let mut y = BigNum::new()?;
|
||||
|
||||
point.affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?;
|
||||
//point.affine_coordinates_gf2m(&group, &mut x, &mut y, &mut ctx)?;
|
||||
|
||||
Ok((ByteBuf::from(x.to_vec()), ByteBuf::from(y.to_vec())))
|
||||
}
|
||||
|
||||
// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface
|
||||
pub(crate) fn serialize_key(curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
|
||||
affine_coordinates(curve, key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn parse_key(curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> {
|
||||
let name = to_openssl_name(curve)?;
|
||||
let group = EcGroup::from_curve_name(name)?;
|
||||
|
||||
let mut ctx = BigNumContext::new()?;
|
||||
let x = BigNum::from_slice(x)?;
|
||||
let y = BigNum::from_slice(y)?;
|
||||
|
||||
let key = EcKey::from_public_key_affine_coordinates(&group, &x, &y)?;
|
||||
// TODO(baloo): what is uncompressed?!
|
||||
let pub_key = key.public_key();
|
||||
|
||||
Ok(pub_key.to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx)?)
|
||||
}
|
||||
|
||||
/// This is run by the platform when starting a series of transactions with a specific authenticator.
|
||||
//pub(crate) fn initialize() {
|
||||
//
|
||||
//}
|
||||
|
||||
/// Generates an encapsulation for the authenticator’s public key and returns the message
|
||||
/// to transmit and the shared secret.
|
||||
pub(crate) fn encapsulate(key: &COSEKey) -> Result<ECDHSecret> {
|
||||
if let COSEKeyType::EC2(ec2key) = &key.key {
|
||||
let curve_name = to_openssl_name(ec2key.curve)?;
|
||||
let group = EcGroup::from_curve_name(curve_name)?;
|
||||
let my_key = EcKey::generate(&group)?;
|
||||
|
||||
encapsulate_helper(&ec2key, key.alg, group, my_key)
|
||||
} else {
|
||||
Err(BackendError::UnsupportedKeyType)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encapsulate_helper(
|
||||
key: &COSEEC2Key,
|
||||
alg: COSEAlgorithm,
|
||||
group: EcGroup,
|
||||
my_key: EcKey<Private>,
|
||||
) -> Result<ECDHSecret> {
|
||||
let mut ctx = BigNumContext::new()?;
|
||||
let mut x = BigNum::new()?;
|
||||
let mut y = BigNum::new()?;
|
||||
|
||||
my_key
|
||||
.public_key()
|
||||
.affine_coordinates_gfp(&group, &mut x, &mut y, &mut ctx)?;
|
||||
|
||||
let my_public_key = COSEKey {
|
||||
alg,
|
||||
key: COSEKeyType::EC2(COSEEC2Key {
|
||||
curve: key.curve.clone(),
|
||||
x: x.to_vec(),
|
||||
y: y.to_vec(),
|
||||
}),
|
||||
};
|
||||
|
||||
// let point = EcPoint::from_bytes(&group, &key.key, &mut ctx)?;
|
||||
let peer_public_key = PKey::from_ec_key(EcKey::from_public_key_affine_coordinates(
|
||||
&group,
|
||||
BigNum::from_slice(&key.x).as_ref()?,
|
||||
BigNum::from_slice(&key.y).as_ref()?,
|
||||
)?)?;
|
||||
|
||||
let my_ec_key = PKey::from_ec_key(my_key)?;
|
||||
let mut deriver = Deriver::new(my_ec_key.as_ref())?;
|
||||
deriver.set_peer(&peer_public_key)?;
|
||||
let shared_sec = deriver.derive_to_vec()?;
|
||||
|
||||
// Hashing the key material
|
||||
let digest = hash(MessageDigest::sha256(), &shared_sec)?;
|
||||
|
||||
Ok(ECDHSecret {
|
||||
remote: COSEKey {
|
||||
alg,
|
||||
key: COSEKeyType::EC2(key.clone()),
|
||||
},
|
||||
my: my_public_key,
|
||||
shared_secret: digest.as_ref().to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_encapsulate(
|
||||
key: &COSEEC2Key,
|
||||
alg: COSEAlgorithm,
|
||||
my_pub_key: &[u8],
|
||||
my_priv_key: &[u8],
|
||||
) -> Result<ECDHSecret> {
|
||||
let curve_name = to_openssl_name(key.curve)?;
|
||||
let group = EcGroup::from_curve_name(curve_name)?;
|
||||
|
||||
let mut ctx = BigNumContext::new()?;
|
||||
let my_pub_point = EcPoint::from_bytes(&group, &my_pub_key, &mut ctx)?;
|
||||
let my_priv_bignum = BigNum::from_slice(my_priv_key)?;
|
||||
let my_key = EcKey::from_private_components(&group, &my_priv_bignum, &my_pub_point)?;
|
||||
|
||||
encapsulate_helper(key, alg, group, my_key)
|
||||
}
|
||||
|
||||
/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext.
|
||||
/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
|
||||
pub(crate) fn encrypt(
|
||||
key: &[u8],
|
||||
plain_text: &[u8], /*PlainText*/
|
||||
) -> Result<Vec<u8> /*CypherText*/> {
|
||||
let cipher = Cipher::aes_256_cbc();
|
||||
|
||||
// TODO(baloo): This might trigger a panic if size is not big enough
|
||||
let mut cypher_text = vec![0; plain_text.len() * 2];
|
||||
cypher_text.resize(plain_text.len() * 2, 0);
|
||||
// Spec says explicitly IV=0
|
||||
let iv = [0u8; 16];
|
||||
let mut encrypter = Crypter::new(cipher, Mode::Encrypt, key, Some(&iv))?;
|
||||
encrypter.pad(false);
|
||||
let mut out_size = 0;
|
||||
out_size += encrypter.update(plain_text, cypher_text.as_mut_slice())?;
|
||||
out_size += encrypter.finalize(cypher_text.as_mut_slice())?;
|
||||
cypher_text.truncate(out_size);
|
||||
Ok(cypher_text)
|
||||
}
|
||||
|
||||
/// Decrypts a ciphertext and returns the plaintext.
|
||||
pub(crate) fn decrypt(
|
||||
key: &[u8],
|
||||
cypher_text: &[u8], /*CypherText*/
|
||||
) -> Result<Vec<u8> /*PlainText*/> {
|
||||
let cipher = Cipher::aes_256_cbc();
|
||||
|
||||
// TODO(baloo): This might trigger a panic if size is not big enough
|
||||
let mut plain_text = vec![0; cypher_text.len() * 2];
|
||||
plain_text.resize(cypher_text.len() * 2, 0);
|
||||
// Spec says explicitly IV=0
|
||||
let iv = [0u8; 16];
|
||||
let mut encrypter = Crypter::new(cipher, Mode::Decrypt, key, Some(&iv))?;
|
||||
encrypter.pad(false);
|
||||
let mut out_size = 0;
|
||||
out_size += encrypter.update(cypher_text, plain_text.as_mut_slice())?;
|
||||
out_size += encrypter.finalize(plain_text.as_mut_slice())?;
|
||||
plain_text.truncate(out_size);
|
||||
|
||||
Ok(plain_text)
|
||||
}
|
||||
|
||||
/// Computes a MAC of the given message.
|
||||
pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> {
|
||||
// Create a PKey
|
||||
let key = PKey::hmac(token)?;
|
||||
|
||||
// Compute the HMAC
|
||||
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer.update(input)?;
|
||||
let hmac = signer.sign_to_vec()?;
|
||||
Ok(hmac)
|
||||
}
|
||||
|
||||
// Currently unsued, because rc_crypto does not expose PKCS12 of NSS, so we can't parse the cert there
|
||||
// To use it in statemachine.rs for example, do:
|
||||
// if let Ok(cdhash) = client_data.hash() {
|
||||
// let verification_data: Vec<u8> = attestation
|
||||
// .auth_data
|
||||
// .to_vec()
|
||||
// .iter()
|
||||
// .chain(cdhash.as_ref().iter())
|
||||
// .copied()
|
||||
// .collect();
|
||||
// let res = attestation.att_statement.verify(&verification_data);
|
||||
// ...
|
||||
// }
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn verify(
|
||||
sig_alg: ECDSACurve,
|
||||
pub_key: &[u8],
|
||||
signature: &[u8],
|
||||
data: &[u8],
|
||||
) -> Result<bool> {
|
||||
let _alg = to_openssl_name(sig_alg)?; // TODO(MS): Actually use this to determine the right MessageDigest below
|
||||
let pkey = X509::from_der(&pub_key)?;
|
||||
let pubkey = pkey.public_key()?;
|
||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey)?;
|
||||
verifier.update(data)?;
|
||||
let res = verifier.verify(signature)?;
|
||||
Ok(res)
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
use super::{
|
||||
/*Signature,*/ COSEAlgorithm, COSEEC2Key, /*PlainText*/ COSEKey, COSEKeyType,
|
||||
/*CypherText,*/ ECDHSecret, ECDSACurve,
|
||||
};
|
||||
use ring::agreement::{
|
||||
agree_ephemeral, Algorithm, EphemeralPrivateKey, UnparsedPublicKey, ECDH_P256, ECDH_P384,
|
||||
};
|
||||
use ring::digest;
|
||||
use ring::hmac;
|
||||
use ring::rand::SystemRandom;
|
||||
use ring::signature::KeyPair;
|
||||
use serde::Serialize;
|
||||
use serde_bytes::ByteBuf;
|
||||
/*
|
||||
initialize()
|
||||
|
||||
This is run by the platform when starting a series of transactions with a specific authenticator.
|
||||
encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error
|
||||
|
||||
Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret.
|
||||
encrypt(key, demPlaintext) → ciphertext
|
||||
|
||||
Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
|
||||
decrypt(key, ciphertext) → plaintext | error
|
||||
|
||||
Decrypts a ciphertext and returns the plaintext.
|
||||
authenticate(key, message) → signature
|
||||
|
||||
Computes a MAC of the given message.
|
||||
*/
|
||||
|
||||
pub type Result<T> = std::result::Result<T, BackendError>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub enum BackendError {
|
||||
AgreementError,
|
||||
UnspecifiedRingError,
|
||||
KeyRejected,
|
||||
UnsupportedKeyType,
|
||||
UnsupportedCurve(ECDSACurve),
|
||||
}
|
||||
|
||||
fn to_ring_curve(curve: ECDSACurve) -> Result<&'static Algorithm> {
|
||||
match curve {
|
||||
ECDSACurve::SECP256R1 => Ok(&ECDH_P256),
|
||||
ECDSACurve::SECP384R1 => Ok(&ECDH_P384),
|
||||
x => Err(BackendError::UnsupportedCurve(x)),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ring::error::Unspecified> for BackendError {
|
||||
fn from(e: ring::error::Unspecified) -> Self {
|
||||
BackendError::UnspecifiedRingError
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ring::error::KeyRejected> for BackendError {
|
||||
fn from(e: ring::error::KeyRejected) -> Self {
|
||||
BackendError::KeyRejected
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_key(curve: ECDSACurve, x: &[u8], y: &[u8]) -> Result<Vec<u8>> {
|
||||
compile_error!(
|
||||
"Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(MS): Maybe remove ByteBuf and return Vec<u8>'s instead for a cleaner interface
|
||||
pub(crate) fn serialize_key(curve: ECDSACurve, key: &[u8]) -> Result<(ByteBuf, ByteBuf)> {
|
||||
compile_error!(
|
||||
"Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
|
||||
)
|
||||
}
|
||||
|
||||
/// This is run by the platform when starting a series of transactions with a specific authenticator.
|
||||
pub(crate) fn initialize() {
|
||||
compile_error!(
|
||||
"Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
|
||||
)
|
||||
}
|
||||
|
||||
/// Generates an encapsulation for the authenticator’s public key and returns the message
|
||||
/// to transmit and the shared secret.
|
||||
pub(crate) fn encapsulate(key: &COSEKey) -> Result<ECDHSecret> {
|
||||
if let COSEKeyType::EC2(ec2key) = &key.key {
|
||||
// let curve_name = to_openssl_name(ec2key.curve)?;
|
||||
// let group = EcGroup::from_curve_name(curve_name)?;
|
||||
// let my_key = EcKey::generate(&group)?;
|
||||
|
||||
// encapsulate_helper(&ec2key, key.alg, group, my_key)
|
||||
let rng = SystemRandom::new();
|
||||
let peer_public_key_alg = to_ring_curve(ec2key.curve)?;
|
||||
let private_key = EphemeralPrivateKey::generate(peer_public_key_alg, &rng)?;
|
||||
let my_public_key = private_key.compute_public_key()?;
|
||||
let (x, y) = serialize_key(ec2key.curve, my_public_key.as_ref())?;
|
||||
let my_public_key = COSEKey {
|
||||
alg: key.alg,
|
||||
key: COSEKeyType::EC2(COSEEC2Key {
|
||||
curve: ec2key.curve,
|
||||
x: x.to_vec(),
|
||||
y: y.to_vec(),
|
||||
}),
|
||||
};
|
||||
|
||||
let key_bytes = parse_key(ec2key.curve, &ec2key.x, &ec2key.y)?;
|
||||
let peer_public_key = UnparsedPublicKey::new(peer_public_key_alg, &key_bytes);
|
||||
|
||||
let shared_secret = agree_ephemeral(private_key, &peer_public_key, (), |key_material| {
|
||||
let digest = digest::digest(&digest::SHA256, key_material);
|
||||
Ok(Vec::from(digest.as_ref()))
|
||||
})
|
||||
.map_err(|_| BackendError::AgreementError)?;
|
||||
|
||||
Ok(ECDHSecret {
|
||||
remote: COSEKey {
|
||||
alg: key.alg,
|
||||
key: COSEKeyType::EC2(ec2key.clone()),
|
||||
},
|
||||
my: my_public_key,
|
||||
shared_secret,
|
||||
})
|
||||
} else {
|
||||
Err(BackendError::UnsupportedKeyType)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_encapsulate(
|
||||
key: &COSEEC2Key,
|
||||
alg: COSEAlgorithm,
|
||||
my_pub_key: &[u8],
|
||||
my_priv_key: &[u8],
|
||||
) -> Result<ECDHSecret> {
|
||||
compile_error!(
|
||||
"Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
|
||||
)
|
||||
}
|
||||
|
||||
/// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext.
|
||||
/// The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length.
|
||||
pub(crate) fn encrypt(
|
||||
key: &[u8],
|
||||
plain_text: &[u8], /*PlainText*/
|
||||
) -> Result<Vec<u8> /*CypherText*/> {
|
||||
// Ring doesn't support AES-CBC yet
|
||||
compile_error!(
|
||||
"Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
|
||||
)
|
||||
}
|
||||
|
||||
/// Decrypts a ciphertext and returns the plaintext.
|
||||
pub(crate) fn decrypt(
|
||||
key: &[u8],
|
||||
cypher_text: &[u8], /*CypherText*/
|
||||
) -> Result<Vec<u8> /*PlainText*/> {
|
||||
// Ring doesn't support AES-CBC yet
|
||||
compile_error!(
|
||||
"Ring-backend is not yet implemented. Compile with `--features crypto_openssl` for now."
|
||||
)
|
||||
}
|
||||
|
||||
/// Computes a MAC of the given message.
|
||||
pub(crate) fn authenticate(token: &[u8], input: &[u8]) -> Result<Vec<u8>> {
|
||||
let s_key = hmac::Key::new(hmac::HMAC_SHA256, token);
|
||||
let tag = hmac::sign(&s_key, input);
|
||||
Ok(tag.as_ref().to_vec())
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
#ifndef __CTAP2_CAPI
|
||||
#define __CTAP2_CAPI
|
||||
#include <stdlib.h>
|
||||
#include "nsString.h"
|
||||
|
||||
extern "C" {
|
||||
const uint8_t CTAP2_SIGN_RESULT_PUBKEY_CRED_ID = 1;
|
||||
const uint8_t CTAP2_SIGN_RESULT_AUTH_DATA = 2;
|
||||
const uint8_t CTAP2_SIGN_RESULT_SIGNATURE = 3;
|
||||
const uint8_t CTAP2_SIGN_RESULT_USER_ID = 4;
|
||||
const uint8_t CTAP2_SIGN_RESULT_USER_NAME = 5;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *id_ptr;
|
||||
size_t id_len;
|
||||
const char *name;
|
||||
} AuthenticatorArgsUser;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *ptr;
|
||||
size_t len;
|
||||
} AuthenticatorArgsChallenge;
|
||||
|
||||
typedef struct {
|
||||
const int32_t *ptr;
|
||||
size_t len;
|
||||
} AuthenticatorArgsPubCred;
|
||||
|
||||
typedef struct {
|
||||
bool resident_key;
|
||||
bool user_verification;
|
||||
bool user_presence;
|
||||
bool force_none_attestation;
|
||||
} AuthenticatorArgsOptions;
|
||||
|
||||
// NOTE: Preconditions
|
||||
// * All rust_u2f_mgr* pointers must refer to pointers which are returned
|
||||
// by rust_u2f_mgr_new, and must be freed with rust_u2f_mgr_free.
|
||||
// * All rust_u2f_khs* pointers must refer to pointers which are returned
|
||||
// by rust_u2f_pkcd_new, and must be freed with rust_u2f_pkcd_free.
|
||||
// * All rust_u2f_res* pointers must refer to pointers passed to the
|
||||
// register() and sign() callbacks. They can be null on failure.
|
||||
|
||||
// The `rust_u2f_key_handles` opaque type is equivalent to the rust type
|
||||
// `Ctap2PubKeyCredDescriptors`
|
||||
struct rust_ctap2_pub_key_cred_descriptors;
|
||||
|
||||
/// Ctap2PubKeyCredDescriptors functions.
|
||||
rust_ctap2_pub_key_cred_descriptors* rust_ctap2_pkcd_new();
|
||||
void rust_ctap2_pkcd_add(rust_ctap2_pub_key_cred_descriptors* pkcd, const uint8_t* id_ptr,
|
||||
size_t id_len, uint8_t transports);
|
||||
/* unsafe */ void rust_ctap2_pkcd_free(rust_ctap2_pub_key_cred_descriptors* khs);
|
||||
|
||||
// The `rust_ctap2_mgr` opaque type is equivalent to the rust type `Ctap2Manager`
|
||||
// struct rust_ctap_manager;
|
||||
|
||||
// The `rust_ctap2_result` opaque type is equivalent to the rust type `RegisterResult`
|
||||
struct rust_ctap2_register_result;
|
||||
|
||||
// The `rust_ctap2_result` opaque type is equivalent to the rust type `RegisterResult`
|
||||
struct rust_ctap2_sign_result;
|
||||
|
||||
// Ctap2 exposes the results directly without repackaging them. Use getter-functions.
|
||||
typedef void (*rust_ctap2_register_callback)(uint64_t, rust_ctap2_register_result*);
|
||||
typedef void (*rust_ctap2_sign_callback)(uint64_t, rust_ctap2_sign_result*);
|
||||
|
||||
// Status updates get sent, if a device needs a PIN, if a device needs to be selected, etc.
|
||||
struct rust_ctap2_status_update_res;
|
||||
// May be called with NULL, in case of an error
|
||||
typedef void (*rust_ctap2_status_update_callback)(rust_ctap2_status_update_res*);
|
||||
|
||||
rust_ctap_manager* rust_ctap2_mgr_new();
|
||||
/* unsafe */ void rust_ctap2_mgr_free(rust_ctap_manager* mgr);
|
||||
|
||||
/* unsafe */ void rust_ctap2_register_res_free(rust_ctap2_register_result* res);
|
||||
/* unsafe */ void rust_ctap2_sign_res_free(rust_ctap2_sign_result* res);
|
||||
|
||||
uint64_t rust_ctap2_mgr_register(
|
||||
rust_ctap_manager* mgr, uint64_t timeout, rust_ctap2_register_callback, rust_ctap2_status_update_callback,
|
||||
AuthenticatorArgsChallenge challenge,
|
||||
const char* relying_party_id, const char *origin_ptr,
|
||||
AuthenticatorArgsUser user, AuthenticatorArgsPubCred pub_cred_params,
|
||||
const rust_ctap2_pub_key_cred_descriptors* exclude_list, AuthenticatorArgsOptions options,
|
||||
const char *pin
|
||||
);
|
||||
|
||||
uint64_t rust_ctap2_mgr_sign(
|
||||
rust_ctap_manager* mgr, uint64_t timeout, rust_ctap2_sign_callback, rust_ctap2_status_update_callback,
|
||||
AuthenticatorArgsChallenge challenge,
|
||||
const char* relying_party_id, const char *origin_ptr,
|
||||
const rust_ctap2_pub_key_cred_descriptors* allow_list, AuthenticatorArgsOptions options,
|
||||
const char *pin
|
||||
);
|
||||
|
||||
void rust_ctap2_mgr_cancel(rust_ctap_manager* mgr);
|
||||
|
||||
// Returns 0 for success, or the U2F_ERROR error code >= 1.
|
||||
uint8_t rust_ctap2_register_result_error(const rust_ctap2_register_result* res);
|
||||
uint8_t rust_ctap2_sign_result_error(const rust_ctap2_sign_result* res);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_register_result_client_data_copy()
|
||||
bool rust_ctap2_register_result_client_data_len(
|
||||
const rust_ctap2_register_result *res,
|
||||
size_t *len
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_register_result_client_data_len)
|
||||
bool rust_ctap2_register_result_client_data_copy(
|
||||
const rust_ctap2_register_result *res,
|
||||
const char *dst
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_register_result_item_copy()
|
||||
bool rust_ctap2_register_result_attestation_len(
|
||||
const rust_ctap2_register_result *res,
|
||||
size_t *len
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_register_result_item_len)
|
||||
bool rust_ctap2_register_result_attestation_copy(
|
||||
const rust_ctap2_register_result* res,
|
||||
uint8_t *dst
|
||||
);
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_register_result_client_data_copy()
|
||||
bool rust_ctap2_sign_result_client_data_len(
|
||||
const rust_ctap2_sign_result *res,
|
||||
size_t *len
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_sign_result_client_data_len)
|
||||
bool rust_ctap2_sign_result_client_data_copy(
|
||||
const rust_ctap2_sign_result *res,
|
||||
const char *dst
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_register_result_client_data_copy()
|
||||
bool rust_ctap2_sign_result_assertions_len(
|
||||
const rust_ctap2_sign_result *res,
|
||||
size_t *len
|
||||
);
|
||||
|
||||
bool rust_ctap2_sign_result_item_contains(
|
||||
const rust_ctap2_sign_result *res,
|
||||
size_t assertion_idx,
|
||||
uint8_t item_idx
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_sign_result_item_copy()
|
||||
bool rust_ctap2_sign_result_item_len(
|
||||
const rust_ctap2_sign_result *res,
|
||||
size_t assertion_idx,
|
||||
uint8_t item_idx,
|
||||
size_t *len
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_sign_result_item_len)
|
||||
bool rust_ctap2_sign_result_item_copy(
|
||||
const rust_ctap2_sign_result* res,
|
||||
size_t assertion_idx,
|
||||
uint8_t item_idx,
|
||||
uint8_t *dst
|
||||
);
|
||||
|
||||
bool rust_ctap2_sign_result_contains_username(
|
||||
const rust_ctap2_sign_result *res,
|
||||
size_t assertion_idx
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_sign_result_username_copy()
|
||||
bool rust_ctap2_sign_result_username_len(
|
||||
const rust_ctap2_sign_result *res,
|
||||
size_t assertion_idx,
|
||||
size_t *len
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_sign_result_username_len)
|
||||
bool rust_ctap2_sign_result_username_copy(
|
||||
const rust_ctap2_sign_result* res,
|
||||
size_t assertion_idx,
|
||||
const char *dst
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_status_update_copy_json()
|
||||
bool rust_ctap2_status_update_len(
|
||||
const rust_ctap2_status_update_res *res,
|
||||
size_t *len
|
||||
);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_status_update_len)
|
||||
bool rust_ctap2_status_update_copy_json(
|
||||
const rust_ctap2_status_update_res *res,
|
||||
const char *dst
|
||||
);
|
||||
|
||||
bool rust_ctap2_status_update_send_pin(
|
||||
const rust_ctap2_status_update_res *res,
|
||||
const char *pin
|
||||
);
|
||||
|
||||
|
||||
/// # Safety
|
||||
/// This frees the memory of a status_update_res
|
||||
bool rust_ctap2_destroy_status_update_res(
|
||||
rust_ctap2_status_update_res *res
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
#endif // __CTAP2_CAPI
|
|
@ -0,0 +1,799 @@
|
|||
use super::utils::from_slice_stream;
|
||||
use crate::crypto::COSEAlgorithm;
|
||||
use crate::ctap2::commands::CommandError;
|
||||
use crate::ctap2::server::RpIdHash;
|
||||
use crate::{crypto::COSEKey, errors::AuthenticatorError};
|
||||
use nom::{
|
||||
bytes::complete::take,
|
||||
combinator::{cond, map},
|
||||
error::Error as NomError,
|
||||
number::complete::{be_u16, be_u32, be_u8},
|
||||
Err as NomErr, IResult,
|
||||
};
|
||||
use serde::ser::{Error as SerError, SerializeMap, Serializer};
|
||||
use serde::{
|
||||
de::{Error as SerdeError, MapAccess, Visitor},
|
||||
Deserialize, Deserializer, Serialize,
|
||||
};
|
||||
use serde_bytes::ByteBuf;
|
||||
use serde_cbor;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum HmacSecretResponse {
|
||||
/// This is returned by MakeCredential calls to display if CredRandom was
|
||||
/// successfully generated
|
||||
Confirmed(bool),
|
||||
/// This is returned by GetAssertion:
|
||||
/// AES256-CBC(shared_secret, HMAC-SHA265(CredRandom, salt1) || HMAC-SHA265(CredRandom, salt2))
|
||||
Secret(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Serialize for HmacSecretResponse {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
HmacSecretResponse::Confirmed(x) => serializer.serialize_bool(*x),
|
||||
HmacSecretResponse::Secret(x) => serializer.serialize_bytes(&x),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for HmacSecretResponse {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct HmacSecretResponseVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for HmacSecretResponseVisitor {
|
||||
type Value = HmacSecretResponse;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a byte array or a boolean")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: SerdeError,
|
||||
{
|
||||
Ok(HmacSecretResponse::Secret(v.to_vec()))
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: SerdeError,
|
||||
{
|
||||
Ok(HmacSecretResponse::Confirmed(v))
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_any(HmacSecretResponseVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct Extension {
|
||||
#[serde(rename = "pinMinLength", skip_serializing_if = "Option::is_none")]
|
||||
pub pin_min_length: Option<u64>,
|
||||
#[serde(rename = "hmac-secret", skip_serializing_if = "Option::is_none")]
|
||||
pub hmac_secret: Option<HmacSecretResponse>,
|
||||
}
|
||||
|
||||
fn parse_extensions<'a>(input: &'a [u8]) -> IResult<&'a [u8], Extension, NomError<&'a [u8]>> {
|
||||
serde_to_nom(input)
|
||||
}
|
||||
|
||||
#[derive(Serialize, PartialEq, Default, Eq, Clone)]
|
||||
pub struct AAGuid(pub [u8; 16]);
|
||||
|
||||
impl AAGuid {
|
||||
pub fn from(src: &[u8]) -> Result<AAGuid, ()> {
|
||||
let mut payload = [0u8; 16];
|
||||
if src.len() != payload.len() {
|
||||
Err(())
|
||||
} else {
|
||||
payload.copy_from_slice(src);
|
||||
Ok(AAGuid(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for AAGuid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"AAGuid({:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x})",
|
||||
self.0[0],
|
||||
self.0[1],
|
||||
self.0[2],
|
||||
self.0[3],
|
||||
self.0[4],
|
||||
self.0[5],
|
||||
self.0[6],
|
||||
self.0[7],
|
||||
self.0[8],
|
||||
self.0[9],
|
||||
self.0[10],
|
||||
self.0[11],
|
||||
self.0[12],
|
||||
self.0[13],
|
||||
self.0[14],
|
||||
self.0[15]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AAGuid {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct AAGuidVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AAGuidVisitor {
|
||||
type Value = AAGuid;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a byte array")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: SerdeError,
|
||||
{
|
||||
let mut buf = [0u8; 16];
|
||||
if v.len() != buf.len() {
|
||||
return Err(E::invalid_length(v.len(), &"16"));
|
||||
}
|
||||
|
||||
buf.copy_from_slice(v);
|
||||
|
||||
Ok(AAGuid(buf))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(AAGuidVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AttestedCredentialData {
|
||||
pub aaguid: AAGuid,
|
||||
pub credential_id: Vec<u8>,
|
||||
pub credential_public_key: COSEKey,
|
||||
}
|
||||
|
||||
fn serde_to_nom<'a, Output>(input: &'a [u8]) -> IResult<&'a [u8], Output>
|
||||
where
|
||||
Output: Deserialize<'a>,
|
||||
{
|
||||
from_slice_stream(input)
|
||||
.map_err(|_e| nom::Err::Error(nom::error::make_error(input, nom::error::ErrorKind::NoneOf)))
|
||||
// can't use custom errorkind because of error type mismatch in parse_attested_cred_data
|
||||
//.map_err(|e| NomErr::Error(Context::Code(input, ErrorKind::Custom(e))))
|
||||
// .map_err(|_| NomErr::Error(Context::Code(input, ErrorKind::Custom(42))))
|
||||
}
|
||||
|
||||
fn parse_attested_cred_data<'a>(
|
||||
input: &'a [u8],
|
||||
) -> IResult<&'a [u8], AttestedCredentialData, NomError<&'a [u8]>> {
|
||||
let (rest, aaguid_res) = map(take(16u8), AAGuid::from)(input)?;
|
||||
// // We can unwrap here, since we _know_ the input will be 16 bytes error out before calling from()
|
||||
let aaguid = aaguid_res.unwrap();
|
||||
let (rest, cred_len) = be_u16(rest)?;
|
||||
let (rest, credential_id) = map(take(cred_len), Vec::from)(rest)?;
|
||||
let (rest, credential_public_key) = serde_to_nom(rest)?;
|
||||
Ok((
|
||||
rest,
|
||||
(AttestedCredentialData {
|
||||
aaguid,
|
||||
credential_id,
|
||||
credential_public_key: credential_public_key,
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
pub struct AuthenticatorDataFlags: u8 {
|
||||
const USER_PRESENT = 0x01;
|
||||
const USER_VERIFIED = 0x04;
|
||||
const ATTESTED = 0x40;
|
||||
const EXTENSION_DATA = 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AuthenticatorData {
|
||||
pub rp_id_hash: RpIdHash,
|
||||
pub flags: AuthenticatorDataFlags,
|
||||
pub counter: u32,
|
||||
pub credential_data: Option<AttestedCredentialData>,
|
||||
pub extensions: Extension,
|
||||
}
|
||||
|
||||
fn parse_ad<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthenticatorData, NomError<&'a [u8]>> {
|
||||
let (rest, rp_id_hash_res) = map(take(32u8), RpIdHash::from)(input)?;
|
||||
// We can unwrap here, since we _know_ the input to from() will be 32 bytes or error out before calling from()
|
||||
let rp_id_hash = rp_id_hash_res.unwrap();
|
||||
// be conservative, there is a couple of reserved values in
|
||||
// AuthenticatorDataFlags, just truncate the one we don't know
|
||||
let (rest, flags) = map(be_u8, AuthenticatorDataFlags::from_bits_truncate)(rest)?;
|
||||
let (rest, counter) = be_u32(rest)?;
|
||||
let (rest, credential_data) = cond(
|
||||
flags.contains(AuthenticatorDataFlags::ATTESTED),
|
||||
parse_attested_cred_data,
|
||||
)(rest)?;
|
||||
let (rest, extensions) = cond(
|
||||
flags.contains(AuthenticatorDataFlags::EXTENSION_DATA),
|
||||
parse_extensions,
|
||||
)(rest)?;
|
||||
// TODO(baloo): we should check for end of buffer and raise a parse
|
||||
// parse error if data is still in the buffer
|
||||
//eof!() >>
|
||||
Ok((
|
||||
rest,
|
||||
AuthenticatorData {
|
||||
rp_id_hash,
|
||||
flags,
|
||||
counter,
|
||||
credential_data,
|
||||
extensions: extensions.unwrap_or_default(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AuthenticatorData {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct AuthenticatorDataVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AuthenticatorDataVisitor {
|
||||
type Value = AuthenticatorData;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a byte array")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: SerdeError,
|
||||
{
|
||||
parse_ad(v)
|
||||
.map(|(_input, value)| value)
|
||||
.map_err(|e| match e {
|
||||
NomErr::Incomplete(nom::Needed::Size(len)) => {
|
||||
E::invalid_length(v.len(), &format!("{}", v.len() + len.get()).as_ref())
|
||||
}
|
||||
NomErr::Incomplete(nom::Needed::Unknown) => {
|
||||
E::invalid_length(v.len(), &"unknown") // We don't know the expected value
|
||||
}
|
||||
// TODO(baloo): is that enough? should we be more
|
||||
// specific on the error type?
|
||||
e => E::custom(e.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(AuthenticatorDataVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticatorData {
|
||||
pub fn to_vec(&self) -> Result<Vec<u8>, AuthenticatorError> {
|
||||
let mut data = Vec::new();
|
||||
data.extend(&self.rp_id_hash.0);
|
||||
data.extend(&[self.flags.bits()]);
|
||||
data.extend(&self.counter.to_be_bytes());
|
||||
|
||||
// TODO(baloo): need to yield credential_data and extensions, but that dependends on flags,
|
||||
// should we consider another type system?
|
||||
if let Some(cred) = &self.credential_data {
|
||||
data.extend(&cred.aaguid.0);
|
||||
data.extend(&(cred.credential_id.len() as u16).to_be_bytes());
|
||||
data.extend(&cred.credential_id);
|
||||
data.extend(
|
||||
&serde_cbor::to_vec(&cred.credential_public_key)
|
||||
.map_err(CommandError::Serializing)?,
|
||||
);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
/// x509 encoded attestation certificate
|
||||
pub struct AttestationCertificate(#[serde(with = "serde_bytes")] pub(crate) Vec<u8>);
|
||||
|
||||
impl AsRef<[u8]> for AttestationCertificate {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Signature(#[serde(with = "serde_bytes")] pub(crate) ByteBuf);
|
||||
|
||||
impl fmt::Debug for Signature {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD);
|
||||
write!(f, "Signature({})", value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Signature {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AttestationStatement {
|
||||
None,
|
||||
Packed(AttestationStatementPacked),
|
||||
// TODO(baloo): there is a couple other options than None and Packed:
|
||||
// https://w3c.github.io/webauthn/#generating-an-attestation-object
|
||||
// https://w3c.github.io/webauthn/#defined-attestation-formats
|
||||
//TPM,
|
||||
//AndroidKey,
|
||||
//AndroidSafetyNet,
|
||||
FidoU2F(AttestationStatementFidoU2F),
|
||||
}
|
||||
|
||||
// Not all crypto-backends currently provide "crypto::verify()", so we do not implement it yet.
|
||||
// Also not sure, if we really need it. Would be a sanity-check only, to verify the signature is valid,
|
||||
// before sendig it out.
|
||||
// impl AttestationStatement {
|
||||
// pub fn verify(&self, data: &[u8]) -> Result<bool, AuthenticatorError> {
|
||||
// match self {
|
||||
// AttestationStatement::None => Ok(true),
|
||||
// AttestationStatement::Unparsed(_) => Err(AuthenticatorError::Custom(
|
||||
// "Unparsed attestation object can't be used to verify signature.".to_string(),
|
||||
// )),
|
||||
// AttestationStatement::FidoU2F(att) => {
|
||||
// let res = crypto::verify(
|
||||
// crypto::SignatureAlgorithm::ES256,
|
||||
// &att.attestation_cert[0].as_ref(),
|
||||
// att.sig.as_ref(),
|
||||
// data,
|
||||
// )?;
|
||||
// Ok(res)
|
||||
// }
|
||||
// AttestationStatement::Packed(att) => {
|
||||
// if att.alg != Alg::ES256 {
|
||||
// return Err(AuthenticatorError::Custom(
|
||||
// "Verification only supported for ES256".to_string(),
|
||||
// ));
|
||||
// }
|
||||
// let res = crypto::verify(
|
||||
// crypto::SignatureAlgorithm::ES256,
|
||||
// att.attestation_cert[0].as_ref(),
|
||||
// att.sig.as_ref(),
|
||||
// data,
|
||||
// )?;
|
||||
// Ok(res)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
// See https://www.w3.org/TR/webauthn/#fido-u2f-attestation
|
||||
pub struct AttestationStatementFidoU2F {
|
||||
pub sig: Signature,
|
||||
|
||||
#[serde(rename = "x5c")]
|
||||
/// Certificate chain in x509 format
|
||||
pub attestation_cert: Vec<AttestationCertificate>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // TODO(MS): Remove me, once we can parse AttestationStatements and use this function
|
||||
impl AttestationStatementFidoU2F {
|
||||
pub fn new(cert: &[u8], signature: &[u8]) -> Self {
|
||||
AttestationStatementFidoU2F {
|
||||
sig: Signature(ByteBuf::from(signature)),
|
||||
attestation_cert: vec![AttestationCertificate(Vec::from(cert))],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
// TODO(baloo): there is a couple other options than x5c:
|
||||
// https://www.w3.org/TR/webauthn/#packed-attestation
|
||||
pub struct AttestationStatementPacked {
|
||||
pub alg: COSEAlgorithm,
|
||||
pub sig: Signature,
|
||||
|
||||
#[serde(rename = "x5c")]
|
||||
/// Certificate chain in x509 format
|
||||
pub attestation_cert: Vec<AttestationCertificate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum AttestationFormat {
|
||||
#[serde(rename = "fido-u2f")]
|
||||
FidoU2F,
|
||||
Packed,
|
||||
None,
|
||||
// TOOD(baloo): only packed is implemented for now, see spec:
|
||||
// https://www.w3.org/TR/webauthn/#defined-attestation-formats
|
||||
//TPM,
|
||||
//AndroidKey,
|
||||
//AndroidSafetyNet,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct AttestationObject {
|
||||
pub auth_data: AuthenticatorData,
|
||||
pub att_statement: AttestationStatement,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AttestationObject {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct AttestationObjectVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AttestationObjectVisitor {
|
||||
type Value = AttestationObject;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a cbor map")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut format: Option<AttestationFormat> = None;
|
||||
let mut auth_data = None;
|
||||
let mut att_statement = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
// Spec for CTAP 2.0 is wrong and fmt should be numbered 1, and auth_data 2:
|
||||
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential
|
||||
// Corrected in CTAP 2.1 and Webauthn spec
|
||||
1 => {
|
||||
if format.is_some() {
|
||||
return Err(SerdeError::duplicate_field("fmt"));
|
||||
}
|
||||
format = Some(map.next_value()?);
|
||||
}
|
||||
2 => {
|
||||
if auth_data.is_some() {
|
||||
return Err(SerdeError::duplicate_field("auth_data"));
|
||||
}
|
||||
auth_data = Some(map.next_value()?);
|
||||
}
|
||||
3 => {
|
||||
let format = format
|
||||
.as_ref()
|
||||
.ok_or_else(|| SerdeError::missing_field("fmt"))?;
|
||||
if att_statement.is_some() {
|
||||
return Err(SerdeError::duplicate_field("att_statement"));
|
||||
}
|
||||
match format {
|
||||
// This should not actually happen, but ...
|
||||
AttestationFormat::None => {
|
||||
att_statement = Some(AttestationStatement::None);
|
||||
}
|
||||
AttestationFormat::Packed => {
|
||||
att_statement =
|
||||
Some(AttestationStatement::Packed(map.next_value()?));
|
||||
}
|
||||
AttestationFormat::FidoU2F => {
|
||||
att_statement =
|
||||
Some(AttestationStatement::FidoU2F(map.next_value()?));
|
||||
}
|
||||
}
|
||||
}
|
||||
k => return Err(M::Error::custom(format!("unexpected key: {:?}", k))),
|
||||
}
|
||||
}
|
||||
|
||||
let auth_data =
|
||||
auth_data.ok_or_else(|| M::Error::custom("found no auth_data".to_string()))?;
|
||||
let att_statement = att_statement.unwrap_or(AttestationStatement::None);
|
||||
|
||||
Ok(AttestationObject {
|
||||
auth_data,
|
||||
att_statement,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(AttestationObjectVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for AttestationObject {
|
||||
/// Serialize can be used to repackage the CBOR answer we get from the token using CTAP-format
|
||||
/// to webauthn-format (string-keys like "authData" instead of numbers). Yes, the specs are weird.
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let map_len = 3;
|
||||
let mut map = serializer.serialize_map(Some(map_len))?;
|
||||
|
||||
let auth_data = self
|
||||
.auth_data
|
||||
.to_vec()
|
||||
.map(|v| serde_cbor::Value::Bytes(v))
|
||||
.map_err(|_| SerError::custom("Failed to serialize auth_data"))?;
|
||||
|
||||
map.serialize_entry(&"authData", &auth_data)?;
|
||||
match self.att_statement {
|
||||
AttestationStatement::None => {
|
||||
// Even with Att None, an empty map is returned in the cbor!
|
||||
map.serialize_entry(&"fmt", &"none")?;
|
||||
let v = serde_cbor::Value::Map(std::collections::BTreeMap::new());
|
||||
map.serialize_entry(&"attStmt", &v)?;
|
||||
}
|
||||
AttestationStatement::Packed(ref v) => {
|
||||
map.serialize_entry(&"fmt", &"packed")?;
|
||||
map.serialize_entry(&"attStmt", v)?;
|
||||
}
|
||||
AttestationStatement::FidoU2F(ref v) => {
|
||||
map.serialize_entry(&"fmt", &"fido-u2f")?;
|
||||
map.serialize_entry(&"attStmt", v)?;
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::utils::from_slice_stream;
|
||||
use super::*;
|
||||
use serde_cbor::from_slice;
|
||||
|
||||
const SAMPLE_ATTESTATION: [u8; 1006] = [
|
||||
0xa3, 0x1, 0x66, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x2, 0x58, 0xc4, 0x49, 0x96, 0xd,
|
||||
0xe5, 0x88, 0xe, 0x8c, 0x68, 0x74, 0x34, 0x17, 0xf, 0x64, 0x76, 0x60, 0x5b, 0x8f, 0xe4,
|
||||
0xae, 0xb9, 0xa2, 0x86, 0x32, 0xc7, 0x99, 0x5c, 0xf3, 0xba, 0x83, 0x1d, 0x97, 0x63, 0x41,
|
||||
0x0, 0x0, 0x0, 0x7, 0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27,
|
||||
0x29, 0xa1, 0x54, 0xa8, 0x0, 0x40, 0xc3, 0xcf, 0x1, 0x3b, 0xc6, 0x26, 0x93, 0x28, 0xfb,
|
||||
0x7f, 0xa9, 0x76, 0xef, 0xa8, 0x4b, 0x66, 0x71, 0xad, 0xa9, 0x64, 0xea, 0xcb, 0x58, 0x76,
|
||||
0x54, 0x51, 0xa, 0xc8, 0x86, 0x4f, 0xbb, 0x53, 0x2d, 0xfb, 0x2, 0xfc, 0xdc, 0xa9, 0x84,
|
||||
0xc2, 0x5c, 0x67, 0x8a, 0x3a, 0xab, 0x57, 0xf3, 0x71, 0x77, 0xd3, 0xd4, 0x41, 0x64, 0x1,
|
||||
0x50, 0xca, 0x6c, 0x42, 0x73, 0x1c, 0x42, 0xcb, 0x81, 0xba, 0xa5, 0x1, 0x2, 0x3, 0x26,
|
||||
0x20, 0x1, 0x21, 0x58, 0x20, 0x9, 0x2e, 0x34, 0xfe, 0xa7, 0xd7, 0x32, 0xc8, 0xae, 0x4c,
|
||||
0xf6, 0x96, 0xbe, 0x7a, 0x12, 0xdc, 0x29, 0xd5, 0xf1, 0xd3, 0xf1, 0x55, 0x4d, 0xdc, 0x87,
|
||||
0xc4, 0xc, 0x9b, 0xd0, 0x17, 0xba, 0xf, 0x22, 0x58, 0x20, 0xc9, 0xf0, 0x97, 0x33, 0x55,
|
||||
0x36, 0x58, 0xd9, 0xdb, 0x76, 0xf5, 0xef, 0x95, 0xcf, 0x8a, 0xc7, 0xfc, 0xc1, 0xb6, 0x81,
|
||||
0x25, 0x5f, 0x94, 0x6b, 0x62, 0x13, 0x7d, 0xd0, 0xc4, 0x86, 0x53, 0xdb, 0x3, 0xa3, 0x63,
|
||||
0x61, 0x6c, 0x67, 0x26, 0x63, 0x73, 0x69, 0x67, 0x58, 0x48, 0x30, 0x46, 0x2, 0x21, 0x0,
|
||||
0xac, 0x2a, 0x78, 0xa8, 0xaf, 0x18, 0x80, 0x39, 0x73, 0x8d, 0x3, 0x5e, 0x4, 0x4d, 0x94,
|
||||
0x4f, 0x3f, 0x57, 0xce, 0x88, 0x41, 0xfa, 0x81, 0x50, 0x40, 0xb6, 0xd1, 0x95, 0xb5, 0xeb,
|
||||
0xe4, 0x6f, 0x2, 0x21, 0x0, 0x8f, 0xf4, 0x15, 0xc9, 0xb3, 0x6d, 0x1c, 0xd, 0x4c, 0xa3,
|
||||
0xcf, 0x99, 0x8a, 0x46, 0xd4, 0x4c, 0x8b, 0x5c, 0x26, 0x3f, 0xdf, 0x22, 0x6c, 0x9b, 0x23,
|
||||
0x83, 0x8b, 0x69, 0x47, 0x67, 0x48, 0x45, 0x63, 0x78, 0x35, 0x63, 0x81, 0x59, 0x2, 0xc1,
|
||||
0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1, 0x2, 0x2, 0x4, 0x18,
|
||||
0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb,
|
||||
0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3, 0x13, 0x23, 0x59,
|
||||
0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20,
|
||||
0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35, 0x37, 0x32, 0x30,
|
||||
0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38, 0x30, 0x31, 0x30,
|
||||
0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30, 0x30, 0x39, 0x30,
|
||||
0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb, 0x30, 0x9, 0x6, 0x3,
|
||||
0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6, 0x3, 0x55, 0x4, 0xa,
|
||||
0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31, 0x22, 0x30, 0x20,
|
||||
0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
|
||||
0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59, 0x75, 0x62, 0x69,
|
||||
0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61,
|
||||
0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30, 0x59, 0x30, 0x13,
|
||||
0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86, 0x48, 0xce, 0x3d,
|
||||
0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49, 0x70, 0x10, 0x62,
|
||||
0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83, 0xf1, 0x0, 0xbe,
|
||||
0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e, 0xcf, 0xca,
|
||||
0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e, 0xbd, 0x37,
|
||||
0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30, 0x6a, 0x30,
|
||||
0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15, 0x31, 0x2e,
|
||||
0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34, 0x38, 0x32,
|
||||
0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c,
|
||||
0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4,
|
||||
0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48, 0x1e, 0x8f,
|
||||
0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc, 0x6, 0x3, 0x55,
|
||||
0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48,
|
||||
0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d, 0x3, 0x97,
|
||||
0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22, 0xfa, 0xa7,
|
||||
0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1, 0x8a, 0x48,
|
||||
0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9, 0xb1, 0xce,
|
||||
0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1, 0xcb, 0xdd,
|
||||
0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15, 0x9e, 0x7f,
|
||||
0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a, 0xea, 0x17,
|
||||
0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7, 0x53, 0xd7,
|
||||
0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d, 0x43, 0x6,
|
||||
0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69, 0x24, 0x22,
|
||||
0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a, 0xa7, 0x15,
|
||||
0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1, 0xc3, 0xb4,
|
||||
0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4, 0x1a, 0xcb,
|
||||
0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84, 0xbd, 0xdd,
|
||||
0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5, 0x95, 0x27,
|
||||
0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd, 0x19, 0x11,
|
||||
0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f, 0x3a, 0xef,
|
||||
0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79,
|
||||
];
|
||||
|
||||
const SAMPLE_CERT_CHAIN: [u8; 709] = [
|
||||
0x81, 0x59, 0x2, 0xc1, 0x30, 0x82, 0x2, 0xbd, 0x30, 0x82, 0x1, 0xa5, 0xa0, 0x3, 0x2, 0x1,
|
||||
0x2, 0x2, 0x4, 0x18, 0xac, 0x46, 0xc0, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7,
|
||||
0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x30, 0x2e, 0x31, 0x2c, 0x30, 0x2a, 0x6, 0x3, 0x55, 0x4, 0x3,
|
||||
0x13, 0x23, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x52, 0x6f,
|
||||
0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x35,
|
||||
0x37, 0x32, 0x30, 0x30, 0x36, 0x33, 0x31, 0x30, 0x20, 0x17, 0xd, 0x31, 0x34, 0x30, 0x38,
|
||||
0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, 0xf, 0x32, 0x30, 0x35, 0x30,
|
||||
0x30, 0x39, 0x30, 0x34, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x30, 0x6e, 0x31, 0xb,
|
||||
0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x53, 0x45, 0x31, 0x12, 0x30, 0x10, 0x6,
|
||||
0x3, 0x55, 0x4, 0xa, 0xc, 0x9, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x41, 0x42, 0x31,
|
||||
0x22, 0x30, 0x20, 0x6, 0x3, 0x55, 0x4, 0xb, 0xc, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x31, 0x27, 0x30, 0x25, 0x6, 0x3, 0x55, 0x4, 0x3, 0xc, 0x1e, 0x59,
|
||||
0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x55, 0x32, 0x46, 0x20, 0x45, 0x45, 0x20, 0x53, 0x65,
|
||||
0x72, 0x69, 0x61, 0x6c, 0x20, 0x34, 0x31, 0x33, 0x39, 0x34, 0x33, 0x34, 0x38, 0x38, 0x30,
|
||||
0x59, 0x30, 0x13, 0x6, 0x7, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x2, 0x1, 0x6, 0x8, 0x2a, 0x86,
|
||||
0x48, 0xce, 0x3d, 0x3, 0x1, 0x7, 0x3, 0x42, 0x0, 0x4, 0x79, 0xea, 0x3b, 0x2c, 0x7c, 0x49,
|
||||
0x70, 0x10, 0x62, 0x23, 0xc, 0xd2, 0x3f, 0xeb, 0x60, 0xe5, 0x29, 0x31, 0x71, 0xd4, 0x83,
|
||||
0xf1, 0x0, 0xbe, 0x85, 0x9d, 0x6b, 0xf, 0x83, 0x97, 0x3, 0x1, 0xb5, 0x46, 0xcd, 0xd4, 0x6e,
|
||||
0xcf, 0xca, 0xe3, 0xe3, 0xf3, 0xf, 0x81, 0xe9, 0xed, 0x62, 0xbd, 0x26, 0x8d, 0x4c, 0x1e,
|
||||
0xbd, 0x37, 0xb3, 0xbc, 0xbe, 0x92, 0xa8, 0xc2, 0xae, 0xeb, 0x4e, 0x3a, 0xa3, 0x6c, 0x30,
|
||||
0x6a, 0x30, 0x22, 0x6, 0x9, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82, 0xc4, 0xa, 0x2, 0x4, 0x15,
|
||||
0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x31, 0x34,
|
||||
0x38, 0x32, 0x2e, 0x31, 0x2e, 0x37, 0x30, 0x13, 0x6, 0xb, 0x2b, 0x6, 0x1, 0x4, 0x1, 0x82,
|
||||
0xe5, 0x1c, 0x2, 0x1, 0x1, 0x4, 0x4, 0x3, 0x2, 0x5, 0x20, 0x30, 0x21, 0x6, 0xb, 0x2b, 0x6,
|
||||
0x1, 0x4, 0x1, 0x82, 0xe5, 0x1c, 0x1, 0x1, 0x4, 0x4, 0x12, 0x4, 0x10, 0xcb, 0x69, 0x48,
|
||||
0x1e, 0x8f, 0xf7, 0x40, 0x39, 0x93, 0xec, 0xa, 0x27, 0x29, 0xa1, 0x54, 0xa8, 0x30, 0xc,
|
||||
0x6, 0x3, 0x55, 0x1d, 0x13, 0x1, 0x1, 0xff, 0x4, 0x2, 0x30, 0x0, 0x30, 0xd, 0x6, 0x9, 0x2a,
|
||||
0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0xb, 0x5, 0x0, 0x3, 0x82, 0x1, 0x1, 0x0, 0x97, 0x9d,
|
||||
0x3, 0x97, 0xd8, 0x60, 0xf8, 0x2e, 0xe1, 0x5d, 0x31, 0x1c, 0x79, 0x6e, 0xba, 0xfb, 0x22,
|
||||
0xfa, 0xa7, 0xe0, 0x84, 0xd9, 0xba, 0xb4, 0xc6, 0x1b, 0xbb, 0x57, 0xf3, 0xe6, 0xb4, 0xc1,
|
||||
0x8a, 0x48, 0x37, 0xb8, 0x5c, 0x3c, 0x4e, 0xdb, 0xe4, 0x83, 0x43, 0xf4, 0xd6, 0xa5, 0xd9,
|
||||
0xb1, 0xce, 0xda, 0x8a, 0xe1, 0xfe, 0xd4, 0x91, 0x29, 0x21, 0x73, 0x5, 0x8e, 0x5e, 0xe1,
|
||||
0xcb, 0xdd, 0x6b, 0xda, 0xc0, 0x75, 0x57, 0xc6, 0xa0, 0xe8, 0xd3, 0x68, 0x25, 0xba, 0x15,
|
||||
0x9e, 0x7f, 0xb5, 0xad, 0x8c, 0xda, 0xf8, 0x4, 0x86, 0x8c, 0xf9, 0xe, 0x8f, 0x1f, 0x8a,
|
||||
0xea, 0x17, 0xc0, 0x16, 0xb5, 0x5c, 0x2a, 0x7a, 0xd4, 0x97, 0xc8, 0x94, 0xfb, 0x71, 0xd7,
|
||||
0x53, 0xd7, 0x9b, 0x9a, 0x48, 0x4b, 0x6c, 0x37, 0x6d, 0x72, 0x3b, 0x99, 0x8d, 0x2e, 0x1d,
|
||||
0x43, 0x6, 0xbf, 0x10, 0x33, 0xb5, 0xae, 0xf8, 0xcc, 0xa5, 0xcb, 0xb2, 0x56, 0x8b, 0x69,
|
||||
0x24, 0x22, 0x6d, 0x22, 0xa3, 0x58, 0xab, 0x7d, 0x87, 0xe4, 0xac, 0x5f, 0x2e, 0x9, 0x1a,
|
||||
0xa7, 0x15, 0x79, 0xf3, 0xa5, 0x69, 0x9, 0x49, 0x7d, 0x72, 0xf5, 0x4e, 0x6, 0xba, 0xc1,
|
||||
0xc3, 0xb4, 0x41, 0x3b, 0xba, 0x5e, 0xaf, 0x94, 0xc3, 0xb6, 0x4f, 0x34, 0xf9, 0xeb, 0xa4,
|
||||
0x1a, 0xcb, 0x6a, 0xe2, 0x83, 0x77, 0x6d, 0x36, 0x46, 0x53, 0x78, 0x48, 0xfe, 0xe8, 0x84,
|
||||
0xbd, 0xdd, 0xf5, 0xb1, 0xba, 0x57, 0x98, 0x54, 0xcf, 0xfd, 0xce, 0xba, 0xc3, 0x44, 0x5,
|
||||
0x95, 0x27, 0xe5, 0x6d, 0xd5, 0x98, 0xf8, 0xf5, 0x66, 0x71, 0x5a, 0xbe, 0x43, 0x1, 0xdd,
|
||||
0x19, 0x11, 0x30, 0xe6, 0xb9, 0xf0, 0xc6, 0x40, 0x39, 0x12, 0x53, 0xe2, 0x29, 0x80, 0x3f,
|
||||
0x3a, 0xef, 0x27, 0x4b, 0xed, 0xbf, 0xde, 0x3f, 0xcb, 0xbd, 0x42, 0xea, 0xd6, 0x79,
|
||||
];
|
||||
|
||||
const SAMPLE_AUTH_DATA_MAKE_CREDENTIAL: [u8; 164] = [
|
||||
0x58, 0xA2, // bytes(162)
|
||||
// authData
|
||||
0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84,
|
||||
0x27, // rp_id_hash
|
||||
0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a,
|
||||
0x87, // rp_id_hash
|
||||
0x05, 0x1d, // rp_id_hash
|
||||
0xC1, // authData Flags
|
||||
0x00, 0x00, 0x00, 0x0b, // authData counter
|
||||
0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
|
||||
0x7d, // AAGUID
|
||||
0x00, 0x10, // credential id length
|
||||
0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c,
|
||||
0x6f, // credential id
|
||||
// credential public key
|
||||
0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1,
|
||||
0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97,
|
||||
0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20,
|
||||
0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c,
|
||||
0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e,
|
||||
0xa6, 0x1c, // pub key end
|
||||
// Extensions
|
||||
0xA1, // map(1)
|
||||
0x6B, // text(11)
|
||||
0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
|
||||
0xF5, // true
|
||||
];
|
||||
|
||||
const SAMPLE_AUTH_DATA_GET_ASSERTION: [u8; 229] = [
|
||||
0x58, 0xE3, // bytes(227)
|
||||
// authData
|
||||
0xc2, 0x89, 0xc5, 0xca, 0x9b, 0x04, 0x60, 0xf9, 0x34, 0x6a, 0xb4, 0xe4, 0x2d, 0x84,
|
||||
0x27, // rp_id_hash
|
||||
0x43, 0x40, 0x4d, 0x31, 0xf4, 0x84, 0x68, 0x25, 0xa6, 0xd0, 0x65, 0xbe, 0x59, 0x7a,
|
||||
0x87, // rp_id_hash
|
||||
0x05, 0x1d, // rp_id_hash
|
||||
0xC1, // authData Flags
|
||||
0x00, 0x00, 0x00, 0x0b, // authData counter
|
||||
0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
|
||||
0x7d, // AAGUID
|
||||
0x00, 0x10, // credential id length
|
||||
0x89, 0x59, 0xce, 0xad, 0x5b, 0x5c, 0x48, 0x16, 0x4e, 0x8a, 0xbc, 0xd6, 0xd9, 0x43, 0x5c,
|
||||
0x6f, // credential id
|
||||
// credential public key
|
||||
0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0xa5, 0xfd, 0x5c, 0xe1, 0xb1,
|
||||
0xc4, 0x58, 0xc5, 0x30, 0xa5, 0x4f, 0xa6, 0x1b, 0x31, 0xbf, 0x6b, 0x04, 0xbe, 0x8b, 0x97,
|
||||
0xaf, 0xde, 0x54, 0xdd, 0x8c, 0xbb, 0x69, 0x27, 0x5a, 0x8a, 0x1b, 0xe1, 0x22, 0x58, 0x20,
|
||||
0xfa, 0x3a, 0x32, 0x31, 0xdd, 0x9d, 0xee, 0xd9, 0xd1, 0x89, 0x7b, 0xe5, 0xa6, 0x22, 0x8c,
|
||||
0x59, 0x50, 0x1e, 0x4b, 0xcd, 0x12, 0x97, 0x5d, 0x3d, 0xff, 0x73, 0x0f, 0x01, 0x27, 0x8e,
|
||||
0xa6, 0x1c, // pub key end
|
||||
// Extensions
|
||||
0xA1, // map(1)
|
||||
0x6B, // text(11)
|
||||
0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
|
||||
0x58, 0x40, // bytes(64)
|
||||
0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB, 0x87,
|
||||
0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22, 0x69, 0xAF,
|
||||
0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95, 0xA6, 0x8E, 0x79,
|
||||
0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58, 0x87, 0xD6, 0x29, 0x0F,
|
||||
0xD4, 0x7A, 0x40, 0xC4,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn parse_cert_chain() {
|
||||
let cert: AttestationCertificate = from_slice(&SAMPLE_CERT_CHAIN[1..]).unwrap();
|
||||
assert_eq!(&cert.0, &SAMPLE_CERT_CHAIN[4..]);
|
||||
|
||||
let _cert: Vec<AttestationCertificate> = from_slice(&SAMPLE_CERT_CHAIN).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_attestation_object() {
|
||||
let value: AttestationObject = from_slice(&SAMPLE_ATTESTATION).unwrap();
|
||||
println!("{:?}", value);
|
||||
|
||||
//assert_eq!(true, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_reader() {
|
||||
let v: Vec<u8> = vec![
|
||||
0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72, 0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72,
|
||||
];
|
||||
let (rest, value): (&[u8], String) = from_slice_stream(&v).unwrap();
|
||||
assert_eq!(value, "foobar");
|
||||
assert_eq!(rest, &[0x66, 0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]);
|
||||
let (rest, value): (&[u8], String) = from_slice_stream(rest).unwrap();
|
||||
assert_eq!(value, "foobar");
|
||||
assert!(rest.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_extensions() {
|
||||
let auth_make: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_MAKE_CREDENTIAL).unwrap();
|
||||
assert_eq!(
|
||||
auth_make.extensions.hmac_secret,
|
||||
Some(HmacSecretResponse::Confirmed(true))
|
||||
);
|
||||
let auth_get: AuthenticatorData = from_slice(&SAMPLE_AUTH_DATA_GET_ASSERTION).unwrap();
|
||||
assert_eq!(
|
||||
auth_get.extensions.hmac_secret,
|
||||
Some(HmacSecretResponse::Secret(vec![
|
||||
0x1F, 0x91, 0x52, 0x6C, 0xAE, 0x45, 0x6E, 0x4C, 0xBB, 0x71, 0xC4, 0xDD, 0xE7, 0xBB,
|
||||
0x87, 0x71, 0x57, 0xE6, 0xE5, 0x4D, 0xFE, 0xD3, 0x01, 0x5D, 0x7D, 0x4D, 0xBB, 0x22,
|
||||
0x69, 0xAF, 0xCD, 0xE6, 0xA9, 0x1B, 0x8D, 0x26, 0x7E, 0xBB, 0xF8, 0x48, 0xEB, 0x95,
|
||||
0xA6, 0x8E, 0x79, 0xC7, 0xAC, 0x70, 0x5E, 0x35, 0x1D, 0x54, 0x3D, 0xB0, 0x16, 0x58,
|
||||
0x87, 0xD6, 0x29, 0x0F, 0xD4, 0x7A, 0x40, 0xC4,
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
/// See: https://github.com/mozilla/authenticator-rs/issues/187
|
||||
#[test]
|
||||
fn test_aaguid_output() {
|
||||
let input = [
|
||||
0xcb, 0x69, 0x48, 0x1e, 0x8f, 0xf0, 0x00, 0x39, 0x93, 0xec, 0x0a, 0x27, 0x29, 0xa1,
|
||||
0x54, 0xa8,
|
||||
];
|
||||
let expected = "AAGuid(cb69481e-8ff0-0039-93ec-0a2729a154a8)";
|
||||
let result = AAGuid::from(&input).expect("Failed to parse AAGuid");
|
||||
let res_str = format!("{:?}", result);
|
||||
assert_eq!(expected, &res_str);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,399 @@
|
|||
use super::commands::CommandError;
|
||||
use crate::transport::errors::HIDError;
|
||||
use serde::de::{self, Deserializer, Error as SerdeError, MapAccess, Visitor};
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde_json as json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt;
|
||||
|
||||
/// https://w3c.github.io/webauthn/#dom-collectedclientdata-tokenbinding
|
||||
// tokenBinding, of type TokenBinding
|
||||
//
|
||||
// This OPTIONAL member contains information about the state of the Token
|
||||
// Binding protocol [TokenBinding] used when communicating with the Relying
|
||||
// Party. Its absence indicates that the client doesn’t support token
|
||||
// binding.
|
||||
//
|
||||
// status, of type TokenBindingStatus
|
||||
//
|
||||
// This member is one of the following:
|
||||
//
|
||||
// supported
|
||||
//
|
||||
// Indicates the client supports token binding, but it was not
|
||||
// negotiated when communicating with the Relying Party.
|
||||
//
|
||||
// present
|
||||
//
|
||||
// Indicates token binding was used when communicating with the
|
||||
// Relying Party. In this case, the id member MUST be present.
|
||||
//
|
||||
// id, of type DOMString
|
||||
//
|
||||
// This member MUST be present if status is present, and MUST be a
|
||||
// base64url encoding of the Token Binding ID that was used when
|
||||
// communicating with the Relying Party.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TokenBinding {
|
||||
Present(String),
|
||||
Supported,
|
||||
}
|
||||
|
||||
impl Serialize for TokenBinding {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
match *self {
|
||||
TokenBinding::Supported => {
|
||||
map.serialize_entry(&"status", &"supported")?;
|
||||
}
|
||||
TokenBinding::Present(ref v) => {
|
||||
map.serialize_entry(&"status", "present")?;
|
||||
// Verify here, that `v` is valid base64 encoded?
|
||||
// base64::decode_config(&v, base64::URL_SAFE_NO_PAD);
|
||||
// For now: Let the token do that.
|
||||
map.serialize_entry(&"id", &v)?;
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TokenBinding {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct TokenBindingVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for TokenBindingVisitor {
|
||||
type Value = TokenBinding;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a byte string")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut id = None;
|
||||
let mut status = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
"status" => {
|
||||
status = Some(map.next_value()?);
|
||||
}
|
||||
"id" => {
|
||||
id = Some(map.next_value()?);
|
||||
}
|
||||
k => {
|
||||
return Err(M::Error::custom(format!("unexpected key: {:?}", k)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(stat) = status {
|
||||
match stat {
|
||||
"present" => {
|
||||
if let Some(id) = id {
|
||||
Ok(TokenBinding::Present(id))
|
||||
} else {
|
||||
Err(SerdeError::missing_field("id"))
|
||||
}
|
||||
}
|
||||
"supported" => Ok(TokenBinding::Supported),
|
||||
k => {
|
||||
return Err(M::Error::custom(format!(
|
||||
"unexpected status key: {:?}",
|
||||
k
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(SerdeError::missing_field("status"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(TokenBindingVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// https://w3c.github.io/webauthn/#dom-collectedclientdata-type
|
||||
// type, of type DOMString
|
||||
//
|
||||
// This member contains the string "webauthn.create" when creating new
|
||||
// credentials, and "webauthn.get" when getting an assertion from an
|
||||
// existing credential. The purpose of this member is to prevent certain
|
||||
// types of signature confusion attacks (where an attacker substitutes one
|
||||
// legitimate signature for another).
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum WebauthnType {
|
||||
Create,
|
||||
Get,
|
||||
}
|
||||
|
||||
impl Serialize for WebauthnType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match *self {
|
||||
WebauthnType::Create => serializer.serialize_str(&"webauthn.create"),
|
||||
WebauthnType::Get => serializer.serialize_str(&"webauthn.get"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for WebauthnType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct WebauthnTypeVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for WebauthnTypeVisitor {
|
||||
type Value = WebauthnType;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match v {
|
||||
"webauthn.create" => Ok(WebauthnType::Create),
|
||||
"webauthn.get" => Ok(WebauthnType::Get),
|
||||
_ => Err(E::custom("unexpected webauthn_type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(WebauthnTypeVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Challenge(pub String);
|
||||
|
||||
impl Challenge {
|
||||
pub fn new(input: Vec<u8>) -> Self {
|
||||
let value = base64::encode_config(&input, base64::URL_SAFE_NO_PAD);
|
||||
Challenge(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Challenge {
|
||||
fn from(v: Vec<u8>) -> Challenge {
|
||||
Challenge::new(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Challenge {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
pub type Origin = String;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct CollectedClientData {
|
||||
#[serde(rename = "type")]
|
||||
pub webauthn_type: WebauthnType,
|
||||
pub challenge: Challenge,
|
||||
pub origin: Origin,
|
||||
// It is optional, according to https://www.w3.org/TR/webauthn/#collectedclientdata-hash-of-the-serialized-client-data
|
||||
// But we are serializing it, so we *have to* set crossOrigin (if not given, we have to set it to false)
|
||||
// Thus, on our side, it is not optional. For deserializing, we provide a default (bool's default == False)
|
||||
#[serde(rename = "crossOrigin", default)]
|
||||
pub cross_origin: bool,
|
||||
#[serde(rename = "tokenBinding", skip_serializing_if = "Option::is_none")]
|
||||
pub token_binding: Option<TokenBinding>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct ClientDataHash([u8; 32]);
|
||||
|
||||
impl PartialEq<[u8]> for ClientDataHash {
|
||||
fn eq(&self, other: &[u8]) -> bool {
|
||||
self.0.eq(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for ClientDataHash {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ClientDataHash {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl<'de> Deserialize<'de> for ClientDataHash {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ClientDataHashVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ClientDataHashVisitor {
|
||||
type Value = ClientDataHash;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a byte string")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let mut out = [0u8; 32];
|
||||
if out.len() != v.len() {
|
||||
return Err(E::invalid_length(v.len(), &"32"));
|
||||
}
|
||||
out.copy_from_slice(v);
|
||||
Ok(ClientDataHash(out))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(ClientDataHashVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CollectedClientDataWrapper {
|
||||
pub client_data: CollectedClientData,
|
||||
pub serialized_data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl CollectedClientDataWrapper {
|
||||
pub fn new(client_data: CollectedClientData) -> Result<Self, HIDError> {
|
||||
let serialized_data = json::to_vec(&client_data).map_err(CommandError::Json)?;
|
||||
Ok(CollectedClientDataWrapper {
|
||||
client_data,
|
||||
serialized_data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> ClientDataHash {
|
||||
// WebIDL's dictionary definition specifies that the order of the struct
|
||||
// is exactly as the WebIDL specification declares it, with an algorithm
|
||||
// for partial dictionaries, so that's how interop works for these
|
||||
// things.
|
||||
// See: https://heycam.github.io/webidl/#dfn-dictionary
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.serialized_data);
|
||||
|
||||
let mut output = [0u8; 32];
|
||||
output.copy_from_slice(hasher.finalize().as_slice());
|
||||
|
||||
ClientDataHash(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::CollectedClientDataWrapper;
|
||||
|
||||
use super::{Challenge, ClientDataHash, CollectedClientData, TokenBinding, WebauthnType};
|
||||
use serde_json as json;
|
||||
|
||||
#[test]
|
||||
fn test_token_binding_status() {
|
||||
let tok = TokenBinding::Present("AAECAw".to_string());
|
||||
|
||||
let json_value = json::to_string(&tok).unwrap();
|
||||
assert_eq!(json_value, "{\"status\":\"present\",\"id\":\"AAECAw\"}");
|
||||
|
||||
let tok = TokenBinding::Supported;
|
||||
|
||||
let json_value = json::to_string(&tok).unwrap();
|
||||
assert_eq!(json_value, "{\"status\":\"supported\"}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webauthn_type() {
|
||||
let t = WebauthnType::Create;
|
||||
|
||||
let json_value = json::to_string(&t).unwrap();
|
||||
assert_eq!(json_value, "\"webauthn.create\"");
|
||||
|
||||
let t = WebauthnType::Get;
|
||||
let json_value = json::to_string(&t).unwrap();
|
||||
assert_eq!(json_value, "\"webauthn.get\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collected_client_data_parsing() {
|
||||
let original_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
|
||||
let parsed: CollectedClientData = serde_json::from_str(&original_str).unwrap();
|
||||
let expected = CollectedClientData {
|
||||
webauthn_type: WebauthnType::Create,
|
||||
challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
|
||||
origin: String::from("example.com"),
|
||||
cross_origin: false,
|
||||
token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
|
||||
};
|
||||
assert_eq!(parsed, expected);
|
||||
|
||||
let back_again = serde_json::to_string(&expected).unwrap();
|
||||
assert_eq!(back_again, original_str);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collected_client_data_defaults() {
|
||||
let cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"crossOrigin\":false,\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
|
||||
let no_cross_origin_str = "{\"type\":\"webauthn.create\",\"challenge\":\"AAECAw\",\"origin\":\"example.com\",\"tokenBinding\":{\"status\":\"present\",\"id\":\"AAECAw\"}}";
|
||||
let parsed: CollectedClientData = serde_json::from_str(&no_cross_origin_str).unwrap();
|
||||
let expected = CollectedClientData {
|
||||
webauthn_type: WebauthnType::Create,
|
||||
challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
|
||||
origin: String::from("example.com"),
|
||||
cross_origin: false,
|
||||
token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
|
||||
};
|
||||
assert_eq!(parsed, expected);
|
||||
|
||||
let back_again = serde_json::to_string(&expected).unwrap();
|
||||
assert_eq!(back_again, cross_origin_str);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collected_client_data() {
|
||||
let client_data = CollectedClientData {
|
||||
webauthn_type: WebauthnType::Create,
|
||||
challenge: Challenge::new(vec![0x00, 0x01, 0x02, 0x03]),
|
||||
origin: String::from("example.com"),
|
||||
cross_origin: false,
|
||||
token_binding: Some(TokenBinding::Present("AAECAw".to_string())),
|
||||
};
|
||||
let c =
|
||||
CollectedClientDataWrapper::new(client_data).expect("Failed to serialize client_data");
|
||||
assert_eq!(
|
||||
c.hash(),
|
||||
// echo -n '{"type":"webauthn.create","challenge":"AAECAw","origin":"example.com","crossOrigin":false,"tokenBinding":{"status":"present","id":"AAECAw"}}' | sha256sum -t
|
||||
ClientDataHash {
|
||||
0: [
|
||||
0x75, 0x35, 0x35, 0x7d, 0x49, 0x6e, 0x33, 0xc8, 0x18, 0x7f, 0xea, 0x8d, 0x11,
|
||||
0x32, 0x64, 0xaa, 0xa4, 0x52, 0x3e, 0x13, 0x40, 0x14, 0x9f, 0xbe, 0x00, 0x3f,
|
||||
0x10, 0x87, 0x54, 0xc3, 0x2d, 0x80
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,784 @@
|
|||
use super::{get_info::AuthenticatorInfo, Command, CommandError, RequestCtap2, StatusCode};
|
||||
use crate::crypto::{
|
||||
authenticate, decrypt, encapsulate, encrypt, BackendError, COSEKey, CryptoError, ECDHSecret,
|
||||
};
|
||||
use crate::transport::errors::HIDError;
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use serde::{
|
||||
de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor},
|
||||
ser::SerializeMap,
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use serde_bytes::ByteBuf;
|
||||
use serde_cbor::de::from_slice;
|
||||
use serde_cbor::ser::to_vec;
|
||||
use serde_cbor::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::error::Error as StdErrorT;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum PINSubcommand {
|
||||
GetRetries = 0x01,
|
||||
GetKeyAgreement = 0x02,
|
||||
SetPIN = 0x03,
|
||||
ChangePIN = 0x04,
|
||||
GetPINToken = 0x05, // superseded by GetPinUvAuth*
|
||||
GetPinUvAuthTokenUsingUvWithPermissions = 0x06,
|
||||
GetUVRetries = 0x07,
|
||||
GetPinUvAuthTokenUsingPinWithPermissions = 0x09, // Yes, 0x08 is missing
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum PinUvAuthTokenPermission {
|
||||
MakeCredential = 0x01, // rp_id required
|
||||
GetAssertion = 0x02, // rp_id required
|
||||
CredentialManagement = 0x04, // rp_id optional
|
||||
BioEnrollment = 0x08, // rp_id ignored
|
||||
LargeBlobWrite = 0x10, // rp_id ignored
|
||||
AuthenticatorConfiguration = 0x20, // rp_id ignored
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientPIN {
|
||||
pin_protocol: Option<u8>,
|
||||
subcommand: PINSubcommand,
|
||||
key_agreement: Option<COSEKey>,
|
||||
pin_auth: Option<PinAuth>,
|
||||
new_pin_enc: Option<ByteBuf>,
|
||||
pin_hash_enc: Option<ByteBuf>,
|
||||
permissions: Option<u8>,
|
||||
rp_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ClientPIN {
|
||||
fn default() -> Self {
|
||||
ClientPIN {
|
||||
pin_protocol: None,
|
||||
subcommand: PINSubcommand::GetRetries,
|
||||
key_agreement: None,
|
||||
pin_auth: None,
|
||||
new_pin_enc: None,
|
||||
pin_hash_enc: None,
|
||||
permissions: None,
|
||||
rp_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ClientPIN {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Need to define how many elements are going to be in the map
|
||||
// beforehand
|
||||
let mut map_len = 1;
|
||||
if self.pin_protocol.is_some() {
|
||||
map_len += 1;
|
||||
}
|
||||
if self.key_agreement.is_some() {
|
||||
map_len += 1;
|
||||
}
|
||||
if self.pin_auth.is_some() {
|
||||
map_len += 1;
|
||||
}
|
||||
if self.new_pin_enc.is_some() {
|
||||
map_len += 1;
|
||||
}
|
||||
if self.pin_hash_enc.is_some() {
|
||||
map_len += 1;
|
||||
}
|
||||
if self.permissions.is_some() {
|
||||
map_len += 1;
|
||||
}
|
||||
if self.rp_id.is_some() {
|
||||
map_len += 1;
|
||||
}
|
||||
|
||||
let mut map = serializer.serialize_map(Some(map_len))?;
|
||||
if let Some(ref pin_protocol) = self.pin_protocol {
|
||||
map.serialize_entry(&1, pin_protocol)?;
|
||||
}
|
||||
let command: u8 = self.subcommand as u8;
|
||||
map.serialize_entry(&2, &command)?;
|
||||
if let Some(ref key_agreement) = self.key_agreement {
|
||||
map.serialize_entry(&3, key_agreement)?;
|
||||
}
|
||||
if let Some(ref pin_auth) = self.pin_auth {
|
||||
map.serialize_entry(&4, &ByteBuf::from(pin_auth.as_ref()))?;
|
||||
}
|
||||
if let Some(ref new_pin_enc) = self.new_pin_enc {
|
||||
map.serialize_entry(&5, new_pin_enc)?;
|
||||
}
|
||||
if let Some(ref pin_hash_enc) = self.pin_hash_enc {
|
||||
map.serialize_entry(&6, pin_hash_enc)?;
|
||||
}
|
||||
if let Some(ref permissions) = self.permissions {
|
||||
map.serialize_entry(&9, permissions)?;
|
||||
}
|
||||
if let Some(ref rp_id) = self.rp_id {
|
||||
map.serialize_entry(&0x0A, rp_id)?;
|
||||
}
|
||||
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ClientPINSubCommand {
|
||||
type Output;
|
||||
fn as_client_pin(&self) -> Result<ClientPIN, CommandError>;
|
||||
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError>;
|
||||
}
|
||||
|
||||
struct ClientPinResponse {
|
||||
key_agreement: Option<COSEKey>,
|
||||
pin_token: Option<EncryptedPinToken>,
|
||||
/// Number of PIN attempts remaining before lockout.
|
||||
pin_retries: Option<u8>,
|
||||
power_cycle_state: Option<bool>,
|
||||
uv_retries: Option<u8>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ClientPinResponse {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ClientPinResponseVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ClientPinResponseVisitor {
|
||||
type Value = ClientPinResponse;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a map")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut key_agreement = None;
|
||||
let mut pin_token = None;
|
||||
let mut pin_retries = None;
|
||||
let mut power_cycle_state = None;
|
||||
let mut uv_retries = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
0x01 => {
|
||||
if key_agreement.is_some() {
|
||||
return Err(SerdeError::duplicate_field("key_agreement"));
|
||||
}
|
||||
key_agreement = map.next_value()?;
|
||||
}
|
||||
0x02 => {
|
||||
if pin_token.is_some() {
|
||||
return Err(SerdeError::duplicate_field("pin_token"));
|
||||
}
|
||||
pin_token = map.next_value()?;
|
||||
}
|
||||
0x03 => {
|
||||
if pin_retries.is_some() {
|
||||
return Err(SerdeError::duplicate_field("pin_retries"));
|
||||
}
|
||||
pin_retries = Some(map.next_value()?);
|
||||
}
|
||||
0x04 => {
|
||||
if power_cycle_state.is_some() {
|
||||
return Err(SerdeError::duplicate_field("power_cycle_state"));
|
||||
}
|
||||
power_cycle_state = Some(map.next_value()?);
|
||||
}
|
||||
0x05 => {
|
||||
if uv_retries.is_some() {
|
||||
return Err(SerdeError::duplicate_field("uv_retries"));
|
||||
}
|
||||
uv_retries = Some(map.next_value()?);
|
||||
}
|
||||
k => {
|
||||
warn!("ClientPinResponse: unexpected key: {:?}", k);
|
||||
let _ = map.next_value::<IgnoredAny>()?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ClientPinResponse {
|
||||
key_agreement,
|
||||
pin_token,
|
||||
pin_retries,
|
||||
power_cycle_state,
|
||||
uv_retries,
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_bytes(ClientPinResponseVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GetKeyAgreement {
|
||||
pin_protocol: u8,
|
||||
}
|
||||
|
||||
impl GetKeyAgreement {
|
||||
pub fn new(info: &AuthenticatorInfo) -> Result<Self, CommandError> {
|
||||
if info.pin_protocols.contains(&1) {
|
||||
Ok(GetKeyAgreement { pin_protocol: 1 })
|
||||
} else {
|
||||
Err(CommandError::UnsupportedPinProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientPINSubCommand for GetKeyAgreement {
|
||||
type Output = KeyAgreement;
|
||||
|
||||
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
|
||||
Ok(ClientPIN {
|
||||
pin_protocol: Some(self.pin_protocol),
|
||||
subcommand: PINSubcommand::GetKeyAgreement,
|
||||
..ClientPIN::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
|
||||
let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
debug!("GetKeyAgreement::parse_response_payload {:?}", value);
|
||||
|
||||
let get_pin_response: ClientPinResponse =
|
||||
from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
if let Some(key_agreement) = get_pin_response.key_agreement {
|
||||
Ok(KeyAgreement(key_agreement))
|
||||
} else {
|
||||
Err(CommandError::MissingRequiredField("key_agreement"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Superseded by GetPinUvAuthTokenUsingUvWithPermissions or GetPinUvAuthTokenUsingPinWithPermissions,
|
||||
/// thus for backwards compatibility only
|
||||
pub struct GetPinToken<'sc, 'pin> {
|
||||
pin_protocol: u8,
|
||||
shared_secret: &'sc ECDHSecret,
|
||||
pin: &'pin Pin,
|
||||
}
|
||||
|
||||
impl<'sc, 'pin> GetPinToken<'sc, 'pin> {
|
||||
pub fn new(
|
||||
info: &AuthenticatorInfo,
|
||||
shared_secret: &'sc ECDHSecret,
|
||||
pin: &'pin Pin,
|
||||
) -> Result<Self, CommandError> {
|
||||
if info.pin_protocols.contains(&1) {
|
||||
Ok(GetPinToken {
|
||||
pin_protocol: 1,
|
||||
shared_secret,
|
||||
pin,
|
||||
})
|
||||
} else {
|
||||
Err(CommandError::UnsupportedPinProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'sc, 'pin> ClientPINSubCommand for GetPinToken<'sc, 'pin> {
|
||||
type Output = PinToken;
|
||||
|
||||
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
|
||||
let input = self.pin.for_pin_token();
|
||||
trace!("pin_hash = {:#04X?}", &input.as_ref());
|
||||
let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref())
|
||||
.map_err(|e| CryptoError::Backend(e))?;
|
||||
trace!("pin_hash_enc = {:#04X?}", &pin_hash_enc);
|
||||
|
||||
Ok(ClientPIN {
|
||||
pin_protocol: Some(self.pin_protocol),
|
||||
subcommand: PINSubcommand::GetPINToken,
|
||||
key_agreement: Some(self.shared_secret.my_public_key().clone()),
|
||||
pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)),
|
||||
..ClientPIN::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
|
||||
let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
debug!("GetKeyAgreement::parse_response_payload {:?}", value);
|
||||
|
||||
let get_pin_response: ClientPinResponse =
|
||||
from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
match get_pin_response.pin_token {
|
||||
Some(encrypted_pin_token) => {
|
||||
let pin_token = decrypt(
|
||||
self.shared_secret.shared_secret(),
|
||||
encrypted_pin_token.as_ref(),
|
||||
)
|
||||
.map_err(|e| CryptoError::Backend(e))?;
|
||||
let pin_token = PinToken(pin_token);
|
||||
Ok(pin_token)
|
||||
}
|
||||
None => Err(CommandError::MissingRequiredField("key_agreement")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
|
||||
pin_protocol: u8,
|
||||
shared_secret: &'sc ECDHSecret,
|
||||
pin: &'pin Pin,
|
||||
permissions: PinUvAuthTokenPermission,
|
||||
rp_id: Option<String>,
|
||||
}
|
||||
|
||||
impl<'sc, 'pin> GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
|
||||
pub fn new(
|
||||
info: &AuthenticatorInfo,
|
||||
shared_secret: &'sc ECDHSecret,
|
||||
pin: &'pin Pin,
|
||||
permissions: PinUvAuthTokenPermission,
|
||||
rp_id: Option<String>,
|
||||
) -> Result<Self, CommandError> {
|
||||
// TODO(MS): Actually handle protocol 2!
|
||||
if info.pin_protocols.contains(&1) {
|
||||
Ok(GetPinUvAuthTokenUsingPinWithPermissions {
|
||||
pin_protocol: 1,
|
||||
shared_secret,
|
||||
pin,
|
||||
permissions,
|
||||
rp_id,
|
||||
})
|
||||
} else {
|
||||
Err(CommandError::UnsupportedPinProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'sc, 'pin> ClientPINSubCommand for GetPinUvAuthTokenUsingPinWithPermissions<'sc, 'pin> {
|
||||
type Output = PinToken;
|
||||
|
||||
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
|
||||
let input = self.pin.for_pin_token();
|
||||
let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref())
|
||||
.map_err(|e| CryptoError::Backend(e))?;
|
||||
|
||||
Ok(ClientPIN {
|
||||
pin_protocol: Some(self.pin_protocol),
|
||||
subcommand: PINSubcommand::GetPinUvAuthTokenUsingPinWithPermissions,
|
||||
key_agreement: Some(self.shared_secret.my_public_key().clone()),
|
||||
pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)),
|
||||
permissions: Some(self.permissions as u8),
|
||||
rp_id: self.rp_id.clone(), // TODO: This could probably be done less wasteful with &str all the way
|
||||
..ClientPIN::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
|
||||
let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
debug!("GetKeyAgreement::parse_response_payload {:?}", value);
|
||||
|
||||
let get_pin_response: ClientPinResponse =
|
||||
from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
match get_pin_response.pin_token {
|
||||
Some(encrypted_pin_token) => {
|
||||
let pin_token = decrypt(
|
||||
self.shared_secret.shared_secret(),
|
||||
encrypted_pin_token.as_ref(),
|
||||
)
|
||||
.map_err(|e| CryptoError::Backend(e))?;
|
||||
let pin_token = PinToken(pin_token);
|
||||
Ok(pin_token)
|
||||
}
|
||||
None => Err(CommandError::MissingRequiredField("key_agreement")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GetRetries {}
|
||||
|
||||
impl GetRetries {
|
||||
pub fn new() -> Self {
|
||||
GetRetries {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientPINSubCommand for GetRetries {
|
||||
type Output = u8;
|
||||
|
||||
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
|
||||
Ok(ClientPIN {
|
||||
subcommand: PINSubcommand::GetRetries,
|
||||
..ClientPIN::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
|
||||
let value: Value = from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
debug!("GetKeyAgreement::parse_response_payload {:?}", value);
|
||||
|
||||
let get_pin_response: ClientPinResponse =
|
||||
from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
match get_pin_response.pin_retries {
|
||||
Some(pin_retries) => Ok(pin_retries),
|
||||
None => Err(CommandError::MissingRequiredField("pin_retries")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SetNewPin<'sc, 'pin> {
|
||||
pin_protocol: u8,
|
||||
shared_secret: &'sc ECDHSecret,
|
||||
new_pin: &'pin Pin,
|
||||
}
|
||||
|
||||
impl<'sc, 'pin> SetNewPin<'sc, 'pin> {
|
||||
pub fn new(
|
||||
info: &AuthenticatorInfo,
|
||||
shared_secret: &'sc ECDHSecret,
|
||||
new_pin: &'pin Pin,
|
||||
) -> Result<Self, CommandError> {
|
||||
if info.pin_protocols.contains(&1) {
|
||||
Ok(SetNewPin {
|
||||
pin_protocol: 1,
|
||||
shared_secret,
|
||||
new_pin,
|
||||
})
|
||||
} else {
|
||||
Err(CommandError::UnsupportedPinProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'sc, 'pin> ClientPINSubCommand for SetNewPin<'sc, 'pin> {
|
||||
type Output = ();
|
||||
|
||||
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
|
||||
if self.new_pin.as_bytes().len() > 63 {
|
||||
return Err(CommandError::StatusCode(
|
||||
StatusCode::PinPolicyViolation,
|
||||
None,
|
||||
));
|
||||
}
|
||||
// Padding the PIN with trailing zeros, according to spec
|
||||
let input: Vec<u8> = self
|
||||
.new_pin
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.chain(std::iter::repeat(&0x00))
|
||||
.take(64)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let shared_secret = self.shared_secret.shared_secret();
|
||||
// AES256-CBC(sharedSecret, IV=0, newPin)
|
||||
let new_pin_enc =
|
||||
encrypt(shared_secret, input.as_ref()).map_err(|e| CryptoError::Backend(e))?;
|
||||
|
||||
// LEFT(HMAC-SHA-265(sharedSecret, newPinEnc), 16)
|
||||
let pin_auth = PinToken(shared_secret.to_vec())
|
||||
.auth(&new_pin_enc)
|
||||
.map_err(CommandError::Crypto)?;
|
||||
|
||||
Ok(ClientPIN {
|
||||
pin_protocol: Some(self.pin_protocol),
|
||||
subcommand: PINSubcommand::SetPIN,
|
||||
key_agreement: Some(self.shared_secret.my_public_key().clone()),
|
||||
new_pin_enc: Some(ByteBuf::from(new_pin_enc)),
|
||||
pin_auth: Some(pin_auth),
|
||||
..ClientPIN::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
|
||||
// Should be an empty response or a valid cbor-value (which we ignore)
|
||||
if input.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
let _: Value = from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChangeExistingPin<'sc, 'pin> {
|
||||
pin_protocol: u8,
|
||||
shared_secret: &'sc ECDHSecret,
|
||||
current_pin: &'pin Pin,
|
||||
new_pin: &'pin Pin,
|
||||
}
|
||||
|
||||
impl<'sc, 'pin> ChangeExistingPin<'sc, 'pin> {
|
||||
pub fn new(
|
||||
info: &AuthenticatorInfo,
|
||||
shared_secret: &'sc ECDHSecret,
|
||||
current_pin: &'pin Pin,
|
||||
new_pin: &'pin Pin,
|
||||
) -> Result<Self, CommandError> {
|
||||
if info.pin_protocols.contains(&1) {
|
||||
Ok(ChangeExistingPin {
|
||||
pin_protocol: 1,
|
||||
shared_secret,
|
||||
current_pin,
|
||||
new_pin,
|
||||
})
|
||||
} else {
|
||||
Err(CommandError::UnsupportedPinProtocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'sc, 'pin> ClientPINSubCommand for ChangeExistingPin<'sc, 'pin> {
|
||||
type Output = ();
|
||||
|
||||
fn as_client_pin(&self) -> Result<ClientPIN, CommandError> {
|
||||
if self.new_pin.as_bytes().len() > 63 {
|
||||
return Err(CommandError::StatusCode(
|
||||
StatusCode::PinPolicyViolation,
|
||||
None,
|
||||
));
|
||||
}
|
||||
// Padding the PIN with trailing zeros, according to spec
|
||||
let input: Vec<u8> = self
|
||||
.new_pin
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.chain(std::iter::repeat(&0x00))
|
||||
.take(64)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let shared_secret = self.shared_secret.shared_secret();
|
||||
// AES256-CBC(sharedSecret, IV=0, newPin)
|
||||
let new_pin_enc =
|
||||
encrypt(shared_secret, input.as_ref()).map_err(|e| CryptoError::Backend(e))?;
|
||||
|
||||
// AES256-CBC(sharedSecret, IV=0, LEFT(SHA-256(oldPin), 16))
|
||||
let input = self.current_pin.for_pin_token();
|
||||
let pin_hash_enc = encrypt(self.shared_secret.shared_secret(), input.as_ref())
|
||||
.map_err(|e| CryptoError::Backend(e))?;
|
||||
|
||||
// LEFT(HMAC-SHA-265(sharedSecret, newPinEnc), 16)
|
||||
let pin_auth = PinToken(shared_secret.to_vec())
|
||||
.auth(&[new_pin_enc.as_slice(), pin_hash_enc.as_slice()].concat())
|
||||
.map_err(CommandError::Crypto)?;
|
||||
|
||||
Ok(ClientPIN {
|
||||
pin_protocol: Some(self.pin_protocol),
|
||||
subcommand: PINSubcommand::ChangePIN,
|
||||
key_agreement: Some(self.shared_secret.my_public_key().clone()),
|
||||
new_pin_enc: Some(ByteBuf::from(new_pin_enc)),
|
||||
pin_hash_enc: Some(ByteBuf::from(pin_hash_enc)),
|
||||
pin_auth: Some(pin_auth),
|
||||
permissions: None,
|
||||
rp_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_response_payload(&self, input: &[u8]) -> Result<Self::Output, CommandError> {
|
||||
// Should be an empty response or a valid cbor-value (which we ignore)
|
||||
if input.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
let _: Value = from_slice(input).map_err(CommandError::Deserializing)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RequestCtap2 for T
|
||||
where
|
||||
T: ClientPINSubCommand,
|
||||
T: fmt::Debug,
|
||||
{
|
||||
type Output = <T as ClientPINSubCommand>::Output;
|
||||
|
||||
fn command() -> Command {
|
||||
Command::ClientPin
|
||||
}
|
||||
|
||||
fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
let client_pin = self.as_client_pin()?;
|
||||
let output = to_vec(&client_pin).map_err(CommandError::Serializing)?;
|
||||
trace!("client subcommmand: {:04X?}", &output);
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn handle_response_ctap2<Dev>(
|
||||
&self,
|
||||
_dev: &mut Dev,
|
||||
input: &[u8],
|
||||
) -> Result<Self::Output, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
trace!("Client pin subcomand response:{:04X?}", &input);
|
||||
if input.is_empty() {
|
||||
return Err(CommandError::InputTooSmall.into());
|
||||
}
|
||||
|
||||
let status: StatusCode = input[0].into();
|
||||
debug!("response status code: {:?}", status);
|
||||
if status.is_ok() {
|
||||
let res = <T as ClientPINSubCommand>::parse_response_payload(self, &input[1..])
|
||||
.map_err(HIDError::Command);
|
||||
res
|
||||
} else {
|
||||
let add_data = if input.len() > 1 {
|
||||
let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Err(CommandError::StatusCode(status, add_data).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeyAgreement(COSEKey);
|
||||
|
||||
impl KeyAgreement {
|
||||
pub fn shared_secret(&self) -> Result<ECDHSecret, CommandError> {
|
||||
encapsulate(&self.0).map_err(|e| CommandError::Crypto(CryptoError::Backend(e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EncryptedPinToken(ByteBuf);
|
||||
|
||||
impl AsRef<[u8]> for EncryptedPinToken {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PinToken(Vec<u8>);
|
||||
|
||||
impl PinToken {
|
||||
pub fn auth(&self, payload: &[u8]) -> Result<PinAuth, CryptoError> {
|
||||
let hmac = authenticate(self.as_ref(), payload)?;
|
||||
|
||||
let mut out = [0u8; 16];
|
||||
out.copy_from_slice(&hmac[0..16]);
|
||||
|
||||
Ok(PinAuth(out.to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PinToken {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Deserialize))]
|
||||
pub struct PinAuth(Vec<u8>);
|
||||
|
||||
impl PinAuth {
|
||||
pub(crate) fn empty_pin_auth() -> Self {
|
||||
PinAuth(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PinAuth {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PinAuth {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serde_bytes::serialize(&self.0[..], serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Pin(String);
|
||||
|
||||
impl fmt::Debug for Pin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Pin(redacted)")
|
||||
}
|
||||
}
|
||||
|
||||
impl Pin {
|
||||
pub fn new(value: &str) -> Pin {
|
||||
Pin(String::from(value))
|
||||
}
|
||||
|
||||
pub fn for_pin_token(&self) -> PinAuth {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.0.as_bytes());
|
||||
|
||||
let mut output = [0u8; 16];
|
||||
let len = output.len();
|
||||
output.copy_from_slice(&hasher.finalize().as_slice()[..len]);
|
||||
|
||||
PinAuth(output.to_vec())
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub enum PinError {
|
||||
PinRequired,
|
||||
PinIsTooShort,
|
||||
PinIsTooLong(usize),
|
||||
InvalidKeyLen,
|
||||
InvalidPin(Option<u8>),
|
||||
PinAuthBlocked,
|
||||
PinBlocked,
|
||||
Backend(BackendError),
|
||||
}
|
||||
|
||||
impl fmt::Display for PinError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
PinError::PinRequired => write!(f, "PinError: Pin required."),
|
||||
PinError::PinIsTooShort => write!(f, "PinError: pin is too short"),
|
||||
PinError::PinIsTooLong(len) => write!(f, "PinError: pin is too long ({})", len),
|
||||
PinError::InvalidKeyLen => write!(f, "PinError: invalid key len"),
|
||||
PinError::InvalidPin(ref e) => {
|
||||
let mut res = write!(f, "PinError: Invalid Pin.");
|
||||
if let Some(pin_retries) = e {
|
||||
res = write!(f, " Retries left: {:?}", pin_retries)
|
||||
}
|
||||
res
|
||||
}
|
||||
PinError::PinAuthBlocked => write!(
|
||||
f,
|
||||
"PinError: Pin authentication blocked. Device needs power cycle."
|
||||
),
|
||||
PinError::PinBlocked => write!(
|
||||
f,
|
||||
"PinError: No retries left. Pin blocked. Device needs reset."
|
||||
),
|
||||
PinError::Backend(ref e) => write!(f, "PinError: Crypto backend error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdErrorT for PinError {}
|
||||
|
||||
impl From<BackendError> for PinError {
|
||||
fn from(e: BackendError) -> Self {
|
||||
PinError::Backend(e)
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,733 @@
|
|||
use super::{Command, CommandError, RequestCtap2, StatusCode};
|
||||
use crate::ctap2::attestation::AAGuid;
|
||||
use crate::ctap2::server::PublicKeyCredentialParameters;
|
||||
use crate::transport::errors::HIDError;
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use serde::{
|
||||
de::{Error as SError, IgnoredAny, MapAccess, Visitor},
|
||||
Deserialize, Deserializer,
|
||||
};
|
||||
use serde_cbor::{de::from_slice, Value};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GetInfo {}
|
||||
|
||||
impl Default for GetInfo {
|
||||
fn default() -> GetInfo {
|
||||
GetInfo {}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestCtap2 for GetInfo {
|
||||
type Output = AuthenticatorInfo;
|
||||
|
||||
fn command() -> Command {
|
||||
Command::GetInfo
|
||||
}
|
||||
|
||||
fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn handle_response_ctap2<Dev>(
|
||||
&self,
|
||||
_dev: &mut Dev,
|
||||
input: &[u8],
|
||||
) -> Result<Self::Output, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
if input.is_empty() {
|
||||
return Err(CommandError::InputTooSmall.into());
|
||||
}
|
||||
|
||||
let status: StatusCode = input[0].into();
|
||||
|
||||
if input.len() > 1 {
|
||||
if status.is_ok() {
|
||||
trace!("parsing authenticator info data: {:#04X?}", &input);
|
||||
let authenticator_info =
|
||||
from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
|
||||
Ok(authenticator_info)
|
||||
} else {
|
||||
let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
|
||||
Err(CommandError::StatusCode(status, Some(data)).into())
|
||||
}
|
||||
} else {
|
||||
Err(CommandError::InputTooSmall.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn true_val() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
|
||||
pub(crate) struct AuthenticatorOptions {
|
||||
/// Indicates that the device is attached to the client and therefore can’t
|
||||
/// be removed and used on another client.
|
||||
#[serde(rename = "plat", default)]
|
||||
pub(crate) platform_device: bool,
|
||||
/// Indicates that the device is capable of storing keys on the device
|
||||
/// itself and therefore can satisfy the authenticatorGetAssertion request
|
||||
/// with allowList parameter not specified or empty.
|
||||
#[serde(rename = "rk", default)]
|
||||
pub(crate) resident_key: bool,
|
||||
|
||||
/// Client PIN:
|
||||
/// If present and set to true, it indicates that the device is capable of
|
||||
/// accepting a PIN from the client and PIN has been set.
|
||||
/// If present and set to false, it indicates that the device is capable of
|
||||
/// accepting a PIN from the client and PIN has not been set yet.
|
||||
/// If absent, it indicates that the device is not capable of accepting a
|
||||
/// PIN from the client.
|
||||
/// Client PIN is one of the ways to do user verification.
|
||||
#[serde(rename = "clientPin")]
|
||||
pub(crate) client_pin: Option<bool>,
|
||||
|
||||
/// Indicates that the device is capable of testing user presence.
|
||||
#[serde(rename = "up", default = "true_val")]
|
||||
pub(crate) user_presence: bool,
|
||||
|
||||
/// Indicates that the device is capable of verifying the user within
|
||||
/// itself. For example, devices with UI, biometrics fall into this
|
||||
/// category.
|
||||
/// If present and set to true, it indicates that the device is capable of
|
||||
/// user verification within itself and has been configured.
|
||||
/// If present and set to false, it indicates that the device is capable of
|
||||
/// user verification within itself and has not been yet configured. For
|
||||
/// example, a biometric device that has not yet been configured will
|
||||
/// return this parameter set to false.
|
||||
/// If absent, it indicates that the device is not capable of user
|
||||
/// verification within itself.
|
||||
/// A device that can only do Client PIN will not return the "uv" parameter.
|
||||
/// If a device is capable of verifying the user within itself as well as
|
||||
/// able to do Client PIN, it will return both "uv" and the Client PIN
|
||||
/// option.
|
||||
// TODO(MS): My Token (key-ID FIDO2) does return Some(false) here, even though
|
||||
// it has no built-in verification method. Not to be trusted...
|
||||
#[serde(rename = "uv")]
|
||||
pub(crate) user_verification: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for AuthenticatorOptions {
|
||||
fn default() -> Self {
|
||||
AuthenticatorOptions {
|
||||
platform_device: false,
|
||||
resident_key: false,
|
||||
client_pin: None,
|
||||
user_presence: true,
|
||||
user_verification: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct AuthenticatorInfo {
|
||||
pub(crate) versions: Vec<String>,
|
||||
pub(crate) extensions: Vec<String>,
|
||||
pub(crate) aaguid: AAGuid,
|
||||
pub(crate) options: AuthenticatorOptions,
|
||||
pub(crate) max_msg_size: Option<usize>,
|
||||
pub(crate) pin_protocols: Vec<u64>,
|
||||
// CTAP 2.1
|
||||
pub(crate) max_credential_count_in_list: Option<usize>,
|
||||
pub(crate) max_credential_id_length: Option<usize>,
|
||||
pub(crate) transports: Option<Vec<String>>,
|
||||
pub(crate) algorithms: Option<Vec<PublicKeyCredentialParameters>>,
|
||||
pub(crate) max_ser_large_blob_array: Option<u64>,
|
||||
pub(crate) force_pin_change: Option<bool>,
|
||||
pub(crate) min_pin_length: Option<u64>,
|
||||
pub(crate) firmware_version: Option<u64>,
|
||||
pub(crate) max_cred_blob_length: Option<u64>,
|
||||
pub(crate) max_rpids_for_set_min_pin_length: Option<u64>,
|
||||
pub(crate) preferred_platform_uv_attempts: Option<u64>,
|
||||
pub(crate) uv_modality: Option<u64>,
|
||||
pub(crate) certifications: Option<BTreeMap<String, u64>>,
|
||||
pub(crate) remaining_discoverable_credentials: Option<u64>,
|
||||
pub(crate) vendor_prototype_config_commands: Option<Vec<u64>>,
|
||||
}
|
||||
|
||||
impl AuthenticatorInfo {
|
||||
pub fn supports_hmac_secret(&self) -> bool {
|
||||
self.extensions.contains(&"hmac-secret".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! parse_next_optional_value {
|
||||
($name:expr, $map:expr) => {
|
||||
if $name.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("$name"));
|
||||
}
|
||||
$name = Some($map.next_value()?);
|
||||
};
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AuthenticatorInfo {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct AuthenticatorInfoVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AuthenticatorInfoVisitor {
|
||||
type Value = AuthenticatorInfo;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a byte array")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut versions = Vec::new();
|
||||
let mut extensions = Vec::new();
|
||||
let mut aaguid = None;
|
||||
let mut options = AuthenticatorOptions::default();
|
||||
let mut max_msg_size = None;
|
||||
let mut pin_protocols = Vec::new();
|
||||
let mut max_credential_count_in_list = None;
|
||||
let mut max_credential_id_length = None;
|
||||
let mut transports = None;
|
||||
let mut algorithms = None;
|
||||
let mut max_ser_large_blob_array = None;
|
||||
let mut force_pin_change = None;
|
||||
let mut min_pin_length = None;
|
||||
let mut firmware_version = None;
|
||||
let mut max_cred_blob_length = None;
|
||||
let mut max_rpids_for_set_min_pin_length = None;
|
||||
let mut preferred_platform_uv_attempts = None;
|
||||
let mut uv_modality = None;
|
||||
let mut certifications = None;
|
||||
let mut remaining_discoverable_credentials = None;
|
||||
let mut vendor_prototype_config_commands = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
0x01 => {
|
||||
if !versions.is_empty() {
|
||||
return Err(serde::de::Error::duplicate_field("versions"));
|
||||
}
|
||||
versions = map.next_value()?;
|
||||
}
|
||||
0x02 => {
|
||||
if !extensions.is_empty() {
|
||||
return Err(serde::de::Error::duplicate_field("extensions"));
|
||||
}
|
||||
extensions = map.next_value()?;
|
||||
}
|
||||
0x03 => {
|
||||
parse_next_optional_value!(aaguid, map);
|
||||
}
|
||||
0x04 => {
|
||||
options = map.next_value()?;
|
||||
}
|
||||
0x05 => {
|
||||
parse_next_optional_value!(max_msg_size, map);
|
||||
}
|
||||
0x06 => {
|
||||
if !pin_protocols.is_empty() {
|
||||
return Err(serde::de::Error::duplicate_field("pin_protocols"));
|
||||
}
|
||||
pin_protocols = map.next_value()?;
|
||||
}
|
||||
0x07 => {
|
||||
parse_next_optional_value!(max_credential_count_in_list, map);
|
||||
}
|
||||
0x08 => {
|
||||
parse_next_optional_value!(max_credential_id_length, map);
|
||||
}
|
||||
0x09 => {
|
||||
parse_next_optional_value!(transports, map);
|
||||
}
|
||||
0x0a => {
|
||||
parse_next_optional_value!(algorithms, map);
|
||||
}
|
||||
0x0b => {
|
||||
parse_next_optional_value!(max_ser_large_blob_array, map);
|
||||
}
|
||||
0x0c => {
|
||||
parse_next_optional_value!(force_pin_change, map);
|
||||
}
|
||||
0x0d => {
|
||||
parse_next_optional_value!(min_pin_length, map);
|
||||
}
|
||||
0x0e => {
|
||||
parse_next_optional_value!(firmware_version, map);
|
||||
}
|
||||
0x0f => {
|
||||
parse_next_optional_value!(max_cred_blob_length, map);
|
||||
}
|
||||
0x10 => {
|
||||
parse_next_optional_value!(max_rpids_for_set_min_pin_length, map);
|
||||
}
|
||||
0x11 => {
|
||||
parse_next_optional_value!(preferred_platform_uv_attempts, map);
|
||||
}
|
||||
0x12 => {
|
||||
parse_next_optional_value!(uv_modality, map);
|
||||
}
|
||||
0x13 => {
|
||||
parse_next_optional_value!(certifications, map);
|
||||
}
|
||||
0x14 => {
|
||||
parse_next_optional_value!(remaining_discoverable_credentials, map);
|
||||
}
|
||||
0x15 => {
|
||||
parse_next_optional_value!(vendor_prototype_config_commands, map);
|
||||
}
|
||||
k => {
|
||||
warn!("GetInfo: unexpected key: {:?}", k);
|
||||
let _ = map.next_value::<IgnoredAny>()?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if versions.is_empty() {
|
||||
return Err(M::Error::custom(
|
||||
"expected at least one version, got none".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(aaguid) = aaguid {
|
||||
Ok(AuthenticatorInfo {
|
||||
versions,
|
||||
extensions,
|
||||
aaguid,
|
||||
options,
|
||||
max_msg_size,
|
||||
pin_protocols,
|
||||
max_credential_count_in_list,
|
||||
max_credential_id_length,
|
||||
transports,
|
||||
algorithms,
|
||||
max_ser_large_blob_array,
|
||||
force_pin_change,
|
||||
min_pin_length,
|
||||
firmware_version,
|
||||
max_cred_blob_length,
|
||||
max_rpids_for_set_min_pin_length,
|
||||
preferred_platform_uv_attempts,
|
||||
uv_modality,
|
||||
certifications,
|
||||
remaining_discoverable_credentials,
|
||||
vendor_prototype_config_commands,
|
||||
})
|
||||
} else {
|
||||
Err(M::Error::custom("No AAGuid specified".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(AuthenticatorInfoVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::consts::{Capability, HIDCmd, CID_BROADCAST};
|
||||
use crate::crypto::COSEAlgorithm;
|
||||
use crate::transport::device_selector::Device;
|
||||
use crate::transport::platform::device::IN_HID_RPT_SIZE;
|
||||
use crate::transport::{hid::HIDDevice, FidoDevice, Nonce};
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use rand::{thread_rng, RngCore};
|
||||
use serde_cbor::de::from_slice;
|
||||
|
||||
// Raw data take from https://github.com/Yubico/python-fido2/blob/master/test/test_ctap2.py
|
||||
pub const AAGUID_RAW: [u8; 16] = [
|
||||
0xF8, 0xA0, 0x11, 0xF3, 0x8C, 0x0A, 0x4D, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1F, 0x9E, 0xDC,
|
||||
0x7D,
|
||||
];
|
||||
|
||||
pub const AUTHENTICATOR_INFO_PAYLOAD: [u8; 89] = [
|
||||
0xa6, // map(6)
|
||||
0x01, // unsigned(1)
|
||||
0x82, // array(2)
|
||||
0x66, // text(6)
|
||||
0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, // "U2F_V2"
|
||||
0x68, // text(8)
|
||||
0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, // "FIDO_2_0"
|
||||
0x02, // unsigned(2)
|
||||
0x82, // array(2)
|
||||
0x63, // text(3)
|
||||
0x75, 0x76, 0x6d, // "uvm"
|
||||
0x6b, // text(11)
|
||||
0x68, 0x6d, 0x61, 0x63, 0x2d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
|
||||
0x03, // unsigned(3)
|
||||
0x50, // bytes(16)
|
||||
0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
|
||||
0x7d, // "\xF8\xA0\u0011\xF3\x8C\nM\u0015\x80\u0006\u0017\u0011\u001F\x9E\xDC}"
|
||||
0x04, // unsigned(4)
|
||||
0xa4, // map(4)
|
||||
0x62, // text(2)
|
||||
0x72, 0x6b, // "rk"
|
||||
0xf5, // primitive(21)
|
||||
0x62, // text(2)
|
||||
0x75, 0x70, // "up"
|
||||
0xf5, // primitive(21)
|
||||
0x64, // text(4)
|
||||
0x70, 0x6c, 0x61, 0x74, // "plat"
|
||||
0xf4, // primitive(20)
|
||||
0x69, // text(9)
|
||||
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x69, 0x6e, // "clientPin"
|
||||
0xf4, // primitive(20)
|
||||
0x05, // unsigned(5)
|
||||
0x19, 0x04, 0xb0, // unsigned(1200)
|
||||
0x06, // unsigned(6)
|
||||
0x81, // array(1)
|
||||
0x01, // unsigned(1)
|
||||
];
|
||||
|
||||
// Real world example from Yubikey Bio
|
||||
pub const AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C: [u8; 409] = [
|
||||
0xB3, // map(19)
|
||||
0x01, // unsigned(1)
|
||||
0x84, // array(4)
|
||||
0x66, // text(6)
|
||||
0x55, 0x32, 0x46, 0x5F, 0x56, 0x32, // "U2F_V2"
|
||||
0x68, // text(8)
|
||||
0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, // "FIDO_2_0"
|
||||
0x6C, // text(12)
|
||||
0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52,
|
||||
0x45, // "FIDO_2_1_PRE"
|
||||
0x68, // text(8)
|
||||
0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, // "FIDO_2_1"
|
||||
0x02, // unsigned(2)
|
||||
0x85, // array(5)
|
||||
0x6B, // text(11)
|
||||
0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, // "credProtect"
|
||||
0x6B, // text(11)
|
||||
0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "hmac-secret"
|
||||
0x6C, // text(12)
|
||||
0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x4B, 0x65,
|
||||
0x79, // "largeBlobKey"
|
||||
0x68, // text(8)
|
||||
0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, // "credBlob"
|
||||
0x6C, // text(12)
|
||||
0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74,
|
||||
0x68, // "minPinLength"
|
||||
0x03, // unsigned(3)
|
||||
0x50, // bytes(16)
|
||||
0xD8, 0x52, 0x2D, 0x9F, 0x57, 0x5B, 0x48, 0x66, 0x88, 0xA9, 0xBA, 0x99, 0xFA, 0x02, 0xF3,
|
||||
0x5B, // "\xD8R-\x9FW[Hf\x88\xA9\xBA\x99\xFA\u0002\xF3["
|
||||
0x04, // unsigned(4)
|
||||
0xB0, // map(16)
|
||||
0x62, // text(2)
|
||||
0x72, 0x6B, // "rk"
|
||||
0xF5, // primitive(21)
|
||||
0x62, // text(2)
|
||||
0x75, 0x70, // "up"
|
||||
0xF5, // primitive(21)
|
||||
0x62, // text(2)
|
||||
0x75, 0x76, // "uv"
|
||||
0xF5, // primitive(21)
|
||||
0x64, // text(4)
|
||||
0x70, 0x6C, 0x61, 0x74, // "plat"
|
||||
0xF4, // primitive(20)
|
||||
0x67, // text(7)
|
||||
0x75, 0x76, 0x54, 0x6F, 0x6B, 0x65, 0x6E, // "uvToken"
|
||||
0xF5, // primitive(21)
|
||||
0x68, // text(8)
|
||||
0x61, 0x6C, 0x77, 0x61, 0x79, 0x73, 0x55, 0x76, // "alwaysUv"
|
||||
0xF5, // primitive(21)
|
||||
0x68, // text(8)
|
||||
0x63, 0x72, 0x65, 0x64, 0x4D, 0x67, 0x6D, 0x74, // "credMgmt"
|
||||
0xF5, // primitive(21)
|
||||
0x69, // text(9)
|
||||
0x61, 0x75, 0x74, 0x68, 0x6E, 0x72, 0x43, 0x66, 0x67, // "authnrCfg"
|
||||
0xF5, // primitive(21)
|
||||
0x69, // text(9)
|
||||
0x62, 0x69, 0x6F, 0x45, 0x6E, 0x72, 0x6F, 0x6C, 0x6C, // "bioEnroll"
|
||||
0xF5, // primitive(21)
|
||||
0x69, // text(9)
|
||||
0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, // "clientPin"
|
||||
0xF5, // primitive(21)
|
||||
0x6A, // text(10)
|
||||
0x6C, 0x61, 0x72, 0x67, 0x65, 0x42, 0x6C, 0x6F, 0x62, 0x73, // "largeBlobs"
|
||||
0xF5, // primitive(21)
|
||||
0x6E, // text(14)
|
||||
0x70, 0x69, 0x6E, 0x55, 0x76, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6F, 0x6B, 0x65,
|
||||
0x6E, // "pinUvAuthToken"
|
||||
0xF5, // primitive(21)
|
||||
0x6F, // text(15)
|
||||
0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74,
|
||||
0x68, // "setMinPINLength"
|
||||
0xF5, // primitive(21)
|
||||
0x70, // text(16)
|
||||
0x6D, 0x61, 0x6B, 0x65, 0x43, 0x72, 0x65, 0x64, 0x55, 0x76, 0x4E, 0x6F, 0x74, 0x52, 0x71,
|
||||
0x64, // "makeCredUvNotRqd"
|
||||
0xF4, // primitive(20)
|
||||
0x75, // text(21)
|
||||
0x63, 0x72, 0x65, 0x64, 0x65, 0x6E, 0x74, 0x69, 0x61, 0x6C, 0x4D, 0x67, 0x6D, 0x74, 0x50,
|
||||
0x72, 0x65, 0x76, 0x69, 0x65, 0x77, // "credentialMgmtPreview"
|
||||
0xF5, // primitive(21)
|
||||
0x78, 0x1B, // text(27)
|
||||
0x75, 0x73, 0x65, 0x72, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F,
|
||||
0x6E, 0x4D, 0x67, 0x6D, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65,
|
||||
0x77, // "userVerificationMgmtPreview"
|
||||
0xF5, // primitive(21)
|
||||
0x05, // unsigned(5)
|
||||
0x19, 0x04, 0xB0, // unsigned(1200)
|
||||
0x06, // unsigned(6)
|
||||
0x82, // array(2)
|
||||
0x02, // unsigned(2)
|
||||
0x01, // unsigned(1)
|
||||
0x07, // unsigned(7)
|
||||
0x08, // unsigned(8)
|
||||
0x08, // unsigned(8)
|
||||
0x18, 0x80, // unsigned(128)
|
||||
0x09, // unsigned(9)
|
||||
0x81, // array(1)
|
||||
0x63, // text(3)
|
||||
0x75, 0x73, 0x62, // "usb"
|
||||
0x0A, // unsigned(10)
|
||||
0x82, // array(2)
|
||||
0xA2, // map(2)
|
||||
0x63, // text(3)
|
||||
0x61, 0x6C, 0x67, // "alg"
|
||||
0x26, // negative(6)
|
||||
0x64, // text(4)
|
||||
0x74, 0x79, 0x70, 0x65, // "type"
|
||||
0x6A, // text(10)
|
||||
0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
|
||||
0xA2, // map(2)
|
||||
0x63, // text(3)
|
||||
0x61, 0x6C, 0x67, // "alg"
|
||||
0x27, // negative(7)
|
||||
0x64, // text(4)
|
||||
0x74, 0x79, 0x70, 0x65, // "type"
|
||||
0x6A, // text(10)
|
||||
0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, // "public-key"
|
||||
0x0B, // unsigned(11)
|
||||
0x19, 0x04, 0x00, // unsigned(1024)
|
||||
0x0C, // unsigned(12)
|
||||
0xF4, // primitive(20)
|
||||
0x0D, // unsigned(13)
|
||||
0x04, // unsigned(4)
|
||||
0x0E, // unsigned(14)
|
||||
0x1A, 0x00, 0x05, 0x05, 0x06, // unsigned(328966)
|
||||
0x0F, // unsigned(15)
|
||||
0x18, 0x20, // unsigned(32)
|
||||
0x10, // unsigned(16)
|
||||
0x01, // unsigned(1)
|
||||
0x11, // unsigned(17)
|
||||
0x03, // unsigned(3)
|
||||
0x12, // unsigned(18)
|
||||
0x02, // unsigned(2)
|
||||
0x14, // unsigned(20)
|
||||
0x18, 0x18, // unsigned(24)
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn parse_authenticator_info() {
|
||||
let authenticator_info: AuthenticatorInfo =
|
||||
from_slice(&AUTHENTICATOR_INFO_PAYLOAD).unwrap();
|
||||
|
||||
let expected = AuthenticatorInfo {
|
||||
versions: vec!["U2F_V2".to_string(), "FIDO_2_0".to_string()],
|
||||
extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
|
||||
aaguid: AAGuid(AAGUID_RAW),
|
||||
options: AuthenticatorOptions {
|
||||
platform_device: false,
|
||||
resident_key: true,
|
||||
client_pin: Some(false),
|
||||
user_presence: true,
|
||||
user_verification: None,
|
||||
},
|
||||
max_msg_size: Some(1200),
|
||||
pin_protocols: vec![1],
|
||||
max_credential_count_in_list: None,
|
||||
max_credential_id_length: None,
|
||||
transports: None,
|
||||
algorithms: None,
|
||||
max_ser_large_blob_array: None,
|
||||
force_pin_change: None,
|
||||
min_pin_length: None,
|
||||
firmware_version: None,
|
||||
max_cred_blob_length: None,
|
||||
max_rpids_for_set_min_pin_length: None,
|
||||
preferred_platform_uv_attempts: None,
|
||||
uv_modality: None,
|
||||
certifications: None,
|
||||
remaining_discoverable_credentials: None,
|
||||
vendor_prototype_config_commands: None,
|
||||
};
|
||||
|
||||
assert_eq!(authenticator_info, expected);
|
||||
|
||||
// Test broken auth info
|
||||
let mut broken_payload = AUTHENTICATOR_INFO_PAYLOAD.to_vec();
|
||||
// Have one more entry in the map
|
||||
broken_payload[0] += 1;
|
||||
// Add the additional entry at the back with an invalid key
|
||||
broken_payload.extend_from_slice(&[
|
||||
0x17, // unsigned(23) -> invalid key-number. CTAP2.1 goes only to 0x15
|
||||
0x6B, // text(11)
|
||||
0x69, 0x6E, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x5F, 0x6B, 0x65, 0x79, // "invalid_key"
|
||||
]);
|
||||
|
||||
let authenticator_info: AuthenticatorInfo = from_slice(&broken_payload).unwrap();
|
||||
assert_eq!(authenticator_info, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_authenticator_info_yk_bio_5c() {
|
||||
let authenticator_info: AuthenticatorInfo =
|
||||
from_slice(&AUTHENTICATOR_INFO_PAYLOAD_YK_BIO_5C).unwrap();
|
||||
|
||||
let expected = AuthenticatorInfo {
|
||||
versions: vec![
|
||||
"U2F_V2".to_string(),
|
||||
"FIDO_2_0".to_string(),
|
||||
"FIDO_2_1_PRE".to_string(),
|
||||
"FIDO_2_1".to_string(),
|
||||
],
|
||||
extensions: vec![
|
||||
"credProtect".to_string(),
|
||||
"hmac-secret".to_string(),
|
||||
"largeBlobKey".to_string(),
|
||||
"credBlob".to_string(),
|
||||
"minPinLength".to_string(),
|
||||
],
|
||||
aaguid: AAGuid([
|
||||
0xd8, 0x52, 0x2d, 0x9f, 0x57, 0x5b, 0x48, 0x66, 0x88, 0xa9, 0xba, 0x99, 0xfa, 0x02,
|
||||
0xf3, 0x5b,
|
||||
]),
|
||||
options: AuthenticatorOptions {
|
||||
platform_device: false,
|
||||
resident_key: true,
|
||||
client_pin: Some(true),
|
||||
user_presence: true,
|
||||
user_verification: Some(true),
|
||||
},
|
||||
max_msg_size: Some(1200),
|
||||
pin_protocols: vec![2, 1],
|
||||
max_credential_count_in_list: Some(8),
|
||||
max_credential_id_length: Some(128),
|
||||
transports: Some(vec!["usb".to_string()]),
|
||||
algorithms: Some(vec![
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::ES256,
|
||||
},
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::EDDSA,
|
||||
},
|
||||
]),
|
||||
max_ser_large_blob_array: Some(1024),
|
||||
force_pin_change: Some(false),
|
||||
min_pin_length: Some(4),
|
||||
firmware_version: Some(328966),
|
||||
max_cred_blob_length: Some(32),
|
||||
max_rpids_for_set_min_pin_length: Some(1),
|
||||
preferred_platform_uv_attempts: Some(3),
|
||||
uv_modality: Some(2),
|
||||
certifications: None,
|
||||
remaining_discoverable_credentials: Some(24),
|
||||
vendor_prototype_config_commands: None,
|
||||
};
|
||||
|
||||
assert_eq!(authenticator_info, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_info_ctap2_only() {
|
||||
let mut device = Device::new("commands/get_info").unwrap();
|
||||
let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
|
||||
|
||||
// channel id
|
||||
let mut cid = [0u8; 4];
|
||||
thread_rng().fill_bytes(&mut cid);
|
||||
|
||||
// init packet
|
||||
let mut msg = CID_BROADCAST.to_vec();
|
||||
msg.extend(vec![HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt
|
||||
msg.extend_from_slice(&nonce);
|
||||
device.add_write(&msg, 0);
|
||||
|
||||
// init_resp packet
|
||||
let mut msg = CID_BROADCAST.to_vec();
|
||||
msg.extend(vec![
|
||||
0x06, /*HIDCmd::Init without TYPE_INIT*/
|
||||
0x00, 0x11,
|
||||
]); // cmd + bcnt
|
||||
msg.extend_from_slice(&nonce);
|
||||
msg.extend_from_slice(&cid); // new channel id
|
||||
|
||||
// We are setting NMSG, to signal that the device does not support CTAP1
|
||||
msg.extend(vec![0x02, 0x04, 0x01, 0x08, 0x01 | 0x04 | 0x08]); // versions + flags (wink+cbor+nmsg)
|
||||
device.add_read(&msg, 0);
|
||||
|
||||
// ctap2 request
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
|
||||
msg.extend(vec![0x04]); // authenticatorGetInfo
|
||||
device.add_write(&msg, 0);
|
||||
|
||||
// ctap2 response
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x5A]); // cmd + bcnt
|
||||
msg.extend(vec![0]); // Status code: Success
|
||||
msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[0..(IN_HID_RPT_SIZE - 8)]);
|
||||
device.add_read(&msg, 0);
|
||||
// Continuation package
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(vec![0x00]); // SEQ
|
||||
msg.extend(&AUTHENTICATOR_INFO_PAYLOAD[(IN_HID_RPT_SIZE - 8)..]);
|
||||
device.add_read(&msg, 0);
|
||||
device
|
||||
.init(Nonce::Use(nonce))
|
||||
.expect("Failed to init device");
|
||||
|
||||
assert_eq!(device.get_cid(), &cid);
|
||||
|
||||
let dev_info = device.get_device_info();
|
||||
assert_eq!(
|
||||
dev_info.cap_flags,
|
||||
Capability::WINK | Capability::CBOR | Capability::NMSG
|
||||
);
|
||||
|
||||
let result = device
|
||||
.get_authenticator_info()
|
||||
.expect("Didn't get any authenticator_info");
|
||||
let expected = AuthenticatorInfo {
|
||||
versions: vec!["U2F_V2".to_string(), "FIDO_2_0".to_string()],
|
||||
extensions: vec!["uvm".to_string(), "hmac-secret".to_string()],
|
||||
aaguid: AAGuid(AAGUID_RAW),
|
||||
options: AuthenticatorOptions {
|
||||
platform_device: false,
|
||||
resident_key: true,
|
||||
client_pin: Some(false),
|
||||
user_presence: true,
|
||||
user_verification: None,
|
||||
},
|
||||
max_msg_size: Some(1200),
|
||||
pin_protocols: vec![1],
|
||||
max_credential_count_in_list: None,
|
||||
max_credential_id_length: None,
|
||||
transports: None,
|
||||
algorithms: None,
|
||||
max_ser_large_blob_array: None,
|
||||
force_pin_change: None,
|
||||
min_pin_length: None,
|
||||
firmware_version: None,
|
||||
max_cred_blob_length: None,
|
||||
max_rpids_for_set_min_pin_length: None,
|
||||
preferred_platform_uv_attempts: None,
|
||||
uv_modality: None,
|
||||
certifications: None,
|
||||
remaining_discoverable_credentials: None,
|
||||
vendor_prototype_config_commands: None,
|
||||
};
|
||||
|
||||
assert_eq!(result, &expected);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
use super::{Command, CommandError, RequestCtap2, StatusCode};
|
||||
use crate::ctap2::commands::get_assertion::GetAssertionResponse;
|
||||
use crate::transport::errors::HIDError;
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use serde_cbor::{de::from_slice, Value};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct GetNextAssertion;
|
||||
|
||||
impl RequestCtap2 for GetNextAssertion {
|
||||
type Output = GetAssertionResponse;
|
||||
|
||||
fn command() -> Command {
|
||||
Command::GetNextAssertion
|
||||
}
|
||||
|
||||
fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn handle_response_ctap2<Dev>(
|
||||
&self,
|
||||
_dev: &mut Dev,
|
||||
input: &[u8],
|
||||
) -> Result<Self::Output, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
if input.is_empty() {
|
||||
return Err(CommandError::InputTooSmall.into());
|
||||
}
|
||||
|
||||
let status: StatusCode = input[0].into();
|
||||
debug!("response status code: {:?}", status);
|
||||
if input.len() > 1 {
|
||||
if status.is_ok() {
|
||||
let assertion = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
|
||||
// TODO(baloo): check assertion response does not have numberOfCredentials
|
||||
Ok(assertion)
|
||||
} else {
|
||||
let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
|
||||
Err(CommandError::StatusCode(status, Some(data)).into())
|
||||
}
|
||||
} else if status.is_ok() {
|
||||
Err(CommandError::InputTooSmall.into())
|
||||
} else {
|
||||
Err(CommandError::StatusCode(status, None).into())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
use super::{CommandError, RequestCtap1, Retryable};
|
||||
use crate::consts::U2F_VERSION;
|
||||
use crate::transport::errors::{ApduErrorStatus, HIDError};
|
||||
use crate::u2ftypes::CTAP1RequestAPDU;
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum U2FInfo {
|
||||
U2F_V2,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
// TODO(baloo): if one does not issue U2F_VERSION before makecredentials or getassertion, token
|
||||
// will return error (ConditionsNotSatified), test this in unit tests
|
||||
pub struct GetVersion {}
|
||||
|
||||
impl Default for GetVersion {
|
||||
fn default() -> GetVersion {
|
||||
GetVersion {}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestCtap1 for GetVersion {
|
||||
type Output = U2FInfo;
|
||||
|
||||
fn handle_response_ctap1(
|
||||
&self,
|
||||
_status: Result<(), ApduErrorStatus>,
|
||||
input: &[u8],
|
||||
) -> Result<Self::Output, Retryable<HIDError>> {
|
||||
if input.is_empty() {
|
||||
return Err(Retryable::Error(HIDError::Command(
|
||||
CommandError::InputTooSmall,
|
||||
)));
|
||||
}
|
||||
|
||||
let expected = String::from("U2F_V2");
|
||||
let result = String::from_utf8_lossy(input);
|
||||
match result {
|
||||
ref data if data == &expected => Ok(U2FInfo::U2F_V2),
|
||||
_ => Err(Retryable::Error(HIDError::UnexpectedVersion)),
|
||||
}
|
||||
}
|
||||
|
||||
fn ctap1_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
let flags = 0;
|
||||
|
||||
let cmd = U2F_VERSION;
|
||||
let data = CTAP1RequestAPDU::serialize(cmd, flags, &[])?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::consts::{Capability, HIDCmd, CID_BROADCAST, SW_NO_ERROR};
|
||||
use crate::transport::device_selector::Device;
|
||||
use crate::transport::{hid::HIDDevice, FidoDevice, Nonce};
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use rand::{thread_rng, RngCore};
|
||||
|
||||
#[test]
|
||||
fn test_get_version_ctap1_only() {
|
||||
let mut device = Device::new("commands/get_version").unwrap();
|
||||
let nonce = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
|
||||
|
||||
// channel id
|
||||
let mut cid = [0u8; 4];
|
||||
thread_rng().fill_bytes(&mut cid);
|
||||
|
||||
// init packet
|
||||
let mut msg = CID_BROADCAST.to_vec();
|
||||
msg.extend(&[HIDCmd::Init.into(), 0x00, 0x08]); // cmd + bcnt
|
||||
msg.extend_from_slice(&nonce);
|
||||
device.add_write(&msg, 0);
|
||||
|
||||
// init_resp packet
|
||||
let mut msg = CID_BROADCAST.to_vec();
|
||||
msg.extend(vec![
|
||||
0x06, /*HIDCmd::Init without !TYPE_INIT*/
|
||||
0x00, 0x11,
|
||||
]); // cmd + bcnt
|
||||
msg.extend_from_slice(&nonce);
|
||||
msg.extend_from_slice(&cid); // new channel id
|
||||
|
||||
// We are not setting CBOR, to signal that the device does not support CTAP1
|
||||
msg.extend(&[0x02, 0x04, 0x01, 0x08, 0x01]); // versions + flags (wink)
|
||||
device.add_read(&msg, 0);
|
||||
|
||||
// ctap1 U2F_VERSION request
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x7]); // cmd + bcnt
|
||||
msg.extend(&[0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0]);
|
||||
device.add_write(&msg, 0);
|
||||
|
||||
// fido response
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(&[HIDCmd::Msg.into(), 0x0, 0x08]); // cmd + bcnt
|
||||
msg.extend(&[0x55, 0x32, 0x46, 0x5f, 0x56, 0x32]); // 'U2F_V2'
|
||||
msg.extend(&SW_NO_ERROR);
|
||||
device.add_read(&msg, 0);
|
||||
|
||||
device
|
||||
.init(Nonce::Use(nonce))
|
||||
.expect("Failed to init device");
|
||||
|
||||
assert_eq!(device.get_cid(), &cid);
|
||||
|
||||
let dev_info = device.get_device_info();
|
||||
assert_eq!(dev_info.cap_flags, Capability::WINK);
|
||||
|
||||
let result = device.get_authenticator_info();
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,461 @@
|
|||
use crate::crypto;
|
||||
use crate::ctap2::client_data::ClientDataHash;
|
||||
use crate::ctap2::commands::client_pin::{GetPinToken, GetRetries, Pin, PinAuth, PinError};
|
||||
use crate::errors::AuthenticatorError;
|
||||
use crate::transport::errors::{ApduErrorStatus, HIDError};
|
||||
use crate::transport::FidoDevice;
|
||||
use serde_cbor::{error::Error as CborError, Value};
|
||||
use serde_json as json;
|
||||
use std::error::Error as StdErrorT;
|
||||
use std::fmt;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub(crate) mod client_pin;
|
||||
pub(crate) mod get_assertion;
|
||||
pub(crate) mod get_info;
|
||||
pub(crate) mod get_next_assertion;
|
||||
pub(crate) mod get_version;
|
||||
pub(crate) mod make_credentials;
|
||||
pub(crate) mod reset;
|
||||
pub(crate) mod selection;
|
||||
|
||||
pub trait Request<T>
|
||||
where
|
||||
Self: fmt::Debug,
|
||||
Self: RequestCtap1<Output = T>,
|
||||
Self: RequestCtap2<Output = T>,
|
||||
{
|
||||
fn is_ctap2_request(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Retryable wraps an error type and may ask manager to retry sending a
|
||||
/// command, this is useful for ctap1 where token will reply with "condition not
|
||||
/// sufficient" because user needs to press the button.
|
||||
#[derive(Debug)]
|
||||
pub enum Retryable<T> {
|
||||
Retry,
|
||||
Error(T),
|
||||
}
|
||||
|
||||
impl<T> Retryable<T> {
|
||||
pub fn is_retry(&self) -> bool {
|
||||
matches!(*self, Retryable::Retry)
|
||||
}
|
||||
|
||||
pub fn is_error(&self) -> bool {
|
||||
!self.is_retry()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Retryable<T> {
|
||||
fn from(e: T) -> Self {
|
||||
Retryable::Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RequestCtap1: fmt::Debug {
|
||||
type Output;
|
||||
|
||||
/// Serializes a request into FIDO v1.x / CTAP1 / U2F format.
|
||||
///
|
||||
/// See [`crate::u2ftypes::CTAP1RequestAPDU::serialize()`]
|
||||
fn ctap1_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
|
||||
where
|
||||
Dev: FidoDevice + Read + Write + fmt::Debug;
|
||||
|
||||
/// Deserializes a response from FIDO v1.x / CTAP1 / U2Fv2 format.
|
||||
fn handle_response_ctap1(
|
||||
&self,
|
||||
status: Result<(), ApduErrorStatus>,
|
||||
input: &[u8],
|
||||
) -> Result<Self::Output, Retryable<HIDError>>;
|
||||
}
|
||||
|
||||
pub trait RequestCtap2: fmt::Debug {
|
||||
type Output;
|
||||
|
||||
fn command() -> Command;
|
||||
|
||||
fn wire_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
|
||||
where
|
||||
Dev: FidoDevice + Read + Write + fmt::Debug;
|
||||
|
||||
fn handle_response_ctap2<Dev>(
|
||||
&self,
|
||||
dev: &mut Dev,
|
||||
input: &[u8],
|
||||
) -> Result<Self::Output, HIDError>
|
||||
where
|
||||
Dev: FidoDevice + Read + Write + fmt::Debug;
|
||||
}
|
||||
|
||||
pub(crate) trait PinAuthCommand {
|
||||
fn pin(&self) -> &Option<Pin>;
|
||||
fn set_pin(&mut self, pin: Option<Pin>);
|
||||
fn pin_auth(&self) -> &Option<PinAuth>;
|
||||
fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>, pin_auth_protocol: Option<u64>);
|
||||
fn client_data_hash(&self) -> ClientDataHash;
|
||||
fn unset_uv_option(&mut self);
|
||||
fn determine_pin_auth<D: FidoDevice>(&mut self, dev: &mut D) -> Result<(), AuthenticatorError> {
|
||||
if !dev.supports_ctap2() {
|
||||
self.set_pin_auth(None, None);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let client_data_hash = self.client_data_hash();
|
||||
let pin_auth = match calculate_pin_auth(dev, &client_data_hash, &self.pin()) {
|
||||
Ok(pin_auth) => pin_auth,
|
||||
Err(e) => {
|
||||
return Err(repackage_pin_errors(dev, e));
|
||||
}
|
||||
};
|
||||
self.set_pin_auth(pin_auth, Some(1)); // TODO(MS): Currently, we only support version 1
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn repackage_pin_errors<D: FidoDevice>(
|
||||
dev: &mut D,
|
||||
error: AuthenticatorError,
|
||||
) -> AuthenticatorError {
|
||||
match error {
|
||||
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::PinInvalid,
|
||||
_,
|
||||
))) => {
|
||||
// If the given PIN was wrong, determine no. of left retries
|
||||
let cmd = GetRetries::new();
|
||||
let retries = dev.send_cbor(&cmd).ok(); // If we got retries, wrap it in Some, otherwise ignore err
|
||||
return AuthenticatorError::PinError(PinError::InvalidPin(retries));
|
||||
}
|
||||
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::PinAuthBlocked,
|
||||
_,
|
||||
))) => {
|
||||
return AuthenticatorError::PinError(PinError::PinAuthBlocked);
|
||||
}
|
||||
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::PinBlocked,
|
||||
_,
|
||||
))) => {
|
||||
return AuthenticatorError::PinError(PinError::PinBlocked);
|
||||
}
|
||||
AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::PinRequired,
|
||||
_,
|
||||
))) => {
|
||||
return AuthenticatorError::PinError(PinError::PinRequired);
|
||||
}
|
||||
// TODO(MS): Add "PinNotSet"
|
||||
// TODO(MS): Add "PinPolicyViolated"
|
||||
err => {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spec: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api
|
||||
// and: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticator-api
|
||||
#[repr(u8)]
|
||||
#[derive(Debug)]
|
||||
pub enum Command {
|
||||
MakeCredentials = 0x01,
|
||||
GetAssertion = 0x02,
|
||||
GetInfo = 0x04,
|
||||
ClientPin = 0x06,
|
||||
Reset = 0x07,
|
||||
GetNextAssertion = 0x08,
|
||||
Selection = 0x0B,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
#[cfg(test)]
|
||||
pub fn from_u8(v: u8) -> Option<Command> {
|
||||
match v {
|
||||
0x01 => Some(Command::MakeCredentials),
|
||||
0x02 => Some(Command::GetAssertion),
|
||||
0x04 => Some(Command::GetInfo),
|
||||
0x06 => Some(Command::ClientPin),
|
||||
0x07 => Some(Command::Reset),
|
||||
0x08 => Some(Command::GetNextAssertion),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StatusCode {
|
||||
/// Indicates successful response.
|
||||
OK,
|
||||
/// The command is not a valid CTAP command.
|
||||
InvalidCommand,
|
||||
/// The command included an invalid parameter.
|
||||
InvalidParameter,
|
||||
/// Invalid message or item length.
|
||||
InvalidLength,
|
||||
/// Invalid message sequencing.
|
||||
InvalidSeq,
|
||||
/// Message timed out.
|
||||
Timeout,
|
||||
/// Channel busy.
|
||||
ChannelBusy,
|
||||
/// Command requires channel lock.
|
||||
LockRequired,
|
||||
/// Command not allowed on this cid.
|
||||
InvalidChannel,
|
||||
/// Invalid/unexpected CBOR error.
|
||||
CBORUnexpectedType,
|
||||
/// Error when parsing CBOR.
|
||||
InvalidCBOR,
|
||||
/// Missing non-optional parameter.
|
||||
MissingParameter,
|
||||
/// Limit for number of items exceeded.
|
||||
LimitExceeded,
|
||||
/// Unsupported extension.
|
||||
UnsupportedExtension,
|
||||
/// Valid credential found in the exclude list.
|
||||
CredentialExcluded,
|
||||
/// Processing (Lengthy operation is in progress).
|
||||
Processing,
|
||||
/// Credential not valid for the authenticator.
|
||||
InvalidCredential,
|
||||
/// Authentication is waiting for user interaction.
|
||||
UserActionPending,
|
||||
/// Processing, lengthy operation is in progress.
|
||||
OperationPending,
|
||||
/// No request is pending.
|
||||
NoOperations,
|
||||
/// Authenticator does not support requested algorithm.
|
||||
UnsupportedAlgorithm,
|
||||
/// Not authorized for requested operation.
|
||||
OperationDenied,
|
||||
/// Internal key storage is full.
|
||||
KeyStoreFull,
|
||||
/// No outstanding operations.
|
||||
NoOperationPending,
|
||||
/// Unsupported option.
|
||||
UnsupportedOption,
|
||||
/// Not a valid option for current operation.
|
||||
InvalidOption,
|
||||
/// Pending keep alive was cancelled.
|
||||
KeepaliveCancel,
|
||||
/// No valid credentials provided.
|
||||
NoCredentials,
|
||||
/// Timeout waiting for user interaction.
|
||||
UserActionTimeout,
|
||||
/// Continuation command, such as, authenticatorGetNextAssertion not
|
||||
/// allowed.
|
||||
NotAllowed,
|
||||
/// PIN Invalid.
|
||||
PinInvalid,
|
||||
/// PIN Blocked.
|
||||
PinBlocked,
|
||||
/// PIN authentication,pinAuth, verification failed.
|
||||
PinAuthInvalid,
|
||||
/// PIN authentication,pinAuth, blocked. Requires power recycle to reset.
|
||||
PinAuthBlocked,
|
||||
/// No PIN has been set.
|
||||
PinNotSet,
|
||||
/// PIN is required for the selected operation.
|
||||
PinRequired,
|
||||
/// PIN policy violation. Currently only enforces minimum length.
|
||||
PinPolicyViolation,
|
||||
/// pinToken expired on authenticator.
|
||||
PinTokenExpired,
|
||||
/// Authenticator cannot handle this request due to memory constraints.
|
||||
RequestTooLarge,
|
||||
/// The current operation has timed out.
|
||||
ActionTimeout,
|
||||
/// User presence is required for the requested operation.
|
||||
UpRequired,
|
||||
|
||||
/// Unknown status.
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
impl StatusCode {
|
||||
fn is_ok(&self) -> bool {
|
||||
matches!(*self, StatusCode::OK)
|
||||
}
|
||||
|
||||
fn device_busy(&self) -> bool {
|
||||
matches!(*self, StatusCode::ChannelBusy)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for StatusCode {
|
||||
fn from(value: u8) -> StatusCode {
|
||||
match value {
|
||||
0x00 => StatusCode::OK,
|
||||
0x01 => StatusCode::InvalidCommand,
|
||||
0x02 => StatusCode::InvalidParameter,
|
||||
0x03 => StatusCode::InvalidLength,
|
||||
0x04 => StatusCode::InvalidSeq,
|
||||
0x05 => StatusCode::Timeout,
|
||||
0x06 => StatusCode::ChannelBusy,
|
||||
0x0A => StatusCode::LockRequired,
|
||||
0x0B => StatusCode::InvalidChannel,
|
||||
0x11 => StatusCode::CBORUnexpectedType,
|
||||
0x12 => StatusCode::InvalidCBOR,
|
||||
0x14 => StatusCode::MissingParameter,
|
||||
0x15 => StatusCode::LimitExceeded,
|
||||
0x16 => StatusCode::UnsupportedExtension,
|
||||
0x19 => StatusCode::CredentialExcluded,
|
||||
0x21 => StatusCode::Processing,
|
||||
0x22 => StatusCode::InvalidCredential,
|
||||
0x23 => StatusCode::UserActionPending,
|
||||
0x24 => StatusCode::OperationPending,
|
||||
0x25 => StatusCode::NoOperations,
|
||||
0x26 => StatusCode::UnsupportedAlgorithm,
|
||||
0x27 => StatusCode::OperationDenied,
|
||||
0x28 => StatusCode::KeyStoreFull,
|
||||
0x2A => StatusCode::NoOperationPending,
|
||||
0x2B => StatusCode::UnsupportedOption,
|
||||
0x2C => StatusCode::InvalidOption,
|
||||
0x2D => StatusCode::KeepaliveCancel,
|
||||
0x2E => StatusCode::NoCredentials,
|
||||
0x2f => StatusCode::UserActionTimeout,
|
||||
0x30 => StatusCode::NotAllowed,
|
||||
0x31 => StatusCode::PinInvalid,
|
||||
0x32 => StatusCode::PinBlocked,
|
||||
0x33 => StatusCode::PinAuthInvalid,
|
||||
0x34 => StatusCode::PinAuthBlocked,
|
||||
0x35 => StatusCode::PinNotSet,
|
||||
0x36 => StatusCode::PinRequired,
|
||||
0x37 => StatusCode::PinPolicyViolation,
|
||||
0x38 => StatusCode::PinTokenExpired,
|
||||
0x39 => StatusCode::RequestTooLarge,
|
||||
0x3A => StatusCode::ActionTimeout,
|
||||
0x3B => StatusCode::UpRequired,
|
||||
|
||||
othr => StatusCode::Unknown(othr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Into<u8> for StatusCode {
|
||||
fn into(self) -> u8 {
|
||||
match self {
|
||||
StatusCode::OK => 0x00,
|
||||
StatusCode::InvalidCommand => 0x01,
|
||||
StatusCode::InvalidParameter => 0x02,
|
||||
StatusCode::InvalidLength => 0x03,
|
||||
StatusCode::InvalidSeq => 0x04,
|
||||
StatusCode::Timeout => 0x05,
|
||||
StatusCode::ChannelBusy => 0x06,
|
||||
StatusCode::LockRequired => 0x0A,
|
||||
StatusCode::InvalidChannel => 0x0B,
|
||||
StatusCode::CBORUnexpectedType => 0x11,
|
||||
StatusCode::InvalidCBOR => 0x12,
|
||||
StatusCode::MissingParameter => 0x14,
|
||||
StatusCode::LimitExceeded => 0x15,
|
||||
StatusCode::UnsupportedExtension => 0x16,
|
||||
StatusCode::CredentialExcluded => 0x19,
|
||||
StatusCode::Processing => 0x21,
|
||||
StatusCode::InvalidCredential => 0x22,
|
||||
StatusCode::UserActionPending => 0x23,
|
||||
StatusCode::OperationPending => 0x24,
|
||||
StatusCode::NoOperations => 0x25,
|
||||
StatusCode::UnsupportedAlgorithm => 0x26,
|
||||
StatusCode::OperationDenied => 0x27,
|
||||
StatusCode::KeyStoreFull => 0x28,
|
||||
StatusCode::NoOperationPending => 0x2A,
|
||||
StatusCode::UnsupportedOption => 0x2B,
|
||||
StatusCode::InvalidOption => 0x2C,
|
||||
StatusCode::KeepaliveCancel => 0x2D,
|
||||
StatusCode::NoCredentials => 0x2E,
|
||||
StatusCode::UserActionTimeout => 0x2f,
|
||||
StatusCode::NotAllowed => 0x30,
|
||||
StatusCode::PinInvalid => 0x31,
|
||||
StatusCode::PinBlocked => 0x32,
|
||||
StatusCode::PinAuthInvalid => 0x33,
|
||||
StatusCode::PinAuthBlocked => 0x34,
|
||||
StatusCode::PinNotSet => 0x35,
|
||||
StatusCode::PinRequired => 0x36,
|
||||
StatusCode::PinPolicyViolation => 0x37,
|
||||
StatusCode::PinTokenExpired => 0x38,
|
||||
StatusCode::RequestTooLarge => 0x39,
|
||||
StatusCode::ActionTimeout => 0x3A,
|
||||
StatusCode::UpRequired => 0x3B,
|
||||
|
||||
StatusCode::Unknown(othr) => othr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommandError {
|
||||
InputTooSmall,
|
||||
MissingRequiredField(&'static str),
|
||||
Deserializing(CborError),
|
||||
Serializing(CborError),
|
||||
StatusCode(StatusCode, Option<Value>),
|
||||
Json(json::Error),
|
||||
Crypto(crypto::CryptoError),
|
||||
UnsupportedPinProtocol,
|
||||
}
|
||||
|
||||
impl fmt::Display for CommandError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
CommandError::InputTooSmall => write!(f, "CommandError: Input is too small"),
|
||||
CommandError::MissingRequiredField(field) => {
|
||||
write!(f, "CommandError: Missing required field {}", field)
|
||||
}
|
||||
CommandError::Deserializing(ref e) => {
|
||||
write!(f, "CommandError: Error while parsing: {}", e)
|
||||
}
|
||||
CommandError::Serializing(ref e) => {
|
||||
write!(f, "CommandError: Error while serializing: {}", e)
|
||||
}
|
||||
CommandError::StatusCode(ref code, ref value) => {
|
||||
write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value)
|
||||
}
|
||||
CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {}", e),
|
||||
CommandError::Crypto(ref e) => write!(f, "CommandError: Crypto error: {:?}", e),
|
||||
CommandError::UnsupportedPinProtocol => {
|
||||
write!(f, "CommandError: Pin protocol is not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdErrorT for CommandError {}
|
||||
|
||||
pub(crate) fn calculate_pin_auth<Dev>(
|
||||
dev: &mut Dev,
|
||||
client_data_hash: &ClientDataHash,
|
||||
pin: &Option<Pin>,
|
||||
) -> Result<Option<PinAuth>, AuthenticatorError>
|
||||
where
|
||||
Dev: FidoDevice,
|
||||
{
|
||||
// Not reusing the shared secret here, if it exists, since we might start again
|
||||
// with a different PIN (e.g. if the last one was wrong)
|
||||
let (shared_secret, info) = dev.establish_shared_secret()?;
|
||||
|
||||
// TODO(MS): What to do if token supports client_pin, but none has been set: Some(false)
|
||||
// AND a Pin is not None?
|
||||
let pin_auth = if info.options.client_pin == Some(true) {
|
||||
let pin = pin
|
||||
.as_ref()
|
||||
.ok_or(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::PinRequired,
|
||||
None,
|
||||
)))?;
|
||||
|
||||
let pin_command = GetPinToken::new(&info, &shared_secret, &pin)?;
|
||||
let pin_token = dev.send_cbor(&pin_command)?;
|
||||
|
||||
Some(
|
||||
pin_token
|
||||
.auth(client_data_hash.as_ref())
|
||||
.map_err(CommandError::Crypto)?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(pin_auth)
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
use super::{Command, CommandError, RequestCtap2, StatusCode};
|
||||
use crate::transport::errors::HIDError;
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use serde_cbor::{de::from_slice, Value};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Reset {}
|
||||
|
||||
impl Default for Reset {
|
||||
fn default() -> Reset {
|
||||
Reset {}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestCtap2 for Reset {
|
||||
type Output = ();
|
||||
|
||||
fn command() -> Command {
|
||||
Command::Reset
|
||||
}
|
||||
|
||||
fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn handle_response_ctap2<Dev>(
|
||||
&self,
|
||||
_dev: &mut Dev,
|
||||
input: &[u8],
|
||||
) -> Result<Self::Output, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
if input.is_empty() {
|
||||
return Err(CommandError::InputTooSmall.into());
|
||||
}
|
||||
|
||||
let status: StatusCode = input[0].into();
|
||||
|
||||
if status.is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
let msg = if input.len() > 1 {
|
||||
let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Err(CommandError::StatusCode(status, msg).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::consts::HIDCmd;
|
||||
use crate::transport::device_selector::Device;
|
||||
use crate::transport::{hid::HIDDevice, FidoDevice};
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use rand::{thread_rng, RngCore};
|
||||
use serde_cbor::{de::from_slice, Value};
|
||||
|
||||
fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> {
|
||||
let mut device = Device::new("commands/Reset").unwrap();
|
||||
// ctap2 request
|
||||
let mut cid = [0u8; 4];
|
||||
thread_rng().fill_bytes(&mut cid);
|
||||
device.set_cid(cid);
|
||||
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
|
||||
msg.extend(vec![0x07]); // authenticatorReset
|
||||
device.add_write(&msg, 0);
|
||||
|
||||
// ctap2 response
|
||||
let len = 0x1 + add.len() as u8;
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt
|
||||
msg.push(cmd); // Status code
|
||||
msg.extend(add); // + maybe additional data
|
||||
device.add_read(&msg, 0);
|
||||
|
||||
device.send_cbor(&Reset {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_ctap2_only() {
|
||||
// Test, if we can parse the status codes specified by the spec
|
||||
|
||||
// Ok()
|
||||
let response = issue_command_and_get_response(0, &vec![]).expect("Unexpected error");
|
||||
assert_eq!(response, ());
|
||||
|
||||
// Denied by the user
|
||||
let response = issue_command_and_get_response(0x27, &vec![]).expect_err("Not an error!");
|
||||
assert!(matches!(
|
||||
response,
|
||||
HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None))
|
||||
));
|
||||
|
||||
// Timeout
|
||||
let response = issue_command_and_get_response(0x2F, &vec![]).expect_err("Not an error!");
|
||||
assert!(matches!(
|
||||
response,
|
||||
HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::UserActionTimeout,
|
||||
None
|
||||
))
|
||||
));
|
||||
|
||||
// Unexpected error with more random CBOR-data
|
||||
let add_data = vec![
|
||||
0x63, // text(3)
|
||||
0x61, 0x6c, 0x67, // "alg"
|
||||
];
|
||||
let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!");
|
||||
match response {
|
||||
HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => {
|
||||
let expected: Value = from_slice(&add_data).unwrap();
|
||||
assert_eq!(d, expected)
|
||||
}
|
||||
e => panic!("Not the expected response: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
use super::{Command, CommandError, RequestCtap2, StatusCode};
|
||||
use crate::transport::errors::HIDError;
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use serde_cbor::{de::from_slice, Value};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Selection {}
|
||||
|
||||
impl Default for Selection {
|
||||
fn default() -> Selection {
|
||||
Selection {}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestCtap2 for Selection {
|
||||
type Output = ();
|
||||
|
||||
fn command() -> Command {
|
||||
Command::Selection
|
||||
}
|
||||
|
||||
fn wire_format<Dev>(&self, _dev: &mut Dev) -> Result<Vec<u8>, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn handle_response_ctap2<Dev>(
|
||||
&self,
|
||||
_dev: &mut Dev,
|
||||
input: &[u8],
|
||||
) -> Result<Self::Output, HIDError>
|
||||
where
|
||||
Dev: U2FDevice,
|
||||
{
|
||||
if input.is_empty() {
|
||||
return Err(CommandError::InputTooSmall.into());
|
||||
}
|
||||
|
||||
let status: StatusCode = input[0].into();
|
||||
|
||||
if status.is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
let msg = if input.len() > 1 {
|
||||
let data: Value = from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
|
||||
Some(data)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Err(CommandError::StatusCode(status, msg).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::consts::HIDCmd;
|
||||
use crate::transport::device_selector::Device;
|
||||
use crate::transport::{hid::HIDDevice, FidoDevice};
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use rand::{thread_rng, RngCore};
|
||||
use serde_cbor::{de::from_slice, Value};
|
||||
|
||||
fn issue_command_and_get_response(cmd: u8, add: &[u8]) -> Result<(), HIDError> {
|
||||
let mut device = Device::new("commands/selection").unwrap();
|
||||
// ctap2 request
|
||||
let mut cid = [0u8; 4];
|
||||
thread_rng().fill_bytes(&mut cid);
|
||||
device.set_cid(cid);
|
||||
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, 0x1]); // cmd + bcnt
|
||||
msg.extend(vec![0x0B]); // authenticatorSelection
|
||||
device.add_write(&msg, 0);
|
||||
|
||||
// ctap2 response
|
||||
let len = 0x1 + add.len() as u8;
|
||||
let mut msg = cid.to_vec();
|
||||
msg.extend(vec![HIDCmd::Cbor.into(), 0x00, len]); // cmd + bcnt
|
||||
msg.push(cmd); // Status code
|
||||
msg.extend(add); // + maybe additional data
|
||||
device.add_read(&msg, 0);
|
||||
|
||||
device.send_cbor(&Selection {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_ctap2_only() {
|
||||
// Test, if we can parse the status codes specified by the spec
|
||||
|
||||
// Ok()
|
||||
let response = issue_command_and_get_response(0, &vec![]).expect("Unexpected error");
|
||||
assert_eq!(response, ());
|
||||
|
||||
// Denied by the user
|
||||
let response = issue_command_and_get_response(0x27, &vec![]).expect_err("Not an error!");
|
||||
assert!(matches!(
|
||||
response,
|
||||
HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, None))
|
||||
));
|
||||
|
||||
// Timeout
|
||||
let response = issue_command_and_get_response(0x2F, &vec![]).expect_err("Not an error!");
|
||||
assert!(matches!(
|
||||
response,
|
||||
HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::UserActionTimeout,
|
||||
None
|
||||
))
|
||||
));
|
||||
|
||||
// Unexpected error with more random CBOR-data
|
||||
let add_data = vec![
|
||||
0x63, // text(3)
|
||||
0x61, 0x6c, 0x67, // "alg"
|
||||
];
|
||||
let response = issue_command_and_get_response(0x02, &add_data).expect_err("Not an error!");
|
||||
match response {
|
||||
HIDError::Command(CommandError::StatusCode(StatusCode::InvalidParameter, Some(d))) => {
|
||||
let expected: Value = from_slice(&add_data).unwrap();
|
||||
assert_eq!(d, expected)
|
||||
}
|
||||
e => panic!("Not the expected response: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
#[allow(dead_code)] // TODO(MS): Remove me asap
|
||||
pub mod commands;
|
||||
pub use commands::get_assertion::AssertionObject;
|
||||
|
||||
pub(crate) mod attestation;
|
||||
|
||||
pub mod client_data;
|
||||
pub mod server;
|
||||
pub(crate) mod utils;
|
|
@ -0,0 +1,510 @@
|
|||
use crate::crypto::COSEAlgorithm;
|
||||
use crate::{errors::AuthenticatorError, AuthenticatorTransports, KeyHandle};
|
||||
use serde::de::MapAccess;
|
||||
use serde::{
|
||||
de::{Error as SerdeError, Visitor},
|
||||
ser::SerializeMap,
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use serde_bytes::ByteBuf;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::convert::{Into, TryFrom};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||
pub struct RpIdHash(pub [u8; 32]);
|
||||
|
||||
impl fmt::Debug for RpIdHash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let value = base64::encode_config(&self.0, base64::URL_SAFE_NO_PAD);
|
||||
write!(f, "RpIdHash({})", value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for RpIdHash {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl RpIdHash {
|
||||
pub fn from(src: &[u8]) -> Result<RpIdHash, AuthenticatorError> {
|
||||
let mut payload = [0u8; 32];
|
||||
if src.len() != payload.len() {
|
||||
Err(AuthenticatorError::InvalidRelyingPartyInput)
|
||||
} else {
|
||||
payload.copy_from_slice(src);
|
||||
Ok(RpIdHash(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, Default)]
|
||||
#[cfg_attr(test, derive(Deserialize))]
|
||||
pub struct RelyingParty {
|
||||
// TODO(baloo): spec is wrong !!!!111
|
||||
// https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#commands
|
||||
// in the example "A PublicKeyCredentialRpEntity DOM object defined as follows:"
|
||||
// inconsistent with https://w3c.github.io/webauthn/#sctn-rp-credential-params
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
// Note: This enum is provided to make old CTAP1/U2F API work. This should be deprecated at some point
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RelyingPartyWrapper {
|
||||
Data(RelyingParty),
|
||||
// CTAP1 hash can be derived from full object, see RelyingParty::hash below,
|
||||
// but very old backends might still provide application IDs.
|
||||
Hash(RpIdHash),
|
||||
}
|
||||
|
||||
impl RelyingPartyWrapper {
|
||||
pub fn hash(&self) -> RpIdHash {
|
||||
match *self {
|
||||
RelyingPartyWrapper::Data(ref d) => {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&d.id);
|
||||
|
||||
let mut output = [0u8; 32];
|
||||
output.copy_from_slice(&hasher.finalize().as_slice());
|
||||
|
||||
RpIdHash(output)
|
||||
}
|
||||
RelyingPartyWrapper::Hash(ref d) => d.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(baloo): should we rename this PublicKeyCredentialUserEntity ?
|
||||
#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize, Default)]
|
||||
pub struct User {
|
||||
#[serde(with = "serde_bytes")]
|
||||
pub id: Vec<u8>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<String>, // This has been removed from Webauthn-2
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", rename = "displayName")]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PublicKeyCredentialParameters {
|
||||
pub alg: COSEAlgorithm,
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for PublicKeyCredentialParameters {
|
||||
type Error = AuthenticatorError;
|
||||
fn try_from(arg: i32) -> Result<Self, Self::Error> {
|
||||
let alg = COSEAlgorithm::try_from(arg as i64)?;
|
||||
Ok(PublicKeyCredentialParameters { alg })
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PublicKeyCredentialParameters {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
map.serialize_entry("alg", &self.alg)?;
|
||||
map.serialize_entry("type", "public-key")?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PublicKeyCredentialParameters {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct PublicKeyCredentialParametersVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for PublicKeyCredentialParametersVisitor {
|
||||
type Value = PublicKeyCredentialParameters;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a map")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut found_type = false;
|
||||
let mut alg = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
"alg" => {
|
||||
if alg.is_some() {
|
||||
return Err(SerdeError::duplicate_field("alg"));
|
||||
}
|
||||
alg = Some(map.next_value()?);
|
||||
}
|
||||
"type" => {
|
||||
if found_type {
|
||||
return Err(SerdeError::duplicate_field("type"));
|
||||
}
|
||||
|
||||
let v: &str = map.next_value()?;
|
||||
if v != "public-key" {
|
||||
return Err(SerdeError::custom(format!("invalid value: {}", v)));
|
||||
}
|
||||
found_type = true;
|
||||
}
|
||||
v => {
|
||||
return Err(SerdeError::unknown_field(v, &[]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_type {
|
||||
return Err(SerdeError::missing_field("type"));
|
||||
}
|
||||
|
||||
let alg = alg.ok_or(SerdeError::missing_field("alg"))?;
|
||||
|
||||
Ok(PublicKeyCredentialParameters { alg })
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(PublicKeyCredentialParametersVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Transport {
|
||||
USB,
|
||||
NFC,
|
||||
BLE,
|
||||
Internal,
|
||||
}
|
||||
|
||||
impl From<AuthenticatorTransports> for Vec<Transport> {
|
||||
fn from(t: AuthenticatorTransports) -> Self {
|
||||
let mut transports = Vec::new();
|
||||
if t.contains(AuthenticatorTransports::USB) {
|
||||
transports.push(Transport::USB);
|
||||
}
|
||||
if t.contains(AuthenticatorTransports::NFC) {
|
||||
transports.push(Transport::NFC);
|
||||
}
|
||||
if t.contains(AuthenticatorTransports::BLE) {
|
||||
transports.push(Transport::BLE);
|
||||
}
|
||||
|
||||
transports
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PublicKeyCredentialDescriptor {
|
||||
pub id: Vec<u8>,
|
||||
pub transports: Vec<Transport>,
|
||||
}
|
||||
|
||||
impl Serialize for PublicKeyCredentialDescriptor {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// TODO(MS): Transports is OPTIONAL, but some older tokens don't understand it
|
||||
// and return a CBOR-Parsing error. It is only a hint for the token,
|
||||
// so we'll leave it out for the moment
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
// let mut map = serializer.serialize_map(Some(3))?;
|
||||
map.serialize_entry("type", "public-key")?;
|
||||
map.serialize_entry("id", &ByteBuf::from(self.id.clone()))?;
|
||||
// map.serialize_entry("transports", &self.transports)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PublicKeyCredentialDescriptor {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct PublicKeyCredentialDescriptorVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for PublicKeyCredentialDescriptorVisitor {
|
||||
type Value = PublicKeyCredentialDescriptor;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a map")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut found_type = false;
|
||||
let mut id = None;
|
||||
let mut transports = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
"id" => {
|
||||
if id.is_some() {
|
||||
return Err(SerdeError::duplicate_field("id"));
|
||||
}
|
||||
let id_bytes: ByteBuf = map.next_value()?;
|
||||
id = Some(id_bytes.into_vec());
|
||||
}
|
||||
"transports" => {
|
||||
if transports.is_some() {
|
||||
return Err(SerdeError::duplicate_field("transports"));
|
||||
}
|
||||
transports = Some(map.next_value()?);
|
||||
}
|
||||
"type" => {
|
||||
if found_type {
|
||||
return Err(SerdeError::duplicate_field("type"));
|
||||
}
|
||||
let v: &str = map.next_value()?;
|
||||
if v != "public-key" {
|
||||
return Err(SerdeError::custom(format!("invalid value: {}", v)));
|
||||
}
|
||||
found_type = true;
|
||||
}
|
||||
v => {
|
||||
return Err(SerdeError::unknown_field(v, &[]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_type {
|
||||
return Err(SerdeError::missing_field("type"));
|
||||
}
|
||||
|
||||
let id = id.ok_or(SerdeError::missing_field("id"))?;
|
||||
let transports = transports.unwrap_or(Vec::new());
|
||||
|
||||
Ok(PublicKeyCredentialDescriptor { id, transports })
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(PublicKeyCredentialDescriptorVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&KeyHandle> for PublicKeyCredentialDescriptor {
|
||||
fn from(kh: &KeyHandle) -> Self {
|
||||
Self {
|
||||
id: kh.credential.clone(),
|
||||
transports: kh.transports.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{
|
||||
COSEAlgorithm, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty,
|
||||
Transport, User,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn serialize_rp() {
|
||||
let rp = RelyingParty {
|
||||
id: String::from("Acme"),
|
||||
name: None,
|
||||
icon: None,
|
||||
};
|
||||
|
||||
let payload = ser::to_vec(&rp).unwrap();
|
||||
assert_eq!(
|
||||
&payload,
|
||||
&[
|
||||
0xa1, // map(1)
|
||||
0x62, // text(2)
|
||||
0x69, 0x64, // "id"
|
||||
0x64, // text(4)
|
||||
0x41, 0x63, 0x6d, 0x65
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_user() {
|
||||
let user = User {
|
||||
id: vec![
|
||||
0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30,
|
||||
0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82,
|
||||
0x01, 0x93, 0x30, 0x82,
|
||||
],
|
||||
icon: Some(String::from("https://pics.example.com/00/p/aBjjjpqPb.png")),
|
||||
name: Some(String::from("johnpsmith@example.com")),
|
||||
display_name: Some(String::from("John P. Smith")),
|
||||
};
|
||||
|
||||
let payload = ser::to_vec(&user).unwrap();
|
||||
println!("payload = {:?}", payload);
|
||||
assert_eq!(
|
||||
payload,
|
||||
vec![
|
||||
0xa4, // map(4)
|
||||
0x62, // text(2)
|
||||
0x69, 0x64, // "id"
|
||||
0x58, 0x20, // bytes(32)
|
||||
0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid
|
||||
0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ...
|
||||
0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ...
|
||||
0x30, 0x82, // ...
|
||||
0x64, // text(4)
|
||||
0x69, 0x63, 0x6f, 0x6e, // "icon"
|
||||
0x78, 0x2b, // text(43)
|
||||
0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x70,
|
||||
0x69, // "https://pics.example.com/00/p/aBjjjpqPb.png"
|
||||
0x63, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // ...
|
||||
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x30, 0x30, 0x2f, 0x70, 0x2f, // ...
|
||||
0x61, 0x42, 0x6a, 0x6a, 0x6a, 0x70, 0x71, 0x50, 0x62, 0x2e, // ...
|
||||
0x70, 0x6e, 0x67, // ...
|
||||
0x64, // text(4)
|
||||
0x6e, 0x61, 0x6d, 0x65, // "name"
|
||||
0x76, // text(22)
|
||||
0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74,
|
||||
0x68, // "johnpsmith@example.com"
|
||||
0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ...
|
||||
0x6f, 0x6d, // ...
|
||||
0x6b, // text(11)
|
||||
0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, // "displayName"
|
||||
0x65, // ...
|
||||
0x6d, // text(13)
|
||||
0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x50, 0x2e, 0x20, 0x53, 0x6d, // "John P. Smith"
|
||||
0x69, 0x74, 0x68, // ...
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_user_noicon_nodisplayname() {
|
||||
let user = User {
|
||||
id: vec![
|
||||
0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30,
|
||||
0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82,
|
||||
0x01, 0x93, 0x30, 0x82,
|
||||
],
|
||||
icon: None,
|
||||
name: Some(String::from("johnpsmith@example.com")),
|
||||
display_name: None,
|
||||
};
|
||||
|
||||
let payload = ser::to_vec(&user).unwrap();
|
||||
println!("payload = {:?}", payload);
|
||||
assert_eq!(
|
||||
payload,
|
||||
vec![
|
||||
0xa2, // map(2)
|
||||
0x62, // text(2)
|
||||
0x69, 0x64, // "id"
|
||||
0x58, 0x20, // bytes(32)
|
||||
0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, 0x38, 0xa0, 0x03, // userid
|
||||
0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, 0x30, 0x82, 0x01, // ...
|
||||
0x38, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x30, 0x82, 0x01, 0x93, // ...
|
||||
0x30, 0x82, // ...
|
||||
0x64, // text(4)
|
||||
0x6e, 0x61, 0x6d, 0x65, // "name"
|
||||
0x76, // text(22)
|
||||
0x6a, 0x6f, 0x68, 0x6e, 0x70, 0x73, 0x6d, 0x69, 0x74,
|
||||
0x68, // "johnpsmith@example.com"
|
||||
0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, // ...
|
||||
0x6f, 0x6d, // ...
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
use serde_cbor::ser;
|
||||
|
||||
#[test]
|
||||
fn public_key() {
|
||||
let keys = vec![
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::ES256,
|
||||
},
|
||||
PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::RS256,
|
||||
},
|
||||
];
|
||||
|
||||
let payload = ser::to_vec(&keys);
|
||||
println!("payload = {:?}", payload);
|
||||
let payload = payload.unwrap();
|
||||
assert_eq!(
|
||||
payload,
|
||||
vec![
|
||||
0x82, // array(2)
|
||||
0xa2, // map(2)
|
||||
0x63, // text(3)
|
||||
0x61, 0x6c, 0x67, // "alg"
|
||||
0x26, // -7 (ES256)
|
||||
0x64, // text(4)
|
||||
0x74, 0x79, 0x70, 0x65, // "type"
|
||||
0x6a, // text(10)
|
||||
0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
|
||||
0x2D, 0x6B, 0x65, 0x79, // ...
|
||||
0xa2, // map(2)
|
||||
0x63, // text(3)
|
||||
0x61, 0x6c, 0x67, // "alg"
|
||||
0x39, 0x01, 0x00, // -257 (RS256)
|
||||
0x64, // text(4)
|
||||
0x74, 0x79, 0x70, 0x65, // "type"
|
||||
0x6a, // text(10)
|
||||
0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
|
||||
0x2D, 0x6B, 0x65, 0x79 // ...
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_key_desc() {
|
||||
let key = PublicKeyCredentialDescriptor {
|
||||
id: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
|
||||
0x1c, 0x1d, 0x1e, 0x1f,
|
||||
],
|
||||
transports: vec![Transport::BLE, Transport::USB],
|
||||
};
|
||||
|
||||
let payload = ser::to_vec(&key);
|
||||
println!("payload = {:?}", payload);
|
||||
let payload = payload.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
vec![
|
||||
// 0xa3, // map(3)
|
||||
0xa2, // map(2)
|
||||
0x64, // text(4)
|
||||
0x74, 0x79, 0x70, 0x65, // "type"
|
||||
0x6a, // text(10)
|
||||
0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, // "public-key"
|
||||
0x2D, 0x6B, 0x65, 0x79, // ...
|
||||
0x62, // text(2)
|
||||
0x69, 0x64, // "id"
|
||||
0x58, 0x20, // bytes(32)
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // key id
|
||||
0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, // ...
|
||||
0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, // ...
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // ...
|
||||
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // ...
|
||||
0x1e,
|
||||
0x1f, // ...
|
||||
|
||||
// Deactivated for now
|
||||
//0x6a, // text(10)
|
||||
//0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, // "transports"
|
||||
//0x6f, 0x72, 0x74, 0x73, // ...
|
||||
//0x82, // array(2)
|
||||
//0x63, // text(3)
|
||||
//0x62, 0x6c, 0x65, // "ble"
|
||||
//0x63, // text(3)
|
||||
//0x75, 0x73, 0x62 // "usb"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
use serde::de;
|
||||
use serde_cbor::error::Result;
|
||||
use serde_cbor::Deserializer;
|
||||
|
||||
pub fn from_slice_stream<'a, T>(slice: &'a [u8]) -> Result<(&'a [u8], T)>
|
||||
where
|
||||
T: de::Deserialize<'a>,
|
||||
{
|
||||
let mut deserializer = Deserializer::from_slice(slice);
|
||||
let value = de::Deserialize::deserialize(&mut deserializer)?;
|
||||
let rest = &slice[deserializer.byte_offset()..];
|
||||
|
||||
Ok((rest, value))
|
||||
}
|
|
@ -0,0 +1,945 @@
|
|||
/* 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::authenticatorservice::{
|
||||
AuthenticatorService, CtapVersion, RegisterArgsCtap2, SignArgsCtap2,
|
||||
};
|
||||
use crate::ctap2::attestation::AttestationStatement;
|
||||
use crate::ctap2::commands::get_assertion::{Assertion, AssertionObject, GetAssertionOptions};
|
||||
use crate::ctap2::commands::make_credentials::MakeCredentialsOptions;
|
||||
use crate::ctap2::server::{
|
||||
PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, User,
|
||||
};
|
||||
use crate::errors::{AuthenticatorError, U2FTokenError};
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::{AttestationObject, CollectedClientDataWrapper, Pin, StatusUpdate};
|
||||
use crate::{RegisterResult, SignResult};
|
||||
use libc::size_t;
|
||||
use rand::{thread_rng, Rng};
|
||||
use serde_cbor;
|
||||
use std::convert::TryFrom;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread;
|
||||
use std::{ptr, slice};
|
||||
|
||||
type Ctap2RegisterResult = Result<(AttestationObject, CString), AuthenticatorError>;
|
||||
type Ctap2PubKeyCredDescriptors = Vec<PublicKeyCredentialDescriptor>;
|
||||
type Ctap2RegisterCallback = extern "C" fn(u64, *mut Ctap2RegisterResult);
|
||||
type Ctap2SignResult = Result<(AssertionObject, CString), AuthenticatorError>;
|
||||
type Ctap2SignCallback = extern "C" fn(u64, *mut Ctap2SignResult);
|
||||
type Ctap2StatusUpdateCallback = extern "C" fn(*mut StatusUpdate);
|
||||
|
||||
const SIGN_RESULT_PUBKEY_CRED_ID: u8 = 1;
|
||||
const SIGN_RESULT_AUTH_DATA: u8 = 2;
|
||||
const SIGN_RESULT_SIGNATURE: u8 = 3;
|
||||
const SIGN_RESULT_USER_ID: u8 = 4;
|
||||
const SIGN_RESULT_USER_NAME: u8 = 5;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AuthenticatorArgsUser {
|
||||
id_ptr: *const u8,
|
||||
id_len: usize,
|
||||
name: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AuthenticatorArgsChallenge {
|
||||
ptr: *const u8,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AuthenticatorArgsPubCred {
|
||||
ptr: *const i32,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AuthenticatorArgsOptions {
|
||||
resident_key: bool,
|
||||
user_verification: bool,
|
||||
user_presence: bool,
|
||||
force_none_attestation: bool,
|
||||
}
|
||||
|
||||
// Generates a new 64-bit transaction id with collision probability 2^-32.
|
||||
fn new_tid() -> u64 {
|
||||
thread_rng().gen::<u64>()
|
||||
}
|
||||
|
||||
unsafe fn from_raw(ptr: *const u8, len: usize) -> Vec<u8> {
|
||||
slice::from_raw_parts(ptr, len).to_vec()
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method must not be called on a handle twice, and the handle is unusable
|
||||
/// after.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_mgr_free(mgr: *mut AuthenticatorService) {
|
||||
if !mgr.is_null() {
|
||||
drop(Box::from_raw(mgr));
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// The handle returned by this method must be freed by the caller.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_pkcd_new() -> *mut Ctap2PubKeyCredDescriptors {
|
||||
Box::into_raw(Box::new(vec![]))
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method must be used on an actual Ctap2PubKeyCredDescriptors CredDescriptorse
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_pkcd_add(
|
||||
pkcd: *mut Ctap2PubKeyCredDescriptors,
|
||||
id_ptr: *const u8,
|
||||
id_len: usize,
|
||||
transports: u8,
|
||||
) {
|
||||
(*pkcd).push(PublicKeyCredentialDescriptor {
|
||||
id: from_raw(id_ptr, id_len),
|
||||
transports: crate::AuthenticatorTransports::from_bits_truncate(transports).into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method must not be called on a handle twice, and the handle is unusable
|
||||
/// after.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_pkcd_free(khs: *mut Ctap2PubKeyCredDescriptors) {
|
||||
if !khs.is_null() {
|
||||
drop(Box::from_raw(khs));
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// The handle returned by this method must be freed by the caller.
|
||||
/// The returned handle can be used with all rust_u2f_mgr_*-functions as well
|
||||
/// but uses CTAP2 as the underlying protocol. CTAP1 requests will be repackaged
|
||||
/// into CTAP2 (if the device supports it)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rust_ctap2_mgr_new() -> *mut AuthenticatorService {
|
||||
if let Ok(mut mgr) = AuthenticatorService::new(CtapVersion::CTAP2) {
|
||||
mgr.add_detected_transports();
|
||||
Box::into_raw(Box::new(mgr))
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
fn rewrap_client_data(
|
||||
client_data: CollectedClientDataWrapper,
|
||||
) -> Result<CString, AuthenticatorError> {
|
||||
let s = CString::new(client_data.serialized_data.clone()).map_err(|_| {
|
||||
AuthenticatorError::Custom("Failed to transform client_data to C String".to_string())
|
||||
})?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn rewrap_register_result(
|
||||
attestation_object: AttestationObject,
|
||||
client_data: CollectedClientDataWrapper,
|
||||
) -> Ctap2RegisterResult {
|
||||
let s = rewrap_client_data(client_data)?;
|
||||
Ok((attestation_object, s))
|
||||
}
|
||||
|
||||
fn rewrap_sign_result(
|
||||
assertion_object: AssertionObject,
|
||||
client_data: CollectedClientDataWrapper,
|
||||
) -> Ctap2SignResult {
|
||||
let s = rewrap_client_data(client_data)?;
|
||||
Ok((assertion_object, s))
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method should not be called on AuthenticatorService handles after
|
||||
/// they've been freed
|
||||
/// All input is copied and it is the callers responsibility to free appropriately.
|
||||
/// Note: `KeyHandles` are used as `PublicKeyCredentialDescriptor`s for the exclude_list
|
||||
/// to keep the API smaller, as they are essentially the same thing.
|
||||
/// `PublicKeyCredentialParameters` in pub_cred_params are represented as i32 with
|
||||
/// their COSE value (see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms)
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_mgr_register(
|
||||
mgr: *mut AuthenticatorService,
|
||||
timeout: u64,
|
||||
callback: Ctap2RegisterCallback,
|
||||
status_callback: Ctap2StatusUpdateCallback,
|
||||
challenge: AuthenticatorArgsChallenge,
|
||||
relying_party_id: *const c_char,
|
||||
origin_ptr: *const c_char,
|
||||
user: AuthenticatorArgsUser,
|
||||
pub_cred_params: AuthenticatorArgsPubCred,
|
||||
exclude_list: *const Ctap2PubKeyCredDescriptors,
|
||||
options: AuthenticatorArgsOptions,
|
||||
pin_ptr: *const c_char,
|
||||
) -> u64 {
|
||||
if mgr.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check buffers.
|
||||
if challenge.ptr.is_null()
|
||||
|| origin_ptr.is_null()
|
||||
|| relying_party_id.is_null()
|
||||
|| user.id_ptr.is_null()
|
||||
|| user.name.is_null()
|
||||
|| exclude_list.is_null()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
let pub_cred_params = match slice::from_raw_parts(pub_cred_params.ptr, pub_cred_params.len)
|
||||
.iter()
|
||||
.map(|x| PublicKeyCredentialParameters::try_from(*x))
|
||||
.collect()
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let pin = if pin_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(Pin::new(&CStr::from_ptr(pin_ptr).to_string_lossy()))
|
||||
};
|
||||
let user = User {
|
||||
id: from_raw(user.id_ptr, user.id_len),
|
||||
name: Some(CStr::from_ptr(user.name).to_string_lossy().to_string()), // TODO(MS): Use to_str() and error out on failure?
|
||||
display_name: None,
|
||||
icon: None,
|
||||
};
|
||||
let rp = RelyingParty {
|
||||
id: CStr::from_ptr(relying_party_id)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
name: None,
|
||||
icon: None,
|
||||
};
|
||||
let origin = CStr::from_ptr(origin_ptr).to_string_lossy().to_string();
|
||||
let challenge = from_raw(challenge.ptr, challenge.len);
|
||||
let exclude_list = (*exclude_list).clone();
|
||||
let force_none_attestation = options.force_none_attestation;
|
||||
|
||||
let (status_tx, status_rx) = channel::<crate::StatusUpdate>();
|
||||
thread::spawn(move || loop {
|
||||
match status_rx.recv() {
|
||||
Ok(r) => {
|
||||
let rb = Box::new(r);
|
||||
status_callback(Box::into_raw(rb));
|
||||
}
|
||||
Err(e) => {
|
||||
status_callback(ptr::null_mut());
|
||||
error!("Error when receiving status update: {:?}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let tid = new_tid();
|
||||
|
||||
let state_callback = StateCallback::<crate::Result<RegisterResult>>::new(Box::new(move |rv| {
|
||||
let res = match rv {
|
||||
Ok(RegisterResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch(
|
||||
"rust_ctap2_mgr_register",
|
||||
2,
|
||||
)),
|
||||
Ok(RegisterResult::CTAP2(mut attestation_object, client_data)) => {
|
||||
if force_none_attestation {
|
||||
attestation_object.att_statement = AttestationStatement::None;
|
||||
}
|
||||
rewrap_register_result(attestation_object, client_data)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
|
||||
callback(tid, Box::into_raw(Box::new(res)));
|
||||
}));
|
||||
|
||||
let ctap_args = RegisterArgsCtap2 {
|
||||
challenge,
|
||||
relying_party: rp,
|
||||
origin,
|
||||
user,
|
||||
pub_cred_params,
|
||||
exclude_list,
|
||||
options: MakeCredentialsOptions {
|
||||
resident_key: options.resident_key.then(|| true),
|
||||
user_verification: options.user_verification.then(|| true),
|
||||
},
|
||||
extensions: Default::default(),
|
||||
pin,
|
||||
};
|
||||
|
||||
let res = (*mgr).register(timeout, ctap_args.into(), status_tx, state_callback);
|
||||
|
||||
if res.is_ok() {
|
||||
tid
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method should not be called on AuthenticatorService handles after
|
||||
/// they've been freed
|
||||
/// Note: `KeyHandles` are used as `PublicKeyCredentialDescriptor`s for the exclude_list
|
||||
/// to keep the API smaller, as they are essentially the same thing.
|
||||
/// `PublicKeyCredentialParameters` in pub_cred_params are represented as i32 with
|
||||
/// their COSE value (see: https://www.iana.org/assignments/cose/cose.xhtml#table-algorithms)
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_mgr_sign(
|
||||
mgr: *mut AuthenticatorService,
|
||||
timeout: u64,
|
||||
callback: Ctap2SignCallback,
|
||||
status_callback: Ctap2StatusUpdateCallback,
|
||||
challenge: AuthenticatorArgsChallenge,
|
||||
relying_party_id: *const c_char,
|
||||
origin_ptr: *const c_char,
|
||||
allow_list: *const Ctap2PubKeyCredDescriptors,
|
||||
options: AuthenticatorArgsOptions,
|
||||
pin_ptr: *const c_char,
|
||||
) -> u64 {
|
||||
if mgr.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check buffers.
|
||||
if challenge.ptr.is_null()
|
||||
|| origin_ptr.is_null()
|
||||
|| relying_party_id.is_null()
|
||||
|| allow_list.is_null()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
let pin = if pin_ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(Pin::new(&CStr::from_ptr(pin_ptr).to_string_lossy()))
|
||||
};
|
||||
let rpid = CStr::from_ptr(relying_party_id)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let origin = CStr::from_ptr(origin_ptr).to_string_lossy().to_string();
|
||||
let challenge = from_raw(challenge.ptr, challenge.len);
|
||||
let allow_list: Vec<_> = (*allow_list).clone();
|
||||
|
||||
let (status_tx, status_rx) = channel::<crate::StatusUpdate>();
|
||||
thread::spawn(move || loop {
|
||||
match status_rx.recv() {
|
||||
Ok(r) => {
|
||||
let rb = Box::new(r);
|
||||
status_callback(Box::into_raw(rb));
|
||||
}
|
||||
Err(e) => {
|
||||
status_callback(ptr::null_mut());
|
||||
error!("Error when receiving status update: {:?}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let single_key_handle = if allow_list.len() == 1 {
|
||||
Some(allow_list.first().unwrap().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let tid = new_tid();
|
||||
let state_callback = StateCallback::<crate::Result<SignResult>>::new(Box::new(move |rv| {
|
||||
let res = match rv {
|
||||
Ok(SignResult::CTAP1(..)) => Err(AuthenticatorError::VersionMismatch(
|
||||
"rust_ctap2_mgr_register",
|
||||
2,
|
||||
)),
|
||||
Ok(SignResult::CTAP2(mut assertion_object, client_data)) => {
|
||||
// The token can omit sending back credentials, if the allow_list had only one
|
||||
// entry. Thus we re-add that here now for all found assertions before handing it out.
|
||||
assertion_object.0.iter_mut().for_each(|x| {
|
||||
x.credentials = x.credentials.clone().or(single_key_handle.clone());
|
||||
});
|
||||
rewrap_sign_result(assertion_object, client_data)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
|
||||
callback(tid, Box::into_raw(Box::new(res)));
|
||||
}));
|
||||
let ctap_args = SignArgsCtap2 {
|
||||
challenge,
|
||||
origin,
|
||||
relying_party_id: rpid,
|
||||
allow_list,
|
||||
options: GetAssertionOptions {
|
||||
user_presence: options.user_presence.then(|| true),
|
||||
user_verification: options.user_verification.then(|| true),
|
||||
},
|
||||
extensions: Default::default(),
|
||||
pin,
|
||||
};
|
||||
|
||||
let res = (*mgr).sign(timeout, ctap_args.into(), status_tx, state_callback);
|
||||
|
||||
if res.is_ok() {
|
||||
tid
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// /// # Safety
|
||||
// ///
|
||||
// /// This method must be used on an actual U2FResult handle
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_register_result_error(res: *const Ctap2RegisterResult) -> u8 {
|
||||
if res.is_null() {
|
||||
return U2FTokenError::Unknown as u8;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok(..) => 0, // No error, the request succeeded.
|
||||
Err(e) => e.as_u2f_errorcode(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_error(res: *const Ctap2SignResult) -> u8 {
|
||||
if res.is_null() {
|
||||
return U2FTokenError::Unknown as u8;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok(..) => 0, // No error, the request succeeded.
|
||||
Err(e) => e.as_u2f_errorcode(),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method should not be called on RegisterResult handles after they've been
|
||||
/// freed or a double-free will occur
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_register_res_free(res: *mut Ctap2RegisterResult) {
|
||||
if !res.is_null() {
|
||||
drop(Box::from_raw(res));
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method should not be called on SignResult handles after they've been
|
||||
/// freed or a double-free will occur
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_res_free(res: *mut Ctap2SignResult) {
|
||||
if !res.is_null() {
|
||||
drop(Box::from_raw(res));
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method should not be called AuthenticatorService handles after they've
|
||||
/// been freed
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_mgr_cancel(mgr: *mut AuthenticatorService) {
|
||||
if !mgr.is_null() {
|
||||
// Ignore return value.
|
||||
let _ = (*mgr).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn client_data_len<T>(
|
||||
res: *const Result<(T, CString), AuthenticatorError>,
|
||||
len: *mut size_t,
|
||||
) -> bool {
|
||||
if res.is_null() || len.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((_, client_data)) => {
|
||||
*len = client_data.as_bytes().len();
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_register_result_client_data_copy()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_register_result_client_data_len(
|
||||
res: *const Ctap2RegisterResult,
|
||||
len: *mut size_t,
|
||||
) -> bool {
|
||||
client_data_len(res, len)
|
||||
}
|
||||
|
||||
/// This function is used to get the length, prior to calling
|
||||
/// rust_ctap2_sign_result_client_data_copy()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_client_data_len(
|
||||
res: *const Ctap2SignResult,
|
||||
len: *mut size_t,
|
||||
) -> bool {
|
||||
client_data_len(res, len)
|
||||
}
|
||||
|
||||
unsafe fn client_data_copy<T>(
|
||||
res: *const Result<(T, CString), AuthenticatorError>,
|
||||
dst: *mut c_char,
|
||||
) -> bool {
|
||||
if dst.is_null() || res.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((_, client_data)) => {
|
||||
ptr::copy_nonoverlapping(client_data.as_ptr(), dst, client_data.as_bytes().len());
|
||||
return true;
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_register_result_client_data_len)
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_register_result_client_data_copy(
|
||||
res: *const Ctap2RegisterResult,
|
||||
dst: *mut c_char,
|
||||
) -> bool {
|
||||
client_data_copy(res, dst)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_register_result_client_data_len)
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_client_data_copy(
|
||||
res: *const Ctap2SignResult,
|
||||
dst: *mut c_char,
|
||||
) -> bool {
|
||||
client_data_copy(res, dst)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get how long the specific item is.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_register_result_attestation_len(
|
||||
res: *const Ctap2RegisterResult,
|
||||
len: *mut size_t,
|
||||
) -> bool {
|
||||
if res.is_null() || len.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Ok((attestation, _)) = &*res {
|
||||
if let Some(item_len) = serde_cbor::to_vec(&attestation).ok().map(|x| x.len()) {
|
||||
*len = item_len;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_register_result_item_len)
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_register_result_attestation_copy(
|
||||
res: *const Ctap2RegisterResult,
|
||||
dst: *mut u8,
|
||||
) -> bool {
|
||||
if res.is_null() || dst.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Ok((attestation, _)) = &*res {
|
||||
if let Ok(item) = serde_cbor::to_vec(&attestation) {
|
||||
ptr::copy_nonoverlapping(item.as_ptr(), dst, item.len());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// This function is used to get how many assertions there are in total
|
||||
/// The returned number can be used as index-maximum to access individual
|
||||
/// fields
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_assertions_len(
|
||||
res: *const Ctap2SignResult,
|
||||
len: *mut size_t,
|
||||
) -> bool {
|
||||
if res.is_null() || len.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((assertions, _)) => {
|
||||
*len = assertions.0.len();
|
||||
return true;
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_result_item_len(assertion: &Assertion, item_idx: u8) -> Option<usize> {
|
||||
match item_idx {
|
||||
SIGN_RESULT_PUBKEY_CRED_ID => assertion.credentials.as_ref().map(|x| x.id.len()),
|
||||
// This is inefficent! Converting twice here. Once for len, once for copy
|
||||
SIGN_RESULT_AUTH_DATA => assertion.auth_data.to_vec().ok().map(|x| x.len()),
|
||||
SIGN_RESULT_SIGNATURE => Some(assertion.signature.len()),
|
||||
SIGN_RESULT_USER_ID => assertion.user.as_ref().map(|u| u.id.len()),
|
||||
SIGN_RESULT_USER_NAME => assertion
|
||||
.user
|
||||
.as_ref()
|
||||
.map(|u| {
|
||||
u.display_name
|
||||
.as_ref()
|
||||
.or(u.name.as_ref())
|
||||
.map(|n| n.as_bytes().len())
|
||||
})
|
||||
.flatten(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_item_contains(
|
||||
res: *const Ctap2SignResult,
|
||||
assertion_idx: usize,
|
||||
item_idx: u8,
|
||||
) -> bool {
|
||||
if res.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((assertions, _)) => {
|
||||
if assertion_idx >= assertions.0.len() {
|
||||
return false;
|
||||
}
|
||||
if item_idx == SIGN_RESULT_AUTH_DATA {
|
||||
// Short-cut to avoid serializing auth_data
|
||||
return true;
|
||||
}
|
||||
sign_result_item_len(&assertions.0[assertion_idx], item_idx).is_some()
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get how long the specific item is.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_item_len(
|
||||
res: *const Ctap2SignResult,
|
||||
assertion_idx: usize,
|
||||
item_idx: u8,
|
||||
len: *mut size_t,
|
||||
) -> bool {
|
||||
if res.is_null() || len.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((assertions, _)) => {
|
||||
if assertion_idx >= assertions.0.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(item_len) = sign_result_item_len(&assertions.0[assertion_idx], item_idx) {
|
||||
*len = item_len;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn sign_result_item_copy(assertion: &Assertion, item_idx: u8, dst: *mut u8) -> bool {
|
||||
if dst.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let tmp_val;
|
||||
let item = match item_idx {
|
||||
SIGN_RESULT_PUBKEY_CRED_ID => assertion.credentials.as_ref().map(|x| x.id.as_ref()),
|
||||
// This is inefficent! Converting twice here. Once for len, once for copy
|
||||
SIGN_RESULT_AUTH_DATA => {
|
||||
tmp_val = assertion.auth_data.to_vec().ok();
|
||||
tmp_val.as_ref().map(|x| x.as_ref())
|
||||
}
|
||||
SIGN_RESULT_SIGNATURE => Some(assertion.signature.as_ref()),
|
||||
SIGN_RESULT_USER_ID => assertion.user.as_ref().map(|u| u.id.as_ref()),
|
||||
SIGN_RESULT_USER_NAME => assertion
|
||||
.user
|
||||
.as_ref()
|
||||
.map(|u| {
|
||||
u.display_name
|
||||
.as_ref()
|
||||
.or(u.name.as_ref())
|
||||
.map(|n| n.as_bytes().as_ref())
|
||||
})
|
||||
.flatten(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
ptr::copy_nonoverlapping(item.as_ptr(), dst, item.len());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_sign_result_item_len)
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_item_copy(
|
||||
res: *const Ctap2SignResult,
|
||||
assertion_idx: usize,
|
||||
item_idx: u8,
|
||||
dst: *mut u8,
|
||||
) -> bool {
|
||||
if res.is_null() || dst.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((assertions, _)) => {
|
||||
if assertion_idx >= assertions.0.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
sign_result_item_copy(&assertions.0[assertion_idx], item_idx, dst)
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_contains_username(
|
||||
res: *const Ctap2SignResult,
|
||||
assertion_idx: usize,
|
||||
) -> bool {
|
||||
if res.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((assertions, _)) => {
|
||||
if assertion_idx >= assertions.0.len() {
|
||||
return false;
|
||||
}
|
||||
assertions.0[assertion_idx]
|
||||
.user
|
||||
.as_ref()
|
||||
.map(|u| u.display_name.as_ref().or(u.name.as_ref()))
|
||||
.is_some()
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get how long the specific username is.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_username_len(
|
||||
res: *const Ctap2SignResult,
|
||||
assertion_idx: usize,
|
||||
len: *mut size_t,
|
||||
) -> bool {
|
||||
if res.is_null() || len.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((assertions, _)) => {
|
||||
if assertion_idx >= assertions.0.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(name_len) = assertions.0[assertion_idx]
|
||||
.user
|
||||
.as_ref()
|
||||
.map(|u| u.display_name.as_ref().or(u.name.as_ref()))
|
||||
.flatten()
|
||||
.map(|x| x.as_bytes().len())
|
||||
{
|
||||
*len = name_len;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_sign_result_username_len)
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_sign_result_username_copy(
|
||||
res: *const Ctap2SignResult,
|
||||
assertion_idx: usize,
|
||||
dst: *mut c_char,
|
||||
) -> bool {
|
||||
if res.is_null() || dst.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
Ok((assertions, _)) => {
|
||||
if assertion_idx >= assertions.0.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(name) = assertions.0[assertion_idx]
|
||||
.user
|
||||
.as_ref()
|
||||
.map(|u| u.display_name.as_ref().or(u.name.as_ref()))
|
||||
.flatten()
|
||||
.map(|u| CString::new(u.clone()).ok())
|
||||
.flatten()
|
||||
{
|
||||
ptr::copy_nonoverlapping(name.as_ptr(), dst, name.as_bytes().len());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is used to get how long the JSON-representation of a status update is.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_status_update_len(
|
||||
res: *const StatusUpdate,
|
||||
len: *mut size_t,
|
||||
) -> bool {
|
||||
if res.is_null() || len.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match serde_json::to_string(&*res) {
|
||||
Ok(s) => {
|
||||
*len = s.len();
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to parse {:?} into json: {:?}", &*res, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This method does not ensure anything about dst before copying, so
|
||||
/// ensure it is long enough (using rust_ctap2_status_update_len)
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_status_update_copy_json(
|
||||
res: *const StatusUpdate,
|
||||
dst: *mut c_char,
|
||||
) -> bool {
|
||||
if res.is_null() || dst.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match serde_json::to_string(&*res) {
|
||||
Ok(s) => {
|
||||
if let Ok(cs) = CString::new(s) {
|
||||
ptr::copy_nonoverlapping(cs.as_ptr(), dst, cs.as_bytes().len());
|
||||
true
|
||||
} else {
|
||||
error!("Failed to convert String to CString");
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to parse {:?} into json: {:?}", &*res, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// We copy the pin, so it is the callers responsibility to free the argument
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_status_update_send_pin(
|
||||
res: *const StatusUpdate,
|
||||
c_pin: *mut c_char,
|
||||
) -> bool {
|
||||
if res.is_null() || c_pin.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match &*res {
|
||||
StatusUpdate::PinError(_, sender) => {
|
||||
if let Ok(pin) = CStr::from_ptr(c_pin).to_str() {
|
||||
sender
|
||||
.send(Pin::new(pin))
|
||||
.map_err(|e| {
|
||||
error!("Failed to send PIN to device-thread");
|
||||
e
|
||||
})
|
||||
.is_ok()
|
||||
} else {
|
||||
error!("Failed to convert PIN from c_char to String");
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Wrong state!");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This function frees the memory of res!
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rust_ctap2_destroy_status_update_res(res: *mut StatusUpdate) -> bool {
|
||||
if res.is_null() {
|
||||
return false;
|
||||
}
|
||||
// Dropping it when we go out of scope
|
||||
drop(Box::from_raw(res));
|
||||
true
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
* 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 use crate::ctap2::commands::{client_pin::PinError, CommandError};
|
||||
pub use crate::transport::errors::HIDError;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
|
@ -9,6 +11,12 @@ use std::sync::mpsc;
|
|||
// This composite error type is patterned from Phil Daniels' blog:
|
||||
// https://www.philipdaniels.com/blog/2019/defining-rust-error-types/
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UnsupportedOption {
|
||||
MaxPinLength,
|
||||
HmacSecret,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthenticatorError {
|
||||
// Errors from external libraries...
|
||||
|
@ -20,12 +28,23 @@ pub enum AuthenticatorError {
|
|||
InternalError(String),
|
||||
U2FToken(U2FTokenError),
|
||||
Custom(String),
|
||||
VersionMismatch(&'static str, u32),
|
||||
HIDError(HIDError),
|
||||
CryptoError,
|
||||
PinError(PinError),
|
||||
UnsupportedOption(UnsupportedOption),
|
||||
}
|
||||
|
||||
impl AuthenticatorError {
|
||||
pub fn as_u2f_errorcode(&self) -> u8 {
|
||||
match *self {
|
||||
AuthenticatorError::U2FToken(ref err) => *err as u8,
|
||||
// TODO: This is somewhat ugly, as we hardcode the error code here, instead of using the
|
||||
// const defined in `u2fhid-capi.h`, which we should.
|
||||
AuthenticatorError::PinError(PinError::PinRequired) => 6u8,
|
||||
AuthenticatorError::PinError(PinError::InvalidPin(_)) => 7u8,
|
||||
AuthenticatorError::PinError(PinError::PinAuthBlocked) => 8u8,
|
||||
AuthenticatorError::PinError(PinError::PinBlocked) => 9u8,
|
||||
_ => U2FTokenError::Unknown as u8,
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +69,19 @@ impl fmt::Display for AuthenticatorError {
|
|||
write!(f, "A u2f token error occurred {:?}", err)
|
||||
}
|
||||
AuthenticatorError::Custom(ref err) => write!(f, "A custom error occurred {:?}", err),
|
||||
AuthenticatorError::VersionMismatch(manager, version) => write!(
|
||||
f,
|
||||
"{} expected arguments of version CTAP{}",
|
||||
manager, version
|
||||
),
|
||||
AuthenticatorError::HIDError(ref e) => write!(f, "Device error: {}", e),
|
||||
AuthenticatorError::CryptoError => {
|
||||
write!(f, "The cryptography implementation encountered an error")
|
||||
}
|
||||
AuthenticatorError::PinError(ref e) => write!(f, "PIN Error: {}", e),
|
||||
AuthenticatorError::UnsupportedOption(ref e) => {
|
||||
write!(f, "Unsupported option: {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +92,18 @@ impl From<io::Error> for AuthenticatorError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<HIDError> for AuthenticatorError {
|
||||
fn from(err: HIDError) -> AuthenticatorError {
|
||||
AuthenticatorError::HIDError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommandError> for AuthenticatorError {
|
||||
fn from(err: CommandError) -> AuthenticatorError {
|
||||
AuthenticatorError::HIDError(HIDError::Command(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<mpsc::SendError<T>> for AuthenticatorError {
|
||||
fn from(err: mpsc::SendError<T>) -> AuthenticatorError {
|
||||
AuthenticatorError::InternalError(err.to_string())
|
||||
|
|
|
@ -5,53 +5,15 @@
|
|||
#[macro_use]
|
||||
mod util;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
|
||||
pub mod hidproto;
|
||||
|
||||
#[cfg(any(target_os = "linux"))]
|
||||
extern crate libudev;
|
||||
|
||||
#[cfg(any(target_os = "linux"))]
|
||||
#[path = "linux/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(any(target_os = "freebsd"))]
|
||||
extern crate devd_rs;
|
||||
|
||||
#[cfg(any(target_os = "freebsd"))]
|
||||
#[path = "freebsd/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(any(target_os = "netbsd"))]
|
||||
#[path = "netbsd/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(any(target_os = "openbsd"))]
|
||||
#[path = "openbsd/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(any(target_os = "macos"))]
|
||||
extern crate core_foundation;
|
||||
|
||||
#[cfg(any(target_os = "macos"))]
|
||||
#[path = "macos/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(any(target_os = "windows"))]
|
||||
#[path = "windows/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "macos",
|
||||
target_os = "windows"
|
||||
)))]
|
||||
#[path = "stub/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
extern crate libc;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
@ -73,10 +35,26 @@ pub use crate::manager::U2FManager;
|
|||
mod capi;
|
||||
pub use crate::capi::*;
|
||||
|
||||
pub mod ctap2;
|
||||
pub use ctap2::attestation::AttestationObject;
|
||||
pub use ctap2::client_data::{CollectedClientData, CollectedClientDataWrapper};
|
||||
pub use ctap2::commands::client_pin::{Pin, PinError};
|
||||
pub use ctap2::AssertionObject;
|
||||
|
||||
mod ctap2_capi;
|
||||
pub use crate::ctap2_capi::*;
|
||||
|
||||
pub mod errors;
|
||||
pub mod statecallback;
|
||||
mod transport;
|
||||
mod virtualdevices;
|
||||
|
||||
mod status_update;
|
||||
pub use status_update::*;
|
||||
|
||||
mod crypto;
|
||||
pub use crypto::COSEAlgorithm;
|
||||
|
||||
// Keep this in sync with the constants in u2fhid-capi.h.
|
||||
bitflags! {
|
||||
pub struct RegisterFlags: u64 {
|
||||
|
@ -98,25 +76,28 @@ bitflags! {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KeyHandle {
|
||||
pub credential: Vec<u8>,
|
||||
pub transports: AuthenticatorTransports,
|
||||
}
|
||||
|
||||
pub type AppId = Vec<u8>;
|
||||
pub type RegisterResult = (Vec<u8>, u2ftypes::U2FDeviceInfo);
|
||||
pub type SignResult = (AppId, Vec<u8>, Vec<u8>, u2ftypes::U2FDeviceInfo);
|
||||
|
||||
pub enum RegisterResult {
|
||||
CTAP1(Vec<u8>, u2ftypes::U2FDeviceInfo),
|
||||
CTAP2(AttestationObject, CollectedClientDataWrapper),
|
||||
}
|
||||
|
||||
pub enum SignResult {
|
||||
CTAP1(AppId, Vec<u8>, Vec<u8>, u2ftypes::U2FDeviceInfo),
|
||||
CTAP2(AssertionObject, CollectedClientDataWrapper),
|
||||
}
|
||||
|
||||
pub type ResetResult = ();
|
||||
|
||||
pub type Result<T> = std::result::Result<T, errors::AuthenticatorError>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StatusUpdate {
|
||||
DeviceAvailable { dev_info: u2ftypes::U2FDeviceInfo },
|
||||
DeviceUnavailable { dev_info: u2ftypes::U2FDeviceInfo },
|
||||
Success { dev_info: u2ftypes::U2FDeviceInfo },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate assert_matches;
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
extern crate libc;
|
||||
|
||||
use std::ffi::{CString, OsString};
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::prelude::*;
|
||||
|
||||
use crate::consts::CID_BROADCAST;
|
||||
use crate::platform::{hidraw, monitor};
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use crate::util::from_unix_result;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Device {
|
||||
path: OsString,
|
||||
fd: libc::c_int,
|
||||
in_rpt_size: usize,
|
||||
out_rpt_size: usize,
|
||||
cid: [u8; 4],
|
||||
dev_info: Option<U2FDeviceInfo>,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn new(path: OsString) -> io::Result<Self> {
|
||||
let cstr = CString::new(path.as_bytes())?;
|
||||
let fd = unsafe { libc::open(cstr.as_ptr(), libc::O_RDWR) };
|
||||
let fd = from_unix_result(fd)?;
|
||||
let (in_rpt_size, out_rpt_size) = hidraw::read_hid_rpt_sizes_or_defaults(fd);
|
||||
Ok(Self {
|
||||
path,
|
||||
fd,
|
||||
in_rpt_size,
|
||||
out_rpt_size,
|
||||
cid: CID_BROADCAST,
|
||||
dev_info: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_u2f(&self) -> bool {
|
||||
hidraw::is_u2f_device(self.fd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Device {
|
||||
fn drop(&mut self) {
|
||||
// Close the fd, ignore any errors.
|
||||
let _ = unsafe { libc::close(self.fd) };
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other: &Device) -> bool {
|
||||
self.path == other.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Device {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let bufp = buf.as_mut_ptr() as *mut libc::c_void;
|
||||
let rv = unsafe { libc::read(self.fd, bufp, buf.len()) };
|
||||
from_unix_result(rv as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Device {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let bufp = buf.as_ptr() as *const libc::c_void;
|
||||
let rv = unsafe { libc::write(self.fd, bufp, buf.len()) };
|
||||
from_unix_result(rv as usize)
|
||||
}
|
||||
|
||||
// USB HID writes don't buffer, so this will be a nop.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl U2FDevice for Device {
|
||||
fn get_cid(&self) -> &[u8; 4] {
|
||||
&self.cid
|
||||
}
|
||||
|
||||
fn set_cid(&mut self, cid: [u8; 4]) {
|
||||
self.cid = cid;
|
||||
}
|
||||
|
||||
fn in_rpt_size(&self) -> usize {
|
||||
self.in_rpt_size
|
||||
}
|
||||
|
||||
fn out_rpt_size(&self) -> usize {
|
||||
self.out_rpt_size
|
||||
}
|
||||
|
||||
fn get_property(&self, prop_name: &str) -> io::Result<String> {
|
||||
monitor::get_property_linux(&self.path, prop_name)
|
||||
}
|
||||
|
||||
fn get_device_info(&self) -> U2FDeviceInfo {
|
||||
// unwrap is okay, as dev_info must have already been set, else
|
||||
// a programmer error
|
||||
self.dev_info.clone().unwrap()
|
||||
}
|
||||
|
||||
fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
|
||||
self.dev_info = Some(dev_info);
|
||||
}
|
||||
}
|
|
@ -2,28 +2,40 @@
|
|||
* 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::authenticatorservice::AuthenticatorTransport;
|
||||
use crate::authenticatorservice::{RegisterArgs, RegisterArgsCtap1, SignArgs};
|
||||
use crate::consts::PARAMETER_SIZE;
|
||||
use crate::crypto::COSEAlgorithm;
|
||||
use crate::ctap2::client_data::{CollectedClientData, WebauthnType};
|
||||
use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions};
|
||||
use crate::ctap2::commands::make_credentials::MakeCredentials;
|
||||
use crate::ctap2::commands::make_credentials::MakeCredentialsOptions;
|
||||
use crate::ctap2::server::{
|
||||
PublicKeyCredentialParameters, RelyingParty, RelyingPartyWrapper, RpIdHash,
|
||||
};
|
||||
use crate::errors::*;
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::statemachine::{StateMachine, StateMachineCtap2};
|
||||
use crate::{Pin, SignFlags};
|
||||
use runloop::RunLoop;
|
||||
use std::io;
|
||||
use std::sync::mpsc::{channel, RecvTimeoutError, Sender};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::authenticatorservice::AuthenticatorTransport;
|
||||
use crate::consts::PARAMETER_SIZE;
|
||||
use crate::errors::*;
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::statemachine::StateMachine;
|
||||
use runloop::RunLoop;
|
||||
|
||||
enum QueueAction {
|
||||
Register {
|
||||
flags: crate::RegisterFlags,
|
||||
RegisterCtap1 {
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: crate::AppId,
|
||||
key_handles: Vec<crate::KeyHandle>,
|
||||
ctap_args: RegisterArgsCtap1,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
},
|
||||
Sign {
|
||||
RegisterCtap2 {
|
||||
timeout: u64,
|
||||
make_credentials: MakeCredentials,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
},
|
||||
SignCtap1 {
|
||||
flags: crate::SignFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
|
@ -32,7 +44,24 @@ enum QueueAction {
|
|||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
},
|
||||
SignCtap2 {
|
||||
timeout: u64,
|
||||
get_assertion: GetAssertion,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
},
|
||||
Cancel,
|
||||
Reset {
|
||||
timeout: u64,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
},
|
||||
SetPin {
|
||||
timeout: u64,
|
||||
new_pin: Pin,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct U2FManager {
|
||||
|
@ -50,27 +79,24 @@ impl U2FManager {
|
|||
|
||||
while alive() {
|
||||
match rx.recv_timeout(Duration::from_millis(50)) {
|
||||
Ok(QueueAction::Register {
|
||||
flags,
|
||||
Ok(QueueAction::RegisterCtap1 {
|
||||
timeout,
|
||||
challenge,
|
||||
application,
|
||||
key_handles,
|
||||
ctap_args,
|
||||
status,
|
||||
callback,
|
||||
}) => {
|
||||
// This must not block, otherwise we can't cancel.
|
||||
sm.register(
|
||||
flags,
|
||||
ctap_args.flags,
|
||||
timeout,
|
||||
challenge,
|
||||
application,
|
||||
key_handles,
|
||||
ctap_args.challenge,
|
||||
ctap_args.application,
|
||||
ctap_args.key_handles,
|
||||
status,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
Ok(QueueAction::Sign {
|
||||
Ok(QueueAction::SignCtap1 {
|
||||
flags,
|
||||
timeout,
|
||||
challenge,
|
||||
|
@ -95,6 +121,17 @@ impl U2FManager {
|
|||
// polling thread before the old one has shut down.
|
||||
sm.cancel();
|
||||
}
|
||||
Ok(QueueAction::RegisterCtap2 { .. }) => {
|
||||
// TODO(MS): What to do here? Error out? Silently ignore?
|
||||
unimplemented!();
|
||||
}
|
||||
Ok(QueueAction::SignCtap2 { .. }) => {
|
||||
// TODO(MS): What to do here? Error out? Silently ignore?
|
||||
unimplemented!();
|
||||
}
|
||||
Ok(QueueAction::Reset { .. }) | Ok(QueueAction::SetPin { .. }) => {
|
||||
unimplemented!();
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => {
|
||||
break;
|
||||
}
|
||||
|
@ -113,30 +150,30 @@ impl U2FManager {
|
|||
impl AuthenticatorTransport for U2FManager {
|
||||
fn register(
|
||||
&mut self,
|
||||
flags: crate::RegisterFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
application: crate::AppId,
|
||||
key_handles: Vec<crate::KeyHandle>,
|
||||
ctap_args: RegisterArgs,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
) -> crate::Result<()> {
|
||||
if challenge.len() != PARAMETER_SIZE || application.len() != PARAMETER_SIZE {
|
||||
let args = match ctap_args {
|
||||
RegisterArgs::CTAP1(args) => args,
|
||||
RegisterArgs::CTAP2(_) => {
|
||||
return Err(AuthenticatorError::VersionMismatch("U2FManager", 1));
|
||||
}
|
||||
};
|
||||
if args.challenge.len() != PARAMETER_SIZE || args.application.len() != PARAMETER_SIZE {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
|
||||
for key_handle in &key_handles {
|
||||
if key_handle.credential.len() > 256 {
|
||||
for key_handle in &args.key_handles {
|
||||
if key_handle.credential.len() >= 256 {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
}
|
||||
|
||||
let action = QueueAction::Register {
|
||||
flags,
|
||||
let action = QueueAction::RegisterCtap1 {
|
||||
timeout,
|
||||
challenge,
|
||||
application,
|
||||
key_handles,
|
||||
ctap_args: args,
|
||||
status,
|
||||
callback,
|
||||
};
|
||||
|
@ -145,40 +182,44 @@ impl AuthenticatorTransport for U2FManager {
|
|||
|
||||
fn sign(
|
||||
&mut self,
|
||||
flags: crate::SignFlags,
|
||||
timeout: u64,
|
||||
challenge: Vec<u8>,
|
||||
app_ids: Vec<crate::AppId>,
|
||||
key_handles: Vec<crate::KeyHandle>,
|
||||
ctap_args: SignArgs,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
) -> crate::Result<()> {
|
||||
if challenge.len() != PARAMETER_SIZE {
|
||||
let args = match ctap_args {
|
||||
SignArgs::CTAP1(args) => args,
|
||||
SignArgs::CTAP2(_) => {
|
||||
return Err(AuthenticatorError::VersionMismatch("U2FManager", 1));
|
||||
}
|
||||
};
|
||||
|
||||
if args.challenge.len() != PARAMETER_SIZE {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
|
||||
if app_ids.is_empty() {
|
||||
if args.app_ids.is_empty() {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
|
||||
for app_id in &app_ids {
|
||||
for app_id in &args.app_ids {
|
||||
if app_id.len() != PARAMETER_SIZE {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
}
|
||||
|
||||
for key_handle in &key_handles {
|
||||
if key_handle.credential.len() > 256 {
|
||||
for key_handle in &args.key_handles {
|
||||
if key_handle.credential.len() >= 256 {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
}
|
||||
|
||||
let action = QueueAction::Sign {
|
||||
flags,
|
||||
let action = QueueAction::SignCtap1 {
|
||||
flags: args.flags,
|
||||
timeout,
|
||||
challenge,
|
||||
app_ids,
|
||||
key_handles,
|
||||
challenge: args.challenge,
|
||||
app_ids: args.app_ids,
|
||||
key_handles: args.key_handles,
|
||||
status,
|
||||
callback,
|
||||
};
|
||||
|
@ -188,6 +229,34 @@ impl AuthenticatorTransport for U2FManager {
|
|||
fn cancel(&mut self) -> crate::Result<()> {
|
||||
Ok(self.tx.send(QueueAction::Cancel)?)
|
||||
}
|
||||
|
||||
fn reset(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()> {
|
||||
Ok(self.tx.send(QueueAction::Reset {
|
||||
timeout,
|
||||
status,
|
||||
callback,
|
||||
})?)
|
||||
}
|
||||
|
||||
fn set_pin(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
new_pin: Pin,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()> {
|
||||
Ok(self.tx.send(QueueAction::SetPin {
|
||||
timeout,
|
||||
new_pin,
|
||||
status,
|
||||
callback,
|
||||
})?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for U2FManager {
|
||||
|
@ -195,3 +264,313 @@ impl Drop for U2FManager {
|
|||
self.queue.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Manager {
|
||||
queue: RunLoop,
|
||||
tx: Sender<QueueAction>,
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
let (tx, rx) = channel();
|
||||
|
||||
// Start a new work queue thread.
|
||||
let queue = RunLoop::new(move |alive| {
|
||||
let mut sm = StateMachineCtap2::new();
|
||||
|
||||
while alive() {
|
||||
match rx.recv_timeout(Duration::from_millis(50)) {
|
||||
Ok(QueueAction::RegisterCtap2 {
|
||||
timeout,
|
||||
make_credentials,
|
||||
status,
|
||||
callback,
|
||||
}) => {
|
||||
// This must not block, otherwise we can't cancel.
|
||||
sm.register(timeout, make_credentials, status, callback);
|
||||
}
|
||||
|
||||
Ok(QueueAction::SignCtap2 {
|
||||
timeout,
|
||||
get_assertion,
|
||||
status,
|
||||
callback,
|
||||
}) => {
|
||||
// This must not block, otherwise we can't cancel.
|
||||
sm.sign(timeout, get_assertion, status, callback);
|
||||
}
|
||||
|
||||
Ok(QueueAction::Cancel) => {
|
||||
// Cancelling must block so that we don't start a new
|
||||
// polling thread before the old one has shut down.
|
||||
sm.cancel();
|
||||
}
|
||||
|
||||
Ok(QueueAction::Reset {
|
||||
timeout,
|
||||
status,
|
||||
callback,
|
||||
}) => {
|
||||
// Reset the token: Delete all keypairs, reset PIN
|
||||
sm.reset(timeout, status, callback);
|
||||
}
|
||||
|
||||
Ok(QueueAction::SetPin {
|
||||
timeout,
|
||||
new_pin,
|
||||
status,
|
||||
callback,
|
||||
}) => {
|
||||
// This must not block, otherwise we can't cancel.
|
||||
sm.set_pin(timeout, new_pin, status, callback);
|
||||
}
|
||||
|
||||
Ok(QueueAction::RegisterCtap1 {
|
||||
timeout: _,
|
||||
ctap_args: _,
|
||||
status: _,
|
||||
callback: _,
|
||||
}) => {
|
||||
// TODO(MS): Remove QueueAction::RegisterCtap1 once U2FManager is deleted.
|
||||
// The repackaging from CTAP1 to CTAP2 happens in self.register()
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
Ok(QueueAction::SignCtap1 {
|
||||
timeout: _,
|
||||
callback: _,
|
||||
flags: _,
|
||||
challenge: _,
|
||||
app_ids: _,
|
||||
key_handles: _,
|
||||
status: _,
|
||||
}) => {
|
||||
// TODO(MS): Remove QueueAction::SignCtap1 once U2FManager is deleted.
|
||||
// The repackaging from CTAP1 to CTAP2 happens in self.sign()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
Err(RecvTimeoutError::Disconnected) => {
|
||||
break;
|
||||
}
|
||||
|
||||
_ => { /* continue */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel any ongoing activity.
|
||||
sm.cancel();
|
||||
})?;
|
||||
|
||||
Ok(Self { queue, tx })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Manager {
|
||||
fn drop(&mut self) {
|
||||
self.queue.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticatorTransport for Manager {
|
||||
fn register(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
ctap_args: RegisterArgs,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
) -> Result<(), AuthenticatorError> {
|
||||
let make_credentials = match ctap_args {
|
||||
RegisterArgs::CTAP2(args) => {
|
||||
let client_data = CollectedClientData {
|
||||
webauthn_type: WebauthnType::Create,
|
||||
challenge: args.challenge.into(),
|
||||
origin: args.origin,
|
||||
cross_origin: false,
|
||||
token_binding: None,
|
||||
};
|
||||
|
||||
MakeCredentials::new(
|
||||
client_data,
|
||||
RelyingPartyWrapper::Data(args.relying_party),
|
||||
Some(args.user),
|
||||
args.pub_cred_params,
|
||||
args.exclude_list,
|
||||
args.options,
|
||||
args.extensions,
|
||||
args.pin,
|
||||
// pin_auth will be filled in Statemachine, once we have a device
|
||||
)
|
||||
}
|
||||
RegisterArgs::CTAP1(args) => {
|
||||
let client_data = CollectedClientData {
|
||||
webauthn_type: WebauthnType::Create,
|
||||
challenge: args.challenge.into(),
|
||||
origin: String::new(),
|
||||
cross_origin: false,
|
||||
token_binding: None,
|
||||
};
|
||||
|
||||
MakeCredentials::new(
|
||||
client_data,
|
||||
RelyingPartyWrapper::Hash(RpIdHash::from(&args.application)?),
|
||||
None,
|
||||
vec![PublicKeyCredentialParameters {
|
||||
alg: COSEAlgorithm::ES256,
|
||||
}],
|
||||
args.key_handles
|
||||
.iter()
|
||||
.map(|k| k.into())
|
||||
.collect::<Vec<_>>(),
|
||||
MakeCredentialsOptions {
|
||||
resident_key: None,
|
||||
user_verification: None,
|
||||
},
|
||||
Default::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}?;
|
||||
|
||||
let action = QueueAction::RegisterCtap2 {
|
||||
timeout,
|
||||
make_credentials,
|
||||
status,
|
||||
callback,
|
||||
};
|
||||
Ok(self.tx.send(action)?)
|
||||
}
|
||||
|
||||
fn sign(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
ctap_args: SignArgs,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
) -> crate::Result<()> {
|
||||
match ctap_args {
|
||||
SignArgs::CTAP1(args) => {
|
||||
if args.challenge.len() != PARAMETER_SIZE {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
|
||||
if args.app_ids.is_empty() {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
|
||||
let client_data = CollectedClientData {
|
||||
webauthn_type: WebauthnType::Get,
|
||||
challenge: args.challenge.into(),
|
||||
origin: String::new(),
|
||||
cross_origin: false,
|
||||
token_binding: None,
|
||||
};
|
||||
let options = if args.flags == SignFlags::empty() {
|
||||
GetAssertionOptions::default()
|
||||
} else {
|
||||
GetAssertionOptions {
|
||||
user_verification: Some(
|
||||
args.flags.contains(SignFlags::REQUIRE_USER_VERIFICATION),
|
||||
),
|
||||
..GetAssertionOptions::default()
|
||||
}
|
||||
};
|
||||
|
||||
for app_id in &args.app_ids {
|
||||
if app_id.len() != PARAMETER_SIZE {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
for key_handle in &args.key_handles {
|
||||
if key_handle.credential.len() >= 256 {
|
||||
return Err(AuthenticatorError::InvalidRelyingPartyInput);
|
||||
}
|
||||
let rp = RelyingPartyWrapper::Hash(RpIdHash::from(&app_id)?);
|
||||
|
||||
let allow_list = vec![key_handle.into()];
|
||||
|
||||
let get_assertion = GetAssertion::new(
|
||||
client_data.clone(),
|
||||
rp,
|
||||
allow_list,
|
||||
options,
|
||||
Default::default(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let action = QueueAction::SignCtap2 {
|
||||
timeout,
|
||||
get_assertion,
|
||||
status: status.clone(),
|
||||
callback: callback.clone(),
|
||||
};
|
||||
self.tx.send(action)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SignArgs::CTAP2(args) => {
|
||||
let client_data = CollectedClientData {
|
||||
webauthn_type: WebauthnType::Get,
|
||||
challenge: args.challenge.into(),
|
||||
origin: args.origin,
|
||||
cross_origin: false,
|
||||
token_binding: None,
|
||||
};
|
||||
|
||||
let get_assertion = GetAssertion::new(
|
||||
client_data.clone(),
|
||||
RelyingPartyWrapper::Data(RelyingParty {
|
||||
id: args.relying_party_id,
|
||||
name: None,
|
||||
icon: None,
|
||||
}),
|
||||
args.allow_list,
|
||||
args.options,
|
||||
args.extensions,
|
||||
args.pin,
|
||||
)?;
|
||||
|
||||
let action = QueueAction::SignCtap2 {
|
||||
timeout,
|
||||
get_assertion,
|
||||
status,
|
||||
callback,
|
||||
};
|
||||
self.tx.send(action)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel(&mut self) -> Result<(), AuthenticatorError> {
|
||||
Ok(self.tx.send(QueueAction::Cancel)?)
|
||||
}
|
||||
|
||||
fn reset(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> Result<(), AuthenticatorError> {
|
||||
Ok(self.tx.send(QueueAction::Reset {
|
||||
timeout,
|
||||
status,
|
||||
callback,
|
||||
})?)
|
||||
}
|
||||
|
||||
fn set_pin(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
new_pin: Pin,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) -> crate::Result<()> {
|
||||
Ok(self.tx.send(QueueAction::SetPin {
|
||||
timeout,
|
||||
new_pin,
|
||||
status,
|
||||
callback,
|
||||
})?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,15 +3,24 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::consts::PARAMETER_SIZE;
|
||||
use crate::errors;
|
||||
use crate::platform::device::Device;
|
||||
use crate::platform::transaction::Transaction;
|
||||
use crate::ctap2::commands::client_pin::{ChangeExistingPin, Pin, PinError, SetNewPin};
|
||||
use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionResult};
|
||||
use crate::ctap2::commands::make_credentials::{MakeCredentials, MakeCredentialsResult};
|
||||
use crate::ctap2::commands::reset::Reset;
|
||||
use crate::ctap2::commands::{
|
||||
repackage_pin_errors, CommandError, PinAuthCommand, Request, StatusCode,
|
||||
};
|
||||
use crate::errors::{self, AuthenticatorError, UnsupportedOption};
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::transport::device_selector::{
|
||||
BlinkResult, Device, DeviceBuildParameters, DeviceCommand, DeviceSelectorEvent,
|
||||
};
|
||||
use crate::transport::platform::transaction::Transaction;
|
||||
use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, Nonce};
|
||||
use crate::u2fprotocol::{u2f_init_device, u2f_is_keyhandle_valid, u2f_register, u2f_sign};
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Mutex;
|
||||
use crate::{send_status, RegisterResult, SignResult, StatusUpdate};
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -44,18 +53,6 @@ where
|
|||
(&app_ids[0], vec![])
|
||||
}
|
||||
|
||||
fn send_status(status_mutex: &Mutex<Sender<crate::StatusUpdate>>, msg: crate::StatusUpdate) {
|
||||
match status_mutex.lock() {
|
||||
Ok(s) => match s.send(msg) {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("Couldn't send status: {:?}", e),
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Couldn't obtain status mutex: {:?}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StateMachine {
|
||||
transaction: Option<Transaction>,
|
||||
|
@ -80,79 +77,88 @@ impl StateMachine {
|
|||
self.cancel();
|
||||
|
||||
let cbc = callback.clone();
|
||||
let status_mutex = Mutex::new(status);
|
||||
|
||||
let transaction = Transaction::new(timeout, cbc.clone(), move |info, alive| {
|
||||
// Create a new device.
|
||||
let dev = &mut match Device::new(info) {
|
||||
Ok(dev) => dev,
|
||||
_ => return,
|
||||
};
|
||||
let transaction = Transaction::new(
|
||||
timeout,
|
||||
cbc.clone(),
|
||||
status,
|
||||
move |info, _, status, alive| {
|
||||
// Create a new device.
|
||||
let dev = &mut match Device::new(info) {
|
||||
Ok(dev) => dev,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// Try initializing it.
|
||||
if !dev.is_u2f() || !u2f_init_device(dev) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We currently support none of the authenticator selection
|
||||
// criteria because we can't ask tokens whether they do support
|
||||
// those features. If flags are set, ignore all tokens for now.
|
||||
//
|
||||
// Technically, this is a ConstraintError because we shouldn't talk
|
||||
// to this authenticator in the first place. But the result is the
|
||||
// same anyway.
|
||||
if !flags.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
send_status(
|
||||
&status_mutex,
|
||||
crate::StatusUpdate::DeviceAvailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
|
||||
// Iterate the exclude list and see if there are any matches.
|
||||
// If so, we'll keep polling the device anyway to test for user
|
||||
// consent, to be consistent with CTAP2 device behavior.
|
||||
let excluded = key_handles.iter().any(|key_handle| {
|
||||
is_valid_transport(key_handle.transports)
|
||||
&& u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential)
|
||||
.unwrap_or(false) /* no match on failure */
|
||||
});
|
||||
|
||||
while alive() {
|
||||
if excluded {
|
||||
let blank = vec![0u8; PARAMETER_SIZE];
|
||||
if u2f_register(dev, &blank, &blank).is_ok() {
|
||||
callback.call(Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::InvalidState,
|
||||
)));
|
||||
break;
|
||||
}
|
||||
} else if let Ok(bytes) = u2f_register(dev, &challenge, &application) {
|
||||
let dev_info = dev.get_device_info();
|
||||
send_status(
|
||||
&status_mutex,
|
||||
crate::StatusUpdate::Success {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
callback.call(Ok((bytes, dev_info)));
|
||||
break;
|
||||
// Try initializing it.
|
||||
if !dev.is_u2f() || !u2f_init_device(dev) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sleep a bit before trying again.
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
// We currently support none of the authenticator selection
|
||||
// criteria because we can't ask tokens whether they do support
|
||||
// those features. If flags are set, ignore all tokens for now.
|
||||
//
|
||||
// Technically, this is a ConstraintError because we shouldn't talk
|
||||
// to this authenticator in the first place. But the result is the
|
||||
// same anyway.
|
||||
if !flags.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
send_status(
|
||||
&status_mutex,
|
||||
crate::StatusUpdate::DeviceUnavailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
});
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::DeviceAvailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
|
||||
// Iterate the exclude list and see if there are any matches.
|
||||
// If so, we'll keep polling the device anyway to test for user
|
||||
// consent, to be consistent with CTAP2 device behavior.
|
||||
let excluded = key_handles.iter().any(|key_handle| {
|
||||
is_valid_transport(key_handle.transports)
|
||||
&& u2f_is_keyhandle_valid(
|
||||
dev,
|
||||
&challenge,
|
||||
&application,
|
||||
&key_handle.credential,
|
||||
)
|
||||
.unwrap_or(false) /* no match on failure */
|
||||
});
|
||||
|
||||
while alive() {
|
||||
if excluded {
|
||||
let blank = vec![0u8; PARAMETER_SIZE];
|
||||
if u2f_register(dev, &blank, &blank).is_ok() {
|
||||
callback.call(Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::InvalidState,
|
||||
)));
|
||||
break;
|
||||
}
|
||||
} else if let Ok(bytes) = u2f_register(dev, &challenge, &application) {
|
||||
let dev_info = dev.get_device_info();
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::Success {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
callback.call(Ok(RegisterResult::CTAP1(bytes, dev_info)));
|
||||
break;
|
||||
}
|
||||
|
||||
// Sleep a bit before trying again.
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::DeviceUnavailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
|
||||
}
|
||||
|
@ -172,104 +178,108 @@ impl StateMachine {
|
|||
|
||||
let cbc = callback.clone();
|
||||
|
||||
let status_mutex = Mutex::new(status);
|
||||
let transaction = Transaction::new(
|
||||
timeout,
|
||||
cbc.clone(),
|
||||
status,
|
||||
move |info, _, status, alive| {
|
||||
// Create a new device.
|
||||
let dev = &mut match Device::new(info) {
|
||||
Ok(dev) => dev,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let transaction = Transaction::new(timeout, cbc.clone(), move |info, alive| {
|
||||
// Create a new device.
|
||||
let dev = &mut match Device::new(info) {
|
||||
Ok(dev) => dev,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// Try initializing it.
|
||||
if !dev.is_u2f() || !u2f_init_device(dev) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We currently don't support user verification because we can't
|
||||
// ask tokens whether they do support that. If the flag is set,
|
||||
// ignore all tokens for now.
|
||||
//
|
||||
// Technically, this is a ConstraintError because we shouldn't talk
|
||||
// to this authenticator in the first place. But the result is the
|
||||
// same anyway.
|
||||
if !flags.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// For each appId, try all key handles. If there's at least one
|
||||
// valid key handle for an appId, we'll use that appId below.
|
||||
let (app_id, valid_handles) =
|
||||
find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| {
|
||||
u2f_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential)
|
||||
.unwrap_or(false) /* no match on failure */
|
||||
});
|
||||
|
||||
// Aggregate distinct transports from all given credentials.
|
||||
let transports = key_handles
|
||||
.iter()
|
||||
.fold(crate::AuthenticatorTransports::empty(), |t, k| {
|
||||
t | k.transports
|
||||
});
|
||||
|
||||
// We currently only support USB. If the RP specifies transports
|
||||
// and doesn't include USB it's probably lying.
|
||||
if !is_valid_transport(transports) {
|
||||
return;
|
||||
}
|
||||
|
||||
send_status(
|
||||
&status_mutex,
|
||||
crate::StatusUpdate::DeviceAvailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
|
||||
'outer: while alive() {
|
||||
// If the device matches none of the given key handles
|
||||
// then just make it blink with bogus data.
|
||||
if valid_handles.is_empty() {
|
||||
let blank = vec![0u8; PARAMETER_SIZE];
|
||||
if u2f_register(dev, &blank, &blank).is_ok() {
|
||||
callback.call(Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::InvalidState,
|
||||
)));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, try to sign.
|
||||
for key_handle in &valid_handles {
|
||||
if let Ok(bytes) = u2f_sign(dev, &challenge, app_id, &key_handle.credential)
|
||||
{
|
||||
let dev_info = dev.get_device_info();
|
||||
send_status(
|
||||
&status_mutex,
|
||||
crate::StatusUpdate::Success {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
callback.call(Ok((
|
||||
app_id.clone(),
|
||||
key_handle.credential.clone(),
|
||||
bytes,
|
||||
dev_info,
|
||||
)));
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
// Try initializing it.
|
||||
if !dev.is_u2f() || !u2f_init_device(dev) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sleep a bit before trying again.
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
// We currently don't support user verification because we can't
|
||||
// ask tokens whether they do support that. If the flag is set,
|
||||
// ignore all tokens for now.
|
||||
//
|
||||
// Technically, this is a ConstraintError because we shouldn't talk
|
||||
// to this authenticator in the first place. But the result is the
|
||||
// same anyway.
|
||||
if !flags.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
send_status(
|
||||
&status_mutex,
|
||||
crate::StatusUpdate::DeviceUnavailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
});
|
||||
// For each appId, try all key handles. If there's at least one
|
||||
// valid key handle for an appId, we'll use that appId below.
|
||||
let (app_id, valid_handles) =
|
||||
find_valid_key_handles(&app_ids, &key_handles, |app_id, key_handle| {
|
||||
u2f_is_keyhandle_valid(dev, &challenge, app_id, &key_handle.credential)
|
||||
.unwrap_or(false) /* no match on failure */
|
||||
});
|
||||
|
||||
// Aggregate distinct transports from all given credentials.
|
||||
let transports = key_handles
|
||||
.iter()
|
||||
.fold(crate::AuthenticatorTransports::empty(), |t, k| {
|
||||
t | k.transports
|
||||
});
|
||||
|
||||
// We currently only support USB. If the RP specifies transports
|
||||
// and doesn't include USB it's probably lying.
|
||||
if !is_valid_transport(transports) {
|
||||
return;
|
||||
}
|
||||
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::DeviceAvailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
|
||||
'outer: while alive() {
|
||||
// If the device matches none of the given key handles
|
||||
// then just make it blink with bogus data.
|
||||
if valid_handles.is_empty() {
|
||||
let blank = vec![0u8; PARAMETER_SIZE];
|
||||
if u2f_register(dev, &blank, &blank).is_ok() {
|
||||
callback.call(Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::InvalidState,
|
||||
)));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, try to sign.
|
||||
for key_handle in &valid_handles {
|
||||
if let Ok(bytes) =
|
||||
u2f_sign(dev, &challenge, app_id, &key_handle.credential)
|
||||
{
|
||||
let dev_info = dev.get_device_info();
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::Success {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
callback.call(Ok(SignResult::CTAP1(
|
||||
app_id.clone(),
|
||||
key_handle.credential.clone(),
|
||||
bytes,
|
||||
dev_info,
|
||||
)));
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep a bit before trying again.
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::DeviceUnavailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
|
||||
}
|
||||
|
@ -281,3 +291,539 @@ impl StateMachine {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
// TODO(MS): To be renamed to `StateMachine` once U2FManager and the original StateMachine can be removed.
|
||||
pub struct StateMachineCtap2 {
|
||||
transaction: Option<Transaction>,
|
||||
}
|
||||
|
||||
impl StateMachineCtap2 {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn init_and_select(
|
||||
info: DeviceBuildParameters,
|
||||
selector: &Sender<DeviceSelectorEvent>,
|
||||
ctap2_only: bool,
|
||||
) -> Option<Device> {
|
||||
// Create a new device.
|
||||
let mut dev = match Device::new(info) {
|
||||
Ok(dev) => dev,
|
||||
Err((e, id)) => {
|
||||
info!("error happened with device: {}", e);
|
||||
selector.send(DeviceSelectorEvent::NotAToken(id)).ok()?;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Try initializing it.
|
||||
if let Err(e) = dev.init(Nonce::CreateRandom) {
|
||||
warn!("error while initializing device: {}", e);
|
||||
selector.send(DeviceSelectorEvent::NotAToken(dev.id())).ok();
|
||||
return None;
|
||||
}
|
||||
|
||||
if ctap2_only && dev.get_authenticator_info().is_none() {
|
||||
info!("Device does not support CTAP2");
|
||||
selector.send(DeviceSelectorEvent::NotAToken(dev.id())).ok();
|
||||
return None;
|
||||
}
|
||||
|
||||
let write_only_clone = match dev.clone_device_as_write_only() {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
// There is probably something seriously wrong here, if this happens.
|
||||
// So `NotAToken()` is probably too weak a response here.
|
||||
warn!("error while cloning device: {:?}", dev.id());
|
||||
selector
|
||||
.send(DeviceSelectorEvent::NotAToken(dev.id()))
|
||||
.ok()?;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let (tx, rx) = channel();
|
||||
selector
|
||||
.send(DeviceSelectorEvent::ImAToken((write_only_clone, tx)))
|
||||
.ok()?;
|
||||
|
||||
// Blocking recv. DeviceSelector will tell us what to do
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(DeviceCommand::Blink) => match dev.block_and_blink() {
|
||||
BlinkResult::DeviceSelected => {
|
||||
// User selected us. Let DeviceSelector know, so it can cancel all other
|
||||
// outstanding open blink-requests.
|
||||
selector
|
||||
.send(DeviceSelectorEvent::SelectedToken(dev.id()))
|
||||
.ok()?;
|
||||
break;
|
||||
}
|
||||
BlinkResult::Cancelled => {
|
||||
info!("Device {:?} was not selected", dev.id());
|
||||
return None;
|
||||
}
|
||||
},
|
||||
Ok(DeviceCommand::Removed) => {
|
||||
info!("Device {:?} was removed", dev.id());
|
||||
return None;
|
||||
}
|
||||
Ok(DeviceCommand::Continue) => {
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Error when trying to receive messages from DeviceSelector! Exiting.");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(dev)
|
||||
}
|
||||
|
||||
fn ask_user_for_pin<U>(
|
||||
error: PinError,
|
||||
status: &Sender<StatusUpdate>,
|
||||
callback: &StateCallback<crate::Result<U>>,
|
||||
) -> Result<Pin, ()> {
|
||||
info!("PIN Error that requires user interaction detected. Sending it back and waiting for a reply");
|
||||
let (tx, rx) = channel();
|
||||
send_status(status, crate::StatusUpdate::PinError(error.clone(), tx));
|
||||
match rx.recv() {
|
||||
Ok(pin) => Ok(pin),
|
||||
Err(_) => {
|
||||
// recv() can only fail, if the other side is dropping the Sender. We are using this as a trick
|
||||
// to let the callback decide if this PinError is recoverable (e.g. with User input) or not (e.g.
|
||||
// locked token). If it is deemed unrecoverable, we error out the 'normal' way with the same error.
|
||||
error!("Callback dropped the channel, so we forward the error to the results-callback: {:?}", error);
|
||||
callback.call(Err(AuthenticatorError::PinError(error)));
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_pin_auth<T: PinAuthCommand, U>(
|
||||
cmd: &mut T,
|
||||
dev: &mut Device,
|
||||
status: &Sender<StatusUpdate>,
|
||||
callback: &StateCallback<crate::Result<U>>,
|
||||
) -> Result<(), ()> {
|
||||
loop {
|
||||
match cmd.determine_pin_auth(dev) {
|
||||
Ok(_) => {
|
||||
break;
|
||||
}
|
||||
Err(AuthenticatorError::PinError(e)) => {
|
||||
let pin = Self::ask_user_for_pin(e, status, callback)?;
|
||||
cmd.set_pin(Some(pin));
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error when determining pinAuth: {:?}", e);
|
||||
callback.call(Err(e));
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CTAP 2.0 spec is a bit vague here, but CTAP 2.1 is very specific, that the request
|
||||
// should either include pinAuth OR uv=true, but not both at the same time.
|
||||
// Do not set user_verification, if pinAuth is provided
|
||||
if cmd.pin_auth().is_some() {
|
||||
cmd.unset_uv_option();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
params: MakeCredentials,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::RegisterResult>>,
|
||||
) {
|
||||
// Abort any prior register/sign calls.
|
||||
self.cancel();
|
||||
let cbc = callback.clone();
|
||||
let transaction = Transaction::new(
|
||||
timeout,
|
||||
cbc.clone(),
|
||||
status,
|
||||
move |info, selector, status, _alive| {
|
||||
let mut dev = match Self::init_and_select(info, &selector, false) {
|
||||
None => {
|
||||
return;
|
||||
}
|
||||
Some(dev) => dev,
|
||||
};
|
||||
|
||||
info!("Device {:?} continues with the register process", dev.id());
|
||||
// TODO(baloo): not sure about this, have to ask
|
||||
// We currently support none of the authenticator selection
|
||||
// criteria because we can't ask tokens whether they do support
|
||||
// those features. If flags are set, ignore all tokens for now.
|
||||
//
|
||||
// Technically, this is a ConstraintError because we shouldn't talk
|
||||
// to this authenticator in the first place. But the result is the
|
||||
// same anyway.
|
||||
//if !flags.is_empty() {
|
||||
// return;
|
||||
//}
|
||||
|
||||
// TODO(baloo): not sure about this, have to ask
|
||||
// Iterate the exclude list and see if there are any matches.
|
||||
// If so, we'll keep polling the device anyway to test for user
|
||||
// consent, to be consistent with CTAP2 device behavior.
|
||||
//let excluded = key_handles.iter().any(|key_handle| {
|
||||
// is_valid_transport(key_handle.transports)
|
||||
// && u2f_is_keyhandle_valid(dev, &challenge, &application, &key_handle.credential)
|
||||
// .unwrap_or(false) /* no match on failure */
|
||||
//});
|
||||
|
||||
// TODO(MS): This is wasteful, but the current setup with read only-functions doesn't allow me
|
||||
// to modify "params" directly.
|
||||
let mut makecred = params.clone();
|
||||
if params.is_ctap2_request() {
|
||||
// First check if extensions have been requested that are not supported by the device
|
||||
if let Some(true) = params.extensions.hmac_secret {
|
||||
if let Some(auth) = dev.get_authenticator_info() {
|
||||
if !auth.supports_hmac_secret() {
|
||||
callback.call(Err(AuthenticatorError::UnsupportedOption(
|
||||
UnsupportedOption::HmacSecret,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second, ask for PIN and get the shared secret
|
||||
if Self::determine_pin_auth(&mut makecred, &mut dev, &status, &callback)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
debug!("------------------------------------------------------------------");
|
||||
debug!("{:?}", makecred);
|
||||
debug!("------------------------------------------------------------------");
|
||||
let resp = dev.send_msg(&makecred);
|
||||
if resp.is_ok() {
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::Success {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
// The DeviceSelector could already be dead, but it might also wait
|
||||
// for us to respond, in order to cancel all other tokens in case
|
||||
// we skipped the "blinking"-action and went straight for the actual
|
||||
// request.
|
||||
let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
|
||||
}
|
||||
match resp {
|
||||
Ok(MakeCredentialsResult::CTAP2(attestation, client_data)) => {
|
||||
callback.call(Ok(RegisterResult::CTAP2(attestation, client_data)))
|
||||
}
|
||||
Ok(MakeCredentialsResult::CTAP1(data)) => {
|
||||
callback.call(Ok(RegisterResult::CTAP1(data, dev.get_device_info())))
|
||||
}
|
||||
|
||||
Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {}
|
||||
Err(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::ChannelBusy,
|
||||
_,
|
||||
))) => {}
|
||||
Err(e) => {
|
||||
warn!("error happened: {}", e);
|
||||
callback.call(Err(AuthenticatorError::HIDError(e)));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.transaction = Some(try_or!(transaction, |e| cbc.call(Err(e))));
|
||||
}
|
||||
|
||||
pub fn sign(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
params: GetAssertion,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::SignResult>>,
|
||||
) {
|
||||
// Abort any prior register/sign calls.
|
||||
self.cancel();
|
||||
let cbc = callback.clone();
|
||||
|
||||
let transaction = Transaction::new(
|
||||
timeout,
|
||||
callback.clone(),
|
||||
status,
|
||||
move |info, selector, status, _alive| {
|
||||
let mut dev = match Self::init_and_select(info, &selector, false) {
|
||||
None => {
|
||||
return;
|
||||
}
|
||||
Some(dev) => dev,
|
||||
};
|
||||
|
||||
info!("Device {:?} continues with the signing process", dev.id());
|
||||
// TODO(MS): This is wasteful, but the current setup with read only-functions doesn't allow me
|
||||
// to modify "params" directly.
|
||||
let mut getassertion = params.clone();
|
||||
if params.is_ctap2_request() {
|
||||
// First check if extensions have been requested that are not supported by the device
|
||||
if params.extensions.hmac_secret.is_some() {
|
||||
if let Some(auth) = dev.get_authenticator_info() {
|
||||
if !auth.supports_hmac_secret() {
|
||||
callback.call(Err(AuthenticatorError::UnsupportedOption(
|
||||
UnsupportedOption::HmacSecret,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second, ask for PIN and get the shared secret
|
||||
if Self::determine_pin_auth(&mut getassertion, &mut dev, &status, &callback)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Third, use the shared secret in the extensions, if requested
|
||||
if let Some(extension) = getassertion.extensions.hmac_secret.as_mut() {
|
||||
if let Some(secret) = dev.get_shared_secret() {
|
||||
match extension.calculate(secret) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
callback.call(Err(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("------------------------------------------------------------------");
|
||||
debug!("{:?}", getassertion);
|
||||
debug!("------------------------------------------------------------------");
|
||||
|
||||
let resp = dev.send_msg(&getassertion);
|
||||
if resp.is_ok() {
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::Success {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
// The DeviceSelector could already be dead, but it might also wait
|
||||
// for us to respond, in order to cancel all other tokens in case
|
||||
// we skipped the "blinking"-action and went straight for the actual
|
||||
// request.
|
||||
let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
|
||||
}
|
||||
match resp {
|
||||
Ok(GetAssertionResult::CTAP1(resp)) => {
|
||||
let app_id = getassertion.rp.hash().as_ref().to_vec();
|
||||
let key_handle = getassertion.allow_list[0].id.clone();
|
||||
|
||||
callback.call(Ok(SignResult::CTAP1(
|
||||
app_id,
|
||||
key_handle,
|
||||
resp,
|
||||
dev.get_device_info(),
|
||||
)))
|
||||
}
|
||||
Ok(GetAssertionResult::CTAP2(assertion, client_data)) => {
|
||||
callback.call(Ok(SignResult::CTAP2(assertion, client_data)))
|
||||
}
|
||||
// TODO(baloo): if key_handle is invalid for this device, it
|
||||
// should reply something like:
|
||||
// CTAP2_ERR_INVALID_CREDENTIAL
|
||||
// have to check
|
||||
Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {}
|
||||
Err(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::ChannelBusy,
|
||||
_,
|
||||
))) => {}
|
||||
Err(e) => {
|
||||
warn!("error happened: {}", e);
|
||||
callback.call(Err(AuthenticatorError::HIDError(e)));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
|
||||
}
|
||||
|
||||
// This blocks.
|
||||
pub fn cancel(&mut self) {
|
||||
if let Some(mut transaction) = self.transaction.take() {
|
||||
info!("Statemachine was cancelled. Cancelling transaction now.");
|
||||
transaction.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) {
|
||||
// Abort any prior register/sign calls.
|
||||
self.cancel();
|
||||
let cbc = callback.clone();
|
||||
|
||||
let transaction = Transaction::new(
|
||||
timeout,
|
||||
callback.clone(),
|
||||
status,
|
||||
move |info, selector, status, _alive| {
|
||||
let reset = Reset {};
|
||||
let mut dev = match Self::init_and_select(info, &selector, true) {
|
||||
None => {
|
||||
return;
|
||||
}
|
||||
Some(dev) => dev,
|
||||
};
|
||||
|
||||
info!("Device {:?} continues with the reset process", dev.id());
|
||||
debug!("------------------------------------------------------------------");
|
||||
debug!("{:?}", reset);
|
||||
debug!("------------------------------------------------------------------");
|
||||
|
||||
let resp = dev.send_cbor(&reset);
|
||||
if resp.is_ok() {
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::Success {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
// The DeviceSelector could already be dead, but it might also wait
|
||||
// for us to respond, in order to cancel all other tokens in case
|
||||
// we skipped the "blinking"-action and went straight for the actual
|
||||
// request.
|
||||
let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id()));
|
||||
}
|
||||
|
||||
match resp {
|
||||
Ok(()) => callback.call(Ok(())),
|
||||
Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {}
|
||||
Err(HIDError::Command(CommandError::StatusCode(
|
||||
StatusCode::ChannelBusy,
|
||||
_,
|
||||
))) => {}
|
||||
Err(e) => {
|
||||
warn!("error happened: {}", e);
|
||||
callback.call(Err(AuthenticatorError::HIDError(e)));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
|
||||
}
|
||||
|
||||
pub fn set_pin(
|
||||
&mut self,
|
||||
timeout: u64,
|
||||
new_pin: Pin,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
callback: StateCallback<crate::Result<crate::ResetResult>>,
|
||||
) {
|
||||
// Abort any prior register/sign calls.
|
||||
self.cancel();
|
||||
|
||||
let cbc = callback.clone();
|
||||
|
||||
let transaction = Transaction::new(
|
||||
timeout,
|
||||
callback.clone(),
|
||||
status,
|
||||
move |info, selector, status, _alive| {
|
||||
let mut dev = match Self::init_and_select(info, &selector, true) {
|
||||
None => {
|
||||
return;
|
||||
}
|
||||
Some(dev) => dev,
|
||||
};
|
||||
|
||||
let (mut shared_secret, authinfo) = match dev.establish_shared_secret() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
callback.call(Err(AuthenticatorError::HIDError(e)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// With CTAP2.1 we will have an adjustable required length for PINs
|
||||
if new_pin.as_bytes().len() < 4 {
|
||||
callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooShort)));
|
||||
return;
|
||||
}
|
||||
|
||||
if new_pin.as_bytes().len() > 64 {
|
||||
callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooLong(
|
||||
new_pin.as_bytes().len(),
|
||||
))));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a client-pin is already set, or if a new one should be created
|
||||
let res = if authinfo.options.client_pin.unwrap_or_default() {
|
||||
let mut res;
|
||||
let mut error = PinError::PinRequired;
|
||||
loop {
|
||||
let current_pin = match Self::ask_user_for_pin(error, &status, &callback) {
|
||||
Ok(pin) => pin,
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
res = ChangeExistingPin::new(
|
||||
&authinfo,
|
||||
&shared_secret,
|
||||
¤t_pin,
|
||||
&new_pin,
|
||||
)
|
||||
.map_err(HIDError::Command)
|
||||
.and_then(|msg| dev.send_cbor(&msg))
|
||||
.map_err(AuthenticatorError::HIDError)
|
||||
.map_err(|e| repackage_pin_errors(&mut dev, e));
|
||||
|
||||
if let Err(AuthenticatorError::PinError(e)) = res {
|
||||
error = e;
|
||||
// We need to re-establish the shared secret for the next round.
|
||||
match dev.establish_shared_secret() {
|
||||
Ok((s, _)) => {
|
||||
shared_secret = s;
|
||||
}
|
||||
Err(e) => {
|
||||
callback.call(Err(AuthenticatorError::HIDError(e)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
res
|
||||
} else {
|
||||
SetNewPin::new(&authinfo, &shared_secret, &new_pin)
|
||||
.map_err(HIDError::Command)
|
||||
.and_then(|msg| dev.send_cbor(&msg))
|
||||
.map_err(AuthenticatorError::HIDError)
|
||||
};
|
||||
callback.call(res);
|
||||
},
|
||||
);
|
||||
self.transaction = Some(try_or!(transaction, move |e| cbc.call(Err(e))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
use super::{u2ftypes, Pin, PinError};
|
||||
use serde::ser::{Serialize, SerializeStruct};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StatusUpdate {
|
||||
/// Device found
|
||||
DeviceAvailable { dev_info: u2ftypes::U2FDeviceInfo },
|
||||
/// Device got removed
|
||||
DeviceUnavailable { dev_info: u2ftypes::U2FDeviceInfo },
|
||||
/// We successfully finished the register or sign request
|
||||
Success { dev_info: u2ftypes::U2FDeviceInfo },
|
||||
/// Sent if a PIN is needed (or was wrong), or some other kind of PIN-related
|
||||
/// error occurred. The Sender is for sending back a PIN (if needed).
|
||||
PinError(PinError, Sender<Pin>),
|
||||
/// Sent, if multiple devices are found and the user has to select one
|
||||
SelectDeviceNotice,
|
||||
/// Sent, once a device was selected (either automatically or by user-interaction)
|
||||
/// and the register or signing process continues with this device
|
||||
DeviceSelected(u2ftypes::U2FDeviceInfo),
|
||||
}
|
||||
|
||||
impl Serialize for StatusUpdate {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_struct("StatusUpdate", 1)?;
|
||||
match &*self {
|
||||
StatusUpdate::DeviceAvailable { dev_info } => {
|
||||
map.serialize_field("DeviceAvailable", &dev_info)?
|
||||
}
|
||||
StatusUpdate::DeviceUnavailable { dev_info } => {
|
||||
map.serialize_field("DeviceUnavailable", &dev_info)?
|
||||
}
|
||||
StatusUpdate::Success { dev_info } => map.serialize_field("Success", &dev_info)?,
|
||||
StatusUpdate::PinError(e, _) => map.serialize_field("PinError", &e)?,
|
||||
StatusUpdate::SelectDeviceNotice => map.serialize_field("SelectDeviceNotice", &())?,
|
||||
StatusUpdate::DeviceSelected(dev_info) => {
|
||||
map.serialize_field("DeviceSelected", &dev_info)?
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn send_status(status: &Sender<StatusUpdate>, msg: StatusUpdate) {
|
||||
match status.send(msg) {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("Couldn't send status: {:?}", e),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::consts::U2F_AUTHENTICATE;
|
||||
|
||||
use super::*;
|
||||
use crate::consts::Capability;
|
||||
use serde_json::to_string;
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
#[test]
|
||||
fn serialize_select() {
|
||||
let st = StatusUpdate::SelectDeviceNotice;
|
||||
let json = to_string(&st).expect("Failed to serialize");
|
||||
assert_eq!(&json, r#"{"SelectDeviceNotice":null}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_invalid_pin() {
|
||||
let (tx, _rx) = channel();
|
||||
let st = StatusUpdate::PinError(PinError::InvalidPin(Some(3)), tx.clone());
|
||||
let json = to_string(&st).expect("Failed to serialize");
|
||||
assert_eq!(&json, r#"{"PinError":{"InvalidPin":3}}"#);
|
||||
|
||||
let st = StatusUpdate::PinError(PinError::InvalidPin(None), tx);
|
||||
let json = to_string(&st).expect("Failed to serialize");
|
||||
assert_eq!(&json, r#"{"PinError":{"InvalidPin":null}}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_success() {
|
||||
let cap = Capability::WINK | Capability::CBOR;
|
||||
let dev = u2ftypes::U2FDeviceInfo {
|
||||
vendor_name: String::from("ABC").into_bytes(),
|
||||
device_name: String::from("DEF").into_bytes(),
|
||||
version_interface: 2,
|
||||
version_major: 5,
|
||||
version_minor: 4,
|
||||
version_build: 3,
|
||||
cap_flags: cap,
|
||||
};
|
||||
let st = StatusUpdate::Success { dev_info: dev };
|
||||
let json = to_string(&st).expect("Failed to serialize");
|
||||
assert_eq!(
|
||||
&json,
|
||||
r#"{"Success":{"vendor_name":[65,66,67],"device_name":[68,69,70],"version_interface":2,"version_major":5,"version_minor":4,"version_build":3,"cap_flags":{"bits":5}}}"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/* 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::statecallback::StateCallback;
|
||||
|
||||
pub struct Transaction {}
|
||||
|
||||
impl Transaction {
|
||||
pub fn new<F, T>(
|
||||
timeout: u64,
|
||||
callback: StateCallback<crate::Result<T>>,
|
||||
new_device_cb: F,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
F: Fn(String, &dyn Fn() -> bool),
|
||||
{
|
||||
callback.call(Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::NotSupported,
|
||||
)));
|
||||
|
||||
Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::NotSupported,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
/* No-op. */
|
||||
}
|
||||
}
|
|
@ -0,0 +1,514 @@
|
|||
use crate::send_status;
|
||||
use crate::transport::hid::HIDDevice;
|
||||
pub use crate::transport::platform::device::Device;
|
||||
use crate::u2ftypes::U2FDevice;
|
||||
use runloop::RunLoop;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::mpsc::{channel, RecvTimeoutError, Sender};
|
||||
use std::time::Duration;
|
||||
|
||||
pub type DeviceID = <Device as HIDDevice>::Id;
|
||||
pub type DeviceBuildParameters = <Device as HIDDevice>::BuildParameters;
|
||||
|
||||
trait DeviceSelectorEventMarker {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum BlinkResult {
|
||||
DeviceSelected,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum DeviceCommand {
|
||||
Blink,
|
||||
Continue,
|
||||
Removed,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DeviceSelectorEvent {
|
||||
Cancel,
|
||||
Timeout,
|
||||
DevicesAdded(Vec<DeviceID>),
|
||||
DeviceRemoved(DeviceID),
|
||||
NotAToken(DeviceID),
|
||||
ImAToken((Device, Sender<DeviceCommand>)),
|
||||
SelectedToken(DeviceID),
|
||||
}
|
||||
|
||||
pub struct DeviceSelector {
|
||||
/// How to send a message to the event loop
|
||||
sender: Sender<DeviceSelectorEvent>,
|
||||
/// Thread of the event loop
|
||||
runloop: RunLoop,
|
||||
}
|
||||
|
||||
impl DeviceSelector {
|
||||
pub fn run(status: Sender<crate::StatusUpdate>) -> Self {
|
||||
let (selector_send, selector_rec) = channel();
|
||||
// let new_device_callback = Arc::new(new_device_cb);
|
||||
let runloop = RunLoop::new(move |alive| {
|
||||
let mut blinking = false;
|
||||
// Device was added, but we wait for its response, if it is a token or not
|
||||
// We save both a write-only copy of the device (for cancellation) and it's thread
|
||||
let mut waiting_for_response = HashSet::new();
|
||||
// All devices that responded with "ImAToken"
|
||||
let mut tokens = HashMap::new();
|
||||
while alive() {
|
||||
let d = Duration::from_secs(100);
|
||||
let res = match selector_rec.recv_timeout(d) {
|
||||
Err(RecvTimeoutError::Disconnected) => {
|
||||
break;
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => DeviceSelectorEvent::Timeout,
|
||||
Ok(res) => res,
|
||||
};
|
||||
|
||||
match res {
|
||||
DeviceSelectorEvent::Timeout | DeviceSelectorEvent::Cancel => {
|
||||
/* TODO */
|
||||
Self::cancel_all(tokens, None);
|
||||
break;
|
||||
}
|
||||
DeviceSelectorEvent::SelectedToken(id) => {
|
||||
if let Some(dev) = tokens.keys().find(|d| d.id() == id) {
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
|
||||
);
|
||||
}
|
||||
Self::cancel_all(tokens, Some(&id));
|
||||
break; // We are done here. The selected device continues without us.
|
||||
}
|
||||
DeviceSelectorEvent::DevicesAdded(ids) => {
|
||||
for id in ids {
|
||||
debug!("Device added event: {:?}", id);
|
||||
waiting_for_response.insert(id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
DeviceSelectorEvent::DeviceRemoved(id) => {
|
||||
debug!("Device removed event: {:?}", id);
|
||||
if !waiting_for_response.remove(&id) {
|
||||
// Note: We _could_ check here if we had multiple tokens and are already blinking
|
||||
// and the removal of this one leads to only one token left. So we could in theory
|
||||
// stop blinking and select it right away. At the moment, I think this is a
|
||||
// too surprising behavior and therefore, we let the remaining device keep on blinking
|
||||
// since the user could add yet another device, instead of using the remaining one.
|
||||
tokens.iter().for_each(|(dev, send)| {
|
||||
if dev.id() == id {
|
||||
let _ = send.send(DeviceCommand::Removed);
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::DeviceUnavailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
tokens.retain(|dev, _| dev.id() != id);
|
||||
if tokens.is_empty() {
|
||||
blinking = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// We are already blinking, so no need to run the code below this match
|
||||
// that figures out if we should blink or not. In fact, currently, we do
|
||||
// NOT want to run this code again, because if you have 2 blinking tokens
|
||||
// and one got removed, we WANT the remaining one to continue blinking.
|
||||
// This is a design choice, because I currently think it is the "less surprising"
|
||||
// option to the user.
|
||||
if blinking {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
DeviceSelectorEvent::NotAToken(id) => {
|
||||
debug!("Device not a token event: {:?}", id);
|
||||
waiting_for_response.remove(&id);
|
||||
}
|
||||
DeviceSelectorEvent::ImAToken((dev, tx)) => {
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::DeviceAvailable {
|
||||
dev_info: dev.get_device_info(),
|
||||
},
|
||||
);
|
||||
let id = dev.id();
|
||||
let _ = waiting_for_response.remove(&id);
|
||||
tokens.insert(dev, tx.clone());
|
||||
if blinking {
|
||||
// We are already blinking, so this new device should blink too.
|
||||
if tx.send(DeviceCommand::Blink).is_err() {
|
||||
// Device thread died in the meantime (which shouldn't happen)
|
||||
tokens.retain(|dev, _| dev.id() != id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All known devices told us, whether they are tokens or not and we have at least one token
|
||||
if waiting_for_response.is_empty() && !tokens.is_empty() {
|
||||
if tokens.len() == 1 {
|
||||
let (dev, tx) = tokens.drain().next().unwrap(); // We just checked that it can't be empty
|
||||
if tx.send(DeviceCommand::Continue).is_err() {
|
||||
// Device thread died in the meantime (which shouldn't happen).
|
||||
// Tokens is empty, so we just start over again
|
||||
continue;
|
||||
}
|
||||
send_status(
|
||||
&status,
|
||||
crate::StatusUpdate::DeviceSelected(dev.get_device_info()),
|
||||
);
|
||||
Self::cancel_all(tokens, Some(&dev.id()));
|
||||
break; // We are done here
|
||||
} else {
|
||||
blinking = true;
|
||||
|
||||
tokens.iter().for_each(|(_dev, tx)| {
|
||||
// A send operation can only fail if the receiving end of a channel is disconnected, implying that the data could never be received.
|
||||
// We ignore errors here for now, but should probably remove the device in such a case (even though it theoretically can't happen)
|
||||
let _ = tx.send(DeviceCommand::Blink);
|
||||
});
|
||||
send_status(&status, crate::StatusUpdate::SelectDeviceNotice);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
runloop: runloop.unwrap(), // TODO
|
||||
sender: selector_send,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_sender(&self) -> Sender<DeviceSelectorEvent> {
|
||||
self.sender.clone()
|
||||
}
|
||||
|
||||
fn cancel_all(tokens: HashMap<Device, Sender<DeviceCommand>>, exclude: Option<&DeviceID>) {
|
||||
tokens
|
||||
.into_keys()
|
||||
.filter(|x| exclude.map_or(true, |y| y != &x.id()))
|
||||
.for_each(|mut dev| dev.cancel().unwrap()); // TODO
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
// We ignore a possible error here, since we don't really care
|
||||
let _ = self.sender.send(DeviceSelectorEvent::Cancel);
|
||||
self.runloop.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
consts::Capability,
|
||||
ctap2::commands::get_info::{AuthenticatorInfo, AuthenticatorOptions},
|
||||
u2ftypes::U2FDeviceInfo,
|
||||
StatusUpdate,
|
||||
};
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
enum ExpectedUpdate {
|
||||
DeviceAvailable,
|
||||
DeviceUnavailable,
|
||||
SelectDeviceNotice,
|
||||
DeviceSelected,
|
||||
}
|
||||
|
||||
fn gen_info(id: String) -> U2FDeviceInfo {
|
||||
U2FDeviceInfo {
|
||||
vendor_name: String::from("ExampleVendor").into_bytes(),
|
||||
device_name: id.into_bytes(),
|
||||
version_interface: 1,
|
||||
version_major: 3,
|
||||
version_minor: 2,
|
||||
version_build: 1,
|
||||
cap_flags: Capability::WINK | Capability::CBOR | Capability::NMSG,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_device_simple_u2f(dev: &mut Device) {
|
||||
dev.set_device_info(gen_info(dev.id()));
|
||||
dev.create_channel();
|
||||
}
|
||||
|
||||
fn make_device_with_pin(dev: &mut Device) {
|
||||
dev.set_device_info(gen_info(dev.id()));
|
||||
dev.create_channel();
|
||||
let info = AuthenticatorInfo {
|
||||
options: AuthenticatorOptions {
|
||||
client_pin: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
dev.set_authenticator_info(info.clone());
|
||||
}
|
||||
|
||||
fn send_i_am_token(dev: &Device, selector: &DeviceSelector) {
|
||||
selector
|
||||
.sender
|
||||
.send(DeviceSelectorEvent::ImAToken((
|
||||
dev.clone_device_as_write_only().unwrap(),
|
||||
dev.sender.clone().unwrap(),
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn send_no_token(dev: &Device, selector: &DeviceSelector) {
|
||||
selector
|
||||
.sender
|
||||
.send(DeviceSelectorEvent::NotAToken(dev.id()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn remove_device(dev: &Device, selector: &DeviceSelector) {
|
||||
selector
|
||||
.sender
|
||||
.send(DeviceSelectorEvent::DeviceRemoved(dev.id()))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
dev.receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Removed
|
||||
);
|
||||
}
|
||||
|
||||
fn recv_status(dev: &Device, status_rx: &Receiver<StatusUpdate>, expected: ExpectedUpdate) {
|
||||
let res = status_rx.recv().unwrap();
|
||||
// Marking it with _, as cargo warns about "unused exp", as it doesn't view the assert below as a usage
|
||||
let _exp = match expected {
|
||||
ExpectedUpdate::DeviceAvailable => StatusUpdate::DeviceUnavailable {
|
||||
dev_info: gen_info(dev.id()),
|
||||
},
|
||||
ExpectedUpdate::DeviceUnavailable => StatusUpdate::DeviceUnavailable {
|
||||
dev_info: gen_info(dev.id()),
|
||||
},
|
||||
ExpectedUpdate::DeviceSelected => StatusUpdate::DeviceSelected(gen_info(dev.id())),
|
||||
ExpectedUpdate::SelectDeviceNotice => StatusUpdate::SelectDeviceNotice,
|
||||
};
|
||||
assert!(matches!(res, _exp));
|
||||
}
|
||||
|
||||
fn add_devices<'a, T>(iter: T, selector: &DeviceSelector)
|
||||
where
|
||||
T: Iterator<Item = &'a Device>,
|
||||
{
|
||||
selector
|
||||
.sender
|
||||
.send(DeviceSelectorEvent::DevicesAdded(
|
||||
iter.map(|f| f.id()).collect(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_selector_one_token_no_late_adds() {
|
||||
let mut devices = vec![
|
||||
Device::new("device selector 1").unwrap(),
|
||||
Device::new("device selector 2").unwrap(),
|
||||
Device::new("device selector 3").unwrap(),
|
||||
Device::new("device selector 4").unwrap(),
|
||||
];
|
||||
|
||||
// Make those actual tokens. The rest is interpreted as non-u2f-devices
|
||||
make_device_with_pin(&mut devices[2]);
|
||||
let (status_tx, status_rx) = channel();
|
||||
let selector = DeviceSelector::run(status_tx);
|
||||
|
||||
// Adding all
|
||||
add_devices(devices.iter(), &selector);
|
||||
devices.iter().filter(|d| !d.is_u2f()).for_each(|d| {
|
||||
send_no_token(d, &selector);
|
||||
});
|
||||
|
||||
send_i_am_token(&devices[2], &selector);
|
||||
|
||||
recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
assert_eq!(
|
||||
devices[2].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Continue
|
||||
);
|
||||
recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceSelected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_selector_all_pins_with_late_add() {
|
||||
let mut devices = vec![
|
||||
Device::new("device selector 1").unwrap(),
|
||||
Device::new("device selector 2").unwrap(),
|
||||
Device::new("device selector 3").unwrap(),
|
||||
Device::new("device selector 4").unwrap(),
|
||||
Device::new("device selector 5").unwrap(),
|
||||
Device::new("device selector 6").unwrap(),
|
||||
];
|
||||
|
||||
// Make those actual tokens. The rest is interpreted as non-u2f-devices
|
||||
make_device_with_pin(&mut devices[2]);
|
||||
make_device_with_pin(&mut devices[4]);
|
||||
make_device_with_pin(&mut devices[5]);
|
||||
|
||||
let (status_tx, status_rx) = channel();
|
||||
let selector = DeviceSelector::run(status_tx);
|
||||
|
||||
// Adding all, except the last one (we simulate that this one is not yet plugged in)
|
||||
add_devices(devices.iter().take(5), &selector);
|
||||
|
||||
// Interleave tokens and non-tokens
|
||||
send_i_am_token(&devices[2], &selector);
|
||||
recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
|
||||
devices.iter().filter(|d| !d.is_u2f()).for_each(|d| {
|
||||
send_no_token(d, &selector);
|
||||
});
|
||||
|
||||
send_i_am_token(&devices[4], &selector);
|
||||
recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
|
||||
// We added 2 devices that are tokens. They should get the blink-command now
|
||||
assert_eq!(
|
||||
devices[2].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Blink
|
||||
);
|
||||
assert_eq!(
|
||||
devices[4].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Blink
|
||||
);
|
||||
recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice);
|
||||
|
||||
// Plug in late device
|
||||
send_i_am_token(&devices[5], &selector);
|
||||
recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
assert_eq!(
|
||||
devices[5].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Blink
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_selector_no_pins_late_mixed_adds() {
|
||||
// Multiple tokes, none of them support a PIN
|
||||
let mut devices = vec![
|
||||
Device::new("device selector 1").unwrap(),
|
||||
Device::new("device selector 2").unwrap(),
|
||||
Device::new("device selector 3").unwrap(),
|
||||
Device::new("device selector 4").unwrap(),
|
||||
Device::new("device selector 5").unwrap(),
|
||||
Device::new("device selector 6").unwrap(),
|
||||
Device::new("device selector 7").unwrap(),
|
||||
];
|
||||
|
||||
// Make those actual tokens. The rest is interpreted as non-u2f-devices
|
||||
make_device_simple_u2f(&mut devices[2]);
|
||||
make_device_simple_u2f(&mut devices[4]);
|
||||
make_device_simple_u2f(&mut devices[5]);
|
||||
|
||||
let (status_tx, status_rx) = channel();
|
||||
let selector = DeviceSelector::run(status_tx);
|
||||
|
||||
// Adding all, except the last one (we simulate that this one is not yet plugged in)
|
||||
add_devices(devices.iter().take(5), &selector);
|
||||
|
||||
// Interleave tokens and non-tokens
|
||||
send_i_am_token(&devices[2], &selector);
|
||||
recv_status(&devices[2], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
|
||||
devices.iter().filter(|d| !d.is_u2f()).for_each(|d| {
|
||||
send_no_token(d, &selector);
|
||||
});
|
||||
|
||||
send_i_am_token(&devices[4], &selector);
|
||||
recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
|
||||
// We added 2 devices that are tokens. They should get the blink-command now
|
||||
assert_eq!(
|
||||
devices[2].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Blink
|
||||
);
|
||||
assert_eq!(
|
||||
devices[4].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Blink
|
||||
);
|
||||
recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice);
|
||||
|
||||
// Plug in late device
|
||||
send_i_am_token(&devices[5], &selector);
|
||||
recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
assert_eq!(
|
||||
devices[5].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Blink
|
||||
);
|
||||
// Remove device again
|
||||
remove_device(&devices[5], &selector);
|
||||
recv_status(&devices[5], &status_rx, ExpectedUpdate::DeviceUnavailable);
|
||||
|
||||
// Now we add a token that has a PIN, it should not get "Continue" but "Blink"
|
||||
make_device_with_pin(&mut devices[6]);
|
||||
send_i_am_token(&devices[6], &selector);
|
||||
recv_status(&devices[6], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
assert_eq!(
|
||||
devices[6].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Blink
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_device_selector_mixed_pins_remove_all() {
|
||||
// Multiple tokes, none of them support a PIN, so we should get Continue-commands
|
||||
// for all of them
|
||||
let mut devices = vec![
|
||||
Device::new("device selector 1").unwrap(),
|
||||
Device::new("device selector 2").unwrap(),
|
||||
Device::new("device selector 3").unwrap(),
|
||||
Device::new("device selector 4").unwrap(),
|
||||
Device::new("device selector 5").unwrap(),
|
||||
Device::new("device selector 6").unwrap(),
|
||||
];
|
||||
|
||||
// Make those actual tokens. The rest is interpreted as non-u2f-devices
|
||||
make_device_with_pin(&mut devices[2]);
|
||||
make_device_with_pin(&mut devices[4]);
|
||||
make_device_with_pin(&mut devices[5]);
|
||||
|
||||
let (status_tx, status_rx) = channel();
|
||||
let selector = DeviceSelector::run(status_tx);
|
||||
|
||||
// Adding all, except the last one (we simulate that this one is not yet plugged in)
|
||||
add_devices(devices.iter(), &selector);
|
||||
|
||||
devices.iter().for_each(|d| {
|
||||
if d.is_u2f() {
|
||||
send_i_am_token(d, &selector);
|
||||
} else {
|
||||
send_no_token(d, &selector);
|
||||
}
|
||||
});
|
||||
|
||||
for idx in [2, 4, 5] {
|
||||
recv_status(&devices[idx], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
assert_eq!(
|
||||
devices[idx].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Blink
|
||||
);
|
||||
}
|
||||
recv_status(&devices[2], &status_rx, ExpectedUpdate::SelectDeviceNotice);
|
||||
|
||||
// Remove all tokens
|
||||
for idx in [2, 4, 5] {
|
||||
remove_device(&devices[idx], &selector);
|
||||
recv_status(&devices[idx], &status_rx, ExpectedUpdate::DeviceUnavailable);
|
||||
}
|
||||
|
||||
// Adding one again
|
||||
send_i_am_token(&devices[4], &selector);
|
||||
recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceAvailable);
|
||||
|
||||
// This should now get a "Continue" instead of "Blinking", because it's the only device
|
||||
assert_eq!(
|
||||
devices[4].receiver.as_ref().unwrap().recv().unwrap(),
|
||||
DeviceCommand::Continue
|
||||
);
|
||||
recv_status(&devices[4], &status_rx, ExpectedUpdate::DeviceSelected);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
use crate::consts::{SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, SW_WRONG_DATA, SW_WRONG_LENGTH};
|
||||
use crate::ctap2::commands::CommandError;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::path;
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ApduErrorStatus {
|
||||
ConditionsNotSatisfied,
|
||||
WrongData,
|
||||
WrongLength,
|
||||
Unknown([u8; 2]),
|
||||
}
|
||||
|
||||
impl ApduErrorStatus {
|
||||
pub fn from(status: [u8; 2]) -> Result<(), ApduErrorStatus> {
|
||||
match status {
|
||||
s if s == SW_NO_ERROR => Ok(()),
|
||||
s if s == SW_CONDITIONS_NOT_SATISFIED => Err(ApduErrorStatus::ConditionsNotSatisfied),
|
||||
s if s == SW_WRONG_DATA => Err(ApduErrorStatus::WrongData),
|
||||
s if s == SW_WRONG_LENGTH => Err(ApduErrorStatus::WrongLength),
|
||||
other => Err(ApduErrorStatus::Unknown(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ApduErrorStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ApduErrorStatus::ConditionsNotSatisfied => write!(f, "Apdu: condition not satisfied"),
|
||||
ApduErrorStatus::WrongData => write!(f, "Apdu: wrong data"),
|
||||
ApduErrorStatus::WrongLength => write!(f, "Apdu: wrong length"),
|
||||
ApduErrorStatus::Unknown(ref u) => write!(f, "Apdu: unknown error: {:?}", u),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Debug)]
|
||||
pub enum HIDError {
|
||||
/// Transport replied with a status not expected
|
||||
DeviceError,
|
||||
UnexpectedInitReplyLen,
|
||||
NonceMismatch,
|
||||
DeviceNotInitialized,
|
||||
DeviceNotSupported,
|
||||
UnsupportedCommand,
|
||||
UnexpectedVersion,
|
||||
IO(Option<path::PathBuf>, io::Error),
|
||||
UnexpectedCmd(u8),
|
||||
Command(CommandError),
|
||||
ApduStatus(ApduErrorStatus),
|
||||
}
|
||||
|
||||
impl From<io::Error> for HIDError {
|
||||
fn from(e: io::Error) -> HIDError {
|
||||
HIDError::IO(None, e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommandError> for HIDError {
|
||||
fn from(e: CommandError) -> HIDError {
|
||||
HIDError::Command(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApduErrorStatus> for HIDError {
|
||||
fn from(e: ApduErrorStatus) -> HIDError {
|
||||
HIDError::ApduStatus(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HIDError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
HIDError::UnexpectedInitReplyLen => {
|
||||
write!(f, "Error: Unexpected reply len when initilizaling")
|
||||
}
|
||||
HIDError::NonceMismatch => write!(f, "Error: Nonce mismatch"),
|
||||
HIDError::DeviceError => write!(f, "Error: device returned error"),
|
||||
HIDError::DeviceNotInitialized => write!(f, "Error: using not initiliazed device"),
|
||||
HIDError::DeviceNotSupported => {
|
||||
write!(f, "Error: requested operation is not available on device")
|
||||
}
|
||||
HIDError::UnexpectedVersion => write!(f, "Error: Unexpected protocol version"),
|
||||
HIDError::UnsupportedCommand => {
|
||||
write!(f, "Error: command is not supported on this device")
|
||||
}
|
||||
HIDError::IO(ref p, ref e) => write!(f, "Error: Ioerror({:?}): {}", p, e),
|
||||
HIDError::Command(ref e) => write!(f, "Error: Error issuing command: {}", e),
|
||||
HIDError::UnexpectedCmd(s) => write!(f, "Error: Unexpected status: {}", s),
|
||||
HIDError::ApduStatus(ref status) => {
|
||||
write!(f, "Error: Unexpected apdu status: {:?}", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,9 +10,10 @@ use std::io::{Read, Write};
|
|||
use std::os::unix::prelude::*;
|
||||
|
||||
use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
|
||||
use crate::platform::uhid;
|
||||
use crate::transport::platform::uhid;
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use crate::util::from_unix_result;
|
||||
use crate::util::io_err;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Device {
|
||||
|
@ -35,8 +36,51 @@ impl Device {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn is_u2f(&self) -> bool {
|
||||
uhid::is_u2f_device(self.fd)
|
||||
pub fn is_u2f(&mut self) -> bool {
|
||||
if !uhid::is_u2f_device(self.fd) {
|
||||
return false;
|
||||
}
|
||||
if let Err(_) = self.ping() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn ping(&mut self) -> io::Result<()> {
|
||||
for i in 0..10 {
|
||||
let mut buf = vec![0u8; 1 + MAX_HID_RPT_SIZE];
|
||||
|
||||
buf[0] = 0; // report number
|
||||
buf[1] = 0xff; // CID_BROADCAST
|
||||
buf[2] = 0xff;
|
||||
buf[3] = 0xff;
|
||||
buf[4] = 0xff;
|
||||
buf[5] = 0x81; // ping
|
||||
buf[6] = 0;
|
||||
buf[7] = 1; // one byte
|
||||
|
||||
self.write(&buf[..])?;
|
||||
|
||||
// Wait for response
|
||||
let mut pfd: libc::pollfd = unsafe { std::mem::zeroed() };
|
||||
pfd.fd = self.fd;
|
||||
pfd.events = libc::POLLIN;
|
||||
let nfds = unsafe { libc::poll(&mut pfd, 1, 100) };
|
||||
if nfds == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
if nfds == 0 {
|
||||
debug!("device timeout {}", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read response
|
||||
self.read(&mut buf[..])?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(io_err("no response from device"))
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::errors;
|
||||
use crate::platform::monitor::Monitor;
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::transport::platformmonitor::Monitor;
|
||||
use runloop::RunLoop;
|
||||
use std::ffi::OsString;
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
use crate::consts::{HIDCmd, CID_BROADCAST};
|
||||
use crate::crypto::ECDHSecret;
|
||||
use crate::ctap2::commands::get_info::AuthenticatorInfo;
|
||||
use crate::transport::{errors::HIDError, Nonce};
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo, U2FHIDCont, U2FHIDInit, U2FHIDInitResp};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use std::cmp::Eq;
|
||||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
use std::io;
|
||||
|
||||
pub trait HIDDevice
|
||||
where
|
||||
Self: io::Read,
|
||||
Self: io::Write,
|
||||
Self: U2FDevice,
|
||||
Self: Sized,
|
||||
Self: fmt::Debug,
|
||||
{
|
||||
type BuildParameters: Sized;
|
||||
type Id: fmt::Debug + PartialEq + Eq + Hash + Sized;
|
||||
|
||||
// Open device, verify that it is indeed a CTAP device and potentially read initial values
|
||||
fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)>;
|
||||
fn id(&self) -> Self::Id;
|
||||
fn initialized(&self) -> bool;
|
||||
// Check if the device is actually a token
|
||||
fn is_u2f(&self) -> bool;
|
||||
fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo>;
|
||||
fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo);
|
||||
fn set_shared_secret(&mut self, secret: ECDHSecret);
|
||||
fn get_shared_secret(&self) -> Option<&ECDHSecret>;
|
||||
fn clone_device_as_write_only(&self) -> Result<Self, HIDError>;
|
||||
|
||||
// Initialize on a protocol-level
|
||||
fn initialize(&mut self, noncecmd: Nonce) -> Result<(), HIDError> {
|
||||
if self.initialized() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let nonce = match noncecmd {
|
||||
Nonce::Use(x) => x,
|
||||
Nonce::CreateRandom => {
|
||||
let mut nonce = [0u8; 8];
|
||||
thread_rng().fill_bytes(&mut nonce);
|
||||
nonce
|
||||
}
|
||||
};
|
||||
|
||||
// Send Init to broadcast address to create a new channel
|
||||
self.set_cid(CID_BROADCAST);
|
||||
let (cmd, raw) = self.sendrecv(HIDCmd::Init, &nonce)?;
|
||||
if cmd != HIDCmd::Init {
|
||||
return Err(HIDError::DeviceError);
|
||||
}
|
||||
|
||||
let rsp = U2FHIDInitResp::read(&raw, &nonce)?;
|
||||
// Get the new Channel ID
|
||||
self.set_cid(rsp.cid);
|
||||
|
||||
let vendor = self
|
||||
.get_property("Manufacturer")
|
||||
.unwrap_or_else(|_| String::from("Unknown Vendor"));
|
||||
let product = self
|
||||
.get_property("Product")
|
||||
.unwrap_or_else(|_| String::from("Unknown Device"));
|
||||
|
||||
self.set_device_info(U2FDeviceInfo {
|
||||
vendor_name: vendor.as_bytes().to_vec(),
|
||||
device_name: product.as_bytes().to_vec(),
|
||||
version_interface: rsp.version_interface,
|
||||
version_major: rsp.version_major,
|
||||
version_minor: rsp.version_minor,
|
||||
version_build: rsp.version_build,
|
||||
cap_flags: rsp.cap_flags,
|
||||
});
|
||||
|
||||
// A CTAPHID host SHALL accept a response size that is longer than the
|
||||
// anticipated size to allow for future extensions of the protocol, yet
|
||||
// maintaining backwards compatibility. Future versions will maintain
|
||||
// the response structure of the current version, but additional fields
|
||||
// may be added.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sendrecv(&mut self, cmd: HIDCmd, send: &[u8]) -> io::Result<(HIDCmd, Vec<u8>)> {
|
||||
let cmd: u8 = cmd.into();
|
||||
self.u2f_write(cmd, send)?;
|
||||
loop {
|
||||
let (cmd, data) = self.u2f_read()?;
|
||||
if cmd != HIDCmd::Keepalive {
|
||||
break Ok((cmd, data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn u2f_write(&mut self, cmd: u8, send: &[u8]) -> io::Result<()> {
|
||||
let mut count = U2FHIDInit::write(self, cmd, send)?;
|
||||
|
||||
// Send continuation packets.
|
||||
let mut sequence = 0u8;
|
||||
while count < send.len() {
|
||||
count += U2FHIDCont::write(self, sequence, &send[count..])?;
|
||||
sequence += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn u2f_read(&mut self) -> io::Result<(HIDCmd, Vec<u8>)> {
|
||||
// Now we read. This happens in 2 chunks: The initial packet, which has
|
||||
// the size we expect overall, then continuation packets, which will
|
||||
// fill in data until we have everything.
|
||||
let (cmd, data) = {
|
||||
let (cmd, mut data) = U2FHIDInit::read(self)?;
|
||||
|
||||
trace!("init frame data read: {:04X?}", &data);
|
||||
let mut sequence = 0u8;
|
||||
while data.len() < data.capacity() {
|
||||
let max = data.capacity() - data.len();
|
||||
data.extend_from_slice(&U2FHIDCont::read(self, sequence, max)?);
|
||||
sequence += 1;
|
||||
}
|
||||
(cmd, data)
|
||||
};
|
||||
trace!("u2f_read({:?}) cmd={:?}: {:04X?}", self.id(), cmd, &data);
|
||||
Ok((cmd, data))
|
||||
}
|
||||
|
||||
fn cancel(&mut self) -> Result<(), HIDError> {
|
||||
let cancel: u8 = HIDCmd::Cancel.into();
|
||||
self.u2f_write(cancel, &[])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/* 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/. */
|
||||
|
||||
extern crate libc;
|
||||
use crate::consts::CID_BROADCAST;
|
||||
use crate::transport::hid::HIDDevice;
|
||||
use crate::transport::platform::{hidraw, monitor};
|
||||
use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use crate::util::from_unix_result;
|
||||
use std::fs::OpenOptions;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Device {
|
||||
path: PathBuf,
|
||||
fd: std::fs::File,
|
||||
in_rpt_size: usize,
|
||||
out_rpt_size: usize,
|
||||
cid: [u8; 4],
|
||||
dev_info: Option<U2FDeviceInfo>,
|
||||
secret: Option<ECDHSecret>,
|
||||
authenticator_info: Option<AuthenticatorInfo>,
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other: &Device) -> bool {
|
||||
// The path should be the only identifying member for a device
|
||||
// If the path is the same, its the same device
|
||||
self.path == other.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Device {}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// The path should be the only identifying member for a device
|
||||
// If the path is the same, its the same device
|
||||
self.path.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Device {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let bufp = buf.as_mut_ptr() as *mut libc::c_void;
|
||||
let rv = unsafe { libc::read(self.fd.as_raw_fd(), bufp, buf.len()) };
|
||||
from_unix_result(rv as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Device {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let bufp = buf.as_ptr() as *const libc::c_void;
|
||||
let rv = unsafe { libc::write(self.fd.as_raw_fd(), bufp, buf.len()) };
|
||||
from_unix_result(rv as usize)
|
||||
}
|
||||
|
||||
// USB HID writes don't buffer, so this will be a nop.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl U2FDevice for Device {
|
||||
fn get_cid(&self) -> &[u8; 4] {
|
||||
&self.cid
|
||||
}
|
||||
|
||||
fn set_cid(&mut self, cid: [u8; 4]) {
|
||||
self.cid = cid;
|
||||
}
|
||||
|
||||
fn in_rpt_size(&self) -> usize {
|
||||
self.in_rpt_size
|
||||
}
|
||||
|
||||
fn out_rpt_size(&self) -> usize {
|
||||
self.out_rpt_size
|
||||
}
|
||||
|
||||
fn get_property(&self, prop_name: &str) -> io::Result<String> {
|
||||
monitor::get_property_linux(&self.path, prop_name)
|
||||
}
|
||||
|
||||
fn get_device_info(&self) -> U2FDeviceInfo {
|
||||
// unwrap is okay, as dev_info must have already been set, else
|
||||
// a programmer error
|
||||
self.dev_info.clone().unwrap()
|
||||
}
|
||||
|
||||
fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
|
||||
self.dev_info = Some(dev_info);
|
||||
}
|
||||
}
|
||||
|
||||
impl HIDDevice for Device {
|
||||
type BuildParameters = PathBuf;
|
||||
type Id = PathBuf;
|
||||
|
||||
fn new(path: PathBuf) -> Result<Self, (HIDError, Self::Id)> {
|
||||
debug!("Opening device {:?}", path);
|
||||
let fd = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&path)
|
||||
.map_err(|e| (HIDError::IO(Some(path.clone()), e), path.clone()))?;
|
||||
let (in_rpt_size, out_rpt_size) = hidraw::read_hid_rpt_sizes_or_defaults(fd.as_raw_fd());
|
||||
let res = Self {
|
||||
path,
|
||||
fd,
|
||||
in_rpt_size,
|
||||
out_rpt_size,
|
||||
cid: CID_BROADCAST,
|
||||
dev_info: None,
|
||||
secret: None,
|
||||
authenticator_info: None,
|
||||
};
|
||||
if res.is_u2f() {
|
||||
info!("new device {:?}", res.path);
|
||||
Ok(res)
|
||||
} else {
|
||||
Err((HIDError::DeviceNotSupported, res.path.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn initialized(&self) -> bool {
|
||||
// During successful init, the broadcast channel id gets repplaced by an actual one
|
||||
self.cid != CID_BROADCAST
|
||||
}
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
self.path.clone()
|
||||
}
|
||||
|
||||
fn is_u2f(&self) -> bool {
|
||||
hidraw::is_u2f_device(self.fd.as_raw_fd())
|
||||
}
|
||||
|
||||
fn get_shared_secret(&self) -> Option<&ECDHSecret> {
|
||||
self.secret.as_ref()
|
||||
}
|
||||
|
||||
fn set_shared_secret(&mut self, secret: ECDHSecret) {
|
||||
self.secret = Some(secret);
|
||||
}
|
||||
|
||||
fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
|
||||
self.authenticator_info.as_ref()
|
||||
}
|
||||
|
||||
fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
|
||||
self.authenticator_info = Some(authenticator_info);
|
||||
}
|
||||
|
||||
/// This is used for cancellation of blocking read()-requests.
|
||||
/// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
|
||||
fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
|
||||
let fd = OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&self.path)
|
||||
.map_err(|e| (HIDError::IO(Some(self.path.clone()), e)))?;
|
||||
|
||||
Ok(Self {
|
||||
path: self.path.clone(),
|
||||
fd,
|
||||
in_rpt_size: self.in_rpt_size,
|
||||
out_rpt_size: self.out_rpt_size,
|
||||
cid: self.cid,
|
||||
dev_info: self.dev_info.clone(),
|
||||
secret: self.secret.clone(),
|
||||
authenticator_info: self.authenticator_info.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FidoDevice for Device {}
|
|
@ -10,7 +10,7 @@ use std::os::unix::io::RawFd;
|
|||
|
||||
use super::hidwrapper::{_HIDIOCGRDESC, _HIDIOCGRDESCSIZE};
|
||||
use crate::consts::MAX_HID_RPT_SIZE;
|
||||
use crate::hidproto::*;
|
||||
use crate::transport::hidproto::*;
|
||||
use crate::util::{from_unix_result, io_err};
|
||||
|
||||
#[allow(non_camel_case_types)]
|
|
@ -46,3 +46,6 @@ include!("ioctl_aarch64be.rs");
|
|||
|
||||
#[cfg(all(target_arch = "s390x", target_endian = "big"))]
|
||||
include!("ioctl_s390xbe.rs");
|
||||
|
||||
#[cfg(all(target_arch = "riscv64", target_endian = "little"))]
|
||||
include!("ioctl_riscv64.rs");
|
|
@ -0,0 +1,5 @@
|
|||
/* automatically generated by rust-bindgen */
|
||||
|
||||
pub type __u32 = ::std::os::raw::c_uint;
|
||||
pub const _HIDIOCGRDESCSIZE: __u32 = 2147764225;
|
||||
pub const _HIDIOCGRDESC: __u32 = 2416199682;
|
|
@ -2,14 +2,16 @@
|
|||
* 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::transport::device_selector::DeviceSelectorEvent;
|
||||
use libc::{c_int, c_short, c_ulong};
|
||||
use libudev::EventType;
|
||||
use runloop::RunLoop;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::sync::Arc;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{mpsc::Sender, Arc};
|
||||
|
||||
const UDEV_SUBSYSTEM: &str = "hidraw";
|
||||
const POLLIN: c_short = 0x0001;
|
||||
|
@ -29,34 +31,54 @@ fn poll(fds: &mut Vec<::libc::pollfd>) -> io::Result<()> {
|
|||
|
||||
pub struct Monitor<F>
|
||||
where
|
||||
F: Fn(OsString, &dyn Fn() -> bool) + Sync,
|
||||
F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
|
||||
+ Sync,
|
||||
{
|
||||
runloops: HashMap<OsString, RunLoop>,
|
||||
runloops: HashMap<PathBuf, RunLoop>,
|
||||
new_device_cb: Arc<F>,
|
||||
selector_sender: Sender<DeviceSelectorEvent>,
|
||||
status_sender: Sender<crate::StatusUpdate>,
|
||||
}
|
||||
|
||||
impl<F> Monitor<F>
|
||||
where
|
||||
F: Fn(OsString, &dyn Fn() -> bool) + Send + Sync + 'static,
|
||||
F: Fn(PathBuf, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
pub fn new(new_device_cb: F) -> Self {
|
||||
pub fn new(
|
||||
new_device_cb: F,
|
||||
selector_sender: Sender<DeviceSelectorEvent>,
|
||||
status_sender: Sender<crate::StatusUpdate>,
|
||||
) -> Self {
|
||||
Self {
|
||||
runloops: HashMap::new(),
|
||||
new_device_cb: Arc::new(new_device_cb),
|
||||
selector_sender,
|
||||
status_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self, alive: &dyn Fn() -> bool) -> io::Result<()> {
|
||||
pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
|
||||
let ctx = libudev::Context::new()?;
|
||||
|
||||
let mut enumerator = libudev::Enumerator::new(&ctx)?;
|
||||
enumerator.match_subsystem(UDEV_SUBSYSTEM)?;
|
||||
|
||||
// Iterate all existing devices.
|
||||
for dev in enumerator.scan_devices()? {
|
||||
if let Some(path) = dev.devnode().map(|p| p.to_owned().into_os_string()) {
|
||||
self.add_device(path);
|
||||
}
|
||||
let paths: Vec<PathBuf> = enumerator
|
||||
.scan_devices()?
|
||||
.filter_map(|dev| dev.devnode().map(|p| p.to_owned()))
|
||||
.collect();
|
||||
|
||||
// Add them all in one go to avoid race conditions in DeviceSelector
|
||||
// (8 devices should be added, but the first returns already before all
|
||||
// others are known to DeviceSelector)
|
||||
self.selector_sender
|
||||
.send(DeviceSelectorEvent::DevicesAdded(paths.clone()))?;
|
||||
for path in paths {
|
||||
self.add_device(path);
|
||||
}
|
||||
|
||||
let mut monitor = libudev::Monitor::new(&ctx)?;
|
||||
|
@ -86,13 +108,13 @@ where
|
|||
}
|
||||
|
||||
fn process_event(&mut self, event: &libudev::Event) {
|
||||
let path = event
|
||||
.device()
|
||||
.devnode()
|
||||
.map(|dn| dn.to_owned().into_os_string());
|
||||
let path = event.device().devnode().map(|dn| dn.to_owned());
|
||||
|
||||
match (event.event_type(), path) {
|
||||
(EventType::Add, Some(path)) => {
|
||||
let _ = self
|
||||
.selector_sender
|
||||
.send(DeviceSelectorEvent::DevicesAdded(vec![path.clone()]));
|
||||
self.add_device(path);
|
||||
}
|
||||
(EventType::Remove, Some(path)) => {
|
||||
|
@ -102,15 +124,16 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn add_device(&mut self, path: OsString) {
|
||||
fn add_device(&mut self, path: PathBuf) {
|
||||
let f = self.new_device_cb.clone();
|
||||
let key = path.clone();
|
||||
|
||||
let selector_sender = self.selector_sender.clone();
|
||||
let status_sender = self.status_sender.clone();
|
||||
debug!("Adding device {}", path.to_string_lossy());
|
||||
|
||||
let runloop = RunLoop::new(move |alive| {
|
||||
if alive() {
|
||||
f(path, alive);
|
||||
f(path, selector_sender, status_sender, alive);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -119,9 +142,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn remove_device(&mut self, path: &OsString) {
|
||||
debug!("Removing device {}", path.to_string_lossy());
|
||||
fn remove_device(&mut self, path: &PathBuf) {
|
||||
let _ = self
|
||||
.selector_sender
|
||||
.send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
|
||||
|
||||
debug!("Removing device {}", path.to_string_lossy());
|
||||
if let Some(runloop) = self.runloops.remove(path) {
|
||||
runloop.cancel();
|
||||
}
|
||||
|
@ -135,7 +161,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_property_linux(path: &OsString, prop_name: &str) -> io::Result<String> {
|
||||
pub fn get_property_linux(path: &PathBuf, prop_name: &str) -> io::Result<String> {
|
||||
let ctx = libudev::Context::new()?;
|
||||
|
||||
let mut enumerator = libudev::Enumerator::new(&ctx)?;
|
|
@ -3,30 +3,45 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::errors;
|
||||
use crate::platform::fd::Fd;
|
||||
use crate::platform::monitor::Monitor;
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::transport::device_selector::{
|
||||
DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
|
||||
};
|
||||
use crate::transport::platform::monitor::Monitor;
|
||||
use runloop::RunLoop;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct Transaction {
|
||||
// Handle to the thread loop.
|
||||
thread: Option<RunLoop>,
|
||||
thread: RunLoop,
|
||||
device_selector: DeviceSelector,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
pub fn new<F, T>(
|
||||
timeout: u64,
|
||||
callback: StateCallback<crate::Result<T>>,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
new_device_cb: F,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
F: Fn(Fd, &dyn Fn() -> bool) + Sync + Send + 'static,
|
||||
F: Fn(
|
||||
DeviceBuildParameters,
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Sync
|
||||
+ Send
|
||||
+ 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let status_sender = status.clone();
|
||||
let device_selector = DeviceSelector::run(status);
|
||||
let selector_sender = device_selector.clone_sender();
|
||||
let thread = RunLoop::new_with_timeout(
|
||||
move |alive| {
|
||||
// Create a new device monitor.
|
||||
let mut monitor = Monitor::new(new_device_cb);
|
||||
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
|
||||
|
||||
// Start polling for new devices.
|
||||
try_or!(monitor.run(alive), |_| callback
|
||||
|
@ -42,12 +57,14 @@ impl Transaction {
|
|||
.map_err(|_| errors::AuthenticatorError::Platform)?;
|
||||
|
||||
Ok(Self {
|
||||
thread: Some(thread),
|
||||
thread,
|
||||
device_selector,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
// This must never be None.
|
||||
self.thread.take().unwrap().cancel();
|
||||
info!("Transaction was cancelled.");
|
||||
self.device_selector.stop();
|
||||
self.thread.cancel();
|
||||
}
|
||||
}
|
|
@ -5,11 +5,15 @@
|
|||
extern crate log;
|
||||
|
||||
use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
|
||||
use crate::platform::iokit::*;
|
||||
use crate::transport::hid::HIDDevice;
|
||||
use crate::transport::platform::iokit::*;
|
||||
use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use core_foundation::base::*;
|
||||
use core_foundation::string::*;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::mpsc::{Receiver, RecvTimeoutError};
|
||||
|
@ -20,25 +24,13 @@ const READ_TIMEOUT: u64 = 15;
|
|||
pub struct Device {
|
||||
device_ref: IOHIDDeviceRef,
|
||||
cid: [u8; 4],
|
||||
report_rx: Receiver<Vec<u8>>,
|
||||
report_rx: Option<Receiver<Vec<u8>>>,
|
||||
dev_info: Option<U2FDeviceInfo>,
|
||||
secret: Option<ECDHSecret>,
|
||||
authenticator_info: Option<AuthenticatorInfo>,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn new(dev_ids: (IOHIDDeviceRef, Receiver<Vec<u8>>)) -> io::Result<Self> {
|
||||
let (device_ref, report_rx) = dev_ids;
|
||||
Ok(Self {
|
||||
device_ref,
|
||||
cid: CID_BROADCAST,
|
||||
report_rx,
|
||||
dev_info: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_u2f(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
unsafe fn get_property_macos(&self, prop_name: &str) -> io::Result<String> {
|
||||
let prop_ref = IOHIDDeviceGetProperty(
|
||||
self.device_ref,
|
||||
|
@ -68,25 +60,45 @@ impl Device {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Device {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Device").field("cid", &self.cid).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other_device: &Device) -> bool {
|
||||
self.device_ref == other_device.device_ref
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Device {}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// The path should be the only identifying member for a device
|
||||
// If the path is the same, its the same device
|
||||
self.device_ref.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Device {
|
||||
fn read(&mut self, mut bytes: &mut [u8]) -> io::Result<usize> {
|
||||
let timeout = Duration::from_secs(READ_TIMEOUT);
|
||||
let data = match self.report_rx.recv_timeout(timeout) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e == RecvTimeoutError::Timeout => {
|
||||
return Err(io::Error::new(io::ErrorKind::TimedOut, e));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, e));
|
||||
}
|
||||
};
|
||||
bytes.write(&data)
|
||||
if let Some(rx) = &self.report_rx {
|
||||
let timeout = Duration::from_secs(READ_TIMEOUT);
|
||||
let data = match rx.recv_timeout(timeout) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e == RecvTimeoutError::Timeout => {
|
||||
return Err(io::Error::new(io::ErrorKind::TimedOut, e));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, e));
|
||||
}
|
||||
};
|
||||
bytes.write(&data)
|
||||
} else {
|
||||
Err(io::Error::from(io::ErrorKind::Unsupported))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,3 +166,62 @@ impl U2FDevice for Device {
|
|||
self.dev_info = Some(dev_info);
|
||||
}
|
||||
}
|
||||
|
||||
impl HIDDevice for Device {
|
||||
type BuildParameters = (IOHIDDeviceRef, Receiver<Vec<u8>>);
|
||||
type Id = IOHIDDeviceRef;
|
||||
|
||||
fn new(dev_ids: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
|
||||
let (device_ref, report_rx) = dev_ids;
|
||||
Ok(Self {
|
||||
device_ref,
|
||||
cid: CID_BROADCAST,
|
||||
report_rx: Some(report_rx),
|
||||
dev_info: None,
|
||||
secret: None,
|
||||
authenticator_info: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn initialized(&self) -> bool {
|
||||
self.cid != CID_BROADCAST
|
||||
}
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
self.device_ref
|
||||
}
|
||||
|
||||
fn is_u2f(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn get_shared_secret(&self) -> Option<&ECDHSecret> {
|
||||
self.secret.as_ref()
|
||||
}
|
||||
|
||||
fn set_shared_secret(&mut self, secret: ECDHSecret) {
|
||||
self.secret = Some(secret);
|
||||
}
|
||||
|
||||
fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
|
||||
self.authenticator_info.as_ref()
|
||||
}
|
||||
|
||||
fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
|
||||
self.authenticator_info = Some(authenticator_info);
|
||||
}
|
||||
|
||||
/// This is used for cancellation of blocking read()-requests.
|
||||
/// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
|
||||
fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
|
||||
Ok(Self {
|
||||
device_ref: self.device_ref,
|
||||
cid: self.cid,
|
||||
report_rx: None,
|
||||
dev_info: self.dev_info.clone(),
|
||||
secret: self.secret.clone(),
|
||||
authenticator_info: self.authenticator_info.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FidoDevice for Device {}
|
|
@ -5,7 +5,8 @@
|
|||
extern crate libc;
|
||||
extern crate log;
|
||||
|
||||
use crate::platform::iokit::*;
|
||||
use crate::transport::device_selector::DeviceSelectorEvent;
|
||||
use crate::transport::platform::iokit::*;
|
||||
use crate::util::io_err;
|
||||
use core_foundation::base::*;
|
||||
use core_foundation::runloop::*;
|
||||
|
@ -22,20 +23,40 @@ struct DeviceData {
|
|||
|
||||
pub struct Monitor<F>
|
||||
where
|
||||
F: Fn((IOHIDDeviceRef, Receiver<Vec<u8>>), &dyn Fn() -> bool) + Sync,
|
||||
F: Fn(
|
||||
(IOHIDDeviceRef, Receiver<Vec<u8>>),
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
manager: IOHIDManagerRef,
|
||||
// Keep alive until the monitor goes away.
|
||||
_matcher: IOHIDDeviceMatcher,
|
||||
map: HashMap<IOHIDDeviceRef, DeviceData>,
|
||||
new_device_cb: F,
|
||||
selector_sender: Sender<DeviceSelectorEvent>,
|
||||
status_sender: Sender<crate::StatusUpdate>,
|
||||
}
|
||||
|
||||
impl<F> Monitor<F>
|
||||
where
|
||||
F: Fn((IOHIDDeviceRef, Receiver<Vec<u8>>), &dyn Fn() -> bool) + Sync + 'static,
|
||||
F: Fn(
|
||||
(IOHIDDeviceRef, Receiver<Vec<u8>>),
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
pub fn new(new_device_cb: F) -> Self {
|
||||
pub fn new(
|
||||
new_device_cb: F,
|
||||
selector_sender: Sender<DeviceSelectorEvent>,
|
||||
status_sender: Sender<crate::StatusUpdate>,
|
||||
) -> Self {
|
||||
let manager = unsafe { IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone) };
|
||||
|
||||
// Match FIDO devices only.
|
||||
|
@ -47,6 +68,8 @@ where
|
|||
_matcher,
|
||||
new_device_cb,
|
||||
map: HashMap::new(),
|
||||
selector_sender,
|
||||
status_sender,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,6 +121,9 @@ where
|
|||
|
||||
fn remove_device(&mut self, device_ref: IOHIDDeviceRef) {
|
||||
if let Some(DeviceData { tx, runloop }) = self.map.remove(&device_ref) {
|
||||
let _ = self
|
||||
.selector_sender
|
||||
.send(DeviceSelectorEvent::DeviceRemoved(device_ref));
|
||||
// Dropping `tx` will make Device::read() fail eventually.
|
||||
drop(tx);
|
||||
|
||||
|
@ -137,7 +163,11 @@ where
|
|||
device_ref: IOHIDDeviceRef,
|
||||
) {
|
||||
let this = unsafe { &mut *(context as *mut Self) };
|
||||
|
||||
let _ = this
|
||||
.selector_sender
|
||||
.send(DeviceSelectorEvent::DevicesAdded(vec![device_ref]));
|
||||
let selector_sender = this.selector_sender.clone();
|
||||
let status_sender = this.status_sender.clone();
|
||||
let (tx, rx) = channel();
|
||||
let f = &this.new_device_cb;
|
||||
|
||||
|
@ -145,7 +175,7 @@ where
|
|||
let runloop = RunLoop::new(move |alive| {
|
||||
// Ensure that the runloop is still alive.
|
||||
if alive() {
|
||||
f((device_ref, rx), alive);
|
||||
f((device_ref, rx), selector_sender, status_sender, alive);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -167,7 +197,14 @@ where
|
|||
|
||||
impl<F> Drop for Monitor<F>
|
||||
where
|
||||
F: Fn((IOHIDDeviceRef, Receiver<Vec<u8>>), &dyn Fn() -> bool) + Sync,
|
||||
F: Fn(
|
||||
(IOHIDDeviceRef, Receiver<Vec<u8>>),
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
unsafe { CFRelease(self.manager as *mut c_void) };
|
|
@ -5,12 +5,15 @@
|
|||
extern crate libc;
|
||||
|
||||
use crate::errors;
|
||||
use crate::platform::iokit::{CFRunLoopEntryObserver, IOHIDDeviceRef, SendableRunLoop};
|
||||
use crate::platform::monitor::Monitor;
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::transport::device_selector::{
|
||||
DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
|
||||
};
|
||||
use crate::transport::platform::iokit::{CFRunLoopEntryObserver, SendableRunLoop};
|
||||
use crate::transport::platform::monitor::Monitor;
|
||||
use core_foundation::runloop::*;
|
||||
use std::os::raw::c_void;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::thread;
|
||||
|
||||
// A transaction will run the given closure in a new thread, thereby using a
|
||||
|
@ -20,21 +23,32 @@ use std::thread;
|
|||
pub struct Transaction {
|
||||
runloop: Option<SendableRunLoop>,
|
||||
thread: Option<thread::JoinHandle<()>>,
|
||||
device_selector: DeviceSelector,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
pub fn new<F, T>(
|
||||
timeout: u64,
|
||||
callback: StateCallback<crate::Result<T>>,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
new_device_cb: F,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
F: Fn((IOHIDDeviceRef, Receiver<Vec<u8>>), &dyn Fn() -> bool) + Sync + Send + 'static,
|
||||
F: Fn(
|
||||
DeviceBuildParameters,
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Sync
|
||||
+ Send
|
||||
+ 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let (tx, rx) = channel();
|
||||
let timeout = (timeout as f64) / 1000.0;
|
||||
|
||||
let status_sender = status.clone();
|
||||
let device_selector = DeviceSelector::run(status);
|
||||
let selector_sender = device_selector.clone_sender();
|
||||
let builder = thread::Builder::new();
|
||||
let thread = builder
|
||||
.spawn(move || {
|
||||
|
@ -47,7 +61,7 @@ impl Transaction {
|
|||
obs.add_to_current_runloop();
|
||||
|
||||
// Create a new HID device monitor and start polling.
|
||||
let mut monitor = Monitor::new(new_device_cb);
|
||||
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
|
||||
try_or!(monitor.start(), |_| callback
|
||||
.call(Err(errors::AuthenticatorError::Platform)));
|
||||
|
||||
|
@ -72,6 +86,7 @@ impl Transaction {
|
|||
Ok(Self {
|
||||
runloop: Some(runloop),
|
||||
thread: Some(thread),
|
||||
device_selector,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -86,6 +101,7 @@ impl Transaction {
|
|||
// This must never be None. This won't block.
|
||||
unsafe { CFRunLoopStop(*self.runloop.take().unwrap()) };
|
||||
|
||||
self.device_selector.stop();
|
||||
// This must never be None. Ignore return value.
|
||||
let _ = self.thread.take().unwrap().join();
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
/* 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::consts::CID_BROADCAST;
|
||||
use crate::crypto::ECDHSecret;
|
||||
use crate::ctap2::commands::get_info::AuthenticatorInfo;
|
||||
use crate::transport::device_selector::DeviceCommand;
|
||||
use crate::transport::{hid::HIDDevice, FidoDevice, HIDError};
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
|
||||
pub(crate) const IN_HID_RPT_SIZE: usize = 64;
|
||||
const OUT_HID_RPT_SIZE: usize = 64;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Device {
|
||||
pub id: String,
|
||||
pub cid: [u8; 4],
|
||||
pub reads: Vec<[u8; IN_HID_RPT_SIZE]>,
|
||||
pub writes: Vec<[u8; OUT_HID_RPT_SIZE + 1]>,
|
||||
pub dev_info: Option<U2FDeviceInfo>,
|
||||
pub authenticator_info: Option<AuthenticatorInfo>,
|
||||
pub sender: Option<Sender<DeviceCommand>>,
|
||||
pub receiver: Option<Receiver<DeviceCommand>>,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn add_write(&mut self, packet: &[u8], fill_value: u8) {
|
||||
// Add one to deal with record index check
|
||||
let mut write = [fill_value; OUT_HID_RPT_SIZE + 1];
|
||||
// Make sure we start with a 0, for HID record index
|
||||
write[0] = 0;
|
||||
// Clone packet data in at 1, since front is padded with HID record index
|
||||
write[1..=packet.len()].clone_from_slice(packet);
|
||||
self.writes.push(write);
|
||||
}
|
||||
|
||||
pub fn add_read(&mut self, packet: &[u8], fill_value: u8) {
|
||||
let mut read = [fill_value; IN_HID_RPT_SIZE];
|
||||
read[..packet.len()].clone_from_slice(packet);
|
||||
self.reads.push(read);
|
||||
}
|
||||
|
||||
pub fn create_channel(&mut self) {
|
||||
let (tx, rx) = channel();
|
||||
self.sender = Some(tx);
|
||||
self.receiver = Some(rx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Device {
|
||||
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
||||
// Pop a vector from the expected writes, check for quality
|
||||
// against bytes array.
|
||||
assert!(
|
||||
!self.writes.is_empty(),
|
||||
"Ran out of expected write values! Wanted to write {:?}",
|
||||
bytes
|
||||
);
|
||||
let check = self.writes.remove(0);
|
||||
assert_eq!(check.len(), bytes.len());
|
||||
assert_eq!(&check, bytes);
|
||||
Ok(bytes.len())
|
||||
}
|
||||
|
||||
// nop
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Device {
|
||||
fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
|
||||
assert!(!self.reads.is_empty(), "Ran out of read values!");
|
||||
let check = self.reads.remove(0);
|
||||
assert_eq!(check.len(), bytes.len());
|
||||
bytes.clone_from_slice(&check);
|
||||
Ok(check.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Device {
|
||||
fn drop(&mut self) {
|
||||
if !std::thread::panicking() {
|
||||
assert!(self.reads.is_empty());
|
||||
assert!(self.writes.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other: &Device) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Device {}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl U2FDevice for Device {
|
||||
fn get_cid<'a>(&'a self) -> &'a [u8; 4] {
|
||||
&self.cid
|
||||
}
|
||||
|
||||
fn set_cid(&mut self, cid: [u8; 4]) {
|
||||
self.cid = cid;
|
||||
}
|
||||
|
||||
fn in_rpt_size(&self) -> usize {
|
||||
IN_HID_RPT_SIZE
|
||||
}
|
||||
|
||||
fn out_rpt_size(&self) -> usize {
|
||||
OUT_HID_RPT_SIZE
|
||||
}
|
||||
|
||||
fn get_property(&self, prop_name: &str) -> io::Result<String> {
|
||||
Ok(format!("{} not implemented", prop_name))
|
||||
}
|
||||
fn get_device_info(&self) -> U2FDeviceInfo {
|
||||
self.dev_info.clone().unwrap()
|
||||
}
|
||||
|
||||
fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
|
||||
self.dev_info = Some(dev_info);
|
||||
}
|
||||
}
|
||||
|
||||
impl HIDDevice for Device {
|
||||
type Id = String;
|
||||
type BuildParameters = &'static str; // None used
|
||||
|
||||
fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
|
||||
self.authenticator_info.as_ref()
|
||||
}
|
||||
|
||||
fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
|
||||
self.authenticator_info = Some(authenticator_info);
|
||||
}
|
||||
|
||||
fn set_shared_secret(&mut self, _: ECDHSecret) {
|
||||
// Nothing
|
||||
}
|
||||
fn get_shared_secret(&self) -> std::option::Option<&ECDHSecret> {
|
||||
None
|
||||
}
|
||||
|
||||
fn new(id: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
|
||||
Ok(Device {
|
||||
id: id.to_string(),
|
||||
cid: CID_BROADCAST,
|
||||
reads: vec![],
|
||||
writes: vec![],
|
||||
dev_info: None,
|
||||
authenticator_info: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn initialized(&self) -> bool {
|
||||
self.get_cid() != &CID_BROADCAST
|
||||
}
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
|
||||
Ok(Device {
|
||||
id: self.id.clone(),
|
||||
cid: self.cid,
|
||||
reads: self.reads.clone(),
|
||||
writes: self.writes.clone(),
|
||||
dev_info: self.dev_info.clone(),
|
||||
authenticator_info: self.authenticator_info.clone(),
|
||||
sender: self.sender.clone(),
|
||||
receiver: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_u2f(&self) -> bool {
|
||||
self.sender.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl FidoDevice for Device {}
|
|
@ -0,0 +1,6 @@
|
|||
/* 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 mod device;
|
||||
pub mod transaction;
|
|
@ -0,0 +1,35 @@
|
|||
/* 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::statecallback::StateCallback;
|
||||
use crate::transport::device_selector::{DeviceBuildParameters, DeviceSelectorEvent};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct Transaction {}
|
||||
|
||||
impl Transaction {
|
||||
pub fn new<F, T>(
|
||||
_timeout: u64,
|
||||
_callback: StateCallback<crate::Result<T>>,
|
||||
_status: Sender<crate::StatusUpdate>,
|
||||
_new_device_cb: F,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
F: Fn(
|
||||
DeviceBuildParameters,
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Sync
|
||||
+ Send
|
||||
+ 'static,
|
||||
T: 'static,
|
||||
{
|
||||
Ok(Self {})
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
info!("Transaction was cancelled.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
use crate::consts::{Capability, HIDCmd};
|
||||
use crate::crypto::ECDHSecret;
|
||||
|
||||
use crate::ctap2::commands::client_pin::{GetKeyAgreement, PinAuth};
|
||||
use crate::ctap2::commands::get_info::{AuthenticatorInfo, GetInfo};
|
||||
use crate::ctap2::commands::get_version::GetVersion;
|
||||
use crate::ctap2::commands::make_credentials::dummy_make_credentials_cmd;
|
||||
use crate::ctap2::commands::selection::Selection;
|
||||
use crate::ctap2::commands::{
|
||||
CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable, StatusCode,
|
||||
};
|
||||
use crate::transport::device_selector::BlinkResult;
|
||||
use crate::transport::errors::{ApduErrorStatus, HIDError};
|
||||
use crate::transport::hid::HIDDevice;
|
||||
use crate::util::io_err;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod device_selector;
|
||||
pub mod errors;
|
||||
pub mod hid;
|
||||
|
||||
#[cfg(all(
|
||||
any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"),
|
||||
not(test)
|
||||
))]
|
||||
pub mod hidproto;
|
||||
|
||||
#[cfg(all(target_os = "linux", not(test)))]
|
||||
#[path = "linux/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(all(target_os = "freebsd", not(test)))]
|
||||
#[path = "freebsd/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(all(target_os = "netbsd", not(test)))]
|
||||
#[path = "netbsd/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(all(target_os = "openbsd", not(test)))]
|
||||
#[path = "openbsd/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(all(target_os = "macos", not(test)))]
|
||||
#[path = "macos/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(all(target_os = "windows", not(test)))]
|
||||
#[path = "windows/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "macos",
|
||||
target_os = "windows",
|
||||
test
|
||||
)))]
|
||||
#[path = "stub/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "mock/mod.rs"]
|
||||
pub mod platform;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Nonce {
|
||||
CreateRandom,
|
||||
Use([u8; 8]),
|
||||
}
|
||||
|
||||
// TODO(MS): This is the lazy way: FidoDevice currently only extends HIDDevice by more functions,
|
||||
// but the goal is to remove U2FDevice entirely and copy over the trait-definition here
|
||||
pub trait FidoDevice: HIDDevice {
|
||||
fn send_msg<'msg, Out, Req: Request<Out>>(&mut self, msg: &'msg Req) -> Result<Out, HIDError> {
|
||||
if !self.initialized() {
|
||||
return Err(HIDError::DeviceNotInitialized);
|
||||
}
|
||||
|
||||
if self.supports_ctap2() && msg.is_ctap2_request() {
|
||||
self.send_cbor(msg)
|
||||
} else {
|
||||
self.send_ctap1(msg)
|
||||
}
|
||||
}
|
||||
|
||||
fn send_cbor<'msg, Req: RequestCtap2>(
|
||||
&mut self,
|
||||
msg: &'msg Req,
|
||||
) -> Result<Req::Output, HIDError> {
|
||||
debug!("sending {:?} to {:?}", msg, self);
|
||||
|
||||
let mut data = msg.wire_format(self)?;
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 1);
|
||||
// CTAP2 command
|
||||
buf.push(Req::command() as u8);
|
||||
// payload
|
||||
buf.append(&mut data);
|
||||
let buf = buf;
|
||||
|
||||
let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf)?;
|
||||
debug!(
|
||||
"got from Device {:?} status={:?}: {:?}",
|
||||
self.id(),
|
||||
cmd,
|
||||
resp
|
||||
);
|
||||
if cmd == HIDCmd::Cbor {
|
||||
Ok(msg.handle_response_ctap2(self, &resp)?)
|
||||
} else {
|
||||
Err(HIDError::UnexpectedCmd(cmd.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn send_ctap1<'msg, Req: RequestCtap1>(
|
||||
&mut self,
|
||||
msg: &'msg Req,
|
||||
) -> Result<Req::Output, HIDError> {
|
||||
debug!("sending {:?} to {:?}", msg, self);
|
||||
let data = msg.ctap1_format(self)?;
|
||||
|
||||
loop {
|
||||
let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data)?;
|
||||
debug!(
|
||||
"got from Device {:?} status={:?}: {:?}",
|
||||
self.id(),
|
||||
cmd,
|
||||
data
|
||||
);
|
||||
if cmd == HIDCmd::Msg {
|
||||
if data.len() < 2 {
|
||||
return Err(io_err("Unexpected Response: shorter than expected").into());
|
||||
}
|
||||
let split_at = data.len() - 2;
|
||||
let status = data.split_off(split_at);
|
||||
// This will bubble up error if status != no error
|
||||
let status = ApduErrorStatus::from([status[0], status[1]]);
|
||||
|
||||
match msg.handle_response_ctap1(status, &data) {
|
||||
Ok(out) => return Ok(out),
|
||||
Err(Retryable::Retry) => {
|
||||
// sleep 100ms then loop again
|
||||
// TODO(baloo): meh, use tokio instead?
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(Retryable::Error(e)) => return Err(e),
|
||||
}
|
||||
} else {
|
||||
return Err(HIDError::UnexpectedCmd(cmd.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is ugly as we have 2 init-functions now, but the fastest way currently.
|
||||
fn init(&mut self, nonce: Nonce) -> Result<(), HIDError> {
|
||||
let resp = <Self as HIDDevice>::initialize(self, nonce)?;
|
||||
// TODO(baloo): this logic should be moved to
|
||||
// transport/mod.rs::Device trait
|
||||
if self.supports_ctap2() {
|
||||
let command = GetInfo::default();
|
||||
let info = self.send_cbor(&command)?;
|
||||
debug!("{:?} infos: {:?}", self.id(), info);
|
||||
|
||||
self.set_authenticator_info(info);
|
||||
}
|
||||
if self.supports_ctap1() {
|
||||
let command = GetVersion::default();
|
||||
// We don't really use the result here
|
||||
self.send_ctap1(&command)?;
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
fn block_and_blink(&mut self) -> BlinkResult {
|
||||
let resp;
|
||||
let supports_select_cmd = self
|
||||
.get_authenticator_info()
|
||||
.map_or(false, |i| i.versions.contains(&String::from("FIDO_2_1")));
|
||||
if supports_select_cmd {
|
||||
let msg = Selection {};
|
||||
resp = self.send_cbor(&msg);
|
||||
} else {
|
||||
// We need to fake a blink-request, because FIDO2.0 forgot to specify one
|
||||
// See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential
|
||||
let mut msg = match dummy_make_credentials_cmd() {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
return BlinkResult::Cancelled;
|
||||
}
|
||||
};
|
||||
// Using a zero-length pinAuth will trigger the device to blink
|
||||
// For CTAP1, this gets ignored anyways and we do a 'normal' register
|
||||
// command, which also just blinks.
|
||||
msg.set_pin_auth(Some(PinAuth::empty_pin_auth()), None);
|
||||
info!("Trying to blink: {:?}", &msg);
|
||||
// We don't care about the Ok-value, just if it is Ok or not
|
||||
resp = self.send_msg(&msg).map(|_| ());
|
||||
}
|
||||
match resp {
|
||||
// Spec only says PinInvalid or PinNotSet should be returned on the fake touch-request,
|
||||
// but Yubikeys for example return PinAuthInvalid. A successful return is also possible
|
||||
// for CTAP1-tokens so we catch those here as well.
|
||||
Ok(_)
|
||||
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)))
|
||||
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)))
|
||||
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _))) => {
|
||||
BlinkResult::DeviceSelected
|
||||
}
|
||||
// We cancelled the receive, because another device was selected.
|
||||
Err(HIDError::Command(CommandError::StatusCode(StatusCode::KeepaliveCancel, _)))
|
||||
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _)))
|
||||
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::UserActionTimeout, _))) => {
|
||||
// TODO: Repeat the request, if it is a UserActionTimeout?
|
||||
debug!("Device {:?} got cancelled", &self);
|
||||
BlinkResult::Cancelled
|
||||
}
|
||||
// Something unexpected happened, so we assume this device is not usable and interpreting
|
||||
// this equivalent to being cancelled.
|
||||
e => {
|
||||
info!("Device {:?} received unexpected answer, so we assume an error occurred and we are NOT using this device (assuming the request was cancelled): {:?}", &self, e);
|
||||
BlinkResult::Cancelled
|
||||
}
|
||||
}
|
||||
}
|
||||
fn supports_ctap1(&self) -> bool {
|
||||
// CAPABILITY_NMSG:
|
||||
// If set to 1, authenticator DOES NOT implement U2FHID_MSG function
|
||||
!self.get_device_info().cap_flags.contains(Capability::NMSG)
|
||||
}
|
||||
|
||||
fn supports_ctap2(&self) -> bool {
|
||||
self.get_device_info().cap_flags.contains(Capability::CBOR)
|
||||
}
|
||||
|
||||
fn establish_shared_secret(&mut self) -> Result<(ECDHSecret, AuthenticatorInfo), HIDError> {
|
||||
if !self.supports_ctap2() {
|
||||
return Err(HIDError::UnsupportedCommand);
|
||||
}
|
||||
|
||||
let info = if let Some(authenticator_info) = self.get_authenticator_info().cloned() {
|
||||
authenticator_info
|
||||
} else {
|
||||
// We should already have it, since it is queried upon `init()`, but just to be safe
|
||||
let info_command = GetInfo::default();
|
||||
let info = self.send_cbor(&info_command)?;
|
||||
debug!("infos: {:?}", info);
|
||||
|
||||
self.set_authenticator_info(info.clone());
|
||||
info
|
||||
};
|
||||
|
||||
// Not reusing the shared secret here, if it exists, since we might start again
|
||||
// with a different PIN (e.g. if the last one was wrong)
|
||||
let pin_command = GetKeyAgreement::new(&info)?;
|
||||
let device_key_agreement = self.send_cbor(&pin_command)?;
|
||||
let shared_secret = device_key_agreement.shared_secret()?;
|
||||
self.set_shared_secret(shared_secret.clone());
|
||||
Ok((shared_secret, info))
|
||||
}
|
||||
}
|
|
@ -11,8 +11,8 @@ use std::mem;
|
|||
|
||||
use crate::consts::CID_BROADCAST;
|
||||
use crate::consts::MAX_HID_RPT_SIZE;
|
||||
use crate::platform::fd::Fd;
|
||||
use crate::platform::uhid;
|
||||
use crate::transport::platform::fd::Fd;
|
||||
use crate::transport::platform::uhid;
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use crate::util::io_err;
|
||||
|
||||
|
@ -97,6 +97,16 @@ impl PartialEq for Device {
|
|||
}
|
||||
}
|
||||
|
||||
impl Eq for Device {}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// The path should be the only identifying member for a device
|
||||
// If the path is the same, its the same device
|
||||
self.fd.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Device {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let bufp = buf.as_mut_ptr() as *mut libc::c_void;
|
|
@ -45,3 +45,17 @@ impl PartialEq for Fd {
|
|||
(st.st_dev == sto.st_dev) & (st.st_ino == sto.st_ino)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Eq for Device {}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
let mut st: libc::stat = unsafe { mem::zeroed() };
|
||||
if unsafe { libc::fstat(self.fileno, &mut st) } == -1 {
|
||||
return;
|
||||
}
|
||||
st.st_dev.hash(state);
|
||||
st.st_ino.hash(state);
|
||||
}
|
||||
}
|
|
@ -2,16 +2,17 @@
|
|||
* 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::transport::device_selector::DeviceSelectorEvent;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use std::sync::{mpsc::Sender, Arc};
|
||||
use runloop::RunLoop;
|
||||
|
||||
use crate::platform::fd::Fd;
|
||||
use crate::transport::platform::fd::Fd;
|
||||
|
||||
// XXX Should use drvctl, but it doesn't do pubsub properly yet so
|
||||
// DRVGETEVENT requires write access to /dev/drvctl. Instead, for now,
|
||||
|
@ -20,24 +21,44 @@ const POLL_TIMEOUT: u64 = 500;
|
|||
|
||||
pub struct Monitor<F>
|
||||
where
|
||||
F: Fn(Fd, &dyn Fn() -> bool) + Send + Sync + 'static,
|
||||
F: Fn(
|
||||
Fd,
|
||||
OsString,
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Sync,
|
||||
{
|
||||
runloops: HashMap<OsString, RunLoop>,
|
||||
new_device_cb: Arc<F>,
|
||||
selector_sender: Sender<DeviceSelectorEvent>,
|
||||
status_sender: Sender<crate::StatusUpdate>,
|
||||
}
|
||||
|
||||
impl<F> Monitor<F>
|
||||
where
|
||||
F: Fn(Fd, &dyn Fn() -> bool) + Send + Sync + 'static,
|
||||
F: Fn(
|
||||
Fd,
|
||||
OsString,
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Sync,
|
||||
{
|
||||
pub fn new(new_device_cb: F) -> Self {
|
||||
pub fn new(
|
||||
new_device_cb: F,
|
||||
selector_sender: Sender<DeviceSelectorEvent>,
|
||||
status_sender: Sender<crate::StatusUpdate>,
|
||||
) -> Self {
|
||||
Self {
|
||||
runloops: HashMap::new(),
|
||||
new_device_cb: Arc::new(new_device_cb),
|
||||
selector_sender,
|
||||
status_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self, alive: &dyn Fn() -> bool) -> io::Result<()> {
|
||||
pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
|
||||
while alive() {
|
||||
for n in 0..100 {
|
||||
let uhidpath = format!("/dev/uhid{}", n);
|
||||
|
@ -60,10 +81,13 @@ where
|
|||
|
||||
fn add_device(&mut self, fd: Fd, path: OsString) {
|
||||
let f = self.new_device_cb.clone();
|
||||
let selector_sender = self.selector_sender.clone();
|
||||
let status_sender = self.status_sender.clone();
|
||||
debug!("Adding device {}", path.to_string_lossy());
|
||||
|
||||
let runloop = RunLoop::new(move |alive| {
|
||||
if alive() {
|
||||
f(fd, alive);
|
||||
f(fd.clone(), path.clone(), selector_sender, status_sender, alive);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -73,6 +97,11 @@ where
|
|||
}
|
||||
|
||||
fn remove_device(&mut self, path: OsString) {
|
||||
let _ = self
|
||||
.selector_sender
|
||||
.send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
|
||||
|
||||
debug!("Removing device {}", path.to_string_lossy());
|
||||
if let Some(runloop) = self.runloops.remove(&path) {
|
||||
runloop.cancel();
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/* 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::statecallback::StateCallback;
|
||||
use crate::transport::platform::fd::Fd;
|
||||
use crate::transport::device_selector::{
|
||||
DeviceBuildParameters, DeviceID, DeviceSelector, DeviceSelectorEvent,
|
||||
};
|
||||
use crate::transport::platform::monitor::Monitor;
|
||||
use runloop::RunLoop;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct Transaction {
|
||||
// Handle to the thread loop.
|
||||
thread: RunLoop,
|
||||
device_selector: DeviceSelector,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
pub fn new<F, T>(
|
||||
timeout: u64,
|
||||
callback: StateCallback<crate::Result<T>>,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
new_device_cb: F,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
F: Fn(
|
||||
DeviceBuildParameters,
|
||||
DeviceID,
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Sync
|
||||
+ Send
|
||||
+ 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let status_sender = status.clone();
|
||||
let device_selector = DeviceSelector::run(status);
|
||||
let selector_sender = device_selector.clone_sender();
|
||||
let thread = RunLoop::new_with_timeout(
|
||||
move |alive| {
|
||||
// Create a new device monitor.
|
||||
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
|
||||
|
||||
// Start polling for new devices.
|
||||
try_or!(monitor.run(alive), |_| callback
|
||||
.call(Err(errors::AuthenticatorError::Platform)));
|
||||
|
||||
// Send an error, if the callback wasn't called already.
|
||||
callback.call(Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::NotAllowed,
|
||||
)));
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
.map_err(|_| errors::AuthenticatorError::Platform)?;
|
||||
|
||||
Ok(Self {
|
||||
thread,
|
||||
device_selector,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
info!("Transaction was cancelled.");
|
||||
self.thread.cancel();
|
||||
self.device_selector.stop();
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ use std::os::raw::c_uchar;
|
|||
|
||||
use crate::hidproto::has_fido_usage;
|
||||
use crate::hidproto::ReportDescriptor;
|
||||
use crate::platform::fd::Fd;
|
||||
use crate::transport::platform::fd::Fd;
|
||||
use crate::util::io_err;
|
||||
|
||||
/* sys/ioccom.h */
|
|
@ -10,7 +10,7 @@ use std::io::{Read, Result, Write};
|
|||
use std::mem;
|
||||
|
||||
use crate::consts::{CID_BROADCAST, MAX_HID_RPT_SIZE};
|
||||
use crate::platform::monitor::FidoDev;
|
||||
use crate::transport::platform::monitor::FidoDev;
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use crate::util::{from_unix_result, io_err};
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::errors;
|
||||
use crate::platform::monitor::{FidoDev, Monitor};
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::transport::platform::monitor::{FidoDev, Monitor};
|
||||
use runloop::RunLoop;
|
||||
|
||||
pub struct Transaction {
|
|
@ -2,22 +2,18 @@
|
|||
* 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::transport::hid::HIDDevice;
|
||||
use crate::transport::FidoDevice;
|
||||
use crate::transport::{AuthenticatorInfo, ECDHSecret, HIDError};
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Device {}
|
||||
|
||||
impl Device {
|
||||
pub fn new(path: String) -> io::Result<Self> {
|
||||
panic!("not implemented");
|
||||
}
|
||||
|
||||
pub fn is_u2f(&self) -> bool {
|
||||
panic!("not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Device {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
panic!("not implemented");
|
||||
|
@ -63,3 +59,52 @@ impl U2FDevice for Device {
|
|||
panic!("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
impl HIDDevice for Device {
|
||||
type BuildParameters = PathBuf;
|
||||
type Id = PathBuf;
|
||||
|
||||
fn new(parameters: Self::BuildParameters) -> Result<Self, (HIDError, Self::Id)> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn initialized(&self) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_u2f(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_shared_secret(&mut self, secret: ECDHSecret) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_shared_secret(&self) -> Option<&ECDHSecret> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FidoDevice for Device {}
|
|
@ -0,0 +1,52 @@
|
|||
/* 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::statecallback::StateCallback;
|
||||
use crate::transport::device_selector::{
|
||||
DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct Transaction {}
|
||||
|
||||
impl Transaction {
|
||||
pub fn new<F, T>(
|
||||
timeout: u64,
|
||||
callback: StateCallback<crate::Result<T>>,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
new_device_cb: F,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
F: Fn(
|
||||
DeviceBuildParameters,
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Sync
|
||||
+ Send
|
||||
+ 'static,
|
||||
T: 'static,
|
||||
{
|
||||
// Just to silence "unused"-warnings
|
||||
let mut device_selector = DeviceSelector::run(status);
|
||||
let _ = DeviceSelectorEvent::DevicesAdded(vec![]);
|
||||
let _ = DeviceSelectorEvent::DeviceRemoved(PathBuf::new());
|
||||
let _ = device_selector.clone_sender();
|
||||
device_selector.stop();
|
||||
|
||||
callback.call(Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::NotSupported,
|
||||
)));
|
||||
|
||||
Err(errors::AuthenticatorError::U2FToken(
|
||||
errors::U2FTokenError::NotSupported,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
/* No-op. */
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/* 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 super::winapi::DeviceCapabilities;
|
||||
use crate::consts::{CID_BROADCAST, FIDO_USAGE_PAGE, FIDO_USAGE_U2FHID, MAX_HID_RPT_SIZE};
|
||||
use crate::transport::hid::HIDDevice;
|
||||
use crate::transport::{AuthenticatorInfo, ECDHSecret, FidoDevice, HIDError};
|
||||
use crate::u2ftypes::{U2FDevice, U2FDeviceInfo};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Device {
|
||||
path: String,
|
||||
file: File,
|
||||
cid: [u8; 4],
|
||||
dev_info: Option<U2FDeviceInfo>,
|
||||
secret: Option<ECDHSecret>,
|
||||
authenticator_info: Option<AuthenticatorInfo>,
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other: &Device) -> bool {
|
||||
self.path == other.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Device {}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// The path should be the only identifying member for a device
|
||||
// If the path is the same, its the same device
|
||||
self.path.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Device {
|
||||
fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
|
||||
// Windows always includes the report ID.
|
||||
let mut input = [0u8; MAX_HID_RPT_SIZE + 1];
|
||||
let _ = self.file.read(&mut input)?;
|
||||
bytes.clone_from_slice(&input[1..]);
|
||||
Ok(bytes.len() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Device {
|
||||
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
||||
self.file.write(bytes)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.file.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl U2FDevice for Device {
|
||||
fn get_cid(&self) -> &[u8; 4] {
|
||||
&self.cid
|
||||
}
|
||||
|
||||
fn set_cid(&mut self, cid: [u8; 4]) {
|
||||
self.cid = cid;
|
||||
}
|
||||
|
||||
fn in_rpt_size(&self) -> usize {
|
||||
MAX_HID_RPT_SIZE
|
||||
}
|
||||
|
||||
fn out_rpt_size(&self) -> usize {
|
||||
MAX_HID_RPT_SIZE
|
||||
}
|
||||
|
||||
fn get_property(&self, _prop_name: &str) -> io::Result<String> {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "Not implemented"))
|
||||
}
|
||||
|
||||
fn get_device_info(&self) -> U2FDeviceInfo {
|
||||
// unwrap is okay, as dev_info must have already been set, else
|
||||
// a programmer error
|
||||
self.dev_info.clone().unwrap()
|
||||
}
|
||||
|
||||
fn set_device_info(&mut self, dev_info: U2FDeviceInfo) {
|
||||
self.dev_info = Some(dev_info);
|
||||
}
|
||||
}
|
||||
|
||||
impl HIDDevice for Device {
|
||||
type BuildParameters = String;
|
||||
type Id = String;
|
||||
|
||||
fn new(path: String) -> Result<Self, (HIDError, Self::Id)> {
|
||||
debug!("Opening device {:?}", path);
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&path)
|
||||
.map_err(|e| (HIDError::IO(Some(path.clone().into()), e), path.clone()))?;
|
||||
let res = Self {
|
||||
path,
|
||||
file,
|
||||
cid: CID_BROADCAST,
|
||||
dev_info: None,
|
||||
secret: None,
|
||||
authenticator_info: None,
|
||||
};
|
||||
if res.is_u2f() {
|
||||
info!("new device {:?}", res.path);
|
||||
Ok(res)
|
||||
} else {
|
||||
Err((HIDError::DeviceNotSupported, res.path.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
fn initialized(&self) -> bool {
|
||||
// During successful init, the broadcast channel id gets repplaced by an actual one
|
||||
self.cid != CID_BROADCAST
|
||||
}
|
||||
|
||||
fn id(&self) -> Self::Id {
|
||||
self.path.clone()
|
||||
}
|
||||
|
||||
fn is_u2f(&self) -> bool {
|
||||
match DeviceCapabilities::new(self.file.as_raw_handle()) {
|
||||
Ok(caps) => caps.usage() == FIDO_USAGE_U2FHID && caps.usage_page() == FIDO_USAGE_PAGE,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_shared_secret(&self) -> Option<&ECDHSecret> {
|
||||
self.secret.as_ref()
|
||||
}
|
||||
|
||||
fn set_shared_secret(&mut self, secret: ECDHSecret) {
|
||||
self.secret = Some(secret);
|
||||
}
|
||||
|
||||
fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> {
|
||||
self.authenticator_info.as_ref()
|
||||
}
|
||||
|
||||
fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) {
|
||||
self.authenticator_info = Some(authenticator_info);
|
||||
}
|
||||
|
||||
/// This is used for cancellation of blocking read()-requests.
|
||||
/// With this, we can clone the Device, pass it to another thread and call "cancel()" on that.
|
||||
fn clone_device_as_write_only(&self) -> Result<Self, HIDError> {
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&self.path)
|
||||
.map_err(|e| (HIDError::IO(Some(self.path.clone().into()), e)))?;
|
||||
|
||||
Ok(Self {
|
||||
path: self.path.clone(),
|
||||
file,
|
||||
cid: self.cid,
|
||||
dev_info: self.dev_info.clone(),
|
||||
secret: self.secret.clone(),
|
||||
authenticator_info: self.authenticator_info.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FidoDevice for Device {}
|
|
@ -2,35 +2,48 @@
|
|||
* 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::platform::winapi::DeviceInfoSet;
|
||||
use crate::transport::device_selector::DeviceSelectorEvent;
|
||||
use crate::transport::platform::winapi::DeviceInfoSet;
|
||||
use runloop::RunLoop;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io;
|
||||
use std::error::Error;
|
||||
use std::iter::FromIterator;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{mpsc::Sender, Arc};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Monitor<F>
|
||||
where
|
||||
F: Fn(String, &dyn Fn() -> bool) + Sync,
|
||||
F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
|
||||
+ Sync,
|
||||
{
|
||||
runloops: HashMap<String, RunLoop>,
|
||||
new_device_cb: Arc<F>,
|
||||
selector_sender: Sender<DeviceSelectorEvent>,
|
||||
status_sender: Sender<crate::StatusUpdate>,
|
||||
}
|
||||
|
||||
impl<F> Monitor<F>
|
||||
where
|
||||
F: Fn(String, &dyn Fn() -> bool) + Send + Sync + 'static,
|
||||
F: Fn(String, Sender<DeviceSelectorEvent>, Sender<crate::StatusUpdate>, &dyn Fn() -> bool)
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
pub fn new(new_device_cb: F) -> Self {
|
||||
pub fn new(
|
||||
new_device_cb: F,
|
||||
selector_sender: Sender<DeviceSelectorEvent>,
|
||||
status_sender: Sender<crate::StatusUpdate>,
|
||||
) -> Self {
|
||||
Self {
|
||||
runloops: HashMap::new(),
|
||||
new_device_cb: Arc::new(new_device_cb),
|
||||
selector_sender,
|
||||
status_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self, alive: &dyn Fn() -> bool) -> io::Result<()> {
|
||||
pub fn run(&mut self, alive: &dyn Fn() -> bool) -> Result<(), Box<dyn Error>> {
|
||||
let mut stored = HashSet::new();
|
||||
|
||||
while alive() {
|
||||
|
@ -42,9 +55,12 @@ where
|
|||
self.remove_device(path);
|
||||
}
|
||||
|
||||
let paths: Vec<_> = devices.difference(&stored).cloned().collect();
|
||||
self.selector_sender
|
||||
.send(DeviceSelectorEvent::DevicesAdded(paths.clone()))?;
|
||||
// Add devices that were plugged in.
|
||||
for path in devices.difference(&stored) {
|
||||
self.add_device(path);
|
||||
for path in paths {
|
||||
self.add_device(&path);
|
||||
}
|
||||
|
||||
// Remember the new set.
|
||||
|
@ -64,10 +80,13 @@ where
|
|||
let f = self.new_device_cb.clone();
|
||||
let path = path.clone();
|
||||
let key = path.clone();
|
||||
let selector_sender = self.selector_sender.clone();
|
||||
let status_sender = self.status_sender.clone();
|
||||
debug!("Adding device {}", path);
|
||||
|
||||
let runloop = RunLoop::new(move |alive| {
|
||||
if alive() {
|
||||
f(path, alive);
|
||||
f(path, selector_sender, status_sender, alive);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -77,6 +96,11 @@ where
|
|||
}
|
||||
|
||||
fn remove_device(&mut self, path: &String) {
|
||||
let _ = self
|
||||
.selector_sender
|
||||
.send(DeviceSelectorEvent::DeviceRemoved(path.clone()));
|
||||
|
||||
debug!("Removing device {}", path);
|
||||
if let Some(runloop) = self.runloops.remove(path) {
|
||||
runloop.cancel();
|
||||
}
|
|
@ -3,30 +3,45 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::errors;
|
||||
use crate::platform::monitor::Monitor;
|
||||
use crate::statecallback::StateCallback;
|
||||
use crate::transport::device_selector::{
|
||||
DeviceBuildParameters, DeviceSelector, DeviceSelectorEvent,
|
||||
};
|
||||
use crate::transport::platform::monitor::Monitor;
|
||||
use runloop::RunLoop;
|
||||
use std::ffi::OsString;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct Transaction {
|
||||
// Handle to the thread loop.
|
||||
thread: Option<RunLoop>,
|
||||
thread: RunLoop,
|
||||
device_selector: DeviceSelector,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
pub fn new<F, T>(
|
||||
timeout: u64,
|
||||
callback: StateCallback<crate::Result<T>>,
|
||||
status: Sender<crate::StatusUpdate>,
|
||||
new_device_cb: F,
|
||||
) -> crate::Result<Self>
|
||||
where
|
||||
F: Fn(OsString, &dyn Fn() -> bool) + Sync + Send + 'static,
|
||||
F: Fn(
|
||||
DeviceBuildParameters,
|
||||
Sender<DeviceSelectorEvent>,
|
||||
Sender<crate::StatusUpdate>,
|
||||
&dyn Fn() -> bool,
|
||||
) + Sync
|
||||
+ Send
|
||||
+ 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let status_sender = status.clone();
|
||||
let device_selector = DeviceSelector::run(status);
|
||||
let selector_sender = device_selector.clone_sender();
|
||||
let thread = RunLoop::new_with_timeout(
|
||||
move |alive| {
|
||||
// Create a new device monitor.
|
||||
let mut monitor = Monitor::new(new_device_cb);
|
||||
let mut monitor = Monitor::new(new_device_cb, selector_sender, status_sender);
|
||||
|
||||
// Start polling for new devices.
|
||||
try_or!(monitor.run(alive), |_| callback
|
||||
|
@ -42,12 +57,14 @@ impl Transaction {
|
|||
.map_err(|_| errors::AuthenticatorError::Platform)?;
|
||||
|
||||
Ok(Self {
|
||||
thread: Some(thread),
|
||||
thread,
|
||||
device_selector,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
// This must never be None.
|
||||
self.thread.take().unwrap().cancel();
|
||||
info!("Transaction was cancelled.");
|
||||
self.device_selector.stop();
|
||||
self.thread.cancel();
|
||||
}
|
||||
}
|
|
@ -15,9 +15,9 @@ use crate::util::io_err;
|
|||
extern crate libc;
|
||||
extern crate winapi;
|
||||
|
||||
use crate::platform::winapi::winapi::shared::{guiddef, minwindef, ntdef, windef};
|
||||
use crate::platform::winapi::winapi::shared::{hidclass, hidpi, hidusage};
|
||||
use crate::platform::winapi::winapi::um::{handleapi, setupapi};
|
||||
use winapi::shared::{guiddef, minwindef, ntdef, windef};
|
||||
use winapi::shared::{hidclass, hidpi, hidusage};
|
||||
use winapi::um::{handleapi, setupapi};
|
||||
|
||||
#[link(name = "setupapi")]
|
||||
extern "system" {
|
||||
|
@ -63,12 +63,6 @@ extern "system" {
|
|||
) -> ntdef::NTSTATUS;
|
||||
}
|
||||
|
||||
macro_rules! offset_of {
|
||||
($ty:ty, $field:ident) => {
|
||||
unsafe { &(*(0 as *const $ty)).$field as *const _ as usize }
|
||||
};
|
||||
}
|
||||
|
||||
fn from_wide_ptr(ptr: *const u16, len: usize) -> String {
|
||||
assert!(!ptr.is_null() && len % 2 == 0);
|
||||
let slice = unsafe { slice::from_raw_parts(ptr, len / 2) };
|
||||
|
@ -214,7 +208,7 @@ impl DeviceInterfaceDetailData {
|
|||
unsafe { (*data).cbSize = cb_size as minwindef::UINT };
|
||||
|
||||
// Compute offset of `SP_DEVICE_INTERFACE_DETAIL_DATA_W.DevicePath`.
|
||||
let offset = offset_of!(setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W, DevicePath);
|
||||
let offset = memoffset::offset_of!(setupapi::SP_DEVICE_INTERFACE_DETAIL_DATA_W, DevicePath);
|
||||
|
||||
Some(Self {
|
||||
data,
|
|
@ -30,11 +30,16 @@ const uint8_t U2F_AUTHENTICATOR_TRANSPORT_NFC = 2;
|
|||
const uint8_t U2F_AUTHENTICATOR_TRANSPORT_BLE = 4;
|
||||
const uint8_t CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL = 8;
|
||||
|
||||
const uint8_t U2F_OK = 0;
|
||||
const uint8_t U2F_ERROR_UKNOWN = 1;
|
||||
const uint8_t U2F_ERROR_NOT_SUPPORTED = 2;
|
||||
const uint8_t U2F_ERROR_INVALID_STATE = 3;
|
||||
const uint8_t U2F_ERROR_CONSTRAINT = 4;
|
||||
const uint8_t U2F_ERROR_NOT_ALLOWED = 5;
|
||||
const uint8_t CTAP_ERROR_PIN_REQUIRED = 6;
|
||||
const uint8_t CTAP_ERROR_PIN_INVALID = 7;
|
||||
const uint8_t CTAP_ERROR_PIN_AUTH_BLOCKED = 8;
|
||||
const uint8_t CTAP_ERROR_PIN_BLOCKED = 9;
|
||||
|
||||
// NOTE: Preconditions
|
||||
// * All rust_u2f_mgr* pointers must refer to pointers which are returned
|
||||
|
@ -45,7 +50,8 @@ const uint8_t U2F_ERROR_NOT_ALLOWED = 5;
|
|||
// register() and sign() callbacks. They can be null on failure.
|
||||
|
||||
// The `rust_u2f_mgr` opaque type is equivalent to the rust type `U2FManager`
|
||||
struct rust_u2f_manager;
|
||||
// TODO(MS): Once CTAP2 support is added, this should probably be renamed.
|
||||
struct rust_ctap_manager;
|
||||
|
||||
// The `rust_u2f_app_ids` opaque type is equivalent to the rust type `U2FAppIds`
|
||||
struct rust_u2f_app_ids;
|
||||
|
@ -62,10 +68,10 @@ typedef void (*rust_u2f_callback)(uint64_t, rust_u2f_result*);
|
|||
|
||||
/// U2FManager functions.
|
||||
|
||||
rust_u2f_manager* rust_u2f_mgr_new();
|
||||
/* unsafe */ void rust_u2f_mgr_free(rust_u2f_manager* mgr);
|
||||
rust_ctap_manager* rust_u2f_mgr_new();
|
||||
/* unsafe */ void rust_u2f_mgr_free(rust_ctap_manager* mgr);
|
||||
|
||||
uint64_t rust_u2f_mgr_register(rust_u2f_manager* mgr, uint64_t flags,
|
||||
uint64_t rust_u2f_mgr_register(rust_ctap_manager* mgr, uint64_t flags,
|
||||
uint64_t timeout, rust_u2f_callback,
|
||||
const uint8_t* challenge_ptr,
|
||||
size_t challenge_len,
|
||||
|
@ -73,13 +79,13 @@ uint64_t rust_u2f_mgr_register(rust_u2f_manager* mgr, uint64_t flags,
|
|||
size_t application_len,
|
||||
const rust_u2f_key_handles* khs);
|
||||
|
||||
uint64_t rust_u2f_mgr_sign(rust_u2f_manager* mgr, uint64_t flags,
|
||||
uint64_t rust_u2f_mgr_sign(rust_ctap_manager* mgr, uint64_t flags,
|
||||
uint64_t timeout, rust_u2f_callback,
|
||||
const uint8_t* challenge_ptr, size_t challenge_len,
|
||||
const rust_u2f_app_ids* app_ids,
|
||||
const rust_u2f_key_handles* khs);
|
||||
|
||||
void rust_u2f_mgr_cancel(rust_u2f_manager* mgr);
|
||||
void rust_u2f_mgr_cancel(rust_ctap_manager* mgr);
|
||||
|
||||
/// U2FAppIds functions.
|
||||
|
||||
|
@ -103,9 +109,11 @@ uint8_t rust_u2f_result_error(const rust_u2f_result* res);
|
|||
// Call this before `[..]_copy()` to allocate enough space.
|
||||
bool rust_u2f_resbuf_length(const rust_u2f_result* res, uint8_t bid,
|
||||
size_t* len);
|
||||
bool rust_u2f_resbuf_contains(const rust_u2f_result* res, uint8_t bid);
|
||||
bool rust_u2f_resbuf_copy(const rust_u2f_result* res, uint8_t bid,
|
||||
uint8_t* dst);
|
||||
/* unsafe */ void rust_u2f_res_free(rust_u2f_result* res);
|
||||
|
||||
}
|
||||
|
||||
#endif // __U2FHID_CAPI
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче