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 =
230                <base64ct::Base64UrlUnpadded as base64ct::Encoding>::decode_vec(&challenge)
231                    .map_err(|e| {
232                        serde::de::Error::custom(format!(
233                    "unable to decode base64urlunpadded into 3-byte intent and 32-byte digest: {e}"
234                ))
235                    })?;
236
237            Ok(Self {
238                public_key,
239                signature,
240                challenge,
241                authenticator_data,
242                client_data_json,
243            })
244        }
245
246        pub(crate) fn from_serialized_bytes<T: AsRef<[u8]>, E: serde::de::Error>(
247            bytes: T,
248        ) -> Result<Self, E> {
249            let bytes = bytes.as_ref();
250            let flag = SignatureScheme::from_byte(
251                *bytes
252                    .first()
253                    .ok_or_else(|| serde::de::Error::custom("missing signature scheme flag"))?,
254            )
255            .map_err(serde::de::Error::custom)?;
256            if flag != SignatureScheme::Passkey {
257                return Err(serde::de::Error::custom("invalid passkey flag"));
258            }
259            let bcs_bytes = &bytes[1..];
260
261            let authenticator = bcs::from_bytes(bcs_bytes).map_err(serde::de::Error::custom)?;
262
263            Self::try_from_raw(authenticator)
264        }
265
266        pub(crate) fn to_bytes(&self) -> Vec<u8> {
267            let authenticator_ref = AuthenticatorRef {
268                authenticator_data: &self.authenticator_data,
269                client_data_json: &self.client_data_json,
270                signature: SimpleSignature::Secp256r1 {
271                    signature: self.signature,
272                    public_key: self.public_key,
273                },
274            };
275
276            let mut buf = Vec::new();
277            buf.push(SignatureScheme::Passkey as u8);
278
279            bcs::serialize_into(&mut buf, &authenticator_ref).expect("serialization cannot fail");
280            buf
281        }
282    }
283
284    /// The client data represents the contextual bindings of both the Relying Party and the client.
285    /// It is a key-value mapping whose keys are strings. Values can be any type that has a valid
286    /// encoding in JSON.
287    ///
288    /// > Note: The [`CollectedClientData`] may be extended in the future. Therefore it’s critical when
289    /// >       parsing to be tolerant of unknown keys and of any reordering of the keys
290    ///
291    /// This struct conforms to the JSON byte serialization format expected of `CollectedClientData`,
292    /// detailed in section [5.8.1.1 Serialization] of the WebAuthn spec. Namely the following
293    /// requirements:
294    ///
295    /// * `type`, `challenge`, `origin`, `crossOrigin` must always be present in the serialized format
296    ///   _in that order_.
297    ///
298    /// <https://w3c.github.io/webauthn/#dictionary-client-data>
299    ///
300    /// [5.8.1.1 Serialization]: https://w3c.github.io/webauthn/#clientdatajson-serialization
301    #[derive(Debug, Clone, Serialize, Deserialize)]
302    #[serde(rename_all = "camelCase")]
303    pub struct CollectedClientData {
304        /// This member contains the value [`ClientDataType::Create`] when creating new credentials, and
305        /// [`ClientDataType::Get`] when getting an assertion from an existing credential. The purpose
306        /// of this member is to prevent certain types of signature confusion attacks (where an attacker
307        ///  substitutes one legitimate signature for another).
308        #[serde(rename = "type")]
309        pub ty: ClientDataType,
310
311        /// This member contains the base64url encoding of the challenge provided by the Relying Party.
312        /// See the [Cryptographic Challenges] security consideration.
313        ///
314        /// [Cryptographic Challenges]: https://w3c.github.io/webauthn/#sctn-cryptographic-challenges
315        ///
316        /// https://w3c.github.io/webauthn/#base64url-encoding
317        ///
318        /// The term Base64url Encoding refers to the base64 encoding using the URL- and filename-safe
319        /// character set defined in Section 5 of [RFC4648], with all trailing '=' characters omitted
320        /// (as permitted by Section 3.2) and without the inclusion of any line breaks, whitespace, or
321        /// other additional characters.
322        pub challenge: String,
323
324        /// This member contains the fully qualified origin of the requester, as provided to the
325        /// authenticator by the client, in the syntax defined by [RFC6454].
326        ///
327        /// [RFC6454]: https://www.rfc-editor.org/rfc/rfc6454
328        pub origin: String,
329        // /// This OPTIONAL member contains the inverse of the sameOriginWithAncestors argument value that
330        // /// was passed into the internal method
331        // #[serde(default, serialize_with = "truthiness")]
332        // #[serde(rename = "type")]
333        // pub cross_origin: Option<bool>,
334    }
335
336    /// Used to limit the values of [`CollectedClientData::ty`] and serializes to static strings.
337    #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
338    pub enum ClientDataType {
339        /// Serializes to the string `"webauthn.get"`
340        ///
341        /// Passkey's in Sui only support the value `"webauthn.get"`, other values will be rejected.
342        #[serde(rename = "webauthn.get")]
343        Get,
344        // /// Serializes to the string `"webauthn.create"`
345        // #[serde(rename = "webauthn.create")]
346        // Create,
347        // /// Serializes to the string `"payment.get"`
348        // /// This variant is part of the Secure Payment Confirmation specification
349        // ///
350        // /// See <https://www.w3.org/TR/secure-payment-confirmation/#client-extension-processing-authentication>
351        // #[serde(rename = "payment.get")]
352        // PaymentGet,
353    }
354}
355
356#[cfg(feature = "proptest")]
357impl proptest::arbitrary::Arbitrary for PasskeyAuthenticator {
358    type Parameters = ();
359    type Strategy = proptest::strategy::BoxedStrategy<Self>;
360
361    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
362        use proptest::collection::vec;
363        use proptest::prelude::*;
364        use serialization::ClientDataType;
365        use serialization::CollectedClientData;
366
367        (
368            any::<Secp256r1PublicKey>(),
369            any::<Secp256r1Signature>(),
370            vec(any::<u8>(), 32),
371            vec(any::<u8>(), 0..32),
372        )
373            .prop_map(
374                |(public_key, signature, challenge_bytes, authenticator_data)| {
375                    let challenge =
376                        <base64ct::Base64UrlUnpadded as base64ct::Encoding>::encode_string(
377                            &challenge_bytes,
378                        );
379                    let client_data_json = serde_json::to_string(&CollectedClientData {
380                        ty: ClientDataType::Get,
381                        challenge,
382                        origin: "http://example.com".to_owned(),
383                    })
384                    .unwrap();
385
386                    Self {
387                        public_key,
388                        signature,
389                        challenge: challenge_bytes,
390                        authenticator_data,
391                        client_data_json,
392                    }
393                },
394            )
395            .boxed()
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use crate::UserSignature;
402
403    #[test]
404    fn base64_encoded_passkey_user_signature() {
405        let b64 = "BiVYDmenOnqS+thmz5m5SrZnWaKXZLVxgh+rri6LHXs25B0AAAAAnQF7InR5cGUiOiJ3ZWJhdXRobi5nZXQiLCAiY2hhbGxlbmdlIjoiQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MyIsImNyb3NzT3JpZ2luIjpmYWxzZSwgInVua25vd24iOiAidW5rbm93biJ9YgJMwqcOmZI7F/N+K5SMe4DRYCb4/cDWW68SFneSHoD2GxKKhksbpZ5rZpdrjSYABTCsFQQBpLORzTvbj4edWKd/AsEBeovrGvHR9Ku7critg6k7qvfFlPUngujXfEzXd8Eg";
406
407        let sig = UserSignature::from_base64(b64).unwrap();
408        assert!(matches!(sig, UserSignature::Passkey(_)));
409    }
410}