sui_sdk_types/crypto/
passkey.rs

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