treetop_bundle/
signing.rs1use crate::{BundleError, FORMAT_VERSION, Result};
2use base64::{Engine as _, engine::general_purpose::STANDARD};
3use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey};
4use ed25519_dalek::{Signature, Signer, Verifier, VerifyingKey};
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use std::collections::BTreeMap;
8use std::fs;
9use std::path::Path;
10use std::str::FromStr;
11
12const SIGNATURE_DOMAIN: &[u8] = b"treetop-bundle-signature-v1\0";
13
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "kebab-case")]
17pub enum SignaturePolicy {
18 #[default]
19 AllowUnsigned,
20 Required,
21}
22
23impl FromStr for SignaturePolicy {
24 type Err = BundleError;
25
26 fn from_str(value: &str) -> Result<Self> {
27 match value {
28 "allow-unsigned" => Ok(Self::AllowUnsigned),
29 "required" => Ok(Self::Required),
30 _ => Err(BundleError::Key(format!(
31 "unknown signature policy {value:?}; expected allow-unsigned or required"
32 ))),
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct BundleSignature {
41 format_version: u32,
42 algorithm: String,
43 key_id: String,
44 signature: String,
45}
46
47impl BundleSignature {
48 pub fn format_version(&self) -> u32 {
49 self.format_version
50 }
51
52 pub fn algorithm(&self) -> &str {
53 &self.algorithm
54 }
55
56 pub fn key_id(&self) -> &str {
57 &self.key_id
58 }
59
60 pub fn signature(&self) -> &str {
61 &self.signature
62 }
63
64 pub(crate) fn validate_format(&self) -> Result<()> {
65 if self.format_version != FORMAT_VERSION {
66 return Err(BundleError::Archive(format!(
67 "unsupported signature format version {}",
68 self.format_version
69 )));
70 }
71 if self.algorithm != "ed25519" {
72 return Err(BundleError::Archive(format!(
73 "unsupported signature algorithm {:?}",
74 self.algorithm
75 )));
76 }
77 let decoded = STANDARD.decode(&self.signature).map_err(|error| {
78 BundleError::Archive(format!("signature is not standard base64: {error}"))
79 })?;
80 Signature::from_slice(&decoded).map_err(|error| {
81 BundleError::Archive(format!("malformed Ed25519 signature: {error}"))
82 })?;
83 Ok(())
84 }
85}
86
87pub struct SigningKey(ed25519_dalek::SigningKey);
89
90impl SigningKey {
91 pub fn from_pkcs8_pem_file(path: impl AsRef<Path>) -> Result<Self> {
93 let path = path.as_ref();
94 let metadata = fs::metadata(path).map_err(|error| BundleError::io(path, error))?;
95 #[cfg(unix)]
96 {
97 use std::os::unix::fs::PermissionsExt;
98 if metadata.permissions().mode() & 0o077 != 0 {
99 return Err(BundleError::Key(format!(
100 "private key file {} is accessible by group or others",
101 path.display()
102 )));
103 }
104 }
105 let pem = fs::read_to_string(path).map_err(|error| BundleError::io(path, error))?;
106 Self::from_pkcs8_pem(&pem)
107 }
108
109 pub fn from_pkcs8_pem(pem: &str) -> Result<Self> {
111 ed25519_dalek::SigningKey::from_pkcs8_pem(pem)
112 .map(Self)
113 .map_err(|error| {
114 BundleError::Key(format!("invalid PKCS#8 Ed25519 private key: {error}"))
115 })
116 }
117
118 pub fn key_id(&self) -> String {
119 key_id(&self.0.verifying_key())
120 }
121
122 pub(crate) fn sign_manifest(&self, manifest: &[u8]) -> BundleSignature {
123 let mut message = Vec::with_capacity(SIGNATURE_DOMAIN.len() + manifest.len());
124 message.extend_from_slice(SIGNATURE_DOMAIN);
125 message.extend_from_slice(manifest);
126 let signature = self.0.sign(&message);
127 BundleSignature {
128 format_version: FORMAT_VERSION,
129 algorithm: "ed25519".to_string(),
130 key_id: self.key_id(),
131 signature: STANDARD.encode(signature.to_bytes()),
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct TrustedKey {
139 key_id: String,
140 key: VerifyingKey,
141}
142
143impl TrustedKey {
144 pub fn from_spki_pem_file(path: impl AsRef<Path>) -> Result<Self> {
145 let path = path.as_ref();
146 let pem = fs::read_to_string(path).map_err(|error| BundleError::io(path, error))?;
147 Self::from_spki_pem(&pem)
148 }
149
150 pub fn from_spki_pem(pem: &str) -> Result<Self> {
151 let key = VerifyingKey::from_public_key_pem(pem).map_err(|error| {
152 BundleError::Key(format!("invalid SPKI Ed25519 public key: {error}"))
153 })?;
154 Ok(Self {
155 key_id: key_id(&key),
156 key,
157 })
158 }
159
160 pub fn key_id(&self) -> &str {
161 &self.key_id
162 }
163
164 fn raw_key(&self) -> [u8; 32] {
165 self.key.to_bytes()
166 }
167
168 fn verify(&self, manifest: &[u8], signature: &Signature) -> Result<()> {
169 let mut message = Vec::with_capacity(SIGNATURE_DOMAIN.len() + manifest.len());
170 message.extend_from_slice(SIGNATURE_DOMAIN);
171 message.extend_from_slice(manifest);
172 self.key
173 .verify(&message, signature)
174 .map_err(|_| BundleError::Archive("invalid_signature".to_string()))
175 }
176}
177
178#[derive(Debug, Clone, Default)]
180pub struct TrustStore(BTreeMap<String, TrustedKey>);
181
182impl TrustStore {
183 pub fn new() -> Self {
184 Self::default()
185 }
186
187 pub fn from_keys(keys: impl IntoIterator<Item = TrustedKey>) -> Result<Self> {
188 let mut store = Self::new();
189 for key in keys {
190 store.insert(key)?;
191 }
192 Ok(store)
193 }
194
195 pub fn insert(&mut self, key: TrustedKey) -> Result<()> {
196 if let Some(existing) = self.0.get(key.key_id()) {
197 if existing.raw_key() != key.raw_key() {
198 return Err(BundleError::Key(format!(
199 "duplicate key ID {} has different public key bytes",
200 key.key_id()
201 )));
202 }
203 return Ok(());
204 }
205 self.0.insert(key.key_id.clone(), key);
206 Ok(())
207 }
208
209 pub fn is_empty(&self) -> bool {
210 self.0.is_empty()
211 }
212
213 pub fn len(&self) -> usize {
214 self.0.len()
215 }
216
217 pub(crate) fn verify(&self, manifest: &[u8], signature: &BundleSignature) -> Result<String> {
218 signature.validate_format()?;
219 let key = self
220 .0
221 .get(signature.key_id())
222 .ok_or_else(|| BundleError::Archive("untrusted_key".to_string()))?;
223 let bytes = STANDARD.decode(signature.signature()).map_err(|error| {
224 BundleError::Archive(format!("signature is not standard base64: {error}"))
225 })?;
226 let signature = Signature::from_slice(&bytes).map_err(|error| {
227 BundleError::Archive(format!("malformed Ed25519 signature: {error}"))
228 })?;
229 key.verify(manifest, &signature)?;
230 Ok(key.key_id.clone())
231 }
232}
233
234fn key_id(key: &VerifyingKey) -> String {
235 let digest = Sha256::digest(key.to_bytes());
236 digest.iter().map(|byte| format!("{byte:02x}")).collect()
237}