Fix SOPS_AGE_SSH_PRIVATE_KEY_FILE for age recipients
Fixes #1999
Problem
Users who convert their SSH public keys to age recipients using ssh-to-age cannot decrypt with SOPS_AGE_SSH_PRIVATE_KEY_FILE. This is a common workflow for reusing existing SSH keys with SOPS.
Example workflow that fails:
# Convert SSH public key to age recipient
ssh-to-age -i ~/.ssh/id_ed25519.pub > age-recipient.txt
# Encrypt to that recipient
sops -e --age $(cat age-recipient.txt) secrets.yaml > secrets.enc.yaml
# Try to decrypt with the SSH key - FAILS before this fix
SOPS_AGE_SSH_PRIVATE_KEY_FILE=~/.ssh/id_ed25519 sops -d secrets.enc.yaml
Root Cause
SOPS only creates an SSH identity from the key file. SSH identities can decrypt data encrypted to SSH recipients, but NOT data encrypted to age X25519 recipients (even when derived from the same key).
Solution
For ed25519 SSH keys, now create both:
- SSH identity - decrypts data encrypted to SSH recipients (existing behavior)
- Age X25519 identity - decrypts data encrypted to age recipients derived from the same key (new)
The conversion uses the same algorithm as ssh-to-age: ed25519 -> curve25519 -> bech32-encoded age identity.
Encrypted (passphrase-protected) SSH keys were already supported - this fix extends that to also create the age identity. The passphrase prompt behavior changes slightly: it's now requested once upfront (instead of lazily via callback) so it can be reused for creating both identities.
Non-ed25519 keys (RSA, ECDSA) are unchanged - they only get an SSH identity. age uses X25519 which is based on Curve25519, the same curve as ed25519, making conversion possible only for ed25519 keys.
Changes
| File | Change |
|---|---|
age/bech32/ |
New: bech32 package (MIT licensed, copied from ssh-to-age) with tests |
age/ssh_parse.go |
Add ed25519PrivateKeyToCurve25519, sshEd25519ToAgeIdentity, extract parseEncryptedSSHKey |
age/keysource.go |
Handle slice of identities instead of single identity |
age/keysource_test.go |
Add regression test for issue #1999 |
Test Plan
- [x] Unit test added that encrypts to an age recipient derived from an SSH key, then decrypts using SOPS with that SSH key
- [x] Existing tests updated to expect 2 identities from ed25519 keys
- [x] bech32 package includes its own test suite
- [x] Manual verification using the workflow above