krabka_remote_storage/worm/manifest/
signing.rs1use {
8 super::{
9 ChainHead, EpochId, HexBytes, ManifestSeq, SegmentManifest, encoding::push_bytes,
10 manifest_head,
11 },
12 krabka_audit::signing::verify_signature,
13 serde::{Deserialize, Serialize},
14};
15
16pub const MANIFEST_DOMAIN: &[u8] = b"krabka-worm-manifest-v1\0";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct ManifestSignature {
25 pub key_id: String,
27 pub public_key: HexBytes,
29 pub signature: HexBytes,
31}
32
33#[must_use]
39pub fn manifest_signing_bytes(
40 key_id: &str,
41 epoch_id: EpochId,
42 seq: ManifestSeq,
43 head: ChainHead,
44) -> Vec<u8> {
45 let kid = key_id.as_bytes();
46 let mut out = Vec::with_capacity(MANIFEST_DOMAIN.len() + 8 + kid.len() + 16 + 8 + 32);
47 out.extend_from_slice(MANIFEST_DOMAIN);
48 push_bytes(&mut out, kid);
49 out.extend_from_slice(epoch_id.0.as_bytes());
50 out.extend_from_slice(&seq.0.to_be_bytes());
51 out.extend_from_slice(&head.0);
52 out
53}
54
55#[must_use]
62pub fn verify_manifest_signature(manifest: &SegmentManifest, public_key: &[u8]) -> bool {
63 let Some(signature) = manifest.signature.as_ref() else {
64 return false;
65 };
66 let head = manifest_head(&manifest.body);
67 let message = manifest_signing_bytes(
68 &signature.key_id,
69 manifest.body.chain.epoch_id,
70 manifest.body.chain.seq,
71 head,
72 );
73 verify_signature(public_key, &message, &signature.signature.0)
74}
75
76#[cfg(test)]
77mod tests {
78 use {
79 super::*,
80 crate::worm::manifest::{
81 MANIFEST_BODY_DOMAIN, ManifestBody, Sha256Digest, canonical_manifest_bytes,
82 test_support::{KEY_ID, sample_body},
83 },
84 assert2::check,
85 krabka_audit::{
86 ids::{EpochMs, Seq},
87 signing::{CHECKPOINT_DOMAIN, FileEd25519Signer, checkpoint_signing_bytes},
88 },
89 ring::{rand::SystemRandom, signature::Ed25519KeyPair},
90 uuid::Uuid,
91 };
92
93 struct FreshSigner {
94 signer: FileEd25519Signer,
95 public_key: Vec<u8>,
96 }
97
98 fn fresh_signer(key_id: &str) -> FreshSigner {
99 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
100 let signer =
101 FileEd25519Signer::from_pkcs8_bytes(pkcs8.as_ref(), key_id.to_owned()).unwrap();
102 let public_key = signer.public_key();
103 FreshSigner { signer, public_key }
104 }
105
106 fn sign_manifest(body: ManifestBody, signer: &FileEd25519Signer) -> SegmentManifest {
107 let head = manifest_head(&body);
108 let message =
109 manifest_signing_bytes(signer.key_id(), body.chain.epoch_id, body.chain.seq, head);
110 SegmentManifest {
111 signature: Some(ManifestSignature {
112 key_id: signer.key_id().to_owned(),
113 public_key: HexBytes(signer.public_key()),
114 signature: HexBytes(signer.sign(&message)),
115 }),
116 body,
117 }
118 }
119
120 #[derive(Clone, Copy)]
121 enum SignatureCheck {
122 Authentic,
123 Body,
124 Sequence,
125 AnotherHead,
126 KeyId,
127 Unsigned,
128 }
129
130 #[yare::parameterized(
131 authentic = { SignatureCheck::Authentic },
132 tampered_body = { SignatureCheck::Body },
133 tampered_sequence = { SignatureCheck::Sequence },
134 signature_for_another_head = { SignatureCheck::AnotherHead },
135 tampered_key_id = { SignatureCheck::KeyId },
136 unsigned = { SignatureCheck::Unsigned },
137 )]
138 fn manifest_signature_round_trips_and_rejects_tampering(case: SignatureCheck) {
139 let FreshSigner { signer, public_key } = fresh_signer(KEY_ID);
140 let base = sample_body();
141 let authentic = sign_manifest(base.clone(), &signer);
142
143 let json = serde_json::to_string(&authentic).unwrap();
144 let reparsed: SegmentManifest = serde_json::from_str(&json).unwrap();
145 check!(reparsed == authentic, "signature survives JSON");
146
147 let mut tampered_body = authentic.clone();
148 tampered_body.body.objects[0].sha256 = Sha256Digest::of(b"swapped body");
149
150 let mut tampered_seq = authentic.clone();
151 tampered_seq.body.chain.seq = ManifestSeq(base.chain.seq.0 + 1);
152
153 let wrong_head_message = manifest_signing_bytes(
155 KEY_ID,
156 base.chain.epoch_id,
157 base.chain.seq,
158 ChainHead([0xab; 32]),
159 );
160 let tampered_head = SegmentManifest {
161 body: base.clone(),
162 signature: Some(ManifestSignature {
163 key_id: KEY_ID.to_owned(),
164 public_key: HexBytes(public_key.clone()),
165 signature: HexBytes(signer.sign(&wrong_head_message)),
166 }),
167 };
168
169 let mut tampered_key_id = authentic.clone();
170 tampered_key_id.signature.as_mut().unwrap().key_id = "worm-key-2".to_owned();
171
172 let unsigned = SegmentManifest {
173 body: base,
174 signature: None,
175 };
176
177 let (manifest, expected) = match case {
178 SignatureCheck::Authentic => (&reparsed, true),
179 SignatureCheck::Body => (&tampered_body, false),
180 SignatureCheck::Sequence => (&tampered_seq, false),
181 SignatureCheck::AnotherHead => (&tampered_head, false),
182 SignatureCheck::KeyId => (&tampered_key_id, false),
183 SignatureCheck::Unsigned => (&unsigned, false),
184 };
185 check!(verify_manifest_signature(manifest, &public_key) == expected);
186
187 let FreshSigner {
188 public_key: other_public_key,
189 ..
190 } = fresh_signer("worm-key-2");
191 check!(!verify_manifest_signature(&authentic, &other_public_key));
192 }
193
194 #[test]
209 fn manifest_signing_bytes_match_the_documented_layout() {
210 let key_id = "worm-key-1";
211 let epoch_id = EpochId(Uuid::from_u128(0x99));
212 let seq = ManifestSeq(7);
213 let head = ChainHead([0xab; 32]);
214
215 let mut expected = Vec::new();
216 expected.extend_from_slice(MANIFEST_DOMAIN);
217 expected.extend_from_slice(&(key_id.len() as u64).to_be_bytes());
218 expected.extend_from_slice(key_id.as_bytes());
219 expected.extend_from_slice(epoch_id.0.as_bytes());
220 expected.extend_from_slice(&seq.0.to_be_bytes());
221 expected.extend_from_slice(&head.0);
222
223 check!(manifest_signing_bytes(key_id, epoch_id, seq, head) == expected);
224 check!(expected.len() == MANIFEST_DOMAIN.len() + 8 + key_id.len() + 16 + 8 + 32);
227 }
228
229 #[test]
230 fn manifest_domain_differs_from_audit_checkpoint_domain() {
231 check!(MANIFEST_DOMAIN != CHECKPOINT_DOMAIN);
232 check!(MANIFEST_BODY_DOMAIN != CHECKPOINT_DOMAIN);
233 check!(MANIFEST_DOMAIN != MANIFEST_BODY_DOMAIN);
234
235 let body = sample_body();
236 check!(canonical_manifest_bytes(&body).starts_with(MANIFEST_BODY_DOMAIN));
237 let head = manifest_head(&body);
238 let manifest_bytes =
239 manifest_signing_bytes(KEY_ID, body.chain.epoch_id, body.chain.seq, head);
240 check!(manifest_bytes.starts_with(MANIFEST_DOMAIN));
241 check!(!manifest_bytes.starts_with(CHECKPOINT_DOMAIN));
242
243 let FreshSigner { signer, public_key } = fresh_signer(KEY_ID);
246 let checkpoint_bytes =
247 checkpoint_signing_bytes(KEY_ID, Seq(body.chain.seq.0), &head.0, EpochMs(0));
248 let cross_purpose = SegmentManifest {
249 body,
250 signature: Some(ManifestSignature {
251 key_id: KEY_ID.to_owned(),
252 public_key: HexBytes(public_key.clone()),
253 signature: HexBytes(signer.sign(&checkpoint_bytes)),
254 }),
255 };
256 check!(!verify_manifest_signature(&cross_purpose, &public_key));
257 }
258}