Skip to main content

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