mirror of
https://git.proxmox.com/git/proxmox
synced 2025-05-16 13:15:11 +00:00
add proxmox::tools::tfa
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
05749ab419
commit
8cbf9cb7c8
@ -11,6 +11,10 @@ description = "Proxmox library"
|
||||
|
||||
exclude = [ "debian" ]
|
||||
|
||||
[[examples]]
|
||||
name = "u2ftest"
|
||||
required-features = [ "tokio", "u2f" ]
|
||||
|
||||
[dependencies]
|
||||
# General dependencies
|
||||
anyhow = "1.0"
|
||||
@ -19,6 +23,7 @@ libc = "0.2"
|
||||
nix = "0.19"
|
||||
|
||||
# tools module:
|
||||
base32 = { version = "0.4", optional = true }
|
||||
base64 = "0.12"
|
||||
endian_trait = { version = "0.6", features = ["arrays"] }
|
||||
regex = "1.2"
|
||||
@ -49,7 +54,7 @@ proxmox-api-macro = { path = "../proxmox-api-macro", optional = true, version =
|
||||
proxmox-sortable-macro = { path = "../proxmox-sortable-macro", optional = true, version = "0.1.1" }
|
||||
|
||||
[features]
|
||||
default = [ "router", "cli", "websocket" ]
|
||||
default = [ "cli", "router", "tfa", "u2f", "websocket" ]
|
||||
sortable-macro = ["proxmox-sortable-macro"]
|
||||
|
||||
# api:
|
||||
@ -58,6 +63,10 @@ test-harness = []
|
||||
cli = [ "router", "hyper", "tokio" ]
|
||||
router = [ "hyper", "tokio" ]
|
||||
websocket = [ "futures", "hyper", "openssl", "tokio/sync", "tokio/io-util", "openssl" ]
|
||||
tfa = [ "openssl" ]
|
||||
u2f = [ "base32" ]
|
||||
|
||||
examples = ["tokio/macros", "u2f"]
|
||||
|
||||
# tools:
|
||||
#valgrind = ["proxmox-tools/valgrind"]
|
||||
|
589
proxmox/examples/u2ftest.rs
Normal file
589
proxmox/examples/u2ftest.rs
Normal file
@ -0,0 +1,589 @@
|
||||
//! # live u2f test
|
||||
//!
|
||||
//! Listens on `localhost:13905` via http (NOT https) and provides a u2f api test server.
|
||||
//!
|
||||
//! To use this, you'll need to create an https wrapper (eg. nginx reverse proxy) with a valid
|
||||
//! appid, then run:
|
||||
//!
|
||||
//! ## Running the API:
|
||||
//!
|
||||
//! NOTE: you need to run this in a directory with a `u2f-api.js` file.
|
||||
//!
|
||||
//! ```
|
||||
//! $ cargo run --example u2ftest --features='examples' <APPID>
|
||||
//! ```
|
||||
//!
|
||||
//! Replace `<APPID>` with a working `https://...` url, the API is expected to be on the top level.
|
||||
//!
|
||||
//! ## Example Veverse Proxy via nginx:
|
||||
//!
|
||||
//! ```
|
||||
//! server {
|
||||
//! listen 443 ssl;
|
||||
//! server_name u2ftest.enonet.errno.eu;
|
||||
//!
|
||||
//! ssl_certificate /etc/nginx/ssl/my.pem;
|
||||
//! ssl_certificate_key /etc/nginx/ssl/my.key;
|
||||
//! ssl_protocols TLSv1.2;
|
||||
//! ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
//!
|
||||
//! root /var/www/html;
|
||||
//!
|
||||
//! location / {
|
||||
//! proxy_pass http://127.0.0.1:13905/;
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! ## Debugging
|
||||
//!
|
||||
//! The registration and authentication api calls store the response data in
|
||||
//! `test-registration.json` and `test-auth.json` which get "retried" at startup by the
|
||||
//! `retry_reg()` and `retry_auth()` calls. This way less hardware interaction is required for
|
||||
//! debugging.
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use hyper::{Body, Request, Response, Server};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::stream::StreamExt;
|
||||
|
||||
use proxmox::tools::tfa::u2f;
|
||||
|
||||
pub const PORT: u16 = 13905;
|
||||
|
||||
#[cfg(not(feature = "examples"))]
|
||||
fn main() {
|
||||
let _unused = do_main();
|
||||
panic!("rebuild with: --features examples");
|
||||
}
|
||||
|
||||
#[cfg(feature = "examples")]
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
do_main().await
|
||||
}
|
||||
|
||||
async fn do_main() -> Result<(), Error> {
|
||||
use std::net::SocketAddr;
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
let addr = ("localhost", PORT)
|
||||
.to_socket_addrs()
|
||||
.ok()
|
||||
.and_then(|mut addrs| addrs.next())
|
||||
.unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], PORT)));
|
||||
|
||||
let appid = std::env::args()
|
||||
.skip(1)
|
||||
.next()
|
||||
.expect("please specify the appid/origin URL");
|
||||
|
||||
let sv = Arc::new(Sv::new(appid));
|
||||
|
||||
// retry the last registration challenge remembered in `test-registration.json`
|
||||
sv.retry_reg()?;
|
||||
|
||||
// retry the last authentication challenge remembered in `test-auth.json`
|
||||
sv.retry_auth()?;
|
||||
|
||||
let make_service = make_service_fn(move |_conn| {
|
||||
let sv = Arc::clone(&sv);
|
||||
async move {
|
||||
Ok::<_, std::convert::Infallible>(service_fn(move |request: Request<Body>| {
|
||||
let sv = Arc::clone(&sv);
|
||||
async move {
|
||||
let res = handle(&sv, request).await.or_else(|err| {
|
||||
let err = serde_json::to_string(&serde_json::json!({
|
||||
"error": err.to_string(),
|
||||
}))
|
||||
.unwrap();
|
||||
Ok::<_, std::convert::Infallible>(
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(Body::from(err))
|
||||
.unwrap(),
|
||||
)
|
||||
});
|
||||
eprintln!("{:#?}", res);
|
||||
res
|
||||
}
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
let server = Server::bind(&addr).serve(make_service);
|
||||
|
||||
server.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_body(mut request_body: Body) -> Result<Vec<u8>, Error> {
|
||||
let mut body = Vec::new();
|
||||
|
||||
while let Some(chunk) = request_body.try_next().await? {
|
||||
if body.len() + chunk.len() > 1024 * 1024 {
|
||||
bail!("request too big");
|
||||
}
|
||||
|
||||
body.extend(chunk);
|
||||
}
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
async fn handle(sv: &Arc<Sv>, request: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
let (parts, body) = request.into_parts();
|
||||
let body = fetch_body(body).await?;
|
||||
|
||||
eprintln!("fetching: {}", parts.uri.path());
|
||||
match parts.uri.path() {
|
||||
"/" => simple("text/html", INDEX_HTML),
|
||||
"/index.html" => simple("text/html", INDEX_HTML),
|
||||
"/u2f-api.js" => file("text/javascript", "u2f-api.js"),
|
||||
"/style.css" => simple("text/css", STYLE_CSS),
|
||||
"/registration" => sv.registration(),
|
||||
"/finish-registration" => sv.finish_registration(serde_json::from_slice(&body)?),
|
||||
"/authenticate" => sv.authenticate(serde_json::from_slice(&body)?),
|
||||
"/finish-auth" => sv.finish_auth(serde_json::from_slice(&body)?),
|
||||
_ => Ok(Response::builder()
|
||||
.status(404)
|
||||
.body(Body::from("not found"))
|
||||
.unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
struct User {
|
||||
data: u2f::Registration,
|
||||
challenges: HashMap<usize, String>,
|
||||
}
|
||||
|
||||
struct Sv {
|
||||
context: u2f::U2f,
|
||||
counter: AtomicUsize,
|
||||
challenges: Mutex<HashMap<usize, String>>,
|
||||
users: Mutex<HashMap<usize, User>>, // key handle
|
||||
}
|
||||
|
||||
impl Sv {
|
||||
fn new(appid: String) -> Self {
|
||||
Self {
|
||||
context: u2f::U2f::new(appid.clone(), appid),
|
||||
counter: AtomicUsize::new(0),
|
||||
challenges: Mutex::new(HashMap::new()),
|
||||
users: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn nextid(&self) -> usize {
|
||||
self.counter.fetch_add(1, Ordering::AcqRel)
|
||||
}
|
||||
|
||||
fn registration(&self) -> Result<Response<Body>, Error> {
|
||||
let challenge = self.context.registration_challenge()?;
|
||||
let id = self.nextid();
|
||||
let output = serde_json::json!({
|
||||
"id": id,
|
||||
"challenge": challenge,
|
||||
"context": &self.context,
|
||||
});
|
||||
self.challenges
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(id, challenge.challenge);
|
||||
json(output)
|
||||
}
|
||||
|
||||
fn finish_registration(&self, mut response: Value) -> Result<Response<Body>, Error> {
|
||||
let id = response["id"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| format_err!("bad or missing ID in response"))? as usize;
|
||||
let rspdata: Value = response
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("response")
|
||||
.ok_or_else(|| format_err!("missing response data"))?;
|
||||
|
||||
let challenge = self
|
||||
.challenges
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&id)
|
||||
.ok_or_else(|| format_err!("no such challenge"))?;
|
||||
|
||||
std::fs::write(
|
||||
"test-registration.json",
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"challenge": challenge,
|
||||
"response": &rspdata,
|
||||
}))?,
|
||||
)?;
|
||||
|
||||
let data = self
|
||||
.context
|
||||
.registration_verify_obj(&challenge, serde_json::from_value(rspdata)?)?;
|
||||
|
||||
match data {
|
||||
Some(data) => {
|
||||
self.users.lock().unwrap().insert(
|
||||
id,
|
||||
User {
|
||||
data,
|
||||
challenges: HashMap::new(),
|
||||
},
|
||||
);
|
||||
json(serde_json::json!({ "id": id }))
|
||||
}
|
||||
None => bail!("registration failed"),
|
||||
}
|
||||
}
|
||||
|
||||
fn retry_reg(&self) -> Result<(), Error> {
|
||||
let data = match std::fs::read("test-registration.json") {
|
||||
Ok(data) => data,
|
||||
Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TestChallenge {
|
||||
challenge: String,
|
||||
response: u2f::RegistrationResponse,
|
||||
}
|
||||
|
||||
let ts: TestChallenge = serde_json::from_slice(&data)?;
|
||||
|
||||
let res = self
|
||||
.context
|
||||
.registration_verify_obj(&ts.challenge, ts.response)?;
|
||||
|
||||
eprintln!("=> {:#?}", res);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn authenticate(&self, params: Value) -> Result<Response<Body>, Error> {
|
||||
let uid = params["uid"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| format_err!("bad or missing user id in auth call"))?
|
||||
as usize;
|
||||
|
||||
let mut users = self.users.lock().unwrap();
|
||||
let user = users
|
||||
.get_mut(&uid)
|
||||
.ok_or_else(|| format_err!("no such user"))?;
|
||||
|
||||
let challenge = self.context.auth_challenge()?;
|
||||
let id = self.nextid();
|
||||
user.challenges.insert(id, challenge.challenge.clone());
|
||||
let output = serde_json::json!({
|
||||
"id": id,
|
||||
"challenge": challenge,
|
||||
"keys": [&user.data.key],
|
||||
});
|
||||
json(output)
|
||||
}
|
||||
|
||||
fn finish_auth(&self, mut response: Value) -> Result<Response<Body>, Error> {
|
||||
let uid = response["uid"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| format_err!("bad or missing user id in auth call"))?
|
||||
as usize;
|
||||
let id = response["id"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| format_err!("bad or missing ID in response"))? as usize;
|
||||
|
||||
let rspdata: Value = response
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("response")
|
||||
.ok_or_else(|| format_err!("missing response data"))?;
|
||||
|
||||
let mut users = self.users.lock().unwrap();
|
||||
let user = users
|
||||
.get_mut(&uid)
|
||||
.ok_or_else(|| format_err!("no such user"))?;
|
||||
|
||||
let challenge = user
|
||||
.challenges
|
||||
.remove(&id)
|
||||
.ok_or_else(|| format_err!("no such challenge for user"))?;
|
||||
|
||||
std::fs::write(
|
||||
"test-auth.json",
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"challenge": challenge,
|
||||
"response": &rspdata,
|
||||
"user": user.data,
|
||||
}))?,
|
||||
)?;
|
||||
|
||||
let rspdata: u2f::AuthResponse = serde_json::from_value(rspdata)?;
|
||||
if user.data.key.key_handle != rspdata.key_handle() {
|
||||
bail!("key handle mismatch");
|
||||
}
|
||||
|
||||
let res = self
|
||||
.context
|
||||
.auth_verify_obj(&user.data.public_key, &challenge, rspdata)?;
|
||||
|
||||
match res {
|
||||
Some(auth) => json(serde_json::json!({
|
||||
"present": auth.user_present,
|
||||
"counter": auth.counter,
|
||||
})),
|
||||
None => bail!("authentication failed"),
|
||||
}
|
||||
}
|
||||
|
||||
fn retry_auth(&self) -> Result<(), Error> {
|
||||
let data = match std::fs::read("test-auth.json") {
|
||||
Ok(data) => data,
|
||||
Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TestChallenge {
|
||||
challenge: String,
|
||||
user: u2f::Registration,
|
||||
response: u2f::AuthResponse,
|
||||
}
|
||||
|
||||
let ts: TestChallenge = serde_json::from_slice(&data)?;
|
||||
|
||||
let res = self
|
||||
.context
|
||||
.auth_verify_obj(&ts.user.public_key, &ts.challenge, ts.response)?;
|
||||
|
||||
eprintln!("=> {:#?}", res);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn json<T: Serialize>(data: T) -> Result<Response<Body>, Error> {
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(serde_json::to_string(&data)?))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
fn simple(content_type: &'static str, data: &'static str) -> Result<Response<Body>, Error> {
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", content_type)
|
||||
.body(Body::from(data))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
fn file(content_type: &'static str, file_name: &'static str) -> Result<Response<Body>, Error> {
|
||||
let file = std::fs::read(file_name)?;
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", content_type)
|
||||
.body(Body::from(file))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
const INDEX_HTML: &str = r##"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="StyleSheet" type="text/css" href="style.css" />
|
||||
<script type="text/javascript" src="/u2f-api.js"></script>
|
||||
<script type="text/javascript">
|
||||
var USER_ID = undefined;
|
||||
|
||||
function clear() {
|
||||
document.getElementById("status").innerText = "";
|
||||
}
|
||||
|
||||
function log(text) {
|
||||
console.log(text);
|
||||
document.getElementById("status").innerText += text + "\n";
|
||||
}
|
||||
|
||||
function some(x) {
|
||||
return x !== undefined && x !== null;
|
||||
}
|
||||
|
||||
async function call(path, body) {
|
||||
let opts = undefined;
|
||||
if (some(body)) {
|
||||
opts = {
|
||||
'method': 'POST',
|
||||
'Content-Type': 'application/json',
|
||||
'body': JSON.stringify(body),
|
||||
};
|
||||
}
|
||||
return (await fetch(path, opts)).json();
|
||||
}
|
||||
|
||||
async function u2f_register(appId, registerRequests, registeredKeys, timeoutSecs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
u2f.register(
|
||||
appId,
|
||||
registerRequests,
|
||||
registeredKeys,
|
||||
(rsp) => {
|
||||
if (rsp.errorCode) {
|
||||
reject(rsp);
|
||||
} else {
|
||||
delete rsp.errorCode;
|
||||
resolve(rsp);
|
||||
}
|
||||
},
|
||||
timeoutSecs,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function u2f_sign(appId, challenge, registeredKeys, timeoutSecs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
u2f.sign(
|
||||
appId,
|
||||
challenge,
|
||||
registeredKeys,
|
||||
(rsp) => {
|
||||
if (rsp.errorCode) {
|
||||
reject(rsp);
|
||||
} else {
|
||||
delete rsp.errorCode;
|
||||
resolve(rsp);
|
||||
}
|
||||
},
|
||||
timeoutSecs,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function register() {
|
||||
try {
|
||||
clear();
|
||||
|
||||
log("fetching registration");
|
||||
let registration = await call("/registration");
|
||||
|
||||
log("logging registration challenge to console");
|
||||
console.log(registration);
|
||||
let data = registration.challenge;
|
||||
let challenge = {
|
||||
"challenge": data.challenge,
|
||||
"version": data.version,
|
||||
};
|
||||
|
||||
log("please press the button on your u2f token");
|
||||
let rsp = await u2f_register(
|
||||
data.appId,
|
||||
[challenge],
|
||||
[],
|
||||
10,
|
||||
);
|
||||
|
||||
log("logging response to console");
|
||||
console.log(rsp);
|
||||
|
||||
log("replying");
|
||||
let reg = await call("/finish-registration", {
|
||||
'response': rsp,
|
||||
'id': registration.id,
|
||||
});
|
||||
log("server responded");
|
||||
log(JSON.stringify(reg));
|
||||
|
||||
let id = reg.id;
|
||||
log("Our user id is now: " + id);
|
||||
USER_ID = id;
|
||||
} catch (ex) {
|
||||
log("An exception occurred:");
|
||||
console.log(ex);
|
||||
log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticate() {
|
||||
try {
|
||||
clear();
|
||||
|
||||
if (!some(USER_ID)) {
|
||||
log("not authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
log("fetching authentication");
|
||||
let auth = await call("/authenticate", {
|
||||
'uid': USER_ID,
|
||||
});
|
||||
|
||||
log("logging authentication");
|
||||
console.log(auth);
|
||||
|
||||
log("please press the button on your u2f token");
|
||||
let rsp = await u2f_sign(
|
||||
auth.challenge.appId,
|
||||
auth.challenge.challenge,
|
||||
auth.keys,
|
||||
10,
|
||||
);
|
||||
|
||||
log("logging token to console");
|
||||
console.log(rsp);
|
||||
|
||||
log("replying");
|
||||
let reg = await call("/finish-auth", {
|
||||
'response': rsp,
|
||||
'uid': USER_ID,
|
||||
'id': auth.id,
|
||||
});
|
||||
log("server responded");
|
||||
log(JSON.stringify(reg));
|
||||
} catch (ex) {
|
||||
log("An exception occurred:");
|
||||
console.log(ex);
|
||||
log(ex);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="buttons">
|
||||
<a href="#" onclick="register();">Register</a>
|
||||
<a href="#" onclick="authenticate();">Authenticate</a>
|
||||
</div>
|
||||
<div id="status">
|
||||
Select an action.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"##;
|
||||
|
||||
const STYLE_CSS: &str = r##"
|
||||
body {
|
||||
background-color: #f8fff8;
|
||||
padding: 3em 10em 3em 10em;
|
||||
}
|
||||
|
||||
p, a, h1, h2, h3, h4 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#status {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
margin: auto;
|
||||
width: 80em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
"##;
|
@ -24,6 +24,9 @@ pub mod vec;
|
||||
#[cfg(feature = "websocket")]
|
||||
pub mod websocket;
|
||||
|
||||
#[cfg(feature = "tfa")]
|
||||
pub mod tfa;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use uuid::Uuid;
|
||||
|
||||
|
@ -134,3 +134,49 @@ pub mod string_as_base64 {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize Vec<u8> as base64url encoded string without padding.
|
||||
///
|
||||
/// Usage example:
|
||||
/// ```
|
||||
/// use serde::{Deserialize, Serialize};
|
||||
///
|
||||
/// # #[derive(Debug)]
|
||||
/// #[derive(Deserialize, PartialEq, Serialize)]
|
||||
/// struct Foo {
|
||||
/// #[serde(with = "proxmox::tools::serde::bytes_as_base64url_nopad")]
|
||||
/// data: Vec<u8>,
|
||||
/// }
|
||||
///
|
||||
/// let obj = Foo { data: vec![1, 2, 3, 4] };
|
||||
/// let json = serde_json::to_string(&obj).unwrap();
|
||||
/// assert_eq!(json, r#"{"data":"AQIDBA"}"#);
|
||||
///
|
||||
/// let deserialized: Foo = serde_json::from_str(&json).unwrap();
|
||||
/// assert_eq!(obj, deserialized);
|
||||
/// ```
|
||||
pub mod bytes_as_base64url_nopad {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&base64::encode_config(
|
||||
data.as_ref(),
|
||||
base64::URL_SAFE_NO_PAD,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
String::deserialize(deserializer).and_then(|string| {
|
||||
base64::decode_config(&string, base64::URL_SAFE_NO_PAD)
|
||||
.map_err(|err| Error::custom(err.to_string()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
4
proxmox/src/tools/tfa/mod.rs
Normal file
4
proxmox/src/tools/tfa/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#[cfg(feature = "u2f")]
|
||||
pub mod u2f;
|
||||
|
||||
pub mod totp;
|
561
proxmox/src/tools/tfa/totp.rs
Normal file
561
proxmox/src/tools/tfa/totp.rs
Normal file
@ -0,0 +1,561 @@
|
||||
//! Implementation of TOTP, U2F and other mechanisms.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{anyhow, bail, Error};
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::sign::Signer;
|
||||
use percent_encoding::{percent_decode, percent_encode};
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
/// Algorithms supported by the TOTP. This is simply an enum limited to the most common
|
||||
/// available implementations.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Algorithm {
|
||||
Sha1,
|
||||
Sha256,
|
||||
Sha512,
|
||||
}
|
||||
|
||||
impl Into<MessageDigest> for Algorithm {
|
||||
fn into(self) -> MessageDigest {
|
||||
match self {
|
||||
Algorithm::Sha1 => MessageDigest::sha1(),
|
||||
Algorithm::Sha256 => MessageDigest::sha256(),
|
||||
Algorithm::Sha512 => MessageDigest::sha512(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Displayed in a way compatible with the `otpauth` URI specification.
|
||||
impl fmt::Display for Algorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Algorithm::Sha1 => write!(f, "SHA1"),
|
||||
Algorithm::Sha256 => write!(f, "SHA256"),
|
||||
Algorithm::Sha512 => write!(f, "SHA512"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed in a way compatible with the `otpauth` URI specification.
|
||||
impl std::str::FromStr for Algorithm {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Error> {
|
||||
Ok(match s {
|
||||
"SHA1" => Algorithm::Sha1,
|
||||
"SHA256" => Algorithm::Sha256,
|
||||
"SHA512" => Algorithm::Sha512,
|
||||
_ => bail!("unsupported algorithm: {}", s),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// OTP secret builder.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(transparent)]
|
||||
pub struct TotpBuilder {
|
||||
inner: Totp,
|
||||
}
|
||||
|
||||
impl From<Totp> for TotpBuilder {
|
||||
#[inline]
|
||||
fn from(inner: Totp) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl TotpBuilder {
|
||||
pub fn secret(mut self, secret: Vec<u8>) -> Self {
|
||||
self.inner.secret = secret;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the requested number of decimal digits.
|
||||
pub fn digits(mut self, digits: u8) -> Self {
|
||||
self.inner.digits = digits;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the algorithm.
|
||||
pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
|
||||
self.inner.algorithm = algorithm;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the issuer.
|
||||
pub fn issuer(mut self, issuer: String) -> Self {
|
||||
self.inner.issuer = Some(issuer);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the account name. This is required to create an URI.
|
||||
pub fn account_name(mut self, account_name: String) -> Self {
|
||||
self.inner.account_name = Some(account_name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the duration, in seconds, for which a value is valid.
|
||||
///
|
||||
/// Panics if `seconds` is 0.
|
||||
pub fn step(mut self, seconds: usize) -> Self {
|
||||
if seconds == 0 {
|
||||
panic!("zero as 'step' value is invalid");
|
||||
}
|
||||
|
||||
self.inner.step = seconds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Finalize the OTP instance.
|
||||
pub fn build(self) -> Totp {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// OTP secret key to produce OTP values with and the desired default number of decimal digits to
|
||||
/// use for its values (defaults to 6).
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Totp {
|
||||
/// The secret shared with the client.
|
||||
secret: Vec<u8>,
|
||||
|
||||
/// The requested number decimal digits.
|
||||
digits: u8,
|
||||
|
||||
/// The algorithm, defaults to sha1.
|
||||
algorithm: Algorithm,
|
||||
|
||||
/// The duration, in seconds, for which a value is valid. Defaults to 30 seconds.
|
||||
step: usize,
|
||||
|
||||
/// An optional issuer. To help users identify their TOTP settings.
|
||||
issuer: Option<String>,
|
||||
|
||||
/// An optional account name, possibly chosen by the user, to identify their TOTP settings.
|
||||
account_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Totp {
|
||||
/// Allow modifying parameters by turning this into a builder.
|
||||
pub fn into_builder(self) -> TotpBuilder {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Duplicate the value into a new builder to modify parameters.
|
||||
pub fn to_builder(&self) -> TotpBuilder {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
/// Create a new empty OTP instance with default values and a predefined secret key.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
secret: Vec::new(),
|
||||
digits: 6,
|
||||
algorithm: Algorithm::Sha1,
|
||||
step: 30,
|
||||
issuer: None,
|
||||
account_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an OTP builder prefilled with default values.
|
||||
pub fn builder() -> TotpBuilder {
|
||||
TotpBuilder {
|
||||
inner: Self::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new OTP secret key builder using a secret specified in hexadecimal bytes.
|
||||
pub fn builder_from_hex(secret: &str) -> Result<TotpBuilder, Error> {
|
||||
crate::tools::hex_to_bin(secret).map(|secret| Self::builder().secret(secret))
|
||||
}
|
||||
|
||||
/// Get the secret key in binary form.
|
||||
pub fn secret(&self) -> &[u8] {
|
||||
&self.secret
|
||||
}
|
||||
|
||||
/// Get the used algorithm.
|
||||
pub fn algorithm(&self) -> Algorithm {
|
||||
self.algorithm
|
||||
}
|
||||
|
||||
/// Get the step duration.
|
||||
pub fn step(&self) -> Duration {
|
||||
Duration::from_secs(self.step as u64)
|
||||
}
|
||||
|
||||
/// Get the issuer, if any.
|
||||
pub fn issuer(&self) -> Option<&str> {
|
||||
self.issuer.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Get the account name, if any.
|
||||
pub fn account_name(&self) -> Option<&str> {
|
||||
self.account_name.as_ref().map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Raw signing function.
|
||||
fn sign(&self, input_data: &[u8]) -> Result<TotpValue, Error> {
|
||||
let secret = PKey::hmac(&self.secret)
|
||||
.map_err(|err| anyhow!("error instantiating hmac key: {}", err))?;
|
||||
|
||||
let mut signer = Signer::new(self.algorithm.into(), &secret)
|
||||
.map_err(|err| anyhow!("error instantiating hmac signer: {}", err))?;
|
||||
|
||||
signer
|
||||
.update(input_data)
|
||||
.map_err(|err| anyhow!("error calculating hmac (error in update): {}", err))?;
|
||||
|
||||
let hmac = signer
|
||||
.sign_to_vec()
|
||||
.map_err(|err| anyhow!("error calculating hmac (error in sign): {}", err))?;
|
||||
|
||||
let byte_offset = usize::from(
|
||||
hmac.last()
|
||||
.ok_or_else(|| anyhow!("error calculating hmac (too short)"))?
|
||||
& 0xF,
|
||||
);
|
||||
|
||||
let value = u32::from_be_bytes(
|
||||
TryFrom::try_from(
|
||||
hmac.get(byte_offset..(byte_offset + 4))
|
||||
.ok_or_else(|| anyhow!("error calculating hmac (too short)"))?,
|
||||
)
|
||||
.unwrap(),
|
||||
) & 0x7fffffff;
|
||||
|
||||
Ok(TotpValue {
|
||||
value,
|
||||
digits: u32::from(self.digits),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a HOTP value for a counter.
|
||||
///
|
||||
/// This is currently private as for actual counter mode we should have a validate helper
|
||||
/// which forces handling of too-low-but-within-range values explicitly!
|
||||
fn counter(&self, count: u64) -> Result<TotpValue, Error> {
|
||||
self.sign(&count.to_be_bytes())
|
||||
}
|
||||
|
||||
/// Convert a time stamp into a counter value. This makes it easier and cheaper to check a
|
||||
/// range of values.
|
||||
fn time_to_counter(&self, time: SystemTime) -> Result<u64, Error> {
|
||||
match time.duration_since(SystemTime::UNIX_EPOCH) {
|
||||
Ok(epoch) => Ok(epoch.as_secs() / (self.step as u64)),
|
||||
Err(_) => bail!("refusing to create otp value for negative time"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a TOTP value for a time stamp.
|
||||
pub fn time(&self, time: SystemTime) -> Result<TotpValue, Error> {
|
||||
self.counter(self.time_to_counter(time)?)
|
||||
}
|
||||
|
||||
/// Verify a time value within a range.
|
||||
///
|
||||
/// This will iterate through `steps` and check if the provided `time + step * step_size`
|
||||
/// matches. If a match is found, the matching step will be returned.
|
||||
pub fn verify(
|
||||
&self,
|
||||
digits: &str,
|
||||
time: SystemTime,
|
||||
steps: std::ops::RangeInclusive<isize>,
|
||||
) -> Result<Option<isize>, Error> {
|
||||
let count = self.time_to_counter(time)? as i64;
|
||||
for step in steps {
|
||||
if self.counter((count + step as i64) as u64)? == digits {
|
||||
return Ok(Some(step));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Create an otpauth URI for this configuration.
|
||||
pub fn to_uri(&self) -> Result<String, Error> {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
write!(out, "otpauth://totp/")?;
|
||||
|
||||
let account_name = match &self.account_name {
|
||||
Some(account_name) => account_name,
|
||||
None => bail!("cannot create otpauth uri without an account name"),
|
||||
};
|
||||
|
||||
let issuer = match &self.issuer {
|
||||
Some(issuer) => {
|
||||
let issuer = percent_encode(issuer.as_bytes(), percent_encoding::NON_ALPHANUMERIC)
|
||||
.to_string();
|
||||
write!(out, "{}:", issuer)?;
|
||||
Some(issuer)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
write!(
|
||||
out,
|
||||
"{}?secret={}",
|
||||
percent_encode(account_name.as_bytes(), percent_encoding::NON_ALPHANUMERIC),
|
||||
base32::encode(base32::Alphabet::RFC4648 { padding: false }, &self.secret),
|
||||
)?;
|
||||
write!(out, "&digits={}", self.digits)?;
|
||||
write!(out, "&algorithm={}", self.algorithm)?;
|
||||
write!(out, "&step={}", self.step)?;
|
||||
|
||||
if let Some(issuer) = issuer {
|
||||
write!(out, "&issuer={}", issuer)?;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Totp {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(uri: &str) -> Result<Self, Error> {
|
||||
if !uri.starts_with("otpauth://totp/") {
|
||||
bail!("not an otpauth uri");
|
||||
}
|
||||
|
||||
let uri = &uri.as_bytes()[15..];
|
||||
let qmark = uri
|
||||
.iter()
|
||||
.position(|&b| b == b'?')
|
||||
.ok_or_else(|| anyhow!("missing '?' in otp uri"))?;
|
||||
|
||||
let account = &uri[..qmark];
|
||||
let uri = &uri[(qmark + 1)..];
|
||||
|
||||
// FIXME: Also split on "%3A" / "%3a"
|
||||
let mut account = account.splitn(2, |&b| b == b':');
|
||||
let first_part = percent_decode(
|
||||
&account
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing account in otpauth uri"))?,
|
||||
)
|
||||
.decode_utf8_lossy()
|
||||
.into_owned();
|
||||
|
||||
let mut totp = Totp::empty();
|
||||
|
||||
match account.next() {
|
||||
Some(account_name) => {
|
||||
totp.issuer = Some(first_part);
|
||||
totp.account_name =
|
||||
Some(percent_decode(account_name).decode_utf8_lossy().to_string());
|
||||
}
|
||||
None => totp.account_name = Some(first_part),
|
||||
}
|
||||
|
||||
for parts in uri.split(|&b| b == b'&') {
|
||||
let mut parts = parts.splitn(2, |&b| b == b'=');
|
||||
let key = percent_decode(
|
||||
&parts
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("bad key in otpauth uri"))?,
|
||||
)
|
||||
.decode_utf8()?;
|
||||
let value = percent_decode(
|
||||
&parts
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("bad value in otpauth uri"))?,
|
||||
);
|
||||
|
||||
match &*key {
|
||||
"secret" => {
|
||||
totp.secret = base32::decode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&value.decode_utf8()?,
|
||||
)
|
||||
.ok_or_else(|| anyhow!("failed to decode otp secret in otpauth url"))?
|
||||
}
|
||||
"digits" => totp.digits = value.decode_utf8()?.parse()?,
|
||||
"algorithm" => totp.algorithm = value.decode_utf8()?.parse()?,
|
||||
"step" => totp.step = value.decode_utf8()?.parse()?,
|
||||
"issuer" => totp.issuer = Some(value.decode_utf8_lossy().into_owned()),
|
||||
_other => bail!("unrecognized otpauth uri parameter: {}", key),
|
||||
}
|
||||
}
|
||||
|
||||
if totp.secret.is_empty() {
|
||||
bail!("missing secret in otpauth url");
|
||||
}
|
||||
|
||||
Ok(totp)
|
||||
}
|
||||
}
|
||||
|
||||
crate::forward_deserialize_to_from_str!(Totp);
|
||||
|
||||
impl Serialize for Totp {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
use serde::ser::Error;
|
||||
|
||||
serializer.serialize_str(
|
||||
&self
|
||||
.to_uri()
|
||||
.map_err(|err| Error::custom(err.to_string()))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A HOTP value with a decimal digit limit.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TotpValue {
|
||||
value: u32,
|
||||
digits: u32,
|
||||
}
|
||||
|
||||
impl TotpValue {
|
||||
/// Change the number of decimal digits used for this HOTP value.
|
||||
pub fn digits(self, digits: u32) -> Self {
|
||||
Self { digits, ..self }
|
||||
}
|
||||
|
||||
/// Get the raw integer value before truncation.
|
||||
pub fn raw(&self) -> u32 {
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Get the integer value truncated to the requested number of decimal digits.
|
||||
pub fn value(&self) -> u32 {
|
||||
self.value % 10u32.pow(self.digits)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TotpValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{0:0width$}",
|
||||
self.value(),
|
||||
width = (self.digits as usize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<u32> for TotpValue {
|
||||
fn eq(&self, other: &u32) -> bool {
|
||||
self.value() == *other
|
||||
}
|
||||
}
|
||||
|
||||
/// For convenience we allow directly comparing with a string. This will make sure the string has
|
||||
/// the exact number of digits while parsing it explicitly as a decimal string.
|
||||
impl PartialEq<&str> for TotpValue {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
// Since we use `from_str_radix` with a radix of 10 explicitly, we can check the number of
|
||||
// bytes against the number of digits.
|
||||
if other.as_bytes().len() != (self.digits as usize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match u32::from_str_radix(*other, 10) {
|
||||
Ok(value) => self.value() == value,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_otp() {
|
||||
// Validated via:
|
||||
// ```sh
|
||||
// $ oathtool --hotp -c1 87259aa6550f059bca8c
|
||||
// 337037
|
||||
// ```
|
||||
const SECRET_1: &str = "87259aa6550f059bca8c";
|
||||
const EXPECTED_1: &str = "337037";
|
||||
const EXPECTED_2: &str = "296746";
|
||||
const EXPECTED_3: &str = "251167";
|
||||
const EXPECTED_4_D8: &str = "11899249";
|
||||
|
||||
let hotp = Totp::builder_from_hex(SECRET_1)
|
||||
.expect("failed to create Totp key")
|
||||
.digits(6)
|
||||
.build();
|
||||
assert_eq!(
|
||||
hotp.counter(1).expect("failed to create hotp value"),
|
||||
EXPECTED_1,
|
||||
);
|
||||
assert_eq!(
|
||||
hotp.counter(2)
|
||||
.expect("failed to create hotp value")
|
||||
.digits(6),
|
||||
EXPECTED_2,
|
||||
);
|
||||
assert_eq!(
|
||||
hotp.counter(3)
|
||||
.expect("failed to create hotp value")
|
||||
.digits(6),
|
||||
EXPECTED_3,
|
||||
);
|
||||
assert_eq!(
|
||||
hotp.counter(4)
|
||||
.expect("failed to create hotp value")
|
||||
.digits(8),
|
||||
EXPECTED_4_D8,
|
||||
);
|
||||
|
||||
let hotp = hotp
|
||||
.into_builder()
|
||||
.account_name("My Account".to_string())
|
||||
.build();
|
||||
let uri = hotp.to_uri().expect("failed to create otpauth uri");
|
||||
let parsed: Totp = uri.parse().expect("failed to parse otp uri");
|
||||
assert_eq!(parsed, hotp);
|
||||
assert_eq!(parsed.issuer, None);
|
||||
assert_eq!(
|
||||
parsed.account_name.as_ref().map(String::as_str),
|
||||
Some("My Account")
|
||||
);
|
||||
|
||||
const SECRET_2: &str = "a60b1b20679b1a64e21a";
|
||||
const EXPECTED: &str = "7757717";
|
||||
// Validated via:
|
||||
// ```sh
|
||||
// $ oathtool --totp -d7 -s30 --now='2020-08-04 15:14:23 UTC' a60b1b20679b1a64e21a
|
||||
// 7757717
|
||||
// $ date -d'2020-08-04 15:14:23 UTC' +%s
|
||||
// 1596554063
|
||||
// ```
|
||||
//
|
||||
let totp = Totp::builder_from_hex(SECRET_2)
|
||||
.expect("failed to create Totp key")
|
||||
.build();
|
||||
assert_eq!(
|
||||
totp.time(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1596554063))
|
||||
.expect("failed to create totp value")
|
||||
.digits(7),
|
||||
EXPECTED,
|
||||
);
|
||||
|
||||
let totp = totp
|
||||
.into_builder()
|
||||
.account_name("The Account Name".to_string())
|
||||
.issuer("An Issuer".to_string())
|
||||
.build();
|
||||
let uri = totp.to_uri().expect("failed to create otpauth uri");
|
||||
let parsed: Totp = uri.parse().expect("failed to parse otp uri");
|
||||
assert_eq!(parsed, totp);
|
||||
assert_eq!(
|
||||
parsed.issuer.as_ref().map(String::as_str),
|
||||
Some("An Issuer")
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.account_name.as_ref().map(String::as_str),
|
||||
Some("The Account Name")
|
||||
);
|
||||
}
|
546
proxmox/src/tools/tfa/u2f.rs
Normal file
546
proxmox/src/tools/tfa/u2f.rs
Normal file
@ -0,0 +1,546 @@
|
||||
//! U2F implementation.
|
||||
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use openssl::ec::{EcGroup, EcKey, EcPoint};
|
||||
use openssl::ecdsa::EcdsaSig;
|
||||
use openssl::pkey::Public;
|
||||
use openssl::sha;
|
||||
use openssl::x509::X509;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::tools::serde::{bytes_as_base64, bytes_as_base64url_nopad};
|
||||
|
||||
const CHALLENGE_LEN: usize = 32;
|
||||
const U2F_VERSION: &str = "U2F_V2";
|
||||
|
||||
/// The "key" part of a registration, passed to `u2f.sign` in the registered keys list.
|
||||
///
|
||||
/// Part of the U2F API, therefore `camelCase` and base64url without padding.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegisteredKey {
|
||||
/// Identifies the key handle on the client side. Used to create authentication challenges, so
|
||||
/// the client knows which key to use. Must be remembered.
|
||||
#[serde(with = "bytes_as_base64url_nopad")]
|
||||
pub key_handle: Vec<u8>,
|
||||
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// Data we get when a u2f token responds to a registration challenge.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Registration {
|
||||
/// The key consisting of key handle and version, which can be passed to the registered-keys
|
||||
/// list in `u2f.sign` in the browser.
|
||||
pub key: RegisteredKey,
|
||||
|
||||
/// Public part of the client key identified via the `key_handle`. Required to verify future
|
||||
/// authentication responses. Must be remembered.
|
||||
#[serde(with = "bytes_as_base64")]
|
||||
pub public_key: Vec<u8>,
|
||||
|
||||
/// Attestation certificate (in DER format) from which we originally copied the `key_handle`.
|
||||
/// Not necessary for authentication, unless the hardware tokens should be restricted to
|
||||
/// specific provider identities. Optional.
|
||||
#[serde(with = "bytes_as_base64")]
|
||||
pub certificate: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Result from a successful authentication. The client's hardware token will inform us about the
|
||||
/// user-presence (it may have been configured to respond automatically instead of requiring user
|
||||
/// interaction), and the number of authentications the key has performed.
|
||||
/// We probably won't make much use of this.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Authentication {
|
||||
/// `true` if the user had to be present.
|
||||
pub user_present: bool,
|
||||
|
||||
/// authentication count
|
||||
pub counter: usize,
|
||||
}
|
||||
|
||||
/// The hardware replies with a client data json object containing some information - this is the
|
||||
/// subset we actually make use of.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ClientData {
|
||||
/// The challenge the device responded to. This should be compared against the server side
|
||||
/// cached challenge!
|
||||
challenge: String,
|
||||
|
||||
/// The origin the the browser told the device the challenge was coming from.
|
||||
origin: String,
|
||||
}
|
||||
|
||||
/// A registration challenge to be sent to the `u2f.register` function in the browser.
|
||||
///
|
||||
/// Part of the U2F API, therefore `camelCase`.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegistrationChallenge {
|
||||
pub challenge: String,
|
||||
pub version: String,
|
||||
pub app_id: String,
|
||||
}
|
||||
|
||||
/// The response we get from a successful call to the `u2f.register` function in the browser.
|
||||
///
|
||||
/// Part of the U2F API, therefore `camelCase`.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RegistrationResponse {
|
||||
registration_data: String,
|
||||
client_data: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
/// Authentication challenge data to be sent to the `u2f.sign` function in the browser. Does not
|
||||
/// include the registered keys.
|
||||
///
|
||||
/// Part of the U2F API, therefore `camelCase`.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthChallenge {
|
||||
pub challenge: String,
|
||||
pub app_id: String,
|
||||
}
|
||||
|
||||
/// The response we get from a successful call to the `u2f.sign` function in the browser.
|
||||
///
|
||||
/// Part of the U2F API, therefore `camelCase` and base64url without padding.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthResponse {
|
||||
#[serde(with = "bytes_as_base64url_nopad")]
|
||||
key_handle: Vec<u8>,
|
||||
client_data: String,
|
||||
signature_data: String,
|
||||
}
|
||||
|
||||
impl AuthResponse {
|
||||
pub fn key_handle(&self) -> &[u8] {
|
||||
&self.key_handle
|
||||
}
|
||||
}
|
||||
|
||||
/// A u2f context to create or verify challenges with.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct U2f {
|
||||
app_id: String,
|
||||
origin: String,
|
||||
}
|
||||
|
||||
impl U2f {
|
||||
/// Create a new U2F context consisting of an appid and origin.
|
||||
pub fn new(app_id: String, origin: String) -> Self {
|
||||
Self { app_id, origin }
|
||||
}
|
||||
|
||||
/// Get a challenge object which can be directly passed to `u2f.register` on the browser side.
|
||||
pub fn registration_challenge(&self) -> Result<RegistrationChallenge, Error> {
|
||||
Ok(RegistrationChallenge {
|
||||
challenge: challenge()?,
|
||||
version: U2F_VERSION.to_owned(),
|
||||
app_id: self.app_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convenience method to verify the json formatted response object string.
|
||||
pub fn registration_verify(
|
||||
&self,
|
||||
challenge: &str,
|
||||
response: &str,
|
||||
) -> Result<Option<Registration>, Error> {
|
||||
let response: RegistrationResponse = serde_json::from_str(response)
|
||||
.map_err(|err| format_err!("error parsing response: {}", err))?;
|
||||
self.registration_verify_obj(challenge, response)
|
||||
}
|
||||
|
||||
/// Verifies the registration response object.
|
||||
pub fn registration_verify_obj(
|
||||
&self,
|
||||
challenge: &str,
|
||||
response: RegistrationResponse,
|
||||
) -> Result<Option<Registration>, Error> {
|
||||
let client_data_decoded = decode(&response.client_data)
|
||||
.map_err(|err| format_err!("error decoding client data in response: {}", err))?;
|
||||
|
||||
let client_data: ClientData = serde_json::from_reader(&mut &client_data_decoded[..])
|
||||
.map_err(|err| format_err!("error parsing client data: {}", err))?;
|
||||
|
||||
if client_data.challenge != challenge {
|
||||
bail!("registration challenge did not match");
|
||||
}
|
||||
|
||||
if client_data.origin != self.origin {
|
||||
bail!(
|
||||
"origin in client registration did not match: {:?} != {:?}",
|
||||
client_data.origin,
|
||||
self.origin,
|
||||
);
|
||||
}
|
||||
|
||||
let registration_data = decode(&response.registration_data)
|
||||
.map_err(|err| format_err!("error decoding registration data in response: {}", err))?;
|
||||
|
||||
let registration_data = RegistrationResponseData::from_raw(®istration_data)?;
|
||||
|
||||
let mut digest = sha::Sha256::new();
|
||||
digest.update(&[0u8]);
|
||||
digest.update(&sha::sha256(self.app_id.as_bytes()));
|
||||
digest.update(&sha::sha256(&client_data_decoded));
|
||||
digest.update(registration_data.key_handle);
|
||||
digest.update(registration_data.public_key);
|
||||
let digest = digest.finish();
|
||||
|
||||
let signature = EcdsaSig::from_der(registration_data.signature)
|
||||
.map_err(|err| format_err!("error decoding signature in response: {}", err))?;
|
||||
|
||||
// can we decode the public key?
|
||||
drop(decode_public_key(registration_data.public_key)?);
|
||||
|
||||
match signature.verify(&digest, ®istration_data.cert_key) {
|
||||
Ok(true) => Ok(Some(Registration {
|
||||
key: RegisteredKey {
|
||||
key_handle: registration_data.key_handle.to_vec(),
|
||||
version: response.version,
|
||||
},
|
||||
public_key: registration_data.public_key.to_vec(),
|
||||
certificate: registration_data.certificate.to_vec(),
|
||||
})),
|
||||
Ok(false) => Ok(None),
|
||||
Err(err) => bail!("openssl error while verifying signature: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a challenge object which can be directly passwd to `u2f.sign` on the browser side.
|
||||
pub fn auth_challenge(&self) -> Result<AuthChallenge, Error> {
|
||||
Ok(AuthChallenge {
|
||||
challenge: challenge()?,
|
||||
app_id: self.app_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convenience method to verify the json formatted response object string.
|
||||
pub fn auth_verify(
|
||||
&self,
|
||||
public_key: &[u8],
|
||||
challenge: &str,
|
||||
response: &str,
|
||||
) -> Result<Option<Authentication>, Error> {
|
||||
let response: AuthResponse = serde_json::from_str(response)
|
||||
.map_err(|err| format_err!("error parsing response: {}", err))?;
|
||||
self.auth_verify_obj(public_key, challenge, response)
|
||||
}
|
||||
|
||||
/// Verifies the authentication response object.
|
||||
pub fn auth_verify_obj(
|
||||
&self,
|
||||
public_key: &[u8],
|
||||
challenge: &str,
|
||||
response: AuthResponse,
|
||||
) -> Result<Option<Authentication>, Error> {
|
||||
let client_data_decoded = decode(&response.client_data)
|
||||
.map_err(|err| format_err!("error decoding client data in response: {}", err))?;
|
||||
|
||||
let client_data: ClientData = serde_json::from_reader(&mut &client_data_decoded[..])
|
||||
.map_err(|err| format_err!("error parsing client data: {}", err))?;
|
||||
|
||||
if client_data.challenge != challenge {
|
||||
bail!("authentication challenge did not match");
|
||||
}
|
||||
|
||||
if client_data.origin != self.origin {
|
||||
bail!(
|
||||
"origin in client authentication did not match: {:?} != {:?}",
|
||||
client_data.origin,
|
||||
self.origin,
|
||||
);
|
||||
}
|
||||
|
||||
let signature_data = decode(&response.signature_data)
|
||||
.map_err(|err| format_err!("error decoding signature data in response: {}", err))?;
|
||||
|
||||
// an ecdsa signature is much longer than 16 bytes but we only need to parse the first 5
|
||||
// anyway...
|
||||
if signature_data.len() < 1 + 4 + 16 {
|
||||
bail!("invalid signature data");
|
||||
}
|
||||
|
||||
let presence_and_counter_bytes = &signature_data[0..5];
|
||||
let user_present = presence_and_counter_bytes[0] != 0;
|
||||
let counter_bytes = &presence_and_counter_bytes[1..];
|
||||
let counter: u32 =
|
||||
u32::from_be(unsafe { std::ptr::read_unaligned(counter_bytes.as_ptr() as *const u32) });
|
||||
let signature = EcdsaSig::from_der(&signature_data[5..])
|
||||
.map_err(|err| format_err!("error decoding signature in response: {}", err))?;
|
||||
|
||||
let public_key = decode_public_key(public_key)?;
|
||||
|
||||
let mut digest = sha::Sha256::new();
|
||||
digest.update(&sha::sha256(self.app_id.as_bytes()));
|
||||
digest.update(presence_and_counter_bytes);
|
||||
digest.update(&sha::sha256(&client_data_decoded));
|
||||
let digest = digest.finish();
|
||||
|
||||
match signature.verify(&digest, &public_key) {
|
||||
Ok(true) => Ok(Some(Authentication {
|
||||
user_present,
|
||||
counter: counter as usize,
|
||||
})),
|
||||
Ok(false) => Ok(None),
|
||||
Err(err) => bail!("openssl error while verifying signature: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// base64url encoding
|
||||
fn encode(data: &[u8]) -> String {
|
||||
let mut out = base64::encode_config(data, base64::URL_SAFE_NO_PAD);
|
||||
while out.ends_with('=') {
|
||||
out.pop();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// base64url decoding
|
||||
fn decode(data: &str) -> Result<Vec<u8>, Error> {
|
||||
Ok(base64::decode_config(data, base64::URL_SAFE_NO_PAD)?)
|
||||
}
|
||||
|
||||
/// produce a challenge, which is just a bunch of random data
|
||||
fn challenge() -> Result<String, Error> {
|
||||
let mut data = MaybeUninit::<[u8; CHALLENGE_LEN]>::uninit();
|
||||
Ok(encode(&unsafe {
|
||||
crate::sys::linux::fill_with_random_data(&mut *data.as_mut_ptr())?;
|
||||
data.assume_init()
|
||||
}))
|
||||
}
|
||||
|
||||
/// Used while parsing the binary registration response. The slices point directly into the
|
||||
/// original response byte data, and the public key is extracted from the contained X509
|
||||
/// certificate.
|
||||
#[derive(Debug)]
|
||||
pub struct RegistrationResponseData<'a> {
|
||||
public_key: &'a [u8],
|
||||
key_handle: &'a [u8],
|
||||
certificate: &'a [u8],
|
||||
signature: &'a [u8],
|
||||
|
||||
/// The client's public key in decoded and parsed form.
|
||||
cert_key: EcKey<Public>,
|
||||
}
|
||||
|
||||
impl<'a> RegistrationResponseData<'a> {
|
||||
/// Parse the binary registration data into its parts and extract the certificate's public key.
|
||||
///
|
||||
/// See https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html
|
||||
pub fn from_raw(data: &'a [u8]) -> Result<Self, Error> {
|
||||
// [ 0x05 | 65b pubkey | 1b keyhandle len | keyhandle | certificate (1 DER obj) | signature ]
|
||||
|
||||
if data.len() <= (1 + 65 + 1 + 71) {
|
||||
bail!("registration data too short");
|
||||
}
|
||||
|
||||
if data[0] != 0x05 {
|
||||
bail!(
|
||||
"invalid registration data, reserved byte is 0x{:02x}, expected 0x05",
|
||||
data[0]
|
||||
);
|
||||
}
|
||||
|
||||
let public_key = &data[1..66];
|
||||
|
||||
let key_handle_len = usize::from(data[66]);
|
||||
let data = &data[67..];
|
||||
|
||||
if data.len() <= key_handle_len + 71 {
|
||||
bail!("registration data invalid too short");
|
||||
}
|
||||
|
||||
let key_handle = &data[..key_handle_len];
|
||||
let data = &data[key_handle_len..];
|
||||
if data[0] != 0x30 {
|
||||
bail!("error decoding X509 certificate: not a SEQUENCE tag");
|
||||
}
|
||||
let cert_len = der_length(&data[1..])? + 1; // plus the tag!
|
||||
let certificate = &data[..cert_len];
|
||||
let x509 = X509::from_der(certificate)
|
||||
.map_err(|err| format_err!("error decoding X509 certificate: {}", err))?;
|
||||
let signature = &data[cert_len..];
|
||||
|
||||
Ok(Self {
|
||||
public_key,
|
||||
key_handle,
|
||||
certificate,
|
||||
signature,
|
||||
cert_key: x509.public_key()?.ec_key()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the raw 65 byte ec public key into an `openssl::EcKey<Public>`.
|
||||
fn decode_public_key(data: &[u8]) -> Result<EcKey<Public>, Error> {
|
||||
if data.len() != 65 {
|
||||
bail!("invalid public key length {}, expected 65", data.len());
|
||||
}
|
||||
|
||||
let group = EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)
|
||||
.map_err(|err| format_err!("openssl error, failed to instantiate ec curve: {}", err))?;
|
||||
|
||||
let mut bn = openssl::bn::BigNumContext::new().map_err(|err| {
|
||||
format_err!(
|
||||
"openssl error, failed to instantiate bignum context: {}",
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
||||
let point = EcPoint::from_bytes(&group, data, &mut bn)
|
||||
.map_err(|err| format_err!("failed to decode public key point: {}", err))?;
|
||||
|
||||
let key = EcKey::from_public_key(&group, &point)
|
||||
.map_err(|err| format_err!("failed to instantiate public key: {}", err))?;
|
||||
|
||||
key.check_key()
|
||||
.map_err(|err| format_err!("public key failed self check: {}", err))?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// The only DER thing we need: lengths.
|
||||
///
|
||||
/// Returns the length *including* the size of the length itself.
|
||||
fn der_length(data: &[u8]) -> Result<usize, Error> {
|
||||
if data[0] == 0 {
|
||||
bail!("error decoding X509 certificate: bad length (0)");
|
||||
}
|
||||
|
||||
if data[0] < 0x80 {
|
||||
return Ok(usize::from(data[0]) + 1);
|
||||
}
|
||||
|
||||
let count = usize::from(data[0] & 0x7F);
|
||||
if count == 0x7F {
|
||||
// X.609; 8.1.3.5, the value `1111111` shall not be used
|
||||
bail!("error decoding X509 certificate: illegal length value");
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
// "indefinite" form not allowed in DER
|
||||
bail!("error decoding X509 certificate: illegal length form");
|
||||
}
|
||||
|
||||
if count > std::mem::size_of::<usize>() {
|
||||
bail!("error decoding X509 certificate: unsupported length");
|
||||
}
|
||||
|
||||
if count > (data.len() - 1) {
|
||||
bail!("error decoding X509 certificate: truncated length data");
|
||||
}
|
||||
|
||||
let mut len = 0;
|
||||
for i in 0..count {
|
||||
len = (len << 8) | usize::from(data[1 + i]);
|
||||
}
|
||||
|
||||
Ok(len + count + 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// The test data in here is generated with a yubi key...
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
const TEST_APPID: &str = "https://u2ftest.enonet.errno.eu";
|
||||
|
||||
const TEST_REGISTRATION_JSON: &str =
|
||||
"{\"challenge\":\"mZoWLngnAh8p98nPkFOIBXecd0CbmgEx5tEd5jNswgY\",\"response\":{\"client\
|
||||
Data\":\"eyJjaGFsbGVuZ2UiOiJtWm9XTG5nbkFoOHA5OG5Qa0ZPSUJYZWNkMENibWdFeDV0RWQ1ak5zd2dZI\
|
||||
iwib3JpZ2luIjoiaHR0cHM6Ly91MmZ0ZXN0LmVub25ldC5lcnJuby5ldSIsInR5cCI6Im5hdmlnYXRvci5pZC5\
|
||||
maW5pc2hFbnJvbGxtZW50In0\",\"registrationData\":\"BQR_9TmMowVeoAHp3ABljCa90eNG87t76D4W\
|
||||
c9nsmK9ihNhhYNxYIq9tnRUPTBZ2X4kZKSB0LXMm32lOKQlNB56QQHlt81cRBfID7BvHk_XIJZc5ks5D3R1ZV1\
|
||||
1fJudp3F-ii_KSdZaFb4cGaq0rEaVDfNR2ZR0T0ApMMCeTIaDAJRQwggJEMIIBLqADAgECAgRVYr6gMAsGCSqG\
|
||||
SIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MD\
|
||||
EwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTQzMjUz\
|
||||
NDY4ODBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEszH3c9gUS5mVy-RYVRfhdYOqR2I2lcvoWsSCyAGfLJuU\
|
||||
Z64EWw5m8TGy6jJDyR_aYC4xjz_F2NKnq65yvRQwmjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0\
|
||||
ODIuMS41MBMGCysGAQQBguUcAgEBBAQDAgUgMAsGCSqGSIb3DQEBCwOCAQEArBbZs262s6m3bXWUs09Z9Pc-28\
|
||||
n96yk162tFHKv0HSXT5xYU10cmBMpypXjjI-23YARoXwXn0bm-BdtulED6xc_JMqbK-uhSmXcu2wJ4ICA81BQd\
|
||||
PutvaizpnjlXgDJjq6uNbsSAp98IStLLp7fW13yUw-vAsWb5YFfK9f46Yx6iakM3YqNvvs9M9EUJYl_VrxBJqn\
|
||||
yLx2iaZlnpr13o8NcsKIJRdMUOBqt_ageQg3ttsyq_3LyoNcu7CQ7x8NmeCGm_6eVnZMQjDmwFdymwEN4OxfnM\
|
||||
5MkcKCYhjqgIGruWkVHsFnJa8qjZXneVvKoiepuUQyDEJ2GcqvhU2YKY1zBFAiEA2mcfAS2XRcWy1lLJikFHGJ\
|
||||
SbtOrrwswjOKEzwp6EonkCIFBxbLAmwUnblAWOVELASi610ZfPK-7qx2VwkWfHqnll\",\"version\":\"U2F\
|
||||
_V2\"}}";
|
||||
|
||||
const TEST_AUTH_JSON: &str =
|
||||
"{\"challenge\":\"8LE_-7Rd1vB3Otn3vJ7GyiwRQtYPMv-BWliCejH0d4Y\",\"response\":{\"clientD\
|
||||
ata\":\"eyJjaGFsbGVuZ2UiOiI4TEVfLTdSZDF2QjNPdG4zdko3R3lpd1JRdFlQTXYtQldsaUNlakgwZDRZIiw\
|
||||
ib3JpZ2luIjoiaHR0cHM6Ly91MmZ0ZXN0LmVub25ldC5lcnJuby5ldSIsInR5cCI6Im5hdmlnYXRvci5pZC5nZX\
|
||||
RBc3NlcnRpb24ifQ\",\"keyHandle\":\"eW3zVxEF8gPsG8eT9cgllzmSzkPdHVlXXV8m52ncX6KL8pJ1loVv\
|
||||
hwZqrSsRpUN81HZlHRPQCkwwJ5MhoMAlFA\",\"signatureData\":\"AQAAAQEwRAIgKdM9cmCLZDxntY-dT_\
|
||||
OXbcVA1D5ewQunXVC-CYZ65pUCIAIOUBsu-dOmTym0ITZt6x75BFUSGlqYRuH5JKBcyO3M\"},\"user\":{\"c\
|
||||
ertificate\":\"MIICRDCCAS6gAwIBAgIEVWK+oDALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUy\
|
||||
RiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDA\
|
||||
mBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE0MzI1MzQ2ODgwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAA\
|
||||
RLMx93PYFEuZlcvkWFUX4XWDqkdiNpXL6FrEgsgBnyyblGeuBFsOZvExsuoyQ8kf2mAuMY8/xdjSp6uucr0UMJo\
|
||||
zswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNTATBgsrBgEEAYLlHAIBAQQEAwIFIDALBgkq\
|
||||
hkiG9w0BAQsDggEBAKwW2bNutrOpt211lLNPWfT3PtvJ/espNetrRRyr9B0l0+cWFNdHJgTKcqV44yPtt2AEaF8\
|
||||
F59G5vgXbbpRA+sXPyTKmyvroUpl3LtsCeCAgPNQUHT7rb2os6Z45V4AyY6urjW7EgKffCErSy6e31td8lMPrwL\
|
||||
Fm+WBXyvX+OmMeompDN2Kjb77PTPRFCWJf1a8QSap8i8dommZZ6a9d6PDXLCiCUXTFDgarf2oHkIN7bbMqv9y8q\
|
||||
DXLuwkO8fDZnghpv+nlZ2TEIw5sBXcpsBDeDsX5zOTJHCgmIY6oCBq7lpFR7BZyWvKo2V53lbyqInqblEMgxCdh\
|
||||
nKr4VNmCmNc=\",\"key\":{\"keyHandle\":\"eW3zVxEF8gPsG8eT9cgllzmSzkPdHVlXXV8m52ncX6KL8pJ\
|
||||
1loVvhwZqrSsRpUN81HZlHRPQCkwwJ5MhoMAlFA\",\"version\":\"U2F_V2\"},\"public-key\":\"BH/1\
|
||||
OYyjBV6gAencAGWMJr3R40bzu3voPhZz2eyYr2KE2GFg3Fgir22dFQ9MFnZfiRkpIHQtcybfaU4pCU0HnpA=\"}\
|
||||
}";
|
||||
|
||||
#[test]
|
||||
fn test_registration() {
|
||||
let data = TEST_REGISTRATION_JSON;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TestChallenge {
|
||||
challenge: String,
|
||||
response: super::RegistrationResponse,
|
||||
}
|
||||
|
||||
let ts: TestChallenge =
|
||||
serde_json::from_str(&data).expect("failed to parse json test data");
|
||||
|
||||
let context = super::U2f::new(TEST_APPID.to_string(), TEST_APPID.to_string());
|
||||
let res = context
|
||||
.registration_verify_obj(&ts.challenge, ts.response)
|
||||
.expect("error trying to verify registration");
|
||||
assert!(
|
||||
res.is_some(),
|
||||
"test registration signature fails verification"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication() {
|
||||
let data = TEST_AUTH_JSON;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TestChallenge {
|
||||
challenge: String,
|
||||
user: super::Registration,
|
||||
response: super::AuthResponse,
|
||||
}
|
||||
|
||||
let ts: TestChallenge =
|
||||
serde_json::from_str(&data).expect("failed to parse json test data");
|
||||
|
||||
let context = super::U2f::new(TEST_APPID.to_string(), TEST_APPID.to_string());
|
||||
let res = context
|
||||
.auth_verify_obj(&ts.user.public_key, &ts.challenge, ts.response)
|
||||
.expect("error trying to verify authentication");
|
||||
assert!(
|
||||
res.is_some(),
|
||||
"test authentication signature fails verification"
|
||||
);
|
||||
}
|
||||
}
|
748
proxmox/u2f-api.js
Normal file
748
proxmox/u2f-api.js
Normal file
@ -0,0 +1,748 @@
|
||||
//Copyright 2014-2015 Google Inc. All rights reserved.
|
||||
|
||||
//Use of this source code is governed by a BSD-style
|
||||
//license that can be found in the LICENSE file or at
|
||||
//https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
/**
|
||||
* @fileoverview The U2F api.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
||||
/**
|
||||
* Namespace for the U2F api.
|
||||
* @type {Object}
|
||||
*/
|
||||
var u2f = u2f || {};
|
||||
|
||||
/**
|
||||
* FIDO U2F Javascript API Version
|
||||
* @number
|
||||
*/
|
||||
var js_api_version;
|
||||
|
||||
/**
|
||||
* The U2F extension id
|
||||
* @const {string}
|
||||
*/
|
||||
// The Chrome packaged app extension ID.
|
||||
// Uncomment this if you want to deploy a server instance that uses
|
||||
// the package Chrome app and does not require installing the U2F Chrome extension.
|
||||
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
|
||||
// The U2F Chrome extension ID.
|
||||
// Uncomment this if you want to deploy a server instance that uses
|
||||
// the U2F Chrome extension to authenticate.
|
||||
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
|
||||
|
||||
|
||||
/**
|
||||
* Message types for messsages to/from the extension
|
||||
* @const
|
||||
* @enum {string}
|
||||
*/
|
||||
u2f.MessageTypes = {
|
||||
'U2F_REGISTER_REQUEST': 'u2f_register_request',
|
||||
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
|
||||
'U2F_SIGN_REQUEST': 'u2f_sign_request',
|
||||
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
|
||||
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
|
||||
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Response status codes
|
||||
* @const
|
||||
* @enum {number}
|
||||
*/
|
||||
u2f.ErrorCodes = {
|
||||
'OK': 0,
|
||||
'OTHER_ERROR': 1,
|
||||
'BAD_REQUEST': 2,
|
||||
'CONFIGURATION_UNSUPPORTED': 3,
|
||||
'DEVICE_INELIGIBLE': 4,
|
||||
'TIMEOUT': 5
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A message for registration requests
|
||||
* @typedef {{
|
||||
* type: u2f.MessageTypes,
|
||||
* appId: ?string,
|
||||
* timeoutSeconds: ?number,
|
||||
* requestId: ?number
|
||||
* }}
|
||||
*/
|
||||
u2f.U2fRequest;
|
||||
|
||||
|
||||
/**
|
||||
* A message for registration responses
|
||||
* @typedef {{
|
||||
* type: u2f.MessageTypes,
|
||||
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
|
||||
* requestId: ?number
|
||||
* }}
|
||||
*/
|
||||
u2f.U2fResponse;
|
||||
|
||||
|
||||
/**
|
||||
* An error object for responses
|
||||
* @typedef {{
|
||||
* errorCode: u2f.ErrorCodes,
|
||||
* errorMessage: ?string
|
||||
* }}
|
||||
*/
|
||||
u2f.Error;
|
||||
|
||||
/**
|
||||
* Data object for a single sign request.
|
||||
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
|
||||
*/
|
||||
u2f.Transport;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a single sign request.
|
||||
* @typedef {Array<u2f.Transport>}
|
||||
*/
|
||||
u2f.Transports;
|
||||
|
||||
/**
|
||||
* Data object for a single sign request.
|
||||
* @typedef {{
|
||||
* version: string,
|
||||
* challenge: string,
|
||||
* keyHandle: string,
|
||||
* appId: string
|
||||
* }}
|
||||
*/
|
||||
u2f.SignRequest;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a sign response.
|
||||
* @typedef {{
|
||||
* keyHandle: string,
|
||||
* signatureData: string,
|
||||
* clientData: string
|
||||
* }}
|
||||
*/
|
||||
u2f.SignResponse;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a registration request.
|
||||
* @typedef {{
|
||||
* version: string,
|
||||
* challenge: string
|
||||
* }}
|
||||
*/
|
||||
u2f.RegisterRequest;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a registration response.
|
||||
* @typedef {{
|
||||
* version: string,
|
||||
* keyHandle: string,
|
||||
* transports: Transports,
|
||||
* appId: string
|
||||
* }}
|
||||
*/
|
||||
u2f.RegisterResponse;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a registered key.
|
||||
* @typedef {{
|
||||
* version: string,
|
||||
* keyHandle: string,
|
||||
* transports: ?Transports,
|
||||
* appId: ?string
|
||||
* }}
|
||||
*/
|
||||
u2f.RegisteredKey;
|
||||
|
||||
|
||||
/**
|
||||
* Data object for a get API register response.
|
||||
* @typedef {{
|
||||
* js_api_version: number
|
||||
* }}
|
||||
*/
|
||||
u2f.GetJsApiVersionResponse;
|
||||
|
||||
|
||||
//Low level MessagePort API support
|
||||
|
||||
/**
|
||||
* Sets up a MessagePort to the U2F extension using the
|
||||
* available mechanisms.
|
||||
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
||||
*/
|
||||
u2f.getMessagePort = function(callback) {
|
||||
if (typeof chrome != 'undefined' && chrome.runtime) {
|
||||
// The actual message here does not matter, but we need to get a reply
|
||||
// for the callback to run. Thus, send an empty signature request
|
||||
// in order to get a failure response.
|
||||
var msg = {
|
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||
signRequests: []
|
||||
};
|
||||
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
|
||||
if (!chrome.runtime.lastError) {
|
||||
// We are on a whitelisted origin and can talk directly
|
||||
// with the extension.
|
||||
u2f.getChromeRuntimePort_(callback);
|
||||
} else {
|
||||
// chrome.runtime was available, but we couldn't message
|
||||
// the extension directly, use iframe
|
||||
u2f.getIframePort_(callback);
|
||||
}
|
||||
});
|
||||
} else if (u2f.isAndroidChrome_()) {
|
||||
u2f.getAuthenticatorPort_(callback);
|
||||
} else if (u2f.isIosChrome_()) {
|
||||
u2f.getIosPort_(callback);
|
||||
} else {
|
||||
// chrome.runtime was not available at all, which is normal
|
||||
// when this origin doesn't have access to any extensions.
|
||||
u2f.getIframePort_(callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect chrome running on android based on the browser's useragent.
|
||||
* @private
|
||||
*/
|
||||
u2f.isAndroidChrome_ = function() {
|
||||
var userAgent = navigator.userAgent;
|
||||
return userAgent.indexOf('Chrome') != -1 &&
|
||||
userAgent.indexOf('Android') != -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect chrome running on iOS based on the browser's platform.
|
||||
* @private
|
||||
*/
|
||||
u2f.isIosChrome_ = function() {
|
||||
return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connects directly to the extension via chrome.runtime.connect.
|
||||
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getChromeRuntimePort_ = function(callback) {
|
||||
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
|
||||
{'includeTlsChannelId': true});
|
||||
setTimeout(function() {
|
||||
callback(new u2f.WrappedChromeRuntimePort_(port));
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a 'port' abstraction to the Authenticator app.
|
||||
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getAuthenticatorPort_ = function(callback) {
|
||||
setTimeout(function() {
|
||||
callback(new u2f.WrappedAuthenticatorPort_());
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a 'port' abstraction to the iOS client app.
|
||||
* @param {function(u2f.WrappedIosPort_)} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getIosPort_ = function(callback) {
|
||||
setTimeout(function() {
|
||||
callback(new u2f.WrappedIosPort_());
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
|
||||
* @param {Port} port
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
u2f.WrappedChromeRuntimePort_ = function(port) {
|
||||
this.port_ = port;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format and return a sign request compliant with the JS API version supported by the extension.
|
||||
* @param {Array<u2f.SignRequest>} signRequests
|
||||
* @param {number} timeoutSeconds
|
||||
* @param {number} reqId
|
||||
* @return {Object}
|
||||
*/
|
||||
u2f.formatSignRequest_ =
|
||||
function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
|
||||
if (js_api_version === undefined || js_api_version < 1.1) {
|
||||
// Adapt request to the 1.0 JS API
|
||||
var signRequests = [];
|
||||
for (var i = 0; i < registeredKeys.length; i++) {
|
||||
signRequests[i] = {
|
||||
version: registeredKeys[i].version,
|
||||
challenge: challenge,
|
||||
keyHandle: registeredKeys[i].keyHandle,
|
||||
appId: appId
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||
signRequests: signRequests,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
requestId: reqId
|
||||
};
|
||||
}
|
||||
// JS 1.1 API
|
||||
return {
|
||||
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
||||
appId: appId,
|
||||
challenge: challenge,
|
||||
registeredKeys: registeredKeys,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
requestId: reqId
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format and return a register request compliant with the JS API version supported by the extension..
|
||||
* @param {Array<u2f.SignRequest>} signRequests
|
||||
* @param {Array<u2f.RegisterRequest>} signRequests
|
||||
* @param {number} timeoutSeconds
|
||||
* @param {number} reqId
|
||||
* @return {Object}
|
||||
*/
|
||||
u2f.formatRegisterRequest_ =
|
||||
function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
|
||||
if (js_api_version === undefined || js_api_version < 1.1) {
|
||||
// Adapt request to the 1.0 JS API
|
||||
for (var i = 0; i < registerRequests.length; i++) {
|
||||
registerRequests[i].appId = appId;
|
||||
}
|
||||
var signRequests = [];
|
||||
for (var i = 0; i < registeredKeys.length; i++) {
|
||||
signRequests[i] = {
|
||||
version: registeredKeys[i].version,
|
||||
challenge: registerRequests[0],
|
||||
keyHandle: registeredKeys[i].keyHandle,
|
||||
appId: appId
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
||||
signRequests: signRequests,
|
||||
registerRequests: registerRequests,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
requestId: reqId
|
||||
};
|
||||
}
|
||||
// JS 1.1 API
|
||||
return {
|
||||
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
||||
appId: appId,
|
||||
registerRequests: registerRequests,
|
||||
registeredKeys: registeredKeys,
|
||||
timeoutSeconds: timeoutSeconds,
|
||||
requestId: reqId
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Posts a message on the underlying channel.
|
||||
* @param {Object} message
|
||||
*/
|
||||
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
|
||||
this.port_.postMessage(message);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Emulates the HTML 5 addEventListener interface. Works only for the
|
||||
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
|
||||
* @param {string} eventName
|
||||
* @param {function({data: Object})} handler
|
||||
*/
|
||||
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
|
||||
function(eventName, handler) {
|
||||
var name = eventName.toLowerCase();
|
||||
if (name == 'message' || name == 'onmessage') {
|
||||
this.port_.onMessage.addListener(function(message) {
|
||||
// Emulate a minimal MessageEvent object
|
||||
handler({'data': message});
|
||||
});
|
||||
} else {
|
||||
console.error('WrappedChromeRuntimePort only supports onMessage');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap the Authenticator app with a MessagePort interface.
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_ = function() {
|
||||
this.requestId_ = -1;
|
||||
this.requestObject_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the Authenticator intent.
|
||||
* @param {Object} message
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
|
||||
var intentUrl =
|
||||
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
|
||||
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
|
||||
';end';
|
||||
document.location = intentUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tells what type of port this is.
|
||||
* @return {String} port type
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
|
||||
return "WrappedAuthenticatorPort_";
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Emulates the HTML 5 addEventListener interface.
|
||||
* @param {string} eventName
|
||||
* @param {function({data: Object})} handler
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
|
||||
var name = eventName.toLowerCase();
|
||||
if (name == 'message') {
|
||||
var self = this;
|
||||
/* Register a callback to that executes when
|
||||
* chrome injects the response. */
|
||||
window.addEventListener(
|
||||
'message', self.onRequestUpdate_.bind(self, handler), false);
|
||||
} else {
|
||||
console.error('WrappedAuthenticatorPort only supports message');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback invoked when a response is received from the Authenticator.
|
||||
* @param function({data: Object}) callback
|
||||
* @param {Object} message message Object
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
|
||||
function(callback, message) {
|
||||
var messageObject = JSON.parse(message.data);
|
||||
var intentUrl = messageObject['intentURL'];
|
||||
|
||||
var errorCode = messageObject['errorCode'];
|
||||
var responseObject = null;
|
||||
if (messageObject.hasOwnProperty('data')) {
|
||||
responseObject = /** @type {Object} */ (
|
||||
JSON.parse(messageObject['data']));
|
||||
}
|
||||
|
||||
callback({'data': responseObject});
|
||||
};
|
||||
|
||||
/**
|
||||
* Base URL for intents to Authenticator.
|
||||
* @const
|
||||
* @private
|
||||
*/
|
||||
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
|
||||
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
|
||||
|
||||
/**
|
||||
* Wrap the iOS client app with a MessagePort interface.
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
u2f.WrappedIosPort_ = function() {};
|
||||
|
||||
/**
|
||||
* Launch the iOS client app request
|
||||
* @param {Object} message
|
||||
*/
|
||||
u2f.WrappedIosPort_.prototype.postMessage = function(message) {
|
||||
var str = JSON.stringify(message);
|
||||
var url = "u2f://auth?" + encodeURI(str);
|
||||
location.replace(url);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tells what type of port this is.
|
||||
* @return {String} port type
|
||||
*/
|
||||
u2f.WrappedIosPort_.prototype.getPortType = function() {
|
||||
return "WrappedIosPort_";
|
||||
};
|
||||
|
||||
/**
|
||||
* Emulates the HTML 5 addEventListener interface.
|
||||
* @param {string} eventName
|
||||
* @param {function({data: Object})} handler
|
||||
*/
|
||||
u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
|
||||
var name = eventName.toLowerCase();
|
||||
if (name !== 'message') {
|
||||
console.error('WrappedIosPort only supports message');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up an embedded trampoline iframe, sourced from the extension.
|
||||
* @param {function(MessagePort)} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getIframePort_ = function(callback) {
|
||||
// Create the iframe
|
||||
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.src = iframeOrigin + '/u2f-comms.html';
|
||||
iframe.setAttribute('style', 'display:none');
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
var channel = new MessageChannel();
|
||||
var ready = function(message) {
|
||||
if (message.data == 'ready') {
|
||||
channel.port1.removeEventListener('message', ready);
|
||||
callback(channel.port1);
|
||||
} else {
|
||||
console.error('First event on iframe port was not "ready"');
|
||||
}
|
||||
};
|
||||
channel.port1.addEventListener('message', ready);
|
||||
channel.port1.start();
|
||||
|
||||
iframe.addEventListener('load', function() {
|
||||
// Deliver the port to the iframe and initialize
|
||||
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
//High-level JS API
|
||||
|
||||
/**
|
||||
* Default extension response timeout in seconds.
|
||||
* @const
|
||||
*/
|
||||
u2f.EXTENSION_TIMEOUT_SEC = 30;
|
||||
|
||||
/**
|
||||
* A singleton instance for a MessagePort to the extension.
|
||||
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
|
||||
* @private
|
||||
*/
|
||||
u2f.port_ = null;
|
||||
|
||||
/**
|
||||
* Callbacks waiting for a port
|
||||
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
|
||||
* @private
|
||||
*/
|
||||
u2f.waitingForPort_ = [];
|
||||
|
||||
/**
|
||||
* A counter for requestIds.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
u2f.reqCounter_ = 0;
|
||||
|
||||
/**
|
||||
* A map from requestIds to client callbacks
|
||||
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
|
||||
* |function((u2f.Error|u2f.SignResponse)))>}
|
||||
* @private
|
||||
*/
|
||||
u2f.callbackMap_ = {};
|
||||
|
||||
/**
|
||||
* Creates or retrieves the MessagePort singleton to use.
|
||||
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
||||
* @private
|
||||
*/
|
||||
u2f.getPortSingleton_ = function(callback) {
|
||||
if (u2f.port_) {
|
||||
callback(u2f.port_);
|
||||
} else {
|
||||
if (u2f.waitingForPort_.length == 0) {
|
||||
u2f.getMessagePort(function(port) {
|
||||
u2f.port_ = port;
|
||||
u2f.port_.addEventListener('message',
|
||||
/** @type {function(Event)} */ (u2f.responseHandler_));
|
||||
|
||||
// Careful, here be async callbacks. Maybe.
|
||||
while (u2f.waitingForPort_.length)
|
||||
u2f.waitingForPort_.shift()(u2f.port_);
|
||||
});
|
||||
}
|
||||
u2f.waitingForPort_.push(callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles response messages from the extension.
|
||||
* @param {MessageEvent.<u2f.Response>} message
|
||||
* @private
|
||||
*/
|
||||
u2f.responseHandler_ = function(message) {
|
||||
var response = message.data;
|
||||
var reqId = response['requestId'];
|
||||
if (!reqId || !u2f.callbackMap_[reqId]) {
|
||||
console.error('Unknown or missing requestId in response.');
|
||||
return;
|
||||
}
|
||||
var cb = u2f.callbackMap_[reqId];
|
||||
delete u2f.callbackMap_[reqId];
|
||||
cb(response['responseData']);
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches an array of sign requests to available U2F tokens.
|
||||
* If the JS API version supported by the extension is unknown, it first sends a
|
||||
* message to the extension to find out the supported API version and then it sends
|
||||
* the sign request.
|
||||
* @param {string=} appId
|
||||
* @param {string=} challenge
|
||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
||||
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
||||
if (js_api_version === undefined) {
|
||||
// Send a message to get the extension to JS API version, then send the actual sign request.
|
||||
u2f.getApiVersion(
|
||||
function (response) {
|
||||
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
|
||||
console.log("Extension JS API Version: ", js_api_version);
|
||||
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
||||
});
|
||||
} else {
|
||||
// We know the JS API version. Send the actual sign request in the supported API version.
|
||||
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches an array of sign requests to available U2F tokens.
|
||||
* @param {string=} appId
|
||||
* @param {string=} challenge
|
||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
||||
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
||||
u2f.getPortSingleton_(function(port) {
|
||||
var reqId = ++u2f.reqCounter_;
|
||||
u2f.callbackMap_[reqId] = callback;
|
||||
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
||||
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
|
||||
port.postMessage(req);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches register requests to available U2F tokens. An array of sign
|
||||
* requests identifies already registered tokens.
|
||||
* If the JS API version supported by the extension is unknown, it first sends a
|
||||
* message to the extension to find out the supported API version and then it sends
|
||||
* the register request.
|
||||
* @param {string=} appId
|
||||
* @param {Array<u2f.RegisterRequest>} registerRequests
|
||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
||||
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
||||
if (js_api_version === undefined) {
|
||||
// Send a message to get the extension to JS API version, then send the actual register request.
|
||||
u2f.getApiVersion(
|
||||
function (response) {
|
||||
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
|
||||
console.log("Extension JS API Version: ", js_api_version);
|
||||
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
||||
callback, opt_timeoutSeconds);
|
||||
});
|
||||
} else {
|
||||
// We know the JS API version. Send the actual register request in the supported API version.
|
||||
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
||||
callback, opt_timeoutSeconds);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches register requests to available U2F tokens. An array of sign
|
||||
* requests identifies already registered tokens.
|
||||
* @param {string=} appId
|
||||
* @param {Array<u2f.RegisterRequest>} registerRequests
|
||||
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
||||
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
||||
u2f.getPortSingleton_(function(port) {
|
||||
var reqId = ++u2f.reqCounter_;
|
||||
u2f.callbackMap_[reqId] = callback;
|
||||
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
||||
var req = u2f.formatRegisterRequest_(
|
||||
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
|
||||
port.postMessage(req);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Dispatches a message to the extension to find out the supported
|
||||
* JS API version.
|
||||
* If the user is on a mobile phone and is thus using Google Authenticator instead
|
||||
* of the Chrome extension, don't send the request and simply return 0.
|
||||
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
|
||||
* @param {number=} opt_timeoutSeconds
|
||||
*/
|
||||
u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
|
||||
u2f.getPortSingleton_(function(port) {
|
||||
// If we are using Android Google Authenticator or iOS client app,
|
||||
// do not fire an intent to ask which JS API version to use.
|
||||
if (port.getPortType) {
|
||||
var apiVersion;
|
||||
switch (port.getPortType()) {
|
||||
case 'WrappedIosPort_':
|
||||
case 'WrappedAuthenticatorPort_':
|
||||
apiVersion = 1.1;
|
||||
break;
|
||||
|
||||
default:
|
||||
apiVersion = 0;
|
||||
break;
|
||||
}
|
||||
callback({ 'js_api_version': apiVersion });
|
||||
return;
|
||||
}
|
||||
var reqId = ++u2f.reqCounter_;
|
||||
u2f.callbackMap_[reqId] = callback;
|
||||
var req = {
|
||||
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
|
||||
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
|
||||
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
|
||||
requestId: reqId
|
||||
};
|
||||
port.postMessage(req);
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user