Repository navigation
Gpg signing support - #34
Merged
Merged
Conversation
emostov
marked this pull request as draft
September 12, 2026 03:30
emostov
force-pushed
the
zeke/akshar-gpg-signing-v2
branch
from
September 12, 2026 04:04
c902559 to
696b106
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
emostov
force-pushed
the
zeke/akshar-gpg-signing-v2
branch
6 times, most recently
from
September 13, 2026 21:42
4f1c7bf to
ae6abc6
Compare
emostov
force-pushed
the
zeke/akshar-gpg-signing-v2
branch
from
September 14, 2026 02:16
ae6abc6 to
e2d057b
Compare
…nfig steps into the git-signing doc.
emostov
force-pushed
the
zeke/akshar-gpg-signing-v2
branch
from
September 14, 2026 02:38
72dd10e to
1087e98
Compare
emostov
marked this pull request as ready for review
September 14, 2026 02:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
tk gpg, which signs Git commits and tags with an OpenPGP key whose private key is a Turnkey wallet account. Git callstkas itsgpg.program; the private key never leaves Turnkey.Also:
tk gpg keys add | remove | list,tk gpg sign <file>, and the git shim. Full setup is indocs/gpg-signing.md.Design decisions
Keys are wallet accounts, not private keys. Each OpenPGP key is one P-256 wallet account with an uncompressed address at BIP-32 path
m/5261136'/0'/<index>'/0'(0x504750, ASCII "PGP"). The account name holds the OpenPGP user ID. Nothing else is stored server-side: the wallet listing is the source of truth,keys list --wallet-idreads it back, andkeys addre-registers a key from it. Accounts in the namespace that this feature could not have made (wrong curve, unnamed, name that is not a user ID) are skipped but still occupy their index.The fingerprint is derived, never stored as truth. An OpenPGP v4 fingerprint is SHA-1 over the public key packet, which is a function of the point and the key creation time. We use the wallet account's
createdAtas the OpenPGP creation time, so the fingerprint is stable across machines and re-exports and needs no clock. The registry keys entries by fingerprint but re-derives it on read and rejects an entry whose point and time do not produce its key, so a registry file cannot make one key impersonate another.Export is byte-stable. The self-signature over the user ID uses the key's creation time, fixed algorithm preferences, and no clock read, so
keys exportyields identical bytes every time. This lets users re-import or paste the key into GitHub at any point without a "which export was it" problem. Detached signatures are the only place the clock is read.Our own RFC 4880 byte layer instead of a PGP crate. The auth crate gains a small
openpgpmodule: new-format packets, MPIs, subpackets, armor with CRC-24, v4 public key packets, and v4 signature packets. We only need to produce a public key block and detached signatures for one algorithm (ECDSA P-256, SHA-256), and signing is delegated to Turnkey through aSignDigesttrait. Pulling in sequoia or rpgp would add a large dependency surface and a private-key model we do not use. The fingerprint computation is checked in tests against an independent hand computation andgpg --list-packets, and the e2e suite has GnuPG verify our output.Signing needs one Turnkey request and no wallet read. The registered entry carries the point, creation time, and user ID, so
signandexportgo straight tosign_raw_payloadwithHashFunction::NoOpon the OpenPGP digest. This matters for the git shim, which runs on every commit.A key's organization selects the credential. Keys are stored in the identity registry, shared by every profile, because a key belongs to an organization rather than to a profile. When signing,
resolve_for_organizationuses an explicit identity (--profile,TK_PROFILE, theTURNKEY_*bundle, or--organization-id) if given and rejects it withOrganizationMismatchif it belongs to another organization. Otherwise it searches profiles for that organization: the only one, or the active profile when several match. Sogit commitworks with no tk flags after atk login, and never silently signs through the wrong org.The git shim is its own output boundary.
main.rsdetects gpg-shaped arguments (--status-fd,--keyid-format,-bsau,-bsa) before clap and hands them togpg::shim. Its stdout, stderr, and exit code are gpg's contract: the armored signature on stdout,[GNUPG:] BEGIN_SIGNINGandSIG_CREATEDlines on fd 2, errors as one line. Anything that is not a signing call, such as--verify, is exec'd to the realgpg(orTK_GPG_PROGRAM) without loading config or credentials. Only descriptor 2 is accepted for--status-fd, since stdout carries the signature.Selecting a key is one code path.
registry::selectresolves an optionalKeyNameagainst any set of keys, whether the registry or a wallet, with aScopethat fixes the wording of the resultingSelectError. Git'suser.signingkey(a fingerprint suffix of at least a long key ID) and the committer ident (an exact user ID) both parse intoKeyName. A name that matches nothing fails rather than falling back to the only key, and an empty set is reported before any comparison so the remedy can be "create one". Remediation text is added at the command layer; the shim rewords it for git users, who can setuser.signingkeybut not pass tk flags.Parsing at the boundary.
UserIdrejects empty values and NUL/CR/LF at clap parse time;SigningKeyNamenormalizes GnuPG's spacing and trailing!and requires 16+ hex chars;FingerprintandUncompressedPointcarry proof of shape. Downstream code takes these types rather than re-checking strings.Tests
createdAt, bad signature scalars, a wallet listing whose cursor never advances).git commit -S/git verify-commitround trip through the shim with GnuPG verifying the result.