1use alloc::string::{String, ToString};
34use alloc::vec::Vec;
35
36use miden_protocol::block::{BlockNumber, ValidatorConfig};
37use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{
38 PublicKey as ValidatorPublicKey,
39 Signature as ValidatorSignature,
40};
41use miden_protocol::crypto::dsa::eddsa_25519_sha512::PublicKey;
42use miden_protocol::crypto::ies::SealingKey;
43use miden_protocol::transaction::{TransactionId, TransactionInputs};
44use miden_protocol::{Hasher, Word};
45use miden_tx::utils::serde::{
46 ByteReader,
47 ByteWriter,
48 Deserializable,
49 DeserializationError,
50 Serializable,
51};
52use rand::CryptoRng;
53
54use super::generated::submission::IesScheme;
55use super::{RpcError, generated as proto};
56
57pub(crate) const TRANSACTION_ENCRYPTION_KEY_STORE_SETTING: &str = "transaction_encryption_key";
62
63const TX_INPUT_SEAL_DOMAIN: &[u8] = b"MIDEN_TX_INPUT_SEAL_V1";
69
70const ATTESTATION_DOMAIN: &[u8] = b"MIDEN_TX_ENCRYPTION_KEY_ATTESTATION_V1";
75
76const SUPPORTED_SCHEME: u32 = IesScheme::X25519Xchacha20Poly1305 as u32;
78
79const MAX_KEY_ID_LEN: usize = 64;
83
84#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct TransactionEncryptionKey {
96 scheme: u32,
97 key_id: Vec<u8>,
98 public_key: PublicKey,
99 genesis_commitment: Word,
100}
101
102impl TransactionEncryptionKey {
103 pub fn key_id(&self) -> &[u8] {
109 &self.key_id
110 }
111
112 pub fn public_key(&self) -> &PublicKey {
114 &self.public_key
115 }
116
117 fn transaction_inputs_associated_data(&self, tx_id: TransactionId) -> Vec<u8> {
120 transaction_inputs_associated_data(
121 self.scheme,
122 &self.key_id,
123 self.genesis_commitment,
124 tx_id,
125 )
126 }
127
128 pub fn sealing_key(&self) -> SealingKey {
130 SealingKey::X25519XChaCha20Poly1305(self.public_key.clone())
131 }
132
133 #[cfg(feature = "testing")]
138 pub fn new_unattested(
139 key_id: Vec<u8>,
140 public_key: PublicKey,
141 genesis_commitment: Word,
142 ) -> Self {
143 Self {
144 scheme: SUPPORTED_SCHEME,
145 key_id,
146 public_key,
147 genesis_commitment,
148 }
149 }
150}
151
152impl Serializable for TransactionEncryptionKey {
153 fn write_into<W: ByteWriter>(&self, target: &mut W) {
154 target.write_u32(self.scheme);
155 target.write_usize(self.key_id.len());
156 target.write_bytes(&self.key_id);
157 self.public_key.write_into(target);
158 self.genesis_commitment.write_into(target);
159 }
160}
161
162impl Deserializable for TransactionEncryptionKey {
163 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
164 let scheme = source.read_u32()?;
165 let key_id_len = source.read_usize()?;
166 validate_key_metadata(scheme, key_id_len).map_err(DeserializationError::InvalidValue)?;
167
168 let key_id = source.read_vec(key_id_len)?;
169 let public_key = PublicKey::read_from(source)?;
170 let genesis_commitment = Word::read_from(source)?;
171
172 Ok(Self {
173 scheme,
174 key_id,
175 public_key,
176 genesis_commitment,
177 })
178 }
179}
180
181#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct NextTransactionEncryptionKey {
190 pub scheme: u32,
192 pub key_id: Vec<u8>,
194 pub public_key: Vec<u8>,
196 pub rotation_block_num: BlockNumber,
198}
199
200#[derive(Clone, Debug, PartialEq, Eq)]
206pub struct ValidatorAttestation {
207 pub validator_key: ValidatorPublicKey,
209 pub signature: ValidatorSignature,
211}
212
213#[derive(Clone, Debug, PartialEq, Eq)]
222pub struct AttestedTransactionEncryptionKey {
223 pub scheme: u32,
225 pub key_id: Vec<u8>,
227 pub public_key: Vec<u8>,
229 pub attestations: Vec<ValidatorAttestation>,
231 pub next_key: Option<NextTransactionEncryptionKey>,
233}
234
235impl AttestedTransactionEncryptionKey {
236 pub fn verify(
247 self,
248 genesis_commitment: Word,
249 validator_keys: &ValidatorConfig,
250 ) -> Result<TransactionEncryptionKey, RpcError> {
251 validate_key_metadata(self.scheme, self.key_id.len())
252 .map_err(RpcError::TransactionEncryptionKeyRejected)?;
253 if let Some(next) = &self.next_key {
254 validate_key_id_len(next.key_id.len(), "next encryption key id")
255 .map_err(RpcError::TransactionEncryptionKeyRejected)?;
256 }
257
258 let commitment = attestation_commitment(
259 self.scheme,
260 &self.key_id,
261 genesis_commitment,
262 &self.public_key,
263 self.next_key.as_ref(),
264 );
265
266 let recognized = validator_keys.keys();
267 let attested = self.attestations.iter().any(|attestation| {
268 recognized.contains(&attestation.validator_key)
269 && attestation.validator_key.verify(commitment, &attestation.signature)
270 });
271 if !attested {
272 return Err(RpcError::TransactionEncryptionKeyRejected(
273 "no attestation from a chain-recognized validator verifies against the key".into(),
274 ));
275 }
276
277 let public_key = PublicKey::read_from_bytes(&self.public_key)
280 .map_err(|err| RpcError::TransactionEncryptionKeyRejected(err.to_string()))?;
281
282 Ok(TransactionEncryptionKey {
283 scheme: self.scheme,
284 key_id: self.key_id,
285 public_key,
286 genesis_commitment,
287 })
288 }
289}
290
291fn validate_key_metadata(scheme: u32, key_id_len: usize) -> Result<(), String> {
292 if scheme != SUPPORTED_SCHEME {
293 return Err(format!("unsupported IES scheme '{scheme}'"));
294 }
295
296 validate_key_id_len(key_id_len, "encryption key id")
297}
298
299fn validate_key_id_len(key_id_len: usize, field: &str) -> Result<(), String> {
300 if key_id_len == 0 {
301 return Err(format!("{field} is empty"));
302 }
303 if key_id_len > MAX_KEY_ID_LEN {
304 return Err(format!(
305 "{field} is {key_id_len} bytes, which exceeds the maximum of {MAX_KEY_ID_LEN}"
306 ));
307 }
308 Ok(())
309}
310
311pub fn attestation_commitment(
322 scheme: u32,
323 key_id: &[u8],
324 genesis_commitment: Word,
325 public_key: &[u8],
326 next_key: Option<&NextTransactionEncryptionKey>,
327) -> Word {
328 let mut payload = Vec::new();
329 payload.extend_from_slice(ATTESTATION_DOMAIN);
330 payload.extend_from_slice(&scheme.to_le_bytes());
331 extend_with_length_prefixed(&mut payload, key_id);
332 payload.extend_from_slice(&genesis_commitment.to_bytes());
333 extend_with_length_prefixed(&mut payload, public_key);
334 if let Some(next) = next_key {
335 payload.extend_from_slice(&next.scheme.to_le_bytes());
336 extend_with_length_prefixed(&mut payload, &next.key_id);
337 extend_with_length_prefixed(&mut payload, &next.public_key);
338 payload.extend_from_slice(&next.rotation_block_num.as_u32().to_le_bytes());
339 }
340
341 Hasher::hash(&payload)
342}
343
344fn extend_with_length_prefixed(payload: &mut Vec<u8>, field: &[u8]) {
349 let len = u32::try_from(field.len()).unwrap_or(u32::MAX);
350 payload.extend_from_slice(&len.to_le_bytes());
351 payload.extend_from_slice(field);
352}
353
354fn transaction_inputs_associated_data(
379 scheme: u32,
380 key_id: &[u8],
381 genesis_commitment: Word,
382 tx_id: TransactionId,
383) -> Vec<u8> {
384 let genesis_commitment = genesis_commitment.to_bytes();
385 let tx_id = tx_id.as_word().to_bytes();
386 let mut transcript = Vec::with_capacity(
387 TX_INPUT_SEAL_DOMAIN.len()
388 + 2 * size_of::<u32>()
389 + key_id.len()
390 + genesis_commitment.len()
391 + tx_id.len(),
392 );
393 transcript.extend_from_slice(TX_INPUT_SEAL_DOMAIN);
394 transcript.extend_from_slice(&scheme.to_le_bytes());
395 extend_with_length_prefixed(&mut transcript, key_id);
396 transcript.extend_from_slice(&genesis_commitment);
397 transcript.extend_from_slice(&tx_id);
398
399 transcript
400}
401
402#[derive(Clone, Debug, PartialEq, Eq)]
411pub struct SealedTransactionInputs {
412 key_id: Vec<u8>,
413 ciphertext: Vec<u8>,
414}
415
416impl SealedTransactionInputs {
417 pub fn key_id(&self) -> &[u8] {
419 &self.key_id
420 }
421
422 pub fn ciphertext(&self) -> &[u8] {
424 &self.ciphertext
425 }
426}
427
428impl From<SealedTransactionInputs> for proto::submission::SealedTransactionInputs {
429 fn from(sealed: SealedTransactionInputs) -> Self {
430 Self {
431 key_id: sealed.key_id,
432 ciphertext: sealed.ciphertext,
433 }
434 }
435}
436
437pub fn seal_transaction_inputs<R: CryptoRng>(
446 rng: &mut R,
447 key: &TransactionEncryptionKey,
448 tx_id: TransactionId,
449 transaction_inputs: &TransactionInputs,
450) -> Result<SealedTransactionInputs, RpcError> {
451 let associated_data = key.transaction_inputs_associated_data(tx_id);
452 let sealed = key
453 .sealing_key()
454 .seal_bytes_with_associated_data(rng, &transaction_inputs.to_bytes(), &associated_data)
455 .map_err(|err| RpcError::TransactionInputsSealingFailed(err.to_string()))?;
456
457 Ok(SealedTransactionInputs {
458 key_id: key.key_id().to_vec(),
459 ciphertext: sealed.to_bytes(),
460 })
461}
462
463#[cfg(test)]
467mod tests {
468 use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey as ValidatorSigningKey;
469 use miden_protocol::crypto::dsa::eddsa_25519_sha512::KeyExchangeKey;
470 use miden_protocol::crypto::ies::{SealedMessage, UnsealingKey};
471 use rand::SeedableRng;
472 use rand_chacha::ChaCha20Rng;
473
474 use super::*;
475
476 const TEST_KEY_ID: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
477
478 #[test]
479 fn transaction_encryption_key_deserialization_validates_metadata() {
480 let (key, _) = key_pair();
481 assert_eq!(TransactionEncryptionKey::read_from_bytes(&key.to_bytes()).unwrap(), key);
482
483 let invalid_keys = [
484 TransactionEncryptionKey {
485 scheme: SUPPORTED_SCHEME + 1,
486 ..key.clone()
487 },
488 TransactionEncryptionKey { key_id: Vec::new(), ..key.clone() },
489 TransactionEncryptionKey {
490 key_id: vec![0; MAX_KEY_ID_LEN + 1],
491 ..key
492 },
493 ];
494 for key in invalid_keys {
495 assert!(matches!(
496 TransactionEncryptionKey::read_from_bytes(&key.to_bytes()),
497 Err(DeserializationError::InvalidValue(_))
498 ));
499 }
500 }
501
502 fn rng() -> ChaCha20Rng {
503 ChaCha20Rng::seed_from_u64(0xface)
504 }
505
506 fn genesis() -> Word {
507 Word::from([1u32, 2, 3, 4])
508 }
509
510 fn tx_id(seed: u32) -> TransactionId {
511 TransactionId::new(
512 Word::from([seed, 0, 0, 0]),
513 Word::from([0, seed, 0, 0]),
514 Word::from([0, 0, seed, 0]),
515 Word::from([0, 0, 0, seed]),
516 )
517 }
518
519 fn key_pair() -> (TransactionEncryptionKey, UnsealingKey) {
522 let secret_key = KeyExchangeKey::with_rng(&mut rng());
523 let key = TransactionEncryptionKey {
524 scheme: SUPPORTED_SCHEME,
525 key_id: TEST_KEY_ID.to_vec(),
526 public_key: secret_key.public_key(),
527 genesis_commitment: genesis(),
528 };
529
530 (key, UnsealingKey::X25519XChaCha20Poly1305(secret_key))
531 }
532
533 fn unseal(
536 unsealing_key: &UnsealingKey,
537 sealed: &SealedTransactionInputs,
538 key: &TransactionEncryptionKey,
539 tx_id: TransactionId,
540 ) -> Result<Vec<u8>, ()> {
541 let associated_data = key.transaction_inputs_associated_data(tx_id);
542
543 unsealing_key
544 .unseal_bytes_with_associated_data(
545 SealedMessage::read_from_bytes(sealed.ciphertext()).unwrap(),
546 &associated_data,
547 )
548 .map_err(|_| ())
549 }
550
551 fn seal(key: &TransactionEncryptionKey, tx_id: TransactionId) -> SealedTransactionInputs {
552 let associated_data = key.transaction_inputs_associated_data(tx_id);
553 let sealed = key
554 .sealing_key()
555 .seal_bytes_with_associated_data(&mut rng(), b"transaction inputs", &associated_data)
556 .unwrap();
557
558 SealedTransactionInputs {
559 key_id: key.key_id().to_vec(),
560 ciphertext: sealed.to_bytes(),
561 }
562 }
563
564 #[test]
574 fn associated_data_matches_the_validator_transcript() {
575 let associated_data =
576 transaction_inputs_associated_data(1, &TEST_KEY_ID, genesis(), tx_id(10));
577
578 let mut expected = Vec::new();
579 expected.extend_from_slice(b"MIDEN_TX_INPUT_SEAL_V1");
580 expected.extend_from_slice(&1u32.to_le_bytes());
581 expected.extend_from_slice(&4u32.to_le_bytes());
582 expected.extend_from_slice(&TEST_KEY_ID);
583 expected.extend_from_slice(&genesis().to_bytes());
584 expected.extend_from_slice(&tx_id(10).as_word().to_bytes());
585
586 assert_eq!(associated_data, expected);
587 assert_eq!(associated_data.len(), 98);
589 }
590
591 #[test]
595 fn sealed_inputs_round_trip() {
596 let (key, unsealing_key) = key_pair();
597 let sealed = seal(&key, tx_id(10));
598
599 assert_eq!(sealed.key_id(), key.key_id());
600 let opened = unseal(&unsealing_key, &sealed, &key, tx_id(10)).unwrap();
601 assert_eq!(opened, b"transaction inputs");
602 }
603
604 #[test]
607 fn unsealing_rejects_a_different_transaction() {
608 let (key, unsealing_key) = key_pair();
609 let sealed = seal(&key, tx_id(10));
610
611 assert!(unseal(&unsealing_key, &sealed, &key, tx_id(11)).is_err());
612 }
613
614 #[test]
617 fn unsealing_rejects_a_different_key_id() {
618 let (key, unsealing_key) = key_pair();
619 let sealed = seal(&key, tx_id(10));
620
621 let rotated = TransactionEncryptionKey { key_id: b"other".to_vec(), ..key };
622 assert!(unseal(&unsealing_key, &sealed, &rotated, tx_id(10)).is_err());
623 }
624
625 #[test]
629 fn sealing_the_same_inputs_twice_yields_different_ciphertexts() {
630 let (key, unsealing_key) = key_pair();
631 let associated_data = key.transaction_inputs_associated_data(tx_id(10));
632 let mut rng = rng();
633 let mut seal_once = || {
634 key.sealing_key()
635 .seal_bytes_with_associated_data(&mut rng, b"transaction inputs", &associated_data)
636 .unwrap()
637 .to_bytes()
638 };
639
640 let first = seal_once();
641 let second = seal_once();
642
643 assert_ne!(first, second);
644 for ciphertext in [first, second] {
645 let sealed = SealedTransactionInputs {
646 key_id: key.key_id().to_vec(),
647 ciphertext,
648 };
649 assert_eq!(
650 unseal(&unsealing_key, &sealed, &key, tx_id(10)).unwrap(),
651 b"transaction inputs"
652 );
653 }
654 }
655
656 fn attested(
661 key: &TransactionEncryptionKey,
662 signer: &ValidatorSigningKey,
663 genesis_commitment: Word,
664 ) -> AttestedTransactionEncryptionKey {
665 attested_with_next(key, signer, genesis_commitment, None)
666 }
667
668 fn attested_with_next(
671 key: &TransactionEncryptionKey,
672 signer: &ValidatorSigningKey,
673 genesis_commitment: Word,
674 next_key: Option<NextTransactionEncryptionKey>,
675 ) -> AttestedTransactionEncryptionKey {
676 let public_key = key.public_key().to_bytes();
677 let commitment = attestation_commitment(
678 SUPPORTED_SCHEME,
679 key.key_id(),
680 genesis_commitment,
681 &public_key,
682 next_key.as_ref(),
683 );
684
685 AttestedTransactionEncryptionKey {
686 scheme: SUPPORTED_SCHEME,
687 key_id: key.key_id().to_vec(),
688 public_key,
689 attestations: vec![ValidatorAttestation {
690 validator_key: signer.public_key(),
691 signature: signer.sign(commitment),
692 }],
693 next_key,
694 }
695 }
696
697 #[test]
698 fn verify_accepts_an_attestation_from_a_recognized_validator() {
699 let (key, _) = key_pair();
700 let signer = ValidatorSigningKey::with_rng(&mut rng());
701 let validator_keys = ValidatorConfig::new(vec![signer.public_key()], 1).unwrap();
702
703 let verified =
704 attested(&key, &signer, genesis()).verify(genesis(), &validator_keys).unwrap();
705
706 assert_eq!(verified, key);
707 }
708
709 #[test]
710 fn verify_rejects_a_validator_absent_from_the_committed_set() {
711 let (key, _) = key_pair();
712 let impostor = ValidatorSigningKey::with_rng(&mut rng());
713 let committed = ValidatorSigningKey::with_rng(&mut ChaCha20Rng::seed_from_u64(7));
714 let validator_keys = ValidatorConfig::new(vec![committed.public_key()], 1).unwrap();
715
716 assert!(attested(&key, &impostor, genesis()).verify(genesis(), &validator_keys).is_err());
717 }
718
719 #[test]
722 fn verify_rejects_a_substituted_public_key() {
723 let (key, _) = key_pair();
724 let signer = ValidatorSigningKey::with_rng(&mut rng());
725 let validator_keys = ValidatorConfig::new(vec![signer.public_key()], 1).unwrap();
726
727 let substitute = KeyExchangeKey::with_rng(&mut ChaCha20Rng::seed_from_u64(99));
728 let mut response = attested(&key, &signer, genesis());
729 response.public_key = substitute.public_key().to_bytes();
730
731 assert!(response.verify(genesis(), &validator_keys).is_err());
732 }
733
734 #[test]
737 fn verify_rejects_an_attestation_from_another_network() {
738 let (key, _) = key_pair();
739 let signer = ValidatorSigningKey::with_rng(&mut rng());
740 let validator_keys = ValidatorConfig::new(vec![signer.public_key()], 1).unwrap();
741
742 let response = attested(&key, &signer, genesis());
743
744 assert!(response.verify(Word::from([9u32, 9, 9, 9]), &validator_keys).is_err());
745 }
746
747 #[test]
750 fn verify_rejects_an_injected_next_key() {
751 let (key, _) = key_pair();
752 let signer = ValidatorSigningKey::with_rng(&mut rng());
753 let validator_keys = ValidatorConfig::new(vec![signer.public_key()], 1).unwrap();
754
755 let mut response = attested(&key, &signer, genesis());
756 response.next_key = Some(NextTransactionEncryptionKey {
757 scheme: SUPPORTED_SCHEME,
758 key_id: vec![1, 2, 3, 4],
759 public_key: KeyExchangeKey::with_rng(&mut ChaCha20Rng::seed_from_u64(11))
760 .public_key()
761 .to_bytes(),
762 rotation_block_num: 100.into(),
763 });
764
765 assert!(response.verify(genesis(), &validator_keys).is_err());
766 }
767
768 #[test]
769 fn verify_rejects_an_unsupported_scheme() {
770 let (key, _) = key_pair();
771 let signer = ValidatorSigningKey::with_rng(&mut rng());
772 let validator_keys = ValidatorConfig::new(vec![signer.public_key()], 1).unwrap();
773
774 let mut response = attested(&key, &signer, genesis());
775 response.scheme = SUPPORTED_SCHEME + 1;
776
777 assert!(response.verify(genesis(), &validator_keys).is_err());
778 }
779
780 #[test]
790 fn attestation_commitment_matches_the_validator_implementation() {
791 let genesis = Word::from([101u32, 102, 103, 104]);
792
793 let no_rotation =
794 attestation_commitment(1, b"golden-key-id", genesis, b"golden-public-key", None);
795 assert_eq!(
796 no_rotation.to_hex(),
797 "0x245d1f2d45d4a60d9edd4576691244d6b9ee16fe67635425dc685cd54918a970"
798 );
799
800 let next = NextTransactionEncryptionKey {
801 scheme: 2,
802 key_id: b"next-key-id".to_vec(),
803 public_key: b"next-public-key".to_vec(),
804 rotation_block_num: BlockNumber::from(7u32),
805 };
806 let with_rotation =
807 attestation_commitment(1, b"golden-key-id", genesis, b"golden-public-key", Some(&next));
808 assert_eq!(
809 with_rotation.to_hex(),
810 "0xddfd7907b6a1ea6f294809ff0ed775f270b649ca15b21f88127c8335945e4752"
811 );
812 }
813}