Skip to main content

miden_validator/signers/
kms.rs

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