Skip to content

feat(git-sign-nostr): implement NIP-GS git object signing with Nostr keys#459

Merged
tlongwell-block merged 4 commits into
mainfrom
feat/git-sign-nostr
May 3, 2026
Merged

feat(git-sign-nostr): implement NIP-GS git object signing with Nostr keys#459
tlongwell-block merged 4 commits into
mainfrom
feat/git-sign-nostr

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

@tlongwell-block tlongwell-block commented May 3, 2026

Summary

Adds git-sign-nostr, a standalone Rust binary implementing NIP-GS (Git Object Signing with Nostr Keys). Git invokes it as gpg.x509.program to produce and verify BIP-340 Schnorr signatures over commit and tag objects.

Vision

This is the signing half of Sprout's agent git authentication story. Combined with git-credential-nostr (#451), agents get a complete git identity:

┌──────────────────────────────────────────────────────────┐
│  Agent Git Identity                                      │
│                                                          │
│  git-credential-nostr (#451) → authenticates pushes      │
│  git-sign-nostr (this PR)    → signs commits/tags        │
│  NIP-OA delegation           → owner authorizes agent    │
└──────────────────────────────────────────────────────────┘

One Nostr keypair, two programs, full git identity with owner delegation.

What's Included

crates/git-sign-nostr/ — The signing binary

  • Sign mode: reads private key (env or keyfile), computes SHA-256 signing hash with domain separation (nostr:git:v1:), produces armored base64 JSON signature
  • Verify mode: validates BIP-340 signature, checks NIP-OA delegation if present, emits GPG-compatible status lines for git's trust framework
  • NIP-OA support: optional owner attestation binds agent keys to owner keys with temporal conditions (created_at<, created_at>)

Security hardening

  • O_NOFOLLOW + fstat keyfile handling (no TOCTOU, no symlink following)
  • Zeroizing<String> for all secret material with explicit drop
  • Bounded reads everywhere (sig files, git config, keyfiles, stdin)
  • fcntl(F_GETFD) validation before borrowing file descriptors
  • env_remove on subprocess spawns (prevents key leakage to git)
  • Fail-closed on invalid auth tags, unrecognized key formats
  • OA temporal conditions enforced on both sign and verify paths
  • Env var length caps (128B for keys, 1KB for auth tags)
  • Checked stdout writes (fail on broken pipe)
  • Critical status-fd writes fail the process if write errors occur

Type safety & code quality

  • OaVerifyResult enum replaces stringly-typed OA status
  • git_config_strict() distinguishes "not set" from "subprocess failed"
  • Trust logic: direct key match takes priority over OA validity
  • Non-canonical decimal rejection in OA conditions (+/- prefix)
  • Strict arg parsing (reject duplicate modes, require trailing -)
  • Single read_payload_stdin() -> Result (no duplicated logic)

Agent spawn integration

  • Wires git-sign-nostr into the managed agent runtime
  • Configures gpg.x509.program and commit.gpgsign automatically

E2E test suite

  • Extended with signing, auth bypass, HMAC tampering, and hook integrity tests

Review Status

Reviewer Verdict Score
Codex CLI (Technical correctness) 9/10
Codex CLI (Security) 9/10
Codex CLI (Fitness for purpose) 9/10
Codex CLI (Robustness) 8/10
Codex CLI (Code quality) 8/10

Specs & Related PRs

…keys

Adds git-sign-nostr, a standalone binary that implements NIP-GS (Git Object
Signing with Nostr Keys). Git invokes it as gpg.x509.program to produce and
verify BIP-340 Schnorr signatures over commit/tag objects.

Key features:
- Sign mode: reads private key, computes SHA-256 signing hash with domain
  separation ("nostr:git:v1:"), produces armored base64 JSON signature
- Verify mode: validates BIP-340 signature, checks NIP-OA delegation if
  present, emits GPG-compatible status lines for git's trust framework
- NIP-OA support: optional owner attestation binds agent keys to owner
  keys with temporal conditions (created_at<, created_at>)
- Security hardening: O_NOFOLLOW+fstat keyfile handling, Zeroizing<String>
  for secrets, bounded reads everywhere, fcntl fd validation, env_remove
  on subprocesses, fail-closed on invalid auth tags

Also wires the binary into the agent spawn path and extends the e2e test
suite with signing, auth bypass, HMAC tampering, and hook integrity tests.

Implements: NIP-GS (PR #455)
Related: git-credential-nostr (PR #451)
Tests: 31 unit tests, 16 e2e tests, zero clippy warnings
Reconcile feat/auto-git-auth-for-agents hardening with PR test coverage:

Architecture improvements:
- KeypairGuard RAII for zeroizing keypair on all exit paths
- Error enum + Result-based flow (no process::exit that skips destructors)
- git_config_strict() for fail-closed auth tag lookup
- validate_conditions() grammar parser for NIP-OA condition strings
- parse_decimal_u32() rejects leading zeros in timestamps
- write_all to stdout (no panic on broken pipe)
- enforce_conditions() for structured temporal condition enforcement
- O_NOFOLLOW + fstat keyfile validation
- fcntl(F_GETFD) validation on status-fd
- Bounded reads everywhere (stdin, sig file, git config)
- env_remove for secrets in git subprocess
- Comprehensive doc comments and README

Ported tests from original PR:
- 4 hex validation tests (is_lower_hex)
- 7 armor edge-case tests (CRLF, wrong begin/end, trailing whitespace, oversized)
- 8 envelope parsing tests (missing fields, wrong types, out-of-range)
- 1 canonical JSON roundtrip test
- 3 format_date tests (epoch, known date, leap day)
- 4 OA tag rejection tests (invalid hex, dangerous conditions, wrong label)
- 3 signing hash determinism tests
- Add OaVerifyResult enum replacing bool in verify path
- Fix trust logic: direct key match takes priority over OA
- Add env var length caps (128B keys, 1KB auth tags)
- Reject non-canonical decimals in OA conditions (+/- prefix)
- read_keyfile_secure returns Zeroizing<String> directly
- Check stdout writes in cmd_sign (fail on broken pipe)
- Add status_or_fail! macro for critical verify status output
- Stricter arg parsing (reject duplicates, require trailing -)
- Merge stdin readers into single Result-returning function

Codex CLI: Technical correctness 9/10, Security 9/10,
Fitness for purpose 9/10.
…rify

- Verify keyfile owner UID matches current user (prevents ACL/privilege bypass)
- Make invalid --status-fd fatal in verify mode (git depends on status output)
- Sign mode still falls back to stderr on invalid fd (advisory status)
@tlongwell-block tlongwell-block merged commit 1feb18e into main May 3, 2026
13 checks passed
@tlongwell-block tlongwell-block deleted the feat/git-sign-nostr branch May 3, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant