1use crate::{BundleError, FORMAT_VERSION, Result};
2use base64::{Engine as _, engine::general_purpose::STANDARD};
3use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, SecretDocument};
4use ed25519_dalek::{Signature, Signer, VerifyingKey};
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use std::collections::BTreeMap;
8use std::fs::{self, File};
9use std::io::Read;
10use std::path::Path;
11use std::str::FromStr;
12use zeroize::Zeroizing;
13
14const SIGNATURE_DOMAIN: &[u8] = b"treetop-bundle-signature-v1\0";
15const PRIVATE_KEY_LABEL: &str = "PRIVATE KEY";
16const ENCRYPTED_PRIVATE_KEY_LABEL: &str = "ENCRYPTED PRIVATE KEY";
17const MAX_PRIVATE_KEY_BYTES: usize = 1024 * 1024;
18
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22pub enum SignaturePolicy {
23 #[default]
24 AllowUnsigned,
25 Required,
26}
27
28impl FromStr for SignaturePolicy {
29 type Err = BundleError;
30
31 fn from_str(value: &str) -> Result<Self> {
32 match value {
33 "allow-unsigned" => Ok(Self::AllowUnsigned),
34 "required" => Ok(Self::Required),
35 _ => Err(BundleError::Key(format!(
36 "unknown signature policy {value:?}; expected allow-unsigned or required"
37 ))),
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct BundleSignature {
46 format_version: u32,
47 algorithm: String,
48 key_id: String,
49 signature: String,
50}
51
52impl BundleSignature {
53 pub fn format_version(&self) -> u32 {
54 self.format_version
55 }
56
57 pub fn algorithm(&self) -> &str {
58 &self.algorithm
59 }
60
61 pub fn key_id(&self) -> &str {
62 &self.key_id
63 }
64
65 pub fn signature(&self) -> &str {
66 &self.signature
67 }
68
69 pub(crate) fn validate_format(&self) -> Result<()> {
70 if self.format_version != FORMAT_VERSION {
71 return Err(BundleError::Archive(format!(
72 "unsupported signature format version {}",
73 self.format_version
74 )));
75 }
76 if self.algorithm != "ed25519" {
77 return Err(BundleError::Archive(format!(
78 "unsupported signature algorithm {:?}",
79 self.algorithm
80 )));
81 }
82 let decoded = STANDARD.decode(&self.signature).map_err(|error| {
83 BundleError::Archive(format!("signature is not standard base64: {error}"))
84 })?;
85 Signature::from_slice(&decoded).map_err(|error| {
86 BundleError::Archive(format!("malformed Ed25519 signature: {error}"))
87 })?;
88 Ok(())
89 }
90}
91
92pub struct SigningKey(ed25519_dalek::SigningKey);
94
95impl SigningKey {
96 pub fn from_pkcs8_pem_file(path: impl AsRef<Path>) -> Result<Self> {
99 let path = path.as_ref();
100 let pem = read_private_key(path)?;
101 Self::from_pkcs8_pem(&pem)
102 }
103
104 pub fn from_pkcs8_pem(pem: &str) -> Result<Self> {
106 match parse_private_key_pem(pem)? {
107 PrivateKeyDocument::Unencrypted(document) => Self::from_pkcs8_document(&document),
108 PrivateKeyDocument::Encrypted(document) => {
109 drop(document);
110 Err(BundleError::SigningKeyPasswordRequired)
111 }
112 }
113 }
114
115 #[cfg(feature = "encrypted-keys")]
119 pub fn from_pkcs8_encrypted_pem_file(
120 path: impl AsRef<Path>,
121 password: impl AsRef<[u8]>,
122 ) -> Result<Self> {
123 let path = path.as_ref();
124 let pem = read_private_key(path)?;
125 Self::from_pkcs8_encrypted_pem(&pem, password)
126 }
127
128 #[cfg(feature = "encrypted-keys")]
132 pub fn from_pkcs8_pem_file_with_password(
133 path: impl AsRef<Path>,
134 password: impl AsRef<[u8]>,
135 ) -> Result<Self> {
136 let path = path.as_ref();
137 let pem = read_private_key(path)?;
138 Self::from_pkcs8_pem_with_password(&pem, password)
139 }
140
141 #[cfg(feature = "encrypted-keys")]
144 pub fn from_pkcs8_encrypted_pem(pem: &str, password: impl AsRef<[u8]>) -> Result<Self> {
145 match parse_private_key_pem(pem)? {
146 PrivateKeyDocument::Encrypted(document) => {
147 Self::from_pkcs8_encrypted_document(&document, password)
148 }
149 PrivateKeyDocument::Unencrypted(_) => Err(BundleError::Key(format!(
150 "invalid encrypted PKCS#8 Ed25519 private key: expected {ENCRYPTED_PRIVATE_KEY_LABEL:?} PEM label"
151 ))),
152 }
153 }
154
155 #[cfg(feature = "encrypted-keys")]
160 pub fn from_pkcs8_pem_with_password(pem: &str, password: impl AsRef<[u8]>) -> Result<Self> {
161 match parse_private_key_pem(pem)? {
162 PrivateKeyDocument::Encrypted(document) => {
163 Self::from_pkcs8_encrypted_document(&document, password)
164 }
165 PrivateKeyDocument::Unencrypted(document) => Self::from_pkcs8_document(&document),
166 }
167 }
168
169 fn from_pkcs8_document(document: &SecretDocument) -> Result<Self> {
170 ed25519_dalek::SigningKey::from_pkcs8_der(document.as_bytes())
171 .map(Self)
172 .map_err(|error| {
173 BundleError::Key(format!("invalid PKCS#8 Ed25519 private key: {error}"))
174 })
175 }
176
177 #[cfg(feature = "encrypted-keys")]
178 fn from_pkcs8_encrypted_document(
179 document: &SecretDocument,
180 password: impl AsRef<[u8]>,
181 ) -> Result<Self> {
182 ed25519_dalek::SigningKey::from_pkcs8_encrypted_der(document.as_bytes(), password)
183 .map(Self)
184 .map_err(|error| {
185 BundleError::Key(format!(
186 "invalid encrypted PKCS#8 Ed25519 private key: {error}"
187 ))
188 })
189 }
190
191 pub fn key_id(&self) -> String {
192 key_id(&self.0.verifying_key())
193 }
194
195 pub(crate) fn sign_manifest(&self, manifest: &[u8]) -> BundleSignature {
196 let mut message = Vec::with_capacity(SIGNATURE_DOMAIN.len() + manifest.len());
197 message.extend_from_slice(SIGNATURE_DOMAIN);
198 message.extend_from_slice(manifest);
199 let signature = self.0.sign(&message);
200 BundleSignature {
201 format_version: FORMAT_VERSION,
202 algorithm: "ed25519".to_string(),
203 key_id: self.key_id(),
204 signature: STANDARD.encode(signature.to_bytes()),
205 }
206 }
207}
208
209#[derive(Debug, Clone)]
211pub struct TrustedKey {
212 key_id: String,
213 key: VerifyingKey,
214}
215
216impl TrustedKey {
217 pub fn from_spki_pem_file(path: impl AsRef<Path>) -> Result<Self> {
218 let path = path.as_ref();
219 let pem = fs::read_to_string(path).map_err(|error| BundleError::io(path, error))?;
220 Self::from_spki_pem(&pem)
221 }
222
223 pub fn from_spki_pem(pem: &str) -> Result<Self> {
224 let key = VerifyingKey::from_public_key_pem(pem).map_err(|error| {
225 BundleError::Key(format!("invalid SPKI Ed25519 public key: {error}"))
226 })?;
227 if key.is_weak() {
228 return Err(BundleError::Key(
229 "weak Ed25519 public keys are not accepted".to_string(),
230 ));
231 }
232 Ok(Self {
233 key_id: key_id(&key),
234 key,
235 })
236 }
237
238 pub fn key_id(&self) -> &str {
239 &self.key_id
240 }
241
242 fn raw_key(&self) -> [u8; 32] {
243 self.key.to_bytes()
244 }
245
246 fn verify(&self, manifest: &[u8], signature: &Signature) -> Result<()> {
247 let mut message = Vec::with_capacity(SIGNATURE_DOMAIN.len() + manifest.len());
248 message.extend_from_slice(SIGNATURE_DOMAIN);
249 message.extend_from_slice(manifest);
250 self.key
251 .verify_strict(&message, signature)
252 .map_err(|_| BundleError::Archive("invalid_signature".to_string()))
253 }
254}
255
256#[derive(Debug, Clone, Default)]
258pub struct TrustStore(BTreeMap<String, TrustedKey>);
259
260impl TrustStore {
261 pub fn new() -> Self {
262 Self::default()
263 }
264
265 pub fn from_keys(keys: impl IntoIterator<Item = TrustedKey>) -> Result<Self> {
266 let mut store = Self::new();
267 for key in keys {
268 store.insert(key)?;
269 }
270 Ok(store)
271 }
272
273 pub fn insert(&mut self, key: TrustedKey) -> Result<()> {
274 if let Some(existing) = self.0.get(key.key_id()) {
275 if existing.raw_key() != key.raw_key() {
276 return Err(BundleError::Key(format!(
277 "duplicate key ID {} has different public key bytes",
278 key.key_id()
279 )));
280 }
281 return Ok(());
282 }
283 self.0.insert(key.key_id.clone(), key);
284 Ok(())
285 }
286
287 pub fn is_empty(&self) -> bool {
288 self.0.is_empty()
289 }
290
291 pub fn len(&self) -> usize {
292 self.0.len()
293 }
294
295 pub(crate) fn verify(&self, manifest: &[u8], signature: &BundleSignature) -> Result<String> {
296 signature.validate_format()?;
297 let key = self
298 .0
299 .get(signature.key_id())
300 .ok_or_else(|| BundleError::Archive("untrusted_key".to_string()))?;
301 let bytes = STANDARD.decode(signature.signature()).map_err(|error| {
302 BundleError::Archive(format!("signature is not standard base64: {error}"))
303 })?;
304 let signature = Signature::from_slice(&bytes).map_err(|error| {
305 BundleError::Archive(format!("malformed Ed25519 signature: {error}"))
306 })?;
307 key.verify(manifest, &signature)?;
308 Ok(key.key_id.clone())
309 }
310}
311
312fn key_id(key: &VerifyingKey) -> String {
313 let digest = Sha256::digest(key.to_bytes());
314 digest.iter().map(|byte| format!("{byte:02x}")).collect()
315}
316
317enum PrivateKeyDocument {
318 Unencrypted(SecretDocument),
319 Encrypted(SecretDocument),
320}
321
322fn parse_private_key_pem(pem: &str) -> Result<PrivateKeyDocument> {
323 let (label, document) = SecretDocument::from_pem(pem)
324 .map_err(|error| BundleError::Key(format!("invalid PKCS#8 private key PEM: {error}")))?;
325 match label {
326 ENCRYPTED_PRIVATE_KEY_LABEL => Ok(PrivateKeyDocument::Encrypted(document)),
327 PRIVATE_KEY_LABEL => Ok(PrivateKeyDocument::Unencrypted(document)),
328 _ => Err(BundleError::Key(format!(
329 "invalid PKCS#8 private key PEM label {label:?}"
330 ))),
331 }
332}
333
334fn read_private_key(path: &Path) -> Result<Zeroizing<String>> {
335 let file = File::open(path).map_err(|error| BundleError::io(path, error))?;
336 let metadata = file
337 .metadata()
338 .map_err(|error| BundleError::io(path, error))?;
339 if !metadata.is_file() {
340 return Err(BundleError::Key(format!(
341 "private key path {} is not a regular file",
342 path.display()
343 )));
344 }
345 #[cfg(unix)]
346 {
347 use std::os::unix::fs::PermissionsExt;
348 if metadata.permissions().mode() & 0o077 != 0 {
349 return Err(BundleError::Key(format!(
350 "private key file {} is accessible by group or others",
351 path.display()
352 )));
353 }
354 }
355 if metadata.len() > MAX_PRIVATE_KEY_BYTES as u64 {
356 return Err(BundleError::Key(format!(
357 "private key file {} exceeds {MAX_PRIVATE_KEY_BYTES} bytes",
358 path.display()
359 )));
360 }
361 let mut pem = Zeroizing::new(String::new());
362 file.take((MAX_PRIVATE_KEY_BYTES as u64) + 1)
363 .read_to_string(&mut pem)
364 .map_err(|error| BundleError::io(path, error))?;
365 if pem.len() > MAX_PRIVATE_KEY_BYTES {
366 return Err(BundleError::Key(format!(
367 "private key file {} exceeds {MAX_PRIVATE_KEY_BYTES} bytes",
368 path.display()
369 )));
370 }
371 Ok(pem)
372}