1use std::fmt;
2use std::io::Cursor;
3
4use golden_core::{GoldenGroup, ParticipantIndex};
5use golden_ehtdh1::wire::{from_wire_bytes, to_wire_bytes};
6use golden_ehtdh1::{
7 Ciphertext,
8 PublicKeySet,
9 SealingKey,
10 SecretShare,
11 SetupContext,
12 UnsealingShare,
13 derive_context_session_id,
14};
15use golden_halo2curves::golden_group::Secp256k1GoldenGroup;
16use miden_protocol::utils::serde::{
17 ByteReader,
18 ByteWriter,
19 Deserializable,
20 DeserializationError,
21 Serializable,
22};
23use rand_core_06::{CryptoRng, RngCore};
24use zeroize::Zeroizing;
25
26use crate::private_record::CONTENT_KEY_BYTES;
27use crate::{PrivateRecordError, PrivateRecordShareRequest, StoredPrivateRecord};
28
29type StorageGroup = Secp256k1GoldenGroup;
31
32#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub struct StorageKeyEpoch([u8; 32]);
35
36impl StorageKeyEpoch {
37 pub const fn new(bytes: [u8; 32]) -> Self {
39 Self(bytes)
40 }
41
42 pub fn from_hex(encoded: impl AsRef<[u8]>) -> Result<Self, hex::FromHexError> {
44 let mut bytes = [0; 32];
45 hex::decode_to_slice(encoded, &mut bytes)?;
46 Ok(Self(bytes))
47 }
48
49 pub const fn as_bytes(&self) -> &[u8; 32] {
51 &self.0
52 }
53}
54
55impl Serializable for StorageKeyEpoch {
56 fn write_into<W: ByteWriter>(&self, target: &mut W) {
57 target.write_bytes(&self.0);
58 }
59}
60
61impl Deserializable for StorageKeyEpoch {
62 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
63 Ok(Self(source.read_array()?))
64 }
65}
66
67pub struct EncodedGoldenOperatorKey {
72 key_epoch: StorageKeyEpoch,
73 setup_context: Vec<u8>,
74 public_key_set: Vec<u8>,
75 secret_share: Zeroizing<Vec<u8>>,
76}
77
78impl fmt::Debug for EncodedGoldenOperatorKey {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 formatter
81 .debug_struct("EncodedGoldenOperatorKey")
82 .field("key_epoch", &self.key_epoch)
83 .field("setup_context_bytes", &self.setup_context.len())
84 .field("public_key_set_bytes", &self.public_key_set.len())
85 .field("secret_share", &"<redacted>")
86 .finish()
87 }
88}
89
90impl EncodedGoldenOperatorKey {
91 const BUNDLE_HEADER: &[u8] = b"miden-validator-storage-key\x01";
92
93 pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
98 let mut bytes = Zeroizing::new(Vec::new());
99 bytes.extend_from_slice(Self::BUNDLE_HEADER);
100 bytes.extend_from_slice(self.key_epoch.as_bytes());
101 for value in [&self.setup_context, &self.public_key_set, &*self.secret_share] {
102 let length = u32::try_from(value.len()).expect("storage key field must fit in u32");
103 bytes.write_u32(length);
104 bytes.extend_from_slice(value);
105 }
106 bytes
107 }
108
109 pub fn from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
115 let mut source = Cursor::new(bytes);
116 if source.read_slice(Self::BUNDLE_HEADER.len())? != Self::BUNDLE_HEADER {
117 return Err(DeserializationError::InvalidValue(
118 "unsupported storage key bundle format".into(),
119 ));
120 }
121 let key_epoch = StorageKeyEpoch::read_from(&mut source)?;
122 let mut values = [Vec::new(), Vec::new(), Vec::new()];
123 for value in &mut values {
124 let length = source.read_u32()? as usize;
125 *value = source.read_slice(length)?.to_vec();
126 }
127 let [setup_context, public_key_set, secret_share] = values;
128 let bundle = Self::new(key_epoch, setup_context, public_key_set, secret_share);
129 if source.has_more_bytes() {
130 return Err(DeserializationError::InvalidValue(
131 "storage key bundle contains trailing bytes".into(),
132 ));
133 }
134 Ok(bundle)
135 }
136
137 pub fn new(
139 key_epoch: StorageKeyEpoch,
140 setup_context: Vec<u8>,
141 public_key_set: Vec<u8>,
142 secret_share: Vec<u8>,
143 ) -> Self {
144 Self {
145 key_epoch,
146 setup_context,
147 public_key_set,
148 secret_share: Zeroizing::new(secret_share),
149 }
150 }
151
152 pub fn into_parts(self) -> (StorageKeyEpoch, Vec<u8>, Vec<u8>, Zeroizing<Vec<u8>>) {
154 (self.key_epoch, self.setup_context, self.public_key_set, self.secret_share)
155 }
156
157 pub fn decode(self) -> Result<GoldenOperatorKey, GoldenOperatorKeyError> {
159 let setup_context = from_wire_bytes(&self.setup_context).map_err(|source| {
160 GoldenOperatorKeyError::InvalidWireValue { field: "setup context", source }
161 })?;
162 let public_key_set = from_wire_bytes(&self.public_key_set).map_err(|source| {
163 GoldenOperatorKeyError::InvalidWireValue { field: "public key set", source }
164 })?;
165 let secret_share = from_wire_bytes(&self.secret_share).map_err(|source| {
166 GoldenOperatorKeyError::InvalidWireValue { field: "secret share", source }
167 })?;
168
169 GoldenOperatorKey::new(self.key_epoch, setup_context, public_key_set, secret_share)
170 }
171}
172
173pub struct GoldenOperatorKey {
175 key_epoch: StorageKeyEpoch,
176 setup_context: SetupContext,
177 public_key_set: PublicKeySet<StorageGroup>,
178 secret_share: SecretShare<StorageGroup>,
179 sealing_key: SealingKey<StorageGroup>,
180}
181
182impl fmt::Debug for GoldenOperatorKey {
183 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184 formatter
185 .debug_struct("GoldenOperatorKey")
186 .field("key_epoch", &self.key_epoch)
187 .field("setup_context", &self.setup_context)
188 .field("public_key_set", &self.public_key_set)
189 .field("secret_share", &"<redacted>")
190 .field("sealing_key", &self.sealing_key)
191 .finish()
192 }
193}
194
195impl GoldenOperatorKey {
196 pub fn new(
198 key_epoch: StorageKeyEpoch,
199 setup_context: SetupContext,
200 public_key_set: PublicKeySet<StorageGroup>,
201 secret_share: SecretShare<StorageGroup>,
202 ) -> Result<Self, GoldenOperatorKeyError> {
203 if setup_context.backend_id != StorageGroup::BACKEND_ID {
204 return Err(GoldenOperatorKeyError::BackendMismatch {
205 expected: StorageGroup::BACKEND_ID,
206 actual: setup_context.backend_id,
207 });
208 }
209 if setup_context.context_session_id
210 != derive_context_session_id(setup_context.decryption_session_id)
211 {
212 return Err(GoldenOperatorKeyError::ContextSessionMismatch);
213 }
214
215 let public_key_set = PublicKeySet::new(
216 public_key_set.threshold,
217 public_key_set.joint_public_key,
218 public_key_set.public_shares,
219 )
220 .map_err(GoldenOperatorKeyError::InvalidPublicKeySet)?;
221
222 if setup_context.threshold != public_key_set.threshold {
223 return Err(GoldenOperatorKeyError::ThresholdMismatch {
224 setup: setup_context.threshold,
225 public_key_set: public_key_set.threshold,
226 });
227 }
228
229 let participants =
230 public_key_set.public_shares.keys().copied().collect::<Vec<ParticipantIndex>>();
231 if setup_context.participants != participants {
232 return Err(GoldenOperatorKeyError::ParticipantSetMismatch);
233 }
234
235 let public_share = public_key_set.public_share(secret_share.participant).ok_or(
236 GoldenOperatorKeyError::UnknownLocalParticipant(secret_share.participant.get()),
237 )?;
238 if public_share.decryption != StorageGroup::mul_generator(&secret_share.decryption)
239 || public_share.context != StorageGroup::mul_generator(&secret_share.context)
240 {
241 return Err(GoldenOperatorKeyError::SecretShareMismatch);
242 }
243 if setup_context.epoch != *key_epoch.as_bytes() {
244 return Err(GoldenOperatorKeyError::EpochMismatch);
245 }
246
247 let sealing_key = SealingKey::new(public_key_set.joint_public_key)
248 .map_err(GoldenOperatorKeyError::InvalidSealingKey)?;
249
250 Ok(Self {
251 key_epoch,
252 setup_context,
253 public_key_set,
254 secret_share,
255 sealing_key,
256 })
257 }
258
259 pub fn encode(&self) -> EncodedGoldenOperatorKey {
261 EncodedGoldenOperatorKey::new(
262 self.key_epoch,
263 to_wire_bytes(&self.setup_context),
264 to_wire_bytes(&self.public_key_set),
265 to_wire_bytes(&self.secret_share),
266 )
267 }
268
269 pub const fn key_epoch(&self) -> StorageKeyEpoch {
271 self.key_epoch
272 }
273
274 pub fn setup_context_id(&self) -> [u8; 32] {
276 self.setup_context.root()
277 }
278
279 pub const fn sealing_key(&self) -> &SealingKey<StorageGroup> {
281 &self.sealing_key
282 }
283
284 pub const fn public_key_set(&self) -> &PublicKeySet<StorageGroup> {
286 &self.public_key_set
287 }
288
289 pub const fn setup_context(&self) -> &SetupContext {
291 &self.setup_context
292 }
293
294 pub const fn participant(&self) -> ParticipantIndex {
296 self.secret_share.participant
297 }
298
299 pub(crate) fn issue_decryption_share<R>(
301 &self,
302 rng: &mut R,
303 ciphertext_bytes: &[u8],
304 context: &[u8],
305 ) -> Result<Vec<u8>, PrivateRecordError>
306 where
307 R: RngCore + CryptoRng,
308 {
309 let ciphertext: Ciphertext<StorageGroup> =
310 from_wire_bytes(ciphertext_bytes).map_err(PrivateRecordError::InvalidGoldenEncoding)?;
311 if ciphertext.encrypted_payload.len() != CONTENT_KEY_BYTES {
312 return Err(PrivateRecordError::InvalidEncryptedRecordKey);
313 }
314 ciphertext
315 .verify_with_associated_data(context)
316 .map_err(PrivateRecordError::InvalidGoldenEncoding)?;
317
318 let share = UnsealingShare::new(self.secret_share.clone())
319 .decrypt_share_with_associated_data(
320 rng,
321 &self.setup_context,
322 &ciphertext,
323 context,
324 context,
325 )
326 .map_err(PrivateRecordError::ShareGeneration)?;
327 Ok(to_wire_bytes(&share))
328 }
329
330 pub fn issue_private_record_share<R>(
332 &self,
333 rng: &mut R,
334 request: &PrivateRecordShareRequest,
335 record: &StoredPrivateRecord,
336 ) -> Result<Vec<u8>, PrivateRecordError>
337 where
338 R: RngCore + CryptoRng,
339 {
340 record.validate_share_request(request, self.key_epoch, self.setup_context_id())?;
341 self.issue_decryption_share(rng, record.encrypted_record_key(), request.context())
342 }
343}
344
345#[derive(Debug, thiserror::Error)]
347pub enum GoldenOperatorKeyError {
348 #[error("invalid Golden {field}")]
350 InvalidWireValue {
351 field: &'static str,
352 #[source]
353 source: golden_ehtdh1::Error,
354 },
355 #[error("Golden backend mismatch: expected {expected}, got {actual}")]
357 BackendMismatch { expected: &'static str, actual: String },
358 #[error("Golden context session does not match the decryption session")]
360 ContextSessionMismatch,
361 #[error("invalid Golden public key set")]
363 InvalidPublicKeySet(#[source] golden_ehtdh1::Error),
364 #[error(
366 "Golden threshold mismatch: setup context uses {setup}, public key set uses {public_key_set}"
367 )]
368 ThresholdMismatch { setup: usize, public_key_set: usize },
369 #[error("Golden participant set mismatch")]
371 ParticipantSetMismatch,
372 #[error("Golden participant {0} is not in the public key set")]
374 UnknownLocalParticipant(u32),
375 #[error("Golden secret share does not match its public share")]
377 SecretShareMismatch,
378 #[error("Golden setup epoch does not match the node storage key epoch")]
380 EpochMismatch,
381 #[error("invalid Golden sealing key")]
383 InvalidSealingKey(#[source] golden_ehtdh1::Error),
384}
385
386#[cfg(test)]
387pub(crate) mod tests {
388 use std::collections::BTreeMap;
389
390 use golden_core::{GoldenScalar, SessionId};
391 use golden_ehtdh1::{PublicShare, derive_context_session_id};
392 use golden_halo2curves::golden_group::Secp256k1Scalar;
393
394 use super::*;
395
396 const EPOCH: StorageKeyEpoch = StorageKeyEpoch::new([9; 32]);
397
398 fn participant(value: u32) -> ParticipantIndex {
399 ParticipantIndex::new(value).unwrap()
400 }
401
402 fn scalar(value: u64) -> Secp256k1Scalar {
403 Secp256k1Scalar::from_u64(value).unwrap()
404 }
405
406 fn evaluate(
407 secret: Secp256k1Scalar,
408 coefficient: Secp256k1Scalar,
409 participant: ParticipantIndex,
410 ) -> Secp256k1Scalar {
411 secret.add(&coefficient.mul(&participant.to_scalar().unwrap()))
412 }
413
414 fn values_for(
415 local_participant: ParticipantIndex,
416 ) -> (SetupContext, PublicKeySet<StorageGroup>, SecretShare<StorageGroup>) {
417 let participants = [participant(1), participant(2), participant(3)];
418 let decryption_secret = scalar(11);
419 let decryption_coefficient = scalar(7);
420 let context_coefficient = scalar(13);
421 let mut public_shares = BTreeMap::new();
422 let mut local_secret_share = None;
423
424 for participant in participants {
425 let decryption = evaluate(decryption_secret, decryption_coefficient, participant);
426 let context = evaluate(scalar(0), context_coefficient, participant);
427 public_shares.insert(
428 participant,
429 PublicShare {
430 decryption: StorageGroup::mul_generator(&decryption),
431 context: StorageGroup::mul_generator(&context),
432 },
433 );
434 if participant == local_participant {
435 local_secret_share = Some(SecretShare { participant, decryption, context });
436 }
437 }
438
439 let decryption_session_id = SessionId([2; 32]);
440 let setup_context = SetupContext {
441 backend_id: StorageGroup::BACKEND_ID.to_owned(),
442 threshold: 2,
443 registry_root: [1; 32],
444 participants: participants.to_vec(),
445 decryption_session_id,
446 context_session_id: derive_context_session_id(decryption_session_id),
447 decryption_transcript_root: [3; 32],
448 context_transcript_root: [4; 32],
449 epoch: *EPOCH.as_bytes(),
450 };
451 let public_key_set =
452 PublicKeySet::new(2, StorageGroup::mul_generator(&decryption_secret), public_shares)
453 .unwrap();
454
455 (setup_context, public_key_set, local_secret_share.unwrap())
456 }
457
458 fn values() -> (SetupContext, PublicKeySet<StorageGroup>, SecretShare<StorageGroup>) {
459 values_for(participant(1))
460 }
461
462 pub(crate) fn operator_keys() -> Vec<GoldenOperatorKey> {
463 [participant(1), participant(2), participant(3)]
464 .into_iter()
465 .map(|participant| {
466 let (setup_context, public_key_set, secret_share) = values_for(participant);
467 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share).unwrap()
468 })
469 .collect()
470 }
471
472 fn operator_key() -> GoldenOperatorKey {
473 operator_keys().remove(0)
474 }
475
476 #[test]
483 #[ignore = "writes fixture files; run explicitly to regenerate"]
484 fn write_insecure_storage_key_fixture() {
485 use std::path::Path;
486
487 let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
488 .join("../../scripts/testdata/insecure-storage-key");
489 fs_err::create_dir_all(&dir).unwrap();
490
491 for index in [1u32, 2, 3] {
492 let (setup_context, public_key_set, secret_share) = values_for(participant(index));
493 let validator_dir = dir.join(format!("validator-{index}"));
494 fs_err::create_dir_all(&validator_dir).unwrap();
495 let operator_key =
496 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share).unwrap();
497 fs_err::write(
498 validator_dir.join("storage-key.bundle"),
499 operator_key.encode().to_bytes(),
500 )
501 .unwrap();
502 }
503 }
504
505 #[test]
506 fn restart_bundle_round_trips() {
507 let expected = operator_key();
508 let bytes = expected.encode().to_bytes();
509 let decoded = EncodedGoldenOperatorKey::from_bytes(&bytes).unwrap().decode().unwrap();
510
511 assert_eq!(decoded.key_epoch(), EPOCH);
512 assert_eq!(decoded.setup_context(), expected.setup_context());
513 assert_eq!(decoded.public_key_set(), expected.public_key_set());
514 assert_eq!(decoded.participant(), expected.participant());
515 assert_eq!(decoded.sealing_key(), expected.sealing_key());
516 assert_eq!(decoded.setup_context_id(), expected.setup_context_id());
517 assert_eq!(*decoded.encode().to_bytes(), *bytes);
518 }
519
520 #[test]
521 fn bundle_rejects_unknown_format() {
522 let mut bytes = operator_key().encode().to_bytes();
523 bytes[EncodedGoldenOperatorKey::BUNDLE_HEADER.len() - 1] = 2;
524 assert!(matches!(
525 EncodedGoldenOperatorKey::from_bytes(&bytes),
526 Err(DeserializationError::InvalidValue(_)),
527 ));
528 }
529
530 #[test]
531 fn bundle_rejects_truncation_and_trailing_bytes() {
532 let mut bytes = operator_key().encode().to_bytes();
533 for length in 0..bytes.len() {
534 assert!(EncodedGoldenOperatorKey::from_bytes(&bytes[..length]).is_err());
535 }
536 bytes.push(0);
537 assert!(EncodedGoldenOperatorKey::from_bytes(&bytes).is_err());
538 }
539
540 #[test]
541 fn bundle_rejects_field_length_exceeding_input() {
542 let mut bytes = operator_key().encode().to_bytes();
543 let offset = EncodedGoldenOperatorKey::BUNDLE_HEADER.len() + 32;
544 bytes[offset..offset + 4].copy_from_slice(&u32::MAX.to_le_bytes());
545 assert!(matches!(
546 EncodedGoldenOperatorKey::from_bytes(&bytes),
547 Err(DeserializationError::UnexpectedEOF),
548 ));
549 }
550
551 #[test]
552 fn restart_bundle_exposes_persisted_parts() {
553 let expected = operator_key();
554 let (key_epoch, setup_context, public_key_set, secret_share) =
555 expected.encode().into_parts();
556 let decoded_setup = from_wire_bytes::<SetupContext>(&setup_context).unwrap();
557 let decoded_public_key_set =
558 from_wire_bytes::<PublicKeySet<StorageGroup>>(&public_key_set).unwrap();
559
560 assert_eq!(key_epoch, EPOCH);
561 assert_eq!(&decoded_setup, expected.setup_context());
562 assert_eq!(&decoded_public_key_set, expected.public_key_set());
563 assert!(from_wire_bytes::<SecretShare<StorageGroup>>(&secret_share).is_ok());
564 }
565
566 #[test]
567 fn rejects_inconsistent_public_setup() {
568 let (mut setup_context, public_key_set, secret_share) = values();
569 setup_context.backend_id = "wrong-backend".to_owned();
570 assert!(matches!(
571 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
572 Err(GoldenOperatorKeyError::BackendMismatch { .. })
573 ));
574
575 let (mut setup_context, public_key_set, secret_share) = values();
576 setup_context.context_session_id = SessionId([8; 32]);
577 assert!(matches!(
578 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
579 Err(GoldenOperatorKeyError::ContextSessionMismatch)
580 ));
581
582 let (mut setup_context, public_key_set, secret_share) = values();
583 setup_context.threshold = 3;
584 assert!(matches!(
585 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
586 Err(GoldenOperatorKeyError::ThresholdMismatch { .. })
587 ));
588
589 let (mut setup_context, public_key_set, secret_share) = values();
590 setup_context.participants.pop();
591 assert!(matches!(
592 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
593 Err(GoldenOperatorKeyError::ParticipantSetMismatch)
594 ));
595 }
596
597 #[test]
598 fn rejects_invalid_local_secret() {
599 let (setup_context, public_key_set, mut secret_share) = values();
600 secret_share.participant = participant(4);
601 assert!(matches!(
602 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
603 Err(GoldenOperatorKeyError::UnknownLocalParticipant(4))
604 ));
605
606 let (setup_context, public_key_set, mut secret_share) = values();
607 secret_share.decryption = secret_share.decryption.add(&scalar(1));
608 assert!(matches!(
609 GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
610 Err(GoldenOperatorKeyError::SecretShareMismatch)
611 ));
612 }
613
614 #[test]
615 fn rejects_epoch_mismatch() {
616 let (setup_context, public_key_set, secret_share) = values();
617 assert!(matches!(
618 GoldenOperatorKey::new(
619 StorageKeyEpoch::new([10; 32]),
620 setup_context,
621 public_key_set,
622 secret_share,
623 ),
624 Err(GoldenOperatorKeyError::EpochMismatch)
625 ));
626 }
627
628 #[test]
629 fn rejects_malformed_restart_value() {
630 let mut encoded = operator_key().encode();
631 encoded.setup_context.pop();
632
633 assert!(matches!(
634 encoded.decode(),
635 Err(GoldenOperatorKeyError::InvalidWireValue { field: "setup context", .. })
636 ));
637 }
638}