use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
use openssl::bn::BigNumContext;
use openssl::ec::{EcGroup, EcKey};
use openssl::ecdsa::EcdsaSig;
use openssl::nid::Nid;
use openssl::pkey::Private;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
pub(crate) const ORIGIN: &str = "http://control.test";
pub(crate) struct SoftPasskey {
key: EcKey<Private>,
pub cred_id: Vec<u8>,
counter: u32,
pub user_verified: bool,
}
impl SoftPasskey {
pub fn new() -> Self {
let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
Self {
key: EcKey::generate(&group).unwrap(),
cred_id: rand::random::<[u8; 16]>().to_vec(),
counter: 0,
user_verified: true,
}
}
pub fn id(&self) -> String {
B64.encode(&self.cred_id)
}
fn flags(&self, attested: bool) -> u8 {
let mut f = 0x01; if self.user_verified {
f |= 0x04; }
if attested {
f |= 0x40; }
f
}
fn cose_key(&self) -> Vec<u8> {
let group = self.key.group();
let mut ctx = BigNumContext::new().unwrap();
let mut x = openssl::bn::BigNum::new().unwrap();
let mut y = openssl::bn::BigNum::new().unwrap();
self.key
.public_key()
.affine_coordinates(group, &mut x, &mut y, &mut ctx)
.unwrap();
let pad = |b: openssl::bn::BigNum| b.to_vec_padded(32).unwrap();
cbor::map(&[
(cbor::int(1), cbor::int(2)), (cbor::int(3), cbor::int(-7)), (cbor::int(-1), cbor::int(1)), (cbor::int(-2), cbor::bytes(&pad(x))),
(cbor::int(-3), cbor::bytes(&pad(y))),
])
}
fn client_data(kind: &str, challenge: &str) -> Vec<u8> {
serde_json::to_vec(&json!({
"type": kind,
"challenge": challenge,
"origin": ORIGIN,
"crossOrigin": false,
}))
.unwrap()
}
pub fn attest(&mut self, options: &Value) -> Value {
let challenge = options["challenge"].as_str().expect("a challenge");
let rp_id = options["rp"]["id"].as_str().expect("an rp id");
let client_data = Self::client_data("webauthn.create", challenge);
self.counter += 1;
let mut auth_data = Sha256::digest(rp_id.as_bytes()).to_vec();
auth_data.push(self.flags(true));
auth_data.extend_from_slice(&self.counter.to_be_bytes());
auth_data.extend_from_slice(&[0u8; 16]); auth_data.extend_from_slice(&(self.cred_id.len() as u16).to_be_bytes());
auth_data.extend_from_slice(&self.cred_id);
auth_data.extend_from_slice(&self.cose_key());
let attestation_object = cbor::map(&[
(cbor::text("fmt"), cbor::text("none")),
(cbor::text("attStmt"), cbor::map(&[])),
(cbor::text("authData"), cbor::bytes(&auth_data)),
]);
json!({
"id": self.id(),
"rawId": self.id(),
"type": "public-key",
"response": {
"attestationObject": B64.encode(attestation_object),
"clientDataJSON": B64.encode(client_data),
},
"clientExtensionResults": {},
})
}
pub fn assert(&mut self, options: &Value) -> Value {
let challenge = options["challenge"].as_str().expect("a challenge");
let rp_id = options["rpId"].as_str().unwrap_or("control.test");
let client_data = Self::client_data("webauthn.get", challenge);
self.counter += 1;
let mut auth_data = Sha256::digest(rp_id.as_bytes()).to_vec();
auth_data.push(self.flags(false));
auth_data.extend_from_slice(&self.counter.to_be_bytes());
let mut signed = auth_data.clone();
signed.extend_from_slice(&Sha256::digest(&client_data));
let sig = EcdsaSig::sign(&Sha256::digest(&signed), &self.key)
.unwrap()
.to_der()
.unwrap();
json!({
"id": self.id(),
"rawId": self.id(),
"type": "public-key",
"response": {
"authenticatorData": B64.encode(auth_data),
"clientDataJSON": B64.encode(client_data),
"signature": B64.encode(sig),
},
"clientExtensionResults": {},
})
}
}
mod cbor {
fn head(major: u8, n: u64) -> Vec<u8> {
let m = major << 5;
match n {
0..=23 => vec![m | n as u8],
24..=0xff => vec![m | 24, n as u8],
0x100..=0xffff => {
let mut v = vec![m | 25];
v.extend_from_slice(&(n as u16).to_be_bytes());
v
}
_ => {
let mut v = vec![m | 26];
v.extend_from_slice(&(n as u32).to_be_bytes());
v
}
}
}
pub fn int(i: i64) -> Vec<u8> {
if i >= 0 {
head(0, i as u64)
} else {
head(1, (-1 - i) as u64)
}
}
pub fn bytes(b: &[u8]) -> Vec<u8> {
let mut v = head(2, b.len() as u64);
v.extend_from_slice(b);
v
}
pub fn text(s: &str) -> Vec<u8> {
let mut v = head(3, s.len() as u64);
v.extend_from_slice(s.as_bytes());
v
}
pub fn map(entries: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
let mut v = head(5, entries.len() as u64);
for (k, val) in entries {
v.extend_from_slice(k);
v.extend_from_slice(val);
}
v
}
}