Skip to content

Commit 1b1c372

Browse files
author
Flavio Oliveira
committed
Verify response signature
1 parent cf42455 commit 1b1c372

File tree

2 files changed

+40
-17
lines changed

2 files changed

+40
-17
lines changed

README.md

-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ Yubikey client API library, [validation protocol version 2.0](https://developers
77
Enables integration with the Yubico validation platform, so you can use Yubikey's one-time-password in your Rust application,
88
allowing a user to authenticate via Yubikey.
99

10-
IMPORTANT: WIP, signature verification hasn't been implemented yet.
11-
1210
# Usage
1311

1412
Add this to your Cargo.toml

src/lib.rs

+40-15
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ use yubicoerror::YubicoError;
1111
use hyper::Client;
1212
use hyper::header::{Headers};
1313
use std::io::prelude::*;
14-
use base64::{encode};
14+
use base64::{encode, decode};
1515
use crypto::mac::{Mac};
1616
use crypto::hmac::Hmac;
1717
use crypto::sha1::Sha1;
1818
use rand::{thread_rng, Rng};
19-
use std::collections::HashMap;
2019
use threadpool::ThreadPool;
20+
use std::collections::BTreeMap;
2121
use std::sync::mpsc::{ channel, Sender };
22-
2322
use url::percent_encoding::{utf8_percent_encode, SIMPLE_ENCODE_SET};
23+
2424
define_encode_set! {
2525
/// This encode set is used in the URL parser for query strings.
2626
pub QUERY_ENCODE_SET = [SIMPLE_ENCODE_SET] | {'+', '='}
@@ -52,15 +52,15 @@ pub struct Request {
5252
#[derive(Clone)]
5353
pub struct Yubico {
5454
client_id: String,
55-
key: String,
55+
key: Vec<u8>,
5656
}
5757

5858
impl Yubico {
5959
/// Creates a new Yubico instance.
6060
pub fn new(client_id: String, key: String) -> Self {
6161
Yubico {
6262
client_id: client_id,
63-
key: key,
63+
key: decode(key.as_ref()).unwrap(),
6464
}
6565
}
6666

@@ -71,10 +71,14 @@ impl Yubico {
7171
_ => {
7272
// TODO: use OsRng to generate a most secure nonce
7373
let nonce: String = thread_rng().gen_ascii_chars().take(40).collect();
74-
let mut query = format!("id={}&otp={}&nonce={}&sl=secure", self.client_id, otp, nonce);
74+
let mut query = format!("id={}&nonce={}&otp={}&sl=secure", self.client_id, nonce, otp);
7575

7676
let signature = self.build_signature(query.clone());
77-
query.push_str(signature.as_ref());
77+
78+
// Append the value under key h to the message.
79+
let signature_param = format!("&h={}", signature);
80+
let encoded = utf8_percent_encode(signature_param.as_ref(), QUERY_ENCODE_SET).collect::<String>();
81+
query.push_str(encoded.as_ref());
7882

7983
let request = Request {otp: otp, nonce: nonce, signature: signature, query: query};
8084

@@ -119,13 +123,11 @@ impl Yubico {
119123

120124
// 1. Apply the HMAC-SHA-1 algorithm on the line as an octet string using the API key as key
121125
// 2. Base 64 encode the resulting value according to RFC 4648
122-
// 3. Append the value under key h to the message.
123126
fn build_signature(&self, query: String) -> String {
124-
let mut hmac = Hmac::new(Sha1::new(), self.key.as_bytes());
127+
let mut hmac = Hmac::new(Sha1::new(), &self.key[..]);
125128
hmac.input(query.as_bytes());
126129
let signature = encode(hmac.result().code());
127-
let signature_str = format!("&h={}", signature);
128-
utf8_percent_encode(signature_str.as_ref(), QUERY_ENCODE_SET).collect::<String>()
130+
format!("{}", signature)
129131
}
130132

131133
// Recommendation is that clients only check that the input consists of 32-48 printable characters
@@ -137,7 +139,13 @@ impl Yubico {
137139
let url = format!("{}?{}", api_host, request.query);
138140
match self.get(url) {
139141
Ok(result) => {
140-
let response_map: HashMap<String, String> = self.build_response_map(result);
142+
let response_map: BTreeMap<String, String> = self.build_response_map(result);
143+
144+
// Signature located in the response must match the signature we will build
145+
let signature_response : &str = &*response_map.get("h").unwrap();
146+
if !self.is_same_signature(signature_response, response_map.clone()) {
147+
sender.send(Response::Signal(Err(YubicoError::SignatureMismatch))).unwrap();
148+
}
141149

142150
// Check if "otp" in the response is the same as the "otp" supplied in the request.
143151
let otp_response : &str = &*response_map.get("otp").unwrap();
@@ -173,8 +181,25 @@ impl Yubico {
173181
}
174182
}
175183

176-
fn build_response_map(&self, result: String) -> HashMap<String, String> {
177-
let mut parameters = HashMap::new();
184+
// Remove the signature itself from the values over for verification.
185+
// Sort the key/value pairs.
186+
fn is_same_signature(&self, signature_response: &str, mut response_map: BTreeMap<String, String>) -> bool {
187+
response_map.remove("h");
188+
189+
let mut query = String::new();
190+
for (key, value) in response_map {
191+
let param = format!("{}={}&", key, value);
192+
query.push_str(param.as_ref());
193+
}
194+
query.pop(); // remove last &
195+
196+
let signature = self.build_signature(query.clone());
197+
198+
if signature == signature_response { true } else { false }
199+
}
200+
201+
fn build_response_map(&self, result: String) -> BTreeMap<String, String> {
202+
let mut parameters = BTreeMap::new();
178203
for line in result.lines() {
179204
let param: Vec<&str> = line.splitn(2, '=').collect();
180205
if param.len() > 1 {
@@ -187,7 +212,7 @@ impl Yubico {
187212
pub fn get(&self, url: String) -> Result<String> {
188213
let client = Client::new();
189214
let mut custom_headers = Headers::new();
190-
custom_headers.set(UserAgent("yubico-rs".to_owned()));
215+
custom_headers.set(UserAgent("github.com/wisespace-io/yubico-rs".to_owned()));
191216

192217
let mut response = String::new();
193218
let mut res = try!(client.get(&url).headers(custom_headers).send());

0 commit comments

Comments
 (0)