did-git-sign 0.4.2

Git commit signing proxy using DID Ed25519 keys via VTA
Documentation

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:

  1. Loads its config (.did-git-sign.json) and retrieves the VTA credential from the OS keyring
  2. Authenticates with the VTA (or reuses a cached token)
  3. Fetches the Ed25519 signing key from the VTA on-the-fly
  4. Produces an SSH signature (PROTOCOL.sshsig format) and writes it to stdout
  5. 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). init discovers the service URL from the DID document, mints a short-lived admin did:key for the setup session, and prints the pnm contexts create command to authorise it.

Install

cargo install did-git-sign

did-git-sign is the signing half of Verifiable Git Infrastructure (VGI); the CI verifier is the separate verify-trust crate.

Setup

Per-repository

did-git-sign init --vta-did did:webvh:scid:your-vta.example.com

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)

did-git-sign init --global --vta-did did:webvh:scid:your-vta.example.com

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):

did-git-sign init \
  --vta-did    did:webvh:scid:your-vta.example.com \
  --key-id     your-vta-key-id \
  --did-key-id did:webvh:scid:your-vta.example.com#key-0 \
  --name       "Your Name" \
  --yes

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:

  1. Saves config to .did-git-sign.json (local) or ~/.config/did-git-sign/config.json (global) — contains only key_id, did_key_id, and user_name
  2. Stores VTA credentials (URL, DIDs, private key) in the OS keyring (macOS Keychain / Linux Secret Service)
  3. Verifies VTA connectivity by authenticating and fetching the signing key
  4. Configures git:
    • gpg.format = ssh
    • gpg.ssh.program = did-git-sign
    • gpg.ssh.defaultKeyFile = <config path>
    • commit.gpgsign = true
    • user.signingKey = <config path>
    • user.email = <DID#key-id> — the commit's identity claim; see below
    • user.name = <name> (if provided)
  5. Creates an allowed_signers file for signature verification and sets gpg.ssh.allowedSignersFile

Usage

After setup, commits are signed automatically:

git commit -m "my signed commit"

Verify signatures:

git log --show-signature

Check your configuration and VTA connectivity:

did-git-sign health

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):

did-git-sign health --resolve-agent-names

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:

  1. The DID_GIT_SIGN_KEY environment variable (per-invocation override).
  2. The did-git-sign.key per-repo git config setting.
  3. The did_key_id in the config file git points at (the init default).

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 \
  git -c user.email=did:webvh:abc:example.com#key-1 commit -m ""

# Pin a persona for this repository:
git config did-git-sign.key   did:webvh:abc:example.com#key-1
git config user.email         did:webvh:abc:example.com#key-1

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 zeroize crate.
  • DID_GIT_SIGN_SSH_KEYGEN override is test-only. The path to ssh-keygen used 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 invokes ssh-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:

{
  "did_key_id": "did:webvh:abc123:example.com#key-0",
  "user_name": "Your Name"
}

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