Skip to content

Commit 1feb18e

Browse files
feat(git-sign-nostr): implement NIP-GS git object signing with Nostr keys (#459)
1 parent f946e9b commit 1feb18e

6 files changed

Lines changed: 3334 additions & 277 deletions

File tree

Cargo.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ members = [
1919
"crates/sprout-sdk",
2020
"crates/sprout-persona",
2121
"crates/git-credential-nostr",
22+
"crates/git-sign-nostr",
2223
]
2324
exclude = ["desktop/src-tauri"]
2425
resolver = "2"

crates/git-sign-nostr/Cargo.toml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[package]
2+
name = "git-sign-nostr"
3+
version.workspace = true
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
description = "NIP-GS git commit/tag signing program using Nostr secp256k1 keys"
9+
readme = "README.md"
10+
publish = false # internal workspace tool, not published to crates.io
11+
12+
[[bin]]
13+
name = "git-sign-nostr"
14+
path = "src/main.rs"
15+
16+
[dependencies]
17+
# Base64 armor encoding/decoding for NIP-GS signature envelopes.
18+
# Not in workspace deps — each crate pins independently (same pattern as
19+
# sprout-relay, sprout-mcp, sprout-cli, git-credential-nostr).
20+
base64 = "0.22"
21+
22+
# Hex encoding for BIP-340 signatures and public keys.
23+
hex = { workspace = true }
24+
25+
# Secret key zeroization on drop.
26+
zeroize = { workspace = true, features = ["derive"] }
27+
28+
# Nostr key parsing (nsec/npub bech32), secp256k1 Schnorr signing, SHA-256.
29+
# Uses the full default feature set because we need: Keys, PublicKey,
30+
# FromBech32, and the re-exported bitcoin::secp256k1 + bitcoin::hashes.
31+
nostr = { workspace = true }
32+
33+
# JSON parsing for NIP-OA auth tag and envelope verification.
34+
serde_json = { workspace = true }
35+
36+
# Timestamp formatting for GnuPG VALIDSIG status lines.
37+
chrono = { workspace = true }
38+
39+
# Unix-specific: O_NOFOLLOW for keyfile open, fcntl for fd validation.
40+
[target.'cfg(unix)'.dependencies]
41+
libc = "0.2"

crates/git-sign-nostr/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# git-sign-nostr
2+
3+
NIP-GS signing program — signs git commits and tags with Nostr secp256k1 keys
4+
using BIP-340 Schnorr signatures.
5+
6+
## Usage
7+
8+
```bash
9+
# Configure git to use nostr signing
10+
git config gpg.format x509
11+
git config gpg.x509.program /path/to/git-sign-nostr
12+
git config commit.gpgsign true
13+
git config tag.gpgsign true
14+
git config user.signingkey <hex-pubkey>
15+
16+
# Set the private key (env var)
17+
export NOSTR_PRIVATE_KEY=<hex-or-nsec>
18+
19+
# Optional: NIP-OA owner attestation
20+
export SPROUT_AUTH_TAG='["auth","<owner-pk>","<conditions>","<owner-sig>"]'
21+
22+
# Commits are now automatically signed
23+
git commit -m "signed with nostr"
24+
25+
# Verify
26+
git verify-commit HEAD
27+
```
28+
29+
## Key Loading Priority
30+
31+
1. `NOSTR_PRIVATE_KEY` environment variable
32+
2. `SPROUT_PRIVATE_KEY` environment variable
33+
3. Keyfile at path from `git config nostr.keyfile`
34+
35+
Keys may be hex (64 chars) or NIP-19 bech32 (`nsec1...`).
36+
37+
## How It Works
38+
39+
Git invokes this program as a signing/verification backend:
40+
41+
- **Sign:** `git-sign-nostr --status-fd=2 -bsau <keyid>` — reads payload from
42+
stdin, writes armored signature to stdout, status lines to fd 2 (stderr)
43+
- **Verify:** `git-sign-nostr --status-fd=1 --verify <sigfile> -` — reads
44+
payload from stdin, verifies signature from file, status lines to fd 1 (stdout)
45+
46+
See [NIP-GS](../../docs/nips/NIP-GS.md) for the full specification.

0 commit comments

Comments
 (0)