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}