use super::Secp256r1PublicKey;
use super::Secp256r1Signature;
use super::SimpleSignature;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PasskeyAuthenticator {
public_key: Secp256r1PublicKey,
signature: Secp256r1Signature,
challenge: Vec<u8>,
authenticator_data: Vec<u8>,
client_data_json: String,
}
impl PasskeyAuthenticator {
pub fn authenticator_data(&self) -> &[u8] {
&self.authenticator_data
}
pub fn client_data_json(&self) -> &str {
&self.client_data_json
}
pub fn challenge(&self) -> &[u8] {
&self.challenge
}
pub fn signature(&self) -> SimpleSignature {
SimpleSignature::Secp256r1 {
signature: self.signature,
public_key: self.public_key,
}
}
pub fn public_key(&self) -> PasskeyPublicKey {
PasskeyPublicKey::new(self.public_key)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
pub struct PasskeyPublicKey(Secp256r1PublicKey);
impl PasskeyPublicKey {
pub fn new(public_key: Secp256r1PublicKey) -> Self {
Self(public_key)
}
pub fn inner(&self) -> &Secp256r1PublicKey {
&self.0
}
}
#[cfg(feature = "serde")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
mod serialization {
use crate::SignatureScheme;
use crate::SimpleSignature;
use super::*;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use serde_with::Bytes;
use serde_with::DeserializeAs;
use std::borrow::Cow;
#[derive(serde::Serialize)]
struct AuthenticatorRef<'a> {
authenticator_data: &'a Vec<u8>,
client_data_json: &'a String,
signature: SimpleSignature,
}
#[derive(serde::Deserialize)]
#[serde(rename = "PasskeyAuthenticator")]
struct Authenticator {
authenticator_data: Vec<u8>,
client_data_json: String,
signature: SimpleSignature,
}
impl Serialize for PasskeyAuthenticator {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
let authenticator_ref = AuthenticatorRef {
authenticator_data: &self.authenticator_data,
client_data_json: &self.client_data_json,
signature: SimpleSignature::Secp256r1 {
signature: self.signature,
public_key: self.public_key,
},
};
authenticator_ref.serialize(serializer)
} else {
let bytes = self.to_bytes();
serializer.serialize_bytes(&bytes)
}
}
}
impl<'de> Deserialize<'de> for PasskeyAuthenticator {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if deserializer.is_human_readable() {
let authenticator = Authenticator::deserialize(deserializer)?;
Self::try_from_raw(authenticator)
} else {
let bytes: Cow<'de, [u8]> = Bytes::deserialize_as(deserializer)?;
Self::from_serialized_bytes(bytes)
}
}
}
impl PasskeyAuthenticator {
pub fn new(
authenticator_data: Vec<u8>,
client_data_json: String,
signature: SimpleSignature,
) -> Option<Self> {
Self::try_from_raw::<serde_json::Error>(Authenticator {
authenticator_data,
client_data_json,
signature,
})
.ok()
}
fn try_from_raw<E: serde::de::Error>(
Authenticator {
authenticator_data,
client_data_json,
signature,
}: Authenticator,
) -> Result<Self, E> {
let SimpleSignature::Secp256r1 {
signature,
public_key,
} = signature
else {
return Err(serde::de::Error::custom(
"expected passkey with secp256r1 signature",
));
};
let CollectedClientData {
ty: _,
challenge,
origin: _,
} = serde_json::from_str(&client_data_json).map_err(serde::de::Error::custom)?;
let challenge = <base64ct::Base64UrlUnpadded as base64ct::Encoding>::decode_vec(
&challenge,
)
.map_err(|e| {
serde::de::Error::custom(format!(
"unable to decode base64urlunpadded into 3-byte intent and 32-byte digest: {e}"
))
})?;
Ok(Self {
public_key,
signature,
challenge,
authenticator_data,
client_data_json,
})
}
pub(crate) fn from_serialized_bytes<T: AsRef<[u8]>, E: serde::de::Error>(
bytes: T,
) -> Result<Self, E> {
let bytes = bytes.as_ref();
let flag = SignatureScheme::from_byte(
*bytes
.first()
.ok_or_else(|| serde::de::Error::custom("missing signature scheme flag"))?,
)
.map_err(serde::de::Error::custom)?;
if flag != SignatureScheme::Passkey {
return Err(serde::de::Error::custom("invalid passkey flag"));
}
let bcs_bytes = &bytes[1..];
let authenticator = bcs::from_bytes(bcs_bytes).map_err(serde::de::Error::custom)?;
Self::try_from_raw(authenticator)
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let authenticator_ref = AuthenticatorRef {
authenticator_data: &self.authenticator_data,
client_data_json: &self.client_data_json,
signature: SimpleSignature::Secp256r1 {
signature: self.signature,
public_key: self.public_key,
},
};
let mut buf = Vec::new();
buf.push(SignatureScheme::Passkey as u8);
bcs::serialize_into(&mut buf, &authenticator_ref).expect("serialization cannot fail");
buf
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct CollectedClientData {
#[serde(rename = "type")]
pub ty: ClientDataType,
pub challenge: String,
pub origin: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
pub(super) enum ClientDataType {
#[serde(rename = "webauthn.get")]
Get,
}
}
#[cfg(feature = "proptest")]
impl proptest::arbitrary::Arbitrary for PasskeyAuthenticator {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
use proptest::collection::vec;
use proptest::prelude::*;
use serialization::ClientDataType;
use serialization::CollectedClientData;
(
any::<Secp256r1PublicKey>(),
any::<Secp256r1Signature>(),
vec(any::<u8>(), 32),
vec(any::<u8>(), 0..32),
)
.prop_map(
|(public_key, signature, challenge_bytes, authenticator_data)| {
let challenge =
<base64ct::Base64UrlUnpadded as base64ct::Encoding>::encode_string(
&challenge_bytes,
);
let client_data_json = serde_json::to_string(&CollectedClientData {
ty: ClientDataType::Get,
challenge,
origin: "http://example.com".to_owned(),
})
.unwrap();
Self {
public_key,
signature,
challenge: challenge_bytes,
authenticator_data,
client_data_json,
}
},
)
.boxed()
}
}
#[cfg(test)]
mod tests {
use crate::UserSignature;
#[test]
fn base64_encoded_passkey_user_signature() {
let b64 = "BiVYDmenOnqS+thmz5m5SrZnWaKXZLVxgh+rri6LHXs25B0AAAAAnQF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCAiY2hhbGxlbmdlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyIsImNyb3NzT3JpZ2luIjpmYWxzZSwgInVua25vd24iOiAidW5rbm93biJ9YgJMwqcOmZI7F/N+K5SMe4DRYCb4/cDWW68SFneSHoD2GxKKhksbpZ5rZpdrjSYABTCsFQQBpLORzTvbj4edWKd/AsEBeovrGvHR9Ku7critg6k7qvfFlPUngujXfEzXd8Eg";
let sig = UserSignature::from_base64(b64).unwrap();
assert!(matches!(sig, UserSignature::Passkey(_)));
}
}