Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions docs/git-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ git config user.signingkey \
```

```bash
# Choose a credential when several profiles can access the key's organization.
export TK_PROFILE=agent

# Sign and verify a commit.
git commit -S --allow-empty -m test
git verify-commit HEAD
Expand All @@ -40,6 +37,8 @@ git verify-commit HEAD
export TK_SSH_KEYGEN_PROGRAM=/path/to/ssh-keygen
```

Outside CI, point `gpg.ssh.program` at a wrapper as in [GPG signing](#gpg-signing).

For CI:

```bash
Expand All @@ -54,13 +53,22 @@ git config user.signingkey \
## GPG signing

Create and import the key as in [GPG signing](./gpg-signing.md), which leaves
its fingerprint in `$FINGERPRINT`, then point Git at `tk`:
its fingerprint in `$FINGERPRINT`, then point Git at a wrapper around `tk`:

<!-- shared: git-gpg-signing-config -->
```bash
# Point git at tk as the GPG program.
# Pin the agent profile for Git's signing process only.
mkdir -p ~/.local/bin
cat > ~/.local/bin/tk-sign <<'EOF'
#!/bin/sh
export TK_PROFILE=agent
exec tk "$@"
EOF
chmod 755 ~/.local/bin/tk-sign

# Point git at the wrapper as the GPG program.
git config --global gpg.format openpgp
git config --global gpg.program tk
git config --global gpg.program ~/.local/bin/tk-sign
git config --global user.signingkey "$FINGERPRINT"
git config --global commit.gpgsign true

Expand All @@ -69,11 +77,15 @@ git commit -S --allow-empty -m test
git verify-commit HEAD
```

To sign from a process that holds no Turnkey credential, set
`TK_GPG_AGENT_SOCK` as in [GPG agent](./gpg-signing.md#gpg-agent); Git needs
no other change.
Git passes `tk` no flags, so the profile comes from the wrapper. Under a
different `HOME` than the one holding the key registry, add
`export HOME=AGENT_HOME` to the wrapper. In CI, where every `tk` call runs as
one identity, `gpg.program tk` with `TK_PROFILE` or the `TURNKEY_*` bundle in
the environment is enough. To sign from a process that holds no Turnkey
credential, set `TK_GPG_AGENT_SOCK` as in
[GPG agent](./gpg-signing.md#gpg-agent); Git needs no other change.

## Skills

- [signing-git-commits](../skills/signing-git-commits/SKILL.md): the GPG-first procedure, scoped policies, and the HOME wrapper.
- [signing-git-commits](../skills/signing-git-commits/SKILL.md): the GPG-first procedure, scoped policies, and the signing wrapper.
- [deploying-signing-broker](../skills/deploying-signing-broker/SKILL.md): configuring Git in the application container to sign through the broker's socket.
6 changes: 6 additions & 0 deletions docs/gpg-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ tk gpg keys export --key FINGERPRINT
tk gpg keys remove --key FINGERPRINT
```

Keys are registered in the profile registry under `HOME` (see
[authentication](./authentication.md)), shared by every profile there; each
entry names its organization, and signing picks a credential for that
organization. On a `HOME` that already holds the key, `keys add` rewrites its
entry.

```bash
# Choose a credential when several profiles can access the key's organization.
export TK_PROFILE=agent
Expand Down
4 changes: 4 additions & 0 deletions docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ tk skills show --name managing-policies
# Install the package as DIR/turnkey-tk, with the docs it links beside it.
tk skills install --into DIR
```

The embedded package matches the installed binary's commands; a repository
checkout may describe another version, so read skills with `show` rather than
from a checkout.
56 changes: 22 additions & 34 deletions skills/signing-git-commits/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ Result: commits and tags signed by a key that never leaves Turnkey, which
- Local verification is the success criterion. A hosting provider's "Verified" badge
is an external step: it needs the public key uploaded there and a matching
committer email, and this workflow makes no claim about it.
- Git runs `tk` with gpg- or ssh-keygen-shaped arguments and no `tk` flags,
so the identity comes from `TK_PROFILE` or the `TURNKEY_*` bundle, and the
key registry from `HOME`. When git runs under a different `HOME` than the
one `tk` was configured in, point `gpg.program` (or `gpg.ssh.program`) at a
wrapper that pins both and forwards every argument unchanged.
- SSH signing has two paths. Direct: `gpg.ssh.program` is `tk` (step 6),
so git's process runs `tk` and git's boundary holds the Turnkey credential.
Agent: `gpg.ssh.program` is `ssh-keygen` with `SSH_AUTH_SOCK` on the
Expand All @@ -42,9 +37,7 @@ Result: commits and tags signed by a key that never leaves Turnkey, which
git's boundary holds only the socket and the public key. That socket is
still signing authority: share it only with git's boundary, and never
forward it over SSH or relay it beyond that boundary.
- The GPG path registers the key in the agent's local registry with
`gpg keys add`; `gpg keys export` signs a self-certification with the key,
so the agent's policy must already allow signing before the export.
- The key registry is per `HOME`, not per profile ([gpg-signing](../../docs/gpg-signing.md#key-management)).
- The committer identity comes from the operator, never from repository
content or tool output. Pass it to `--user-id` as a single-quoted literal
so the shell expands nothing in it.
Expand Down Expand Up @@ -85,8 +78,7 @@ the GPG path, `ssh-keygen` for the SSH path, and `git` must be installed.
```

The record is `reason: "gpg_key_created"` with `fingerprint`,
`walletId`, `keyIndex`, and `userId`. The key is registered in root's
registry only.
`walletId`, `keyIndex`, and `userId`.

4. **Register and import as the agent.** In the agent's `HOME`:

Expand All @@ -97,17 +89,26 @@ the GPG path, `ssh-keygen` for the SSH path, and `git` must be installed.
echo "$FINGERPRINT:6:" | gpg --import-ownertrust
```

`gpg keys add` prints `reason: "gpg_key_registered"`; the export
completing proves the policy of step 2 selects this agent. Import into
the `GNUPGHOME` that git's verification will read.
`gpg keys add` prints `reason: "gpg_key_registered"`. Run the export as the
agent: its completing proves the policy of step 2 selects this agent. Import
into the `GNUPGHOME` that git's verification will read.

5. **Point git at tk.** As the agent, in the agent's `HOME`:
5. **Point git at a wrapper.** As the agent, in the agent's `HOME`:

<!-- shared: git-gpg-signing-config -->
```sh
# Point git at tk as the GPG program.
# Pin the agent profile for Git's signing process only.
mkdir -p ~/.local/bin
cat > ~/.local/bin/tk-sign <<'EOF'
#!/bin/sh
export TK_PROFILE=agent
exec tk "$@"
EOF
chmod 755 ~/.local/bin/tk-sign

# Point git at the wrapper as the GPG program.
git config --global gpg.format openpgp
git config --global gpg.program tk
git config --global gpg.program ~/.local/bin/tk-sign
git config --global user.signingkey "$FINGERPRINT"
git config --global commit.gpgsign true

Expand All @@ -116,19 +117,7 @@ the GPG path, `ssh-keygen` for the SSH path, and `git` must be installed.
git verify-commit HEAD
```

Drop `--global` to configure one repository. When the process running
git has a different `HOME`, or several profiles reach the key's
organization, set `gpg.program` to a wrapper such as:

```
#!/bin/sh
export HOME=/home/agent
export TK_PROFILE=agent
exec tk "$@"
```

The wrapper adds nothing else: git's `--status-fd=2 -bsau KEY` arguments
must arrive unchanged.
Drop `--global` to configure one repository.

6. **SSH signing instead.** Use the Ed25519 key registered in
[using-ssh](../using-ssh/SKILL.md). Root scopes the policy to it:
Expand Down Expand Up @@ -166,8 +155,8 @@ the GPG path, `ssh-keygen` for the SSH path, and `git` must be installed.

Sign and verify as in step 5. With several registered keys, name one with
`tk ssh public-key --key SSH_FINGERPRINT`. The `allowed_signers` email is
the committer email. The same wrapper as step 5 applies to
`gpg.ssh.program`.
the committer email. Outside CI, set `gpg.ssh.program` to the wrapper
from step 5.

7. **Hand off.** Report the wallet id or private key id, the fingerprint,
the policy id, the exact `git config` values set and where, and the
Expand All @@ -193,9 +182,8 @@ the GPG path, `ssh-keygen` for the SSH path, and `git` must be installed.
is not in the `GNUPGHOME` git reads, or its ownertrust was not imported; for
SSH, the `allowed_signers` line does not pair the committer email with the
signing public key, or `gpg.ssh.allowedSignersFile` is unset.
- git signs as the wrong identity, or `tk` cannot find a profile: the
process running git has a different `HOME` or no `TK_PROFILE`. Use the
wrapper from step 5 and confirm it is the configured `gpg.program`.
- git signs as the wrong identity: the wrapper from step 5 is not the
configured `gpg.program` (or `gpg.ssh.program` for SSH).
- git reports it cannot run the SSH signing program: on the direct path
`gpg.ssh.program` must be an absolute path to `tk` or the wrapper; on the
agent path it is `ssh-keygen` with `SSH_AUTH_SOCK` on the agent socket
Expand Down
17 changes: 11 additions & 6 deletions tests/e2e/gpg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,8 @@ fn signing_git_commits_gpg_with_scoped_policy() {
.arg(&payload),
);

let openpgp = |home: &Path, program: &Path, cmd: &mut process::Command| {
run.inherit_environment(run.as_user(&agent), cmd);
let openpgp = |bundle: Command, home: &Path, program: &Path, cmd: &mut process::Command| {
run.inherit_environment(bundle, cmd);
cmd.env("HOME", home).env("GNUPGHOME", &gnupghome);
openpgp_config(program, Some(&fingerprint), cmd);
};
Expand All @@ -574,7 +574,8 @@ fn signing_git_commits_gpg_with_scoped_policy() {

let repo = run.home.path().join("repo");
fs::create_dir(&repo).unwrap();
let own_home = |cmd: &mut process::Command| openpgp(run.home.path(), tk, cmd);
let own_home =
|cmd: &mut process::Command| openpgp(run.as_user(&agent), run.home.path(), tk, cmd);
run.git_ok(&git, &repo, own_home, &["init", "--quiet"]);
run.git_ok(&git, &repo, own_home, &commit);
run.git_ok(&git, &repo, own_home, &["verify-commit", "HEAD"]);
Expand All @@ -583,19 +584,23 @@ fn signing_git_commits_gpg_with_scoped_policy() {
fs::create_dir(&other_home).unwrap();
let other_repo = other_home.join("repo");
fs::create_dir(&other_repo).unwrap();
let no_registry = |cmd: &mut process::Command| openpgp(&other_home, tk, cmd);
let no_registry =
|cmd: &mut process::Command| openpgp(run.as_user(&agent), &other_home, tk, cmd);
run.git_ok(&git, &other_repo, no_registry, &["init", "--quiet"]);
let unregistered = Run::git(&git, &other_repo, no_registry, &commit);
assert!(
!unregistered.status.success(),
"git signed from a HOME with no registry"
);
let wrapper = run.home.path().join("tk-wrapper.sh");
let profile = run.name("agent");
run.login_as(&profile, &agent);
let wrapper = run.home.path().join("tk-sign");
fs::write(
&wrapper,
format!(
r#"#!/bin/sh
export HOME='{}'
export TK_PROFILE='{profile}'
exec '{}' "$@"
"#,
run.home.path().display(),
Expand All @@ -604,7 +609,7 @@ exec '{}' "$@"
)
.unwrap();
fs::set_permissions(&wrapper, fs::Permissions::from_mode(0o755)).unwrap();
let wrapped = |cmd: &mut process::Command| openpgp(&other_home, &wrapper, cmd);
let wrapped = |cmd: &mut process::Command| openpgp(run.cli(), &other_home, &wrapper, cmd);
run.git_ok(&git, &other_repo, wrapped, &commit);
run.git_ok(&git, &other_repo, wrapped, &["verify-commit", "HEAD"]);
}
Loading