Fix Terra Classic / Keplr: ADR-036 verify + correct frontend signArbitrary #1

Closed
opened 2026-08-10 00:40:44 +00:00 by PlasticDigits · 13 comments
PlasticDigits commented 2026-08-10 00:40:44 +00:00 (Migrated from gitlab.com)

Summary

Make Terra Classic (TERRA_CLASSIC) signing work end-to-end with Keplr on the web portal, matching ADR-036 signArbitrary semantics on both the frontend and the API verifier.

Out of scope for this issue: Telegram, Solana, bot enforcement, WalletConnect / multi-wallet pickers.

Related gap analysis: gaps/GAP_1786322222.md (Terra / Keplr critical finding).


Current codebase

Portal (web/)

  • web/src/pages/terra.ts enables Keplr on columbus-5, obtains an OfflineSigner via getOfflineSigner, then calls:

    signer.signArbitrary(account.address, message)
    

    Keplr’s real API is window.keplr.signArbitrary(chainId, signerAddress, data), not a 2-arg Offline-signer method. The current call is incorrect against production Keplr.

  • Signature + pubkey are submitted to POST /api/v1/signatures/wallet with network: "TERRA_CLASSIC".

API (api/)

  • api/src/verify/terra.rs is documented as “ADR-036 style signArbitrary” but verifies ECDSA over raw message.as_bytes().
  • Keplr signArbitrary signs an ADR-036 amino/JSON sign/MsgSignData document (hashed per cosmos ADR-036), not the bare UTF-8 legal message.
  • Unit tests in the same file round-trip raw-byte signatures only — they never exercise a Keplr/ADR-036 vector, so CI currently green-lights a broken production path.
  • Account checks: api/src/account.rs only requires a terra prefix (weak bech32 validation).
  • Routing: api/src/verify/mod.rs → api/src/routes/signatures.rs wallet submit.

Message format (shared)

  • Canonical wallet message is built by packages/cl8y-clickwrap/src/message.ts / api/src/message.rs (version, effective date, property, network, account, timestamp). That plain-text string is what the user should see in Keplr; ADR-036 wraps that string as the signed data payload.

Why this is needed

Terra Classic is an in-scope portal network. Today the path is non-functional against real Keplr: wrong client API and wrong server verify. Integrators and end users following sign_urls.terra / /sign/terra-classic cannot complete acceptance. Leaving it broken erodes trust in the legal portal and blocks CL8Y properties that rely on Terra Classic wallets.


Constraints / guardrails

  1. Scope: Web portal + API verify for TERRA_CLASSIC only. Do not change Telegram/Solana flows in this issue.
  2. Chain: Keep Terra Classic (columbus-5) unless product explicitly expands; do not silently retarget Terra 2.0.
  3. Legal message: Do not change the canonical CL8Y acceptance message text without a coordinated Rust + SDK golden-test update.
  4. ADR-036 fidelity: Server must verify what Keplr actually signs (ADR-036), not invent a custom envelope.
  5. No secrets in client: Only public chain id / address / signature / pubkey in the browser.
  6. Backward compatibility: If any stored Terra proofs used the old raw-verify scheme, document migration (likely none in prod if path never worked). Prefer a clean ADR-036-only verify.
  7. Address binding: Continue deriving/checking that the provided pubkey corresponds to the claimed terra… address.
  8. Deps: Prefer well-known cosmos ADR-036 hashing approach; avoid large frontend SDKs unless necessary — portal can call keplr.signArbitrary directly.

Relevant files

Area Path
Portal Terra page web/src/pages/terra.ts
API Terra verify api/src/verify/terra.rs
Verify dispatch api/src/verify/mod.rs
Wallet submit api/src/routes/signatures.rs, api/src/signatures.rs
Account normalize api/src/account.rs
Message builders api/src/message.rs, packages/cl8y-clickwrap/src/message.ts
E2E (to extend) web/e2e/
Types / SDK network map packages/cl8y-clickwrap/src/types.ts

  1. Frontend: Call await window.keplr.signArbitrary(TERRA_CHAIN_ID, account.address, message) (or TextEncoder bytes if Keplr requires Uint8Array — match Keplr docs). Submit signature + pub_key.value (base64) as today.
  2. API: Implement ADR-036 digest construction for the arbitrary sign payload, then verify secp256k1 signature with the provided compressed pubkey; re-derive bech32 address and compare to account_id.
  3. Tests: Replace/extend unit tests with a known ADR-036 vector (fixture key + message → signature). Add API integration test for Terra wallet submit. Prefer mocked Keplr in Playwright (see testing issue) once verify works.
  4. Validation: Strengthen Terra address validation (bech32 charset/checksum), not only starts_with("terra").
  5. Docs: Update README integrator notes if Terra was previously described as working.

Acceptance criteria

  • Portal Terra page uses Keplr’s documented signArbitrary(chainId, signer, data) API.
  • API verifies ADR-036 signatures produced by that flow (not raw message bytes).
  • Successful submit yields signed_latest: true for (property, TERRA_CLASSIC, account).
  • Rejects wrong pubkey, wrong address, tampered message, expired/skewed timestamp (existing message rules).
  • Unit tests use ADR-036 vectors; raw-only roundtrip is removed or clearly non-production.
  • Terra Classic (columbus-5) remains the target chain.
  • No regressions to EVM wallet submit / verify.
  • Telegram/Solana code paths untouched (or only trivial shared helpers).

Test plan (functional paths)

  1. API unit: ADR-036 sign → verify happy path; wrong sig; pubkey/address mismatch; malformed base64; non-terra address.
  2. API integration: Publish terms → POST /signatures/wallet with Terra ADR-036 proof → GET /signatures/status signed_latest=true.
  3. Portal (manual or e2e mock): /sign/terra-classic?property=… → Keplr (or mock) → success UI.
  4. Cross-check: Same account signing EVM for another property remains independent (property isolation).
  5. Timestamp skew: Client timestamp outside ±300s rejected.
  6. Regression: Existing EVM unit + integration + Playwright still pass.

Test plan (attack / abuse / hack vectors)

Vector Expectation
Replay signature for different property / version_label / account Reject (message bind)
Submit ADR-036 sig for message A claiming message B in JSON body Reject
Pubkey for address A, account_id address B Reject
Truncated / malleable signature encodings Reject
Extremely large message / pubkey / signature bodies Reject or limited by body limits (add if missing)
Wrong chain’s Keplr account (if somehow submitted) Fail address/pubkey checks
Brute force random base64 sigs Always reject; rate limits apply on write

Verification criteria

  • cd api && cargo test includes Terra ADR-036 tests green.
  • Manual or scripted: real Keplr against local API accepts once; status endpoint confirms.
  • CI test:rust + test:e2e (or new Terra e2e) green.
  • Gap finding for Terra in gaps/GAP_1786322222.md considered resolved for portal scope.
## Summary Make Terra Classic (`TERRA_CLASSIC`) signing work end-to-end with Keplr on the **web portal**, matching ADR-036 `signArbitrary` semantics on both the frontend and the API verifier. **Out of scope for this issue:** Telegram, Solana, bot enforcement, WalletConnect / multi-wallet pickers. **Related gap analysis:** `gaps/GAP_1786322222.md` (Terra / Keplr critical finding). --- ## Current codebase ### Portal (`web/`) - `web/src/pages/terra.ts` enables Keplr on `columbus-5`, obtains an OfflineSigner via `getOfflineSigner`, then calls: ```ts signer.signArbitrary(account.address, message) ``` Keplr’s real API is `window.keplr.signArbitrary(chainId, signerAddress, data)`, not a 2-arg Offline-signer method. The current call is incorrect against production Keplr. - Signature + pubkey are submitted to `POST /api/v1/signatures/wallet` with `network: "TERRA_CLASSIC"`. ### API (`api/`) - `api/src/verify/terra.rs` is documented as “ADR-036 style `signArbitrary`” but verifies **ECDSA over raw `message.as_bytes()`**. - Keplr `signArbitrary` signs an ADR-036 amino/JSON `sign/MsgSignData` document (hashed per cosmos ADR-036), not the bare UTF-8 legal message. - Unit tests in the same file round-trip raw-byte signatures only — they never exercise a Keplr/ADR-036 vector, so CI currently green-lights a broken production path. - Account checks: `api/src/account.rs` only requires a `terra` prefix (weak bech32 validation). - Routing: `api/src/verify/mod.rs` → `api/src/routes/signatures.rs` wallet submit. ### Message format (shared) - Canonical wallet message is built by `packages/cl8y-clickwrap/src/message.ts` / `api/src/message.rs` (version, effective date, property, network, account, timestamp). That plain-text string is what the user should see in Keplr; ADR-036 wraps **that** string as the signed data payload. --- ## Why this is needed Terra Classic is an in-scope portal network. Today the path is **non-functional** against real Keplr: wrong client API and wrong server verify. Integrators and end users following `sign_urls.terra` / `/sign/terra-classic` cannot complete acceptance. Leaving it broken erodes trust in the legal portal and blocks CL8Y properties that rely on Terra Classic wallets. --- ## Constraints / guardrails 1. **Scope:** Web portal + API verify for `TERRA_CLASSIC` only. Do not change Telegram/Solana flows in this issue. 2. **Chain:** Keep Terra Classic (`columbus-5`) unless product explicitly expands; do not silently retarget Terra 2.0. 3. **Legal message:** Do not change the canonical CL8Y acceptance message text without a coordinated Rust + SDK golden-test update. 4. **ADR-036 fidelity:** Server must verify what Keplr actually signs (ADR-036), not invent a custom envelope. 5. **No secrets in client:** Only public chain id / address / signature / pubkey in the browser. 6. **Backward compatibility:** If any stored Terra proofs used the old raw-verify scheme, document migration (likely none in prod if path never worked). Prefer a clean ADR-036-only verify. 7. **Address binding:** Continue deriving/checking that the provided pubkey corresponds to the claimed `terra…` address. 8. **Deps:** Prefer well-known cosmos ADR-036 hashing approach; avoid large frontend SDKs unless necessary — portal can call `keplr.signArbitrary` directly. --- ## Relevant files | Area | Path | |------|------| | Portal Terra page | `web/src/pages/terra.ts` | | API Terra verify | `api/src/verify/terra.rs` | | Verify dispatch | `api/src/verify/mod.rs` | | Wallet submit | `api/src/routes/signatures.rs`, `api/src/signatures.rs` | | Account normalize | `api/src/account.rs` | | Message builders | `api/src/message.rs`, `packages/cl8y-clickwrap/src/message.ts` | | E2E (to extend) | `web/e2e/` | | Types / SDK network map | `packages/cl8y-clickwrap/src/types.ts` | --- ## Recommended direction 1. **Frontend:** Call `await window.keplr.signArbitrary(TERRA_CHAIN_ID, account.address, message)` (or `TextEncoder` bytes if Keplr requires `Uint8Array` — match Keplr docs). Submit `signature` + `pub_key.value` (base64) as today. 2. **API:** Implement ADR-036 digest construction for the arbitrary sign payload, then verify secp256k1 signature with the provided compressed pubkey; re-derive bech32 address and compare to `account_id`. 3. **Tests:** Replace/extend unit tests with a known ADR-036 vector (fixture key + message → signature). Add API integration test for Terra wallet submit. Prefer mocked Keplr in Playwright (see testing issue) once verify works. 4. **Validation:** Strengthen Terra address validation (bech32 charset/checksum), not only `starts_with("terra")`. 5. **Docs:** Update README integrator notes if Terra was previously described as working. --- ## Acceptance criteria - [ ] Portal Terra page uses Keplr’s documented `signArbitrary(chainId, signer, data)` API. - [ ] API verifies ADR-036 signatures produced by that flow (not raw message bytes). - [ ] Successful submit yields `signed_latest: true` for `(property, TERRA_CLASSIC, account)`. - [ ] Rejects wrong pubkey, wrong address, tampered message, expired/skewed timestamp (existing message rules). - [ ] Unit tests use ADR-036 vectors; raw-only roundtrip is removed or clearly non-production. - [ ] Terra Classic (`columbus-5`) remains the target chain. - [ ] No regressions to EVM wallet submit / verify. - [ ] Telegram/Solana code paths untouched (or only trivial shared helpers). --- ## Test plan (functional paths) 1. **API unit:** ADR-036 sign → verify happy path; wrong sig; pubkey/address mismatch; malformed base64; non-terra address. 2. **API integration:** Publish terms → `POST /signatures/wallet` with Terra ADR-036 proof → `GET /signatures/status` `signed_latest=true`. 3. **Portal (manual or e2e mock):** `/sign/terra-classic?property=…` → Keplr (or mock) → success UI. 4. **Cross-check:** Same account signing EVM for another property remains independent (property isolation). 5. **Timestamp skew:** Client timestamp outside ±300s rejected. 6. **Regression:** Existing EVM unit + integration + Playwright still pass. --- ## Test plan (attack / abuse / hack vectors) | Vector | Expectation | |--------|-------------| | Replay signature for different `property` / `version_label` / `account` | Reject (message bind) | | Submit ADR-036 sig for message A claiming message B in JSON body | Reject | | Pubkey for address A, `account_id` address B | Reject | | Truncated / malleable signature encodings | Reject | | Extremely large message / pubkey / signature bodies | Reject or limited by body limits (add if missing) | | Wrong chain’s Keplr account (if somehow submitted) | Fail address/pubkey checks | | Brute force random base64 sigs | Always reject; rate limits apply on write | --- ## Verification criteria - `cd api && cargo test` includes Terra ADR-036 tests green. - Manual or scripted: real Keplr against local API accepts once; status endpoint confirms. - CI `test:rust` + `test:e2e` (or new Terra e2e) green. - Gap finding for Terra in `gaps/GAP_1786322222.md` considered resolved for portal scope.
PlasticDigits commented 2026-08-10 00:41:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #4

mentioned in issue #4
PlasticDigits commented 2026-08-10 00:41:44 +00:00 (Migrated from gitlab.com)

marked as related to #4

marked as related to #4
PlasticDigits commented 2026-08-10 00:58:38 +00:00 (Migrated from gitlab.com)

mentioned in merge request !23

mentioned in merge request !23
PlasticDigits commented 2026-08-10 00:59:04 +00:00 (Migrated from gitlab.com)

Implemented in !23 (worktree fix/terra-classic-adr036).

  • Portal: window.keplr.signArbitrary(columbus-5, …)
  • API: CosmJS ADR-036 verify + bech32 account normalize
  • Tests: unit vectors, integration submit→signed_latest, Playwright Keplr mock
  • Docs: README Terra section, skills/terra-classic-adr036/SKILL.md, gap note update
Implemented in !23 (worktree `fix/terra-classic-adr036`). - Portal: `window.keplr.signArbitrary(columbus-5, …)` - API: CosmJS ADR-036 verify + bech32 account normalize - Tests: unit vectors, integration submit→`signed_latest`, Playwright Keplr mock - Docs: README Terra section, `skills/terra-classic-adr036/SKILL.md`, gap note update
PlasticDigits commented 2026-08-10 01:54:09 +00:00 (Migrated from gitlab.com)

Merge readiness check (MR !23)

Reviewed tip 6ec0af6 against issue #1 acceptance criteria.

Acceptance criteria

Criterion Status
Portal uses Keplr signArbitrary(chainId, signer, data) Met
API verifies ADR-036 (not raw bytes) Met
Success → signed_latest: true Met (integration + Playwright)
Rejects wrong pubkey / address / tampered message / skew Met (unit + HTTP abuse integration)
ADR-036 unit vectors; raw-only removed Met
Stay on columbus-5 Met
No EVM regressions Met
Telegram/Solana untouched Met

CI on tip: pipeline #2745946647 green (test:rust + test:e2e). MR source was stale at 74a80e6 and has been updated to 6ec0af6.

Residual / follow-ups (non-blocking)

  1. Manual Keplr smoke still unchecked in MR test plan — mock e2e + CosmJS vectors cover CI; one real-extension sign recommended before production.
  2. Optional: dedicated HTTP negative for mismatched account_id in body (covered in verify_terra unit path, not a separate HTTP case).
  3. Process note (resolved for merge): MR head was previously 2 commits behind fix/terra-classic-adr036.
## Merge readiness check (MR !23) Reviewed tip `6ec0af6` against issue #1 acceptance criteria. ### Acceptance criteria | Criterion | Status | |-----------|--------| | Portal uses Keplr `signArbitrary(chainId, signer, data)` | **Met** | | API verifies ADR-036 (not raw bytes) | **Met** | | Success → `signed_latest: true` | **Met** (integration + Playwright) | | Rejects wrong pubkey / address / tampered message / skew | **Met** (unit + HTTP abuse integration) | | ADR-036 unit vectors; raw-only removed | **Met** | | Stay on `columbus-5` | **Met** | | No EVM regressions | **Met** | | Telegram/Solana untouched | **Met** | CI on tip: pipeline [#2745946647](https://gitlab.com/PlasticDigits/cl8y-ecosystem-legal/-/pipelines/2745946647) green (`test:rust` + `test:e2e`). MR source was stale at `74a80e6` and has been updated to `6ec0af6`. ### Residual / follow-ups (non-blocking) 1. **Manual Keplr smoke** still unchecked in MR test plan — mock e2e + CosmJS vectors cover CI; one real-extension sign recommended before production. 2. Optional: dedicated HTTP negative for mismatched `account_id` in body (covered in `verify_terra` unit path, not a separate HTTP case). 3. Process note (resolved for merge): MR head was previously 2 commits behind `fix/terra-classic-adr036`.
PlasticDigits commented 2026-08-10 02:03:31 +00:00 (Migrated from gitlab.com)

mentioned in commit 03849d3bed

mentioned in commit 03849d3bed404af9be62c633cc3ee518fd81ac7b
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-10 02:03:31 +00:00
PlasticDigits commented 2026-08-10 02:04:28 +00:00 (Migrated from gitlab.com)

Merged

MR !23 merged to main as 03849d3 (source tip 6ec0af6).

Issue #1 acceptance criteria are satisfied in code + CI (test:rust, test:e2e). Remaining optional: one manual real-Keplr smoke before production cutover.

## Merged MR !23 merged to `main` as `03849d3` (source tip `6ec0af6`). Issue #1 acceptance criteria are satisfied in code + CI (`test:rust`, `test:e2e`). Remaining optional: one manual real-Keplr smoke before production cutover.
PlasticDigits commented 2026-08-10 02:29:46 +00:00 (Migrated from gitlab.com)

mentioned in commit 69e117eb88

mentioned in commit 69e117eb88d9f5e985ca1c0108fdf88810858a13
PlasticDigits commented 2026-08-10 02:29:57 +00:00 (Migrated from gitlab.com)

mentioned in merge request !24

mentioned in merge request !24
PlasticDigits commented 2026-08-18 00:20:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #9

mentioned in issue #9
PlasticDigits commented 2026-08-18 00:20:54 +00:00 (Migrated from gitlab.com)

marked as related to #9

marked as related to #9
PlasticDigits commented 2026-08-18 14:41:50 +00:00 (Migrated from gitlab.com)

mentioned in merge request !29

mentioned in merge request !29
PlasticDigits commented 2026-08-18 15:00:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #10

mentioned in issue #10
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-ecosystem-legal#1
No description provided.