Skip to main content

polyc_crypto/
lib.rs

1//! Provenance signatures for polychrome.
2//!
3//! Wraps `commonware-cryptography` ed25519 so the harness can sign the bytes
4//! that back a tool call's `signature` field and the control plane can verify
5//! them. A fixed [`NAMESPACE`] provides cross-domain separation (a signature
6//! minted here cannot be replayed in another context).
7//!
8//! This primitive is runtime-agnostic — it does not pull in `commonware-runtime`
9//! or `commonware-p2p`.
10
11use commonware_codec::{DecodeExt, Encode};
12use commonware_cryptography::{
13    Signer as _, Verifier as _,
14    ed25519::{PrivateKey, PublicKey, Signature},
15};
16
17pub mod approval;
18pub mod handoff;
19pub mod toolcall;
20
21/// Domain-separation namespace prepended to every polychrome signature.
22pub const NAMESPACE: &[u8] = b"polychrome.v1";
23
24/// An ed25519 signing key.
25#[derive(Clone)]
26pub struct Signer(PrivateKey);
27
28impl Signer {
29    /// Build a [`Signer`] from a deterministic seed.
30    ///
31    /// **Insecure**: for tests and examples only. Production keys come from a
32    /// secret store / WIF, not a seed.
33    #[must_use]
34    pub fn from_seed(seed: u64) -> Self {
35        Self(PrivateKey::from_seed(seed))
36    }
37
38    /// The verifying public key, encoded as bytes (pass to [`verify`]).
39    #[must_use]
40    pub fn public_key_bytes(&self) -> Vec<u8> {
41        self.0.public_key().encode().to_vec()
42    }
43
44    /// Sign `msg` under [`NAMESPACE`]; returns the signature bytes (suitable
45    /// for a `signature` wire field).
46    #[must_use]
47    pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
48        self.0.sign(NAMESPACE, msg).encode().to_vec()
49    }
50}
51
52/// Verify `sig` over `msg` against an encoded `public_key`.
53///
54/// Returns `false` on any decode failure or signature mismatch — never panics.
55#[must_use]
56pub fn verify(public_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
57    let Ok(pk) = PublicKey::decode(public_key) else {
58        return false;
59    };
60    let Ok(sig) = Signature::decode(sig) else {
61        return false;
62    };
63    pk.verify(NAMESPACE, msg, &sig)
64}
65
66#[cfg(test)]
67mod tests {
68    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
69
70    use super::*;
71
72    #[test]
73    fn sign_then_verify_round_trips() {
74        let signer = Signer::from_seed(1);
75        let pk = signer.public_key_bytes();
76        let msg = b"tool-call canonical bytes";
77        let sig = signer.sign(msg);
78        assert!(verify(&pk, msg, &sig));
79    }
80
81    #[test]
82    fn wrong_message_fails() {
83        let signer = Signer::from_seed(1);
84        let pk = signer.public_key_bytes();
85        let sig = signer.sign(b"original");
86        assert!(!verify(&pk, b"tampered", &sig));
87    }
88
89    #[test]
90    fn wrong_key_fails() {
91        let signer = Signer::from_seed(1);
92        let other = Signer::from_seed(2);
93        let msg = b"msg";
94        let sig = signer.sign(msg);
95        assert!(!verify(&other.public_key_bytes(), msg, &sig));
96    }
97
98    #[test]
99    fn garbage_inputs_return_false_not_panic() {
100        assert!(!verify(b"not-a-key", b"m", b"not-a-sig"));
101        assert!(!verify(&[], &[], &[]));
102    }
103}