Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Windows 10 WebAuthn API #204

Merged
merged 2 commits into from
Oct 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ jobs:
os:
- ubuntu-latest
- windows-latest
name: WebAuthn Rust Frameworks (Build and Test)
include:
- os: windows-latest
features: --features win10
rust_version: stable
- os: windows-latest
features: --features nfc,usb,win10
rust_version: stable
exclude:
- os: windows-latest
rust_version: 1.45.0

name: Build and Test
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
Expand Down
8 changes: 7 additions & 1 deletion designs/authenticator-library.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Authenticator library

* **Published:** 2022-09-24
* **Last updated:** 2022-10-07

This describes the state of the `webauthn-authenticator-rs` library, and some
potential longer term improvements.

## Current state
## Current state (Sep 2022)

At present, there are two disjoint traits provided by the `webauthn-authenticator-rs` library:

Expand Down Expand Up @@ -94,6 +97,9 @@ This would provide access to NFC and USB-HID tokens through our own library, and

### Implement an `AuthenticatorBackend` for platform-specific WebAuthn APIs

- [ ] macOS Passkey API
- [x] Windows 10 WebAuthn API (added Oct 2022)

This will require `webauthn-authenticator-rs` to carry some platform-specific code.

This is _immediately_ necessary on Windows 10, and would unlock access to platform authenticators.
2 changes: 2 additions & 0 deletions webauthn-authenticator-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ repository = "https://github.com/kanidm/webauthn-rs"
u2fhid = ["authenticator"]
nfc = ["pcsc"]
usb = ["hidapi"]
win10 = ["windows"]

default = []

Expand All @@ -35,6 +36,7 @@ authenticator = { version = "0.3.2-dev.1", optional = true, default-features = f

pcsc = { version = "2", optional = true }
hidapi = { version = "1.4.2", optional = true }
windows = { version = "0.41.0", optional = true, features = ["Win32_Graphics_Gdi", "Win32_Networking_WindowsWebServices", "Win32_Foundation", "Win32_UI_WindowsAndMessaging", "Win32_System_LibraryLoader", "Win32_Graphics_Dwm" ] }
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
Expand Down
124 changes: 124 additions & 0 deletions webauthn-authenticator-rs/examples/authenticate/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#[macro_use]
extern crate tracing;

use std::io::{stdin, stdout, Write};

use webauthn_authenticator_rs::prelude::Url;
use webauthn_authenticator_rs::softtoken::SoftToken;
use webauthn_authenticator_rs::AuthenticatorBackend;
use webauthn_rs_core::proto::RequestAuthenticationExtensions;
use webauthn_rs_core::WebauthnCore as Webauthn;

fn select_provider() -> Box<dyn AuthenticatorBackend> {
let mut providers: Vec<(&str, fn() -> Box<dyn AuthenticatorBackend>)> = Vec::new();

providers.push(("SoftToken", || Box::new(SoftToken::new().unwrap().0)));

#[cfg(feature = "u2fhid")]
providers.push(("Mozilla", || {
Box::new(webauthn_authenticator_rs::u2fhid::U2FHid::default())
}));

#[cfg(feature = "win10")]
providers.push(("Windows 10", || {
Box::new(webauthn_authenticator_rs::win10::Win10::default())
}));

if providers.is_empty() {
panic!("oops, no providers available in this build!");
}

loop {
println!("Select a provider:");
for (i, (name, _)) in providers.iter().enumerate() {
println!("({}): {}", i + 1, name);
}

let mut buf = String::new();
print!("? ");
stdout().flush().ok();
stdin().read_line(&mut buf).expect("Cannot read stdin");
let selected: Result<u64, _> = buf.trim().parse();
match selected {
Ok(v) => {
if v < 1 || (v as usize) > providers.len() {
println!("Input out of range: {}", v);
} else {
let p = providers.remove((v as usize) - 1);
println!("Using {}...", p.0);
return p.1();
}
}
Err(_) => println!("Input was not a number"),
}
println!();
}
}

fn main() {
tracing_subscriber::fmt::init();

let mut u = select_provider();

// WARNING: don't use this as an example of how to use the library!
let wan = Webauthn::new_unsafe_experts_only(
"https://localhost:8080/auth",
"localhost",
vec![url::Url::parse("https://localhost:8080").unwrap()],
Some(1),
None,
None,
);

let unique_id = [
158, 170, 228, 89, 68, 28, 73, 194, 134, 19, 227, 153, 107, 220, 150, 238,
];
let name = "william";

let (chal, reg_state) = wan
.generate_challenge_register(&unique_id, name, name, false)
.unwrap();

info!("🍿 challenge -> {:x?}", chal);

let r = u
.perform_register(
Url::parse("https://localhost:8080").unwrap(),
chal.public_key,
60_000,
)
.unwrap();

let cred = wan.register_credential(&r, &reg_state, None).unwrap();

trace!(?cred);

let (chal, auth_state) = wan
.generate_challenge_authenticate(
vec![cred],
Some(RequestAuthenticationExtensions {
appid: Some("example.app.id".to_string()),
uvm: None,
}),
)
.unwrap();

let r = u
.perform_auth(
Url::parse("https://localhost:8080").unwrap(),
chal.public_key,
60_000,
)
.map_err(|e| {
error!("Error -> {:x?}", e);
e
})
.expect("Failed to auth");
trace!(?r);

let auth_res = wan
.authenticate_credential(&r, &auth_state)
.expect("webauth authentication denied");

info!("auth_res -> {:x?}", auth_res);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ mod core;

fn main() {
tracing_subscriber::fmt::init();

core::event_loop();
}
3 changes: 3 additions & 0 deletions webauthn-authenticator-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub mod usb;
#[cfg(feature = "u2fhid")]
pub mod u2fhid;

#[cfg(feature = "win10")]
pub mod win10;

pub struct WebauthnAuthenticator<T>
where
T: AuthenticatorBackend,
Expand Down
88 changes: 88 additions & 0 deletions webauthn-authenticator-rs/src/win10/clientdata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! Wrappers for [CollectedClientData].
use base64urlsafedata::Base64UrlSafeData;
use std::collections::BTreeMap;
use std::pin::Pin;
use webauthn_rs_proto::CollectedClientData;

use super::WinWrapper;
use crate::{error::WebauthnCError, Url};

use windows::{
core::HSTRING,
w,
Win32::Networking::WindowsWebServices::{
WEBAUTHN_CLIENT_DATA, WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
},
};
// Most constants are `&str`, but APIs expect `HSTRING`... there's no good work-around.
// https://github.com/microsoft/windows-rs/issues/2049
/// [windows::Win32::Networking::WindowsWebServices::WEBAUTHN_HASH_ALGORITHM_SHA_256]
const SHA_256: &HSTRING = w!("SHA-256");

/// Wrapper for [WEBAUTHN_CLIENT_DATA] to ensure pointer lifetime.
pub struct WinClientData {
native: WEBAUTHN_CLIENT_DATA,
client_data_json: String,
}

impl WinClientData {
pub fn client_data_json(&self) -> &String {
&self.client_data_json
}
}

impl WinWrapper<CollectedClientData> for WinClientData {
type NativeType = WEBAUTHN_CLIENT_DATA;
fn new(clientdata: &CollectedClientData) -> Result<Pin<Box<Self>>, WebauthnCError> {
// Construct an incomplete type first, so that all the pointers are fixed.
let res = Self {
native: WEBAUTHN_CLIENT_DATA::default(),
client_data_json: serde_json::to_string(clientdata)
.map_err(|_| WebauthnCError::Json)?,
};

let mut boxed = Box::pin(res);

// Create the real native type, which contains bare pointers.
let native = WEBAUTHN_CLIENT_DATA {
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
cbClientDataJSON: boxed.client_data_json.len() as u32,
pbClientDataJSON: boxed.client_data_json.as_ptr() as *mut _,
pwszHashAlgId: SHA_256.into(),
};

// Update the boxed type with the proper native object.
unsafe {
let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).native = native;
}

Ok(boxed)
}

fn native_ptr(&self) -> &WEBAUTHN_CLIENT_DATA {
&self.native
}
}

pub fn creation_to_clientdata(origin: Url, challenge: Base64UrlSafeData) -> CollectedClientData {
CollectedClientData {
type_: "webauthn.create".to_string(),
challenge,
origin,
token_binding: None,
cross_origin: None,
unknown_keys: BTreeMap::new(),
}
}

pub fn get_to_clientdata(origin: Url, challenge: Base64UrlSafeData) -> CollectedClientData {
CollectedClientData {
type_: "webauthn.get".to_string(),
challenge,
origin,
token_binding: None,
cross_origin: None,
unknown_keys: BTreeMap::new(),
}
}
108 changes: 108 additions & 0 deletions webauthn-authenticator-rs/src/win10/cose.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Wrappers for [PubKeyCredParams].
use crate::prelude::WebauthnCError;
use std::pin::Pin;
use webauthn_rs_proto::PubKeyCredParams;

use super::WinWrapper;

use windows::{
core::HSTRING,
Win32::Networking::WindowsWebServices::{
WEBAUTHN_COSE_CREDENTIAL_PARAMETER, WEBAUTHN_COSE_CREDENTIAL_PARAMETERS,
WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION,
},
};

/// Wrapper for [WEBAUTHN_COSE_CREDENTIAL_PARAMETER] to ensure pointer lifetime.
struct WinCoseCredentialParameter {
native: WEBAUTHN_COSE_CREDENTIAL_PARAMETER,
_typ: HSTRING,
}

impl WinCoseCredentialParameter {
fn from(p: &PubKeyCredParams) -> Pin<Box<Self>> {
let res = Self {
native: Default::default(),
_typ: p.type_.clone().into(),
};

let mut boxed = Box::pin(res);

let native = WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
dwVersion: WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION,
pwszCredentialType: (&boxed._typ).into(),
lAlg: p.alg as i32,
};

unsafe {
let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).native = native;
}

boxed
}
}

pub struct WinCoseCredentialParameters {
native: WEBAUTHN_COSE_CREDENTIAL_PARAMETERS,
_params: Vec<Pin<Box<WinCoseCredentialParameter>>>,
_l: Vec<WEBAUTHN_COSE_CREDENTIAL_PARAMETER>,
}

impl WinWrapper<Vec<PubKeyCredParams>> for WinCoseCredentialParameters {
type NativeType = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS;

fn new(params: &Vec<PubKeyCredParams>) -> Result<Pin<Box<Self>>, WebauthnCError> {
let params: Vec<Pin<Box<WinCoseCredentialParameter>>> = params
.iter()
.map(WinCoseCredentialParameter::from)
.collect();
Ok(WinCoseCredentialParameters::from_wrapped(params))
}

fn native_ptr(&self) -> &WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
&self.native
}
}

impl WinCoseCredentialParameters {
fn from_wrapped(params: Vec<Pin<Box<WinCoseCredentialParameter>>>) -> Pin<Box<Self>> {
let len = params.len();
let res = Self {
native: Default::default(),
_l: Vec::with_capacity(len),
_params: params,
};

// Box and pin the struct so it's on the heap and doesn't move.
let mut boxed = Box::pin(res);

// Put in all the "native" values
let p_ptr = boxed._params.as_ptr();
unsafe {
let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
let l = &mut Pin::get_unchecked_mut(mut_ref)._l;
let l_ptr = l.as_mut_ptr();
for i in 0..len {
*l_ptr.add(i) = (*p_ptr.add(i)).native;
}

l.set_len(len);
}

// let mut l: Vec<WEBAUTHN_COSE_CREDENTIAL_PARAMETER> =
// params.iter().map(|p| p.native).collect();

let native = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
cCredentialParameters: boxed._l.len() as u32,
pCredentialParameters: boxed._l.as_mut_ptr() as *mut _,
};

unsafe {
let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).native = native;
}

boxed
}
}
Loading