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