Skip to main content

defuse_core/payload/
webauthn.rs

1use 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            // `UV` (User Verified) flag is only set by FIDO2-capable devices with
41            // PIN / biometric setup.
42            //
43            // FIDO U2F (CTAP 1) authenticators (such as old Ledger and Yubikey
44            // devices) only set `UP` (User Present) flag and doesn't support `UV`
45            // (User Verified).
46            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}