#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
use ciborium::Value as CborValue;
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows::core::PCWSTR;
use windows::Win32::Foundation::HWND;
use windows::Win32::Networking::WindowsWebServices::*;
use windows::Win32::System::Console::GetConsoleWindow;
use windows::Win32::UI::WindowsAndMessaging::{
GetDesktopWindow, GetForegroundWindow, IsWindowVisible,
};
use super::{Result, WebAuthnAssertion, WebAuthnCredential, WebAuthnError};
#[derive(Debug, Clone)]
pub struct MakeCredentialParams<'params> {
pub rp_id: &'params str,
pub rp_name: &'params str,
pub user_id: &'params [u8],
pub user_name: &'params str,
pub user_display_name: &'params str,
pub timeout_ms: u32,
pub hwnd: Option<isize>,
}
#[derive(Debug, Clone)]
pub struct GetAssertionParams<'params> {
pub rp_id: &'params str,
pub credential_id: &'params [u8],
pub client_data: &'params [u8],
pub timeout_ms: u32,
pub hwnd: Option<isize>,
}
pub fn delete_platform_credential(credential_id: &[u8]) -> Result<()> {
#[allow(unsafe_code)]
unsafe {
WebAuthNDeletePlatformCredential(credential_id).map_err(map_webauthn_error)
}
}
pub fn is_platform_authenticator_available() -> bool {
#[allow(unsafe_code)]
unsafe {
match WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable() {
Ok(b) => b.as_bool(),
Err(_) => false,
}
}
}
pub fn make_credential(params: MakeCredentialParams<'_>) -> Result<WebAuthnCredential> {
let rp_id_w = to_wide(params.rp_id);
let rp_name_w = to_wide(params.rp_name);
let user_name_w = to_wide(params.user_name);
let user_display_w = to_wide(params.user_display_name);
let mut user_id_buf: Vec<u8> = params.user_id.to_vec();
let mut client_data_json = canonical_make_client_data();
#[allow(unsafe_code)]
let result = unsafe {
let hwnd = pick_hwnd(params.hwnd);
let rp = WEBAUTHN_RP_ENTITY_INFORMATION {
dwVersion: WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION,
pwszId: PCWSTR(rp_id_w.as_ptr()),
pwszName: PCWSTR(rp_name_w.as_ptr()),
pwszIcon: PCWSTR::null(),
};
let user = WEBAUTHN_USER_ENTITY_INFORMATION {
dwVersion: WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION,
cbId: u32::try_from(user_id_buf.len())
.map_err(|_| WebAuthnError::InvalidResponse("user_id too long".into()))?,
pbId: user_id_buf.as_mut_ptr(),
pwszName: PCWSTR(user_name_w.as_ptr()),
pwszIcon: PCWSTR::null(),
pwszDisplayName: PCWSTR(user_display_w.as_ptr()),
};
let mut cose_param = WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
dwVersion: 1,
pwszCredentialType: WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY,
lAlg: WEBAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256,
};
let cose_params = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
cCredentialParameters: 1,
pCredentialParameters: &mut cose_param,
};
let client_data = WEBAUTHN_CLIENT_DATA {
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
cbClientDataJSON: u32::try_from(client_data_json.len())
.map_err(|_| WebAuthnError::InvalidResponse("client_data_json too long".into()))?,
pbClientDataJSON: client_data_json.as_mut_ptr(),
pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
};
let opts = WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS {
dwVersion: WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_1,
dwTimeoutMilliseconds: params.timeout_ms,
dwAuthenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM,
dwUserVerificationRequirement: WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
dwAttestationConveyancePreference: WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
bRequireResidentKey: false.into(),
bPreferResidentKey: false.into(),
..Default::default()
};
let opts_ptr: *const WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS = &opts;
WebAuthNAuthenticatorMakeCredential(
hwnd,
&rp,
&user,
&cose_params,
&client_data,
Some(opts_ptr),
)
};
let attestation_ptr = result.map_err(map_webauthn_error)?;
#[allow(unsafe_code)]
let credential = unsafe {
let att = &*attestation_ptr;
let credential_id = slice_from_raw(att.pbCredentialId, att.cbCredentialId).to_vec();
let authenticator_data =
slice_from_raw(att.pbAuthenticatorData, att.cbAuthenticatorData).to_vec();
let resident = att.bResidentKey.as_bool();
WebAuthNFreeCredentialAttestation(Some(attestation_ptr));
let (x, y) = parse_pubkey_from_authenticator_data(&authenticator_data)?;
WebAuthnCredential {
credential_id,
public_key_x: x,
public_key_y: y,
authenticator_data,
resident,
}
};
Ok(credential)
}
pub fn get_assertion(params: GetAssertionParams<'_>) -> Result<WebAuthnAssertion> {
let rp_id_w = to_wide(params.rp_id);
let mut credential_id_buf: Vec<u8> = params.credential_id.to_vec();
let mut client_data_buf: Vec<u8> = params.client_data.to_vec();
#[allow(unsafe_code)]
let result = unsafe {
let hwnd = pick_hwnd(params.hwnd);
let mut allow_cred = WEBAUTHN_CREDENTIAL {
dwVersion: 1,
cbId: u32::try_from(credential_id_buf.len())
.map_err(|_| WebAuthnError::InvalidResponse("credential_id too long".into()))?,
pbId: credential_id_buf.as_mut_ptr(),
pwszCredentialType: WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY,
};
let allow_list = WEBAUTHN_CREDENTIALS {
cCredentials: 1,
pCredentials: &mut allow_cred,
};
let client_data = WEBAUTHN_CLIENT_DATA {
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
cbClientDataJSON: u32::try_from(client_data_buf.len())
.map_err(|_| WebAuthnError::InvalidResponse("client_data too long".into()))?,
pbClientDataJSON: client_data_buf.as_mut_ptr(),
pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
};
let opts = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS {
dwVersion: 1,
dwTimeoutMilliseconds: params.timeout_ms,
CredentialList: allow_list,
dwAuthenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM,
dwUserVerificationRequirement: WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED,
pAllowCredentialList: std::ptr::null_mut(),
..Default::default()
};
let opts_ptr: *const WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS = &opts;
WebAuthNAuthenticatorGetAssertion(
hwnd,
PCWSTR(rp_id_w.as_ptr()),
&client_data,
Some(opts_ptr),
)
};
let assertion_ptr = result.map_err(map_webauthn_error)?;
#[allow(unsafe_code)]
let assertion = unsafe {
let asn = &*assertion_ptr;
let authenticator_data =
slice_from_raw(asn.pbAuthenticatorData, asn.cbAuthenticatorData).to_vec();
let signature_der = slice_from_raw(asn.pbSignature, asn.cbSignature).to_vec();
WebAuthNFreeAssertion(assertion_ptr);
if authenticator_data.len() < 37 {
return Err(WebAuthnError::InvalidResponse(format!(
"authenticator_data too short: {} bytes",
authenticator_data.len()
)));
}
let flags = authenticator_data[32];
let counter = u32::from_be_bytes([
authenticator_data[33],
authenticator_data[34],
authenticator_data[35],
authenticator_data[36],
]);
WebAuthnAssertion {
signature_der,
authenticator_data,
flags,
counter,
}
};
Ok(assertion)
}
fn to_wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
#[allow(unsafe_code)]
unsafe fn pick_hwnd(provided: Option<isize>) -> HWND {
let candidates: [HWND; 4] = [
provided
.map(|raw| HWND(raw as *mut _))
.unwrap_or(HWND(std::ptr::null_mut())),
GetConsoleWindow(),
GetForegroundWindow(),
GetDesktopWindow(),
];
for candidate in candidates {
if !candidate.0.is_null() && IsWindowVisible(candidate).as_bool() {
return candidate;
}
}
GetDesktopWindow()
}
#[allow(unsafe_code)]
unsafe fn slice_from_raw<'params>(ptr: *const u8, len: u32) -> &'params [u8] {
if ptr.is_null() || len == 0 {
return &[];
}
std::slice::from_raw_parts(ptr, len as usize)
}
fn map_webauthn_error(e: windows::core::Error) -> WebAuthnError {
let hr = e.code();
if hr.0 as u32 == 0x80090028 {
return WebAuthnError::UserCanceled;
}
if hr.0 as u32 == 0x800704C7 {
return WebAuthnError::UserCanceled;
}
if hr.0 as u32 == 0x80004004 {
return WebAuthnError::UserCanceled;
}
if hr.0 as u32 == 0x800705B4 {
return WebAuthnError::Timeout;
}
let name = lookup_error_name(hr);
WebAuthnError::Backend {
hr: hr.0 as u32,
name,
}
}
#[allow(unsafe_code)]
fn lookup_error_name(hr: windows::core::HRESULT) -> String {
unsafe {
let pw = WebAuthNGetErrorName(hr);
if pw.0.is_null() {
return String::from("(unnamed)");
}
let mut len = 0_usize;
while *pw.0.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(pw.0, len);
String::from_utf16_lossy(slice)
}
}
fn canonical_make_client_data() -> Vec<u8> {
br#"{"type":"webauthn.create","challenge":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","origin":"sshenc:keygen"}"#.to_vec()
}
fn parse_pubkey_from_authenticator_data(authenticator_data: &[u8]) -> Result<([u8; 32], [u8; 32])> {
if authenticator_data.len() < 37 {
return Err(WebAuthnError::InvalidResponse(format!(
"authenticator_data too short for header: {} bytes",
authenticator_data.len()
)));
}
let flags = authenticator_data[32];
if flags & 0x40 == 0 {
return Err(WebAuthnError::InvalidResponse(
"AT flag not set in authenticator_data; cannot extract pubkey".into(),
));
}
let attested_start = 37;
if authenticator_data.len() < attested_start + 18 {
return Err(WebAuthnError::InvalidResponse(
"authenticator_data too short for attested credential header".into(),
));
}
let cred_len_off = attested_start + 16;
let cred_id_len = u16::from_be_bytes([
authenticator_data[cred_len_off],
authenticator_data[cred_len_off + 1],
]) as usize;
let cose_start = cred_len_off + 2 + cred_id_len;
if authenticator_data.len() < cose_start {
return Err(WebAuthnError::InvalidResponse(
"authenticator_data too short for COSE_Key blob".into(),
));
}
let cose_bytes = &authenticator_data[cose_start..];
let cose_value: CborValue = ciborium::from_reader(cose_bytes)
.map_err(|e| WebAuthnError::InvalidResponse(format!("COSE CBOR parse failed: {e}")))?;
let map = match cose_value {
CborValue::Map(m) => m,
_ => {
return Err(WebAuthnError::InvalidResponse(
"COSE_Key value is not a CBOR map".into(),
))
}
};
let mut x: Option<[u8; 32]> = None;
let mut y: Option<[u8; 32]> = None;
for (k, v) in map.iter() {
let k_int = match k {
CborValue::Integer(i) => i128::from(*i),
_ => continue,
};
let v_bytes = match v {
CborValue::Bytes(b) => b.as_slice(),
_ => continue,
};
if k_int == -2 && v_bytes.len() == 32 {
let mut buf = [0_u8; 32];
buf.copy_from_slice(v_bytes);
x = Some(buf);
} else if k_int == -3 && v_bytes.len() == 32 {
let mut buf = [0_u8; 32];
buf.copy_from_slice(v_bytes);
y = Some(buf);
}
}
match (x, y) {
(Some(x), Some(y)) => Ok((x, y)),
_ => Err(WebAuthnError::InvalidResponse(
"COSE_Key missing -2/-3 (x/y) byte strings".into(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_authenticator_data(x: &[u8; 32], y: &[u8; 32]) -> Vec<u8> {
let mut cose_bytes = Vec::new();
let cose = CborValue::Map(vec![
(CborValue::Integer(1.into()), CborValue::Integer(2.into())), (
CborValue::Integer(3.into()),
CborValue::Integer((-7_i32).into()),
), (
CborValue::Integer((-1_i32).into()),
CborValue::Integer(1.into()),
), (
CborValue::Integer((-2_i32).into()),
CborValue::Bytes(x.to_vec()),
),
(
CborValue::Integer((-3_i32).into()),
CborValue::Bytes(y.to_vec()),
),
]);
ciborium::into_writer(&cose, &mut cose_bytes).expect("cbor encode");
let mut ad = Vec::new();
ad.extend_from_slice(&[0_u8; 32]); ad.push(0x45); ad.extend_from_slice(&0_u32.to_be_bytes()); ad.extend_from_slice(&[0_u8; 16]); ad.extend_from_slice(&16_u16.to_be_bytes()); ad.extend_from_slice(&[0_u8; 16]); ad.extend_from_slice(&cose_bytes);
ad
}
#[test]
fn parses_ecdsa_p256_pubkey() {
let mut x = [0_u8; 32];
let mut y = [0_u8; 32];
for i in 0..32 {
x[i] = i as u8;
y[i] = (i + 32) as u8;
}
let ad = synth_authenticator_data(&x, &y);
let (got_x, got_y) = parse_pubkey_from_authenticator_data(&ad).expect("parse ok");
assert_eq!(got_x, x);
assert_eq!(got_y, y);
}
#[test]
fn rejects_truncated_authenticator_data() {
let short = vec![0_u8; 36];
assert!(parse_pubkey_from_authenticator_data(&short).is_err());
}
#[test]
fn rejects_missing_at_flag() {
let mut ad = vec![0_u8; 64];
ad[32] = 0x05; assert!(parse_pubkey_from_authenticator_data(&ad).is_err());
}
}