FiloSottile/age · error

malformed SSH recipient: %q: %v

Error message

malformed SSH recipient: %q: %v

What it means

ParseRecipient parses an SSH recipient string with golang.org/x/crypto/ssh.ParseAuthorizedKey. If the string is not a valid SSH authorized_keys line (bad base64, wrong format, unsupported content), it wraps the underlying error in "malformed SSH recipient". The %q shows the offending string and %v the parse failure reason.

Source

Thrown at agessh/agessh.go:176

	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,
	}, nil
}

func ParseRecipient(s string) (age.Recipient, error) {
	pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s))
	if err != nil {
		return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err)
	}

	var r age.Recipient
	switch t := pubKey.Type(); t {
	case "ssh-rsa":
		r, err = NewRSARecipient(pubKey)
	case "ssh-ed25519":
		r, err = NewEd25519Recipient(pubKey)
	default:
		return nil, fmt.Errorf("unknown SSH recipient type: %q", t)
	}
	if err != nil {
		return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err)
	}

	return r, nil
}

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Pass the public key in authorized_keys format, e.g. 'ssh-ed25519 AAAA... user@host'.
  2. Verify with `ssh-keygen -lf keyfile.pub` that the key parses before giving it to age.
  3. Ensure you are not passing a private key or an age1... recipient where an SSH recipient is expected.

Example fix

// before
r, err := agessh.ParseRecipient(string(privKeyBytes)) // private key passed
// after
pubBytes, _ := os.ReadFile("id_ed25519.pub")
r, err := agessh.ParseRecipient(strings.TrimSpace(string(pubBytes)))
Defensive patterns

Strategy: validation

Validate before calling

s := strings.TrimSpace(recipientStr)
fields := strings.Fields(s)
if len(fields) < 2 || !strings.HasPrefix(fields[0], "ssh-") {
    return errors.New("recipient must be an authorized_keys-format SSH public key")
}
if _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s)); err != nil {
    return fmt.Errorf("invalid SSH public key: %w", err)
}

Type guard

func isSSHPublicKey(s string) bool {
    _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(s)))
    return err == nil
}

Try / catch

r, err := agessh.ParseRecipient(s)
if err != nil {
    return fmt.Errorf("check recipient format (type base64 comment): %w", err)
}

Prevention

When it happens

Trigger: Calling agessh.ParseRecipient with a string that ssh.ParseAuthorizedKey rejects: not in 'type base64 [comment]' format, invalid base64, truncated key, or extra invalid fields.

Common situations: Passing a private key file contents instead of the public key; quoting or whitespace corruption in config files; passing an age native recipient (age1...) to the SSH parser; copy-paste that dropped characters.

Understand the failure class

Related errors


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