FiloSottile/age · error
unexpected public key type
Error message
unexpected public key type
What it means
After confirming the SSH key type is "ssh-rsa", NewRSARecipient extracts the underlying crypto key via ssh.CryptoPublicKey.CryptoPublicKey() and requires it to be a *rsa.PublicKey. This error fires when the type assertion to *rsa.PublicKey fails, meaning the key reports ssh-rsa on the wire but its Go crypto representation is not an *rsa.PublicKey. This is effectively unreachable with stock golang.org/x/crypto/ssh parsers and indicates a non-standard ssh.PublicKey implementation.
Source
Thrown at agessh/agessh.go:61
sshKey ssh.PublicKey
pubKey *rsa.PublicKey
}
var _ age.Recipient = &RSARecipient{}
func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
if pk.Type() != "ssh-rsa" {
return nil, errors.New("SSH public key is not an RSA key")
}
r := &RSARecipient{
sshKey: pk,
}
if pk, ok := pk.(ssh.CryptoPublicKey); ok {
if pk, ok := pk.CryptoPublicKey().(*rsa.PublicKey); ok {
r.pubKey = pk
} else {
return nil, errors.New("unexpected public key type")
}
} else {
return nil, errors.New("pk does not implement ssh.CryptoPublicKey")
}
if r.pubKey.N.BitLen() < 2048 {
return nil, errors.New("RSA key size is too small")
}
return r, nil
}
func (r *RSARecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
if r.pubKey.N.BitLen() < 2048 {
return nil, errors.New("RSA key size is too small")
}
l := &age.Stanza{
Type: "ssh-rsa",
Args: []string{sshFingerprint(r.sshKey)},
}View on GitHub (pinned to b74dce4cdb)
Solutions
- Ensure CryptoPublicKey() of the custom key returns the underlying *rsa.PublicKey.
- If wrapping a key, embed/forward the original key's CryptoPublicKey() result.
- Prefer keys parsed by golang.org/x/crypto/ssh.ParsePublicKey or ParseAuthorizedKey, which always return consistent implementations.
Example fix
// before
type myKey struct{ ssh.PublicKey } // CryptoPublicKey may not delegate
// after
type myKey struct{ ssh.PublicKey }
func (k myKey) CryptoPublicKey() crypto.PublicKey { return k.PublicKey.CryptoPublicKey() } Defensive patterns
Strategy: type-guard
Validate before calling
cpk, ok := pk.(ssh.CryptoPublicKey)
if !ok {
return errors.New("key lacks CryptoPublicKey")
}
if _, ok := cpk.CryptoPublicKey().(*rsa.PublicKey); !ok {
return errors.New("underlying key is not *rsa.PublicKey")
} Type guard
func toRSAPublicKey(pk ssh.PublicKey) (*rsa.PublicKey, bool) {
cpk, ok := pk.(ssh.CryptoPublicKey)
if !ok { return nil, false }
rsa, ok := cpk.CryptoPublicKey().(*rsa.PublicKey)
return rsa, ok
} Try / catch
rec, err := agessh.NewRSARecipient(pk)
if err != nil {
if strings.Contains(err.Error(), "unexpected public key type") {
return fmt.Errorf("inconsistent key implementation %T", pk)
}
return err
} Prevention
- Only pass keys from ssh.ParsePublicKey/ssh.NewPublicKey/ParseAuthorizedKey.
- If wrapping ssh.PublicKey, always delegate CryptoPublicKey().
- Add a unit test asserting custom key types satisfy the interface contract.
When it happens
Trigger: Passing a custom ssh.PublicKey implementation whose Type() returns "ssh-rsa" but whose CryptoPublicKey() returns something other than *rsa.PublicKey (e.g. nil or a wrapper type).
Common situations: Custom ssh.PublicKey wrappers/proxies (logging or key-policy middleware) that delegate Type() but not CryptoPublicKey() correctly; keys produced by non-standard parsing code.
Related errors
- pk does not implement ssh.CryptoPublicKey
- RSA key size is too small
- SSH public key is not an RSA key
- SSH public key is not an Ed25519 key
- mismatched private and public SSH key
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/8ed2346afe962ead.
Report an issue: GitHub.