Skip to main content

treetop_bundle/
signing.rs

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/// Signature requirements applied while opening an archive.
20#[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/// Detached signature metadata stored in `signature.json`.
43#[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
92/// An Ed25519 private signing key loaded from PKCS#8 PEM.
93pub struct SigningKey(ed25519_dalek::SigningKey);
94
95impl SigningKey {
96    /// Load an unencrypted private key and, on Unix, reject
97    /// group/other-accessible files.
98    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    /// Decode an unencrypted PKCS#8 PEM private key.
105    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    /// Load a password-encrypted private key and, on Unix, reject
116    /// group/other-accessible files. Requires the default `encrypted-keys`
117    /// feature.
118    #[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    /// Load either an encrypted or unencrypted private key, using the password
129    /// when the PEM is encrypted. Requires the default `encrypted-keys`
130    /// feature.
131    #[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    /// Decode a password-encrypted PKCS#8 PEM private key. Requires the
142    /// default `encrypted-keys` feature.
143    #[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    /// Decode either an encrypted or unencrypted PKCS#8 PEM private key,
156    /// using the password only when the PEM is encrypted. The PEM document is
157    /// decoded once before selecting the DER decoder. Requires the default
158    /// `encrypted-keys` feature.
159    #[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/// A trusted Ed25519 public key loaded from SPKI PEM.
210#[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/// Trusted public keys indexed by their content-derived key IDs.
257#[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}