├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── example ├── Cargo.toml ├── README.md ├── Rocket.toml ├── gem_cert.sh ├── src │ └── main.rs └── static │ └── index.html └── src ├── authorization.rs ├── crypto.rs ├── lib.rs ├── messages.rs ├── protocol.rs ├── register.rs ├── u2ferror.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | 4 | # IDEA 5 | *.iml 6 | .idea*.* 7 | .idea/* 8 | *.pem -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | dist: trusty 4 | rust: 5 | - stable 6 | - nightly 7 | 8 | cache: 9 | cargo: true 10 | 11 | matrix: 12 | allow_failures: 13 | - rust: nightly -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "u2f" 3 | version = "0.2.0" 4 | authors = ["Flavio Oliveira "] 5 | 6 | description = "Rust FIDO U2F Library" 7 | license = "MIT OR Apache-2.0" 8 | keywords = ["authentication", "encryption", "U2F", "2fa"] 9 | categories = ["authentication"] 10 | repository = "https://github.com/wisespace-io/u2f-rs" 11 | readme = "README.md" 12 | 13 | [badges] 14 | travis-ci = { repository = "wisespace-io/u2f-rs" } 15 | 16 | [lib] 17 | name = "u2f" 18 | path = "src/lib.rs" 19 | 20 | [dependencies] 21 | time = "0.1" 22 | bytes = "0.4" 23 | base64 = "0.11" 24 | chrono = "0.4" 25 | serde = "1.0" 26 | serde_json = "1.0" 27 | serde_derive = "1.0" 28 | byteorder = "1.3" 29 | openssl = "0.10" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 2 | 3 | Licensed under either of 4 | 5 | * Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) 6 | * MIT license (http://opensource.org/licenses/MIT) 7 | 8 | at your option. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust FIDO U2F Library   [![Build Status]][travis] [![Latest Version]][crates.io] [![MIT licensed]][MIT] [![Apache-2.0 licensed]][APACHE] 2 | 3 | [Build Status]: https://travis-ci.org/wisespace-io/u2f-rs.png?branch=master 4 | [travis]: https://travis-ci.org/wisespace-io/u2f-rs 5 | [Latest Version]: https://img.shields.io/crates/v/u2f.svg 6 | [crates.io]: https://crates.io/crates/u2f 7 | [MIT licensed]: https://img.shields.io/badge/License-MIT-blue.svg 8 | [MIT]: ./LICENSE-MIT 9 | [Apache-2.0 licensed]: https://img.shields.io/badge/License-Apache%202.0-blue.svg 10 | [APACHE]: ./LICENSE-APACHE 11 | 12 | ## u2f-rs 13 | 14 | Rust [FIDO U2F](https://fidoalliance.org/specifications/download/) library is a simple server side implementation to register and check signatures provided by U2F clients/devices. See [U2F Technical Overview](https://developers.yubico.com/U2F/Protocol_details/Overview.html) 15 | 16 | ## Usage 17 | 18 | Add this to your Cargo.toml 19 | 20 | ```toml 21 | [dependencies] 22 | u2f = "0.2" 23 | ``` 24 | 25 | Make sure that you have read [Using a U2F library](https://developers.yubico.com/U2F/Libraries/Using_a_library.html) before continuing. 26 | 27 | See provided [example](https://github.com/wisespace-io/u2f-rs/tree/master/example) 28 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "u2f-app" 3 | version = "0.2.0" 4 | authors = ["Flavio Oliveira "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | u2f = { path = "../" } 9 | serde = "^1.0" 10 | serde_json = "^1.0" 11 | lazy_static = "0.2" 12 | 13 | [dependencies.rocket] 14 | version = "0.4.0" 15 | features = ["tls"] 16 | 17 | [dependencies.rocket_contrib] 18 | version = "0.4.0" 19 | default-features = false 20 | features = ["json"] 21 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Nightly Rust 2 | 3 | The example uses [Rocket](https://github.com/SergioBenitez/Rocket/) so it requires Nightly Rust. 4 | 5 | ## Usage 6 | 7 | The certificate/private key pair used can be generated via openssl: 8 | 9 | ``` 10 | openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 -keyout key.pem -out cert.pem 11 | ``` 12 | 13 | Update Rocket.toml with the proper location 14 | 15 | ``` 16 | [global.tls] 17 | certs = "private/cert.pem" 18 | key = "private/key.pem" 19 | ``` 20 | 21 | The certificate is self-signed. You will need to trust it directly for your browser to refer to the connection as secure. 22 | 23 | Build and open the demo app 24 | 25 | ``` 26 | https://localhost:30443 27 | ``` -------------------------------------------------------------------------------- /example/Rocket.toml: -------------------------------------------------------------------------------- 1 | [development] 2 | address = "localhost" 3 | port = 30443 4 | 5 | # The certificate/private key pair used here was generated via openssl: 6 | # 7 | # openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 \ 8 | # -keyout key.pem -out cert.pem 9 | # 10 | # The certificate is self-signed. As such, you will need to trust it directly 11 | # for your browser to refer to the connection as secure. You should NEVER use 12 | # this certificate/key pair. It is here for DEMONSTRATION PURPOSES ONLY. 13 | [global.tls] 14 | certs = "private/cert.pem" 15 | key = "private/key.pem" -------------------------------------------------------------------------------- /example/gem_cert.sh: -------------------------------------------------------------------------------- 1 | mkdir private 2 | cd private/ 3 | 4 | openssl req -x509 -newkey rsa:4096 -nodes -sha256 -days 3650 -keyout key.pem -out cert.pem -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro_hygiene, decl_macro)] 2 | 3 | extern crate u2f; 4 | extern crate rocket; 5 | extern crate serde_json; 6 | 7 | #[macro_use] extern crate lazy_static; 8 | extern crate rocket_contrib; 9 | 10 | use std::io; 11 | 12 | use u2f::protocol::*; 13 | use u2f::messages::*; 14 | use u2f::register::*; 15 | 16 | use rocket::{State, catch, catchers, get, post, routes}; 17 | use rocket_contrib::json; 18 | use rocket_contrib::json::{Json, JsonValue}; 19 | use rocket::response::status::NotFound; 20 | use rocket::response::NamedFile; 21 | use rocket::http::{Cookie, Cookies}; 22 | 23 | use std::error::Error; 24 | use std::sync::Mutex; 25 | 26 | static APP_ID : &'static str = "https://localhost:30443"; 27 | 28 | lazy_static! { 29 | // In a real application this could be a database lookup. 30 | static ref REGISTRATIONS: Mutex> = { 31 | let registrations: Mutex> = Mutex::new(vec![]); 32 | registrations 33 | }; 34 | } 35 | 36 | struct U2fClient { 37 | pub u2f: U2f 38 | } 39 | 40 | #[get("/")] 41 | fn index() -> io::Result { 42 | NamedFile::open("static/index.html") 43 | } 44 | 45 | #[get("/api/register_request", format = "application/json")] 46 | fn register_request(mut cookies: Cookies, state: State) -> Json { 47 | let challenge = state.u2f.generate_challenge().unwrap(); 48 | let challenge_str = serde_json::to_string(&challenge); 49 | 50 | // Only for this demo we will keep the challenge in a private (encrypted) cookie 51 | cookies.add_private(Cookie::new("challenge", challenge_str.unwrap())); 52 | 53 | // Send registration request to the browser. 54 | let u2f_request = state.u2f.request(challenge.clone(), REGISTRATIONS.lock().unwrap().clone()); 55 | 56 | Json(u2f_request.unwrap()) 57 | } 58 | 59 | #[post("/api/register_response", format = "application/json", data = "")] 60 | fn register_response(mut cookies: Cookies, response: Json, state: State) -> Result> { 61 | 62 | let cookie = cookies.get_private("challenge"); 63 | 64 | if let Some(ref cookie) = cookie { 65 | let challenge: Challenge = serde_json::from_str(cookie.value()).unwrap(); 66 | let registration = state.u2f.register_response(challenge, response.into_inner()); 67 | match registration { 68 | Ok(reg) => { 69 | REGISTRATIONS.lock().unwrap().push(reg); 70 | cookies.remove_private(Cookie::named("challenge")); 71 | return Ok(json!({"status": "success"})); 72 | }, 73 | Err(e) => { 74 | return Err(NotFound(format!("{:?}", e.description()))); 75 | } 76 | } 77 | } else { 78 | return Err(NotFound(format!("Not able to recover challenge"))); 79 | } 80 | } 81 | 82 | #[get("/api/sign_request", format = "application/json")] 83 | fn sign_request(mut cookies: Cookies, state: State) -> Json { 84 | let challenge = state.u2f.generate_challenge().unwrap(); 85 | let challenge_str = serde_json::to_string(&challenge); 86 | 87 | // Only for this demo we will keep the challenge in a private (encrypted) cookie 88 | cookies.add_private(Cookie::new("challenge", challenge_str.unwrap())); 89 | 90 | let signed_request = state.u2f.sign_request(challenge, REGISTRATIONS.lock().unwrap().clone()); 91 | 92 | return Json(signed_request); 93 | } 94 | 95 | #[post("/api/sign_response", format = "application/json", data = "")] 96 | fn sign_response(mut cookies: Cookies, response: Json, state: State) -> Result> { 97 | let cookie = cookies.get_private("challenge"); 98 | if let Some(ref cookie) = cookie { 99 | let challenge: Challenge = serde_json::from_str(cookie.value()).unwrap(); 100 | 101 | let registrations = REGISTRATIONS.lock().unwrap().clone(); 102 | let sign_resp = response.into_inner(); 103 | 104 | let mut _counter: u32 = 0; 105 | for registration in registrations { 106 | let response = state.u2f.sign_response(challenge.clone(), registration, sign_resp.clone(), _counter); 107 | match response { 108 | Ok(new_counter) => { 109 | _counter = new_counter; 110 | return Ok(json!({"status": "success"})); 111 | }, 112 | Err(_e) => { 113 | break; 114 | } 115 | } 116 | } 117 | return Err(NotFound(format!("error verifying response"))); 118 | } else { 119 | return Err(NotFound(format!("Not able to recover challenge"))); 120 | } 121 | } 122 | 123 | #[catch(404)] 124 | fn not_found() -> JsonValue { 125 | json!({ 126 | "status": "error", 127 | "reason": "Resource was not found." 128 | }) 129 | } 130 | 131 | fn rocket() -> rocket::Rocket { 132 | let u2f_client = U2fClient { 133 | u2f: U2f::new(APP_ID.into()) 134 | }; 135 | 136 | rocket::ignite() 137 | .mount("/", routes![index, register_request, register_response, sign_request, sign_response]) 138 | .register(catchers![not_found]) 139 | .manage(u2f_client) 140 | } 141 | 142 | fn main() { 143 | rocket().launch(); 144 | } -------------------------------------------------------------------------------- /example/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Rust U2F App test

10 | 11 |

12 | 13 | 14 | 124 | 125 | -------------------------------------------------------------------------------- /src/authorization.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, BufMut}; 2 | use std::io::Cursor; 3 | use openssl::sha::sha256; 4 | 5 | 6 | use crate::u2ferror::U2fError; 7 | 8 | 9 | /// The `Result` type used in this crate. 10 | type Result = ::std::result::Result; 11 | 12 | #[derive(Serialize, Clone)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct Authorization { 15 | pub counter: u32, 16 | pub user_presence: bool, 17 | } 18 | 19 | pub fn parse_sign_response(app_id: String, client_data: Vec, public_key: Vec, sign_data: Vec) -> Result { 20 | 21 | if sign_data.len() <= 5 { 22 | return Err(U2fError::InvalidSignatureData) 23 | } 24 | 25 | let user_presence_flag = &sign_data[0]; 26 | let counter = &sign_data[1..=4]; 27 | let signature = &sign_data[5..]; 28 | 29 | // Let's build the msg to verify the signature 30 | let app_id_hash = sha256(&app_id.into_bytes()); 31 | let client_data_hash = sha256(&client_data[..]); 32 | 33 | let mut msg = vec![]; 34 | msg.put(app_id_hash.as_ref()); 35 | msg.put(user_presence_flag.clone()); 36 | msg.put(counter.clone()); 37 | msg.put(client_data_hash.as_ref()); 38 | 39 | let public_key = super::crypto::NISTP256Key::from_bytes(&public_key)?; 40 | 41 | // The signature is to be verified by the relying party using the public key obtained during registration. 42 | let verified = public_key.verify_signature(&signature[..], msg.as_ref())?; 43 | if !verified { 44 | return Err(U2fError::BadSignature) 45 | } 46 | 47 | let authorization = Authorization { 48 | counter: get_counter(counter), 49 | user_presence: true 50 | }; 51 | 52 | Ok(authorization) 53 | } 54 | 55 | 56 | fn get_counter(counter: &[u8]) -> u32 { 57 | let mut buf = Cursor::new(&counter[..]); 58 | buf.get_u32_be() 59 | } -------------------------------------------------------------------------------- /src/crypto.rs: -------------------------------------------------------------------------------- 1 | //! Cryptographic operation wrapper for Webauthn. This module exists to 2 | //! allow ease of auditing, safe operation wrappers for the webauthn library, 3 | //! and cryptographic provider abstraction. This module currently uses OpenSSL 4 | //! as the cryptographic primitive provider. 5 | 6 | // Source can be found here: https://github.com/Firstyear/webauthn-rs/blob/master/src/crypto.rs 7 | 8 | #![allow(non_camel_case_types)] 9 | 10 | use openssl::{bn, ec, hash, nid, sign, x509}; 11 | use std::convert::TryFrom; 12 | 13 | // use super::constants::*; 14 | use u2ferror::U2fError; 15 | use openssl::pkey::Public; 16 | 17 | // use super::proto::*; 18 | 19 | // Why OpenSSL over another rust crate? 20 | // - Well, the openssl crate allows us to reconstruct a public key from the 21 | // x/y group coords, where most others want a pkcs formatted structure. As 22 | // a result, it's easiest to use openssl as it gives us exactly what we need 23 | // for these operations, and despite it's many challenges as a library, it 24 | // has resources and investment into it's maintenance, so we can a least 25 | // assert a higher level of confidence in it that . 26 | 27 | // Object({Integer(-3): Bytes([48, 185, 178, 204, 113, 186, 105, 138, 190, 33, 160, 46, 131, 253, 100, 177, 91, 243, 126, 128, 245, 119, 209, 59, 186, 41, 215, 196, 24, 222, 46, 102]), Integer(-2): Bytes([158, 212, 171, 234, 165, 197, 86, 55, 141, 122, 253, 6, 92, 242, 242, 114, 158, 221, 238, 163, 127, 214, 120, 157, 145, 226, 232, 250, 144, 150, 218, 138]), Integer(-1): U64(1), Integer(1): U64(2), Integer(3): I64(-7)}) 28 | // 29 | 30 | /// An X509PublicKey. This is what is otherwise known as a public certificate 31 | /// which comprises a public key and other signed metadata related to the issuer 32 | /// of the key. 33 | pub struct X509PublicKey { 34 | pubk: x509::X509, 35 | } 36 | 37 | impl std::fmt::Debug for X509PublicKey { 38 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 39 | write!(f, "X509PublicKey") 40 | } 41 | } 42 | 43 | impl TryFrom<&[u8]> for X509PublicKey { 44 | type Error = U2fError; 45 | 46 | // Must be DER bytes. If you have PEM, base64decode first! 47 | fn try_from(d: &[u8]) -> Result { 48 | let pubk = x509::X509::from_der(d).map_err(|e| U2fError::OpenSSLError(e))?; 49 | Ok(X509PublicKey { pubk: pubk }) 50 | } 51 | } 52 | 53 | impl X509PublicKey { 54 | pub (crate) fn common_name(&self) -> Option { 55 | let cert = &self.pubk; 56 | 57 | let subject = cert.subject_name(); 58 | let common = subject.entries_by_nid(openssl::nid::Nid::COMMONNAME) 59 | .next() 60 | .map(|b| b.data().as_slice()); 61 | 62 | if let Some(common) = common { 63 | std::str::from_utf8(common).ok().map(|s| s.to_string()) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | pub(crate) fn is_secp256r1(&self) -> Result { 70 | // Can we get the public key? 71 | let pk = self 72 | .pubk 73 | .public_key() 74 | .map_err(|e| U2fError::OpenSSLError(e))?; 75 | 76 | let ec_key = pk.ec_key().map_err(|e| U2fError::OpenSSLError(e))?; 77 | 78 | ec_key 79 | .check_key() 80 | .map_err(|e| U2fError::OpenSSLError(e))?; 81 | 82 | let ec_grpref = ec_key.group(); 83 | 84 | let ec_curve = ec_grpref 85 | .curve_name() 86 | .ok_or(U2fError::OpenSSLNoCurveName)?; 87 | 88 | Ok(ec_curve == nid::Nid::X9_62_PRIME256V1) 89 | } 90 | 91 | pub(crate) fn verify_signature( 92 | &self, 93 | signature: &[u8], 94 | verification_data: &[u8], 95 | ) -> Result { 96 | let pkey = self 97 | .pubk 98 | .public_key() 99 | .map_err(|e| U2fError::OpenSSLError(e))?; 100 | 101 | // TODO: Should this determine the hash type from the x509 cert? Or other? 102 | let mut verifier = sign::Verifier::new(hash::MessageDigest::sha256(), &pkey) 103 | .map_err(|e| U2fError::OpenSSLError(e))?; 104 | verifier 105 | .update(verification_data) 106 | .map_err(|e| U2fError::OpenSSLError(e))?; 107 | verifier 108 | .verify(signature) 109 | .map_err(|e| U2fError::OpenSSLError(e)) 110 | } 111 | } 112 | 113 | pub struct NISTP256Key { 114 | /// The key's public X coordinate. 115 | pub x: [u8; 32], 116 | /// The key's public Y coordinate. 117 | pub y: [u8; 32], 118 | } 119 | 120 | 121 | impl NISTP256Key { 122 | pub fn from_bytes(public_key_bytes: &[u8]) -> Result { 123 | if public_key_bytes.len() != 65 { 124 | return Err(U2fError::InvalidPublicKey) 125 | } 126 | 127 | if public_key_bytes[0] != 0x04 { 128 | return Err(U2fError::InvalidPublicKey) 129 | } 130 | 131 | let mut x:[u8; 32] = Default::default(); 132 | x.copy_from_slice(&public_key_bytes[1..=32]); 133 | 134 | let mut y:[u8; 32] = Default::default(); 135 | y.copy_from_slice(&public_key_bytes[33..=64]); 136 | 137 | Ok(NISTP256Key { 138 | x, y 139 | }) 140 | } 141 | 142 | fn get_key(&self) -> Result, U2fError> { 143 | let ec_group = ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1) 144 | .map_err(|e| U2fError::OpenSSLError(e))?; 145 | 146 | let xbn = 147 | bn::BigNum::from_slice(&self.x).map_err(|e| U2fError::OpenSSLError(e))?; 148 | let ybn = 149 | bn::BigNum::from_slice(&self.y).map_err(|e| U2fError::OpenSSLError(e))?; 150 | 151 | let ec_key = openssl::ec::EcKey::from_public_key_affine_coordinates(&ec_group, &xbn, &ybn) 152 | .map_err(|e| U2fError::OpenSSLError(e))?; 153 | 154 | // Validate the key is sound. IIRC this actually checks the values 155 | // are correctly on the curve as specified 156 | ec_key.check_key() 157 | .map_err(|e| U2fError::OpenSSLError(e))?; 158 | 159 | Ok(ec_key) 160 | } 161 | 162 | 163 | pub fn verify_signature(&self, signature: &[u8], verification_data: &[u8]) 164 | -> Result 165 | { 166 | let pkey = self.get_key()?; 167 | 168 | let signature = openssl::ecdsa::EcdsaSig::from_der(signature).map_err(|e| U2fError::OpenSSLError(e))?; 169 | let hash = openssl::sha::sha256(&verification_data); 170 | 171 | signature.verify(hash.as_ref(), &pkey).map_err(|e| U2fError::OpenSSLError(e)) 172 | } 173 | } 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | extern crate serde; 4 | extern crate serde_json; 5 | 6 | extern crate time; 7 | extern crate bytes; 8 | extern crate byteorder; 9 | extern crate chrono; 10 | extern crate base64; 11 | extern crate openssl; 12 | 13 | mod util; 14 | 15 | pub mod u2ferror; 16 | pub mod register; 17 | pub mod messages; 18 | pub mod protocol; 19 | pub mod authorization; 20 | mod crypto; -------------------------------------------------------------------------------- /src/messages.rs: -------------------------------------------------------------------------------- 1 | // As defined by FIDO U2F Javascript API. 2 | // https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-javascript-api.html#registration 3 | 4 | #[derive(Serialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct U2fRegisterRequest { 7 | pub app_id: String, 8 | pub register_requests: Vec, 9 | pub registered_keys: Vec, 10 | } 11 | 12 | #[derive(Serialize)] 13 | pub struct RegisterRequest { 14 | pub version: String, 15 | pub challenge: String 16 | } 17 | 18 | #[derive(Serialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct RegisteredKey { 21 | pub version: String, 22 | pub key_handle: Option, 23 | pub app_id: String 24 | } 25 | 26 | #[derive(Deserialize)] 27 | #[serde(rename_all = "camelCase")] 28 | pub struct RegisterResponse { 29 | pub registration_data: String, 30 | pub version: String, 31 | pub client_data: String 32 | } 33 | 34 | #[derive(Serialize)] 35 | #[serde(rename_all = "camelCase")] 36 | pub struct U2fSignRequest { 37 | pub app_id: String, 38 | pub challenge: String, 39 | pub registered_keys: Vec 40 | } 41 | 42 | #[derive(Clone, Deserialize)] 43 | #[serde(rename_all = "camelCase")] 44 | pub struct SignResponse { 45 | pub key_handle: String, 46 | pub signature_data: String, 47 | pub client_data: String 48 | } -------------------------------------------------------------------------------- /src/protocol.rs: -------------------------------------------------------------------------------- 1 | use crate::util::*; 2 | use crate::messages::*; 3 | use crate::register::*; 4 | use crate::authorization::*; 5 | 6 | use base64::{encode_config, decode_config, URL_SAFE_NO_PAD}; 7 | use chrono::prelude::*; 8 | use time::Duration; 9 | use crate::u2ferror::U2fError; 10 | 11 | type Result = ::std::result::Result; 12 | 13 | #[derive(Clone)] 14 | pub struct U2f { 15 | app_id: String, 16 | } 17 | 18 | #[derive(Deserialize, Serialize, Clone)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct Challenge { 21 | pub app_id: String, 22 | pub challenge: String, 23 | pub timestamp: String, 24 | } 25 | 26 | impl Challenge { 27 | pub fn new() -> Self { 28 | Challenge { 29 | app_id: String::new(), 30 | challenge: String::new(), 31 | timestamp: String::new() 32 | } 33 | } 34 | } 35 | 36 | impl U2f { 37 | // The app ID is a string used to uniquely identify an U2F app 38 | pub fn new(app_id: String) -> Self { 39 | U2f { 40 | app_id: app_id, 41 | } 42 | } 43 | 44 | pub fn generate_challenge(&self) -> Result { 45 | let utc: DateTime = Utc::now(); 46 | 47 | let challenge_bytes = generate_challenge(32)?; 48 | let challenge = Challenge { 49 | challenge : encode_config(&challenge_bytes, URL_SAFE_NO_PAD), 50 | timestamp : format!("{:?}", utc), 51 | app_id : self.app_id.clone() 52 | }; 53 | 54 | Ok(challenge.clone()) 55 | } 56 | 57 | pub fn request(&self, challenge: Challenge, registrations: Vec) -> Result { 58 | let u2f_request = U2fRegisterRequest { 59 | app_id : self.app_id.clone(), 60 | register_requests: self.register_request(challenge), 61 | registered_keys: self.registered_keys(registrations) 62 | }; 63 | 64 | Ok(u2f_request) 65 | } 66 | 67 | fn register_request(&self, challenge: Challenge) -> Vec { 68 | let mut requests: Vec = vec![]; 69 | 70 | let request = RegisterRequest { 71 | version : U2F_V2.into(), 72 | challenge: challenge.challenge 73 | }; 74 | requests.push(request); 75 | 76 | requests 77 | } 78 | 79 | pub fn register_response(&self, challenge: Challenge, response: RegisterResponse) -> Result { 80 | if expiration(challenge.timestamp) > Duration::seconds(300) { 81 | return Err(U2fError::ChallengeExpired); 82 | } 83 | 84 | let registration_data: Vec = decode_config(&response.registration_data[..], URL_SAFE_NO_PAD).unwrap(); 85 | let client_data: Vec = decode_config(&response.client_data[..], URL_SAFE_NO_PAD).unwrap(); 86 | 87 | parse_registration(challenge.app_id, client_data, registration_data) 88 | } 89 | 90 | fn registered_keys(&self, registrations: Vec) -> Vec { 91 | let mut keys: Vec = vec![]; 92 | 93 | for registration in registrations { 94 | keys.push(get_registered_key(self.app_id.clone(), registration.key_handle)); 95 | } 96 | 97 | keys 98 | } 99 | 100 | pub fn sign_request(&self, challenge: Challenge, registrations: Vec) -> U2fSignRequest { 101 | let mut keys: Vec = vec![]; 102 | 103 | for registration in registrations { 104 | keys.push(get_registered_key(self.app_id.clone(), registration.key_handle)); 105 | } 106 | 107 | let signed_request = U2fSignRequest { 108 | app_id : self.app_id.clone(), 109 | challenge: encode_config(challenge.challenge.as_bytes(), URL_SAFE_NO_PAD), 110 | registered_keys: keys 111 | }; 112 | 113 | signed_request 114 | } 115 | 116 | pub fn sign_response(&self, challenge: Challenge, reg: Registration, sign_resp: SignResponse, counter: u32) -> Result { 117 | if expiration(challenge.timestamp) > Duration::seconds(300) { 118 | return Err(U2fError::ChallengeExpired); 119 | } 120 | 121 | if sign_resp.key_handle != get_encoded(®.key_handle[..]) { 122 | return Err(U2fError::WrongKeyHandler); 123 | } 124 | 125 | let client_data: Vec = decode_config(&sign_resp.client_data[..], URL_SAFE_NO_PAD).map_err(|_e| U2fError::InvalidClientData)?; 126 | let sign_data: Vec = decode_config(&sign_resp.signature_data[..], URL_SAFE_NO_PAD).map_err(|_e| U2fError::InvalidSignatureData)?; 127 | 128 | let public_key = reg.pub_key; 129 | 130 | let auth = parse_sign_response(self.app_id.clone(), client_data.clone(), public_key, sign_data.clone()); 131 | 132 | match auth { 133 | Ok(ref res) => { 134 | // CounterTooLow is raised when the counter value received from the device is 135 | // lower than last stored counter value. 136 | if res.counter < counter { 137 | return Err(U2fError::CounterTooLow); 138 | } 139 | else { 140 | return Ok(res.counter); 141 | } 142 | }, 143 | Err(e) => return Err(e), 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /src/register.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Bytes, BufMut}; 2 | use openssl::sha::sha256; 3 | use byteorder::{ByteOrder, BigEndian}; 4 | 5 | use crate::util::*; 6 | use crate::messages::RegisteredKey; 7 | use crate::u2ferror::U2fError; 8 | use std::convert::TryFrom; 9 | 10 | /// The `Result` type used in this crate. 11 | type Result = ::std::result::Result; 12 | 13 | // Single enrolment or pairing between an application and a token. 14 | #[derive(Serialize, Clone)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct Registration { 17 | pub key_handle: Vec, 18 | pub pub_key: Vec, 19 | 20 | // AttestationCert can be null for Authenticate requests. 21 | pub attestation_cert: Option>, 22 | pub device_name: Option, 23 | } 24 | 25 | pub fn parse_registration(app_id: String, client_data: Vec, registration_data: Vec) -> Result { 26 | let reserved_byte = registration_data[0]; 27 | if reserved_byte != 0x05 { 28 | return Err(U2fError::InvalidReservedByte); 29 | } 30 | 31 | let mut mem = Bytes::from(registration_data); 32 | 33 | //Start parsing ... advance the reserved byte. 34 | let _ = mem.split_to(1); 35 | 36 | // P-256 NIST elliptic curve 37 | let public_key = mem.split_to(65); 38 | 39 | // Key Handle 40 | let key_handle_size = mem.split_to(1); 41 | let key_len = BigEndian::read_uint(&key_handle_size[..], 1); 42 | let key_handle = mem.split_to(key_len as usize); 43 | 44 | // The certificate length needs to be inferred by parsing. 45 | let cert_len = asn_length(mem.clone()).unwrap(); 46 | let attestation_certificate = mem.split_to(cert_len); 47 | 48 | // Remaining data corresponds to the signature 49 | let signature = mem; 50 | 51 | // Let's build the msg to verify the signature 52 | let app_id_hash = sha256(&app_id.into_bytes()); 53 | let client_data_hash = sha256(&client_data[..]); 54 | 55 | let mut msg = vec![0x00]; // A byte reserved for future use [1 byte] with the value 0x00 56 | msg.put(app_id_hash.as_ref()); 57 | msg.put(client_data_hash.as_ref()); 58 | msg.put(key_handle.clone()); 59 | msg.put(public_key.clone()); 60 | 61 | 62 | // The signature is to be verified by the relying party using the public key certified 63 | // in the attestation certificate. 64 | let cerificate_public_key = super::crypto::X509PublicKey::try_from(&attestation_certificate[..])?; 65 | 66 | if !(cerificate_public_key.is_secp256r1()?) { 67 | return Err(U2fError::BadCertificate); 68 | } 69 | 70 | let verified = cerificate_public_key.verify_signature(&signature[..], &msg[..])?; 71 | 72 | if !verified { 73 | return Err(U2fError::BadCertificate); 74 | } 75 | 76 | let registration = Registration { 77 | key_handle: key_handle[..].to_vec(), 78 | pub_key: public_key[..].to_vec(), 79 | attestation_cert: Some(attestation_certificate[..].to_vec()), 80 | device_name: cerificate_public_key.common_name(), 81 | }; 82 | 83 | Ok(registration) 84 | } 85 | 86 | pub fn get_registered_key(app_id: String, key_handle: Vec) -> RegisteredKey { 87 | RegisteredKey { 88 | app_id: app_id, 89 | version: U2F_V2.into(), 90 | key_handle: Some(get_encoded(key_handle.as_slice())) 91 | } 92 | } -------------------------------------------------------------------------------- /src/u2ferror.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | pub enum U2fError { 6 | Asm1DecoderError, 7 | BadSignature, 8 | RandomSecureBytesError, 9 | InvalidReservedByte, 10 | ChallengeExpired, 11 | WrongKeyHandler, 12 | InvalidClientData, 13 | InvalidSignatureData, 14 | InvalidUserPresenceByte, 15 | BadCertificate, 16 | NotTrustedAnchor, 17 | CounterTooLow, 18 | OpenSSLNoCurveName, 19 | InvalidPublicKey, 20 | OpenSSLError(openssl::error::ErrorStack), 21 | } 22 | 23 | impl fmt::Display for U2fError { 24 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 25 | match &self { 26 | U2fError::Asm1DecoderError => write!(f, "ASM1 Decoder error"), 27 | U2fError::BadSignature => write!(f, "Not able to verify signature"), 28 | U2fError::RandomSecureBytesError => write!(f, "Not able to generate random bytes"), 29 | U2fError::InvalidReservedByte => write!(f, "Invalid Reserved Byte"), 30 | U2fError::ChallengeExpired => write!(f, "Challenge Expired"), 31 | U2fError::WrongKeyHandler => write!(f, "Wrong Key Handler"), 32 | U2fError::InvalidClientData => write!(f, "Invalid Client Data"), 33 | U2fError::InvalidSignatureData => write!(f, "Invalid Signature Data"), 34 | U2fError::InvalidUserPresenceByte => write!(f, "Invalid User Presence Byte"), 35 | U2fError::BadCertificate => write!(f, "Failed to parse certificate"), 36 | U2fError::NotTrustedAnchor => write!(f, "Not Trusted Anchor"), 37 | U2fError::CounterTooLow => write!(f, "Counter too low"), 38 | U2fError::InvalidPublicKey => write!(f, "Invalid public key"), 39 | U2fError::OpenSSLNoCurveName => write!(f, "OpenSSL no curve name"), 40 | U2fError::OpenSSLError(e) => e.fmt(f), 41 | } 42 | } 43 | } 44 | 45 | impl error::Error for U2fError { 46 | fn description(&self) -> &str { 47 | match &self { 48 | U2fError::Asm1DecoderError => "Error attempting to decode Asm1 message", 49 | U2fError::BadSignature => "Error attempting to verify provided signature", 50 | U2fError::RandomSecureBytesError => "Error attempting to generate random bytes", 51 | U2fError::InvalidReservedByte => "Error attempting to parse Reserved Byte", 52 | U2fError::ChallengeExpired => "Challenge has expired", 53 | U2fError::WrongKeyHandler => "Wrong Key Handler", 54 | U2fError::InvalidClientData => "Invalid Client Data", 55 | U2fError::InvalidSignatureData => "Invalid Signature Data", 56 | U2fError::InvalidUserPresenceByte => "Invalid User Presence Byte", 57 | U2fError::BadCertificate => "Failed to parse certificate", 58 | U2fError::NotTrustedAnchor => "Not Trusted Anchor", 59 | U2fError::CounterTooLow => "Counter too low", 60 | U2fError::InvalidPublicKey => "Invalid public key", 61 | U2fError::OpenSSLNoCurveName => "OpenSSL no curve name", 62 | U2fError::OpenSSLError(e) => e.description(), 63 | } 64 | } 65 | 66 | fn cause(&self) -> Option<&dyn error::Error> { 67 | match *self { 68 | U2fError::Asm1DecoderError => None, 69 | U2fError::BadSignature => None, 70 | U2fError::RandomSecureBytesError => None, 71 | U2fError::InvalidReservedByte => None, 72 | U2fError::ChallengeExpired => None, 73 | U2fError::WrongKeyHandler => None, 74 | U2fError::InvalidClientData => None, 75 | U2fError::InvalidSignatureData => None, 76 | U2fError::InvalidUserPresenceByte => None, 77 | U2fError::BadCertificate => None, 78 | U2fError::NotTrustedAnchor => None, 79 | U2fError::CounterTooLow => None, 80 | U2fError::InvalidPublicKey => None, 81 | U2fError::OpenSSLNoCurveName => None, 82 | U2fError::OpenSSLError(_) => None, 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use time::Duration; 3 | use openssl::rand; 4 | use bytes::{Bytes}; 5 | use base64::{encode_config, URL_SAFE_NO_PAD}; 6 | use crate::u2ferror::U2fError; 7 | 8 | /// The `Result` type used in this crate. 9 | type Result = ::std::result::Result; 10 | 11 | pub const U2F_V2: &'static str = "U2F_V2"; 12 | 13 | // Generates a challenge from a secure, random source. 14 | pub fn generate_challenge(size: usize) -> Result> { 15 | let mut bytes: Vec = vec![0; size]; 16 | rand::rand_bytes(&mut bytes).map_err(|_e| U2fError::RandomSecureBytesError)?; 17 | Ok(bytes) 18 | } 19 | 20 | pub fn expiration(timestamp: String) -> Duration { 21 | let now: DateTime = Utc::now(); 22 | 23 | let ts = timestamp.parse::>(); 24 | 25 | now.signed_duration_since(ts.unwrap()) 26 | } 27 | 28 | // Decode initial bytes of buffer as ASN and return the length of the encoded structure. 29 | // http://en.wikipedia.org/wiki/X.690 30 | pub fn asn_length(mem: Bytes) -> Result { 31 | let buffer : &[u8] = &mem[..]; 32 | 33 | if mem.len() < 2 || buffer[0] != 0x30 { // Type 34 | return Err(U2fError::Asm1DecoderError); 35 | } 36 | 37 | let len = buffer[1]; // Len 38 | if len & 0x80 == 0 { 39 | return Ok((len & 0x7f) as usize); 40 | } 41 | 42 | let numbem_of_bytes = len & 0x7f; 43 | if numbem_of_bytes == 0 { 44 | return Err(U2fError::Asm1DecoderError); 45 | } 46 | 47 | let mut length: usize = 0; 48 | for num in 0..numbem_of_bytes { 49 | length = length*0x100 + (buffer[(2+num) as usize] as usize); 50 | } 51 | 52 | length = length + (numbem_of_bytes as usize); 53 | 54 | Ok(length + 2) // Add the 2 initial bytes: type and length. 55 | } 56 | 57 | pub fn get_encoded(data: &[u8]) -> String { 58 | let encoded: String = encode_config(data, URL_SAFE_NO_PAD); 59 | 60 | encoded.trim_end_matches('=').to_string() 61 | } --------------------------------------------------------------------------------