Skip to main content

miden_node_proto/domain/
encryption.rs

1//! Sealing of transaction inputs against the validator set's shared encryption key.
2//!
3//! This module is the single definition of the associated-data transcript, so the sealing side
4//! (clients and the node's own submitters) and the unsealing side (the validator) cannot drift.
5//! A drift would not fail to compile: it would reject every submission at runtime with an opaque
6//! AEAD error, so the transcript is pinned by a golden vector in the tests below.
7
8use 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
20/// Domain tag prefixed to the associated data of sealed transaction inputs.
21///
22/// Separates this transcript from every other use of the same key material, in particular from the
23/// key attestation signed with the validator's signing key.
24pub const TX_INPUT_SEAL_DOMAIN: &[u8] = b"MIDEN_TX_INPUT_SEAL_V1";
25
26/// Domain tag prefixed to the validator-signed encryption key payload.
27pub const ATTESTATION_DOMAIN: &[u8] = b"MIDEN_TX_ENCRYPTION_KEY_ATTESTATION_V1";
28
29/// Upper bound on the length of an encryption key identifier.
30///
31/// Key identifiers are 4 bytes today (the leading bytes of the public key commitment). The bound
32/// exists so that a hostile or misconfigured key endpoint cannot drive an unbounded allocation, and
33/// so that the length cast in the transcript cannot overflow.
34pub const MAX_KEY_ID_LEN: usize = 64;
35
36/// Wire identifier of the only IES scheme the node currently supports.
37const SCHEME_X25519_XCHACHA20_POLY1305: u32 = 1;
38
39// ENCRYPTION KEY
40// ================================================================================================
41
42/// Encryption schemes supported by transaction input submission.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[repr(u32)]
45pub enum TransactionEncryptionScheme {
46    /// X25519 key agreement with XChaCha20-Poly1305 authenticated encryption.
47    X25519XChaCha20Poly1305 = SCHEME_X25519_XCHACHA20_POLY1305,
48}
49
50impl TransactionEncryptionScheme {
51    /// Returns the integer used for this scheme on the wire and in signed transcripts.
52    pub const fn as_u32(self) -> u32 {
53        self as u32
54    }
55
56    /// Returns the protobuf enum value for this scheme.
57    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/// Public metadata for a scheduled transaction encryption key.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct NextEncryptionKeyInfo {
77    /// Encryption scheme for the scheduled key.
78    pub scheme: TransactionEncryptionScheme,
79    /// Opaque identifier of the scheduled key.
80    pub key_id: Vec<u8>,
81    /// Encoded public key.
82    pub public_key: Vec<u8>,
83    /// Block at which the scheduled key becomes current.
84    pub rotation_block_num: u32,
85}
86
87/// Public metadata for the transaction encryption key served by a validator.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct TransactionEncryptionKeyInfo {
90    /// Encryption scheme for the current key.
91    pub scheme: TransactionEncryptionScheme,
92    /// Opaque identifier of the current key.
93    pub key_id: Vec<u8>,
94    /// Encoded public key.
95    pub public_key: Vec<u8>,
96    /// Scheduled replacement key, when one exists.
97    pub next_key: Option<NextEncryptionKeyInfo>,
98}
99
100impl TransactionEncryptionKeyInfo {
101    /// Returns the commitment a validator signs to attest this key for one network.
102    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/// Trusted chain state used to verify a served transaction encryption key.
114#[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    /// Creates trusted state from a genesis commitment and its validator signing keys.
122    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/// A transaction encryption key whose attestation matches trusted chain state.
134#[derive(Debug, Clone)]
135pub struct VerifiedTransactionEncryptionKey {
136    info: TransactionEncryptionKeyInfo,
137    public_key: EncryptionPublicKey,
138    genesis_commitment: Word,
139}
140
141impl VerifiedTransactionEncryptionKey {
142    /// Returns the verified key metadata.
143    pub const fn info(&self) -> &TransactionEncryptionKeyInfo {
144        &self.info
145    }
146
147    /// Returns the decoded encryption public key.
148    pub const fn public_key(&self) -> &EncryptionPublicKey {
149        &self.public_key
150    }
151
152    /// Returns the network genesis commitment covered by the attestation.
153    pub const fn genesis_commitment(&self) -> Word {
154        self.genesis_commitment
155    }
156}
157
158// ASSOCIATED DATA
159// ================================================================================================
160
161/// Builds the associated data authenticating a sealed set of transaction inputs.
162///
163/// This is the single definition of the transcript. Both sides derive it independently and it is
164/// never transmitted, so a mismatch surfaces as an authentication failure rather than as accepted
165/// but unauthenticated data.
166///
167/// The layout is `TX_INPUT_SEAL_DOMAIN || scheme || len(key_id) || key_id || genesis_commitment ||
168/// transaction_id`, where the scheme and the length prefix are 4 bytes little-endian. The domain tag
169/// is a fixed-width constant, `scheme` is fixed-width, `key_id` is length-prefixed and the two
170/// trailing fields are a fixed 32 bytes each, so no two distinct inputs produce the same transcript.
171///
172/// Each binding serves a purpose:
173/// - `scheme` and `key_id` tie the blob to one key, so inputs sealed against a retired key fail to
174///   authenticate rather than silently decrypting.
175/// - `genesis_commitment` ties the blob to one network. This matters in practice because every
176///   development stack shares the same insecure default key, so without it a blob captured on one
177///   network would replay onto another.
178/// - `transaction_id` ties the blob to one transaction, so a captured blob cannot be replayed onto a
179///   different transaction.
180///
181/// Deliberately absent is the serialized transaction. The RPC rebuilds `ProvenTransaction` with
182/// output-note decorators stripped before forwarding a submission, so binding those bytes would
183/// reject every relayed transaction. The transaction id is invariant under that rebuild, which is
184/// why it is bound instead.
185pub 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    // Callers bound `key_id` to MAX_KEY_ID_LEN, so this cast cannot realistically fail. Saturate
203    // rather than panic anyway: this runs inside a request handler on the validator.
204    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// ERRORS
213// ================================================================================================
214
215/// Failure to decode or verify a served transaction encryption key.
216#[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/// Failure to seal transaction inputs.
243#[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
249// ATTESTATION
250// ================================================================================================
251
252/// Verifies a served transaction encryption key against trusted chain state.
253pub 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
299/// Decodes all key fields which are covered by the validator attestation.
300fn 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
345/// Validates a key identifier before it is used in a transcript or allocation.
346fn 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
359/// Computes the validator-signed commitment over transaction encryption key metadata.
360fn 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
393/// Appends a length-prefixed field to the attestation transcript.
394fn 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// SEALER
403// ================================================================================================
404
405/// Seals transaction inputs against the validator set's shared encryption key.
406///
407/// Built from a verified transaction encryption key and reusable for any number of transactions.
408/// Holding one avoids re-fetching the key per submission; callers should discard it when the
409/// validator reports an unknown key ID.
410#[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    /// Builds a sealer from a key whose validator attestation has already been verified.
420    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    /// The identifier of the key this sealer seals against.
430    pub fn key_id(&self) -> &[u8] {
431        &self.key_id
432    }
433
434    /// Seals `transaction_inputs` for the transaction identified by `tx_id`.
435    ///
436    /// `transaction_inputs` must be the encoding of
437    /// [`miden_protocol::transaction::TransactionInputs::to_bytes`].
438    ///
439    /// Each call draws a fresh ephemeral key, so sealing the same inputs twice is safe and yields
440    /// different ciphertexts.
441    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// TESTS
465// ================================================================================================
466
467#[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    /// A key signed by the validator committed in trusted chain state verifies.
521    #[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    /// An untrusted RPC cannot omit or rely on a malformed validator attestation.
539    #[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    /// A malformed attestation does not hide a later valid attestation.
566    #[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    /// A valid signature does not help when its signer is absent from trusted chain state.
587    #[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    /// Every served key field and the network identity are covered by the signature.
603    #[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    /// Key metadata is bounded and decoded before it can become domain state.
642    #[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    /// Pins the transcript byte-for-byte, which also pins *which* fields it binds.
671    ///
672    /// Both sides derive the transcript through this one function, so a change to it would pass
673    /// every other test in the workspace and surface only as every submission on the network failing
674    /// to authenticate. This vector is the only thing that catches that.
675    #[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        // 22-byte tag + 4 scheme + 4 length + 4 key id + 32 genesis + 32 transaction id.
689        assert_eq!(ad.len(), 98);
690    }
691}