Add manual signed peer card exchange
This commit is contained in:
parent
20c88af800
commit
1ab24631cd
14 changed files with 360 additions and 25 deletions
|
|
@ -8,4 +8,6 @@ license.workspace = true
|
|||
[dependencies]
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
hex.workspace = true
|
||||
geth-crypto = { path = "../geth-crypto" }
|
||||
geth-types = { path = "../geth-types" }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue