use base64::{
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
Engine,
};
use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
use p256::elliptic_curve::sec1::FromEncodedPoint;
use p256::{EncodedPoint, PublicKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::beta::constant_time_eq;
use crate::primitives::{Authenticator, AuthenticatorContext, EnrolledCredential};
use crate::{Error, Result};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebAuthnAssertion {
#[serde(default, rename = "credentialId", alias = "credential_id")]
pub credential_id: Option<String>,
#[serde(rename = "authenticatorData", alias = "authenticator_data")]
pub authenticator_data: String,
#[serde(rename = "clientDataJSON", alias = "client_data_json")]
pub client_data_json: String,
pub signature: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebAuthnEnrollment {
pub credential_id: String,
pub public_key_x: String,
pub public_key_y: String,
#[serde(default, alias = "deviceName", rename = "device_name")]
pub device_name: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebAuthnPublicKey {
pub x: String,
pub y: String,
#[serde(default)]
pub device_name: String,
}
pub struct WebAuthn;
impl Authenticator for WebAuthn {
type Enrollment = WebAuthnEnrollment;
type Assertion = WebAuthnAssertion;
type PublicKey = WebAuthnPublicKey;
type Context = AuthenticatorContext;
fn verify_enrollment(
enrollment: &Self::Enrollment,
_context: &Self::Context,
) -> Result<EnrolledCredential<Self::PublicKey>> {
let cid = STANDARD
.decode(&enrollment.credential_id)
.map_err(|_| Error::Malformed("WebAuthn enrollment: credential_id not base64"))?;
let x = STANDARD
.decode(&enrollment.public_key_x)
.map_err(|_| Error::Malformed("WebAuthn enrollment: x not base64"))?;
let y = STANDARD
.decode(&enrollment.public_key_y)
.map_err(|_| Error::Malformed("WebAuthn enrollment: y not base64"))?;
let _ = public_key_from_xy(&x, &y)?;
Ok(EnrolledCredential {
credential_id: cid,
public_key: WebAuthnPublicKey {
x: enrollment.public_key_x.clone(),
y: enrollment.public_key_y.clone(),
device_name: enrollment.device_name.clone(),
},
})
}
fn verify_assertion(
public_key: &Self::PublicKey,
beta: &[u8; 32],
assertion: &Self::Assertion,
context: &Self::Context,
) -> Result<()> {
let auth_data = STANDARD
.decode(&assertion.authenticator_data)
.map_err(|_| Error::AuthorizationInvalid)?;
let client_data_json = STANDARD
.decode(&assertion.client_data_json)
.map_err(|_| Error::AuthorizationInvalid)?;
let sig_der = STANDARD
.decode(&assertion.signature)
.map_err(|_| Error::AuthorizationInvalid)?;
let client_obj: serde_json::Value =
serde_json::from_slice(&client_data_json).map_err(|_| Error::AuthorizationInvalid)?;
let cd_type = client_obj
.get("type")
.and_then(|v| v.as_str())
.ok_or(Error::AuthorizationInvalid)?;
if cd_type != "webauthn.get" {
return Err(Error::AuthorizationInvalid);
}
let origin = client_obj
.get("origin")
.and_then(|v| v.as_str())
.ok_or(Error::AuthorizationInvalid)?;
if !constant_time_eq(origin.as_bytes(), context.origin.as_bytes()) {
return Err(Error::AuthorizationInvalid);
}
let challenge_b64u = client_obj
.get("challenge")
.and_then(|v| v.as_str())
.ok_or(Error::AuthorizationInvalid)?;
let challenge_bytes = URL_SAFE_NO_PAD
.decode(challenge_b64u)
.map_err(|_| Error::AuthorizationInvalid)?;
if challenge_bytes.len() != 32 || !constant_time_eq(&challenge_bytes, beta) {
return Err(Error::AuthorizationInvalid);
}
if auth_data.len() < 37 {
return Err(Error::AuthorizationInvalid);
}
let expected_rp_id_hash = Sha256::digest(context.rp_id.as_bytes());
if !constant_time_eq(&auth_data[..32], expected_rp_id_hash.as_slice()) {
return Err(Error::AuthorizationInvalid);
}
let flags = auth_data[32];
if flags & 0x01 == 0 {
return Err(Error::AuthorizationInvalid);
}
if context.require_uv && flags & 0x04 == 0 {
return Err(Error::AuthorizationInvalid);
}
let client_data_hash = Sha256::digest(&client_data_json);
let mut signed = Vec::with_capacity(auth_data.len() + 32);
signed.extend_from_slice(&auth_data);
signed.extend_from_slice(&client_data_hash);
let x_bytes = STANDARD
.decode(&public_key.x)
.map_err(|_| Error::AuthorizationInvalid)?;
let y_bytes = STANDARD
.decode(&public_key.y)
.map_err(|_| Error::AuthorizationInvalid)?;
let pk = public_key_from_xy(&x_bytes, &y_bytes)?;
let vk = VerifyingKey::from(&pk);
let raw_sig = der_to_raw_rs(&sig_der)?;
let sig =
Signature::try_from(raw_sig.as_slice()).map_err(|_| Error::AuthorizationInvalid)?;
vk.verify(&signed, &sig)
.map_err(|_| Error::AuthorizationInvalid)
}
fn check_credential_binding(credential_id: &[u8], assertion: &Self::Assertion) -> Result<()> {
if let Some(asserted) = assertion.credential_id.as_deref() {
let asserted_bytes = STANDARD
.decode(asserted)
.map_err(|_| Error::AuthorizationInvalid)?;
if !constant_time_eq(&asserted_bytes, credential_id) {
return Err(Error::AuthorizationInvalid);
}
}
Ok(())
}
}
fn public_key_from_xy(x: &[u8], y: &[u8]) -> Result<PublicKey> {
if x.len() != 32 || y.len() != 32 {
return Err(Error::Malformed("WebAuthn: x/y must be 32 bytes"));
}
let mut x_arr = [0u8; 32];
x_arr.copy_from_slice(x);
let mut y_arr = [0u8; 32];
y_arr.copy_from_slice(y);
let point = EncodedPoint::from_affine_coordinates(&x_arr.into(), &y_arr.into(), false);
let pk_opt: Option<PublicKey> = PublicKey::from_encoded_point(&point).into();
pk_opt.ok_or(Error::Malformed("WebAuthn: invalid P-256 point"))
}
fn der_to_raw_rs(der: &[u8]) -> Result<[u8; 64]> {
if der.len() < 8 || der[0] != 0x30 {
return Err(Error::AuthorizationInvalid);
}
let seq_len = der[1] as usize;
if seq_len + 2 > der.len() {
return Err(Error::AuthorizationInvalid);
}
let mut offset = 2usize;
if der[offset] != 0x02 {
return Err(Error::AuthorizationInvalid);
}
let r_len = der[offset + 1] as usize;
if offset + 2 + r_len > der.len() {
return Err(Error::AuthorizationInvalid);
}
let r = &der[offset + 2..offset + 2 + r_len];
offset += 2 + r_len;
if offset >= der.len() || der[offset] != 0x02 {
return Err(Error::AuthorizationInvalid);
}
let s_len = der[offset + 1] as usize;
if offset + 2 + s_len > der.len() {
return Err(Error::AuthorizationInvalid);
}
let s = &der[offset + 2..offset + 2 + s_len];
let mut out = [0u8; 64];
let r_take = r.len().min(32);
let r_src_start = r.len() - r_take;
let r_dst_start = 32 - r_take;
out[r_dst_start..32].copy_from_slice(&r[r_src_start..]);
let s_take = s.len().min(32);
let s_src_start = s.len() - s_take;
let s_dst_start = 32 + (32 - s_take);
out[s_dst_start..64].copy_from_slice(&s[s_src_start..]);
Ok(out)
}