feat: scope server pins by port, default port inherits (agreed with bunshin)
This commit is contained in:
parent
96a558da16
commit
13c624f2b0
7 changed files with 149 additions and 48 deletions
|
|
@ -170,8 +170,9 @@ fn main() {
|
||||||
/// reports the distinction; only the CLI knows the command.
|
/// reports the distinction; only the CLI knows the command.
|
||||||
fn hint(error: Option<&fumi::error::Error>) {
|
fn hint(error: Option<&fumi::error::Error>) {
|
||||||
match error {
|
match error {
|
||||||
Some(fumi::error::Error::NotPinned { host }) => eprintln!(
|
Some(fumi::error::Error::NotPinned { host, port }) => eprintln!(
|
||||||
"obtain the key from the operator through a trusted channel, then:\n fumi trust {host} <key>"
|
"obtain the key from the operator through a trusted channel, then:\n fumi trust {} <key>",
|
||||||
|
fumi::address::trust_label(host, *port)
|
||||||
),
|
),
|
||||||
Some(fumi::error::Error::KeyChanged { address, offered, .. }) => {
|
Some(fumi::error::Error::KeyChanged { address, offered, .. }) => {
|
||||||
if let Ok(addr) = Address::parse(address) {
|
if let Ok(addr) = Address::parse(address) {
|
||||||
|
|
@ -358,21 +359,30 @@ fn trust(store: &Store, host: &str, key_b32: &str, force: bool) -> Result<()> {
|
||||||
v.len()
|
v.len()
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
// A pin is per server, and the reference client strips any :port the
|
// A pin is scoped per server and port (address::trust_label): the port
|
||||||
// same way its address grammar does: at the first colon.
|
// splits from the right so a bare host means the default port, and an
|
||||||
let host = host.split(':').next().unwrap_or(host);
|
// explicit default port normalizes onto the same bare row existing
|
||||||
match store.server_pin(host)? {
|
// stores already hold.
|
||||||
|
let (host, port) = match host.rsplit_once(':') {
|
||||||
|
Some((h, p)) => match p.parse::<u16>() {
|
||||||
|
Ok(port) => (h, port),
|
||||||
|
Err(_) => (host, fumi::address::DEFAULT_PORT),
|
||||||
|
},
|
||||||
|
None => (host, fumi::address::DEFAULT_PORT),
|
||||||
|
};
|
||||||
|
match store.server_pin(host, port)? {
|
||||||
Some(old) if old != key && !force => {
|
Some(old) if old != key && !force => {
|
||||||
return Err(anyhow!(format!(
|
return Err(anyhow!(format!(
|
||||||
"{host} is already pinned to {}; use --force to replace",
|
"{} is already pinned to {}; use --force to replace",
|
||||||
|
fumi::address::trust_label(host, port),
|
||||||
b32(&old)
|
b32(&old)
|
||||||
))
|
))
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
store.pin_server(host, &key)?;
|
store.pin_server(host, port, &key)?;
|
||||||
println!("pinned {host} {}", b32(&key));
|
println!("pinned {} {}", fumi::address::trust_label(host, port), b32(&key));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,22 @@ use crate::crypto::{b32, unb32, KEY_LEN};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
pub const DEFAULT_PORT: u16 = 1961;
|
pub const DEFAULT_PORT: u16 = 1961;
|
||||||
|
|
||||||
|
/// The trust label of a host: the bare host on the default port, `host:port`
|
||||||
|
/// on any other. One rule for the store's pin keys and the trust errors, so
|
||||||
|
/// a pin and its error always name the same trust object. The fallback is
|
||||||
|
/// asymmetric by design: the default port inherits the legacy bare-host pin,
|
||||||
|
/// and a non-default port never falls back to it — a second domain on one
|
||||||
|
/// host earns its own trust on first use instead of a mismatch against its
|
||||||
|
/// sibling's key.
|
||||||
|
pub fn trust_label(host: &str, port: u16) -> String {
|
||||||
|
if port == DEFAULT_PORT {
|
||||||
|
host.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{host}:{port}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A Reticulum destination hash, as printed by every RNS tool: 32 lowercase
|
/// A Reticulum destination hash, as printed by every RNS tool: 32 lowercase
|
||||||
/// hexadecimal characters (RNS.md sec 13.2).
|
/// hexadecimal characters (RNS.md sec 13.2).
|
||||||
pub const RNS_HASH_HEX: usize = 32;
|
pub const RNS_HASH_HEX: usize = 32;
|
||||||
|
|
|
||||||
|
|
@ -79,10 +79,11 @@ pub fn connect(
|
||||||
) -> Result<Session, Error> {
|
) -> Result<Session, Error> {
|
||||||
match addr.scheme {
|
match addr.scheme {
|
||||||
Scheme::Tcp => {
|
Scheme::Tcp => {
|
||||||
let pinned = store.server_pin(&addr.host)?;
|
let pinned = store.server_pin(&addr.host, addr.port)?;
|
||||||
if pinned.is_none() && require_pin {
|
if pinned.is_none() && require_pin {
|
||||||
return Err(Error::NotPinned {
|
return Err(Error::NotPinned {
|
||||||
host: addr.host.clone(),
|
host: addr.host.clone(),
|
||||||
|
port: addr.port,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// The dial hint lets a host that resolves DNS itself (an
|
// The dial hint lets a host that resolves DNS itself (an
|
||||||
|
|
@ -94,6 +95,19 @@ pub fn connect(
|
||||||
pinned,
|
pinned,
|
||||||
timeout,
|
timeout,
|
||||||
)?;
|
)?;
|
||||||
|
// Pin enforcement lives here, one layer above the transport: the
|
||||||
|
// identity host names the trust object even when a dial hint
|
||||||
|
// routed the packets elsewhere (sec 4).
|
||||||
|
if let Some(pinned) = pinned {
|
||||||
|
if !ct_eq(&pinned, transport.server_static()) {
|
||||||
|
return Err(Error::PinMismatch {
|
||||||
|
host: addr.host.clone(),
|
||||||
|
port: addr.port,
|
||||||
|
pinned,
|
||||||
|
presented: *transport.server_static(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
let unpinned_static = if pinned.is_none() {
|
let unpinned_static = if pinned.is_none() {
|
||||||
Some(*transport.server_static())
|
Some(*transport.server_static())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ pub enum Error {
|
||||||
UnknownStatus(u8, String),
|
UnknownStatus(u8, String),
|
||||||
/// The host has no pinned server key, so the session cannot be
|
/// The host has no pinned server key, so the session cannot be
|
||||||
/// authenticated (sec 4). A GUI routes this to pinning; an auth failure
|
/// authenticated (sec 4). A GUI routes this to pinning; an auth failure
|
||||||
/// is terminal.
|
/// is terminal. The port scopes the pin (address::trust_label).
|
||||||
NotPinned { host: String },
|
NotPinned { host: String, port: u16 },
|
||||||
/// A pinned server presented a different key (sec 4): a hard abort, the
|
/// A pinned server presented a different key (sec 4): a hard abort, the
|
||||||
/// pin is the entire trust model.
|
/// pin is the entire trust model. The port scopes the pin
|
||||||
|
/// (address::trust_label).
|
||||||
PinMismatch {
|
PinMismatch {
|
||||||
host: String,
|
host: String,
|
||||||
|
port: u16,
|
||||||
pinned: [u8; KEY_LEN],
|
pinned: [u8; KEY_LEN],
|
||||||
presented: [u8; KEY_LEN],
|
presented: [u8; KEY_LEN],
|
||||||
},
|
},
|
||||||
|
|
@ -89,14 +91,20 @@ impl fmt::Display for Error {
|
||||||
Self::UnknownStatus(status, what) => {
|
Self::UnknownStatus(status, what) => {
|
||||||
write!(f, "{what} failed: unknown status {status}")
|
write!(f, "{what} failed: unknown status {status}")
|
||||||
}
|
}
|
||||||
Self::NotPinned { host } => write!(f, "no pinned key for {host}"),
|
Self::NotPinned { host, port } => write!(
|
||||||
|
f,
|
||||||
|
"no pinned key for {}",
|
||||||
|
crate::address::trust_label(host, *port)
|
||||||
|
),
|
||||||
Self::PinMismatch {
|
Self::PinMismatch {
|
||||||
host,
|
host,
|
||||||
|
port,
|
||||||
pinned,
|
pinned,
|
||||||
presented,
|
presented,
|
||||||
} => write!(
|
} => write!(
|
||||||
f,
|
f,
|
||||||
"{host} presented a different key than the one pinned\n pinned: {}\n presented: {}",
|
"{} presented a different key than the one pinned\n pinned: {}\n presented: {}",
|
||||||
|
crate::address::trust_label(host, *port),
|
||||||
b32(pinned),
|
b32(pinned),
|
||||||
b32(presented)
|
b32(presented)
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -249,11 +249,21 @@ pub fn import(store: &Store, master: &[u8; KEY_LEN], text: &str) -> Result<Impor
|
||||||
|
|
||||||
for (host, key) in &payload.servers {
|
for (host, key) in &payload.servers {
|
||||||
let key: Option<[u8; KEY_LEN]> = unb32(key).ok().and_then(|k| k.try_into().ok());
|
let key: Option<[u8; KEY_LEN]> = unb32(key).ok().and_then(|k| k.try_into().ok());
|
||||||
|
// A backup pin key is a bare host (the default port) or host:port,
|
||||||
|
// scoping exactly as the live store does; the port splits from the
|
||||||
|
// right, so a host without a valid port suffix stays whole.
|
||||||
|
let (host, port) = match host.rsplit_once(':') {
|
||||||
|
Some((h, p)) => match p.parse::<u16>() {
|
||||||
|
Ok(port) => (h.to_string(), port),
|
||||||
|
Err(_) => (host.clone(), crate::address::DEFAULT_PORT),
|
||||||
|
},
|
||||||
|
None => (host.clone(), crate::address::DEFAULT_PORT),
|
||||||
|
};
|
||||||
match key {
|
match key {
|
||||||
None => summary.malformed += 1,
|
None => summary.malformed += 1,
|
||||||
Some(key) => match store.server_pin(host)? {
|
Some(key) => match store.server_pin(&host, port)? {
|
||||||
None => {
|
None => {
|
||||||
store.pin_server(host, &key)?;
|
store.pin_server(&host, port, &key)?;
|
||||||
summary.pins_added += 1;
|
summary.pins_added += 1;
|
||||||
}
|
}
|
||||||
Some(existing) if existing != key => summary.pins_conflicted += 1,
|
Some(existing) if existing != key => summary.pins_conflicted += 1,
|
||||||
|
|
@ -337,7 +347,7 @@ mod tests {
|
||||||
fn seeded_store() -> (Store, [u8; KEY_LEN]) {
|
fn seeded_store() -> (Store, [u8; KEY_LEN]) {
|
||||||
let store = Store::open_in_memory().unwrap();
|
let store = Store::open_in_memory().unwrap();
|
||||||
let master = [7u8; KEY_LEN];
|
let master = [7u8; KEY_LEN];
|
||||||
store.pin_server("example.org", &[1u8; KEY_LEN]).unwrap();
|
store.pin_server("example.org", crate::address::DEFAULT_PORT, &[1u8; KEY_LEN]).unwrap();
|
||||||
store
|
store
|
||||||
.save_contact("alice@example.org", &[2u8; KEY_LEN], true)
|
.save_contact("alice@example.org", &[2u8; KEY_LEN], true)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -366,7 +376,7 @@ mod tests {
|
||||||
assert_eq!(summary.contacts_added, 1);
|
assert_eq!(summary.contacts_added, 1);
|
||||||
assert_eq!(summary.malformed, 0);
|
assert_eq!(summary.malformed, 0);
|
||||||
|
|
||||||
assert_eq!(fresh.server_pin("example.org").unwrap(), Some([1u8; KEY_LEN]));
|
assert_eq!(fresh.server_pin("example.org", crate::address::DEFAULT_PORT).unwrap(), Some([1u8; KEY_LEN]));
|
||||||
let (key, verified) = fresh.contact("alice@example.org").unwrap().unwrap();
|
let (key, verified) = fresh.contact("alice@example.org").unwrap().unwrap();
|
||||||
assert_eq!(key, [2u8; KEY_LEN]);
|
assert_eq!(key, [2u8; KEY_LEN]);
|
||||||
assert!(verified);
|
assert!(verified);
|
||||||
|
|
@ -395,13 +405,13 @@ mod tests {
|
||||||
let file = export(&store, &master).unwrap();
|
let file = export(&store, &master).unwrap();
|
||||||
|
|
||||||
let mine = Store::open_in_memory().unwrap();
|
let mine = Store::open_in_memory().unwrap();
|
||||||
mine.pin_server("example.org", &[6u8; KEY_LEN]).unwrap();
|
mine.pin_server("example.org", crate::address::DEFAULT_PORT, &[6u8; KEY_LEN]).unwrap();
|
||||||
mine.save_contact("alice@example.org", &[7u8; KEY_LEN], false)
|
mine.save_contact("alice@example.org", &[7u8; KEY_LEN], false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let summary = import(&mine, &master, &file).unwrap();
|
let summary = import(&mine, &master, &file).unwrap();
|
||||||
assert_eq!(summary.pins_conflicted, 1);
|
assert_eq!(summary.pins_conflicted, 1);
|
||||||
assert_eq!(summary.contacts_conflicted, 1);
|
assert_eq!(summary.contacts_conflicted, 1);
|
||||||
assert_eq!(mine.server_pin("example.org").unwrap(), Some([6u8; KEY_LEN]));
|
assert_eq!(mine.server_pin("example.org", crate::address::DEFAULT_PORT).unwrap(), Some([6u8; KEY_LEN]));
|
||||||
let (key, _) = mine.contact("alice@example.org").unwrap().unwrap();
|
let (key, _) = mine.contact("alice@example.org").unwrap().unwrap();
|
||||||
assert_eq!(key, [7u8; KEY_LEN]);
|
assert_eq!(key, [7u8; KEY_LEN]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -276,9 +276,12 @@ impl Store {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn server_pin(&self, host: &str) -> Result<Option<[u8; KEY_LEN]>, Error> {
|
pub fn server_pin(&self, host: &str, port: u16) -> Result<Option<[u8; KEY_LEN]>, Error> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.one::<Option<Vec<u8>>>("SELECT static FROM servers WHERE host = ?1", &[&host])?
|
.one::<Option<Vec<u8>>>(
|
||||||
|
"SELECT static FROM servers WHERE host = ?1",
|
||||||
|
&[&crate::address::trust_label(host, port)],
|
||||||
|
)?
|
||||||
.flatten()
|
.flatten()
|
||||||
.and_then(|k| k.try_into().ok()))
|
.and_then(|k| k.try_into().ok()))
|
||||||
}
|
}
|
||||||
|
|
@ -286,9 +289,11 @@ impl Store {
|
||||||
/// Removes a pin: the settings screen's unpin, the inverse of
|
/// Removes a pin: the settings screen's unpin, the inverse of
|
||||||
/// `pin_server`. The next session against that host is trust on first
|
/// `pin_server`. The next session against that host is trust on first
|
||||||
/// use again (sec 4, sec 8).
|
/// use again (sec 4, sec 8).
|
||||||
pub fn unpin_server(&self, host: &str) -> Result<(), Error> {
|
pub fn unpin_server(&self, host: &str, port: u16) -> Result<(), Error> {
|
||||||
self.db()
|
self.db().execute(
|
||||||
.execute("DELETE FROM servers WHERE host = ?1", params![host])?;
|
"DELETE FROM servers WHERE host = ?1",
|
||||||
|
params![&crate::address::trust_label(host, port)],
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,12 +312,20 @@ impl Store {
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pin_server(&self, host: &str, key: &[u8; KEY_LEN]) -> Result<(), Error> {
|
/// Pins a server key, scoped by port (address::trust_label): the default
|
||||||
|
/// port keeps the bare-host row existing stores already hold, and any
|
||||||
|
/// other port earns its own row.
|
||||||
|
pub fn pin_server(
|
||||||
|
&self,
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
key: &[u8; KEY_LEN],
|
||||||
|
) -> Result<(), Error> {
|
||||||
self.db()
|
self.db()
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO servers (host, static, pinned_at) VALUES (?1, ?2, ?3) \
|
"INSERT INTO servers (host, static, pinned_at) VALUES (?1, ?2, ?3) \
|
||||||
ON CONFLICT (host) DO UPDATE SET static = ?2, pinned_at = ?3",
|
ON CONFLICT (host) DO UPDATE SET static = ?2, pinned_at = ?3",
|
||||||
params![host, key, now()],
|
params![&crate::address::trust_label(host, port), key, now()],
|
||||||
)
|
)
|
||||||
.map(|_| ())?;
|
.map(|_| ())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -659,6 +672,7 @@ fn row_stored(row: &rusqlite::Row<'_>) -> rusqlite::Result<Stored> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::account::{identity_seed, Account};
|
use crate::account::{identity_seed, Account};
|
||||||
|
use crate::address::DEFAULT_PORT;
|
||||||
|
|
||||||
fn master() -> [u8; 32] {
|
fn master() -> [u8; 32] {
|
||||||
core::array::from_fn(|i| i as u8)
|
core::array::from_fn(|i| i as u8)
|
||||||
|
|
@ -798,11 +812,11 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn pins_and_contacts_round_trip() {
|
fn pins_and_contacts_round_trip() {
|
||||||
let store = temp_store("pins");
|
let store = temp_store("pins");
|
||||||
assert!(store.server_pin("example.org").unwrap().is_none());
|
assert!(store.server_pin("example.org", DEFAULT_PORT).unwrap().is_none());
|
||||||
store.pin_server("example.org", &[5u8; 32]).unwrap();
|
store.pin_server("example.org", DEFAULT_PORT, &[5u8; 32]).unwrap();
|
||||||
assert_eq!(store.server_pin("example.org").unwrap(), Some([5u8; 32]));
|
assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), Some([5u8; 32]));
|
||||||
store.pin_server("example.org", &[6u8; 32]).unwrap();
|
store.pin_server("example.org", DEFAULT_PORT, &[6u8; 32]).unwrap();
|
||||||
assert_eq!(store.server_pin("example.org").unwrap(), Some([6u8; 32]));
|
assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), Some([6u8; 32]));
|
||||||
|
|
||||||
let key = Account::new(master(), 1).unwrap().me().pk();
|
let key = Account::new(master(), 1).unwrap().me().pk();
|
||||||
assert!(store.contact("bob@example.org").unwrap().is_none());
|
assert!(store.contact("bob@example.org").unwrap().is_none());
|
||||||
|
|
@ -913,14 +927,48 @@ mod tests {
|
||||||
assert_eq!(store.accepted_state("a@example.org").unwrap(), Some(false));
|
assert_eq!(store.accepted_state("a@example.org").unwrap(), Some(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pins scope by port with an asymmetric fallback: the default port
|
||||||
|
/// inherits the bare-host row, and any other port never falls back to
|
||||||
|
/// it — a second server on one host earns its own trust on first use,
|
||||||
|
/// not a mismatch against its sibling's key.
|
||||||
|
#[test]
|
||||||
|
fn pins_scope_by_port_asymmetrically() {
|
||||||
|
let store = temp_store("pin-scope");
|
||||||
|
store
|
||||||
|
.pin_server("example.org", DEFAULT_PORT, &[1u8; KEY_LEN])
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.pin_server("example.org", 1962, &[2u8; KEY_LEN])
|
||||||
|
.unwrap();
|
||||||
|
// The default port reads the legacy bare-host row.
|
||||||
|
assert_eq!(
|
||||||
|
store.server_pin("example.org", DEFAULT_PORT).unwrap(),
|
||||||
|
Some([1u8; KEY_LEN])
|
||||||
|
);
|
||||||
|
// A non-default port reads only its own scoped row.
|
||||||
|
assert_eq!(
|
||||||
|
store.server_pin("example.org", 1962).unwrap(),
|
||||||
|
Some([2u8; KEY_LEN])
|
||||||
|
);
|
||||||
|
// The rule: no fallback to the bare row from another port.
|
||||||
|
assert_eq!(store.server_pin("example.org", 1963).unwrap(), None);
|
||||||
|
// Unpin is scoped too; the default-port row survives it.
|
||||||
|
store.unpin_server("example.org", 1962).unwrap();
|
||||||
|
assert_eq!(store.server_pin("example.org", 1962).unwrap(), None);
|
||||||
|
assert_eq!(
|
||||||
|
store.server_pin("example.org", DEFAULT_PORT).unwrap(),
|
||||||
|
Some([1u8; KEY_LEN])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unpin_returns_to_trust_on_first_use() {
|
fn unpin_returns_to_trust_on_first_use() {
|
||||||
let store = temp_store("unpin");
|
let store = temp_store("unpin");
|
||||||
store.pin_server("example.org", &[1u8; KEY_LEN]).unwrap();
|
store.pin_server("example.org", DEFAULT_PORT, &[1u8; KEY_LEN]).unwrap();
|
||||||
store.unpin_server("example.org").unwrap();
|
store.unpin_server("example.org", DEFAULT_PORT).unwrap();
|
||||||
assert_eq!(store.server_pin("example.org").unwrap(), None);
|
assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), None);
|
||||||
// Unpinning what was never pinned is not an error.
|
// Unpinning what was never pinned is not an error.
|
||||||
store.unpin_server("never.example.org").unwrap();
|
store.unpin_server("never.example.org", DEFAULT_PORT).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use snow::{Builder, TransportState};
|
use snow::{Builder, TransportState};
|
||||||
|
|
||||||
use crate::crypto::{ct_eq, KEY_LEN};
|
use crate::crypto::KEY_LEN;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::transport::{
|
use crate::transport::{
|
||||||
Response, Transport, TransportBindValues, MAX_FRAME, NOISE_PARAMS, NOISE_PAYLOAD, PROLOGUE,
|
Response, Transport, TransportBindValues, MAX_FRAME, NOISE_PARAMS, NOISE_PAYLOAD, PROLOGUE,
|
||||||
|
|
@ -40,16 +40,11 @@ impl TcpTransport {
|
||||||
let mut refused = None;
|
let mut refused = None;
|
||||||
for attempt in 1..=HANDSHAKE_ATTEMPTS {
|
for attempt in 1..=HANDSHAKE_ATTEMPTS {
|
||||||
match handshake(host, port, timeout) {
|
match handshake(host, port, timeout) {
|
||||||
Ok((stream, noise, server_static, bind)) => {
|
Ok((stream, noise, _server_static, bind)) => {
|
||||||
if let Some(pinned) = pinned {
|
// Pin enforcement lives one layer up, in client::connect:
|
||||||
if !ct_eq(&pinned, &server_static) {
|
// it knows the identity host, which a dial hint may have
|
||||||
return Err(Error::PinMismatch {
|
// routed away from, so the mismatch error must name the
|
||||||
host: host.to_string(),
|
// trust object and not wherever the packets went.
|
||||||
pinned,
|
|
||||||
presented: server_static,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(TcpTransport {
|
return Ok(TcpTransport {
|
||||||
stream,
|
stream,
|
||||||
noise,
|
noise,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue