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 canon;
19pub mod handoff;
20pub mod mandate;
21pub mod toolcall;
22
23/// Domain-separation namespace prepended to every polychrome signature.
24pub const NAMESPACE: &[u8] = b"polychrome.v1";
25
26/// An ed25519 signing key.
27#[derive(Clone)]
28pub struct Signer(PrivateKey);
29
30impl Signer {
31    /// Build a [`Signer`] from a deterministic seed.
32    ///
33    /// **Insecure**: for tests and examples only. Production keys come from a
34    /// secret store / WIF, not a seed.
35    #[must_use]
36    pub fn from_seed(seed: u64) -> Self {
37        Self(PrivateKey::from_seed(seed))
38    }
39
40    /// The verifying public key, encoded as bytes (pass to [`verify`]).
41    #[must_use]
42    pub fn public_key_bytes(&self) -> Vec<u8> {
43        self.0.public_key().encode().to_vec()
44    }
45
46    /// Sign `msg` under [`NAMESPACE`]; returns the signature bytes (suitable
47    /// for a `signature` wire field).
48    #[must_use]
49    pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
50        self.0.sign(NAMESPACE, msg).encode().to_vec()
51    }
52}
53
54/// Verify `sig` over `msg` against an encoded `public_key`.
55///
56/// Returns `false` on any decode failure or signature mismatch — never panics.
57#[must_use]
58pub fn verify(public_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
59    let Ok(pk) = PublicKey::decode(public_key) else {
60        return false;
61    };
62    let Ok(sig) = Signature::decode(sig) else {
63        return false;
64    };
65    pk.verify(NAMESPACE, msg, &sig)
66}
67
68#[cfg(test)]
69mod tests {
70    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
71
72    use super::*;
73
74    #[test]
75    fn sign_then_verify_round_trips() {
76        let signer = Signer::from_seed(1);
77        let pk = signer.public_key_bytes();
78        let msg = b"tool-call canonical bytes";
79        let sig = signer.sign(msg);
80        assert!(verify(&pk, msg, &sig));
81    }
82
83    #[test]
84    fn wrong_message_fails() {
85        let signer = Signer::from_seed(1);
86        let pk = signer.public_key_bytes();
87        let sig = signer.sign(b"original");
88        assert!(!verify(&pk, b"tampered", &sig));
89    }
90
91    #[test]
92    fn wrong_key_fails() {
93        let signer = Signer::from_seed(1);
94        let other = Signer::from_seed(2);
95        let msg = b"msg";
96        let sig = signer.sign(msg);
97        assert!(!verify(&other.public_key_bytes(), msg, &sig));
98    }
99
100    #[test]
101    fn garbage_inputs_return_false_not_panic() {
102        assert!(!verify(b"not-a-key", b"m", b"not-a-sig"));
103        assert!(!verify(&[], &[], &[]));
104    }
105}