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