Skip to content

Commit d6b6132

Browse files
authored
Merge pull request #23 from n0-computer/signed-announce
Signed announce
2 parents 2d85560 + 32ad3a5 commit d6b6132

File tree

8 files changed

+207
-108
lines changed

8 files changed

+207
-108
lines changed

content-discovery/Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

content-discovery/iroh-mainline-content-discovery-cli/src/args.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,6 @@ impl ContentArg {
3838
},
3939
}
4040
}
41-
42-
/// Get the host of the content. Only defined for tickets.
43-
pub fn host(&self) -> Option<NodeId> {
44-
match self {
45-
ContentArg::Hash(_) => None,
46-
ContentArg::HashAndFormat(_) => None,
47-
ContentArg::Ticket(ticket) => Some(ticket.node_addr().node_id),
48-
}
49-
}
5041
}
5142

5243
impl Display for ContentArg {
@@ -81,16 +72,12 @@ pub struct AnnounceArgs {
8172
#[clap(long)]
8273
pub tracker: NodeId,
8374

84-
/// The host to announce. Not needed if content is a ticket.
85-
#[clap(long)]
86-
pub host: Option<NodeId>,
87-
8875
/// The content to announce.
8976
///
9077
/// Content can be specified as a hash, a hash and format, or a ticket.
9178
/// If a hash is specified, the format is assumed to be raw.
9279
/// Unless a ticket is specified, the host must be specified.
93-
pub content: Vec<ContentArg>,
80+
pub content: ContentArg,
9481

9582
/// Announce that the peer has only partial data.
9683
#[clap(long)]

content-discovery/iroh-mainline-content-discovery-cli/src/main.rs

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
pub mod args;
22

33
use std::{
4-
collections::BTreeSet,
54
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
5+
str::FromStr,
66
};
77

88
use anyhow::Context;
@@ -11,7 +11,7 @@ use clap::Parser;
1111
use futures::StreamExt;
1212
use iroh_mainline_content_discovery::{
1313
create_quinn_client,
14-
protocol::{Announce, AnnounceKind, Query, QueryFlags, ALPN},
14+
protocol::{AbsoluteTime, Announce, AnnounceKind, Query, QueryFlags, SignedAnnounce, ALPN},
1515
to_infohash,
1616
};
1717
use iroh_net::{
@@ -54,44 +54,32 @@ pub async fn accept_conn(
5454

5555
async fn announce(args: AnnounceArgs) -> anyhow::Result<()> {
5656
// todo: uncomment once the connection problems are fixed
57-
// for now, a random node id is more reliable.
58-
// let key = load_secret_key(tracker_path(CLIENT_KEY)?).await?;
59-
let key = iroh_net::key::SecretKey::generate();
60-
let content = args.content.iter().map(|x| x.hash_and_format()).collect();
61-
let host = if let Some(host) = args.host {
62-
host
63-
} else {
64-
let hosts = args
65-
.content
66-
.iter()
67-
.filter_map(|x| x.host())
68-
.collect::<BTreeSet<_>>();
69-
if hosts.len() != 1 {
70-
anyhow::bail!(
71-
"content for all tickets must be from the same host, unless a host is specified"
72-
);
73-
}
74-
*hosts.iter().next().unwrap()
57+
let Ok(key) = std::env::var("ANNOUNCE_SECRET") else {
58+
eprintln!("ANNOUNCE_SECRET environment variable must be set to a valid secret key");
59+
anyhow::bail!("ANNOUNCE_SECRET env var not set");
7560
};
76-
println!("announcing to {}", args.tracker);
77-
println!("host {} has", host);
78-
for content in &content {
79-
println!(" {}", content);
80-
}
81-
let endpoint = create_endpoint(key, args.magic_port.unwrap_or_default(), false).await?;
61+
let Ok(key) = iroh_net::key::SecretKey::from_str(&key) else {
62+
anyhow::bail!("ANNOUNCE_SECRET env var is not a valid secret key");
63+
};
64+
let content = args.content.hash_and_format();
65+
println!("announcing to {}: {}", args.tracker, content);
66+
let endpoint = create_endpoint(key.clone(), args.magic_port.unwrap_or_default(), false).await?;
8267
let connection = endpoint.connect_by_node_id(&args.tracker, ALPN).await?;
8368
println!("connected to {:?}", connection.remote_address());
8469
let kind = if args.partial {
8570
AnnounceKind::Partial
8671
} else {
8772
AnnounceKind::Complete
8873
};
74+
let timestamp = AbsoluteTime::now();
8975
let announce = Announce {
90-
host,
76+
host: key.public(),
9177
kind,
9278
content,
79+
timestamp,
9380
};
94-
iroh_mainline_content_discovery::announce(connection, announce).await?;
81+
let signed_announce = SignedAnnounce::new(announce, &key)?;
82+
iroh_mainline_content_discovery::announce(connection, signed_announce).await?;
9583
println!("done");
9684
Ok(())
9785
}
@@ -114,8 +102,12 @@ async fn query(args: QueryArgs) -> anyhow::Result<()> {
114102
"querying tracker {} for content {}",
115103
args.tracker, args.content
116104
);
117-
for peer in res.hosts {
118-
println!("{}", peer);
105+
for sa in res.hosts {
106+
if sa.verify().is_ok() {
107+
println!("{}: {:?}", sa.announce.host, sa.announce.kind);
108+
} else {
109+
println!("invalid announce");
110+
}
119111
}
120112
Ok(())
121113
}
@@ -143,7 +135,13 @@ async fn query_dht(args: QueryDhtArgs) -> anyhow::Result<()> {
143135
);
144136
while let Some(item) = stream.next().await {
145137
match item {
146-
Ok(provider) => println!("found provider {}", provider),
138+
Ok(announce) => {
139+
if announce.verify().is_ok() {
140+
println!("found verified provider {}", announce.host);
141+
} else {
142+
println!("found unverified provider");
143+
}
144+
}
147145
Err(e) => println!("error: {}", e),
148146
}
149147
}

content-discovery/iroh-mainline-content-discovery/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ futures = { version = "0.3.25", optional = true }
2626
rcgen = { version = "0.12.0", optional = true }
2727
rustls = { version = "0.21", optional = true }
2828
genawaiter = { version = "0.99.1", features = ["futures03"], optional = true }
29+
serde-big-array = "0.5.1"
2930

3031
[features]
3132
client = ["iroh-pkarr-node-discovery", "mainline", "quinn", "tracing", "anyhow", "rcgen", "genawaiter", "rustls", "futures", "postcard"]

content-discovery/iroh-mainline-content-discovery/src/client.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use iroh_pkarr_node_discovery::PkarrNodeDiscovery;
1313
use mainline::common::{GetPeerResponse, StoreQueryMetdata};
1414

1515
use crate::protocol::{
16-
Announce, Query, QueryResponse, Request, Response, ALPN, REQUEST_SIZE_LIMIT,
16+
Query, QueryResponse, Request, Response, SignedAnnounce, ALPN, REQUEST_SIZE_LIMIT,
1717
};
1818

1919
/// Announce to a tracker.
@@ -24,10 +24,13 @@ use crate::protocol::{
2424
/// `tracker` is the node id of the tracker to announce to. It must understand the [TRACKER_ALPN] protocol.
2525
/// `content` is the content to announce.
2626
/// `kind` is the kind of the announcement. We can claim to have the complete data or only some of it.
27-
pub async fn announce(connection: quinn::Connection, args: Announce) -> anyhow::Result<()> {
27+
pub async fn announce(
28+
connection: quinn::Connection,
29+
signed_announce: SignedAnnounce,
30+
) -> anyhow::Result<()> {
2831
let (mut send, mut recv) = connection.open_bi().await?;
2932
tracing::debug!("opened bi stream");
30-
let request = Request::Announce(args);
33+
let request = Request::Announce(signed_announce);
3134
let request = postcard::to_stdvec(&request)?;
3235
tracing::debug!("sending announce");
3336
send.write_all(&request).await?;
@@ -65,7 +68,7 @@ async fn query_socket_one(
6568
endpoint: impl QuinnConnectionProvider<SocketAddr>,
6669
addr: SocketAddr,
6770
args: Query,
68-
) -> anyhow::Result<Vec<NodeId>> {
71+
) -> anyhow::Result<Vec<SignedAnnounce>> {
6972
let connection = endpoint.connect(addr).await?;
7073
let result = query(connection, args).await?;
7174
Ok(result.hosts)
@@ -75,7 +78,7 @@ async fn query_magic_one(
7578
endpoint: MagicEndpoint,
7679
node_id: &NodeId,
7780
args: Query,
78-
) -> anyhow::Result<Vec<NodeId>> {
81+
) -> anyhow::Result<Vec<SignedAnnounce>> {
7982
let connection = endpoint.connect_by_node_id(node_id, ALPN).await?;
8083
let result = query(connection, args).await?;
8184
Ok(result.hosts)
@@ -101,7 +104,7 @@ pub fn query_trackers(
101104
trackers: impl IntoIterator<Item = NodeId>,
102105
args: Query,
103106
query_parallelism: usize,
104-
) -> impl Stream<Item = anyhow::Result<NodeId>> {
107+
) -> impl Stream<Item = anyhow::Result<SignedAnnounce>> {
105108
futures::stream::iter(trackers)
106109
.map(move |tracker| {
107110
let endpoint = endpoint.clone();
@@ -123,7 +126,7 @@ pub fn query_dht(
123126
dht: mainline::dht::Dht,
124127
args: Query,
125128
query_parallelism: usize,
126-
) -> impl Stream<Item = anyhow::Result<NodeId>> {
129+
) -> impl Stream<Item = anyhow::Result<SignedAnnounce>> {
127130
let dht = dht.as_async();
128131
let info_hash = to_infohash(args.content);
129132
let response: mainline::common::Response<GetPeerResponse> = dht.get_peers(info_hash);

content-discovery/iroh-mainline-content-discovery/src/protocol.rs

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
//! The protocol for communicating with the tracker.
2+
use std::{
3+
ops::{Deref, Sub},
4+
time::{Duration, SystemTime},
5+
};
6+
27
use iroh_bytes::HashAndFormat;
38
use iroh_net::NodeId;
49
use serde::{Deserialize, Serialize};
5-
use std::collections::BTreeSet;
10+
use serde_big_array::BigArray;
611

712
/// The ALPN string for this protocol
813
pub const ALPN: &[u8] = b"n0/tracker/1";
@@ -28,20 +33,98 @@ impl AnnounceKind {
2833
}
2934
}
3035

36+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
37+
pub struct AbsoluteTime(u64);
38+
39+
impl AbsoluteTime {
40+
pub fn now() -> Self {
41+
Self::try_from(SystemTime::now()).unwrap()
42+
}
43+
}
44+
45+
impl Sub for AbsoluteTime {
46+
type Output = Duration;
47+
48+
fn sub(self, rhs: Self) -> Self::Output {
49+
Duration::from_micros(self.0 - rhs.0)
50+
}
51+
}
52+
53+
impl TryFrom<SystemTime> for AbsoluteTime {
54+
type Error = anyhow::Error;
55+
56+
fn try_from(value: SystemTime) -> Result<Self, Self::Error> {
57+
Ok(Self(
58+
value
59+
.duration_since(std::time::UNIX_EPOCH)
60+
.expect("Time went backwards")
61+
.as_micros()
62+
.try_into()
63+
.expect("time too large"),
64+
))
65+
}
66+
}
67+
68+
impl From<AbsoluteTime> for SystemTime {
69+
fn from(value: AbsoluteTime) -> Self {
70+
std::time::UNIX_EPOCH + Duration::from_micros(value.0)
71+
}
72+
}
73+
3174
/// Announce that a peer claims to have some blobs or set of blobs.
3275
///
3376
/// A peer can announce having some data, but it should also be able to announce
3477
/// that another peer has the data. This is why the peer is included.
35-
#[derive(Debug, Clone, Serialize, Deserialize)]
78+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
3679
pub struct Announce {
3780
/// The peer that supposedly has the data.
38-
///
39-
/// Should we get this from the connection?
4081
pub host: NodeId,
41-
/// The blobs or sets that the peer claims to have.
42-
pub content: BTreeSet<HashAndFormat>,
82+
/// The content that the peer claims to have.
83+
pub content: HashAndFormat,
4384
/// The kind of the announcement.
4485
pub kind: AnnounceKind,
86+
/// The timestamp of the announce.
87+
pub timestamp: AbsoluteTime,
88+
}
89+
90+
/// A signed announce.
91+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
92+
pub struct SignedAnnounce {
93+
/// Announce.
94+
pub announce: Announce,
95+
/// Signature of the announce, signed by the host of the announce.
96+
///
97+
/// The signature is over the announce, serialized with postcard.
98+
#[serde(with = "BigArray")]
99+
pub signature: [u8; 64],
100+
}
101+
102+
impl Deref for SignedAnnounce {
103+
type Target = Announce;
104+
105+
fn deref(&self) -> &Self::Target {
106+
&self.announce
107+
}
108+
}
109+
110+
impl SignedAnnounce {
111+
/// Create a new signed announce.
112+
pub fn new(announce: Announce, secret_key: &iroh_net::key::SecretKey) -> anyhow::Result<Self> {
113+
let announce_bytes = postcard::to_allocvec(&announce)?;
114+
let signature = secret_key.sign(&announce_bytes).to_bytes();
115+
Ok(Self {
116+
announce,
117+
signature,
118+
})
119+
}
120+
121+
/// Verify the announce, and return the announce if it's valid.
122+
pub fn verify(&self) -> anyhow::Result<()> {
123+
let announce_bytes = postcard::to_allocvec(&self.announce)?;
124+
let signature = iroh_net::key::Signature::from_bytes(&self.signature);
125+
self.announce.host.verify(&announce_bytes, &signature)?;
126+
Ok(())
127+
}
45128
}
46129

47130
///
@@ -76,20 +159,18 @@ pub struct Query {
76159
/// A response to a query.
77160
#[derive(Debug, Clone, Serialize, Deserialize)]
78161
pub struct QueryResponse {
79-
/// The content that was queried.
80-
pub content: HashAndFormat,
81162
/// The hosts that supposedly have the content.
82163
///
83164
/// If there are any addrs, they are as seen from the tracker,
84165
/// so they might or might not be useful.
85-
pub hosts: Vec<NodeId>,
166+
pub hosts: Vec<SignedAnnounce>,
86167
}
87168

88169
/// A request to the tracker.
89170
#[derive(Debug, Clone, Serialize, Deserialize)]
90171
pub enum Request {
91172
/// Announce info
92-
Announce(Announce),
173+
Announce(SignedAnnounce),
93174
/// Query info
94175
Query(Query),
95176
}

0 commit comments

Comments
 (0)