use alloc::string::ToString;
use alloc::vec::Vec;
use miden_protocol::block::{BlockNumber, ValidatorKeys};
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{
PublicKey as ValidatorPublicKey,
Signature as ValidatorSignature,
};
use miden_protocol::crypto::dsa::eddsa_25519_sha512::PublicKey;
use miden_protocol::crypto::ies::SealingKey;
use miden_protocol::transaction::{TransactionId, TransactionInputs};
use miden_protocol::{Hasher, Word};
use miden_tx::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use rand::CryptoRng;
use super::generated::transaction::IesScheme;
use super::{RpcError, generated as proto};
pub(crate) const TRANSACTION_ENCRYPTION_KEY_STORE_SETTING: &str = "transaction_encryption_key";
const TX_INPUT_SEAL_DOMAIN: &[u8] = b"MIDEN_TX_INPUT_SEAL_V1";
const ATTESTATION_DOMAIN: &[u8] = b"MIDEN_TX_ENCRYPTION_KEY_ATTESTATION_V1";
const SUPPORTED_SCHEME: u32 = IesScheme::X25519Xchacha20Poly1305 as u32;
const MAX_KEY_ID_LEN: usize = 64;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransactionEncryptionKey {
scheme: u32,
key_id: Vec<u8>,
public_key: PublicKey,
genesis_commitment: Word,
}
impl TransactionEncryptionKey {
pub fn key_id(&self) -> &[u8] {
&self.key_id
}
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
fn transaction_inputs_associated_data(&self, tx_id: TransactionId) -> Vec<u8> {
transaction_inputs_associated_data(
self.scheme,
&self.key_id,
self.genesis_commitment,
tx_id,
)
}
pub fn sealing_key(&self) -> SealingKey {
SealingKey::X25519XChaCha20Poly1305(self.public_key.clone())
}
#[cfg(feature = "testing")]
pub fn new_unattested(
key_id: Vec<u8>,
public_key: PublicKey,
genesis_commitment: Word,
) -> Self {
Self {
scheme: SUPPORTED_SCHEME,
key_id,
public_key,
genesis_commitment,
}
}
}
impl Serializable for TransactionEncryptionKey {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_u32(self.scheme);
target.write_usize(self.key_id.len());
target.write_bytes(&self.key_id);
self.public_key.write_into(target);
self.genesis_commitment.write_into(target);
}
}
impl Deserializable for TransactionEncryptionKey {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let scheme = source.read_u32()?;
let key_id_len = source.read_usize()?;
let key_id = source.read_vec(key_id_len)?;
let public_key = PublicKey::read_from(source)?;
let genesis_commitment = Word::read_from(source)?;
Ok(Self {
scheme,
key_id,
public_key,
genesis_commitment,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NextTransactionEncryptionKey {
pub scheme: u32,
pub key_id: Vec<u8>,
pub public_key: Vec<u8>,
pub rotation_block_num: BlockNumber,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValidatorAttestation {
pub validator_key: ValidatorPublicKey,
pub signature: ValidatorSignature,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AttestedTransactionEncryptionKey {
pub scheme: u32,
pub key_id: Vec<u8>,
pub public_key: Vec<u8>,
pub attestations: Vec<ValidatorAttestation>,
pub next_key: Option<NextTransactionEncryptionKey>,
}
impl AttestedTransactionEncryptionKey {
pub fn verify(
self,
genesis_commitment: Word,
validator_keys: &ValidatorKeys,
) -> Result<TransactionEncryptionKey, RpcError> {
if self.scheme != SUPPORTED_SCHEME {
return Err(RpcError::TransactionEncryptionKeyRejected(format!(
"unsupported IES scheme '{}'",
self.scheme
)));
}
validate_key_id(&self.key_id, "encryption key id")?;
if let Some(next) = &self.next_key {
validate_key_id(&next.key_id, "next encryption key id")?;
}
let commitment = attestation_commitment(
self.scheme,
&self.key_id,
genesis_commitment,
&self.public_key,
self.next_key.as_ref(),
);
let recognized = validator_keys.as_keys();
let attested = self.attestations.iter().any(|attestation| {
recognized.contains(&attestation.validator_key)
&& attestation.validator_key.verify(commitment, &attestation.signature)
});
if !attested {
return Err(RpcError::TransactionEncryptionKeyRejected(
"no attestation from a chain-recognized validator verifies against the key".into(),
));
}
let public_key = PublicKey::read_from_bytes(&self.public_key)
.map_err(|err| RpcError::TransactionEncryptionKeyRejected(err.to_string()))?;
Ok(TransactionEncryptionKey {
scheme: self.scheme,
key_id: self.key_id,
public_key,
genesis_commitment,
})
}
}
fn validate_key_id(key_id: &[u8], field: &str) -> Result<(), RpcError> {
if key_id.is_empty() {
return Err(RpcError::TransactionEncryptionKeyRejected(format!("{field} is empty")));
}
if key_id.len() > MAX_KEY_ID_LEN {
return Err(RpcError::TransactionEncryptionKeyRejected(format!(
"{field} is {} bytes, which exceeds the maximum of {MAX_KEY_ID_LEN}",
key_id.len()
)));
}
Ok(())
}
pub fn attestation_commitment(
scheme: u32,
key_id: &[u8],
genesis_commitment: Word,
public_key: &[u8],
next_key: Option<&NextTransactionEncryptionKey>,
) -> Word {
let mut payload = Vec::new();
payload.extend_from_slice(ATTESTATION_DOMAIN);
payload.extend_from_slice(&scheme.to_le_bytes());
extend_with_length_prefixed(&mut payload, key_id);
payload.extend_from_slice(&genesis_commitment.to_bytes());
extend_with_length_prefixed(&mut payload, public_key);
if let Some(next) = next_key {
payload.extend_from_slice(&next.scheme.to_le_bytes());
extend_with_length_prefixed(&mut payload, &next.key_id);
extend_with_length_prefixed(&mut payload, &next.public_key);
payload.extend_from_slice(&next.rotation_block_num.as_u32().to_le_bytes());
}
Hasher::hash(&payload)
}
fn extend_with_length_prefixed(payload: &mut Vec<u8>, field: &[u8]) {
let len = u32::try_from(field.len()).unwrap_or(u32::MAX);
payload.extend_from_slice(&len.to_le_bytes());
payload.extend_from_slice(field);
}
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());
extend_with_length_prefixed(&mut transcript, key_id);
transcript.extend_from_slice(&genesis_commitment);
transcript.extend_from_slice(&tx_id);
transcript
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SealedTransactionInputs {
key_id: Vec<u8>,
ciphertext: Vec<u8>,
}
impl SealedTransactionInputs {
pub fn key_id(&self) -> &[u8] {
&self.key_id
}
pub fn ciphertext(&self) -> &[u8] {
&self.ciphertext
}
}
impl From<SealedTransactionInputs> for proto::transaction::SealedTransactionInputs {
fn from(sealed: SealedTransactionInputs) -> Self {
Self {
key_id: sealed.key_id,
ciphertext: sealed.ciphertext,
}
}
}
pub fn seal_transaction_inputs<R: CryptoRng>(
rng: &mut R,
key: &TransactionEncryptionKey,
tx_id: TransactionId,
transaction_inputs: &TransactionInputs,
) -> Result<SealedTransactionInputs, RpcError> {
let associated_data = key.transaction_inputs_associated_data(tx_id);
let sealed = key
.sealing_key()
.seal_bytes_with_associated_data(rng, &transaction_inputs.to_bytes(), &associated_data)
.map_err(|err| RpcError::TransactionInputsSealingFailed(err.to_string()))?;
Ok(SealedTransactionInputs {
key_id: key.key_id().to_vec(),
ciphertext: sealed.to_bytes(),
})
}
#[cfg(test)]
mod tests {
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey as ValidatorSigningKey;
use miden_protocol::crypto::dsa::eddsa_25519_sha512::KeyExchangeKey;
use miden_protocol::crypto::ies::{SealedMessage, UnsealingKey};
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
use super::*;
const TEST_KEY_ID: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
fn rng() -> ChaCha20Rng {
ChaCha20Rng::seed_from_u64(0xface)
}
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 key_pair() -> (TransactionEncryptionKey, UnsealingKey) {
let secret_key = KeyExchangeKey::with_rng(&mut rng());
let key = TransactionEncryptionKey {
scheme: SUPPORTED_SCHEME,
key_id: TEST_KEY_ID.to_vec(),
public_key: secret_key.public_key(),
genesis_commitment: genesis(),
};
(key, UnsealingKey::X25519XChaCha20Poly1305(secret_key))
}
fn unseal(
unsealing_key: &UnsealingKey,
sealed: &SealedTransactionInputs,
key: &TransactionEncryptionKey,
tx_id: TransactionId,
) -> Result<Vec<u8>, ()> {
let associated_data = key.transaction_inputs_associated_data(tx_id);
unsealing_key
.unseal_bytes_with_associated_data(
SealedMessage::read_from_bytes(sealed.ciphertext()).unwrap(),
&associated_data,
)
.map_err(|_| ())
}
fn seal(key: &TransactionEncryptionKey, tx_id: TransactionId) -> SealedTransactionInputs {
let associated_data = key.transaction_inputs_associated_data(tx_id);
let sealed = key
.sealing_key()
.seal_bytes_with_associated_data(&mut rng(), b"transaction inputs", &associated_data)
.unwrap();
SealedTransactionInputs {
key_id: key.key_id().to_vec(),
ciphertext: sealed.to_bytes(),
}
}
#[test]
fn associated_data_matches_the_validator_transcript() {
let associated_data =
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!(associated_data, expected);
assert_eq!(associated_data.len(), 98);
}
#[test]
fn sealed_inputs_round_trip() {
let (key, unsealing_key) = key_pair();
let sealed = seal(&key, tx_id(10));
assert_eq!(sealed.key_id(), key.key_id());
let opened = unseal(&unsealing_key, &sealed, &key, tx_id(10)).unwrap();
assert_eq!(opened, b"transaction inputs");
}
#[test]
fn unsealing_rejects_a_different_transaction() {
let (key, unsealing_key) = key_pair();
let sealed = seal(&key, tx_id(10));
assert!(unseal(&unsealing_key, &sealed, &key, tx_id(11)).is_err());
}
#[test]
fn unsealing_rejects_a_different_key_id() {
let (key, unsealing_key) = key_pair();
let sealed = seal(&key, tx_id(10));
let rotated = TransactionEncryptionKey { key_id: b"other".to_vec(), ..key };
assert!(unseal(&unsealing_key, &sealed, &rotated, tx_id(10)).is_err());
}
#[test]
fn sealing_the_same_inputs_twice_yields_different_ciphertexts() {
let (key, unsealing_key) = key_pair();
let associated_data = key.transaction_inputs_associated_data(tx_id(10));
let mut rng = rng();
let mut seal_once = || {
key.sealing_key()
.seal_bytes_with_associated_data(&mut rng, b"transaction inputs", &associated_data)
.unwrap()
.to_bytes()
};
let first = seal_once();
let second = seal_once();
assert_ne!(first, second);
for ciphertext in [first, second] {
let sealed = SealedTransactionInputs {
key_id: key.key_id().to_vec(),
ciphertext,
};
assert_eq!(
unseal(&unsealing_key, &sealed, &key, tx_id(10)).unwrap(),
b"transaction inputs"
);
}
}
fn attested(
key: &TransactionEncryptionKey,
signer: &ValidatorSigningKey,
genesis_commitment: Word,
) -> AttestedTransactionEncryptionKey {
attested_with_next(key, signer, genesis_commitment, None)
}
fn attested_with_next(
key: &TransactionEncryptionKey,
signer: &ValidatorSigningKey,
genesis_commitment: Word,
next_key: Option<NextTransactionEncryptionKey>,
) -> AttestedTransactionEncryptionKey {
let public_key = key.public_key().to_bytes();
let commitment = attestation_commitment(
SUPPORTED_SCHEME,
key.key_id(),
genesis_commitment,
&public_key,
next_key.as_ref(),
);
AttestedTransactionEncryptionKey {
scheme: SUPPORTED_SCHEME,
key_id: key.key_id().to_vec(),
public_key,
attestations: vec![ValidatorAttestation {
validator_key: signer.public_key(),
signature: signer.sign(commitment),
}],
next_key,
}
}
#[test]
fn verify_accepts_an_attestation_from_a_recognized_validator() {
let (key, _) = key_pair();
let signer = ValidatorSigningKey::with_rng(&mut rng());
let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
let verified =
attested(&key, &signer, genesis()).verify(genesis(), &validator_keys).unwrap();
assert_eq!(verified, key);
}
#[test]
fn verify_rejects_a_validator_absent_from_the_committed_set() {
let (key, _) = key_pair();
let impostor = ValidatorSigningKey::with_rng(&mut rng());
let committed = ValidatorSigningKey::with_rng(&mut ChaCha20Rng::seed_from_u64(7));
let validator_keys = ValidatorKeys::new(vec![committed.public_key()]).unwrap();
assert!(attested(&key, &impostor, genesis()).verify(genesis(), &validator_keys).is_err());
}
#[test]
fn verify_rejects_a_substituted_public_key() {
let (key, _) = key_pair();
let signer = ValidatorSigningKey::with_rng(&mut rng());
let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
let substitute = KeyExchangeKey::with_rng(&mut ChaCha20Rng::seed_from_u64(99));
let mut response = attested(&key, &signer, genesis());
response.public_key = substitute.public_key().to_bytes();
assert!(response.verify(genesis(), &validator_keys).is_err());
}
#[test]
fn verify_rejects_an_attestation_from_another_network() {
let (key, _) = key_pair();
let signer = ValidatorSigningKey::with_rng(&mut rng());
let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
let response = attested(&key, &signer, genesis());
assert!(response.verify(Word::from([9u32, 9, 9, 9]), &validator_keys).is_err());
}
#[test]
fn verify_rejects_an_injected_next_key() {
let (key, _) = key_pair();
let signer = ValidatorSigningKey::with_rng(&mut rng());
let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
let mut response = attested(&key, &signer, genesis());
response.next_key = Some(NextTransactionEncryptionKey {
scheme: SUPPORTED_SCHEME,
key_id: vec![1, 2, 3, 4],
public_key: KeyExchangeKey::with_rng(&mut ChaCha20Rng::seed_from_u64(11))
.public_key()
.to_bytes(),
rotation_block_num: 100.into(),
});
assert!(response.verify(genesis(), &validator_keys).is_err());
}
#[test]
fn verify_rejects_an_unsupported_scheme() {
let (key, _) = key_pair();
let signer = ValidatorSigningKey::with_rng(&mut rng());
let validator_keys = ValidatorKeys::new(vec![signer.public_key()]).unwrap();
let mut response = attested(&key, &signer, genesis());
response.scheme = SUPPORTED_SCHEME + 1;
assert!(response.verify(genesis(), &validator_keys).is_err());
}
#[test]
fn attestation_commitment_matches_the_validator_implementation() {
let genesis = Word::from([101u32, 102, 103, 104]);
let no_rotation =
attestation_commitment(1, b"golden-key-id", genesis, b"golden-public-key", None);
assert_eq!(
no_rotation.to_hex(),
"0x245d1f2d45d4a60d9edd4576691244d6b9ee16fe67635425dc685cd54918a970"
);
let next = NextTransactionEncryptionKey {
scheme: 2,
key_id: b"next-key-id".to_vec(),
public_key: b"next-public-key".to_vec(),
rotation_block_num: BlockNumber::from(7u32),
};
let with_rotation =
attestation_commitment(1, b"golden-key-id", genesis, b"golden-public-key", Some(&next));
assert_eq!(
with_rotation.to_hex(),
"0xddfd7907b6a1ea6f294809ff0ed775f270b649ca15b21f88127c8335945e4752"
);
}
}