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