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