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}