Skip to main content

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,
10    rsa::{Base64UrlUnpadded, Encoding},
11    secp256r1::{Secp256r1PublicKey, Secp256r1Signature},
12    traits::ToFromBytes,
13};
14use iota_sdk_crypto::{Verifier, passkey::PasskeyVerifier};
15use iota_sdk_types::crypto::IntentMessage;
16use once_cell::sync::OnceCell;
17use passkey_types::webauthn::{ClientDataType, CollectedClientData};
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)]
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    signature: Secp256r1Signature,
53
54    /// Compact r1 public key upon passkey creation.
55    /// Initialized from `user_signature` in `RawPasskeyAuthenticator`.
56    pk: Secp256r1PublicKey,
57
58    /// Decoded `client_data_json.challenge` which is expected to be the signing
59    /// message `hash(Intent | bcs_message)`
60    _challenge: [u8; DefaultHash::OUTPUT_SIZE],
61
62    /// Initialization of bytes for passkey in serialized form.
63    bytes: OnceCell<Vec<u8>>,
64}
65
66/// A raw passkey authenticator struct used during deserialization. Can be
67/// converted to [struct PasskeyAuthenticator].
68#[derive(Serialize, Deserialize, Debug)]
69pub struct RawPasskeyAuthenticator {
70    pub authenticator_data: Vec<u8>,
71    pub client_data_json: String,
72    pub user_signature: Signature,
73}
74
75/// Convert [struct RawPasskeyAuthenticator] to [struct PasskeyAuthenticator]
76/// with validations.
77impl TryFrom<RawPasskeyAuthenticator> for PasskeyAuthenticator {
78    type Error = IotaError;
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                IotaError::InvalidSignature {
84                    error: "Invalid client data json".to_string(),
85                }
86            })?;
87
88        if client_data_json_parsed.ty != ClientDataType::Get {
89            return Err(IotaError::InvalidSignature {
90                error: "Invalid client data type".to_string(),
91            });
92        };
93
94        let challenge = Base64UrlUnpadded::decode_vec(&client_data_json_parsed.challenge)
95            .map_err(|_| IotaError::InvalidSignature {
96                error: "Invalid encoded challenge".to_string(),
97            })?
98            .try_into()
99            .map_err(|_| IotaError::InvalidSignature {
100                error: "Invalid size for challenge".to_string(),
101            })?;
102
103        if raw.user_signature.scheme() != SignatureScheme::Secp256r1 {
104            return Err(IotaError::InvalidSignature {
105                error: "Invalid signature scheme".to_string(),
106            });
107        };
108
109        let pk = Secp256r1PublicKey::from_bytes(raw.user_signature.public_key_bytes()).map_err(
110            |_| IotaError::InvalidSignature {
111                error: "Invalid r1 pk".to_string(),
112            },
113        )?;
114
115        let signature = Secp256r1Signature::from_bytes(raw.user_signature.signature_bytes())
116            .map_err(|_| IotaError::InvalidSignature {
117                error: "Invalid r1 sig".to_string(),
118            })?;
119
120        Ok(PasskeyAuthenticator {
121            authenticator_data: raw.authenticator_data,
122            client_data_json: raw.client_data_json,
123            signature,
124            pk,
125            _challenge: challenge,
126            bytes: OnceCell::new(),
127        })
128    }
129}
130
131impl<'de> Deserialize<'de> for PasskeyAuthenticator {
132    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
133    where
134        D: Deserializer<'de>,
135    {
136        use serde::de::Error;
137
138        let serializable = RawPasskeyAuthenticator::deserialize(deserializer)?;
139        serializable
140            .try_into()
141            .map_err(|e: IotaError| Error::custom(e.to_string()))
142    }
143}
144
145impl Serialize for PasskeyAuthenticator {
146    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147    where
148        S: serde::ser::Serializer,
149    {
150        let mut bytes = Vec::with_capacity(Secp256r1IotaSignature::LENGTH);
151        bytes.push(SignatureScheme::Secp256r1.flag());
152        bytes.extend_from_slice(self.signature.as_ref());
153        bytes.extend_from_slice(self.pk.as_ref());
154
155        let raw = RawPasskeyAuthenticator {
156            authenticator_data: self.authenticator_data.clone(),
157            client_data_json: self.client_data_json.clone(),
158            user_signature: Signature::Secp256r1IotaSignature(
159                Secp256r1IotaSignature::from_bytes(&bytes).unwrap(), /* ok to unwrap since the
160                                                                      * bytes are constructed as
161                                                                      * 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, IotaError> {
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) -> IotaResult<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(Secp256r1IotaSignature::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::Secp256r1IotaSignature(Secp256r1IotaSignature::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    /// Verify an intent message of a transaction with an passkey authenticator.
227    fn verify_claims<T>(
228        &self,
229        intent_msg: &IntentMessage<T>,
230        author: IotaAddress,
231        _aux_verify_data: &VerifyParams,
232    ) -> IotaResult
233    where
234        T: Serialize,
235    {
236        let digest = intent_msg.signing_digest();
237
238        // TODO https://github.com/iotaledger/iota/issues/11607
239        let authenticator = iota_sdk_types::PasskeyAuthenticator::from_bytes(self.as_bytes())
240            .map_err(|_| IotaError::InvalidSignature {
241                error: "Invalid passkey authenticator bytes".to_string(),
242            })?;
243
244        PasskeyVerifier::new()
245            .with_address(author)
246            .verify(&*digest, &authenticator)
247            .map_err(|e| IotaError::InvalidSignature {
248                error: format!("Invalid passkey authentication: {e}"),
249            })
250    }
251}
252
253impl ToFromBytes for PasskeyAuthenticator {
254    fn from_bytes(bytes: &[u8]) -> Result<Self, FastCryptoError> {
255        // The first byte matches the flag of PasskeyAuthenticator.
256        if bytes.first().ok_or(FastCryptoError::InvalidInput)?
257            != &SignatureScheme::PasskeyAuthenticator.flag()
258        {
259            return Err(FastCryptoError::InvalidInput);
260        }
261        let passkey: PasskeyAuthenticator =
262            bcs::from_bytes(&bytes[1..]).map_err(|_| FastCryptoError::InvalidSignature)?;
263        Ok(passkey)
264    }
265}
266
267impl AsRef<[u8]> for PasskeyAuthenticator {
268    fn as_ref(&self) -> &[u8] {
269        self.bytes
270            .get_or_try_init::<_, eyre::Report>(|| {
271                let as_bytes = bcs::to_bytes(self).expect("BCS serialization should not fail");
272                let mut bytes = Vec::with_capacity(1 + as_bytes.len());
273                bytes.push(SignatureScheme::PasskeyAuthenticator.flag());
274                bytes.extend_from_slice(as_bytes.as_slice());
275                Ok(bytes)
276            })
277            .expect("OnceCell invariant violated")
278    }
279}
280
281/// Compute the signing digest that the signature committed over as `hash(intent
282/// || tx_data)`
283pub fn to_signing_message<T: Serialize>(
284    intent_msg: &IntentMessage<T>,
285) -> [u8; DefaultHash::OUTPUT_SIZE] {
286    let mut hasher = DefaultHash::default();
287    bcs::serialize_into(&mut hasher, intent_msg).expect("Message serialization should not fail");
288    hasher.finalize().digest
289}