⎇ d3649bfd1a80153652c2a028315369bb4b184024
crates/core/brahman-handshake/src/signature.rs
Historia
Quién tocó qué
crudo
feat(brahman-handshake): Fase 3 — trust remoto via firma Ed25519
1//! Firma y verificación del payload del `Hello` para trust remoto.
2//!
3//! Usa la identidad Ed25519 de libp2p (la misma keypair que el peer
4//! presenta al swarm vía Noise). Esto ancla la identidad criptográfica
5//! del Ente a la identidad de transporte: si Noise autenticó al
6//! `peer_id` X, sólo X puede firmar Cards válidas para esa conexión.
7//!
8//! ## Payload firmado
9//!
10//! Bytes postcard de la tupla `(WireCard, Option<WitInterface>)`. Se
11//! eligió postcard porque ya es el wire format del resto del protocolo:
12//! mismo determinismo, sin convertir a otro formato sólo para firmar.
13//!
14//! Cualquier campo que entre al payload firmado en el futuro debe
15//! añadirse al final de la tupla (postcard es position-dependent), o
16//! bumpearse el [`SIGNATURE_VERSION`] para distinguir esquemas.
17
18use brahman_card::{WireCard, WitInterface};
19use brahman_net::{Keypair, PeerId, PublicKey};
20
21use crate::messages::HelloSignature;
22
23/// Versión del esquema de payload firmado. Si cambia el shape de
24/// `(WireCard, Option<WitInterface>)` o cómo se serializa, bump este
25/// número y el verificador rechaza firmas antiguas.
26pub const SIGNATURE_VERSION: u8 = 1;
27
28/// Errores de verificación de firma.
29#[derive(Debug, thiserror::Error)]
30pub enum SignatureError {
31 #[error("public_key inválida (libp2p decode protobuf): {0}")]
32 DecodeKey(String),
33 #[error("encode del payload falló: {0}")]
34 EncodePayload(String),
35 #[error("firma rechazada: bytes inválidos para la public_key")]
36 Invalid,
37 #[error("peer_id de la firma ({signer}) no coincide con el peer libp2p autenticado ({expected})")]
38 PeerMismatch { signer: PeerId, expected: PeerId },
39 #[error("firma del Hello faltante (requerida para conexión remota libp2p)")]
40 Missing,
41 #[error("firma del Hello inesperada en path local sin trust remoto")]
42 Unexpected,
43}
44
45/// Construye los bytes canónicos a firmar/verificar para un Hello.
46/// Postcard determinístico de `(version, WireCard, Option<WitInterface>)`.
47fn payload_bytes(card: &WireCard, wit: &Option<WitInterface>) -> Result<Vec<u8>, SignatureError> {
48 let tup = (SIGNATURE_VERSION, card, wit);
49 postcard::to_allocvec(&tup).map_err(|e| SignatureError::EncodePayload(e.to_string()))
50}
51
52/// Firma `(card, wit)` con la `keypair`. La public key derivada de
53/// `keypair` debe coincidir con la identidad libp2p del peer cuando
54/// el verificador la chequee.
55pub fn sign_hello(
56 keypair: &Keypair,
57 card: &WireCard,
58 wit: &Option<WitInterface>,
59) -> Result<HelloSignature, SignatureError> {
60 let bytes = payload_bytes(card, wit)?;
61 let signature_bytes = keypair
62 .sign(&bytes)
63 .map_err(|e| SignatureError::EncodePayload(e.to_string()))?;
64 Ok(HelloSignature {
65 public_key: keypair.public().encode_protobuf(),
66 signature: signature_bytes,
67 })
68}
69
70/// Verifica que `sig` es una firma válida sobre `(card, wit)` y que
71/// la public key declarada coincide con `expected_peer` (la identidad
72/// libp2p autenticada por Noise).
73///
74/// Devuelve `Ok(())` si todo cuadra; si no, el error concreto.
75pub fn verify_hello(
76 sig: &HelloSignature,
77 card: &WireCard,
78 wit: &Option<WitInterface>,
79 expected_peer: PeerId,
80) -> Result<(), SignatureError> {
81 let public_key = PublicKey::try_decode_protobuf(&sig.public_key)
82 .map_err(|e| SignatureError::DecodeKey(e.to_string()))?;
83 let signer_peer = public_key.to_peer_id();
84 if signer_peer != expected_peer {
85 return Err(SignatureError::PeerMismatch {
86 signer: signer_peer,
87 expected: expected_peer,
88 });
89 }
90 let bytes = payload_bytes(card, wit)?;
91 if !public_key.verify(&bytes, &sig.signature) {
92 return Err(SignatureError::Invalid);
93 }
94 Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use brahman_card::Card;
101
102 fn sample_card() -> WireCard {
103 Card::new("test.signed").into()
104 }
105
106 #[test]
107 fn sign_then_verify_roundtrip() {
108 let kp = Keypair::generate_ed25519();
109 let peer = kp.public().to_peer_id();
110 let card = sample_card();
111 let wit = None;
112 let sig = sign_hello(&kp, &card, &wit).unwrap();
113 verify_hello(&sig, &card, &wit, peer).expect("firma propia debe verificar");
114 }
115
116 #[test]
117 fn verify_rejects_wrong_peer() {
118 let kp = Keypair::generate_ed25519();
119 let other = Keypair::generate_ed25519().public().to_peer_id();
120 let card = sample_card();
121 let wit = None;
122 let sig = sign_hello(&kp, &card, &wit).unwrap();
123 let err = verify_hello(&sig, &card, &wit, other).unwrap_err();
124 assert!(matches!(err, SignatureError::PeerMismatch { .. }), "got {err:?}");
125 }
126
127 #[test]
128 fn verify_rejects_tampered_card() {
129 let kp = Keypair::generate_ed25519();
130 let peer = kp.public().to_peer_id();
131 let original = sample_card();
132 let wit = None;
133 let sig = sign_hello(&kp, &original, &wit).unwrap();
134
135 // Verificamos contra una Card distinta (mismo shape, distinto label).
136 let tampered: WireCard = Card::new("test.tampered").into();
137 let err = verify_hello(&sig, &tampered, &wit, peer).unwrap_err();
138 assert!(matches!(err, SignatureError::Invalid), "got {err:?}");
139 }
140
141 #[test]
142 fn verify_rejects_corrupted_signature() {
143 let kp = Keypair::generate_ed25519();
144 let peer = kp.public().to_peer_id();
145 let card = sample_card();
146 let wit = None;
147 let mut sig = sign_hello(&kp, &card, &wit).unwrap();
148 // Flip un bit de la firma.
149 if let Some(b) = sig.signature.last_mut() {
150 *b ^= 0x01;
151 }
152 let err = verify_hello(&sig, &card, &wit, peer).unwrap_err();
153 assert!(matches!(err, SignatureError::Invalid), "got {err:?}");
154 }
155}