rightkit_agent_engine_api_integrations/
plugin_signature.rs1use serde::{Deserialize, Serialize};
4use std::path::{Path, PathBuf};
5
6#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum SignatureStatus {
9 Signed,
10 Unsigned,
11 Unknown,
12}
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct PluginSignatureRecord {
17 pub status: SignatureStatus,
18 #[serde(default)]
19 pub signed_by: String,
20 #[serde(default)]
21 pub signing_public_key: String,
22 #[serde(default)]
23 pub signature: String,
24 #[serde(default)]
25 pub installed_at: String,
26 #[serde(default)]
27 pub install_source: String,
28}
29
30impl PluginSignatureRecord {
31 pub fn signed(
32 signed_by: String,
33 signing_public_key: String,
34 signature: String,
35 install_source: String,
36 now_iso: String,
37 ) -> Self {
38 Self {
39 status: SignatureStatus::Signed,
40 signed_by,
41 signing_public_key,
42 signature,
43 installed_at: now_iso,
44 install_source,
45 }
46 }
47
48 pub fn unsigned(install_source: String, now_iso: String) -> Self {
49 Self {
50 status: SignatureStatus::Unsigned,
51 signed_by: String::new(),
52 signing_public_key: String::new(),
53 signature: String::new(),
54 installed_at: now_iso,
55 install_source,
56 }
57 }
58}
59
60pub fn sidecar_path(plugin_install_dir: &Path) -> PathBuf {
61 plugin_install_dir.join(".signature.json")
62}
63
64pub fn read_record(plugin_install_dir: &Path) -> PluginSignatureRecord {
65 let path = sidecar_path(plugin_install_dir);
66 match std::fs::read_to_string(&path) {
67 Ok(raw) => serde_json::from_str(&raw).unwrap_or_else(|_| unknown_record()),
68 Err(_) => unknown_record(),
69 }
70}
71
72pub fn write_record(plugin_install_dir: &Path, record: &PluginSignatureRecord) {
73 let path = sidecar_path(plugin_install_dir);
74 match serde_json::to_string_pretty(record) {
75 Ok(json) => {
76 if let Err(error) = std::fs::write(&path, json) {
77 eprintln!(
78 "[plugin-signature] failed to write provenance to {}: {error}",
79 path.display()
80 );
81 }
82 }
83 Err(error) => eprintln!("[plugin-signature] serialise record failed: {error}"),
84 }
85}
86
87pub fn is_acceptable(record: &PluginSignatureRecord, policy_reject_unsigned: bool) -> bool {
88 match record.status {
89 SignatureStatus::Signed | SignatureStatus::Unknown => true,
90 SignatureStatus::Unsigned => !policy_reject_unsigned,
91 }
92}
93
94fn unknown_record() -> PluginSignatureRecord {
95 PluginSignatureRecord {
96 status: SignatureStatus::Unknown,
97 signed_by: String::new(),
98 signing_public_key: String::new(),
99 signature: String::new(),
100 installed_at: String::new(),
101 install_source: String::new(),
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 fn scratch() -> PathBuf {
110 let dir = std::env::temp_dir().join(format!(
111 "rk-plugin-signature-{}-{}",
112 std::process::id(),
113 std::time::SystemTime::now()
114 .duration_since(std::time::UNIX_EPOCH)
115 .unwrap()
116 .as_nanos()
117 ));
118 std::fs::create_dir_all(&dir).unwrap();
119 dir
120 }
121
122 #[test]
123 fn a_written_record_reads_back() {
124 let dir = scratch();
125 let record = PluginSignatureRecord::signed(
126 "Acme".into(),
127 "pk".into(),
128 "sig".into(),
129 "marketplace".into(),
130 "2026-11-02T00:00:00Z".into(),
131 );
132 write_record(&dir, &record);
133 assert_eq!(sidecar_path(&dir), dir.join(".signature.json"));
134 let back = read_record(&dir);
135 assert_eq!(back.status, SignatureStatus::Signed);
136 assert_eq!(back.signed_by, "Acme");
137 assert_eq!(back.signing_public_key, "pk");
138 assert_eq!(back.signature, "sig");
139 assert_eq!(back.install_source, "marketplace");
140 assert_eq!(back.installed_at, "2026-11-02T00:00:00Z");
141 let raw = std::fs::read_to_string(sidecar_path(&dir)).unwrap();
142 assert!(raw.contains("\"status\": \"signed\""), "{raw}");
143 assert!(raw.contains("\"signedBy\": \"Acme\""), "{raw}");
144 let _ = std::fs::remove_dir_all(&dir);
145 }
146
147 #[test]
148 fn a_missing_or_corrupt_sidecar_reads_as_unknown() {
149 let dir = scratch();
150 assert_eq!(read_record(&dir).status, SignatureStatus::Unknown);
151 std::fs::write(sidecar_path(&dir), "{ nope").unwrap();
152 let record = read_record(&dir);
153 assert_eq!(record.status, SignatureStatus::Unknown);
154 assert!(record.signed_by.is_empty());
155 let _ = std::fs::remove_dir_all(&dir);
156 }
157
158 #[test]
159 fn only_unsigned_plugins_are_refused_and_only_under_the_policy() {
160 let unsigned = PluginSignatureRecord::unsigned("local".into(), "t".into());
161 assert_eq!(unsigned.status, SignatureStatus::Unsigned);
162 assert!(unsigned.signed_by.is_empty());
163 assert!(is_acceptable(&unsigned, false));
164 assert!(!is_acceptable(&unsigned, true));
165 let signed = PluginSignatureRecord::signed(
166 "A".into(),
167 "k".into(),
168 "s".into(),
169 "m".into(),
170 "t".into(),
171 );
172 assert!(is_acceptable(&signed, true));
173 assert!(is_acceptable(&unknown_record(), true));
174 }
175}