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::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    /// Decoded `client_data_json.challenge` which is expected to be the signing
66    /// message `hash(Intent | bcs_message)`
67    #[serde(skip)]
68    challenge: [u8; DefaultHash::OUTPUT_SIZE],
69
70    /// Initialization of bytes for passkey in serialized form.
71    #[serde(skip)]
72    bytes: OnceCell<Vec<u8>>,
73}
74
75/// An raw passkey authenticator struct used during deserialization. Can be
76/// converted to [struct PasskeyAuthenticator].
77#[derive(Serialize, Deserialize, Debug)]
78pub struct RawPasskeyAuthenticator {
79    pub authenticator_data: Vec<u8>,
80    pub client_data_json: String,
81    pub user_signature: Signature,
82}
83
84/// Convert [struct RawPasskeyAuthenticator] to [struct PasskeyAuthenticator]
85/// with validations.
86impl TryFrom<RawPasskeyAuthenticator> for PasskeyAuthenticator {
87    type Error = IotaError;
88
89    fn try_from(raw: RawPasskeyAuthenticator) -> Result<Self, Self::Error> {
90        let client_data_json_parsed: CollectedClientData =
91            serde_json::from_str(&raw.client_data_json).map_err(|_| {
92                IotaError::InvalidSignature {
93                    error: "Invalid client data json".to_string(),
94                }
95            })?;
96
97        if client_data_json_parsed.ty != ClientDataType::Get {
98            return Err(IotaError::InvalidSignature {
99                error: "Invalid client data type".to_string(),
100            });
101        };
102
103        let challenge = Base64UrlUnpadded::decode_vec(&client_data_json_parsed.challenge)
104            .map_err(|_| IotaError::InvalidSignature {
105                error: "Invalid encoded challenge".to_string(),
106            })?
107            .try_into()
108            .map_err(|_| IotaError::InvalidSignature {
109                error: "Invalid encoded challenge".to_string(),
110            })?;
111
112        if raw.user_signature.scheme() != SignatureScheme::Secp256r1 {
113            return Err(IotaError::InvalidSignature {
114                error: "Invalid signature scheme".to_string(),
115            });
116        };
117
118        let pk = Secp256r1PublicKey::from_bytes(raw.user_signature.public_key_bytes()).map_err(
119            |_| IotaError::InvalidSignature {
120                error: "Invalid r1 pk".to_string(),
121            },
122        )?;
123
124        let signature = Secp256r1Signature::from_bytes(raw.user_signature.signature_bytes())
125            .map_err(|_| IotaError::InvalidSignature {
126                error: "Invalid r1 sig".to_string(),
127            })?;
128
129        Ok(PasskeyAuthenticator {
130            authenticator_data: raw.authenticator_data,
131            client_data_json: raw.client_data_json,
132            signature,
133            pk,
134            challenge,
135            bytes: OnceCell::new(),
136        })
137    }
138}
139
140impl<'de> Deserialize<'de> for PasskeyAuthenticator {
141    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
142    where
143        D: Deserializer<'de>,
144    {
145        use serde::de::Error;
146
147        let serializable = RawPasskeyAuthenticator::deserialize(deserializer)?;
148        serializable
149            .try_into()
150            .map_err(|e: IotaError| Error::custom(e.to_string()))
151    }
152}
153
154impl Serialize for PasskeyAuthenticator {
155    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
156    where
157        S: serde::ser::Serializer,
158    {
159        let mut bytes = Vec::with_capacity(Secp256r1IotaSignature::LENGTH);
160        bytes.push(SignatureScheme::Secp256r1.flag());
161        bytes.extend_from_slice(self.signature.as_ref());
162        bytes.extend_from_slice(self.pk.as_ref());
163
164        let raw = RawPasskeyAuthenticator {
165            authenticator_data: self.authenticator_data.clone(),
166            client_data_json: self.client_data_json.clone(),
167            user_signature: Signature::Secp256r1IotaSignature(
168                Secp256r1IotaSignature::from_bytes(&bytes).unwrap(),
169            ),
170        };
171        raw.serialize(serializer)
172    }
173}
174impl PasskeyAuthenticator {
175    /// A constructor for [struct PasskeyAuthenticator] with custom
176    /// defined fields. Used for testing.
177    pub fn new_for_testing(
178        authenticator_data: Vec<u8>,
179        client_data_json: String,
180        user_signature: Signature,
181    ) -> Result<Self, IotaError> {
182        let raw = RawPasskeyAuthenticator {
183            authenticator_data,
184            client_data_json,
185            user_signature,
186        };
187        raw.try_into()
188    }
189
190    /// Returns the public key of the passkey authenticator.
191    pub fn get_pk(&self) -> IotaResult<PublicKey> {
192        Ok(PublicKey::Passkey((&self.pk).into()))
193    }
194}
195
196/// Necessary trait for [struct SenderSignedData].
197impl PartialEq for PasskeyAuthenticator {
198    fn eq(&self, other: &Self) -> bool {
199        self.as_ref() == other.as_ref()
200    }
201}
202
203/// Necessary trait for [struct SenderSignedData].
204impl Eq for PasskeyAuthenticator {}
205
206/// Necessary trait for [struct SenderSignedData].
207impl Hash for PasskeyAuthenticator {
208    fn hash<H: Hasher>(&self, state: &mut H) {
209        self.as_ref().hash(state);
210    }
211}
212
213impl AuthenticatorTrait for PasskeyAuthenticator {
214    fn verify_user_authenticator_epoch(
215        &self,
216        _epoch: EpochId,
217        _max_epoch_upper_bound_delta: Option<u64>,
218    ) -> IotaResult {
219        Ok(())
220    }
221
222    /// Verify an intent message of a transaction with an passkey authenticator.
223    fn verify_claims<T>(
224        &self,
225        intent_msg: &IntentMessage<T>,
226        author: IotaAddress,
227        _aux_verify_data: &VerifyParams,
228        _zklogin_inputs_cache: Arc<VerifiedDigestCache<ZKLoginInputsDigest>>,
229    ) -> IotaResult
230    where
231        T: Serialize,
232    {
233        // Check the intent and signing is consisted from what's parsed from
234        // client_data_json.challenge
235        if self.challenge != to_signing_message(intent_msg) {
236            return Err(IotaError::InvalidSignature {
237                error: "Invalid challenge".to_string(),
238            });
239        };
240
241        // Construct msg = authenticator_data || sha256(client_data_json).
242        let mut message = self.authenticator_data.clone();
243        let client_data_hash = Sha256::digest(self.client_data_json.as_bytes()).digest;
244        message.extend_from_slice(&client_data_hash);
245
246        // Check if author is derived from the public key.
247        if author != IotaAddress::from(&self.get_pk()?) {
248            return Err(IotaError::InvalidSignature {
249                error: "Invalid author".to_string(),
250            });
251        };
252
253        // Verify the signature against pk and message.
254        self.pk
255            .verify(&message, &self.signature)
256            .map_err(|_| IotaError::InvalidSignature {
257                error: "Fails to verify".to_string(),
258            })
259    }
260}
261
262impl ToFromBytes for PasskeyAuthenticator {
263    fn from_bytes(bytes: &[u8]) -> Result<Self, FastCryptoError> {
264        // The first byte matches the flag of PasskeyAuthenticator.
265        if bytes.first().ok_or(FastCryptoError::InvalidInput)?
266            != &SignatureScheme::PasskeyAuthenticator.flag()
267        {
268            return Err(FastCryptoError::InvalidInput);
269        }
270        let passkey: PasskeyAuthenticator =
271            bcs::from_bytes(&bytes[1..]).map_err(|_| FastCryptoError::InvalidSignature)?;
272        Ok(passkey)
273    }
274}
275
276impl AsRef<[u8]> for PasskeyAuthenticator {
277    fn as_ref(&self) -> &[u8] {
278        self.bytes
279            .get_or_try_init::<_, eyre::Report>(|| {
280                let as_bytes = bcs::to_bytes(self).expect("BCS serialization should not fail");
281                let mut bytes = Vec::with_capacity(1 + as_bytes.len());
282                bytes.push(SignatureScheme::PasskeyAuthenticator.flag());
283                bytes.extend_from_slice(as_bytes.as_slice());
284                Ok(bytes)
285            })
286            .expect("OnceCell invariant violated")
287    }
288}
289
290/// Compute the signing digest that the signature committed over as `hash(intent
291/// || tx_data)`
292pub fn to_signing_message<T: Serialize>(
293    intent_msg: &IntentMessage<T>,
294) -> [u8; DefaultHash::OUTPUT_SIZE] {
295    let mut hasher = DefaultHash::default();
296    bcs::serialize_into(&mut hasher, intent_msg).expect("Message serialization should not fail");
297    hasher.finalize().digest
298}