miden_validator/signers/
mod.rs1mod 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
19pub enum ValidatorSigner {
24 Kms(KmsSigner),
25 Local(SigningKey),
26}
27
28impl ValidatorSigner {
29 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 pub fn new_local(secret_key: SigningKey) -> Self {
40 Self::Local(secret_key)
41 }
42
43 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 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#[tonic::async_trait]
82pub trait TransactionInputDecrypter: Send + Sync {
83 async fn encryption_key(&self) -> anyhow::Result<TransactionEncryptionKeyInfo>;
85
86 async fn decrypt_transaction_inputs(
90 &self,
91 ciphertext: &[u8],
92 associated_data: &[u8],
93 ) -> anyhow::Result<Vec<u8>>;
94}
95
96pub struct LocalX25519TransactionInputDecrypter {
98 secret_key: KeyExchangeKey,
99}
100
101impl LocalX25519TransactionInputDecrypter {
102 pub const SCHEME: TransactionEncryptionScheme =
104 TransactionEncryptionScheme::X25519XChaCha20Poly1305;
105
106 pub fn new(secret_key: KeyExchangeKey) -> Self {
108 Self { secret_key }
109 }
110
111 pub fn public_key(&self) -> EncryptionPublicKey {
113 self.secret_key.public_key()
114 }
115
116 pub fn key_id(&self) -> Vec<u8> {
119 self.public_key().to_commitment().to_bytes()[..4].to_vec()
120 }
121
122 #[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#[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 #[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 #[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 #[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 assert!(
220 decrypter
221 .decrypt_transaction_inputs(&sealed, b"wrong associated data")
222 .await
223 .is_err()
224 );
225
226 let other = decrypter_from(&[8u8; 32]);
228 assert!(other.decrypt_transaction_inputs(&sealed, associated_data).await.is_err());
229
230 assert!(
232 decrypter
233 .decrypt_transaction_inputs(b"not a sealed message", associated_data)
234 .await
235 .is_err()
236 );
237 }
238}