FiloSottile/age · error

SSH public key is not an RSA key

Error message

SSH public key is not an RSA key

What it means

NewRSARecipient wraps an ssh.PublicKey as an age RSA recipient, but only accepts keys whose SSH wire type is exactly "ssh-rsa". This error is returned when the key's Type() is something else, e.g. an Ed25519, ECDSA (ecdsa-sha2-nistp256), or certificate (cert-v01@openssh.com) key. It is a pure input-validation failure; the key object itself was rejected before any crypto is attempted.

Source

Thrown at agessh/agessh.go:51

)

func sshFingerprint(pk ssh.PublicKey) string {
	h := sha256.Sum256(pk.Marshal())
	return format.EncodeToString(h[:4])
}

const oaepLabel = "age-encryption.org/v1/ssh-rsa"

type RSARecipient struct {
	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

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Check the key type before calling: only pass keys where pk.Type() == "ssh-rsa".
  2. If the key is intentionally Ed25519, use agessh.NewEd25519Recipient instead.
  3. When parsing a key file, select the ssh-rsa entry from the authorized_keys line rather than the first line.
  4. Generate a dedicated RSA key: ssh-keygen -t rsa -b 4096 -f key_rsa (with -m PEM if it must be an encrypted identity file).

Example fix

// before
rec, err := agessh.NewRSARecipient(pubKey) // pubKey is ssh-ed25519
// after
if pubKey.Type() == "ssh-rsa" {
    rec, err = agessh.NewRSARecipient(pubKey)
} else if pubKey.Type() == "ssh-ed25519" {
    rec, err = agessh.NewEd25519Recipient(pubKey)
}
Defensive patterns

Strategy: validation

Validate before calling

if pk.Type() != "ssh-rsa" {
    return fmt.Errorf("expected ssh-rsa key, got %s", pk.Type())
}

Type guard

func isSSHRSA(pk ssh.PublicKey) bool { return pk != nil && pk.Type() == "ssh-rsa" }

Try / catch

rec, err := agessh.NewRSARecipient(pk)
if err != nil {
    if strings.Contains(err.Error(), "not an RSA key") {
        // fall back to a type-appropriate recipient
    }
    return err
}

Prevention

When it happens

Trigger: Calling agessh.NewRSARecipient(pk) with pk.Type() != "ssh-rsa" — directly, or indirectly via agessh.ParseRecipient on an authorized_keys line for a non-RSA key, or via agessh.NewEncryptedSSHIdentity whose decrypted key is non-RSA.

Common situations: Parsing an authorized_keys/id_*.pub file containing multiple keys and passing an Ed25519 or ECDSA entry to NewRSARecipient; using an OpenSSH certificate instead of a raw public key; generating modern keys (ssh-keygen defaults to Ed25519 since OpenSSH 8.x) and assuming RSA.

Related errors


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