Skip to content

Conversation

@gokselk
Copy link

@gokselk gokselk commented Dec 7, 2025

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:

  1. SSH identity - decrypts data encrypted to SSH recipients (existing behavior)
  2. 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

  • Unit test added that encrypts to an age recipient derived from an SSH key, then decrypts using SOPS with that SSH key
  • Existing tests updated to expect 2 identities from ed25519 keys
  • bech32 package includes its own test suite
  • Manual verification using the workflow above

Enable SSH ed25519 keys to decrypt data encrypted to age recipients
derived from the same key (via ssh-to-age or similar tools).

Changes:
- Add bech32 package for encoding age secret keys
- Convert ed25519 SSH keys to age X25519 identities during key loading
- Handle both encrypted and unencrypted SSH keys with single passphrase prompt
- Return multiple identities from parseSSHIdentitiesFromPrivateKeyFile

Fixes getsops#1999

Signed-off-by: gokselk <[email protected]>
@gokselk gokselk force-pushed the fix/ssh-ed25519-age-recipient-decryption branch from a153789 to b19cafa Compare December 7, 2025 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SOPS_AGE_SSH_PRIVATE_KEY_FILE fails without an error

1 participant