hopr_internal_types/
announcement.rs

1use std::fmt::{Display, Formatter};
2
3use hopr_crypto_types::prelude::*;
4use hopr_primitive_types::prelude::*;
5use multiaddr::Multiaddr;
6use tracing::debug;
7
8/// Holds the signed binding of the chain key and the packet key.
9///
10/// The signature is done via the offchain key to bind it with the on-chain key. The structure
11/// then makes it on-chain, making it effectively cross-signed with both keys (offchain and onchain).
12/// This is used to attest on-chain that node owns the corresponding packet key and links it with
13/// the chain key.
14#[derive(Clone, Debug, PartialEq)]
15pub struct KeyBinding {
16    pub chain_key: Address,
17    pub packet_key: OffchainPublicKey,
18    pub signature: OffchainSignature,
19}
20
21impl KeyBinding {
22    const SIGNING_SIZE: usize = 16 + Address::SIZE + OffchainPublicKey::SIZE;
23
24    fn prepare_for_signing(chain_key: &Address, packet_key: &OffchainPublicKey) -> [u8; Self::SIGNING_SIZE] {
25        let mut to_sign = [0u8; Self::SIGNING_SIZE];
26        to_sign[0..16].copy_from_slice(b"HOPR_KEY_BINDING");
27        to_sign[16..36].copy_from_slice(chain_key.as_ref());
28        to_sign[36..].copy_from_slice(packet_key.as_ref());
29        to_sign
30    }
31
32    /// Create and sign new key binding of the given chain key and packet key.
33    pub fn new(chain_key: Address, packet_key: &OffchainKeypair) -> Self {
34        let to_sign = Self::prepare_for_signing(&chain_key, packet_key.public());
35        Self {
36            chain_key,
37            packet_key: *packet_key.public(),
38            signature: OffchainSignature::sign_message(&to_sign, packet_key),
39        }
40    }
41}
42
43impl KeyBinding {
44    /// Re-construct binding from the chain key and packet key, while also verifying the given signature of the binding.
45    /// Fails if the signature is not valid for the given entries.
46    pub fn from_parts(
47        chain_key: Address,
48        packet_key: OffchainPublicKey,
49        signature: OffchainSignature,
50    ) -> crate::errors::Result<Self> {
51        let to_verify = Self::prepare_for_signing(&chain_key, &packet_key);
52        signature
53            .verify_message(&to_verify, &packet_key)
54            .then_some(Self {
55                chain_key,
56                packet_key,
57                signature,
58            })
59            .ok_or(CryptoError::SignatureVerification.into())
60    }
61}
62
63impl Display for KeyBinding {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        write!(f, "keybinding {} <-> {}", self.chain_key, self.packet_key)
66    }
67}
68
69/// Decapsulates the multiaddress (= strips the /p2p/<peer_id> suffix).
70/// If it is already decapsulated, the function is an identity.
71pub fn decapsulate_multiaddress(multiaddr: Multiaddr) -> Multiaddr {
72    multiaddr
73        .into_iter()
74        .take_while(|p| !matches!(p, multiaddr::Protocol::P2p(_)))
75        .collect()
76}
77
78/// Structure containing data used for an on-chain announcement.
79/// That is the decapsulated multiaddress (with the /p2p/{peer_id} suffix removed) and
80/// optional `KeyBinding` (an announcement can be done with key bindings or without)
81///
82/// NOTE: This currently supports only announcing of a single multiaddress
83#[derive(Clone, Debug, PartialEq)]
84pub struct AnnouncementData {
85    multiaddress: Multiaddr,
86    pub key_binding: Option<KeyBinding>,
87}
88
89impl AnnouncementData {
90    /// Constructs structure from multiaddress and optionally also `KeyBinding`.
91    /// The multiaddress must not be empty. It should be the external address of the node.
92    /// It may contain a trailing PeerId (encapsulated multiaddr) or come without. If the
93    /// peerId is present, it must match with the keybinding.
94    pub fn new(multiaddress: Multiaddr, key_binding: Option<KeyBinding>) -> Result<Self, GeneralError> {
95        if multiaddress.is_empty() {
96            debug!("Received empty multiaddr");
97            return Err(GeneralError::InvalidInput);
98        }
99
100        if let Some(binding) = &key_binding {
101            // Encapsulate first (if already encapsulated, the operation verifies that peer id matches the given one)
102            match multiaddress.with_p2p(binding.packet_key.into()) {
103                Ok(mut multiaddress) => {
104                    // Now decapsulate again, because we store decapsulated multiaddress only (without the
105                    // /p2p/<peer_id> suffix)
106                    multiaddress.pop();
107                    Ok(Self {
108                        multiaddress,
109                        key_binding,
110                    })
111                }
112                Err(multiaddress) => Err(GeneralError::NonSpecificError(format!(
113                    "{multiaddress} does not match the keybinding {} peer id",
114                    binding.packet_key.to_peerid_str()
115                ))),
116            }
117        } else {
118            Ok(Self {
119                multiaddress: multiaddress.to_owned(),
120                key_binding: None,
121            })
122        }
123    }
124
125    /// Returns the multiaddress associated with this announcement.
126    /// Note that the returned multiaddress is *always* decapsulated (= without the /p2p/<peer_id> suffix)
127    pub fn multiaddress(&self) -> &Multiaddr {
128        &self.multiaddress
129    }
130}
131
132impl Display for AnnouncementData {
133    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
134        if let Some(binding) = &self.key_binding {
135            write!(f, "announcement of {} with {binding}", self.multiaddress)
136        } else {
137            write!(f, "announcement of {}", self.multiaddress)
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use hex_literal::hex;
145    use hopr_crypto_types::keypairs::{Keypair, OffchainKeypair};
146    use hopr_primitive_types::primitives::Address;
147    use multiaddr::Multiaddr;
148
149    use crate::{
150        announcement::{AnnouncementData, KeyBinding},
151        prelude::decapsulate_multiaddress,
152    };
153
154    lazy_static::lazy_static! {
155        static ref KEY_PAIR: OffchainKeypair = OffchainKeypair::from_secret(&hex!("60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d")).expect("lazy static keypair should be constructible");
156        static ref CHAIN_ADDR: Address = Address::try_from(hex!("78392d47e3522219e2802e7d6c45ee84b5d5c185").as_ref()).expect("lazy static address should be constructible");
157        static ref SECOND_KEY_PAIR: OffchainKeypair = OffchainKeypair::from_secret(&hex!("c24bd833704dd2abdae3933fcc9962c2ac404f84132224c474147382d4db2299")).expect("lazy static keypair should be constructible");
158    }
159
160    #[test]
161    fn test_key_binding() -> anyhow::Result<()> {
162        let kb_1 = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
163        let kb_2 = KeyBinding::from_parts(kb_1.chain_key, kb_1.packet_key, kb_1.signature.clone())?;
164
165        assert_eq!(kb_1, kb_2, "must be equal");
166
167        Ok(())
168    }
169
170    #[test]
171    fn test_announcement() -> anyhow::Result<()> {
172        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
173        let peer_id = KEY_PAIR.public().to_peerid_str();
174
175        for (ma_str, decapsulated_ma_str) in vec![
176            (
177                format!("/ip4/127.0.0.1/tcp/10000/p2p/{peer_id}"),
178                "/ip4/127.0.0.1/tcp/10000".to_string(),
179            ),
180            (
181                format!("/ip6/::1/tcp/10000/p2p/{peer_id}"),
182                "/ip6/::1/tcp/10000".to_string(),
183            ),
184            (
185                format!("/dns4/hoprnet.org/tcp/10000/p2p/{peer_id}"),
186                "/dns4/hoprnet.org/tcp/10000".to_string(),
187            ),
188            (
189                format!("/dns6/hoprnet.org/tcp/10000/p2p/{peer_id}"),
190                "/dns6/hoprnet.org/tcp/10000".to_string(),
191            ),
192            (
193                format!("/ip4/127.0.0.1/udp/10000/quic/p2p/{peer_id}"),
194                "/ip4/127.0.0.1/udp/10000/quic".to_string(),
195            ),
196        ] {
197            let maddr: Multiaddr = ma_str.parse()?;
198
199            let ad = AnnouncementData::new(maddr, Some(key_binding.clone()))?;
200            assert_eq!(decapsulated_ma_str, ad.multiaddress().to_string());
201            assert_eq!(Some(key_binding.clone()), ad.key_binding);
202        }
203
204        Ok(())
205    }
206
207    #[test]
208    fn test_announcement_no_keybinding() -> anyhow::Result<()> {
209        let maddr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".to_string().parse()?;
210
211        let ad = AnnouncementData::new(maddr, None)?;
212
213        assert_eq!(None, ad.key_binding);
214
215        Ok(())
216    }
217
218    #[test]
219    fn test_announcement_decapsulated_ma() -> anyhow::Result<()> {
220        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
221        let maddr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".to_string().parse()?;
222
223        let ad = AnnouncementData::new(maddr, Some(key_binding.clone()))?;
224        assert_eq!("/ip4/127.0.0.1/tcp/10000", ad.multiaddress().to_string());
225        assert_eq!(Some(key_binding), ad.key_binding);
226
227        Ok(())
228    }
229
230    #[test]
231    fn test_announcement_wrong_peerid() -> anyhow::Result<()> {
232        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
233        let peer_id = SECOND_KEY_PAIR.public().to_peerid_str();
234        let maddr: Multiaddr = format!("/ip4/127.0.0.1/tcp/10000/p2p/{peer_id}").parse()?;
235
236        assert!(AnnouncementData::new(maddr, Some(key_binding.clone())).is_err());
237
238        Ok(())
239    }
240
241    #[test]
242    fn test_decapsulate_multiaddr() -> anyhow::Result<()> {
243        let maddr_1: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse()?;
244        let maddr_2 = maddr_1
245            .clone()
246            .with_p2p(OffchainKeypair::random().public().into())
247            .map_err(|e| anyhow::anyhow!(e.to_string()))?;
248
249        assert_eq!(maddr_1, decapsulate_multiaddress(maddr_2), "multiaddresses must match");
250        assert_eq!(
251            maddr_1,
252            decapsulate_multiaddress(maddr_1.clone()),
253            "decapsulation must be idempotent"
254        );
255
256        Ok(())
257    }
258}