Skip to main content

miden_validator/signers/
mod.rs

1mod kms;
2pub use kms::{KmsSigner, decrypt_key_material};
3use miden_node_proto::domain::encryption::{
4    TransactionEncryptionKeyInfo,
5    TransactionEncryptionScheme,
6};
7use miden_node_tracing::spawn::spawn_blocking_in_current_span;
8use miden_protocol::Word;
9use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature, SigningKey};
10use miden_protocol::crypto::dsa::eddsa_25519_sha512::{
11    KeyExchangeKey,
12    PublicKey as EncryptionPublicKey,
13};
14#[cfg(test)]
15use miden_protocol::crypto::ies::SealingKey;
16use miden_protocol::crypto::ies::{SealedMessage, UnsealingKey};
17use miden_protocol::utils::serde::{Deserializable, Serializable};
18
19// VALIDATOR SIGNER
20// =================================================================================================
21
22/// Signer that the Validator uses to sign blocks.
23pub enum ValidatorSigner {
24    Kms(KmsSigner),
25    Local(SigningKey),
26}
27
28impl ValidatorSigner {
29    /// Constructs a signer which uses an AWS KMS key for signing.
30    ///
31    /// See [`KmsSigner`] for details as to env var configuration and AWS IAM policies
32    /// required to use this functionality.
33    pub async fn new_kms(key_id: impl Into<String>) -> anyhow::Result<Self> {
34        let kms_signer = KmsSigner::new(key_id).await?;
35        Ok(Self::Kms(kms_signer))
36    }
37
38    /// Constructs a signer which uses a local secret key for signing.
39    pub fn new_local(secret_key: SigningKey) -> Self {
40        Self::Local(secret_key)
41    }
42
43    /// Returns the public key corresponding to the configured signer.
44    pub fn public_key(&self) -> PublicKey {
45        match self {
46            Self::Kms(signer) => signer.public_key(),
47            Self::Local(signer) => signer.public_key(),
48        }
49    }
50
51    /// Signs a commitment using the configured signer.
52    pub async fn sign_commitment(&self, commitment: Word) -> anyhow::Result<Signature> {
53        let signature = match self {
54            Self::Kms(signer) => signer.sign(commitment).await?,
55            Self::Local(signer) => spawn_blocking_in_current_span({
56                let signer = signer.clone();
57                move || signer.sign(commitment)
58            })
59            .await
60            .unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic())),
61        };
62
63        Ok(signature)
64    }
65}
66
67// TRANSACTION INPUT DECRYPTER
68// =================================================================================================
69
70/// Decryption counterpart to [`ValidatorSigner`] for the shared transaction encryption
71/// (submission) key.
72///
73/// Unlike the signing key, the key material behind an implementation must be identical across
74/// every validator in the set. This lets any validator unseal an encrypted submission, regardless
75/// of which validator attested the encryption key to the client.
76///
77/// The interface deliberately does not assume that secret key bytes exist in the validator
78/// process: an implementation may hold a local secret (see
79/// [`LocalX25519TransactionInputDecrypter`]) or delegate decryption to an external system such as
80/// a TEE that only exposes a decrypt operation.
81#[tonic::async_trait]
82pub trait TransactionInputDecrypter: Send + Sync {
83    /// Returns the public metadata of the current encryption key.
84    async fn encryption_key(&self) -> anyhow::Result<TransactionEncryptionKeyInfo>;
85
86    /// Decrypts transaction inputs sealed against the current encryption key.
87    ///
88    /// The ciphertext is a serialized [`SealedMessage`].
89    async fn decrypt_transaction_inputs(
90        &self,
91        ciphertext: &[u8],
92        associated_data: &[u8],
93    ) -> anyhow::Result<Vec<u8>>;
94}
95
96/// [`TransactionInputDecrypter`] backed by a locally provisioned X25519 shared secret.
97pub struct LocalX25519TransactionInputDecrypter {
98    secret_key: KeyExchangeKey,
99}
100
101impl LocalX25519TransactionInputDecrypter {
102    /// The IES scheme used for transaction input encryption.
103    pub const SCHEME: TransactionEncryptionScheme =
104        TransactionEncryptionScheme::X25519XChaCha20Poly1305;
105
106    /// Constructs a decrypter from a locally provisioned shared secret.
107    pub fn new(secret_key: KeyExchangeKey) -> Self {
108        Self { secret_key }
109    }
110
111    /// Returns the public key of the shared encryption key.
112    pub fn public_key(&self) -> EncryptionPublicKey {
113        self.secret_key.public_key()
114    }
115
116    /// Returns the opaque identifier of the current encryption key: the first 4 bytes of the public
117    /// key commitment.
118    pub fn key_id(&self) -> Vec<u8> {
119        self.public_key().to_commitment().to_bytes()[..4].to_vec()
120    }
121
122    /// Returns the sealing key that clients use to encrypt messages to the validator set.
123    #[cfg(test)]
124    pub fn sealing_key(&self) -> SealingKey {
125        SealingKey::X25519XChaCha20Poly1305(self.public_key())
126    }
127}
128
129#[tonic::async_trait]
130impl TransactionInputDecrypter for LocalX25519TransactionInputDecrypter {
131    async fn encryption_key(&self) -> anyhow::Result<TransactionEncryptionKeyInfo> {
132        Ok(TransactionEncryptionKeyInfo {
133            scheme: Self::SCHEME,
134            key_id: self.key_id(),
135            public_key: self.public_key().to_bytes(),
136            next_key: None,
137        })
138    }
139
140    async fn decrypt_transaction_inputs(
141        &self,
142        ciphertext: &[u8],
143        associated_data: &[u8],
144    ) -> anyhow::Result<Vec<u8>> {
145        use anyhow::Context;
146
147        let message = SealedMessage::read_from_bytes(ciphertext)
148            .context("failed to deserialize the sealed message")?;
149
150        let secret_key = self.secret_key.clone();
151        let associated_data = associated_data.to_vec();
152        spawn_blocking_in_current_span(move || {
153            UnsealingKey::X25519XChaCha20Poly1305(secret_key)
154                .unseal_bytes_with_associated_data(message, &associated_data)
155                .context("AEAD authentication failed")
156        })
157        .await
158        .unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic()))
159    }
160}
161
162// TESTS
163// =================================================================================================
164
165#[cfg(test)]
166mod tests {
167    use miden_protocol::utils::serde::Deserializable;
168    use rand::rng;
169
170    use super::*;
171
172    fn decrypter_from(secret: &[u8; 32]) -> LocalX25519TransactionInputDecrypter {
173        LocalX25519TransactionInputDecrypter::new(KeyExchangeKey::read_from_bytes(secret).unwrap())
174    }
175
176    /// Loading the same shared secret must yield the same key metadata and attestation commitment
177    /// on every validator instance.
178    #[tokio::test]
179    async fn same_secret_yields_same_public_material() {
180        let genesis = Word::try_from([1u64, 2, 3, 4]).unwrap();
181        let info_a = decrypter_from(&[7u8; 32]).encryption_key().await.unwrap();
182        let info_b = decrypter_from(&[7u8; 32]).encryption_key().await.unwrap();
183
184        assert_eq!(info_a, info_b);
185        assert_eq!(info_a.attestation_commitment(genesis), info_b.attestation_commitment(genesis));
186    }
187
188    /// Different secrets must yield different public keys and key ids.
189    #[tokio::test]
190    async fn different_secrets_yield_different_public_material() {
191        let genesis = Word::try_from([1u64, 2, 3, 4]).unwrap();
192        let info_a = decrypter_from(&[7u8; 32]).encryption_key().await.unwrap();
193        let info_b = decrypter_from(&[8u8; 32]).encryption_key().await.unwrap();
194
195        assert_eq!(info_a.scheme, info_b.scheme);
196        assert_ne!(info_a.public_key, info_b.public_key);
197        assert_ne!(info_a.key_id, info_b.key_id);
198        assert_ne!(info_a.attestation_commitment(genesis), info_b.attestation_commitment(genesis));
199    }
200
201    /// A message sealed against the decrypter's sealing key must decrypt to the original plaintext,
202    /// and decryption must reject a mismatched associated data or a mismatched key.
203    #[tokio::test]
204    async fn seal_decrypt_roundtrip() {
205        let mut rng = rng();
206        let decrypter = decrypter_from(&[7u8; 32]);
207        let plaintext = b"transaction inputs";
208        let associated_data = b"scheme|key_id|chain|tx";
209
210        let sealed = decrypter
211            .sealing_key()
212            .seal_bytes_with_associated_data(&mut rng, plaintext, associated_data)
213            .unwrap()
214            .to_bytes();
215        let opened = decrypter.decrypt_transaction_inputs(&sealed, associated_data).await.unwrap();
216        assert_eq!(opened.as_slice(), plaintext);
217
218        // Mismatched associated data must fail authentication.
219        assert!(
220            decrypter
221                .decrypt_transaction_inputs(&sealed, b"wrong associated data")
222                .await
223                .is_err()
224        );
225
226        // A different shared secret must fail to decrypt.
227        let other = decrypter_from(&[8u8; 32]);
228        assert!(other.decrypt_transaction_inputs(&sealed, associated_data).await.is_err());
229
230        // Garbage ciphertext must fail to deserialize.
231        assert!(
232            decrypter
233                .decrypt_transaction_inputs(b"not a sealed message", associated_data)
234                .await
235                .is_err()
236        );
237    }
238}