FiloSottile/age · error

SSH public key is not an Ed25519 key

Error message

SSH public key is not an Ed25519 key

What it means

NewEd25519Recipient accepts only SSH keys of type "ssh-ed25519". This error is returned when the passed ssh.PublicKey has a different wire type — e.g. ssh-rsa, ECDSA (ecdsa-sha2-nistp*), or an OpenSSH certificate. Like the RSA counterpart, it is upfront key-type validation before any curve25519 conversion.

Source

Thrown at agessh/agessh.go:151

		block.Body, []byte(oaepLabel))
	if err != nil {
		// The fingerprint is only a short hint, and might collide with the
		// fingerprint of a different recipient.
		return nil, age.ErrIncorrectIdentity
	}
	return fileKey, nil
}

type Ed25519Recipient struct {
	sshKey         ssh.PublicKey
	theirPublicKey []byte
}

var _ age.Recipient = &Ed25519Recipient{}

func NewEd25519Recipient(pk ssh.PublicKey) (*Ed25519Recipient, error) {
	if pk.Type() != "ssh-ed25519" {
		return nil, errors.New("SSH public key is not an Ed25519 key")
	}

	cpk, ok := pk.(ssh.CryptoPublicKey)
	if !ok {
		return nil, errors.New("pk does not implement ssh.CryptoPublicKey")
	}
	epk, ok := cpk.CryptoPublicKey().(ed25519.PublicKey)
	if !ok {
		return nil, errors.New("unexpected public key type")
	}
	mpk, err := ed25519PublicKeyToCurve25519(epk)
	if err != nil {
		return nil, fmt.Errorf("invalid Ed25519 public key: %v", err)
	}

	return &Ed25519Recipient{
		sshKey:         pk,
		theirPublicKey: mpk,

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Check pk.Type() == "ssh-ed25519" before calling; route ssh-rsa keys to NewRSARecipient.
  2. Generate an Ed25519 key: ssh-keygen -t ed25519 -f id_ed25519.
  3. When parsing multi-key files, match the specific key line by fingerprint or type.

Example fix

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

Strategy: validation

Validate before calling

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

Type guard

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

Try / catch

rec, err := agessh.NewEd25519Recipient(pk)
if err != nil {
    if strings.Contains(err.Error(), "not an Ed25519 key") {
        return fmt.Errorf("recipient key %s is not Ed25519", pk.Type())
    }
    return err
}

Prevention

When it happens

Trigger: Calling agessh.NewEd25519Recipient(pk) with pk.Type() != "ssh-ed25519" — directly, via agessh.ParseRecipient on a non-Ed25519 authorized_keys entry, or via agessh.NewEncryptedSSHIdentity wrapping a non-Ed25519 private key.

Common situations: Iterating an authorized_keys file and assuming all keys are Ed25519; passing an RSA key because the code path was copied from an RSA implementation; encrypted identity files generated from RSA private keys.

Related errors


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