defuse_core/payload/
webauthn.rs1use defuse_digest::{Digest, sha2::Sha256};
2pub use defuse_webauthn::WebauthnAssertion;
3use defuse_webauthn::{IgnoreUserVerification, ed25519::Ed25519, p256::P256};
4use serde::{Deserialize, Serialize, de::DeserializeOwned};
5
6use crate::{
7 PublicKey, Signature,
8 payload::{Payload, SignedPayload},
9};
10
11use super::{DefusePayload, ExtractDefusePayload};
12
13#[cfg_attr(feature = "schemars-v0_8", derive(::schemars::JsonSchema))]
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SignedWebAuthnPayload {
16 pub payload: String,
17
18 #[serde(flatten)]
19 pub assertion: WebauthnAssertion,
20
21 pub public_key: PublicKey,
22 pub signature: Signature,
23}
24
25impl Payload for SignedWebAuthnPayload {
26 #[inline]
27 fn hash(&self) -> [u8; 32] {
28 Sha256::digest(self.payload.as_bytes()).into()
29 }
30}
31
32impl SignedPayload for SignedWebAuthnPayload {
33 type PublicKey = PublicKey;
34
35 #[inline]
36 fn verify(&self) -> Option<Self::PublicKey> {
37 type Webauthn<A> = defuse_webauthn::Webauthn<
38 A,
39 IgnoreUserVerification,
46 >;
47
48 match (self.public_key, self.signature) {
49 (PublicKey::Ed25519(pk), Signature::Ed25519(sig)) => Webauthn::<Ed25519>::verify(
50 &pk.try_into().ok()?,
51 self.hash(),
52 &self.assertion,
53 &sig.into(),
54 ),
55 (PublicKey::P256(pk), Signature::P256(sig)) => Webauthn::<P256>::verify(
56 &pk.try_into().ok()?,
57 self.hash(),
58 &self.assertion,
59 &sig.try_into().ok()?,
60 ),
61 _ => false,
62 }
63 .then_some(&self.public_key)
64 .copied()
65 }
66}
67
68impl<T> ExtractDefusePayload<T> for SignedWebAuthnPayload
69where
70 T: DeserializeOwned,
71{
72 type Error = serde_json::Error;
73
74 #[inline]
75 fn extract_defuse_payload(self) -> Result<DefusePayload<T>, Self::Error> {
76 serde_json::from_str(&self.payload)
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use crate::{AccountIdRef, intents::DefuseIntents};
83
84 use super::*;
85
86 #[test]
87 fn p256() {
88 const SIGNER_ID: &AccountIdRef =
89 AccountIdRef::new_or_panic("0x3602b546589a8fcafdce7fad64a46f91db0e4d50");
90
91 let p: SignedWebAuthnPayload = serde_json::from_str(r#"{
92 "standard": "webauthn",
93 "payload": "{\"signer_id\":\"0x3602b546589a8fcafdce7fad64a46f91db0e4d50\",\"verifying_contract\":\"defuse.test.near\",\"deadline\":\"2025-03-30T00:00:00Z\",\"nonce\":\"A3nsY1GMVjzyXL3mUzOOP3KT+5a0Ruy+QDNWPhchnxM=\",\"intents\":[{\"intent\":\"transfer\",\"receiver_id\":\"user1.test.near\",\"tokens\":{\"nep141:ft1.poa-factory.test.near\":\"1000\"}}]}",
94 "public_key": "p256:2V8Np9vGqLiwVZ8qmMmpkxU7CTRqje4WtwFeLimSwuuyF1rddQK5fELiMgxUnYbVjbZHCNnGc6fAe4JeDcVxgj3Q",
95 "signature": "p256:3KBMZ72BHUiVfE1ey5dpi3KgbXvSEf9kuxgBEax7qLBQtidZExxxjjQk1hTTGFRrPvUoEStfrjoFNVVW4Abar94W",
96 "client_data_json": "{\"type\":\"webauthn.get\",\"challenge\":\"4cveZsIe6p-WaEcL-Lhtzt3SZuXbYsjDdlFhLNrSjjk\",\"origin\":\"https://defuse-widget-git-feat-passkeys-defuse-94bbc1b2.vercel.app\"}",
97 "authenticator_data": "933cQogpBzE3RSAYSAkfWoNEcBd3X84PxE8iRrRVxMgdAAAAAA=="
98}"#).unwrap();
99
100 let public_key = p.verify().expect("invalid signature");
101 assert_eq!(
102 public_key,
103 "p256:2V8Np9vGqLiwVZ8qmMmpkxU7CTRqje4WtwFeLimSwuuyF1rddQK5fELiMgxUnYbVjbZHCNnGc6fAe4JeDcVxgj3Q"
104 .parse()
105 .unwrap(),
106 );
107 assert_eq!(public_key.to_implicit_account_id(), SIGNER_ID);
108
109 let dp: DefusePayload<DefuseIntents> = p.extract_defuse_payload().unwrap();
110 dbg!(&dp);
111 assert_eq!(dp.signer_id, SIGNER_ID);
112 }
113
114 #[test]
115 fn ed25519() {
116 const SIGNER_ID: &AccountIdRef = AccountIdRef::new_or_panic(
117 "19a8cd22b37802c3cbc0031f55c70f3858ac48dbfb7697c435da637fea0e0e47",
118 );
119
120 let p: SignedWebAuthnPayload = serde_json::from_str(r#" {
121 "standard": "webauthn",
122 "payload": "{\"signer_id\":\"19a8cd22b37802c3cbc0031f55c70f3858ac48dbfb7697c435da637fea0e0e47\",\"verifying_contract\":\"intents.near\",\"deadline\":{\"timestamp\":1732035219},\"nonce\":\"XVoKfmScb3G+XqH9ke/fSlJ/3xO59sNhCxhpG821BH8=\",\"intents\":[{\"intent\":\"token_diff\",\"diff\":{\"nep141:base-0x833589fcd6edb6e08f4c7c32d4f71b54bda02913.omft.near\":\"-1000\",\"nep141:eth-0xdac17f958d2ee523a2206206994597c13d831ec7.omft.near\":\"998\"}}]}",
123 "public_key": "ed25519:2jAUugnvWPvMaftKj5TDkyfsfxBwYjkMSf5MRtqDUMHY",
124 "signature": "ed25519:2yBp5oExa9BBZQf8habpjLUaSiprvT7srHrK38Bxt9zL1yrkQSeeXMLmkihKCd9frmTdk24YctUdzNN5nGqHWHgb",
125 "client_data_json": "{\"type\":\"webauthn.get\",\"challenge\":\"PfRFOFrLxCfyomuDryxhv6v2OzJIWqyMXaMikUYHSmY\",\"origin\":\"http://localhost:3000\"}",
126 "authenticator_data": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFZ50DuA"
127}"#).unwrap();
128
129 let public_key = p.verify().expect("invalid signature");
130 assert_eq!(
131 public_key,
132 "ed25519:2jAUugnvWPvMaftKj5TDkyfsfxBwYjkMSf5MRtqDUMHY"
133 .parse()
134 .unwrap(),
135 );
136 assert_eq!(public_key.to_implicit_account_id(), SIGNER_ID);
137 }
138}