use buffa::Message as _;
use polyc_proto::proto::polychrome::agent::v1::AssertedAttribution;
use sha2::{Digest, Sha256};
use crate::{Signer, verify};
pub(crate) const EDGE_IDENTITY_DOMAIN_PREFIX: &[u8] = b"polychrome.edge-identity.v1\0";
fn canonical_bytes(a: &AssertedAttribution) -> Vec<u8> {
let mut canonical = a.clone();
canonical.signature_hex.clear();
let encoded = canonical.encode_to_vec();
let mut bytes = Vec::with_capacity(EDGE_IDENTITY_DOMAIN_PREFIX.len() + encoded.len());
bytes.extend_from_slice(EDGE_IDENTITY_DOMAIN_PREFIX);
bytes.extend_from_slice(&encoded);
bytes
}
#[must_use]
pub fn sign_edge_assertion(signer: &Signer, a: &AssertedAttribution) -> String {
crate::hex::lower(&signer.sign(&canonical_bytes(a)))
}
pub fn sign_edge_assertion_into(signer: &Signer, a: &mut AssertedAttribution) {
a.signature_hex = sign_edge_assertion(signer, a);
}
#[must_use]
pub fn content_hash_hex(messages_encoded: &[u8]) -> String {
crate::hex::lower(&Sha256::digest(messages_encoded))
}
#[must_use]
pub fn verify_edge_assertion(public_key: &[u8], a: &AssertedAttribution) -> bool {
let Some(sig) = crate::hex::decode(&a.signature_hex) else {
return false;
};
verify(public_key, &canonical_bytes(a), &sig)
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use polyc_proto::proto::polychrome::persona::v1::ExternalIdentity;
use super::*;
fn sample_caller() -> ExternalIdentity {
ExternalIdentity {
provider: "slack".to_owned(),
scope: "team-1".to_owned(),
external_id: "U123".to_owned(),
display_name: "Ada".to_owned(),
..Default::default()
}
}
fn sample_assertion() -> AssertedAttribution {
AssertedAttribution {
edge_id: "slack".to_owned(),
conversation_id: "slack:team-1:general".to_owned(),
nonce: "nonce-1".to_owned(),
issued_unix_ms: 1_700_000_000_000,
caller: buffa::MessageField::some(sample_caller()),
exec_id: "exec-1".to_owned(),
content_hash: content_hash_hex(b"messages-bytes"),
..Default::default()
}
}
#[test]
fn round_trips() {
let signer = Signer::from_seed(31);
let pk = signer.public_key_bytes();
let mut a = sample_assertion();
sign_edge_assertion_into(&signer, &mut a);
assert!(!a.signature_hex.is_empty());
assert!(verify_edge_assertion(&pk, &a));
}
#[test]
fn content_hash_hex_is_deterministic_and_content_sensitive() {
let a = content_hash_hex(b"hello");
let b = content_hash_hex(b"hello");
let c = content_hash_hex(b"hellp");
assert_eq!(a, b);
assert_ne!(a, c);
assert_eq!(a.len(), 64, "sha256 hex is 64 lowercase hex chars");
assert!(
a.chars()
.all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase())
);
}
#[test]
fn tampered_exec_id_fails() {
let signer = Signer::from_seed(31);
let pk = signer.public_key_bytes();
let mut a = sample_assertion();
sign_edge_assertion_into(&signer, &mut a);
a.exec_id = "exec-2".to_owned();
assert!(!verify_edge_assertion(&pk, &a));
}
#[test]
fn a_tampered_namespace_claim_fails_verification() {
let signer = Signer::from_seed(31);
let pk = signer.public_key_bytes();
let mut a = sample_assertion();
a.namespace = "slack".to_owned();
sign_edge_assertion_into(&signer, &mut a);
assert!(verify_edge_assertion(&pk, &a));
a.namespace = "mail".to_owned();
assert!(!verify_edge_assertion(&pk, &a));
}
#[test]
fn tampered_content_hash_fails() {
let signer = Signer::from_seed(31);
let pk = signer.public_key_bytes();
let mut a = sample_assertion();
sign_edge_assertion_into(&signer, &mut a);
a.content_hash = content_hash_hex(b"different-messages-bytes");
assert!(!verify_edge_assertion(&pk, &a));
}
#[test]
fn tampered_edge_id_fails() {
let signer = Signer::from_seed(31);
let pk = signer.public_key_bytes();
let mut a = sample_assertion();
sign_edge_assertion_into(&signer, &mut a);
a.edge_id = "telegram".to_owned();
assert!(!verify_edge_assertion(&pk, &a));
}
#[test]
fn tampered_caller_fails() {
let signer = Signer::from_seed(31);
let pk = signer.public_key_bytes();
let mut a = sample_assertion();
sign_edge_assertion_into(&signer, &mut a);
a.caller = buffa::MessageField::some(ExternalIdentity {
external_id: "U999".to_owned(),
..sample_caller()
});
assert!(!verify_edge_assertion(&pk, &a));
}
#[test]
fn wrong_key_fails() {
let signer = Signer::from_seed(31);
let other = Signer::from_seed(32);
let mut a = sample_assertion();
sign_edge_assertion_into(&signer, &mut a);
assert!(!verify_edge_assertion(&other.public_key_bytes(), &a));
}
#[test]
fn unsigned_fails() {
let signer = Signer::from_seed(31);
let a = sample_assertion();
assert!(!verify_edge_assertion(&signer.public_key_bytes(), &a));
}
#[test]
fn garbage_signature_hex_returns_false_not_panic() {
let signer = Signer::from_seed(31);
let pk = signer.public_key_bytes();
let mut a = sample_assertion();
a.signature_hex = "not-hex!".to_owned();
assert!(!verify_edge_assertion(&pk, &a));
a.signature_hex = "abc".to_owned();
assert!(!verify_edge_assertion(&pk, &a));
}
#[test]
fn domain_confusion_is_rejected() {
let signer = Signer::from_seed(31);
let pk = signer.public_key_bytes();
let mut a = sample_assertion();
a.signature_hex.clear();
let raw_encoded = a.encode_to_vec();
let forged_sig = signer.sign(&raw_encoded);
a.signature_hex = crate::hex::lower(&forged_sig);
assert!(
!verify_edge_assertion(&pk, &a),
"a signature minted over the undomain-prefixed encoding must not verify"
);
}
}