1use std::fmt;
2
3use golden_core::{GoldenGroup, ParticipantIndex};
4use golden_ehtdh1::wire::{from_wire_bytes, to_wire_bytes};
5use golden_ehtdh1::{
6 Ciphertext,
7 PublicKeySet,
8 SealingKey,
9 SecretShare,
10 SetupContext,
11 UnsealingShare,
12 derive_context_session_id,
13};
14use golden_halo2curves::golden_group::Secp256k1GoldenGroup;
15use rand_core_06::{CryptoRng, RngCore};
16use zeroize::Zeroizing;
17
18use crate::private_record::CONTENT_KEY_BYTES;
19use crate::{PrivateRecordError, PrivateRecordShareRequest, StoredPrivateRecord};
20
21type StorageGroup = Secp256k1GoldenGroup;
23
24#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct StorageKeyEpoch([u8; 32]);
27
28impl StorageKeyEpoch {
29 pub const fn new(bytes: [u8; 32]) -> Self {
31 Self(bytes)
32 }
33
34 pub const fn as_bytes(&self) -> &[u8; 32] {
36 &self.0
37 }
38}
39
40pub struct EncodedGoldenOperatorKey {
42 key_epoch: StorageKeyEpoch,
43 setup_context: Vec<u8>,
44 public_key_set: Vec<u8>,
45 secret_share: Zeroizing<Vec<u8>>,
46}
47
48impl fmt::Debug for EncodedGoldenOperatorKey {
49 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 formatter
51 .debug_struct("EncodedGoldenOperatorKey")
52 .field("key_epoch", &self.key_epoch)
53 .field("setup_context_bytes", &self.setup_context.len())
54 .field("public_key_set_bytes", &self.public_key_set.len())
55 .field("secret_share", &"<redacted>")
56 .finish()
57 }
58}
59
60impl EncodedGoldenOperatorKey {
61 pub fn new(
63 key_epoch: StorageKeyEpoch,
64 setup_context: Vec<u8>,
65 public_key_set: Vec<u8>,
66 secret_share: Vec<u8>,
67 ) -> Self {
68 Self {
69 key_epoch,
70 setup_context,
71 public_key_set,
72 secret_share: Zeroizing::new(secret_share),
73 }
74 }
75
76 pub fn into_parts(self) -> (StorageKeyEpoch, Vec<u8>, Vec<u8>, Zeroizing<Vec<u8>>) {
78 (self.key_epoch, self.setup_context, self.public_key_set, self.secret_share)
79 }
80
81 pub fn decode(self) -> Result<GoldenOperatorKey, GoldenOperatorKeyError> {
83 let setup_context = from_wire_bytes(&self.setup_context).map_err(|source| {
84 GoldenOperatorKeyError::InvalidWireValue { field: "setup context", source }
85 })?;
86 let public_key_set = from_wire_bytes(&self.public_key_set).map_err(|source| {
87 GoldenOperatorKeyError::InvalidWireValue { field: "public key set", source }
88 })?;
89 let secret_share = from_wire_bytes(&self.secret_share).map_err(|source| {
90 GoldenOperatorKeyError::InvalidWireValue { field: "secret share", source }
91 })?;
92
93 GoldenOperatorKey::new(self.key_epoch, setup_context, public_key_set, secret_share)
94 }
95}
96
97pub struct GoldenOperatorKey {
99 key_epoch: StorageKeyEpoch,
100 setup_context: SetupContext,
101 public_key_set: PublicKeySet<StorageGroup>,
102 secret_share: SecretShare<StorageGroup>,
103 sealing_key: SealingKey<StorageGroup>,
104}
105
106impl fmt::Debug for GoldenOperatorKey {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 formatter
109 .debug_struct("GoldenOperatorKey")
110 .field("key_epoch", &self.key_epoch)
111 .field("setup_context", &self.setup_context)
112 .field("public_key_set", &self.public_key_set)
113 .field("secret_share", &"<redacted>")
114 .field("sealing_key", &self.sealing_key)
115 .finish()
116 }
117}
118
119impl GoldenOperatorKey {
120 pub fn new(
122 key_epoch: StorageKeyEpoch,
123 setup_context: SetupContext,
124 public_key_set: PublicKeySet<StorageGroup>,
125 secret_share: SecretShare<StorageGroup>,
126 ) -> Result<Self, GoldenOperatorKeyError> {
127 if setup_context.backend_id != StorageGroup::BACKEND_ID {
128 return Err(GoldenOperatorKeyError::BackendMismatch {
129 expected: StorageGroup::BACKEND_ID,
130 actual: setup_context.backend_id,
131 });
132 }
133 if setup_context.context_session_id
134 != derive_context_session_id(setup_context.decryption_session_id)
135 {
136 return Err(GoldenOperatorKeyError::ContextSessionMismatch);
137 }
138
139 let public_key_set = PublicKeySet::new(
140 public_key_set.threshold,
141 public_key_set.joint_public_key,
142 public_key_set.public_shares,
143 )
144 .map_err(GoldenOperatorKeyError::InvalidPublicKeySet)?;
145
146 if setup_context.threshold != public_key_set.threshold {
147 return Err(GoldenOperatorKeyError::ThresholdMismatch {
148 setup: setup_context.threshold,
149 public_key_set: public_key_set.threshold,
150 });
151 }
152
153 let participants =
154 public_key_set.public_shares.keys().copied().collect::<Vec<ParticipantIndex>>();
155 if setup_context.participants != participants {
156 return Err(GoldenOperatorKeyError::ParticipantSetMismatch);
157 }
158
159 let public_share = public_key_set.public_share(secret_share.participant).ok_or(
160 GoldenOperatorKeyError::UnknownLocalParticipant(secret_share.participant.get()),
161 )?;
162 if public_share.decryption != StorageGroup::mul_generator(&secret_share.decryption)
163 || public_share.context != StorageGroup::mul_generator(&secret_share.context)
164 {
165 return Err(GoldenOperatorKeyError::SecretShareMismatch);
166 }
167 if setup_context.epoch != *key_epoch.as_bytes() {
168 return Err(GoldenOperatorKeyError::EpochMismatch);
169 }
170
171 let sealing_key = SealingKey::new(public_key_set.joint_public_key)
172 .map_err(GoldenOperatorKeyError::InvalidSealingKey)?;
173
174 Ok(Self {
175 key_epoch,
176 setup_context,
177 public_key_set,
178 secret_share,
179 sealing_key,
180 })
181 }
182
183 pub fn encode(&self) -> EncodedGoldenOperatorKey {
185 EncodedGoldenOperatorKey::new(
186 self.key_epoch,
187 to_wire_bytes(&self.setup_context),
188 to_wire_bytes(&self.public_key_set),
189 to_wire_bytes(&self.secret_share),
190 )
191 }
192
193 pub const fn key_epoch(&self) -> StorageKeyEpoch {
195 self.key_epoch
196 }
197
198 pub fn setup_context_id(&self) -> [u8; 32] {
200 self.setup_context.root()
201 }
202
203 pub const fn sealing_key(&self) -> &SealingKey<StorageGroup> {
205 &self.sealing_key
206 }
207
208 pub const fn public_key_set(&self) -> &PublicKeySet<StorageGroup> {
210 &self.public_key_set
211 }
212
213 pub const fn setup_context(&self) -> &SetupContext {
215 &self.setup_context
216 }
217
218 pub const fn participant(&self) -> ParticipantIndex {
220 self.secret_share.participant
221 }
222
223 pub(crate) fn issue_decryption_share<R>(
225 &self,
226 rng: &mut R,
227 ciphertext_bytes: &[u8],
228 context: &[u8],
229 ) -> Result<Vec<u8>, PrivateRecordError>
230 where
231 R: RngCore + CryptoRng,
232 {
233 let ciphertext: Ciphertext<StorageGroup> =
234 from_wire_bytes(ciphertext_bytes).map_err(PrivateRecordError::InvalidGoldenEncoding)?;
235 if ciphertext.encrypted_payload.len() != CONTENT_KEY_BYTES {
236 return Err(PrivateRecordError::InvalidEncryptedRecordKey);
237 }
238 ciphertext
239 .verify_with_associated_data(context)
240 .map_err(PrivateRecordError::InvalidGoldenEncoding)?;
241
242 let share = UnsealingShare::new(self.secret_share.clone())
243 .decrypt_share_with_associated_data(
244 rng,
245 &self.setup_context,
246 &ciphertext,
247 context,
248 context,
249 )
250 .map_err(PrivateRecordError::ShareGeneration)?;
251 Ok(to_wire_bytes(&share))
252 }
253
254 pub fn issue_private_record_share<R>(
256 &self,
257 rng: &mut R,
258 request: &PrivateRecordShareRequest,
259 record: &StoredPrivateRecord,
260 ) -> Result<Vec<u8>, PrivateRecordError>
261 where
262 R: RngCore + CryptoRng,
263 {
264 record.validate_share_request(request, self.key_epoch, self.setup_context_id())?;
265 self.issue_decryption_share(rng, record.encrypted_record_key(), request.context())
266 }
267}
268
269#[derive(Debug, thiserror::Error)]
271pub enum GoldenOperatorKeyError {
272 #[error("invalid Golden {field}")]
274 InvalidWireValue {
275 field: &'static str,
276 #[source]
277 source: golden_ehtdh1::Error,
278 },
279 #[error("Golden backend mismatch: expected {expected}, got {actual}")]
281 BackendMismatch { expected: &'static str, actual: String },
282 #[error("Golden context session does not match the decryption session")]
284 ContextSessionMismatch,
285 #[error("invalid Golden public key set")]
287 InvalidPublicKeySet(#[source] golden_ehtdh1::Error),
288 #[error(
290 "Golden threshold mismatch: setup context uses {setup}, public key set uses {public_key_set}"
291 )]
292 ThresholdMismatch { setup: usize, public_key_set: usize },
293 #[error("Golden participant set mismatch")]
295 ParticipantSetMismatch,
296 #[error("Golden participant {0} is not in the public key set")]
298 UnknownLocalParticipant(u32),
299 #[error("Golden secret share does not match its public share")]
301 SecretShareMismatch,
302 #[error("Golden setup epoch does not match the node storage key epoch")]
304 EpochMismatch,
305 #[error("invalid Golden sealing key")]
307 InvalidSealingKey(#[source] golden_ehtdh1::Error),
308}
309
310#[cfg(test)]
311pub(crate) mod tests {
312 use std::collections::BTreeMap;
313
314 use golden_core::{GoldenScalar, SessionId};
315 use golden_ehtdh1::{PublicShare, derive_context_session_id};
316 use golden_halo2curves::golden_group::Secp256k1Scalar;
317
318 use super::*;
319
320 const EPOCH: StorageKeyEpoch = StorageKeyEpoch::new([9; 32]);
321
322 fn participant(value: u32) -> ParticipantIndex {
323 ParticipantIndex::new(value).unwrap()
324 }
325
326 fn scalar(value: u64) -> Secp256k1Scalar {
327 Secp256k1Scalar::from_u64(value).unwrap()
328 }
329
330 fn evaluate(
331 secret: Secp256k1Scalar,
332 coefficient: Secp256k1Scalar,
333 participant: ParticipantIndex,
334 ) -> Secp256k1Scalar {
335 secret.add(&coefficient.mul(&participant.to_scalar().unwrap()))
336 }
337
338 fn values_for(
339 local_participant: ParticipantIndex,
340 ) -> (SetupContext, PublicKeySet<StorageGroup>, SecretShare<StorageGroup>) {
341 let participants = [participant(1), participant(2), participant(3)];
342 let decryption_secret = scalar(11);
343 let decryption_coefficient = scalar(7);
344 let context_coefficient = scalar(13);
345 let mut public_shares = BTreeMap::new();
346 let mut local_secret_share = None;
347
348 for participant in participants {
349 let decryption = evaluate(decryption_secret, decryption_coefficient, participant);
350 let context = evaluate(scalar(0), context_coefficient, participant);
351 public_shares.insert(
352 participant,
353 PublicShare {
354 decryption: StorageGroup::mul_generator(&decryption),
355 context: StorageGroup::mul_generator(&context),
356 },
357 );
358 if participant == local_participant {
359 local_secret_share = Some(SecretShare { participant, decryption, context });
360 }
361 }
362
363 let decryption_session_id = SessionId([2; 32]);
364 let setup_context = SetupContext {
365 backend_id: StorageGroup::BACKEND_ID.to_owned(),
366 threshold: 2,
367 registry_root: [1; 32],
368 participants: participants.to_vec(),
369 decryption_session_id,
370 context_session_id: derive_context_session_id(decryption_session_id),
371 decryption_transcript_root: [3; 32],
372 context_transcript_root: [4; 32],
373 epoch: *EPOCH.as_bytes(),
374 };
375 let public_key_set =
376 PublicKeySet::new(2, StorageGroup::mul_generator(&decryption_secret), public_shares)
377 .unwrap();
378
379 (setup_context, public_key_set, local_secret_share.unwrap())
380 }
381
382 fn values() -> (SetupContext, PublicKeySet<StorageGroup>, SecretShare<StorageGroup>) {
383 values_for(participant(1))
384 }
385
386 pub(crate) fn operator_keys() -> Vec<GoldenOperatorKey> {
387 [participant(1), participant(2), participant(3)]
388 .into_iter()
389 .map(|participant| {
390 let (setup_context, public_key_set, secret_share) = values_for(participant);
391 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share).unwrap()
392 })
393 .collect()
394 }
395
396 fn operator_key() -> GoldenOperatorKey {
397 operator_keys().remove(0)
398 }
399
400 #[test]
417 #[ignore = "writes fixture files; run explicitly to regenerate"]
418 fn write_insecure_storage_key_fixture() {
419 use std::path::Path;
420
421 let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
422 .join("../../scripts/testdata/insecure-storage-key");
423 fs_err::create_dir_all(&dir).unwrap();
424
425 let (setup_context, public_key_set, _) = values_for(participant(1));
426 fs_err::write(dir.join("setup-context.wire"), to_wire_bytes(&setup_context)).unwrap();
427 fs_err::write(dir.join("public-key-set.wire"), to_wire_bytes(&public_key_set)).unwrap();
428
429 for index in [1u32, 2, 3] {
430 let (.., secret_share) = values_for(participant(index));
431 let validator_dir = dir.join(format!("validator-{index}"));
432 fs_err::create_dir_all(&validator_dir).unwrap();
433 fs_err::write(validator_dir.join("secret-share.wire"), to_wire_bytes(&secret_share))
434 .unwrap();
435 }
436 }
437
438 #[test]
439 fn restart_bundle_round_trips() {
440 let expected = operator_key();
441 let decoded = expected.encode().decode().unwrap();
442
443 assert_eq!(decoded.key_epoch(), EPOCH);
444 assert_eq!(decoded.setup_context(), expected.setup_context());
445 assert_eq!(decoded.public_key_set(), expected.public_key_set());
446 assert_eq!(decoded.participant(), expected.participant());
447 assert_eq!(decoded.sealing_key(), expected.sealing_key());
448 assert_eq!(decoded.setup_context_id(), expected.setup_context_id());
449 }
450
451 #[test]
452 fn restart_bundle_exposes_persisted_parts() {
453 let expected = operator_key();
454 let (key_epoch, setup_context, public_key_set, secret_share) =
455 expected.encode().into_parts();
456 let decoded_setup = from_wire_bytes::<SetupContext>(&setup_context).unwrap();
457 let decoded_public_key_set =
458 from_wire_bytes::<PublicKeySet<StorageGroup>>(&public_key_set).unwrap();
459
460 assert_eq!(key_epoch, EPOCH);
461 assert_eq!(&decoded_setup, expected.setup_context());
462 assert_eq!(&decoded_public_key_set, expected.public_key_set());
463 assert!(from_wire_bytes::<SecretShare<StorageGroup>>(&secret_share).is_ok());
464 }
465
466 #[test]
467 fn rejects_inconsistent_public_setup() {
468 let (mut setup_context, public_key_set, secret_share) = values();
469 setup_context.backend_id = "wrong-backend".to_owned();
470 assert!(matches!(
471 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
472 Err(GoldenOperatorKeyError::BackendMismatch { .. })
473 ));
474
475 let (mut setup_context, public_key_set, secret_share) = values();
476 setup_context.context_session_id = SessionId([8; 32]);
477 assert!(matches!(
478 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
479 Err(GoldenOperatorKeyError::ContextSessionMismatch)
480 ));
481
482 let (mut setup_context, public_key_set, secret_share) = values();
483 setup_context.threshold = 3;
484 assert!(matches!(
485 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
486 Err(GoldenOperatorKeyError::ThresholdMismatch { .. })
487 ));
488
489 let (mut setup_context, public_key_set, secret_share) = values();
490 setup_context.participants.pop();
491 assert!(matches!(
492 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
493 Err(GoldenOperatorKeyError::ParticipantSetMismatch)
494 ));
495 }
496
497 #[test]
498 fn rejects_invalid_local_secret() {
499 let (setup_context, public_key_set, mut secret_share) = values();
500 secret_share.participant = participant(4);
501 assert!(matches!(
502 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
503 Err(GoldenOperatorKeyError::UnknownLocalParticipant(4))
504 ));
505
506 let (setup_context, public_key_set, mut secret_share) = values();
507 secret_share.decryption = secret_share.decryption.add(&scalar(1));
508 assert!(matches!(
509 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
510 Err(GoldenOperatorKeyError::SecretShareMismatch)
511 ));
512 }
513
514 #[test]
515 fn rejects_epoch_mismatch() {
516 let (setup_context, public_key_set, secret_share) = values();
517 assert!(matches!(
518 GoldenOperatorKey::new(
519 StorageKeyEpoch::new([10; 32]),
520 setup_context,
521 public_key_set,
522 secret_share,
523 ),
524 Err(GoldenOperatorKeyError::EpochMismatch)
525 ));
526 }
527
528 #[test]
529 fn rejects_malformed_restart_value() {
530 let mut encoded = operator_key().encode();
531 encoded.setup_context.pop();
532
533 assert!(matches!(
534 encoded.decode(),
535 Err(GoldenOperatorKeyError::InvalidWireValue { field: "setup context", .. })
536 ));
537 }
538}