Skip to content

Add Signer abstraction with LocalSigner and AWS KMS-backed KmsSigner#103

Merged
koko1123 merged 2 commits into
mainfrom
koko/signer-abstraction
Jul 1, 2026
Merged

Add Signer abstraction with LocalSigner and AWS KMS-backed KmsSigner#103
koko1123 merged 2 commits into
mainfrom
koko/signer-abstraction

Conversation

@koko1123

@koko1123 koko1123 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What

Introduces a general signer abstraction, modeled on alloy's Signer trait, and an AWS KMS implementation:

  • signer.Signer - tagged union interface (address / signHash / signMessage) owned inline by Wallet (value semantics, no lifetime fix-ups for by-value consumers like the zig-sdk's Context).
  • signer.LocalSigner - the existing in-memory secp256k1 signer, renamed from Signer (same behavior; keeps signAuthorization, hashPersonalMessage).
  • signer.KmsSigner + new src/kms.zig - signs via AWS KMS ECC_SECG_P256K1 keys that never leave the HSM. The KMS module implements only what a signer needs: Sign + GetPublicKey, SigV4 request signing (HMAC-SHA256 - baseline-CPU safe for Fargate), credential resolution (static env vars, then the ECS container-credentials endpoint), DER signature decode, EIP-2 low-s normalization, and parity recovery by matching the cached address.
  • Wallet.init now takes a Signer; Wallet.initLocal(allocator, private_key, provider) is the drop-in convenience for the previous raw-key init. wallet.signer_instance renamed to wallet.signer.
  • flashbots' auth signer is a LocalSigner (always a local key by nature).

Why

Part of moving Strobe bot/service keys off plaintext env vars onto AWS KMS (operators fund an address; IAM kms:Sign gates signing; key material is non-exportable). gator-liquidators and future Zig bots consume this via Wallet.initWithSigner-style wiring; a follow-up bumps gator to this eth.zig and swaps LIQUIDATOR_PRIVATE_KEY for a KMS key id.

Breaking changes (intentional, semantics-honest naming)

  • signer.Signer (struct) -> signer.LocalSigner; signer.Signer is now the union interface.
  • Wallet.init(allocator, [32]u8, provider) -> Wallet.initLocal(...); Wallet.init now takes a signer.Signer.
  • Consumers (gator-liquidators, perpcity-zig-sdk) will be updated when they bump their eth.zig pin - separate PRs.

Zig 0.16 notes

Wall-clock time reads through std.Io.Clock.now(.real, io) (std.time.timestamp was removed); env access uses libc std.c.getenv (0.16 moved ambient env behind the Io model; eth.zig already links libc for its vendored crypto C).

Verification

  • zig build (0.16.0) - clean.
  • zig build test - full suite green, including new unit tests: SigV4 signature against the AWS documented test vector, DER decode (padding + sign-byte stripping), SPKI pubkey extraction, sha256 vector.
  • zig fmt --check - clean.
  • Live KMS integration (sign + recover against the perpcity-bot-dev key) is the next validation step, done from the consuming bot.

Summary by CodeRabbit

  • New Features
    • Added AWS KMS-backed signing support alongside local in-memory keys.
    • Wallets can now be created from an existing signer or via a convenience local-key constructor.
  • Bug Fixes
    • Improved Flashbots relay authentication by standardizing how the signed auth header is generated and recovered.
  • Tests
    • Updated signing and wallet/relay unit tests to match the new signer flow.

@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eth-zig Ready Ready Preview, Comment Jul 1, 2026 8:01pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5514e921-7f7a-4432-8d34-f4b0371efd89

📥 Commits

Reviewing files that changed from the base of the PR and between 589a70f and ec98e79.

📒 Files selected for processing (4)
  • src/flashbots.zig
  • src/kms.zig
  • src/signer.zig
  • src/wallet.zig
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/wallet.zig
  • src/flashbots.zig
  • src/kms.zig
  • src/signer.zig

📝 Walkthrough

Walkthrough

This PR adds a KMS client, splits signer behavior into local and KMS-backed implementations, and updates wallet and Flashbots auth code to use the new signer API. Related tests and root module wiring are updated accordingly.

Changes

KMS-backed signing support

Layer / File(s) Summary
AWS KMS client
src/kms.zig, src/root.zig
Adds the KMS client, credential lookup, SigV4 request signing, response decoding, DER/public-key helpers, unit tests, and module re-export/compile wiring.
LocalSigner and KmsSigner
src/signer.zig
Adds LocalSigner, adds KmsSigner, changes Signer into a tagged union, and updates signer tests to use the local implementation.
Wallet constructor and signer field
src/wallet.zig, src/abigen.zig
Renames the stored wallet signer field, changes Wallet.init to accept a prebuilt signer, adds Wallet.initLocal, rewires signing and lifecycle methods, and updates wallet typecheck tests.
Flashbots auth signer update
src/flashbots.zig
Switches Flashbots auth signing to LocalSigner, updates Relay initialization and cleanup, and adjusts the auth-header test to match the new hashing path.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing a new signer abstraction with LocalSigner and KMS-backed signing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch koko/signer-abstraction

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/flashbots.zig (1)

256-258: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Zero the Flashbots auth key during Relay deinit.

auth_signer now owns a LocalSigner, but Relay.deinit only closes the HTTP client. The auth private key remains in memory after teardown.

Proposed fix
     pub fn deinit(self: *Relay) void {
+        self.auth_signer.deinit();
         self.client.deinit();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/flashbots.zig` around lines 256 - 258, Relay.deinit currently only tears
down the HTTP client, leaving auth_signer’s LocalSigner private key in memory.
Update Relay.deinit to also clean up auth_signer by invoking the LocalSigner
teardown/zeroing logic before or alongside client.deinit, using the Relay and
auth_signer symbols to locate the cleanup path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/flashbots.zig`:
- Line 196: `computeAuthHeader` only avoids one by-value copy, but `LocalSigner`
is still copied in `signHash` and `address`, and `Relay.deinit` leaves
`auth_signer` resident in memory. Update the `LocalSigner` methods to use
pointer receivers and adjust the call sites in `computeAuthHeader`/`Relay` so
the signer is passed by reference, then make `Relay.deinit` explicitly wipe or
zero the `auth_signer` storage before releasing the relay.

In `@src/kms.zig`:
- Around line 399-410: decodeDerSignature currently parses only the DER prefix
and can accept malformed signatures with trailing data or empty INTEGER values.
Update decodeDerSignature to read and validate the SEQUENCE length up front,
ensure the full input length matches the encoded DER structure, and reject any
extra bytes. Also tighten readDerInteger so zero-length INTEGERs for r or s
return KmsError.ResponseInvalid instead of being accepted.
- Around line 207-211: The ECS credential duplication in the return literal can
leak already-allocated fields if a later duplicate fails. Refactor the
credential-building logic in the function that returns the parsed credentials so
each allocation is tracked and any failure frees previously duplicated values
before returning; use the existing symbols parsed.value.AccessKeyId,
parsed.value.SecretAccessKey, parsed.value.Token, and allocator.dupe to locate
the block and move the construction out of the literal into a cleanup-safe
sequence.

In `@src/signer.zig`:
- Around line 202-236: The borrowed-pointer contract in Signer.fromKms is broken
because Signer.deinit currently calls KmsSigner.deinit on a caller-owned signer.
Update the Signer union and its deinit path so the KMS variant is treated as
borrowed and not released here, or switch fromKms to store an owned KmsSigner
value/box if Wallet is meant to own it. Keep the fix localized to
Signer.fromKms, Signer.deinit, and the .kms handling in
address/signHash/signMessage.

---

Outside diff comments:
In `@src/flashbots.zig`:
- Around line 256-258: Relay.deinit currently only tears down the HTTP client,
leaving auth_signer’s LocalSigner private key in memory. Update Relay.deinit to
also clean up auth_signer by invoking the LocalSigner teardown/zeroing logic
before or alongside client.deinit, using the Relay and auth_signer symbols to
locate the cleanup path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: caad1054-efdc-4f0a-aebe-065a1642c604

📥 Commits

Reviewing files that changed from the base of the PR and between c00c091 and 589a70f.

📒 Files selected for processing (6)
  • src/abigen.zig
  • src/flashbots.zig
  • src/kms.zig
  • src/root.zig
  • src/signer.zig
  • src/wallet.zig

Comment thread src/flashbots.zig
Comment thread src/kms.zig
Comment thread src/kms.zig
Comment thread src/signer.zig
@koko1123

koko1123 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review findings:

  • Relay.deinit leaves auth key resident - fixed: Relay.deinit now calls auth_signer.deinit() (secure-zero) before closing the HTTP client.
  • decodeDerSignature accepts malformed DER - fixed: SEQUENCE length must now span the input exactly, both INTEGERs must consume it fully (trailing bytes rejected), and zero-length INTEGERs are rejected. Added three regression tests (trailing bytes, length mismatch, zero-length INTEGER).
  • ECS credential dupes can leak on mid-way allocation failure - fixed: field-by-field dupe with errdefer cleanup.
  • Signer.fromKms borrow vs Signer.deinit release contradiction - fixed: the kms variant is documented as borrowed and Signer.deinit no longer releases it; the owner calls KmsSigner.deinit. Wallet.deinit doc updated to match.

Not changed:

  • Pointer receivers for LocalSigner methods - intentional value semantics. The key is a 32-byte array; Zig copies it by value on each call regardless of receiver form (the union dispatch also captures by value), and eliminating every transient stack copy is not achievable without major API friction for no practical gain. The durable copy is what matters, and that is now zeroed via Relay.deinit / LocalSigner.deinit. This matches the pre-existing design.

@koko1123
koko1123 merged commit 8c9a20d into main Jul 1, 2026
13 checks passed
koko1123 added a commit that referenced this pull request Jul 9, 2026
v0.8.0 was cut out-of-band (git tag on 8c9a20d / #103 only, no GitHub
Release), so release-please's manifest and the version-marked extra-files
were never bumped and stayed at 0.7.0. With the manifest at 0.7.0,
release-please recomputes 0.8.0 from the feat commits since v0.7.0 and
collides with the existing v0.8.0 tag ("proposes wrong versions").

Adopt v0.8.0 as the released version by bumping the manifest plus the three
release-please extra-files (build.zig.zon, README.md, installation.mdx) to
match the existing tag. After this merges, release-please diffs from v0.8.0
and proposes 0.8.1 for the #104 fix.

Also fixes build.zig.zon self-reporting 0.7.0 while downstream (gator,
perpcity-zig-sdk) pins v0.8.0.

Does not delete the v0.8.0 tag (load-bearing: downstream pins its SHA) and
leaves CHANGELOG.md for the separate v0.8.0 GitHub Release backfill.
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