use miden_protocol::Word;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{
PublicKey as ValidatorPublicKey,
Signature as ValidatorSignature,
};
use miden_protocol::crypto::dsa::eddsa_25519_sha512::PublicKey as EncryptionPublicKey;
use miden_protocol::crypto::ies::SealingKey;
use miden_protocol::transaction::TransactionId;
use miden_protocol::utils::serde::{Deserializable, Serializable};
use crate::generated as proto;
pub const TX_INPUT_SEAL_DOMAIN: &[u8] = b"MIDEN_TX_INPUT_SEAL_V1";
pub const ATTESTATION_DOMAIN: &[u8] = b"MIDEN_TX_ENCRYPTION_KEY_ATTESTATION_V1";
pub const MAX_KEY_ID_LEN: usize = 64;
const SCHEME_X25519_XCHACHA20_POLY1305: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum TransactionEncryptionScheme {
X25519XChaCha20Poly1305 = SCHEME_X25519_XCHACHA20_POLY1305,
}
impl TransactionEncryptionScheme {
pub const fn as_u32(self) -> u32 {
self as u32
}
pub const fn as_i32(self) -> i32 {
self as i32
}
}
impl TryFrom<i32> for TransactionEncryptionScheme {
type Error = TransactionEncryptionKeyError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Err(TransactionEncryptionKeyError::UnspecifiedScheme),
1 => Ok(Self::X25519XChaCha20Poly1305),
other => Err(TransactionEncryptionKeyError::UnsupportedScheme(other)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NextEncryptionKeyInfo {
pub scheme: TransactionEncryptionScheme,
pub key_id: Vec<u8>,
pub public_key: Vec<u8>,
pub rotation_block_num: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransactionEncryptionKeyInfo {
pub scheme: TransactionEncryptionScheme,
pub key_id: Vec<u8>,
pub public_key: Vec<u8>,
pub next_key: Option<NextEncryptionKeyInfo>,
}
impl TransactionEncryptionKeyInfo {
pub fn attestation_commitment(&self, genesis_commitment: Word) -> Word {
attestation_commitment(
self.scheme,
&self.key_id,
genesis_commitment,
&self.public_key,
self.next_key.as_ref(),
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct TrustedTransactionEncryptionState<'a> {
genesis_commitment: Word,
validator_signing_keys: &'a [ValidatorPublicKey],
}
impl<'a> TrustedTransactionEncryptionState<'a> {
pub const fn new(
genesis_commitment: Word,
validator_signing_keys: &'a [ValidatorPublicKey],
) -> Self {
Self {
genesis_commitment,
validator_signing_keys,
}
}
}
#[derive(Debug, Clone)]
pub struct VerifiedTransactionEncryptionKey {
info: TransactionEncryptionKeyInfo,
public_key: EncryptionPublicKey,
genesis_commitment: Word,
}
impl VerifiedTransactionEncryptionKey {
pub const fn info(&self) -> &TransactionEncryptionKeyInfo {
&self.info
}
pub const fn public_key(&self) -> &EncryptionPublicKey {
&self.public_key
}
pub const fn genesis_commitment(&self) -> Word {
self.genesis_commitment
}
}
pub fn transaction_inputs_associated_data(
scheme: u32,
key_id: &[u8],
genesis_commitment: Word,
tx_id: TransactionId,
) -> Vec<u8> {
let genesis_commitment = genesis_commitment.to_bytes();
let tx_id = tx_id.as_word().to_bytes();
let mut transcript = Vec::with_capacity(
TX_INPUT_SEAL_DOMAIN.len()
+ 2 * size_of::<u32>()
+ key_id.len()
+ genesis_commitment.len()
+ tx_id.len(),
);
transcript.extend_from_slice(TX_INPUT_SEAL_DOMAIN);
transcript.extend_from_slice(&scheme.to_le_bytes());
let key_id_len = u32::try_from(key_id.len()).unwrap_or(u32::MAX);
transcript.extend_from_slice(&key_id_len.to_le_bytes());
transcript.extend_from_slice(key_id);
transcript.extend_from_slice(&genesis_commitment);
transcript.extend_from_slice(&tx_id);
transcript
}
#[derive(Debug, thiserror::Error)]
pub enum TransactionEncryptionKeyError {
#[error("encryption key scheme is unspecified")]
UnspecifiedScheme,
#[error("unsupported encryption key scheme {0}")]
UnsupportedScheme(i32),
#[error("{field} is empty")]
EmptyKeyId { field: &'static str },
#[error("{field} is {len} bytes, which exceeds the maximum of {MAX_KEY_ID_LEN}")]
KeyIdTooLong { field: &'static str, len: usize },
#[error("invalid {field}")]
InvalidEncryptionPublicKey {
field: &'static str,
#[source]
source: miden_protocol::utils::serde::DeserializationError,
},
#[error("trusted validator signing keys are empty")]
NoTrustedValidatorKeys,
#[error("transaction encryption key has no validator attestations")]
NoAttestations,
#[error("transaction encryption key has no attestation from a trusted validator")]
NoTrustedAttestation,
#[error("trusted validator attestation does not cover the transaction encryption key")]
InvalidAttestation,
}
#[derive(Debug, thiserror::Error)]
pub enum TransactionInputSealError {
#[error("failed to seal the transaction inputs")]
Seal(#[source] miden_protocol::crypto::ies::IesError),
}
pub fn verify_transaction_encryption_key(
key: proto::transaction::TransactionEncryptionKey,
trusted: TrustedTransactionEncryptionState<'_>,
) -> Result<VerifiedTransactionEncryptionKey, TransactionEncryptionKeyError> {
if trusted.validator_signing_keys.is_empty() {
return Err(TransactionEncryptionKeyError::NoTrustedValidatorKeys);
}
if key.attestations.is_empty() {
return Err(TransactionEncryptionKeyError::NoAttestations);
}
let (info, public_key) = decode_key_info(&key)?;
let commitment = info.attestation_commitment(trusted.genesis_commitment);
let mut found_trusted_signer = false;
for attestation in key.attestations {
let Ok(validator_public_key) =
ValidatorPublicKey::read_from_bytes(&attestation.validator_public_key)
else {
continue;
};
if !trusted.validator_signing_keys.contains(&validator_public_key) {
continue;
}
found_trusted_signer = true;
let Ok(signature) = ValidatorSignature::read_from_bytes(&attestation.signature) else {
continue;
};
if signature.verify(commitment, &validator_public_key) {
return Ok(VerifiedTransactionEncryptionKey {
info,
public_key,
genesis_commitment: trusted.genesis_commitment,
});
}
}
if found_trusted_signer {
Err(TransactionEncryptionKeyError::InvalidAttestation)
} else {
Err(TransactionEncryptionKeyError::NoTrustedAttestation)
}
}
fn decode_key_info(
key: &proto::transaction::TransactionEncryptionKey,
) -> Result<(TransactionEncryptionKeyInfo, EncryptionPublicKey), TransactionEncryptionKeyError> {
let scheme = TransactionEncryptionScheme::try_from(key.scheme)?;
validate_key_id(&key.key_id, "encryption key id")?;
let public_key = EncryptionPublicKey::read_from_bytes(&key.public_key).map_err(|source| {
TransactionEncryptionKeyError::InvalidEncryptionPublicKey {
field: "encryption public key",
source,
}
})?;
let next_key = key
.next_key
.as_ref()
.map(|next| {
let scheme = TransactionEncryptionScheme::try_from(next.scheme)?;
validate_key_id(&next.key_id, "next encryption key id")?;
EncryptionPublicKey::read_from_bytes(&next.public_key).map_err(|source| {
TransactionEncryptionKeyError::InvalidEncryptionPublicKey {
field: "next encryption public key",
source,
}
})?;
Ok(NextEncryptionKeyInfo {
scheme,
key_id: next.key_id.clone(),
public_key: next.public_key.clone(),
rotation_block_num: next.rotation_block_num,
})
})
.transpose()?;
Ok((
TransactionEncryptionKeyInfo {
scheme,
key_id: key.key_id.clone(),
public_key: key.public_key.clone(),
next_key,
},
public_key,
))
}
fn validate_key_id(
key_id: &[u8],
field: &'static str,
) -> Result<(), TransactionEncryptionKeyError> {
if key_id.is_empty() {
return Err(TransactionEncryptionKeyError::EmptyKeyId { field });
}
if key_id.len() > MAX_KEY_ID_LEN {
return Err(TransactionEncryptionKeyError::KeyIdTooLong { field, len: key_id.len() });
}
Ok(())
}
fn attestation_commitment(
scheme: TransactionEncryptionScheme,
key_id: &[u8],
genesis_commitment: Word,
public_key: &[u8],
next_key: Option<&NextEncryptionKeyInfo>,
) -> Word {
let genesis_commitment = genesis_commitment.to_bytes();
let next_key_size = next_key
.map(|next| 3 * size_of::<u32>() + next.key_id.len() + next.public_key.len())
.unwrap_or_default();
let mut payload = Vec::with_capacity(
ATTESTATION_DOMAIN.len()
+ 3 * size_of::<u32>()
+ key_id.len()
+ genesis_commitment.len()
+ public_key.len()
+ next_key_size,
);
payload.extend_from_slice(ATTESTATION_DOMAIN);
payload.extend_from_slice(&scheme.as_u32().to_le_bytes());
extend_with_length_prefixed(&mut payload, key_id, "key id");
payload.extend_from_slice(&genesis_commitment);
extend_with_length_prefixed(&mut payload, public_key, "public key");
if let Some(next) = next_key {
payload.extend_from_slice(&next.scheme.as_u32().to_le_bytes());
extend_with_length_prefixed(&mut payload, &next.key_id, "next key id");
extend_with_length_prefixed(&mut payload, &next.public_key, "next public key");
payload.extend_from_slice(&next.rotation_block_num.to_le_bytes());
}
miden_protocol::Hasher::hash(&payload)
}
fn extend_with_length_prefixed(payload: &mut Vec<u8>, field: &[u8], name: &str) {
let len = u32::try_from(field.len())
.unwrap_or_else(|_| panic!("{name} length must fit in u32"))
.to_le_bytes();
payload.extend_from_slice(&len);
payload.extend_from_slice(field);
}
#[derive(Debug, Clone)]
pub struct TransactionInputsSealer {
scheme: TransactionEncryptionScheme,
key_id: Vec<u8>,
sealing_key: SealingKey,
genesis_commitment: Word,
}
impl TransactionInputsSealer {
pub fn new(key: VerifiedTransactionEncryptionKey) -> Self {
Self {
scheme: key.info.scheme,
key_id: key.info.key_id,
sealing_key: SealingKey::X25519XChaCha20Poly1305(key.public_key),
genesis_commitment: key.genesis_commitment,
}
}
pub fn key_id(&self) -> &[u8] {
&self.key_id
}
pub fn seal(
&self,
tx_id: TransactionId,
transaction_inputs: &[u8],
) -> Result<proto::transaction::SealedTransactionInputs, TransactionInputSealError> {
let associated_data = transaction_inputs_associated_data(
self.scheme.as_u32(),
&self.key_id,
self.genesis_commitment,
tx_id,
);
let sealed = self
.sealing_key
.seal_bytes_with_associated_data(&mut rand::rng(), transaction_inputs, &associated_data)
.map_err(TransactionInputSealError::Seal)?;
Ok(proto::transaction::SealedTransactionInputs {
key_id: self.key_id.clone(),
ciphertext: sealed.to_bytes(),
})
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
use miden_protocol::crypto::dsa::eddsa_25519_sha512::KeyExchangeKey;
use super::*;
const TEST_KEY_ID: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF];
fn genesis() -> Word {
Word::from([1u32, 2, 3, 4])
}
fn tx_id(seed: u32) -> TransactionId {
TransactionId::new(
Word::from([seed, 0, 0, 0]),
Word::from([0, seed, 0, 0]),
Word::from([0, 0, seed, 0]),
Word::from([0, 0, 0, seed]),
)
}
fn signing_key(seed: u8) -> SigningKey {
SigningKey::read_from_bytes(&[seed; 32]).expect("test signing key should decode")
}
fn unsigned_encryption_key() -> proto::transaction::TransactionEncryptionKey {
proto::transaction::TransactionEncryptionKey {
scheme: TransactionEncryptionScheme::X25519XChaCha20Poly1305.as_i32(),
key_id: TEST_KEY_ID.to_vec(),
public_key: KeyExchangeKey::read_from_bytes(&[7u8; 32])
.unwrap()
.public_key()
.to_bytes(),
attestations: Vec::new(),
next_key: None,
}
}
fn signed_encryption_key(
signer: &SigningKey,
genesis_commitment: Word,
) -> proto::transaction::TransactionEncryptionKey {
let mut key = unsigned_encryption_key();
let (info, _) = decode_key_info(&key).unwrap();
key.attestations = vec![proto::transaction::ValidatorKeyAttestation {
validator_public_key: signer.public_key().to_bytes(),
signature: signer.sign(info.attestation_commitment(genesis_commitment)).to_bytes(),
}];
key
}
#[test]
fn verifies_trusted_validator_attestation() {
let signer = signing_key(1);
let trusted_keys = [signer.public_key()];
let key = signed_encryption_key(&signer, genesis());
let verified = verify_transaction_encryption_key(
key,
TrustedTransactionEncryptionState::new(genesis(), &trusted_keys),
)
.unwrap();
assert_eq!(verified.info().key_id, TEST_KEY_ID);
assert_eq!(verified.info().scheme, TransactionEncryptionScheme::X25519XChaCha20Poly1305);
assert_eq!(verified.genesis_commitment(), genesis());
}
#[test]
fn rejects_missing_and_malformed_attestations() {
let signer = signing_key(1);
let trusted_keys = [signer.public_key()];
let trusted = TrustedTransactionEncryptionState::new(genesis(), &trusted_keys);
assert_matches!(
verify_transaction_encryption_key(unsigned_encryption_key(), trusted),
Err(TransactionEncryptionKeyError::NoAttestations)
);
let mut malformed_key = signed_encryption_key(&signer, genesis());
malformed_key.attestations[0].validator_public_key.clear();
assert_matches!(
verify_transaction_encryption_key(malformed_key, trusted),
Err(TransactionEncryptionKeyError::NoTrustedAttestation)
);
let mut malformed_signature = signed_encryption_key(&signer, genesis());
malformed_signature.attestations[0].signature.clear();
assert_matches!(
verify_transaction_encryption_key(malformed_signature, trusted),
Err(TransactionEncryptionKeyError::InvalidAttestation)
);
}
#[test]
fn skips_malformed_attestations() {
let signer = signing_key(1);
let trusted_keys = [signer.public_key()];
let mut key = signed_encryption_key(&signer, genesis());
key.attestations.insert(
0,
proto::transaction::ValidatorKeyAttestation {
validator_public_key: Vec::new(),
signature: Vec::new(),
},
);
verify_transaction_encryption_key(
key,
TrustedTransactionEncryptionState::new(genesis(), &trusted_keys),
)
.unwrap();
}
#[test]
fn rejects_untrusted_validator_attestation() {
let trusted_signer = signing_key(1);
let untrusted_signer = signing_key(2);
let trusted_keys = [trusted_signer.public_key()];
assert_matches!(
verify_transaction_encryption_key(
signed_encryption_key(&untrusted_signer, genesis()),
TrustedTransactionEncryptionState::new(genesis(), &trusted_keys),
),
Err(TransactionEncryptionKeyError::NoTrustedAttestation)
);
}
#[test]
fn rejects_changed_attested_fields() {
let signer = signing_key(1);
let trusted_keys = [signer.public_key()];
let trusted = TrustedTransactionEncryptionState::new(genesis(), &trusted_keys);
let key = signed_encryption_key(&signer, genesis());
let mut changed_scheme = key.clone();
changed_scheme.scheme = 0;
let mut changed_key_id = key.clone();
changed_key_id.key_id[0] ^= 1;
let mut changed_public_key = key.clone();
changed_public_key.public_key =
KeyExchangeKey::read_from_bytes(&[8u8; 32]).unwrap().public_key().to_bytes();
let mut injected_next_key = key.clone();
injected_next_key.next_key = Some(proto::transaction::NextTransactionEncryptionKey {
scheme: key.scheme,
key_id: vec![1, 2, 3, 4],
public_key: KeyExchangeKey::read_from_bytes(&[9u8; 32])
.unwrap()
.public_key()
.to_bytes(),
rotation_block_num: 100,
});
for changed in [changed_scheme, changed_key_id, changed_public_key, injected_next_key] {
assert!(verify_transaction_encryption_key(changed, trusted).is_err());
}
assert_matches!(
verify_transaction_encryption_key(
key,
TrustedTransactionEncryptionState::new(Word::from([9u32, 9, 9, 9]), &trusted_keys),
),
Err(TransactionEncryptionKeyError::InvalidAttestation)
);
}
#[test]
fn rejects_invalid_key_metadata() {
let signer = signing_key(1);
let trusted_keys = [signer.public_key()];
let trusted = TrustedTransactionEncryptionState::new(genesis(), &trusted_keys);
let mut empty_key_id = signed_encryption_key(&signer, genesis());
empty_key_id.key_id.clear();
assert_matches!(
verify_transaction_encryption_key(empty_key_id, trusted),
Err(TransactionEncryptionKeyError::EmptyKeyId { .. })
);
let mut oversized_key_id = signed_encryption_key(&signer, genesis());
oversized_key_id.key_id = vec![0; MAX_KEY_ID_LEN + 1];
assert_matches!(
verify_transaction_encryption_key(oversized_key_id, trusted),
Err(TransactionEncryptionKeyError::KeyIdTooLong { .. })
);
let mut invalid_public_key = signed_encryption_key(&signer, genesis());
invalid_public_key.public_key.clear();
assert_matches!(
verify_transaction_encryption_key(invalid_public_key, trusted),
Err(TransactionEncryptionKeyError::InvalidEncryptionPublicKey { .. })
);
}
#[test]
fn associated_data_is_stable() {
let ad = transaction_inputs_associated_data(1, &TEST_KEY_ID, genesis(), tx_id(10));
let mut expected = Vec::new();
expected.extend_from_slice(b"MIDEN_TX_INPUT_SEAL_V1");
expected.extend_from_slice(&1u32.to_le_bytes());
expected.extend_from_slice(&4u32.to_le_bytes());
expected.extend_from_slice(&TEST_KEY_ID);
expected.extend_from_slice(&genesis().to_bytes());
expected.extend_from_slice(&tx_id(10).as_word().to_bytes());
assert_eq!(ad, expected);
assert_eq!(ad.len(), 98);
}
}