specs/001-gpg-event-signing/spec.md
Ref: Size: 8.3 KiB
# Feature Specification: Ed25519 Signing for Event Commits **Feature Branch**: `001-gpg-event-signing` **Created**: 2026-03-21 **Status**: Draft **Input**: User description: "implement signing. signing is mandatory" ## User Scenarios & Testing ### User Story 1 - Sign Event Commits (Priority: P1) As a contributor, every event commit I create (issues, comments, patches, reviews) is automatically signed with my Ed25519 key, so that other collaborators can verify the event actually came from me. **Why this priority**: Without signing, anyone with push access can forge events as another user. Signing is the core purpose of this feature and provides the authenticity guarantee that everything else builds on. **Independent Test**: Can be fully tested by creating an issue or comment and verifying the resulting commit carries a valid Ed25519 signature matching the author's key. **Acceptance Scenarios**: 1. **Given** a user has an Ed25519 signing key configured, **When** they create any event (issue, comment, patch, review, label, assign, close, reopen), **Then** the resulting commit is signed with their Ed25519 key. 2. **Given** a user has no signing key configured, **When** they attempt to create any event, **Then** the operation fails with a clear error message explaining that a signing key is required. 3. **Given** a user creates an event, **When** another user inspects the event commit, **Then** the signature is visible and attributable to the author. --- ### User Story 2 - Verify Signatures on Sync (Priority: P2) As a collaborator syncing from a remote, all incoming event commits are verified for valid Ed25519 signatures, so that forged or tampered events are rejected before they enter my local state. **Why this priority**: Signing without verification is incomplete — verification closes the trust loop and ensures the integrity of the collaboration history. **Independent Test**: Can be tested by syncing from a remote that contains both validly-signed and unsigned/forged commits and confirming only valid ones are accepted. **Acceptance Scenarios**: 1. **Given** a remote contains event commits with valid Ed25519 signatures, **When** I sync, **Then** the events are fetched and reconciled normally. 2. **Given** a remote contains event commits without signatures, **When** I sync, **Then** those commits are rejected and the user is warned about unsigned events. 3. **Given** a remote contains event commits with invalid or unrecognized signatures, **When** I sync, **Then** those commits are rejected and the user is warned with details about the verification failure. 4. **Given** a sync encounters some valid and some invalid commits on the same ref, **When** verification fails, **Then** the entire ref update is rejected (no partial application) and the user is informed which commits failed verification. --- ### User Story 3 - Merge Commit Signing During Reconciliation (Priority: P3) As a collaborator, when sync reconciliation produces a merge commit (concurrent edits from multiple users), that merge commit is also signed with my Ed25519 key, maintaining the signing chain. **Why this priority**: Merge commits created during reconciliation are new commits — if left unsigned, they break the trust chain even when all source commits were properly signed. **Independent Test**: Can be tested by creating a fork scenario (two users edit the same issue concurrently), syncing to trigger reconciliation, and verifying the resulting merge commit is signed. **Acceptance Scenarios**: 1. **Given** two users have divergent histories on the same ref, **When** sync reconciliation creates a merge commit, **Then** the merge commit is signed with the syncing user's Ed25519 key. --- ### Edge Cases - What happens when syncing commits signed by a key not in the known-keys list? Verification fails and the commits are rejected, with a message indicating the signing key is unknown. - What happens during sync if the signing user's secret key is unavailable (e.g., on a CI machine)? The sync fails at the reconciliation step if a merge commit is needed, since it cannot be signed. Fast-forward syncs that require no new commits succeed since no signing is needed. ## Clarifications ### Session 2026-03-21 - Q: How should existing unsigned event commits (created before this feature) be handled during sync verification? → A: Reject them — all historical unsigned commits fail verification. No grandfathering. - Q: Which signing approach should the implementation use? → A: Ed25519 with pure-Rust crypto (aligned with Radicle's approach), not GPG. Use the `ed25519-dalek` or similar pure-Rust crate for in-process signing and verification. - Q: Where should the Ed25519 signature be stored within the event commit? → A: As fields in `event.json` (e.g., `"signature": "base64..."`, `"pubkey": "base64..."`). - Q: How should the Ed25519 keypair be provisioned for a user? → A: Require explicit `collab init-key` command that generates and stores the keypair. No auto-generation. - Q: What data should be signed within the event? → A: Sign the entire serialized `event.json` content (excluding `signature` and `pubkey` fields themselves). ## Requirements ### Functional Requirements - **FR-001**: System MUST sign every event commit with the author's Ed25519 key when creating issues, comments, patches, reviews, labels, assignments, closures, and reopens. - **FR-002**: System MUST sign merge commits created during sync reconciliation with the syncing user's Ed25519 key. - **FR-003**: System MUST verify Ed25519 signatures on all incoming event commits during sync before applying them to local state. - **FR-004**: System MUST reject incoming event commits that have no signature, an invalid signature, or a signature from an unrecognized key. - **FR-004a**: System MUST reject existing unsigned event commits during sync — there is no grandfathering for pre-feature historical commits. - **FR-005**: System MUST fail with a clear, actionable error message when a user attempts to create an event without a signing key configured. - **FR-006**: System MUST provide a `collab init-key` command that generates an Ed25519 keypair and stores it in a user-local configuration path. The system MUST NOT auto-generate keys. - **FR-007**: System MUST report which specific commits failed signature verification during sync, including the reason for failure. - **FR-008**: System MUST reject ref updates atomically — if any commit in a ref's history fails verification, the entire ref is not updated. - **FR-009**: System MUST embed the Ed25519 signature and public key as fields in `event.json` (`"signature"` and `"pubkey"`, both base64-encoded) so they are transportable across remotes. - **FR-010**: System MUST sign the entire serialized `event.json` content (excluding the `signature` and `pubkey` fields themselves). Verification reproduces this canonical serialization and checks against the embedded signature. ### Key Entities - **Event Commit**: A git commit containing an `event.json` blob, now required to carry an Ed25519 signature from the event author. - **Ed25519 Signing Key**: The user's Ed25519 private key used to sign event data, stored locally by the system. - **Ed25519 Public Key**: The user's public key, shared with collaborators to verify signatures. - **Signature Verification Result**: The outcome of verifying a commit's signature — valid, invalid, missing, or unknown key — used to accept or reject synced events. ## Success Criteria ### Measurable Outcomes - **SC-001**: 100% of event commits created by the system carry a valid Ed25519 signature. - **SC-002**: 100% of unsigned or invalidly-signed incoming event commits are rejected during sync. - **SC-003**: Users without a configured signing key receive an error on their first attempted action, before any unsigned commit is created. - **SC-004**: Signature verification adds no more than 1 second of overhead per 100 synced commits. ## Assumptions - The system uses pure-Rust Ed25519 cryptography (e.g., `ed25519-dalek` crate) for signing and verification — no external GPG or SSH binaries required. - Signing and verification happen in-process, not via subprocess calls. - Collaborators exchange public keys out-of-band (e.g., direct exchange, included in repo metadata) — key distribution is outside the scope of this feature. - The Ed25519 keypair is generated and managed by the system, not imported from external GPG/SSH keyrings.