defuse_core/payload/
webauthn.rs1use defuse_crypto::{
2 Ed25519PublicKey, Ed25519Signature, P256Signature, Payload, PublicKey, Signature,
3 SignedPayload, compress_public_key,
4};
5use defuse_webauthn::{Algorithm, Ed25519, P256, PayloadSignature, UserVerification};
6use near_sdk::{CryptoHash, env, near, serde::de::DeserializeOwned, serde_json};
7
8use super::{DefusePayload, ExtractDefusePayload};
9
10#[near(serializers = [json])]
11#[derive(Debug, Clone)]
12pub struct SignedWebAuthnPayload {
13 pub payload: String,
14 pub public_key: PublicKey,
15 #[cfg_attr(all(feature = "abi", not(target_arch = "wasm32")), schemars(skip))]
18 #[serde(flatten)]
19 pub signature: PayloadSignature<Ed25519OrP256>,
20}
21
22impl Payload for SignedWebAuthnPayload {
23 #[inline]
24 fn hash(&self) -> CryptoHash {
25 env::sha256_array(self.payload.as_bytes())
26 }
27}
28
29#[derive(Debug, Clone)]
30pub struct Ed25519OrP256;
31
32impl Algorithm for Ed25519OrP256 {
33 type PublicKey = PublicKey;
34
35 type Signature = Signature;
36
37 #[inline]
38 fn verify(msg: &[u8], public_key: &Self::PublicKey, signature: &Self::Signature) -> bool {
39 match (public_key, signature) {
40 (PublicKey::Ed25519(public_key), Signature::Ed25519(signature)) => Ed25519::verify(
41 msg,
42 &Ed25519PublicKey(*public_key),
43 &Ed25519Signature(*signature),
44 ),
45
46 (PublicKey::P256(public_key), Signature::P256(signature)) => P256::verify(
47 msg,
48 &compress_public_key(*public_key),
49 &P256Signature(*signature),
50 ),
51
52 _ => false,
53 }
54 }
55}
56
57impl SignedPayload for SignedWebAuthnPayload {
58 type PublicKey = PublicKey;
59
60 #[inline]
61 fn verify(&self) -> Option<Self::PublicKey> {
62 self.signature
63 .verify(self.hash(), &self.public_key, UserVerification::Ignore)
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 super::*;
84 use near_sdk::{AccountIdRef, serde_json};
85
86 #[test]
87 fn p256() {
88 let p: SignedWebAuthnPayload = serde_json::from_str(r#"{
89 "standard": "webauthn",
90 "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\"}}]}",
91 "public_key": "p256:2V8Np9vGqLiwVZ8qmMmpkxU7CTRqje4WtwFeLimSwuuyF1rddQK5fELiMgxUnYbVjbZHCNnGc6fAe4JeDcVxgj3Q",
92 "signature": "p256:3KBMZ72BHUiVfE1ey5dpi3KgbXvSEf9kuxgBEax7qLBQtidZExxxjjQk1hTTGFRrPvUoEStfrjoFNVVW4Abar94W",
93 "client_data_json": "{\"type\":\"webauthn.get\",\"challenge\":\"4cveZsIe6p-WaEcL-Lhtzt3SZuXbYsjDdlFhLNrSjjk\",\"origin\":\"https://defuse-widget-git-feat-passkeys-defuse-94bbc1b2.vercel.app\"}",
94 "authenticator_data": "933cQogpBzE3RSAYSAkfWoNEcBd3X84PxE8iRrRVxMgdAAAAAA=="
95}"#).unwrap();
96
97 let public_key = p.verify().expect("invalid signature");
98 assert_eq!(
99 public_key,
100 "p256:2V8Np9vGqLiwVZ8qmMmpkxU7CTRqje4WtwFeLimSwuuyF1rddQK5fELiMgxUnYbVjbZHCNnGc6fAe4JeDcVxgj3Q"
101 .parse()
102 .unwrap(),
103 );
104 assert_eq!(
105 public_key.to_implicit_account_id(),
106 AccountIdRef::new_or_panic("0x3602b546589a8fcafdce7fad64a46f91db0e4d50")
107 );
108 }
109
110 #[test]
111 fn ed25519() {
112 let p: SignedWebAuthnPayload = serde_json::from_str(r#" {
113 "standard": "webauthn",
114 "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\"}}]}",
115 "public_key": "ed25519:2jAUugnvWPvMaftKj5TDkyfsfxBwYjkMSf5MRtqDUMHY",
116 "signature": "ed25519:2yBp5oExa9BBZQf8habpjLUaSiprvT7srHrK38Bxt9zL1yrkQSeeXMLmkihKCd9frmTdk24YctUdzNN5nGqHWHgb",
117 "client_data_json": "{\"type\":\"webauthn.get\",\"challenge\":\"PfRFOFrLxCfyomuDryxhv6v2OzJIWqyMXaMikUYHSmY\",\"origin\":\"http://localhost:3000\"}",
118 "authenticator_data": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFZ50DuA"
119}"#).unwrap();
120
121 let public_key = p.verify().expect("invalid signature");
122 assert_eq!(
123 public_key,
124 "ed25519:2jAUugnvWPvMaftKj5TDkyfsfxBwYjkMSf5MRtqDUMHY"
125 .parse()
126 .unwrap(),
127 );
128 assert_eq!(
129 public_key.to_implicit_account_id(),
130 AccountIdRef::new_or_panic(
131 "19a8cd22b37802c3cbc0031f55c70f3858ac48dbfb7697c435da637fea0e0e47"
132 )
133 );
134 }
135}