polyc-crypto 0.1.3

Provenance signatures (commonware-cryptography ed25519) for polychrome tool calls.
Documentation
//! Provenance signatures for polychrome.
//!
//! Wraps `commonware-cryptography` ed25519 so the harness can sign the bytes
//! that back a tool call's `signature` field and the control plane can verify
//! them. A fixed [`NAMESPACE`] provides cross-domain separation (a signature
//! minted here cannot be replayed in another context).
//!
//! This primitive is runtime-agnostic — it does not pull in `commonware-runtime`
//! or `commonware-p2p`.

use commonware_codec::{DecodeExt, Encode};
use commonware_cryptography::{
    Signer as _, Verifier as _,
    ed25519::{PrivateKey, PublicKey, Signature},
};

pub mod approval;
pub mod handoff;
pub mod toolcall;

/// Domain-separation namespace prepended to every polychrome signature.
pub const NAMESPACE: &[u8] = b"polychrome.v1";

/// An ed25519 signing key.
#[derive(Clone)]
pub struct Signer(PrivateKey);

impl Signer {
    /// Build a [`Signer`] from a deterministic seed.
    ///
    /// **Insecure**: for tests and examples only. Production keys come from a
    /// secret store / WIF, not a seed.
    #[must_use]
    pub fn from_seed(seed: u64) -> Self {
        Self(PrivateKey::from_seed(seed))
    }

    /// The verifying public key, encoded as bytes (pass to [`verify`]).
    #[must_use]
    pub fn public_key_bytes(&self) -> Vec<u8> {
        self.0.public_key().encode().to_vec()
    }

    /// Sign `msg` under [`NAMESPACE`]; returns the signature bytes (suitable
    /// for a `signature` wire field).
    #[must_use]
    pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
        self.0.sign(NAMESPACE, msg).encode().to_vec()
    }
}

/// Verify `sig` over `msg` against an encoded `public_key`.
///
/// Returns `false` on any decode failure or signature mismatch — never panics.
#[must_use]
pub fn verify(public_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
    let Ok(pk) = PublicKey::decode(public_key) else {
        return false;
    };
    let Ok(sig) = Signature::decode(sig) else {
        return false;
    };
    pk.verify(NAMESPACE, msg, &sig)
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]

    use super::*;

    #[test]
    fn sign_then_verify_round_trips() {
        let signer = Signer::from_seed(1);
        let pk = signer.public_key_bytes();
        let msg = b"tool-call canonical bytes";
        let sig = signer.sign(msg);
        assert!(verify(&pk, msg, &sig));
    }

    #[test]
    fn wrong_message_fails() {
        let signer = Signer::from_seed(1);
        let pk = signer.public_key_bytes();
        let sig = signer.sign(b"original");
        assert!(!verify(&pk, b"tampered", &sig));
    }

    #[test]
    fn wrong_key_fails() {
        let signer = Signer::from_seed(1);
        let other = Signer::from_seed(2);
        let msg = b"msg";
        let sig = signer.sign(msg);
        assert!(!verify(&other.public_key_bytes(), msg, &sig));
    }

    #[test]
    fn garbage_inputs_return_false_not_panic() {
        assert!(!verify(b"not-a-key", b"m", b"not-a-sig"));
        assert!(!verify(&[], &[], &[]));
    }
}