use buffa::Message as _;
use polyc_proto::proto::polychrome::handoff::v1::{Handoff, HandoffReturn};
use crate::{Signer, verify};
fn handoff_canonical_bytes(handoff: &Handoff) -> Vec<u8> {
let mut canonical = handoff.clone();
canonical.signature_hex.clear();
canonical.encode_to_vec()
}
fn handoff_return_canonical_bytes(ret: &HandoffReturn) -> Vec<u8> {
let mut canonical = ret.clone();
canonical.signature_hex.clear();
canonical.encode_to_vec()
}
pub fn sign_handoff_into(signer: &Signer, handoff: &mut Handoff) {
handoff.signed_by = signer.public_key_bytes();
handoff.signature_hex.clear();
let sig = signer.sign(&handoff_canonical_bytes(handoff));
handoff.signature_hex = hex_encode(&sig);
}
pub fn sign_handoff_return_into(signer: &Signer, ret: &mut HandoffReturn) {
ret.signed_by = signer.public_key_bytes();
ret.signature_hex.clear();
let sig = signer.sign(&handoff_return_canonical_bytes(ret));
ret.signature_hex = hex_encode(&sig);
}
#[must_use]
pub fn verify_handoff(public_key: &[u8], handoff: &Handoff) -> bool {
let Some(sig) = hex_decode(&handoff.signature_hex) else {
return false;
};
verify(public_key, &handoff_canonical_bytes(handoff), &sig)
}
#[must_use]
pub fn verify_handoff_return(public_key: &[u8], ret: &HandoffReturn) -> bool {
let Some(sig) = hex_decode(&ret.signature_hex) else {
return false;
};
verify(public_key, &handoff_return_canonical_bytes(ret), &sig)
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
fn hex_decode(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len() / 2);
for pair in bytes.chunks_exact(2) {
let hi = nibble(pair[0])?;
let lo = nibble(pair[1])?;
out.push((hi << 4) | lo);
}
Some(out)
}
const fn nibble(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use polyc_proto::proto::polychrome::agent::v1::{Content, Message, TextContent, content};
use super::*;
fn text_msg(role: &str, text: &str) -> Message {
Message {
role: role.to_owned(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::Text(Box::new(TextContent {
text: text.to_owned(),
..Default::default()
}))),
..Default::default()
}),
internal_only: false,
..Default::default()
}
}
fn sample_handoff() -> Handoff {
Handoff {
child_conversation_id: "child-7".to_owned(),
child_agent_id: "researcher".to_owned(),
carried_count: 2,
carried_context: vec![text_msg("user", "find prior art"), text_msg("model", "ok")],
reason: "delegate research".to_owned(),
..Default::default()
}
}
fn sample_return() -> HandoffReturn {
HandoffReturn {
child_conversation_id: "child-7".to_owned(),
final_message: buffa::MessageField::some(text_msg("model", "result: 42")),
..Default::default()
}
}
#[test]
fn handoff_round_trips() {
let signer = Signer::from_seed(11);
let mut h = sample_handoff();
sign_handoff_into(&signer, &mut h);
assert!(!h.signature_hex.is_empty());
assert_eq!(h.signed_by, signer.public_key_bytes());
assert!(verify_handoff(&h.signed_by, &h));
}
#[test]
fn handoff_tampered_child_id_fails() {
let signer = Signer::from_seed(11);
let mut h = sample_handoff();
sign_handoff_into(&signer, &mut h);
h.child_conversation_id = "child-evil".to_owned();
assert!(!verify_handoff(&h.signed_by, &h));
}
#[test]
fn handoff_tampered_carried_context_fails() {
let signer = Signer::from_seed(11);
let mut h = sample_handoff();
sign_handoff_into(&signer, &mut h);
h.carried_context.push(text_msg("user", "leaked"));
assert!(!verify_handoff(&h.signed_by, &h));
}
#[test]
fn handoff_wrong_key_fails() {
let signer = Signer::from_seed(11);
let other = Signer::from_seed(12);
let mut h = sample_handoff();
sign_handoff_into(&signer, &mut h);
assert!(!verify_handoff(&other.public_key_bytes(), &h));
}
#[test]
fn handoff_unsigned_fails() {
let signer = Signer::from_seed(11);
let h = sample_handoff();
assert!(!verify_handoff(&signer.public_key_bytes(), &h));
}
#[test]
fn handoff_return_round_trips() {
let signer = Signer::from_seed(13);
let mut r = sample_return();
sign_handoff_return_into(&signer, &mut r);
assert!(verify_handoff_return(&signer.public_key_bytes(), &r));
}
#[test]
fn handoff_return_tampered_final_message_fails() {
let signer = Signer::from_seed(13);
let mut r = sample_return();
sign_handoff_return_into(&signer, &mut r);
r.final_message = buffa::MessageField::some(text_msg("model", "tampered"));
assert!(!verify_handoff_return(&signer.public_key_bytes(), &r));
}
#[test]
fn handoff_garbage_signature_hex_returns_false() {
let mut h = sample_handoff();
h.signature_hex = "not-hex!".to_owned();
assert!(!verify_handoff(b"any-key", &h));
h.signature_hex = "abcd".to_owned();
assert!(!verify_handoff(b"any-key", &h));
}
#[test]
fn hex_round_trip() {
for bytes in [
&b""[..],
&[0x00, 0xff, 0x10, 0xab][..],
&(0u8..=255).collect::<Vec<_>>()[..],
] {
let s = hex_encode(bytes);
assert_eq!(hex_decode(&s).as_deref(), Some(bytes));
}
assert!(hex_decode("abc").is_none(), "odd length rejected");
assert!(hex_decode("zz").is_none(), "non-hex rejected");
}
}