Skip to main content

shape_runtime/crypto/
keychain.rs

1//! Trusted author keychain for module signature verification.
2//!
3//! The `Keychain` stores a set of trusted Ed25519 public keys with associated
4//! trust levels, and provides verification of module signatures against the
5//! trust policy.
6
7use super::signing::ModuleSignatureData;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// How much trust is granted to a particular author key.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum TrustLevel {
14    /// Trusted for all modules.
15    Full,
16    /// Trusted only for modules whose names match one of the listed prefixes.
17    Scoped(Vec<String>),
18    /// Trusted only for a single specific manifest hash.
19    Pinned([u8; 32]),
20}
21
22/// A trusted author entry in the keychain.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TrustedAuthor {
25    /// Human-readable name for this author.
26    pub name: String,
27    /// Ed25519 public key (32 bytes).
28    pub public_key: [u8; 32],
29    /// Trust level governing which modules this key may sign.
30    pub trust_level: TrustLevel,
31}
32
33/// A keychain managing trusted author keys and module signature verification.
34#[derive(Clone)]
35pub struct Keychain {
36    trusted: HashMap<[u8; 32], TrustedAuthor>,
37    require_signatures: bool,
38}
39
40/// Result of verifying a module against the keychain.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum VerifyResult {
43    /// Signature is valid and the author is trusted for this module.
44    Trusted,
45    /// No signature present and signatures are not required.
46    Unsigned,
47    /// Verification failed for the given reason.
48    Rejected(String),
49}
50
51impl Keychain {
52    /// Create a new keychain.
53    ///
54    /// When `require_signatures` is `true`, unsigned modules are rejected.
55    pub fn new(require_signatures: bool) -> Self {
56        Self {
57            trusted: HashMap::new(),
58            require_signatures,
59        }
60    }
61
62    /// Add or replace a trusted author in the keychain.
63    pub fn add_trusted(&mut self, author: TrustedAuthor) {
64        self.trusted.insert(author.public_key, author);
65    }
66
67    /// Remove a trusted author by public key.
68    ///
69    /// Returns the removed author, or `None` if the key was not in the keychain.
70    pub fn remove_trusted(&mut self, public_key: &[u8; 32]) -> Option<TrustedAuthor> {
71        self.trusted.remove(public_key)
72    }
73
74    /// Check whether the given public key is trusted for a module with the
75    /// specified name and manifest hash.
76    pub fn is_trusted(
77        &self,
78        public_key: &[u8; 32],
79        module_name: &str,
80        manifest_hash: &[u8; 32],
81    ) -> bool {
82        let Some(author) = self.trusted.get(public_key) else {
83            return false;
84        };
85        match &author.trust_level {
86            TrustLevel::Full => true,
87            TrustLevel::Scoped(prefixes) => prefixes
88                .iter()
89                .any(|prefix| module_name.starts_with(prefix)),
90            TrustLevel::Pinned(pinned_hash) => pinned_hash == manifest_hash,
91        }
92    }
93
94    /// Verify a module's signature against the keychain trust policy.
95    ///
96    /// Checks:
97    /// 1. If no signature is present, passes only when signatures are not required.
98    /// 2. Cryptographic validity of the Ed25519 signature.
99    /// 3. The signing key is in the keychain and trusted for this module.
100    pub fn verify_module(
101        &self,
102        module_name: &str,
103        manifest_hash: &[u8; 32],
104        signature: Option<&ModuleSignatureData>,
105    ) -> VerifyResult {
106        let Some(sig) = signature else {
107            return if self.require_signatures {
108                VerifyResult::Rejected("module is unsigned and signatures are required".into())
109            } else {
110                VerifyResult::Unsigned
111            };
112        };
113
114        if !sig.verify(manifest_hash) {
115            return VerifyResult::Rejected("invalid signature".into());
116        }
117
118        if !self.is_trusted(&sig.author_key, module_name, manifest_hash) {
119            return VerifyResult::Rejected(format!(
120                "author key {} is not trusted for module '{}'",
121                hex::encode(sig.author_key),
122                module_name,
123            ));
124        }
125
126        VerifyResult::Trusted
127    }
128
129    /// Whether this keychain requires all modules to be signed.
130    pub fn requires_signatures(&self) -> bool {
131        self.require_signatures
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::crypto::signing::generate_keypair;
139
140    fn make_author(name: &str, key: [u8; 32], trust: TrustLevel) -> TrustedAuthor {
141        TrustedAuthor {
142            name: name.to_string(),
143            public_key: key,
144            trust_level: trust,
145        }
146    }
147
148    #[test]
149    fn test_unsigned_allowed_when_not_required() {
150        let kc = Keychain::new(false);
151        let result = kc.verify_module("my_mod", &[0u8; 32], None);
152        assert_eq!(result, VerifyResult::Unsigned);
153    }
154
155    #[test]
156    fn test_unsigned_rejected_when_required() {
157        let kc = Keychain::new(true);
158        let result = kc.verify_module("my_mod", &[0u8; 32], None);
159        assert!(matches!(result, VerifyResult::Rejected(_)));
160    }
161
162    #[test]
163    fn test_full_trust_verifies() {
164        let (signing_key, verifying_key) = generate_keypair();
165        let mut kc = Keychain::new(true);
166        kc.add_trusted(make_author(
167            "alice",
168            verifying_key.to_bytes(),
169            TrustLevel::Full,
170        ));
171
172        let hash = [1u8; 32];
173        let sig = ModuleSignatureData::sign(&hash, &signing_key);
174        assert_eq!(
175            kc.verify_module("anything", &hash, Some(&sig)),
176            VerifyResult::Trusted
177        );
178    }
179
180    #[test]
181    fn test_scoped_trust_allows_matching_prefix() {
182        let (signing_key, verifying_key) = generate_keypair();
183        let mut kc = Keychain::new(true);
184        kc.add_trusted(make_author(
185            "bob",
186            verifying_key.to_bytes(),
187            TrustLevel::Scoped(vec!["std::".to_string()]),
188        ));
189
190        let hash = [2u8; 32];
191        let sig = ModuleSignatureData::sign(&hash, &signing_key);
192        assert_eq!(
193            kc.verify_module("std::core::math", &hash, Some(&sig)),
194            VerifyResult::Trusted
195        );
196    }
197
198    #[test]
199    fn test_scoped_trust_rejects_non_matching() {
200        let (signing_key, verifying_key) = generate_keypair();
201        let mut kc = Keychain::new(true);
202        kc.add_trusted(make_author(
203            "bob",
204            verifying_key.to_bytes(),
205            TrustLevel::Scoped(vec!["std::".to_string()]),
206        ));
207
208        let hash = [2u8; 32];
209        let sig = ModuleSignatureData::sign(&hash, &signing_key);
210        let result = kc.verify_module("vendor::malware", &hash, Some(&sig));
211        assert!(matches!(result, VerifyResult::Rejected(_)));
212    }
213
214    #[test]
215    fn test_pinned_trust_matching_hash() {
216        let (signing_key, verifying_key) = generate_keypair();
217        let pinned_hash = [5u8; 32];
218        let mut kc = Keychain::new(true);
219        kc.add_trusted(make_author(
220            "carol",
221            verifying_key.to_bytes(),
222            TrustLevel::Pinned(pinned_hash),
223        ));
224
225        let sig = ModuleSignatureData::sign(&pinned_hash, &signing_key);
226        assert_eq!(
227            kc.verify_module("some_mod", &pinned_hash, Some(&sig)),
228            VerifyResult::Trusted
229        );
230    }
231
232    #[test]
233    fn test_pinned_trust_wrong_hash() {
234        let (signing_key, verifying_key) = generate_keypair();
235        let pinned_hash = [5u8; 32];
236        let mut kc = Keychain::new(true);
237        kc.add_trusted(make_author(
238            "carol",
239            verifying_key.to_bytes(),
240            TrustLevel::Pinned(pinned_hash),
241        ));
242
243        let different_hash = [6u8; 32];
244        let sig = ModuleSignatureData::sign(&different_hash, &signing_key);
245        let result = kc.verify_module("some_mod", &different_hash, Some(&sig));
246        assert!(matches!(result, VerifyResult::Rejected(_)));
247    }
248
249    #[test]
250    fn test_untrusted_key_rejected() {
251        let (signing_key, _) = generate_keypair();
252        let kc = Keychain::new(true);
253
254        let hash = [3u8; 32];
255        let sig = ModuleSignatureData::sign(&hash, &signing_key);
256        let result = kc.verify_module("my_mod", &hash, Some(&sig));
257        assert!(matches!(result, VerifyResult::Rejected(_)));
258    }
259
260    #[test]
261    fn test_invalid_signature_rejected() {
262        let (signing_key, verifying_key) = generate_keypair();
263        let mut kc = Keychain::new(true);
264        kc.add_trusted(make_author(
265            "dave",
266            verifying_key.to_bytes(),
267            TrustLevel::Full,
268        ));
269
270        let hash = [4u8; 32];
271        let mut sig = ModuleSignatureData::sign(&hash, &signing_key);
272        sig.signature[0] ^= 0xFF; // corrupt
273        let result = kc.verify_module("mod", &hash, Some(&sig));
274        assert!(matches!(result, VerifyResult::Rejected(_)));
275    }
276
277    #[test]
278    fn test_remove_trusted() {
279        let (_, verifying_key) = generate_keypair();
280        let mut kc = Keychain::new(false);
281        let key_bytes = verifying_key.to_bytes();
282        kc.add_trusted(make_author("eve", key_bytes, TrustLevel::Full));
283        assert!(kc.is_trusted(&key_bytes, "any", &[0u8; 32]));
284
285        let removed = kc.remove_trusted(&key_bytes);
286        assert!(removed.is_some());
287        assert!(!kc.is_trusted(&key_bytes, "any", &[0u8; 32]));
288    }
289}