specs/001-gpg-event-signing/plan.md
Ref: Size: 5.7 KiB
# Implementation Plan: Ed25519 Signing for Event Commits **Branch**: `001-gpg-event-signing` | **Date**: 2026-03-21 | **Spec**: `specs/001-gpg-event-signing/spec.md` **Input**: Feature specification from `/specs/001-gpg-event-signing/spec.md` ## Summary Add Ed25519 signing and verification to all event commits in git-collab. Every event (issues, comments, patches, reviews, etc.) will embed a cryptographic signature and public key in `event.json`. Sync verification rejects unsigned or invalidly-signed events. Uses pure-Rust `ed25519-dalek` crate — no external GPG/SSH binaries. Keypair provisioned via explicit `collab init-key` command. ## Technical Context **Language/Version**: Rust 2021 edition **Primary Dependencies**: git2 0.19, clap 4, serde/serde_json 1, chrono 0.4, thiserror 2. New: `ed25519-dalek`, `rand`, `base64` **Storage**: Git-native (event DAG stored as commits with `event.json` blobs under `refs/collab/`) **Testing**: `cargo test` with `tempfile` for integration tests **Target Platform**: Linux (CLI tool) **Project Type**: CLI tool / library **Performance Goals**: Signature verification < 1 second per 100 commits (SC-004) **Constraints**: Pure-Rust crypto, no external binary dependencies, in-process signing **Scale/Scope**: Single-user local operation with multi-user sync via git remotes ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* Constitution is unconfigured (template only). No gates to enforce. Proceeding. ## Architecture ### Signing Flow ``` User creates event (e.g., issue open) → Event struct built (without signature fields) → Serialize to canonical JSON (deterministic key ordering) → Load Ed25519 private key from ~/.config/git-collab/signing-key → Sign canonical JSON bytes → Add "signature" (base64) and "pubkey" (base64) fields to Event → Serialize final event.json with signature → Create git commit as before ``` ### Verification Flow (Sync) ``` Fetch remote refs → For each incoming commit, read event.json → Extract and remove "signature" and "pubkey" fields → Reserialize remaining fields to canonical JSON → Verify signature against pubkey and canonical bytes → If valid: accept commit for reconciliation → If invalid/missing: reject entire ref update, report details ``` ### Key Management ``` collab init-key → Generate Ed25519 keypair using ed25519-dalek + rand → Store private key at ~/.config/git-collab/signing-key (base64) → Store public key at ~/.config/git-collab/signing-key.pub (base64) → Print public key for sharing with collaborators ``` ## Project Structure ### Documentation (this feature) ```text specs/001-gpg-event-signing/ ├── plan.md # This file ├── spec.md # Feature specification ├── research.md # Phase 0 output ├── data-model.md # Phase 1 output ├── quickstart.md # Phase 1 output ├── contracts/ # Phase 1 output └── tasks.md # Phase 2 output (speckit.tasks) ``` ### Source Code (repository root) ```text src/ ├── main.rs # CLI entry point ├── lib.rs # Library entry, run() dispatcher ├── cli.rs # Clap command definitions (add init-key) ├── event.rs # Event/Action structs (add signature, pubkey fields) ├── dag.rs # DAG operations (integrate signing into commit creation) ├── signing.rs # NEW: Ed25519 key management, sign, verify functions ├── identity.rs # Author/signature (unchanged) ├── state.rs # State materialization (unchanged) ├── issue.rs # Issue operations (unchanged — signing injected at dag layer) ├── patch.rs # Patch operations (unchanged — signing injected at dag layer) ├── sync.rs # Sync logic (add verification before reconciliation) ├── tui.rs # TUI dashboard (unchanged) └── error.rs # Error types (add signing/verification errors) tests/ ├── common/mod.rs # Test helpers (add key generation helpers) ├── collab_test.rs # Existing tests (update to use signing) ├── sync_test.rs # Sync tests (add verification tests) └── cli_test.rs # CLI tests (add init-key tests) ``` **Structure Decision**: Single-crate CLI project. New `signing.rs` module contains all Ed25519 logic. Signing is injected at the DAG layer (`dag.rs`) so all event creation paths automatically sign. Verification is added in `sync.rs` before reconciliation. ## Key Design Decisions 1. **Signing at DAG layer, not domain layer**: `dag::create_root_event()` and `dag::append_event()` handle signing so that issue.rs, patch.rs, etc. don't need modification. The signing key is passed as a parameter. 2. **Canonical serialization**: Use `serde_json::to_string()` (not pretty-printed) with sorted keys for the signable payload. The `signature` and `pubkey` fields are added *after* signing, so they're naturally excluded from the signed content. 3. **Two-step serialization**: First serialize Event without signature fields → sign → then serialize SignedEvent (Event + signature + pubkey) as pretty-printed JSON for the blob. This means Event struct stays clean; a wrapper handles the signature fields. 4. **Key storage path**: `~/.config/git-collab/` following XDG conventions. Private key file permissions set to 0o600. 5. **Verification is all-or-nothing per ref**: If any commit in a ref's history fails verification, the entire ref is rejected (FR-008). This is checked by walking the DAG during sync. ## Complexity Tracking No constitution violations to justify.