miden_validator/signers/kms.rs
1use anyhow::Context;
2use aws_sdk_kms::error::SdkError;
3use aws_sdk_kms::operation::sign::SignError;
4use aws_sdk_kms::primitives::Blob;
5use aws_sdk_kms::types::SigningAlgorithmSpec;
6use miden_protocol::Word;
7use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature};
8use miden_protocol::crypto::hash::keccak::Keccak256;
9use miden_protocol::utils::serde::{DeserializationError, Serializable};
10
11// KMS SIGNER ERROR
12// ================================================================================================
13
14#[derive(Debug, thiserror::Error)]
15pub enum KmsSignerError {
16 /// The KMS backend errored out.
17 #[error("KMS service failure")]
18 KmsServiceError(#[source] Box<SdkError<SignError>>),
19 /// The KMS backend did not error but returned an empty signature.
20 #[error("KMS request returned an empty result")]
21 EmptyBlob,
22 /// The KMS backend returned a signature with an invalid format.
23 #[error("invalid signature format")]
24 SignatureFormatError(#[source] DeserializationError),
25 /// The KMS backend returned a signature that was not able to be verified.
26 #[error("invalid signature")]
27 InvalidSignature,
28}
29
30// KMS SIGNER
31// ================================================================================================
32
33/// Block signer that uses AWS KMS to create signatures.
34pub struct KmsSigner {
35 key_id: String,
36 pub_key: PublicKey,
37 client: aws_sdk_kms::Client,
38}
39
40impl KmsSigner {
41 /// Constructs a new KMS signer and retrieves the corresponding public key from the AWS backend.
42 ///
43 /// The supplied `key_id` must be a valid AWS KMS key ID in the AWS region corresponding to the
44 /// typical `AWS_REGION` env var.
45 ///
46 /// A policy statement such as the following is required to allow a process on an EC2 instance
47 /// to use this signer:
48 /// ```json
49 /// {
50 /// "Sid": "AllowEc2RoleUseOfKey",
51 /// "Effect": "Allow",
52 /// "Principal": {
53 /// "AWS": "arn:aws:iam::<account_id>:role/<role_name>"
54 /// },
55 /// "Action": [
56 /// "kms:Sign",
57 /// "kms:Verify",
58 /// "kms:DescribeKey"
59 /// "kms:GetPublicKey"
60 /// ],
61 /// "Resource": "*"
62 /// },
63 /// ```
64 pub async fn new(key_id: impl Into<String>) -> anyhow::Result<Self> {
65 let version = aws_config::BehaviorVersion::v2026_01_12();
66 let config = aws_config::load_defaults(version).await;
67 let client = aws_sdk_kms::Client::new(&config);
68 let key_id = key_id.into();
69
70 // Retrieve DER-encoded SPKI.
71 let pub_key_output = client.get_public_key().key_id(key_id.clone()).send().await?;
72 let spki_der = pub_key_output.public_key().ok_or(KmsSignerError::EmptyBlob)?.as_ref();
73
74 // Decode the compressed SPKI as a Miden public key.
75 let pub_key = PublicKey::from_der(spki_der)?;
76 Ok(Self { key_id, pub_key, client })
77 }
78
79 pub async fn sign(&self, commitment: Word) -> Result<Signature, KmsSignerError> {
80 // The Validator produces Ethereum-style ECDSA (secp256k1) signatures over Keccak-256
81 // digests. AWS KMS does not support SHA-3 hashing for ECDSA keys (ECC_SECG_P256K1 being the
82 // corresponding AWS key-spec), so we pre-hash the message and pass MessageType::Digest. KMS
83 // signs the provided 32-byte digest verbatim.
84 let msg = commitment.to_bytes();
85 let digest = Keccak256::hash(&msg);
86
87 // Request signature from KMS backend.
88 let sign_output = self
89 .client
90 .sign()
91 .key_id(&self.key_id)
92 .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256)
93 .message_type(aws_sdk_kms::types::MessageType::Digest)
94 .message(digest.to_bytes().into())
95 .send()
96 .await
97 .map_err(Box::from)
98 .map_err(KmsSignerError::KmsServiceError)?;
99
100 // Decode DER-encoded signature.
101 let sig_der = sign_output.signature().ok_or(KmsSignerError::EmptyBlob)?;
102 // Recovery id is not used by verify(pk), so 0 is fine.
103 let recovery_id = 0;
104 let sig = Signature::from_der(sig_der.as_ref(), recovery_id)
105 .map_err(KmsSignerError::SignatureFormatError)?;
106
107 // Check the returned signature.
108 if sig.verify(commitment, &self.pub_key) {
109 Ok(sig)
110 } else {
111 Err(KmsSignerError::InvalidSignature)
112 }
113 }
114
115 pub fn public_key(&self) -> PublicKey {
116 self.pub_key.clone()
117 }
118}
119
120// KMS KEY MATERIAL DECRYPTION
121// ================================================================================================
122
123/// Recovers key material wrapped with an AWS KMS key by calling `kms:Decrypt`.
124///
125/// The ciphertext must have been produced by `kms:Encrypt` under a symmetric KMS key. The KMS key
126/// ID is embedded in the ciphertext blob, so it does not need to be supplied. The caller's AWS
127/// identity requires the `kms:Decrypt` permission on that key, analogous to the policy documented
128/// on [`KmsSigner::new`].
129///
130/// Note that unlike [`KmsSigner`], where the private key never leaves KMS, the decrypted key
131/// material is returned to and held by the calling process.
132pub async fn decrypt_key_material(ciphertext: Vec<u8>) -> anyhow::Result<Vec<u8>> {
133 let version = aws_config::BehaviorVersion::v2026_01_12();
134 let config = aws_config::load_defaults(version).await;
135 let client = aws_sdk_kms::Client::new(&config);
136
137 let output = client
138 .decrypt()
139 .ciphertext_blob(Blob::new(ciphertext))
140 .send()
141 .await
142 .context("KMS decrypt request failed")?;
143
144 let plaintext = output.plaintext().context("KMS decrypt returned no plaintext")?;
145 Ok(plaintext.as_ref().to_vec())
146}