iota_types/
passkey_authenticator.rs

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