From 14afd71b9e6b34df3425ca05f70705f3256da36c Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 20 Jun 2026 17:04:36 +0100 Subject: [PATCH] refactor(error): per-transport TransportError and generic ceremony Error --- libwebauthn-tests/tests/basic_ctap1.rs | 7 +- libwebauthn-tests/tests/large_blob.rs | 12 +- libwebauthn-tests/tests/preflight.rs | 21 +- libwebauthn-tests/tests/prf.rs | 18 +- libwebauthn/examples/common/mod.rs | 4 +- .../features/webauthn_preflight_hid.rs | 14 +- .../examples/features/webauthn_prf_hid.rs | 19 +- .../examples/management/bio_enrollment_hid.rs | 5 +- .../management/cred_management_hid.rs | 14 +- .../persistent_cred_management_hid.rs | 9 +- libwebauthn/src/fido.rs | 10 +- libwebauthn/src/lib.rs | 2 +- .../src/management/authenticator_config.rs | 80 ++-- libwebauthn/src/management/bio_enrollment.rs | 51 ++- .../src/management/credential_management.rs | 48 ++- libwebauthn/src/ops/u2f.rs | 27 +- libwebauthn/src/ops/webauthn/large_blob.rs | 397 ++++++++++-------- libwebauthn/src/pin/mod.rs | 176 ++++---- libwebauthn/src/pin/persistent_token.rs | 25 +- libwebauthn/src/proto/ctap1/protocol.rs | 67 ++- libwebauthn/src/proto/ctap2/cbor/request.rs | 30 +- libwebauthn/src/proto/ctap2/cose.rs | 77 ++-- libwebauthn/src/proto/ctap2/model.rs | 4 +- .../src/proto/ctap2/model/get_assertion.rs | 37 +- .../src/proto/ctap2/model/make_credential.rs | 117 ++++-- libwebauthn/src/proto/ctap2/preflight.rs | 17 +- libwebauthn/src/proto/ctap2/protocol.rs | 227 ++++++---- libwebauthn/src/transport/ble/channel.rs | 82 ++-- libwebauthn/src/transport/ble/device.rs | 25 +- libwebauthn/src/transport/ble/error.rs | 18 + libwebauthn/src/transport/ble/mod.rs | 2 + .../src/transport/cable/advertisement.rs | 16 +- libwebauthn/src/transport/cable/channel.rs | 47 ++- .../src/transport/cable/connection_stages.rs | 41 +- libwebauthn/src/transport/cable/crypto.rs | 11 +- .../src/transport/cable/data_channel.rs | 20 +- libwebauthn/src/transport/cable/error.rs | 82 +++- .../src/transport/cable/known_devices.rs | 27 +- libwebauthn/src/transport/cable/l2cap.rs | 22 +- libwebauthn/src/transport/cable/protocol.rs | 82 ++-- .../src/transport/cable/qr_code_device.rs | 25 +- libwebauthn/src/transport/cable/tunnel.rs | 69 ++- libwebauthn/src/transport/channel.rs | 25 +- libwebauthn/src/transport/device.rs | 8 +- libwebauthn/src/transport/error.rs | 43 +- libwebauthn/src/transport/hid/channel.rs | 149 +++---- libwebauthn/src/transport/hid/device.rs | 15 +- libwebauthn/src/transport/hid/error.rs | 32 ++ libwebauthn/src/transport/hid/mod.rs | 2 + libwebauthn/src/transport/mock/channel.rs | 30 +- libwebauthn/src/transport/nfc/channel.rs | 97 ++--- libwebauthn/src/transport/nfc/device.rs | 19 +- libwebauthn/src/transport/nfc/error.rs | 28 ++ libwebauthn/src/transport/nfc/libnfc/mod.rs | 82 +--- libwebauthn/src/transport/nfc/mod.rs | 2 + libwebauthn/src/transport/nfc/pcsc/mod.rs | 35 +- libwebauthn/src/u2f.rs | 40 +- libwebauthn/src/webauthn.rs | 126 +++--- libwebauthn/src/webauthn/error.rs | 40 +- libwebauthn/src/webauthn/pin_uv_auth_token.rs | 157 ++++--- 60 files changed, 1698 insertions(+), 1316 deletions(-) create mode 100644 libwebauthn/src/transport/ble/error.rs create mode 100644 libwebauthn/src/transport/hid/error.rs create mode 100644 libwebauthn/src/transport/nfc/error.rs diff --git a/libwebauthn-tests/tests/basic_ctap1.rs b/libwebauthn-tests/tests/basic_ctap1.rs index e2d8ef62..aef493b9 100644 --- a/libwebauthn-tests/tests/basic_ctap1.rs +++ b/libwebauthn-tests/tests/basic_ctap1.rs @@ -3,7 +3,7 @@ use std::time::Duration; use libwebauthn::ops::u2f::{RegisterRequest, SignRequest}; use libwebauthn::transport::{Channel, ChannelSettings, Device}; use libwebauthn::u2f::U2F; -use libwebauthn::webauthn::{CtapError, Error}; +use libwebauthn::webauthn::{CtapError, WebAuthnError}; use libwebauthn::UvUpdate; use libwebauthn_tests::virt::get_virtual_device; use tokio::sync::broadcast::Receiver; @@ -82,7 +82,10 @@ async fn test_webauthn_ctap1_exclude_list() { RegisterRequest::new_u2f_v2(APP_ID, challenge, vec![registered_key], TIMEOUT, false); let result = channel.u2f_register(&excluded_request).await; assert!( - matches!(result, Err(Error::Ctap(CtapError::CredentialExcluded))), + matches!( + result, + Err(WebAuthnError::Ctap(CtapError::CredentialExcluded)) + ), "expected CredentialExcluded, got {:?}", result ); diff --git a/libwebauthn-tests/tests/large_blob.rs b/libwebauthn-tests/tests/large_blob.rs index d4a5dbc3..b1c42f74 100644 --- a/libwebauthn-tests/tests/large_blob.rs +++ b/libwebauthn-tests/tests/large_blob.rs @@ -13,7 +13,7 @@ use libwebauthn::proto::ctap2::{ Ctap2PublicKeyCredentialUserEntity, }; use libwebauthn::transport::{Channel, ChannelSettings, Device}; -use libwebauthn::webauthn::{Error, PlatformError, WebAuthn}; +use libwebauthn::webauthn::{PlatformError, WebAuthn, WebAuthnError}; use libwebauthn::UvUpdate; use libwebauthn_tests::virt::get_virtual_device; use rand::{thread_rng, Rng}; @@ -637,7 +637,10 @@ async fn test_webauthn_large_blob_write_requires_single_allow_credential() { .webauthn_get_assertion(&two) .await .expect_err("write with two allowCredentials must be rejected"); - assert_eq!(err, Error::Platform(PlatformError::NotSupported)); + assert!(matches!( + err, + WebAuthnError::Platform(PlatformError::NotSupported) + )); let mut none = ga_request( &cred_a, @@ -649,7 +652,10 @@ async fn test_webauthn_large_blob_write_requires_single_allow_credential() { .webauthn_get_assertion(&none) .await .expect_err("write with empty allowCredentials must be rejected"); - assert_eq!(err, Error::Platform(PlatformError::NotSupported)); + assert!(matches!( + err, + WebAuthnError::Platform(PlatformError::NotSupported) + )); update_handle.await.unwrap(); } diff --git a/libwebauthn-tests/tests/preflight.rs b/libwebauthn-tests/tests/preflight.rs index 3619df0c..d9d6a362 100644 --- a/libwebauthn-tests/tests/preflight.rs +++ b/libwebauthn-tests/tests/preflight.rs @@ -11,8 +11,9 @@ use libwebauthn::proto::ctap2::{ }; use libwebauthn::proto::CtapError; use libwebauthn::transport::hid::channel::HidChannel; +use libwebauthn::transport::hid::HidError; use libwebauthn::transport::{Channel, ChannelSettings, Device}; -use libwebauthn::webauthn::{Error, WebAuthn}; +use libwebauthn::webauthn::{WebAuthn, WebAuthnError}; use libwebauthn::UvUpdate; use libwebauthn_tests::virt::get_virtual_device; use rand::{thread_rng, Rng}; @@ -36,7 +37,7 @@ async fn make_credential_call( channel: &mut HidChannel<'_>, user_id: &[u8], exclude_list: Option>, -) -> Result<(Ctap2PublicKeyCredentialDescriptor, [u8; 32]), Error> { +) -> Result<(Ctap2PublicKeyCredentialDescriptor, [u8; 32]), WebAuthnError> { make_credential_call_with_rp(channel, user_id, exclude_list, "example.org").await } @@ -45,7 +46,7 @@ async fn make_credential_call_with_rp( user_id: &[u8], exclude_list: Option>, rp_id: &str, -) -> Result<(Ctap2PublicKeyCredentialDescriptor, [u8; 32]), Error> { +) -> Result<(Ctap2PublicKeyCredentialDescriptor, [u8; 32]), WebAuthnError> { let challenge: [u8; 32] = thread_rng().gen(); let make_credentials_request = MakeCredentialRequest { origin: rp_id.to_owned(), @@ -71,7 +72,7 @@ async fn make_credential_call_with_rp( async fn get_assertion_call( channel: &mut HidChannel<'_>, allow_list: Vec, -) -> Result { +) -> Result> { let challenge: [u8; 32] = thread_rng().gen(); let get_assertion = GetAssertionRequest { origin: "example.org".to_owned(), @@ -182,7 +183,7 @@ async fn preflight_mixed_exclude_list() { let res = make_credential_call(&mut channel, &user_id, Some(exclude_list)).await; assert!(matches!( res, - Err(Error::Ctap(CtapError::CredentialExcluded)) + Err(WebAuthnError::Ctap(CtapError::CredentialExcluded)) )); expected_uv_updates( @@ -216,7 +217,10 @@ async fn preflight_no_allow_list() { let allow_list = Vec::new(); let res = get_assertion_call(&mut channel, allow_list).await; - assert!(matches!(res, Err(Error::Ctap(CtapError::NoCredentials)))); + assert!(matches!( + res, + Err(WebAuthnError::Ctap(CtapError::NoCredentials)) + )); expected_uv_updates( state_recv, @@ -250,7 +254,10 @@ async fn preflight_nonsense_allow_list() { assert!(filtered_list.is_empty()); let res = get_assertion_call(&mut channel, allow_list).await; - assert!(matches!(res, Err(Error::Ctap(CtapError::NoCredentials)))); + assert!(matches!( + res, + Err(WebAuthnError::Ctap(CtapError::NoCredentials)) + )); expected_uv_updates( state_recv, diff --git a/libwebauthn-tests/tests/prf.rs b/libwebauthn-tests/tests/prf.rs index 4d53c55d..35d5b7ad 100644 --- a/libwebauthn-tests/tests/prf.rs +++ b/libwebauthn-tests/tests/prf.rs @@ -8,8 +8,9 @@ use libwebauthn::ops::webauthn::{ use libwebauthn::pin::PinManagement; use libwebauthn::proto::ctap2::{Ctap2PinUvAuthProtocol, Ctap2PublicKeyCredentialDescriptor}; use libwebauthn::transport::hid::channel::HidChannel; +use libwebauthn::transport::hid::HidError; use libwebauthn::transport::{Channel, ChannelSettings, Ctap2AuthTokenStore, Device}; -use libwebauthn::webauthn::{Error as WebAuthnError, PlatformError, WebAuthn}; +use libwebauthn::webauthn::{PlatformError, WebAuthn, WebAuthnError}; use libwebauthn::UvUpdate; use libwebauthn::{ ops::webauthn::{MakeCredentialRequest, ResidentKeyRequirement, UserVerificationRequirement}, @@ -490,7 +491,7 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) { &challenge, prf, "Wrongly encoded credential_id", - WebAuthnError::Platform(PlatformError::SyntaxError), + PlatformError::SyntaxError, ) .await; @@ -514,7 +515,7 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) { &challenge, prf, "Empty credential_id", - WebAuthnError::Platform(PlatformError::SyntaxError), + PlatformError::SyntaxError, ) .await; @@ -538,7 +539,7 @@ async fn run_test_battery(channel: &mut HidChannel<'_>, using_pin: bool) { &challenge, prf, "Empty allow_list, set eval_by_credential", - WebAuthnError::Platform(PlatformError::NotSupported), + PlatformError::NotSupported, ) .await; @@ -621,7 +622,7 @@ async fn run_failed_test( challenge: &[u8; 32], prf: PrfInput, printoutput: &str, - expected_error: WebAuthnError, + expected_error: PlatformError, ) { let get_assertion = GetAssertionRequest { relying_party_id: "example.org".to_owned(), @@ -637,7 +638,7 @@ async fn run_failed_test( top_origin: None, }; - let response: Result<(), WebAuthnError> = loop { + let response: Result<(), WebAuthnError> = loop { match channel.webauthn_get_assertion(&get_assertion).await { Ok(_) => panic!("Success, even though it should have errored out!"), Err(WebAuthnError::Ctap(ctap_error)) => { @@ -651,7 +652,10 @@ async fn run_failed_test( }; }; - assert_eq!(response, Err(expected_error), "{printoutput}:"); + match response { + Err(WebAuthnError::Platform(got)) => assert_eq!(got, expected_error, "{printoutput}:"), + other => panic!("{printoutput}: expected {expected_error:?}, got {other:?}"), + } println!("Success for test: {printoutput}") } diff --git a/libwebauthn/examples/common/mod.rs b/libwebauthn/examples/common/mod.rs index bece3d7e..6cfa33af 100644 --- a/libwebauthn/examples/common/mod.rs +++ b/libwebauthn/examples/common/mod.rs @@ -123,12 +123,12 @@ macro_rules! retry_user_errors { loop { match $call.await { Ok(response) => break Ok(response), - Err(libwebauthn::webauthn::Error::Ctap(ctap_error)) => { + Err(libwebauthn::webauthn::WebAuthnError::Ctap(ctap_error)) => { if ctap_error.is_retryable_user_error() { println!("Oops, try again! Error: {}", ctap_error); continue; } - break Err(libwebauthn::webauthn::Error::Ctap(ctap_error)); + break Err(libwebauthn::webauthn::WebAuthnError::Ctap(ctap_error)); } Err(err) => break Err(err), } diff --git a/libwebauthn/examples/features/webauthn_preflight_hid.rs b/libwebauthn/examples/features/webauthn_preflight_hid.rs index 7cfd886a..253dccdc 100644 --- a/libwebauthn/examples/features/webauthn_preflight_hid.rs +++ b/libwebauthn/examples/features/webauthn_preflight_hid.rs @@ -14,7 +14,7 @@ use libwebauthn::proto::ctap2::{ }; use libwebauthn::transport::hid::list_devices; use libwebauthn::transport::{Channel, ChannelSettings, Device}; -use libwebauthn::webauthn::{CtapError, Error as WebAuthnError, WebAuthn}; +use libwebauthn::webauthn::{CtapError, WebAuthn, WebAuthnError}; #[path = "../common/mod.rs"] mod common; @@ -103,11 +103,11 @@ pub async fn main() -> Result<(), Box> { Ok(()) } -async fn make_credential_call( - channel: &mut impl Channel, +async fn make_credential_call( + channel: &mut C, user_id: &[u8], exclude_list: Option>, -) -> Result { +) -> Result> { let challenge: [u8; 32] = thread_rng().gen(); let make_credentials_request = MakeCredentialRequest { challenge: Vec::from(challenge), @@ -128,10 +128,10 @@ async fn make_credential_call( .map(|x| (&x.authenticator_data).try_into().unwrap()) } -async fn get_assertion_call( - channel: &mut impl Channel, +async fn get_assertion_call( + channel: &mut C, allow_list: Vec, -) -> Result { +) -> Result> { let challenge: [u8; 32] = thread_rng().gen(); let get_assertion = GetAssertionRequest { relying_party_id: "example.org".to_owned(), diff --git a/libwebauthn/examples/features/webauthn_prf_hid.rs b/libwebauthn/examples/features/webauthn_prf_hid.rs index 2e8db5e0..e746a50b 100644 --- a/libwebauthn/examples/features/webauthn_prf_hid.rs +++ b/libwebauthn/examples/features/webauthn_prf_hid.rs @@ -17,7 +17,7 @@ use libwebauthn::proto::ctap2::{ }; use libwebauthn::transport::hid::list_devices; use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; -use libwebauthn::webauthn::{Error as WebAuthnError, PlatformError, WebAuthn}; +use libwebauthn::webauthn::{PlatformError, WebAuthn, WebAuthnError}; #[path = "../common/mod.rs"] mod common; @@ -274,7 +274,7 @@ pub async fn main() -> Result<(), Box> { eval_by_credential, }, "Wrongly encoded credential_id", - WebAuthnError::Platform(PlatformError::SyntaxError), + PlatformError::SyntaxError, ) .await; @@ -296,7 +296,7 @@ pub async fn main() -> Result<(), Box> { eval_by_credential, }, "Empty credential_id", - WebAuthnError::Platform(PlatformError::SyntaxError), + PlatformError::SyntaxError, ) .await; @@ -318,7 +318,7 @@ pub async fn main() -> Result<(), Box> { eval_by_credential, }, "Empty allow_list, set eval_by_credential", - WebAuthnError::Platform(PlatformError::NotSupported), + PlatformError::NotSupported, ) .await; } @@ -361,7 +361,7 @@ async fn run_failed_test( challenge: &[u8; 32], prf: PrfInput, printoutput: &str, - expected_error: WebAuthnError, + expected_error: PlatformError, ) { let get_assertion = GetAssertionRequest { relying_party_id: "example.org".to_owned(), @@ -377,9 +377,12 @@ async fn run_failed_test( timeout: TIMEOUT, }; - let response = retry_user_errors!(channel.webauthn_get_assertion(&get_assertion)) - .map(|_| panic!("Success, even though it should have errored out!")); + let response = retry_user_errors!(channel.webauthn_get_assertion(&get_assertion)); - assert_eq!(response, Err(expected_error), "{printoutput}:"); + match response { + Ok(_) => panic!("Success, even though it should have errored out!"), + Err(WebAuthnError::Platform(got)) => assert_eq!(got, expected_error, "{printoutput}:"), + Err(other) => panic!("{printoutput}: expected {expected_error:?}, got {other:?}"), + } println!("Success for test: {printoutput}") } diff --git a/libwebauthn/examples/management/bio_enrollment_hid.rs b/libwebauthn/examples/management/bio_enrollment_hid.rs index ebfbc765..0e582059 100644 --- a/libwebauthn/examples/management/bio_enrollment_hid.rs +++ b/libwebauthn/examples/management/bio_enrollment_hid.rs @@ -7,8 +7,9 @@ use text_io::read; use libwebauthn::management::BioEnrollment; use libwebauthn::proto::ctap2::{Ctap2, Ctap2GetInfoResponse, Ctap2LastEnrollmentSampleStatus}; use libwebauthn::transport::hid::list_devices; +use libwebauthn::transport::hid::HidError; use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; -use libwebauthn::webauthn::Error as WebAuthnError; +use libwebauthn::webauthn::WebAuthnError; #[path = "../common/mod.rs"] mod common; @@ -101,7 +102,7 @@ pub async fn main() -> Result<(), Box> { } let idx = common::prompt_index(options.len()); - let result: Result = match options[idx] { + let result: Result> = match options[idx] { Operation::GetModality => { retry_user_errors!(channel.get_bio_modality(TIMEOUT)).map(|x| format!("{x:?}")) } diff --git a/libwebauthn/examples/management/cred_management_hid.rs b/libwebauthn/examples/management/cred_management_hid.rs index 30f56761..e16ef896 100644 --- a/libwebauthn/examples/management/cred_management_hid.rs +++ b/libwebauthn/examples/management/cred_management_hid.rs @@ -7,8 +7,9 @@ use libwebauthn::proto::ctap2::{ }; use libwebauthn::proto::CtapError; use libwebauthn::transport::hid::list_devices; +use libwebauthn::transport::hid::HidError; use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; -use libwebauthn::webauthn::Error as WebAuthnError; +use libwebauthn::webauthn::WebAuthnError; use std::io::{self, Write}; use text_io::read; @@ -31,7 +32,7 @@ fn format_credential(cred: &Ctap2CredentialData) -> String { async fn enumerate_rps( channel: &mut T, -) -> Result, WebAuthnError> { +) -> Result, WebAuthnError> { let (rp, total_rps) = retry_user_errors!(channel.enumerate_rps_begin(TIMEOUT))?; let mut rps = vec![rp]; for _ in 1..total_rps { @@ -44,7 +45,7 @@ async fn enumerate_rps( async fn enumerate_credentials_for_rp( channel: &mut T, rp_id_hash: &[u8], -) -> Result, WebAuthnError> { +) -> Result, WebAuthnError> { let (credential, num_of_creds) = retry_user_errors!(channel.enumerate_credentials_begin(rp_id_hash, TIMEOUT))?; let mut credentials = vec![credential]; @@ -77,7 +78,7 @@ impl Display for Operation { } #[tokio::main] -pub async fn main() -> Result<(), WebAuthnError> { +pub async fn main() -> Result<(), WebAuthnError> { common::setup_logging(); let devices = list_devices().await.unwrap(); @@ -86,7 +87,10 @@ pub async fn main() -> Result<(), WebAuthnError> { for mut device in devices { println!("Selected HID authenticator: {}", &device); let mut channel = device.channel(ChannelSettings::default()).await?; - channel.wink(TIMEOUT).await?; + channel + .wink(TIMEOUT) + .await + .map_err(WebAuthnError::Transport)?; let state_recv = channel.get_ux_update_receiver(); tokio::spawn(common::handle_uv_updates(state_recv)); diff --git a/libwebauthn/examples/management/persistent_cred_management_hid.rs b/libwebauthn/examples/management/persistent_cred_management_hid.rs index d7b27e91..0e4e522f 100644 --- a/libwebauthn/examples/management/persistent_cred_management_hid.rs +++ b/libwebauthn/examples/management/persistent_cred_management_hid.rs @@ -16,8 +16,9 @@ use libwebauthn::management::CredentialManagement; use libwebauthn::pin::persistent_token::{MemoryPersistentTokenStore, PersistentTokenStore}; use libwebauthn::proto::ctap2::Ctap2; use libwebauthn::transport::hid::list_devices; +use libwebauthn::transport::hid::HidError; use libwebauthn::transport::{Channel as _, ChannelSettings, Device}; -use libwebauthn::webauthn::Error as WebAuthnError; +use libwebauthn::webauthn::WebAuthnError; #[path = "../common/mod.rs"] mod common; @@ -25,7 +26,7 @@ mod common; const TIMEOUT: Duration = Duration::from_secs(10); #[tokio::main] -pub async fn main() -> Result<(), WebAuthnError> { +pub async fn main() -> Result<(), WebAuthnError> { common::setup_logging(); // In production, use a securely stored implementation. See the module docs. @@ -75,7 +76,9 @@ pub async fn main() -> Result<(), WebAuthnError> { Ok(()) } -async fn print_metadata(channel: &mut impl CredentialManagement) -> Result<(), WebAuthnError> { +async fn print_metadata( + channel: &mut T, +) -> Result<(), WebAuthnError> { let metadata = channel.get_credential_metadata(TIMEOUT).await?; println!( "Discoverable credentials: {} (max remaining: {})", diff --git a/libwebauthn/src/fido.rs b/libwebauthn/src/fido.rs index f12ae8b6..d5f5c575 100644 --- a/libwebauthn/src/fido.rs +++ b/libwebauthn/src/fido.rs @@ -31,7 +31,7 @@ use crate::{ ctap2::{Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType}, CtapError, }, - webauthn::{Error, PlatformError}, + webauthn::PlatformError, }; #[derive(Debug, PartialEq, Eq)] @@ -124,7 +124,7 @@ impl AuthenticatorData where T: Clone + Serialize, { - pub fn to_response_bytes(&self) -> Result, Error> { + pub fn to_response_bytes(&self) -> Result, PlatformError> { // Return the device's authData verbatim. Re-encoding from the parsed // fields would reorder or drop unmodeled extensions, invalidating the // authenticator's signature over these exact bytes. @@ -143,7 +143,7 @@ where res.write_u32::(self.signature_count) .map_err(|e| { error!("Failed to create AuthenticatorData output vec at signature_count: {e:?}"); - Error::Platform(PlatformError::InvalidDeviceResponse) + PlatformError::InvalidDeviceResponse })?; if let Some(att_data) = &self.attested_credential { @@ -159,7 +159,7 @@ where error!( "Failed to create AuthenticatorData output vec at attested_credential.credential_id: {e:?}" ); - Error::Platform(PlatformError::InvalidDeviceResponse) + PlatformError::InvalidDeviceResponse })?; res.extend(&att_data.credential_id); res.extend(&att_data.credential_public_key); @@ -169,7 +169,7 @@ where { res.extend(cbor::to_vec(&self.extensions).map_err(|e| { error!(%e, "Failed to create AuthenticatorData output vec at extensions"); - Error::Platform(PlatformError::InvalidDeviceResponse) + PlatformError::InvalidDeviceResponse })?); } Ok(res) diff --git a/libwebauthn/src/lib.rs b/libwebauthn/src/lib.rs index c5f8af13..f6ac9bad 100644 --- a/libwebauthn/src/lib.rs +++ b/libwebauthn/src/lib.rs @@ -129,7 +129,7 @@ macro_rules! unwrap_field { "Device response did not contain expected field: {}", stringify!($field) ); - return Err(Error::Platform(PlatformError::InvalidDeviceResponse)); + return Err($crate::webauthn::error::PlatformError::InvalidDeviceResponse.into()); } }}; } diff --git a/libwebauthn/src/management/authenticator_config.rs b/libwebauthn/src/management/authenticator_config.rs index 412bce89..cc920c24 100644 --- a/libwebauthn/src/management/authenticator_config.rs +++ b/libwebauthn/src/management/authenticator_config.rs @@ -1,7 +1,7 @@ use crate::proto::ctap2::cbor; use crate::proto::ctap2::Ctap2ClientPinRequest; use crate::transport::Channel; -pub use crate::webauthn::error::{CtapError, Error, PlatformError}; +pub use crate::webauthn::error::{CtapError, PlatformError, WebAuthnError}; use crate::webauthn::handle_errors; use crate::webauthn::pin_uv_auth_token::{user_verification, UsedPinUvAuthToken}; use crate::{ @@ -19,24 +19,34 @@ use std::time::Duration; use tracing::info; #[async_trait] -pub trait AuthenticatorConfig { - async fn toggle_always_uv(&mut self, timeout: Duration) -> Result<(), Error>; +pub trait AuthenticatorConfig: Channel { + async fn toggle_always_uv( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError>; - async fn enable_enterprise_attestation(&mut self, timeout: Duration) -> Result<(), Error>; + async fn enable_enterprise_attestation( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError>; async fn set_min_pin_length( &mut self, new_pin_length: u64, timeout: Duration, - ) -> Result<(), Error>; + ) -> Result<(), WebAuthnError>; - async fn force_change_pin(&mut self, force: bool, timeout: Duration) -> Result<(), Error>; + async fn force_change_pin( + &mut self, + force: bool, + timeout: Duration, + ) -> Result<(), WebAuthnError>; async fn set_min_pin_length_rpids( &mut self, rpids: Vec, timeout: Duration, - ) -> Result<(), Error>; + ) -> Result<(), WebAuthnError>; } #[async_trait] @@ -44,11 +54,14 @@ impl AuthenticatorConfig for C where C: Channel, { - async fn toggle_always_uv(&mut self, timeout: Duration) -> Result<(), Error> { + async fn toggle_always_uv( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError> { let info = self.ctap2_get_info().await?; // CTAP 2.1 6.2.5: toggleAlwaysUv is gated on the alwaysUv option only. if !info.option_enabled("authnrCfg") || !info.option_exists("alwaysUv") { - return Err(Error::Platform(PlatformError::NotSupported)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } let mut req = Ctap2AuthenticatorConfigRequest::new_toggle_always_uv(); @@ -71,10 +84,13 @@ where } } - async fn enable_enterprise_attestation(&mut self, timeout: Duration) -> Result<(), Error> { + async fn enable_enterprise_attestation( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError> { let info = self.ctap2_get_info().await?; if !info.option_enabled("authnrCfg") || !info.option_exists("ep") { - return Err(Error::Platform(PlatformError::NotSupported)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } let mut req = Ctap2AuthenticatorConfigRequest::new_enable_enterprise_attestation(); @@ -101,10 +117,10 @@ where &mut self, new_pin_length: u64, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), WebAuthnError> { let info = self.ctap2_get_info().await?; if !info.option_enabled("authnrCfg") || !info.option_exists("setMinPINLength") { - return Err(Error::Platform(PlatformError::NotSupported)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } let mut req = Ctap2AuthenticatorConfigRequest::new_set_min_pin_length(new_pin_length); @@ -127,10 +143,14 @@ where } } - async fn force_change_pin(&mut self, force: bool, timeout: Duration) -> Result<(), Error> { + async fn force_change_pin( + &mut self, + force: bool, + timeout: Duration, + ) -> Result<(), WebAuthnError> { let info = self.ctap2_get_info().await?; if !info.option_enabled("authnrCfg") || !info.option_exists("setMinPINLength") { - return Err(Error::Platform(PlatformError::NotSupported)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } let mut req = Ctap2AuthenticatorConfigRequest::new_force_change_pin(force); @@ -157,17 +177,17 @@ where &mut self, rpids: Vec, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), WebAuthnError> { let info = self.ctap2_get_info().await?; if !info.option_enabled("authnrCfg") || !info.option_exists("setMinPINLength") || !info.supports_extension("minPinLength") { - return Err(Error::Platform(PlatformError::NotSupported)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } let max_rpids = u64::from(info.max_rpids_for_setminpinlength.unwrap_or(u32::MAX)); if rpids.len() as u64 > max_rpids { - return Err(Error::Platform(PlatformError::RequestTooLarge)); + return Err(WebAuthnError::Platform(PlatformError::RequestTooLarge)); } let mut req = Ctap2AuthenticatorConfigRequest::new_set_min_pin_length_rpids(rpids); @@ -199,7 +219,7 @@ impl Ctap2UserVerifiableRequest for Ctap2AuthenticatorConfigRequest { &mut self, uv_proto: &dyn PinUvAuthProtocol, uv_auth_token: &[u8], - ) -> Result<(), Error> { + ) -> Result<(), PlatformError> { // pinUvAuthParam (0x04): the result of calling // authenticate(pinUvAuthToken, 32×0xff || 0x0d || uint8(subCommand) || subCommandParams). let mut data = vec![0xff; 32]; @@ -240,7 +260,7 @@ mod test { use std::collections::HashMap; use std::time::Duration; - use super::{AuthenticatorConfig, Error, PlatformError}; + use super::{AuthenticatorConfig, PlatformError, WebAuthnError}; use crate::proto::ctap2::cbor::{self, CborRequest, CborResponse}; use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse}; use crate::transport::mock::channel::MockChannel; @@ -265,7 +285,10 @@ mod test { ); let result = channel.toggle_always_uv(TIMEOUT).await; - assert_eq!(result, Err(Error::Platform(PlatformError::NotSupported))); + assert!(matches!( + result, + Err(WebAuthnError::Platform(PlatformError::NotSupported)) + )); } #[tokio::test] @@ -280,7 +303,10 @@ mod test { ); let result = channel.toggle_always_uv(TIMEOUT).await; - assert_eq!(result, Err(Error::Platform(PlatformError::NotSupported))); + assert!(matches!( + result, + Err(WebAuthnError::Platform(PlatformError::NotSupported)) + )); } #[tokio::test] @@ -300,7 +326,10 @@ mod test { let result = channel .set_min_pin_length_rpids(vec!["example.com".to_string()], TIMEOUT) .await; - assert_eq!(result, Err(Error::Platform(PlatformError::NotSupported))); + assert!(matches!( + result, + Err(WebAuthnError::Platform(PlatformError::NotSupported)) + )); } #[tokio::test] @@ -321,6 +350,9 @@ mod test { let rpids = vec!["example.com".to_string(), "example.org".to_string()]; let result = channel.set_min_pin_length_rpids(rpids, TIMEOUT).await; - assert_eq!(result, Err(Error::Platform(PlatformError::RequestTooLarge))); + assert!(matches!( + result, + Err(WebAuthnError::Platform(PlatformError::RequestTooLarge)) + )); } } diff --git a/libwebauthn/src/management/bio_enrollment.rs b/libwebauthn/src/management/bio_enrollment.rs index 8df5866f..e2c66cd8 100644 --- a/libwebauthn/src/management/bio_enrollment.rs +++ b/libwebauthn/src/management/bio_enrollment.rs @@ -11,7 +11,7 @@ use crate::{ transport::Channel, unwrap_field, webauthn::{ - error::{CtapError, Error, PlatformError}, + error::{CtapError, PlatformError, WebAuthnError}, handle_errors, pin_uv_auth_token::{user_verification, UsedPinUvAuthToken}, }, @@ -23,42 +23,45 @@ use std::time::Duration; use tracing::{info, warn}; #[async_trait] -pub trait BioEnrollment { +pub trait BioEnrollment: Channel { async fn get_bio_modality( &mut self, timeout: Duration, - ) -> Result; + ) -> Result>; async fn get_fingerprint_sensor_info( &mut self, timeout: Duration, - ) -> Result; + ) -> Result>; async fn get_bio_enrollments( &mut self, timeout: Duration, - ) -> Result, Error>; + ) -> Result, WebAuthnError>; async fn remove_bio_enrollment( &mut self, template_id: &[u8], timeout: Duration, - ) -> Result<(), Error>; + ) -> Result<(), WebAuthnError>; async fn rename_bio_enrollment( &mut self, template_id: &[u8], template_friendly_name: &str, timeout: Duration, - ) -> Result<(), Error>; + ) -> Result<(), WebAuthnError>; async fn start_new_bio_enrollment( &mut self, enrollment_timeout: Option, timeout: Duration, - ) -> Result<(Vec, Ctap2LastEnrollmentSampleStatus, u64), Error>; + ) -> Result<(Vec, Ctap2LastEnrollmentSampleStatus, u64), WebAuthnError>; async fn capture_next_bio_enrollment_sample( &mut self, template_id: &[u8], enrollment_timeout: Option, timeout: Duration, - ) -> Result<(Ctap2LastEnrollmentSampleStatus, u64), Error>; - async fn cancel_current_bio_enrollment(&mut self, timeout: Duration) -> Result<(), Error>; + ) -> Result<(Ctap2LastEnrollmentSampleStatus, u64), WebAuthnError>; + async fn cancel_current_bio_enrollment( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError>; } #[derive(Debug, Clone)] @@ -77,7 +80,7 @@ where async fn get_bio_modality( &mut self, timeout: Duration, - ) -> Result { + ) -> Result> { let req = Ctap2BioEnrollmentRequest::new_get_modality(); // No UV needed let resp = self.ctap2_bio_enrollment(&req, timeout).await?; @@ -85,7 +88,7 @@ where Some(modality) => Ok(modality), None => { warn!("Channel did not return modality."); - Err(Error::Ctap(CtapError::Other)) + Err(WebAuthnError::Ctap(CtapError::Other)) } } } @@ -93,13 +96,13 @@ where async fn get_fingerprint_sensor_info( &mut self, timeout: Duration, - ) -> Result { + ) -> Result> { let req = Ctap2BioEnrollmentRequest::new_fingerprint_sensor_info(); // No UV needed let resp = self.ctap2_bio_enrollment(&req, timeout).await?; let Some(fingerprint_kind) = resp.fingerprint_kind else { warn!("Channel did not return fingerprint_kind in sensor info."); - return Err(Error::Ctap(CtapError::Other)); + return Err(WebAuthnError::Ctap(CtapError::Other)); }; Ok(Ctap2BioEnrollmentFingerprintSensorInfo { fingerprint_kind, @@ -111,7 +114,7 @@ where async fn get_bio_enrollments( &mut self, timeout: Duration, - ) -> Result, Error> { + ) -> Result, WebAuthnError> { let mut req = Ctap2BioEnrollmentRequest::new_enumerate_enrollments(); let resp = loop { @@ -138,7 +141,7 @@ where &mut self, template_id: &[u8], timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), WebAuthnError> { let mut req = Ctap2BioEnrollmentRequest::new_remove_enrollment(template_id); loop { @@ -169,7 +172,7 @@ where template_id: &[u8], template_friendly_name: &str, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), WebAuthnError> { let mut req = Ctap2BioEnrollmentRequest::new_rename_enrollment(template_id, template_friendly_name); loop { @@ -198,7 +201,8 @@ where &mut self, enrollment_timeout: Option, timeout: Duration, - ) -> Result<(Vec, Ctap2LastEnrollmentSampleStatus, u64), Error> { + ) -> Result<(Vec, Ctap2LastEnrollmentSampleStatus, u64), WebAuthnError> + { let mut req = Ctap2BioEnrollmentRequest::new_start_new_enrollment(enrollment_timeout); let resp = loop { @@ -230,7 +234,7 @@ where template_id: &[u8], enrollment_timeout: Option, timeout: Duration, - ) -> Result<(Ctap2LastEnrollmentSampleStatus, u64), Error> { + ) -> Result<(Ctap2LastEnrollmentSampleStatus, u64), WebAuthnError> { let mut req = Ctap2BioEnrollmentRequest::new_next_enrollment(template_id, enrollment_timeout); @@ -257,7 +261,10 @@ where Ok((sample_status, remaining_samples)) } - async fn cancel_current_bio_enrollment(&mut self, timeout: Duration) -> Result<(), Error> { + async fn cancel_current_bio_enrollment( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError> { let mut req = Ctap2BioEnrollmentRequest::new_cancel_current_enrollment(); loop { @@ -293,11 +300,11 @@ impl Ctap2UserVerifiableRequest for Ctap2BioEnrollmentRequest { &mut self, uv_proto: &dyn PinUvAuthProtocol, uv_auth_token: &[u8], - ) -> Result<(), Error> { + ) -> Result<(), PlatformError> { // pinUvAuthParam (0x05): authenticate(pinUvAuthToken, fingerprint (0x01) || enumerateEnrollments (0x04)). let subcommand = self .subcommand - .ok_or(Error::Platform(PlatformError::InvalidDeviceResponse))?; + .ok_or(PlatformError::InvalidDeviceResponse)?; let mut data = vec![ Ctap2BioEnrollmentModality::Fingerprint as u8, subcommand as u8, diff --git a/libwebauthn/src/management/credential_management.rs b/libwebauthn/src/management/credential_management.rs index 5fab620a..8274cf09 100644 --- a/libwebauthn/src/management/credential_management.rs +++ b/libwebauthn/src/management/credential_management.rs @@ -11,7 +11,7 @@ use crate::{ transport::Channel, unwrap_field, webauthn::{ - error::{CtapError, Error, PlatformError}, + error::{CtapError, PlatformError, WebAuthnError}, handle_errors, pin_uv_auth_token::{user_verification, UsedPinUvAuthToken}, }, @@ -23,34 +23,39 @@ use std::time::Duration; use tracing::info; #[async_trait] -pub trait CredentialManagement { +pub trait CredentialManagement: Channel { async fn get_credential_metadata( &mut self, timeout: Duration, - ) -> Result; - async fn enumerate_rps_begin(&mut self, timeout: Duration) - -> Result<(Ctap2RPData, u64), Error>; - async fn enumerate_rps_next_rp(&mut self, timeout: Duration) -> Result; + ) -> Result>; + async fn enumerate_rps_begin( + &mut self, + timeout: Duration, + ) -> Result<(Ctap2RPData, u64), WebAuthnError>; + async fn enumerate_rps_next_rp( + &mut self, + timeout: Duration, + ) -> Result>; async fn enumerate_credentials_begin( &mut self, rpid_hash: &[u8], timeout: Duration, - ) -> Result<(Ctap2CredentialData, u64), Error>; + ) -> Result<(Ctap2CredentialData, u64), WebAuthnError>; async fn enumerate_credentials_next( &mut self, timeout: Duration, - ) -> Result; + ) -> Result>; async fn delete_credential( &mut self, credential_id: &Ctap2PublicKeyCredentialDescriptor, timeout: Duration, - ) -> Result<(), Error>; + ) -> Result<(), WebAuthnError>; async fn update_user_info( &mut self, credential_id: &Ctap2PublicKeyCredentialDescriptor, user: &Ctap2PublicKeyCredentialUserEntity, timeout: Duration, - ) -> Result<(), Error>; + ) -> Result<(), WebAuthnError>; } #[async_trait] @@ -61,7 +66,7 @@ where async fn get_credential_metadata( &mut self, timeout: Duration, - ) -> Result { + ) -> Result> { let mut req = Ctap2CredentialManagementRequest::new_get_credential_metadata(); let resp = loop { let uv_auth_used = user_verification( @@ -91,7 +96,7 @@ where async fn enumerate_rps_begin( &mut self, timeout: Duration, - ) -> Result<(Ctap2RPData, u64), Error> { + ) -> Result<(Ctap2RPData, u64), WebAuthnError> { let mut req = Ctap2CredentialManagementRequest::new_enumerate_rps_begin(); let resp = loop { let uv_auth_used = user_verification( @@ -121,7 +126,10 @@ where )) } - async fn enumerate_rps_next_rp(&mut self, timeout: Duration) -> Result { + async fn enumerate_rps_next_rp( + &mut self, + timeout: Duration, + ) -> Result> { let mut req = Ctap2CredentialManagementRequest::new_enumerate_rps_next_rp(); req.use_legacy_preview = self.cred_mgmt_preview(); let resp = self.ctap2_credential_management(&req, timeout).await?; @@ -135,7 +143,7 @@ where &mut self, rpid_hash: &[u8], timeout: Duration, - ) -> Result<(Ctap2CredentialData, u64), Error> { + ) -> Result<(Ctap2CredentialData, u64), WebAuthnError> { let mut req = Ctap2CredentialManagementRequest::new_enumerate_credentials_begin(rpid_hash); let resp = loop { let uv_auth_used = user_verification( @@ -170,7 +178,7 @@ where async fn enumerate_credentials_next( &mut self, timeout: Duration, - ) -> Result { + ) -> Result> { let mut req = Ctap2CredentialManagementRequest::new_enumerate_credentials_next(); req.use_legacy_preview = self.cred_mgmt_preview(); let resp = self.ctap2_credential_management(&req, timeout).await?; @@ -188,7 +196,7 @@ where &mut self, credential_id: &Ctap2PublicKeyCredentialDescriptor, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), WebAuthnError> { let mut req = Ctap2CredentialManagementRequest::new_delete_credential(credential_id); loop { let uv_auth_used = user_verification( @@ -216,7 +224,7 @@ where credential_id: &Ctap2PublicKeyCredentialDescriptor, user: &Ctap2PublicKeyCredentialUserEntity, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), WebAuthnError> { let mut req = Ctap2CredentialManagementRequest::new_update_user_information(credential_id, user); loop { @@ -230,7 +238,7 @@ where // Preview mode does not support "updateUserInfo" subcommand if req.use_legacy_preview { - return Err(Error::Ctap(CtapError::InvalidCommand)); + return Err(WebAuthnError::Ctap(CtapError::InvalidCommand)); } // On success, this is an all-empty Ctap2AuthenticatorConfigResponse @@ -255,10 +263,10 @@ impl Ctap2UserVerifiableRequest for Ctap2CredentialManagementRequest { &mut self, uv_proto: &dyn PinUvAuthProtocol, uv_auth_token: &[u8], - ) -> Result<(), Error> { + ) -> Result<(), PlatformError> { let subcommand = self .subcommand - .ok_or(Error::Platform(PlatformError::InvalidDeviceResponse))?; + .ok_or(PlatformError::InvalidDeviceResponse)?; let mut data = vec![subcommand as u8]; // e.g. pinUvAuthParam (0x04): authenticate(pinUvAuthToken, enumerateCredentialsBegin (0x04) || subCommandParams). diff --git a/libwebauthn/src/ops/u2f.rs b/libwebauthn/src/ops/u2f.rs index b4d2e552..144c3bf2 100644 --- a/libwebauthn/src/ops/u2f.rs +++ b/libwebauthn/src/ops/u2f.rs @@ -18,7 +18,7 @@ use crate::proto::ctap2::{ Ctap2AttestationStatement, Ctap2GetAssertionResponse, Ctap2MakeCredentialResponse, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType, FidoU2fAttestationStmt, }; -use crate::webauthn::{CtapError, Error, PlatformError}; +use crate::webauthn::{CtapError, PlatformError, WebAuthnError}; // FIDO U2F operations can be aliased to CTAP1 requests, as they have no other representation. pub type RegisterRequest = Ctap1RegisterRequest; @@ -44,43 +44,43 @@ impl SignRequest { } pub trait UpgradableResponse { - fn try_upgrade(&self, request: &R) -> Result; + fn try_upgrade(&self, request: &R) -> Result>; } impl UpgradableResponse for RegisterResponse { - fn try_upgrade( + fn try_upgrade( &self, request: &MakeCredentialRequest, - ) -> Result { + ) -> Result> { // Let x9encodedUserPublicKeybe the user public key returned in the U2F registration response message [U2FRawMsgs]. // Let coseEncodedCredentialPublicKey be the result of converting x9encodedUserPublicKey’s value // from ANS X9.62 / Sec-1 v2 uncompressed curve point representation [SEC1V2] // to COSE_Key representation ([RFC8152] Section 7). let Ok(encoded_point) = p256::EncodedPoint::from_bytes(&self.public_key) else { error!(?self.public_key, "Failed to parse public key as SEC-1 v2 encoded point"); - return Err(Error::Ctap(CtapError::Other)); + return Err(WebAuthnError::Ctap(CtapError::Other)); }; let x_bytes = encoded_point.x().ok_or_else(|| { error!("Public key is the identity point"); - Error::Platform(PlatformError::CryptoError( + WebAuthnError::Platform(PlatformError::CryptoError( "public key is the identity point".into(), )) })?; let y_bytes = encoded_point.y().ok_or_else(|| { error!("Public key is identity or compressed"); - Error::Platform(PlatformError::CryptoError( + WebAuthnError::Platform(PlatformError::CryptoError( "public key is identity or compressed".into(), )) })?; let x: heapless::Vec = heapless::Vec::from_slice(x_bytes.as_bytes()).map_err(|_| { - Error::Platform(PlatformError::CryptoError( + WebAuthnError::Platform(PlatformError::CryptoError( "x coordinate exceeds 32 bytes".into(), )) })?; let y: heapless::Vec = heapless::Vec::from_slice(y_bytes.as_bytes()).map_err(|_| { - Error::Platform(PlatformError::CryptoError( + WebAuthnError::Platform(PlatformError::CryptoError( "y coordinate exceeds 32 bytes".into(), )) })?; @@ -97,7 +97,7 @@ impl UpgradableResponse for Regis len = cose_encoded_public_key.len(), "COSE-encoded P-256 public key is not 77 bytes" ); - return Err(Error::Platform(PlatformError::CryptoError( + return Err(WebAuthnError::Platform(PlatformError::CryptoError( "unexpected COSE-encoded public key length".into(), ))); } @@ -175,7 +175,10 @@ impl UpgradableResponse for Regis } impl UpgradableResponse for SignResponse { - fn try_upgrade(&self, request: &SignRequest) -> Result { + fn try_upgrade( + &self, + request: &SignRequest, + ) -> Result> { // Generate authenticatorData from the U2F authentication response message received from the authenticator: // Copy bits 0 (the UP bit) and bit 1 from the CTAP2/U2F response user presence byte to bits 0 and 1 of the @@ -197,7 +200,7 @@ impl UpgradableResponse for SignResponse { let authenticator_data = AuthenticatorData { rp_id_hash: request.app_id_hash.clone().try_into().map_err(|_| { error!("app_id_hash has invalid length, expected 32 bytes"); - Error::Platform(PlatformError::InvalidDeviceResponse) + WebAuthnError::Platform(PlatformError::InvalidDeviceResponse) })?, flags, signature_count, diff --git a/libwebauthn/src/ops/webauthn/large_blob.rs b/libwebauthn/src/ops/webauthn/large_blob.rs index bcaa403e..e85bc389 100644 --- a/libwebauthn/src/ops/webauthn/large_blob.rs +++ b/libwebauthn/src/ops/webauthn/large_blob.rs @@ -12,7 +12,7 @@ use tracing::{debug, trace, warn}; use crate::pin::PinUvAuthProtocol; use crate::proto::ctap2::cbor::Value; use crate::proto::ctap2::{Ctap2, Ctap2LargeBlobsRequest, Ctap2PinUvAuthProtocol}; -use crate::webauthn::Error; +use crate::webauthn::WebAuthnError; /// Spec default for `maxFragmentLength` when `maxMsgSize` is absent (CTAP 2.2 §6.10.2). pub(crate) const LARGE_BLOB_DEFAULT_FRAGMENT: u32 = 960; @@ -28,14 +28,14 @@ const LARGE_BLOB_NONCE_LEN: usize = 12; const LARGE_BLOB_AD_PREFIX: &[u8] = b"blob"; #[derive(thiserror::Error, Debug)] -pub(crate) enum LargeBlobError { +pub(crate) enum LargeBlobError { #[error("On-device largeBlobArray is malformed: {0}")] Corrupted(String), /// CTAP 2.2 §6.10.6 line 303 "Return an error": delete called but no entry decrypted under our key. #[error("largeBlobArray has no entry to delete for this credential")] NoMatch, #[error(transparent)] - Webauthn(#[from] Error), + Webauthn(#[from] WebAuthnError), } /// `maxFragmentLength` per CTAP 2.2 §6.10.2 (`maxMsgSize - 64`, default 960). Floored at 1. @@ -62,17 +62,17 @@ pub(crate) async fn fetch_large_blob_entries( channel: &mut C, max_fragment: u32, timeout: Duration, -) -> Result, LargeBlobError> { +) -> Result, LargeBlobError> { let serialized = fetch_serialized_array(channel, max_fragment, timeout).await?; let array_bytes = strip_array_trailer(&serialized)?; parse_large_blob_array(array_bytes) } /// Decrypt the first entry that authenticates under `key`. -pub(crate) fn decrypt_first_matching( +pub(crate) fn decrypt_first_matching( entries: &[LargeBlobMapEntry], key: &[u8; 32], -) -> Result>, LargeBlobError> { +) -> Result>, LargeBlobError> { for entry in entries { if let Some(plaintext) = entry.try_decrypt(key)? { return Ok(Some(plaintext)); @@ -88,7 +88,7 @@ async fn read_authenticator_large_blob( large_blob_key: &[u8; 32], max_fragment: u32, timeout: Duration, -) -> Result>, LargeBlobError> { +) -> Result>, LargeBlobError> { let entries = fetch_large_blob_entries(channel, max_fragment, timeout).await?; decrypt_first_matching(&entries, large_blob_key) } @@ -97,7 +97,7 @@ async fn fetch_serialized_array( channel: &mut C, max_fragment: u32, timeout: Duration, -) -> Result, LargeBlobError> { +) -> Result, LargeBlobError> { let mut out: Vec = Vec::new(); let mut offset: u32 = 0; loop { @@ -148,7 +148,7 @@ pub(crate) struct LargeBlobMapEntry { impl LargeBlobMapEntry { /// `Ok(None)` on AEAD failure (skip to next entry), `Err` only on structural problems. - fn try_decrypt(&self, key: &[u8; 32]) -> Result>, LargeBlobError> { + fn try_decrypt(&self, key: &[u8; 32]) -> Result>, LargeBlobError> { if self.nonce.len() != LARGE_BLOB_NONCE_LEN { return Ok(None); } @@ -199,7 +199,7 @@ impl LargeBlobMapEntry { } } -fn strip_array_trailer(serialized: &[u8]) -> Result<&[u8], LargeBlobError> { +fn strip_array_trailer(serialized: &[u8]) -> Result<&[u8], LargeBlobError> { if serialized.len() < LARGE_BLOB_HASH_LEN { return Err(LargeBlobError::Corrupted(format!( "serialized array length {} < trailer length {}", @@ -219,7 +219,7 @@ fn strip_array_trailer(serialized: &[u8]) -> Result<&[u8], LargeBlobError> { } /// Parse entries, skipping any with per-entry structural errors (CTAP 2.2 §6.10.3). -fn parse_large_blob_array(bytes: &[u8]) -> Result, LargeBlobError> { +fn parse_large_blob_array(bytes: &[u8]) -> Result, LargeBlobError> { if bytes.is_empty() { return Ok(Vec::new()); } @@ -286,11 +286,11 @@ fn parse_large_blob_array(bytes: &[u8]) -> Result, LargeB } /// Encrypt+compress one entry under `key`, returning the canonical CBOR map per CTAP 2.2 §6.10.3. -pub(crate) fn encrypt_entry( +pub(crate) fn encrypt_entry( key: &[u8; 32], nonce: &[u8], plaintext: &[u8], -) -> Result, LargeBlobError> { +) -> Result, LargeBlobError> { use flate2::write::DeflateEncoder; use flate2::Compression; use std::io::Write; @@ -373,18 +373,18 @@ pub(crate) fn build_serialized_array(entries: &[Vec]) -> Vec { /// `pinUvAuthParam` for an `authenticatorLargeBlobs(set)` chunk. CTAP 2.2 §6.10.2: /// `authenticate(token, 32×0xff || h'0c00' || uint32LittleEndian(offset) || SHA-256(set))`. -pub(crate) fn large_blob_pin_uv_auth_param( +pub(crate) fn large_blob_pin_uv_auth_param( token: &[u8], proto: &dyn PinUvAuthProtocol, offset: u32, chunk: &[u8], -) -> Result, Error> { +) -> Result, WebAuthnError> { let mut buf = Vec::with_capacity(32 + 2 + 4 + 32); buf.extend_from_slice(&[0xff; 32]); buf.extend_from_slice(&[0x0c, 0x00]); buf.extend_from_slice(&offset.to_le_bytes()); buf.extend_from_slice(&Sha256::digest(chunk)); - proto.authenticate(token, &buf) + Ok(proto.authenticate(token, &buf)?) } /// One array element kept as its exact CBOR bytes, plus a best-effort decode for ownership testing. @@ -393,7 +393,7 @@ struct RawArrayEntry { value: Option, } -fn read_byte(cursor: &mut std::io::Cursor<&[u8]>) -> Result { +fn read_byte(cursor: &mut std::io::Cursor<&[u8]>) -> Result> { use std::io::Read; let mut b = [0u8; 1]; cursor @@ -403,7 +403,7 @@ fn read_byte(cursor: &mut std::io::Cursor<&[u8]>) -> Result Ok(byte) } -fn read_uint(cursor: &mut std::io::Cursor<&[u8]>, n: usize) -> Result { +fn read_uint(cursor: &mut std::io::Cursor<&[u8]>, n: usize) -> Result> { let mut val: u64 = 0; for _ in 0..n { val = (val << 8) | read_byte(cursor)? as u64; @@ -412,7 +412,7 @@ fn read_uint(cursor: &mut std::io::Cursor<&[u8]>, n: usize) -> Result) -> Result { +fn read_array_header(cursor: &mut std::io::Cursor<&[u8]>) -> Result> { let initial = read_byte(cursor)?; if initial >> 5 != 4 { return Err(LargeBlobError::Corrupted(format!( @@ -453,7 +453,7 @@ fn encode_array_header(n: usize) -> Vec { } /// Parse the top-level array into per-element raw byte spans, preserving foreign entries exactly. -fn parse_array_raw_entries(bytes: &[u8]) -> Result, LargeBlobError> { +fn parse_array_raw_entries(bytes: &[u8]) -> Result, LargeBlobError> { if bytes.is_empty() { return Ok(Vec::new()); } @@ -532,11 +532,11 @@ fn entry_decrypts_under_key(entry: &Value, key: &[u8; 32]) -> bool { /// Drop entries that AEAD-verify under `drop_key`, optionally append `new_entry`, append the trailer. /// Foreign entries are spliced back by their original bytes per CTAP 2.2 §6.10.2. -fn rebuild_serialized_array( +fn rebuild_serialized_array( existing: &[RawArrayEntry], drop_key: &[u8; 32], new_entry: Option>, -) -> Result, LargeBlobError> { +) -> Result, LargeBlobError> { let mut kept: Vec<&[u8]> = Vec::with_capacity(existing.len() + 1); for entry in existing { if entry @@ -566,9 +566,9 @@ async fn fetch_or_initial( channel: &mut C, max_fragment: u32, timeout: Duration, -) -> Result, LargeBlobError> { +) -> Result, LargeBlobError> { let serialized = fetch_serialized_array(channel, max_fragment, timeout).await?; - match strip_array_trailer(&serialized) { + match strip_array_trailer::(&serialized) { Ok(array_bytes) => parse_array_raw_entries(array_bytes), Err(_) => { warn!("largeBlobArray trailer mismatch; treating as initial empty array (CTAP 2.2 §6.10.2)"); @@ -585,7 +585,7 @@ async fn upload_serialized_array( max_fragment: u32, pin_uv_auth: Option<(&[u8], Ctap2PinUvAuthProtocol)>, timeout: Duration, -) -> Result<(), LargeBlobError> { +) -> Result<(), LargeBlobError> { let total: u32 = serialized .len() .try_into() @@ -654,7 +654,7 @@ pub(crate) async fn write_authenticator_large_blob( max_fragment: u32, pin_uv_auth: Option<(&[u8], Ctap2PinUvAuthProtocol)>, timeout: Duration, -) -> Result<(), LargeBlobError> { +) -> Result<(), LargeBlobError> { if (blob.len() as u64) > LARGE_BLOB_MAX_ORIG_SIZE { return Err(LargeBlobError::Corrupted(format!( "blob length {} exceeds platform cap {LARGE_BLOB_MAX_ORIG_SIZE}", @@ -678,7 +678,7 @@ pub(crate) async fn delete_authenticator_large_blob( max_fragment: u32, pin_uv_auth: Option<(&[u8], Ctap2PinUvAuthProtocol)>, timeout: Duration, -) -> Result<(), LargeBlobError> { +) -> Result<(), LargeBlobError> { let existing = fetch_or_initial(channel, max_fragment, timeout).await?; let any_owned = existing.iter().any(|e| { e.value @@ -696,6 +696,7 @@ pub(crate) async fn delete_authenticator_large_blob( #[cfg(test)] mod tests { use super::*; + use std::convert::Infallible; fn rp_id_hash(rp_id: &str) -> Vec { use sha2::{Digest, Sha256}; @@ -733,14 +734,14 @@ mod tests { let key = [0x42u8; 32]; let nonce = [0x07u8; 12]; let plaintext = b"the quick brown fox".to_vec(); - let entry_bytes = encrypt_entry(&key, &nonce, &plaintext).expect("encrypt"); + let entry_bytes = encrypt_entry::(&key, &nonce, &plaintext).expect("encrypt"); let serialized = build_serialized_array(&[entry_bytes]); - let array_bytes = strip_array_trailer(&serialized).expect("trailer"); - let parsed = parse_large_blob_array(array_bytes).expect("parse"); + let array_bytes = strip_array_trailer::(&serialized).expect("trailer"); + let parsed = parse_large_blob_array::(array_bytes).expect("parse"); assert_eq!(parsed.len(), 1); let plaintext_decoded = parsed[0] - .try_decrypt(&key) + .try_decrypt::(&key) .expect("decrypt") .expect("entry should verify under the correct key"); assert_eq!(plaintext_decoded, plaintext); @@ -752,12 +753,13 @@ mod tests { let wrong_key = [0x43u8; 32]; let nonce = [0x07u8; 12]; let plaintext = b"secret".to_vec(); - let entry_bytes = encrypt_entry(&real_key, &nonce, &plaintext).expect("encrypt"); + let entry_bytes = + encrypt_entry::(&real_key, &nonce, &plaintext).expect("encrypt"); let serialized = build_serialized_array(&[entry_bytes]); - let array_bytes = strip_array_trailer(&serialized).expect("trailer"); - let parsed = parse_large_blob_array(array_bytes).expect("parse"); + let array_bytes = strip_array_trailer::(&serialized).expect("trailer"); + let parsed = parse_large_blob_array::(array_bytes).expect("parse"); let res = parsed[0] - .try_decrypt(&wrong_key) + .try_decrypt::(&wrong_key) .expect("decrypt should not error on AEAD failure"); assert!(res.is_none()); } @@ -767,22 +769,22 @@ mod tests { let mut serialized = build_serialized_array(&[]); let last = serialized.len() - 1; serialized[last] ^= 0xff; - let err = strip_array_trailer(&serialized).unwrap_err(); + let err = strip_array_trailer::(&serialized).unwrap_err(); assert!(matches!(err, LargeBlobError::Corrupted(_))); } #[test] fn truncated_serialized_array_is_rejected() { let too_short = vec![0u8; 8]; - let err = strip_array_trailer(&too_short).unwrap_err(); + let err = strip_array_trailer::(&too_short).unwrap_err(); assert!(matches!(err, LargeBlobError::Corrupted(_))); } #[test] fn empty_array_parses_to_zero_entries() { let serialized = build_serialized_array(&[]); - let array_bytes = strip_array_trailer(&serialized).unwrap(); - let parsed = parse_large_blob_array(array_bytes).unwrap(); + let array_bytes = strip_array_trailer::(&serialized).unwrap(); + let parsed = parse_large_blob_array::(array_bytes).unwrap(); assert!(parsed.is_empty()); } @@ -792,17 +794,17 @@ mod tests { let key_b = [0xb2u8; 32]; let key_c = [0xc3u8; 32]; let nonce = [0x55u8; 12]; - let entry_a = encrypt_entry(&key_a, &nonce, b"alpha").unwrap(); - let entry_b = encrypt_entry(&key_b, &nonce, b"bravo").unwrap(); - let entry_c = encrypt_entry(&key_c, &nonce, b"charlie").unwrap(); + let entry_a = encrypt_entry::(&key_a, &nonce, b"alpha").unwrap(); + let entry_b = encrypt_entry::(&key_b, &nonce, b"bravo").unwrap(); + let entry_c = encrypt_entry::(&key_c, &nonce, b"charlie").unwrap(); let serialized = build_serialized_array(&[entry_a, entry_b, entry_c]); - let array_bytes = strip_array_trailer(&serialized).unwrap(); - let parsed = parse_large_blob_array(array_bytes).unwrap(); + let array_bytes = strip_array_trailer::(&serialized).unwrap(); + let parsed = parse_large_blob_array::(array_bytes).unwrap(); assert_eq!(parsed.len(), 3); let mut found_b = None; for e in &parsed { - if let Some(pt) = e.try_decrypt(&key_b).unwrap() { + if let Some(pt) = e.try_decrypt::(&key_b).unwrap() { found_b = Some(pt); } } @@ -818,7 +820,7 @@ mod tests { let key = [0xCAu8; 32]; let nonce = [0x33u8; 12]; - let good = encrypt_entry(&key, &nonce, b"survivor").unwrap(); + let good = encrypt_entry::(&key, &nonce, b"survivor").unwrap(); let bad_entry_bytes = { let mut buf = Vec::new(); @@ -827,10 +829,11 @@ mod tests { buf }; let serialized = build_serialized_array(&[bad_entry_bytes, good]); - let array_bytes = strip_array_trailer(&serialized).unwrap(); - let parsed = parse_large_blob_array(array_bytes).expect("parse must not error"); + let array_bytes = strip_array_trailer::(&serialized).unwrap(); + let parsed = + parse_large_blob_array::(array_bytes).expect("parse must not error"); assert_eq!(parsed.len(), 1, "bad entry skipped, good entry kept"); - let pt = parsed[0].try_decrypt(&key).unwrap().unwrap(); + let pt = parsed[0].try_decrypt::(&key).unwrap().unwrap(); assert_eq!(pt, b"survivor"); } @@ -842,7 +845,7 @@ mod tests { let key = [0xCBu8; 32]; let nonce = [0x44u8; 12]; - let good = encrypt_entry(&key, &nonce, b"present").unwrap(); + let good = encrypt_entry::(&key, &nonce, b"present").unwrap(); let incomplete = { let mut map = BTreeMap::new(); @@ -854,10 +857,11 @@ mod tests { buf }; let serialized = build_serialized_array(&[incomplete, good]); - let array_bytes = strip_array_trailer(&serialized).unwrap(); - let parsed = parse_large_blob_array(array_bytes).expect("parse must not error"); + let array_bytes = strip_array_trailer::(&serialized).unwrap(); + let parsed = + parse_large_blob_array::(array_bytes).expect("parse must not error"); assert_eq!(parsed.len(), 1); - let pt = parsed[0].try_decrypt(&key).unwrap().unwrap(); + let pt = parsed[0].try_decrypt::(&key).unwrap().unwrap(); assert_eq!(pt, b"present"); } @@ -870,7 +874,7 @@ mod tests { let key = [0xC0u8; 32]; let nonce = [0x11u8; 12]; let plaintext = b"hello, largeBlob".to_vec(); - let entry = encrypt_entry(&key, &nonce, &plaintext).unwrap(); + let entry = encrypt_entry::(&key, &nonce, &plaintext).unwrap(); let serialized = build_serialized_array(&[entry]); assert!( serialized.len() < LARGE_BLOB_DEFAULT_FRAGMENT as usize, @@ -957,7 +961,7 @@ mod tests { let large_blob_key = [0x77u8; 32]; let nonce = [0x22u8; 12]; let plaintext = b"webauthn end-to-end largeBlob".to_vec(); - let entry = encrypt_entry(&large_blob_key, &nonce, &plaintext).unwrap(); + let entry = encrypt_entry::(&large_blob_key, &nonce, &plaintext).unwrap(); let serialized_array = build_serialized_array(&[entry]); let credential_id = b"cred-id".to_vec(); @@ -1089,8 +1093,8 @@ mod tests { let key1 = [0x22u8; 32]; let pt0 = b"blob for credential zero".to_vec(); let pt1 = b"blob for credential one".to_vec(); - let entry0 = encrypt_entry(&key0, &[0xa0u8; 12], &pt0).unwrap(); - let entry1 = encrypt_entry(&key1, &[0xb1u8; 12], &pt1).unwrap(); + let entry0 = encrypt_entry::(&key0, &[0xa0u8; 12], &pt0).unwrap(); + let entry1 = encrypt_entry::(&key1, &[0xb1u8; 12], &pt1).unwrap(); let serialized_array = build_serialized_array(&[entry0, entry1]); assert!( serialized_array.len() < LARGE_BLOB_DEFAULT_FRAGMENT as usize, @@ -1207,7 +1211,8 @@ mod tests { let offset: u32 = 0x12345678; let proto = PinUvAuthProtocolTwo::new(); - let got = large_blob_pin_uv_auth_param(&token, &proto, offset, chunk).expect("auth_param"); + let got = large_blob_pin_uv_auth_param::(&token, &proto, offset, chunk) + .expect("auth_param"); let mut expected_msg = Vec::new(); expected_msg.extend_from_slice(&[0xff; 32]); @@ -1225,7 +1230,7 @@ mod tests { fn entry_decrypts_under_key_matches_owned_entry() { let key = [0x42u8; 32]; let nonce = [0x07u8; 12]; - let entry_bytes = encrypt_entry(&key, &nonce, b"owned blob").unwrap(); + let entry_bytes = encrypt_entry::(&key, &nonce, b"owned blob").unwrap(); let entry: Value = crate::proto::ctap2::cbor::from_slice(&entry_bytes).unwrap(); assert!(entry_decrypts_under_key(&entry, &key)); } @@ -1235,7 +1240,8 @@ mod tests { let owner = [0xa1u8; 32]; let other = [0xb2u8; 32]; let nonce = [0x33u8; 12]; - let entry_bytes = encrypt_entry(&owner, &nonce, b"someone else's blob").unwrap(); + let entry_bytes = + encrypt_entry::(&owner, &nonce, b"someone else's blob").unwrap(); let entry: Value = crate::proto::ctap2::cbor::from_slice(&entry_bytes).unwrap(); assert!(!entry_decrypts_under_key(&entry, &other)); } @@ -1251,20 +1257,20 @@ mod tests { let owner_a = [0xa1u8; 32]; let owner_b = [0xb2u8; 32]; let nonce = [0x55u8; 12]; - let entry_a = encrypt_entry(&owner_a, &nonce, b"alpha").unwrap(); - let entry_b = encrypt_entry(&owner_b, &nonce, b"bravo").unwrap(); + let entry_a = encrypt_entry::(&owner_a, &nonce, b"alpha").unwrap(); + let entry_b = encrypt_entry::(&owner_b, &nonce, b"bravo").unwrap(); - let new_entry = encrypt_entry(&owner_a, &[0x99u8; 12], b"alpha v2").unwrap(); + let new_entry = encrypt_entry::(&owner_a, &[0x99u8; 12], b"alpha v2").unwrap(); - let rebuilt = rebuild_serialized_array( + let rebuilt = rebuild_serialized_array::( &[raw_entry(&entry_a), raw_entry(&entry_b)], &owner_a, Some(new_entry), ) .unwrap(); - let array_bytes = strip_array_trailer(&rebuilt).unwrap(); - let parsed = parse_array_raw_entries(array_bytes).unwrap(); + let array_bytes = strip_array_trailer::(&rebuilt).unwrap(); + let parsed = parse_array_raw_entries::(array_bytes).unwrap(); assert_eq!( parsed.len(), 2, @@ -1285,14 +1291,17 @@ mod tests { let owner_a = [0xa1u8; 32]; let owner_b = [0xb2u8; 32]; let nonce = [0x55u8; 12]; - let entry_a = encrypt_entry(&owner_a, &nonce, b"alpha").unwrap(); - let entry_b = encrypt_entry(&owner_b, &nonce, b"bravo").unwrap(); + let entry_a = encrypt_entry::(&owner_a, &nonce, b"alpha").unwrap(); + let entry_b = encrypt_entry::(&owner_b, &nonce, b"bravo").unwrap(); - let rebuilt = - rebuild_serialized_array(&[raw_entry(&entry_a), raw_entry(&entry_b)], &owner_a, None) - .unwrap(); - let array_bytes = strip_array_trailer(&rebuilt).unwrap(); - let parsed = parse_array_raw_entries(array_bytes).unwrap(); + let rebuilt = rebuild_serialized_array::( + &[raw_entry(&entry_a), raw_entry(&entry_b)], + &owner_a, + None, + ) + .unwrap(); + let array_bytes = strip_array_trailer::(&rebuilt).unwrap(); + let parsed = parse_array_raw_entries::(array_bytes).unwrap(); assert_eq!(parsed.len(), 1); assert!(entry_decrypts_under_key( parsed[0].value.as_ref().unwrap(), @@ -1306,10 +1315,11 @@ mod tests { let owner_a = [0xa1u8; 32]; let owner_b = [0xb2u8; 32]; let nonce = [0x55u8; 12]; - let entry_b = encrypt_entry(&owner_b, &nonce, b"bravo").unwrap(); - let rebuilt = rebuild_serialized_array(&[raw_entry(&entry_b)], &owner_a, None).unwrap(); - let array_bytes = strip_array_trailer(&rebuilt).unwrap(); - let parsed = parse_array_raw_entries(array_bytes).unwrap(); + let entry_b = encrypt_entry::(&owner_b, &nonce, b"bravo").unwrap(); + let rebuilt = + rebuild_serialized_array::(&[raw_entry(&entry_b)], &owner_a, None).unwrap(); + let array_bytes = strip_array_trailer::(&rebuilt).unwrap(); + let parsed = parse_array_raw_entries::(array_bytes).unwrap(); assert_eq!(parsed.len(), 1); assert!(entry_decrypts_under_key( parsed[0].value.as_ref().unwrap(), @@ -1323,10 +1333,10 @@ mod tests { let owner_a = [0xa1u8; 32]; let owner_b = [0xb2u8; 32]; - let entry_a_bytes = encrypt_entry(&owner_a, &[0x55u8; 12], b"alpha").unwrap(); + let entry_a_bytes = encrypt_entry::(&owner_a, &[0x55u8; 12], b"alpha").unwrap(); // Foreign entry under owner_b carrying an extra (future) key 0x07. - let entry_b_base = encrypt_entry(&owner_b, &[0x66u8; 12], b"bravo").unwrap(); + let entry_b_base = encrypt_entry::(&owner_b, &[0x66u8; 12], b"bravo").unwrap(); let Value::Map(mut map_b) = crate::proto::ctap2::cbor::from_slice(&entry_b_base).unwrap() else { panic!("entry_b is a map"); @@ -1339,16 +1349,16 @@ mod tests { entry_b_bytes.extend_from_slice(&canonical[1..]); entry_b_bytes.push(0xFF); - let new_entry = encrypt_entry(&owner_a, &[0x99u8; 12], b"alpha v2").unwrap(); + let new_entry = encrypt_entry::(&owner_a, &[0x99u8; 12], b"alpha v2").unwrap(); - let rebuilt = rebuild_serialized_array( + let rebuilt = rebuild_serialized_array::( &[raw_entry(&entry_a_bytes), raw_entry(&entry_b_bytes)], &owner_a, Some(new_entry), ) .unwrap(); - let array_bytes = strip_array_trailer(&rebuilt).unwrap(); - let parsed = parse_array_raw_entries(array_bytes).unwrap(); + let array_bytes = strip_array_trailer::(&rebuilt).unwrap(); + let parsed = parse_array_raw_entries::(array_bytes).unwrap(); assert_eq!(parsed.len(), 2); assert_eq!( parsed[0].raw, entry_b_bytes, @@ -1366,32 +1376,37 @@ mod tests { #[test] fn parse_array_raw_entries_rejects_hostile_headers() { - let e0 = encrypt_entry(&[0x01u8; 32], &[0u8; 12], b"a").unwrap(); - let e1 = encrypt_entry(&[0x02u8; 32], &[0u8; 12], b"b").unwrap(); + let e0 = encrypt_entry::(&[0x01u8; 32], &[0u8; 12], b"a").unwrap(); + let e1 = encrypt_entry::(&[0x02u8; 32], &[0u8; 12], b"b").unwrap(); let mut canonical = encode_array_header(2); canonical.extend_from_slice(&e0); canonical.extend_from_slice(&e1); - assert_eq!(parse_array_raw_entries(&canonical).unwrap().len(), 2); + assert_eq!( + parse_array_raw_entries::(&canonical) + .unwrap() + .len(), + 2 + ); // Huge declared count (array(2^32-1)) with no element bytes: bounded alloc, errors fast. - assert!(parse_array_raw_entries(&[0x9a, 0xff, 0xff, 0xff, 0xff]).is_err()); + assert!(parse_array_raw_entries::(&[0x9a, 0xff, 0xff, 0xff, 0xff]).is_err()); // Valid array plus one trailing byte: rejected by the full-consumption check. let mut trailing = canonical.clone(); trailing.push(0x00); - assert!(parse_array_raw_entries(&trailing).is_err()); + assert!(parse_array_raw_entries::(&trailing).is_err()); // Header count smaller than the actual element bytes: rejected. let mut short = encode_array_header(1); short.extend_from_slice(&e0); short.extend_from_slice(&e1); - assert!(parse_array_raw_entries(&short).is_err()); + assert!(parse_array_raw_entries::(&short).is_err()); } #[test] fn rebuild_meets_minimum_17_bytes_when_empty() { // CTAP 2.2 §6.10.2: serialized array length MUST be >= 17. - let rebuilt = rebuild_serialized_array(&[], &[0u8; 32], None).unwrap(); + let rebuilt = rebuild_serialized_array::(&[], &[0u8; 32], None).unwrap(); assert!(rebuilt.len() >= 17); // Empty array: 0x80 (1 byte) + 16-byte trailer = 17 bytes. assert_eq!(rebuilt.len(), 17); @@ -1403,7 +1418,7 @@ mod tests { /// emission against future serializer drift. #[test] fn rebuild_empty_array_matches_spec_initial_bytes() { - let rebuilt = rebuild_serialized_array(&[], &[0u8; 32], None).unwrap(); + let rebuilt = rebuild_serialized_array::(&[], &[0u8; 32], None).unwrap(); assert_eq!(hex::encode(&rebuilt), "8076be8b528d0075f7aae98d6fa57a6d3c"); } @@ -1421,15 +1436,16 @@ mod tests { let plaintext = b"round-trip blob".to_vec(); let nonce = [0x07u8; 12]; - let entry_bytes = encrypt_entry(&key, &nonce, &plaintext).unwrap(); - let serialized = rebuild_serialized_array(&[], &key, Some(entry_bytes)).unwrap(); + let entry_bytes = encrypt_entry::(&key, &nonce, &plaintext).unwrap(); + let serialized = + rebuild_serialized_array::(&[], &key, Some(entry_bytes)).unwrap(); assert!( serialized.len() <= LARGE_BLOB_DEFAULT_FRAGMENT as usize, "test fixture must fit in one chunk" ); - let auth_param = - large_blob_pin_uv_auth_param(&token, &proto, 0, &serialized).expect("auth_param"); + let auth_param = large_blob_pin_uv_auth_param::(&token, &proto, 0, &serialized) + .expect("auth_param"); let set_req = Ctap2LargeBlobsRequest::new_set_first( serialized.clone(), serialized.len() as u32, @@ -1478,7 +1494,8 @@ mod tests { for (offset, chunk_len) in [(0u32, 32), (32u32, 32), (64u32, 6)] { let chunk = serialized[offset as usize..(offset as usize + chunk_len)].to_vec(); let auth_param = - large_blob_pin_uv_auth_param(&token, &proto, offset, &chunk).expect("auth_param"); + large_blob_pin_uv_auth_param::(&token, &proto, offset, &chunk) + .expect("auth_param"); let req = if offset == 0 { Ctap2LargeBlobsRequest::new_set_first( chunk, @@ -1571,7 +1588,7 @@ mod tests { let key = [0xC0u8; 32]; let nonce = [0x11u8; 12]; let plaintext = incompressible(31); - let entry = encrypt_entry(&key, &nonce, &plaintext).unwrap(); + let entry = encrypt_entry::(&key, &nonce, &plaintext).unwrap(); let serialized = build_serialized_array(&[entry]); assert!( serialized.len() > 2 * MF as usize, @@ -1589,7 +1606,7 @@ mod tests { let entries = fetch_large_blob_entries(&mut channel, MF, Duration::from_secs(5)) .await .expect("fetch"); - let got = decrypt_first_matching(&entries, &key).expect("decrypt"); + let got = decrypt_first_matching::(&entries, &key).expect("decrypt"); assert_eq!(got.as_deref(), Some(plaintext.as_slice())); } @@ -1601,7 +1618,7 @@ mod tests { let key = [0xD0u8; 32]; let nonce = [0x22u8; 12]; let plaintext = incompressible(37); - let entry = encrypt_entry(&key, &nonce, &plaintext).unwrap(); + let entry = encrypt_entry::(&key, &nonce, &plaintext).unwrap(); let serialized = build_serialized_array(&[entry]); assert!( serialized.len() > 2 * MF as usize, @@ -1619,7 +1636,7 @@ mod tests { let entries = fetch_large_blob_entries(&mut channel, MF, Duration::from_secs(5)) .await .expect("fetch"); - let got = decrypt_first_matching(&entries, &key).expect("decrypt"); + let got = decrypt_first_matching::(&entries, &key).expect("decrypt"); assert_eq!(got.as_deref(), Some(plaintext.as_slice())); } @@ -1660,7 +1677,7 @@ mod tests { orig_size: over, }; assert!(oversize - .try_decrypt(&key) + .try_decrypt::(&key) .expect("must not error") .is_none()); @@ -1691,7 +1708,7 @@ mod tests { nonce: nonce.to_vec(), orig_size: 4, }; - let err = bomb.try_decrypt(&key).unwrap_err(); + let err = bomb.try_decrypt::(&key).unwrap_err(); assert!( matches!(&err, LargeBlobError::Corrupted(msg) if msg.contains("decompressed length")), "expected length-mismatch Corrupted, got {err:?}" @@ -1708,7 +1725,7 @@ mod tests { let nonce = [0x07u8; 12]; let plaintext = b"largeBlob deflate rawness payload ".repeat(16); - let entry_bytes = encrypt_entry(&key, &nonce, &plaintext).expect("encrypt"); + let entry_bytes = encrypt_entry::(&key, &nonce, &plaintext).expect("encrypt"); let Value::Map(map) = crate::proto::ctap2::cbor::from_slice::(&entry_bytes).unwrap() else { @@ -1783,98 +1800,133 @@ mod tests { /// under the same key MUST emit distinct nonces. #[tokio::test] async fn each_write_uses_a_distinct_nonce() { - use crate::proto::ctap2::{ - Ctap2AuthenticatorConfigRequest, Ctap2BioEnrollmentRequest, Ctap2BioEnrollmentResponse, - Ctap2ClientPinRequest, Ctap2ClientPinResponse, Ctap2CredentialManagementRequest, - Ctap2CredentialManagementResponse, Ctap2GetAssertionRequest, Ctap2GetAssertionResponse, - Ctap2GetInfoResponse, Ctap2LargeBlobsRequest, Ctap2LargeBlobsResponse, - Ctap2MakeCredentialRequest, Ctap2MakeCredentialResponse, + use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; + use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; + use crate::proto::ctap2::Ctap2LargeBlobsResponse; + use crate::transport::mock::channel::MockChannel; + use crate::transport::{ + device::SupportedProtocols, AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore, }; + use crate::UvUpdate; + use std::fmt::{self, Display}; + use tokio::sync::broadcast; - // Records each uploaded array; get() replays the most recent one (RMW round-trip). + // Records each uploaded array; a get() replays the most recent one (RMW round-trip). struct RecordingChannel { + inner: MockChannel, current: Vec, sets: Vec>, + pending_get: bool, } - #[async_trait::async_trait] - impl Ctap2 for RecordingChannel { - async fn ctap2_large_blobs( - &mut self, - request: &Ctap2LargeBlobsRequest, - _timeout: Duration, - ) -> Result { - if request.get.is_some() { - Ok(Ctap2LargeBlobsResponse { - config: Some(serde_bytes::ByteBuf::from(self.current.clone())), - }) - } else if let Some(set) = request.set.as_ref() { - self.current = set.to_vec(); - self.sets.push(set.to_vec()); - Ok(Ctap2LargeBlobsResponse::default()) - } else { - panic!("largeBlobs request was neither get nor set"); - } + impl Display for RecordingChannel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "RecordingChannel") } - async fn ctap2_get_info(&mut self) -> Result { - unimplemented!() + } + + impl Ctap2AuthTokenStore for RecordingChannel { + fn store_auth_data(&mut self, data: AuthTokenData) { + self.inner.store_auth_data(data); } - async fn ctap2_make_credential( - &mut self, - _r: &Ctap2MakeCredentialRequest, - _t: Duration, - ) -> Result { - unimplemented!() + fn get_auth_data(&self) -> Option<&AuthTokenData> { + self.inner.get_auth_data() } - async fn ctap2_client_pin( - &mut self, - _r: &Ctap2ClientPinRequest, - _t: Duration, - ) -> Result { - unimplemented!() + fn clear_uv_auth_token_store(&mut self) { + self.inner.clear_uv_auth_token_store(); } - async fn ctap2_get_assertion( - &mut self, - _r: &Ctap2GetAssertionRequest, - _t: Duration, - ) -> Result { - unimplemented!() + fn set_cred_mgmt_preview(&mut self, uses_preview: bool) { + self.inner.set_cred_mgmt_preview(uses_preview); } - async fn ctap2_get_next_assertion( - &mut self, - _t: Duration, - ) -> Result { - unimplemented!() + fn cred_mgmt_preview(&self) -> bool { + self.inner.cred_mgmt_preview() } - async fn ctap2_selection(&mut self, _t: Duration) -> Result<(), Error> { - unimplemented!() + } + + #[async_trait::async_trait] + impl Channel for RecordingChannel { + type UxUpdate = UvUpdate; + type TransportError = Infallible; + + fn get_ux_update_sender(&self) -> &broadcast::Sender { + self.inner.get_ux_update_sender() + } + async fn supported_protocols( + &self, + ) -> Result> { + self.inner.supported_protocols().await + } + async fn status(&self) -> ChannelStatus { + ChannelStatus::Ready } - async fn ctap2_authenticator_config( + async fn close(&mut self) {} + async fn apdu_send( &mut self, - _r: &Ctap2AuthenticatorConfigRequest, - _t: Duration, - ) -> Result<(), Error> { + _request: &ApduRequest, + _timeout: Duration, + ) -> Result<(), Self::TransportError> { unimplemented!() } - async fn ctap2_bio_enrollment( + async fn apdu_recv( &mut self, - _r: &Ctap2BioEnrollmentRequest, - _t: Duration, - ) -> Result { + _timeout: Duration, + ) -> Result { unimplemented!() } - async fn ctap2_credential_management( + async fn cbor_send( &mut self, - _r: &Ctap2CredentialManagementRequest, - _t: Duration, - ) -> Result { - unimplemented!() + request: &CborRequest, + _timeout: Duration, + ) -> Result<(), Self::TransportError> { + let Value::Map(map) = + crate::proto::ctap2::cbor::from_slice::(&request.encoded_data).unwrap() + else { + panic!("largeBlobs request must be a CBOR map"); + }; + let mut set_bytes: Option> = None; + let mut is_get = false; + for (k, v) in map.iter() { + let Value::Integer(key) = k else { continue }; + match *key { + 1 => is_get = true, + 2 => { + if let Value::Bytes(b) = v { + set_bytes = Some(b.clone()); + } + } + _ => {} + } + } + if let Some(bytes) = set_bytes { + self.current = bytes.clone(); + self.sets.push(bytes); + self.pending_get = false; + } else if is_get { + self.pending_get = true; + } else { + panic!("largeBlobs request was neither get nor set"); + } + Ok(()) + } + async fn cbor_recv( + &mut self, + _timeout: Duration, + ) -> Result { + let resp = if self.pending_get { + Ctap2LargeBlobsResponse { + config: Some(serde_bytes::ByteBuf::from(self.current.clone())), + } + } else { + Ctap2LargeBlobsResponse::default() + }; + let bytes = crate::proto::ctap2::cbor::to_vec(&resp).unwrap(); + Ok(CborResponse::new_success_from_slice(&bytes)) } } fn entry_nonce(serialized: &[u8]) -> Vec { - let array_bytes = strip_array_trailer(serialized).expect("trailer"); - let parsed = parse_large_blob_array(array_bytes).expect("parse"); + let array_bytes = strip_array_trailer::(serialized).expect("trailer"); + let parsed = parse_large_blob_array::(array_bytes).expect("parse"); assert_eq!(parsed.len(), 1, "exactly one entry per write"); parsed[0].nonce.clone() } @@ -1883,8 +1935,10 @@ mod tests { let blob = b"distinct-nonce blob".to_vec(); let mut channel = RecordingChannel { + inner: MockChannel::new(), current: build_serialized_array(&[]), sets: Vec::new(), + pending_get: false, }; for _ in 0..2 { write_authenticator_large_blob( @@ -1920,7 +1974,8 @@ mod tests { let offset: u32 = 0x12345678; let proto = PinUvAuthProtocolOne::new(); - let got = large_blob_pin_uv_auth_param(&token, &proto, offset, chunk).expect("auth_param"); + let got = large_blob_pin_uv_auth_param::(&token, &proto, offset, chunk) + .expect("auth_param"); let mut expected_msg = Vec::new(); expected_msg.extend_from_slice(&[0xff; 32]); diff --git a/libwebauthn/src/pin/mod.rs b/libwebauthn/src/pin/mod.rs index 3c8a4b4c..e3c93d42 100644 --- a/libwebauthn/src/pin/mod.rs +++ b/libwebauthn/src/pin/mod.rs @@ -41,7 +41,7 @@ use crate::{ }, transport::Channel, webauthn::{ - error::{Error, PlatformError}, + error::{PlatformError, WebAuthnError}, pin_uv_auth_token::{obtain_pin, obtain_shared_secret, select_uv_proto}, }, }; @@ -91,36 +91,36 @@ pub trait PinUvAuthProtocol: Send + Sync { fn encapsulate( &self, peer_public_key: &cose::PublicKey, - ) -> Result<(cose::PublicKey, Vec), Error>; + ) -> Result<(cose::PublicKey, Vec), PlatformError>; // 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. - fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, Error>; + fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, PlatformError>; // decrypt(key, ciphertext) → plaintext | error // Decrypts a ciphertext and returns the plaintext. - fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, Error>; + fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, PlatformError>; // authenticate(key, message) → signature // Computes a MAC of the given message. - fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, Error>; + fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, PlatformError>; } trait ECPrivateKeyPinUvAuthProtocol { fn private_key(&self) -> &EphemeralSecret; fn public_key(&self) -> &P256PublicKey; - fn kdf(&self, bytes: &[u8]) -> Result, Error>; + fn kdf(&self, bytes: &[u8]) -> Result, PlatformError>; } /// Common functionality between ECDH-based PIN/UV auth protocols (1 & 2) trait ECDHPinUvAuthProtocol { - fn ecdh(&self, peer_public_key: &cose::PublicKey) -> Result, Error>; + fn ecdh(&self, peer_public_key: &cose::PublicKey) -> Result, PlatformError>; fn encapsulate( &self, peer_public_key: &cose::PublicKey, - ) -> Result<(cose::PublicKey, Vec), Error>; - fn get_public_key(&self) -> Result; + ) -> Result<(cose::PublicKey, Vec), PlatformError>; + fn get_public_key(&self) -> Result; } pub struct PinUvAuthProtocolOne { @@ -162,7 +162,7 @@ impl ECPrivateKeyPinUvAuthProtocol for PinUvAuthProtocolOne { } /// kdf(Z) → sharedSecret - fn kdf(&self, bytes: &[u8]) -> Result, Error> { + fn kdf(&self, bytes: &[u8]) -> Result, PlatformError> { let mut hasher = Sha256::default(); hasher.update(bytes); Ok(hasher.finalize().to_vec()) @@ -177,7 +177,7 @@ where fn encapsulate( &self, peer_public_key: &cose::PublicKey, - ) -> Result<(cose::PublicKey, Vec), Error> { + ) -> Result<(cose::PublicKey, Vec), PlatformError> { // Let sharedSecret be the result of calling ecdh(peerCoseKey). Return any resulting error. let shared_secret = self.ecdh(peer_public_key)?; @@ -186,7 +186,7 @@ where } /// ecdh(peerCoseKey) → sharedSecret | error - fn ecdh(&self, peer_public_key: &cose::PublicKey) -> Result, Error> { + fn ecdh(&self, peer_public_key: &cose::PublicKey) -> Result, PlatformError> { // Parse peerCoseKey as specified for getPublicKey, below, and produce a P-256 point, Y. // If unsuccessful, or if the resulting point is not on the curve, return error. let cose::PublicKey::EcdhEsHkdf256Key(peer_public_key) = peer_public_key else { @@ -194,7 +194,9 @@ where ?peer_public_key, "Unsupported peerCoseKey format. Only EcdhEsHkdf256Key is supported." ); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; // x and y must be exactly 32 bytes (P-256 field size). `cosey` accepts // any length up to 32; validate before converting to `&FieldBytes`. @@ -203,19 +205,21 @@ where x_len = peer_public_key.x.as_bytes().len(), "Peer public key x coordinate is not 32 bytes" ); - Error::Ctap(CtapError::Other) + PlatformError::CryptoError(String::from("pin/uv crypto operation failed")) })?; let y: &[u8; 32] = peer_public_key.y.as_bytes().try_into().map_err(|_| { error!( y_len = peer_public_key.y.as_bytes().len(), "Peer public key y coordinate is not 32 bytes" ); - Error::Ctap(CtapError::Other) + PlatformError::CryptoError(String::from("pin/uv crypto operation failed")) })?; let encoded_point = EncodedPoint::from_affine_coordinates(x.into(), y.into(), false); let Some(peer_public_key) = P256PublicKey::from_encoded_point(&encoded_point).into() else { error!("Failed to parse public key."); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; // Calculate xY, the shared point. (I.e. the scalar-multiplication of the peer’s point, Y, with the @@ -227,32 +231,20 @@ where } /// getPublicKey() - fn get_public_key(&self) -> Result { + fn get_public_key(&self) -> Result { let point = EncodedPoint::from(self.public_key()); let x_bytes = point.x().ok_or_else(|| { error!("Public key is the identity point"); - Error::Platform(PlatformError::CryptoError( - "public key is the identity point".into(), - )) + PlatformError::CryptoError("public key is the identity point".into()) })?; let y_bytes = point.y().ok_or_else(|| { error!("Public key is identity or compressed"); - Error::Platform(PlatformError::CryptoError( - "public key is identity or compressed".into(), - )) + PlatformError::CryptoError("public key is identity or compressed".into()) })?; - let x: heapless::Vec = - heapless::Vec::from_slice(x_bytes.as_bytes()).map_err(|_| { - Error::Platform(PlatformError::CryptoError( - "x coordinate exceeds 32 bytes".into(), - )) - })?; - let y: heapless::Vec = - heapless::Vec::from_slice(y_bytes.as_bytes()).map_err(|_| { - Error::Platform(PlatformError::CryptoError( - "y coordinate exceeds 32 bytes".into(), - )) - })?; + let x: heapless::Vec = heapless::Vec::from_slice(x_bytes.as_bytes()) + .map_err(|_| PlatformError::CryptoError("x coordinate exceeds 32 bytes".into()))?; + let y: heapless::Vec = heapless::Vec::from_slice(y_bytes.as_bytes()) + .map_err(|_| PlatformError::CryptoError("y coordinate exceeds 32 bytes".into()))?; Ok(cose::PublicKey::EcdhEsHkdf256Key( cose::EcdhEsHkdf256PublicKey { x: x.into(), @@ -268,33 +260,33 @@ impl PinUvAuthProtocol for PinUvAuthProtocolOne { } #[instrument(skip_all)] - fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, Error> { + fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, PlatformError> { // Return the AES-256-CBC encryption of demPlaintext using an all-zero IV. // (No padding is performed as the size of demPlaintext is required to be a multiple of the AES block length.) let iv: &[u8] = &[0; 16]; let Ok(enc) = Aes256CbcEncryptor::new_from_slices(key, iv) else { error!(?key, "Invalid key for AES-256 encryption"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; Ok(enc.encrypt_padded_vec_mut::(plaintext)) } #[instrument(skip_all)] - fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, Error> { + fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, PlatformError> { // Return the first 16 bytes of the result of computing HMAC-SHA-256 with the given key and message. let hmac = hmac_sha256(key, message)?; // HMAC-SHA-256 produces 32 bytes, so this slice is always valid. let truncated = hmac.get(..16).ok_or_else(|| { error!(len = hmac.len(), "HMAC output shorter than 16 bytes"); - Error::Platform(PlatformError::CryptoError( - "HMAC output shorter than 16 bytes".into(), - )) + PlatformError::CryptoError("HMAC output shorter than 16 bytes".into()) })?; Ok(Vec::from(truncated)) } #[instrument(skip_all)] - fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, Error> { + fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, PlatformError> { // If the size of demCiphertext is not a multiple of the AES block length, return error. // Otherwise return the AES-256-CBC decryption of demCiphertext using an all-zero IV. if !ciphertext.len().is_multiple_of(16) { @@ -302,17 +294,23 @@ impl PinUvAuthProtocol for PinUvAuthProtocolOne { ?ciphertext, "Ciphertext length is not a multiple of AES block length" ); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); } let iv: &[u8] = &[0; 16]; let Ok(dec) = Aes256CbcDecryptor::new_from_slices(key, iv) else { error!(?key, "Invalid key for AES-256 decryption"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; let Ok(plaintext) = dec.decrypt_padded_vec_mut::(ciphertext) else { error!("Unpad error while decrypting"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; Ok(plaintext) } @@ -320,7 +318,7 @@ impl PinUvAuthProtocol for PinUvAuthProtocolOne { fn encapsulate( &self, peer_public_key: &cose::PublicKey, - ) -> Result<(cose::PublicKey, Vec), Error> { + ) -> Result<(cose::PublicKey, Vec), PlatformError> { ::encapsulate(self, peer_public_key) } } @@ -364,7 +362,7 @@ impl ECPrivateKeyPinUvAuthProtocol for PinUvAuthProtocolTwo { } /// kdf(Z) → sharedSecret - fn kdf(&self, ikm: &[u8]) -> Result, Error> { + fn kdf(&self, ikm: &[u8]) -> Result, PlatformError> { // Returns: // HKDF-SHA-256(salt = 32 zero bytes, IKM = Z, L = 32, info = "CTAP2 HMAC key") || // HKDF-SHA-256(salt = 32 zero bytes, IKM = Z, L = 32, info = "CTAP2 AES key") @@ -384,18 +382,18 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo { fn encapsulate( &self, peer_public_key: &cose::PublicKey, - ) -> Result<(cose::PublicKey, Vec), Error> { + ) -> Result<(cose::PublicKey, Vec), PlatformError> { ::encapsulate(self, peer_public_key) } - fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, Error> { + fn encrypt(&self, key: &[u8], plaintext: &[u8]) -> Result, PlatformError> { // Discard the first 32 bytes of key. (This selects the AES-key portion of the shared secret.) let key = key.get(32..).ok_or_else(|| { error!( key_len = key.len(), "key shorter than 32 bytes; cannot select AES-key portion" ); - Error::Ctap(CtapError::Other) + PlatformError::CryptoError(String::from("pin/uv crypto operation failed")) })?; // Let iv be a 16-byte, random bytestring. @@ -405,7 +403,9 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo { // (No padding is performed as the size of demPlaintext is required to be a multiple of the AES block length.) let Ok(enc) = Aes256CbcEncryptor::new_from_slices(key, &iv) else { error!(?key, "Invalid key for AES-256 encryption"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; let ct = enc.encrypt_padded_vec_mut::(plaintext); @@ -415,20 +415,22 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo { Ok(out) } - fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, Error> { + fn decrypt(&self, key: &[u8], ciphertext: &[u8]) -> Result, PlatformError> { // Discard the first 32 bytes of key. (This selects the AES-key portion of the shared secret.) let key = key.get(32..).ok_or_else(|| { error!( key_len = key.len(), "key shorter than 32 bytes; cannot select AES-key portion" ); - Error::Ctap(CtapError::Other) + PlatformError::CryptoError(String::from("pin/uv crypto operation failed")) })?; // If demPlaintext is less than 16 bytes in length, return an error if ciphertext.len() < 16 { error!({ len = ciphertext.len() }, "Invalid length for ciphertext"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; // Split demPlaintext after the 16th byte to produce two subspans, iv and ct. @@ -437,16 +439,20 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo { // Return the AES-256-CBC decryption of ct using key and iv. let Ok(dec) = Aes256CbcDecryptor::new_from_slices(key, iv) else { error!(?key, "Invalid key for AES-256 decryption"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; let Ok(plaintext) = dec.decrypt_padded_vec_mut::(ciphertext) else { error!("Unpad error while decrypting"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::CryptoError(String::from( + "pin/uv crypto operation failed", + ))); }; Ok(plaintext) } - fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, Error> { + fn authenticate(&self, key: &[u8], message: &[u8]) -> Result, PlatformError> { // If key is longer than 32 bytes, discard the excess. (This selects the HMAC-key portion of the shared secret. // When key is the pinUvAuthToken, it is exactly 32 bytes long and thus this step has no effect.) let key = key.get(..32).ok_or_else(|| { @@ -454,7 +460,7 @@ impl PinUvAuthProtocol for PinUvAuthProtocolTwo { key_len = key.len(), "key shorter than 32 bytes; cannot select HMAC-key portion" ); - Error::Ctap(CtapError::Other) + PlatformError::CryptoError(String::from("pin/uv crypto operation failed")) })?; // Return the result of computing HMAC-SHA-256 on key and message. @@ -471,23 +477,21 @@ pub fn pin_hash(pin: &[u8]) -> Vec { hashed.into_iter().take(16).collect() } -pub fn hmac_sha256(key: &[u8], message: &[u8]) -> Result, Error> { +pub fn hmac_sha256(key: &[u8], message: &[u8]) -> Result, PlatformError> { let mut hmac = HmacSha256::new_from_slice(key).map_err(|e| { error!("HMAC key error: {e}"); - Error::Platform(PlatformError::CryptoError(format!("HMAC key error: {e}"))) + PlatformError::CryptoError(format!("HMAC key error: {e}")) })?; hmac.update(message); Ok(hmac.finalize().into_bytes().to_vec()) } -pub fn hkdf_sha256(salt: Option<&[u8]>, ikm: &[u8], info: &[u8]) -> Result, Error> { +pub fn hkdf_sha256(salt: Option<&[u8]>, ikm: &[u8], info: &[u8]) -> Result, PlatformError> { let hk = Hkdf::::new(salt, ikm); let mut okm = [0u8; 32]; // fixed L = 32 hk.expand(info, &mut okm).map_err(|e| { error!("HKDF expand error: {e}"); - Error::Platform(PlatformError::CryptoError(format!( - "HKDF expand error: {e}" - ))) + PlatformError::CryptoError(format!("HKDF expand error: {e}")) })?; Ok(Vec::from(okm)) } @@ -498,13 +502,13 @@ pub(crate) mod internal { use super::*; #[async_trait] - pub trait PinManagementInternal { + pub trait PinManagementInternal: Channel { async fn change_pin_internal( &mut self, get_info_response: &Ctap2GetInfoResponse, new_pin: String, timeout: Duration, - ) -> Result<(), Error>; + ) -> Result<(), WebAuthnError>; } #[async_trait] @@ -517,7 +521,7 @@ pub(crate) mod internal { get_info_response: &Ctap2GetInfoResponse, new_pin: String, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), WebAuthnError> { // CTAP 2.1 sends the PIN as UTF-8 in Unicode Normalization Form C. let new_pin = ComposingNormalizerBorrowed::new_nfc() .normalize(&new_pin) @@ -526,12 +530,12 @@ pub(crate) mod internal { // If the minPINLength member of the authenticatorGetInfo response is absent, then let platformMinPINLengthInCodePoints be 4. if new_pin.chars().count() < get_info_response.min_pin_length.unwrap_or(4) as usize { // If platformCollectedPinLengthInCodePoints is less than platformMinPINLengthInCodePoints then the platform SHOULD display a "PIN too short" error message to the user. - return Err(Error::Platform(PlatformError::PinTooShort)); + return Err(WebAuthnError::Platform(PlatformError::PinTooShort)); } // If the byte length of "newPin" is greater than the max UTF-8 representation limit of 63 bytes, then the platform SHOULD display a "PIN too long" error message to the user. if new_pin.len() >= 64 { - return Err(Error::Platform(PlatformError::PinTooLong)); + return Err(WebAuthnError::Platform(PlatformError::PinTooLong)); } // A successful PIN set/change invalidates this authenticator's persistent token @@ -552,13 +556,15 @@ pub(crate) mod internal { .await else { error!("No supported PIN/UV auth protocols found"); - return Err(Error::Ctap(CtapError::Other)); + return Err(WebAuthnError::Ctap(CtapError::Other)); }; let current_pin = match get_info_response .options .as_ref() - .ok_or(Error::Platform(PlatformError::InvalidDeviceResponse))? + .ok_or(WebAuthnError::Platform( + PlatformError::InvalidDeviceResponse, + ))? .get("clientPin") { // Obtaining the current PIN, if one is set @@ -578,7 +584,7 @@ pub(crate) mod internal { // Device does not support PIN None => { - return Err(Error::Platform(PlatformError::PinNotSupported)); + return Err(WebAuthnError::Platform(PlatformError::PinNotSupported)); } }; @@ -645,7 +651,11 @@ use internal::PinManagementInternal; #[async_trait] pub trait PinManagement: PinManagementInternal { - async fn change_pin(&mut self, new_pin: String, timeout: Duration) -> Result<(), Error>; + async fn change_pin( + &mut self, + new_pin: String, + timeout: Duration, + ) -> Result<(), WebAuthnError>; } #[async_trait] @@ -653,7 +663,11 @@ impl PinManagement for C where C: Channel, { - async fn change_pin(&mut self, new_pin: String, timeout: Duration) -> Result<(), Error> { + async fn change_pin( + &mut self, + new_pin: String, + timeout: Duration, + ) -> Result<(), WebAuthnError> { let get_info_response = self.ctap2_get_info().await?; self.change_pin_internal(&get_info_response, new_pin, timeout) .await @@ -680,7 +694,7 @@ mod tests { let key = make_peer_key(&x, &y); let result = PinUvAuthProtocol::encapsulate(&proto, &key); - assert!(matches!(result, Err(Error::Ctap(CtapError::Other)))); + assert!(matches!(result, Err(PlatformError::CryptoError(_)))); } #[test] @@ -691,7 +705,7 @@ mod tests { let key = make_peer_key(&x, &y); let result = PinUvAuthProtocol::encapsulate(&proto, &key); - assert!(matches!(result, Err(Error::Ctap(CtapError::Other)))); + assert!(matches!(result, Err(PlatformError::CryptoError(_)))); } #[test] @@ -702,14 +716,14 @@ mod tests { let key = make_peer_key(&x, &y); let result = PinUvAuthProtocol::encapsulate(&proto, &key); - assert!(matches!(result, Err(Error::Ctap(CtapError::Other)))); + assert!(matches!(result, Err(PlatformError::CryptoError(_)))); } #[test] fn proto_two_authenticate_rejects_empty_key() { let proto = PinUvAuthProtocolTwo::new(); let result = proto.authenticate(&[], b"clientDataHash"); - assert!(matches!(result, Err(Error::Ctap(CtapError::Other)))); + assert!(matches!(result, Err(PlatformError::CryptoError(_)))); } #[test] @@ -717,7 +731,7 @@ mod tests { let proto = PinUvAuthProtocolTwo::new(); let short_key = [0u8; 16]; let result = proto.authenticate(&short_key, b"hello"); - assert!(matches!(result, Err(Error::Ctap(CtapError::Other)))); + assert!(matches!(result, Err(PlatformError::CryptoError(_)))); } #[test] @@ -726,7 +740,7 @@ mod tests { let short_key = [0u8; 16]; let plaintext = [0u8; 16]; let result = proto.encrypt(&short_key, &plaintext); - assert!(matches!(result, Err(Error::Ctap(CtapError::Other)))); + assert!(matches!(result, Err(PlatformError::CryptoError(_)))); } #[test] @@ -735,6 +749,6 @@ mod tests { let short_key = [0u8; 16]; let ct = [0u8; 32]; let result = proto.decrypt(&short_key, &ct); - assert!(matches!(result, Err(Error::Ctap(CtapError::Other)))); + assert!(matches!(result, Err(PlatformError::CryptoError(_)))); } } diff --git a/libwebauthn/src/pin/persistent_token.rs b/libwebauthn/src/pin/persistent_token.rs index ba6158d5..d5bf2d2a 100644 --- a/libwebauthn/src/pin/persistent_token.rs +++ b/libwebauthn/src/pin/persistent_token.rs @@ -14,8 +14,7 @@ use tracing::{debug, error, trace, warn}; use zeroize::ZeroizeOnDrop; use crate::proto::ctap2::{Ctap2GetInfoResponse, Ctap2PinUvAuthProtocol}; -use crate::proto::CtapError; -use crate::webauthn::error::{Error, PlatformError}; +use crate::webauthn::error::PlatformError; type Aes128CbcDecryptor = cbc::Decryptor; @@ -126,15 +125,13 @@ impl PersistentTokenStore for MemoryPersistentTokenStore { /// Derive the 16-byte AES-128 key for `encIdentifier` from a persistent token, per /// CTAP 2.3-PS 6.4: `HKDF-SHA-256(salt = 32 zero bytes, IKM = token, L = 16, info = "encIdentifier")`. -fn enc_identifier_key(token: &[u8]) -> Result<[u8; 16], Error> { +fn enc_identifier_key(token: &[u8]) -> Result<[u8; 16], PlatformError> { let hkdf = Hkdf::::new(Some(&ENC_IDENTIFIER_HKDF_SALT), token); let mut key = [0u8; 16]; hkdf.expand(ENC_IDENTIFIER_HKDF_INFO, &mut key) .map_err(|e| { error!("HKDF expand error deriving encIdentifier key: {e}"); - Error::Platform(PlatformError::CryptoError(format!( - "HKDF expand error: {e}" - ))) + PlatformError::CryptoError(format!("HKDF expand error: {e}")) })?; Ok(key) } @@ -144,27 +141,27 @@ fn enc_identifier_key(token: &[u8]) -> Result<[u8; 16], Error> { pub(crate) fn decrypt_enc_identifier( token: &[u8], enc_identifier: &[u8], -) -> Result<[u8; 16], Error> { +) -> Result<[u8; 16], PlatformError> { if enc_identifier.len() != 32 { error!( len = enc_identifier.len(), "encIdentifier is not a 16-byte IV followed by one 16-byte ciphertext block" ); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::InvalidDeviceResponse); } let (iv, ciphertext) = enc_identifier.split_at(16); let key = enc_identifier_key(token)?; let Ok(decryptor) = Aes128CbcDecryptor::new_from_slices(&key, iv) else { error!("Invalid key or IV for AES-128-CBC encIdentifier decryption"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::InvalidDeviceResponse); }; let Ok(plaintext) = decryptor.decrypt_padded_vec_mut::(ciphertext) else { error!("Decrypt error while recovering device identifier"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::InvalidDeviceResponse); }; plaintext.try_into().map_err(|_| { error!("Recovered device identifier was not 16 bytes"); - Error::Ctap(CtapError::Other) + PlatformError::InvalidDeviceResponse }) } @@ -206,15 +203,15 @@ pub(crate) async fn store_minted_token( info: &Ctap2GetInfoResponse, token: &[u8], pin_uv_auth_protocol: Ctap2PinUvAuthProtocol, -) -> Result { +) -> Result { let Some(enc_identifier) = info.enc_identifier.as_ref() else { warn!("perCredMgmtRO advertised but no encIdentifier returned; cannot persist token"); - return Err(Error::Ctap(CtapError::Other)); + return Err(PlatformError::InvalidDeviceResponse); }; let device_identifier = decrypt_enc_identifier(token, enc_identifier)?; let aaguid: [u8; 16] = info.aaguid[..].try_into().map_err(|_| { error!(len = info.aaguid.len(), "AAGUID was not 16 bytes"); - Error::Ctap(CtapError::Other) + PlatformError::InvalidDeviceResponse })?; reap_superseded_records(store, &device_identifier).await; let id = new_record_id(); diff --git a/libwebauthn/src/proto/ctap1/protocol.rs b/libwebauthn/src/proto/ctap1/protocol.rs index f4c913b0..ee621d7d 100644 --- a/libwebauthn/src/proto/ctap1/protocol.rs +++ b/libwebauthn/src/proto/ctap1/protocol.rs @@ -12,21 +12,26 @@ use super::{ }; use crate::proto::ctap1::model::Preflight; use crate::proto::CtapError; -use crate::transport::{error::TransportError, Channel}; -use crate::webauthn::error::Error; +use crate::transport::Channel; +use crate::webauthn::error::{PlatformError, WebAuthnError}; use crate::UvUpdate; const UP_SLEEP: Duration = Duration::from_millis(150); const VERSION_TIMEOUT: Duration = Duration::from_millis(500); #[async_trait] -pub trait Ctap1 { - async fn ctap1_version(&mut self) -> Result; +pub trait Ctap1: Channel { + async fn ctap1_version( + &mut self, + ) -> Result>; async fn ctap1_register( &mut self, op: &Ctap1RegisterRequest, - ) -> Result; - async fn ctap1_sign(&mut self, op: &Ctap1SignRequest) -> Result; + ) -> Result>; + async fn ctap1_sign( + &mut self, + op: &Ctap1SignRequest, + ) -> Result>; } #[async_trait] @@ -35,11 +40,18 @@ where C: Channel, { #[instrument(skip_all)] - async fn ctap1_version(&mut self) -> Result { + async fn ctap1_version( + &mut self, + ) -> Result> { let request = &Ctap1VersionRequest::new(); let apdu_request: ApduRequest = request.into(); - self.apdu_send(&apdu_request, VERSION_TIMEOUT).await?; - let apdu_response = self.apdu_recv(VERSION_TIMEOUT).await?; + self.apdu_send(&apdu_request, VERSION_TIMEOUT) + .await + .map_err(WebAuthnError::Transport)?; + let apdu_response = self + .apdu_recv(VERSION_TIMEOUT) + .await + .map_err(WebAuthnError::Transport)?; let response: Ctap1VersionResponse = apdu_response.try_into().or(Err(CtapError::Other))?; debug!({ ?response.version }, "CTAP1 version response"); Ok(response) @@ -49,7 +61,7 @@ where async fn ctap1_register( &mut self, request: &Ctap1RegisterRequest, - ) -> Result { + ) -> Result> { debug!({ %request.require_user_presence }, "CTAP1 register request"); trace!(?request); self.send_ux_update(UvUpdate::PresenceRequired.into()).await; @@ -62,9 +74,9 @@ where match self.ctap1_sign(preflight).await { Ok(_) => { info!("Already-registered credential found during preflight request."); - return Err(Error::Ctap(CtapError::CredentialExcluded)); + return Err(WebAuthnError::Ctap(CtapError::CredentialExcluded)); } - Err(Error::Ctap(CtapError::NoCredentials)) => { + Err(WebAuthnError::Ctap(CtapError::NoCredentials)) => { debug!("Credential doesn't already exist, continuing."); } Err(err) => { @@ -78,7 +90,7 @@ where let status = apdu_response.status().or(Err(CtapError::Other))?; if status != ApduResponseStatus::NoError { error!(?status, "APDU response has error code"); - return Err(Error::Ctap(CtapError::from(status))); + return Err(WebAuthnError::Ctap(CtapError::from(status))); } let response: Ctap1RegisterResponse = apdu_response.try_into().or(Err(CtapError::Other))?; @@ -88,7 +100,10 @@ where } #[instrument(skip_all, fields(preflight = !request.require_user_presence))] - async fn ctap1_sign(&mut self, request: &Ctap1SignRequest) -> Result { + async fn ctap1_sign( + &mut self, + request: &Ctap1SignRequest, + ) -> Result> { debug!({ %request.require_user_presence }, "CTAP1 sign request"); trace!(?request); self.send_ux_update(UvUpdate::PresenceRequired.into()).await; @@ -98,7 +113,7 @@ where let status = apdu_response.status().or(Err(CtapError::Other))?; if status != ApduResponseStatus::NoError { error!(?status, "APDU response has error code"); - return Err(Error::Ctap(CtapError::from(status))); + return Err(WebAuthnError::Ctap(CtapError::from(status))); } let response: Ctap1SignResponse = apdu_response.try_into().or(Err(CtapError::Other))?; @@ -112,24 +127,30 @@ async fn send_apdu_request_wait_uv( channel: &mut C, request: &ApduRequest, timeout: Duration, -) -> Result { +) -> Result> { tokio_timeout(timeout, async { loop { - channel.apdu_send(request, timeout).await?; - let apdu_response = channel.apdu_recv(timeout).await?; - let apdu_status = apdu_response - .status() - .or(Err(Error::Transport(TransportError::InvalidFraming)))?; + channel + .apdu_send(request, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let apdu_response = channel + .apdu_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; + let apdu_status = apdu_response.status().or(Err(WebAuthnError::Platform( + PlatformError::InvalidDeviceResponse, + )))?; let ctap_error: CtapError = apdu_status.into(); match ctap_error { CtapError::Ok => return Ok(apdu_response), CtapError::UserPresenceRequired => (), // Sleep some more. - _ => return Err(Error::Ctap(ctap_error)), + _ => return Err(WebAuthnError::Ctap(ctap_error)), }; debug!("UP required. Sleeping for {:?}.", UP_SLEEP); sleep(UP_SLEEP).await; } }) .await - .or(Err(Error::Ctap(CtapError::UserActionTimeout)))? + .or(Err(WebAuthnError::Ctap(CtapError::UserActionTimeout)))? } diff --git a/libwebauthn/src/proto/ctap2/cbor/request.rs b/libwebauthn/src/proto/ctap2/cbor/request.rs index 0d4bf2c5..8e380e55 100644 --- a/libwebauthn/src/proto/ctap2/cbor/request.rs +++ b/libwebauthn/src/proto/ctap2/cbor/request.rs @@ -1,6 +1,7 @@ use std::io::Error as IOError; use crate::proto::ctap2::cbor; +use crate::proto::ctap2::cbor::CborError; use crate::proto::ctap2::model::Ctap2ClientPinRequest; use crate::proto::ctap2::model::Ctap2CommandCode; use crate::proto::ctap2::model::Ctap2GetAssertionRequest; @@ -9,7 +10,6 @@ use crate::proto::ctap2::Ctap2AuthenticatorConfigRequest; use crate::proto::ctap2::Ctap2BioEnrollmentRequest; use crate::proto::ctap2::Ctap2CredentialManagementRequest; use crate::proto::ctap2::Ctap2LargeBlobsRequest; -use crate::webauthn::Error; #[derive(Debug, Clone, PartialEq)] pub struct CborRequest { @@ -39,8 +39,8 @@ impl CborRequest { } impl TryFrom<&Ctap2MakeCredentialRequest> for CborRequest { - type Error = Error; - fn try_from(request: &Ctap2MakeCredentialRequest) -> Result { + type Error = CborError; + fn try_from(request: &Ctap2MakeCredentialRequest) -> Result { Ok(CborRequest { command: Ctap2CommandCode::AuthenticatorMakeCredential, encoded_data: cbor::to_vec(&request)?, @@ -49,8 +49,8 @@ impl TryFrom<&Ctap2MakeCredentialRequest> for CborRequest { } impl TryFrom<&Ctap2GetAssertionRequest> for CborRequest { - type Error = Error; - fn try_from(request: &Ctap2GetAssertionRequest) -> Result { + type Error = CborError; + fn try_from(request: &Ctap2GetAssertionRequest) -> Result { Ok(CborRequest { command: Ctap2CommandCode::AuthenticatorGetAssertion, encoded_data: cbor::to_vec(&request)?, @@ -59,8 +59,8 @@ impl TryFrom<&Ctap2GetAssertionRequest> for CborRequest { } impl TryFrom<&Ctap2ClientPinRequest> for CborRequest { - type Error = Error; - fn try_from(request: &Ctap2ClientPinRequest) -> Result { + type Error = CborError; + fn try_from(request: &Ctap2ClientPinRequest) -> Result { Ok(CborRequest { command: Ctap2CommandCode::AuthenticatorClientPin, encoded_data: cbor::to_vec(&request)?, @@ -69,8 +69,8 @@ impl TryFrom<&Ctap2ClientPinRequest> for CborRequest { } impl TryFrom<&Ctap2AuthenticatorConfigRequest> for CborRequest { - type Error = Error; - fn try_from(request: &Ctap2AuthenticatorConfigRequest) -> Result { + type Error = CborError; + fn try_from(request: &Ctap2AuthenticatorConfigRequest) -> Result { Ok(CborRequest { command: Ctap2CommandCode::AuthenticatorConfig, encoded_data: cbor::to_vec(&request)?, @@ -79,8 +79,8 @@ impl TryFrom<&Ctap2AuthenticatorConfigRequest> for CborRequest { } impl TryFrom<&Ctap2BioEnrollmentRequest> for CborRequest { - type Error = Error; - fn try_from(request: &Ctap2BioEnrollmentRequest) -> Result { + type Error = CborError; + fn try_from(request: &Ctap2BioEnrollmentRequest) -> Result { let command = if request.use_legacy_preview { Ctap2CommandCode::AuthenticatorBioEnrollmentPreview } else { @@ -94,8 +94,8 @@ impl TryFrom<&Ctap2BioEnrollmentRequest> for CborRequest { } impl TryFrom<&Ctap2CredentialManagementRequest> for CborRequest { - type Error = Error; - fn try_from(request: &Ctap2CredentialManagementRequest) -> Result { + type Error = CborError; + fn try_from(request: &Ctap2CredentialManagementRequest) -> Result { let command = if request.use_legacy_preview { Ctap2CommandCode::AuthenticatorCredentialManagementPreview } else { @@ -109,8 +109,8 @@ impl TryFrom<&Ctap2CredentialManagementRequest> for CborRequest { } impl TryFrom<&Ctap2LargeBlobsRequest> for CborRequest { - type Error = Error; - fn try_from(request: &Ctap2LargeBlobsRequest) -> Result { + type Error = CborError; + fn try_from(request: &Ctap2LargeBlobsRequest) -> Result { Ok(CborRequest { command: Ctap2CommandCode::AuthenticatorLargeBlobs, encoded_data: cbor::to_vec(&request)?, diff --git a/libwebauthn/src/proto/ctap2/cose.rs b/libwebauthn/src/proto/ctap2/cose.rs index a5b5c38d..f86c5605 100644 --- a/libwebauthn/src/proto/ctap2/cose.rs +++ b/libwebauthn/src/proto/ctap2/cose.rs @@ -16,7 +16,7 @@ use spki::{AlgorithmIdentifierOwned, ObjectIdentifier, SubjectPublicKeyInfoOwned use tracing::warn; use crate::proto::ctap2::Ctap2COSEAlgorithmIdentifier; -use crate::webauthn::{Error, PlatformError}; +use crate::webauthn::PlatformError; /// COSE Key Common Parameters ([RFC 9052] §7.1). const COSE_KEY_LABEL_KTY: i128 = 1; @@ -72,53 +72,53 @@ impl<'de> Deserialize<'de> for CoseEncodedKey { type CoseMap = std::collections::BTreeMap; -fn parse_cose_map(bytes: &[u8]) -> Result { +fn parse_cose_map(bytes: &[u8]) -> Result { let value: Value = serde_cbor_2::from_slice(bytes).map_err(|e| { warn!(%e, "failed to parse COSE_Key as CBOR"); - Error::Platform(PlatformError::InvalidDeviceResponse) + PlatformError::InvalidDeviceResponse })?; match value { Value::Map(map) => Ok(map), _ => { warn!("COSE_Key is not a CBOR map"); - Err(Error::Platform(PlatformError::InvalidDeviceResponse)) + Err(PlatformError::InvalidDeviceResponse) } } } -fn map_integer(map: &CoseMap, label: i128) -> Result { +fn map_integer(map: &CoseMap, label: i128) -> Result { match map.get(&Value::Integer(label)) { Some(Value::Integer(i)) => Ok(*i), Some(_) => { warn!(label, "COSE_Key field is not an integer"); - Err(Error::Platform(PlatformError::InvalidDeviceResponse)) + Err(PlatformError::InvalidDeviceResponse) } None => { warn!(label, "COSE_Key missing required field"); - Err(Error::Platform(PlatformError::InvalidDeviceResponse)) + Err(PlatformError::InvalidDeviceResponse) } } } -fn map_bytes(map: &CoseMap, label: i128) -> Result<&[u8], Error> { +fn map_bytes(map: &CoseMap, label: i128) -> Result<&[u8], PlatformError> { match map.get(&Value::Integer(label)) { Some(Value::Bytes(b)) => Ok(b.as_slice()), Some(_) => { warn!(label, "COSE_Key field is not a byte string"); - Err(Error::Platform(PlatformError::InvalidDeviceResponse)) + Err(PlatformError::InvalidDeviceResponse) } None => { warn!(label, "COSE_Key missing required field"); - Err(Error::Platform(PlatformError::InvalidDeviceResponse)) + Err(PlatformError::InvalidDeviceResponse) } } } -fn read_alg_from_map(map: &CoseMap) -> Result { +fn read_alg_from_map(map: &CoseMap) -> Result { let alg_i128 = map_integer(map, COSE_KEY_LABEL_ALG)?; let alg_i32 = i32::try_from(alg_i128).map_err(|_| { warn!(alg = %alg_i128, "COSE_Key `alg` outside i32 range"); - Error::Platform(PlatformError::InvalidDeviceResponse) + PlatformError::InvalidDeviceResponse })?; Ok(Ctap2COSEAlgorithmIdentifier(alg_i32)) } @@ -128,7 +128,7 @@ fn read_alg_from_map(map: &CoseMap) -> Result Result { +pub(crate) fn read_alg(bytes: &[u8]) -> Result { let map = parse_cose_map(bytes)?; read_alg_from_map(&map) } @@ -145,7 +145,7 @@ pub(crate) fn read_alg(bytes: &[u8]) -> Result Result>, Error> { +pub fn to_spki(bytes: &[u8]) -> Result>, PlatformError> { let map = parse_cose_map(bytes)?; let alg = read_alg_from_map(&map)?; @@ -173,32 +173,32 @@ pub fn to_spki(bytes: &[u8]) -> Result>, Error> { } } -fn require_kty(map: &CoseMap, expected: i128) -> Result<(), Error> { +fn require_kty(map: &CoseMap, expected: i128) -> Result<(), PlatformError> { let kty = map_integer(map, COSE_KEY_LABEL_KTY)?; if kty != expected { warn!(expected, got = kty, "COSE_Key kty mismatch"); - return Err(Error::Platform(PlatformError::InvalidDeviceResponse)); + return Err(PlatformError::InvalidDeviceResponse); } Ok(()) } -fn require_crv(map: &CoseMap, expected: i128) -> Result<(), Error> { +fn require_crv(map: &CoseMap, expected: i128) -> Result<(), PlatformError> { let crv = map_integer(map, COSE_KEY_LABEL_CRV)?; if crv != expected { warn!(expected, got = crv, "COSE_Key crv mismatch"); - return Err(Error::Platform(PlatformError::InvalidDeviceResponse)); + return Err(PlatformError::InvalidDeviceResponse); } Ok(()) } -fn p256_spki(x: &[u8], y: &[u8]) -> Result, Error> { +fn p256_spki(x: &[u8], y: &[u8]) -> Result, PlatformError> { if x.len() != 32 || y.len() != 32 { warn!( x_len = x.len(), y_len = y.len(), "P-256 coordinates wrong size" ); - return Err(Error::Platform(PlatformError::InvalidDeviceResponse)); + return Err(PlatformError::InvalidDeviceResponse); } let mut point = Vec::with_capacity(65); point.push(0x04); // uncompressed point indicator (SEC1) @@ -207,9 +207,9 @@ fn p256_spki(x: &[u8], y: &[u8]) -> Result, Error> { let curve_oid_der = OID_SECP256R1 .to_der() - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?; - let parameters = der::Any::from_der(&curve_oid_der) - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?; + .map_err(|_| PlatformError::InvalidDeviceResponse)?; + let parameters = + der::Any::from_der(&curve_oid_der).map_err(|_| PlatformError::InvalidDeviceResponse)?; let spki = SubjectPublicKeyInfoOwned { algorithm: AlgorithmIdentifierOwned { @@ -217,17 +217,17 @@ fn p256_spki(x: &[u8], y: &[u8]) -> Result, Error> { parameters: Some(parameters), }, subject_public_key: BitString::from_bytes(&point) - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?, + .map_err(|_| PlatformError::InvalidDeviceResponse)?, }; spki.to_der() - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse)) + .map_err(|_| PlatformError::InvalidDeviceResponse) } -fn ed25519_spki(x: &[u8]) -> Result, Error> { +fn ed25519_spki(x: &[u8]) -> Result, PlatformError> { if x.len() != 32 { warn!(len = x.len(), "Ed25519 public key wrong size"); - return Err(Error::Platform(PlatformError::InvalidDeviceResponse)); + return Err(PlatformError::InvalidDeviceResponse); } let spki = SubjectPublicKeyInfoOwned { @@ -236,11 +236,11 @@ fn ed25519_spki(x: &[u8]) -> Result, Error> { parameters: None, }, subject_public_key: BitString::from_bytes(x) - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?, + .map_err(|_| PlatformError::InvalidDeviceResponse)?, }; spki.to_der() - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse)) + .map_err(|_| PlatformError::InvalidDeviceResponse) } #[derive(Sequence)] @@ -249,22 +249,21 @@ struct Pkcs1RsaPublicKey<'a> { public_exponent: der::asn1::UintRef<'a>, } -fn rs256_spki(n: &[u8], e: &[u8]) -> Result, Error> { +fn rs256_spki(n: &[u8], e: &[u8]) -> Result, PlatformError> { let inner = Pkcs1RsaPublicKey { - modulus: der::asn1::UintRef::new(n) - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?, + modulus: der::asn1::UintRef::new(n).map_err(|_| PlatformError::InvalidDeviceResponse)?, public_exponent: der::asn1::UintRef::new(e) - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?, + .map_err(|_| PlatformError::InvalidDeviceResponse)?, }; let inner_der = inner .to_der() - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?; + .map_err(|_| PlatformError::InvalidDeviceResponse)?; let null_der = Null .to_der() - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?; - let parameters = der::Any::from_der(&null_der) - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?; + .map_err(|_| PlatformError::InvalidDeviceResponse)?; + let parameters = + der::Any::from_der(&null_der).map_err(|_| PlatformError::InvalidDeviceResponse)?; let spki = SubjectPublicKeyInfoOwned { algorithm: AlgorithmIdentifierOwned { @@ -272,11 +271,11 @@ fn rs256_spki(n: &[u8], e: &[u8]) -> Result, Error> { parameters: Some(parameters), }, subject_public_key: BitString::from_bytes(&inner_der) - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse))?, + .map_err(|_| PlatformError::InvalidDeviceResponse)?, }; spki.to_der() - .map_err(|_| Error::Platform(PlatformError::InvalidDeviceResponse)) + .map_err(|_| PlatformError::InvalidDeviceResponse) } #[cfg(test)] diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index 3fde3cf0..f09a9b3d 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -2,7 +2,7 @@ use crate::proto::ctap1::Ctap1Transport; use crate::Transport; use crate::{ ops::webauthn::idl::create::PublicKeyCredentialUserEntity, pin::PinUvAuthProtocol, - webauthn::Error, + webauthn::PlatformError, }; use num_enum::{IntoPrimitive, TryFromPrimitive}; @@ -382,7 +382,7 @@ pub trait Ctap2UserVerifiableRequest { &mut self, uv_proto: &dyn PinUvAuthProtocol, uv_auth_token: &[u8], - ) -> Result<(), Error>; + ) -> Result<(), PlatformError>; fn client_data_hash(&self) -> Option<&[u8]> { None } diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index b378f6d3..de0f746f 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -9,7 +9,7 @@ use crate::{ pin::PinUvAuthProtocol, proto::ctap2::cbor::{map_to_json_object, Value}, transport::AuthTokenData, - webauthn::{Error, PlatformError}, + webauthn::PlatformError, }; use super::{ @@ -150,7 +150,7 @@ impl Ctap2GetAssertionRequest { pub(crate) fn from_webauthn_request( req: &GetAssertionRequest, info: &Ctap2GetInfoResponse, - ) -> Result { + ) -> Result { // Cloning it, so we can modify it let mut req = req.clone(); // LargeBlob (NOTE: Not to be confused with LargeBlobKey) @@ -260,23 +260,23 @@ impl Ctap2GetAssertionRequestExtensions { fn convert_prf_to_native( &mut self, allow_list: &[Ctap2PublicKeyCredentialDescriptor], - ) -> Result<(), Error> { + ) -> Result<(), PlatformError> { let Some(GetAssertionHmacOrPrfInput::Prf(prf_input)) = &self.hmac_or_prf else { return Ok(()); }; // Same WebAuthn L3 §10.1.4 client checks as prf_to_hmac_input below. if !prf_input.eval_by_credential.is_empty() && allow_list.is_empty() { - return Err(Error::Platform(PlatformError::NotSupported)); + return Err(PlatformError::NotSupported); } let mut eval_by_credential = BTreeMap::new(); for (enc_cred_id, value) in &prf_input.eval_by_credential { if enc_cred_id.is_empty() { - return Err(Error::Platform(PlatformError::SyntaxError)); + return Err(PlatformError::SyntaxError); } - let cred_id = base64_url::decode(enc_cred_id) - .map_err(|_| Error::Platform(PlatformError::SyntaxError))?; + let cred_id = + base64_url::decode(enc_cred_id).map_err(|_| PlatformError::SyntaxError)?; // Entries not matching the allow list are skipped, mirroring prf_to_hmac_input. if allow_list.iter().any(|cred| cred.id == cred_id) { eval_by_credential.insert(ByteBuf::from(cred_id), Ctap2PrfSalts::from(value)); @@ -303,7 +303,7 @@ impl Ctap2GetAssertionRequestExtensions { &mut self, allow_list: &[Ctap2PublicKeyCredentialDescriptor], auth_data: &AuthTokenData, - ) -> Result<(), Error> { + ) -> Result<(), PlatformError> { let input = match &self.hmac_or_prf { None => None, Some(GetAssertionHmacOrPrfInput::HmacGetSecret(hmac_get_secret_input)) => { @@ -361,12 +361,12 @@ impl Ctap2GetAssertionRequestExtensions { eval: &Option, eval_by_credential: &HashMap, allow_list: &[Ctap2PublicKeyCredentialDescriptor], - ) -> Result, Error> { + ) -> Result, PlatformError> { // https://w3c.github.io/webauthn/#prf // // 1. If evalByCredential is not empty but allowCredentials is empty, return a DOMException whose name is “NotSupportedError”. if !eval_by_credential.is_empty() && allow_list.is_empty() { - return Err(Error::Platform(PlatformError::NotSupported)); + return Err(PlatformError::NotSupported); } // 4.0 Let ev be null, and try to find any applicable PRF input(s): @@ -374,10 +374,10 @@ impl Ctap2GetAssertionRequestExtensions { for (enc_cred_id, prf_value) in eval_by_credential { // 2. If any key in evalByCredential is the empty string, or is not a valid base64url encoding, or does not equal the id of some element of allowCredentials after performing base64url decoding, then return a DOMException whose name is “SyntaxError”. if enc_cred_id.is_empty() { - return Err(Error::Platform(PlatformError::SyntaxError)); + return Err(PlatformError::SyntaxError); } - let cred_id = base64_url::decode(enc_cred_id) - .map_err(|_| Error::Platform(PlatformError::SyntaxError))?; + let cred_id = + base64_url::decode(enc_cred_id).map_err(|_| PlatformError::SyntaxError)?; // 4.1 If evalByCredential is present and contains an entry whose key is the base64url encoding of the credential ID that will be returned, let ev be the value of that entry. let found_cred_id = allow_list.iter().find(|x| x.id == cred_id); @@ -558,10 +558,10 @@ impl Ctap2UserVerifiableRequest for Ctap2GetAssertionRequest { &mut self, uv_proto: &dyn PinUvAuthProtocol, uv_auth_token: &[u8], - ) -> Result<(), Error> { + ) -> Result<(), PlatformError> { let hash = self .client_data_hash() - .ok_or(Error::Platform(PlatformError::InvalidDeviceResponse))?; + .ok_or(PlatformError::InvalidDeviceResponse)?; let uv_auth_param = uv_proto.authenticate(uv_auth_token, hash)?; self.pin_auth_proto = Some(uv_proto.version() as u32); self.pin_auth_param = Some(ByteBuf::from(uv_auth_param)); @@ -1160,7 +1160,7 @@ mod tests { let request = prf_request(vec![cred.clone()], None, by_cred); let result = Ctap2GetAssertionRequest::from_webauthn_request(&request, &info); assert!( - matches!(result, Err(Error::Platform(PlatformError::SyntaxError))), + matches!(result, Err(PlatformError::SyntaxError)), "key {bad_key:?}" ); } @@ -1180,10 +1180,7 @@ mod tests { let info = info_with_extensions(&["prf"]); let result = Ctap2GetAssertionRequest::from_webauthn_request(&request, &info); - assert!(matches!( - result, - Err(Error::Platform(PlatformError::NotSupported)) - )); + assert!(matches!(result, Err(PlatformError::NotSupported))); } #[test] diff --git a/libwebauthn/src/proto/ctap2/model/make_credential.rs b/libwebauthn/src/proto/ctap2/model/make_credential.rs index 429b94ea..63ad7d37 100644 --- a/libwebauthn/src/proto/ctap2/model/make_credential.rs +++ b/libwebauthn/src/proto/ctap2/model/make_credential.rs @@ -15,7 +15,7 @@ use crate::{ pin::PinUvAuthProtocol, proto::{ctap2::cbor::Value, CtapError}, transport::AuthTokenData, - webauthn::{Error, PlatformError}, + webauthn::{PlatformError, WebAuthnError}, }; use ctap_types::ctap2::credential_management::CredentialProtectionPolicy as Ctap2CredentialProtectionPolicy; use serde::{Deserialize, Serialize}; @@ -123,10 +123,10 @@ impl Ctap2MakeCredentialRequest { .is_none_or(|extensions| extensions.skip_serializing()) } - pub(crate) fn from_webauthn_request( + pub(crate) fn from_webauthn_request( req: &MakeCredentialRequest, info: &Ctap2GetInfoResponse, - ) -> Result { + ) -> Result> { // Checking if extensions can be fulfilled let extensions = match &req.extensions { Some(ext) => { @@ -223,10 +223,10 @@ pub struct Ctap2PrfMakeCredentialInput { } impl Ctap2MakeCredentialsRequestExtensions { - fn from_webauthn_request( + fn from_webauthn_request( requested_extensions: &MakeCredentialsRequestExtensions, info: &Ctap2GetInfoResponse, - ) -> Result { + ) -> Result> { // CredProtection // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#credProtectFeatureDetection // When enforceCredentialProtectionPolicy is true, and credentialProtectionPolicy's value @@ -239,7 +239,7 @@ impl Ctap2MakeCredentialsRequestExtensions { && cred_protection.policy != CredentialProtectionPolicy::UserVerificationOptional && !info.supports_extension("credProtect") { - return Err(Error::Ctap(CtapError::UnsupportedExtension)); + return Err(WebAuthnError::Ctap(CtapError::UnsupportedExtension)); } } @@ -254,7 +254,7 @@ impl Ctap2MakeCredentialsRequestExtensions { Some(MakeCredentialLargeBlobExtension::Required) => { // Required + unsupported must fail rather than silently degrade. if !info.option_enabled("largeBlobs") { - return Err(Error::Ctap(CtapError::UnsupportedExtension)); + return Err(WebAuthnError::Ctap(CtapError::UnsupportedExtension)); } Some(true) } @@ -322,7 +322,10 @@ impl Ctap2MakeCredentialsRequestExtensions { } /// Encrypts the buffered PRF input with the channel's shared secret; CTAP 2.2 § 12.8. - pub fn calculate_hmac_secret_mc(&mut self, auth_data: &AuthTokenData) -> Result<(), Error> { + pub fn calculate_hmac_secret_mc( + &mut self, + auth_data: &AuthTokenData, + ) -> Result<(), PlatformError> { let Some(prf_input) = self.prf_input.take() else { return Ok(()); }; @@ -423,10 +426,10 @@ impl Ctap2UserVerifiableRequest for Ctap2MakeCredentialRequest { &mut self, uv_proto: &dyn PinUvAuthProtocol, uv_auth_token: &[u8], - ) -> Result<(), Error> { + ) -> Result<(), PlatformError> { let hash = self .client_data_hash() - .ok_or(Error::Platform(PlatformError::InvalidDeviceResponse))?; + .ok_or(PlatformError::InvalidDeviceResponse)?; let uv_auth_param = uv_proto.authenticate(uv_auth_token, hash)?; self.pin_auth_proto = Some(uv_proto.version() as u32); self.pin_auth_param = Some(ByteBuf::from(uv_auth_param)); @@ -504,6 +507,7 @@ mod tests { CredentialProtectionExtension, MakeCredentialPrfInput, MakeCredentialRequest, }; use std::collections::HashMap; + use std::convert::Infallible; use std::time::Duration; fn info_with_options(options: &[(&str, bool)]) -> Ctap2GetInfoResponse { @@ -526,11 +530,12 @@ mod tests { ..MakeCredentialsRequestExtensions::default() }; - let result = - Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info); + let result = Ctap2MakeCredentialsRequestExtensions::from_webauthn_request::( + &requested, &info, + ); assert!(matches!( result, - Err(Error::Ctap(CtapError::UnsupportedExtension)) + Err(WebAuthnError::Ctap(CtapError::UnsupportedExtension)) )); } @@ -545,11 +550,12 @@ mod tests { ..MakeCredentialsRequestExtensions::default() }; - let result = - Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info); + let result = Ctap2MakeCredentialsRequestExtensions::from_webauthn_request::( + &requested, &info, + ); assert!(matches!( result, - Err(Error::Ctap(CtapError::UnsupportedExtension)) + Err(WebAuthnError::Ctap(CtapError::UnsupportedExtension)) )); } @@ -564,8 +570,10 @@ mod tests { }; let extensions = - Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info) - .unwrap(); + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request::( + &requested, &info, + ) + .unwrap(); assert_eq!(extensions.large_blob_key, Some(true)); } @@ -580,8 +588,10 @@ mod tests { }; let extensions = - Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info) - .unwrap(); + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request::( + &requested, &info, + ) + .unwrap(); assert_eq!(extensions.large_blob_key, None); } @@ -604,11 +614,12 @@ mod tests { let info = info_with_options(&[("clientPin", true)]); let requested = requested_with_cred_protect(CredentialProtectionPolicy::UserVerificationRequired, true); - let result = - Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info); + let result = Ctap2MakeCredentialsRequestExtensions::from_webauthn_request::( + &requested, &info, + ); assert!(matches!( result, - Err(Error::Ctap(CtapError::UnsupportedExtension)) + Err(WebAuthnError::Ctap(CtapError::UnsupportedExtension)) )); } @@ -619,8 +630,10 @@ mod tests { let requested = requested_with_cred_protect(CredentialProtectionPolicy::UserVerificationRequired, true); let extensions = - Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info) - .unwrap(); + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request::( + &requested, &info, + ) + .unwrap(); assert_eq!( extensions.cred_protect, Some(Ctap2CredentialProtectionPolicy::Required) @@ -633,8 +646,10 @@ mod tests { let requested = requested_with_cred_protect(CredentialProtectionPolicy::UserVerificationOptional, true); let extensions = - Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info) - .unwrap(); + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request::( + &requested, &info, + ) + .unwrap(); assert_eq!( extensions.cred_protect, Some(Ctap2CredentialProtectionPolicy::Optional) @@ -649,8 +664,10 @@ mod tests { false, ); let extensions = - Ctap2MakeCredentialsRequestExtensions::from_webauthn_request(&requested, &info) - .unwrap(); + Ctap2MakeCredentialsRequestExtensions::from_webauthn_request::( + &requested, &info, + ) + .unwrap(); assert_eq!( extensions.cred_protect, Some(Ctap2CredentialProtectionPolicy::Required) @@ -691,7 +708,8 @@ mod tests { first: vec![3u8; 32], second: None, })); - let ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let ext = ctap.extensions.unwrap(); assert_eq!(ext.hmac_secret, Some(true)); assert!(ext.prf_input.is_some()); @@ -705,7 +723,8 @@ mod tests { first: vec![3u8; 32], second: None, })); - let ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let ext = ctap.extensions.unwrap(); assert_eq!(ext.hmac_secret, Some(true)); assert!(ext.prf_input.is_none()); @@ -716,7 +735,8 @@ mod tests { fn prf_without_eval_does_not_buffer_prf_input() { let info = info_with_extensions(&["hmac-secret", "hmac-secret-mc"]); let req = mc_request_with_prf(None); - let ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let ext = ctap.extensions.unwrap(); assert_eq!(ext.hmac_secret, Some(true)); assert!(ext.prf_input.is_none()); @@ -729,7 +749,8 @@ mod tests { first: b"create-first".to_vec(), second: None, })); - let ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let ext = ctap.extensions.as_ref().unwrap(); assert!(ext.hmac_secret.is_none(), "hmac-secret must not be sent"); assert!(ext.prf_input.is_none(), "no hmac-secret-mc buffering"); @@ -765,7 +786,8 @@ mod tests { fn native_prf_without_eval_sends_empty_map() { let info = info_with_extensions(&["prf"]); let req = mc_request_with_prf(None); - let ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let ext = ctap.extensions.as_ref().unwrap(); assert_eq!(ext.prf, Some(Ctap2PrfMakeCredentialInput { eval: None })); assert!(!ext.skip_serializing()); @@ -782,7 +804,8 @@ mod tests { first: b"x".to_vec(), second: None, })); - let ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let ext = ctap.extensions.as_ref().unwrap(); assert!(ext.prf.is_some()); assert!(ext.hmac_secret.is_none()); @@ -817,7 +840,8 @@ mod tests { first: b"input".to_vec(), second: None, })); - let ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let bytes = crate::proto::ctap2::cbor::to_vec(&ctap).unwrap(); let parsed: BTreeMap = crate::proto::ctap2::cbor::from_slice(&bytes).unwrap(); @@ -838,7 +862,8 @@ mod tests { }), ..mc_request_with_prf(None) }; - let ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let ext = ctap.extensions.unwrap(); assert!(ext.prf.is_none()); assert_eq!(ext.hmac_secret, Some(true)); @@ -910,7 +935,7 @@ mod tests { let info_mc = info_with_extensions(&["hmac-secret", "hmac-secret-mc"]); let info_no_mc = info_with_extensions(&["hmac-secret"]); - let with = Ctap2MakeCredentialRequest::from_webauthn_request( + let with = Ctap2MakeCredentialRequest::from_webauthn_request::( &mc_request_with_prf(Some(PrfInputValue::default())), &info_mc, ) @@ -918,9 +943,11 @@ mod tests { assert!(with.needs_shared_secret(&info_mc)); assert!(!with.needs_shared_secret(&info_no_mc)); - let without = - Ctap2MakeCredentialRequest::from_webauthn_request(&mc_request_with_prf(None), &info_mc) - .unwrap(); + let without = Ctap2MakeCredentialRequest::from_webauthn_request::( + &mc_request_with_prf(None), + &info_mc, + ) + .unwrap(); assert!(!without.needs_shared_secret(&info_mc)); } @@ -934,7 +961,8 @@ mod tests { first: vec![9u8; 32], second: None, })); - let mut ctap = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let mut ctap = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); let pin_proto = Ctap2PinUvAuthProtocol::One; let auth = AuthTokenData::new( @@ -972,7 +1000,7 @@ mod tests { use cosey::{Bytes, PublicKey}; let info = info_with_extensions(&["hmac-secret", "hmac-secret-mc"]); - let mut ctap = Ctap2MakeCredentialRequest::from_webauthn_request( + let mut ctap = Ctap2MakeCredentialRequest::from_webauthn_request::( &mc_request_with_prf(Some(PrfInputValue { first: vec![0xAB; 32], second: Some(vec![0xCD; 32]), @@ -1007,7 +1035,8 @@ mod tests { let info = Ctap2GetInfoResponse::default(); let req = mc_request_with_prf(None); - let mut ctap2 = Ctap2MakeCredentialRequest::from_webauthn_request(&req, &info).unwrap(); + let mut ctap2 = + Ctap2MakeCredentialRequest::from_webauthn_request::(&req, &info).unwrap(); ctap2.ensure_uv_set(); assert_eq!( diff --git a/libwebauthn/src/proto/ctap2/preflight.rs b/libwebauthn/src/proto/ctap2/preflight.rs index 35ffa052..c8d62bda 100644 --- a/libwebauthn/src/proto/ctap2/preflight.rs +++ b/libwebauthn/src/proto/ctap2/preflight.rs @@ -6,7 +6,7 @@ use super::{Ctap2GetAssertionRequest, Ctap2PublicKeyCredentialDescriptor}; use crate::{ proto::ctap2::{model::Ctap2GetAssertionOptions, Ctap2}, transport::Channel, - webauthn::error::{CtapError, Error}, + webauthn::error::{CtapError, WebAuthnError}, }; /// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#pre-flight @@ -24,7 +24,7 @@ pub async fn ctap2_preflight( credentials: &[Ctap2PublicKeyCredentialDescriptor], client_data_hash: &[u8], rp: &str, -) -> Result, Error> { +) -> Result, WebAuthnError> { ctap2_preflight_with_appid(channel, credentials, client_data_hash, rp, None).await } @@ -39,7 +39,7 @@ pub async fn ctap2_preflight_with_appid( client_data_hash: &[u8], rp: &str, appid_exclude: Option<&str>, -) -> Result, Error> { +) -> Result, WebAuthnError> { info!("Credential list BEFORE preflight: {credentials:?}"); let mut filtered_list = Vec::new(); for credential in credentials { @@ -76,7 +76,7 @@ async fn preflight_one( credential: &Ctap2PublicKeyCredentialDescriptor, client_data_hash: &[u8], rp: &str, -) -> Result, Error> { +) -> Result, WebAuthnError> { let preflight_request = Ctap2GetAssertionRequest { relying_party_id: rp.to_string(), client_data_hash: ByteBuf::from(client_data_hash), @@ -110,7 +110,7 @@ async fn preflight_one( Ok(Some(id)) } // Only CTAP2_ERR_NO_CREDENTIALS proves the credential is absent. - Err(Error::Ctap(CtapError::NoCredentials)) => { + Err(WebAuthnError::Ctap(CtapError::NoCredentials)) => { debug!("Pre-flight: Not found under {rp:?}"); Ok(None) } @@ -133,7 +133,7 @@ mod tests { Ctap2GetAssertionRequest, Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialType, }; use crate::transport::mock::channel::MockChannel; - use crate::webauthn::error::{CtapError, Error}; + use crate::webauthn::error::{CtapError, WebAuthnError}; fn credential(id: &[u8]) -> Ctap2PublicKeyCredentialDescriptor { Ctap2PublicKeyCredentialDescriptor { @@ -177,7 +177,10 @@ mod tests { ); let result = ctap2_preflight(&mut channel, &[cred], &hash, "example.org").await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::OperationDenied))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::OperationDenied)) + )); } #[tokio::test] diff --git a/libwebauthn/src/proto/ctap2/protocol.rs b/libwebauthn/src/proto/ctap2/protocol.rs index 73e00dfd..8acde140 100644 --- a/libwebauthn/src/proto/ctap2/protocol.rs +++ b/libwebauthn/src/proto/ctap2/protocol.rs @@ -7,7 +7,7 @@ use crate::proto::ctap2::cbor::{self, CborRequest}; use crate::proto::ctap2::{Ctap2BioEnrollmentResponse, Ctap2CommandCode}; use crate::transport::Channel; use crate::unwrap_field; -use crate::webauthn::error::{CtapError, Error, PlatformError}; +use crate::webauthn::error::{CtapError, PlatformError, WebAuthnError}; use super::model::Ctap2ClientPinResponse; use super::{ @@ -29,55 +29,62 @@ macro_rules! parse_cbor { stringify!($type), e ); - return Err(Error::Platform(PlatformError::InvalidDeviceResponse)); + return Err(WebAuthnError::Platform( + PlatformError::InvalidDeviceResponse, + )); } } }}; } #[async_trait] -pub trait Ctap2 { - async fn ctap2_get_info(&mut self) -> Result; +pub trait Ctap2: Channel { + async fn ctap2_get_info( + &mut self, + ) -> Result>; async fn ctap2_make_credential( &mut self, request: &Ctap2MakeCredentialRequest, timeout: Duration, - ) -> Result; + ) -> Result>; async fn ctap2_client_pin( &mut self, request: &Ctap2ClientPinRequest, timeout: Duration, - ) -> Result; + ) -> Result>; async fn ctap2_get_assertion( &mut self, request: &Ctap2GetAssertionRequest, timeout: Duration, - ) -> Result; + ) -> Result>; async fn ctap2_get_next_assertion( &mut self, timeout: Duration, - ) -> Result; - async fn ctap2_selection(&mut self, timeout: Duration) -> Result<(), Error>; + ) -> Result>; + async fn ctap2_selection( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError>; async fn ctap2_authenticator_config( &mut self, request: &Ctap2AuthenticatorConfigRequest, timeout: Duration, - ) -> Result<(), Error>; + ) -> Result<(), WebAuthnError>; async fn ctap2_bio_enrollment( &mut self, request: &Ctap2BioEnrollmentRequest, timeout: Duration, - ) -> Result; + ) -> Result>; async fn ctap2_credential_management( &mut self, request: &Ctap2CredentialManagementRequest, timeout: Duration, - ) -> Result; + ) -> Result>; async fn ctap2_large_blobs( &mut self, request: &Ctap2LargeBlobsRequest, timeout: Duration, - ) -> Result; + ) -> Result>; } #[async_trait] @@ -86,13 +93,20 @@ where C: Channel, { #[instrument(skip_all)] - async fn ctap2_get_info(&mut self) -> Result { + async fn ctap2_get_info( + &mut self, + ) -> Result> { let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo); - self.cbor_send(&cbor_request, TIMEOUT_GET_INFO).await?; - let cbor_response = self.cbor_recv(TIMEOUT_GET_INFO).await?; + self.cbor_send(&cbor_request, TIMEOUT_GET_INFO) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(TIMEOUT_GET_INFO) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => (), - error => return Err(Error::Ctap(error)), + error => return Err(WebAuthnError::Ctap(error)), }; let data = unwrap_field!(cbor_response.data); let ctap_response = parse_cbor!(Ctap2GetInfoResponse, &data); @@ -106,13 +120,18 @@ where &mut self, request: &Ctap2MakeCredentialRequest, timeout: Duration, - ) -> Result { + ) -> Result> { trace!(?request); - self.cbor_send(&request.try_into()?, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&request.try_into()?, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => (), - error => return Err(Error::Ctap(error)), + error => return Err(WebAuthnError::Ctap(error)), }; let data = unwrap_field!(cbor_response.data); trace!("MakeCredential: {:?}", data); @@ -127,13 +146,18 @@ where &mut self, request: &Ctap2GetAssertionRequest, timeout: Duration, - ) -> Result { + ) -> Result> { trace!(?request); - self.cbor_send(&request.try_into()?, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&request.try_into()?, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => (), - error => return Err(Error::Ctap(error)), + error => return Err(WebAuthnError::Ctap(error)), }; let data = unwrap_field!(cbor_response.data); trace!("GetAssertion: {:?}", data); @@ -147,14 +171,19 @@ where async fn ctap2_get_next_assertion( &mut self, timeout: Duration, - ) -> Result { + ) -> Result> { debug!("CTAP2 GetNextAssertion request"); let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorGetNextAssertion); - self.cbor_send(&cbor_request, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&cbor_request, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => (), - error => return Err(Error::Ctap(error)), + error => return Err(WebAuthnError::Ctap(error)), }; let data = unwrap_field!(cbor_response.data); let ctap_response = parse_cbor!(Ctap2GetAssertionResponse, &data); @@ -164,19 +193,27 @@ where } #[instrument(skip_all)] - async fn ctap2_selection(&mut self, timeout: Duration) -> Result<(), Error> { + async fn ctap2_selection( + &mut self, + timeout: Duration, + ) -> Result<(), WebAuthnError> { debug!("CTAP2 Authenticator Selection request"); let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorSelection); - self.cbor_send(&cbor_request, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&cbor_request, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => { return Ok(()); } error => { warn!(?error, "Selection request failed with status code"); - return Err(Error::Ctap(error)); + return Err(WebAuthnError::Ctap(error)); } } } @@ -186,13 +223,18 @@ where &mut self, request: &Ctap2ClientPinRequest, timeout: Duration, - ) -> Result { + ) -> Result> { trace!(?request); - self.cbor_send(&request.try_into()?, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&request.try_into()?, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => (), - error => return Err(Error::Ctap(error)), + error => return Err(WebAuthnError::Ctap(error)), }; if let Some(data) = cbor_response.data { let ctap_response = parse_cbor!(Ctap2ClientPinResponse, &data); @@ -212,10 +254,15 @@ where &mut self, request: &Ctap2AuthenticatorConfigRequest, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), WebAuthnError> { trace!(?request); - self.cbor_send(&request.try_into()?, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&request.try_into()?, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => { return Ok(()); @@ -225,7 +272,7 @@ where ?error, "Authenticator config request failed with status code" ); - return Err(Error::Ctap(error)); + return Err(WebAuthnError::Ctap(error)); } } } @@ -235,13 +282,18 @@ where &mut self, request: &Ctap2BioEnrollmentRequest, timeout: Duration, - ) -> Result { + ) -> Result> { trace!(?request); - self.cbor_send(&request.try_into()?, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&request.try_into()?, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => (), - error => return Err(Error::Ctap(error)), + error => return Err(WebAuthnError::Ctap(error)), }; if let Some(data) = cbor_response.data { let ctap_response = parse_cbor!(Ctap2BioEnrollmentResponse, &data); @@ -261,13 +313,18 @@ where &mut self, request: &Ctap2CredentialManagementRequest, timeout: Duration, - ) -> Result { + ) -> Result> { trace!(?request); - self.cbor_send(&request.try_into()?, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&request.try_into()?, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => (), - error => return Err(Error::Ctap(error)), + error => return Err(WebAuthnError::Ctap(error)), }; if let Some(data) = cbor_response.data { let ctap_response = parse_cbor!(Ctap2CredentialManagementResponse, &data); @@ -287,13 +344,18 @@ where &mut self, request: &Ctap2LargeBlobsRequest, timeout: Duration, - ) -> Result { + ) -> Result> { trace!(?request); - self.cbor_send(&request.try_into()?, timeout).await?; - let cbor_response = self.cbor_recv(timeout).await?; + self.cbor_send(&request.try_into()?, timeout) + .await + .map_err(WebAuthnError::Transport)?; + let cbor_response = self + .cbor_recv(timeout) + .await + .map_err(WebAuthnError::Transport)?; match cbor_response.status_code { CtapError::Ok => (), - error => return Err(Error::Ctap(error)), + error => return Err(WebAuthnError::Ctap(error)), }; if let Some(data) = cbor_response.data { let ctap_response = parse_cbor!(Ctap2LargeBlobsResponse, &data); @@ -322,7 +384,7 @@ mod tests { }; use crate::proto::ctap2::Ctap2CommandCode; use crate::transport::mock::channel::MockChannel; - use crate::webauthn::error::{CtapError, Error}; + use crate::webauthn::error::{CtapError, WebAuthnError}; use super::Ctap2; @@ -342,7 +404,10 @@ mod tests { channel.push_command_pair(expected_request, error_response(CtapError::Other)); let result = channel.ctap2_get_info().await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::Other))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::Other)) + )); } // Regression test: cable's `cbor_send` blocks for the BLE handshake before @@ -356,9 +421,8 @@ mod tests { channel.push_command_pair(expected_request, error_response(CtapError::Other)); let result = channel.ctap2_get_info().await; - assert_eq!( - result.err(), - Some(Error::Ctap(CtapError::Other)), + assert!( + matches!(result.err(), Some(WebAuthnError::Ctap(CtapError::Other))), "GetInfo must not impose a wall-clock timeout that fires before \ the channel's own cbor_send returns" ); @@ -372,7 +436,10 @@ mod tests { channel.push_command_pair(expected_request, error_response(CtapError::OperationDenied)); let result = channel.ctap2_make_credential(&request, TIMEOUT).await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::OperationDenied))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::OperationDenied)) + )); } #[tokio::test] @@ -391,7 +458,10 @@ mod tests { channel.push_command_pair(expected_request, error_response(CtapError::NoCredentials)); let result = channel.ctap2_get_assertion(&request, TIMEOUT).await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::NoCredentials))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::NoCredentials)) + )); } #[tokio::test] @@ -404,7 +474,10 @@ mod tests { channel.push_command_pair(expected_request, error_response(CtapError::NotAllowed)); let result = channel.ctap2_get_next_assertion(TIMEOUT).await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::NotAllowed))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::NotAllowed)) + )); } #[tokio::test] @@ -421,7 +494,10 @@ mod tests { channel.push_command_pair(expected_request, response); let result = channel.ctap2_get_next_assertion(TIMEOUT).await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::Other))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::Other)) + )); } #[tokio::test] @@ -432,7 +508,10 @@ mod tests { channel.push_command_pair(expected_request, error_response(CtapError::PINBlocked)); let result = channel.ctap2_client_pin(&request, TIMEOUT).await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::PINBlocked))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::PINBlocked)) + )); } #[tokio::test] @@ -447,10 +526,10 @@ mod tests { ); let result = channel.ctap2_selection(TIMEOUT).await; - assert_eq!( + assert!(matches!( result.err(), - Some(Error::Ctap(CtapError::UserActionTimeout)) - ); + Some(WebAuthnError::Ctap(CtapError::UserActionTimeout)) + )); } #[tokio::test] @@ -464,10 +543,10 @@ mod tests { ); let result = channel.ctap2_authenticator_config(&request, TIMEOUT).await; - assert_eq!( + assert!(matches!( result.err(), - Some(Error::Ctap(CtapError::UnauthorizedPermission)) - ); + Some(WebAuthnError::Ctap(CtapError::UnauthorizedPermission)) + )); } #[tokio::test] @@ -486,7 +565,10 @@ mod tests { channel.push_command_pair(expected_request, error_response(CtapError::InvalidOption)); let result = channel.ctap2_bio_enrollment(&request, TIMEOUT).await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::InvalidOption))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::InvalidOption)) + )); } #[tokio::test] @@ -505,6 +587,9 @@ mod tests { channel.push_command_pair(expected_request, error_response(CtapError::PINRequired)); let result = channel.ctap2_credential_management(&request, TIMEOUT).await; - assert_eq!(result.err(), Some(Error::Ctap(CtapError::PINRequired))); + assert!(matches!( + result.err(), + Some(WebAuthnError::Ctap(CtapError::PINRequired)) + )); } } diff --git a/libwebauthn/src/transport/ble/channel.rs b/libwebauthn/src/transport/ble/channel.rs index fbfa2d7f..3befa876 100644 --- a/libwebauthn/src/transport/ble/channel.rs +++ b/libwebauthn/src/transport/ble/channel.rs @@ -7,14 +7,13 @@ use crate::fido::FidoRevision; use crate::pin::persistent_token::PersistentTokenStore; use crate::proto::ctap1::apdu::{ApduRequest, ApduResponse}; use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; -use crate::proto::CtapError; use crate::transport::ble::btleplug; +use crate::transport::ble::error::BleError; use crate::transport::channel::{ AuthTokenData, Channel, ChannelSettings, ChannelStatus, Ctap2AuthTokenStore, }; use crate::transport::device::SupportedProtocols; -use crate::transport::error::TransportError; -use crate::webauthn::error::Error; +use crate::webauthn::error::WebAuthnError; use crate::Transport; use crate::UvUpdate; @@ -44,15 +43,15 @@ impl<'a> BleChannel<'a> { device: &'a BleDevice, revisions: &SupportedRevisions, settings: ChannelSettings, - ) -> Result, Error> { + ) -> Result, WebAuthnError> { let (ux_update_sender, _) = broadcast::channel(16); let revision = revisions .select_best() - .ok_or(Error::Transport(TransportError::NegotiationFailed))?; + .ok_or(WebAuthnError::Transport(BleError::NegotiationFailed))?; let connection = btleplug::connect(&device.btleplug_device.peripheral, &revision) .await - .or(Err(Error::Transport(TransportError::ConnectionFailed)))?; + .map_err(|e| WebAuthnError::Transport(BleError::Gatt(e)))?; let channel = BleChannel { status: ChannelStatus::Ready, device, @@ -67,7 +66,7 @@ impl<'a> BleChannel<'a> { .connection .subscribe() .await - .or(Err(Error::Transport(TransportError::TransportUnavailable)))?; + .map_err(|e| WebAuthnError::Transport(BleError::Gatt(e)))?; Ok(channel) } } @@ -81,12 +80,13 @@ impl Display for BleChannel<'_> { #[async_trait] impl<'a> Channel for BleChannel<'a> { type UxUpdate = UvUpdate; + type TransportError = BleError; fn transport(&self) -> Transport { Transport::Ble } - async fn supported_protocols(&self) -> Result { + async fn supported_protocols(&self) -> Result> { Ok(self.revision.into()) } @@ -100,40 +100,32 @@ impl<'a> Channel for BleChannel<'a> { } #[instrument(level = Level::DEBUG, skip_all)] - async fn apdu_send(&mut self, request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + async fn apdu_send( + &mut self, + request: &ApduRequest, + _timeout: Duration, + ) -> Result<(), BleError> { debug!({rev = ?self.revision}, "Sending APDU request"); trace!(?request); - let request_apdu_packet = request.raw_long().or(Err(TransportError::InvalidFraming))?; + let request_apdu_packet = request.raw_long()?; let request_frame = BleFrame::new(BleCommand::Msg, &request_apdu_packet); - self.connection - .frame_send(&request_frame) - .await - .or(Err(Error::Transport(TransportError::ConnectionFailed)))?; + self.connection.frame_send(&request_frame).await?; Ok(()) } #[instrument(level = Level::DEBUG, skip_all)] - async fn apdu_recv(&mut self, timeout: Duration) -> Result { - let response_frame = self - .connection - .frame_recv(timeout) - .await - .map_err(|e| match e { - btleplug::Error::Timeout => Error::Transport(TransportError::Timeout), - _ => Error::Transport(TransportError::ConnectionFailed), - })?; + async fn apdu_recv(&mut self, timeout: Duration) -> Result { + let response_frame = self.connection.frame_recv(timeout).await?; match response_frame.cmd { - BleCommand::Error => return Err(Error::Transport(TransportError::InvalidFraming)), // Encapsulation layer error - BleCommand::Cancel => return Err(Error::Ctap(CtapError::KeepAliveCancel)), - BleCommand::Keepalive | BleCommand::Ping => return Err(Error::Ctap(CtapError::Other)), // Unexpected + BleCommand::Error => return Err(BleError::UnexpectedFrame), // Encapsulation layer error + BleCommand::Cancel => return Err(BleError::Cancelled), + BleCommand::Keepalive | BleCommand::Ping => return Err(BleError::UnexpectedFrame), BleCommand::Msg => {} } let response_apdu_packet = &response_frame.data; - let response_apdu: ApduResponse = response_apdu_packet - .try_into() - .or(Err(TransportError::InvalidFraming))?; + let response_apdu: ApduResponse = response_apdu_packet.try_into()?; debug!("Received APDU response"); trace!(?response_apdu); @@ -145,41 +137,27 @@ impl<'a> Channel for BleChannel<'a> { &mut self, request: &CborRequest, _timeout: std::time::Duration, - ) -> Result<(), Error> { + ) -> Result<(), BleError> { debug!("Sending CBOR request"); trace!(?request); - let cbor_request = request - .raw_long() - .map_err(|e| TransportError::IoError(e.kind()))?; + let cbor_request = request.raw_long()?; let request_frame = BleFrame::new(BleCommand::Msg, &cbor_request); - self.connection - .frame_send(&request_frame) - .await - .or(Err(Error::Transport(TransportError::ConnectionFailed)))?; + self.connection.frame_send(&request_frame).await?; Ok(()) } #[instrument(level = Level::DEBUG, skip_all)] - async fn cbor_recv(&mut self, timeout: std::time::Duration) -> Result { - let response_frame = self - .connection - .frame_recv(timeout) - .await - .map_err(|e| match e { - btleplug::Error::Timeout => Error::Transport(TransportError::Timeout), - _ => Error::Transport(TransportError::ConnectionFailed), - })?; + async fn cbor_recv(&mut self, timeout: std::time::Duration) -> Result { + let response_frame = self.connection.frame_recv(timeout).await?; match response_frame.cmd { - BleCommand::Error => return Err(Error::Transport(TransportError::InvalidFraming)), // Encapsulation layer error - BleCommand::Cancel => return Err(Error::Ctap(CtapError::KeepAliveCancel)), - BleCommand::Keepalive | BleCommand::Ping => return Err(Error::Ctap(CtapError::Other)), // Unexpected + BleCommand::Error => return Err(BleError::UnexpectedFrame), // Encapsulation layer error + BleCommand::Cancel => return Err(BleError::Cancelled), + BleCommand::Keepalive | BleCommand::Ping => return Err(BleError::UnexpectedFrame), BleCommand::Msg => {} } let cbor_response_packet = &response_frame.data; - let cbor_response: CborResponse = cbor_response_packet - .try_into() - .or(Err(TransportError::InvalidFraming))?; + let cbor_response: CborResponse = cbor_response_packet.try_into()?; debug!("Received CBOR response"); trace!(?cbor_response); diff --git a/libwebauthn/src/transport/ble/device.rs b/libwebauthn/src/transport/ble/device.rs index cb677f4a..e37e82b2 100644 --- a/libwebauthn/src/transport/ble/device.rs +++ b/libwebauthn/src/transport/ble/device.rs @@ -5,10 +5,10 @@ use async_trait::async_trait; use hex::ToHex; use tracing::{info, instrument}; +use crate::transport::ble::error::BleError; use crate::transport::device::Device; -use crate::transport::error::TransportError; use crate::transport::ChannelSettings; -use crate::webauthn::error::Error; +use crate::webauthn::error::WebAuthnError; use super::btleplug::manager::SupportedRevisions; use super::btleplug::{supported_fido_revisions, FidoDevice as BtleplugFidoDevice}; @@ -22,10 +22,9 @@ pub async fn is_available() -> bool { } #[instrument] -pub async fn list_devices() -> Result, Error> { +pub async fn list_devices() -> Result, BleError> { let devices: Vec<_> = btleplug::list_fido_devices() - .await - .or(Err(Error::Transport(TransportError::TransportUnavailable)))? + .await? .iter() .map(|bluez_device| bluez_device.into()) .collect(); @@ -79,8 +78,14 @@ impl fmt::Display for BleDevice { #[async_trait] impl<'d> Device<'d, Ble, BleChannel<'d>> for BleDevice { - async fn channel(&'d mut self, settings: ChannelSettings) -> Result, Error> { - let revisions = self.supported_revisions().await?; + async fn channel( + &'d mut self, + settings: ChannelSettings, + ) -> Result, WebAuthnError> { + let revisions = self + .supported_revisions() + .await + .map_err(WebAuthnError::Transport)?; let channel = BleChannel::new(self, &revisions, settings).await?; Ok(channel) } @@ -93,12 +98,10 @@ impl<'d> Device<'d, Ble, BleChannel<'d>> for BleDevice { } impl BleDevice { - async fn supported_revisions(&mut self) -> Result { + async fn supported_revisions(&mut self) -> Result { let revisions = match self.revisions { None => { - let revisions = supported_fido_revisions(&self.btleplug_device.peripheral) - .await - .or(Err(Error::Transport(TransportError::NegotiationFailed)))?; + let revisions = supported_fido_revisions(&self.btleplug_device.peripheral).await?; self.revisions = Some(revisions); revisions } diff --git a/libwebauthn/src/transport/ble/error.rs b/libwebauthn/src/transport/ble/error.rs new file mode 100644 index 00000000..1f179fd9 --- /dev/null +++ b/libwebauthn/src/transport/ble/error.rs @@ -0,0 +1,18 @@ +//! Errors specific to the BLE transport. + +use crate::transport::ble::btleplug; + +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum BleError { + #[error("BLE GATT error: {0}")] + Gatt(#[from] btleplug::Error), + #[error("BLE framing error: {0}")] + Framing(#[from] std::io::Error), + #[error("no supported FIDO revision")] + NegotiationFailed, + #[error("device cancelled the operation")] + Cancelled, + #[error("unexpected BLE frame")] + UnexpectedFrame, +} diff --git a/libwebauthn/src/transport/ble/mod.rs b/libwebauthn/src/transport/ble/mod.rs index dcd6f92f..64a73a5f 100644 --- a/libwebauthn/src/transport/ble/mod.rs +++ b/libwebauthn/src/transport/ble/mod.rs @@ -3,11 +3,13 @@ use std::fmt::Display; pub mod btleplug; pub mod channel; pub mod device; +pub mod error; pub mod framing; pub use device::is_available; pub use device::list_devices; pub use device::BleDevice; +pub use error::BleError; use super::Transport; diff --git a/libwebauthn/src/transport/cable/advertisement.rs b/libwebauthn/src/transport/cable/advertisement.rs index 1e21abba..25e0a30b 100644 --- a/libwebauthn/src/transport/cable/advertisement.rs +++ b/libwebauthn/src/transport/cable/advertisement.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::proto::ctap2::cbor::Value; use crate::transport::ble::btleplug::{self, FidoDevice}; use crate::transport::cable::crypto::trial_decrypt_advert; -use crate::transport::error::TransportError; +use crate::transport::cable::error::CableError; const CABLE_UUID_FIDO: &str = "0000fff9-0000-1000-8000-00805f9b34fb"; const CABLE_UUID_GOOGLE: &str = "0000fde2-0000-1000-8000-00805f9b34fb"; @@ -72,14 +72,14 @@ impl From<[u8; 16]> for DecryptedAdvert { #[instrument(skip_all, err)] pub(crate) async fn await_advertisement( eid_key: &[u8], -) -> Result<(FidoDevice, DecryptedAdvert), TransportError> { +) -> Result<(FidoDevice, DecryptedAdvert), CableError> { let uuids = &[ - Uuid::parse_str(CABLE_UUID_FIDO).or(Err(TransportError::InvalidEndpoint))?, - Uuid::parse_str(CABLE_UUID_GOOGLE).or(Err(TransportError::InvalidEndpoint))?, // Deprecated, but may still be in use. + Uuid::parse_str(CABLE_UUID_FIDO)?, + Uuid::parse_str(CABLE_UUID_GOOGLE)?, // Deprecated, but may still be in use. ]; let stream = btleplug::manager::start_discovery_for_service_data(uuids) .await - .or(Err(TransportError::TransportUnavailable))?; + .or(Err(CableError::TransportUnavailable))?; let mut stream = pin!(stream); while let Some((adapter, peripheral, data)) = stream.as_mut().next().await { @@ -87,7 +87,7 @@ pub(crate) async fn await_advertisement( let Some(device) = btleplug::manager::get_device(peripheral.clone()) .await - .or(Err(TransportError::TransportUnavailable))? + .or(Err(CableError::TransportUnavailable))? else { warn!( ?peripheral, @@ -126,13 +126,13 @@ pub(crate) async fn await_advertisement( adapter .stop_scan() .await - .or(Err(TransportError::TransportUnavailable))?; + .or(Err(CableError::TransportUnavailable))?; return Ok((device, advert)); } warn!("BLE advertisement discovery stream terminated"); - Err(TransportError::TransportUnavailable) + Err(CableError::TransportUnavailable) } #[cfg(test)] diff --git a/libwebauthn/src/transport/cable/channel.rs b/libwebauthn/src/transport/cable/channel.rs index 852f4c32..5bc1cacb 100644 --- a/libwebauthn/src/transport/cable/channel.rs +++ b/libwebauthn/src/transport/cable/channel.rs @@ -12,12 +12,12 @@ use crate::proto::{ ctap1::apdu::{ApduRequest, ApduResponse}, ctap2::cbor::{CborRequest, CborResponse}, }; -use crate::transport::error::TransportError; +use crate::transport::cable::error::CableError; use crate::transport::AuthTokenData; use crate::transport::{ channel::ChannelStatus, device::SupportedProtocols, Channel, Ctap2AuthTokenStore, }; -use crate::webauthn::error::Error; +use crate::webauthn::error::WebAuthnError; use crate::Transport; use crate::UvUpdate; @@ -51,7 +51,7 @@ pub struct CableChannel { } impl CableChannel { - async fn wait_for_connection(&self) -> Result<(), Error> { + async fn wait_for_connection(&self) -> Result<(), CableError> { let mut rx = self.connection_state_receiver.clone(); // If already connected, return immediately @@ -65,22 +65,20 @@ impl CableChannel { // the caller can't observe the timing difference and the asymmetry // was accidental. if *rx.borrow() == ConnectionState::Terminated { - return Err(Error::Transport(TransportError::ConnectionFailed)); + return Err(CableError::ConnectionFailed); } // Wait for state change while rx.changed().await.is_ok() { match *rx.borrow() { ConnectionState::Connected => return Ok(()), - ConnectionState::Terminated => { - return Err(Error::Transport(TransportError::ConnectionFailed)) - } + ConnectionState::Terminated => return Err(CableError::ConnectionFailed), ConnectionState::Connecting => continue, } } // If the sender was dropped, consider it a failure - Err(Error::Transport(TransportError::ConnectionLost)) + Err(CableError::ConnectionLost) } } @@ -113,7 +111,7 @@ pub enum CableUpdate { /// Connected to the authenticator device via the tunnel server. Connected, /// The connection to the authenticator device has failed. - Error(TransportError), + Error(CableError), } impl From for CableUxUpdate { @@ -125,12 +123,13 @@ impl From for CableUxUpdate { #[async_trait] impl Channel for CableChannel { type UxUpdate = CableUxUpdate; + type TransportError = CableError; fn transport(&self) -> Transport { Transport::Hybrid } - async fn supported_protocols(&self) -> Result { + async fn supported_protocols(&self) -> Result> { Ok(SupportedProtocols::fido2_only()) } @@ -145,17 +144,25 @@ impl Channel for CableChannel { // TODO Send CableTunnelMessageType#Shutdown and drop the connection } - async fn apdu_send(&mut self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + async fn apdu_send( + &mut self, + _request: &ApduRequest, + _timeout: Duration, + ) -> Result<(), CableError> { error!("APDU send not supported in caBLE transport"); - Err(Error::Transport(TransportError::TransportUnavailable)) + Err(CableError::TransportUnavailable) } - async fn apdu_recv(&mut self, _timeout: Duration) -> Result { + async fn apdu_recv(&mut self, _timeout: Duration) -> Result { error!("APDU recv not supported in caBLE transport"); - Err(Error::Transport(TransportError::TransportUnavailable)) + Err(CableError::TransportUnavailable) } - async fn cbor_send(&mut self, request: &CborRequest, timeout: Duration) -> Result<(), Error> { + async fn cbor_send( + &mut self, + request: &CborRequest, + timeout: Duration, + ) -> Result<(), CableError> { // First, wait for connection to be established (no timeout for handshake) self.wait_for_connection().await?; @@ -164,26 +171,26 @@ impl Channel for CableChannel { Ok(Ok(_)) => Ok(()), Ok(Err(error)) => { error!(%error, "CBOR request send failure"); - Err(Error::Transport(TransportError::TransportUnavailable)) + Err(CableError::TransportUnavailable) } Err(elapsed) => { error!({ %elapsed, ?timeout }, "CBOR request send timeout"); - Err(Error::Transport(TransportError::Timeout)) + Err(CableError::Timeout) } } } - async fn cbor_recv(&mut self, timeout: Duration) -> Result { + async fn cbor_recv(&mut self, timeout: Duration) -> Result { // First, wait for connection to be established (no timeout for handshake) self.wait_for_connection().await?; // Now apply timeout only to the actual CBOR operation match time::timeout(timeout, self.cbor_receiver.recv()).await { Ok(Some(response)) => Ok(response), - Ok(None) => Err(Error::Transport(TransportError::TransportUnavailable)), + Ok(None) => Err(CableError::TransportUnavailable), Err(elapsed) => { error!({ %elapsed, ?timeout }, "CBOR response recv timeout"); - Err(Error::Transport(TransportError::Timeout)) + Err(CableError::Timeout) } } } diff --git a/libwebauthn/src/transport/cable/connection_stages.rs b/libwebauthn/src/transport/cable/connection_stages.rs index 7f3cd9f8..740c64bc 100644 --- a/libwebauthn/src/transport/cable/connection_stages.rs +++ b/libwebauthn/src/transport/cable/connection_stages.rs @@ -14,8 +14,7 @@ use super::qr_code_device::CableQrCodeDevice; use super::tunnel; use crate::proto::ctap2::cbor::{CborRequest, CborResponse}; use crate::transport::ble::btleplug::FidoDevice; -use crate::transport::error::TransportError; -use crate::webauthn::error::Error; +use crate::transport::cable::error::CableError; use std::sync::Arc; #[derive(Debug)] @@ -24,7 +23,7 @@ pub(crate) struct ProximityCheckInput { } impl ProximityCheckInput { - pub fn new_for_qr_code(qr_device: &CableQrCodeDevice) -> Result { + pub fn new_for_qr_code(qr_device: &CableQrCodeDevice) -> Result { let eid_key: [u8; 64] = derive( qr_device.qr_code.qr_secret.as_ref(), None, @@ -36,7 +35,7 @@ impl ProximityCheckInput { pub fn new_for_known_device( known_device: &CableKnownDevice, client_nonce: &ClientNonce, - ) -> Result { + ) -> Result { let eid_key: [u8; 64] = derive( &known_device.device_info.link_secret, Some(client_nonce), @@ -75,7 +74,7 @@ impl ConnectionInput { pub fn new_for_qr_code( qr_device: &CableQrCodeDevice, proximity_output: &ProximityCheckOutput, - ) -> Result { + ) -> Result { let tunnel_domain = decode_tunnel_domain_from_advert(&proximity_output.advert)?; let routing_id_str = hex::encode(proximity_output.advert.routing_id); @@ -84,8 +83,8 @@ impl ConnectionInput { None, KeyPurpose::TunnelID, ) - .map_err(|_| TransportError::InvalidKey)?; - let tunnel_id = tunnel_id_full.get(..16).ok_or(TransportError::InvalidKey)?; + .map_err(|_| CableError::InvalidKey)?; + let tunnel_id = tunnel_id_full.get(..16).ok_or(CableError::InvalidKey)?; let tunnel_id_str = hex::encode(tunnel_id); let connection_type = CableTunnelConnectionType::QrCode { @@ -159,7 +158,7 @@ impl HandshakeInput { qr_device: &CableQrCodeDevice, connection_output: ConnectionOutput, proximity_output: ProximityCheckOutput, - ) -> Result { + ) -> Result { let advert_plaintext = &proximity_output.advert.plaintext; let psk = derive_psk(qr_device.qr_code.qr_secret.as_ref(), advert_plaintext)?; Ok(Self { @@ -174,7 +173,7 @@ impl HandshakeInput { known_device: &CableKnownDevice, connection_output: ConnectionOutput, proximity_output: ProximityCheckOutput, - ) -> Result { + ) -> Result { let link_secret = known_device.device_info.link_secret; let advert_plaintext = proximity_output.advert.plaintext; let psk = derive_psk(&link_secret, &advert_plaintext)?; @@ -226,7 +225,7 @@ impl TunnelConnectionInput { #[async_trait] pub(crate) trait UxUpdateSender: Send + Sync { async fn send_update(&self, update: CableUxUpdate); - async fn send_error(&self, error: TransportError); + async fn send_error(&self, error: CableError); async fn set_connection_state(&self, state: ConnectionState); } @@ -257,7 +256,7 @@ impl UxUpdateSender for MpscUxUpdateSender { } } - async fn send_error(&self, error: TransportError) { + async fn send_error(&self, error: CableError) { self.send_update(CableUxUpdate::CableUpdate(CableUpdate::Error(error))) .await; let _ = self.connection_state_tx.send(ConnectionState::Terminated); @@ -272,7 +271,7 @@ impl UxUpdateSender for MpscUxUpdateSender { pub(crate) async fn proximity_check_stage( input: ProximityCheckInput, ux_sender: &dyn UxUpdateSender, -) -> Result { +) -> Result { debug!("Starting proximity check stage"); ux_sender @@ -289,7 +288,7 @@ pub(crate) async fn proximity_check_stage( pub(crate) async fn connection_stage( input: ConnectionInput, ux_sender: &dyn UxUpdateSender, -) -> Result { +) -> Result { debug!(?input.tunnel_domain, "Starting connection stage"); ux_sender @@ -311,7 +310,7 @@ pub(crate) async fn connection_stage( /// back to the tunnel, whose routing details are always present in the advert. async fn connect_data_channel( input: &ConnectionInput, -) -> Result, TransportError> { +) -> Result, CableError> { if let Some(ble) = input.ble { match L2capDataChannel::connect(ble.address, ble.address_type, ble.psm).await { Ok(channel) => { @@ -352,7 +351,7 @@ async fn connect_data_channel( pub(crate) async fn handshake_stage( input: HandshakeInput, ux_sender: &dyn UxUpdateSender, -) -> Result { +) -> Result { debug!("Starting handshake stage"); ux_sender @@ -380,23 +379,19 @@ pub(crate) async fn handshake_stage( }) } -fn derive_psk(secret: &[u8], advert_plaintext: &[u8]) -> Result<[u8; 32], Error> { +fn derive_psk(secret: &[u8], advert_plaintext: &[u8]) -> Result<[u8; 32], CableError> { let derived = derive(secret, Some(advert_plaintext), KeyPurpose::Psk)?; let mut psk: [u8; 32] = [0u8; 32]; - psk.copy_from_slice( - derived - .get(..32) - .ok_or(Error::Transport(TransportError::InvalidKey))?, - ); + psk.copy_from_slice(derived.get(..32).ok_or(CableError::InvalidKey)?); Ok(psk) } pub(crate) fn decode_tunnel_domain_from_advert( advert: &DecryptedAdvert, -) -> Result { +) -> Result { tunnel::decode_tunnel_server_domain(advert.encoded_tunnel_server_domain) .ok_or_else(|| { error!({ encoded = %advert.encoded_tunnel_server_domain }, "Failed to decode tunnel server domain"); - TransportError::InvalidFraming + CableError::InvalidFraming }) } diff --git a/libwebauthn/src/transport/cable/crypto.rs b/libwebauthn/src/transport/cable/crypto.rs index bb8dc67d..b0b721f8 100644 --- a/libwebauthn/src/transport/cable/crypto.rs +++ b/libwebauthn/src/transport/cable/crypto.rs @@ -5,8 +5,7 @@ use sha2::Sha256; use tracing::{instrument, warn}; use crate::pin::hmac_sha256; -use crate::transport::error::TransportError; -use crate::webauthn::error::Error; +use crate::transport::cable::error::CableError; pub enum KeyPurpose { EIDKey = 1, @@ -14,13 +13,17 @@ pub enum KeyPurpose { Psk = 3, } -pub fn derive(secret: &[u8], salt: Option<&[u8]>, purpose: KeyPurpose) -> Result<[u8; 64], Error> { +pub fn derive( + secret: &[u8], + salt: Option<&[u8]>, + purpose: KeyPurpose, +) -> Result<[u8; 64], CableError> { let purpose32 = [purpose as u8, 0, 0, 0]; let hkdf = Hkdf::::new(salt, secret); let mut output = [0u8; 64]; hkdf.expand(&purpose32, &mut output) - .map_err(|_| Error::Transport(TransportError::InvalidKey))?; + .map_err(|_| CableError::InvalidKey)?; Ok(output) } diff --git a/libwebauthn/src/transport/cable/data_channel.rs b/libwebauthn/src/transport/cable/data_channel.rs index e27979a9..a2ac9c96 100644 --- a/libwebauthn/src/transport/cable/data_channel.rs +++ b/libwebauthn/src/transport/cable/data_channel.rs @@ -6,7 +6,7 @@ use tokio_tungstenite::tungstenite::{Error, Message}; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tracing::error; -use crate::transport::error::TransportError; +use crate::transport::cable::error::CableError; /// A bidirectional channel carrying discrete protocol messages: the Noise /// handshake messages, then the encrypted CTAP frames. caBLE rides this over a @@ -14,11 +14,11 @@ use crate::transport::error::TransportError; #[async_trait] pub(crate) trait CableDataChannel: Send { /// Sends one message as a discrete unit. - async fn send(&mut self, message: &[u8]) -> Result<(), TransportError>; + async fn send(&mut self, message: &[u8]) -> Result<(), CableError>; /// Receives the next message. `Ok(None)` signals a clean close by the peer. /// Must be cancel-safe so it can be used as a `tokio::select!` branch. - async fn recv(&mut self) -> Result>, TransportError>; + async fn recv(&mut self) -> Result>, CableError>; } /// [`CableDataChannel`] over the caBLE WebSocket tunnel. Each protocol message is @@ -35,20 +35,20 @@ impl WebSocketDataChannel { #[async_trait] impl CableDataChannel for WebSocketDataChannel { - async fn send(&mut self, message: &[u8]) -> Result<(), TransportError> { + async fn send(&mut self, message: &[u8]) -> Result<(), CableError> { self.stream .send(Message::Binary(message.to_vec().into())) .await .map_err(|e| { error!(?e, "Failed to send WebSocket message"); match e { - Error::Io(io) => TransportError::IoError(io.kind()), - _ => TransportError::ConnectionFailed, + Error::Io(io) => CableError::from(io), + _ => CableError::ConnectionFailed, } }) } - async fn recv(&mut self) -> Result>, TransportError> { + async fn recv(&mut self) -> Result>, CableError> { loop { match self.stream.next().await { Some(Ok(Message::Binary(data))) => return Ok(Some(data.into())), @@ -58,15 +58,15 @@ impl CableDataChannel for WebSocketDataChannel { } Some(Ok(other)) => { error!(?other, "Unexpected WebSocket message type"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } Some(Err(Error::Io(e))) => { error!(?e, "Failed to read WebSocket message"); - return Err(TransportError::IoError(e.kind())); + return Err(CableError::from(e)); } Some(Err(e)) => { error!(?e, "Failed to read WebSocket message"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } } } diff --git a/libwebauthn/src/transport/cable/error.rs b/libwebauthn/src/transport/cable/error.rs index 66aaa541..0914ec01 100644 --- a/libwebauthn/src/transport/cable/error.rs +++ b/libwebauthn/src/transport/cable/error.rs @@ -1,4 +1,84 @@ -//! Errors specific to the caBLE tunnel-server transport. +//! Errors specific to the caBLE / hybrid transport. + +use std::sync::Arc; + +use tokio_tungstenite::tungstenite::http::header::InvalidHeaderValue; +use tokio_tungstenite::tungstenite::Error as TungsteniteError; + +use crate::proto::ctap2::cbor::CborError; + +/// caBLE transport error. `Clone` because it rides the [`CableUpdate`] UX +/// broadcast stream, which requires `Clone`; non-`Clone` native causes +/// (`snow`, `io`, `tungstenite`, `serde_cbor`, `http`) are kept behind an +/// `Arc` rather than flattened. +#[derive(thiserror::Error, Debug, Clone)] +#[non_exhaustive] +pub enum CableError { + #[error("noise protocol error: {0}")] + Noise(Arc), + #[error("input/output error: {0}")] + Io(Arc), + #[error("websocket error: {0}")] + WebSocket(Arc), + #[error("cbor error: {0}")] + Cbor(Arc), + #[error("url parse error: {0}")] + Url(#[from] url::ParseError), + #[error("uuid parse error: {0}")] + Uuid(#[from] uuid::Error), + #[error("invalid http header: {0}")] + HttpHeader(Arc), + #[error(transparent)] + CableTunnel(#[from] CableTunnelError), + #[error("connection failed")] + ConnectionFailed, + #[error("connection lost")] + ConnectionLost, + #[error("invalid endpoint")] + InvalidEndpoint, + #[error("invalid framing")] + InvalidFraming, + #[error("transport unavailable")] + TransportUnavailable, + #[error("timeout")] + Timeout, + #[error("invalid key")] + InvalidKey, + #[error("invalid signature")] + InvalidSignature, + #[error("encryption failed")] + EncryptionFailed, +} + +impl From for CableError { + fn from(error: snow::Error) -> Self { + CableError::Noise(Arc::new(error)) + } +} + +impl From for CableError { + fn from(error: std::io::Error) -> Self { + CableError::Io(Arc::new(error)) + } +} + +impl From for CableError { + fn from(error: TungsteniteError) -> Self { + CableError::WebSocket(Arc::new(error)) + } +} + +impl From for CableError { + fn from(error: InvalidHeaderValue) -> Self { + CableError::HttpHeader(Arc::new(error)) + } +} + +impl From for CableError { + fn from(error: CborError) -> Self { + CableError::Cbor(Arc::new(error)) + } +} #[derive(thiserror::Error, Debug, PartialEq, Clone)] pub enum CableTunnelError { diff --git a/libwebauthn/src/transport/cable/known_devices.rs b/libwebauthn/src/transport/cable/known_devices.rs index 3eadfb03..6a64706d 100644 --- a/libwebauthn/src/transport/cable/known_devices.rs +++ b/libwebauthn/src/transport/cable/known_devices.rs @@ -9,10 +9,10 @@ use crate::transport::cable::connection_stages::{ UxUpdateSender, }; -use crate::transport::error::TransportError; +use crate::transport::cable::error::CableError; use crate::transport::ChannelSettings; use crate::transport::Device; -use crate::webauthn::error::Error; +use crate::webauthn::error::WebAuthnError; use async_trait::async_trait; use futures::lock::Mutex; @@ -102,24 +102,24 @@ impl CableKnownDeviceInfo { pub(crate) fn new( tunnel_domain: &str, linking_info: &CableLinkingInfo, - ) -> Result { + ) -> Result { let info = Self { contact_id: linking_info.contact_id.to_vec(), link_id: linking_info .link_id .clone() .try_into() - .map_err(|_| TransportError::InvalidFraming)?, + .map_err(|_| CableError::InvalidFraming)?, link_secret: linking_info .link_secret .clone() .try_into() - .map_err(|_| TransportError::InvalidFraming)?, + .map_err(|_| CableError::InvalidFraming)?, public_key: linking_info .authenticator_public_key .clone() .try_into() - .map_err(|_| TransportError::InvalidFraming)?, + .map_err(|_| CableError::InvalidFraming)?, name: linking_info.authenticator_name.clone(), tunnel_domain: tunnel_domain.to_string(), }; @@ -153,7 +153,7 @@ impl CableKnownDevice { hint: ClientPayloadHint, device_info: &CableKnownDeviceInfo, store: Arc, - ) -> Result { + ) -> Result { let device = CableKnownDevice { hint, device_info: device_info.clone(), @@ -166,7 +166,7 @@ impl CableKnownDevice { async fn connection( known_device: &CableKnownDevice, ux_sender: &super::connection_stages::MpscUxUpdateSender, - ) -> Result { + ) -> Result { let client_nonce = rand::random::(); // Stage 1: Connection (no proximity check needed for known devices) @@ -192,7 +192,10 @@ impl CableKnownDevice { #[async_trait] impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice { - async fn channel(&'d mut self, settings: ChannelSettings) -> Result { + async fn channel( + &'d mut self, + settings: ChannelSettings, + ) -> Result> { debug!(?self.device_info.tunnel_domain, "Creating channel to tunnel server"); let (ux_update_sender, _) = broadcast::channel(16); @@ -211,11 +214,7 @@ impl<'d> Device<'d, Cable, CableChannel> for CableKnownDevice { let handshake_output = match Self::connection(&known_device, &ux_sender).await { Ok(handshake_output) => handshake_output, Err(e) => { - let transport_err = match e { - Error::Transport(t) => t, - _ => TransportError::ConnectionFailed, - }; - ux_sender.send_error(transport_err).await; + ux_sender.send_error(e).await; return; } }; diff --git a/libwebauthn/src/transport/cable/l2cap.rs b/libwebauthn/src/transport/cable/l2cap.rs index ad197921..f9459e93 100644 --- a/libwebauthn/src/transport/cable/l2cap.rs +++ b/libwebauthn/src/transport/cable/l2cap.rs @@ -9,7 +9,7 @@ use tokio::time::Instant; use tracing::{debug, error, warn}; use super::data_channel::CableDataChannel; -use crate::transport::error::TransportError; +use crate::transport::cable::error::CableError; /// End-of-Message sequence terminating every L2CAP message (CRLF). const EOM: [u8; 2] = [0x0D, 0x0A]; @@ -40,7 +40,7 @@ impl L2capDataChannel { addr: BDAddr, addr_type: Option, psm: u16, - ) -> Result { + ) -> Result { let (addr, addr_type) = bdaddr_to_bluer(addr, addr_type)?; let stream = @@ -48,7 +48,7 @@ impl L2capDataChannel { .await .map_err(|e| { error!(?e, %addr, psm, "Failed to connect L2CAP CoC"); - TransportError::ConnectionFailed + CableError::from(e) })?; await_send_mtu(&stream).await; @@ -86,19 +86,19 @@ async fn await_send_mtu(stream: &bluer::l2cap::Stream) { #[async_trait] impl CableDataChannel for L2capDataChannel { - async fn send(&mut self, message: &[u8]) -> Result<(), TransportError> { + async fn send(&mut self, message: &[u8]) -> Result<(), CableError> { self.stream.write_all(message).await.map_err(|e| { error!(?e, "Failed to write L2CAP message"); - TransportError::IoError(e.kind()) + CableError::from(e) })?; self.stream.write_all(&EOM).await.map_err(|e| { error!(?e, "Failed to write L2CAP EOM"); - TransportError::IoError(e.kind()) + CableError::from(e) })?; Ok(()) } - async fn recv(&mut self) -> Result>, TransportError> { + async fn recv(&mut self) -> Result>, CableError> { loop { if let Some(message) = split_next_message(&mut self.read_buf) { return Ok(Some(message)); @@ -121,7 +121,7 @@ impl CableDataChannel for L2capDataChannel { } Err(e) => { error!(?e, "Failed to read L2CAP message"); - return Err(TransportError::IoError(e.kind())); + return Err(CableError::from(e)); } }; if n == 0 { @@ -130,7 +130,7 @@ impl CableDataChannel for L2capDataChannel { return Ok(None); } error!(buffered = self.read_buf.len(), "L2CAP closed mid-message"); - return Err(TransportError::ConnectionLost); + return Err(CableError::ConnectionLost); } self.read_buf .extend_from_slice(chunk.get(..n).unwrap_or(&[])); @@ -152,10 +152,10 @@ fn split_next_message(buf: &mut Vec) -> Option> { fn bdaddr_to_bluer( addr: BDAddr, addr_type: Option, -) -> Result<(bluer::Address, bluer::AddressType), TransportError> { +) -> Result<(bluer::Address, bluer::AddressType), CableError> { let addr = bluer::Address::from_str(&addr.to_string()).map_err(|e| { error!(?e, "Failed to parse Bluetooth address"); - TransportError::InvalidEndpoint + CableError::InvalidEndpoint })?; let addr_type = match addr_type { Some(AddressType::Public) => bluer::AddressType::LePublic, diff --git a/libwebauthn/src/transport/cable/protocol.rs b/libwebauthn/src/transport/cable/protocol.rs index 13822223..73f68bb8 100644 --- a/libwebauthn/src/transport/cable/protocol.rs +++ b/libwebauthn/src/transport/cable/protocol.rs @@ -21,8 +21,8 @@ use super::known_devices::{CableKnownDeviceInfo, CableKnownDeviceInfoStore}; use crate::proto::ctap2::cbor::{self, CborRequest, CborResponse, Value}; use crate::proto::ctap2::{Ctap2CommandCode, Ctap2GetInfoResponse}; use crate::transport::cable::connection_stages::TunnelConnectionInput; +use crate::transport::cable::error::CableError; use crate::transport::cable::known_devices::CableKnownDeviceId; -use crate::transport::error::TransportError; const P256_X962_LENGTH: usize = 65; const MAX_CBOR_SIZE: usize = 1024 * 1024; @@ -44,10 +44,10 @@ impl CableTunnelMessage { payload: ByteBuf::from(payload.to_vec()), } } - pub fn from_slice(slice: &[u8]) -> Result { - let (type_byte, payload) = slice.split_first().ok_or(TransportError::InvalidFraming)?; + pub fn from_slice(slice: &[u8]) -> Result { + let (type_byte, payload) = slice.split_first().ok_or(CableError::InvalidFraming)?; if payload.is_empty() { - return Err(TransportError::InvalidFraming); + return Err(CableError::InvalidFraming); } let message_type = match *type_byte { @@ -55,7 +55,7 @@ impl CableTunnelMessage { 1 => CableTunnelMessageType::Ctap, 2 => CableTunnelMessageType::Update, _ => { - return Err(TransportError::InvalidFraming); + return Err(CableError::InvalidFraming); } }; @@ -173,7 +173,7 @@ pub(crate) async fn do_handshake( data_channel: &mut dyn CableDataChannel, psk: [u8; 32], connection_type: &CableTunnelConnectionType, -) -> Result { +) -> Result { let noise_handshake = match connection_type { CableTunnelConnectionType::QrCode { private_key, .. } => { let local_private_key = private_key.to_owned().to_bytes(); @@ -198,7 +198,7 @@ pub(crate) async fn do_handshake( Ok(handshake) => handshake, Err(e) => { error!(?e, "Failed to build Noise handshake"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } }; @@ -207,14 +207,14 @@ pub(crate) async fn do_handshake( Ok(msg_len) => msg_len, Err(e) => { error!(?e, "Failed to write initial handshake message"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } }; let initial_msg: Vec = initial_msg_buffer .get(..initial_msg_len) .map(<[u8]>::to_vec) - .ok_or(TransportError::ConnectionFailed)?; + .ok_or(CableError::ConnectionFailed)?; trace!( { handshake = ?initial_msg }, "Sending initial handshake message" @@ -232,7 +232,7 @@ pub(crate) async fn do_handshake( } Ok(None) => { error!("Connection was closed before handshake was complete"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } Err(e) => { error!(?e, "Failed to read handshake response"); @@ -245,7 +245,7 @@ pub(crate) async fn do_handshake( { len = response.len() }, "Peer handshake message is too short" ); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } let mut payload = [0u8; 1024]; @@ -253,7 +253,7 @@ pub(crate) async fn do_handshake( Ok(len) => len, Err(e) => { error!(?e, "Failed to read handshake response message"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } }; @@ -264,7 +264,7 @@ pub(crate) async fn do_handshake( if !noise_handshake.is_handshake_finished() { error!("Handshake did not complete"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } Ok(TunnelNoiseState { @@ -275,7 +275,7 @@ pub(crate) async fn do_handshake( /// Returns `Ok(())` on a clean close and `Err(_)` on any fault that leaves /// the encrypted channel unusable; callers surface `Err(_)` via `send_error`. -pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), TransportError> { +pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), CableError> { let get_info_response_serialized: Vec = match input.data_channel.recv().await { Ok(Some(message)) => match connection_recv_initial(message, &mut input.noise_state).await { Ok(initial) => initial, @@ -286,7 +286,7 @@ pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), T }, Ok(None) => { error!("Connection closed before initial message was received"); - return Err(TransportError::ConnectionLost); + return Err(CableError::ConnectionLost); } Err(e) => { error!(?e, "Failed to read initial message"); @@ -338,7 +338,7 @@ pub(crate) async fn connection(mut input: TunnelConnectionInput) -> Result<(), T let response = CborResponse::new_success_from_slice(&get_info_response_serialized); if let Err(e) = input.cbor_rx_send.send(response).await { error!(?e, "CBOR response receiver dropped"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } } _ => { @@ -364,19 +364,17 @@ async fn connection_send( request: CborRequest, data_channel: &mut dyn CableDataChannel, noise_state: &mut TunnelNoiseState, -) -> Result<(), TransportError> { +) -> Result<(), CableError> { debug!("Sending CBOR request"); trace!(?request); - let cbor_request = request - .raw_long() - .map_err(|e| TransportError::IoError(e.kind()))?; + let cbor_request = request.raw_long().map_err(CableError::from)?; if cbor_request.len() > MAX_CBOR_SIZE { error!( cbor_request_len = cbor_request.len(), "CBOR request too large" ); - return Err(TransportError::InvalidFraming); + return Err(CableError::InvalidFraming); } trace!(?cbor_request, cbor_request_len = cbor_request.len()); @@ -403,7 +401,7 @@ async fn connection_send( } Err(e) => { error!(?e, "Failed to encrypt frame"); - return Err(TransportError::EncryptionFailed); + return Err(CableError::EncryptionFailed); } } @@ -417,12 +415,12 @@ async fn connection_send( /// Strip the trailing padding-length byte and `padding_len` bytes of padding /// from a decrypted Noise transport frame, returning `InvalidFraming` on an /// empty plaintext or a declared padding length that exceeds the frame. -fn strip_frame_padding(mut decrypted_frame: Vec) -> Result, TransportError> { +fn strip_frame_padding(mut decrypted_frame: Vec) -> Result, CableError> { let padding_len = match decrypted_frame.last() { Some(&b) => b as usize, None => { error!("Decrypted frame is empty; cannot read padding length"); - return Err(TransportError::InvalidFraming); + return Err(CableError::InvalidFraming); } }; let new_len = decrypted_frame @@ -433,7 +431,7 @@ fn strip_frame_padding(mut decrypted_frame: Vec) -> Result, Transpor frame_len = decrypted_frame.len(), padding_len, "Padding length exceeds frame length" ); - TransportError::InvalidFraming + CableError::InvalidFraming })?; decrypted_frame.truncate(new_len); Ok(decrypted_frame) @@ -442,7 +440,7 @@ fn strip_frame_padding(mut decrypted_frame: Vec) -> Result, Transpor async fn decrypt_frame( encrypted_frame: Vec, noise_state: &mut TunnelNoiseState, -) -> Result, TransportError> { +) -> Result, CableError> { let mut decrypted_frame = vec![0u8; MAX_CBOR_SIZE]; match noise_state .transport_state @@ -455,7 +453,7 @@ async fn decrypt_frame( } Err(e) => { error!(?e, "Failed to decrypt CBOR response"); - return Err(TransportError::EncryptionFailed); + return Err(CableError::EncryptionFailed); } } @@ -472,14 +470,14 @@ async fn decrypt_frame( async fn connection_recv_initial( encrypted_frame: Vec, noise_state: &mut TunnelNoiseState, -) -> Result, TransportError> { +) -> Result, CableError> { let decrypted_frame = decrypt_frame(encrypted_frame, noise_state).await?; let initial_message: CableInitialMessage = match cbor::from_slice(&decrypted_frame) { Ok(initial_message) => initial_message, Err(e) => { error!(?e, "Failed to decode initial message"); - return Err(TransportError::InvalidFraming); + return Err(CableError::InvalidFraming); } }; @@ -487,16 +485,14 @@ async fn connection_recv_initial( Ok(get_info_response) => get_info_response, Err(e) => { error!(?e, "Failed to decode GetInfo response"); - return Err(TransportError::InvalidFraming); + return Err(CableError::InvalidFraming); } }; Ok(initial_message.info.to_vec()) } -async fn connection_recv_update( - message: &[u8], -) -> Result, TransportError> { +async fn connection_recv_update(message: &[u8]) -> Result, CableError> { // TODO(#66): Android adds a 999-key to the end the message, which is not part of the standard. // For now, we parse the message to a map and manuually import fields. @@ -504,7 +500,7 @@ async fn connection_recv_update( Ok(update_message) => update_message, Err(e) => { error!(?e, "Failed to decode update message"); - return Err(TransportError::InvalidFraming); + return Err(CableError::InvalidFraming); } }; @@ -566,7 +562,7 @@ async fn connection_recv( encrypted_frame: Vec, cbor_rx_send: &Sender, noise_state: &mut TunnelNoiseState, -) -> Result { +) -> Result { let decrypted_frame = decrypt_frame(encrypted_frame, noise_state).await?; let cable_message: CableTunnelMessage = CableTunnelMessage::from_slice(&decrypted_frame) @@ -581,14 +577,14 @@ async fn connection_recv( CableTunnelMessageType::Ctap => { let cbor_response: CborResponse = (&cable_message.payload.to_vec()) .try_into() - .or(Err(TransportError::InvalidFraming))?; + .or(Err(CableError::InvalidFraming))?; debug!("Received CBOR response"); trace!(?cbor_response); cbor_rx_send .send(cbor_response) .await - .or(Err(TransportError::ConnectionFailed))?; + .or(Err(CableError::ConnectionFailed))?; Ok(RecvOutcome::Continue) } CableTunnelMessageType::Update => { @@ -664,7 +660,7 @@ fn parse_known_device( tunnel_domain: &str, linking_info: &CableLinkingInfo, handshake_hash: &[u8], -) -> Result { +) -> Result { let known_device = CableKnownDeviceInfo::new(tunnel_domain, linking_info)?; let secret_key = SecretKey::from(private_key); @@ -672,7 +668,7 @@ fn parse_known_device( PublicKey::from_sec1_bytes(&linking_info.authenticator_public_key) else { error!("Failed to parse public key."); - return Err(TransportError::InvalidKey); + return Err(CableError::InvalidKey); }; let shared_secret: Vec = ecdh::diffie_hellman( @@ -683,14 +679,14 @@ fn parse_known_device( .to_vec(); let mut hmac = - Hmac::::new_from_slice(&shared_secret).map_err(|_| TransportError::InvalidKey)?; + Hmac::::new_from_slice(&shared_secret).map_err(|_| CableError::InvalidKey)?; hmac.update(handshake_hash); let expected_mac = hmac.finalize().into_bytes().to_vec(); if expected_mac != linking_info.handshake_signature { error!("Invalid handshake signature, rejecting update message"); trace!(?expected_mac, ?linking_info.handshake_signature); - return Err(TransportError::InvalidSignature); + return Err(CableError::InvalidSignature); } debug!("Parsed known device with valid signature"); @@ -780,7 +776,7 @@ mod tests { #[test] fn strip_frame_padding_rejects_empty() { let result = strip_frame_padding(Vec::new()); - assert!(matches!(result, Err(TransportError::InvalidFraming))); + assert!(matches!(result, Err(CableError::InvalidFraming))); } #[test] @@ -788,7 +784,7 @@ mod tests { // Length 1 + declared padding of 5 -> would require subtracting 6 from 1. let frame = vec![0x05u8]; let result = strip_frame_padding(frame); - assert!(matches!(result, Err(TransportError::InvalidFraming))); + assert!(matches!(result, Err(CableError::InvalidFraming))); } #[test] diff --git a/libwebauthn/src/transport/cable/qr_code_device.rs b/libwebauthn/src/transport/cable/qr_code_device.rs index d3b065b9..5f85e8a3 100644 --- a/libwebauthn/src/transport/cable/qr_code_device.rs +++ b/libwebauthn/src/transport/cable/qr_code_device.rs @@ -25,10 +25,10 @@ use super::tunnel::KNOWN_TUNNEL_DOMAINS; use super::{channel::CableChannel, channel::ConnectionState, Cable}; use crate::proto::ctap2::cbor; use crate::transport::cable::digit_encode; +use crate::transport::cable::error::CableError; use crate::transport::ChannelSettings; use crate::transport::Device; -use crate::webauthn::error::Error; -use crate::webauthn::TransportError; +use crate::webauthn::error::WebAuthnError; #[derive(Debug, Clone, Copy, Serialize, PartialEq)] pub enum QrCodeOperationHint { @@ -153,7 +153,7 @@ impl CableQrCodeDevice { hint: QrCodeOperationHint, store: Arc, transports: CableTransports, - ) -> Result { + ) -> Result { Self::new(hint, true, Some(store), transports) } @@ -162,7 +162,7 @@ impl CableQrCodeDevice { state_assisted: bool, store: Option>, transports: CableTransports, - ) -> Result { + ) -> Result { let private_key_scalar = NonZeroScalar::random(&mut OsRng); let private_key = SecretKey::from(private_key_scalar); let public_key: [u8; 33] = private_key @@ -171,7 +171,7 @@ impl CableQrCodeDevice { .to_encoded_point(true) .as_bytes() .try_into() - .map_err(|_| Error::Transport(TransportError::InvalidKey))?; + .map_err(|_| CableError::InvalidKey)?; let mut qr_secret = [0u8; 16]; OsRng.fill_bytes(&mut qr_secret); @@ -206,7 +206,7 @@ impl CableQrCodeDevice { pub fn new_transient( hint: QrCodeOperationHint, transports: CableTransports, - ) -> Result { + ) -> Result { Self::new(hint, false, None, transports) } @@ -214,7 +214,7 @@ impl CableQrCodeDevice { async fn connection( qr_device: &CableQrCodeDevice, ux_sender: &MpscUxUpdateSender, - ) -> Result { + ) -> Result { // Stage 1: Proximity check let proximity_input = ProximityCheckInput::new_for_qr_code(qr_device)?; let proximity_output = proximity_check_stage(proximity_input, ux_sender).await?; @@ -240,7 +240,10 @@ impl Display for CableQrCodeDevice { #[async_trait] impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice { - async fn channel(&'d mut self, settings: ChannelSettings) -> Result { + async fn channel( + &'d mut self, + settings: ChannelSettings, + ) -> Result> { let (ux_update_sender, _) = broadcast::channel(16); let (cbor_tx_send, cbor_tx_recv) = mpsc::channel(16); let (cbor_rx_send, cbor_rx_recv) = mpsc::channel(16); @@ -257,11 +260,7 @@ impl<'d> Device<'d, Cable, CableChannel> for CableQrCodeDevice { let handshake_output = match Self::connection(&qr_device, &ux_sender).await { Ok(handshake_output) => handshake_output, Err(e) => { - let transport_err = match e { - Error::Transport(t) => t, - _ => TransportError::ConnectionFailed, - }; - ux_sender.send_error(transport_err).await; + ux_sender.send_error(e).await; return; } }; diff --git a/libwebauthn/src/transport/cable/tunnel.rs b/libwebauthn/src/transport/cable/tunnel.rs index 1f710ac2..528bcc66 100644 --- a/libwebauthn/src/transport/cable/tunnel.rs +++ b/libwebauthn/src/transport/cable/tunnel.rs @@ -13,7 +13,7 @@ use super::error::CableTunnelError; use super::known_devices::CableKnownDeviceId; use super::protocol::CableTunnelConnectionType; use crate::proto::ctap2::cbor; -use crate::transport::error::TransportError; +use crate::transport::cable::error::CableError; const MAX_TUNNEL_REDIRECTS: usize = 5; @@ -66,42 +66,30 @@ pub fn decode_tunnel_server_domain(encoded: u16) -> Option { pub(crate) fn build_tunnel_request( url: &str, connection_type: &CableTunnelConnectionType, -) -> Result { - let mut request = url - .into_client_request() - .or(Err(TransportError::InvalidEndpoint))?; +) -> Result { + let mut request = url.into_client_request().map_err(CableError::from)?; let headers = request.headers_mut(); - headers.insert( - "Sec-WebSocket-Protocol", - "fido.cable" - .parse() - .or(Err(TransportError::InvalidEndpoint))?, - ); + headers.insert("Sec-WebSocket-Protocol", "fido.cable".parse()?); if let CableTunnelConnectionType::KnownDevice { client_payload, .. } = connection_type { - let client_payload = - cbor::to_vec(client_payload).or(Err(TransportError::InvalidEndpoint))?; + let client_payload = cbor::to_vec(client_payload)?; headers.insert( "X-caBLE-Client-Payload", - hex::encode(client_payload) - .parse() - .or(Err(TransportError::InvalidEndpoint))?, + hex::encode(client_payload).parse()?, ); } Ok(request) } /// Resolves a redirect Location, which may be relative, against the current URL. -fn resolve_redirect_target(base: &str, location: &str) -> Result { - let base = Url::parse(base).or(Err(TransportError::InvalidEndpoint))?; - let target = base - .join(location) - .or(Err(TransportError::InvalidEndpoint))?; +fn resolve_redirect_target(base: &str, location: &str) -> Result { + let base = Url::parse(base)?; + let target = base.join(location)?; Ok(target.to_string()) } /// Maps a non-101 tunnel handshake status to a transport error, distinguishing 410 Gone. -fn tunnel_status_error(status: StatusCode) -> TransportError { +fn tunnel_status_error(status: StatusCode) -> CableError { if status == StatusCode::GONE { CableTunnelError::Gone.into() } else { @@ -111,12 +99,12 @@ fn tunnel_status_error(status: StatusCode) -> TransportError { /// The known-device id to forget on a 410 Gone, for a known-device connection. pub(crate) fn known_device_id_to_forget( - error: &TransportError, + error: &CableError, connection_type: &CableTunnelConnectionType, ) -> Option { match (error, connection_type) { ( - TransportError::CableTunnel(CableTunnelError::Gone), + CableError::CableTunnel(CableTunnelError::Gone), CableTunnelConnectionType::KnownDevice { authenticator_public_key, .. @@ -129,7 +117,7 @@ pub(crate) fn known_device_id_to_forget( pub(crate) async fn connect( tunnel_domain: &str, connection_type: &CableTunnelConnectionType, -) -> Result>, TransportError> { +) -> Result>, CableError> { ensure_rustls_crypto_provider(); let mut connect_url = match connection_type { @@ -156,7 +144,7 @@ pub(crate) async fn connect( debug!(?response, "Connected to tunnel server"); if response.status() != StatusCode::SWITCHING_PROTOCOLS { error!(?response, "Failed to switch to websocket protocol"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); } debug!("Tunnel server returned success"); return Ok(ws_stream); @@ -164,9 +152,12 @@ pub(crate) async fn connect( Err(error) => error, }; - let TungsteniteError::Http(response) = error else { - error!(?error, "Failed to connect to tunnel server"); - return Err(TransportError::ConnectionFailed); + let response = match error { + TungsteniteError::Http(response) => response, + error => { + error!(?error, "Failed to connect to tunnel server"); + return Err(CableError::from(error)); + } }; let status = response.status(); @@ -177,7 +168,7 @@ pub(crate) async fn connect( .and_then(|value| value.to_str().ok()) else { error!(?status, "Tunnel redirect missing a usable Location header"); - return Err(TransportError::ConnectionFailed); + return Err(CableError::ConnectionFailed); }; connect_url = resolve_redirect_target(&connect_url, location)?; debug!(?connect_url, "Following tunnel redirect"); @@ -291,7 +282,7 @@ mod tests { let connection_type = known_device_connection_type(public_key.clone()); assert_eq!( known_device_id_to_forget( - &TransportError::CableTunnel(CableTunnelError::Gone), + &CableError::CableTunnel(CableTunnelError::Gone), &connection_type ), Some(hex::encode(&public_key)) @@ -303,7 +294,7 @@ mod tests { let connection_type = qr_connection_type(); assert_eq!( known_device_id_to_forget( - &TransportError::CableTunnel(CableTunnelError::Gone), + &CableError::CableTunnel(CableTunnelError::Gone), &connection_type ), None @@ -314,20 +305,20 @@ mod tests { fn non_gone_error_does_not_forget_known_device() { let connection_type = known_device_connection_type(vec![7u8; 65]); assert_eq!( - known_device_id_to_forget(&TransportError::ConnectionFailed, &connection_type), + known_device_id_to_forget(&CableError::ConnectionFailed, &connection_type), None ); } #[test] fn gone_status_maps_to_distinct_error() { - assert_eq!( + assert!(matches!( tunnel_status_error(StatusCode::GONE), - TransportError::CableTunnel(CableTunnelError::Gone) - ); - assert_eq!( + CableError::CableTunnel(CableTunnelError::Gone) + )); + assert!(matches!( tunnel_status_error(StatusCode::BAD_GATEWAY), - TransportError::CableTunnel(CableTunnelError::UnexpectedStatus(502)) - ); + CableError::CableTunnel(CableTunnelError::UnexpectedStatus(502)) + )); } } diff --git a/libwebauthn/src/transport/channel.rs b/libwebauthn/src/transport/channel.rs index 105a14c4..29601be6 100644 --- a/libwebauthn/src/transport/channel.rs +++ b/libwebauthn/src/transport/channel.rs @@ -10,7 +10,7 @@ use crate::proto::{ ctap1::apdu::{ApduRequest, ApduResponse}, ctap2::cbor::{CborRequest, CborResponse}, }; -use crate::webauthn::error::Error; +use crate::webauthn::error::WebAuthnError; use crate::Transport; use crate::UvUpdate; @@ -44,6 +44,9 @@ pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore { /// UX updates for this channel, must include UV updates. type UxUpdate: Send + Sync + Debug + From; + /// Per-transport concrete error. Set by each transport to its own enum. + type TransportError: std::error::Error + Send + Sync + 'static; + /// Broadcast sender fanning UX updates out to subscribed receivers. fn get_ux_update_sender(&self) -> &broadcast::Sender; @@ -64,7 +67,9 @@ pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore { }; } - async fn supported_protocols(&self) -> Result; + async fn supported_protocols( + &self, + ) -> Result>; async fn status(&self) -> ChannelStatus; async fn close(&mut self); @@ -74,11 +79,19 @@ pub trait Channel: Send + Sync + Display + Ctap2AuthTokenStore { Transport::Usb } - async fn apdu_send(&mut self, request: &ApduRequest, timeout: Duration) -> Result<(), Error>; - async fn apdu_recv(&mut self, timeout: Duration) -> Result; + async fn apdu_send( + &mut self, + request: &ApduRequest, + timeout: Duration, + ) -> Result<(), Self::TransportError>; + async fn apdu_recv(&mut self, timeout: Duration) -> Result; - async fn cbor_send(&mut self, request: &CborRequest, timeout: Duration) -> Result<(), Error>; - async fn cbor_recv(&mut self, timeout: Duration) -> Result; + async fn cbor_send( + &mut self, + request: &CborRequest, + timeout: Duration, + ) -> Result<(), Self::TransportError>; + async fn cbor_recv(&mut self, timeout: Duration) -> Result; /// Allows channels to disable support for pre-flight requests fn supports_preflight() -> bool { diff --git a/libwebauthn/src/transport/device.rs b/libwebauthn/src/transport/device.rs index e6fd21cd..826c772f 100644 --- a/libwebauthn/src/transport/device.rs +++ b/libwebauthn/src/transport/device.rs @@ -4,7 +4,7 @@ use crate::fido::FidoRevision; use async_trait::async_trait; use crate::transport::ble::btleplug::manager::SupportedRevisions; -use crate::webauthn::error::Error; +use crate::webauthn::error::WebAuthnError; use super::{Channel, ChannelSettings, Transport}; @@ -14,8 +14,10 @@ where T: Transport, C: Channel + 'd, { - async fn channel(&'d mut self, settings: ChannelSettings) -> Result; - // async fn supported_protocols(&mut self) -> Result; + async fn channel( + &'d mut self, + settings: ChannelSettings, + ) -> Result>; } #[derive(Debug, Copy, Clone, Default)] diff --git a/libwebauthn/src/transport/error.rs b/libwebauthn/src/transport/error.rs index d5cdf1a0..2200ec0a 100644 --- a/libwebauthn/src/transport/error.rs +++ b/libwebauthn/src/transport/error.rs @@ -1,39 +1,4 @@ -use crate::transport::cable::error::CableTunnelError; - -#[derive(thiserror::Error, Debug, PartialEq, Clone)] -pub enum TransportError { - #[error("connection failed")] - ConnectionFailed, - #[error("connection lost")] - ConnectionLost, - /// An error from the caBLE tunnel-server transport. - #[error(transparent)] - CableTunnel(#[from] CableTunnelError), - #[error("invalid endpoint")] - InvalidEndpoint, - #[error("invalid framing")] - InvalidFraming, - #[error("negotiation failed")] - NegotiationFailed, - #[error("transport unavailable")] - TransportUnavailable, - #[error("timeout")] - Timeout, - #[error("device not found")] - UnknownDevice, - #[error("invalid key")] - InvalidKey, - #[error("invalid signature")] - InvalidSignature, - #[error("input/output error: {0}")] - IoError(std::io::ErrorKind), - /// Noise transport-mode encrypt or decrypt failed; the channel is unusable. - #[error("encryption failed")] - EncryptionFailed, -} - -impl From for TransportError { - fn from(_error: snow::Error) -> Self { - TransportError::NegotiationFailed - } -} +//! Per-transport errors live with each transport (e.g. `hid::HidError`, +//! `ble::BleError`, `cable::CableError`). The ceremony error is +//! [`WebAuthnError`](crate::webauthn::error::WebAuthnError), generic over the +//! channel's concrete transport error. diff --git a/libwebauthn/src/transport/hid/channel.rs b/libwebauthn/src/transport/hid/channel.rs index 4a1778df..10925441 100644 --- a/libwebauthn/src/transport/hid/channel.rs +++ b/libwebauthn/src/transport/hid/channel.rs @@ -27,13 +27,13 @@ use crate::transport::channel::{ AuthTokenData, Channel, ChannelSettings, ChannelStatus, Ctap2AuthTokenStore, }; use crate::transport::device::SupportedProtocols; -use crate::transport::error::TransportError; #[cfg(feature = "virt")] use crate::transport::hid::device::HidPipeBackend; +use crate::transport::hid::error::HidError; use crate::transport::hid::framing::{ HidCommand, HidMessage, HidMessageParser, HidMessageParserState, }; -use crate::webauthn::error::{Error, PlatformError}; +use crate::webauthn::error::WebAuthnError; use crate::Transport; use crate::UvUpdate; @@ -95,7 +95,7 @@ impl<'d> HidChannel<'d> { pub async fn new( device: &'d HidDevice, settings: ChannelSettings, - ) -> Result, Error> { + ) -> Result, WebAuthnError> { let (ux_update_sender, _) = broadcast::channel(16); let (handle_tx, handle_rx) = mpsc::channel(1); let handle = HidChannelHandle { tx: handle_tx }; @@ -105,7 +105,7 @@ impl<'d> HidChannel<'d> { device, open_device: match &device.backend { HidBackendDevice::HidApiDevice(_) => { - let hidapi_device = Self::hid_open(device)?; + let hidapi_device = Self::hid_open(device).map_err(WebAuthnError::Transport)?; OpenHidDevice::HidApiDevice(Arc::new(Mutex::new((hidapi_device, handle_rx)))) } #[cfg(feature = "virt")] @@ -122,7 +122,10 @@ impl<'d> HidChannel<'d> { #[cfg(feature = "virt")] pin_protocol_override: None, }; - channel.init = channel.init(INIT_TIMEOUT).await?; + channel.init = channel + .init(INIT_TIMEOUT) + .await + .map_err(WebAuthnError::Transport)?; Ok(channel) } @@ -131,7 +134,7 @@ impl<'d> HidChannel<'d> { } #[instrument(skip_all)] - pub async fn wink(&mut self, timeout: Duration) -> Result { + pub async fn wink(&mut self, timeout: Duration) -> Result { if !self.init.caps.contains(Caps::WINK) { warn!(?self.init.caps, "WINK capability is not supported"); return Ok(false); @@ -150,7 +153,7 @@ impl<'d> HidChannel<'d> { pub async fn blink_and_wait_for_user_presence( &mut self, timeout: Duration, - ) -> Result { + ) -> Result> { let supported = self.supported_protocols().await?; if supported.fido2 { let get_info_response = self.ctap2_get_info().await?; @@ -164,9 +167,9 @@ impl<'d> HidChannel<'d> { let ctap2_request = Ctap2MakeCredentialRequest::dummy(); match self.ctap2_make_credential(&ctap2_request, timeout).await { Ok(_) - | Err(Error::Ctap(CtapError::PINInvalid)) - | Err(Error::Ctap(CtapError::PINAuthInvalid)) - | Err(Error::Ctap(CtapError::PINNotSet)) => Ok(true), + | Err(WebAuthnError::Ctap(CtapError::PINInvalid)) + | Err(WebAuthnError::Ctap(CtapError::PINAuthInvalid)) + | Err(WebAuthnError::Ctap(CtapError::PINNotSet)) => Ok(true), Err(_) => Ok(false), } } @@ -175,9 +178,9 @@ impl<'d> HidChannel<'d> { let register_request = Ctap1RegisterRequest::dummy(timeout); match self.ctap1_register(®ister_request).await { Ok(_) - | Err(Error::Ctap(CtapError::PINInvalid)) - | Err(Error::Ctap(CtapError::PINAuthInvalid)) - | Err(Error::Ctap(CtapError::PINNotSet)) => Ok(true), + | Err(WebAuthnError::Ctap(CtapError::PINInvalid)) + | Err(WebAuthnError::Ctap(CtapError::PINAuthInvalid)) + | Err(WebAuthnError::Ctap(CtapError::PINNotSet)) => Ok(true), Err(_) => Ok(false), } } else { @@ -187,7 +190,7 @@ impl<'d> HidChannel<'d> { } #[instrument(level = Level::DEBUG, skip_all)] - async fn init(&mut self, timeout: Duration) -> Result { + async fn init(&mut self, timeout: Duration) -> Result { let nonce: [u8; 8] = thread_rng().gen(); let request = HidMessage::broadcast(HidCommand::Init, &nonce); @@ -196,7 +199,7 @@ impl<'d> HidChannel<'d> { if response.cmd != HidCommand::Init { warn!(?response.cmd, "Invalid response to INIT request"); - return Err(Error::Transport(TransportError::InvalidEndpoint)); + return Err(HidError::InvalidInit); } if response.payload.len() < INIT_PAYLOAD_LEN { @@ -204,56 +207,44 @@ impl<'d> HidChannel<'d> { { len = response.payload.len() }, "INIT payload is too small" ); - return Err(Error::Transport(TransportError::InvalidEndpoint)); + return Err(HidError::InvalidInit); } let payload_nonce = response .payload .get(..INIT_NONCE_LEN) - .ok_or(Error::Transport(TransportError::InvalidEndpoint))?; + .ok_or(HidError::InvalidInit)?; if payload_nonce != nonce.as_slice() { warn!("INIT nonce mismatch. Terminating."); - return Err(Error::Transport(TransportError::InvalidEndpoint)); + return Err(HidError::InvalidInit); } let mut cursor = IOCursor::new(response.payload); cursor .seek(SeekFrom::Start(8)) - .map_err(|e| Error::Transport(TransportError::IoError(e.kind())))?; + .map_err(HidError::InitResponseParse)?; let init = InitResponse { cid: cursor .read_u32::() - .map_err(|e| Error::Transport(TransportError::IoError(e.kind())))?, - protocol_version: cursor - .read_u8() - .map_err(|e| Error::Transport(TransportError::IoError(e.kind())))?, - version_major: cursor - .read_u8() - .map_err(|e| Error::Transport(TransportError::IoError(e.kind())))?, - version_minor: cursor - .read_u8() - .map_err(|e| Error::Transport(TransportError::IoError(e.kind())))?, - version_build: cursor - .read_u8() - .map_err(|e| Error::Transport(TransportError::IoError(e.kind())))?, - caps: Caps::from_bits_truncate( - cursor - .read_u8() - .map_err(|e| Error::Transport(TransportError::IoError(e.kind())))?, - ), + .map_err(HidError::InitResponseParse)?, + protocol_version: cursor.read_u8().map_err(HidError::InitResponseParse)?, + version_major: cursor.read_u8().map_err(HidError::InitResponseParse)?, + version_minor: cursor.read_u8().map_err(HidError::InitResponseParse)?, + version_build: cursor.read_u8().map_err(HidError::InitResponseParse)?, + caps: Caps::from_bits_truncate(cursor.read_u8().map_err(HidError::InitResponseParse)?), }; debug!(?init, "Device init complete"); Ok(init) } - fn hid_open(device: &HidDevice) -> Result { + fn hid_open(device: &HidDevice) -> Result { let hidapi = get_hidapi()?; match &device.backend { - HidBackendDevice::HidApiDevice(device) => Ok(device - .open_device(&hidapi) - .or(Err(Error::Transport(TransportError::ConnectionFailed)))?), + HidBackendDevice::HidApiDevice(device) => { + Ok(device.open_device(&hidapi).map_err(HidError::Open)?) + } #[cfg(feature = "virt")] #[allow(clippy::unreachable)] // virtual devices never go through hid_open @@ -262,7 +253,7 @@ impl<'d> HidChannel<'d> { } #[instrument(level = Level::DEBUG, skip_all)] - pub async fn hid_cancel(&self) -> Result<(), Error> { + pub async fn hid_cancel(&self) -> Result<(), HidError> { self.hid_send(&HidMessage::new(self.init.cid, HidCommand::Cancel, &[])) .await } @@ -310,16 +301,16 @@ impl<'d> HidChannel<'d> { */ #[instrument(skip_all, fields(cmd = ?msg.cmd, payload_len = msg.payload.len()))] - pub async fn hid_send(&self, msg: &HidMessage) -> Result<(), Error> { + pub async fn hid_send(&self, msg: &HidMessage) -> Result<(), HidError> { match &self.open_device { OpenHidDevice::HidApiDevice(hidapi_device) => { let Ok(mut guard) = hidapi_device.lock() else { warn!("Poisoned lock on HID API device"); - return Err(Error::Transport(TransportError::ConnectionLost)); + return Err(HidError::DeviceLockPoisoned); }; let (device, cancel_rx) = guard.deref_mut(); let response = Self::hid_send_hidapi(device, cancel_rx, msg); - if matches!(response, Err(Error::Platform(PlatformError::Cancelled))) { + if matches!(response, Err(HidError::Cancelled)) { // Using hid_send_hidapi directly, instead of hid_cancel, to avoid recursion let _ = Self::hid_send_hidapi( device, @@ -346,13 +337,11 @@ impl<'d> HidChannel<'d> { device: &hidapi::HidDevice, cancel_rx: &mut Receiver, msg: &HidMessage, - ) -> Result<(), Error> { - let packets = msg - .packets(PACKET_SIZE) - .or(Err(Error::Transport(TransportError::InvalidFraming)))?; + ) -> Result<(), HidError> { + let packets = msg.packets(PACKET_SIZE).map_err(HidError::PacketEncode)?; for (i, packet) in packets.iter().enumerate() { if !matches!(cancel_rx.try_recv(), Err(TryRecvError::Empty)) { - return Err(Error::Platform(PlatformError::Cancelled)); + return Err(HidError::Cancelled); } let mut report: Vec = vec![REPORT_ID]; @@ -360,15 +349,13 @@ impl<'d> HidChannel<'d> { report.extend(vec![0; PACKET_SIZE - packet.len()]); debug!({ packet = i, len = report.len() }, "Sending packet as HID report",); trace!(?report); - device - .write(&report) - .or(Err(Error::Transport(TransportError::ConnectionLost)))?; + device.write(&report).map_err(HidError::Write)?; } Ok(()) } #[instrument(skip_all)] - pub async fn hid_recv(&self, timeout: Duration) -> Result { + pub async fn hid_recv(&self, timeout: Duration) -> Result { loop { let response = match &self.open_device { OpenHidDevice::HidApiDevice(hidapi_device) => { @@ -381,16 +368,13 @@ impl<'d> HidChannel<'d> { tokio::task::spawn_blocking(move || { let Ok(mut guard) = device.lock() else { warn!("Poisoned lock on HID API device"); - return Err(Error::Transport(TransportError::ConnectionLost)); + return Err(HidError::DeviceLockPoisoned); }; let (device, cancel_rx) = guard.deref_mut(); Self::hid_recv_hidapi(device, cancel_rx, timeout) }) .await - .map_err(|e| { - warn!(?e, "HID read task failed"); - Error::Transport(TransportError::ConnectionLost) - })? + .map_err(HidError::TaskJoin)? } #[cfg(feature = "virt")] #[allow(clippy::panic)] @@ -411,8 +395,7 @@ impl<'d> HidChannel<'d> { debug!("Ignoring HID keep-alive"); continue; } - Err(Error::Platform(PlatformError::Cancelled)) - | Err(Error::Transport(TransportError::Timeout)) => { + Err(HidError::Cancelled) | Err(HidError::Timeout) => { // CTAP 2.2 §11.2.9.1.5: send CTAPHID_CANCEL when the // platform gives up (caller cancelled or wall-clock // budget exhausted). @@ -428,12 +411,12 @@ impl<'d> HidChannel<'d> { device: &hidapi::HidDevice, cancel_rx: &mut Receiver, timeout: Duration, - ) -> Result { + ) -> Result { let mut parser = HidMessageParser::new(); let deadline = Instant::now().checked_add(timeout); loop { if !matches!(cancel_rx.try_recv(), Err(TryRecvError::Empty)) { - return Err(Error::Platform(PlatformError::Cancelled)); + return Err(HidError::Cancelled); } // Cap each read at HID_READ_POLL_INTERVAL so we re-check the @@ -445,14 +428,14 @@ impl<'d> HidChannel<'d> { }; if remaining.is_zero() { warn!("HID receive timed out before any data was read"); - return Err(Error::Transport(TransportError::Timeout)); + return Err(HidError::Timeout); } let read_for = remaining.min(HID_READ_POLL_INTERVAL); let mut report = [0; PACKET_SIZE]; let bytes_read = device .read_timeout(&mut report, read_for.as_millis() as i32) - .or(Err(Error::Transport(TransportError::ConnectionLost)))?; + .map_err(HidError::Read)?; if bytes_read == 0 { // hidapi signals per-iteration timeout as Ok(0); retry // against the remaining budget rather than passing the @@ -462,17 +445,14 @@ impl<'d> HidChannel<'d> { } debug!({ len = bytes_read }, "Received HID report"); trace!(?report); - if let HidMessageParserState::Done = parser - .update(&report) - .or(Err(Error::Transport(TransportError::InvalidFraming)))? + if let HidMessageParserState::Done = + parser.update(&report).map_err(HidError::FrameParse)? { break; } } - let response = parser - .message() - .or(Err(Error::Transport(TransportError::InvalidFraming)))?; + let response = parser.message().map_err(HidError::FrameParse)?; debug!({ cmd = ?response.cmd, payload_len = response.payload.len() }, "Received U2F HID response"); trace!(?response); Ok(response) @@ -505,12 +485,13 @@ impl Display for HidChannel<'_> { #[async_trait] impl Channel for HidChannel<'_> { type UxUpdate = UvUpdate; + type TransportError = HidError; fn transport(&self) -> Transport { Transport::Usb } - async fn supported_protocols(&self) -> Result { + async fn supported_protocols(&self) -> Result> { let cbor_supported = self.init.caps.contains(Caps::CBOR); let apdu_supported = !self.init.caps.contains(Caps::NO_MSG); Ok(SupportedProtocols { @@ -529,28 +510,30 @@ impl Channel for HidChannel<'_> { &mut self, request: &ApduRequest, _timeout: std::time::Duration, - ) -> Result<(), Error> { + ) -> Result<(), HidError> { let cid = self.init.cid; debug!({ cid }, "Sending APDU request"); trace!(?request); - let apdu_raw = request - .raw_long() - .map_err(|e| TransportError::IoError(e.kind()))?; + let apdu_raw = request.raw_long().map_err(HidError::PacketEncode)?; self.hid_send(&HidMessage::new(cid, HidCommand::Msg, &apdu_raw)) .await?; Ok(()) } - async fn apdu_recv(&mut self, timeout: std::time::Duration) -> Result { + async fn apdu_recv(&mut self, timeout: std::time::Duration) -> Result { let hid_response = self.hid_recv(timeout).await?; - let apdu_response = ApduResponse::try_from(&hid_response.payload) - .or(Err(Error::Transport(TransportError::InvalidFraming)))?; + let apdu_response = + ApduResponse::try_from(&hid_response.payload).map_err(HidError::ResponseDecode)?; debug!("Received APDU response"); trace!(?apdu_response); Ok(apdu_response) } - async fn cbor_send(&mut self, request: &CborRequest, _timeout: Duration) -> Result<(), Error> { + async fn cbor_send( + &mut self, + request: &CborRequest, + _timeout: Duration, + ) -> Result<(), HidError> { let cid = self.init.cid; debug!({ cid }, "Sending CBOR request"); trace!(?request); @@ -563,10 +546,10 @@ impl Channel for HidChannel<'_> { Ok(()) } - async fn cbor_recv(&mut self, timeout: Duration) -> Result { + async fn cbor_recv(&mut self, timeout: Duration) -> Result { let hid_response = self.hid_recv(timeout).await?; - let cbor_response = CborResponse::try_from(&hid_response.payload) - .or(Err(Error::Transport(TransportError::InvalidFraming)))?; + let cbor_response = + CborResponse::try_from(&hid_response.payload).map_err(HidError::ResponseDecode)?; debug!( { status = ?cbor_response.status_code }, "Received CBOR response" diff --git a/libwebauthn/src/transport/hid/device.rs b/libwebauthn/src/transport/hid/device.rs index c5365b58..6960a09a 100644 --- a/libwebauthn/src/transport/hid/device.rs +++ b/libwebauthn/src/transport/hid/device.rs @@ -11,10 +11,10 @@ use tracing::{debug, info, instrument}; #[cfg(feature = "virt")] use super::framing::HidMessage; -use crate::transport::error::TransportError; +use crate::transport::hid::error::HidError; use crate::transport::usb::{usb_id_from_hidraw, UsbDeviceId}; use crate::transport::{ChannelSettings, Device}; -use crate::webauthn::error::Error; +use crate::webauthn::error::WebAuthnError; #[cfg(feature = "virt")] pub trait HidPipeBackend: fmt::Debug + Send { @@ -68,12 +68,12 @@ impl fmt::Display for HidDevice { } } -pub(crate) fn get_hidapi() -> Result { - HidApi::new().or(Err(Error::Transport(TransportError::TransportUnavailable))) +pub(crate) fn get_hidapi() -> Result { + HidApi::new().map_err(HidError::ApiInit) } #[instrument] -pub async fn list_devices() -> Result, Error> { +pub async fn list_devices() -> Result, HidError> { let devices: Vec<_> = get_hidapi()? .device_list() .filter(|device| device.usage_page() == 0xF1D0) @@ -94,7 +94,10 @@ pub fn virtual_device(backend: B) -> HidDevice { #[async_trait] impl<'d> Device<'d, Hid, HidChannel<'d>> for HidDevice { - async fn channel(&'d mut self, settings: ChannelSettings) -> Result, Error> { + async fn channel( + &'d mut self, + settings: ChannelSettings, + ) -> Result, WebAuthnError> { let channel = HidChannel::new(self, settings).await?; Ok(channel) } diff --git a/libwebauthn/src/transport/hid/error.rs b/libwebauthn/src/transport/hid/error.rs new file mode 100644 index 00000000..db7abafb --- /dev/null +++ b/libwebauthn/src/transport/hid/error.rs @@ -0,0 +1,32 @@ +//! Errors specific to the USB HID transport. + +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum HidError { + #[error("failed to initialize hidapi: {0}")] + ApiInit(#[source] hidapi::HidError), + #[error("failed to open HID device: {0}")] + Open(#[source] hidapi::HidError), + #[error("failed to write to HID device: {0}")] + Write(#[source] hidapi::HidError), + #[error("failed to read from HID device: {0}")] + Read(#[source] hidapi::HidError), + #[error("failed to parse INIT response: {0}")] + InitResponseParse(#[source] std::io::Error), + #[error("failed to encode HID packets: {0}")] + PacketEncode(#[source] std::io::Error), + #[error("failed to parse HID frame: {0}")] + FrameParse(#[source] std::io::Error), + #[error("failed to decode device response: {0}")] + ResponseDecode(#[source] std::io::Error), + #[error("HID read task failed: {0}")] + TaskJoin(#[source] tokio::task::JoinError), + #[error("invalid INIT response")] + InvalidInit, + #[error("HID device lock poisoned")] + DeviceLockPoisoned, + #[error("HID operation timed out")] + Timeout, + #[error("HID operation cancelled")] + Cancelled, +} diff --git a/libwebauthn/src/transport/hid/mod.rs b/libwebauthn/src/transport/hid/mod.rs index e691e8d6..d09c9b9e 100644 --- a/libwebauthn/src/transport/hid/mod.rs +++ b/libwebauthn/src/transport/hid/mod.rs @@ -2,12 +2,14 @@ use std::fmt::Display; pub mod channel; pub mod device; +pub mod error; pub mod framing; pub mod init; pub use device::{list_devices, HidDevice}; #[cfg(feature = "virt")] pub use device::{virtual_device, HidPipeBackend}; +pub use error::HidError; use super::Transport; diff --git a/libwebauthn/src/transport/mock/channel.rs b/libwebauthn/src/transport/mock/channel.rs index 884af746..11d00398 100644 --- a/libwebauthn/src/transport/mock/channel.rs +++ b/libwebauthn/src/transport/mock/channel.rs @@ -13,9 +13,10 @@ use crate::{ transport::{ device::SupportedProtocols, AuthTokenData, Channel, ChannelStatus, Ctap2AuthTokenStore, }, - webauthn::Error, + webauthn::WebAuthnError, UvUpdate, }; +use std::convert::Infallible; pub struct MockChannel { expected_requests: VecDeque, @@ -97,12 +98,15 @@ impl Display for MockChannel { #[async_trait] impl Channel for MockChannel { type UxUpdate = UvUpdate; + type TransportError = Infallible; fn get_ux_update_sender(&self) -> &broadcast::Sender { &self.ux_update_sender } - async fn supported_protocols(&self) -> Result { + async fn supported_protocols( + &self, + ) -> Result> { Ok(SupportedProtocols { u2f: false, fido2: true, @@ -115,14 +119,25 @@ impl Channel for MockChannel { unimplemented!(); } - async fn apdu_send(&mut self, _request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + async fn apdu_send( + &mut self, + _request: &ApduRequest, + _timeout: Duration, + ) -> Result<(), Self::TransportError> { unimplemented!(); } - async fn apdu_recv(&mut self, _timeout: Duration) -> Result { + async fn apdu_recv( + &mut self, + _timeout: Duration, + ) -> Result { unimplemented!(); } - async fn cbor_send(&mut self, request: &CborRequest, _timeout: Duration) -> Result<(), Error> { + async fn cbor_send( + &mut self, + request: &CborRequest, + _timeout: Duration, + ) -> Result<(), Self::TransportError> { if let Some(delay) = self.pre_send_delay { sleep(delay).await; } @@ -138,7 +153,10 @@ impl Channel for MockChannel { ); Ok(()) } - async fn cbor_recv(&mut self, _timeout: Duration) -> Result { + async fn cbor_recv( + &mut self, + _timeout: Duration, + ) -> Result { let response = self .responses .pop_back() diff --git a/libwebauthn/src/transport/nfc/channel.rs b/libwebauthn/src/transport/nfc/channel.rs index 6b448e48..3f88ab71 100644 --- a/libwebauthn/src/transport/nfc/channel.rs +++ b/libwebauthn/src/transport/nfc/channel.rs @@ -1,9 +1,7 @@ -use apdu::core::HandleError; use apdu::{command, Command, Response}; -use apdu_core; use async_trait::async_trait; use std::fmt; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{Display, Formatter}; use std::sync::Arc; use std::time::Duration; use tokio::sync::broadcast; @@ -19,8 +17,8 @@ use crate::transport::channel::{ AuthTokenData, Channel, ChannelSettings, ChannelStatus, Ctap2AuthTokenStore, }; use crate::transport::device::SupportedProtocols; -use crate::transport::error::TransportError; -use crate::webauthn::Error; +use crate::transport::nfc::error::NfcError; +use crate::webauthn::error::WebAuthnError; use crate::Transport; use crate::UvUpdate; @@ -42,47 +40,16 @@ fn is_fido2_version(version: &[u8]) -> bool { pub type CancelNfcOperation = (); -#[derive(thiserror::Error)] -pub enum NfcError { - /// APDU error returned by the card. - Apdu(#[from] apdu::Error), - - /// Unexpected error occurred on the device. - Device(#[from] HandleError), -} - -impl From for Error { - fn from(input: NfcError) -> Self { - trace!("{:?}", input); - let output = match input { - NfcError::Apdu(_apdu_error) => TransportError::InvalidFraming, - NfcError::Device(_) => TransportError::ConnectionLost, - }; - Error::Transport(output) - } -} - -impl Debug for NfcError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for NfcError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - NfcError::Apdu(e) => Display::fmt(e, f), - NfcError::Device(e) => Display::fmt(e, f), - } - } -} - pub trait HandlerInCtx { /// Handles the APDU command in a specific context. /// Implementations must transmit the command to the card through a reader, /// then receive the response from them, returning length of the data written. - fn handle_in_ctx(&mut self, ctx: Ctx, command: &[u8], response: &mut [u8]) - -> apdu_core::Result; + fn handle_in_ctx( + &mut self, + ctx: Ctx, + command: &[u8], + response: &mut [u8], + ) -> Result; } pub trait NfcBackend: HandlerInCtx + Display {} @@ -159,15 +126,17 @@ where } #[instrument(skip_all)] - pub async fn wink(&mut self, _timeout: Duration) -> Result { + pub async fn wink(&mut self, _timeout: Duration) -> Result> { warn!("WINK capability is not supported"); Ok(false) } - pub async fn select_fido2(&mut self) -> Result<(), Error> { + pub async fn select_fido2(&mut self) -> Result<(), WebAuthnError> { // Given legacy support for CTAP1/U2F, the client MUST determine the capabilities of the device at the selection stage. let command = command::select_file(SELECT_P1, SELECT_P2, FIDO2_AID); - let response = self.handle(self.ctx, command)?; + let response = self + .handle(self.ctx, command) + .map_err(WebAuthnError::Transport)?; let mut u2f = false; let mut fido2 = false; if is_fido2_version(&response) { @@ -210,7 +179,7 @@ where let mut rapdu = Vec::new(); let len = self.handle_in_ctx(ctx, &command_buf, &mut buf)?; - let resp_bytes = buf.get(..len).ok_or(HandleError::NotEnoughBuffer(len))?; + let resp_bytes = buf.get(..len).ok_or(NfcError::BufferOverflow(len))?; let mut resp = Response::from(resp_bytes); let (mut sw1, mut sw2) = resp.trailer; @@ -220,7 +189,7 @@ where let get_response_cmd = command_get_response(0x00, 0x00, sw2); let get_response_buf = Vec::from(get_response_cmd); let len = self.handle_in_ctx(ctx, &get_response_buf, &mut buf)?; - let resp_bytes = buf.get(..len).ok_or(HandleError::NotEnoughBuffer(len))?; + let resp_bytes = buf.get(..len).ok_or(NfcError::BufferOverflow(len))?; resp = Response::from(resp_bytes); (sw1, sw2) = resp.trailer; rapdu.extend_from_slice(resp.payload); @@ -248,7 +217,7 @@ where pub async fn blink_and_wait_for_user_presence( &mut self, _timeout: Duration, - ) -> Result { + ) -> Result> { unimplemented!() } } @@ -259,12 +228,13 @@ where Ctx: Copy + Send + Sync + fmt::Debug + Display, { type UxUpdate = UvUpdate; + type TransportError = NfcError; fn transport(&self) -> Transport { Transport::Nfc } - async fn supported_protocols(&self) -> Result { + async fn supported_protocols(&self) -> Result> { Ok(self.supported) } @@ -275,21 +245,22 @@ where async fn close(&mut self) {} #[instrument(level = Level::DEBUG, skip_all)] - async fn apdu_send(&mut self, request: &ApduRequest, _timeout: Duration) -> Result<(), Error> { + async fn apdu_send( + &mut self, + request: &ApduRequest, + _timeout: Duration, + ) -> Result<(), NfcError> { let resp = self.handle_raw(self.ctx, request)?; trace!("apdu_send {:?}", resp); - let apdu_response = ApduResponse::try_from(&resp) - .or(Err(Error::Transport(TransportError::InvalidFraming)))?; + let apdu_response = ApduResponse::try_from(&resp).map_err(NfcError::ResponseDecode)?; self.apdu_response = Some(apdu_response); Ok(()) } #[instrument(level = Level::DEBUG, skip_all)] - async fn apdu_recv(&mut self, _timeout: Duration) -> Result { - self.apdu_response - .take() - .ok_or(Error::Transport(TransportError::InvalidFraming)) + async fn apdu_recv(&mut self, _timeout: Duration) -> Result { + self.apdu_response.take().ok_or(NfcError::NoResponse) } #[instrument(level = Level::DEBUG, skip_all)] @@ -297,7 +268,7 @@ where &mut self, request: &CborRequest, _timeout: std::time::Duration, - ) -> Result<(), Error> { + ) -> Result<(), NfcError> { let data = &request.ctap_hid_data(); let mut rest: &[u8] = data; @@ -337,17 +308,14 @@ where // return Err(Error::Transport(TransportError::InvalidFraming)); // } - let cbor_response = CborResponse::try_from(&resp) - .or(Err(Error::Transport(TransportError::InvalidFraming)))?; + let cbor_response = CborResponse::try_from(&resp).map_err(NfcError::ResponseDecode)?; self.cbor_response = Some(cbor_response); Ok(()) } #[instrument(level = Level::DEBUG, skip_all)] - async fn cbor_recv(&mut self, _timeout: std::time::Duration) -> Result { - self.cbor_response - .take() - .ok_or(Error::Transport(TransportError::InvalidFraming)) + async fn cbor_recv(&mut self, _timeout: std::time::Duration) -> Result { + self.cbor_response.take().ok_or(NfcError::NoResponse) } fn get_ux_update_sender(&self) -> &broadcast::Sender { @@ -393,6 +361,7 @@ mod tests { use crate::proto::ctap1::apdu::{ApduRequest, ApduResponseStatus}; use crate::proto::CtapError; use crate::transport::channel::Channel; + use crate::transport::nfc::error::NfcError; #[test] fn fido2_versions_are_recognised() { @@ -436,7 +405,7 @@ mod tests { _ctx: u8, _command: &[u8], response: &mut [u8], - ) -> apdu_core::Result { + ) -> Result { let n = self.response.len(); response[..n].copy_from_slice(&self.response); Ok(n) diff --git a/libwebauthn/src/transport/nfc/device.rs b/libwebauthn/src/transport/nfc/device.rs index d2934156..f5622a0d 100644 --- a/libwebauthn/src/transport/nfc/device.rs +++ b/libwebauthn/src/transport/nfc/device.rs @@ -6,10 +6,11 @@ use tracing::{debug, info, instrument, trace}; use crate::{ transport::{device::Device, hid::HidDevice, Channel, ChannelSettings, UsbDeviceId}, - webauthn::Error, + webauthn::error::WebAuthnError, }; use super::channel::NfcChannel; +use super::error::NfcError; #[cfg(feature = "nfc-backend-libnfc")] use super::libnfc; #[cfg(feature = "nfc-backend-pcsc")] @@ -72,14 +73,18 @@ impl NfcDevice { } } - async fn channel_sync(&self, settings: ChannelSettings) -> Result, Error> { + async fn channel_sync( + &self, + settings: ChannelSettings, + ) -> Result, WebAuthnError> { trace!("nfc channel {:?}", self); let mut channel: NfcChannel = match &self.info { #[cfg(feature = "nfc-backend-libnfc")] DeviceInfo::LibNfc(info) => info.channel(settings), #[cfg(feature = "nfc-backend-pcsc")] DeviceInfo::Pcsc(info) => info.channel(settings), - }?; + } + .map_err(WebAuthnError::Transport)?; channel.select_fido2().await?; Ok(channel) @@ -91,13 +96,13 @@ impl<'d> Device<'d, Nfc, NfcChannel> for NfcDevice { async fn channel( &'d mut self, settings: ChannelSettings, - ) -> Result, Error> { + ) -> Result, WebAuthnError> { self.channel_sync(settings).await } } async fn is_fido(device: &NfcDevice) -> bool { - async fn inner(device: &NfcDevice) -> Result { + async fn inner(device: &NfcDevice) -> Result> { let chan = device.channel_sync(ChannelSettings::default()).await?; let protocols = chan.supported_protocols().await?; Ok(protocols.fido2 || protocols.u2f) @@ -109,7 +114,7 @@ async fn is_fido(device: &NfcDevice) -> bool { #[instrument] /// Returns Ok(None) if no devices are found, otherwise returns /// the first device found by either NFC-backend. -pub async fn get_nfc_device() -> Result, Error> { +pub async fn get_nfc_device() -> Result, WebAuthnError> { // See https://github.com/linux-credentials/libwebauthn/issues/154 for // why we only return the first found device here. // We'd otherwise need to deduplicate found devices here, as @@ -123,7 +128,7 @@ pub async fn get_nfc_device() -> Result, Error> { ]; for list_devices in list_devices_fns { - for device in list_devices()? { + for device in list_devices().map_err(WebAuthnError::Transport)? { if is_fido(&device).await { return Ok(Some(device)); } diff --git a/libwebauthn/src/transport/nfc/error.rs b/libwebauthn/src/transport/nfc/error.rs new file mode 100644 index 00000000..84b8b3dd --- /dev/null +++ b/libwebauthn/src/transport/nfc/error.rs @@ -0,0 +1,28 @@ +//! Errors specific to the NFC transport. + +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum NfcError { + #[error("APDU error: {0}")] + Apdu(#[from] apdu::Error), + #[cfg(feature = "nfc-backend-pcsc")] + #[error("PC/SC error: {0}")] + Pcsc(#[from] pcsc::Error), + #[cfg(feature = "nfc-backend-libnfc")] + #[error("libnfc error: {0}")] + LibNfc(#[from] nfc1::Error), + #[error("input/output error: {0}")] + Io(#[from] std::io::Error), + #[error("failed to decode device response: {0}")] + ResponseDecode(#[source] std::io::Error), + #[error("response exceeds buffer: {0} bytes")] + BufferOverflow(usize), + #[error("no NFC reader available")] + NoReader, + #[error("no NFC target selected")] + NoTarget, + #[error("no response available")] + NoResponse, + #[error("mutex poisoned")] + MutexPoisoned, +} diff --git a/libwebauthn/src/transport/nfc/libnfc/mod.rs b/libwebauthn/src/transport/nfc/libnfc/mod.rs index e5a25301..832ef8ef 100644 --- a/libwebauthn/src/transport/nfc/libnfc/mod.rs +++ b/libwebauthn/src/transport/nfc/libnfc/mod.rs @@ -1,11 +1,8 @@ use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; use super::device::NfcDevice; +use super::error::NfcError; use super::Context; -use crate::transport::error::TransportError; use crate::transport::ChannelSettings; -use crate::webauthn::Error; -use apdu::core::HandleError; -use apdu_core; use std::fmt; use std::fmt::Debug; use std::io::Write; @@ -30,39 +27,6 @@ impl fmt::Display for Info { } } -fn map_error(_err: nfc1::Error) -> Error { - Error::Transport(TransportError::ConnectionFailed) -} - -impl From for Error { - fn from(input: nfc1::Error) -> Self { - trace!("{:?}", input); - let output = match input { - // rs-nfc1 errors - nfc1::Error::Malloc => TransportError::TransportUnavailable, - nfc1::Error::Undefined(_c_int) => TransportError::TransportUnavailable, - nfc1::Error::UndefinedModulationType => TransportError::TransportUnavailable, - nfc1::Error::NoDeviceFound => TransportError::TransportUnavailable, - - // libnfc errors - nfc1::Error::Io => TransportError::ConnectionLost, - nfc1::Error::InvalidArgument => TransportError::NegotiationFailed, - nfc1::Error::DeviceNotSupported => TransportError::InvalidEndpoint, - nfc1::Error::NoSuchDeviceFound => TransportError::InvalidEndpoint, - nfc1::Error::BufferOverflow => TransportError::InvalidFraming, - nfc1::Error::Timeout => TransportError::Timeout, - nfc1::Error::OperationAborted => TransportError::InvalidFraming, - nfc1::Error::NotImplemented => TransportError::NegotiationFailed, - nfc1::Error::TargetReleased => TransportError::NegotiationFailed, - nfc1::Error::RfTransmissionError => TransportError::NegotiationFailed, - nfc1::Error::MifareAuthFailed => TransportError::NegotiationFailed, - nfc1::Error::Soft => TransportError::Timeout, - nfc1::Error::Chip => TransportError::InvalidFraming, - }; - Error::Transport(output) - } -} - impl Info { pub fn new(connstring: &str) -> Self { Info { @@ -70,16 +34,13 @@ impl Info { } } - pub fn channel(&self, settings: ChannelSettings) -> Result, Error> { - let context = nfc1::Context::new().map_err(map_error)?; + pub fn channel(&self, settings: ChannelSettings) -> Result, NfcError> { + let context = nfc1::Context::new()?; let mut chan = Channel::new(self, context)?; { - let mut device = chan - .device - .lock() - .map_err(|_| Error::Transport(TransportError::ConnectionFailed))?; + let mut device = chan.device.lock().map_err(|_| NfcError::MutexPoisoned)?; device.initiator_init()?; device.set_property_bool(nfc1::Property::InfiniteSelect, false)?; @@ -104,7 +65,7 @@ pub struct Channel { unsafe impl Send for Channel {} impl Channel { - pub fn new(info: &Info, mut context: nfc1::Context) -> Result { + pub fn new(info: &Info, mut context: nfc1::Context) -> Result { let mut device = context.open_with_connstring(&info.connstring)?; let name = device.name().to_owned(); @@ -137,11 +98,8 @@ impl Channel { } } - fn connect_to_target(&mut self) -> Result { - let mut device = self - .device - .lock() - .map_err(|_| Error::Transport(TransportError::ConnectionFailed))?; + fn connect_to_target(&mut self) -> Result { + let mut device = self.device.lock().map_err(|_| NfcError::MutexPoisoned)?; // Assume baudrates are already sorted higher to lower let baudrates = device.get_supported_baud_rate(nfc1::Mode::Initiator, MODULATION_TYPE)?; let modulations = baudrates @@ -151,9 +109,7 @@ impl Channel { baud_rate: *baud_rate, }) .collect::>(); - let modulation = modulations - .last() - .ok_or(Error::Transport(TransportError::TransportUnavailable))?; + let modulation = modulations.last().ok_or(NfcError::NoTarget)?; let is_one_rate = modulations.len() == 1; for i in 0..2 { if i > 0 { @@ -179,7 +135,7 @@ impl Channel { } } - Err(Error::Transport(TransportError::TransportUnavailable)) + Err(NfcError::NoTarget) } } @@ -192,26 +148,24 @@ where _ctx: Ctx, command: &[u8], mut response: &mut [u8], - ) -> apdu_core::Result { + ) -> Result { let timeout = nfc1::Timeout::Duration(TIMEOUT); let len = response.len(); trace!("TX: {:?}", command); let rapdu = self .device .lock() - .map_err(|_| HandleError::Nfc(Box::new(std::io::Error::other("mutex poisoned"))))? + .map_err(|_| NfcError::MutexPoisoned)? .initiator_transceive_bytes(command, len, timeout) - .map_err(|e| HandleError::Nfc(Box::new(e)))?; + .map_err(NfcError::LibNfc)?; trace!("RX: {:?}", rapdu); if response.len() < rapdu.len() { - return Err(HandleError::NotEnoughBuffer(rapdu.len())); + return Err(NfcError::BufferOverflow(rapdu.len())); } - response - .write(&rapdu) - .map_err(|e| HandleError::Nfc(Box::new(e))) + response.write(&rapdu).map_err(NfcError::Io) } } @@ -233,12 +187,10 @@ pub(crate) fn is_nfc_available() -> bool { } #[instrument] -pub(crate) fn list_devices() -> Result, Error> { - let mut context = - nfc1::Context::new().map_err(|_| Error::Transport(TransportError::TransportUnavailable))?; +pub(crate) fn list_devices() -> Result, NfcError> { + let mut context = nfc1::Context::new()?; let devices = context - .list_devices(MAX_DEVICES) - .map_err(|_| Error::Transport(TransportError::TransportUnavailable))? + .list_devices(MAX_DEVICES)? .iter() .map(|x| NfcDevice::new_libnfc(Info::new(x))) .collect::>(); diff --git a/libwebauthn/src/transport/nfc/mod.rs b/libwebauthn/src/transport/nfc/mod.rs index ce8624c3..e56de0d5 100644 --- a/libwebauthn/src/transport/nfc/mod.rs +++ b/libwebauthn/src/transport/nfc/mod.rs @@ -3,6 +3,8 @@ use std::fmt::{Display, Formatter}; pub mod channel; pub mod commands; pub mod device; +pub mod error; +pub use error::NfcError; #[cfg(feature = "nfc-backend-libnfc")] pub mod libnfc; #[cfg(feature = "nfc-backend-pcsc")] diff --git a/libwebauthn/src/transport/nfc/pcsc/mod.rs b/libwebauthn/src/transport/nfc/pcsc/mod.rs index caa8dd53..b050d390 100644 --- a/libwebauthn/src/transport/nfc/pcsc/mod.rs +++ b/libwebauthn/src/transport/nfc/pcsc/mod.rs @@ -1,11 +1,9 @@ use super::channel::{HandlerInCtx, NfcBackend, NfcChannel}; use super::device::NfcDevice; +use super::error::NfcError; use super::Context; -use crate::transport::error::TransportError; use crate::transport::usb::UsbDeviceId; use crate::transport::ChannelSettings; -use crate::webauthn::Error; -use apdu::core::HandleError; use pcsc; use std::ffi::{CStr, CString}; use std::fmt; @@ -64,18 +62,6 @@ impl fmt::Display for Info { } } -impl From for Error { - fn from(input: pcsc::Error) -> Self { - trace!("{:?}", input); - let output = match input { - pcsc::Error::NoSmartcard => TransportError::ConnectionFailed, - _ => TransportError::InvalidFraming, - }; - - Error::Transport(output) - } -} - impl Info { pub fn new(name: &CStr) -> Self { let cstring = name.to_owned(); @@ -86,7 +72,7 @@ impl Info { } } - pub fn channel(&self, settings: ChannelSettings) -> Result, Error> { + pub fn channel(&self, settings: ChannelSettings) -> Result, NfcError> { let context = pcsc::Context::establish(pcsc::Scope::User)?; let chan = Channel::new(self, context)?; @@ -124,7 +110,7 @@ pub(crate) fn usb_id_from_reader(name: &CStr) -> Option { } impl Channel { - pub fn new(info: &Info, context: pcsc::Context) -> Result { + pub fn new(info: &Info, context: pcsc::Context) -> Result { let card = context.connect(&info.name, pcsc::ShareMode::Shared, pcsc::Protocols::ANY)?; let chan = Self { @@ -153,16 +139,11 @@ where _ctx: Ctx, command: &[u8], response: &mut [u8], - ) -> apdu_core::Result { + ) -> Result { trace!("TX: {:?}", command); - let card = self - .card - .lock() - .map_err(|_| HandleError::Nfc(Box::new(std::io::Error::other("mutex poisoned"))))?; - let rapdu = card - .transmit(command, response) - .map_err(|e| HandleError::Nfc(Box::new(e)))?; + let card = self.card.lock().map_err(|_| NfcError::MutexPoisoned)?; + let rapdu = card.transmit(command, response).map_err(NfcError::Pcsc)?; trace!("RX: {:?}", rapdu); Ok(rapdu.len()) @@ -179,11 +160,11 @@ pub(crate) fn is_nfc_available() -> bool { } #[instrument] -pub(crate) fn list_devices() -> Result, Error> { +pub(crate) fn list_devices() -> Result, NfcError> { let ctx = pcsc::Context::establish(pcsc::Scope::User)?; let len = ctx.list_readers_len()?; if len == 0 { - return Err(Error::Transport(TransportError::TransportUnavailable)); + return Err(NfcError::NoReader); } let mut readers_buf = vec![0; len]; let devices = ctx diff --git a/libwebauthn/src/u2f.rs b/libwebauthn/src/u2f.rs index 79481e6b..6aecf532 100644 --- a/libwebauthn/src/u2f.rs +++ b/libwebauthn/src/u2f.rs @@ -15,14 +15,22 @@ use crate::fido::FidoProtocol; use crate::ops::u2f::{RegisterRequest, SignRequest}; use crate::ops::u2f::{RegisterResponse, SignResponse}; use crate::proto::ctap1::Ctap1; -use crate::transport::{error::TransportError, Channel}; -use crate::webauthn::error::Error; +use crate::transport::Channel; +use crate::webauthn::error::{PlatformError, WebAuthnError}; #[async_trait] -pub trait U2F { - async fn u2f_negotiate_protocol(&mut self) -> Result; - async fn u2f_register(&mut self, op: &RegisterRequest) -> Result; - async fn u2f_sign(&mut self, op: &SignRequest) -> Result; +pub trait U2F: Channel { + async fn u2f_negotiate_protocol( + &mut self, + ) -> Result>; + async fn u2f_register( + &mut self, + op: &RegisterRequest, + ) -> Result>; + async fn u2f_sign( + &mut self, + op: &SignRequest, + ) -> Result>; } #[async_trait] @@ -31,11 +39,13 @@ where C: Channel, { #[instrument(skip_all)] - async fn u2f_negotiate_protocol(&mut self) -> Result { + async fn u2f_negotiate_protocol( + &mut self, + ) -> Result> { let supported = self.supported_protocols().await?; if !supported.u2f && !supported.fido2 { warn!("Negotiation failed: channel doesn't support U2F nor FIDO2"); - return Err(Error::Transport(TransportError::NegotiationFailed)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } // Ensure CTAP1 version is reported correctly. self.ctap1_version().await?; @@ -44,20 +54,26 @@ where } #[instrument(skip_all, fields(dev = %self))] - async fn u2f_register(&mut self, op: &RegisterRequest) -> Result { + async fn u2f_register( + &mut self, + op: &RegisterRequest, + ) -> Result> { let protocol = self.u2f_negotiate_protocol().await?; match protocol { FidoProtocol::U2F => self.ctap1_register(op).await, - _ => Err(Error::Transport(TransportError::NegotiationFailed)), + _ => Err(WebAuthnError::Platform(PlatformError::NotSupported)), } } #[instrument(skip_all, fields(dev = %self))] - async fn u2f_sign(&mut self, op: &SignRequest) -> Result { + async fn u2f_sign( + &mut self, + op: &SignRequest, + ) -> Result> { let protocol = self.u2f_negotiate_protocol().await?; match protocol { FidoProtocol::U2F => self.ctap1_sign(op).await, - _ => Err(Error::Transport(TransportError::NegotiationFailed)), + _ => Err(WebAuthnError::Platform(PlatformError::NotSupported)), } } } diff --git a/libwebauthn/src/webauthn.rs b/libwebauthn/src/webauthn.rs index 9764f10d..386bd4c4 100644 --- a/libwebauthn/src/webauthn.rs +++ b/libwebauthn/src/webauthn.rs @@ -10,7 +10,7 @@ //! User verification is handled internally by the [`pin_uv_auth_token`] module, //! which manages PIN and biometric UV, reuse of a cached pinUvAuthToken, shared //! secret establishment, and the fallback from biometric to PIN. Failures are -//! reported as [`Error`], which distinguishes CTAP protocol errors +//! reported as [`WebAuthnError`], which distinguishes CTAP protocol errors //! ([`CtapError`]), transport errors, and platform errors. pub mod error; @@ -36,9 +36,8 @@ use crate::proto::ctap2::{ Ctap2GetInfoResponse, Ctap2MakeCredentialRequest, Ctap2PublicKeyCredentialDescriptor, Ctap2UserVerificationOperation, }; -pub use crate::transport::error::TransportError; use crate::transport::{AuthTokenData, Channel}; -pub use crate::webauthn::error::{CtapError, Error, PlatformError}; +pub use crate::webauthn::error::{CtapError, PlatformError, WebAuthnError}; use crate::UvUpdate; use pin_uv_auth_token::{user_verification, UsedPinUvAuthToken}; @@ -58,25 +57,28 @@ fn filter_oversized_credentials( } } -fn ensure_credential_count(count: usize, info: &Ctap2GetInfoResponse) -> Result<(), Error> { +fn ensure_credential_count(count: usize, info: &Ctap2GetInfoResponse) -> Result<(), PlatformError> { if let Some(max) = info.max_credential_count_in_list() { if count > max { warn!( count, max, "credential list exceeds maxCredentialCountInList" ); - return Err(Error::Platform(PlatformError::RequestTooLarge)); + return Err(PlatformError::RequestTooLarge); } } Ok(()) } -fn ensure_msg_size(request: &CborRequest, info: &Ctap2GetInfoResponse) -> Result<(), Error> { +fn ensure_msg_size( + request: &CborRequest, + info: &Ctap2GetInfoResponse, +) -> Result<(), PlatformError> { let size = request.ctap_hid_data().len(); let max = info.max_msg_size(); if size > max { warn!(size, max, "serialized request exceeds maxMsgSize"); - return Err(Error::Platform(PlatformError::RequestTooLarge)); + return Err(PlatformError::RequestTooLarge); } Ok(()) } @@ -84,7 +86,7 @@ fn ensure_msg_size(request: &CborRequest, info: &Ctap2GetInfoResponse) -> Result fn enforce_get_assertion_limits( request: &Ctap2GetAssertionRequest, info: &Ctap2GetInfoResponse, -) -> Result<(), Error> { +) -> Result<(), PlatformError> { ensure_credential_count(request.allow.len(), info)?; ensure_msg_size(&request.try_into()?, info) } @@ -92,7 +94,7 @@ fn enforce_get_assertion_limits( fn enforce_make_credential_limits( request: &Ctap2MakeCredentialRequest, info: &Ctap2GetInfoResponse, -) -> Result<(), Error> { +) -> Result<(), PlatformError> { let exclude_count = request.exclude.as_ref().map_or(0, Vec::len); ensure_credential_count(exclude_count, info)?; ensure_msg_size(&request.try_into()?, info) @@ -114,14 +116,14 @@ macro_rules! handle_errors { }; (@inner $channel: expr, $resp: expr, $uv_auth_used: expr, $timeout: expr, $on_persistent_reject: block) => { match $resp { - Err(Error::Ctap(CtapError::PINAuthInvalid)) + Err(WebAuthnError::Ctap(CtapError::PINAuthInvalid)) if $uv_auth_used == UsedPinUvAuthToken::FromEphemeralStorage => { info!("PINAuthInvalid: Clearing auth token storage and trying again."); $channel.clear_uv_auth_token_store(); continue; } - Err(Error::Ctap(CtapError::PINAuthInvalid)) + Err(WebAuthnError::Ctap(CtapError::PINAuthInvalid)) if matches!($uv_auth_used, UsedPinUvAuthToken::FromPersistentStorage(_)) => { info!("PINAuthInvalid on a persistent token: evicting the record and retrying."); @@ -133,7 +135,7 @@ macro_rules! handle_errors { $on_persistent_reject continue; } - Err(Error::Ctap(CtapError::UVInvalid)) => { + Err(WebAuthnError::Ctap(CtapError::UVInvalid)) => { let attempts_left = $channel .ctap2_client_pin(&Ctap2ClientPinRequest::new_get_uv_retries(), $timeout) .await @@ -143,7 +145,7 @@ macro_rules! handle_errors { $channel .send_ux_update(UvUpdate::UvRetry { attempts_left }.into()) .await; - break Err(Error::Ctap(CtapError::UVInvalid)); + break Err(WebAuthnError::Ctap(CtapError::UVInvalid)); } x => { break x; @@ -154,15 +156,15 @@ macro_rules! handle_errors { pub(crate) use handle_errors; #[async_trait] -pub trait WebAuthn { +pub trait WebAuthn: Channel { async fn webauthn_make_credential( &mut self, op: &MakeCredentialRequest, - ) -> Result; + ) -> Result>; async fn webauthn_get_assertion( &mut self, op: &GetAssertionRequest, - ) -> Result; + ) -> Result>; } #[async_trait] @@ -174,7 +176,7 @@ where async fn webauthn_make_credential( &mut self, op: &MakeCredentialRequest, - ) -> Result { + ) -> Result> { let upgraded; let prf_present = op.extensions.as_ref().is_some_and(|e| e.prf.is_some()); let op = if prf_forces_uv_upgrade(prf_present, op.user_verification) { @@ -201,7 +203,7 @@ where async fn webauthn_get_assertion( &mut self, op: &GetAssertionRequest, - ) -> Result { + ) -> Result> { let upgraded; let prf_present = op.extensions.as_ref().is_some_and(|e| e.prf.is_some()); let op = if prf_forces_uv_upgrade(prf_present, op.user_verification) { @@ -226,7 +228,7 @@ where async fn make_credential_fido2( channel: &mut C, op: &MakeCredentialRequest, -) -> Result { +) -> Result> { let get_info_response = channel.ctap2_get_info().await?; let mut ctap2_request = Ctap2MakeCredentialRequest::from_webauthn_request(op, &get_info_response)?; @@ -300,7 +302,7 @@ async fn make_credential_fido2( async fn make_credential_u2f( channel: &mut C, op: &MakeCredentialRequest, -) -> Result { +) -> Result> { let register_request: RegisterRequest = op.try_downgrade()?; channel @@ -312,7 +314,7 @@ async fn make_credential_u2f( async fn get_assertion_fido2( channel: &mut C, op: &GetAssertionRequest, -) -> Result { +) -> Result> { // WebAuthn L3 §10.1.5: largeBlob.write/delete requires exactly one allowCredentials entry. let large_blob_ext = op.extensions.as_ref().and_then(|e| e.large_blob.as_ref()); if matches!( @@ -325,7 +327,7 @@ async fn get_assertion_fido2( count = op.allow.len(), "largeBlob.write/delete requires exactly one allowCredentials entry" ); - return Err(Error::Platform(PlatformError::NotSupported)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } let get_info_response = channel.ctap2_get_info().await?; @@ -354,12 +356,12 @@ async fn get_assertion_fido2( let _ = channel .ctap2_make_credential(&dummy_request, op.timeout) .await; - return Err(Error::Ctap(CtapError::NoCredentials)); + return Err(WebAuthnError::Ctap(CtapError::NoCredentials)); } ctap2_request.allow = filtered_allow_list; } else if ctap2_request.allow.is_empty() && !op.allow.is_empty() { // No preflight (cable): all entries were oversized, so don't fall through to an empty allowList. - return Err(Error::Ctap(CtapError::NoCredentials)); + return Err(WebAuthnError::Ctap(CtapError::NoCredentials)); } enforce_get_assertion_limits(&ctap2_request, &get_info_response)?; @@ -408,13 +410,16 @@ async fn get_assertion_fido2( hasher.update(op.relying_party_id.as_bytes()); hasher.finalize() }; - let validate_rp_id_hash = |resp: &Ctap2GetAssertionResponse| -> Result<(), Error> { - if resp.authenticator_data.rp_id_hash.as_slice() != expected_rp_id_hash.as_slice() { - warn!("getAssertion rpIdHash does not match the requested RP ID"); - return Err(Error::Platform(PlatformError::InvalidDeviceResponse)); - } - Ok(()) - }; + let validate_rp_id_hash = + |resp: &Ctap2GetAssertionResponse| -> Result<(), WebAuthnError> { + if resp.authenticator_data.rp_id_hash.as_slice() != expected_rp_id_hash.as_slice() { + warn!("getAssertion rpIdHash does not match the requested RP ID"); + return Err(WebAuthnError::Platform( + PlatformError::InvalidDeviceResponse, + )); + } + Ok(()) + }; validate_rp_id_hash(&response)?; // Cap iteration so a hostile numberOfCredentials cannot force an unbounded loop. @@ -458,13 +463,15 @@ async fn get_assertion_fido2( .iter() .map(|resp| { let blob = match (entries.as_ref(), extract_large_blob_key(resp)) { - (Some(entries), Some(key)) => match decrypt_first_matching(entries, &key) { - Ok(blob) => blob, - Err(e) => { - warn!(?e, "largeBlob decrypt failed; no blob returned"); - None + (Some(entries), Some(key)) => { + match decrypt_first_matching::(entries, &key) { + Ok(blob) => blob, + Err(e) => { + warn!(?e, "largeBlob decrypt failed; no blob returned"); + None + } } - }, + } _ => None, }; GetAssertionLargeBlobExtensionOutput { @@ -629,7 +636,7 @@ async fn write_or_delete_for_first( async fn get_assertion_u2f( channel: &mut C, op: &GetAssertionRequest, -) -> Result { +) -> Result> { use sha2::{Digest, Sha256}; let sign_requests: Vec = op.try_downgrade()?; @@ -666,7 +673,7 @@ async fn get_assertion_u2f( } return Ok(upgraded); } - Err(Error::Ctap(CtapError::NoCredentials)) => { + Err(WebAuthnError::Ctap(CtapError::NoCredentials)) => { debug!("No credentials found, trying with the next."); } Err(err) => { @@ -679,21 +686,21 @@ async fn get_assertion_u2f( } } warn!("None of the credentials in the original request's allowList were found."); - Err(Error::Ctap(CtapError::NoCredentials)) + Err(WebAuthnError::Ctap(CtapError::NoCredentials)) } #[instrument(skip_all)] async fn negotiate_protocol( channel: &mut C, allow_u2f: bool, -) -> Result { +) -> Result> { let supported = channel.supported_protocols().await?; if !supported.u2f && !supported.fido2 { - return Err(Error::Transport(TransportError::NegotiationFailed)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } if !allow_u2f && !supported.fido2 { - return Err(Error::Transport(TransportError::NegotiationFailed)); + return Err(WebAuthnError::Platform(PlatformError::NotSupported)); } let fido_protocol = if supported.fido2 { @@ -816,11 +823,14 @@ mod tests { #[async_trait] impl Channel for NoPreflightChannel { type UxUpdate = UvUpdate; + type TransportError = std::convert::Infallible; fn get_ux_update_sender(&self) -> &broadcast::Sender { self.inner.get_ux_update_sender() } - async fn supported_protocols(&self) -> Result { + async fn supported_protocols( + &self, + ) -> Result> { self.inner.supported_protocols().await } async fn status(&self) -> ChannelStatus { @@ -831,20 +841,26 @@ mod tests { &mut self, request: &ApduRequest, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), Self::TransportError> { self.inner.apdu_send(request, timeout).await } - async fn apdu_recv(&mut self, timeout: Duration) -> Result { + async fn apdu_recv( + &mut self, + timeout: Duration, + ) -> Result { self.inner.apdu_recv(timeout).await } async fn cbor_send( &mut self, request: &CborRequest, timeout: Duration, - ) -> Result<(), Error> { + ) -> Result<(), Self::TransportError> { self.inner.cbor_send(request, timeout).await } - async fn cbor_recv(&mut self, timeout: Duration) -> Result { + async fn cbor_recv( + &mut self, + timeout: Duration, + ) -> Result { self.inner.cbor_recv(timeout).await } fn supports_preflight() -> bool { @@ -971,7 +987,7 @@ mod tests { ctap2_get_assertion(vec![descriptor(b"a"), descriptor(b"b"), descriptor(b"c")]); assert_eq!( enforce_get_assertion_limits(&request, &info), - Err(Error::Platform(PlatformError::RequestTooLarge)) + Err(PlatformError::RequestTooLarge) ); } @@ -995,7 +1011,7 @@ mod tests { request.exclude = Some(vec![descriptor(b"a"), descriptor(b"b")]); assert_eq!( enforce_make_credential_limits(&request, &info), - Err(Error::Platform(PlatformError::RequestTooLarge)) + Err(PlatformError::RequestTooLarge) ); } @@ -1008,7 +1024,7 @@ mod tests { let request = ctap2_get_assertion(vec![descriptor(&[0u8; 2000])]); assert_eq!( enforce_get_assertion_limits(&request, &info), - Err(Error::Platform(PlatformError::RequestTooLarge)) + Err(PlatformError::RequestTooLarge) ); } @@ -1065,10 +1081,12 @@ mod tests { ); let result = get_assertion_fido2(&mut channel, &op).await; - assert_eq!( + assert!(matches!( result.err(), - Some(Error::Platform(PlatformError::InvalidDeviceResponse)) - ); + Some(WebAuthnError::Platform( + PlatformError::InvalidDeviceResponse + )) + )); } } } diff --git a/libwebauthn/src/webauthn/error.rs b/libwebauthn/src/webauthn/error.rs index cc8784b6..31f57145 100644 --- a/libwebauthn/src/webauthn/error.rs +++ b/libwebauthn/src/webauthn/error.rs @@ -1,17 +1,37 @@ +use crate::proto::ctap2::cbor::CborError; pub use crate::proto::CtapError; -use crate::{proto::ctap2::cbor::CborError, webauthn::TransportError}; -#[derive(thiserror::Error, Debug, PartialEq)] -pub enum Error { - #[error("Transport error: {0}")] - Transport(#[from] TransportError), - #[error("Ctap error: {0}")] - Ctap(#[from] CtapError), - #[error("Platform error: {0}")] - Platform(#[from] PlatformError), +/// Ceremony-level error, generic over the channel's concrete transport error. +/// +/// `E` is bound exactly once, by the channel that runs the operation +/// ([`Channel::TransportError`](crate::transport::Channel::TransportError)). +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum Error { + #[error("ctap error: {0}")] + Ctap(#[source] CtapError), + #[error("transport error: {0}")] + Transport(#[source] E), + #[error("platform error: {0}")] + Platform(#[source] PlatformError), +} + +/// Former name of the ceremony [`Error`], retained to avoid call-site churn. +pub use self::Error as WebAuthnError; + +impl From for Error { + fn from(error: CtapError) -> Self { + Error::Ctap(error) + } +} + +impl From for Error { + fn from(error: PlatformError) -> Self { + Error::Platform(error) + } } -impl From for Error { +impl From for Error { fn from(error: CborError) -> Self { Error::Platform(PlatformError::CborError(error)) } diff --git a/libwebauthn/src/webauthn/pin_uv_auth_token.rs b/libwebauthn/src/webauthn/pin_uv_auth_token.rs index 90905055..460ff7c4 100644 --- a/libwebauthn/src/webauthn/pin_uv_auth_token.rs +++ b/libwebauthn/src/webauthn/pin_uv_auth_token.rs @@ -18,9 +18,8 @@ use crate::proto::ctap2::{ Ctap2, Ctap2AuthTokenPermissionRole, Ctap2ClientPinRequest, Ctap2GetInfoResponse, Ctap2PinUvAuthProtocol, Ctap2UserVerifiableRequest, Ctap2UserVerificationOperation, }; -pub use crate::transport::error::TransportError; use crate::transport::{AuthTokenData, Channel, Ctap2AuthTokenPermission}; -pub use crate::webauthn::error::{CtapError, Error, PlatformError}; +pub use crate::webauthn::error::{CtapError, PlatformError, WebAuthnError}; use crate::{PinNotSetUpdate, PinRequiredUpdate, UvUpdate}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -71,7 +70,7 @@ fn enforce_no_mc_ga_with_client_pin( uv_operation: Ctap2UserVerificationOperation, permissions: Ctap2AuthTokenPermissionRole, uv_unavailable: bool, -) -> Result { +) -> Result { let is_client_pin = matches!( uv_operation, Ctap2UserVerificationOperation::GetPinToken @@ -90,7 +89,7 @@ fn enforce_no_mc_ga_with_client_pin( Ok(Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions) } else { error!("noMcGaPermissionsWithClientPin is set but built-in UV is unavailable for mc/ga"); - Err(Error::Platform(PlatformError::NoUvAvailable)) + Err(PlatformError::NoUvAvailable) } } @@ -100,7 +99,7 @@ pub(crate) async fn user_verification( user_verification: UserVerificationRequirement, ctap2_request: &mut R, timeout: Duration, -) -> Result +) -> Result> where C: Channel, R: Ctap2UserVerifiableRequest, @@ -162,7 +161,7 @@ async fn user_verification_helper( user_verification: UserVerificationRequirement, ctap2_request: &mut R, timeout: Duration, -) -> Result +) -> Result> where C: Channel, R: Ctap2UserVerifiableRequest, @@ -194,7 +193,7 @@ where dev_uv_protected = get_info_response.is_uv_protected(); // Still no usable UV: do not silently fall back to a shared-secret-only result. if !dev_uv_protected { - return Err(Error::Platform(PlatformError::NoUvAvailable)); + return Err(WebAuthnError::Platform(PlatformError::NoUvAvailable)); } } @@ -230,9 +229,9 @@ where .uv_operation(uv_blocked || skip_uv) .ok_or({ if uv_blocked { - Error::Ctap(CtapError::UvBlocked) + WebAuthnError::Ctap(CtapError::UvBlocked) } else { - Error::Platform(PlatformError::NoUvAvailable) + WebAuthnError::Platform(PlatformError::NoUvAvailable) } })?; if let Ctap2UserVerificationOperation::LegacyUv = uv_operation { @@ -291,7 +290,7 @@ where .await else { error!("No supported PIN/UV auth protocols found"); - return Err(Error::Ctap(CtapError::Other)); + return Err(WebAuthnError::Ctap(CtapError::Other)); }; // For operations that include a PIN, we want to fetch one before obtaining a shared secret. @@ -348,7 +347,7 @@ where &shared_secret, &pin_hash(&pin.ok_or_else(|| { error!("PIN expected but not available"); - Error::Ctap(CtapError::PINRequired) + WebAuthnError::Ctap(CtapError::PINRequired) })?), )?, ) @@ -361,7 +360,7 @@ where &shared_secret, &pin_hash(&pin.ok_or_else(|| { error!("PIN expected but not available"); - Error::Ctap(CtapError::PINRequired) + WebAuthnError::Ctap(CtapError::PINRequired) })?), )?, ctap2_request.permissions(), @@ -386,12 +385,12 @@ where break (uv_proto, shared_secret, public_key, uv_operation, Some(t)); } // Internal retry, because we otherwise can't fall back to PIN, if the UV is blocked - Err(Error::Ctap(CtapError::UvBlocked)) => { + Err(WebAuthnError::Ctap(CtapError::UvBlocked)) => { warn!("UV failed too many times and is now blocked. Trying to fall back to PIN."); uv_blocked = true; continue; } - Err(Error::Ctap(CtapError::UVInvalid)) => { + Err(WebAuthnError::Ctap(CtapError::UVInvalid)) => { let attempts_left = channel .ctap2_client_pin(&Ctap2ClientPinRequest::new_get_uv_retries(), timeout) .await @@ -411,7 +410,7 @@ where continue; } } - return Err(Error::Ctap(CtapError::UVInvalid)); + return Err(WebAuthnError::Ctap(CtapError::UVInvalid)); } Err(x) => { return Err(x); @@ -448,11 +447,11 @@ where { let token_response = token_response.ok_or_else(|| { error!("Expected token response but got None"); - Error::Ctap(CtapError::Other) + WebAuthnError::Ctap(CtapError::Other) })?; let Some(encrypted_pin_uv_auth_token) = token_response.pin_uv_auth_token else { error!("Client PIN response did not include a PIN UV auth token"); - return Err(Error::Ctap(CtapError::Other)); + return Err(WebAuthnError::Ctap(CtapError::Other)); }; let uv_auth_token = @@ -465,7 +464,7 @@ where token_len = uv_auth_token.len(), "Decrypted pinUvAuthToken has an invalid length" ); - return Err(Error::Ctap(CtapError::Other)); + return Err(WebAuthnError::Ctap(CtapError::Other)); } if ctap2_request.wants_persistent_token() { @@ -518,7 +517,7 @@ pub(crate) async fn obtain_shared_secret( channel: &mut C, pin_proto: &dyn PinUvAuthProtocol, timeout: Duration, -) -> Result<(PublicKey, Vec), Error> +) -> Result<(PublicKey, Vec), WebAuthnError> where C: Channel, { @@ -528,9 +527,9 @@ where .await?; let Some(public_key) = client_pin_response.key_agreement else { error!("Missing public key from Client PIN response"); - return Err(Error::Ctap(CtapError::Other)); + return Err(WebAuthnError::Ctap(CtapError::Other)); }; - pin_proto.encapsulate(&public_key) + Ok(pin_proto.encapsulate(&public_key)?) } pub(crate) async fn obtain_pin( @@ -539,7 +538,7 @@ pub(crate) async fn obtain_pin( pin_proto: Ctap2PinUvAuthProtocol, reason: PinRequestReason, timeout: Duration, -) -> Result, Error> +) -> Result, WebAuthnError> where C: Channel, { @@ -575,7 +574,7 @@ where Ok(pin) => pin, Err(_) => { info!("User cancelled operation: no PIN provided"); - return Err(Error::Ctap(CtapError::PINRequired)); + return Err(WebAuthnError::Ctap(CtapError::PINRequired)); } }; // CTAP 2.1 sends the PIN as UTF-8 in Unicode Normalization Form C. @@ -590,7 +589,7 @@ pub(crate) async fn try_to_set_pin( info: &Ctap2GetInfoResponse, mut reason: PinNotSetReason, timeout: Duration, -) -> Result<(), Error> +) -> Result<(), WebAuthnError> where C: Channel, { @@ -621,7 +620,7 @@ where Ok(pin) => pin, Err(_) => { info!("User cancelled operation: no PIN provided"); - return Err(Error::Platform(PlatformError::Cancelled)); + return Err(WebAuthnError::Platform(PlatformError::Cancelled)); } }; match channel @@ -632,15 +631,15 @@ where // PIN was successfully set. The user can now try to finish the ongoing operation. return Ok(()); } - Err(Error::Platform(PlatformError::PinTooShort)) => { + Err(WebAuthnError::Platform(PlatformError::PinTooShort)) => { reason = PinNotSetReason::PinTooShort; continue; } - Err(Error::Platform(PlatformError::PinTooLong)) => { + Err(WebAuthnError::Platform(PlatformError::PinTooLong)) => { reason = PinNotSetReason::PinTooLong; continue; } - Err(Error::Ctap(CtapError::PINPolicyViolation)) => { + Err(WebAuthnError::Ctap(CtapError::PINPolicyViolation)) => { reason = PinNotSetReason::PinPolicyViolation; continue; } @@ -689,10 +688,18 @@ mod test { use super::{ enforce_no_mc_ga_with_client_pin, obtain_pin, pin_uv_auth_token_len_valid, - user_verification, CtapError, Error, + user_verification, CtapError, WebAuthnError, }; + use std::convert::Infallible; const TIMEOUT: Duration = Duration::from_secs(1); + fn assert_uv_result( + actual: &Result>, + expected: &Result>, + ) { + assert_eq!(format!("{actual:?}"), format!("{expected:?}")); + } + #[test] fn pin_uv_auth_token_len_valid_enforces_spec_lengths() { use Ctap2PinUvAuthProtocol::{One, Two}; @@ -754,7 +761,7 @@ mod test { info_extensions: Option<&[&'static str]>, uv_requirement: UserVerificationRequirement, extensions: Option, - expected_result: Result, + expected_result: Result>, ) { let mut channel = MockChannel::new(); let status_recv = channel.get_ux_update_receiver(); @@ -770,7 +777,7 @@ mod test { let resp = user_verification(&mut channel, uv_requirement, &mut getassertion, TIMEOUT).await; - assert_eq!(resp, expected_result); + assert_uv_result(&resp, &expected_result); // Nothing ended up in the auth store assert!(channel.get_auth_data().is_none()); // No updates should be sent, since we are exiting early @@ -833,9 +840,11 @@ mod test { handle.await.unwrap(); - assert_eq!( - resp, - Err(Error::Platform(crate::webauthn::PlatformError::Cancelled)) + assert_uv_result( + &resp, + &Err(WebAuthnError::Platform( + crate::webauthn::PlatformError::Cancelled, + )), ); } @@ -1040,7 +1049,7 @@ mod test { let expected_result = Ok(UsedPinUvAuthToken::NewlyCalculated( Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions, )); - assert_eq!(resp, expected_result); + assert_uv_result(&resp, &expected_result); // Something ended up in the auth store assert!(channel.get_auth_data().is_some()); assert_eq!( @@ -1257,7 +1266,7 @@ mod test { let resp = user_verification(&mut channel, uv_requirement, &mut getassertion, TIMEOUT).await; - assert_eq!(resp, expected_result); + assert_uv_result(&resp, &expected_result); // Something ended up in the auth store assert!(channel.get_auth_data().is_some()); assert!(channel.get_auth_data().unwrap().pin_uv_auth_token.is_none()); @@ -1307,11 +1316,11 @@ mod test { ) .await; - assert_eq!( - resp, - Err(Error::Platform( - crate::webauthn::PlatformError::NoUvAvailable - )) + assert_uv_result( + &resp, + &Err(WebAuthnError::Platform( + crate::webauthn::PlatformError::NoUvAvailable, + )), ); // No shared-secret-only fallback ended up in the auth store. assert!(channel.get_auth_data().is_none()); @@ -1417,7 +1426,7 @@ mod test { let resp = user_verification(&mut channel, uv_requirement, &mut getassertion, TIMEOUT).await; - assert_eq!(resp, expected_result); + assert_uv_result(&resp, &expected_result); // Something ended up in the auth store assert!(channel.get_auth_data().is_some()); assert_eq!( @@ -1562,7 +1571,7 @@ mod test { let resp = user_verification(&mut channel, uv_requirement, &mut getassertion, TIMEOUT).await; - assert_eq!(resp, expected_result); + assert_uv_result(&resp, &expected_result); // Something ended up in the auth store assert!(channel.get_auth_data().is_some()); assert_eq!( @@ -1691,11 +1700,11 @@ mod test { .await; // Reused from the persistent store, carrying the record id for invalidation. - assert_eq!( - result, - Ok(UsedPinUvAuthToken::FromPersistentStorage( - "rec-1".to_string() - )) + assert_uv_result( + &result, + &Ok(UsedPinUvAuthToken::FromPersistentStorage( + "rec-1".to_string(), + )), ); // The reused token must never enter the ephemeral cache. assert!(channel.get_auth_data().is_none()); @@ -1789,11 +1798,11 @@ mod test { ) .await; - assert_eq!( - result, - Ok(UsedPinUvAuthToken::NewlyCalculated( - Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions - )) + assert_uv_result( + &result, + &Ok(UsedPinUvAuthToken::NewlyCalculated( + Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions, + )), ); // The minted pcmr token is persisted, not cached ephemerally. assert!(channel.get_auth_data().is_none()); @@ -1913,11 +1922,11 @@ mod test { .await; // Recognition missed, so the token was minted, not reused. - assert_eq!( - result, - Ok(UsedPinUvAuthToken::NewlyCalculated( - Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions - )) + assert_uv_result( + &result, + &Ok(UsedPinUvAuthToken::NewlyCalculated( + Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingUvWithPermissions, + )), ); // The stale record was reaped and replaced by exactly one fresh record. let listed = store.list().await; @@ -2263,11 +2272,11 @@ mod test { .await; // A full token via PIN, not the OnlyForSharedSecret downgrade. - assert_eq!( - resp, - Ok(UsedPinUvAuthToken::NewlyCalculated( + assert_uv_result( + &resp, + &Ok(UsedPinUvAuthToken::NewlyCalculated( Ctap2UserVerificationOperation::GetPinUvAuthTokenUsingPinWithPermissions, - )) + )), ); let auth_data = channel.get_auth_data().expect("auth data stored"); assert_eq!(auth_data.pin_uv_auth_token.as_ref().unwrap(), &token); @@ -2314,7 +2323,7 @@ mod test { mc_ga, true ), - Err(Error::Platform(PlatformError::NoUvAvailable)) + Err(PlatformError::NoUvAvailable) ); // Policy set but no built-in UV at all: error on the clientPin mc/ga path. @@ -2328,7 +2337,7 @@ mod test { ); assert_eq!( enforce_no_mc_ga_with_client_pin(&info_no_uv, GetPinToken, mc_ga, false), - Err(Error::Platform(PlatformError::NoUvAvailable)) + Err(PlatformError::NoUvAvailable) ); // A non-mc/ga permission (e.g. credential management) is unaffected by the policy. assert_eq!( @@ -2389,7 +2398,10 @@ mod test { ) .await; - assert_eq!(resp, Err(Error::Platform(PlatformError::NoUvAvailable))); + assert_uv_result( + &resp, + &Err(WebAuthnError::Platform(PlatformError::NoUvAvailable)), + ); assert!(channel.get_auth_data().is_none()); // No PIN prompt: we never start the forbidden clientPin token request. assert!(status_recv.is_empty()); @@ -2428,7 +2440,10 @@ mod test { .await; recv_handle.await.expect("Failed to join update thread"); - assert_eq!(resp, Err(Error::Platform(PlatformError::Cancelled))); + assert_uv_result( + &resp, + &Err(WebAuthnError::Platform(PlatformError::Cancelled)), + ); assert!(channel.get_auth_data().is_none()); } @@ -2442,14 +2457,20 @@ mod test { let result = channel .change_pin_internal(&info, "\u{e9}\u{e9}\u{e9}".to_string(), TIMEOUT) .await; - assert_eq!(result, Err(Error::Platform(PlatformError::PinTooShort))); + assert!(matches!( + result, + Err(WebAuthnError::Platform(PlatformError::PinTooShort)) + )); // 4 code points clears the length gate (it then fails later for an unrelated reason). let mut channel = MockChannel::new(); let result = channel .change_pin_internal(&info, "\u{e9}\u{e9}\u{e9}\u{e9}".to_string(), TIMEOUT) .await; - assert_ne!(result, Err(Error::Platform(PlatformError::PinTooShort))); + assert!(!matches!( + result, + Err(WebAuthnError::Platform(PlatformError::PinTooShort)) + )); } #[tokio::test]