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 github.com/Mic92/ssh-to-age library which implements the ed25519 -> curve25519 -> bech32-encoded age identity conversion.

For encrypted (passphrase-protected) SSH keys, a lazyEd25519AgeIdentity wrapper is used that:

  • Defers passphrase prompting until decryption is actually attempted
  • Pre-filters by recipient to avoid unnecessary passphrase prompts when the key doesn't match
  • Caches the decrypted identity for subsequent use

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/ssh_parse.go Add lazyEd25519AgeIdentity for lazy passphrase prompting, recipientMatcher interface, use ssh-to-age library for key conversion
age/keysource.go Filter identities by recipient before decryption, handle slice of identities instead of single identity
age/keysource_test.go Add regression test for issue #1999
go.mod Add github.com/Mic92/ssh-to-age dependency

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
  • Manual verification with encrypted SSH key (passphrase prompting works correctly)

@felixfontein
Copy link
Contributor

Thanks for your contribution. I don't see that this PR has a chance of getting merged in its current form though. It contains a lot of apparently vendored code from an unknown source (I don't see any links to the source) that we would now have to maintain.

@gokselk
Copy link
Author

gokselk commented Dec 21, 2025

@felixfontein Thanks for taking a look! Yeah fair point, I should've added proper attribution. The bech32 code is from ssh-to-age which itself copies from age's internal package.

The tricky part is age keeps their bech32 internal, so we can't just import it directly. A few options:

  1. Keep vendored code, just add source links to age's internal/bech32
  2. Use github.com/Mic92/ssh-to-age/bech32 - same code but exported, drop-in replacement
  3. Use github.com/btcsuite/btcd/btcutil/bech32 - bit weird adding a bitcoin lib for this though

Let me know which direction you'd prefer and I'll update the PR.

@felixfontein
Copy link
Contributor

I'd probably import the code from ssh-to-age:

Use github.com/Mic92/ssh-to-age/bech32 - same code but exported, drop-in replacement

@gokselk gokselk force-pushed the fix/ssh-ed25519-age-recipient-decryption branch 2 times, most recently from 2c123f1 to bf361ae Compare December 23, 2025 21:01
@gokselk
Copy link
Author

gokselk commented Dec 23, 2025

I'd probably import the code from ssh-to-age:

Use github.com/Mic92/ssh-to-age/bech32 - same code but exported, drop-in replacement

Done, switched to github.com/Mic92/ssh-to-age/bech32 and removed the vendored code.

@gokselk gokselk marked this pull request as draft January 4, 2026 20:36
Enable SSH ed25519 keys to decrypt data encrypted to age recipients
derived from the same key (via ssh-to-age or similar tools).

Fixes getsops#1999

Signed-off-by: gokselk <gokselk.dev@gmail.com>
@gokselk gokselk force-pushed the fix/ssh-ed25519-age-recipient-decryption branch from bf361ae to 0211dc6 Compare January 5, 2026 00:17
@gokselk gokselk marked this pull request as ready for review January 5, 2026 00:18
@gokselk
Copy link
Author

gokselk commented Jan 5, 2026

Updated the PR description. The implementation has changed quite a bit. Also, the ssh-to-age tags don't follow Go module version numbering conventions, so I've opened an issue to address this: Mic92/ssh-to-age#190

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SOPS_AGE_SSH_PRIVATE_KEY_FILE fails without an error

2 participants