Skip to content

Feat/security hardening - #385

Merged
Depo-dev merged 13 commits into
Telocel-Labs:devfrom
DioChuks:feat/security-hardening
Aug 31, 2026
Merged

Feat/security hardening#385
Depo-dev merged 13 commits into
Telocel-Labs:devfrom
DioChuks:feat/security-hardening

Conversation

@DioChuks

Copy link
Copy Markdown
Contributor

Summary

Successfully hardened API key validation in the Go REST API, added CLI/script key generation tooling, documented the complete API key lifecycle, updated environment configuration references, and verified all tests pass.

Changes Made

1. API Key Validation Middleware

  • auth.go:
    • Added crypto/subtle import.
    • Implemented ConstantTimeContains(validHashes, targetHash) helper using subtle.ConstantTimeCompare to eliminate timing side-channel vulnerabilities during hash comparisons.
    • Updated ParseKeyHashes to automatically fall back to single-key environment variable API_KEY when API_KEY_HASHES is not set.
    • Ensured raw key strings are never stored in memory structures or emitted in logs.

2. Key Generation Tooling

  • services/api/cmd/keygen/main.go:
    • Created a Go CLI utility that generates a 32-byte cryptographically secure random API key (crypto/rand), hex-encodes it (64 characters), computes its HMAC-SHA256 digest using API_KEY_SALT, and prints deployment configuration instructions.
  • scripts/generate-api-key.sh:
    • Created helper script wrapping go run ./cmd/keygen for quick key generation.

3. Documentation & Environment References

  • [docs/api-keys.md]docs/api-keys.md):
    • Documented overview, security model, key generation instructions, environment setup, zero-downtime key rotation procedure, and Phase 1 vs Phase 2 architecture.
  • .env.example & docs/ENVIRONMENT.md:
    • Added documentation and usage guidance for API_KEY, API_KEY_HASHES, and API_KEY_SALT.

4. Unit Tests

  • auth_test.go:
    • Updated unit tests to verify valid key, missing key (401 + UNAUTHORIZED), invalid key (401 + UNAUTHORIZED), public /v1/health bypass, API_KEY fallback, constant-time hash comparison logic, and raw key non-leakage.

Verification Results

Unit Tests

Ran go test ./... in services/api:

  • All test packages (handlers, middleware, httputil, config, cursor, grpc, internal/*, validation, ws) passed 100%.

Closes #48

@drips-wave

drips-wave Bot commented Jul 31, 2026

Copy link
Copy Markdown

@DioChuks Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

match |= subtle.ConstantTimeCompare([]byte(hash), []byte(target))
}
}
return match == 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The timing-attack premise doesn't hold here. What's being compared is hmacKeyHash(key) — an HMAC-SHA256 digest of the secret, not the secret itself. Leaking how many leading bytes of a digest matched doesn't get an attacker closer to a valid key, because they'd have to invert the HMAC to use it.

So this trades an O(1) map lookup for an O(n) scan over every hash on each request, for no security gain. The crypto/subtle use in admin.go is different and correct — that one compares the raw ADMIN_API_KEY.

Happy to be shown otherwise if you've got a concrete attack in mind, but as written I'd leave the map lookup alone.

Comment thread services/api/middleware/auth.go Outdated
// (as stored in API_KEY_HASHES or API_KEY) into a set for lookup.
func ParseKeyHashes(raw string) map[string]struct{} {
out := map[string]struct{}{}
if raw == "" {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This makes a parse function read the environment, which is surprising — ParseKeyHashes(raw) now ignores its own argument and reaches for API_KEY when raw is empty.

It also quietly widens auth: API_KEY isn't documented as an auth source, and the values in API_KEY_HASHES are HMAC digests, so anyone setting API_KEY to a plaintext key gets a silent no-match rather than an error.

If we want an API_KEY fallback it should be a deliberate change with its own docs and tests, resolved at the call site rather than inside the parser.

Comment thread cmd/keygen/main.go Outdated
@@ -0,0 +1,47 @@
package main

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This file is byte-identical to services/api/cmd/keygen/main.go in the same PR. There's no go.mod at the repo root, so this copy isn't in any module and won't build — worth deleting and keeping the one under services/api/.

@DioChuks

Copy link
Copy Markdown
Contributor Author

Alright will resolve 'em

Depo-dev and others added 4 commits August 31, 2026 12:30
ParseKeyHashes ignored its own argument and fell back to os.Getenv("API_KEY")
when raw was empty. That made a parse function depend on the environment and
quietly widened the auth surface: API_KEY is not documented as an auth source,
and API_KEY_HASHES holds HMAC digests, so a plaintext API_KEY could only ever
produce a silent no-match rather than a clear error.

An API_KEY fallback, if wanted, belongs at the call site with its own docs and
tests.

go vet and go test ./middleware/ pass.
Byte-identical to services/api/cmd/keygen/main.go. There is no go.mod at the
repo root, so this copy belongs to no module and never built; the copy under
services/api is the real one and still builds.
@Depo-dev
Depo-dev dismissed their stale review August 31, 2026 14:16

Addressed in this PR: the API_KEY environment fallback is removed from ParseKeyHashes, and the duplicate cmd/keygen at the repo root is deleted (it was byte-identical to services/api/cmd/keygen and, with no root go.mod, belonged to no module).

Not addressed: ConstantTimeContains still does an O(n) subtle.ConstantTimeCompare scan rather than a map lookup. That remains a performance point rather than a security one — the values compared are HMAC digests, not the raw key — so it is not holding up the merge. Filing it separately.

@Depo-dev
Depo-dev merged commit 300453a into Telocel-Labs:dev Aug 31, 2026
Depo-dev added a commit that referenced this pull request Aug 31, 2026
- database/migrations/0031 + schema.sql: both branches created a 0031
  network-constraint migration. Kept dev's (via #593): #592's allowed only
  ('mainnet','testnet'), which would reject the futurenet and sandbox values
  the Network enum and the rest of the schema accept.
- streamer: both implement reorg detection for #196. Kept dev's
  check_and_handle_reorg (from #535) — it enforces max_reorg_depth and has a
  passing integration test — and dropped #592's detect_and_repair_reorg,
  its end-to-end test, and its now-unused make_event/mock_ledger_hash helpers.
- db/mod.rs: removed #592's recent_ledger_hashes and rewind_for_reorg with
  their tests. get_recent_ledger_metadata covers the first and the rollback
  inside check_and_handle_reorg covers the second.
- metrics.rs: both defined REORGS_TOTAL and record_reorg. Kept #592's
  documented versions, dropped the terse duplicates.
- .env.example: kept the idempotency encryption key and dropped the API_KEY
  entry, which documents a fallback #385 removed — auth.go now reads only
  API_KEY_HASHES and API_KEY_SALT. Corrected the stale comments too.

cargo clippy -p trident-indexer --all-targets -- -D warnings, go build and
go vet all pass.
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.

security: harden API key validation and document key lifecycle

2 participants