Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 276 additions & 17 deletions passkey-authenticator/src/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ use std::path::Path;
use passkey_transports::hid::{Command, Message};
use passkey_transports::hidraw::{DeviceInfo, HidDevice, HidrawError, enumerate_fido_devices};
use passkey_types::ctap2::{
Ctap2Command, Ctap2Error, StatusCode, U2FError, get_assertion, get_info, make_credential,
Ctap2ClientPinSubcommand, Ctap2Code, Ctap2Command, Ctap2Error, StatusCode, U2FError,
client_pin, get_assertion, get_info, make_credential,
};
use passkey_types::{Bytes, webauthn};
use tokio::sync::mpsc;

use crate::Ctap2Api;
Expand Down Expand Up @@ -106,6 +108,72 @@ pub struct LinuxAuthenticatorInner {
}

impl LinuxAuthenticatorInner {
/// Determine whether the device supports CTAP 2.1 or greater. If it does, issue an
/// `authenticatorSelection` command. Otherwise, issue `authenticatorMakeCredential` with a
/// zero-length `pinUvAuthToken`. Returns when the user provides UP on the device.
pub async fn authenticator_selection(&mut self) -> Result<(), StatusCode> {
let info: get_info::Response =
ciborium::de::from_reader(self.get_info_cbor.get_payload()).unwrap_or_default();
let authenticator_selection_unsupported = info
.versions
.iter()
.all(|e| *e == get_info::Version::U2F_V2 || *e == get_info::Version::FIDO_2_0);
if !authenticator_selection_unsupported {
let response = self
.send_cbor_with_cancel(Ctap2Command::AuthenticatorSelection, &[])
.await;
if matches!(
response,
Err(TransactionError::Status(StatusCode::Ctap2(
Ctap2Code::Known(Ctap2Error::Ok)
)))
) {
Ok(())
} else {
response.map(|_| ()).map_err(StatusCode::from)
}
} else {
let dummy_request = make_credential::Request {
client_data_hash: Bytes::from(&[][..]),
rp: make_credential::PublicKeyCredentialRpEntity {
id: String::new(),
name: None,
},
user: webauthn::PublicKeyCredentialUserEntity {
id: Bytes::from(&[][..]),
display_name: String::new(),
name: String::new(),
},
pub_key_cred_params: webauthn::PublicKeyCredentialParameters::default_algorithms(),
exclude_list: None,
extensions: None,
options: make_credential::Options {
rk: false,
up: false,
uv: false,
},
pin_auth: Some(Bytes::from(&[][..])),
pin_protocol: Some(1),
};
let mut body = Vec::new();
ciborium::ser::into_writer(&dummy_request, &mut body)
.map_err(|_| StatusCode::from(U2FError::Other))?;
let response = self
.send_cbor_with_cancel(Ctap2Command::MakeCredential, &body)
.await;
if matches!(
response,
Err(TransactionError::Status(StatusCode::Ctap2(
Ctap2Code::Known(Ctap2Error::PinNotSet | Ctap2Error::PinInvalid)
)))
) {
Ok(())
} else {
response.map(|_| ()).map_err(StatusCode::from)
}
}
}

/// Issue `authenticatorMakeCredential` against the device.
pub async fn make_credential(
&mut self,
Expand Down Expand Up @@ -138,6 +206,186 @@ impl LinuxAuthenticatorInner {
.map_err(|_| StatusCode::from(Ctap2Error::InvalidCbor))
}

/// Fetch public key from device using the given protocol.
pub async fn get_public_key(&mut self, protocol: u8) -> Result<coset::CoseKey, StatusCode> {
let request = client_pin::Request {
pin_uv_auth_protocol: Some(protocol),
sub_command: Ctap2ClientPinSubcommand::GetKeyAgreement,
key_agreement: None,
pin_uv_auth_param: None,
new_pin_enc: None,
pin_hash_enc: None,
permissions: None,
rp_id: None,
};
let mut body = Vec::new();
ciborium::ser::into_writer(&request, &mut body)
.map_err(|_| StatusCode::from(U2FError::Other))?;
let response = self
.send_cbor_with_cancel(Ctap2Command::ClientPin, &body)
.await
.map_err(StatusCode::from)?;
let response: client_pin::Response =
ciborium::de::from_reader(response.get_payload()).unwrap();
// TODO: remove this expect
Ok(response.key_agreement.expect("should have a key agreement"))
}

/// `getPinToken` subcommand of `clientPin`.
pub async fn get_pin_token(
&mut self,
protocol: u8,
key_agreement: coset::CoseKey,
pin_hash_enc: Bytes,
) -> Result<Bytes, StatusCode> {
let request = client_pin::Request {
pin_uv_auth_protocol: Some(protocol),
sub_command: Ctap2ClientPinSubcommand::GetPinToken,
key_agreement: Some(key_agreement),
pin_uv_auth_param: None,
new_pin_enc: None,
pin_hash_enc: Some(pin_hash_enc),
permissions: None,
rp_id: None,
};
let mut body = Vec::new();
ciborium::ser::into_writer(&request, &mut body)
.map_err(|_| StatusCode::from(U2FError::Other))?;
let response = self
.send_cbor_with_cancel(Ctap2Command::ClientPin, &body)
.await
.map_err(StatusCode::from)?;
let response: client_pin::Response =
ciborium::de::from_reader(response.get_payload()).unwrap_or_default();
// TODO: remove this expect
Ok(response
.pin_uv_auth_token
.expect("should have a pinUvAuthToken"))
}

/// `getPinUvAuthTokenUsingUvWithPermissions` subcommand of `clientPin`.
pub async fn get_pin_uv_auth_token_using_uv(
&mut self,
protocol: u8,
key_agreement: coset::CoseKey,
permissions: client_pin::Permissions,
// rp_id is required for both make_credential and get_assertion, but we leave
// it as an Option here in case we need to add support for other permissions and don't
// want to break backwards compatibility.
rp_id: Option<String>,
) -> Result<Bytes, StatusCode> {
let request = client_pin::Request {
pin_uv_auth_protocol: Some(protocol),
sub_command: Ctap2ClientPinSubcommand::GetPinUvAuthTokenUsingUvWithPermissions,
key_agreement: Some(key_agreement),
pin_uv_auth_param: None,
new_pin_enc: None,
pin_hash_enc: None,
permissions: Some(permissions),
rp_id,
};
let mut body = Vec::new();
ciborium::ser::into_writer(&request, &mut body)
.map_err(|_| StatusCode::from(U2FError::Other))?;
let response = self
.send_cbor_with_cancel(Ctap2Command::ClientPin, &body)
.await
.map_err(StatusCode::from)?;
let response: client_pin::Response =
ciborium::de::from_reader(response.get_payload()).unwrap_or_default();
// TODO: remove this expect
Ok(response
.pin_uv_auth_token
.expect("should have a pinUvAuthToken"))
}

/// `getPinUvAuthTokenUsingPinWithPermissions` subcommand of `clientPin`.
pub async fn get_pin_uv_auth_token_using_pin(
&mut self,
protocol: u8,
key_agreement: coset::CoseKey,
pin_hash_enc: Bytes,
permissions: client_pin::Permissions,
// rp_id is required for both make_credential and get_assertion, but we leave
// it as an Option here in case we need to add support for other permissions and don't
// want to break backwards compatibility.
rp_id: Option<String>,
) -> Result<Bytes, StatusCode> {
let request = client_pin::Request {
pin_uv_auth_protocol: Some(protocol),
sub_command: Ctap2ClientPinSubcommand::GetPinUvAuthTokenUsingPinWithPermissions,
key_agreement: Some(key_agreement),
pin_uv_auth_param: None,
new_pin_enc: None,
pin_hash_enc: Some(pin_hash_enc),
permissions: Some(permissions),
rp_id,
};
let mut body = Vec::new();
ciborium::ser::into_writer(&request, &mut body)
.map_err(|_| StatusCode::from(U2FError::Other))?;
let response = self
.send_cbor_with_cancel(Ctap2Command::ClientPin, &body)
.await
.map_err(StatusCode::from)?;
let response: client_pin::Response =
ciborium::de::from_reader(response.get_payload()).unwrap_or_default();
// TODO: remove this expect
Ok(response
.pin_uv_auth_token
.expect("should have a pinUvAuthToken"))
}

/// `getPinRetries` subcommand of `clientPin`.
pub async fn get_pin_retries(&mut self, protocol: u8) -> Result<u32, StatusCode> {
let request = client_pin::Request {
pin_uv_auth_protocol: Some(protocol),
sub_command: Ctap2ClientPinSubcommand::GetPinRetries,
key_agreement: None,
pin_uv_auth_param: None,
new_pin_enc: None,
pin_hash_enc: None,
permissions: None,
rp_id: None,
};
let mut body = Vec::new();
ciborium::ser::into_writer(&request, &mut body)
.map_err(|_| StatusCode::from(U2FError::Other))?;
let response = self
.send_cbor_with_cancel(Ctap2Command::ClientPin, &body)
.await
.map_err(StatusCode::from)?;
let response: client_pin::Response =
ciborium::de::from_reader(response.get_payload()).unwrap_or_default();
// TODO: remove this expect
Ok(response.pin_retries.expect("Should have pin retries"))
}

/// `getUvRetries` subcommand of `clientPin`.
pub async fn get_uv_retries(&mut self, protocol: u8) -> Result<u32, StatusCode> {
let request = client_pin::Request {
pin_uv_auth_protocol: Some(protocol),
sub_command: Ctap2ClientPinSubcommand::GetUvRetries,
key_agreement: None,
pin_uv_auth_param: None,
new_pin_enc: None,
pin_hash_enc: None,
permissions: None,
rp_id: None,
};
let mut body = Vec::new();
ciborium::ser::into_writer(&request, &mut body)
.map_err(|_| StatusCode::from(U2FError::Other))?;
let response = self
.send_cbor_with_cancel(Ctap2Command::ClientPin, &body)
.await
.map_err(StatusCode::from)?;
let response: client_pin::Response =
ciborium::de::from_reader(response.get_payload()).unwrap_or_default();
// TODO: remove this expect
Ok(response.pin_retries.expect("Should have UV retries"))
}

/// Send a CTAPHID_CBOR request and await its response, forwarding any
/// signal received on `cancel_rx` to the device as a `CTAPHID_CANCEL`
/// without cancelling the outstanding `recv`. The pending recv is kept
Expand Down Expand Up @@ -182,6 +430,33 @@ impl LinuxAuthenticator {
enumerate_fido_devices()
}

/// Whether builtin UV is configured for this device.
pub fn uv_configured(&self) -> bool {
self.info().options.and_then(|o| o.uv).unwrap_or(false)
}

/// Whether a PIN is configured for this device.
pub fn pin_configured(&self) -> bool {
self.info()
.options
.and_then(|o| o.client_pin)
.unwrap_or(false)
}

/// Whether this device supports storing resident keys.
pub fn rk_supported(&self) -> bool {
self.info().options.is_some_and(|o| o.rk)
}

/// Whether the authenticator supports authenticatorClientPIN's
/// getPinUvAuthTokenUsingUvWithPermissions subcommand.
pub fn pin_uv_auth_token_supported(&self) -> bool {
self.info()
.options
.map(|o| o.pin_uv_auth_token == Some(true))
.unwrap_or(false)
}

/// Open a specific `/dev/hidrawN` path, run `CTAPHID_INIT` to obtain a private
/// channel, and prime the cached `authenticatorGetInfo` response.
pub async fn open(path: &Path) -> Result<Self, OpenError> {
Expand Down Expand Up @@ -221,22 +496,6 @@ impl LinuxAuthenticator {
pub fn info(&self) -> get_info::Response {
ciborium::de::from_reader(self.inner.get_info_cbor.get_payload()).unwrap_or_default()
}

/// Issue `authenticatorMakeCredential` against the device.
pub async fn make_credential(
&mut self,
request: make_credential::Request,
) -> Result<make_credential::Response, StatusCode> {
self.inner.make_credential(request).await
}

/// Issue `authenticatorGetAssertion` against the device.
pub async fn get_assertion(
&mut self,
request: get_assertion::Request,
) -> Result<get_assertion::Response, StatusCode> {
self.inner.get_assertion(request).await
}
}

/// Internal error type for CBOR transactions. Maps cleanly to both [`StatusCode`]
Expand Down
4 changes: 3 additions & 1 deletion passkey-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ android-asset-validation = ["dep:nom"]
testable = ["dep:mockall"]
tokio = ["dep:tokio"]
typeshare = ["passkey-types/typeshare", "dep:typeshare"]
linux = ["dep:tokio", "passkey-authenticator/linux", "tokio/rt"]
linux = ["dep:async-trait", "dep:tokio", "dep:zeroize", "passkey-authenticator/linux", "tokio/rt"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-trait = { version = "0.1", optional = true }
ciborium = "0.2"
coset = { workspace = true }
idna = "1"
Expand All @@ -40,6 +41,7 @@ serde_json = "1"
tokio = { version = "1", features = ["sync", "time", "rt"], optional = true }
typeshare = { version = "1", optional = true }
url = "2"
zeroize = { version = "1", features = ["zeroize_derive"], optional = true }

[dev-dependencies]
coset = { workspace = true }
Expand Down
Loading
Loading