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