mod kms;
pub use kms::{KmsSigner, decrypt_key_material};
use miden_node_proto::domain::encryption::{
TransactionEncryptionKeyInfo,
TransactionEncryptionScheme,
};
use miden_node_tracing::spawn::spawn_blocking_in_current_span;
use miden_protocol::Word;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature, SigningKey};
use miden_protocol::crypto::dsa::eddsa_25519_sha512::{
KeyExchangeKey,
PublicKey as EncryptionPublicKey,
};
#[cfg(test)]
use miden_protocol::crypto::ies::SealingKey;
use miden_protocol::crypto::ies::{SealedMessage, UnsealingKey};
use miden_protocol::utils::serde::{Deserializable, Serializable};
pub enum ValidatorSigner {
Kms(KmsSigner),
Local(SigningKey),
}
impl ValidatorSigner {
pub async fn new_kms(key_id: impl Into<String>) -> anyhow::Result<Self> {
let kms_signer = KmsSigner::new(key_id).await?;
Ok(Self::Kms(kms_signer))
}
pub fn new_local(secret_key: SigningKey) -> Self {
Self::Local(secret_key)
}
pub fn public_key(&self) -> PublicKey {
match self {
Self::Kms(signer) => signer.public_key(),
Self::Local(signer) => signer.public_key(),
}
}
pub async fn sign_commitment(&self, commitment: Word) -> anyhow::Result<Signature> {
let signature = match self {
Self::Kms(signer) => signer.sign(commitment).await?,
Self::Local(signer) => spawn_blocking_in_current_span({
let signer = signer.clone();
move || signer.sign(commitment)
})
.await
.unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic())),
};
Ok(signature)
}
}
#[tonic::async_trait]
pub trait TransactionInputDecrypter: Send + Sync {
async fn encryption_key(&self) -> anyhow::Result<TransactionEncryptionKeyInfo>;
async fn decrypt_transaction_inputs(
&self,
ciphertext: &[u8],
associated_data: &[u8],
) -> anyhow::Result<Vec<u8>>;
}
pub struct LocalX25519TransactionInputDecrypter {
secret_key: KeyExchangeKey,
}
impl LocalX25519TransactionInputDecrypter {
pub const SCHEME: TransactionEncryptionScheme =
TransactionEncryptionScheme::X25519XChaCha20Poly1305;
pub fn new(secret_key: KeyExchangeKey) -> Self {
Self { secret_key }
}
pub fn public_key(&self) -> EncryptionPublicKey {
self.secret_key.public_key()
}
pub fn key_id(&self) -> Vec<u8> {
self.public_key().to_commitment().to_bytes()[..4].to_vec()
}
#[cfg(test)]
pub fn sealing_key(&self) -> SealingKey {
SealingKey::X25519XChaCha20Poly1305(self.public_key())
}
}
#[tonic::async_trait]
impl TransactionInputDecrypter for LocalX25519TransactionInputDecrypter {
async fn encryption_key(&self) -> anyhow::Result<TransactionEncryptionKeyInfo> {
Ok(TransactionEncryptionKeyInfo {
scheme: Self::SCHEME,
key_id: self.key_id(),
public_key: self.public_key().to_bytes(),
next_key: None,
})
}
async fn decrypt_transaction_inputs(
&self,
ciphertext: &[u8],
associated_data: &[u8],
) -> anyhow::Result<Vec<u8>> {
use anyhow::Context;
let message = SealedMessage::read_from_bytes(ciphertext)
.context("failed to deserialize the sealed message")?;
let secret_key = self.secret_key.clone();
let associated_data = associated_data.to_vec();
spawn_blocking_in_current_span(move || {
UnsealingKey::X25519XChaCha20Poly1305(secret_key)
.unseal_bytes_with_associated_data(message, &associated_data)
.context("AEAD authentication failed")
})
.await
.unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic()))
}
}
#[cfg(test)]
mod tests {
use miden_protocol::utils::serde::Deserializable;
use rand::rng;
use super::*;
fn decrypter_from(secret: &[u8; 32]) -> LocalX25519TransactionInputDecrypter {
LocalX25519TransactionInputDecrypter::new(KeyExchangeKey::read_from_bytes(secret).unwrap())
}
#[tokio::test]
async fn same_secret_yields_same_public_material() {
let genesis = Word::try_from([1u64, 2, 3, 4]).unwrap();
let info_a = decrypter_from(&[7u8; 32]).encryption_key().await.unwrap();
let info_b = decrypter_from(&[7u8; 32]).encryption_key().await.unwrap();
assert_eq!(info_a, info_b);
assert_eq!(info_a.attestation_commitment(genesis), info_b.attestation_commitment(genesis));
}
#[tokio::test]
async fn different_secrets_yield_different_public_material() {
let genesis = Word::try_from([1u64, 2, 3, 4]).unwrap();
let info_a = decrypter_from(&[7u8; 32]).encryption_key().await.unwrap();
let info_b = decrypter_from(&[8u8; 32]).encryption_key().await.unwrap();
assert_eq!(info_a.scheme, info_b.scheme);
assert_ne!(info_a.public_key, info_b.public_key);
assert_ne!(info_a.key_id, info_b.key_id);
assert_ne!(info_a.attestation_commitment(genesis), info_b.attestation_commitment(genesis));
}
#[tokio::test]
async fn seal_decrypt_roundtrip() {
let mut rng = rng();
let decrypter = decrypter_from(&[7u8; 32]);
let plaintext = b"transaction inputs";
let associated_data = b"scheme|key_id|chain|tx";
let sealed = decrypter
.sealing_key()
.seal_bytes_with_associated_data(&mut rng, plaintext, associated_data)
.unwrap()
.to_bytes();
let opened = decrypter.decrypt_transaction_inputs(&sealed, associated_data).await.unwrap();
assert_eq!(opened.as_slice(), plaintext);
assert!(
decrypter
.decrypt_transaction_inputs(&sealed, b"wrong associated data")
.await
.is_err()
);
let other = decrypter_from(&[8u8; 32]);
assert!(other.decrypt_transaction_inputs(&sealed, associated_data).await.is_err());
assert!(
decrypter
.decrypt_transaction_inputs(b"not a sealed message", associated_data)
.await
.is_err()
);
}
}