did-git-sign
A standalone CLI tool that signs git commits using DID Ed25519 keys managed by a Verifiable Trust Agent (VTA). It acts as a git SSH signing proxy — no private key material ever touches disk.
How It Works
Git supports pluggable signing programs via gpg.ssh.program. When you commit,
git calls did-git-sign with the commit data on stdin. The tool:
- Loads its config (
.did-git-sign.json) and retrieves the VTA credential from the OS keyring - Authenticates with the VTA (or reuses a cached token)
- Fetches the Ed25519 signing key from the VTA on-the-fly
- Produces an SSH signature (PROTOCOL.sshsig format) and writes it to stdout
- Zeroizes the key material from memory
Your DID verification method ID (e.g. did:webvh:abc:example.com#key-0) is used
as the git user.email, linking every commit to your decentralized identity.
That field is load-bearing, not decorative. An sshsig blob carries a raw Ed25519
key and no identity, so the committer header is the only place a commit
states which DID signed it — verify-trust reads it, resolves
that DID, and requires it to publish the signing key. A commit whose committer
is an ordinary address fails CI as noSignerDid however valid its signature.
Signing therefore refuses when the committer names a DID other than the key's;
see Selecting which community persona signs.
Prerequisites
- A running VTA with your persona DID(s) and Ed25519 signing key(s) provisioned.
- Your VTA DID (e.g.
did:webvh:scid:your-vta.example.com).initdiscovers the service URL from the DID document, mints a short-lived admin did:key for the setup session, and prints thepnm contexts createcommand to authorise it.
Install
did-git-sign is the signing half of
Verifiable Git Infrastructure (VGI);
the CI verifier is the separate verify-trust crate.
Setup
Per-repository
init resolves the VTA, mints a temporary admin did:key, and prints a
pnm contexts create … command. Run it in your Personal Network Manager to
authorise the setup session, press Enter, then select the persona and signing
key interactively. This writes .did-git-sign.json in the current directory
and configures the local git repo.
Global (all repositories)
Saves config to ~/.config/did-git-sign/ and sets global git config.
This also sets user.email to your DID key id for every repository on the
machine — that is the identity your commits claim, and it must match the key
that signs them. Right for one community; wrong for two, and quietly so, since
commits in the other community would claim this DID. init prints the
per-remote alternative when you use --global; see
Selecting which community persona signs.
Non-interactive
Name the persona and key to skip the picker (and --yes to skip the
"press Enter once authorised" prompt):
Options
| Flag | Description |
|---|---|
--vta-did |
VTA DID; the service URL is discovered from its document (required) |
--context |
Context id to provision into (default did-git-sign) |
--key-id |
VTA key id for the signing key (skips interactive selection) |
--did-key-id |
DID verification-method id to sign as (skips interactive selection) |
--name |
Git user.name (optional) |
--vta-url |
Override the VTA URL instead of resolving it from the DID |
--global |
Configure global git instead of per-repo |
--yes |
Assume the admin grant is already registered; skip the prompt |
What init configures
The init command performs the following:
- Saves config to
.did-git-sign.json(local) or~/.config/did-git-sign/config.json(global) — contains onlykey_id,did_key_id, anduser_name - Stores VTA credentials (URL, DIDs, private key) in the OS keyring (macOS Keychain / Linux Secret Service)
- Verifies VTA connectivity by authenticating and fetching the signing key
- Configures git:
gpg.format = sshgpg.ssh.program = did-git-signgpg.ssh.defaultKeyFile = <config path>commit.gpgsign = trueuser.signingKey = <config path>user.email = <DID#key-id>— the commit's identity claim; see belowuser.name = <name>(if provided)
- Creates an
allowed_signersfile for signature verification and setsgpg.ssh.allowedSignersFile
Usage
After setup, commits are signed automatically:
Verify signatures:
Check your configuration and VTA connectivity:
Showing names instead of DIDs
init's context and DID pickers label each entry with a human name where the
VTA has one — an ACL label, or the context's own name — falling back to an
abbreviated DID. Pass --resolve-agent-names to init or health to also
read back the agent name a DID document claims (example.com/@alice):
Each claimed name is resolved forward and must lead back to the DID that claims
it before it is shown as that DID's; a claim that does not round-trip is tagged
[unverified], because alsoKnownAs is self-asserted and an unchecked name is
only what a DID says about itself. Resolution costs an outbound HTTPS fetch per
claimed name, so it is opt-in. Names never replace the DID in a summary or
diagnostic — they are printed above it.
Selecting which community persona signs
With more than one provisioned persona, you can choose which one signs without
re-running init. At sign time the signing key is resolved in this order:
- The
DID_GIT_SIGN_KEYenvironment variable (per-invocation override). - The
did-git-sign.keyper-repo git config setting. - The
did_key_idin the config file git points at (theinitdefault).
The value is the persona's did:webvh:…#key-N. It must have credentials stored
in the keyring (i.e. you ran init for that persona); otherwise signing fails
with a clear message rather than silently signing as a different persona.
# One commit as a specific persona (move user.email with it — see below):
DID_GIT_SIGN_KEY=did:webvh:abc:example.com#key-1 \
# Pin a persona for this repository:
The persona and the committer must agree. The key selection above chooses
what signs; user.email chooses what the commit claims. Naming different
DIDs produces a commit that cannot verify — the claimed DID does not publish
the key that signed — so signing refuses outright, naming both halves, rather
than writing a commit that fails in CI as unknownKey.
For contributors in more than one community, do not manage this per repository
by hand: a git config --local you forget does not error, it signs as the
wrong community. Use git's conditional includes, one file per community, with
both settings together so they cannot drift:
# ~/.gitconfig
[includeIf "hasconfig:remote.*.url:https://github.com/OpenVTC/**"]
path = ~/.config/git/community-openvtc
# ~/.config/git/community-openvtc
[user]
email = did:webvh:abc:example.com#key-0
[did-git-sign]
key = did:webvh:abc:example.com#key-0
hasconfig:remote.*.url (git ≥ 2.36) keys off the remote, so membership follows
the repository rather than where it was cloned; includeIf "gitdir:…" matches
on path instead if your layout is authoritative.
Security Model
- No key material on disk — the VTA credential private key is stored in the OS keyring, and the Ed25519 signing key is fetched from the VTA at sign-time and held only in memory.
- Token caching — the VTA access token is cached in the OS keyring to avoid re-authentication on every commit. Tokens are validated with a 30-second safety margin before reuse.
- Zeroization — signing key material is zeroized immediately after use via
the
zeroizecrate. DID_GIT_SIGN_SSH_KEYGENoverride is test-only. The path tossh-keygenused for the verify / find-principals / check-novalidate delegation paths can be overridden via this environment variable so test fixtures can point at a mock binary. Do not set it in production. An attacker with write access to your environment could redirect signature verification to a binary that always returns success and silently accept forged signatures. The override has no effect on the signing path, which never invokesssh-keygen.
Architecture
git commit
|
v
git calls: did-git-sign -Y sign -f .did-git-sign.json -n git
| (stdin: commit data)
v
did-git-sign:
1. Load config from .did-git-sign.json (key_id + did_key_id only)
2. Load VTA credentials from OS keyring
3. Authenticate with VTA (or use cached token from keyring)
4. Fetch Ed25519 key: VTA.get_key_secret(key_id)
5. Sign commit data (PROTOCOL.sshsig format)
6. Output SSH signature to stdout
7. Zeroize key material
|
v
git stores signature in commit
Config File Format
The .did-git-sign.json file contains only your DID identity:
No VTA credentials or key identifiers are stored on disk. All VTA
configuration and sensitive material is stored in the OS keyring under the
service name did-git-sign:
| Keyring Entry | Contents |
|---|---|
{did_key_id}:vta |
VTA URL, VTA DID, credential DID, credential private key, signing key ID |
{did_key_id}:token |
Cached VTA access token and expiry |