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
- Pass the public key in authorized_keys format, e.g. 'ssh-ed25519 AAAA... user@host'.
- Verify with `ssh-keygen -lf keyfile.pub` that the key parses before giving it to age.
- 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
- Pass the .pub public key file contents, never the private key.
- Trim whitespace and newlines from config-sourced recipient strings.
- Sanity-check keys with `ssh-keygen -lf` before adding to configs.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- unknown SSH recipient type: %q
- malformed recipient %q: %v
- malformed recipient %q: invalid type %q
- SSH public key is not an RSA key
- unexpected public key type
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/ae5f410eaa2645df.
Report an issue: GitHub.