Merge #239
239: examples-features: Add a featured CTAP example r=jrvanwhy a=alistair23
This PR adds a fully featured CTAP example.
In order to do this we use a few external crates:
* ctap2-authenticator: This handles CBOR decoding (using serde_cbor) and parses the CTAP data
* p256, subtle and generic-array: Are used for Elliptic Curve crypto
A few remaining issues:
* ~~Platforms without atomics don't work. ~~There are patches sent to the upstream projects, we just need them to update and then the crates we use to update.~~ Just waiting on the next release (0.4.11+) of https://github.com/rust-lang/log/releases~~
* ~~One change for ctap2-authenticator is required, I'll send a patch soon.~~
* The nonce is static. This is not cryptographically sound. This needs to be updated to use Tocks TRNG HIL
* Some functionality is still unimplemented
* ~~`self.hmac.init_driver().unwrap` doesn't work in a callback without implementing debug. I get this error otherwise: method not found in `core::result::Result<libtock::hmac::HmacDriver<'_>, libtock::result::TockError>`. Any ideas?~~
* Security audit, eventually
This PR adds functionality to:
* Handle information requests from the host
* Generate keys when requested from the host
* Sign and attest data sent from the host
This was all tested with the Yubico Python FIDO library. It will also tested and can be used to register and login to websites using FIDO2.
Co-authored-by: Alistair Francis <alistair.francis@wdc.com>
diff --git a/Cargo.toml b/Cargo.toml
index 5109187..caeae02 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,16 @@
# We pin the serde version because newer serde versions may not be compatible
# with the nightly toolchain used by libtock-rs.
serde = { version = "=1.0.114", default-features = false, features = ["derive"] }
+ctap2-authenticator = { git = "https://gitlab.com/ctap2-authenticator/ctap2-authenticator.git" }
+p256 = { version = "0.5.0" , default-features = false, features = ["arithmetic", "ecdsa", "ecdsa-core"] }
+subtle = { version = "2.3.0", default-features = false, features = ["i128"] }
+generic-array = { version = "0.14.3" }
+
+# We need to override this to allow builds for targets that don't support atomics.
+# Once a version newer then 0.4.11 is released we can update to use that.
+# See: https://github.com/rust-lang/log/releases
+[patch.crates-io]
+log = { git = "https://github.com/rust-lang/log.git", branch = "master" }
[[example]]
name = "alloc_error"
@@ -32,6 +42,11 @@
required-features = ["alloc", "custom_alloc_error_handler"]
[[example]]
+name = "ctap_features"
+path = "examples-features/ctap.rs"
+required-features = ["alloc", "custom_alloc_error_handler"]
+
+[[example]]
name = "ble_scanning"
path = "examples-features/ble_scanning.rs"
required-features = ["alloc"]
diff --git a/examples-features/ctap.rs b/examples-features/ctap.rs
new file mode 100644
index 0000000..3a66eff
--- /dev/null
+++ b/examples-features/ctap.rs
@@ -0,0 +1,507 @@
+//! This is a featured CTAP example
+//! WARNING! This currently uses unsound crypto operations
+//! This is only a demo and should not be used in real enviroments
+#![no_std]
+#![feature(alloc_error_handler)]
+
+extern crate alloc;
+
+use core::alloc::Layout;
+use core::cell::Cell;
+use core::convert::TryInto;
+use core::fmt::{self, Debug, Error, Formatter};
+use core::time::Duration;
+use ctap2_authenticator::credentials::{RpCredential, UserCredential};
+use ctap2_authenticator::usbhid::{
+ CtapHidCapabilities, CtapHidPlatform, KeepaliveResponse, TransactionProcessor,
+};
+use ctap2_authenticator::Authenticator;
+use ctap2_authenticator::{
+ AuthenticatorPlatform, CredentialDescriptorList, CtapOptions, PublicKey, Signature,
+};
+use generic_array::GenericArray;
+use libtock::ctap::{CtapRecvBuffer, CtapSendBuffer};
+use libtock::hmac::{HmacDataBuffer, HmacDestBuffer, HmacDriverFactory, HmacKeyBuffer};
+use libtock::println;
+use libtock::result::TockResult;
+use libtock::syscalls;
+use p256::ecdsa::{signature::Signer, SigningKey};
+use p256::elliptic_curve::ff::PrimeField;
+use p256::{Scalar, SecretKey};
+use subtle::{Choice, ConditionallySelectable};
+
+#[derive(Debug, Clone, PartialEq, Eq, Copy)]
+pub struct PrivateKey(Scalar);
+
+impl ConditionallySelectable for PrivateKey {
+ fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
+ Self(ConditionallySelectable::conditional_select(
+ &a.0, &b.0, choice,
+ ))
+ }
+}
+
+impl Default for PrivateKey {
+ fn default() -> Self {
+ Self(Scalar::default())
+ }
+}
+
+impl PrivateKey {
+ pub fn from_bytes(bytes: &[u8; 32]) -> Option<Self> {
+ Scalar::from_repr(GenericArray::clone_from_slice(bytes)).map(|s| Self(s))
+ }
+}
+
+/// This is the provided implementation of `CtapHidPlatform` to be used in the `UsbContext`
+pub(crate) struct UsbKeyHidPlatform {}
+
+impl UsbKeyHidPlatform {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+
+impl CtapHidPlatform for UsbKeyHidPlatform {
+ // Should be kept in sync with the Crates version if possible
+ const MAJOR_VERSION: u8 = 0;
+ const MINOR_VERSION: u8 = 0;
+ const BUILD_VERSION: u8 = 0;
+ const CAPABILITIES: CtapHidCapabilities = CtapHidCapabilities {
+ wink: true,
+ cbor: true,
+ msg: false,
+ };
+
+ fn wink(&mut self) {}
+
+ fn cancel(&mut self) {
+ println!("cancel");
+ unimplemented!()
+ }
+
+ fn keepalive_needed(&mut self) -> KeepaliveResponse {
+ println!("keepalive_needed");
+ unimplemented!()
+ }
+
+ fn start_timer(&mut self) {
+ println!("start_timer");
+ }
+
+ fn has_timed_out(&mut self) -> bool {
+ println!("has_timed_out");
+ false
+ }
+}
+
+#[derive(Clone, Copy)]
+pub struct HmacKeyCredential(pub(crate) [u8; 40]);
+
+impl HmacKeyCredential {
+ fn new(mac: &[u8; 32], nonce: [u8; 8]) -> Self {
+ let mut new = [0; 40];
+ new[..32].copy_from_slice(&mac[..]);
+ new[32..].copy_from_slice(&nonce[..]);
+ Self(new)
+ }
+
+ fn get_mac(&self) -> [u8; 32] {
+ let mut mac = [0; 32];
+ mac.copy_from_slice(&self.0[..32]);
+ mac
+ }
+}
+
+impl AsRef<[u8]> for HmacKeyCredential {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
+ }
+}
+
+impl Debug for HmacKeyCredential {
+ fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+ write!(
+ f,
+ "HmacKeyCredential {{ mac: {:?}, nonce: {:?} }}",
+ &self.0[..32],
+ &self.0[32..]
+ )
+ }
+}
+
+impl PartialEq for HmacKeyCredential {
+ fn eq(&self, other: &Self) -> bool {
+ self.0[..] == other.0[..]
+ }
+}
+
+impl Eq for HmacKeyCredential {}
+
+const ITERATOR_MAX_LENGTH: usize = 8;
+
+pub struct HmacCredentialQueue {
+ index: usize,
+ length: usize,
+ list: [Option<(HmacKeyCredential, u32)>; ITERATOR_MAX_LENGTH],
+}
+
+impl HmacCredentialQueue {
+ pub(crate) fn new() -> Self {
+ Self {
+ index: 0,
+ length: 0,
+ list: [None; ITERATOR_MAX_LENGTH],
+ }
+ }
+
+ pub(crate) fn push(&mut self, credential: HmacKeyCredential, counter: u32) -> Result<(), ()> {
+ if self.length < ITERATOR_MAX_LENGTH {
+ self.list[self.length] = Some((credential, counter));
+ self.length += 1;
+ Ok(())
+ } else {
+ Err(())
+ }
+ }
+
+ pub(crate) fn len(&self) -> usize {
+ self.length
+ }
+}
+
+impl Iterator for HmacCredentialQueue {
+ type Item = (HmacKeyCredential, u32);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.list[self.index].is_some() {
+ self.index += 1;
+ self.list[self.index - 1]
+ } else {
+ None
+ }
+ }
+}
+
+pub(crate) struct CtapPlatform {
+ hmac: HmacDriverFactory,
+}
+
+impl CtapPlatform {
+ fn new(hmac: HmacDriverFactory) -> Self {
+ Self { hmac }
+ }
+}
+
+impl fmt::Debug for CtapPlatform {
+ fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ Ok(())
+ }
+}
+
+impl CtapPlatform {
+ fn generate_key_seed(
+ &mut self,
+ rp_id: &str,
+ nonce: &[u8; 8],
+ ) -> [u8; libtock::hmac::DEST_BUFFER_SIZE] {
+ let hmac_driver;
+ match self.hmac.init_driver() {
+ Err(_) => {
+ panic!("Hmac init error");
+ }
+ Ok(driver) => {
+ hmac_driver = driver;
+ }
+ }
+
+ let mut key_buffer = HmacKeyBuffer::default();
+ for (i, d) in rp_id.as_bytes().iter().enumerate() {
+ key_buffer[i] = *d;
+ }
+ // We need to make sure this isn't dropped straight away
+ let key_buffer_ret = hmac_driver.init_key_buffer(&mut key_buffer);
+ if key_buffer_ret.is_err() {
+ panic!("Hmac key buffer init error");
+ }
+
+ let mut data_buffer = HmacDataBuffer::default();
+ for (i, d) in nonce.iter().enumerate() {
+ data_buffer[i] = *d;
+ }
+ // We need to make sure this isn't dropped straight away
+ let data_buffer_ret = hmac_driver.init_data_buffer(&mut data_buffer);
+ if data_buffer_ret.is_err() {
+ panic!("Hmac data buffer init error");
+ }
+
+ let mut dest_buffer = HmacDestBuffer::default();
+ let seed_buffer;
+ match hmac_driver.init_dest_buffer(&mut dest_buffer) {
+ Err(_) => {
+ panic!("Hmac subscribe error");
+ }
+ Ok(buffer) => {
+ seed_buffer = buffer;
+ }
+ }
+
+ let mut callback = |_result, _digest| {};
+
+ // We need to make sure this isn't dropped straight away
+ let subscribe = hmac_driver.subscribe(&mut callback);
+ if subscribe.is_err() {
+ panic!("Hmac subscribe error");
+ }
+
+ if hmac_driver.run().is_err() {
+ panic!("Hmac run error");
+ }
+
+ // Yield waiting for the HMAC callback
+ unsafe {
+ syscalls::raw::yieldk();
+ }
+
+ let mut temp_buffer = [0; libtock::hmac::DEST_BUFFER_SIZE];
+ seed_buffer.read_bytes(&mut temp_buffer[..]);
+ temp_buffer
+ }
+
+ /// Generates a Credential from a CredentialId and checks that the Id is valid
+ fn checked_generate_hmac_cred(
+ &mut self,
+ rp_id: &str,
+ credential_id: &[u8],
+ ) -> Option<HmacKeyCredential> {
+ if credential_id.len() != 40 {
+ return None;
+ }
+
+ let received_mac = &credential_id[..32];
+
+ let seed_buffer = self.generate_key_seed(rp_id, &credential_id[32..40].try_into().unwrap());
+
+ let credential =
+ HmacKeyCredential::new(&seed_buffer, credential_id[32..].try_into().unwrap());
+
+ let generated_mac = &credential.0[..32];
+
+ if generated_mac == received_mac {
+ Some(credential)
+ } else {
+ None
+ }
+ }
+}
+
+impl AuthenticatorPlatform for CtapPlatform {
+ const AAGUID: [u8; 16] = [
+ 0xf8, 0xa0, 0x11, 0xf3, 0x8c, 0x0a, 0x4d, 0x15, 0x80, 0x06, 0x17, 0x11, 0x1f, 0x9e, 0xdc,
+ 0x7d,
+ ];
+ const CERTIFICATE: Option<&'static [u8]> = None;
+ const MAX_MSG_LENGTH: u16 = 7609;
+ const SUPPORTED_OPTIONS: CtapOptions = CtapOptions {
+ plat: None,
+ rk: Some(true),
+ client_pin: None,
+ up: Some(true),
+ uv: Some(false),
+ };
+ type CredentialId = HmacKeyCredential;
+ type PublicKeyBuffer = [u8; 32];
+ type SignatureBuffer = [u8; 32];
+ type CredentialIterator = HmacCredentialQueue;
+
+ fn reset(&mut self) -> Result<(), ()> {
+ println!("reset");
+ unimplemented!()
+ }
+
+ fn check_exclude_list(&mut self, _rp_id: &str, _list: CredentialDescriptorList) -> bool {
+ println!("check_exclude_list");
+ unimplemented!()
+ }
+
+ fn locate_credentials(
+ &mut self,
+ rp_id: &str,
+ list: Option<CredentialDescriptorList>,
+ ) -> (u16, Self::CredentialIterator) {
+ match list {
+ None => (0, HmacCredentialQueue::new()),
+ Some(list) => {
+ let mut result = HmacCredentialQueue::new();
+
+ for descriptor in list {
+ match self.checked_generate_hmac_cred(rp_id, descriptor.get_id()) {
+ None => (),
+ Some(credential) => {
+ match result.push(credential, 0) {
+ Ok(()) => (),
+ // NOTE: We just truncate if we don't have enough space to store all
+ // fitting credentials
+ Err(()) => break,
+ }
+ }
+ }
+ }
+
+ (result.len() as u16, result)
+ }
+ }
+ }
+
+ fn create_credential(
+ &mut self,
+ rp: RpCredential<&[u8], &str>,
+ _user: UserCredential<&[u8], &str>,
+ ) -> (Self::CredentialId, PublicKey<Self::PublicKeyBuffer>, u32) {
+ println!("create_credential");
+ let rp_id = core::str::from_utf8(rp.rp_id()).unwrap();
+ // This nonce is static!!!!
+ // This is really bad cryptowise, let's print a warning
+ // TODO: Convert this to generate a nonce from Tock's TRNG
+ println!("WARNING!!! The nonce is static, this key is insecure");
+ println!("WARNING!!! Do not use this anywhere important");
+ let nonce = [0; 8];
+
+ let seed_buffer = self.generate_key_seed(rp_id, &nonce);
+
+ let credential = HmacKeyCredential::new(&seed_buffer, nonce.try_into().unwrap());
+
+ let secret_key = SecretKey::from_bytes(&seed_buffer[0..32]).unwrap();
+
+ let pub_key = p256::EncodedPoint::from_secret_key(&secret_key, false);
+
+ let x: [u8; 32] = pub_key.as_bytes()[1..33].try_into().unwrap();
+ let y: [u8; 32] = pub_key.as_bytes()[33..].try_into().unwrap();
+
+ let ret_key = PublicKey::nistp256(x, y);
+
+ (credential, ret_key, 0)
+ }
+
+ fn attest(
+ &mut self,
+ id: &Self::CredentialId,
+ data: &[u8],
+ ) -> Option<Signature<Self::SignatureBuffer>> {
+ // TODO: Should attest be different then sign?
+ let attest = {
+ let secret_key = SecretKey::from_bytes(id.get_mac()).unwrap();
+
+ let signer = SigningKey::from(&secret_key);
+
+ let sig = signer.sign(data);
+
+ let mut r: [u8; 32] = [0; 32];
+ r.clone_from_slice(&sig.r().as_ref().to_bytes());
+ let mut s: [u8; 32] = [0; 32];
+ s.clone_from_slice(&sig.s().as_ref().to_bytes());
+
+ Signature::nistp256(r, s)
+ };
+
+ Some(attest)
+ }
+
+ fn sign(
+ &mut self,
+ id: &Self::CredentialId,
+ data: &[u8],
+ ) -> Option<Signature<Self::SignatureBuffer>> {
+ let attest = {
+ let secret_key = SecretKey::from_bytes(id.get_mac()).unwrap();
+ let signer = SigningKey::from(&secret_key);
+ let sig = signer.sign(data);
+
+ let mut r: [u8; 32] = [0; 32];
+ r.clone_from_slice(&sig.r().as_ref().to_bytes());
+ let mut s: [u8; 32] = [0; 32];
+ s.clone_from_slice(&sig.s().as_ref().to_bytes());
+
+ Signature::nistp256(r, s)
+ };
+
+ Some(attest)
+ }
+
+ fn start_timeout(&mut self) {
+ println!("start_timeout");
+ unimplemented!()
+ }
+
+ fn has_timed_out(&mut self, _time: Duration) -> bool {
+ println!("has_timed_out");
+ unimplemented!()
+ }
+}
+
+#[alloc_error_handler]
+unsafe fn alloc_error_handler(_: Layout) -> ! {
+ println!("alloc_error_handler called");
+ loop {
+ syscalls::raw::yieldk();
+ }
+}
+
+#[libtock::main]
+async fn main() -> TockResult<()> {
+ let mut tp: Cell<TransactionProcessor<&'static mut [u8], UsbKeyHidPlatform>>;
+ let mut temp_buffer = [0; libtock::ctap::RECV_BUFFER_SIZE];
+ let mut drivers = libtock::retrieve_drivers()?;
+ drivers.console.create_console();
+
+ println!("Starting CTAP feature example");
+
+ let ctap_driver = drivers.ctap.init_driver()?;
+
+ let mut recv_buffer = CtapRecvBuffer::default();
+ let recv_buffer = ctap_driver.init_recv_buffer(&mut recv_buffer)?;
+
+ let mut send_buffer = CtapSendBuffer::default();
+ let mut send_buffer = ctap_driver.init_send_buffer(&mut send_buffer)?;
+
+ static mut BUFFER: &'static mut [u8] = &mut [0; 2048];
+ unsafe {
+ tp = Cell::new(TransactionProcessor::new(BUFFER, UsbKeyHidPlatform::new()));
+ }
+
+ let mut authenticator = Authenticator::create(CtapPlatform::new(drivers.hmac));
+
+ let mut callback = |sent, _| {
+ let mut looping = true;
+ recv_buffer.read_bytes(&mut temp_buffer[..]);
+
+ let temp_tp = tp.get_mut();
+ let mut poll_msg = if sent == 0 { Some(&temp_buffer) } else { None };
+
+ while looping {
+ looping = false;
+ let (maybe_request, maybe_data) = temp_tp.poll(poll_msg);
+
+ if let Some(request) = maybe_request {
+ let response = authenticator.process(request);
+ temp_tp.response(response);
+ looping = true;
+ }
+
+ if let Some(mut data) = maybe_data {
+ send_buffer.write_bytes(&mut data[0..]);
+ let _ = ctap_driver.send_data();
+ }
+
+ poll_msg = None;
+ }
+ let _ = ctap_driver.allow_receive();
+ };
+
+ let _subscription = ctap_driver.subscribe(&mut callback)?;
+ ctap_driver.allow_receive()?;
+
+ loop {
+ unsafe { syscalls::raw::yieldk() };
+ }
+}