A Keychain-backed credential helper for Code Storage
I've been playing with Pierre Code Storage, a hosted Git service built for machines more than people. I wanted to clone their Git repos locally and push back changes. There's no username and password, and no personal access token to paste anywhere. Instead, every Git request carries a short-lived JWT signed with a private key you register with your organisation. (If you just want a way to make it work, here it is.)
The authentication docs are clear enough. Git over HTTPS uses Basic auth, the username is literally t, and the password is an ES256 JWT that looks something like this:
{
"iss": "your-org",
"sub": "git-sj26",
"repo": "hello",
"scopes": ["git:read", "git:write"],
"iat": 1790822400,
"exp": 1790826000
}
That's fine for a backend minting tokens for a CI job. It's less fine for me at a terminal wanting to git push. There's no official credential helper, and a token that expires in an hour isn't something I want to keep pasting into remote URLs.
Git credential helpers are tiny
A git credential helper is just an executable called git-credential-<name>. Git runs it with get, writes some key=value lines to stdin, and reads some back:
$ printf 'protocol=https\nhost=your-org.code.storage\npath=hello.git\n\n' | git credential-code-storage get
username=t
password=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...
So the helper only has to take the org from the host, the repo from the path, sign a fresh token, and print it. No caching, no refreshing. store and erase do nothing.
The first version was a few dozen lines of Ruby. It pulled the PEM out of the Keychain as a generic password, signed with OpenSSL, and squashed the DER signature into the raw r || s form that JWS wants. It worked, and I pushed with it. But it bugged me. The private key was sitting in the Keychain as a readable secret, and the helper loaded the raw key bytes into memory on every single git fetch.
Keep the key in the Keychain
What I really wanted was for the key to go into the Keychain once and never come out. Ask the Keychain to sign something, get a signature back, never touch the key.
The obvious answer is the Secure Enclave, but it can only use keys it generates itself. Code Storage generates the key pair in your browser, registers the public half, and shows you the private PEM exactly once. There's no API to register a public key I generate myself, so the Secure Enclave is out.
The next best thing is the login keychain. You can import a private key there marked as sensitive and never-extractable, with an access control list that trusts only one binary. That's what security import -x -T <app> does. The helper is now a small Swift program that does the same thing through Security.framework:
$ pbpaste | git-credential-code-storage import your-org
$ pbcopy </dev/null
After that, signing is a SecKeyCreateSignature call with .ecdsaSignatureMessageX962SHA256. The Keychain hands back a DER signature, which the helper still has to convert into JWS's r || s. The key itself is in the helper's memory once, during import, and never again. SecKeyCopyExternalRepresentation and security export both fail with errSecDataNotAvailable, even when the helper itself asks. Another program that tries to sign with the key gets a Keychain prompt, or is denied outright.
To be honest about it: anything running as me can still run the helper and get a token out of it. The Keychain stops the key from being copied, not from being used. But a token that covers one repository and expires in an hour is a much smaller thing to lose than the key.
The annoying part is that the APIs that make this work, SecItemImport, SecAccessCreate and SecTrustedApplicationCreateFromPath, have been deprecated for years. The modern data protection keychain needs a keychain-access-groups entitlement and a provisioning profile, which a plain command line tool can't have. So, deprecated APIs it is.
Setting it up
$ git config --global "credential.https://*.code.storage.helper" ""
$ git config --global --add "credential.https://*.code.storage.helper" code-storage
$ git config --global "credential.https://*.code.storage.useHttpPath" true
The empty helper stops osxkeychain from jumping in and storing a token that'll be dead in an hour. useHttpPath matters more than it looks. Without it Git doesn't send the path to the helper, so the helper can't scope the token to a repository.
Don't put t@ in the URL
This one cost me a while. The docs show remote URLs like https://t:JWT@your-org.code.storage/repo.git, so I set my remote to https://t@your-org.code.storage/hello.git. The helper was minting perfectly valid tokens, but every push failed with 403 Invalid or expired token.
With a username in the URL, Git sends it straight away with an empty password. Code Storage answers that with a 403, not a 401. Git only asks credential helpers after a 401, so it never asked mine. Drop the username and it all just works:
$ git remote set-url origin https://your-org.code.storage/hello.git
$ git push origin main
To https://your-org.code.storage/hello.git
* [new branch] main -> main
Caveats
The release binary is signed ad hoc, not with a Developer ID, and isn't notarized. curl doesn't quarantine downloads so the install instructions work, but a browser download will need xattr -d com.apple.quarantine.
The Keychain ACL trusts the exact binary, by its code directory hash. Every rebuild or new release looks like a different program, so the first signature after an upgrade shows a Keychain prompt. Check you actually just upgraded, then click Always Allow. If you have an Apple Development certificate, make install CODESIGN_IDENTITY=... signs with a stable identity and the prompts go away.
I wrote this pairing with Amp, which did most of the Security.framework spelunking. It's MIT licensed, and on GitHub, with a universal v0.1.0 release.