]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-datastore/src/manifest.rs
7799f906169aaba84d4b12336fe50fa216d7d72f
[proxmox-backup.git] / pbs-datastore / src / manifest.rs
1 use std::convert::TryFrom;
2 use std::path::Path;
3
4 use anyhow::{bail, format_err, Error};
5
6 use serde_json::{json, Value};
7 use serde::{Deserialize, Serialize};
8
9 use crate::{BackupDir, CryptMode, CryptConfig, Fingerprint};
10
11 pub const MANIFEST_BLOB_NAME: &str = "index.json.blob";
12 pub const MANIFEST_LOCK_NAME: &str = ".index.json.lck";
13 pub const CLIENT_LOG_BLOB_NAME: &str = "client.log.blob";
14 pub const ENCRYPTED_KEY_BLOB_NAME: &str = "rsa-encrypted.key.blob";
15
16 mod hex_csum {
17 use serde::{self, Deserialize, Serializer, Deserializer};
18
19 pub fn serialize<S>(
20 csum: &[u8; 32],
21 serializer: S,
22 ) -> Result<S::Ok, S::Error>
23 where
24 S: Serializer,
25 {
26 let s = proxmox::tools::digest_to_hex(csum);
27 serializer.serialize_str(&s)
28 }
29
30 pub fn deserialize<'de, D>(
31 deserializer: D,
32 ) -> Result<[u8; 32], D::Error>
33 where
34 D: Deserializer<'de>,
35 {
36 let s = String::deserialize(deserializer)?;
37 proxmox::tools::hex_to_digest(&s).map_err(serde::de::Error::custom)
38 }
39 }
40
41 fn crypt_mode_none() -> CryptMode { CryptMode::None }
42 fn empty_value() -> Value { json!({}) }
43
44 #[derive(Serialize, Deserialize)]
45 #[serde(rename_all="kebab-case")]
46 pub struct FileInfo {
47 pub filename: String,
48 #[serde(default="crypt_mode_none")] // to be compatible with < 0.8.0 backups
49 pub crypt_mode: CryptMode,
50 pub size: u64,
51 #[serde(with = "hex_csum")]
52 pub csum: [u8; 32],
53 }
54
55 impl FileInfo {
56
57 /// Return expected CryptMode of referenced chunks
58 ///
59 /// Encrypted Indices should only reference encrypted chunks, while signed or plain indices
60 /// should only reference plain chunks.
61 pub fn chunk_crypt_mode (&self) -> CryptMode {
62 match self.crypt_mode {
63 CryptMode::Encrypt => CryptMode::Encrypt,
64 CryptMode::SignOnly | CryptMode::None => CryptMode::None,
65 }
66 }
67 }
68
69 #[derive(Serialize, Deserialize)]
70 #[serde(rename_all="kebab-case")]
71 pub struct BackupManifest {
72 backup_type: String,
73 backup_id: String,
74 backup_time: i64,
75 files: Vec<FileInfo>,
76 #[serde(default="empty_value")] // to be compatible with < 0.8.0 backups
77 pub unprotected: Value,
78 pub signature: Option<String>,
79 }
80
81 #[derive(PartialEq)]
82 pub enum ArchiveType {
83 FixedIndex,
84 DynamicIndex,
85 Blob,
86 }
87
88 impl ArchiveType {
89 pub fn from_path(archive_name: impl AsRef<Path>) -> Result<Self, Error> {
90 let archive_name = archive_name.as_ref();
91 let archive_type = match archive_name.extension().and_then(|ext| ext.to_str()) {
92 Some("didx") => ArchiveType::DynamicIndex,
93 Some("fidx") => ArchiveType::FixedIndex,
94 Some("blob") => ArchiveType::Blob,
95 _ => bail!("unknown archive type: {:?}", archive_name),
96 };
97 Ok(archive_type)
98 }
99 }
100
101 //#[deprecated(note = "use ArchivType::from_path instead")] later...
102 pub fn archive_type<P: AsRef<Path>>(
103 archive_name: P,
104 ) -> Result<ArchiveType, Error> {
105 ArchiveType::from_path(archive_name)
106 }
107
108 impl BackupManifest {
109
110 pub fn new(snapshot: BackupDir) -> Self {
111 Self {
112 backup_type: snapshot.group().backup_type().into(),
113 backup_id: snapshot.group().backup_id().into(),
114 backup_time: snapshot.backup_time(),
115 files: Vec::new(),
116 unprotected: json!({}),
117 signature: None,
118 }
119 }
120
121 pub fn add_file(&mut self, filename: String, size: u64, csum: [u8; 32], crypt_mode: CryptMode) -> Result<(), Error> {
122 let _archive_type = ArchiveType::from_path(&filename)?; // check type
123 self.files.push(FileInfo { filename, size, csum, crypt_mode });
124 Ok(())
125 }
126
127 pub fn files(&self) -> &[FileInfo] {
128 &self.files[..]
129 }
130
131 pub fn lookup_file_info(&self, name: &str) -> Result<&FileInfo, Error> {
132
133 let info = self.files.iter().find(|item| item.filename == name);
134
135 match info {
136 None => bail!("manifest does not contain file '{}'", name),
137 Some(info) => Ok(info),
138 }
139 }
140
141 pub fn verify_file(&self, name: &str, csum: &[u8; 32], size: u64) -> Result<(), Error> {
142
143 let info = self.lookup_file_info(name)?;
144
145 if size != info.size {
146 bail!("wrong size for file '{}' ({} != {})", name, info.size, size);
147 }
148
149 if csum != &info.csum {
150 bail!("wrong checksum for file '{}'", name);
151 }
152
153 Ok(())
154 }
155
156 // Generate canonical json
157 fn to_canonical_json(value: &Value) -> Result<Vec<u8>, Error> {
158 pbs_tools::json::to_canonical_json(value)
159 }
160
161 /// Compute manifest signature
162 ///
163 /// By generating a HMAC SHA256 over the canonical json
164 /// representation, The 'unpreotected' property is excluded.
165 pub fn signature(&self, crypt_config: &CryptConfig) -> Result<[u8; 32], Error> {
166 Self::json_signature(&serde_json::to_value(&self)?, crypt_config)
167 }
168
169 fn json_signature(data: &Value, crypt_config: &CryptConfig) -> Result<[u8; 32], Error> {
170
171 let mut signed_data = data.clone();
172
173 signed_data.as_object_mut().unwrap().remove("unprotected"); // exclude
174 signed_data.as_object_mut().unwrap().remove("signature"); // exclude
175
176 let canonical = Self::to_canonical_json(&signed_data)?;
177
178 let sig = crypt_config.compute_auth_tag(&canonical);
179
180 Ok(sig)
181 }
182
183 /// Converts the Manifest into json string, and add a signature if there is a crypt_config.
184 pub fn to_string(&self, crypt_config: Option<&CryptConfig>) -> Result<String, Error> {
185
186 let mut manifest = serde_json::to_value(&self)?;
187
188 if let Some(crypt_config) = crypt_config {
189 let sig = self.signature(crypt_config)?;
190 manifest["signature"] = proxmox::tools::digest_to_hex(&sig).into();
191 let fingerprint = &crypt_config.fingerprint();
192 manifest["unprotected"]["key-fingerprint"] = serde_json::to_value(fingerprint)?;
193 }
194
195 let manifest = serde_json::to_string_pretty(&manifest).unwrap();
196 Ok(manifest)
197 }
198
199 pub fn fingerprint(&self) -> Result<Option<Fingerprint>, Error> {
200 match &self.unprotected["key-fingerprint"] {
201 Value::Null => Ok(None),
202 value => Ok(Some(serde_json::from_value(value.clone())?))
203 }
204 }
205
206 /// Checks if a BackupManifest and a CryptConfig share a valid fingerprint combination.
207 ///
208 /// An unsigned manifest is valid with any or no CryptConfig.
209 /// A signed manifest is only valid with a matching CryptConfig.
210 pub fn check_fingerprint(&self, crypt_config: Option<&CryptConfig>) -> Result<(), Error> {
211 if let Some(fingerprint) = self.fingerprint()? {
212 match crypt_config {
213 None => bail!(
214 "missing key - manifest was created with key {}",
215 fingerprint,
216 ),
217 Some(crypt_config) => {
218 let config_fp = crypt_config.fingerprint();
219 if config_fp != fingerprint {
220 bail!(
221 "wrong key - manifest's key {} does not match provided key {}",
222 fingerprint,
223 config_fp
224 );
225 }
226 }
227 }
228 };
229
230 Ok(())
231 }
232
233 /// Try to read the manifest. This verifies the signature if there is a crypt_config.
234 pub fn from_data(data: &[u8], crypt_config: Option<&CryptConfig>) -> Result<BackupManifest, Error> {
235 let json: Value = serde_json::from_slice(data)?;
236 let signature = json["signature"].as_str().map(String::from);
237
238 if let Some(ref crypt_config) = crypt_config {
239 if let Some(signature) = signature {
240 let expected_signature = proxmox::tools::digest_to_hex(&Self::json_signature(&json, crypt_config)?);
241
242 let fingerprint = &json["unprotected"]["key-fingerprint"];
243 if fingerprint != &Value::Null {
244 let fingerprint = serde_json::from_value(fingerprint.clone())?;
245 let config_fp = crypt_config.fingerprint();
246 if config_fp != fingerprint {
247 bail!(
248 "wrong key - unable to verify signature since manifest's key {} does not match provided key {}",
249 fingerprint,
250 config_fp
251 );
252 }
253 }
254 if signature != expected_signature {
255 bail!("wrong signature in manifest");
256 }
257 } else {
258 // not signed: warn/fail?
259 }
260 }
261
262 let manifest: BackupManifest = serde_json::from_value(json)?;
263 Ok(manifest)
264 }
265 }
266
267
268 impl TryFrom<super::DataBlob> for BackupManifest {
269 type Error = Error;
270
271 fn try_from(blob: super::DataBlob) -> Result<Self, Error> {
272 // no expected digest available
273 let data = blob.decode(None, None)
274 .map_err(|err| format_err!("decode backup manifest blob failed - {}", err))?;
275 let json: Value = serde_json::from_slice(&data[..])
276 .map_err(|err| format_err!("unable to parse backup manifest json - {}", err))?;
277 let manifest: BackupManifest = serde_json::from_value(json)?;
278 Ok(manifest)
279 }
280 }
281
282
283 #[test]
284 fn test_manifest_signature() -> Result<(), Error> {
285
286 use crate::{KeyDerivationConfig};
287
288 let pw = b"test";
289
290 let kdf = KeyDerivationConfig::Scrypt {
291 n: 65536,
292 r: 8,
293 p: 1,
294 salt: Vec::new(),
295 };
296
297 let testkey = kdf.derive_key(pw)?;
298
299 let crypt_config = CryptConfig::new(testkey)?;
300
301 let snapshot: BackupDir = "host/elsa/2020-06-26T13:56:05Z".parse()?;
302
303 let mut manifest = BackupManifest::new(snapshot);
304
305 manifest.add_file("test1.img.fidx".into(), 200, [1u8; 32], CryptMode::Encrypt)?;
306 manifest.add_file("abc.blob".into(), 200, [2u8; 32], CryptMode::None)?;
307
308 manifest.unprotected["note"] = "This is not protected by the signature.".into();
309
310 let text = manifest.to_string(Some(&crypt_config))?;
311
312 let manifest: Value = serde_json::from_str(&text)?;
313 let signature = manifest["signature"].as_str().unwrap().to_string();
314
315 assert_eq!(signature, "d7b446fb7db081662081d4b40fedd858a1d6307a5aff4ecff7d5bf4fd35679e9");
316
317 let manifest: BackupManifest = serde_json::from_value(manifest)?;
318 let expected_signature = proxmox::tools::digest_to_hex(&manifest.signature(&crypt_config)?);
319
320 assert_eq!(signature, expected_signature);
321
322 Ok(())
323 }