commit 640023e0f838fd2cd635af43ef49498c18253399 Author: Arda Xi Date: Thu Dec 27 20:22:33 2018 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..940e2fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +Cargo.lock +src/bin diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..94f2be1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ctap" +description = "A Rust implementation of the FIDO2 CTAP protocol" +version = "0.1.0" +license = "Apache-2.0/MIT" +homepage = "https://github.com/ArdaXi/ctap" +repository = "https://github.com/ArdaXi/ctap" +authors = ["Arda Xi "] +edition = "2018" + +[dependencies] +rand = "0.6" +failure = "0.1" +failure_derive = "0.1" +num-traits = "0.2" +num-derive = "0.2" +byteorder = "1" +cbor-codec = "0.7" +ring = "0.13" +untrusted = "0.6" +rust-crypto = "0.2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..f44efd4 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +# ctap + +ctap is a library implementing the [FIDO2 CTAP](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html) protocol. + +## Usage example + +```rust +let devices = ctap::get_devices()?; +let device_info = &devices[0]; +let mut device = ctap::FidoDevice::new(device_info)?; + +// This can be omitted if the FIDO device is not configured with a PIN. +let pin = "test"; +device.unlock(pin)?; + +// In a real application these values would come from the requesting app. +let rp_id = "rp_id"; +let user_id = [0]; +let user_name = "user_name"; +let client_data_hash = [0; 32]; +let cred = device.make_credential( + rp_id, + &user_id, + user_name, + &client_data_hash +)?; + +// In a real application the credential would be stored and used later. +let result = device.get_assertion(&cred, &client_data_hash); +``` + +## Limitations + +Currently, this library only supports Linux. Testing and contributions for +other platforms is welcome. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/src/LICENSE-APACHE b/src/LICENSE-APACHE new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/src/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/src/LICENSE-MIT b/src/LICENSE-MIT new file mode 100644 index 0000000..f61ddce --- /dev/null +++ b/src/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Ariën Holthuizen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/cbor.rs b/src/cbor.rs new file mode 100644 index 0000000..434fadb --- /dev/null +++ b/src/cbor.rs @@ -0,0 +1,707 @@ +// This file is part of ctap, a Rust implementation of the FIDO2 protocol. +// Copyright (c) Ariën Holthuizen +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +use cbor_codec::{Config, Encoder, Decoder, GenericDecoder, GenericEncoder}; +use cbor_codec::value::Value; +use cbor_codec::value; + +use byteorder::{WriteBytesExt, ReadBytesExt, BigEndian, ByteOrder}; +use failure::ResultExt; + +use std::collections::HashMap; +use std::io::Cursor; + +use super::error::*; + +pub enum Request<'a> { + MakeCredential(MakeCredentialRequest<'a>), + GetAssertion(GetAssertionRequest<'a>), + GetInfo, + ClientPin(ClientPinRequest<'a>), +} + +impl<'a> Request<'a> { + pub fn encode(&self, writer: &mut W) -> FidoResult<()> { + let mut encoder = Encoder::new(writer); + match self { + Request::MakeCredential(req) => req.encode(&mut encoder), + Request::GetAssertion(req) => req.encode(&mut encoder), + Request::GetInfo => { + encoder + .writer() + .write_u8(0x04) + .context(FidoErrorKind::CborEncode) + .map_err(From::from) + } + Request::ClientPin(req) => req.encode(&mut encoder), + } + } + + pub fn decode(&self, reader: R) -> FidoResult { + Ok(match self { + Request::MakeCredential(_) => Response::MakeCredential( + MakeCredentialResponse::decode(reader)?, + ), + Request::GetAssertion(_) => Response::GetAssertion( + GetAssertionResponse::decode(reader)?, + ), + Request::GetInfo => Response::GetInfo(GetInfoResponse::decode(reader)?), + Request::ClientPin(_) => Response::ClientPin(ClientPinResponse::decode(reader)?), + }) + } +} + +#[derive(Debug)] +pub enum Response { + MakeCredential(MakeCredentialResponse), + GetAssertion(GetAssertionResponse), + GetInfo(GetInfoResponse), + ClientPin(ClientPinResponse), +} + +#[derive(Default, Debug)] +pub struct MakeCredentialRequest<'a> { + pub client_data_hash: &'a [u8], + pub rp: PublicKeyCredentialRpEntity<'a>, + pub user: PublicKeyCredentialUserEntity<'a>, + pub pub_key_cred_params: &'a [(&'a str, i32)], + pub exclude_list: &'a [PublicKeyCredentialDescriptor], + pub extensions: &'a [(&'a str, &'a Value)], + pub options: Option, + pub pin_auth: Option<[u8; 16]>, + pub pin_protocol: Option, +} + +impl<'a> MakeCredentialRequest<'a> { + pub fn encode(&self, mut encoder: &mut Encoder) -> FidoResult<()> { + encoder.writer().write_u8(0x01).context( + FidoErrorKind::CborEncode, + )?; // authenticatorMakeCredential + let mut length = 4; + length += !self.exclude_list.is_empty() as usize; + length += !self.extensions.is_empty() as usize; + length += self.options.is_some() as usize; + length += self.pin_auth.is_some() as usize; + length += self.pin_protocol.is_some() as usize; + encoder.object(length)?; + encoder.u8(0x01)?; // clientDataHash + encoder.bytes(&self.client_data_hash)?; + encoder.u8(0x02)?; // rp + self.rp.encode(&mut encoder)?; + encoder.u8(0x03)?; // user + self.user.encode(&mut encoder)?; + encoder.u8(0x04)?; // pubKeyCredParams + encoder.array(self.pub_key_cred_params.len())?; + for (cred_type, alg) in self.pub_key_cred_params { + encoder.object(2)?; + encoder.text("alg")?; + encoder.i32(*alg)?; + encoder.text("type")?; + encoder.text(&cred_type)?; + } + if self.exclude_list.len() > 0 { + encoder.u8(0x05)?; // excludeList + encoder.array(self.exclude_list.len())?; + for item in self.exclude_list { + item.encode(&mut encoder)?; + } + } + if self.extensions.len() > 0 { + encoder.u8(0x06)?; // extensions + encoder.object(self.extensions.len())?; + for (key, value) in self.extensions { + encoder.text(key)?; + let mut generic = GenericEncoder::new(encoder.writer()); + generic.value(value)?; + } + } + if let Some(options) = &self.options { + if options.encoded() { + encoder.u8(0x07)?; // options + options.encode(&mut encoder)?; + } + } + if let Some(pin_auth) = &self.pin_auth { + encoder.u8(0x08)?; // pinAuth + encoder.bytes(pin_auth)?; + } + if let Some(pin_protocol) = &self.pin_protocol { + encoder.u8(0x09)?; // pinProtocol + encoder.u8(*pin_protocol)?; + } + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct MakeCredentialResponse { + pub format: String, + pub auth_data: AuthenticatorData, +} + +impl MakeCredentialResponse { + pub fn decode(mut reader: R) -> FidoResult { + let status = reader.read_u8().context(FidoErrorKind::CborDecode)?; + if status != 0 { + Err(FidoErrorKind::CborError(status))? + } + let mut decoder = Decoder::new(Config::default(), reader); + let mut response = MakeCredentialResponse::default(); + for _ in 0..decoder.object()? { + let key = decoder.u8()?; + match key { + 0x01 => response.format = decoder.text()?, + 0x02 => response.auth_data = AuthenticatorData::from_bytes(&decoder.bytes()?)?, + 0x03 => break, // TODO: parse attestation + _ => continue, + } + } + Ok(response) + } +} + +#[derive(Debug, Default)] +pub struct GetAssertionRequest<'a> { + pub rp_id: &'a str, + pub client_data_hash: &'a [u8], + pub allow_list: &'a [PublicKeyCredentialDescriptor], + pub extensions: &'a [(&'a str, &'a Value)], + pub options: Option, + pub pin_auth: Option<[u8; 16]>, + pub pin_protocol: Option, +} + +impl<'a> GetAssertionRequest<'a> { + pub fn encode(&self, mut encoder: &mut Encoder) -> FidoResult<()> { + encoder.writer().write_u8(0x02).context( + FidoErrorKind::CborEncode, + )?; // authenticatorGetAssertion + let mut length = 2; + length += !self.allow_list.is_empty() as usize; + length += !self.extensions.is_empty() as usize; + length += self.options.is_some() as usize; + length += self.pin_auth.is_some() as usize; + length += self.pin_protocol.is_some() as usize; + encoder.object(length)?; + encoder.u8(0x01)?; // rpId + encoder.text(&self.rp_id)?; + encoder.u8(0x02)?; // clientDataHash + encoder.bytes(self.client_data_hash)?; + if !self.allow_list.is_empty() { + encoder.u8(0x03)?; // allowList + encoder.array(self.allow_list.len())?; + for item in self.allow_list { + item.encode(&mut encoder)?; + } + } + if self.extensions.len() > 0 { + encoder.u8(0x04)?; // extensions + encoder.object(self.extensions.len())?; + for (key, value) in self.extensions { + encoder.text(key)?; + let mut generic = GenericEncoder::new(encoder.writer()); + generic.value(value)?; + } + } + if let Some(options) = &self.options { + if options.encoded() { + encoder.u8(0x05)?; // options + options.encode(&mut encoder)?; + } + } + if let Some(pin_auth) = &self.pin_auth { + encoder.u8(0x06)?; // pinAuth + encoder.bytes(pin_auth)?; + } + if let Some(pin_protocol) = &self.pin_protocol { + encoder.u8(0x07)?; // pinProtocol + encoder.u8(*pin_protocol)?; + } + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct GetAssertionResponse { + pub credential: Option, + pub auth_data_bytes: Vec, + pub auth_data: AuthenticatorData, + pub signature: Vec, +} + +impl GetAssertionResponse { + pub fn decode(mut reader: R) -> FidoResult { + let status = reader.read_u8().context(FidoErrorKind::CborDecode)?; + if status != 0 { + Err(FidoErrorKind::CborError(status))? + } + let mut decoder = Decoder::new(Config::default(), reader); + let mut response = GetAssertionResponse::default(); + for _ in 0..decoder.object()? { + let key = decoder.u8()?; + match key { + 0x01 => { + response.credential = Some(PublicKeyCredentialDescriptor::decode(&mut decoder)?) + } + 0x02 => { + response.auth_data_bytes = decoder.bytes()?; + response.auth_data = AuthenticatorData::from_bytes(&response.auth_data_bytes)?; + } + 0x03 => response.signature = decoder.bytes()?, + _ => continue, + } + } + Ok(response) + } +} + +#[derive(Debug, Default)] +pub struct GetInfoResponse { + pub versions: Vec, + pub extensions: Vec, + pub aaguid: [u8; 16], + pub options: OptionsInfo, + pub max_msg_size: u16, + pub pin_protocols: Vec, +} + +impl GetInfoResponse { + pub fn decode(mut reader: R) -> FidoResult { + let status = reader.read_u8().context(FidoErrorKind::CborDecode)?; + if status != 0 { + Err(FidoErrorKind::CborError(status))? + } + let mut decoder = Decoder::new(Config::default(), reader); + let mut response = GetInfoResponse::default(); + for _ in 0..decoder.object()? { + match decoder.u8()? { + 0x01 => { + for _ in 0..decoder.array()? { + response.versions.push(decoder.text()?); + } + } + 0x02 => { + for _ in 0..decoder.array()? { + response.extensions.push(decoder.text()?); + } + } + 0x03 => response.aaguid.copy_from_slice(&decoder.bytes()?[..]), + 0x04 => response.options = OptionsInfo::decode(&mut decoder)?, + 0x05 => response.max_msg_size = decoder.u16()?, + 0x06 => { + for _ in 0..decoder.array()? { + response.pin_protocols.push(decoder.u8()?); + } + } + _ => continue, + } + } + Ok(response) + } +} + +#[derive(Debug, Default)] +pub struct ClientPinRequest<'a> { + pub pin_protocol: u8, + pub sub_command: u8, + pub key_agreement: Option<&'a CoseKey>, + pub pin_auth: Option<[u8; 16]>, + pub new_pin_enc: Option>, + pub pin_hash_enc: Option<[u8; 16]>, +} + +impl<'a> ClientPinRequest<'a> { + pub fn encode(&self, encoder: &mut Encoder) -> FidoResult<()> { + encoder.writer().write_u8(0x06).context( + FidoErrorKind::CborEncode, + )?; // authenticatorClientPIN + let mut length = 2; + length += self.key_agreement.is_some() as usize; + length += self.pin_auth.is_some() as usize; + length += self.new_pin_enc.is_some() as usize; + length += self.pin_hash_enc.is_some() as usize; + encoder.object(length)?; + encoder.u8(0x01)?; // pinProtocol + encoder.u8(self.pin_protocol)?; + encoder.u8(0x02)?; // subCommand + encoder.u8(self.sub_command)?; + if let Some(key_agreement) = self.key_agreement { + encoder.u8(0x03)?; // keyAgreement + key_agreement.encode(encoder)?; + } + if let Some(pin_auth) = &self.pin_auth { + encoder.u8(0x04)?; // pinAuth + encoder.bytes(pin_auth)?; + } + if let Some(new_pin_enc) = &self.new_pin_enc { + encoder.u8(0x05)?; // newPinEnc + encoder.bytes(&new_pin_enc)?; + } + if let Some(pin_hash_enc) = &self.pin_hash_enc { + encoder.u8(0x06)?; // pinHashEnc + encoder.bytes(pin_hash_enc)?; + } + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct ClientPinResponse { + pub key_agreement: Option, + pub pin_token: Option<[u8; 16]>, + pub retries: Option, +} + +impl ClientPinResponse { + pub fn decode(mut reader: R) -> FidoResult { + let status = reader.read_u8().context(FidoErrorKind::CborDecode)?; + if status != 0 { + Err(FidoErrorKind::CborError(status))? + } + let mut decoder = Decoder::new(Config::default(), reader); + let mut response = ClientPinResponse::default(); + for _ in 0..decoder.object()? { + match decoder.u8()? { + 0x01 => { + let mut generic = GenericDecoder::from_decoder(decoder); + response.key_agreement = Some(CoseKey::decode(&mut generic)?); + decoder = generic.into_inner(); + } + 0x02 => { + let mut pin_token = [0; 16]; + pin_token.copy_from_slice(&decoder.bytes()?[..]); + response.pin_token = Some(pin_token) + } + 0x03 => response.retries = Some(decoder.u8()?), + _ => continue, + } + } + Ok(response) + } +} + + +#[derive(Debug)] +pub struct OptionsInfo { + pub plat: bool, + pub rk: bool, + pub client_pin: Option, + pub up: bool, + pub uv: Option, +} + +impl Default for OptionsInfo { + fn default() -> Self { + OptionsInfo { + plat: false, + rk: false, + client_pin: None, + up: true, + uv: None, + } + } +} + +impl OptionsInfo { + pub fn decode(decoder: &mut Decoder) -> FidoResult { + let mut options = OptionsInfo::default(); + for _ in 0..decoder.object()? { + match decoder.text()?.as_ref() { + "plat" => options.plat = decoder.bool()?, + "rk" => options.rk = decoder.bool()?, + "clientPin" => options.client_pin = Some(decoder.bool()?), + "up" => options.up = decoder.bool()?, + "uv" => options.uv = Some(decoder.bool()?), + _ => continue, + } + } + Ok(options) + } +} + +#[derive(Debug, Default)] +pub struct AuthenticatorData { + pub rp_id_hash: [u8; 32], + pub up: bool, + pub uv: bool, + pub sign_count: u32, + pub attested_credential_data: AttestedCredentialData, + pub extensions: HashMap, +} + +impl AuthenticatorData { + pub fn from_bytes(bytes: &[u8]) -> FidoResult { + let mut data = AuthenticatorData::default(); + data.rp_id_hash.copy_from_slice(&bytes[0..32]); + let flags = bytes[32]; + data.up = (flags & 0x01) == 0x01; + data.uv = (flags & 0x02) == 0x02; + data.sign_count = BigEndian::read_u32(&bytes[33..37]); + if bytes.len() < 38 { + return Ok(data); + } + let mut cur = Cursor::new(&bytes[37..]); + let attested_credential_data = AttestedCredentialData::from_bytes(&mut cur)?; + data.attested_credential_data = attested_credential_data; + if cur.position() >= (bytes.len() - 37) as u64 { + return Ok(data); + } + let mut decoder = GenericDecoder::new(Config::default(), cur); + for _ in 0..decoder.borrow_mut().object()? { + let key = decoder.borrow_mut().text()?; + let value = decoder.value()?; + data.extensions.insert(key.to_string(), value); + } + Ok(data) + } +} + +#[derive(Debug, Default)] +pub struct AttestedCredentialData { + pub aaguid: [u8; 16], + pub credential_id: Vec, + pub credential_public_key: CoseKey, +} + +impl AttestedCredentialData { + pub fn from_bytes(cur: &mut Cursor<&[u8]>) -> FidoResult { + let mut response = AttestedCredentialData::default(); + let bytes = cur.get_ref(); + if bytes.is_empty() { + return Ok(response); + } + response.aaguid.copy_from_slice(&bytes[0..16]); + let id_length = BigEndian::read_u16(&bytes[16..18]) as usize; + response.credential_id = Vec::from(&bytes[18..(18 + id_length)]); + cur.set_position(18 + id_length as u64); + let mut decoder = GenericDecoder::new(Config::default(), cur); + response.credential_public_key = CoseKey::decode(&mut decoder)?; + Ok(response) + } +} + +#[derive(Debug, Default)] +pub struct P256Key { + x: [u8; 32], + y: [u8; 32], +} + +impl P256Key { + pub fn from_cose(cose: &CoseKey) -> FidoResult { + if cose.key_type != 2 || cose.algorithm != -7 { + Err(FidoErrorKind::KeyType)? + } + if let (Some(Value::U8(curve)), + Some(Value::Bytes(value::Bytes::Bytes(x))), + Some(Value::Bytes(value::Bytes::Bytes(y)))) = + ( + cose.parameters.get(&-1), + cose.parameters.get(&-2), + cose.parameters.get(&-3), + ) + { + if *curve != 1 { + Err(FidoErrorKind::KeyType)? + } + let mut key = P256Key::default(); + key.x.copy_from_slice(&x); + key.y.copy_from_slice(&y); + return Ok(key); + } + Err(FidoErrorKind::KeyType)? + } + + pub fn from_bytes(bytes: &[u8]) -> FidoResult { + if bytes.len() != 65 || bytes[0] != 0x04 { + Err(FidoErrorKind::CborDecode)? + } + let mut res = P256Key::default(); + res.x.copy_from_slice(&bytes[1..33]); + res.y.copy_from_slice(&bytes[33..65]); + Ok(res) + } + + pub fn to_cose(&self) -> CoseKey { + CoseKey { + key_type: 2, + algorithm: -7, + parameters: [ + (-1, Value::U8(1)), + (-2, Value::Bytes(value::Bytes::Bytes(self.x.to_vec()))), + (-3, Value::Bytes(value::Bytes::Bytes(self.y.to_vec()))), + ].iter() + .cloned() + .collect(), + } + } + + pub fn bytes(&self) -> [u8; 65] { + let mut bytes = [0; 65]; + bytes[0] = 0x04; + bytes[1..33].copy_from_slice(&self.x); + bytes[33..65].copy_from_slice(&self.y); + bytes + } +} + +#[derive(Debug, Default)] +pub struct CoseKey { + key_type: u16, + algorithm: i32, + parameters: HashMap, +} + +impl CoseKey { + pub fn encode(&self, encoder: &mut Encoder) -> FidoResult<()> { + let size = 1 + self.parameters.len(); + encoder.object(size)?; + encoder.i16(0x01)?; // keyType + encoder.u16(self.key_type)?; + //encoder.i16(0x02)?; // algorithm + //encoder.i32(self.algorithm)?; + for (key, value) in self.parameters.iter() { + encoder.i16(*key)?; + let mut generic = GenericEncoder::new(encoder.writer()); + generic.value(value)?; + } + Ok(()) + } + + pub fn decode(generic: &mut GenericDecoder) -> FidoResult { + let items; + { + let decoder = generic.borrow_mut(); + items = decoder.object()?; + } + let mut cose_key = CoseKey::default(); + cose_key.algorithm = -7; + for _ in 0..items { + match generic.borrow_mut().i16()? { + 0x01 => cose_key.key_type = generic.borrow_mut().u16()?, + 0x02 => cose_key.algorithm = generic.borrow_mut().i32()?, + key if key < 0 => { + cose_key.parameters.insert(key, generic.value()?); + } + _ => { + generic.value()?; // skip unknown parameter + } + } + } + Ok(cose_key) + } +} + +#[derive(Debug, Default)] +pub struct PublicKeyCredentialRpEntity<'a> { + pub id: &'a str, + pub name: Option<&'a str>, + pub icon: Option<&'a str>, +} + +impl<'a> PublicKeyCredentialRpEntity<'a> { + pub fn encode(&self, encoder: &mut Encoder) -> FidoResult<()> { + let mut length = 1; + length += self.name.is_some() as usize; + length += self.icon.is_some() as usize; + encoder.object(length)?; + encoder.text("id")?; + encoder.text(&self.id)?; + if let Some(icon) = &self.icon { + encoder.text("icon")?; + encoder.text(&icon)?; + } + if let Some(name) = &self.name { + encoder.text("name")?; + encoder.text(&name)?; + } + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct PublicKeyCredentialUserEntity<'a> { + pub id: &'a [u8], + pub name: &'a str, + pub icon: Option<&'a str>, + pub display_name: Option<&'a str>, +} + +impl<'a> PublicKeyCredentialUserEntity<'a> { + pub fn encode(&self, encoder: &mut Encoder) -> FidoResult<()> { + let mut length = 2; + length += self.icon.is_some() as usize; + length += self.display_name.is_some() as usize; + encoder.object(length)?; + encoder.text("id")?; + encoder.bytes(&self.id)?; + if let Some(icon) = &self.icon { + encoder.text("icon")?; + encoder.text(&icon)?; + } + encoder.text("name")?; + encoder.text(&self.name)?; + if let Some(display_name) = &self.display_name { + encoder.text("displayName")?; + encoder.text(&display_name)?; + } + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct PublicKeyCredentialDescriptor { + pub cred_type: String, + pub id: Vec, +} + +impl PublicKeyCredentialDescriptor { + pub fn decode(decoder: &mut Decoder) -> FidoResult { + let mut response = PublicKeyCredentialDescriptor::default(); + for _ in 0..decoder.object()? { + match decoder.text()?.as_ref() { + "id" => response.id = decoder.bytes()?, + "type" => response.cred_type = decoder.text()?, + _ => continue, + } + } + Ok(response) + } + + pub fn encode(&self, encoder: &mut Encoder) -> FidoResult<()> { + encoder.object(2)?; + encoder.text("id")?; + encoder.bytes(&self.id)?; + encoder.text("type")?; + encoder.text(&self.cred_type)?; + Ok(()) + } +} + +#[derive(Debug)] +pub struct AuthenticatorOptions { + pub rk: bool, + pub uv: bool, +} + +impl AuthenticatorOptions { + pub fn encoded(&self) -> bool { + self.rk || self.uv + } + + pub fn encode(&self, encoder: &mut Encoder) -> FidoResult<()> { + let length = (self.rk as usize) + (self.uv as usize); + encoder.object(length)?; + if self.rk { + encoder.text("rk")?; + encoder.bool(true)?; + } + if self.uv { + encoder.text("uv")?; + encoder.bool(true)?; + } + Ok(()) + } +} diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..0e7ced1 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,123 @@ +// This file is part of ctap, a Rust implementation of the FIDO2 protocol. +// Copyright (c) Ariën Holthuizen +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +use ring::{agreement, rand, digest, hmac, signature}; +use ring::error::Unspecified; +use untrusted::Input; +use rust_crypto::blockmodes::NoPadding; +use rust_crypto::aes; +use rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer}; +use failure::ResultExt; +use super::cbor::{CoseKey, P256Key}; +use super::error::*; + +#[derive(Debug)] +pub struct SharedSecret { + pub public_key: CoseKey, + pub shared_secret: [u8; 32], +} + +impl SharedSecret { + pub fn new(peer_key: &CoseKey) -> FidoResult { + let rng = rand::SystemRandom::new(); + let private = agreement::EphemeralPrivateKey::generate(&agreement::ECDH_P256, &rng) + .context(FidoErrorKind::GenerateKey)?; + let public = &mut [0u8; agreement::PUBLIC_KEY_MAX_LEN][..private.public_key_len()]; + private.compute_public_key(public).context( + FidoErrorKind::GenerateKey, + )?; + let peer = P256Key::from_cose(peer_key) + .context(FidoErrorKind::ParsePublic)? + .bytes(); + let peer = Input::from(&peer); + let shared_secret = agreement::agree_ephemeral( + private, + &agreement::ECDH_P256, + peer, + Unspecified, + |material| Ok(digest::digest(&digest::SHA256, material)), + ).context(FidoErrorKind::GenerateSecret)?; + let mut res = SharedSecret { + public_key: P256Key::from_bytes(&public) + .context(FidoErrorKind::ParsePublic)? + .to_cose(), + shared_secret: [0; 32], + }; + res.shared_secret.copy_from_slice(shared_secret.as_ref()); + Ok(res) + } + + pub fn encrypt_pin(&self, pin: &str) -> FidoResult<[u8; 16]> { + let mut encryptor = aes::cbc_encryptor( + aes::KeySize::KeySize256, + &self.shared_secret, + &[0u8; 16], + NoPadding, + ); + let pin_bytes = pin.as_bytes(); + let hash = digest::digest(&digest::SHA256, &pin_bytes); + let in_bytes = &hash.as_ref()[0..16]; + let mut input = RefReadBuffer::new(&in_bytes); + let mut out_bytes = [0; 16]; + let mut output = RefWriteBuffer::new(&mut out_bytes); + encryptor.encrypt(&mut input, &mut output, true).map_err( + |_| { + FidoErrorKind::EncryptPin + }, + )?; + Ok(out_bytes) + } + + pub fn decrypt_token(&self, data: &mut [u8]) -> FidoResult { + let mut decryptor = aes::cbc_decryptor( + aes::KeySize::KeySize256, + &self.shared_secret, + &[0u8; 16], + NoPadding, + ); + let mut input = RefReadBuffer::new(data); + let mut out_bytes = [0; 16]; + let mut output = RefWriteBuffer::new(&mut out_bytes); + decryptor.decrypt(&mut input, &mut output, true).map_err( + |_| { + FidoErrorKind::DecryptPin + }, + )?; + Ok(PinToken(hmac::SigningKey::new(&digest::SHA256, &out_bytes))) + } +} + +pub struct PinToken(hmac::SigningKey); + +impl PinToken { + pub fn auth(&self, data: &[u8]) -> [u8; 16] { + let signature = hmac::sign(&self.0, &data); + let mut out = [0; 16]; + out.copy_from_slice(&signature.as_ref()[0..16]); + out + } +} + +pub fn verify_signature( + public_key: &[u8], + client_data: &[u8], + auth_data: &[u8], + signature: &[u8], +) -> bool { + let public_key = Input::from(&public_key); + let msg_len = client_data.len() + auth_data.len(); + let mut msg = Vec::with_capacity(msg_len); + msg.extend_from_slice(auth_data); + msg.extend_from_slice(client_data); + let msg = Input::from(&msg); + let signature = Input::from(signature); + signature::verify( + &signature::ECDSA_P256_SHA256_ASN1, + public_key, + msg, + signature, + ).is_ok() +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..bb7b8f7 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,101 @@ +// This file is part of ctap, a Rust implementation of the FIDO2 protocol. +// Copyright (c) Ariën Holthuizen +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +use cbor_codec::{EncodeError, DecodeError}; + +use std::fmt; +use std::fmt::Display; +use failure::{Context, Backtrace, Fail}; + +pub type FidoResult = Result; + +#[derive(Debug)] +pub struct FidoError(Context); + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)] +pub enum FidoErrorKind { + #[fail(display = "Read/write error with device.")] + Io, + #[fail(display = "Error while reading packet from device.")] + ReadPacket, + #[fail(display = "Error while writing packet to device.")] + WritePacket, + #[fail(display = "Error while parsing CTAP from device.")] + ParseCtap, + #[fail(display = "Error while encoding CBOR for device.")] + CborEncode, + #[fail(display = "Error while decoding CBOR from device.")] + CborDecode, + #[fail(display = "Packets received from device in the wrong order.")] + InvalidSequence, + #[fail(display = "Failed to generate private keypair.")] + GenerateKey, + #[fail(display = "Failed to generate shared secret.")] + GenerateSecret, + #[fail(display = "Failed to parse public key.")] + ParsePublic, + #[fail(display = "Failed to encrypt PIN.")] + EncryptPin, + #[fail(display = "Failed to decrypt PIN.")] + DecryptPin, + #[fail(display = "Supplied key has incorrect type.")] + KeyType, + #[fail(display = "Device returned error: 0x{:x}", _0)] + CborError(u8), + #[fail(display = "Device does not support FIDO2")] + DeviceUnsupported, + #[fail(display = "This operating requires a PIN but none was provided.")] + PinRequired, +} + +impl Fail for FidoError { + fn cause(&self) -> Option<&Fail> { + self.0.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.0.backtrace() + } +} + +impl Display for FidoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl FidoError { + pub fn kind(&self) -> FidoErrorKind { + *self.0.get_context() + } +} + +impl From for FidoError { + #[inline(always)] + fn from(kind: FidoErrorKind) -> FidoError { + FidoError(Context::new(kind)) + } +} + +impl From> for FidoError { + fn from(inner: Context) -> FidoError { + FidoError(inner) + } +} + +impl From for FidoError { + #[inline(always)] + fn from(err: EncodeError) -> FidoError { + FidoError(err.context(FidoErrorKind::CborEncode)) + } +} + +impl From for FidoError { + #[inline(always)] + fn from(err: DecodeError) -> FidoError { + FidoError(err.context(FidoErrorKind::CborDecode)) + } +} diff --git a/src/hid_common.rs b/src/hid_common.rs new file mode 100644 index 0000000..c5258ef --- /dev/null +++ b/src/hid_common.rs @@ -0,0 +1,15 @@ +// This file is part of ctap, a Rust implementation of the FIDO2 protocol. +// Copyright (c) Ariën Holthuizen +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +use std::path::PathBuf; + +#[derive(Debug, Clone)] +/// Storage for device related information +pub struct DeviceInfo { + pub path: PathBuf, + pub usage_page: u16, + pub usage: u16, +} diff --git a/src/hid_linux.rs b/src/hid_linux.rs new file mode 100644 index 0000000..b04e75c --- /dev/null +++ b/src/hid_linux.rs @@ -0,0 +1,86 @@ +// This file is part of ctap, a Rust implementation of the FIDO2 protocol. +// Copyright (c) Ariën Holthuizen +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +use std::io; +use std::fs; +use std::path::PathBuf; +use byteorder::{ByteOrder, LittleEndian}; +pub use super::hid_common::*; + +static REPORT_DESCRIPTOR_KEY_MASK: u8 = 0xfc; +static LONG_ITEM_ENCODING: u8 = 0xfe; +static USAGE_PAGE: u8 = 0x04; +static USAGE: u8 = 0x08; +static REPORT_SIZE: u8 = 0x74; + +pub fn enumerate() -> io::Result> { + fs::read_dir("/sys/class/hidraw")? + .filter_map(|entry| entry.ok()) + .map(|entry| path_to_device(&entry.path())) + .collect() +} + +fn path_to_device(path: &PathBuf) -> io::Result { + let mut rd_path = path.clone(); + rd_path.push("device/report_descriptor"); + let rd = fs::read(rd_path)?; + let mut usage_page: u16 = 0; + let mut usage: u16 = 0; + let mut report_size: u16 = 0; + let mut pos: usize = 0; + + while pos < rd.len() { + let key = rd[pos]; + let mut key_size: usize = 1; + let mut size: u8; + + if key == LONG_ITEM_ENCODING { + key_size = 3; + size = rd[pos + 1]; + } else { + size = key & 0x03; + + if size == 0x03 { + size = 0x04 + } + } + + if key & REPORT_DESCRIPTOR_KEY_MASK == USAGE_PAGE { + if size != 2 { + usage_page = u16::from(rd[pos + 1]) + } else { + usage_page = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]); + } + } + + if key & REPORT_DESCRIPTOR_KEY_MASK == USAGE { + if size != 2 { + usage = u16::from(rd[pos + 1]) + } else { + usage = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]); + } + } + + if key & REPORT_DESCRIPTOR_KEY_MASK == REPORT_SIZE { + if size != 2 { + report_size = u16::from(rd[pos + 1]) + } else { + report_size = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]); + } + } + + pos = pos + key_size + size as usize; + } + + let mut device_path = PathBuf::from("/dev"); + device_path.push(path.file_name().unwrap()); + + Ok(DeviceInfo { + path: device_path, + usage_page, + usage, + }) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..47e8854 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,410 @@ +// This file is part of ctap, a Rust implementation of the FIDO2 protocol. +// Copyright (c) Ariën Holthuizen +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +//! An implementation of the CTAP2 protocol over USB. +//! +//! # Example +//! +//! ``` +//! # fn do_fido() -> ctap::FidoResult<()> { +//! let devices = ctap::get_devices()?; +//! let device_info = &devices[0]; +//! let mut device = ctap::FidoDevice::new(device_info)?; +//! +//! // This can be omitted if the FIDO device is not configured with a PIN. +//! let pin = "test"; +//! device.unlock(pin)?; +//! +//! // In a real application these values would come from the requesting app. +//! let rp_id = "rp_id"; +//! let user_id = [0]; +//! let user_name = "user_name"; +//! let client_data_hash = [0; 32]; +//! let cred = device.make_credential( +//! rp_id, +//! &user_id, +//! user_name, +//! &client_data_hash +//! )?; +//! +//! // In a real application the credential would be stored and used later. +//! let result = device.get_assertion(&cred, &client_data_hash); +//! # Ok(()) +//! # } + +#![allow(dead_code)] + +extern crate rand; +extern crate failure; +#[macro_use] +extern crate failure_derive; +#[macro_use] +extern crate num_derive; +extern crate num_traits; +extern crate byteorder; +extern crate cbor as cbor_codec; +extern crate ring; +extern crate untrusted; +extern crate crypto as rust_crypto; + +mod packet; +mod hid_common; +mod hid_linux; +mod error; +mod crypto; +mod cbor; + +use std::cmp; +use std::u8; +use std::u16; +use std::fs; +use std::io::{Read, Write, Cursor}; + +use failure::{Fail, ResultExt}; +use rand::prelude::*; +use num_traits::FromPrimitive; +use self::hid_linux as hid; +use self::packet::CtapCommand; +use self::packet::Packet; +pub use self::error::*; + +static BROADCAST_CID: [u8; 4] = [0xff, 0xff, 0xff, 0xff]; + +/// Looks for any connected HID devices and returns those that support FIDO. +pub fn get_devices() -> error::FidoResult> { + Ok( + hid::enumerate() + .context(FidoErrorKind::Io)? + .into_iter() + .filter(|dev| dev.usage_page == 0xf1d0 && dev.usage == 0x21) + .collect(), + ) +} + +/// A credential created by a FIDO2 authenticator. +#[derive(Debug)] +pub struct FidoCredential { + /// The ID provided by the authenticator. + pub id: Vec, + /// The public key provided by the authenticator, in uncompressed form. + pub public_key: Vec, + /// The Relying Party ID provided by the platform when this key was generated. + pub rp_id: String, +} + +/// An opened FIDO authenticator. +pub struct FidoDevice { + device: fs::File, + packet_size: u16, + channel_id: [u8; 4], + needs_pin: bool, + shared_secret: Option, + pin_token: Option, +} + +impl FidoDevice { + /// Open and initialize a given device. DeviceInfo is provided by the `get_devices` + /// function. This method will allocate a channel for this application, verify that + /// it supports FIDO2, and checks if a PIN is set. + /// + /// This method will fail if the device can't be opened, if the device returns + /// malformed data or if the device is not supported. + pub fn new(device: &hid::DeviceInfo) -> error::FidoResult { + let mut options = fs::OpenOptions::new(); + options.read(true).write(true); + let mut dev = FidoDevice { + device: options.open(&device.path).context(FidoErrorKind::Io)?, + packet_size: 64, + channel_id: BROADCAST_CID, + needs_pin: false, + shared_secret: None, + pin_token: None, + }; + dev.init()?; + Ok(dev) + } + + fn init(&mut self) -> FidoResult<()> { + let mut nonce = [0u8; 8]; + thread_rng().fill_bytes(&mut nonce); + let response = self.exchange(CtapCommand::Init, &nonce)?; + if response.len() < 17 || response[0..8] != nonce { + Err(FidoErrorKind::ParseCtap)? + } + self.channel_id.copy_from_slice(&response[8..12]); + let response = match self.cbor(cbor::Request::GetInfo)? { + cbor::Response::GetInfo(resp) => resp, + _ => Err(FidoErrorKind::CborDecode)?, + }; + if !response.versions.iter().any(|ver| ver == "FIDO_2_0") { + Err(FidoErrorKind::DeviceUnsupported)? + } + if !response.pin_protocols.iter().any(|ver| *ver == 1) { + Err(FidoErrorKind::DeviceUnsupported)? + } + self.needs_pin = response.options.client_pin == Some(true); + Ok(()) + } + + fn init_shared_secret(&mut self) -> FidoResult<()> { + let mut request = cbor::ClientPinRequest::default(); + request.pin_protocol = 1; + request.sub_command = 0x02; // getKeyAgreement + let response = match self.cbor(cbor::Request::ClientPin(request))? { + cbor::Response::ClientPin(resp) => resp, + _ => Err(FidoErrorKind::CborDecode)?, + }; + if let Some(key_agreement) = response.key_agreement { + self.shared_secret = Some(crypto::SharedSecret::new(&key_agreement)?); + Ok(()) + } else { + Err(FidoErrorKind::CborDecode)? + } + } + + /// Unlock the device with the provided PIN. Internally this will generate + /// an ECDH keypair, send the encrypted PIN to the device and store the PIN + /// token that the device generates on every power cycle. The PIN itself is + /// not stored. + /// + /// This method will fail if the device returns malformed data or the PIN is + /// incorrect. + pub fn unlock(&mut self, pin: &str) -> FidoResult<()> { + while self.shared_secret.is_none() { + self.init_shared_secret()?; + } + // If the PIN is invalid the device should create a new agreementKey, + // so we only replace shared_secret on success. + let shared_secret = self.shared_secret.take().unwrap(); + let mut request = cbor::ClientPinRequest::default(); + request.pin_protocol = 1; + request.sub_command = 0x05; // getPINToken + request.key_agreement = Some(&shared_secret.public_key); + request.pin_hash_enc = Some(shared_secret.encrypt_pin(pin)?); + let response = match self.cbor(cbor::Request::ClientPin(request))? { + cbor::Response::ClientPin(resp) => resp, + _ => Err(FidoErrorKind::CborDecode)?, + }; + if let Some(mut pin_token) = response.pin_token { + self.pin_token = Some(shared_secret.decrypt_token(&mut pin_token)?); + self.shared_secret = Some(shared_secret); + Ok(()) + } else { + Err(FidoErrorKind::CborDecode)? + } + } + + /// Request a new credential from the authenticator. The `rp_id` should be + /// a stable string used to identify the party for whom the credential is + /// created, for convenience it will be returned with the credential. + /// `user_id` and `user_name` are not required when requesting attestations + /// but they MAY be displayed to the user and MAY be stored on the device + /// to be returned with an attestation if the device supports this. + /// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`, + /// this is only used to verify the attestation provided by the + /// authenticator. When not implementing WebAuthN this can be any random + /// 32-byte array. + /// + /// This method will fail if a PIN is required but the device is not + /// unlocked or if the device returns malformed data. + pub fn make_credential( + &mut self, + rp_id: &str, + user_id: &[u8], + user_name: &str, + client_data_hash: &[u8], + ) -> FidoResult { + if self.needs_pin && self.pin_token.is_none() { + Err(FidoErrorKind::PinRequired)? + } + if client_data_hash.len() != 32 { + Err(FidoErrorKind::CborEncode)? + } + let pin_auth = self.pin_token.as_ref().map( + |token| token.auth(&client_data_hash), + ); + let rp = cbor::PublicKeyCredentialRpEntity { + id: rp_id, + name: None, + icon: None, + }; + let user = cbor::PublicKeyCredentialUserEntity { + id: user_id, + name: user_name, + icon: None, + display_name: None, + }; + let pub_key_cred_params = [("public-key", -7)]; + let request = cbor::MakeCredentialRequest { + client_data_hash, + rp, + user, + pub_key_cred_params: &pub_key_cred_params, + exclude_list: Default::default(), + extensions: Default::default(), + options: Some(cbor::AuthenticatorOptions { + rk: false, + uv: true, + }), + pin_auth, + pin_protocol: pin_auth.and(Some(0x01)), + }; + let response = match self.cbor(cbor::Request::MakeCredential(request))? { + cbor::Response::MakeCredential(resp) => resp, + _ => Err(FidoErrorKind::CborDecode)?, + }; + let public_key = cbor::P256Key::from_cose( + &response + .auth_data + .attested_credential_data + .credential_public_key, + )? + .bytes(); + Ok(FidoCredential { + id: response.auth_data.attested_credential_data.credential_id, + rp_id: String::from(rp_id), + public_key: Vec::from(&public_key[..]), + }) + } + + /// Request an assertion from the authenticator for a given credential. + /// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`, + /// this is signed and verified as part of the attestation. When not + /// implementing WebAuthN this can be any random 32-byte array. + /// + /// This method will return whether the assertion matches the credential + /// provided, and will fail if a PIN is required but not provided or if the + /// device returns malformed data. + pub fn get_assertion( + &mut self, + credential: &FidoCredential, + client_data_hash: &[u8], + ) -> FidoResult { + if self.needs_pin && self.pin_token.is_none() { + Err(FidoErrorKind::PinRequired)? + } + if client_data_hash.len() != 32 { + Err(FidoErrorKind::CborEncode)? + } + let pin_auth = self.pin_token.as_ref().map( + |token| token.auth(&client_data_hash), + ); + let allow_list = [ + cbor::PublicKeyCredentialDescriptor { + cred_type: String::from("public-key"), + id: credential.id.clone(), + }, + ]; + let request = cbor::GetAssertionRequest { + rp_id: &credential.rp_id, + client_data_hash: client_data_hash, + allow_list: &allow_list, + extensions: Default::default(), + options: Some(cbor::AuthenticatorOptions { + rk: false, + uv: true, + }), + pin_auth, + pin_protocol: pin_auth.and(Some(0x01)), + }; + let response = match self.cbor(cbor::Request::GetAssertion(request))? { + cbor::Response::GetAssertion(resp) => resp, + _ => Err(FidoErrorKind::CborDecode)?, + }; + Ok(crypto::verify_signature( + &credential.public_key, + &client_data_hash, + &response.auth_data_bytes, + &response.signature, + )) + } + + fn cbor(&mut self, request: cbor::Request) -> FidoResult { + let mut buf = Cursor::new(Vec::new()); + request.encode(&mut buf).context(FidoErrorKind::CborEncode)?; + let response = self.exchange(CtapCommand::Cbor, &buf.into_inner())?; + request + .decode(Cursor::new(response)) + .context(FidoErrorKind::CborDecode) + .map_err(From::from) + } + + fn exchange(&mut self, cmd: CtapCommand, payload: &[u8]) -> FidoResult> { + self.send(&cmd, payload)?; + self.receive(&cmd) + } + + fn send(&mut self, cmd: &CtapCommand, payload: &[u8]) -> FidoResult<()> { + if payload.is_empty() || payload.len() > u16::MAX as usize { + Err(FidoErrorKind::WritePacket)? + } + let to_send = payload.len() as u16; + let max_payload = (self.packet_size - 7) as usize; + let (frame, payload) = payload.split_at(cmp::min(payload.len(), max_payload)); + { + let packet = packet::InitPacket::new(&self.channel_id, cmd, to_send, frame); + self.device.write(packet.to_wire_format()).context( + FidoErrorKind::WritePacket, + )?; + } + if payload.is_empty() { + return Ok(()); + } + let max_payload = (self.packet_size - 5) as usize; + for (seq, frame) in (0..u8::MAX).zip(payload.chunks(max_payload)) { + let packet = packet::ContPacket::new(&self.channel_id, seq, frame); + self.device.write(packet.to_wire_format()).context( + FidoErrorKind::WritePacket, + )?; + } + self.device.flush().context(FidoErrorKind::WritePacket)?; + Ok(()) + } + + fn receive(&mut self, cmd: &CtapCommand) -> FidoResult> { + let mut first_packet: Option = None; + let mut packet_size = 0; + while first_packet.is_none() { + let mut buf = [0; 64]; + packet_size = self.device.read(&mut buf).context( + FidoErrorKind::ReadPacket, + )?; + let packet = packet::InitPacket::from_wire_format(&buf[0..packet_size]); + if packet.cmd() == CtapCommand::Error { + Err( + packet::CtapError::from_u8(packet.payload()[0]) + .unwrap_or(packet::CtapError::Other) + .context(FidoErrorKind::ParseCtap), + )? + } + if packet.cid() == self.channel_id && &packet.cmd() == cmd { + first_packet = Some(packet); + } + } + let first_packet = first_packet.unwrap(); + let mut data = first_packet.payload()[0..(packet_size - 7)].to_vec(); + let mut to_read = (first_packet.size() as isize) - data.len() as isize; + let mut seq = 0; + while to_read > 0 { + let mut buf = [0; 64]; + let packet_size = self.device.read(&mut buf).context( + FidoErrorKind::ReadPacket, + )?; + let packet = packet::ContPacket::from_wire_format(&buf[0..packet_size]); + if packet.cid() != self.channel_id { + continue; + } + if packet.seq() != seq { + Err(FidoErrorKind::InvalidSequence)? + } + let payload_size = packet_size - 5; + to_read -= payload_size as isize; + data.extend(&packet.payload()[0..payload_size]); + seq += 1; + } + Ok(data) + } +} diff --git a/src/packet.rs b/src/packet.rs new file mode 100644 index 0000000..19825df --- /dev/null +++ b/src/packet.rs @@ -0,0 +1,143 @@ +// This file is part of ctap, a Rust implementation of the FIDO2 protocol. +// Copyright (c) Ariën Holthuizen +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +use num_traits::{FromPrimitive, ToPrimitive}; + +static FRAME_INIT: u8 = 0x80; + +#[repr(u8)] +#[derive(FromPrimitive, ToPrimitive, PartialEq)] +pub enum CtapCommand { + Invalid = 0x00, + Ping = 0x01, + Msg = 0x03, + Lock = 0x04, + Init = 0x06, + Wink = 0x08, + Cbor = 0x10, + Cancel = 0x11, + Keepalive = 0x3b, + Error = 0x3f, +} + +impl CtapCommand { + pub fn to_wire_format(&self) -> u8 { + match self.to_u8() { + Some(x) => x, + None => 0x00, + } + } +} + +#[repr(u8)] +#[derive(FromPrimitive, Fail, Debug)] +pub enum CtapError { + #[fail(display = "The command in the request is invalid")] + InvalidCmd = 0x01, + #[fail(display = "The parameter(s) in the request is invalid")] + InvalidPar = 0x02, + #[fail(display = "The length field (BCNT) is invalid for the request ")] + InvalidLen = 0x03, + #[fail(display = "The sequence does not match expected value ")] + InvalidSeq = 0x04, + #[fail(display = "The message has timed out ")] + MsgTimeout = 0x05, + #[fail(display = "The device is busy for the requesting channel ")] + ChannelBusy = 0x06, + #[fail(display = "Command requires channel lock ")] + LockRequired = 0x0A, + #[fail(display = "Reserved error")] + NA = 0x0B, + #[fail(display = "Unspecified error")] + Other = 0x7F, +} + +pub trait Packet { + fn from_wire_format(data: &[u8]) -> Self; + + fn to_wire_format(&self) -> &[u8]; +} + +pub struct InitPacket(pub [u8; 65]); + +impl InitPacket { + pub fn new(cid: &[u8], cmd: &CtapCommand, size: u16, payload: &[u8]) -> InitPacket { + let mut packet = InitPacket([0; 65]); + packet.0[1..5].copy_from_slice(cid); + packet.0[5] = FRAME_INIT | cmd.to_wire_format(); + packet.0[6] = ((size >> 8) & 0xff) as u8; + packet.0[7] = (size & 0xff) as u8; + packet.0[8..(payload.len() + 8)].copy_from_slice(payload); + packet + } + + pub fn cid(&self) -> &[u8] { + &self.0[1..5] + } + + pub fn cmd(&self) -> CtapCommand { + match CtapCommand::from_u8(self.0[5] ^ FRAME_INIT) { + Some(cmd) => cmd, + None => CtapCommand::Invalid, + } + } + + pub fn size(&self) -> u16 { + ((u16::from(self.0[6])) << 8) | u16::from(self.0[7]) + } + + pub fn payload(&self) -> &[u8] { + &self.0[8..65] + } +} + +impl Packet for InitPacket { + fn from_wire_format(data: &[u8]) -> InitPacket { + let mut packet = InitPacket([0; 65]); + packet.0[1..65].copy_from_slice(data); + packet + } + + fn to_wire_format(&self) -> &[u8] { + &self.0 + } +} + +pub struct ContPacket(pub [u8; 65]); + +impl ContPacket { + pub fn new(cid: &[u8], seq: u8, payload: &[u8]) -> ContPacket { + let mut packet = ContPacket([0; 65]); + packet.0[1..5].copy_from_slice(cid); + packet.0[5] = seq; + packet.0[6..(payload.len() + 6)].copy_from_slice(payload); + packet + } + + pub fn cid(&self) -> &[u8] { + &self.0[1..5] + } + + pub fn seq(&self) -> u8 { + self.0[5] + } + + pub fn payload(&self) -> &[u8] { + &self.0[6..65] + } +} + +impl Packet for ContPacket { + fn from_wire_format(data: &[u8]) -> ContPacket { + let mut packet = ContPacket([0; 65]); + packet.0[1..65].copy_from_slice(data); + packet + } + + fn to_wire_format(&self) -> &[u8] { + &self.0 + } +}