Skip to main content

fedimint_core/net/
guardian_metadata.rs

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