fedimint_core/net/
guardian_metadata.rs

1use bitcoin::hashes::{Hash, sha256};
2use bitcoin::secp256k1::Message;
3use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
4use serde::{Deserialize, Serialize};
5
6use crate::util::SafeUrl;
7
8const GUARDIAN_METADATA_MESSAGE_TAG: &[u8] = b"fedimint-guardian-metadata";
9/// Allow messages with timestamps up to 1 hour in the future
10const MAX_FUTURE_TIMESTAMP_SECS: u64 = 3600;
11
12#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq)]
13pub struct GuardianMetadata {
14    pub api_urls: Vec<SafeUrl>,
15    /// z-base32 encoded Pkarr id
16    pub pkarr_id_z32: String,
17    pub timestamp_secs: u64,
18}
19
20#[derive(Debug, Clone, Eq, Hash, PartialEq)]
21pub struct SignedGuardianMetadata {
22    /// The raw bytes that were signed (JSON-encoded GuardianMetadata)
23    pub bytes: Vec<u8>,
24    /// The parsed GuardianMetadata value
25    pub value: GuardianMetadata,
26    pub signature: secp256k1::schnorr::Signature,
27}
28
29#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq, Encodable, Decodable)]
30pub struct SignedGuardianMetadataSubmission {
31    #[serde(flatten)]
32    pub signed_guardian_metadata: SignedGuardianMetadata,
33    pub peer_id: crate::PeerId,
34}
35
36// Implement Serialize/Deserialize for SignedGuardianMetadata for JSON
37//
38// Format: {"content": "<json string>", "signature": "<hex-encoded signature>"}
39// The `content` field contains the exact JSON string that was signed (preserved
40// byte-for-byte). The `signature` field contains the hex-encoded Schnorr
41// signature over the content bytes.
42impl Serialize for SignedGuardianMetadata {
43    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44    where
45        S: serde::Serializer,
46    {
47        use serde::ser::SerializeStruct;
48        let mut state = serializer.serialize_struct("SignedGuardianMetadata", 2)?;
49
50        // Serialize bytes as a UTF-8 string (content field)
51        let content = String::from_utf8(self.bytes.clone())
52            .map_err(|e| serde::ser::Error::custom(format!("Invalid UTF-8 in bytes: {e}")))?;
53        state.serialize_field("content", &content)?;
54
55        // Serialize signature as hex string
56        state.serialize_field("signature", &hex::encode(self.signature.as_ref()))?;
57        state.end()
58    }
59}
60
61impl<'de> Deserialize<'de> for SignedGuardianMetadata {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: serde::Deserializer<'de>,
65    {
66        use serde::de::Error;
67
68        #[derive(Deserialize)]
69        struct SignedGuardianMetadataHelper {
70            content: String,
71            signature: String,
72        }
73
74        let helper = SignedGuardianMetadataHelper::deserialize(deserializer)?;
75
76        let bytes = helper.content.into_bytes();
77        let value: GuardianMetadata = serde_json::from_slice(&bytes).map_err(D::Error::custom)?;
78        let signature_bytes = hex::decode(&helper.signature).map_err(D::Error::custom)?;
79        let signature = secp256k1::schnorr::Signature::from_slice(&signature_bytes)
80            .map_err(D::Error::custom)?;
81
82        Ok(Self {
83            bytes,
84            value,
85            signature,
86        })
87    }
88}
89
90// Implement Encodable/Decodable for SignedGuardianMetadata only
91impl Encodable for SignedGuardianMetadata {
92    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
93        // Encode the bytes and signature (value is derived from bytes)
94        self.bytes.consensus_encode(writer)?;
95        self.signature.consensus_encode(writer)?;
96        Ok(())
97    }
98}
99
100impl Decodable for SignedGuardianMetadata {
101    fn consensus_decode_partial_from_finite_reader<R: std::io::Read>(
102        reader: &mut R,
103        modules: &fedimint_core::module::registry::ModuleDecoderRegistry,
104    ) -> Result<Self, DecodeError> {
105        let bytes = Vec::<u8>::consensus_decode_partial_from_finite_reader(reader, modules)?;
106        let value: GuardianMetadata = serde_json::from_slice(&bytes)
107            .map_err(|e| DecodeError::new_custom(anyhow::anyhow!("Invalid JSON: {e}")))?;
108        let signature = secp256k1::schnorr::Signature::consensus_decode_partial_from_finite_reader(
109            reader, modules,
110        )?;
111
112        Ok(Self {
113            bytes,
114            value,
115            signature,
116        })
117    }
118}
119
120fn compute_tagged_hash(json_bytes: &[u8]) -> sha256::Hash {
121    use bitcoin::hashes::HashEngine;
122    let mut engine = sha256::HashEngine::default();
123    engine.input(GUARDIAN_METADATA_MESSAGE_TAG);
124    engine.input(json_bytes);
125    sha256::Hash::from_engine(engine)
126}
127
128#[derive(Debug, thiserror::Error)]
129pub enum VerificationError {
130    #[error("Invalid signature")]
131    InvalidSignature,
132    #[error("Timestamp {timestamp_secs} is too far in the future (max allowed: {max_allowed})")]
133    TimestampTooFarInFuture {
134        timestamp_secs: u64,
135        max_allowed: u64,
136    },
137}
138
139impl GuardianMetadata {
140    pub fn new(api_urls: Vec<SafeUrl>, pkarr_id_z32: String, timestamp_secs: u64) -> Self {
141        Self {
142            api_urls,
143            pkarr_id_z32,
144            timestamp_secs,
145        }
146    }
147
148    pub fn sign<C: secp256k1::Signing>(
149        &self,
150        ctx: &secp256k1::Secp256k1<C>,
151        key: &secp256k1::Keypair,
152    ) -> SignedGuardianMetadata {
153        // Serialize to JSON and compute tagged hash
154        let bytes = serde_json::to_vec(self).expect("JSON serialization should not fail");
155        let tagged_hash = compute_tagged_hash(&bytes);
156
157        let msg = Message::from_digest(*tagged_hash.as_ref());
158        let signature = ctx.sign_schnorr(&msg, key);
159
160        SignedGuardianMetadata {
161            bytes,
162            value: self.clone(),
163            signature,
164        }
165    }
166}
167
168impl SignedGuardianMetadata {
169    /// Returns the parsed GuardianMetadata value
170    pub fn guardian_metadata(&self) -> &GuardianMetadata {
171        &self.value
172    }
173
174    /// Compute the tagged hash from the stored bytes
175    pub fn tagged_hash(&self) -> sha256::Hash {
176        compute_tagged_hash(&self.bytes)
177    }
178
179    /// Verifies the signature and timestamp validity.
180    ///
181    /// Returns `Ok(())` if the signature is valid for the given public key and
182    /// the timestamp is not too far in the future relative to `now`.
183    pub fn verify<C: secp256k1::Verification>(
184        &self,
185        ctx: &secp256k1::Secp256k1<C>,
186        pk: &secp256k1::PublicKey,
187        now: std::time::Duration,
188    ) -> Result<(), VerificationError> {
189        // First check the signature
190        let msg = Message::from_digest(*self.tagged_hash().as_ref());
191        ctx.verify_schnorr(&self.signature, &msg, &pk.x_only_public_key().0)
192            .map_err(|_| VerificationError::InvalidSignature)?;
193
194        // Then check the timestamp isn't too far in the future
195        let current_secs = now.as_secs();
196        let max_allowed_timestamp = current_secs.saturating_add(MAX_FUTURE_TIMESTAMP_SECS);
197
198        if max_allowed_timestamp < self.value.timestamp_secs {
199            return Err(VerificationError::TimestampTooFarInFuture {
200                timestamp_secs: self.value.timestamp_secs,
201                max_allowed: max_allowed_timestamp,
202            });
203        }
204
205        Ok(())
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use std::time::Duration;
212
213    use super::*;
214    use crate::module::registry::ModuleRegistry;
215
216    #[test]
217    fn signed_guardian_metadata_json_roundtrip() {
218        let ctx = secp256k1::Secp256k1::new();
219        let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
220        let public_key = secp256k1::PublicKey::from_keypair(&keypair);
221
222        let timestamp_secs = 1000;
223        let metadata = GuardianMetadata::new(
224            vec!["wss://example.com/api".parse().unwrap()],
225            "test_pkarr_id".to_string(),
226            timestamp_secs,
227        );
228
229        let signed = metadata.sign(&ctx, &keypair);
230
231        // Serialize to JSON
232        let json = serde_json::to_string(&signed).expect("serialization should succeed");
233
234        // Verify JSON structure
235        let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
236        assert!(
237            json_value.get("content").is_some(),
238            "should have content field"
239        );
240        assert!(
241            json_value.get("signature").is_some(),
242            "should have signature field"
243        );
244
245        // Deserialize from JSON
246        let deserialized: SignedGuardianMetadata =
247            serde_json::from_str(&json).expect("deserialization should succeed");
248
249        // Compare original and deserialized
250        assert_eq!(signed.bytes, deserialized.bytes);
251        assert_eq!(signed.value, deserialized.value);
252        assert_eq!(signed.signature, deserialized.signature);
253        assert_eq!(signed, deserialized);
254
255        // Verify signature still works after roundtrip
256        let now = Duration::from_secs(timestamp_secs);
257        deserialized
258            .verify(&ctx, &public_key, now)
259            .expect("signature should verify after roundtrip");
260
261        // Verify extracted metadata matches original
262        assert_eq!(*deserialized.guardian_metadata(), metadata);
263    }
264
265    #[test]
266    fn signed_guardian_metadata_encodable_roundtrip() {
267        let ctx = secp256k1::Secp256k1::new();
268        let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
269        let public_key = secp256k1::PublicKey::from_keypair(&keypair);
270
271        let timestamp_secs = 1000;
272        let metadata = GuardianMetadata::new(
273            vec!["wss://example.com/api".parse().unwrap()],
274            "test_pkarr_id".to_string(),
275            timestamp_secs,
276        );
277
278        let signed = metadata.sign(&ctx, &keypair);
279
280        // Encode to bytes
281        let encoded = signed.consensus_encode_to_vec();
282
283        // Decode from bytes
284        let deserialized: SignedGuardianMetadata =
285            Decodable::consensus_decode_whole(&encoded, &ModuleRegistry::default())
286                .expect("decoding should succeed");
287
288        // Compare original and deserialized
289        assert_eq!(signed.bytes, deserialized.bytes);
290        assert_eq!(signed.value, deserialized.value);
291        assert_eq!(signed.signature, deserialized.signature);
292        assert_eq!(signed, deserialized);
293
294        // Verify signature still works after roundtrip
295        let now = Duration::from_secs(timestamp_secs);
296        deserialized
297            .verify(&ctx, &public_key, now)
298            .expect("signature should verify after roundtrip");
299
300        // Verify extracted metadata matches original
301        assert_eq!(*deserialized.guardian_metadata(), metadata);
302    }
303
304    #[test]
305    fn verify_valid_signature_and_timestamp() {
306        let ctx = secp256k1::Secp256k1::new();
307        let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
308        let public_key = secp256k1::PublicKey::from_keypair(&keypair);
309
310        let timestamp_secs = 10000;
311        let metadata = GuardianMetadata::new(
312            vec!["wss://example.com/api".parse().unwrap()],
313            "test_pkarr_id".to_string(),
314            timestamp_secs,
315        );
316        let signed = metadata.sign(&ctx, &keypair);
317
318        // Verify succeeds when now == timestamp
319        signed
320            .verify(&ctx, &public_key, Duration::from_secs(timestamp_secs))
321            .expect("should verify with matching timestamp");
322
323        // Verify succeeds when now is after timestamp (metadata from the past)
324        signed
325            .verify(
326                &ctx,
327                &public_key,
328                Duration::from_secs(timestamp_secs + 1000),
329            )
330            .expect("should verify with past timestamp");
331
332        // Verify succeeds when timestamp is slightly in the future (within allowed
333        // window)
334        signed
335            .verify(
336                &ctx,
337                &public_key,
338                Duration::from_secs(timestamp_secs - MAX_FUTURE_TIMESTAMP_SECS),
339            )
340            .expect("should verify when timestamp is within allowed future window");
341    }
342
343    #[test]
344    fn verify_rejects_invalid_signature() {
345        let ctx = secp256k1::Secp256k1::new();
346        let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
347        let wrong_keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
348        let wrong_public_key = secp256k1::PublicKey::from_keypair(&wrong_keypair);
349
350        let timestamp_secs = 1000;
351        let metadata = GuardianMetadata::new(
352            vec!["wss://example.com/api".parse().unwrap()],
353            "test_pkarr_id".to_string(),
354            timestamp_secs,
355        );
356        let signed = metadata.sign(&ctx, &keypair);
357
358        // Verify fails with wrong public key
359        let result = signed.verify(&ctx, &wrong_public_key, Duration::from_secs(timestamp_secs));
360        assert!(
361            matches!(result, Err(VerificationError::InvalidSignature)),
362            "should reject invalid signature"
363        );
364    }
365
366    #[test]
367    fn verify_rejects_timestamp_too_far_in_future() {
368        let ctx = secp256k1::Secp256k1::new();
369        let keypair = secp256k1::Keypair::new(&ctx, &mut secp256k1::rand::thread_rng());
370        let public_key = secp256k1::PublicKey::from_keypair(&keypair);
371
372        let timestamp_secs = 10000;
373        let metadata = GuardianMetadata::new(
374            vec!["wss://example.com/api".parse().unwrap()],
375            "test_pkarr_id".to_string(),
376            timestamp_secs,
377        );
378        let signed = metadata.sign(&ctx, &keypair);
379
380        // Verify fails when timestamp is too far in the future
381        let now_secs = timestamp_secs - MAX_FUTURE_TIMESTAMP_SECS - 1;
382        let result = signed.verify(&ctx, &public_key, Duration::from_secs(now_secs));
383        assert!(
384            matches!(
385                result,
386                Err(VerificationError::TimestampTooFarInFuture {
387                    timestamp_secs: ts,
388                    ..
389                }) if ts == timestamp_secs
390            ),
391            "should reject timestamp too far in future"
392        );
393    }
394}