diff --git a/docs/git-signing.md b/docs/git-signing.md index 39a0115..fa0726a 100644 --- a/docs/git-signing.md +++ b/docs/git-signing.md @@ -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 @@ -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 @@ -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`: ```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 @@ -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. diff --git a/docs/gpg-signing.md b/docs/gpg-signing.md index 98c9069..b61d29a 100644 --- a/docs/gpg-signing.md +++ b/docs/gpg-signing.md @@ -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 diff --git a/docs/skills.md b/docs/skills.md index b3ffb20..7c71259 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -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. diff --git a/skills/signing-git-commits/SKILL.md b/skills/signing-git-commits/SKILL.md index 4ab54b6..48a764b 100644 --- a/skills/signing-git-commits/SKILL.md +++ b/skills/signing-git-commits/SKILL.md @@ -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 @@ -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. @@ -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`: @@ -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`: ```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 @@ -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: @@ -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 @@ -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 diff --git a/tests/e2e/gpg.rs b/tests/e2e/gpg.rs index b4d7bcf..9a71e86 100644 --- a/tests/e2e/gpg.rs +++ b/tests/e2e/gpg.rs @@ -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); }; @@ -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"]); @@ -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(), @@ -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"]); }