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