Skip to content

feat: Add AWS KMS signer backend#3171

Open
alpe wants to merge 7 commits intomainfrom
alex/3163_kms
Open

feat: Add AWS KMS signer backend#3171
alpe wants to merge 7 commits intomainfrom
alex/3163_kms

Conversation

@alpe
Copy link
Contributor

@alpe alpe commented Mar 17, 2026

Part of #3163

Overview

Manual test:

Setup AWS permission:

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "AllowKeyCreation",
			"Effect": "Allow",
			"Action": [
				"kms:CreateKey",
				"kms:TagResource",
				"kms:EnableKey",
				"kms:PutKeyPolicy",
				"kms:GetPublicKey",
				"kms:Sign",
				"kms:ListKeys",
				"kms:ListAliases"

			],
			"Resource": "*"
		}
	]
}

Create KMS key:

 aws kms create-key \ 
  --description "ev-node signer" \
  --key-usage SIGN_VERIFY \
  --key-spec ECC_NIST_EDWARDS25519

Copy KeyId from response.

export EVNODE_E2E_AWS_KMS_KEY_ID=<keyId>
export EVNODE_E2E_AWS_KMS_REGION=
cd test/e2e
go test -v -tags e2e,evm -run TestEvmSequencerWithAWSKMSSignerE2E -count=1 --evm-binary=$(pwd)/../../build/testapp

Summary by CodeRabbit

  • New Features

    • Added AWS KMS signer backend, enabling sequencer nodes to use AWS Key Management Service for remote key management and cryptographic operations
  • Documentation

    • Added comprehensive AWS KMS signer setup guide covering prerequisites, IAM policies, configuration, and troubleshooting
  • Refactor

    • Enhanced signer interface to support context-aware signing operations for improved cancellation and timeout handling

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

This pull request adds AWS KMS signer backend support by introducing context-aware signing throughout the codebase. It implements a new AWS KMS signer package, establishes a signer factory pattern for creating signers based on configuration, updates the core Signer interface to accept context, and threads context through all signing operations. AWS SDK v2 dependencies are added, along with configuration flags, documentation, and end-to-end tests.

Changes

Cohort / File(s) Summary
AWS KMS Signer Implementation
pkg/signer/aws/signer.go, pkg/signer/aws/signer_test.go, pkg/signer/aws/README.md
New AWS KMS-backed Ed25519 signer with NewKmsSigner constructor, public key caching, exponential backoff retry logic, and configurable timeout/max-retry options. Includes comprehensive mock-based test coverage for signing, retries, and error cases.
Signer Factory Pattern
pkg/signer/factory.go, pkg/signer/factory_test.go
New factory entrypoints NewSigner and NewSignerForInit that delegate signer creation to helpers based on SignerType (file or awskms), with error handling for unknown types and directory management for file signers.
Signer Interface Context Support
pkg/signer/signer.go, pkg/signer/file/local.go, pkg/signer/noop/signer.go, pkg/signer/file/file_signer_test.go, pkg/signer/noop/signer_test.go
Updated Signer.Sign interface to accept context.Context as first parameter. Implemented in file signer and noop signer with early context error checks. Updated all test invocations to pass context.
File Signer Documentation
pkg/signer/file/README.md, pkg/signer/file/doc.go, pkg/signer/file/example_test.go
Updated documentation examples and tests to reflect new Sign(ctx, message) signature requiring context parameter.
Configuration & Validation
pkg/config/config.go, pkg/config/defaults.go, pkg/config/config_test.go
Added SignerConfig fields for AWS KMS (KmsKeyID, KmsRegion, KmsProfile, KmsTimeout, KmsMaxRetries), corresponding CLI flag constants, flag registration, and conditional validation for KMS configuration.
Init Command Updates
apps/evm/cmd/init.go, apps/grpc/cmd/init.go, apps/testapp/cmd/init.go, pkg/cmd/init.go, pkg/cmd/init_test.go
Updated all init command handlers to pass cmd.Context() into rollcmd.CreateSigner, and refactored CreateSigner to use signer factory with context support. Removed manual passphrase/path handling in favor of factory delegation.
Block Signing Context Threading
block/internal/executing/executor.go, block/internal/submitting/da_submitter.go
Updated block header signing and DA envelope signing to pass context through the signing chain. signHeader and signing helpers now accept and propagate context to signer.Sign calls.
Signing Call Sites in Tests
block/internal/submitting/da_submitter_integration_test.go, block/internal/submitting/da_submitter_test.go, block/internal/submitting/submitter_test.go, block/internal/syncing/da_retriever_strict_test.go, block/internal/syncing/da_retriever_test.go, block/internal/syncing/p2p_handler_test.go, block/internal/syncing/syncer_test.go
Updated test signer invocations to pass t.Context() into Sign calls, and updated fake/mock signers to match the context-aware interface.
Type Utils Context Propagation
types/utils.go, types/utils_test.go, types/signed_header_test.go, pkg/sync/sync_service_test.go, pkg/store/store_adapter_test.go
Added context parameter to signed header generation functions (GetRandomSignedHeaderCustom, GetRandomNextSignedHeader, GetFirstSignedHeader, GetSignature). Updated callers and tests to pass context.Background() or t.Context().
Runtime Node Initialization
pkg/cmd/run_node.go
Refactored signer loading to use factory pattern via NewSigner, removed manual file-system and passphrase handling, delegated creation to factory based on SignerType. Added logging for AWS KMS signer initialization.
Dependencies
go.mod, apps/evm/go.mod, apps/grpc/go.mod, apps/testapp/go.mod, test/e2e/go.mod
Added AWS SDK for Go v2 core modules and KMS service client (github.com/aws/aws-sdk-go-v2, config, credentials, KMS service, Smithy), along with internal AWS packages for credentials/endpoints/signing.
Documentation & E2E Tests
CHANGELOG.md, docs/.vitepress/config.ts, docs/guides/operations/aws-kms-signer.md, test/e2e/evm_aws_kms_e2e_test.go
Added changelog entry for AWS KMS signer, sidebar navigation link, comprehensive AWS KMS setup guide with prerequisites/IAM policy/configuration/troubleshooting, and end-to-end test verifying EVM sequencer with AWS KMS signer can process transactions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • Feature: Add KMS signer backend #3163: This PR directly implements the AWS KMS signer backend feature requested in this issue, including new pkg/signer/aws package, signer factory wiring, context-aware Signer.Sign interface, configuration flags, AWS SDK dependencies, and comprehensive testing.

Possibly related PRs

  • feat: remote signing #2078: Both PRs refactor signer abstraction and introduce remote-signing support patterns; this PR extends that foundation by adding AWS KMS as a remote signer backend and making signing operations context-aware throughout the codebase.

Suggested reviewers

  • tac0turtle
  • julienrbrt
  • Manav-Aggarwal

Poem

🐰 Whiskers twitching with delight,
AWS keys shine so bright,
Context flows through every call,
Factory builds signers for all,
KMS magic, no secrets revealed!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description is incomplete. While it references the related issue (#3163) and provides manual test instructions with IAM permissions, KMS key creation commands, and E2E test invocation, it lacks the required overview section that should explain the PR's goal, rationale, and context. Add a clear 'Overview' section explaining the purpose, goals, and rationale for adding the AWS KMS signer backend support to the codebase.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat: Add AWS KMS signer backend' accurately summarizes the main change: introducing AWS KMS as a new signer backend.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch alex/3163_kms
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

The latest Buf updates on your PR. Results from workflow CI / buf-check (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed✅ passedMar 19, 2026, 1:28 PM

@claude
Copy link
Contributor

claude bot commented Mar 17, 2026

Claude finished @alpe's task in 2m 1s —— View job


Code Review: feat: Add AWS KMS signer backend

  • Gather context and understand the request
  • Read core implementation files
  • Review AWS KMS signer implementation
  • Review factory and config changes
  • Review tests
  • Review documentation
  • Post comprehensive review feedback

Overall this is a solid, well-scoped implementation. The interface-based design (KMSClient), eager public key caching, and retry/backoff logic are all good patterns. A few issues worth addressing before merge:


Issues

🐛 Default timeout inconsistency — pkg/signer/aws/signer.go

The Options struct comment documents "Default: 10s" but kmsSignerFromClient (line 87) actually defaults to 1s:

// Options struct comment says: "Timeout for individual KMS Sign API calls. Default: 10s."
o := Options{Timeout: 1 * time.Second, MaxRetries: 3}  // <-- 1s, not 10s

Meanwhile DefaultConfig() in defaults.go sets KmsTimeout: DurationWrapper{10 * time.Second}. In practice the config default wins (10s), but if a caller uses kmsSignerFromClient directly (e.g. in tests with nil opts), they get 1s. Either align the constant to 10s or fix the doc comment.

Fix this →


🐛 time.After goroutine/timer leak in retry loop — pkg/signer/aws/signer.go:167

case <-time.After(backoff):

time.After allocates a timer that cannot be garbage collected until it fires. In a retry loop this accumulates timers until they each expire. Prefer time.NewTimer with a deferred Stop():

timer := time.NewTimer(backoff)
select {
case <-ctx.Done():
    timer.Stop()
    return nil, fmt.Errorf("KMS Sign canceled: %w", ctx.Err())
case <-timer.C:
}

Fix this →


⚠️ Magic string literals for signer type — multiple files

The string "awskms" (and "file") appears as a raw literal in at least 5 files:

  • pkg/signer/factory.go:28,51
  • pkg/config/config.go:410
  • pkg/cmd/run_node.go:113,140
  • pkg/cmd/init.go:20
  • test/e2e/evm_aws_kms_e2e_test.go:56,85

A typo in any one of these would silently fall through to "unknown signer type". Consider exporting constants from the signer or config package:

const (
    SignerTypeFile   = "file"
    SignerTypeAWSKMS = "awskms"
)

Fix this →


⚠️ Retry backoff can overflow for large MaxRetriespkg/signer/aws/signer.go:163

backoff := time.Duration(100<<uint(attempt-1)) * time.Millisecond

With attempt >= 64, the left-shift overflows int. The current default of 3 makes this unreachable in practice, but there is no guard against a user setting KmsMaxRetries to a large value. A simple cap (e.g. max 30s backoff) would be safer.


ℹ️ Unnecessary getter methods — pkg/signer/aws/signer.go:39-41

func (o *Options) timeout() time.Duration { return o.Timeout }
func (o *Options) maxRetries() int         { return o.MaxRetries }

These are used exactly once each and add no logic. Direct field access (s.opts.Timeout, s.opts.MaxRetries) is clearer.


ℹ️ Asymmetric startup logging — pkg/cmd/run_node.go:140-143

Only the KMS signer gets an explicit log message. The file signer is silent. Either log all signer types uniformly (e.g. move into NewSigner itself), or log the signer type as a structured field on a single message:

logger.Info().Str("signer_type", nodeConfig.Signer.SignerType).Msg("initialized signer")

ℹ️ SignerConfig validation doesn't guard against zero KmsTimeout in defaults — pkg/config/config.go:414

if c.Signer.KmsTimeout.Duration <= 0 {
    return errors.New("evnode.signer.kms_timeout must be positive")
}

This correctly rejects zero. However, validation only runs when signer_type == "awskms". If a user accidentally sets kms_timeout: 0s in their YAML but uses the file signer, no error is raised. This is fine behavior-wise but worth a comment.


Positive highlights

  • Interface extraction (KMSClient) makes the unit tests clean and avoids needing a real AWS client.
  • Eager public key fetch at construction — fail fast at startup rather than at first signing attempt.
  • Retry with exponential backoff and context cancellation is well implemented; retryable vs. non-retryable error classification covers the important AWS error codes.
  • RWMutex for cached state correctly protects the concurrent read path.
  • E2E test is nicely structured — skips cleanly when env vars are absent, and checks actual transaction inclusion.
  • Documentation clearly separates "all permissions for key creation" from "minimum runtime permissions" — a security win.

@codecov
Copy link

codecov bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 71.30045% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.23%. Comparing base (d27d099) to head (5363cdb).

Files with missing lines Patch % Lines
pkg/signer/aws/signer.go 68.46% 25 Missing and 10 partials ⚠️
pkg/cmd/run_node.go 50.00% 6 Missing and 4 partials ⚠️
pkg/config/config.go 46.15% 6 Missing and 1 partial ⚠️
pkg/cmd/init.go 73.33% 2 Missing and 2 partials ⚠️
pkg/signer/factory.go 84.61% 2 Missing and 2 partials ⚠️
block/internal/submitting/da_submitter.go 80.00% 2 Missing ⚠️
pkg/signer/noop/signer.go 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3171      +/-   ##
==========================================
+ Coverage   61.14%   61.23%   +0.09%     
==========================================
  Files         117      119       +2     
  Lines       12082    12235     +153     
==========================================
+ Hits         7387     7492     +105     
- Misses       3868     3904      +36     
- Partials      827      839      +12     
Flag Coverage Δ
combined 61.23% <71.30%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alpe alpe changed the title feat: Add KMS signer backend feat: Add AWS KMS signer backend Mar 19, 2026
@alpe alpe marked this pull request as ready for review March 19, 2026 13:35
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
types/utils_test.go (1)

82-82: Consider using t.Context() for consistency.

While context.Background() works correctly here, other test files in this PR use t.Context() which is more idiomatic for tests. It automatically cancels when the test completes, providing better cleanup semantics.

🔧 Suggested change
-			firstSignedHeader, err := types.GetFirstSignedHeader(context.Background(), noopSigner, tc.chainID)
+			firstSignedHeader, err := types.GetFirstSignedHeader(t.Context(), noopSigner, tc.chainID)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/utils_test.go` at line 82, Replace the call to context.Background()
with the test's cancellable context by using t.Context() when invoking
types.GetFirstSignedHeader; update the specific invocation firstSignedHeader,
err := types.GetFirstSignedHeader(context.Background(), noopSigner, tc.chainID)
to pass t.Context() instead so the test uses the per-test context and cancels
automatically when the test finishes.
pkg/cmd/init_test.go (1)

49-50: Line 49 now overstates the scenario.

This subtest is specifically exercising an unknown signer type (remote), not non-file signers in general. Renaming the case description will keep the test intent aligned with what it actually covers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cmd/init_test.go` around lines 49 - 50, Update the test case description
to accurately reflect that this subtest exercises an unknown "remote" signer
type: change the comment and/or header text that currently reads "Case 3:
Non-File signer, Aggregator -> Error (unknown signer type)" to something like
"Case 3: Remote signer, Aggregator -> Error (unknown signer type)" so the
t.Run("RemoteSigner_Aggregator", ...) intent matches the comment.
pkg/cmd/run_node.go (1)

140-142: Consider enhancing AWS KMS log with key ID for observability.

Including the key ID (or a truncated version) in the log message would help operators verify which KMS key is in use during startup.

🔧 Optional enhancement
 		if nodeConfig.Signer.SignerType == "awskms" {
-			logger.Info().Msg("initialized AWS KMS signer via factory")
+			logger.Info().
+				Str("kms_key_id", nodeConfig.Signer.KmsKeyID).
+				Msg("initialized AWS KMS signer via factory")
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/cmd/run_node.go` around lines 140 - 142, The AWS KMS startup log only
notes the signer type; update the block that checks nodeConfig.Signer.SignerType
== "awskms" to also read the KMS key identifier from the signer config (e.g.,
nodeConfig.Signer.KeyID or nodeConfig.Signer.AwsKmsKeyID), truncate it for
safety (for example to the first 8 characters) and include that truncated key id
in the logger.Info() message so operators can see which KMS key was used while
avoiding full secret exposure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/config/config.go`:
- Around line 305-306: The SignerType struct tag comment currently lists "(file,
awskms)" but the CLI flag description includes "(file, grpc, awskms)"; make them
consistent by either adding "grpc" to the SignerType field comment (SignerType
string `mapstructure:"signer_type" yaml:"signer_type" comment:"Type of remote
signer to use (file, grpc, awskms)"`) or by removing "grpc" from the CLI flag
help text if grpc is not supported—update the comment associated with SignerType
(and SignerPath documentation if needed) or the flag description in the CLI
registration so both list the same valid signer types.

In `@pkg/signer/aws/README.md`:
- Around line 3-15: Update the README for the awskms signer to add explicit IAM
permissions and KMS key configuration required: state that the IAM principal
needs kms:GetPublicKey and kms:Sign on the configured key, and that the KMS key
must be created with KeyUsage=SIGN_VERIFY and KeySpec=ECC_NIST_EDWARDS25519;
mention that the implementation of signer.Signer calls GetPublicKey eagerly
during initialization (and fails fast on misconfiguration) and uses Sign for
signing operations so those permissions/settings are mandatory for successful
initialization and runtime signing.

In `@pkg/signer/aws/signer.go`:
- Around line 31-37: The documented default Timeout in the Options struct (10s)
is inconsistent with the fallback used in kmsSignerFromClient (1s); update the
implementation to use a single default (e.g., defaultTimeout = 10 * time.Second)
and ensure kmsSignerFromClient and any other fallback logic use that constant
when opts is nil or opts.Timeout <= 0; modify the fallback for MaxRetries
similarly if needed and verify all uses (notably kmsSignerFromClient and the
related sign/constructor paths) reference the same default constants instead of
hardcoded values.
- Around line 207-216: GetAddress currently returns the backing slice s.address
from KmsSigner allowing callers to mutate cached state after the RLock is
released; while holding the read lock (in KmsSigner.GetAddress) allocate a new
byte slice, copy the contents of s.address into it, and return that copy instead
of the original slice (keep the existing nil check and error path, but ensure
the copy is created before releasing s.mu.RUnlock so callers cannot corrupt the
signer's cached identity).

In `@pkg/signer/factory.go`:
- Around line 33-35: Reject empty signer_path in the signer factory before
calling filepath.Abs: retrieve the raw value used (config.Signer.SignerPath or
the trimmed value from strings.TrimSuffix(config.Signer.SignerPath,
"signer.json")), check if it is empty (after trimming) and return a clear error
instead of calling filepath.Abs; only call filepath.Abs when the trimmed signer
path is non-empty and then continue with existing resolution logic that assigns
to signerPath. Ensure the validation happens in the same initialization area
where signerPath is computed (the code using filepath.Abs and signerPath) so
misconfiguration cannot fall back to the process CWD.

In `@types/utils.go`:
- Around line 192-198: GetRandomNextSignedHeader is building
newSignedHeader.Header via GetRandomNextHeader but then calls GetSignature with
the old signedHeader.Header, producing a signature that won't verify; change the
call to GetSignature(ctx, newSignedHeader.Header, signer), assign the returned
signature to newSignedHeader.Signature (or the appropriate field on
SignedHeader), and preserve the existing error check/return so the function
returns the newly built SignedHeader with a signature over its own Header.

---

Nitpick comments:
In `@pkg/cmd/init_test.go`:
- Around line 49-50: Update the test case description to accurately reflect that
this subtest exercises an unknown "remote" signer type: change the comment
and/or header text that currently reads "Case 3: Non-File signer, Aggregator ->
Error (unknown signer type)" to something like "Case 3: Remote signer,
Aggregator -> Error (unknown signer type)" so the
t.Run("RemoteSigner_Aggregator", ...) intent matches the comment.

In `@pkg/cmd/run_node.go`:
- Around line 140-142: The AWS KMS startup log only notes the signer type;
update the block that checks nodeConfig.Signer.SignerType == "awskms" to also
read the KMS key identifier from the signer config (e.g.,
nodeConfig.Signer.KeyID or nodeConfig.Signer.AwsKmsKeyID), truncate it for
safety (for example to the first 8 characters) and include that truncated key id
in the logger.Info() message so operators can see which KMS key was used while
avoiding full secret exposure.

In `@types/utils_test.go`:
- Line 82: Replace the call to context.Background() with the test's cancellable
context by using t.Context() when invoking types.GetFirstSignedHeader; update
the specific invocation firstSignedHeader, err :=
types.GetFirstSignedHeader(context.Background(), noopSigner, tc.chainID) to pass
t.Context() instead so the test uses the per-test context and cancels
automatically when the test finishes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a074a34e-fd54-4bb5-acf1-846e6f9e5871

📥 Commits

Reviewing files that changed from the base of the PR and between d27d099 and 5363cdb.

⛔ Files ignored due to path filters (6)
  • apps/evm/go.sum is excluded by !**/*.sum
  • apps/grpc/go.sum is excluded by !**/*.sum
  • apps/testapp/go.sum is excluded by !**/*.sum
  • execution/evm/test/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
  • test/e2e/go.sum is excluded by !**/*.sum
📒 Files selected for processing (45)
  • CHANGELOG.md
  • apps/evm/cmd/init.go
  • apps/evm/go.mod
  • apps/grpc/cmd/init.go
  • apps/grpc/go.mod
  • apps/testapp/cmd/init.go
  • apps/testapp/go.mod
  • block/internal/executing/executor.go
  • block/internal/submitting/da_submitter.go
  • block/internal/submitting/da_submitter_integration_test.go
  • block/internal/submitting/da_submitter_test.go
  • block/internal/submitting/submitter_test.go
  • block/internal/syncing/da_retriever_strict_test.go
  • block/internal/syncing/da_retriever_test.go
  • block/internal/syncing/p2p_handler_test.go
  • block/internal/syncing/syncer_test.go
  • docs/.vitepress/config.ts
  • docs/guides/operations/aws-kms-signer.md
  • go.mod
  • pkg/cmd/init.go
  • pkg/cmd/init_test.go
  • pkg/cmd/run_node.go
  • pkg/config/config.go
  • pkg/config/config_test.go
  • pkg/config/defaults.go
  • pkg/signer/aws/README.md
  • pkg/signer/aws/signer.go
  • pkg/signer/aws/signer_test.go
  • pkg/signer/factory.go
  • pkg/signer/factory_test.go
  • pkg/signer/file/README.md
  • pkg/signer/file/doc.go
  • pkg/signer/file/example_test.go
  • pkg/signer/file/file_signer_test.go
  • pkg/signer/file/local.go
  • pkg/signer/noop/signer.go
  • pkg/signer/noop/signer_test.go
  • pkg/signer/signer.go
  • pkg/store/store_adapter_test.go
  • pkg/sync/sync_service_test.go
  • test/e2e/evm_aws_kms_e2e_test.go
  • test/e2e/go.mod
  • types/signed_header_test.go
  • types/utils.go
  • types/utils_test.go

Comment on lines +305 to +306
SignerType string `mapstructure:"signer_type" yaml:"signer_type" comment:"Type of remote signer to use (file, awskms)"`
SignerPath string `mapstructure:"signer_path" yaml:"signer_path" comment:"Path to the signer file or address"`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor inconsistency in signer type documentation.

The struct field comment on line 305 lists (file, awskms) but the flag description on line 577 lists (file, grpc, awskms). These should be consistent.

📝 Proposed fix

Either update the struct comment to match the flag:

-	SignerType    string          `mapstructure:"signer_type" yaml:"signer_type" comment:"Type of remote signer to use (file, awskms)"`
+	SignerType    string          `mapstructure:"signer_type" yaml:"signer_type" comment:"Type of remote signer to use (file, grpc, awskms)"`

Or update the flag description if grpc is not a valid type:

-	cmd.Flags().String(FlagSignerType, def.Signer.SignerType, "type of signer to use (file, grpc, awskms)")
+	cmd.Flags().String(FlagSignerType, def.Signer.SignerType, "type of signer to use (file, awskms)")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SignerType string `mapstructure:"signer_type" yaml:"signer_type" comment:"Type of remote signer to use (file, awskms)"`
SignerPath string `mapstructure:"signer_path" yaml:"signer_path" comment:"Path to the signer file or address"`
SignerType string `mapstructure:"signer_type" yaml:"signer_type" comment:"Type of remote signer to use (file, grpc, awskms)"`
SignerPath string `mapstructure:"signer_path" yaml:"signer_path" comment:"Path to the signer file or address"`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/config.go` around lines 305 - 306, The SignerType struct tag
comment currently lists "(file, awskms)" but the CLI flag description includes
"(file, grpc, awskms)"; make them consistent by either adding "grpc" to the
SignerType field comment (SignerType string `mapstructure:"signer_type"
yaml:"signer_type" comment:"Type of remote signer to use (file, grpc, awskms)"`)
or by removing "grpc" from the CLI flag help text if grpc is not
supported—update the comment associated with SignerType (and SignerPath
documentation if needed) or the flag description in the CLI registration so both
list the same valid signer types.

Comment on lines +3 to +15
This package implements `signer.Signer` using AWS KMS.

It uses KMS for `Sign` operations and caches the public key/address in memory after initialization.

## Requirements

- AWS credentials must be available via the standard AWS SDK credential chain.
- The configured KMS key must be an asymmetric **Ed25519** key.

## Configuration

Set `evnode.signer.signer_type` to `awskms` and provide at least `kms_key_id`.

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd pkg/signer/aws && find . -type f -name "*.md" -o -name "*.go" | head -20

Repository: evstack/ev-node

Length of output: 99


🏁 Script executed:

cat -n pkg/signer/aws/README.md

Repository: evstack/ev-node

Length of output: 1086


🏁 Script executed:

cd pkg/signer/aws && ls -la

Repository: evstack/ev-node

Length of output: 352


🏁 Script executed:

cd pkg/signer/aws && wc -l *.go | tail -1

Repository: evstack/ev-node

Length of output: 70


🏁 Script executed:

cat -n pkg/signer/aws/signer.go

Repository: evstack/ev-node

Length of output: 8302


🏁 Script executed:

cat -n pkg/signer/aws/signer_test.go | head -100

Repository: evstack/ev-node

Length of output: 3771


🌐 Web query:

AWS KMS Ed25519 key creation KeyUsage SIGN_VERIFY KeySpec ECC_NIST_EDWARDS25519

💡 Result:

Yes, AWS KMS supports creating an asymmetric key with KeySpec 'ECC_NIST_EDWARDS25519' (Ed25519 on Edwards25519 curve) and KeyUsage 'SIGN_VERIFY'. This feature was announced in November 2025 and is available as of 2026-03-19. To create such a key, use the CreateKey API or AWS CLI command: aws kms create-key \ --key-spec ECC_NIST_EDWARDS25519 \ --key-usage SIGN_VERIFY This creates a customer-managed KMS key for signing and verification using EdDSA (Ed25519). The key supports signing algorithms like ED25519_SHA_512 (requires MessageType: RAW) and ED25519_PH_SHA_512 (requires MessageType: DIGEST). It cannot be used for encryption/decryption or key agreement. KeySpec 'ECC_NIST_EDWARDS25519' is listed in official AWS KMS documentation for asymmetric NIST-standard elliptic curve keys, valid only for SIGN_VERIFY usage.

Citations:


Add KMS permission and key configuration requirements to the README.

The README should explicitly document the IAM permissions and KMS key settings required for this signer to function. Without these details, users following the setup instructions will encounter failures during signer initialization.

Add to the Requirements section:

  • IAM permissions: kms:GetPublicKey and kms:Sign on the specified KMS key
  • KMS key configuration: created with KeyUsage=SIGN_VERIFY and KeySpec=ECC_NIST_EDWARDS25519

The code calls GetPublicKey eagerly during initialization (failing fast on misconfiguration) and uses Sign for all signing operations, both of which require these permissions and key settings to succeed. Users without these configured will see initialization errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signer/aws/README.md` around lines 3 - 15, Update the README for the
awskms signer to add explicit IAM permissions and KMS key configuration
required: state that the IAM principal needs kms:GetPublicKey and kms:Sign on
the configured key, and that the KMS key must be created with
KeyUsage=SIGN_VERIFY and KeySpec=ECC_NIST_EDWARDS25519; mention that the
implementation of signer.Signer calls GetPublicKey eagerly during initialization
(and fails fast on misconfiguration) and uses Sign for signing operations so
those permissions/settings are mandatory for successful initialization and
runtime signing.

Comment on lines +31 to +37
// Options configures optional KmsSigner behaviour.
type Options struct {
// Timeout for individual KMS Sign API calls. Default: 10s.
Timeout time.Duration
// MaxRetries for transient KMS failures during Sign. Default: 3.
MaxRetries int
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align the implementation with the documented default timeout.

Options says the default sign timeout is 10s, but kmsSignerFromClient falls back to 1s when opts is nil or Timeout <= 0. That makes the exported constructor much more aggressive than advertised.

Proposed fix
-	o := Options{Timeout: 1 * time.Second, MaxRetries: 3}
+	o := Options{Timeout: 10 * time.Second, MaxRetries: 3}

Also applies to: 87-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signer/aws/signer.go` around lines 31 - 37, The documented default
Timeout in the Options struct (10s) is inconsistent with the fallback used in
kmsSignerFromClient (1s); update the implementation to use a single default
(e.g., defaultTimeout = 10 * time.Second) and ensure kmsSignerFromClient and any
other fallback logic use that constant when opts is nil or opts.Timeout <= 0;
modify the fallback for MaxRetries similarly if needed and verify all uses
(notably kmsSignerFromClient and the related sign/constructor paths) reference
the same default constants instead of hardcoded values.

Comment on lines +207 to +216
// GetAddress returns the cached address derived from the public key.
func (s *KmsSigner) GetAddress() ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()

if s.address == nil {
return nil, fmt.Errorf("address not loaded")
}

return s.address, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Return a copy of the cached address.

GetAddress currently exposes the backing slice stored on the signer. Any caller can mutate that slice after the lock is released and corrupt the signer's cached identity.

Proposed fix
 func (s *KmsSigner) GetAddress() ([]byte, error) {
 	s.mu.RLock()
-	defer s.mu.RUnlock()
-
 	if s.address == nil {
+		s.mu.RUnlock()
 		return nil, fmt.Errorf("address not loaded")
 	}
 
-	return s.address, nil
+	address := append([]byte(nil), s.address...)
+	s.mu.RUnlock()
+	return address, nil
 }
As per coding guidelines, `**/*.go`: "Be careful with concurrent access to shared state".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// GetAddress returns the cached address derived from the public key.
func (s *KmsSigner) GetAddress() ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if s.address == nil {
return nil, fmt.Errorf("address not loaded")
}
return s.address, nil
// GetAddress returns the cached address derived from the public key.
func (s *KmsSigner) GetAddress() ([]byte, error) {
s.mu.RLock()
if s.address == nil {
s.mu.RUnlock()
return nil, fmt.Errorf("address not loaded")
}
address := append([]byte(nil), s.address...)
s.mu.RUnlock()
return address, nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signer/aws/signer.go` around lines 207 - 216, GetAddress currently
returns the backing slice s.address from KmsSigner allowing callers to mutate
cached state after the RLock is released; while holding the read lock (in
KmsSigner.GetAddress) allocate a new byte slice, copy the contents of s.address
into it, and return that copy instead of the original slice (keep the existing
nil check and error path, but ensure the copy is created before releasing
s.mu.RUnlock so callers cannot corrupt the signer's cached identity).

Comment on lines +33 to +35
// Resolve signer path; allow absolute, relative to node root, or relative to CWD if resolution fails
signerPath, err := filepath.Abs(strings.TrimSuffix(config.Signer.SignerPath, "signer.json"))
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject an empty signer_path before calling filepath.Abs.

filepath.Abs("") resolves to the process working directory, so a misconfigured file signer can silently create/load ./signer.json from wherever the node was started. That's a risky fallback for key material.

Proposed fix
-		signerPath, err := filepath.Abs(strings.TrimSuffix(config.Signer.SignerPath, "signer.json"))
+		signerPath := strings.TrimSpace(config.Signer.SignerPath)
+		if signerPath == "" {
+			return nil, fmt.Errorf("signer_path is required when using local file signer")
+		}
+		signerPath = strings.TrimSuffix(signerPath, "signer.json")
+		if signerPath == "" {
+			signerPath = "."
+		}
+		signerPath, err := filepath.Abs(filepath.Clean(signerPath))
 		if err != nil {
 			return nil, err
 		}
As per coding guidelines, `**/*.go`: "Validate all inputs from external sources".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Resolve signer path; allow absolute, relative to node root, or relative to CWD if resolution fails
signerPath, err := filepath.Abs(strings.TrimSuffix(config.Signer.SignerPath, "signer.json"))
if err != nil {
// Resolve signer path; allow absolute, relative to node root, or relative to CWD if resolution fails
signerPath := strings.TrimSpace(config.Signer.SignerPath)
if signerPath == "" {
return nil, fmt.Errorf("signer_path is required when using local file signer")
}
signerPath = strings.TrimSuffix(signerPath, "signer.json")
if signerPath == "" {
signerPath = "."
}
signerPath, err := filepath.Abs(filepath.Clean(signerPath))
if err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signer/factory.go` around lines 33 - 35, Reject empty signer_path in the
signer factory before calling filepath.Abs: retrieve the raw value used
(config.Signer.SignerPath or the trimmed value from
strings.TrimSuffix(config.Signer.SignerPath, "signer.json")), check if it is
empty (after trimming) and return a clear error instead of calling filepath.Abs;
only call filepath.Abs when the trimmed signer path is non-empty and then
continue with existing resolution logic that assigns to signerPath. Ensure the
validation happens in the same initialization area where signerPath is computed
(the code using filepath.Abs and signerPath) so misconfiguration cannot fall
back to the process CWD.

Comment on lines +192 to +198
func GetRandomNextSignedHeader(ctx context.Context, signedHeader *SignedHeader, signer signer.Signer, chainID string) (*SignedHeader, error) {
newSignedHeader := &SignedHeader{
Header: GetRandomNextHeader(signedHeader.Header, chainID),
Signer: signedHeader.Signer,
}

signature, err := GetSignature(signedHeader.Header, signer)
signature, err := GetSignature(ctx, signedHeader.Header, signer)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sign the newly built header here.

GetRandomNextSignedHeader creates newSignedHeader.Header but still signs signedHeader.Header. The returned SignedHeader therefore carries a signature for different bytes and won't verify.

Proposed fix
-	signature, err := GetSignature(ctx, signedHeader.Header, signer)
+	signature, err := GetSignature(ctx, newSignedHeader.Header, signer)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func GetRandomNextSignedHeader(ctx context.Context, signedHeader *SignedHeader, signer signer.Signer, chainID string) (*SignedHeader, error) {
newSignedHeader := &SignedHeader{
Header: GetRandomNextHeader(signedHeader.Header, chainID),
Signer: signedHeader.Signer,
}
signature, err := GetSignature(signedHeader.Header, signer)
signature, err := GetSignature(ctx, signedHeader.Header, signer)
func GetRandomNextSignedHeader(ctx context.Context, signedHeader *SignedHeader, signer signer.Signer, chainID string) (*SignedHeader, error) {
newSignedHeader := &SignedHeader{
Header: GetRandomNextHeader(signedHeader.Header, chainID),
Signer: signedHeader.Signer,
}
signature, err := GetSignature(ctx, newSignedHeader.Header, signer)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/utils.go` around lines 192 - 198, GetRandomNextSignedHeader is building
newSignedHeader.Header via GetRandomNextHeader but then calls GetSignature with
the old signedHeader.Header, producing a signature that won't verify; change the
call to GetSignature(ctx, newSignedHeader.Header, signer), assign the returned
signature to newSignedHeader.Signature (or the appropriate field on
SignedHeader), and preserve the existing error check/return so the function
returns the newly built SignedHeader with a signature over its own Header.

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.

1 participant