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.PublicKey

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Obtain the file from a trusted source or re-encrypt it; the stanza structure is invalid.
  2. Verify the header with 'age-inspect' or by printing stanzas before unwrapping.
  3. If a tool you wrote builds stanzas, emit exactly one arg (the sshFingerprint) for ssh-rsa recipients.
  4. 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

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


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