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}