1use {
8 crate::{
9 metadata::RemoteLogSegmentMetadata,
10 worm::{
11 chain::WormChainRecord,
12 config::WormConfig,
13 error::WormError,
14 manifest::{
15 ChainStamp, HexBytes, MANIFEST_FORMAT_VERSION, ManifestBody, ManifestSignature,
16 ObjectEntry, SegmentIdentity, SegmentManifest, manifest_head,
17 manifest_signing_bytes,
18 },
19 },
20 },
21 bytes::Bytes,
22 krabka_audit::signing::FileEd25519Signer,
23 std::sync::Arc,
24};
25
26#[derive(derive_more::Debug)]
37pub struct WormArchiver {
38 #[debug("{:?}", signer.as_ref().map(|signer| signer.key_id()))]
39 signer: Option<Arc<FileEd25519Signer>>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct SealedManifest {
45 pub manifest: SegmentManifest,
47 pub bytes: Bytes,
49 pub receipt: WormChainRecord,
52}
53
54impl WormArchiver {
55 #[must_use]
58 pub fn new(signer: Option<Arc<FileEd25519Signer>>) -> Self { Self { signer } }
59
60 pub fn from_config(cfg: &WormConfig) -> Result<Self, WormError> {
67 let signer = match (cfg.signing_key_path.as_ref(), cfg.signing_key_id.as_ref()) {
68 (None, None) => None,
69 (Some(path), Some(key_id)) => {
70 let signer = FileEd25519Signer::from_pkcs8_file(path, key_id.clone())
71 .map_err(|e| WormError::SigningKey(e.to_string()))?;
72 Some(Arc::new(signer))
73 }
74 (Some(_), None) => {
78 return Err(WormError::SigningKey(
79 "signing key path is set without a key id".to_owned(),
80 ));
81 }
82 (None, Some(_)) => {
83 return Err(WormError::SigningKey(
84 "signing key id is set without a key path".to_owned(),
85 ));
86 }
87 };
88 Ok(Self::new(signer))
89 }
90
91 pub fn seal(
106 &self,
107 metadata: &RemoteLogSegmentMetadata,
108 objects: Vec<ObjectEntry>,
109 ) -> Result<SealedManifest, WormError> {
110 let stamped = metadata
111 .custom_metadata()
112 .ok_or(WormError::MissingChainStamp)
113 .and_then(WormChainRecord::from_custom_metadata)?;
114 let chain = ChainStamp {
117 epoch_id: stamped.epoch_id,
118 seq: stamped.seq,
119 prev_head: stamped.prev_head,
120 };
121
122 let body = ManifestBody {
123 format_version: MANIFEST_FORMAT_VERSION,
124 segment: SegmentIdentity::from_metadata(metadata),
125 objects,
126 chain,
127 };
128 let head = manifest_head(&body);
129 let signature = self.signer.as_ref().map(|signer| {
130 let message = manifest_signing_bytes(signer.key_id(), chain.epoch_id, chain.seq, head);
131 ManifestSignature {
132 key_id: signer.key_id().to_owned(),
133 public_key: HexBytes(signer.public_key()),
134 signature: HexBytes(signer.sign(&message)),
135 }
136 });
137
138 let manifest = SegmentManifest { body, signature };
139 let bytes = serde_json::to_vec(&manifest).map_err(|e| WormError::Codec(e.to_string()))?;
143 Ok(SealedManifest {
144 manifest,
145 bytes: Bytes::from(bytes),
146 receipt: WormChainRecord::request(chain).with_head(head),
147 })
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use {
154 super::*,
155 crate::{
156 metadata::{
157 CustomMetadata, RemoteLogSegmentDetails, RemoteLogSegmentId, RemoteLogSegmentState,
158 TopicIdPartition,
159 },
160 worm::manifest::{
161 ChainHead, EpochId, ManifestSeq, Sha256Digest, verify_manifest_signature,
162 },
163 },
164 assert2::{assert, check},
165 krabka_ids::LeaderEpoch,
166 ring::{rand::SystemRandom, signature::Ed25519KeyPair},
167 std::{fs, path::PathBuf},
168 tempfile::TempDir,
169 uuid::Uuid,
170 };
171
172 const KEY_ID: &str = "worm-archiver-key";
173
174 fn epoch() -> EpochId { EpochId(Uuid::from_u128(0xabcd)) }
175
176 fn metadata(segment_id: u128, stamp: Option<ChainStamp>) -> RemoteLogSegmentMetadata {
177 let md = RemoteLogSegmentMetadata::new(
178 RemoteLogSegmentId::new(
179 TopicIdPartition::new(Uuid::from_u128(1), "orders", 0),
180 Uuid::from_u128(segment_id),
181 ),
182 0,
183 99,
184 1_713_000_000_000,
185 1,
186 1_713_000_001_000,
187 RemoteLogSegmentDetails::new(
188 4096,
189 RemoteLogSegmentState::CopySegmentStarted,
190 maplit::btreemap! {LeaderEpoch(0) => 0},
191 ),
192 )
193 .unwrap();
194 match stamp {
195 Some(stamp) => md.with_custom_metadata(
196 WormChainRecord::request(stamp)
197 .to_custom_metadata()
198 .unwrap(),
199 ),
200 None => md,
201 }
202 }
203
204 fn objects() -> Vec<ObjectEntry> {
205 vec![ObjectEntry {
206 suffix: ".log".to_owned(),
207 key: "orders-0/00000000000000000000.log".to_owned(),
208 size_bytes: 10,
209 sha256: Sha256Digest::of(b"0123456789"),
210 e_tag: Some("0".to_owned()),
211 version_id: None,
212 create_precondition: true,
213 }]
214 }
215
216 fn signer() -> Arc<FileEd25519Signer> {
217 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
218 let signer =
219 FileEd25519Signer::from_pkcs8_bytes(pkcs8.as_ref(), KEY_ID.to_owned()).unwrap();
220 Arc::new(signer)
221 }
222
223 fn key_file(dir: &TempDir) -> PathBuf {
226 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
227 let path = dir.path().join("worm.pk8");
228 fs::write(&path, pkcs8.as_ref()).unwrap();
229 path
230 }
231
232 #[test]
233 fn seal_head_chains_from_the_stamp() {
234 let stamp = ChainStamp {
235 epoch_id: epoch(),
236 seq: ManifestSeq(7),
237 prev_head: ChainHead([3u8; 32]),
238 };
239 let sealed = WormArchiver::new(Some(signer()))
240 .seal(&metadata(10, Some(stamp)), objects())
241 .unwrap();
242
243 check!(sealed.manifest.body.chain == stamp);
244 check!(sealed.manifest.body.objects == objects());
245 check!(sealed.manifest.body.format_version == MANIFEST_FORMAT_VERSION);
246 check!(
249 sealed.receipt
250 == WormChainRecord {
251 epoch_id: stamp.epoch_id,
252 seq: stamp.seq,
253 prev_head: stamp.prev_head,
254 head: Some(manifest_head(&sealed.manifest.body)),
255 manifest_version_id: None,
256 }
257 );
258 let decoded: SegmentManifest = serde_json::from_slice(&sealed.bytes).unwrap();
260 check!(decoded == sealed.manifest);
261 }
262
263 #[test]
264 fn seal_without_signer_leaves_manifest_unsigned_but_chained() {
265 let stamp = ChainStamp {
266 epoch_id: epoch(),
267 seq: ManifestSeq(0),
268 prev_head: ChainHead::GENESIS,
269 };
270 let sealed = WormArchiver::new(None)
271 .seal(&metadata(11, Some(stamp)), objects())
272 .unwrap();
273
274 check!(sealed.manifest.signature.is_none());
275 check!(sealed.manifest.body.chain == stamp);
276 check!(sealed.receipt.head == Some(manifest_head(&sealed.manifest.body)));
277 check!(!verify_manifest_signature(&sealed.manifest, &[]));
280 }
281
282 #[test]
283 fn seal_refuses_metadata_with_no_chain_stamp() {
284 let err = WormArchiver::new(Some(signer()))
285 .seal(&metadata(12, None), objects())
286 .unwrap_err();
287
288 check!(matches!(err, WormError::MissingChainStamp));
289 }
290
291 #[test]
292 fn seal_refuses_metadata_with_an_undecodable_chain_stamp() {
293 let md = metadata(13, None).with_custom_metadata(CustomMetadata(b"not json".to_vec()));
294
295 let err = WormArchiver::new(None).seal(&md, objects()).unwrap_err();
296
297 check!(matches!(err, WormError::MalformedChainRecord(_)));
298 }
299
300 #[test]
301 fn two_sealed_manifests_chain() {
302 let archiver = WormArchiver::new(Some(signer()));
303 let first = ChainStamp {
304 epoch_id: epoch(),
305 seq: ManifestSeq(0),
306 prev_head: ChainHead::GENESIS,
307 };
308
309 let a = archiver
310 .seal(&metadata(20, Some(first)), objects())
311 .unwrap();
312 assert!(let Some(second) = a.receipt.next_stamp());
313 let b = archiver
314 .seal(&metadata(21, Some(second)), objects())
315 .unwrap();
316
317 let a_head = manifest_head(&a.manifest.body);
318 check!(b.manifest.body.chain.prev_head == a_head);
319 check!(b.manifest.body.chain.seq == ManifestSeq(1));
320 check!(b.manifest.body.chain.epoch_id == first.epoch_id);
321 check!(manifest_head(&b.manifest.body) != a_head);
324 }
325
326 #[test]
327 fn sealed_manifest_signature_verifies_with_the_signing_key() {
328 let signer = signer();
329 let public_key = signer.public_key();
330 let sealed = WormArchiver::new(Some(signer))
331 .seal(
332 &metadata(
333 30,
334 Some(ChainStamp {
335 epoch_id: epoch(),
336 seq: ManifestSeq(2),
337 prev_head: ChainHead([9u8; 32]),
338 }),
339 ),
340 objects(),
341 )
342 .unwrap();
343
344 assert!(let Some(signature) = sealed.manifest.signature.as_ref());
345 check!(signature.key_id == KEY_ID);
346 check!(signature.public_key == HexBytes(public_key.clone()));
347 check!(verify_manifest_signature(&sealed.manifest, &public_key));
348
349 let mut tampered = sealed.manifest.clone();
351 tampered.body.objects[0].size_bytes += 1;
352 check!(!verify_manifest_signature(&tampered, &public_key));
353 }
354
355 #[test]
356 fn from_config_loads_a_key_only_when_both_halves_are_set() {
357 let dir = TempDir::new().unwrap();
358 let path = key_file(&dir);
359
360 check!(
361 WormArchiver::from_config(&WormConfig::default())
362 .unwrap()
363 .signer
364 .is_none(),
365 "empty config"
366 );
367 let both = WormConfig {
368 signing_key_path: Some(path),
369 signing_key_id: Some(KEY_ID.to_owned()),
370 write_only: false,
371 };
372 assert!(let Ok(archiver) = WormArchiver::from_config(&both));
373 assert!(let Some(signer) = archiver.signer.as_ref());
374 check!(signer.key_id() == KEY_ID);
375 }
376
377 #[yare::parameterized(
378 path_without_id = { true, false, true },
379 id_without_path = { false, true, true },
380 missing_key_file = { true, true, false },
381 )]
382 fn from_config_rejects_incomplete_or_missing_signing_keys(
383 has_path: bool,
384 has_id: bool,
385 key_file_exists: bool,
386 ) {
387 let dir = TempDir::new().unwrap();
388 let path = if key_file_exists {
389 key_file(&dir)
390 } else {
391 dir.path().join("absent.pk8")
392 };
393 let cfg = WormConfig {
394 signing_key_path: has_path.then_some(path),
395 signing_key_id: has_id.then(|| KEY_ID.to_owned()),
396 write_only: false,
397 };
398 check!(matches!(
399 WormArchiver::from_config(&cfg),
400 Err(WormError::SigningKey(_))
401 ));
402 }
403
404 #[test]
405 fn archiver_debug_names_the_key_and_shows_no_key_material() {
406 let signer = signer();
407 let public_key = hex::encode(signer.public_key());
408 let rendered = format!("{:?}", WormArchiver::new(Some(signer)));
409
410 check!(rendered.contains(KEY_ID));
411 check!(!rendered.contains(&public_key));
412 check!(format!("{:?}", WormArchiver::new(None)).contains("None"));
413 }
414}