specs/001-gpg-event-signing/research.md
Ref: Size: 4.0 KiB
# Research: Ed25519 Signing for Event Commits
## R1: Ed25519 Crate Selection
**Decision**: Use `ed25519-dalek` crate for Ed25519 signing and verification.
**Rationale**: Most widely used pure-Rust Ed25519 implementation. Well-audited, maintained by the RustCrypto community. Provides `SigningKey`, `VerifyingKey`, and `Signature` types with straightforward API. Compatible with standard Ed25519 signatures.
**Alternatives considered**:
- `ring`: Faster but less ergonomic API, heavier dependency, not pure-Rust (uses C/ASM)
- `ec25519-compact`: Smaller crate used by Radicle, but less ecosystem support
- `ed25519-zebra`: Zcash-focused, batch verification oriented — overkill for this use case
**Crate versions**:
- `ed25519-dalek = "2"` (with `rand_core` feature for key generation)
- `rand = "0.8"` (for `OsRng` entropy source)
- `base64 = "0.22"` (for encoding keys and signatures in JSON)
## R2: Canonical Serialization Strategy
**Decision**: Serialize Event struct (without signature/pubkey fields) using `serde_json::to_string()` with a custom serializer that sorts map keys.
**Rationale**: JSON key ordering is not guaranteed by default in serde_json (it uses insertion order). For signature verification to work across different serialization contexts, key ordering must be deterministic. Using `serde_json`'s `sort_keys` feature (via `to_value()` then serializing the Value) guarantees canonical output.
**Approach**:
1. Serialize `Event` to `serde_json::Value`
2. The Value type naturally sorts object keys when serialized
3. Use `serde_json::to_string(&value)` for compact, sorted JSON
4. This byte string is what gets signed
**Alternatives considered**:
- JCS (JSON Canonicalization Scheme, RFC 8785): More rigorous but requires a separate crate and may over-complicate for this use case
- Hash the git tree instead: Ties signature to git internals rather than event content; harder to verify independently
## R3: Event Struct Design for Signatures
**Decision**: Use a wrapper `SignedEvent` struct that contains `Event` fields plus `signature` and `pubkey`, rather than adding optional fields to `Event`.
**Rationale**: Keeps the `Event` struct clean for internal use (construction, business logic). The `SignedEvent` is what gets serialized to `event.json`. On deserialization, the signature fields are extracted for verification, and the remaining fields reconstitute the `Event`.
**Implementation**:
```rust
// In event.rs — Event stays unchanged
// In signing.rs
#[derive(Serialize, Deserialize)]
struct SignedEvent {
#[serde(flatten)]
event: Event,
signature: String, // base64-encoded Ed25519 signature
pubkey: String, // base64-encoded Ed25519 public key
}
```
## R4: Key Storage Location and Format
**Decision**: Store keypair at `~/.config/git-collab/signing-key` (private) and `~/.config/git-collab/signing-key.pub` (public), both base64-encoded.
**Rationale**: Follows XDG Base Directory conventions. Separating public and private keys into distinct files is standard practice. Base64 encoding keeps files text-friendly and easy to copy/share (public key).
**Security**:
- Private key file: permissions 0o600 (owner read/write only)
- Directory: permissions 0o700
- No encryption of private key at rest (consistent with SSH key defaults; passphrase support deferred)
## R5: Verification During Sync
**Decision**: Verify all commits in a ref's DAG during sync, before reconciliation. Walk from root to tip, verify each commit's event.json signature.
**Rationale**: FR-008 requires atomic rejection — if any commit fails, the entire ref is rejected. Walking the full DAG ensures no unsigned commits slip through, including historical ones (FR-004a: no grandfathering).
**Performance**: Ed25519 verification is ~15,000 ops/second on modern hardware. Even 1000 commits per ref would complete in <100ms, well within SC-004 (1s per 100 commits).
**Edge case**: Merge commits (Action::Merge) created during reconciliation also need signatures. The syncing user's key signs these.