1use alloc::string::ToString;
34use alloc::vec::Vec;
35
36use miden_protocol::block::{BlockNumber, ValidatorKeys};
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::transaction::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 let key_id = source.read_vec(key_id_len)?;
167 let public_key = PublicKey::read_from(source)?;
168 let genesis_commitment = Word::read_from(source)?;
169
170 Ok(Self {
171 scheme,
172 key_id,
173 public_key,
174 genesis_commitment,
175 })
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
187pub struct NextTransactionEncryptionKey {
188 pub scheme: u32,
190 pub key_id: Vec<u8>,
192 pub public_key: Vec<u8>,
194 pub rotation_block_num: BlockNumber,
196}
197
198#[derive(Clone, Debug, PartialEq, Eq)]
204pub struct ValidatorAttestation {
205 pub validator_key: ValidatorPublicKey,
207 pub signature: ValidatorSignature,
209}
210
211#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct AttestedTransactionEncryptionKey {
221 pub scheme: u32,
223 pub key_id: Vec<u8>,
225 pub public_key: Vec<u8>,
227 pub attestations: Vec<ValidatorAttestation>,
229 pub next_key: Option<NextTransactionEncryptionKey>,
231}
232
233impl AttestedTransactionEncryptionKey {
234 pub fn verify(
245 self,
246 genesis_commitment: Word,
247 validator_keys: &ValidatorKeys,
248 ) -> Result<TransactionEncryptionKey, RpcError> {
249 if self.scheme != SUPPORTED_SCHEME {
250 return Err(RpcError::TransactionEncryptionKeyRejected(format!(
251 "unsupported IES scheme '{}'",
252 self.scheme
253 )));
254 }
255
256 validate_key_id(&self.key_id, "encryption key id")?;
257 if let Some(next) = &self.next_key {
258 validate_key_id(&next.key_id, "next encryption key id")?;
259 }
260
261 let commitment = attestation_commitment(
262 self.scheme,
263 &self.key_id,
264 genesis_commitment,
265 &self.public_key,
266 self.next_key.as_ref(),
267 );
268
269 let recognized = validator_keys.as_keys();
270 let attested = self.attestations.iter().any(|attestation| {
271 recognized.contains(&attestation.validator_key)
272 && attestation.validator_key.verify(commitment, &attestation.signature)
273 });
274 if !attested {
275 return Err(RpcError::TransactionEncryptionKeyRejected(
276 "no attestation from a chain-recognized validator verifies against the key".into(),
277 ));
278 }
279
280 let public_key = PublicKey::read_from_bytes(&self.public_key)
283 .map_err(|err| RpcError::TransactionEncryptionKeyRejected(err.to_string()))?;
284
285 Ok(TransactionEncryptionKey {
286 scheme: self.scheme,
287 key_id: self.key_id,
288 public_key,
289 genesis_commitment,
290 })
291 }
292}
293
294fn validate_key_id(key_id: &[u8], field: &str) -> Result<(), RpcError> {
299 if key_id.is_empty() {
300 return Err(RpcError::TransactionEncryptionKeyRejected(format!("{field} is empty")));
301 }
302 if key_id.len() > MAX_KEY_ID_LEN {
303 return Err(RpcError::TransactionEncryptionKeyRejected(format!(
304 "{field} is {} bytes, which exceeds the maximum of {MAX_KEY_ID_LEN}",
305 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::transaction::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 fn rng() -> ChaCha20Rng {
479 ChaCha20Rng::seed_from_u64(0xface)
480 }
481
482 fn genesis() -> Word {
483 Word::from([1u32, 2, 3, 4])
484 }
485
486 fn tx_id(seed: u32) -> TransactionId {
487 TransactionId::new(
488 Word::from([seed, 0, 0, 0]),
489 Word::from([0, seed, 0, 0]),
490 Word::from([0, 0, seed, 0]),
491 Word::from([0, 0, 0, seed]),
492 )
493 }
494
495 fn key_pair() -> (TransactionEncryptionKey, UnsealingKey) {
498 let secret_key = KeyExchangeKey::with_rng(&mut rng());
499 let key = TransactionEncryptionKey {
500 scheme: SUPPORTED_SCHEME,
501 key_id: TEST_KEY_ID.to_vec(),
502 public_key: secret_key.public_key(),
503 genesis_commitment: genesis(),
504 };
505
506 (key, UnsealingKey::X25519XChaCha20Poly1305(secret_key))
507 }
508
509 fn unseal(
512 unsealing_key: &UnsealingKey,
513 sealed: &SealedTransactionInputs,
514 key: &TransactionEncryptionKey,
515 tx_id: TransactionId,
516 ) -> Result<Vec<u8>, ()> {
517 let associated_data = key.transaction_inputs_associated_data(tx_id);
518
519 unsealing_key
520 .unseal_bytes_with_associated_data(
521 SealedMessage::read_from_bytes(sealed.ciphertext()).unwrap(),
522 &associated_data,
523 )
524 .map_err(|_| ())
525 }
526
527 fn seal(key: &TransactionEncryptionKey, tx_id: TransactionId) -> SealedTransactionInputs {
528 let associated_data = key.transaction_inputs_associated_data(tx_id);
529 let sealed = key
530 .sealing_key()
531 .seal_bytes_with_associated_data(&mut rng(), b"transaction inputs", &associated_data)
532 .unwrap();
533
534 SealedTransactionInputs {
535 key_id: key.key_id().to_vec(),
536 ciphertext: sealed.to_bytes(),
537 }
538 }
539
540 #[test]
550 fn associated_data_matches_the_validator_transcript() {
551 let associated_data =
552 transaction_inputs_associated_data(1, &TEST_KEY_ID, genesis(), tx_id(10));
553
554 let mut expected = Vec::new();
555 expected.extend_from_slice(b"MIDEN_TX_INPUT_SEAL_V1");
556 expected.extend_from_slice(&1u32.to_le_bytes());
557 expected.extend_from_slice(&4u32.to_le_bytes());
558 expected.extend_from_slice(&TEST_KEY_ID);
559 expected.extend_from_slice(&genesis().to_bytes());
560 expected.extend_from_slice(&tx_id(10).as_word().to_bytes());
561
562 assert_eq!(associated_data, expected);
563 assert_eq!(associated_data.len(), 98);
565 }
566
567 #[test]
571 fn sealed_inputs_round_trip() {
572 let (key, unsealing_key) = key_pair();
573 let sealed = seal(&key, tx_id(10));
574
575 assert_eq!(sealed.key_id(), key.key_id());
576 let opened = unseal(&unsealing_key, &sealed, &key, tx_id(10)).unwrap();
577 assert_eq!(opened, b"transaction inputs");
578 }
579
580 #[test]
583 fn unsealing_rejects_a_different_transaction() {
584 let (key, unsealing_key) = key_pair();
585 let sealed = seal(&key, tx_id(10));
586
587 assert!(unseal(&unsealing_key, &sealed, &key, tx_id(11)).is_err());
588 }
589
590 #[test]
593 fn unsealing_rejects_a_different_key_id() {
594 let (key, unsealing_key) = key_pair();
595 let sealed = seal(&key, tx_id(10));
596
597 let rotated = TransactionEncryptionKey { key_id: b"other".to_vec(), ..key };
598 assert!(unseal(&unsealing_key, &sealed, &rotated, tx_id(10)).is_err());
599 }
600
601 #[test]
605 fn sealing_the_same_inputs_twice_yields_different_ciphertexts() {
606 let (key, unsealing_key) = key_pair();
607 let associated_data = key.transaction_inputs_associated_data(tx_id(10));
608 let mut rng = rng();
609 let mut seal_once = || {
610 key.sealing_key()
611 .seal_bytes_with_associated_data(&mut rng, b"transaction inputs", &associated_data)
612 .unwrap()
613 .to_bytes()
614 };
615
616 let first = seal_once();
617 let second = seal_once();
618
619 assert_ne!(first, second);
620 for ciphertext in [first, second] {
621 let sealed = SealedTransactionInputs {
622 key_id: key.key_id().to_vec(),
623 ciphertext,
624 };
625 assert_eq!(
626 unseal(&unsealing_key, &sealed, &key, tx_id(10)).unwrap(),
627 b"transaction inputs"
628 );
629 }
630 }
631
632 fn attested(
637 key: &TransactionEncryptionKey,
638 signer: &ValidatorSigningKey,
639 genesis_commitment: Word,
640 ) -> AttestedTransactionEncryptionKey {
641 attested_with_next(key, signer, genesis_commitment, None)
642 }
643
644 fn attested_with_next(
647 key: &TransactionEncryptionKey,
648 signer: &ValidatorSigningKey,
649 genesis_commitment: Word,
650 next_key: Option<NextTransactionEncryptionKey>,
651 ) -> AttestedTransactionEncryptionKey {
652 let public_key = key.public_key().to_bytes();
653 let commitment = attestation_commitment(
654 SUPPORTED_SCHEME,
655 key.key_id(),
656 genesis_commitment,
657 &public_key,
658 next_key.as_ref(),
659 );
660
661 AttestedTransactionEncryptionKey {
662 scheme: SUPPORTED_SCHEME,
663 key_id: key.key_id().to_vec(),
664 public_key,
665 attestations: vec![ValidatorAttestation {
666 validator_key: signer.public_key(),
667 signature: signer.sign(commitment),
668 }],
669 next_key,
670 }
671 }
672
673 #[test]
674 fn verify_accepts_an_attestation_from_a_recognized_validator() {
675 let (key, _) = key_pair();
676 let signer = ValidatorSigningKey::with_rng(&mut rng());
677 let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
678
679 let verified =
680 attested(&key, &signer, genesis()).verify(genesis(), &validator_keys).unwrap();
681
682 assert_eq!(verified, key);
683 }
684
685 #[test]
686 fn verify_rejects_a_validator_absent_from_the_committed_set() {
687 let (key, _) = key_pair();
688 let impostor = ValidatorSigningKey::with_rng(&mut rng());
689 let committed = ValidatorSigningKey::with_rng(&mut ChaCha20Rng::seed_from_u64(7));
690 let validator_keys = ValidatorKeys::new(vec![committed.public_key()]).unwrap();
691
692 assert!(attested(&key, &impostor, genesis()).verify(genesis(), &validator_keys).is_err());
693 }
694
695 #[test]
698 fn verify_rejects_a_substituted_public_key() {
699 let (key, _) = key_pair();
700 let signer = ValidatorSigningKey::with_rng(&mut rng());
701 let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
702
703 let substitute = KeyExchangeKey::with_rng(&mut ChaCha20Rng::seed_from_u64(99));
704 let mut response = attested(&key, &signer, genesis());
705 response.public_key = substitute.public_key().to_bytes();
706
707 assert!(response.verify(genesis(), &validator_keys).is_err());
708 }
709
710 #[test]
713 fn verify_rejects_an_attestation_from_another_network() {
714 let (key, _) = key_pair();
715 let signer = ValidatorSigningKey::with_rng(&mut rng());
716 let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
717
718 let response = attested(&key, &signer, genesis());
719
720 assert!(response.verify(Word::from([9u32, 9, 9, 9]), &validator_keys).is_err());
721 }
722
723 #[test]
726 fn verify_rejects_an_injected_next_key() {
727 let (key, _) = key_pair();
728 let signer = ValidatorSigningKey::with_rng(&mut rng());
729 let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
730
731 let mut response = attested(&key, &signer, genesis());
732 response.next_key = Some(NextTransactionEncryptionKey {
733 scheme: SUPPORTED_SCHEME,
734 key_id: vec![1, 2, 3, 4],
735 public_key: KeyExchangeKey::with_rng(&mut ChaCha20Rng::seed_from_u64(11))
736 .public_key()
737 .to_bytes(),
738 rotation_block_num: 100.into(),
739 });
740
741 assert!(response.verify(genesis(), &validator_keys).is_err());
742 }
743
744 #[test]
745 fn verify_rejects_an_unsupported_scheme() {
746 let (key, _) = key_pair();
747 let signer = ValidatorSigningKey::with_rng(&mut rng());
748 let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
749
750 let mut response = attested(&key, &signer, genesis());
751 response.scheme = SUPPORTED_SCHEME + 1;
752
753 assert!(response.verify(genesis(), &validator_keys).is_err());
754 }
755
756 #[test]
766 fn attestation_commitment_matches_the_validator_implementation() {
767 let genesis = Word::from([101u32, 102, 103, 104]);
768
769 let no_rotation =
770 attestation_commitment(1, b"golden-key-id", genesis, b"golden-public-key", None);
771 assert_eq!(
772 no_rotation.to_hex(),
773 "0x245d1f2d45d4a60d9edd4576691244d6b9ee16fe67635425dc685cd54918a970"
774 );
775
776 let next = NextTransactionEncryptionKey {
777 scheme: 2,
778 key_id: b"next-key-id".to_vec(),
779 public_key: b"next-public-key".to_vec(),
780 rotation_block_num: BlockNumber::from(7u32),
781 };
782 let with_rotation =
783 attestation_commitment(1, b"golden-key-id", genesis, b"golden-public-key", Some(&next));
784 assert_eq!(
785 with_rotation.to_hex(),
786 "0xddfd7907b6a1ea6f294809ff0ed775f270b649ca15b21f88127c8335945e4752"
787 );
788 }
789}