sui_types/
passkey_authenticator.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3use crate::crypto::PublicKey;
4use crate::crypto::Secp256r1SuiSignature;
5use crate::crypto::SuiSignatureInner;
6use crate::error::SuiErrorKind;
7use crate::signature_verification::VerifiedDigestCache;
8use crate::{
9    base_types::{EpochId, SuiAddress},
10    crypto::{DefaultHash, Signature, SignatureScheme, SuiSignature},
11    digests::ZKLoginInputsDigest,
12    error::{SuiError, SuiResult},
13    signature::{AuthenticatorTrait, VerifyParams},
14};
15use fastcrypto::hash::{HashFunction, Sha256};
16use fastcrypto::rsa::{Base64UrlUnpadded, Encoding};
17use fastcrypto::secp256r1::{Secp256r1PublicKey, Secp256r1Signature};
18use fastcrypto::traits::VerifyingKey;
19use fastcrypto::{error::FastCryptoError, traits::ToFromBytes};
20use once_cell::sync::OnceCell;
21use passkey_types::webauthn::{ClientDataType, CollectedClientData};
22use schemars::JsonSchema;
23use serde::{Deserialize, Deserializer, Serialize};
24use shared_crypto::intent::IntentMessage;
25use std::hash::Hash;
26use std::hash::Hasher;
27use std::sync::Arc;
28
29#[cfg(test)]
30#[path = "unit_tests/passkey_authenticator_test.rs"]
31mod passkey_authenticator_test;
32
33/// An passkey authenticator with parsed fields. See field defition below. Can be initialized from [struct RawPasskeyAuthenticator].
34#[derive(Debug, Clone, JsonSchema)]
35pub struct PasskeyAuthenticator {
36    /// `authenticatorData` is a bytearray that encodes
37    /// [Authenticator Data](https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data)
38    /// structure returned by the authenticator attestation
39    /// response as is.
40    authenticator_data: Vec<u8>,
41
42    /// `clientDataJSON` contains a JSON-compatible
43    /// UTF-8 encoded string of the client data which
44    /// is passed to the authenticator by the client
45    /// during the authentication request (see [CollectedClientData](https://www.w3.org/TR/webauthn-2/#dictdef-collectedclientdata))
46    client_data_json: String,
47
48    /// Normalized r1 signature returned by passkey.
49    /// Initialized from `user_signature` in `RawPasskeyAuthenticator`.
50    #[serde(skip)]
51    signature: Secp256r1Signature,
52
53    /// Compact r1 public key upon passkey creation.
54    /// Initialized from `user_signature` in `RawPasskeyAuthenticator`.
55    #[serde(skip)]
56    pk: Secp256r1PublicKey,
57
58    /// Decoded `client_data_json.challenge` which is expected to be the signing message
59    /// `hash(Intent | bcs_message)`
60    #[serde(skip)]
61    challenge: [u8; DefaultHash::OUTPUT_SIZE],
62
63    /// Initialization of bytes for passkey in serialized form.
64    #[serde(skip)]
65    bytes: OnceCell<Vec<u8>>,
66}
67
68/// A raw passkey authenticator struct used during deserialization. Can be converted to [struct PasskeyAuthenticator].
69#[derive(Serialize, Deserialize, Debug)]
70pub struct RawPasskeyAuthenticator {
71    pub authenticator_data: Vec<u8>,
72    pub client_data_json: String,
73    pub user_signature: Signature,
74}
75
76/// Convert [struct RawPasskeyAuthenticator] to [struct PasskeyAuthenticator] with validations.
77impl TryFrom<RawPasskeyAuthenticator> for PasskeyAuthenticator {
78    type Error = SuiError;
79
80    fn try_from(raw: RawPasskeyAuthenticator) -> Result<Self, Self::Error> {
81        let client_data_json_parsed: CollectedClientData =
82            serde_json::from_str(&raw.client_data_json).map_err(|_| {
83                SuiErrorKind::InvalidSignature {
84                    error: "Invalid client data json".to_string(),
85                }
86            })?;
87
88        if client_data_json_parsed.ty != ClientDataType::Get {
89            return Err(SuiErrorKind::InvalidSignature {
90                error: "Invalid client data type".to_string(),
91            }
92            .into());
93        };
94
95        let challenge = Base64UrlUnpadded::decode_vec(&client_data_json_parsed.challenge)
96            .map_err(|_| SuiErrorKind::InvalidSignature {
97                error: "Invalid encoded challenge".to_string(),
98            })?
99            .try_into()
100            .map_err(|_| SuiErrorKind::InvalidSignature {
101                error: "Invalid size for challenge".to_string(),
102            })?;
103
104        if raw.user_signature.scheme() != SignatureScheme::Secp256r1 {
105            return Err(SuiErrorKind::InvalidSignature {
106                error: "Invalid signature scheme".to_string(),
107            }
108            .into());
109        };
110
111        let pk = Secp256r1PublicKey::from_bytes(raw.user_signature.public_key_bytes()).map_err(
112            |_| SuiErrorKind::InvalidSignature {
113                error: "Invalid r1 pk".to_string(),
114            },
115        )?;
116
117        let signature = Secp256r1Signature::from_bytes(raw.user_signature.signature_bytes())
118            .map_err(|_| SuiErrorKind::InvalidSignature {
119                error: "Invalid r1 sig".to_string(),
120            })?;
121
122        Ok(PasskeyAuthenticator {
123            authenticator_data: raw.authenticator_data,
124            client_data_json: raw.client_data_json,
125            signature,
126            pk,
127            challenge,
128            bytes: OnceCell::new(),
129        })
130    }
131}
132
133impl<'de> Deserialize<'de> for PasskeyAuthenticator {
134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135    where
136        D: Deserializer<'de>,
137    {
138        use serde::de::Error;
139
140        let serializable = RawPasskeyAuthenticator::deserialize(deserializer)?;
141        serializable
142            .try_into()
143            .map_err(|e: SuiError| Error::custom(e.to_string()))
144    }
145}
146
147impl Serialize for PasskeyAuthenticator {
148    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: serde::ser::Serializer,
151    {
152        let mut bytes = Vec::with_capacity(Secp256r1SuiSignature::LENGTH);
153        bytes.push(SignatureScheme::Secp256r1.flag());
154        bytes.extend_from_slice(self.signature.as_ref());
155        bytes.extend_from_slice(self.pk.as_ref());
156
157        let raw = RawPasskeyAuthenticator {
158            authenticator_data: self.authenticator_data.clone(),
159            client_data_json: self.client_data_json.clone(),
160            user_signature: Signature::Secp256r1SuiSignature(
161                Secp256r1SuiSignature::from_bytes(&bytes).unwrap(), // ok to unwrap since the bytes are constructed as valid above.
162            ),
163        };
164        raw.serialize(serializer)
165    }
166}
167
168impl PasskeyAuthenticator {
169    /// A constructor for [struct PasskeyAuthenticator] with custom
170    /// defined fields. Used for testing.
171    pub fn new_for_testing(
172        authenticator_data: Vec<u8>,
173        client_data_json: String,
174        user_signature: Signature,
175    ) -> Result<Self, SuiError> {
176        let raw = RawPasskeyAuthenticator {
177            authenticator_data,
178            client_data_json,
179            user_signature,
180        };
181        raw.try_into()
182    }
183
184    /// Returns the public key of the passkey authenticator.
185    pub fn get_pk(&self) -> SuiResult<PublicKey> {
186        Ok(PublicKey::Passkey((&self.pk).into()))
187    }
188
189    pub fn authenticator_data(&self) -> &[u8] {
190        &self.authenticator_data
191    }
192
193    pub fn client_data_json(&self) -> &str {
194        &self.client_data_json
195    }
196
197    pub fn signature(&self) -> Signature {
198        let mut bytes = Vec::with_capacity(Secp256r1SuiSignature::LENGTH);
199        bytes.push(SignatureScheme::Secp256r1.flag());
200        bytes.extend_from_slice(self.signature.as_ref());
201        bytes.extend_from_slice(self.pk.as_ref());
202
203        // Safe to unwrap because signature and pk are serialized from valid struct.
204        Signature::Secp256r1SuiSignature(Secp256r1SuiSignature::from_bytes(&bytes).unwrap())
205    }
206}
207
208/// Necessary trait for [struct SenderSignedData].
209impl PartialEq for PasskeyAuthenticator {
210    fn eq(&self, other: &Self) -> bool {
211        self.as_ref() == other.as_ref()
212    }
213}
214
215/// Necessary trait for [struct SenderSignedData].
216impl Eq for PasskeyAuthenticator {}
217
218/// Necessary trait for [struct SenderSignedData].
219impl Hash for PasskeyAuthenticator {
220    fn hash<H: Hasher>(&self, state: &mut H) {
221        self.as_ref().hash(state);
222    }
223}
224
225impl AuthenticatorTrait for PasskeyAuthenticator {
226    fn verify_user_authenticator_epoch(
227        &self,
228        _epoch: EpochId,
229        _max_epoch_upper_bound_delta: Option<u64>,
230    ) -> SuiResult {
231        Ok(())
232    }
233
234    /// Verify an intent message of a transaction with an passkey authenticator.
235    fn verify_claims<T>(
236        &self,
237        intent_msg: &IntentMessage<T>,
238        author: SuiAddress,
239        _aux_verify_data: &VerifyParams,
240        _zklogin_inputs_cache: Arc<VerifiedDigestCache<ZKLoginInputsDigest>>,
241    ) -> SuiResult
242    where
243        T: Serialize,
244    {
245        // Check if author is derived from the public key.
246        if author != SuiAddress::from(&self.get_pk()?) {
247            return Err(SuiErrorKind::InvalidSignature {
248                error: "Invalid author".to_string(),
249            }
250            .into());
251        };
252
253        // Check the intent and signing is consisted from what's parsed from client_data_json.challenge
254        if self.challenge != to_signing_message(intent_msg) {
255            return Err(SuiErrorKind::InvalidSignature {
256                error: "Invalid challenge".to_string(),
257            }
258            .into());
259        };
260
261        // Construct msg = authenticator_data || sha256(client_data_json).
262        let mut message = self.authenticator_data.clone();
263        let client_data_hash = Sha256::digest(self.client_data_json.as_bytes()).digest;
264        message.extend_from_slice(&client_data_hash);
265
266        // Verify the signature against pk and message.
267        self.pk.verify(&message, &self.signature).map_err(|_| {
268            SuiErrorKind::InvalidSignature {
269                error: "Fails to verify".to_string(),
270            }
271            .into()
272        })
273    }
274}
275
276impl ToFromBytes for PasskeyAuthenticator {
277    fn from_bytes(bytes: &[u8]) -> Result<Self, FastCryptoError> {
278        // The first byte matches the flag of PasskeyAuthenticator.
279        if bytes.first().ok_or(FastCryptoError::InvalidInput)?
280            != &SignatureScheme::PasskeyAuthenticator.flag()
281        {
282            return Err(FastCryptoError::InvalidInput);
283        }
284        let passkey: PasskeyAuthenticator =
285            bcs::from_bytes(&bytes[1..]).map_err(|_| FastCryptoError::InvalidSignature)?;
286        Ok(passkey)
287    }
288}
289
290impl AsRef<[u8]> for PasskeyAuthenticator {
291    fn as_ref(&self) -> &[u8] {
292        self.bytes
293            .get_or_try_init::<_, eyre::Report>(|| {
294                let as_bytes = bcs::to_bytes(self).expect("BCS serialization should not fail");
295                let mut bytes = Vec::with_capacity(1 + as_bytes.len());
296                bytes.push(SignatureScheme::PasskeyAuthenticator.flag());
297                bytes.extend_from_slice(as_bytes.as_slice());
298                Ok(bytes)
299            })
300            .expect("OnceCell invariant violated")
301    }
302}
303
304/// Compute the signing digest that the signature committed over as `hash(intent || tx_data)`
305pub fn to_signing_message<T: Serialize>(
306    intent_msg: &IntentMessage<T>,
307) -> [u8; DefaultHash::OUTPUT_SIZE] {
308    let mut hasher = DefaultHash::default();
309    bcs::serialize_into(&mut hasher, intent_msg).expect("Message serialization should not fail");
310    hasher.finalize().digest
311}