hopr_internal_types/
announcement.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
use hopr_crypto_types::prelude::*;
use hopr_primitive_types::prelude::*;
use multiaddr::Multiaddr;
use std::fmt::{Display, Formatter};
use tracing::debug;

/// Holds the signed binding of the chain key and the packet key.
///
/// The signature is done via the offchain key to bind it with the on-chain key. The structure
/// then makes it on-chain, making it effectively cross-signed with both keys (offchain and onchain).
/// This is used to attest on-chain that node owns the corresponding packet key and links it with
/// the chain key.
#[derive(Clone, Debug, PartialEq)]
pub struct KeyBinding {
    pub chain_key: Address,
    pub packet_key: OffchainPublicKey,
    pub signature: OffchainSignature,
}

impl KeyBinding {
    const SIGNING_SIZE: usize = 16 + Address::SIZE + OffchainPublicKey::SIZE;
    fn prepare_for_signing(chain_key: &Address, packet_key: &OffchainPublicKey) -> [u8; Self::SIGNING_SIZE] {
        let mut to_sign = [0u8; Self::SIGNING_SIZE];
        to_sign[0..16].copy_from_slice(b"HOPR_KEY_BINDING");
        to_sign[16..36].copy_from_slice(chain_key.as_ref());
        to_sign[36..].copy_from_slice(packet_key.as_ref());
        to_sign
    }

    /// Create and sign new key binding of the given chain key and packet key.
    pub fn new(chain_key: Address, packet_key: &OffchainKeypair) -> Self {
        let to_sign = Self::prepare_for_signing(&chain_key, packet_key.public());
        Self {
            chain_key,
            packet_key: *packet_key.public(),
            signature: OffchainSignature::sign_message(&to_sign, packet_key),
        }
    }
}

impl KeyBinding {
    /// Re-construct binding from the chain key and packet key, while also verifying the given signature of the binding.
    /// Fails if the signature is not valid for the given entries.
    pub fn from_parts(
        chain_key: Address,
        packet_key: OffchainPublicKey,
        signature: OffchainSignature,
    ) -> crate::errors::Result<Self> {
        let to_verify = Self::prepare_for_signing(&chain_key, &packet_key);
        signature
            .verify_message(&to_verify, &packet_key)
            .then_some(Self {
                chain_key,
                packet_key,
                signature,
            })
            .ok_or(CryptoError::SignatureVerification.into())
    }
}

impl Display for KeyBinding {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "keybinding {} <-> {}", self.chain_key, self.packet_key)
    }
}

/// Decapsulates the multiaddress (= strips the /p2p/<peer_id> suffix).
/// If it is already decapsulated, the function is an identity.
pub fn decapsulate_multiaddress(multiaddr: Multiaddr) -> Multiaddr {
    multiaddr
        .into_iter()
        .take_while(|p| !matches!(p, multiaddr::Protocol::P2p(_)))
        .collect()
}

/// Structure containing data used for an on-chain announcement.
/// That is the decapsulated multiaddress (with the /p2p/{peer_id} suffix removed) and
/// optional `KeyBinding` (an announcement can be done with key bindings or without)
///
/// NOTE: This currently supports only announcing of a single multiaddress
#[derive(Clone, Debug, PartialEq)]
pub struct AnnouncementData {
    multiaddress: Multiaddr,
    pub key_binding: Option<KeyBinding>,
}

impl AnnouncementData {
    /// Constructs structure from multiaddress and optionally also `KeyBinding`.
    /// The multiaddress must not be empty. It should be the external address of the node.
    /// It may contain a trailing PeerId (encapsulated multiaddr) or come without. If the
    /// peerId is present, it must match with the keybinding.
    pub fn new(multiaddress: Multiaddr, key_binding: Option<KeyBinding>) -> Result<Self, GeneralError> {
        if multiaddress.is_empty() {
            debug!("Received empty multiaddr");
            return Err(GeneralError::InvalidInput);
        }

        if let Some(binding) = &key_binding {
            // Encapsulate first (if already encapsulated, the operation verifies that peer id matches the given one)
            match multiaddress.with_p2p(binding.packet_key.into()) {
                Ok(mut multiaddress) => {
                    // Now decapsulate again, because we store decapsulated multiaddress only (without the /p2p/<peer_id> suffix)
                    multiaddress.pop();
                    Ok(Self {
                        multiaddress,
                        key_binding,
                    })
                }
                Err(multiaddress) => Err(GeneralError::NonSpecificError(format!(
                    "{multiaddress} does not match the keybinding {} peer id",
                    binding.packet_key.to_peerid_str()
                ))),
            }
        } else {
            Ok(Self {
                multiaddress: multiaddress.to_owned(),
                key_binding: None,
            })
        }
    }

    /// Returns the multiaddress associated with this announcement.
    /// Note that the returned multiaddress is *always* decapsulated (= without the /p2p/<peer_id> suffix)
    pub fn multiaddress(&self) -> &Multiaddr {
        &self.multiaddress
    }
}

impl Display for AnnouncementData {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        if let Some(binding) = &self.key_binding {
            write!(f, "announcement of {} with {binding}", self.multiaddress)
        } else {
            write!(f, "announcement of {}", self.multiaddress)
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::announcement::{AnnouncementData, KeyBinding};
    use crate::prelude::decapsulate_multiaddress;
    use hex_literal::hex;
    use hopr_crypto_types::keypairs::{Keypair, OffchainKeypair};
    use hopr_primitive_types::primitives::Address;
    use multiaddr::Multiaddr;

    lazy_static::lazy_static! {
        static ref KEY_PAIR: OffchainKeypair = OffchainKeypair::from_secret(&hex!("60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d")).expect("lazy static keypair should be constructible");
        static ref CHAIN_ADDR: Address = Address::try_from(hex!("78392d47e3522219e2802e7d6c45ee84b5d5c185").as_ref()).expect("lazy static address should be constructible");
        static ref SECOND_KEY_PAIR: OffchainKeypair = OffchainKeypair::from_secret(&hex!("c24bd833704dd2abdae3933fcc9962c2ac404f84132224c474147382d4db2299")).expect("lazy static keypair should be constructible");
    }

    #[test]
    fn test_key_binding() -> anyhow::Result<()> {
        let kb_1 = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
        let kb_2 = KeyBinding::from_parts(kb_1.chain_key, kb_1.packet_key, kb_1.signature.clone())?;

        assert_eq!(kb_1, kb_2, "must be equal");

        Ok(())
    }

    #[test]
    fn test_announcement() -> anyhow::Result<()> {
        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
        let peer_id = KEY_PAIR.public().to_peerid_str();

        for (ma_str, decapsulated_ma_str) in vec![
            (
                format!("/ip4/127.0.0.1/tcp/10000/p2p/{peer_id}"),
                "/ip4/127.0.0.1/tcp/10000".to_string(),
            ),
            (
                format!("/ip6/::1/tcp/10000/p2p/{peer_id}"),
                "/ip6/::1/tcp/10000".to_string(),
            ),
            (
                format!("/dns4/hoprnet.org/tcp/10000/p2p/{peer_id}"),
                "/dns4/hoprnet.org/tcp/10000".to_string(),
            ),
            (
                format!("/dns6/hoprnet.org/tcp/10000/p2p/{peer_id}"),
                "/dns6/hoprnet.org/tcp/10000".to_string(),
            ),
            (
                format!("/ip4/127.0.0.1/udp/10000/quic/p2p/{peer_id}"),
                "/ip4/127.0.0.1/udp/10000/quic".to_string(),
            ),
        ] {
            let maddr: Multiaddr = ma_str.parse()?;

            let ad = AnnouncementData::new(maddr, Some(key_binding.clone()))?;
            assert_eq!(decapsulated_ma_str, ad.multiaddress().to_string());
            assert_eq!(Some(key_binding.clone()), ad.key_binding);
        }

        Ok(())
    }

    #[test]
    fn test_announcement_no_keybinding() -> anyhow::Result<()> {
        let maddr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".to_string().parse()?;

        let ad = AnnouncementData::new(maddr, None)?;

        assert_eq!(None, ad.key_binding);

        Ok(())
    }

    #[test]
    fn test_announcement_decapsulated_ma() -> anyhow::Result<()> {
        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
        let maddr: Multiaddr = "/ip4/127.0.0.1/tcp/10000".to_string().parse()?;

        let ad = AnnouncementData::new(maddr, Some(key_binding.clone()))?;
        assert_eq!("/ip4/127.0.0.1/tcp/10000", ad.multiaddress().to_string());
        assert_eq!(Some(key_binding), ad.key_binding);

        Ok(())
    }

    #[test]
    fn test_announcement_wrong_peerid() -> anyhow::Result<()> {
        let key_binding = KeyBinding::new(*CHAIN_ADDR, &KEY_PAIR);
        let peer_id = SECOND_KEY_PAIR.public().to_peerid_str();
        let maddr: Multiaddr = format!("/ip4/127.0.0.1/tcp/10000/p2p/{peer_id}").parse()?;

        assert!(AnnouncementData::new(maddr, Some(key_binding.clone())).is_err());

        Ok(())
    }

    #[test]
    fn test_decapsulate_multiaddr() -> anyhow::Result<()> {
        let maddr_1: Multiaddr = "/ip4/127.0.0.1/tcp/10000".parse()?;
        let maddr_2 = maddr_1
            .clone()
            .with_p2p(OffchainKeypair::random().public().into())
            .map_err(|e| anyhow::anyhow!(e.to_string()))?;

        assert_eq!(maddr_1, decapsulate_multiaddress(maddr_2), "multiaddresses must match");
        assert_eq!(
            maddr_1,
            decapsulate_multiaddress(maddr_1.clone()),
            "decapsulation must be idempotent"
        );

        Ok(())
    }
}