krabka_remote_storage/diskless/capture/
authentication.rs1use {
4 super::{CAPTURE_HEAD_NAME, CAPTURE_STATE_KEY, DisklessWalCapture},
5 crate::worm::{
6 ChainHead, ChainStamp, EpochId, HexBytes, MANIFEST_FORMAT_VERSION, ManifestBody,
7 ManifestSeq, ManifestSignature, ObjectEntry, SegmentIdentity, SegmentManifest,
8 Sha256Digest, TrustedManifestKeys, manifest_head, manifest_signing_bytes,
9 verify_manifest_signature,
10 },
11 krabka_audit::FileEd25519Signer,
12 std::collections::{BTreeMap, BTreeSet},
13 uuid::Uuid,
14};
15
16impl DisklessWalCapture {
17 fn unsigned_bytes(&self) -> Result<Vec<u8>, String> {
18 let mut unsigned = self.clone();
19 unsigned.authentication = None;
20 serde_json::to_vec(&unsigned).map_err(|error| error.to_string())
21 }
22
23 pub fn seal(
29 &mut self,
30 mut objects: Vec<ObjectEntry>,
31 signer: &FileEd25519Signer,
32 ) -> Result<ChainHead, String> {
33 self.validate()?;
34 let expected = self
35 .partitions
36 .iter()
37 .flat_map(|partition| &partition.ranges)
38 .map(|range| range.object_key.clone())
39 .collect::<BTreeSet<_>>();
40 objects.sort_by(|a, b| a.key.cmp(&b.key));
41 let actual = objects
42 .iter()
43 .map(|object| object.key.clone())
44 .collect::<BTreeSet<_>>();
45 if expected != actual || objects.len() != actual.len() {
46 return Err("signed WAL claims do not exactly cover capture references".to_owned());
47 }
48 let state = self.unsigned_bytes()?;
49 let state_digest = Sha256Digest::of(&state);
50 objects.push(ObjectEntry {
51 suffix: ".capture-state".to_owned(),
52 key: CAPTURE_STATE_KEY.to_owned(),
53 size_bytes: state.len() as u64,
54 sha256: state_digest,
55 e_tag: None,
56 version_id: None,
57 create_precondition: false,
58 });
59 let mut segment_id = [0; 16];
60 segment_id.copy_from_slice(&state_digest.0[..16]);
61 let identity = SegmentIdentity {
62 topic: CAPTURE_HEAD_NAME.to_owned(),
63 topic_id: Uuid::nil(),
64 partition: 0,
65 segment_id: Uuid::from_bytes(segment_id),
66 start_offset: 0,
67 end_offset: 0,
68 max_timestamp_ms: i64::try_from(self.captured_at_ms).unwrap_or(i64::MAX),
69 broker_id: -1,
70 event_timestamp_ms: i64::try_from(self.captured_at_ms).unwrap_or(i64::MAX),
71 segment_size_bytes: objects
72 .iter()
73 .map(|object| object.size_bytes)
74 .sum::<u64>()
75 .try_into()
76 .unwrap_or(i64::MAX),
77 leader_epochs: BTreeMap::new(),
78 txn_index_empty: true,
79 };
80 let chain = ChainStamp {
81 epoch_id: EpochId(Uuid::nil()),
82 seq: ManifestSeq(0),
83 prev_head: ChainHead::GENESIS,
84 };
85 let body = ManifestBody {
86 format_version: MANIFEST_FORMAT_VERSION,
87 segment: identity,
88 objects,
89 chain,
90 };
91 let head = manifest_head(&body);
92 let message = manifest_signing_bytes(signer.key_id(), chain.epoch_id, chain.seq, head);
93 self.authentication = Some(SegmentManifest {
94 body,
95 signature: Some(ManifestSignature {
96 key_id: signer.key_id().to_owned(),
97 public_key: HexBytes(signer.public_key()),
98 signature: HexBytes(signer.sign(&message)),
99 }),
100 });
101 Ok(head)
102 }
103
104 pub fn authenticate(
110 &self,
111 trusted: &TrustedManifestKeys,
112 expected_head: Option<&str>,
113 ) -> Result<BTreeMap<String, ObjectEntry>, String> {
114 self.validate()?;
115 let manifest = self
116 .authentication
117 .as_ref()
118 .ok_or_else(|| "diskless capture has no signed WORM boundary".to_owned())?;
119 if manifest.body.format_version != MANIFEST_FORMAT_VERSION {
120 return Err("unsupported diskless capture manifest version".to_owned());
121 }
122 let signature = manifest
123 .signature
124 .as_ref()
125 .ok_or_else(|| "diskless capture WORM boundary is unsigned".to_owned())?;
126 let key = trusted
127 .get(&signature.key_id)
128 .ok_or_else(|| format!("diskless capture uses untrusted key `{}`", signature.key_id))?;
129 if !verify_manifest_signature(manifest, key) {
130 return Err("diskless capture WORM signature is invalid".to_owned());
131 }
132 let head = manifest_head(&manifest.body).to_string();
133 let expected_head = expected_head.ok_or_else(|| {
134 format!("trusted diskless restore requires --worm-expect-head {CAPTURE_HEAD_NAME}=HEX")
135 })?;
136 if !head.eq_ignore_ascii_case(expected_head) {
137 return Err(format!(
138 "pinned diskless capture head is {expected_head}, authenticated head is {head}"
139 ));
140 }
141 let state = self.unsigned_bytes()?;
142 let mut claims = BTreeMap::new();
143 let mut state_claim = None;
144 for object in &manifest.body.objects {
145 if object.key == CAPTURE_STATE_KEY {
146 if state_claim.replace(object).is_some() {
147 return Err("duplicate signed diskless capture-state claim".to_owned());
148 }
149 } else if claims.insert(object.key.clone(), object.clone()).is_some() {
150 return Err(format!("duplicate signed WAL claim `{}`", object.key));
151 }
152 }
153 let state_claim = state_claim
154 .ok_or_else(|| "signed diskless capture-state claim is missing".to_owned())?;
155 if state_claim.size_bytes != state.len() as u64
156 || state_claim.sha256 != Sha256Digest::of(&state)
157 {
158 return Err("diskless capture state changed after signing".to_owned());
159 }
160 let expected = self
161 .partitions
162 .iter()
163 .flat_map(|partition| &partition.ranges)
164 .map(|range| range.object_key.as_str())
165 .collect::<BTreeSet<_>>();
166 if expected != claims.keys().map(String::as_str).collect() {
167 return Err("signed WAL claims do not exactly cover capture references".to_owned());
168 }
169 Ok(claims)
170 }
171}