mirror of
https://github.com/trussed-dev/ctap-types.git
synced 2026-06-20 04:16:17 -07:00
Reorganize a little
This commit is contained in:
@@ -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
|
||||
|
||||
+2
-4
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ctap-types"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Nicolas Stalder <n@stalder.io>"]
|
||||
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 = []
|
||||
|
||||
+16
-108
@@ -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<Response, Error>;
|
||||
@@ -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<Response> {
|
||||
// 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<A: ctap1::Authenticator + ctap2::Authenticator> Authenticator for A {}
|
||||
|
||||
// pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
//! # cosey
|
||||
//! Because why wouldn't pile JOSE on top of CBOR...
|
||||
//!
|
||||
//! Data types and serde for public COSE_Keys
|
||||
//!
|
||||
|
||||
+155
-89
@@ -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<u8> for ControlByte {
|
||||
impl TryFrom<u8> for ControlByte {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(byte: u8) -> Result<ControlByte> {
|
||||
@@ -34,100 +121,37 @@ impl core::convert::TryFrom<u8> for ControlByte {
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
#[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<const S: usize>(
|
||||
&self,
|
||||
buf: &mut iso7816::Data<S>,
|
||||
@@ -150,12 +174,14 @@ impl Response {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<const S: usize> core::convert::TryFrom<&ApduCommand<S>> for Command {
|
||||
|
||||
impl<const S: usize> TryFrom<&iso7816::Command<S>> for Request {
|
||||
type Error = Error;
|
||||
fn try_from(apdu: &ApduCommand<S>) -> Result<Command> {
|
||||
#[inline(never)]
|
||||
fn try_from(apdu: &iso7816::Command<S>) -> Result<Request> {
|
||||
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<const S: usize> core::convert::TryFrom<&ApduCommand<S>> 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<const S: usize> core::convert::TryFrom<&ApduCommand<S>> 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<const S: usize> core::convert::TryFrom<&ApduCommand<S>> 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<const S: usize> core::convert::TryFrom<&ApduCommand<S>> 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<register::Response>;
|
||||
/// Authenticate with a U2F credential.
|
||||
fn authenticate(
|
||||
&mut self,
|
||||
request: &authenticate::Request,
|
||||
) -> Result<authenticate::Response>;
|
||||
/// Supported U2F version.
|
||||
fn version() -> [u8; 6] {
|
||||
*b"U2F_V2"
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn call_ctap1(&mut self, request: &Request) -> Result<Response> {
|
||||
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<A: Authenticator> crate::Rpc<Error, Request, Response> for A {
|
||||
/// Dispatches the enum of possible requests into the appropriate trait method.
|
||||
fn call(&mut self, request: &Request) -> Result<Response> {
|
||||
self.call_ctap1(request)
|
||||
}
|
||||
}
|
||||
|
||||
+326
-3
@@ -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<T> = core::result::Result<T, Error>;
|
||||
|
||||
|
||||
#[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<CtapMappingError> 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<Self> {
|
||||
|
||||
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<const N: usize>(&self, buffer: &mut Vec<u8, N>) {
|
||||
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<AUTHENTICATOR_DATA_LENGTH>;
|
||||
// The reason for this non-use of CBOR is for compatibility with
|
||||
// FIDO U2F authentication signatures.
|
||||
impl<A: SerializeAttestedCredentialData, E: serde::Serialize> AuthenticatorData<A, E> {
|
||||
#[inline(never)]
|
||||
pub fn serialize(&self) -> SerializedAuthenticatorData {
|
||||
// let mut bytes = Vec::<u8, AUTHENTICATOR_DATA_LENGTH>::new();
|
||||
let mut bytes = SerializedAuthenticatorData::new();
|
||||
@@ -210,3 +362,174 @@ impl<A: SerializeAttestedCredentialData, E: serde::Serialize> 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<make_credential::Response>;
|
||||
fn get_assertion(
|
||||
&mut self,
|
||||
request: &get_assertion::Request,
|
||||
) -> Result<get_assertion::Response>;
|
||||
fn get_next_assertion(&mut self) -> Result<get_assertion::Response>;
|
||||
fn reset(&mut self) -> Result<()>;
|
||||
fn client_pin(&mut self, request: &client_pin::Request) -> Result<client_pin::Response>;
|
||||
fn credential_management(
|
||||
&mut self,
|
||||
request: &credential_management::Request,
|
||||
) -> Result<credential_management::Response>;
|
||||
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<Response> {
|
||||
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<A: Authenticator> crate::Rpc<Error, Request, Response> for A {
|
||||
/// Dispatches the enum of possible requests into the appropriate trait method.
|
||||
#[inline(never)]
|
||||
fn call(&mut self, request: &Request) -> Result<Response> {
|
||||
self.call_ctap2(request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,7 +55,7 @@ pub type AllowList = Vec<PublicKeyCredentialDescriptor, MAX_CREDENTIAL_COUNT_IN_
|
||||
#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)]
|
||||
// #[serde(rename_all = "camelCase")]
|
||||
#[serde_indexed(offset = 1)]
|
||||
pub struct Parameters {
|
||||
pub struct Request {
|
||||
pub rp_id: String<64>,
|
||||
pub client_data_hash: Bytes<32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub use super::get_assertion::Response;
|
||||
@@ -11,7 +11,7 @@ use crate::webauthn::*;
|
||||
// // Approach 1:
|
||||
// pub type AuthenticatorExtensions = heapless::LinearMap<String<11>, 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<Self, Self::Error> {
|
||||
@@ -24,8 +24,8 @@ use crate::webauthn::*;
|
||||
// }
|
||||
// }
|
||||
|
||||
impl core::convert::TryFrom<u8> for CredentialProtectionPolicy {
|
||||
type Error = crate::authenticator::Error;
|
||||
impl TryFrom<u8> for CredentialProtectionPolicy {
|
||||
type Error = super::Error;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
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,
|
||||
|
||||
+10
-6
@@ -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<Error, Request, Response> {
|
||||
fn call(&mut self, request: &Request) -> core::result::Result<Response, Error>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use core::convert::TryFrom;
|
||||
|
||||
/// the authenticator API, consisting of "operations"
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Operation {
|
||||
|
||||
-10
@@ -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<Response, Error>)
|
||||
}
|
||||
@@ -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<u8>, T: serde::Serialize>(
|
||||
object: &'a T,
|
||||
bytes: &'b mut heapless_bytes::Bytes<N>,
|
||||
) -> Result<usize> {
|
||||
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<N: heapless::ArrayLength<u8>, T: serde::Serialize>(object: &T) -> Result<heapless_bytes::Bytes<N>> {
|
||||
let mut data = heapless_bytes::Bytes::<N>::new();
|
||||
cbor_serialize_bytes(object, &mut data)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
|
||||
pub fn cbor_deserialize<'de, T: serde::Deserialize<'de>>(
|
||||
buffer: &'de [u8],
|
||||
) -> Result<T> {
|
||||
// cortex_m_semihosting::hprintln!("deserializing {:?}", buffer).ok();
|
||||
de::from_bytes(buffer)
|
||||
}
|
||||
|
||||
+8
-3
@@ -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;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Subset of WebAuthn types that crept into CTAP.
|
||||
|
||||
use crate::sizes::*;
|
||||
use crate::{Bytes, String};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
Reference in New Issue
Block a user