Skip to content

feat: Add KMS signer backend#3171

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

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

Conversation

@alpe
Copy link
Contributor

@alpe alpe commented Mar 17, 2026

Resolves #3163

Overview

Includes AWS and GCP remote signers

Manual test:

See:

Example
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

    • AWS KMS signer backend with configurable key ID, region, and profile settings
    • GCP KMS signer backend with support for service account credentials
    • Configurable timeout and retry settings for both KMS providers
  • Documentation

    • AWS KMS signer setup guide with IAM permissions and key creation instructions
    • GCP KMS signer setup guide with credential configuration examples

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

A cloud KMS signer backend implementation enabling operators to manage Ed25519 signing keys in AWS KMS and GCP Cloud KMS instead of on-disk encrypted files. The Signer interface is updated to accept context.Context, and signer creation is abstracted through a factory pattern supporting file-based and KMS-backed implementations with configurable timeouts and retry logic.

Changes

Cohort / File(s) Summary
Configuration & Validation
pkg/config/config.go, pkg/config/config_test.go, pkg/config/defaults.go
Added SignerKMSConfig with AWS and GCP provider-specific sub-configs (key ID, region, profile, timeout, max retries). Updated Config.Validate() to enforce provider-specific requirements and timeout/retry constraints. Registered new CLI flags for KMS configuration.
Signer Interface & Context Updates
pkg/signer/signer.go, pkg/signer/noop/signer.go, pkg/signer/noop/signer_test.go, pkg/signer/file/local.go, pkg/signer/file/file_signer_test.go, pkg/signer/file/example_test.go, pkg/signer/file/README.md, pkg/signer/file/doc.go
Updated Signer interface Sign() method signature to accept context.Context. Applied context-aware signing throughout file-based and noop signer implementations and their tests; added context error checks before signing operations.
AWS KMS Implementation
pkg/signer/aws/README.md, pkg/signer/aws/signer.go, pkg/signer/aws/signer_test.go
Implemented AWS KMS signer with Ed25519 support. Features include credential chain integration, eager public-key caching, exponential backoff retry logic with configurable max retries and per-call timeouts, and retryability classification for transient errors. Comprehensive test coverage for construction, signing, retries, and error handling.
GCP KMS Implementation
pkg/signer/gcp/README.md, pkg/signer/gcp/signer.go, pkg/signer/gcp/signer_test.go
Implemented GCP Cloud KMS signer with Ed25519 support. Features include credential file or Application Default Credentials support, public-key parsing and caching, CRC32C integrity verification, and exponential backoff with configurable retries and timeouts. Extensive test coverage for all code paths including retry behavior and integrity check failures.
Factory & Initialization
pkg/signer/factory.go, pkg/signer/factory_test.go, pkg/cmd/init.go, pkg/cmd/init_test.go, pkg/cmd/run_node.go, apps/evm/cmd/init.go, apps/grpc/cmd/init.go, apps/testapp/cmd/init.go
Created factory functions NewSigner() and NewSignerForInit() that delegate to a unified newSigner() helper supporting file, AWS KMS, and GCP KMS signers. Updated all application init commands to accept and propagate context.Context to signer creation. Refactored node startup to use factory instead of direct filesystem signer loading.
Block Execution & Signing Context Propagation
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
Threaded context through all block signing paths: ProduceBlock passes ctx to signHeader, DA submitter passes ctx to envelope/data signing, and all signer.Sign() calls now include context parameter. Updated test helpers and mocks to accept context.
Syncing & Data Retrieval Context Updates
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 all signing call sites in data retrieval and syncing tests to pass test context via t.Context() to signer operations. Replaced context.Background() with t.Context() where appropriate for signature generation in test helpers.
Types & Utilities Context Updates
types/utils.go, types/utils_test.go, types/signed_header_test.go, pkg/sync/sync_service_test.go, pkg/store/store_adapter_test.go
Updated random signed-header generation utilities (GetRandomSignedHeaderCustom, GetRandomNextSignedHeader, GetFirstSignedHeader, GetSignature) to accept and propagate context.Context through signing operations. Updated all call sites in tests.
Documentation
docs/guides/operations/aws-kms-signer.md, docs/guides/operations/gcp-kms-signer.md, docs/.vitepress/config.ts
Added comprehensive guides for AWS and GCP KMS signer setup with prerequisites, IAM permissions, CLI examples, configuration snippets, and troubleshooting. Added sidebar navigation entry for AWS KMS signer guide.
Changelog
CHANGELOG.md
Added entry under [Unreleased] documenting the AWS KMS signer backend addition (PR #3171).
Dependency Management
go.mod, apps/evm/go.mod, apps/grpc/go.mod, apps/testapp/go.mod, test/e2e/go.mod, execution/evm/test/go.mod
Added direct dependencies on AWS SDK v2 (aws-sdk-go-v2, config, KMS service) and GCP Cloud KMS (cloud.google.com/go/kms, google.golang.org/api). Added transitive dependencies for cloud authentication, OpenTelemetry instrumentation, and networking utilities across all go.mod files.
End-to-End Tests
test/e2e/evm_kms_e2e_test.go
Added E2E test suite validating EVM sequencer with both AWS and GCP KMS signers. Tests verify initialization, node startup, and transaction inclusion with environment-variable-based credential handling and optional region/profile/credentials configuration.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application Init
    participant Ctx as Context
    participant Factory as Factory<br/>(pkg/signer)
    participant Config as Config<br/>Validator
    participant KMS as KMS Client<br/>(AWS/GCP)
    participant Signer as Signer<br/>Implementation

    App->>Ctx: cmd.Context()
    App->>Factory: NewSignerForInit(ctx, config, passphrase)
    Factory->>Config: Validate signer type & KMS provider
    Config-->>Factory: OK or error
    alt Validation Fails
        Factory-->>App: error
    else File Signer
        Factory->>Signer: CreateFileSystemSigner(...)
        Signer-->>Factory: *FileSystemSigner
    else AWS KMS
        Factory->>KMS: aws.NewKmsSigner(ctx, region, profile, keyID, opts)
        KMS->>KMS: Load AWS credentials
        KMS->>KMS: Create KMS client
        KMS->>KMS: GetPublicKey(ctx)
        KMS->>KMS: Cache & parse public key
        KMS-->>Factory: *KmsSigner
    else GCP KMS
        Factory->>KMS: gcp.NewKmsSigner(ctx, keyName, opts)
        KMS->>KMS: Load GCP credentials
        KMS->>KMS: Create KMS client
        KMS->>KMS: GetPublicKey(ctx)
        KMS->>KMS: Parse & cache public key
        KMS-->>Factory: *KmsSigner
    end
    Factory->>Signer: GetPublic() & GetAddress()
    Signer-->>Factory: pubKey, address
    Factory-->>App: Signer, nil
Loading
sequenceDiagram
    participant Executor as Block Executor
    participant Signer as Signer<br/>(File/AWS/GCP)
    participant KMS as KMS Service<br/>(if AWS/GCP)
    participant Cache as Public Key<br/>Cache

    Executor->>Executor: ProduceBlock(ctx)
    Executor->>Executor: signHeader(ctx, header)
    Executor->>Signer: Sign(ctx, headerBytes)
    
    alt File Signer
        Signer->>Signer: ed25519.Sign(privKey, msg)
        Signer-->>Executor: signature
    else AWS/GCP KMS
        Signer->>Signer: ctx.Err() check
        alt Context Canceled
            Signer-->>Executor: context error
        else Normal Flow
            Signer->>KMS: Sign(ctx, message)
            KMS->>KMS: Apply per-call timeout
            alt Retryable Error
                KMS->>KMS: Exponential backoff
                KMS->>KMS: Retry (up to MaxRetries+1)
            else Non-Retryable Error
                KMS-->>Signer: error
            end
            KMS-->>Signer: signature
            Signer->>Cache: Return cached pubKey
            Signer-->>Executor: signature
        end
    end
    
    Executor->>Executor: Continue block production with signature
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • tac0turtle
  • julienrbrt
  • chatton

Poem

🐰 A signer in the cloud so high,
No keys upon the disk to spy,
AWS and GCP align,
With context flowing, all is fine,
From AWS to blocks we sign!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR description provides issue reference and overview with AWS/GCP signer links, but structure is minimal and lacks detail about changes, testing, and objectives. Expand description with: (1) detailed summary of implementation approach; (2) list of key files/modules modified; (3) testing strategy and E2E test coverage; (4) backward compatibility impact; (5) link to full documentation.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a KMS signer backend to support cloud-based key management for AWS and GCP.
Linked Issues check ✅ Passed All primary coding objectives from #3163 are met: KMS Signer interface implementation [AWS, GCP], integration into configuration and startup, public key caching, and remote signing via cloud provider APIs.
Out of Scope Changes check ✅ Passed All changes are directly related to KMS signer implementation. Context threading through signing calls and dependency updates for AWS/GCP KMS SDKs are necessary supporting changes aligned with the feature scope.

✏️ 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

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 20, 2026, 2:50 PM

@claude
Copy link
Contributor

claude bot commented Mar 17, 2026

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


Code Review: PR #3171 — AWS KMS and GCP KMS Signer Backends

  • Read all key changed files
  • Analyzed code quality, architecture, and idiomatic Go
  • Reviewed error handling and resilience
  • Reviewed security and credential management
  • Reviewed performance and resource efficiency
  • Reviewed test coverage and reliability
  • Reviewed documentation
  • Verified all CodeRabbit-identified issues

CI Run


Summary

This PR adds AWS KMS and GCP KMS remote signer backends for the sequencer, plus a factory that selects a signer implementation at startup. The architecture is sound — a clean Signer interface, a separate package per backend, mock-able client interfaces, and eager public-key caching for fail-fast startup. The GCP signer is notably better than the AWS signer in a few respects (10s default timeout vs 1s, CRC32C integrity verification, a capped backoff function). The main concerns are one confirmed bug in types/utils.go, a default timeout that is too aggressive, and two mutable-slice issues that could cause subtle data-race-like bugs in callers.


Critical

types/utils.go:198GetRandomNextSignedHeader signs the OLD header, not the new one

This is a real bug. The function constructs newSignedHeader with a new Header, but then calls GetSignature on signedHeader.Header (the previous block's header). The returned newSignedHeader will carry a signature that does not verify against its own header bytes.

// BUG: signs signedHeader.Header, not newSignedHeader.Header
signature, err := GetSignature(ctx, signedHeader.Header, signer)

Should be:

signature, err := GetSignature(ctx, newSignedHeader.Header, signer)

This function is used in tests and test helpers throughout the codebase. Any test that verifies the chain of signed headers calling this function will silently carry an invalid signature. While the immediate impact may be confined to tests, fixing it is required because it breaks the invariant that a SignedHeader's signature covers its own header.


Major

pkg/signer/aws/signer.go:87 — Default timeout is 1 second, documented as 10 seconds

The struct comment says Default: 10s but kmsSignerFromClient sets Timeout: 1 * time.Second. The GCP counterpart correctly uses 10 * time.Second. A 1-second timeout is almost certainly too short for real-world KMS calls under load or across regions, and will cause spurious DeadlineExceeded errors that then trigger retries, burning retry budget unnecessarily.

// aws/signer.go:87 — should be 10s to match doc and GCP
o := Options{Timeout: 1 * time.Second, MaxRetries: 3}

The README example also shows timeout: 1s, which suggests the 1-second value was intentional for the README example but leaked into the default. The comment should either be corrected or the default raised to 10s to match the GCP signer and the struct doc.

pkg/signer/aws/signer.go:216 and pkg/signer/gcp/signer.go:283GetAddress returns the internal slice directly

Both GetAddress implementations return s.address without copying it. Callers can mutate the internal cache, which would silently corrupt subsequent GetAddress calls for the lifetime of the signer. The file signer has the same pattern. Given that the address is computed once and stored in a []byte field, this is a latent correctness issue.

// Correct fix: return a copy
addr := make([]byte, len(s.address))
copy(addr, s.address)
return addr, nil

pkg/signer/factory.go:35 — Empty signer_path silently resolves to the current working directory

When config.Signer.SignerPath is empty, filepath.Abs("") returns the process's CWD. This is surprising behavior — an operator who forgets to set signer_path will get a signer file created in whatever directory the process was launched from, with no warning. An explicit empty-string check should be added before calling filepath.Abs:

if config.Signer.SignerPath == "" {
    return nil, fmt.Errorf("signer_path must be set when using file signer")
}

pkg/signer/gcp/signer.go:87 — Incorrect option.WithAuthCredentialsFile call signature

option.WithAuthCredentialsFile in google.golang.org/api/option takes a single string argument (the file path). The call here passes two arguments — option.ServiceAccount and the file path — which does not match the function signature. The correct function for supplying a service-account credentials file is option.WithCredentialsFile(opts.CredentialsFile). This is likely a compilation error or will panic at runtime if the GCP credential path is exercised.

// Wrong:
option.WithAuthCredentialsFile(option.ServiceAccount, opts.CredentialsFile)
// Correct:
option.WithCredentialsFile(opts.CredentialsFile)

pkg/config/config.go:315signer_type comment documents grpc as a valid value but the factory does not support it

The SignerConfig.SignerType comment says (file, grpc, kms), but factory.go only handles "file" and "kms". A "grpc" type will fall through to the default case and return an error. Either the comment should be updated to remove grpc or the factory should handle it (if gRPC signer support is planned, the comment is premature and misleading for operators reading the config).


Minor

pkg/signer/gcp/signer.go:211-214 — Integrity failures are retried but not counted as retryable errors

When verifySignResponse returns an error (CRC32C mismatch or missing field), the loop calls continue without checking isRetryableKMSError. This means integrity failures are always retried up to maxAttempts, which may be appropriate — but the intent should be made explicit with a comment. It also means the final error message will say "KMS Sign failed after N attempts" and surface the integrity error, which is good.

pkg/signer/aws/signer.go:163 — Exponential backoff does not cap

The AWS signer computes backoff := time.Duration(100<<uint(attempt-1)) * time.Millisecond. With 3 retries the values are 100ms, 200ms, 400ms — harmless in practice — but there is no upper bound. If MaxRetries is set to a large value (e.g., 10), attempt 9 would give 100 << 8 = 25600ms (25 seconds). The GCP signer correctly caps at maxRetryBackoff = 5s. The AWS signer should adopt the same capping strategy.

pkg/signer/gcp/signer.go:45-51cloudKMSClient wrapper is needlessly verbose

The thin cloudKMSClient struct exists only to adapt *kms.KeyManagementClient to the KMSClient interface. Since Go allows embedding or direct method forwarding, a simpler approach is possible. This is a style nit and does not affect correctness.

pkg/signer/aws/signer.go:219-241isRetryableKMSError treats all net.Error as retryable

Treating every net.Error as retryable is reasonable for transient connectivity issues, but net.Error.Timeout() should be preferred to distinguish actual timeouts from permanent network errors. The current code will retry a connection-refused error if it happens to implement net.Error, which could hide misconfiguration.

types/utils.goGetRandomNextSignedHeader does not update ValidatorHash and ProposerAddress on newSignedHeader

GetRandomNextHeader copies ProposerAddress and ValidatorHash from the old header (correct), but GetRandomNextSignedHeader then leaves the Signer field set to signedHeader.Signer rather than deriving it from the provided signer argument. This means that if the caller passes a different signer (e.g., after key rotation in a test), the Signer field in the new header will still reference the old signer's identity.


Nit

  • pkg/signer/aws/signer.go:39-41timeout() and maxRetries() are single-line methods with no added value over direct field access. They could be removed in favor of s.opts.Timeout and s.opts.MaxRetries to reduce indirection.
  • pkg/signer/gcp/signer.go:64-66 — Same as above for GCP.
  • pkg/signer/aws/signer_test.go:135TestSign_KMSFailure hardcodes int32(4) for the expected call count. Expressing this as int32(defaultMaxRetries + 1) or a named constant would make the assertion self-documenting.
  • pkg/signer/gcp/signer_test.go:146 — Same hardcoded int32(4).
  • pkg/signer/factory_test.go — There are no success-path integration tests for the kms branch (AWS or GCP). The error-path tests are good, but a test using kmsSignerFromClient with a mock would further lock down the factory wiring.
  • pkg/signer/aws/README.md — The README example shows timeout: 1s, which matches the (incorrect) 1-second default. Once the timeout default is corrected to 10s, the README example should also be updated.
  • pkg/signer/noop/signer.go and pkg/signer/file/local.go — Both packages define a private getAddress function with identical bodies. This is a minor duplication; a shared internal helper (or exposing it through the signer package) would avoid drift.

Testing Assessment

  • AWS signer: good coverage of happy path, retry behavior, non-retryable errors, and public-key caching. Missing: context cancellation mid-retry, MaxRetries = -1 edge case (the condition opts.MaxRetries >= 0 means -1 keeps the default, which is undocumented).
  • GCP signer: excellent — adds CRC32C integrity tests on top of the AWS test matrix, plus the retryBackoff unit test and recovery-on-retry test.
  • Factory: error paths covered; success paths for KMS branches not covered (acceptable given the mocking complexity, but worth noting).
  • types/utils.go: GetRandomNextSignedHeader has no test that actually verifies the signature on the returned header, which is why the bug at line 198 has gone undetected.

Checklist for Merge

  • Fix types/utils.go:198 — sign newSignedHeader.Header, not signedHeader.Header
  • Fix pkg/signer/aws/signer.go:87 — raise default timeout to 10s or correct the comment
  • Fix GetAddress in both AWS and GCP signers to return a copy of the internal slice
  • Fix pkg/signer/gcp/signer.go:87 — use option.WithCredentialsFile (verify compilation)
  • Fix or remove grpc from the signer_type config comment
  • Add a backoff cap to the AWS signer matching the GCP implementation
  • Add an explicit check for empty signer_path in the file signer branch of the factory
  • Add a test for GetRandomNextSignedHeader that verifies the signature covers the new header

@codecov
Copy link

codecov bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 74.20147% with 105 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.48%. Comparing base (3a710a9) to head (5006360).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/signer/gcp/signer.go 69.34% 29 Missing and 13 partials ⚠️
pkg/signer/aws/signer.go 68.46% 25 Missing and 10 partials ⚠️
pkg/cmd/run_node.go 38.46% 12 Missing and 4 partials ⚠️
pkg/cmd/init.go 73.33% 2 Missing and 2 partials ⚠️
pkg/signer/factory.go 90.47% 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.15%   61.48%   +0.32%     
==========================================
  Files         117      120       +3     
  Lines       12082    12421     +339     
==========================================
+ Hits         7389     7637     +248     
- Misses       3867     3933      +66     
- Partials      826      851      +25     
Flag Coverage Δ
combined 61.48% <74.20%> (+0.32%) ⬆️

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 +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.

* Add remote signer GCO KMS

* Review feedback

* Minor updates
@alpe alpe changed the title feat: Add AWS KMS signer backend feat: Add KMS signer backend Mar 20, 2026
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: 3

🧹 Nitpick comments (4)
docs/guides/operations/aws-kms-signer.md (1)

12-33: Add an explicit least-privilege warning for the example IAM policy.

The example policy is broad (Resource: "*") and includes key-management actions; please label it as bootstrap/admin-only and recommend scoped runtime permissions (already partially covered in Line 35–38) to reduce accidental over-permissioning in production.

Also applies to: 35-38

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

In `@docs/guides/operations/aws-kms-signer.md` around lines 12 - 33, Add a clear
least-privilege warning above the example IAM policy JSON that currently uses
"Resource": "*" and includes key-management actions (e.g., "kms:CreateKey",
"kms:PutKeyPolicy", "kms:EnableKey", "kms:Sign"); label this policy as
bootstrap/admin-only and explicitly state it must not be used in production,
then reference and recommend the scoped runtime permissions described later (the
smaller set in the docs) as the safe alternative and suggest replacing wildcard
Resource with specific KMS key ARNs for production.
test/e2e/evm_kms_e2e_test.go (1)

32-32: Calling flag.Parse() multiple times can cause issues

flag.Parse() is called in both TestEvmSequencerWithAWSKMSSignerE2E (line 32) and TestEvmSequencerWithGCPKMSSignerE2E (line 72). When both tests run in the same test binary execution, this can cause problems as flags are already parsed. The Go testing framework already handles flag parsing before tests run.

♻️ Remove redundant flag.Parse() calls
 func TestEvmSequencerWithAWSKMSSignerE2E(t *testing.T) {
 	if testing.Short() {
 		t.Skip("skip e2e in short mode")
 	}
-	flag.Parse()

 	kmsKeyID := os.Getenv("EVNODE_E2E_AWS_KMS_KEY_ID")
 func TestEvmSequencerWithGCPKMSSignerE2E(t *testing.T) {
 	if testing.Short() {
 		t.Skip("skip e2e in short mode")
 	}
-	flag.Parse()

 	kmsKeyName := os.Getenv("EVNODE_E2E_GCP_KMS_KEY_NAME")

Also applies to: 72-72

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

In `@test/e2e/evm_kms_e2e_test.go` at line 32, The tests call flag.Parse()
redundantly in TestEvmSequencerWithAWSKMSSignerE2E and
TestEvmSequencerWithGCPKMSSignerE2E which can break when the test binary parses
flags once; remove the flag.Parse() calls from both test functions (search for
TestEvmSequencerWithAWSKMSSignerE2E and TestEvmSequencerWithGCPKMSSignerE2E) so
the standard Go test harness handles flag parsing, and run go test to confirm no
other tests rely on manual flag parsing.
pkg/signer/gcp/signer.go (2)

262-272: Inconsistent mutex pattern between GetPublic and GetAddress

GetPublic manually unlocks before the nil check while GetAddress uses defer s.mu.RUnlock(). For consistency and safety, consider using defer in both methods.

♻️ Use defer for consistency
 func (s *KmsSigner) GetPublic() (crypto.PubKey, error) {
 	s.mu.RLock()
-	pubKey := s.pubKey
-	s.mu.RUnlock()
+	defer s.mu.RUnlock()
+	pubKey := s.pubKey

 	if pubKey == nil {
 		return nil, fmt.Errorf("public key not loaded")
 	}

 	return pubKey, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signer/gcp/signer.go` around lines 262 - 272, GetPublic currently
acquires a read lock and manually unlocks before checking pubKey; change it to
mirror GetAddress by calling s.mu.RLock() then immediately deferring
s.mu.RUnlock(), assign pubKey := s.pubKey, and perform the nil check after the
defer so the lock-unlock pattern is consistent and safe (update the GetPublic
method accordingly to use defer s.mu.RUnlock()).

194-201: Consider using defer cancel() for the per-call timeout context

The cancel() function is called immediately after the API call, but if there's a panic between context creation and the cancel call, the context could leak. Using defer is the idiomatic pattern.

♻️ Use defer for cancel()
 	callCtx, cancel := context.WithTimeout(ctx, timeout)
+	defer cancel()
 	dataCRC32C := int64(crc32.Checksum(message, castagnoliTable))
 	out, err := s.client.AsymmetricSign(callCtx, &kmspb.AsymmetricSignRequest{
 		Name:       s.keyName,
 		Data:       message,
 		DataCrc32C: wrapperspb.Int64(dataCRC32C),
 	})
-	cancel()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/signer/gcp/signer.go` around lines 194 - 201, The context cancel function
created by callCtx, cancel := context.WithTimeout(ctx, timeout) is invoked
immediately with cancel() which can leak the context on panic; after creating
callCtx and cancel, replace the immediate cancel() call with defer cancel() so
the context is always cancelled when the surrounding function returns (keep the
call to s.client.AsymmetricSign using callCtx and the DataCrc32C wrapper as-is).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/evm/go.mod`:
- Line 239: Update the grpc dependency entry to google.golang.org/grpc v1.79.3
in the go.mod (replace the existing google.golang.org/grpc v1.79.2 // indirect
line), then run go mod tidy (or go get google.golang.org/grpc@v1.79.3) to update
the lockfiles and verify module downloads; this addresses the CVE by ensuring
the project uses the patched grpc version.

In `@docs/guides/operations/aws-kms-signer.md`:
- Around line 72-75: Remove the misleading expected startup log "initialized AWS
KMS signer via factory" from the docs and either delete that sentence or replace
it with the actual log emitted by the AWS signer initialization path; update the
guide text that references the signer factory so it reflects the real behavior
of the AWS signer initialization path (see aws signer implementation in
pkg/signer/aws/signer.go and the factory behavior in pkg/signer/factory.go) so
operators are not told to expect a nonexistent log line.

In `@go.mod`:
- Line 48: The go.mod currently pins the vulnerable module
"google.golang.org/grpc v1.79.2"; update that dependency to a patched gRPC
release that fixes GHSA-p77j-4mvh-x3m3 (e.g., upgrade to a patched version >=
v1.79.3 or the latest patched release), run "go get
google.golang.org/grpc@<patched-version>" and "go mod tidy", run tests, and
ensure any other references (such as the duplicate pin in apps/evm/go.mod) are
likewise updated and no replace directives or transitive dependencies re-pin the
vulnerable v1.79.2.

---

Nitpick comments:
In `@docs/guides/operations/aws-kms-signer.md`:
- Around line 12-33: Add a clear least-privilege warning above the example IAM
policy JSON that currently uses "Resource": "*" and includes key-management
actions (e.g., "kms:CreateKey", "kms:PutKeyPolicy", "kms:EnableKey",
"kms:Sign"); label this policy as bootstrap/admin-only and explicitly state it
must not be used in production, then reference and recommend the scoped runtime
permissions described later (the smaller set in the docs) as the safe
alternative and suggest replacing wildcard Resource with specific KMS key ARNs
for production.

In `@pkg/signer/gcp/signer.go`:
- Around line 262-272: GetPublic currently acquires a read lock and manually
unlocks before checking pubKey; change it to mirror GetAddress by calling
s.mu.RLock() then immediately deferring s.mu.RUnlock(), assign pubKey :=
s.pubKey, and perform the nil check after the defer so the lock-unlock pattern
is consistent and safe (update the GetPublic method accordingly to use defer
s.mu.RUnlock()).
- Around line 194-201: The context cancel function created by callCtx, cancel :=
context.WithTimeout(ctx, timeout) is invoked immediately with cancel() which can
leak the context on panic; after creating callCtx and cancel, replace the
immediate cancel() call with defer cancel() so the context is always cancelled
when the surrounding function returns (keep the call to s.client.AsymmetricSign
using callCtx and the DataCrc32C wrapper as-is).

In `@test/e2e/evm_kms_e2e_test.go`:
- Line 32: The tests call flag.Parse() redundantly in
TestEvmSequencerWithAWSKMSSignerE2E and TestEvmSequencerWithGCPKMSSignerE2E
which can break when the test binary parses flags once; remove the flag.Parse()
calls from both test functions (search for TestEvmSequencerWithAWSKMSSignerE2E
and TestEvmSequencerWithGCPKMSSignerE2E) so the standard Go test harness handles
flag parsing, and run go test to confirm no other tests rely on manual flag
parsing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d986867e-d2c9-40c3-968d-ac865e15dbe4

📥 Commits

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

⛔ 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 (19)
  • apps/evm/go.mod
  • apps/grpc/go.mod
  • apps/testapp/go.mod
  • docs/guides/operations/aws-kms-signer.md
  • docs/guides/operations/gcp-kms-signer.md
  • execution/evm/test/go.mod
  • go.mod
  • 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/factory.go
  • pkg/signer/factory_test.go
  • pkg/signer/gcp/README.md
  • pkg/signer/gcp/signer.go
  • pkg/signer/gcp/signer_test.go
  • test/e2e/evm_kms_e2e_test.go
  • test/e2e/go.mod
✅ Files skipped from review due to trivial changes (5)
  • execution/evm/test/go.mod
  • docs/guides/operations/gcp-kms-signer.md
  • pkg/signer/gcp/README.md
  • pkg/signer/aws/README.md
  • pkg/signer/factory_test.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • pkg/config/config_test.go
  • pkg/config/defaults.go
  • apps/grpc/go.mod
  • test/e2e/go.mod
  • apps/testapp/go.mod
  • pkg/signer/factory.go
  • pkg/cmd/run_node.go
  • pkg/config/config.go

google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/grpc v1.79.2 // indirect
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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

What is the latest version of google.golang.org/grpc and does it fix GHSA-p77j-4mvh-x3m3?

💡 Result:

The latest version of google.golang.org/grpc is v1.79.3, released on 2026-03-17. Yes, it fixes GHSA-p77j-4mvh-x3m3. The release notes explicitly state under Security: "server: fix an authorization bypass where malformed :path headers (missing the leading slash) could bypass path-based restricted 'deny' rules in interceptors like grpc/authz. Any request with a non-canonical path is now immediately rejected with an Unimplemented error. (#8981)". This matches the vulnerability description in advisories (CVE-2026-33186), which involves authorization bypass via missing leading slash in :path.

Citations:


Upgrade google.golang.org/grpc to v1.79.3 to fix critical authorization bypass vulnerability

google.golang.org/grpc v1.79.2 contains GHSA-p77j-4mvh-x3m3 (CVE-2026-33186), an authorization bypass where malformed :path headers missing the leading slash can bypass path-based access controls in gRPC interceptors. Since the KMS signer uses gRPC to communicate with cloud KMS services, this vulnerability affects the security of signing operations. Upgrade to v1.79.3, which fixes this issue by rejecting non-canonical paths.

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

In `@apps/evm/go.mod` at line 239, Update the grpc dependency entry to
google.golang.org/grpc v1.79.3 in the go.mod (replace the existing
google.golang.org/grpc v1.79.2 // indirect line), then run go mod tidy (or go
get google.golang.org/grpc@v1.79.3) to update the lockfiles and verify module
downloads; this addresses the CVE by ensuring the project uses the patched grpc
version.

Comment on lines +72 to +75
You should see a startup log line:

`initialized AWS KMS signer via factory`

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

Remove or replace the expected startup log line; it does not exist in current code paths.

Line 72–75 tells operators to expect initialized AWS KMS signer via factory, but the AWS signer initialization path and signer factory path do not emit that log (pkg/signer/aws/signer.go:52-110, pkg/signer/factory.go:50-77). This creates a false negative during bring-up.

Proposed doc fix
-You should see a startup log line:
-
-`initialized AWS KMS signer via factory`
+Confirm startup succeeded by checking that `evnode` starts without signer initialization errors.
+If startup fails, use the exact errors in the troubleshooting section below.
📝 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
You should see a startup log line:
`initialized AWS KMS signer via factory`
Confirm startup succeeded by checking that `evnode` starts without signer initialization errors.
If startup fails, use the exact errors in the troubleshooting section below.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/guides/operations/aws-kms-signer.md` around lines 72 - 75, Remove the
misleading expected startup log "initialized AWS KMS signer via factory" from
the docs and either delete that sentence or replace it with the actual log
emitted by the AWS signer initialization path; update the guide text that
references the signer factory so it reflects the real behavior of the AWS signer
initialization path (see aws signer implementation in pkg/signer/aws/signer.go
and the factory behavior in pkg/signer/factory.go) so operators are not told to
expect a nonexistent log line.

golang.org/x/net v0.52.0
golang.org/x/sync v0.20.0
google.golang.org/api v0.272.0
google.golang.org/grpc v1.79.2
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 | 🔴 Critical

Critical: Same gRPC authorization bypass vulnerability in root module

This is the same GHSA-p77j-4mvh-x3m3 vulnerability flagged in apps/evm/go.mod. The google.golang.org/grpc v1.79.2 dependency is used directly for KMS communication. Upgrading to a patched version should be prioritized before merging this PR.

🧰 Tools
🪛 OSV Scanner (2.3.3)

[CRITICAL] 48-48: google.golang.org/grpc 1.79.2: gRPC-Go has an authorization bypass via missing leading slash in :path

(GHSA-p77j-4mvh-x3m3)

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

In `@go.mod` at line 48, The go.mod currently pins the vulnerable module
"google.golang.org/grpc v1.79.2"; update that dependency to a patched gRPC
release that fixes GHSA-p77j-4mvh-x3m3 (e.g., upgrade to a patched version >=
v1.79.3 or the latest patched release), run "go get
google.golang.org/grpc@<patched-version>" and "go mod tidy", run tests, and
ensure any other references (such as the duplicate pin in apps/evm/go.mod) are
likewise updated and no replace directives or transitive dependencies re-pin the
vulnerable v1.79.2.

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.

Feature: Add KMS signer backend

1 participant