hopr_internal_types/
announcement.rs

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