Skip to main content

mockforge_plugin_loader/signature/
trust_store.rs

1//! Publisher key trust, revocation, expiry, and configured store resolution.
2use super::{DEFAULT_TRUST_STORE_PATH, TRUST_STORE_ENV};
3use crate::{LoaderResult, PluginLoaderConfig, PluginLoaderError};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::{
7    fs,
8    path::{Path, PathBuf},
9};
10
11/// One entry in a [`TrustStoreFile`].
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TrustedKeyEntry {
14    /// Key ID referenced by `plugin.sig`
15    pub key_id: String,
16    /// Hex-encoded public key bytes (32 bytes for Ed25519)
17    pub public_key: String,
18    /// Signatures from this key are rejected at or after this instant
19    #[serde(default)]
20    pub expires_at: Option<DateTime<Utc>>,
21    /// Revoked keys are rejected even if also listed as trusted
22    #[serde(default)]
23    pub revoked: bool,
24}
25
26/// On-disk trust store for plugin signing keys.
27///
28/// ```json
29/// { "keys": [ { "key_id": "acme-2026", "public_key": "<hex>",
30///               "expires_at": "2027-01-01T00:00:00Z", "revoked": false } ] }
31/// ```
32#[derive(Debug, Clone, Default, Serialize, Deserialize)]
33pub struct TrustStoreFile {
34    /// Keys in the store
35    #[serde(default)]
36    pub keys: Vec<TrustedKeyEntry>,
37}
38
39impl TrustStoreFile {
40    /// Read and parse a trust store file.
41    pub fn load(path: &Path) -> LoaderResult<Self> {
42        let raw = fs::read_to_string(path).map_err(|e| {
43            PluginLoaderError::security(format!(
44                "Failed to read trust store {}: {}",
45                path.display(),
46                e
47            ))
48        })?;
49        serde_json::from_str(&raw).map_err(|e| {
50            PluginLoaderError::security(format!(
51                "Failed to parse trust store {}: {}",
52                path.display(),
53                e
54            ))
55        })
56    }
57
58    /// Add this store's keys to a loader configuration. Returns the
59    /// number of keys now trusted from this file.
60    pub fn apply_to(&self, config: &mut PluginLoaderConfig) -> LoaderResult<usize> {
61        let mut trusted = 0;
62        for entry in &self.keys {
63            if entry.revoked {
64                if !config.revoked_keys.contains(&entry.key_id) {
65                    config.revoked_keys.push(entry.key_id.clone());
66                }
67                continue;
68            }
69            let key = hex::decode(entry.public_key.trim()).map_err(|e| {
70                PluginLoaderError::security(format!(
71                    "Invalid public key hex for key '{}': {}",
72                    entry.key_id, e
73                ))
74            })?;
75            if !config.trusted_keys.contains(&entry.key_id) {
76                config.trusted_keys.push(entry.key_id.clone());
77            }
78            config.key_data.insert(entry.key_id.clone(), key);
79            if let Some(expires_at) = entry.expires_at {
80                config.key_expires_at.insert(entry.key_id.clone(), expires_at);
81            }
82            trusted += 1;
83        }
84        Ok(trusted)
85    }
86}
87
88/// Resolve the trust store path: an explicit path, else
89/// `MOCKFORGE_PLUGIN_TRUSTED_KEYS_FILE`, else
90/// `~/.mockforge/trusted-keys.json` when it exists.
91pub fn resolve_trust_store_path(explicit: Option<&Path>) -> Option<PathBuf> {
92    if let Some(path) = explicit {
93        return Some(path.to_path_buf());
94    }
95    if let Ok(path) = std::env::var(TRUST_STORE_ENV) {
96        if !path.trim().is_empty() {
97            return Some(PathBuf::from(shellexpand::tilde(path.trim()).as_ref()));
98        }
99    }
100    let default = PathBuf::from(shellexpand::tilde(DEFAULT_TRUST_STORE_PATH).as_ref());
101    default.exists().then_some(default)
102}