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