mur_common/skill/
publisher_trust.rs1use anyhow::Context as _;
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10
11pub const MUR_OFFICIAL_PUBLISHER_KEY_FP: &str = "ed25519-861d2acb";
15
16pub const MUR_OFFICIAL_LICENSE_KEY_FP: &str = "ed25519-71594984";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum PublisherTrust {
25 Trusted,
26 Revoked,
27 Unknown,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TrustedPublisher {
33 pub name: String,
34 pub key_fp: String,
35 #[serde(default)]
36 pub comment: String,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct PublisherKeyring {
42 pub schema_version: u32,
43 #[serde(default)]
44 pub publishers: Vec<TrustedPublisher>,
45 #[serde(default)]
46 pub revoked: Vec<String>,
47}
48
49impl PublisherKeyring {
50 pub fn path(mur_home: &Path) -> PathBuf {
52 mur_home.join("trust").join("publishers.yaml")
53 }
54
55 pub fn classify(&self, key_fp: &str) -> PublisherTrust {
60 if self.revoked.iter().any(|r| r == key_fp) {
61 return PublisherTrust::Revoked;
62 }
63 if self.publishers.iter().any(|p| p.key_fp == key_fp) {
64 return PublisherTrust::Trusted;
65 }
66 PublisherTrust::Unknown
67 }
68
69 fn seed() -> Self {
71 PublisherKeyring {
72 schema_version: 1,
73 publishers: vec![TrustedPublisher {
74 name: "mur".to_string(),
75 key_fp: MUR_OFFICIAL_PUBLISHER_KEY_FP.to_string(),
76 comment: "MUR official publisher (pinned trust root)".to_string(),
77 }],
78 revoked: Vec::new(),
79 }
80 }
81
82 pub fn load_or_seed(mur_home: &Path) -> anyhow::Result<Self> {
84 let p = Self::path(mur_home);
85 if p.exists() {
86 let text =
87 std::fs::read_to_string(&p).with_context(|| format!("read {}", p.display()))?;
88 serde_yaml_ng::from_str(&text)
89 .map_err(|e| anyhow::anyhow!("parse publisher keyring: {e}"))
90 } else {
91 let kr = Self::seed();
92 kr.save(mur_home)?;
93 Ok(kr)
94 }
95 }
96
97 pub fn save(&self, mur_home: &Path) -> anyhow::Result<()> {
99 let p = Self::path(mur_home);
100 if let Some(parent) = p.parent() {
101 std::fs::create_dir_all(parent)
102 .with_context(|| format!("create dir {}", parent.display()))?;
103 }
104 let yaml = serde_yaml_ng::to_string(self)
105 .map_err(|e| anyhow::anyhow!("serialize publisher keyring: {e}"))?;
106 let tmp_path = p.with_extension("yaml.tmp");
108 std::fs::write(&tmp_path, yaml.as_bytes())
109 .with_context(|| format!("write {}", tmp_path.display()))?;
110 std::fs::rename(&tmp_path, &p)
111 .with_context(|| format!("rename {} -> {}", tmp_path.display(), p.display()))?;
112 Ok(())
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn kr() -> PublisherKeyring {
121 PublisherKeyring {
122 schema_version: 1,
123 publishers: vec![TrustedPublisher {
124 name: "mur".into(),
125 key_fp: "ed25519-aabbccdd".into(),
126 comment: "official".into(),
127 }],
128 revoked: vec!["ed25519-deadbeef".into()],
129 }
130 }
131
132 #[test]
133 fn classify_trusted_revoked_unknown() {
134 let k = kr();
135 assert_eq!(k.classify("ed25519-aabbccdd"), PublisherTrust::Trusted);
136 assert_eq!(k.classify("ed25519-deadbeef"), PublisherTrust::Revoked);
137 assert_eq!(k.classify("ed25519-00000000"), PublisherTrust::Unknown);
138 }
139
140 #[test]
141 fn revoked_beats_trusted() {
142 let k = PublisherKeyring {
144 schema_version: 1,
145 publishers: vec![TrustedPublisher {
146 name: "mur".into(),
147 key_fp: "ed25519-aabbccdd".into(),
148 comment: String::new(),
149 }],
150 revoked: vec!["ed25519-aabbccdd".into()],
151 };
152 assert_eq!(k.classify("ed25519-aabbccdd"), PublisherTrust::Revoked);
153 }
154
155 #[test]
156 fn load_or_seed_creates_file_with_official_key() {
157 let tmp = tempfile::tempdir().expect("tempdir");
158 let kr = PublisherKeyring::load_or_seed(tmp.path()).expect("load_or_seed");
159 assert_eq!(
160 kr.classify(MUR_OFFICIAL_PUBLISHER_KEY_FP),
161 PublisherTrust::Trusted
162 );
163 assert!(PublisherKeyring::path(tmp.path()).exists());
165 let kr2 = PublisherKeyring::load_or_seed(tmp.path()).expect("reload");
167 assert_eq!(
168 kr2.classify(MUR_OFFICIAL_PUBLISHER_KEY_FP),
169 PublisherTrust::Trusted
170 );
171 }
172}