Skip to content

Comments

feat: KMS BlockSigner#1677

Open
sergerad wants to merge 36 commits intonextfrom
sergerad-signing-backend
Open

feat: KMS BlockSigner#1677
sergerad wants to merge 36 commits intonextfrom
sergerad-signing-backend

Conversation

@sergerad
Copy link
Collaborator

@sergerad sergerad commented Feb 16, 2026

Context

The genesis bootstrap and validator processes currently only support ECDSA private keys via command line. We want to be able to perform genesis and validator block signing through more secure, remote backends such as AWS KMS.

Relates to #1316.

Tested locally against AWS KMS (both bootstrap and chain running with separate validator).

Changes

  • Add BlockSigner trait (removed from miden-base).
  • Add KmsSigner struct which implements BlockSigner.
  • Update all command scaffolding to allow for bootstrap command and validator command to use either local key or KMS key.
  • Bump toolchain to Rust 1.91 (for latest aws SDK support).

@sergerad sergerad marked this pull request as ready for review February 19, 2026 00:57
UnvalidatedTransactions(Vec<TransactionId>),
#[error("failed to build block")]
BlockBuildingFailed(#[source] ProposedBlockError),
BlockBuildingFailed(#[from] ProposedBlockError),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
BlockBuildingFailed(#[from] ProposedBlockError),
BlockBuildingFailed(#[source] ProposedBlockError),

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really should not use #[from]. It makes it too easy to cast things incorrectly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea sorry I have been stubborn about this. Any references you have for me to look at on that topic? Like linters/style guides. If we have consensus about not using #[from] at all I'll make sure not to in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have anything explicitly about this; we had this general error discussion 0xMiden/protocol#966, maybe @PhilippGackstatter has one for #[from] vs `#[source]?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this blog post motivates adding context well: https://fast.github.io/blog/stop-forwarding-errors-start-designing-them/. In particular the "The Problem: Error Handling Without Purpose" section:

When debugging, does “serialization error: expected , or }” tell you which request, which field, which code path led here?

#[source] is better than #[from] because it forces you to give some thought on how to best add context.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks yea the article is insightful.

#[source] is better than #[from] because it forces you to give some thought on how to best add context.

I agree with the notion that it could prompt you to add more context. But I don't think it ensures that. And I don't think it is necessary to achieve the goal of ensuring context being added when it is needed or formatting errors in such a way that they dont just forward/bubble up info.

Have updated the PR to use #[source] in any case!

Comment on lines +28 to +30
async fn sign(&self, header: &BlockHeader) -> Result<Signature, Self::Error> {
Ok(self.sign(header.commitment()))
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not a blocking operation? How long does signing take?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its async only because of the trait definition

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but we shouldn't make blocking calls in an async function. Apparently we can expect this to take a few ms to maybe 100ms which is high but probably acceptable for us.

Its the downside of having this trait -- it doesn't really match reality; one call is async IO, the other is local blocking sync function.

So really this should be doing something like

tokio::task::block_in_place(self.sign(header.commitment())).await

but that isn't great either.

type Error = KmsSignerError;

async fn sign(&self, header: &BlockHeader) -> Result<Signature, Self::Error> {
// KMS backend doesn't support Keccak-256 so we do it ourselves.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand on what this means?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated - anything still unclear?

        // AWS KMS does not support SHA3 (Keccak-256), so we need to produce the digest ourselves.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - I don't know how to produce such a digest. Is there an algorithm you followed, or how did you know what to do?

Currently I'm just taking it as being okay; but you could really have written any algorithm here and I wouldn't know any different.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aka how can I audit this code to ensure its correct.

Copy link
Collaborator Author

@sergerad sergerad Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about now?

        // The Validator produces Ethereum-style ECDSA (secp256k1) signatures over Keccak-256
        // digests. AWS KMS does not support SHA-3 hashing for ECDSA keys
        // (ECC_SECG_P256K1 being the corresponding AWS key-spec), so we pre-hash the
        // message and pass MessageType::Digest. KMS signs the provided 32-byte digest
        // verbatim.

Comment on lines +61 to +62
#[group(required = true, multiple = false)]
pub struct ValidatorKey {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested that this works as expected? The default with an optional and group required sounds like it might be wonky. I think its correctly defined, I'm just curious whether clap handles this properly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❮ ./target/debug/miden-node validator start --data-directory /tmp/1 http://0.0.0.0:9191 --key.kms-id bded4b64-636a-4d27-8475-c2b8f8e76e75 --key.hex asdf

error: the argument '--key.kms-id <VALIDATOR_KMS_KEY_ID>' cannot be used with '--key.hex <VALIDATOR_KEY>'
❯ ./target/debug/miden-node bundled bootstrap --validator.key.hex asdb --validator.key.kms-id asdf

error: the argument '--validator.key.hex <VALIDATOR_KEY>' cannot be used with '--validator.key.kms-id <VALIDATOR_KMS_KEY_ID>'

Comment on lines +55 to +62
/// Cannot be used with `key.kms-id`.
#[arg(
long = "key.hex",
env = ENV_VALIDATOR_KEY,
value_name = "VALIDATOR_KEY",
default_value = INSECURE_VALIDATOR_KEY_HEX,
group = "key"
)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. And you have to repeat because its a different arg prefix?

We should take a look at conf which builds on top of clap-derive but seems less weird around use cases like this.

But for now this is fine 👍

UnvalidatedTransactions(Vec<TransactionId>),
#[error("failed to build block")]
BlockBuildingFailed(#[source] ProposedBlockError),
BlockBuildingFailed(#[from] ProposedBlockError),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have anything explicitly about this; we had this general error discussion 0xMiden/protocol#966, maybe @PhilippGackstatter has one for #[from] vs `#[source]?

type Error = KmsSignerError;

async fn sign(&self, header: &BlockHeader) -> Result<Signature, Self::Error> {
// KMS backend doesn't support Keccak-256 so we do it ourselves.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - I don't know how to produce such a digest. Is there an algorithm you followed, or how did you know what to do?

Currently I'm just taking it as being okay; but you could really have written any algorithm here and I wouldn't know any different.

type Error = KmsSignerError;

async fn sign(&self, header: &BlockHeader) -> Result<Signature, Self::Error> {
// KMS backend doesn't support Keccak-256 so we do it ourselves.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aka how can I audit this code to ensure its correct.

Comment on lines +28 to +30
async fn sign(&self, header: &BlockHeader) -> Result<Signature, Self::Error> {
Ok(self.sign(header.commitment()))
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but we shouldn't make blocking calls in an async function. Apparently we can expect this to take a few ms to maybe 100ms which is high but probably acceptable for us.

Its the downside of having this trait -- it doesn't really match reality; one call is async IO, the other is local blocking sync function.

So really this should be doing something like

tokio::task::block_in_place(self.sign(header.commitment())).await

but that isn't great either.

Comment on lines 226 to 241
let signer = validator_key.into_signer().await?;
match signer {
ValidatorSigner::Kms(signer) => {
Self::bootstrap_with_signer(config, signer, accounts_directory, data_directory)
.await
},
ValidatorSigner::Local(signer) => {
Self::bootstrap_with_signer(config, signer, accounts_directory, data_directory)
.await
},
}
}

async fn bootstrap_with_signer(
config: GenesisConfig,
signer: impl BlockSigner,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe &dyn would make it easier?

Suggested change
let signer = validator_key.into_signer().await?;
match signer {
ValidatorSigner::Kms(signer) => {
Self::bootstrap_with_signer(config, signer, accounts_directory, data_directory)
.await
},
ValidatorSigner::Local(signer) => {
Self::bootstrap_with_signer(config, signer, accounts_directory, data_directory)
.await
},
}
}
async fn bootstrap_with_signer(
config: GenesisConfig,
signer: impl BlockSigner,
let signer = validator_key.into_signer().await?;
Self::bootstrap_with_signer(config, &signer, accounts_directory, data_directory).await
}
async fn bootstrap_with_signer(
config: GenesisConfig,
signer: &dyn BlockSigner,

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately the associated (Error) type is problematic for trait object

the value of the associated type `Error` in `miden_node_utils::signer::BlockSigner` must be specified

We can look at removing the trait instead after this PR maybe.

@sergerad sergerad force-pushed the sergerad-signing-backend branch from 34a4837 to d35c8e4 Compare February 23, 2026 00:06
@sergerad sergerad requested a review from bobbinth February 24, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants