defuse_core/payload/
webauthn.rs

1use defuse_crypto::{
2    Ed25519PublicKey, Ed25519Signature, P256Signature, Payload, SignedPayload, compress_public_key,
3};
4use defuse_digest::{Digest, Sha256};
5use defuse_webauthn::{Algorithm, Ed25519, P256, PayloadSignature, UserVerification};
6use near_sdk::{CryptoHash, near, serde::de::DeserializeOwned, serde_json};
7
8use crate::{PublicKey, Signature};
9
10use super::{DefusePayload, ExtractDefusePayload};
11
12#[near(serializers = [json])]
13#[derive(Debug, Clone)]
14pub struct SignedWebAuthnPayload {
15    pub payload: String,
16    pub public_key: PublicKey,
17    // schemars@0.8 does not respect it's `schemars(bound = "...")`
18    // attribute: https://github.com/GREsau/schemars/blob/104b0fd65055d4b46f8dcbe38cdd2ef2c4098fe2/schemars_derive/src/lib.rs#L193-L206
19    #[cfg_attr(feature = "abi", schemars(skip))]
20    #[serde(flatten)]
21    pub signature: PayloadSignature<Ed25519OrP256>,
22}
23
24impl Payload for SignedWebAuthnPayload {
25    #[inline]
26    fn hash(&self) -> CryptoHash {
27        Sha256::digest(self.payload.as_bytes()).into()
28    }
29}
30
31#[derive(Debug, Clone)]
32pub struct Ed25519OrP256;
33
34impl Algorithm for Ed25519OrP256 {
35    type PublicKey = PublicKey;
36
37    type Signature = Signature;
38
39    #[inline]
40    fn verify(msg: &[u8], public_key: &Self::PublicKey, signature: &Self::Signature) -> bool {
41        match (public_key, signature) {
42            (PublicKey::Ed25519(public_key), Signature::Ed25519(signature)) => Ed25519::verify(
43                msg,
44                &Ed25519PublicKey(*public_key),
45                &Ed25519Signature(*signature),
46            ),
47
48            (PublicKey::P256(public_key), Signature::P256(signature)) => P256::verify(
49                msg,
50                &compress_public_key(*public_key),
51                &P256Signature(*signature),
52            ),
53
54            _ => false,
55        }
56    }
57}
58
59impl SignedPayload for SignedWebAuthnPayload {
60    type PublicKey = PublicKey;
61
62    #[inline]
63    fn verify(&self) -> Option<Self::PublicKey> {
64        self.signature
65            .verify(self.hash(), &self.public_key, UserVerification::Ignore)
66            .then_some(&self.public_key)
67            .copied()
68    }
69}
70
71impl<T> ExtractDefusePayload<T> for SignedWebAuthnPayload
72where
73    T: DeserializeOwned,
74{
75    type Error = serde_json::Error;
76
77    #[inline]
78    fn extract_defuse_payload(self) -> Result<DefusePayload<T>, Self::Error> {
79        serde_json::from_str(&self.payload)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use near_sdk::{AccountIdRef, serde_json};
87
88    #[test]
89    fn p256() {
90        let p: SignedWebAuthnPayload = serde_json::from_str(r#"{
91  "standard": "webauthn",
92  "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\"}}]}",
93  "public_key": "p256:2V8Np9vGqLiwVZ8qmMmpkxU7CTRqje4WtwFeLimSwuuyF1rddQK5fELiMgxUnYbVjbZHCNnGc6fAe4JeDcVxgj3Q",
94  "signature": "p256:3KBMZ72BHUiVfE1ey5dpi3KgbXvSEf9kuxgBEax7qLBQtidZExxxjjQk1hTTGFRrPvUoEStfrjoFNVVW4Abar94W",
95  "client_data_json": "{\"type\":\"webauthn.get\",\"challenge\":\"4cveZsIe6p-WaEcL-Lhtzt3SZuXbYsjDdlFhLNrSjjk\",\"origin\":\"https://defuse-widget-git-feat-passkeys-defuse-94bbc1b2.vercel.app\"}",
96  "authenticator_data": "933cQogpBzE3RSAYSAkfWoNEcBd3X84PxE8iRrRVxMgdAAAAAA=="
97}"#).unwrap();
98
99        let public_key = p.verify().expect("invalid signature");
100        assert_eq!(
101            public_key,
102            "p256:2V8Np9vGqLiwVZ8qmMmpkxU7CTRqje4WtwFeLimSwuuyF1rddQK5fELiMgxUnYbVjbZHCNnGc6fAe4JeDcVxgj3Q"
103                .parse()
104                .unwrap(),
105        );
106        assert_eq!(
107            public_key.to_implicit_account_id(),
108            AccountIdRef::new_or_panic("0x3602b546589a8fcafdce7fad64a46f91db0e4d50")
109        );
110    }
111
112    #[test]
113    fn ed25519() {
114        let p: SignedWebAuthnPayload = serde_json::from_str(r#" {
115  "standard": "webauthn",
116  "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\"}}]}",
117  "public_key": "ed25519:2jAUugnvWPvMaftKj5TDkyfsfxBwYjkMSf5MRtqDUMHY",
118  "signature": "ed25519:2yBp5oExa9BBZQf8habpjLUaSiprvT7srHrK38Bxt9zL1yrkQSeeXMLmkihKCd9frmTdk24YctUdzNN5nGqHWHgb",
119  "client_data_json": "{\"type\":\"webauthn.get\",\"challenge\":\"PfRFOFrLxCfyomuDryxhv6v2OzJIWqyMXaMikUYHSmY\",\"origin\":\"http://localhost:3000\"}",
120  "authenticator_data": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFZ50DuA"
121}"#).unwrap();
122
123        let public_key = p.verify().expect("invalid signature");
124        assert_eq!(
125            public_key,
126            "ed25519:2jAUugnvWPvMaftKj5TDkyfsfxBwYjkMSf5MRtqDUMHY"
127                .parse()
128                .unwrap(),
129        );
130        assert_eq!(
131            public_key.to_implicit_account_id(),
132            AccountIdRef::new_or_panic(
133                "19a8cd22b37802c3cbc0031f55c70f3858ac48dbfb7697c435da637fea0e0e47"
134            )
135        );
136    }
137}