diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f023b43 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.0] - 2022-03-05 + +- use 2021 edition +- make CTAP1 and CTAP2 more homogeneous +- add Authenticator traits +- lower `MAX_CREDENTIAL_ID_LENGTH` to 255 bytes, which seems to be the + limit used in practice (coming from U2F's size bytes) +- replace `MESSAGE_SIZE` with a theoretical and a realistic constant + diff --git a/Cargo.toml b/Cargo.toml index 9462210..6dfe686 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ctap-types" -version = "0.1.0" +version = "0.2.0" authors = ["Nicolas Stalder "] edition = "2021" @@ -14,13 +14,11 @@ delog = "0.1" heapless = { version = "0.7", default-features = false, features = ["serde"] } heapless-bytes = "0.3" interchange = "0.2.1" +iso7816 = "0.1" serde = { version = "1", default-features = false, features = ["derive"] } serde-indexed = "0.1" serde_repr = "0.1" -# iso7816 = { git = "https://github.com/ycrypto/iso7816" } -iso7816 = "0.1.0-alpha.1" - [features] log-all = ["cbor-smol/log-all"] log-none = [] diff --git a/src/authenticator.rs b/src/authenticator.rs index 178c279..46abf84 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -1,6 +1,10 @@ -//! The FIDO CTAP Authenticator API is a completely irregular RPC protocol. -//! Anytime there is some consistency in one place, another choice is made -//! in another place. Sorry! +//! The FIDO CTAP Authenticator API in terms of RPC with our types. + +use crate::ctap1; +use crate::ctap2; + +pub use ctap1::Authenticator as Ctap1Authenticator; +pub use ctap2::Authenticator as Ctap2Authenticator; // pub trait Authenticator { // fn process(&mut self, request: &mut Request) -> Result; @@ -26,113 +30,17 @@ pub enum Response { Ctap2(ctap2::Response), } -pub mod ctap1 { - pub use crate::ctap1; - - #[derive(Clone, Debug, PartialEq)] - pub enum Request { - Register(ctap1::Register), - Authenticate(ctap1::Register), - Version, - } - - #[derive(Clone, Debug, PartialEq)] - pub enum Response { - // Compiler not letting this enum be empty. - #[allow(non_camel_case_types)] - _unused, - } +/// Authenticator which supports both CTAP1 and CTAP2. +pub trait Authenticator: ctap1::Authenticator + ctap2::Authenticator { + // fn call(&mut self, request: &Request) -> Result { + // Ok(match request { + // Request::Ctap1(request) => Response::Ctap1(self.call_ctap1(request)?), + // Request::Ctap2(request) => Response::Ctap2(self.call_ctap2(request)?), + // }) + // } } -pub mod ctap2 { - pub use crate::ctap2::*; - #[derive(Clone, Debug, PartialEq)] - #[allow(clippy::large_enum_variant)] - // clippy says...large size difference - pub enum Request { - // 0x1 - MakeCredential(make_credential::Parameters), - // 0x2 - GetAssertion(get_assertion::Parameters), - // 0x8 - GetNextAssertion, - // 0x4 - GetInfo, - // 0x6 - ClientPin(client_pin::Parameters), - // 0x7 - Reset, - // 0xA - CredentialManagement(credential_management::Parameters), - // vendor, to be embellished - // Q: how to handle the associated CBOR structures - Vendor(crate::operation::VendorOperation), - } - - #[derive(Clone, Debug, PartialEq)] - pub enum Response { - MakeCredential(make_credential::Response), - GetAssertion(get_assertion::Response), - GetNextAssertion(get_assertion::Response), - GetInfo(get_info::Response), - ClientPin(client_pin::Response), - Reset, - CredentialManagement(credential_management::Response), - // Q: how to handle the associated CBOR structures - Vendor, - } -} +impl Authenticator for A {} // pub type Result = core::result::Result; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum Error { - Success = 0x00, - InvalidCommand = 0x01, - InvalidParameter = 0x02, - InvalidLength = 0x03, - InvalidSeq = 0x04, - Timeout = 0x05, - ChannelBusy = 0x06, - LockRequired = 0x0A, - InvalidChannel = 0x0B, - CborUnexpectedType = 0x11, - InvalidCbor = 0x12, - MissingParameter = 0x14, - LimitExceeded = 0x15, - UnsupportedExtension = 0x16, - CredentialExcluded = 0x19, - Processing = 0x21, - InvalidCredential = 0x22, - UserActionPending = 0x23, - OperationPending = 0x24, - NoOperations = 0x25, - UnsupportedAlgorithm = 0x26, - OperationDenied = 0x27, - KeyStoreFull = 0x28, - NotBusy = 0x29, - NoOperationPending = 0x2A, - UnsupportedOption = 0x2B, - InvalidOption = 0x2C, - KeepaliveCancel = 0x2D, - NoCredentials = 0x2E, - UserActionTimeout = 0x2F, - NotAllowed = 0x30, - PinInvalid = 0x31, - PinBlocked = 0x32, - PinAuthInvalid = 0x33, - PinAuthBlocked = 0x34, - PinNotSet = 0x35, - PinRequired = 0x36, - PinPolicyViolation = 0x37, - PinTokenExpired = 0x38, - RequestTooLarge = 0x39, - ActionTimeout = 0x3A, - UpRequired = 0x3B, - Other = 0x7F, - SpecLast = 0xDF, - ExtensionFirst = 0xE0, - ExtensionLast = 0xEF, - VendorFirst = 0xF0, - VendorLast = 0xFF, -} diff --git a/src/authenticator/ctap1.rs b/src/authenticator/ctap1.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/authenticator/ctap2.rs b/src/authenticator/ctap2.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/cose.rs b/src/cose.rs index 9a5c042..116bf82 100644 --- a/src/cose.rs +++ b/src/cose.rs @@ -1,4 +1,4 @@ -//! # cosey +//! Because why wouldn't pile JOSE on top of CBOR... //! //! Data types and serde for public COSE_Keys //! diff --git a/src/ctap1.rs b/src/ctap1.rs index 079bc39..aaaae13 100644 --- a/src/ctap1.rs +++ b/src/ctap1.rs @@ -1,11 +1,98 @@ -use iso7816::{Command as ApduCommand, Instruction}; - +//! Types for CTAP1. +//! +//! Note that all ctap1::Authenticators automatically implement RPC with [`Request`] and +//! [`Response`]. use crate::Bytes; pub const NO_ERROR: u16 = 0x9000; +/// Re-export of the iso7816::Status. pub use iso7816::Status as Error; +pub mod authenticate { + use super::{Bytes, ControlByte}; + + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Request { + pub control_byte: ControlByte, + pub challenge: Bytes<32>, + pub app_id: Bytes<32>, + pub key_handle: Bytes<255>, + } + + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Response { + pub user_presence: u8, + pub count: u32, + pub signature: Bytes<72>, + } + + // impl AuthenticateResponse { + // pub fn new(user_presence: u8, count: u32, signature: Bytes<72>) -> Self { + // Self { + // user_presence, + // count, + // signature, + // } + // } + // } +} + +pub mod register { + use super::Bytes; + + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Request { + pub challenge: Bytes<32>, + pub app_id: Bytes<32>, + } + + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Response { + pub header_byte: u8, + pub public_key: Bytes<65>, + pub key_handle: Bytes<255>, + pub attestation_certificate: Bytes<1024>, + pub signature: Bytes<72>, + } + + impl Response { + pub fn new( + header_byte: u8, + public_key: &crate::cose::EcdhEsHkdf256PublicKey, + key_handle: &[u8], + signature: Bytes<72>, + attestation_certificate: &[u8], + ) -> Self { + debug_assert!(key_handle.len() <= 255); + debug_assert!(attestation_certificate.len() <= 1024); + debug_assert!(signature.len() <= 72); + + let mut public_key_bytes = Bytes::new(); + let mut key_handle_bytes = Bytes::new(); + let mut cert_bytes = Bytes::new(); + + public_key_bytes.push(0x04).unwrap(); + public_key_bytes.extend_from_slice(&public_key.x).unwrap(); + public_key_bytes.extend_from_slice(&public_key.y).unwrap(); + + key_handle_bytes.extend_from_slice(key_handle).unwrap(); + + cert_bytes + .extend_from_slice(attestation_certificate) + .unwrap(); + + Self { + header_byte, + public_key: public_key_bytes, + key_handle: key_handle_bytes, + attestation_certificate: cert_bytes, + signature, + } + } + } +} + #[repr(u8)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ControlByte { @@ -19,7 +106,7 @@ pub enum ControlByte { DontEnforceUserPresenceAndSign = 0x08, } -impl core::convert::TryFrom for ControlByte { +impl TryFrom for ControlByte { type Error = Error; fn try_from(byte: u8) -> Result { @@ -34,100 +121,37 @@ impl core::convert::TryFrom for ControlByte { pub type Result = core::result::Result; -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Register { - pub challenge: Bytes<32>, - pub app_id: Bytes<32>, -} +/// Type alias for convenience. +pub type Register = register::Request; +/// Type alias for convenience. +pub type Authenticate = authenticate::Request; -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct RegisterResponse { - pub header_byte: u8, - pub public_key: Bytes<65>, - pub key_handle: Bytes<255>, - pub attestation_certificate: Bytes<1024>, - pub signature: Bytes<72>, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Authenticate { - pub control_byte: ControlByte, - pub challenge: Bytes<32>, - pub app_id: Bytes<32>, - pub key_handle: Bytes<255>, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AuthenticateResponse { - user_presence: u8, - count: u32, - signature: Bytes<72>, -} +/// Type alias for convenience. +pub type RegisterResponse = register::Response; +/// Type alias for convenience. +pub type AuthenticateResponse = authenticate::Response; #[derive(Clone, Debug, Eq, PartialEq)] #[allow(clippy::large_enum_variant)] -pub enum Command { - Register(Register), - Authenticate(Authenticate), +/// Enum of all CTAP1 requests. +pub enum Request { + Register(register::Request), + Authenticate(authenticate::Request), Version, } #[derive(Clone, Debug, Eq, PartialEq)] #[allow(clippy::large_enum_variant)] +/// Enum of all CTAP1 responses. pub enum Response { - Register(RegisterResponse), - Authenticate(AuthenticateResponse), + Register(register::Response), + Authenticate(authenticate::Response), Version([u8; 6]), } -impl RegisterResponse { - pub fn new( - header_byte: u8, - public_key: &crate::cose::EcdhEsHkdf256PublicKey, - key_handle: &[u8], - signature: Bytes<72>, - attestation_certificate: &[u8], - ) -> Self { - debug_assert!(key_handle.len() <= 255); - debug_assert!(attestation_certificate.len() <= 1024); - debug_assert!(signature.len() <= 72); - - let mut public_key_bytes = Bytes::new(); - let mut key_handle_bytes = Bytes::new(); - let mut cert_bytes = Bytes::new(); - - public_key_bytes.push(0x04).unwrap(); - public_key_bytes.extend_from_slice(&public_key.x).unwrap(); - public_key_bytes.extend_from_slice(&public_key.y).unwrap(); - - key_handle_bytes.extend_from_slice(key_handle).unwrap(); - - cert_bytes - .extend_from_slice(attestation_certificate) - .unwrap(); - - Self { - header_byte, - public_key: public_key_bytes, - key_handle: key_handle_bytes, - attestation_certificate: cert_bytes, - signature, - } - } -} - -impl AuthenticateResponse { - pub fn new(user_presence: u8, count: u32, signature: Bytes<72>) -> Self { - Self { - user_presence, - count, - signature, - } - } -} - impl Response { #[allow(clippy::result_unit_err)] + #[inline(never)] pub fn serialize( &self, buf: &mut iso7816::Data, @@ -150,12 +174,14 @@ impl Response { } } } -impl core::convert::TryFrom<&ApduCommand> for Command { + +impl TryFrom<&iso7816::Command> for Request { type Error = Error; - fn try_from(apdu: &ApduCommand) -> Result { + #[inline(never)] + fn try_from(apdu: &iso7816::Command) -> Result { let cla = apdu.class().into_inner(); let ins = match apdu.instruction() { - Instruction::Unknown(ins) => ins, + iso7816::Instruction::Unknown(ins) => ins, _ins => 0, }; let p1 = apdu.p1; @@ -168,7 +194,7 @@ impl core::convert::TryFrom<&ApduCommand> for Command { if ins == 0x3 { // for some weird historical reason, [0, 3, 0, 0, 0, 0, 0, 0, 0] // is valid to send here. - return Ok(Command::Version); + return Ok(Request::Version); }; let request = apdu.data(); @@ -179,7 +205,7 @@ impl core::convert::TryFrom<&ApduCommand> for Command { if request.len() != 64 { return Err(Error::IncorrectDataParameter); } - Ok(Command::Register(Register { + Ok(Request::Register(Register { challenge: Bytes::from_slice(&request[..32]).unwrap(), app_id: Bytes::from_slice(&request[32..]).unwrap(), })) @@ -195,7 +221,7 @@ impl core::convert::TryFrom<&ApduCommand> for Command { if request.len() != 65 + key_handle_length { return Err(Error::IncorrectDataParameter); } - Ok(Command::Authenticate(Authenticate { + Ok(Request::Authenticate(Authenticate { control_byte, challenge: Bytes::from_slice(&request[..32]).unwrap(), app_id: Bytes::from_slice(&request[32..64]).unwrap(), @@ -204,9 +230,49 @@ impl core::convert::TryFrom<&ApduCommand> for Command { } // version - 0x3 => Ok(Command::Version), + 0x3 => Ok(Request::Version), _ => Err(Error::InstructionNotSupportedOrInvalid), } } } + +/// CTAP1 (U2F) authenticator API +/// +/// Note that all Authenticators automatically implement RPC with [`Request`] and +/// [`Response`]. +pub trait Authenticator { + /// Register a U2F credential. + fn register(&mut self, request: ®ister::Request) -> Result; + /// Authenticate with a U2F credential. + fn authenticate( + &mut self, + request: &authenticate::Request, + ) -> Result; + /// Supported U2F version. + fn version() -> [u8; 6] { + *b"U2F_V2" + } + + #[inline(never)] + fn call_ctap1(&mut self, request: &Request) -> Result { + match request { + Request::Register(reg) => { + debug_now!("CTAP1.REG"); + Ok(Response::Register(self.register(reg)?)) + } + Request::Authenticate(auth) => { + debug_now!("CTAP1.AUTH"); + Ok(Response::Authenticate(self.authenticate(auth)?)) + } + Request::Version => Ok(Response::Version(Self::version())), + } + } +} + +impl crate::Rpc for A { + /// Dispatches the enum of possible requests into the appropriate trait method. + fn call(&mut self, request: &Request) -> Result { + self.call_ctap1(request) + } +} diff --git a/src/ctap2.rs b/src/ctap2.rs index 597b0e7..5c77767 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -1,16 +1,167 @@ +//! Types for CTAP2. +//! +//! Note that all ctap2::Authenticators automatically implement RPC with [`Request`] and +//! [`Response`]. use bitflags::bitflags; +use cbor_smol::cbor_deserialize; use serde::{Deserialize, Serialize}; -use crate::sizes::*; -use crate::Bytes; +use crate::{ + sizes::*, + Bytes, Vec, +}; + +pub use crate::operation::{Operation, VendorOperation}; pub mod client_pin; pub mod credential_management; pub mod get_assertion; pub mod get_info; -pub mod get_next_assertion; pub mod make_credential; + +pub type Result = core::result::Result; + + +#[derive(Clone, Debug, PartialEq)] +#[allow(clippy::large_enum_variant)] +// clippy says...large size difference +/// Enum of all CTAP2 requests. +pub enum Request { + // 0x1 + MakeCredential(make_credential::Request), + // 0x2 + GetAssertion(get_assertion::Request), + // 0x8 + GetNextAssertion, + // 0x4 + GetInfo, + // 0x6 + ClientPin(client_pin::Request), + // 0x7 + Reset, + // 0xA + CredentialManagement(credential_management::Request), + // vendor, to be embellished + // Q: how to handle the associated CBOR structures + Vendor(crate::operation::VendorOperation), +} + +pub enum CtapMappingError { + InvalidCommand(u8), + ParsingError(cbor_smol::Error), +} + +impl From for Error { + fn from(mapping_error: CtapMappingError) -> Error { + match mapping_error { + CtapMappingError::InvalidCommand(_cmd) => { + Error::InvalidCommand + } + CtapMappingError::ParsingError(cbor_error) => { + match cbor_error { + cbor_smol::Error::SerdeMissingField =>Error::MissingParameter, + _ => Error::InvalidCbor + } + } + } + + } +} + +impl Request { + /// Deserialize from CBOR where the first byte denotes the operation. + #[inline(never)] + pub fn deserialize(data: &[u8]) -> Result { + + if data.len() < 1 { + return Err(CtapMappingError::ParsingError(cbor_smol::Error::DeserializeUnexpectedEnd))?; + } + + let (&op, data) = data.split_first() + .ok_or_else(|| CtapMappingError::ParsingError(cbor_smol::Error::DeserializeUnexpectedEnd))?; + + let operation = Operation::try_from(op) + .map_err(|_| { + debug_now!("invalid operation {}", op); + CtapMappingError::InvalidCommand(op) + })?; + + info!("deser {:?}", operation); + Ok(match operation { + Operation::MakeCredential => + Request::MakeCredential(cbor_deserialize(data).map_err(CtapMappingError::ParsingError)?), + + Operation::GetAssertion => + Request::GetAssertion(cbor_deserialize(data).map_err(CtapMappingError::ParsingError)?), + + Operation::GetNextAssertion => Request::GetNextAssertion, + + Operation::CredentialManagement | Operation::PreviewCredentialManagement => + Request::CredentialManagement(cbor_deserialize(data).map_err(CtapMappingError::ParsingError)?), + + Operation::Reset => Request::Reset, + + Operation::GetInfo => Request::GetInfo, + + Operation::ClientPin => + Request::ClientPin(cbor_deserialize(data).map_err(CtapMappingError::ParsingError)?), + + // NB: FIDO Alliance "stole" 0x40 and 0x41, so these are not available + Operation::Vendor(vendor_operation) => Request::Vendor(vendor_operation), + + Operation::BioEnrollment | + Operation::PreviewBioEnrollment | + Operation::Config | + Operation::LargeBlobs | + Operation::Selection => { + debug_now!("unhandled CBOR operation {:?}", operation); + return Err(CtapMappingError::InvalidCommand(op))?; + } + }) + } +} + +#[derive(Clone, Debug, PartialEq)] +/// Enum of all CTAP2 responses. +pub enum Response { + MakeCredential(make_credential::Response), + GetAssertion(get_assertion::Response), + GetNextAssertion(get_assertion::Response), + GetInfo(get_info::Response), + ClientPin(client_pin::Response), + Reset, + CredentialManagement(credential_management::Response), + // Q: how to handle the associated CBOR structures + Vendor, +} + +impl Response { + #[inline(never)] + pub fn serialize(&self, buffer: &mut Vec) { + buffer.resize_default(buffer.capacity()).ok(); + let (status, data) = buffer.split_first_mut().unwrap(); + use Response::*; + use cbor_smol::cbor_serialize; + let outcome = match self { + GetInfo(response) => cbor_serialize(response, data), + MakeCredential(response) => cbor_serialize(response, data), + ClientPin(response) => cbor_serialize(response, data), + GetAssertion(response) | GetNextAssertion(response) => cbor_serialize(response, data), + CredentialManagement(response) => cbor_serialize(response, data), + Reset | Vendor => Ok([].as_slice()), + }; + if let Ok(slice) = outcome { + *status = 0; + let l = slice.len(); + buffer.resize_default(l + 1).ok(); + } else { + *status = Error::Other as u8; + buffer.resize_default(1).ok(); + } + } +} + // TODO: this is a bit weird to model... // Need to be able to "skip unknown keys" in deserialization // @@ -148,6 +299,7 @@ pub type SerializedAuthenticatorData = Bytes; // The reason for this non-use of CBOR is for compatibility with // FIDO U2F authentication signatures. impl AuthenticatorData { + #[inline(never)] pub fn serialize(&self) -> SerializedAuthenticatorData { // let mut bytes = Vec::::new(); let mut bytes = SerializedAuthenticatorData::new(); @@ -210,3 +362,174 @@ impl AuthenticatorData< // ES256, // EdDSA, // } + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Error { + Success = 0x00, + InvalidCommand = 0x01, + InvalidParameter = 0x02, + InvalidLength = 0x03, + InvalidSeq = 0x04, + Timeout = 0x05, + ChannelBusy = 0x06, + LockRequired = 0x0A, + InvalidChannel = 0x0B, + CborUnexpectedType = 0x11, + InvalidCbor = 0x12, + MissingParameter = 0x14, + LimitExceeded = 0x15, + UnsupportedExtension = 0x16, + CredentialExcluded = 0x19, + Processing = 0x21, + InvalidCredential = 0x22, + UserActionPending = 0x23, + OperationPending = 0x24, + NoOperations = 0x25, + UnsupportedAlgorithm = 0x26, + OperationDenied = 0x27, + KeyStoreFull = 0x28, + NotBusy = 0x29, + NoOperationPending = 0x2A, + UnsupportedOption = 0x2B, + InvalidOption = 0x2C, + KeepaliveCancel = 0x2D, + NoCredentials = 0x2E, + UserActionTimeout = 0x2F, + NotAllowed = 0x30, + PinInvalid = 0x31, + PinBlocked = 0x32, + PinAuthInvalid = 0x33, + PinAuthBlocked = 0x34, + PinNotSet = 0x35, + PinRequired = 0x36, + PinPolicyViolation = 0x37, + PinTokenExpired = 0x38, + RequestTooLarge = 0x39, + ActionTimeout = 0x3A, + UpRequired = 0x3B, + Other = 0x7F, + SpecLast = 0xDF, + ExtensionFirst = 0xE0, + ExtensionLast = 0xEF, + VendorFirst = 0xF0, + VendorLast = 0xFF, +} + +/// CTAP2 authenticator API +/// +/// Note that all Authenticators automatically implement [`crate::Rpc`] with [`Request`] and +/// [`Response`]. +pub trait Authenticator { + fn get_info(&mut self) -> get_info::Response; + fn make_credential( + &mut self, + request: &make_credential::Request, + ) -> Result; + fn get_assertion( + &mut self, + request: &get_assertion::Request, + ) -> Result; + fn get_next_assertion(&mut self) -> Result; + fn reset(&mut self) -> Result<()>; + fn client_pin(&mut self, request: &client_pin::Request) -> Result; + fn credential_management( + &mut self, + request: &credential_management::Request, + ) -> Result; + fn vendor(&mut self, op: VendorOperation) -> Result<()>; + + /// Dispatches the enum of possible requests into the appropriate trait method. + #[inline(never)] + fn call_ctap2(&mut self, request: &Request) -> Result { + match request { + // 0x4 + Request::GetInfo => { + debug_now!("CTAP2.GI"); + Ok(Response::GetInfo(self.get_info())) + } + + // 0x2 + Request::MakeCredential(request) => { + debug_now!("CTAP2.MC"); + Ok(Response::MakeCredential( + self.make_credential(request).map_err(|e| { + debug!("error: {:?}", e); + e + })?, + )) + } + + // 0x1 + Request::GetAssertion(request) => { + debug_now!("CTAP2.GA"); + Ok(Response::GetAssertion( + self.get_assertion(request).map_err(|e| { + debug!("error: {:?}", e); + e + })?, + )) + } + + // 0x8 + Request::GetNextAssertion => { + debug_now!("CTAP2.GNA"); + Ok(Response::GetNextAssertion( + self.get_next_assertion().map_err(|e| { + debug!("error: {:?}", e); + e + })?, + )) + } + + // 0x7 + Request::Reset => { + debug_now!("CTAP2.RST"); + self.reset().map_err(|e| { + debug!("error: {:?}", e); + e + })?; + Ok(Response::Reset) + } + + // 0x6 + Request::ClientPin(request) => { + debug_now!("CTAP2.PIN"); + Ok(Response::ClientPin(self.client_pin(request).map_err( + |e| { + debug!("error: {:?}", e); + e + }, + )?)) + } + + // 0xA + Request::CredentialManagement(request) => { + debug_now!("CTAP2.CM"); + Ok(Response::CredentialManagement( + self.credential_management(request).map_err(|e| { + debug!("error: {:?}", e); + e + })?, + )) + } + + // Not stable + Request::Vendor(op) => { + debug_now!("CTAP2.V"); + self.vendor(*op).map_err(|e| { + debug!("error: {:?}", e); + e + })?; + Ok(Response::Vendor) + } + } + } +} + +impl crate::Rpc for A { + /// Dispatches the enum of possible requests into the appropriate trait method. + #[inline(never)] + fn call(&mut self, request: &Request) -> Result { + self.call_ctap2(request) + } +} diff --git a/src/ctap2/client_pin.rs b/src/ctap2/client_pin.rs index f7bdf7c..b74c3b4 100644 --- a/src/ctap2/client_pin.rs +++ b/src/ctap2/client_pin.rs @@ -23,7 +23,7 @@ pub enum PinV1Subcommand { #[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)] #[serde_indexed(offset = 1)] -pub struct Parameters { +pub struct Request { // 0x01 // PIN protocol version chosen by the client. // For this version of the spec, this SHALL be the number 1. diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 164867d..397c051 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -1,14 +1,17 @@ -use crate::{Bytes16, Bytes32}; use serde_indexed::{DeserializeIndexed, SerializeIndexed}; use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::{ + Bytes, cose::PublicKey, webauthn::{ PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }, }; +type Bytes16 = Bytes<16>; +type Bytes32 = Bytes<32>; + #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize_repr, Deserialize_repr)] // #[derive(Clone,Debug,Eq,PartialEq,Serialize, Deserialize)] // #[serde(tag = "credProtect")] @@ -52,7 +55,7 @@ pub struct SubcommandParameters { #[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)] #[serde_indexed(offset = 1)] -pub struct Parameters { +pub struct Request { // 0x01 pub sub_command: Subcommand, // 0x02 diff --git a/src/ctap2/get_assertion.rs b/src/ctap2/get_assertion.rs index 22801eb..df9f435 100644 --- a/src/ctap2/get_assertion.rs +++ b/src/ctap2/get_assertion.rs @@ -55,7 +55,7 @@ pub type AllowList = Vec, pub client_data_hash: Bytes<32>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/ctap2/get_next_assertion.rs b/src/ctap2/get_next_assertion.rs deleted file mode 100644 index 88c1905..0000000 --- a/src/ctap2/get_next_assertion.rs +++ /dev/null @@ -1 +0,0 @@ -pub use super::get_assertion::Response; diff --git a/src/ctap2/make_credential.rs b/src/ctap2/make_credential.rs index 38cb413..e712125 100644 --- a/src/ctap2/make_credential.rs +++ b/src/ctap2/make_credential.rs @@ -11,7 +11,7 @@ use crate::webauthn::*; // // Approach 1: // pub type AuthenticatorExtensions = heapless::LinearMap, bool, 2>; -// impl core::convert::TryFrom<&String<44>> for CredentialProtectionPolicy { +// impl TryFrom<&String<44>> for CredentialProtectionPolicy { // type Error = crate::authenticator::Error; // fn try_from(value: &String<44>) -> Result { @@ -24,8 +24,8 @@ use crate::webauthn::*; // } // } -impl core::convert::TryFrom for CredentialProtectionPolicy { - type Error = crate::authenticator::Error; +impl TryFrom for CredentialProtectionPolicy { + type Error = super::Error; fn try_from(value: u8) -> Result { Ok(match value { @@ -69,7 +69,7 @@ pub struct Extensions { #[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)] // #[serde(rename_all = "camelCase")] #[serde_indexed(offset = 1)] -pub struct Parameters { +pub struct Request { pub client_data_hash: Bytes<32>, pub rp: PublicKeyCredentialRpEntity, pub user: PublicKeyCredentialUserEntity, diff --git a/src/lib.rs b/src/lib.rs index 6d3282b..641f1fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,22 +19,26 @@ extern crate delog; generate_macros!(); -pub use heapless::spsc::{Consumer, Producer, Queue}; +pub use heapless; +pub use heapless_bytes; pub use heapless::{String, Vec}; pub use heapless_bytes::Bytes; -pub type Bytes16 = Bytes<16>; -pub type Bytes32 = Bytes<32>; pub mod authenticator; pub mod cose; pub mod ctap1; pub mod ctap2; -pub mod operation; -pub mod rpc; -// pub mod serde; +pub(crate) mod operation; pub use cbor_smol as serde; pub mod sizes; pub mod webauthn; +pub use ctap2::{Error, Result}; + #[cfg(test)] mod tests {} + +/// Call a remote procedure with a request, receive a response, maybe. +pub trait Rpc { + fn call(&mut self, request: &Request) -> core::result::Result; +} diff --git a/src/operation.rs b/src/operation.rs index b83b8f6..11360af 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,5 +1,3 @@ -use core::convert::TryFrom; - /// the authenticator API, consisting of "operations" #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Operation { diff --git a/src/rpc.rs b/src/rpc.rs deleted file mode 100644 index c2fee6e..0000000 --- a/src/rpc.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![allow(clippy::declare_interior_mutable_const)] -use crate::authenticator::{Error, Request, Response}; - -// PRIOR ART: -// https://xenomai.org/documentation/xenomai-2.4/html/api/group__native__queue.html -// https://doc.micrium.com/display/osiiidoc/Using+Message+Queues - -interchange::interchange! { - CtapInterchange: (Request, Result) -} diff --git a/src/serde.rs b/src/serde.rs deleted file mode 100644 index 65d546b..0000000 --- a/src/serde.rs +++ /dev/null @@ -1,53 +0,0 @@ -pub mod de; -pub mod ser; -pub mod error; - -pub use error::{Error, Result}; - -// pub use de::from_bytes; -// pub use de::take_from_bytes; - -// kudos to postcard, this is much nicer than returning size -pub fn cbor_serialize<'a, 'b, T: serde::Serialize>( - object: &'a T, - buffer: &'b mut [u8], -) -> Result<&'b [u8]> { - let writer = ser::SliceWriter::new(buffer); - let mut ser = ser::Serializer::new(writer); - - object.serialize(&mut ser)?; - - let writer = ser.into_inner(); - let size = writer.bytes_written(); - - Ok(&buffer[..size]) -} - - -pub fn cbor_serialize_bytes<'a, 'b, N: heapless::ArrayLength, T: serde::Serialize>( - object: &'a T, - bytes: &'b mut heapless_bytes::Bytes, -) -> Result { - let len_before = bytes.len(); - let mut ser = ser::Serializer::new(bytes); - - object.serialize(&mut ser)?; - - Ok(ser.into_inner().len() - len_before) -} - - -pub fn cbor_serialize_bytes, T: serde::Serialize>(object: &T) -> Result> { - let mut data = heapless_bytes::Bytes::::new(); - cbor_serialize_bytes(object, &mut data)?; - Ok(data) -} - - -pub fn cbor_deserialize<'de, T: serde::Deserialize<'de>>( - buffer: &'de [u8], -) -> Result { - // cortex_m_semihosting::hprintln!("deserializing {:?}", buffer).ok(); - de::from_bytes(buffer) -} - diff --git a/src/sizes.rs b/src/sizes.rs index c48b6e0..c6d7568 100644 --- a/src/sizes.rs +++ b/src/sizes.rs @@ -11,11 +11,16 @@ pub const ASN1_SIGNATURE_LENGTH: usize = 77; pub const COSE_KEY_LENGTH: usize = 256; // pub const COSE_KEY_LENGTH_BYTES: usize = 256; -pub const MAX_CREDENTIAL_ID_LENGTH: usize = 512; -pub const MAX_CREDENTIAL_ID_LENGTH_PLUS_256: usize = 768; +pub const MAX_CREDENTIAL_ID_LENGTH: usize = 255; +pub const MAX_CREDENTIAL_ID_LENGTH_PLUS_256: usize = 767; pub const MAX_CREDENTIAL_COUNT_IN_LIST: usize = 10; pub const PACKET_SIZE: usize = 64; // 7609 bytes -pub const MESSAGE_SIZE: usize = PACKET_SIZE - 7 + 128 * (PACKET_SIZE - 5); +/// The theoretical maximal message size, which however is far +/// too large for most platforms. +pub const THEORETICAL_MAX_MESSAGE_SIZE: usize = PACKET_SIZE - 7 + 128 * (PACKET_SIZE - 5); +/// The size used by Yubico, which means that no platforms will +/// realistically expect a larger size. +pub const REALISTIC_MAX_MESSAGE_SIZE: usize = 1200; diff --git a/src/webauthn.rs b/src/webauthn.rs index b88fda3..95322a2 100644 --- a/src/webauthn.rs +++ b/src/webauthn.rs @@ -1,3 +1,5 @@ +//! Subset of WebAuthn types that crept into CTAP. + use crate::sizes::*; use crate::{Bytes, String}; use serde::{Deserialize, Serialize};