Add manual signed peer card exchange

This commit is contained in:
Eric Wendland 2026-05-18 04:03:52 +02:00
commit 1ab24631cd
14 changed files with 360 additions and 25 deletions

View file

@ -1,3 +1,4 @@
use geth_crypto::AgentKey;
use geth_types::{AgentId, NodeId, UnixMillis};
use serde::{Deserialize, Serialize};
@ -13,20 +14,93 @@ pub struct PeerCard {
}
impl PeerCard {
pub fn signed(
node_id: NodeId,
agent_key: &AgentKey,
endpoints: Vec<EndpointCandidate>,
issued_at: UnixMillis,
) -> Result<Self, DiscoveryError> {
let agent_id = agent_key.agent_id();
let payload = PeerCardSigningPayload {
node_id: node_id.clone(),
agent_id: agent_id.clone(),
endpoints: endpoints.clone(),
issued_at,
};
let signature = agent_key.sign_canonical(PEER_CARD_SIGNATURE_NAMESPACE, &payload)?;
Ok(Self {
node_id,
agent_id: agent_id.clone(),
endpoints,
issued_at,
signature: SignatureMetadata {
namespace: PEER_CARD_SIGNATURE_NAMESPACE.to_owned(),
signer: agent_id.to_string(),
public_key: agent_key.public_key_hex(),
signature: hex::encode(signature),
},
})
}
pub fn validate_candidate(&self) -> Result<(), DiscoveryError> {
if self.endpoints.is_empty() {
return Err(DiscoveryError::MissingEndpoint);
}
self.verify_signature()
}
pub fn verify_signature(&self) -> Result<(), DiscoveryError> {
if self.signature.namespace != PEER_CARD_SIGNATURE_NAMESPACE {
return Err(DiscoveryError::InvalidSignatureNamespace(
self.signature.namespace.clone(),
));
}
if self.signature.signer.is_empty() || self.signature.signature.is_empty() {
if self.signature.signer.is_empty()
|| self.signature.public_key.is_empty()
|| self.signature.signature.is_empty()
{
return Err(DiscoveryError::UnsignedPeerCard);
}
if self.signature.signer != self.agent_id.as_str() {
return Err(DiscoveryError::SignerMismatch {
signer: self.signature.signer.clone(),
agent: self.agent_id.to_string(),
});
}
let public_key = hex::decode(&self.signature.public_key)?;
let fingerprint = geth_crypto::key_fingerprint(&public_key);
if fingerprint != self.signature.signer {
return Err(DiscoveryError::SignerPublicKeyMismatch {
signer: self.signature.signer.clone(),
fingerprint,
});
}
let signature = hex::decode(&self.signature.signature)?;
geth_crypto::verify_canonical(
&public_key,
PEER_CARD_SIGNATURE_NAMESPACE,
&self.signing_payload(),
&signature,
)?;
Ok(())
}
fn signing_payload(&self) -> PeerCardSigningPayload {
PeerCardSigningPayload {
node_id: self.node_id.clone(),
agent_id: self.agent_id.clone(),
endpoints: self.endpoints.clone(),
issued_at: self.issued_at,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct PeerCardSigningPayload {
node_id: NodeId,
agent_id: AgentId,
endpoints: Vec<EndpointCandidate>,
issued_at: UnixMillis,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -40,6 +114,8 @@ pub struct EndpointCandidate {
pub struct SignatureMetadata {
pub namespace: String,
pub signer: String,
#[serde(default)]
pub public_key: String,
pub signature: String,
}
@ -94,6 +170,14 @@ pub enum DiscoveryError {
UnsignedPeerCard,
#[error("invalid peer card signature namespace: {0}")]
InvalidSignatureNamespace(String),
#[error("peer card signer {signer} does not match agent {agent}")]
SignerMismatch { signer: String, agent: String },
#[error("peer card signer {signer} does not match public key fingerprint {fingerprint}")]
SignerPublicKeyMismatch { signer: String, fingerprint: String },
#[error("invalid peer card hex: {0}")]
Hex(#[from] hex::FromHexError),
#[error("peer card signature error: {0}")]
Crypto(#[from] geth_crypto::CryptoError),
}
#[must_use]
@ -106,21 +190,19 @@ mod tests {
use super::*;
fn signed_card() -> PeerCard {
PeerCard {
node_id: "node:laptop".into(),
agent_id: "agent:abc".into(),
endpoints: vec![EndpointCandidate {
let key = AgentKey::generate();
let agent_id = key.agent_id();
PeerCard::signed(
format!("node:{agent_id}").into(),
&key,
vec![EndpointCandidate {
endpoint_id: "endpoint:iroh".to_owned(),
relay_url: None,
source: DiscoverySource::Manual,
}],
issued_at: UnixMillis(1),
signature: SignatureMetadata {
namespace: PEER_CARD_SIGNATURE_NAMESPACE.to_owned(),
signer: "agent:abc".to_owned(),
signature: "sig:test".to_owned(),
},
}
UnixMillis(1),
)
.expect("signed card")
}
#[test]
@ -146,4 +228,14 @@ mod tests {
Err(DiscoveryError::UnsignedPeerCard)
));
}
#[test]
fn tampered_peer_card_signature_is_rejected() {
let mut card = signed_card();
card.endpoints[0].endpoint_id = "endpoint:tampered".to_owned();
assert!(matches!(
card.validate_candidate(),
Err(DiscoveryError::Crypto(geth_crypto::CryptoError::Verify))
));
}
}