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

  1. Ensure CryptoPublicKey() of the custom key returns the underlying *rsa.PublicKey.
  2. If wrapping a key, embed/forward the original key's CryptoPublicKey() result.
  3. 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

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


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/8ed2346afe962ead. Report an issue: GitHub.