use chia_protocol::Bytes32;
use chia_traits::Streamable as _;
use dig_message::{
envelope::InteractionShape, open_message, seal_message, DigMessageEnvelope, ReplayGuard,
SealParams,
};
use dig_tls::bls::{public_key_bytes, SecretKey};
use dig_tls::PeerId;
use crate::error::{DigPeerError, Result};
pub const RPC_MESSAGE_TYPE: u32 = 0x0000_5250;
pub struct SealingIdentity {
secret_key: SecretKey,
epoch: u32,
replay_guard: ReplayGuard,
counter: u64,
}
impl std::fmt::Debug for SealingIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SealingIdentity")
.field("epoch", &self.epoch)
.field("counter", &self.counter)
.field("secret_key", &"<redacted BLS sk>")
.finish()
}
}
impl SealingIdentity {
#[must_use]
pub fn new(secret_key: SecretKey, epoch: u32) -> Self {
Self {
secret_key,
epoch,
replay_guard: ReplayGuard::default(),
counter: 0,
}
}
#[must_use]
pub fn public_key(&self) -> [u8; 48] {
public_key_bytes(&self.secret_key)
}
pub fn seal_request(
&mut self,
sender: PeerId,
recipient: PeerId,
recipient_pub: &[u8; 48],
payload: &[u8],
) -> Result<(Vec<u8>, Bytes32)> {
self.counter = self.counter.wrapping_add(1);
let correlation_id = correlation_from(sender, recipient, self.counter);
let now_ms = now_ms();
let params = SealParams {
sender_sk: &self.secret_key,
sender: peer_id_to_bytes32(sender),
sender_epoch: self.epoch,
recipient: peer_id_to_bytes32(recipient),
recipient_pub,
message_type: RPC_MESSAGE_TYPE,
shape: InteractionShape::Request,
correlation_id,
stream: None,
counter: self.counter,
timestamp_ms: now_ms,
expires_at: 0,
payload,
};
let envelope = seal_message(¶ms).map_err(|e| DigPeerError::Seal(e.to_string()))?;
let bytes = envelope
.to_bytes()
.map_err(|e| DigPeerError::Seal(e.to_string()))?;
Ok((bytes, correlation_id))
}
pub fn open_response(
&mut self,
sender_pub: &[u8; 48],
expected_correlation: Bytes32,
bytes: &[u8],
) -> Result<Vec<u8>> {
let envelope =
DigMessageEnvelope::from_bytes(bytes).map_err(|e| DigPeerError::Seal(e.to_string()))?;
let resolver = |_did: Bytes32, _epoch: u32| -> Option<[u8; 48]> { Some(*sender_pub) };
let opened = open_message(
&self.secret_key,
&envelope,
resolver,
&mut self.replay_guard,
now_ms(),
)
.map_err(|e| DigPeerError::Seal(e.to_string()))?;
if opened.correlation_id != expected_correlation {
return Err(DigPeerError::Misdelivered);
}
Ok(opened.payload)
}
}
fn peer_id_to_bytes32(peer_id: PeerId) -> Bytes32 {
Bytes32::new(*peer_id.as_bytes())
}
fn correlation_from(sender: PeerId, recipient: PeerId, counter: u64) -> Bytes32 {
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&counter.to_be_bytes());
for (i, b) in sender.as_bytes().iter().enumerate() {
bytes[8 + (i % 24)] ^= *b;
}
for (i, b) in recipient.as_bytes().iter().enumerate() {
bytes[8 + (i % 24)] ^= b.rotate_left(3);
}
Bytes32::new(bytes)
}
fn now_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn sk(label: &str) -> SecretKey {
let mut seed = [0u8; 32];
let bytes = label.as_bytes();
seed[..bytes.len().min(32)].copy_from_slice(&bytes[..bytes.len().min(32)]);
SecretKey::from_seed(&seed)
}
fn pid(byte: u8) -> PeerId {
PeerId::from_bytes([byte; 32])
}
#[test]
fn sealed_request_is_ciphertext_and_round_trips_to_the_intended_recipient() {
let sender_sk = sk("seal/sender");
let recipient_sk = sk("seal/recipient");
let recipient_pub = public_key_bytes(&recipient_sk);
let sender_pub = public_key_bytes(&sender_sk);
let mut sender = SealingIdentity::new(sender_sk, 0);
let plaintext = br#"{"jsonrpc":"2.0","id":1,"method":"dig.getPeers"}"#;
let (wire, correlation) = sender
.seal_request(pid(0xAA), pid(0xBB), &recipient_pub, plaintext)
.expect("seal succeeds");
assert!(
!contains_subslice(&wire, b"dig.getPeers"),
"the plaintext method name leaked into the sealed on-wire bytes"
);
let mut recipient = SealingIdentity::new(recipient_sk, 0);
let recovered = recipient
.open_response(&sender_pub, correlation, &wire)
.expect("recipient opens the sealed message");
assert_eq!(recovered, plaintext);
}
#[test]
fn message_sealed_to_one_peer_cannot_be_opened_by_another() {
let sender_sk = sk("wrong/sender");
let sender_pub = public_key_bytes(&sender_sk);
let intended_pub = public_key_bytes(&sk("wrong/intended"));
let wrong_sk = sk("wrong/eavesdropper");
let mut sender = SealingIdentity::new(sender_sk, 0);
let (wire, correlation) = sender
.seal_request(pid(1), pid(2), &intended_pub, b"secret-directed-payload")
.expect("seal succeeds");
let mut wrong = SealingIdentity::new(wrong_sk, 0);
let opened = wrong.open_response(&sender_pub, correlation, &wire);
assert!(
matches!(opened, Err(DigPeerError::Seal(_))),
"a peer the message was NOT sealed to must fail to open it, got {opened:?}"
);
}
#[test]
fn mismatched_correlation_is_rejected_as_misdelivery() {
let sender_sk = sk("corr/sender");
let sender_pub = public_key_bytes(&sender_sk);
let recipient_sk = sk("corr/recipient");
let recipient_pub = public_key_bytes(&recipient_sk);
let mut sender = SealingIdentity::new(sender_sk, 0);
let (wire, _correlation) = sender
.seal_request(pid(3), pid(4), &recipient_pub, b"payload")
.expect("seal succeeds");
let mut recipient = SealingIdentity::new(recipient_sk, 0);
let wrong_correlation = Bytes32::new([0x77; 32]);
let opened = recipient.open_response(&sender_pub, wrong_correlation, &wire);
assert!(
matches!(opened, Err(DigPeerError::Misdelivered)),
"a mismatched correlation must be a Misdelivery, got {opened:?}"
);
}
fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
}