verify_trust/
pgp_exempt.rs1use anyhow::{Context, Result, bail};
20use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey};
21use pgp::types::KeyDetails;
22use std::path::Path;
23
24pub struct ExemptKeyring {
27 keys: Vec<SignedPublicKey>,
28}
29
30impl ExemptKeyring {
31 pub fn from_armored(text: &str) -> Result<Self> {
35 const BLOCK_START: &str = "-----BEGIN PGP PUBLIC KEY BLOCK-----";
36 let mut keys = Vec::new();
37 for block in text.split(BLOCK_START).skip(1) {
38 let block = format!("{BLOCK_START}{block}");
39 let (parsed, _headers) = SignedPublicKey::from_string_many(&block)
40 .context("exempt keyring did not parse")?;
41 keys.extend(
42 parsed
43 .collect::<std::result::Result<Vec<_>, _>>()
44 .context("exempt keyring contains an invalid key")?,
45 );
46 }
47 if keys.is_empty() {
48 bail!("exempt keyring contains no keys");
49 }
50 Ok(Self { keys })
51 }
52
53 pub fn load(path: &Path) -> Result<Self> {
55 let text = std::fs::read_to_string(path)
56 .with_context(|| format!("cannot read exempt keyring {}", path.display()))?;
57 Self::from_armored(&text)
58 }
59
60 pub fn verify(&self, armored_signature: &str, payload: &[u8]) -> Result<String, String> {
64 let (signature, _headers) = DetachedSignature::from_string(armored_signature)
65 .map_err(|e| format!("PGP signature did not parse: {e}"))?;
66
67 for key in &self.keys {
68 let fingerprint = hex::encode(key.primary_key.fingerprint().as_bytes());
69 if signature.verify(key, payload).is_ok() {
70 return Ok(fingerprint);
71 }
72 for subkey in &key.public_subkeys {
73 if signature.verify(subkey, payload).is_ok() {
74 return Ok(fingerprint);
75 }
76 }
77 }
78 Err("signature matches no key in the exempt keyring".to_string())
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 #![allow(clippy::unwrap_used, clippy::expect_used)]
85
86 use super::*;
87 use pgp::composed::{ArmorOptions, KeyType, SecretKeyParamsBuilder, SignedSecretKey};
88 use pgp::crypto::hash::HashAlgorithm;
89 use pgp::types::Password;
90 use rand::SeedableRng;
91 use rand::rngs::StdRng;
92
93 fn test_key(seed: u64, user: &str) -> SignedSecretKey {
94 let mut rng = StdRng::seed_from_u64(seed);
95 SecretKeyParamsBuilder::default()
96 .key_type(KeyType::Ed25519)
97 .can_sign(true)
98 .primary_user_id(user.to_string())
99 .build()
100 .unwrap()
101 .generate(&mut rng)
102 .unwrap()
103 }
104
105 fn armored_public(key: &SignedSecretKey) -> String {
106 SignedPublicKey::from(key.clone())
107 .to_armored_string(ArmorOptions::default())
108 .unwrap()
109 }
110
111 fn sign(key: &SignedSecretKey, payload: &[u8]) -> String {
112 let mut rng = StdRng::seed_from_u64(42);
113 DetachedSignature::sign_binary_data(
114 &mut rng,
115 &key.primary_key,
116 &Password::empty(),
117 HashAlgorithm::Sha256,
118 payload,
119 )
120 .unwrap()
121 .to_armored_string(ArmorOptions::default())
122 .unwrap()
123 }
124
125 #[test]
126 fn signature_by_a_keyring_key_is_exempt() {
127 let key = test_key(1, "platform@example.com");
128 let keyring = ExemptKeyring::from_armored(&armored_public(&key)).unwrap();
129 let payload = b"tree abc\n\nmerge commit";
130
131 let fingerprint = keyring.verify(&sign(&key, payload), payload).unwrap();
132 assert_eq!(
133 fingerprint,
134 hex::encode(key.primary_key.fingerprint().as_bytes())
135 );
136 }
137
138 #[test]
139 fn signature_by_an_unknown_key_is_rejected() {
140 let trusted = test_key(1, "platform@example.com");
141 let attacker = test_key(2, "attacker@example.com");
142 let keyring = ExemptKeyring::from_armored(&armored_public(&trusted)).unwrap();
143 let payload = b"tree abc\n\nmerge commit";
144
145 assert!(keyring.verify(&sign(&attacker, payload), payload).is_err());
146 }
147
148 #[test]
149 fn tampered_payload_is_rejected() {
150 let key = test_key(1, "platform@example.com");
151 let keyring = ExemptKeyring::from_armored(&armored_public(&key)).unwrap();
152 let signature = sign(&key, b"original payload");
153
154 assert!(keyring.verify(&signature, b"tampered payload").is_err());
155 }
156
157 #[test]
158 fn multi_key_keyring_matches_any_member() {
159 let a = test_key(1, "a@example.com");
160 let b = test_key(2, "b@example.com");
161 let both = format!("{}{}", armored_public(&a), armored_public(&b));
162 let keyring = ExemptKeyring::from_armored(&both).unwrap();
163 let payload = b"payload";
164
165 assert!(keyring.verify(&sign(&b, payload), payload).is_ok());
166 }
167
168 #[test]
169 fn empty_or_garbage_keyring_is_a_hard_error() {
170 assert!(ExemptKeyring::from_armored("").is_err());
171 assert!(ExemptKeyring::from_armored("not a keyring").is_err());
172 }
173}