Skip to content

feat(repo): add quoter-signer kms signing and custody attestation - #179

Draft
julien-devatom wants to merge 10 commits into
kms-middleware-checks-f85533from
kms-signing-middleware-bf9e1a
Draft

feat(repo): add quoter-signer kms signing and custody attestation#179
julien-devatom wants to merge 10 commits into
kms-middleware-checks-f85533from
kms-signing-middleware-bf9e1a

Conversation

@julien-devatom

@julien-devatom julien-devatom commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Implements the KMS signing layer of TIB-2026-08-12 in the quoter-signer middleware — the third increment, stacked on #178 (deterministic policy checks; retarget to main after it merges).

Why

The middleware is the future sole kms:Sign principal on the maker key, but the skeleton so far never touched KMS: intents passing every deterministic check were denied without proving anything about custody. This increment gives the image its custody root:

  • Maker-key custody attestation, started best-effort at container init when the deployment is fully configured and wired into the evaluation pipeline after the deterministic checks: kms:GetPublicKey on the deployment-pinned key, exact ECC_SECG_P256K1/SIGN_VERIFY/ECDSA_SHA_256 shape, the resolved key ARN, strict canonical-SPKI parse, on-curve validation, and a derived address that must equal the policy-pinned maker. The cached attestation carries a five-minute freshness bound (KMS_ATTESTATION_FRESHNESS_MS), so drift on a warm container is re-checked at the next window; drift fails closed (KmsAttestationFailedError), a failed call is a typed retryable denial (KmsUnavailableError), missing/invalid KMS addressing refuses to serve (KmsNotConfiguredError), and the TIB's middleware.kms_error log event lands.
  • The digest-signing primitive the sign-what-you-encode stages will call: kms:Sign (MessageType: 'DIGEST', ECDSA_SHA_256) issued against the resolved key ARN captured at attestation — never the configured alias, which could be repointed to an unattested key — on a single-attempt client (no SDK retries, so every CloudTrail Sign event reconciles with exactly one middleware signing record) → strict canonical ECDSA-DER parse → low-s normalization → recovery check across both parities against the attested maker → mandatory KMS request id capture (the CloudTrail reconciliation join key; a response without one, or with a blank one, is rejected rather than becoming an unreconcilable signature).

Every intent is still deniedkms:Sign is never called by any reachable path, and intents passing every stage end in SigningNotImplementedError until the encode stages and the reservation ledger land. The execution role needs kms:GetPublicKey only.

Design choices

  • No ASN.1 library in the root-of-trust image: DER is canonical, so the uncompressed-secp256k1 SubjectPublicKeyInfo has exactly one 88-byte encoding — validated by exact prefix template plus @noble/curves on-curve check, unlike the bot's asn1js path. The DER signature parser mirrors the bot's proven parser rather than sharing a module, per the middleware's established mirror-not-share auditability rule. Both recorded in the TIB's Dependencies section.
  • Attestation runs after the deterministic checks: caller mistakes are answered without a KMS call, and malformed floods never reach AWS.
  • Per-container memoized attestation with eviction on failure: one warm container attests once; a transient outage or a since-fixed custody drift re-attests on the next invocation instead of poisoning the environment.
  • Transport seam (KmsTransport): the AWS adapter maps response fields verbatim; every custody decision lives in unit-tested code. Handler tests inject fakes via the new createHandler factory; the default transport itself is covered against a mocked SDK asserting the exact pinned command shapes (including the single-attempt client config and ARN-pinned Sign).
  • @aws-sdk/client-kms and @noble/curves move into the pnpm catalog so the bot's direct-KMS path and the middleware stay on the same audited versions.

Validation

  • pnpm --filter @morpho-org/quoter-signer run typecheck, pnpm --filter @morpho-org/quoter-bot run typecheck — clean
  • pnpm lint — 0 warnings, pnpm format — applied
  • Service suite: 258 tests (adversarial SPKI/DER vectors mirrored from the bot's proven suite, mocked-SDK transport tests, handler attestation/eviction/log-hygiene cases); full pnpm test: only the pre-existing RPC_URL_8453 fork suites fail locally (environmental)
  • Mutation checks: disabling low-s normalization fails 2 tests; un-pinning the Sign target from the attested ARN fails 2 tests
  • reviewer agent pass: no blocking findings; all 4 suggestions applied
  • Review round 1: carapulse blocking finding (blank request id) and both codex P2s (ARN pinning, single-attempt client) fixed in 484b491
  • Review round 2: cold-start attestation warm-up + freshness bound with drift fail-closed (carapulse) and the corrected denial contract (codex) landed in bd36b8b, with warm-up/pending-attestation/stale-drift tests (mutation-checked)
  • Review round 3 (codex approved bd36b8b): the freshness bound is now enforced by the signing primitive itself — signDigest() refuses with a retryable KmsAttestationStaleError once its attestation ages past the window (carapulse blocking finding), landed in 5ed3c3e with a boundary test (mutation-checked)

🤖 Generated with Claude Code

julien-devatom and others added 7 commits August 25, 2026 15:49
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- pin offer tick to uint256 and maxAssets to uint128 per the Midnight struct
- require an alias-qualified quoter-signer Lambda ARN (no version/unqualified)
- clear foreign signer sources for CLI middleware overrides in both directions
- report the fail-closed middleware identity as a sanitized signer observation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third TIB-2026-08-12 increment: the middleware now parses a strict
QUOTER_SIGNER_POLICY deployment document (surface/mode/chain/maker pins,
market allowlist with tick and fee ceilings, offer time windows, routine
and protected fee ceilings with the emergency-bump reserve, remediation
variants) and denies out-of-policy intents with a typed violation naming
the violated check. Still fail-closed: no KMS access, nothing signed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-f85533

# Conflicts:
#	docs/decisions/TIB-2026-08-12-quoter-bot-kms-signing-middleware.md
#	pnpm-lock.yaml
#	services/quoter-signer/README.md
#	services/quoter-signer/package.json
#	services/quoter-signer/src/index.ts
#	services/quoter-signer/src/intent.utils.ts
#	services/quoter-signer/test/index.test.ts
The pinned v1 SHA runs the downloaded foundryup via bash, which broke
repo-wide (main included) when upstream foundryup became a binary:
'cannot execute binary file' before any test executes. v1.9.1 downloads
the pinned foundryup-init.sh installer and executes foundryup directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- viem-first bytes32 identity: market/group keys via hexToBigInt instead
  of toLowerCase in the policy parser, the check stage, and the intent
  parser's market count
- refuse to serve a setup-remediation deployment with an empty
  remediation manifest (empty policy) instead of denying per intent
- README create-function command now passes the policy via
  --environment file://environment.json

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the KMS signing layer of TIB-2026-08-12 in the quoter-signer
middleware: strict maker-key custody attestation (GetPublicKey, exact
canonical-DER SPKI template parse with on-curve validation, derived
address pinned to the policy maker) wired into the fail-closed
evaluation pipeline, plus the digest-signing primitive the upcoming
encode stages will call (Sign with MessageType DIGEST, strict canonical
DER parse, low-s normalization, recovery check across both parities,
mandatory KMS request id capture for CloudTrail reconciliation).

Every intent is still denied: kms:Sign is never called and the
encode-and-sign surfaces are later increments. New deployment
parameters QUOTER_SIGNER_KMS_KEY_ID / QUOTER_SIGNER_KMS_REGION are
strictly parsed; KMS failures map to typed KmsNotConfiguredError,
retryable KmsUnavailableError, and KmsAttestationFailedError denials
with the TIB's middleware.kms_error log event. The attested signer is
memoized per execution environment with eviction on failure.

@aws-sdk/client-kms and @noble/curves move into the pnpm catalog so
the bot's direct-KMS path and the middleware stay on the same audited
versions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@julien-devatom julien-devatom self-assigned this Aug 26, 2026
@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@codex review

@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@prd-carapulse[bot] can you review and approve if all good

@prd-carapulse

prd-carapulse Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

PR #179 review complete.

Changes requested — not approved.

  • services/quoter-signer/src/kms-signer.utils.ts:266: empty or whitespace-only kmsRequestId passes the undefined check. This violates the mandatory CloudTrail reconciliation key requirement. Reject blank/non-string IDs as missing-request-id; add a regression test.
  • Review posted: feat(repo): add quoter-signer kms signing and custody attestation #179 (review)
  • CI: Lint, Typecheck, Dead-Code passed. Test still pending.
  • Local tests blocked by repository engine requirement: Node ^24.14.1; runtime has Node v26.3.0.

@prd-carapulse prd-carapulse 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.

Found one blocking custody-boundary issue. See inline comment.

Comment thread services/quoter-signer/src/kms-signer.utils.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f52128a8e6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread services/quoter-signer/src/kms-signer.utils.ts
Comment thread services/quoter-signer/src/kms-signer.utils.ts Outdated
- Reject blank or whitespace-only KMS request ids as missing-request-id
  (carapulse blocking): a blank join key is as unreconcilable as an
  absent one; valid ids pass through untrimmed.
- Pin Sign calls to the resolved key ARN captured at attestation
  (codex P2): a repointed alias can no longer route signing to an
  unattested key; attestation now requires the resolved ARN (new
  key-arn reason).
- Single-attempt KMS client, no SDK retries (codex P2): every
  CloudTrail Sign event must reconcile with exactly one middleware
  signing record; retry decisions belong to the middleware's callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@codex review

@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@prd-carapulse[bot] can you review and approve if all good

@prd-carapulse

prd-carapulse Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Review result: Changes requested. PR not approved.

Blocking finding: KMS custody attestation is lazy: createHandler() calls createKmsMakerSigner() only after the first valid in-policy invocation, then caches it without freshness or refresh checks. The TIB requires cold-start/before-serving attestation and fail-closed behavior when attestation becomes stale, missing, or drifted.

Required fix: Add the startup/dedicated attestation and freshness/context gates, with tests covering pre-attestation serving and stale/drift refresh—or narrow the PR contract and docs so this is not presented as the custody gate.

CI: Lint, Typecheck, Dead-Code, and Notify pass. Test remains pending.

Formal CHANGES_REQUESTED review posted on commit 484b49104f246edac59f87d14854b8a968de05c0.

@prd-carapulse prd-carapulse 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.

Changes requested: the KMS custody check is lazy, not a startup/before-serving attestation. The TIB requires each active signing deployment to attest at cold start and before serving, then keep readiness red / signing closed when the attestation is stale or missing. Here createHandler() only calls createKmsMakerSigner() after a valid in-policy invocation (index.ts:145), and a successful promise is cached without a freshness/refresh mechanism. A malformed or out-of-policy request can therefore reach a serving container before custody is proven, and a later key/image/config drift is not detected. Add the required invocation/context-gated startup or dedicated attestation path (or narrow the PR contract and docs so this is not presented as the custody gate), with tests for pre-attestation serving and stale/drift refresh. CI Test is also still pending, so approval is withheld.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 484b49104f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread services/quoter-signer/src/index.ts
- Cold-start attestation warm-up (carapulse): when the policy document
  and KMS variables are configured, custody proving starts at container
  init, before the first invocation; serving never depends on it and a
  misconfigured deployment stays a typed denial, never an init crash.
- Attestation freshness bound (carapulse): the cached attested signer
  expires after KMS_ATTESTATION_FRESHNESS_MS (5 min), so key or
  deployment drift on a warm container fails closed at the next window;
  eviction is race-guarded so a stale rejection cannot drop a fresher
  attestation. README narrows the claim: the TIB's registry-backed
  startup/readiness attestation remains a later increment, and this
  build's guarantee is that the digest-signing primitive is reachable
  only behind a fresh attestation.
- Denial-contract docs (codex): a denial no longer claims zero KMS
  activity — kms:Sign is never called, but the attested path performs
  the read-only GetPublicKey attestation.
- Tests: cold-start warm-up, pre-attestation wire-contract serving,
  stale re-attestation with drift fail-closed (mutation-checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@julien-devatom

Copy link
Copy Markdown
Collaborator Author

Round-two findings addressed in bd36b8b:

  • Startup attestation (carapulse): createHandler now starts a best-effort custody attestation at container init whenever the policy document and KMS variables are configured — before the first invocation — and the cached attestation carries a 5-minute freshness bound (KMS_ATTESTATION_FRESHNESS_MS): past the window the next signing-relevant invocation re-attests and fails closed on key/deployment drift, with race-guarded eviction. Tests cover cold-start warm-up, wire-contract serving while attestation is pending, and stale re-attestation with drift (mutation-checked). The README also narrows the claim per your alternative: the TIB's registry-backed startup/readiness attestation (setup/health surface, manifest-pinned freshness, scheduled refresh, alias/image validation) is explicitly a later increment, and this build's stated guarantee is that the digest-signing primitive is reachable only behind a fresh attestation — an unattested container serves only wire-contract/policy denials, and no signing surface exists at all. CI Test is green on the current head.
  • Denial contract (codex): the envelope no longer claims zero KMS activity on denial.

@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@codex review

@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@prd-carapulse[bot] can you review and approve if all good

@prd-carapulse

prd-carapulse Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Changes requested — not approved.

Blocking: services/quoter-signer/src/kms-signer.utils.ts:275signDigest() remains callable after KMS_ATTESTATION_FRESHNESS_MS expires. Freshness is checked only when the handler resolves the signer, not immediately before signing. Re-attest or enforce expiry in the signer, then add an expiry-path test.

Checks: ✅ Lint, Typecheck, Dead-Code. ⏳ Test still running.

Review: #179 (review)

@prd-carapulse prd-carapulse 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.

Changes requested. The custody freshness invariant is not enforced by the exported signer itself; CI is otherwise green except Test is still running.

Comment thread services/quoter-signer/src/kms-signer.utils.ts
@prd-carapulse

prd-carapulse Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Verdict: Changes Requested 🔴 (1 blocking issue, 0 suggestions)

Blocking

  • services/quoter-signer/src/kms-signer.utils.ts:275signDigest() can be called after KMS_ATTESTATION_FRESHNESS_MS expires. Freshness is checked only by the handler cache when resolving the signer, not by the exported signing primitive. Re-attest or enforce expiry immediately before signing, then add an expiry-path test.

Checks

  • ✅ Lint, Typecheck, Dead-Code
  • ⏳ Test still in progress

Review: #179 (review)

Reviewed by Hermes Agent

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: bd36b8b413

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Apply carapulse round-three blocking finding: signDigest() now records
its attestation time and refuses to sign once it ages past
KMS_ATTESTATION_FRESHNESS_MS (typed retryable KmsAttestationStaleError,
no KMS call), so the freshness bound holds for a held signer object and
never depends on the handler cache's resolution discipline alone. The
constant moves into kms-signer.utils.ts, which owns attestation; expiry
boundary test added (mutation-checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@codex review

@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@prd-carapulse[bot] can you review and approve if all good

@prd-carapulse

prd-carapulse Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Reviewed PR #179 at 5ed3c3e47da05f497931f47ce07e98bd3ab79c8f.

  • No blocking code findings.
  • git diff --check: pass.
  • Lint, Typecheck, Dead-Code, Notify: pass.
  • Test: still in_progress.

Approval withheld until Test completes successfully. Review posted: #179 (review)

Local test execution was blocked by Node engine mismatch: required ^24.14.1, runtime v26.3.0.

@prd-carapulse prd-carapulse 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.

Reviewed current head 5ed3c3e.

No blocking code findings: the KMS request-id validation, ARN pinning, single-attempt client, cold-start attestation, cache refresh, signer freshness gate, strict SPKI/DER validation, low-s normalization, recovery checks, and regression coverage are present. git diff --check passes.

Approval withheld: required GitHub Test check is still in_progress; Lint, Typecheck, Dead-Code, and Notify pass. Approve after Test completes successfully.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 5ed3c3e47d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@julien-devatom

Copy link
Copy Markdown
Collaborator Author

@prd-carapulse[bot] the Test check has completed successfully since your last review of 5ed3c3e (all checks green) — can you review and approve if all good

@prd-carapulse

prd-carapulse Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Approved PR #179 at 5ed3c3e47da05f497931f47ce07e98bd3ab79c8f.

All checks pass: Lint, Test, Typecheck, Dead-Code, Notify. No blocking correctness or security findings. Merge not performed.

@prd-carapulse prd-carapulse 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.

Reviewed current head 5ed3c3e. All required checks pass: Lint, Test, Typecheck, Dead-Code, and Notify. No blocking correctness or security findings.

@julien-devatom
julien-devatom force-pushed the kms-middleware-checks-f85533 branch from f00b0bb to 81ff252 Compare August 27, 2026 07:14
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