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 vector_sender() -> PeerId {
let mut b = [0u8; 32];
for (i, x) in b.iter_mut().enumerate() {
*x = (i as u8).wrapping_mul(7).wrapping_add(0x13);
}
PeerId::from_bytes(b)
}
fn vector_recipient() -> PeerId {
let mut b = [0u8; 32];
for (i, x) in b.iter_mut().enumerate() {
*x = (i as u8).wrapping_mul(11).wrapping_add(0xa7);
}
PeerId::from_bytes(b)
}
const VECTOR_SEED: [u8; 32] = [
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
0x1e, 0x1f,
];
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[test]
fn vector_bls_g1_public_key_from_fixed_seed() {
let pk = public_key_bytes(&SecretKey::from_seed(&VECTOR_SEED));
assert_eq!(hex(&pk), "8f336467f057b373bb3c43815a10ec131119d1bf50c14fa3f9ad86c0ec074f920f936a5315a8365a37fee0afa34c32c6", "BLS-G1 derivation drifted");
}
#[test]
fn vector_correlation_id_for_a_fixed_directed_pair() {
let mut id = SealingIdentity::new(SecretKey::from_seed(&VECTOR_SEED), 7);
let recipient_pub = public_key_bytes(&SecretKey::from_seed(&[0x5au8; 32]));
let (_wire, correlation) = id
.seal_request(
vector_sender(),
vector_recipient(),
&recipient_pub,
b"vector-payload",
)
.expect("seal succeeds");
assert_eq!(
hex(correlation.as_ref()),
"0000000000000001e8982b38b82918e8b402f1613edf7f1e3999fa5b83d26191",
"correlation id derivation drifted"
);
}
#[test]
fn vector_sealed_envelope_header_bytes() {
let mut id = SealingIdentity::new(SecretKey::from_seed(&VECTOR_SEED), 7);
let recipient_pub = public_key_bytes(&SecretKey::from_seed(&[0x5au8; 32]));
let (wire, _correlation) = id
.seal_request(
vector_sender(),
vector_recipient(),
&recipient_pub,
b"vector-payload",
)
.expect("seal succeeds");
let envelope = DigMessageEnvelope::from_bytes(&wire).expect("envelope parses");
let header = envelope.header_bytes().expect("header serializes");
assert_eq!(hex(&header), "0100005250050000000000000001e8982b38b82918e8b402f1613edf7f1e3999fa5b83d26191131a21282f363d444b525960676e757c838a91989fa6adb4bbc2c9d0d7dee5eca7b2bdc8d3dee9f4ff0a15202b36414c57626d78838e99a4afbac5d0dbe6f1fc0000000700", "envelope header encoding drifted");
}
#[test]
fn vector_peer_id_to_bytes32_streamable_encoding() {
let mut id = SealingIdentity::new(SecretKey::from_seed(&VECTOR_SEED), 7);
let recipient_pub = public_key_bytes(&SecretKey::from_seed(&[0x5au8; 32]));
let (wire, _c) = id
.seal_request(
vector_sender(),
vector_recipient(),
&recipient_pub,
b"vector-payload",
)
.expect("seal succeeds");
let envelope = DigMessageEnvelope::from_bytes(&wire).expect("envelope parses");
assert_eq!(
hex(&chia_traits::Streamable::to_bytes(&envelope.sender).unwrap()),
"131a21282f363d444b525960676e757c838a91989fa6adb4bbc2c9d0d7dee5ec"
);
assert_eq!(
hex(&chia_traits::Streamable::to_bytes(&envelope.recipient).unwrap()),
"a7b2bdc8d3dee9f4ff0a15202b36414c57626d78838e99a4afbac5d0dbe6f1fc"
);
}
fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
haystack.windows(needle.len()).any(|w| w == needle)
}
}