use std::{
hash::{Hash, Hasher},
sync::Arc,
};
use fastcrypto::{
error::FastCryptoError,
hash::{HashFunction, Sha256},
rsa::{Base64UrlUnpadded, Encoding},
secp256r1::{Secp256r1PublicKey, Secp256r1Signature},
traits::{ToFromBytes, VerifyingKey},
};
use once_cell::sync::OnceCell;
use passkey_types::webauthn::{ClientDataType, CollectedClientData};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use shared_crypto::intent::{INTENT_PREFIX_LENGTH, Intent, IntentMessage};
use crate::{
base_types::{EpochId, IotaAddress},
crypto::{
DefaultHash, IotaSignature, IotaSignatureInner, PublicKey, Secp256r1IotaSignature,
Signature, SignatureScheme,
},
digests::ZKLoginInputsDigest,
error::{IotaError, IotaResult},
signature::{AuthenticatorTrait, VerifyParams},
signature_verification::VerifiedDigestCache,
};
#[cfg(test)]
#[path = "unit_tests/passkey_authenticator_test.rs"]
mod passkey_authenticator_test;
#[derive(Debug, Clone, JsonSchema)]
pub struct PasskeyAuthenticator {
authenticator_data: Vec<u8>,
client_data_json: String,
#[serde(skip)]
signature: Secp256r1Signature,
#[serde(skip)]
pk: Secp256r1PublicKey,
#[serde(skip)]
intent: Intent,
#[serde(skip)]
digest: [u8; DefaultHash::OUTPUT_SIZE],
#[serde(skip)]
bytes: OnceCell<Vec<u8>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct RawPasskeyAuthenticator {
pub authenticator_data: Vec<u8>,
pub client_data_json: String,
pub user_signature: Signature,
}
impl TryFrom<RawPasskeyAuthenticator> for PasskeyAuthenticator {
type Error = IotaError;
fn try_from(raw: RawPasskeyAuthenticator) -> Result<Self, Self::Error> {
let client_data_json_parsed: CollectedClientData =
serde_json::from_str(&raw.client_data_json).map_err(|_| {
IotaError::InvalidSignature {
error: "Invalid client data json".to_string(),
}
})?;
if client_data_json_parsed.ty != ClientDataType::Get {
return Err(IotaError::InvalidSignature {
error: "Invalid client data type".to_string(),
});
};
let parsed_challenge = Base64UrlUnpadded::decode_vec(&client_data_json_parsed.challenge)
.map_err(|_| IotaError::InvalidSignature {
error: "Invalid encoded challenge".to_string(),
})?;
let intent =
Intent::from_bytes(&parsed_challenge[..INTENT_PREFIX_LENGTH]).map_err(|_| {
IotaError::InvalidSignature {
error: "Invalid intent from challenge".to_string(),
}
})?;
let digest = parsed_challenge[INTENT_PREFIX_LENGTH..]
.try_into()
.map_err(|_| IotaError::InvalidSignature {
error: "Invalid digest from challenge".to_string(),
})?;
if raw.user_signature.scheme() != SignatureScheme::Secp256r1 {
return Err(IotaError::InvalidSignature {
error: "Invalid signature scheme".to_string(),
});
};
let pk = Secp256r1PublicKey::from_bytes(raw.user_signature.public_key_bytes()).map_err(
|_| IotaError::InvalidSignature {
error: "Invalid r1 pk".to_string(),
},
)?;
let signature = Secp256r1Signature::from_bytes(raw.user_signature.signature_bytes())
.map_err(|_| IotaError::InvalidSignature {
error: "Invalid r1 sig".to_string(),
})?;
Ok(PasskeyAuthenticator {
authenticator_data: raw.authenticator_data,
client_data_json: raw.client_data_json,
signature,
pk,
intent,
digest,
bytes: OnceCell::new(),
})
}
}
impl<'de> Deserialize<'de> for PasskeyAuthenticator {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let serializable = RawPasskeyAuthenticator::deserialize(deserializer)?;
serializable
.try_into()
.map_err(|e: IotaError| Error::custom(e.to_string()))
}
}
impl Serialize for PasskeyAuthenticator {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let mut bytes = Vec::with_capacity(Secp256r1IotaSignature::LENGTH);
bytes.push(SignatureScheme::Secp256r1.flag());
bytes.extend_from_slice(self.signature.as_ref());
bytes.extend_from_slice(self.pk.as_ref());
let raw = RawPasskeyAuthenticator {
authenticator_data: self.authenticator_data.clone(),
client_data_json: self.client_data_json.clone(),
user_signature: Signature::Secp256r1IotaSignature(
Secp256r1IotaSignature::from_bytes(&bytes).unwrap(),
),
};
raw.serialize(serializer)
}
}
impl PasskeyAuthenticator {
pub fn new_for_testing(
authenticator_data: Vec<u8>,
client_data_json: String,
user_signature: Signature,
) -> Result<Self, IotaError> {
let raw = RawPasskeyAuthenticator {
authenticator_data,
client_data_json,
user_signature,
};
raw.try_into()
}
pub fn get_pk(&self) -> IotaResult<PublicKey> {
Ok(PublicKey::Passkey((&self.pk).into()))
}
}
impl PartialEq for PasskeyAuthenticator {
fn eq(&self, other: &Self) -> bool {
self.as_ref() == other.as_ref()
}
}
impl Eq for PasskeyAuthenticator {}
impl Hash for PasskeyAuthenticator {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_ref().hash(state);
}
}
impl AuthenticatorTrait for PasskeyAuthenticator {
fn verify_user_authenticator_epoch(
&self,
_epoch: EpochId,
_max_epoch_upper_bound_delta: Option<u64>,
) -> IotaResult {
Ok(())
}
fn verify_claims<T>(
&self,
intent_msg: &IntentMessage<T>,
author: IotaAddress,
_aux_verify_data: &VerifyParams,
_zklogin_inputs_cache: Arc<VerifiedDigestCache<ZKLoginInputsDigest>>,
) -> IotaResult
where
T: Serialize,
{
if intent_msg.intent != self.intent || to_signing_digest(intent_msg) != self.digest {
return Err(IotaError::InvalidSignature {
error: "Invalid challenge".to_string(),
});
};
let mut message = self.authenticator_data.clone();
let client_data_hash = Sha256::digest(self.client_data_json.as_bytes()).digest;
message.extend_from_slice(&client_data_hash);
if author != IotaAddress::from(&self.get_pk()?) {
return Err(IotaError::InvalidSignature {
error: "Invalid author".to_string(),
});
};
self.pk
.verify(&message, &self.signature)
.map_err(|_| IotaError::InvalidSignature {
error: "Fails to verify".to_string(),
})
}
}
impl ToFromBytes for PasskeyAuthenticator {
fn from_bytes(bytes: &[u8]) -> Result<Self, FastCryptoError> {
if bytes.first().ok_or(FastCryptoError::InvalidInput)?
!= &SignatureScheme::PasskeyAuthenticator.flag()
{
return Err(FastCryptoError::InvalidInput);
}
let passkey: PasskeyAuthenticator =
bcs::from_bytes(&bytes[1..]).map_err(|_| FastCryptoError::InvalidSignature)?;
Ok(passkey)
}
}
impl AsRef<[u8]> for PasskeyAuthenticator {
fn as_ref(&self) -> &[u8] {
self.bytes
.get_or_try_init::<_, eyre::Report>(|| {
let as_bytes = bcs::to_bytes(self).expect("BCS serialization should not fail");
let mut bytes = Vec::with_capacity(1 + as_bytes.len());
bytes.push(SignatureScheme::PasskeyAuthenticator.flag());
bytes.extend_from_slice(as_bytes.as_slice());
Ok(bytes)
})
.expect("OnceCell invariant violated")
}
}
pub fn to_signing_message<T: Serialize>(
intent_msg: &IntentMessage<T>,
) -> [u8; INTENT_PREFIX_LENGTH + DefaultHash::OUTPUT_SIZE] {
let mut extended = [0; INTENT_PREFIX_LENGTH + DefaultHash::OUTPUT_SIZE];
extended[..INTENT_PREFIX_LENGTH].copy_from_slice(&intent_msg.intent.to_bytes());
extended[INTENT_PREFIX_LENGTH..].copy_from_slice(&to_signing_digest(intent_msg));
extended
}
pub fn to_signing_digest<T: Serialize>(
intent_msg: &IntentMessage<T>,
) -> [u8; DefaultHash::OUTPUT_SIZE] {
let mut hasher = DefaultHash::default();
bcs::serialize_into(&mut hasher, &intent_msg.value)
.expect("Message serialization should not fail");
hasher.finalize().digest
}