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