Skip to main content

treeship_core/attestation/
signer.rs

1use ed25519_dalek::{SigningKey, VerifyingKey, Signer as DalekSigner};
2use rand::rngs::OsRng;
3
4/// `Signer` is the interface for anything that can sign PAE bytes.
5///
6/// The abstraction lets us swap in hardware keys (Secure Enclave, YubiKey),
7/// FROST threshold keys, or test signers without changing the attestation layer.
8///
9/// Implementations must sign the PAE bytes as-is — never hash them again,
10/// never parse them. The PAE construction has already bound the payloadType
11/// and payload into a single unambiguous byte string.
12pub trait Signer: Send + Sync {
13    /// Signs the PAE bytes. Returns raw signature bytes.
14    /// Ed25519 signatures are always 64 bytes.
15    fn sign(&self, pae: &[u8]) -> Result<Vec<u8>, SignerError>;
16
17    /// The stable key identifier. Format: "key_<hex>" from the keystore.
18    fn key_id(&self) -> &str;
19
20    /// The raw public key bytes (32 bytes for Ed25519).
21    /// Used for key registration, Verifier construction, and fingerprinting.
22    fn public_key_bytes(&self) -> Vec<u8>;
23}
24
25/// An error produced by a Signer.
26#[derive(Debug)]
27pub struct SignerError(pub String);
28
29impl std::fmt::Display for SignerError {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "signer error: {}", self.0)
32    }
33}
34
35impl std::error::Error for SignerError {}
36
37/// The default Ed25519 signer.
38///
39/// Holds an Ed25519 signing key in memory. In production, keys are loaded
40/// from the encrypted keystore — this struct is never constructed with a
41/// plaintext key in application code.
42///
43/// `ed25519-dalek` uses the `subtle` crate throughout for constant-time
44/// scalar operations, providing side-channel resistance.
45pub struct Ed25519Signer {
46    key_id:      String,
47    signing_key: SigningKey,
48}
49
50impl Ed25519Signer {
51    /// Constructs an Ed25519Signer from a pre-loaded 64-byte private key.
52    pub fn from_bytes(key_id: impl Into<String>, bytes: &[u8; 32]) -> Result<Self, SignerError> {
53        let key_id = key_id.into();
54        if key_id.is_empty() {
55            return Err(SignerError("key_id must not be empty".into()));
56        }
57        let signing_key = SigningKey::from_bytes(bytes);
58        Ok(Self { key_id, signing_key })
59    }
60
61    /// Generates a fresh Ed25519 keypair using the OS CSPRNG.
62    ///
63    /// Used by `treeship init` and tests. In production, key generation
64    /// goes through the keystore which handles encrypted storage.
65    pub fn generate(key_id: impl Into<String>) -> Result<Self, SignerError> {
66        let key_id = key_id.into();
67        if key_id.is_empty() {
68            return Err(SignerError("key_id must not be empty".into()));
69        }
70        let signing_key = SigningKey::generate(&mut OsRng);
71        Ok(Self { key_id, signing_key })
72    }
73
74    /// Returns the `VerifyingKey` (public key) for building a `Verifier`.
75    pub fn verifying_key(&self) -> VerifyingKey {
76        self.signing_key.verifying_key()
77    }
78
79    /// Returns the 32-byte private key scalar.
80    /// Only exposed for keystore serialization — never log or transmit this.
81    pub fn secret_bytes(&self) -> [u8; 32] {
82        self.signing_key.to_bytes()
83    }
84}
85
86impl Signer for Ed25519Signer {
87    fn sign(&self, pae: &[u8]) -> Result<Vec<u8>, SignerError> {
88        // ed25519-dalek's sign() uses the full ExpandedSecretKey internally,
89        // which includes both the scalar and the nonce material. No need for
90        // an external random source — the nonce is deterministic from the key
91        // and message (RFC 8032 §5.1.6).
92        let signature = self.signing_key.sign(pae);
93        Ok(signature.to_bytes().to_vec())
94    }
95
96    fn key_id(&self) -> &str {
97        &self.key_id
98    }
99
100    fn public_key_bytes(&self) -> Vec<u8> {
101        self.signing_key.verifying_key().to_bytes().to_vec()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::attestation::pae;
109
110    fn test_pae() -> Vec<u8> {
111        pae("application/vnd.treeship.action.v1+json", b"{\"actor\":\"agent://test\"}")
112    }
113
114    #[test]
115    fn generate_succeeds() {
116        let s = Ed25519Signer::generate("key_test_01").unwrap();
117        assert_eq!(s.key_id(), "key_test_01");
118        assert_eq!(s.public_key_bytes().len(), 32);
119    }
120
121    #[test]
122    fn empty_key_id_errors() {
123        assert!(Ed25519Signer::generate("").is_err());
124    }
125
126    #[test]
127    fn sign_produces_64_bytes() {
128        let signer = Ed25519Signer::generate("key_test").unwrap();
129        let sig = signer.sign(&test_pae()).unwrap();
130        assert_eq!(sig.len(), 64, "Ed25519 signatures are always 64 bytes");
131    }
132
133    #[test]
134    fn sign_is_deterministic_for_same_key_and_message() {
135        // Ed25519 (RFC 8032) uses deterministic nonce — same key + message
136        // always produces the same signature. This is a security property:
137        // non-deterministic signing would leak key material if the RNG is weak.
138        let signer = Ed25519Signer::generate("key_det").unwrap();
139        let msg    = test_pae();
140        let sig1   = signer.sign(&msg).unwrap();
141        let sig2   = signer.sign(&msg).unwrap();
142        assert_eq!(sig1, sig2, "Ed25519 signing must be deterministic");
143    }
144
145    #[test]
146    fn different_keys_produce_different_signatures() {
147        let s1 = Ed25519Signer::generate("key_1").unwrap();
148        let s2 = Ed25519Signer::generate("key_2").unwrap();
149        let msg = test_pae();
150        assert_ne!(
151            s1.sign(&msg).unwrap(),
152            s2.sign(&msg).unwrap(),
153            "Different keys must produce different signatures"
154        );
155    }
156
157    #[test]
158    fn different_messages_produce_different_signatures() {
159        let signer = Ed25519Signer::generate("key_test").unwrap();
160        let pae1 = pae("application/vnd.treeship.action.v1+json",   b"{\"a\":1}");
161        let pae2 = pae("application/vnd.treeship.approval.v1+json",  b"{\"a\":1}");
162        assert_ne!(
163            signer.sign(&pae1).unwrap(),
164            signer.sign(&pae2).unwrap()
165        );
166    }
167
168    #[test]
169    fn roundtrip_from_bytes() {
170        let original = Ed25519Signer::generate("key_rt").unwrap();
171        let secret   = original.secret_bytes();
172        let restored = Ed25519Signer::from_bytes("key_rt", &secret).unwrap();
173
174        assert_eq!(original.public_key_bytes(), restored.public_key_bytes());
175
176        let msg   = test_pae();
177        let sig_a = original.sign(&msg).unwrap();
178        let sig_b = restored.sign(&msg).unwrap();
179        assert_eq!(sig_a, sig_b, "Restored key must produce identical signatures");
180    }
181}