1use miden_protobuf::{DecodeMessageExt, VerifyWith};
9use miden_protocol::Word;
10use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{
11 PublicKey as ValidatorPublicKey,
12 Signature as ValidatorSignature,
13};
14use miden_protocol::crypto::dsa::eddsa_25519_sha512::PublicKey as EncryptionPublicKey;
15use miden_protocol::crypto::ies::SealingKey;
16use miden_protocol::transaction::TransactionId;
17use miden_protocol::utils::serde::{Deserializable, Serializable};
18
19use crate::generated as proto;
20
21pub const TX_INPUT_SEAL_DOMAIN: &[u8] = b"MIDEN_TX_INPUT_SEAL_V1";
26
27pub const ATTESTATION_DOMAIN: &[u8] = b"MIDEN_TX_ENCRYPTION_KEY_ATTESTATION_V1";
29
30pub const MAX_KEY_ID_LEN: usize = 64;
36
37const SCHEME_X25519_XCHACHA20_POLY1305: u32 = 1;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[repr(u32)]
46pub enum TransactionEncryptionScheme {
47 X25519XChaCha20Poly1305 = SCHEME_X25519_XCHACHA20_POLY1305,
49}
50
51impl TransactionEncryptionScheme {
52 pub const fn as_u32(self) -> u32 {
54 self as u32
55 }
56
57 pub const fn as_i32(self) -> i32 {
59 self as i32
60 }
61}
62
63impl TryFrom<i32> for TransactionEncryptionScheme {
64 type Error = TransactionEncryptionKeyError;
65
66 fn try_from(value: i32) -> Result<Self, Self::Error> {
67 match value {
68 0 => Err(TransactionEncryptionKeyError::UnspecifiedScheme),
69 1 => Ok(Self::X25519XChaCha20Poly1305),
70 other => Err(TransactionEncryptionKeyError::UnsupportedScheme(other)),
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct NextEncryptionKeyInfo {
78 pub scheme: TransactionEncryptionScheme,
80 pub key_id: Vec<u8>,
82 pub public_key: Vec<u8>,
84 pub rotation_block_num: u32,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct TransactionEncryptionKeyInfo {
91 pub scheme: TransactionEncryptionScheme,
93 pub key_id: Vec<u8>,
95 pub public_key: Vec<u8>,
97 pub next_key: Option<NextEncryptionKeyInfo>,
99}
100
101impl TransactionEncryptionKeyInfo {
102 pub fn attestation_commitment(&self, genesis_commitment: Word) -> Word {
104 attestation_commitment(
105 self.scheme,
106 &self.key_id,
107 genesis_commitment,
108 &self.public_key,
109 self.next_key.as_ref(),
110 )
111 }
112}
113
114#[derive(Debug, Clone, Copy)]
116pub struct TrustedTransactionEncryptionState<'a> {
117 genesis_commitment: Word,
118 validator_signing_keys: &'a [ValidatorPublicKey],
119}
120
121impl<'a> TrustedTransactionEncryptionState<'a> {
122 pub const fn new(
124 genesis_commitment: Word,
125 validator_signing_keys: &'a [ValidatorPublicKey],
126 ) -> Self {
127 Self {
128 genesis_commitment,
129 validator_signing_keys,
130 }
131 }
132}
133
134#[derive(Debug, Clone)]
136pub struct VerifiedTransactionEncryptionKey {
137 info: TransactionEncryptionKeyInfo,
138 public_key: EncryptionPublicKey,
139 genesis_commitment: Word,
140}
141
142impl VerifiedTransactionEncryptionKey {
143 pub const fn info(&self) -> &TransactionEncryptionKeyInfo {
145 &self.info
146 }
147
148 pub const fn public_key(&self) -> &EncryptionPublicKey {
150 &self.public_key
151 }
152
153 pub const fn genesis_commitment(&self) -> Word {
155 self.genesis_commitment
156 }
157}
158
159pub fn transaction_inputs_associated_data(
187 scheme: u32,
188 key_id: &[u8],
189 genesis_commitment: Word,
190 tx_id: TransactionId,
191) -> Vec<u8> {
192 let genesis_commitment = genesis_commitment.to_bytes();
193 let tx_id = tx_id.as_word().to_bytes();
194 let mut transcript = Vec::with_capacity(
195 TX_INPUT_SEAL_DOMAIN.len()
196 + 2 * size_of::<u32>()
197 + key_id.len()
198 + genesis_commitment.len()
199 + tx_id.len(),
200 );
201 transcript.extend_from_slice(TX_INPUT_SEAL_DOMAIN);
202 transcript.extend_from_slice(&scheme.to_le_bytes());
203 let key_id_len = u32::try_from(key_id.len()).unwrap_or(u32::MAX);
206 transcript.extend_from_slice(&key_id_len.to_le_bytes());
207 transcript.extend_from_slice(key_id);
208 transcript.extend_from_slice(&genesis_commitment);
209 transcript.extend_from_slice(&tx_id);
210 transcript
211}
212
213#[derive(Debug, thiserror::Error)]
218pub enum TransactionEncryptionKeyError {
219 #[error("encryption key scheme is unspecified")]
220 UnspecifiedScheme,
221 #[error("unsupported encryption key scheme {0}")]
222 UnsupportedScheme(i32),
223 #[error("{field} is empty")]
224 EmptyKeyId { field: &'static str },
225 #[error("{field} is {len} bytes, which exceeds the maximum of {MAX_KEY_ID_LEN}")]
226 KeyIdTooLong { field: &'static str, len: usize },
227 #[error("invalid {field}")]
228 InvalidEncryptionPublicKey {
229 field: &'static str,
230 #[source]
231 source: miden_protocol::utils::serde::DeserializationError,
232 },
233 #[error("trusted validator signing keys are empty")]
234 NoTrustedValidatorKeys,
235 #[error("transaction encryption key has no validator attestations")]
236 NoAttestations,
237 #[error("transaction encryption key has no attestation from a trusted validator")]
238 NoTrustedAttestation,
239 #[error("trusted validator attestation does not cover the transaction encryption key")]
240 InvalidAttestation,
241}
242
243#[derive(Debug, thiserror::Error)]
245pub enum TransactionInputSealError {
246 #[error("failed to seal the transaction inputs")]
247 Seal(#[source] miden_protocol::crypto::ies::IesError),
248}
249
250impl<'a> VerifyWith<TrustedTransactionEncryptionState<'a>>
254 for proto::submission::TransactionEncryptionKey
255{
256 type Verified = VerifiedTransactionEncryptionKey;
257 type Error = TransactionEncryptionKeyError;
258
259 fn verify_with(
263 self,
264 trusted: TrustedTransactionEncryptionState<'a>,
265 ) -> Result<Self::Verified, Self::Error> {
266 let key = self;
267 if trusted.validator_signing_keys.is_empty() {
268 return Err(TransactionEncryptionKeyError::NoTrustedValidatorKeys);
269 }
270 if key.attestations.is_empty() {
271 return Err(TransactionEncryptionKeyError::NoAttestations);
272 }
273
274 let (info, public_key) = key.decode_key_info()?;
275 let commitment = info.attestation_commitment(trusted.genesis_commitment);
276 let mut found_trusted_signer = false;
277
278 for attestation in key.attestations {
279 let Some(validator_public_key) = attestation.validator_public_key else {
280 continue;
281 };
282 let Ok(validator_public_key) = validator_public_key.decode_and_verify() else {
283 continue;
284 };
285
286 if !trusted.validator_signing_keys.contains(&validator_public_key) {
287 continue;
288 }
289 found_trusted_signer = true;
290
291 let Some(signature) = attestation.signature else {
292 continue;
293 };
294 let Ok(signature): Result<ValidatorSignature, _> = signature.decode_and_verify() else {
295 continue;
296 };
297 if signature.verify(commitment, &validator_public_key) {
298 return Ok(VerifiedTransactionEncryptionKey {
299 info,
300 public_key,
301 genesis_commitment: trusted.genesis_commitment,
302 });
303 }
304 }
305
306 if found_trusted_signer {
307 Err(TransactionEncryptionKeyError::InvalidAttestation)
308 } else {
309 Err(TransactionEncryptionKeyError::NoTrustedAttestation)
310 }
311 }
312}
313
314impl proto::submission::TransactionEncryptionKey {
315 fn decode_key_info(
318 &self,
319 ) -> Result<(TransactionEncryptionKeyInfo, EncryptionPublicKey), TransactionEncryptionKeyError>
320 {
321 let scheme = TransactionEncryptionScheme::try_from(self.scheme)?;
322 validate_key_id(&self.key_id, "encryption key id")?;
323 let public_key =
324 EncryptionPublicKey::read_from_bytes(&self.public_key).map_err(|source| {
325 TransactionEncryptionKeyError::InvalidEncryptionPublicKey {
326 field: "encryption public key",
327 source,
328 }
329 })?;
330
331 let next_key = self
332 .next_key
333 .as_ref()
334 .map(|next| {
335 let scheme = TransactionEncryptionScheme::try_from(next.scheme)?;
336 validate_key_id(&next.key_id, "next encryption key id")?;
337 EncryptionPublicKey::read_from_bytes(&next.public_key).map_err(|source| {
338 TransactionEncryptionKeyError::InvalidEncryptionPublicKey {
339 field: "next encryption public key",
340 source,
341 }
342 })?;
343
344 Ok(NextEncryptionKeyInfo {
345 scheme,
346 key_id: next.key_id.clone(),
347 public_key: next.public_key.clone(),
348 rotation_block_num: next.rotation_block_num,
349 })
350 })
351 .transpose()?;
352
353 Ok((
354 TransactionEncryptionKeyInfo {
355 scheme,
356 key_id: self.key_id.clone(),
357 public_key: self.public_key.clone(),
358 next_key,
359 },
360 public_key,
361 ))
362 }
363}
364
365fn validate_key_id(
367 key_id: &[u8],
368 field: &'static str,
369) -> Result<(), TransactionEncryptionKeyError> {
370 if key_id.is_empty() {
371 return Err(TransactionEncryptionKeyError::EmptyKeyId { field });
372 }
373 if key_id.len() > MAX_KEY_ID_LEN {
374 return Err(TransactionEncryptionKeyError::KeyIdTooLong { field, len: key_id.len() });
375 }
376 Ok(())
377}
378
379fn attestation_commitment(
381 scheme: TransactionEncryptionScheme,
382 key_id: &[u8],
383 genesis_commitment: Word,
384 public_key: &[u8],
385 next_key: Option<&NextEncryptionKeyInfo>,
386) -> Word {
387 let genesis_commitment = genesis_commitment.to_bytes();
388 let next_key_size = next_key
389 .map(|next| 3 * size_of::<u32>() + next.key_id.len() + next.public_key.len())
390 .unwrap_or_default();
391 let mut payload = Vec::with_capacity(
392 ATTESTATION_DOMAIN.len()
393 + 3 * size_of::<u32>()
394 + key_id.len()
395 + genesis_commitment.len()
396 + public_key.len()
397 + next_key_size,
398 );
399 payload.extend_from_slice(ATTESTATION_DOMAIN);
400 payload.extend_from_slice(&scheme.as_u32().to_le_bytes());
401 extend_with_length_prefixed(&mut payload, key_id, "key id");
402 payload.extend_from_slice(&genesis_commitment);
403 extend_with_length_prefixed(&mut payload, public_key, "public key");
404 if let Some(next) = next_key {
405 payload.extend_from_slice(&next.scheme.as_u32().to_le_bytes());
406 extend_with_length_prefixed(&mut payload, &next.key_id, "next key id");
407 extend_with_length_prefixed(&mut payload, &next.public_key, "next public key");
408 payload.extend_from_slice(&next.rotation_block_num.to_le_bytes());
409 }
410 miden_protocol::Hasher::hash(&payload)
411}
412
413fn extend_with_length_prefixed(payload: &mut Vec<u8>, field: &[u8], name: &str) {
415 let len = u32::try_from(field.len())
416 .unwrap_or_else(|_| panic!("{name} length must fit in u32"))
417 .to_le_bytes();
418 payload.extend_from_slice(&len);
419 payload.extend_from_slice(field);
420}
421
422#[derive(Debug, Clone)]
431pub struct TransactionInputsSealer {
432 scheme: TransactionEncryptionScheme,
433 key_id: Vec<u8>,
434 sealing_key: SealingKey,
435 genesis_commitment: Word,
436}
437
438impl TransactionInputsSealer {
439 pub fn new(key: VerifiedTransactionEncryptionKey) -> Self {
441 Self {
442 scheme: key.info.scheme,
443 key_id: key.info.key_id,
444 sealing_key: SealingKey::X25519XChaCha20Poly1305(key.public_key),
445 genesis_commitment: key.genesis_commitment,
446 }
447 }
448
449 pub fn key_id(&self) -> &[u8] {
451 &self.key_id
452 }
453
454 pub fn seal(
462 &self,
463 tx_id: TransactionId,
464 transaction_inputs: &[u8],
465 ) -> Result<proto::submission::SealedTransactionInputs, TransactionInputSealError> {
466 let associated_data = transaction_inputs_associated_data(
467 self.scheme.as_u32(),
468 &self.key_id,
469 self.genesis_commitment,
470 tx_id,
471 );
472 let sealed = self
473 .sealing_key
474 .seal_bytes_with_associated_data(&mut rand::rng(), transaction_inputs, &associated_data)
475 .map_err(TransactionInputSealError::Seal)?;
476
477 Ok(proto::submission::SealedTransactionInputs {
478 key_id: self.key_id.clone(),
479 ciphertext: sealed.to_bytes(),
480 })
481 }
482}
483
484#[cfg(test)]
488mod tests {
489 use assert_matches::assert_matches;
490 use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
491 use miden_protocol::crypto::dsa::eddsa_25519_sha512::KeyExchangeKey;
492
493 use super::*;
494
495 const TEST_KEY_ID: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];
496
497 fn genesis() -> Word {
498 Word::from([1u32, 2, 3, 4])
499 }
500
501 fn tx_id(seed: u32) -> TransactionId {
502 TransactionId::new(
503 Word::from([seed, 0, 0, 0]),
504 Word::from([0, seed, 0, 0]),
505 Word::from([0, 0, seed, 0]),
506 Word::from([0, 0, 0, seed]),
507 )
508 }
509
510 fn signing_key(seed: u8) -> SigningKey {
511 SigningKey::read_from_bytes(&[seed; 32]).expect("test signing key should decode")
512 }
513
514 fn unsigned_encryption_key() -> proto::submission::TransactionEncryptionKey {
515 proto::submission::TransactionEncryptionKey {
516 scheme: TransactionEncryptionScheme::X25519XChaCha20Poly1305.as_i32(),
517 key_id: TEST_KEY_ID.to_vec(),
518 public_key: KeyExchangeKey::read_from_bytes(&[7u8; 32])
519 .unwrap()
520 .public_key()
521 .to_bytes(),
522 attestations: Vec::new(),
523 next_key: None,
524 }
525 }
526
527 fn signed_encryption_key(
528 signer: &SigningKey,
529 genesis_commitment: Word,
530 ) -> proto::submission::TransactionEncryptionKey {
531 let mut key = unsigned_encryption_key();
532 let (info, _) = key.decode_key_info().unwrap();
533 key.attestations = vec![proto::submission::ValidatorKeyAttestation {
534 validator_public_key: Some(signer.public_key().into()),
535 signature: Some(signer.sign(info.attestation_commitment(genesis_commitment)).into()),
536 }];
537 key
538 }
539
540 #[test]
542 fn verifies_trusted_validator_attestation() {
543 let signer = signing_key(1);
544 let trusted_keys = [signer.public_key()];
545 let key = signed_encryption_key(&signer, genesis());
546
547 let verified = key
548 .verify_with(TrustedTransactionEncryptionState::new(genesis(), &trusted_keys))
549 .unwrap();
550
551 assert_eq!(verified.info().key_id, TEST_KEY_ID);
552 assert_eq!(verified.info().scheme, TransactionEncryptionScheme::X25519XChaCha20Poly1305);
553 assert_eq!(verified.genesis_commitment(), genesis());
554 }
555
556 #[test]
558 fn rejects_missing_and_malformed_attestations() {
559 let signer = signing_key(1);
560 let trusted_keys = [signer.public_key()];
561 let trusted = TrustedTransactionEncryptionState::new(genesis(), &trusted_keys);
562
563 assert_matches!(
564 unsigned_encryption_key().verify_with(trusted),
565 Err(TransactionEncryptionKeyError::NoAttestations)
566 );
567
568 let mut malformed_key = signed_encryption_key(&signer, genesis());
569 malformed_key.attestations[0].validator_public_key = Some(proto::primitives::PublicKey {
570 key: Some(proto::primitives::public_key::Key::EcdsaK256Keccak(Vec::new())),
571 });
572 assert_matches!(
573 malformed_key.verify_with(trusted),
574 Err(TransactionEncryptionKeyError::NoTrustedAttestation)
575 );
576
577 let mut malformed_signature = signed_encryption_key(&signer, genesis());
578 malformed_signature.attestations[0].signature = Some(proto::primitives::Signature {
579 signature: Some(proto::primitives::signature::Signature::EcdsaK256Keccak(Vec::new())),
580 });
581 assert_matches!(
582 malformed_signature.verify_with(trusted),
583 Err(TransactionEncryptionKeyError::InvalidAttestation)
584 );
585 }
586
587 #[test]
589 fn skips_malformed_attestations() {
590 let signer = signing_key(1);
591 let trusted_keys = [signer.public_key()];
592 let mut key = signed_encryption_key(&signer, genesis());
593 key.attestations.insert(
594 0,
595 proto::submission::ValidatorKeyAttestation {
596 validator_public_key: None,
597 signature: None,
598 },
599 );
600
601 key.verify_with(TrustedTransactionEncryptionState::new(genesis(), &trusted_keys))
602 .unwrap();
603 }
604
605 #[test]
607 fn rejects_untrusted_validator_attestation() {
608 let trusted_signer = signing_key(1);
609 let untrusted_signer = signing_key(2);
610 let trusted_keys = [trusted_signer.public_key()];
611
612 assert_matches!(
613 signed_encryption_key(&untrusted_signer, genesis())
614 .verify_with(TrustedTransactionEncryptionState::new(genesis(), &trusted_keys)),
615 Err(TransactionEncryptionKeyError::NoTrustedAttestation)
616 );
617 }
618
619 #[test]
621 fn rejects_changed_attested_fields() {
622 let signer = signing_key(1);
623 let trusted_keys = [signer.public_key()];
624 let trusted = TrustedTransactionEncryptionState::new(genesis(), &trusted_keys);
625 let key = signed_encryption_key(&signer, genesis());
626
627 let mut changed_scheme = key.clone();
628 changed_scheme.scheme = 0;
629 let mut changed_key_id = key.clone();
630 changed_key_id.key_id[0] ^= 1;
631 let mut changed_public_key = key.clone();
632 changed_public_key.public_key =
633 KeyExchangeKey::read_from_bytes(&[8u8; 32]).unwrap().public_key().to_bytes();
634 let mut injected_next_key = key.clone();
635 injected_next_key.next_key = Some(proto::submission::NextTransactionEncryptionKey {
636 scheme: key.scheme,
637 key_id: vec![1, 2, 3, 4],
638 public_key: KeyExchangeKey::read_from_bytes(&[9u8; 32])
639 .unwrap()
640 .public_key()
641 .to_bytes(),
642 rotation_block_num: 100,
643 });
644
645 for changed in [changed_scheme, changed_key_id, changed_public_key, injected_next_key] {
646 assert!(changed.verify_with(trusted).is_err());
647 }
648
649 assert_matches!(
650 key.verify_with(TrustedTransactionEncryptionState::new(
651 Word::from([9u32, 9, 9, 9]),
652 &trusted_keys
653 )),
654 Err(TransactionEncryptionKeyError::InvalidAttestation)
655 );
656 }
657
658 #[test]
660 fn rejects_invalid_key_metadata() {
661 let signer = signing_key(1);
662 let trusted_keys = [signer.public_key()];
663 let trusted = TrustedTransactionEncryptionState::new(genesis(), &trusted_keys);
664
665 let mut empty_key_id = signed_encryption_key(&signer, genesis());
666 empty_key_id.key_id.clear();
667 assert_matches!(
668 empty_key_id.verify_with(trusted),
669 Err(TransactionEncryptionKeyError::EmptyKeyId { .. })
670 );
671
672 let mut oversized_key_id = signed_encryption_key(&signer, genesis());
673 oversized_key_id.key_id = vec![0; MAX_KEY_ID_LEN + 1];
674 assert_matches!(
675 oversized_key_id.verify_with(trusted),
676 Err(TransactionEncryptionKeyError::KeyIdTooLong { .. })
677 );
678
679 let mut invalid_public_key = signed_encryption_key(&signer, genesis());
680 invalid_public_key.public_key.clear();
681 assert_matches!(
682 invalid_public_key.verify_with(trusted),
683 Err(TransactionEncryptionKeyError::InvalidEncryptionPublicKey { .. })
684 );
685 }
686
687 #[test]
693 fn associated_data_is_stable() {
694 let ad = transaction_inputs_associated_data(1, &TEST_KEY_ID, genesis(), tx_id(10));
695
696 let mut expected = Vec::new();
697 expected.extend_from_slice(b"MIDEN_TX_INPUT_SEAL_V1");
698 expected.extend_from_slice(&1u32.to_le_bytes());
699 expected.extend_from_slice(&4u32.to_le_bytes());
700 expected.extend_from_slice(&TEST_KEY_ID);
701 expected.extend_from_slice(&genesis().to_bytes());
702 expected.extend_from_slice(&tx_id(10).as_word().to_bytes());
703
704 assert_eq!(ad, expected);
705 assert_eq!(ad.len(), 98);
707 }
708}