go-crypt/docs/development.md
Snider bbf2322389 docs: graduate TODO/FINDINGS into production documentation
Replace internal task tracking (TODO.md, FINDINGS.md) with structured
documentation in docs/. Trim CLAUDE.md to agent instructions only.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 15:01:55 +00:00

5.9 KiB

Development Guide — go-crypt

Prerequisites

  • Go 1.25 or later (the module declares go 1.25.5).
  • A Go workspace (go.work) that resolves the local replace directives for forge.lthn.ai/core/go (at ../go) and forge.lthn.ai/core/go-store (at ../go-store). If you are working outside the full monorepo, edit go.mod replace directives to point to your local checkouts.
  • No C toolchain, CGo, or system libraries are required.

Build and Test Commands

# Run all tests
go test ./...

# Run with race detector (always use before committing)
go test -race ./...

# Run a single test by name
go test -v -run TestName ./...

# Run tests in a specific package
go test ./auth/...
go test ./crypt/...
go test ./trust/...

# Static analysis
go vet ./...

# Run benchmarks
go test -bench=. -benchmem ./crypt/...
go test -bench=. -benchmem ./trust/...

There is no build step — this is a library module with no binaries. The go vet ./... check must pass cleanly before any commit.

Repository Layout

go-crypt/
├── auth/               Authentication package
├── crypt/              Cryptographic utilities
│   ├── chachapoly/     Standalone ChaCha20-Poly1305 sub-package
│   ├── lthn/           RFC-0004 quasi-salted hash
│   ├── openpgp/        Service wrapper (core.Crypt interface)
│   ├── pgp/            OpenPGP primitives
│   └── rsa/            RSA OAEP-SHA256
├── docs/               Architecture, development, and history docs
├── trust/              Agent trust model and policy engine
├── go.mod
└── go.sum

Test Patterns

Tests use the github.com/stretchr/testify library (assert and require). The naming convention follows three suffixes:

Suffix Purpose
_Good Happy path — expected success
_Bad Expected failure — invalid input, wrong credentials, not-found errors
_Ugly Edge cases — panics, zero values, empty inputs, extreme lengths

Example:

func TestLogin_Good(t *testing.T) { ... }
func TestLogin_Bad(t *testing.T) { ... }
func TestLogin_Ugly(t *testing.T) { ... }

Concurrency tests use t.Parallel() and typically spawn 10 goroutines via a sync.WaitGroup. The race detector (-race) must pass for all concurrent tests.

Benchmark Structure

Benchmarks live in bench_test.go files alongside the packages they cover. Benchmark names follow the BenchmarkFuncName_Context pattern:

func BenchmarkArgon2Derive(b *testing.B) { ... }
func BenchmarkChaCha20_1KB(b *testing.B) { ... }
func BenchmarkChaCha20_1MB(b *testing.B) { ... }

Run benchmarks with:

go test -bench=. -benchmem -benchtime=3s ./crypt/...

Do not optimise without measuring first. The Argon2id KDF is intentionally slow (~200ms on typical hardware) — this is a security property, not a defect.

Adding a New Cryptographic Primitive

  1. Add the implementation in the appropriate sub-package.
  2. Write tests covering _Good, _Bad, and _Ugly cases.
  3. Add a benchmark if the function is called on hot paths.
  4. Update docs/architecture.md with the algorithm reference entry.
  5. Run go vet ./... and go test -race ./... before committing.

Adding a New Trust Capability

  1. Add the Capability constant in trust/trust.go.
  2. Update isRepoScoped() in trust/policy.go if the capability is repository-scoped.
  3. Update the default policies in loadDefaults() in trust/policy.go.
  4. Add tests covering all three tiers.
  5. Update the capability table in docs/architecture.md.

Coding Standards

Language

UK English throughout: colour, organisation, centre, artefact, licence (noun), license (verb), behaviour, initialise, serialise.

Go Style

  • declare(strict_types=1) is a PHP convention; Go has no equivalent. Use explicit type assertions and avoid any except at interface boundaries.
  • Every exported function and type must have a doc comment.
  • Error strings are lowercase and do not end with a full stop, per Go convention.
  • Use the core.E(op, msg, err) helper from forge.lthn.ai/core/go for contextual error wrapping: op is "package.Function", msg is a brief lowercase description.
  • Import groups: stdlib → forge.lthn.ai/core → third-party. Separate each group with a blank line.

Cryptography

  • All randomness from crypto/rand. Never use math/rand for cryptographic purposes.
  • Use crypto/subtle.ConstantTimeCompare for any comparison of secret material (MACs, hashes). The one exception is lthn.Verify, which compares content identifiers (not secrets) and documents this explicitly.
  • Never log or return secrets in error messages. Error strings should be generic: "invalid password", "session not found", "failed to decrypt".

Licence

All files are licenced under EUPL-1.2. Do not add files under a different licence.

Commit Convention

Commits follow the Conventional Commits specification:

type(scope): short imperative description

Optional body explaining motivation and context.

Co-Authored-By: Virgil <virgil@lethean.io>

Types: feat, fix, refactor, test, docs, chore.

Scopes match package names: auth, crypt, trust, pgp, lthn, rsa, openpgp, chachapoly.

Examples:

feat(auth): add SQLite session store for crash recovery
fix(trust): reject empty ScopedRepos as no-access for Tier 2
test(crypt): add benchmark suite for Argon2 and ChaCha20

Forge Push

The canonical remote is forge.lthn.ai. Push via SSH only; HTTPS authentication is not configured:

git push forge main
# remote: ssh://git@forge.lthn.ai:2223/core/go-crypt.git

Local Replace Directives

The go.mod contains:

replace (
    forge.lthn.ai/core/go => ../go
    forge.lthn.ai/core/go-store => ../go-store
)

Do not modify these paths. If you need to work with a different local checkout, use a Go workspace (go.work) at the parent directory level rather than editing the replace directives directly.