iota_sdk_types/crypto/
passkey.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use super::{Secp256r1PublicKey, Secp256r1Signature, SimpleSignature};
6
7/// A passkey authenticator.
8///
9/// # BCS
10///
11/// The BCS serialized form for this type is defined by the following ABNF:
12///
13/// ```text
14/// passkey-bcs = bytes               ; where the contents of the bytes are
15///                                   ; defined by <passkey>
16/// passkey     = passkey-flag
17///               bytes               ; passkey authenticator data
18///               client-data-json    ; valid json
19///               simple-signature    ; required to be a secp256r1 signature
20///
21/// client-data-json = string ; valid json
22/// ```
23///
24/// See [CollectedClientData](https://www.w3.org/TR/webauthn-2/#dictdef-collectedclientdata) for
25/// the required json-schema for the `client-data-json` rule. In addition, IOTA
26/// currently requires that the `CollectedClientData.type` field is required to
27/// be `webauthn.get`.
28///
29/// Note: Due to historical reasons, signatures are serialized slightly
30/// different from the majority of the types in IOTA. In particular if a
31/// signature is ever embedded in another structure it generally is serialized
32/// as `bytes` meaning it has a length prefix that defines the length of
33/// the completely serialized signature.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct PasskeyAuthenticator {
36    /// The secp256r1 public key for this passkey.
37    public_key: Secp256r1PublicKey,
38    /// The secp256r1 signature from the passkey.
39    signature: Secp256r1Signature,
40    /// Parsed base64url decoded challenge bytes from
41    /// `client_data_json.challenge`.
42    challenge: Vec<u8>,
43    /// Opaque authenticator data for this passkey signature.
44    ///
45    /// See [Authenticator Data](https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data) for
46    /// more information on this field.
47    authenticator_data: Vec<u8>,
48    /// Structured, unparsed, JSON for this passkey signature.
49    ///
50    /// See [CollectedClientData](https://www.w3.org/TR/webauthn-2/#dictdef-collectedclientdata)
51    /// for more information on this field.
52    client_data_json: String,
53}
54
55impl PasskeyAuthenticator {
56    /// Opaque authenticator data for this passkey signature.
57    ///
58    /// See [Authenticator Data](https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data) for
59    /// more information on this field.
60    pub fn authenticator_data(&self) -> &[u8] {
61        &self.authenticator_data
62    }
63
64    /// Structured, unparsed, JSON for this passkey signature.
65    ///
66    /// See [CollectedClientData](https://www.w3.org/TR/webauthn-2/#dictdef-collectedclientdata)
67    /// for more information on this field.
68    pub fn client_data_json(&self) -> &str {
69        &self.client_data_json
70    }
71
72    /// The parsed challenge message for this passkey signature.
73    ///
74    /// This is parsed by decoding the base64url data from the
75    /// `client_data_json.challenge` field.
76    pub fn challenge(&self) -> &[u8] {
77        &self.challenge
78    }
79
80    /// The passkey signature.
81    pub fn signature(&self) -> SimpleSignature {
82        SimpleSignature::Secp256r1 {
83            signature: self.signature,
84            public_key: self.public_key,
85        }
86    }
87
88    /// The passkey public key
89    pub fn public_key(&self) -> PasskeyPublicKey {
90        PasskeyPublicKey::new(self.public_key)
91    }
92}
93
94/// Public key of a `PasskeyAuthenticator`.
95///
96/// This is used to derive the onchain `Address` for a `PasskeyAuthenticator`.
97///
98/// # BCS
99///
100/// The BCS serialized form for this type is defined by the following ABNF:
101///
102/// ```text
103/// passkey-public-key = passkey-flag secp256r1-public-key
104/// ```
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct PasskeyPublicKey(Secp256r1PublicKey);
107
108impl PasskeyPublicKey {
109    pub fn new(public_key: Secp256r1PublicKey) -> Self {
110        Self(public_key)
111    }
112
113    /// The underlying `Secp256r1PublicKey` for this passkey.
114    pub fn inner(&self) -> &Secp256r1PublicKey {
115        &self.0
116    }
117}
118
119#[cfg(feature = "serde")]
120#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
121mod serialization {
122    use std::borrow::Cow;
123
124    use serde::{Deserialize, Deserializer, Serialize, Serializer};
125    use serde_with::{Bytes, DeserializeAs};
126
127    use super::*;
128    use crate::{SignatureScheme, SimpleSignature, crypto::SignatureFromBytesError};
129
130    #[derive(serde::Serialize)]
131    struct AuthenticatorRef<'a> {
132        authenticator_data: &'a Vec<u8>,
133        client_data_json: &'a String,
134        signature: SimpleSignature,
135    }
136
137    #[derive(serde::Deserialize)]
138    #[serde(rename = "PasskeyAuthenticator")]
139    #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
140    struct Authenticator {
141        authenticator_data: Vec<u8>,
142        client_data_json: String,
143        signature: SimpleSignature,
144    }
145
146    #[cfg(feature = "schemars")]
147    impl schemars::JsonSchema for PasskeyAuthenticator {
148        fn schema_name() -> String {
149            Authenticator::schema_name()
150        }
151
152        fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
153            Authenticator::json_schema(gen)
154        }
155    }
156
157    impl Serialize for PasskeyAuthenticator {
158        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
159        where
160            S: Serializer,
161        {
162            if serializer.is_human_readable() {
163                let authenticator_ref = AuthenticatorRef {
164                    authenticator_data: &self.authenticator_data,
165                    client_data_json: &self.client_data_json,
166                    signature: SimpleSignature::Secp256r1 {
167                        signature: self.signature,
168                        public_key: self.public_key,
169                    },
170                };
171
172                authenticator_ref.serialize(serializer)
173            } else {
174                let bytes = self.to_bytes();
175                serializer.serialize_bytes(&bytes)
176            }
177        }
178    }
179
180    impl<'de> Deserialize<'de> for PasskeyAuthenticator {
181        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182        where
183            D: Deserializer<'de>,
184        {
185            if deserializer.is_human_readable() {
186                let authenticator = Authenticator::deserialize(deserializer)?;
187                Self::try_from_raw(authenticator)
188            } else {
189                let bytes: Cow<'de, [u8]> = Bytes::deserialize_as(deserializer)?;
190                Self::from_serialized_bytes(bytes)
191            }
192            .map_err(serde::de::Error::custom)
193        }
194    }
195
196    impl PasskeyAuthenticator {
197        pub fn new(
198            authenticator_data: Vec<u8>,
199            client_data_json: String,
200            signature: SimpleSignature,
201        ) -> Option<Self> {
202            Self::try_from_raw(Authenticator {
203                authenticator_data,
204                client_data_json,
205                signature,
206            })
207            .ok()
208        }
209
210        fn try_from_raw(
211            Authenticator {
212                authenticator_data,
213                client_data_json,
214                signature,
215            }: Authenticator,
216        ) -> Result<Self, SignatureFromBytesError> {
217            let SimpleSignature::Secp256r1 {
218                signature,
219                public_key,
220            } = signature
221            else {
222                return Err(SignatureFromBytesError::new(
223                    "expected passkey with secp256r1 signature",
224                ));
225            };
226
227            let CollectedClientData {
228                ty: _,
229                challenge,
230                origin: _,
231            } = serde_json::from_str(&client_data_json).map_err(SignatureFromBytesError::new)?;
232
233            // decode unpadded url endoded base64 data per spec:
234            // https://w3c.github.io/webauthn/#base64url-encoding
235            let challenge = <base64ct::Base64UrlUnpadded as base64ct::Encoding>::decode_vec(
236                &challenge,
237            )
238            .map_err(|e| {
239                SignatureFromBytesError::new(format!(
240                    "unable to decode base64urlunpadded into 3-byte intent and 32-byte digest: {e}"
241                ))
242            })?;
243
244            Ok(Self {
245                public_key,
246                signature,
247                challenge,
248                authenticator_data,
249                client_data_json,
250            })
251        }
252
253        pub fn from_serialized_bytes(
254            bytes: impl AsRef<[u8]>,
255        ) -> Result<Self, SignatureFromBytesError> {
256            let bytes = bytes.as_ref();
257            let flag =
258                SignatureScheme::from_byte(*bytes.first().ok_or_else(|| {
259                    SignatureFromBytesError::new("missing signature scheme flag")
260                })?)
261                .map_err(SignatureFromBytesError::new)?;
262            if flag != SignatureScheme::Passkey {
263                return Err(SignatureFromBytesError::new("invalid passkey flag"));
264            }
265            let bcs_bytes = &bytes[1..];
266
267            let authenticator = bcs::from_bytes(bcs_bytes).map_err(SignatureFromBytesError::new)?;
268
269            Self::try_from_raw(authenticator)
270        }
271
272        pub(crate) fn to_bytes(&self) -> Vec<u8> {
273            let authenticator_ref = AuthenticatorRef {
274                authenticator_data: &self.authenticator_data,
275                client_data_json: &self.client_data_json,
276                signature: SimpleSignature::Secp256r1 {
277                    signature: self.signature,
278                    public_key: self.public_key,
279                },
280            };
281
282            let mut buf = Vec::new();
283            buf.push(SignatureScheme::Passkey as u8);
284
285            bcs::serialize_into(&mut buf, &authenticator_ref).expect("serialization cannot fail");
286            buf
287        }
288    }
289
290    /// The client data represents the contextual bindings of both the Relying
291    /// Party and the client. It is a key-value mapping whose keys are
292    /// strings. Values can be any type that has a valid encoding in JSON.
293    ///
294    /// > Note: The [`CollectedClientData`] may be extended in the future.
295    /// > Therefore it’s critical when
296    /// > parsing to be tolerant of unknown keys and of any reordering of the
297    /// > keys
298    ///
299    /// This struct conforms to the JSON byte serialization format expected of
300    /// `CollectedClientData`, detailed in section [5.8.1.1 Serialization]
301    /// of the WebAuthn spec. Namely the following requirements:
302    ///
303    /// * `type`, `challenge`, `origin`, `crossOrigin` must always be present in
304    ///   the serialized format _in that order_.
305    ///
306    /// <https://w3c.github.io/webauthn/#dictionary-client-data>
307    ///
308    /// [5.8.1.1 Serialization]: https://w3c.github.io/webauthn/#clientdatajson-serialization
309    #[derive(Debug, Clone, Serialize, Deserialize)]
310    #[serde(rename_all = "camelCase")]
311    pub struct CollectedClientData {
312        /// This member contains the value [`ClientDataType::Create`] when
313        /// creating new credentials, and [`ClientDataType::Get`] when
314        /// getting an assertion from an existing credential. The purpose
315        /// of this member is to prevent certain types of signature confusion
316        /// attacks (where an attacker  substitutes one legitimate
317        /// signature for another).
318        #[serde(rename = "type")]
319        pub ty: ClientDataType,
320        /// This member contains the base64url encoding of the challenge
321        /// provided by the Relying Party. See the [Cryptographic
322        /// Challenges] security consideration.
323        ///
324        /// [Cryptographic Challenges]: https://w3c.github.io/webauthn/#sctn-cryptographic-challenges
325        ///
326        /// https://w3c.github.io/webauthn/#base64url-encoding
327        ///
328        /// The term Base64url Encoding refers to the base64 encoding using the
329        /// URL- and filename-safe character set defined in Section 5 of
330        /// [RFC4648], with all trailing '=' characters omitted
331        /// (as permitted by Section 3.2) and without the inclusion of any line
332        /// breaks, whitespace, or other additional characters.
333        pub challenge: String,
334        /// This member contains the fully qualified origin of the requester, as
335        /// provided to the authenticator by the client, in the syntax
336        /// defined by [RFC6454].
337        ///
338        /// [RFC6454]: https://www.rfc-editor.org/rfc/rfc6454
339        pub origin: String,
340        // /// This OPTIONAL member contains the inverse of the sameOriginWithAncestors argument
341        // value that /// was passed into the internal method
342        // #[serde(default, serialize_with = "truthiness")]
343        // #[serde(rename = "type")]
344        // pub cross_origin: Option<bool>,
345    }
346
347    /// Used to limit the values of [`CollectedClientData::ty`] and serializes
348    /// to static strings.
349    #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
350    pub enum ClientDataType {
351        /// Serializes to the string `"webauthn.get"`
352        ///
353        /// Passkey's in IOTA only support the value `"webauthn.get"`, other
354        /// values will be rejected.
355        #[serde(rename = "webauthn.get")]
356        Get,
357        // /// Serializes to the string `"webauthn.create"`
358        // #[serde(rename = "webauthn.create")]
359        // Create,
360        // /// Serializes to the string `"payment.get"`
361        // /// This variant is part of the Secure Payment Confirmation specification
362        // ///
363        // /// See <https://www.w3.org/TR/secure-payment-confirmation/#client-extension-processing-authentication>
364        // #[serde(rename = "payment.get")]
365        // PaymentGet,
366    }
367}
368
369#[cfg(feature = "proptest")]
370impl proptest::arbitrary::Arbitrary for PasskeyAuthenticator {
371    type Parameters = ();
372    type Strategy = proptest::strategy::BoxedStrategy<Self>;
373
374    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
375        use proptest::{collection::vec, prelude::*};
376        use serialization::{ClientDataType, CollectedClientData};
377
378        (
379            any::<Secp256r1PublicKey>(),
380            any::<Secp256r1Signature>(),
381            vec(any::<u8>(), 32),
382            vec(any::<u8>(), 0..32),
383        )
384            .prop_map(
385                |(public_key, signature, challenge_bytes, authenticator_data)| {
386                    let challenge =
387                        <base64ct::Base64UrlUnpadded as base64ct::Encoding>::encode_string(
388                            &challenge_bytes,
389                        );
390                    let client_data_json = serde_json::to_string(&CollectedClientData {
391                        ty: ClientDataType::Get,
392                        challenge,
393                        origin: "http://example.com".to_owned(),
394                    })
395                    .unwrap();
396
397                    Self {
398                        public_key,
399                        signature,
400                        challenge: challenge_bytes,
401                        authenticator_data,
402                        client_data_json,
403                    }
404                },
405            )
406            .boxed()
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use crate::UserSignature;
413
414    #[test]
415    fn base64_encoded_passkey_user_signature() {
416        let b64 = "BiVYDmenOnqS+thmz5m5SrZnWaKXZLVxgh+rri6LHXs25B0AAAAAnQF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCAiY2hhbGxlbmdlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyIsImNyb3NzT3JpZ2luIjpmYWxzZSwgInVua25vd24iOiAidW5rbm93biJ9YgJMwqcOmZI7F/N+K5SMe4DRYCb4/cDWW68SFneSHoD2GxKKhksbpZ5rZpdrjSYABTCsFQQBpLORzTvbj4edWKd/AsEBeovrGvHR9Ku7critg6k7qvfFlPUngujXfEzXd8Eg";
417
418        let sig = UserSignature::from_base64(b64).unwrap();
419        assert!(matches!(sig, UserSignature::Passkey(_)));
420    }
421}