Skip to content

Commit

Permalink
Redesign how credentials are looked up in GetAssertion
Browse files Browse the repository at this point in the history
  • Loading branch information
nickray committed Mar 5, 2022
1 parent a3ae881 commit b548b33
Show file tree
Hide file tree
Showing 15 changed files with 1,145 additions and 552 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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]

- use 2021 edition
- use @szszszsz's credential ID shortening
- get rid of the two big heaps, only cache timestamp + filename in GetAssertion
- bump to the released dependencies
- integrate `dispatch-fido`
14 changes: 10 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fido-authenticator"
version = "0.0.0-unreleased"
version = "0.1.0"
authors = ["Nicolas Stalder <[email protected]>"]
edition = "2021"
license = "Apache-2.0 OR MIT"
Expand All @@ -10,6 +10,7 @@ documentation = "https://docs.rs/fido-authenticator"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
ctap-types = "0.1.0"
delog = "0.1.0"
heapless = "0.7"
interchange = "0.2.0"
Expand All @@ -18,10 +19,15 @@ serde = { version = "1.0", default-features = false }
serde_cbor = { version = "0.11.0", default-features = false }
serde-indexed = "0.1.0"

ctap-types = { git = "https://github.com/solokeys/ctap-types" }
trussed = { git = "https://github.com/trussed-dev/trussed" }

apdu-dispatch = { version = "0.1", optional = true }
ctaphid-dispatch = { version = "0.1", optional = true }
iso7816 = { version = "0.1", optional = true }

[features]
default = []
dispatch = ["apdu-dispatch", "ctaphid-dispatch", "iso7816"]
disable-reset-time-window = []
enable-fido-pre = []

Expand All @@ -32,6 +38,6 @@ log-debug = []
log-warn = []
log-error = []

# [dev-dependencies]
[dev-dependencies]
# quickcheck = "1"
# rand = "0.8.4"
rand = "0.8.4"
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ As used in the [SoloKeys][solokeys] [Solo 2][solo2] and [Nitrokey 3][nitro3].
[ctap21ps]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html
[webauthnl2]: https://www.w3.org/TR/webauthn-2/

### Setup

For attestation to work, the authenticator's state needs to be provisioned with a batch
attestation key and certificate. They are expected in files `/fido/sec/00` and `/fido/x5c/00`,
respectively.

In the context of the SoloKeys Solo 2, "secure" devices are pre-provisioned; for "unlocked" devices,
if the firmware contains the provisioner app, this can be done with the CLI:

```sh
solo2 pki dev fido batch.key batch.cert
solo2 app provision store-fido-batch-key batch.key
solo2 app provision store-fido-batch-cert batch.cert
```

#### License

`fido-authenticator` is fully open source.
Expand Down
9 changes: 5 additions & 4 deletions src/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use trussed::{
};

pub(crate) use ctap_types::{
Bytes, Bytes32, String, Vec,
Bytes, String,
// authenticator::{ctap1, ctap2, Error, Request, Response},
ctap2::credential_management::CredentialProtectionPolicy,
sizes::*,
Expand Down Expand Up @@ -172,7 +172,8 @@ impl PartialOrd<&Credential> for Credential {
}
}

pub(crate) type CredentialList = Vec<Credential, {ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST}>;
// Bad idea - huge stack
// pub(crate) type CredentialList = Vec<Credential, {ctap_types::sizes::MAX_CREDENTIAL_COUNT_IN_LIST}>;

impl Into<PublicKeyCredentialDescriptor> for CredentialId {
fn into(self) -> PublicKeyCredentialDescriptor {
Expand Down Expand Up @@ -240,15 +241,15 @@ impl Credential {
&self,
trussed: &mut T,
key_encryption_key: KeyId,
rp_id_hash: Option<&Bytes32>,
rp_id_hash: Option<&Bytes<32>>,
)
-> Result<CredentialId>
{
let serialized_credential = self.strip().serialize()?;
let message = &serialized_credential;
// info!("serialized cred = {:?}", message).ok();

let rp_id_hash: Bytes32 = if let Some(hash) = rp_id_hash {
let rp_id_hash: Bytes<32> = if let Some(hash) = rp_id_hash {
hash.clone()
} else {
syscall!(trussed.hash_sha256(&self.rp.id.as_ref()))
Expand Down
97 changes: 34 additions & 63 deletions src/ctap1.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! The `ctap1::Authenticator` trait and its implementation.

use ctap_types::{
Bytes,
ctap1::{
self,
Command as Request,
Response,
Authenticator,
ControlByte,
register, authenticate,
Result,
Error,
},
heapless_bytes::Bytes,
};

use trussed::{
Expand All @@ -33,58 +33,25 @@ use crate::{
UserPresence,
};

/// CTAP1 (U2F) authenticator API
///
/// Ahh... life could be so simple!
//
// TODO: Lift into ctap-types?
pub trait Authenticator {
/// Register a U2F credential.
fn register(&mut self, request: &ctap1::Register) -> Result<ctap1::RegisterResponse>;
/// Authenticate with a U2F credential.
fn authenticate(&mut self, request: &ctap1::Authenticate) -> Result<ctap1::AuthenticateResponse>;
/// Supported U2F version.
fn version() -> [u8; 6] {
*b"U2F_V2"
}
}

impl<UP, T> crate::Authenticator<UP, T>
where UP: UserPresence,
T: TrussedRequirements,
{
/// Dispatches the enum of possible requests into the ctap1 [`Authenticator`] trait methods.
pub fn call_ctap1(&mut self, request: &Request) -> Result<Response> {
info!("called ctap1");
self.state.persistent.load_if_not_initialised(&mut self.trussed);

match request {
Request::Register(reg) =>
Ok(Response::Register(self.register(reg)?)),

Request::Authenticate(auth) =>
Ok(Response::Authenticate(self.authenticate(auth)?)),

Request::Version =>
Ok(ctap1::Response::Version(Self::version())),

}
}

// #[deprecated(note="please use `call_ctap1` instead")]
/// Alias of `call_ctap1`, may be deprecated in the future.
pub fn call_u2f(&mut self, request: &Request) -> Result<Response> {
self.call_ctap1(request)
}

}

type Commitment = Bytes::<324>;

/// Implement `ctap1::Authenticator` for our Authenticator.
///
/// ## References
/// The "proposed standard" of U2F V1.2 applies to CTAP1.
/// - [Message formats](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html)
/// - [App ID](https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-appid-and-facets-v1.2-ps-20170411.html)
impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenticator<UP, T>
{
fn register(&mut self, reg: &ctap1::Register) -> Result<ctap1::RegisterResponse> {
/// Register a new credential, this always uses P-256 keys.
///
/// Note that attestation is mandatory in CTAP1/U2F, so if the state
/// is not provisioned with a key/cert, this method will fail.
/// <https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-request-message---u2f_register>
///
/// Also note that CTAP1 credentials should be assertable over CTAP2. I believe this is
/// currently not the case.
fn register(&mut self, reg: &register::Request) -> Result<register::Response> {
self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT)
.map_err(|_| Error::ConditionsOfUseNotSatisfied)?;

Expand All @@ -95,25 +62,31 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
let serialized_cose_public_key = syscall!(self.trussed.serialize_p256_key(
public_key, KeySerialization::EcdhEsHkdf256
)).serialized_key;
syscall!(self.trussed.delete(public_key));
let cose_key: ctap_types::cose::EcdhEsHkdf256PublicKey
= trussed::cbor_deserialize(&serialized_cose_public_key).unwrap();

let wrapping_key = self.state.persistent.key_wrapping_key(&mut self.trussed)
.map_err(|_| Error::UnspecifiedCheckingError)?;
debug!("wrapping u2f private key");
// debug!("wrapping u2f private key");

let wrapped_key = syscall!(self.trussed.wrap_key_chacha8poly1305(
wrapping_key,
private_key,
&reg.app_id,
)).wrapped_key;
// debug!("wrapped_key = {:?}", &wrapped_key);

syscall!(self.trussed.delete(private_key));

let key = Key::WrappedKey(wrapped_key.to_bytes().map_err(|_| Error::UnspecifiedCheckingError)?);
let nonce = syscall!(self.trussed.random_bytes(12)).bytes.as_slice().try_into().unwrap();

let mut rp_id = heapless::String::new();

// We do not know the rpId string in U2F. Just using placeholder.
// TODO: Is this true?
// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#cross-version-credentials>
rp_id.push_str("u2f").ok();
let rp = ctap_types::webauthn::PublicKeyCredentialRpEntity{
id: rp_id,
Expand Down Expand Up @@ -146,8 +119,6 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
// 12.b generate credential ID { = AEAD(Serialize(Credential)) }
let kek = self.state.persistent.key_encryption_key(&mut self.trussed).map_err(|_| Error::NotEnoughMemory)?;
let credential_id = credential.id(&mut self.trussed, kek, Some(&reg.app_id)).map_err(|_| Error::NotEnoughMemory)?;
syscall!(self.trussed.delete(public_key));
syscall!(self.trussed.delete(private_key));

let mut commitment = Commitment::new();

Expand Down Expand Up @@ -183,7 +154,7 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
};


Ok(ctap1::RegisterResponse::new(
Ok(register::Response::new(
0x05,
&cose_key,
&credential_id.0,
Expand All @@ -192,11 +163,11 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
))
}

fn authenticate(&mut self, auth: &ctap1::Authenticate) -> Result<ctap1::AuthenticateResponse> {
fn authenticate(&mut self, auth: &authenticate::Request) -> Result<authenticate::Response> {
let cred = Credential::try_from_bytes(self, &auth.app_id, &auth.key_handle);

let user_presence_byte = match auth.control_byte {
ctap1::ControlByte::CheckOnly => {
ControlByte::CheckOnly => {
// if the control byte is set to 0x07 by the FIDO Client,
// the U2F token is supposed to simply check whether the
// provided key handle was originally created by this token
Expand All @@ -206,12 +177,12 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
Err(Error::IncorrectDataParameter)
};
},
ctap1::ControlByte::EnforceUserPresenceAndSign => {
ControlByte::EnforceUserPresenceAndSign => {
self.up.user_present(&mut self.trussed, constants::U2F_UP_TIMEOUT)
.map_err(|_| Error::ConditionsOfUseNotSatisfied)?;
0x01
},
ctap1::ControlByte::DontEnforceUserPresenceAndSign => 0x00,
ControlByte::DontEnforceUserPresenceAndSign => 0x00,
};

let cred = cred.map_err(|_| Error::IncorrectDataParameter)?;
Expand Down Expand Up @@ -262,11 +233,11 @@ impl<UP: UserPresence, T: TrussedRequirements> Authenticator for crate::Authenti
SignatureSerialization::Asn1Der
)).signature.to_bytes().unwrap();

Ok(ctap1::AuthenticateResponse::new(
user_presence_byte,
sig_count,
Ok(authenticate::Response {
user_presence: user_presence_byte,
count: sig_count,
signature,
))
})
}

}
Expand Down
Loading

0 comments on commit b548b33

Please sign in to comment.