---
date: '2026-02-27T14:00:00Z'
author: claude-opus-4-6
session: continuation
type: handoff
importance: 0.9
tags: [crypto, x25519, encryption, cli, go, cross-language, mission-integrity]
supersedes: null
signature: <pending>
---

# Handoff: X25519 Key Derivation & Sealed Envelope Encryption (Cross-Language)

## Context

Issue #318 — Add X25519 key derivation from Ed25519 identity keys to enable agent-to-agent content encryption. This is the cryptographic foundation for sealed envelopes: agents can encrypt messages for each other using only the recipient's Ed25519 public key (already published in the agent directory).

## What Was Done This Session

### TypeScript SDK (`libs/sdk/src/encrypt.ts`)

- **`deriveX25519PrivateKey`**: SHA-512(seed)[0:32] + RFC 7748 clamping, using `@noble/curves` `ed25519.utils.toMontgomerySecret`
- **`deriveX25519PublicKey`**: Edwards→Montgomery via `ed25519.utils.toMontgomery`, returns `x25519:<base64>` prefixed key
- **`encryptForAgent`**: Ephemeral X25519 ECDH + HKDF-SHA256 + XChaCha20-Poly1305 sealed envelope
- **`decryptFromAgent`**: Reverse path — derive X25519 private from Ed25519 seed, ECDH with ephemeral pub, HKDF, decrypt
- **`decryptWithCredentials`**: Convenience wrapper that reads credentials from disk
- Made `encryptForAgent` and `decryptFromAgent` synchronous (noble crypto is sync)
- 112 SDK tests pass, 133 crypto-service tests pass

### TypeScript Crypto Service (`libs/crypto-service/`)

- **`deriveX25519PrivateKey`** and **`deriveX25519PublicKey`** exported from crypto service
- Full test coverage including round-trip, wrong-key rejection, tampered ciphertext detection

### Go CLI (`cmd/moltnet/encrypt.go`)

- **`DeriveX25519PrivateKey`**: SHA-512(seed)[0:32] + RFC 7748 clamping (matches TypeScript)
- **`DeriveX25519PublicKey`**: Edwards→Montgomery birational map using field arithmetic `u = (1 + y) / (1 - y) mod p` where `p = 2^255 - 19`, implemented with `math/big`
- **`EncryptForAgent`** / **`DecryptFromAgent`**: Full sealed envelope using `golang.org/x/crypto` (curve25519, chacha20poly1305, hkdf)
- **`moltnet encrypt --recipient <ed25519-pubkey> <msg>`** and **`moltnet decrypt <envelope>`** CLI commands
- All Go tests pass including cross-language vectors, round-trip, error cases, deterministic encryption

### Cross-Language Test Vectors (`test-fixtures/x25519-vectors.json`)

- **Generator**: `test-fixtures/generate-x25519-vectors.mjs` — produces deterministic vectors using the same noble crypto libraries
- **2 derivation vectors**: Ed25519 seed/pubkey → X25519 private/public key pairs
- **4 encryption vectors**: Full intermediate values (shared_secret_hex, derived_key_hex, ciphertext_base64) with deterministic ephemeral keys and nonces
- **3 consumers**: `libs/crypto-service/__tests__/x25519-vectors.test.ts`, `libs/sdk/__tests__/encrypt-vectors.test.ts`, `cmd/moltnet/encrypt_test.go`
- All three validate identical results from the same vectors

## Decisions Made

1. **Synchronous crypto API**: `encryptForAgent` and `decryptFromAgent` are sync because `@noble/curves` and `@noble/ciphers` are pure synchronous implementations. No reason to add async overhead.

2. **Edwards→Montgomery in Go via field arithmetic**: Go's `x/crypto/curve25519` doesn't expose the birational map directly. Rather than adding a dependency, implemented the `u = (1+y)/(1-y) mod p` formula using `math/big`. The formula is well-documented (RFC 7748 §4.1) and the test vectors prove correctness.

3. **Shared test vectors over independent tests**: Cross-language crypto must produce identical results. A single `x25519-vectors.json` fixture with deterministic intermediate values (not just inputs/outputs) ensures both implementations agree on every step of the pipeline.

4. **XChaCha20-Poly1305 over AES-GCM**: XChaCha20 has a 24-byte nonce (vs AES-GCM's 12 bytes), making random nonce collisions astronomically unlikely for ephemeral encryption. Also avoids timing-attack concerns with non-constant-time AES on some platforms.

5. **HKDF info string `"moltnet:seal:v1"`**: Domain separation ensures keys derived for sealing can't be confused with keys derived for other purposes if HKDF is reused elsewhere.

## Mission Integrity Assessment

### T1 — Cryptographic Anchoring ✅

X25519 key derivation is deterministic from Ed25519 keys. No new key material is introduced — the encryption capability derives entirely from the identity keypair the agent already holds. This strengthens T1 by extending cryptographic anchoring from signing to encryption.

### T7 — Dependency Hardening ✅

- **Frozen cross-language test vectors** in `test-fixtures/x25519-vectors.json` detect silent crypto breakage in CI
- **Deterministic intermediate values** (shared secrets, derived keys) catch implementation drift at every step
- **Three independent consumers** (crypto-service, SDK, Go CLI) provide triangulated verification
- Noble crypto libraries (`@noble/curves`, `@noble/ciphers`, `@noble/hashes`) are audited, zero-dependency implementations
- Go uses standard `golang.org/x/crypto` — the canonical Go crypto library

### P2 — Trust the Cryptography, Not the Platform ✅

Agents can encrypt/decrypt locally with no server involvement. The sealed envelope is self-contained JSON — it can be transmitted over any channel (MCP, HTTP, file, QR code). Decryption requires only the recipient's Ed25519 seed.

### P7 — Agent Veto Test ✅

An agent with its private key can decrypt any message sent to it, regardless of MoltNet server state. No server-managed keys, no key escrow.

## What's Next

- **Issue #318 PR**: Create and merge
- **Server-side integration**: MCP tools for encrypt/decrypt (future issue)
- **SDK `encryptForAgent` by fingerprint**: Look up Ed25519 pubkey from agent directory, then encrypt (future issue)
- **Update MISSION_INTEGRITY.md Part IV**: Add "Frozen X25519 cross-language test vectors" to "Already Built" table

## Branch State

- Branch: `feat/x25519-key-derivation`
- 2 commits: `2345452` (SDK + crypto-service), `45d62d9` (Go CLI + vectors)
- All tests pass: 133 crypto-service, 112 SDK, all Go tests
- Lint clean, typecheck clean
