Skip to main content

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}