FiloSottile/age · error
invalid ssh-rsa recipient block
Error message
invalid ssh-rsa recipient block
What it means
RSAIdentity.unwrap only accepts stanzas of type "ssh-rsa" carrying exactly one argument (the key fingerprint). This error means an "ssh-rsa"-typed stanza arrived with a different number of arguments, so the stanza is malformed rather than merely belonging to another identity (that case returns age.ErrIncorrectIdentity). It indicates a corrupted, hand-crafted, or future/mutated age header.
Source
Thrown at agessh/agessh.go:125
}
func (i *RSAIdentity) Recipient() *RSARecipient {
return &RSARecipient{
sshKey: i.sshKey,
pubKey: &i.k.PublicKey,
}
}
func (i *RSAIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
func (i *RSAIdentity) unwrap(block *age.Stanza) ([]byte, error) {
if block.Type != "ssh-rsa" {
return nil, age.ErrIncorrectIdentity
}
if len(block.Args) != 1 {
return nil, errors.New("invalid ssh-rsa recipient block")
}
if block.Args[0] != sshFingerprint(i.sshKey) {
return nil, age.ErrIncorrectIdentity
}
fileKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, i.k,
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.PublicKeyView on GitHub (pinned to b74dce4cdb)
Solutions
- Obtain the file from a trusted source or re-encrypt it; the stanza structure is invalid.
- Verify the header with 'age-inspect' or by printing stanzas before unwrapping.
- If a tool you wrote builds stanzas, emit exactly one arg (the sshFingerprint) for ssh-rsa recipients.
- Treat this as a fatal parse error — do not retry with the same identity.
Example fix
// before (producer)
st := &age.Stanza{Type: "ssh-rsa", Args: []string{fp, extra}}
// after
st := &age.Stanza{Type: "ssh-rsa", Args: []string{fp}} Defensive patterns
Strategy: try-catch
Validate before calling
// inspect stanza before unwrapping
if block.Type == "ssh-rsa" && len(block.Args) != 1 {
return fmt.Errorf("malformed ssh-rsa stanza: %d args", len(block.Args))
} Type guard
func isWellFormedRSABlock(b *age.Stanza) bool {
return b != nil && b.Type == "ssh-rsa" && len(b.Args) == 1
} Try / catch
fileKey, err := identity.Unwrap(stanza)
if err != nil {
if !errors.Is(err, age.ErrIncorrectIdentity) {
// malformed stanza: log and abort; retrying with other identities won't help
return fmt.Errorf("corrupt age header: %w", err)
}
return err // try next identity
} Prevention
- Never hand-edit age file headers; treat them as binary-opaque.
- Transfer .age files in binary-safe channels (no truncation/mangling).
- If you emit stanzas, emit exactly one arg (fingerprint) for ssh-rsa.
- Distinguish ErrIncorrectIdentity (skippable) from structural errors (fatal).
When it happens
Trigger: Calling Unwrap on a recipient stanza whose Type == "ssh-rsa" but len(Args) != 1 — e.g. an age file whose header stanza was edited, truncated, or generated by a non-conforming writer.
Common situations: Manually edited age files; files transferred through lossy pipelines that mangled the header; testing malicious/malformed headers against the parser; a producer bug emitting extra stanza args.
Related errors
- invalid ssh-ed25519 recipient block
- empty line in armored data
- invalid stanza type: %q
- invalid stanza argument: %q
- malformed stanza opening line: %q
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/e9df8f1bb3af0514.
Report an issue: GitHub.