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