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_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
21/// Domain tag prefixed to the associated data of sealed transaction inputs.
22///
23/// Separates this transcript from every other use of the same key material, in particular from the
24/// key attestation signed with the validator's signing key.
25pub const TX_INPUT_SEAL_DOMAIN: &[u8] = b"MIDEN_TX_INPUT_SEAL_V1";
26
27/// Domain tag prefixed to the validator-signed encryption key payload.
28pub const ATTESTATION_DOMAIN: &[u8] = b"MIDEN_TX_ENCRYPTION_KEY_ATTESTATION_V1";
29
30/// Upper bound on the length of an encryption key identifier.
31///
32/// Key identifiers are 4 bytes today (the leading bytes of the public key commitment). The bound
33/// exists so that a hostile or misconfigured key endpoint cannot drive an unbounded allocation, and
34/// so that the length cast in the transcript cannot overflow.
35pub const MAX_KEY_ID_LEN: usize = 64;
36
37/// Wire identifier of the only IES scheme the node currently supports.
38const SCHEME_X25519_XCHACHA20_POLY1305: u32 = 1;
39
40// ENCRYPTION KEY
41// ================================================================================================
42
43/// Encryption schemes supported by transaction input submission.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[repr(u32)]
46pub enum TransactionEncryptionScheme {
47    /// X25519 key agreement with XChaCha20-Poly1305 authenticated encryption.
48    X25519XChaCha20Poly1305 = SCHEME_X25519_XCHACHA20_POLY1305,
49}
50
51impl TransactionEncryptionScheme {
52    /// Returns the integer used for this scheme on the wire and in signed transcripts.
53    pub const fn as_u32(self) -> u32 {
54        self as u32
55    }
56
57    /// Returns the protobuf enum value for this scheme.
58    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/// Public metadata for a scheduled transaction encryption key.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct NextEncryptionKeyInfo {
78    /// Encryption scheme for the scheduled key.
79    pub scheme: TransactionEncryptionScheme,
80    /// Opaque identifier of the scheduled key.
81    pub key_id: Vec<u8>,
82    /// Encoded public key.
83    pub public_key: Vec<u8>,
84    /// Block at which the scheduled key becomes current.
85    pub rotation_block_num: u32,
86}
87
88/// Public metadata for the transaction encryption key served by a validator.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct TransactionEncryptionKeyInfo {
91    /// Encryption scheme for the current key.
92    pub scheme: TransactionEncryptionScheme,
93    /// Opaque identifier of the current key.
94    pub key_id: Vec<u8>,
95    /// Encoded public key.
96    pub public_key: Vec<u8>,
97    /// Scheduled replacement key, when one exists.
98    pub next_key: Option<NextEncryptionKeyInfo>,
99}
100
101impl TransactionEncryptionKeyInfo {
102    /// Returns the commitment a validator signs to attest this key for one network.
103    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/// Trusted chain state used to verify a served transaction encryption key.
115#[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    /// Creates trusted state from a genesis commitment and its validator signing keys.
123    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/// A transaction encryption key whose attestation matches trusted chain state.
135#[derive(Debug, Clone)]
136pub struct VerifiedTransactionEncryptionKey {
137    info: TransactionEncryptionKeyInfo,
138    public_key: EncryptionPublicKey,
139    genesis_commitment: Word,
140}
141
142impl VerifiedTransactionEncryptionKey {
143    /// Returns the verified key metadata.
144    pub const fn info(&self) -> &TransactionEncryptionKeyInfo {
145        &self.info
146    }
147
148    /// Returns the decoded encryption public key.
149    pub const fn public_key(&self) -> &EncryptionPublicKey {
150        &self.public_key
151    }
152
153    /// Returns the network genesis commitment covered by the attestation.
154    pub const fn genesis_commitment(&self) -> Word {
155        self.genesis_commitment
156    }
157}
158
159// ASSOCIATED DATA
160// ================================================================================================
161
162/// Builds the associated data authenticating a sealed set of transaction inputs.
163///
164/// This is the single definition of the transcript. Both sides derive it independently and it is
165/// never transmitted, so a mismatch surfaces as an authentication failure rather than as accepted
166/// but unauthenticated data.
167///
168/// The layout is `TX_INPUT_SEAL_DOMAIN || scheme || len(key_id) || key_id || genesis_commitment ||
169/// transaction_id`, where the scheme and the length prefix are 4 bytes little-endian. The domain tag
170/// is a fixed-width constant, `scheme` is fixed-width, `key_id` is length-prefixed and the two
171/// trailing fields are a fixed 32 bytes each, so no two distinct inputs produce the same transcript.
172///
173/// Each binding serves a purpose:
174/// - `scheme` and `key_id` tie the blob to one key, so inputs sealed against a retired key fail to
175///   authenticate rather than silently decrypting.
176/// - `genesis_commitment` ties the blob to one network. This matters in practice because every
177///   development stack shares the same insecure default key, so without it a blob captured on one
178///   network would replay onto another.
179/// - `transaction_id` ties the blob to one transaction, so a captured blob cannot be replayed onto a
180///   different transaction.
181///
182/// Deliberately absent is the serialized transaction. The RPC rebuilds `ProvenTransaction` with
183/// output-note decorators stripped before forwarding a submission, so binding those bytes would
184/// reject every relayed transaction. The transaction id is invariant under that rebuild, which is
185/// why it is bound instead.
186pub 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    // Callers bound `key_id` to MAX_KEY_ID_LEN, so this cast cannot realistically fail. Saturate
204    // rather than panic anyway: this runs inside a request handler on the validator.
205    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// ERRORS
214// ================================================================================================
215
216/// Failure to decode or verify a served transaction encryption key.
217#[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/// Failure to seal transaction inputs.
244#[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
250// ATTESTATION
251// ================================================================================================
252
253impl<'a> VerifyWith<TrustedTransactionEncryptionState<'a>>
254    for proto::submission::TransactionEncryptionKey
255{
256    type Verified = VerifiedTransactionEncryptionKey;
257    type Error = TransactionEncryptionKeyError;
258
259    /// Verify the key against the supplied trusted network and validator keys. Decode attestations
260    /// separately so a malformed attestation cannot hide a valid one. Whole-message decoding would
261    /// reject the key before checking the remaining attestations.
262    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    /// Decodes the key fields covered by the validator attestation. This method does not verify the
316    /// attestation.
317    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
365/// Validates a key identifier before it is used in a transcript or allocation.
366fn 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
379/// Computes the validator-signed commitment over transaction encryption key metadata.
380fn 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
413/// Appends a length-prefixed field to the attestation transcript.
414fn 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// SEALER
423// ================================================================================================
424
425/// Seals transaction inputs against the validator set's shared encryption key.
426///
427/// Built from a verified transaction encryption key and reusable for any number of transactions.
428/// Holding one avoids re-fetching the key per submission; callers should discard it when the
429/// validator reports an unknown key ID.
430#[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    /// Builds a sealer from a key whose validator attestation has already been verified.
440    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    /// The identifier of the key this sealer seals against.
450    pub fn key_id(&self) -> &[u8] {
451        &self.key_id
452    }
453
454    /// Seals `transaction_inputs` for the transaction identified by `tx_id`.
455    ///
456    /// `transaction_inputs` must be the encoding of
457    /// [`miden_protocol::transaction::TransactionInputs::to_bytes`].
458    ///
459    /// Each call draws a fresh ephemeral key, so sealing the same inputs twice is safe and yields
460    /// different ciphertexts.
461    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// TESTS
485// ================================================================================================
486
487#[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    /// A key signed by the validator committed in trusted chain state verifies.
541    #[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    /// An untrusted RPC cannot omit or rely on a malformed validator attestation.
557    #[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    /// A malformed attestation does not hide a later valid attestation.
588    #[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    /// A valid signature does not help when its signer is absent from trusted chain state.
606    #[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    /// Every served key field and the network identity are covered by the signature.
620    #[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    /// Key metadata is bounded and decoded before it can become domain state.
659    #[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    /// Pins the transcript byte-for-byte, which also pins *which* fields it binds.
688    ///
689    /// Both sides derive the transcript through this one function, so a change to it would pass
690    /// every other test in the workspace and surface only as every submission on the network failing
691    /// to authenticate. This vector is the only thing that catches that.
692    #[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        // 22-byte tag + 4 scheme + 4 length + 4 key id + 32 genesis + 32 transaction id.
706        assert_eq!(ad.len(), 98);
707    }
708}