FiloSottile/age · error

invalid ssh-ed25519 recipient block

Error message

invalid ssh-ed25519 recipient block

What it means

Ed25519Identity.unwrap accepts only "ssh-ed25519" stanzas carrying exactly two arguments: the ephemeral share and the base64-encoded (RawStdEncoding) X25519 public key of the recipient. This error means the stanza is structurally malformed — wrong argument count — as opposed to belonging to a different identity (which returns age.ErrIncorrectIdentity). It indicates a corrupted or non-conforming age header.

Source

Thrown at agessh/agessh.go:315

}

func (i *Ed25519Identity) Recipient() *Ed25519Recipient {
	return &Ed25519Recipient{
		sshKey:         i.sshKey,
		theirPublicKey: i.ourPublicKey,
	}
}

func (i *Ed25519Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
	return multiUnwrap(i.unwrap, stanzas)
}

func (i *Ed25519Identity) unwrap(block *age.Stanza) ([]byte, error) {
	if block.Type != "ssh-ed25519" {
		return nil, age.ErrIncorrectIdentity
	}
	if len(block.Args) != 2 {
		return nil, errors.New("invalid ssh-ed25519 recipient block")
	}
	publicKey, err := format.DecodeString(block.Args[1])
	if err != nil {
		return nil, fmt.Errorf("failed to parse ssh-ed25519 recipient: %v", err)
	}
	if len(publicKey) != curve25519.PointSize {
		return nil, errors.New("invalid ssh-ed25519 recipient block")
	}

	if block.Args[0] != sshFingerprint(i.sshKey) {
		return nil, age.ErrIncorrectIdentity
	}

	sharedSecret, err := curve25519.X25519(i.secretKey, publicKey)
	if err != nil {
		return nil, fmt.Errorf("invalid X25519 recipient: %v", err)
	}

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Re-acquire or re-encrypt the file; the stanza cannot be repaired reliably.
  2. Inspect the file's stanza args (count and decodability) before unwrapping.
  3. If you generate stanzas in your own tooling, emit exactly two args: ephemeral share and format.EncodeString(publicKey).
  4. Treat it as a parse failure, not an identity mismatch — do not retry with other identities expecting success.

Example fix

// before (producer)
st := &age.Stanza{Type: "ssh-ed25519", Args: []string{ephemeral}} // missing key arg
// after
st := &age.Stanza{Type: "ssh-ed25519", Args: []string{ephemeral, format.EncodeString(publicKey)}}
Defensive patterns

Strategy: try-catch

Validate before calling

if block.Type == "ssh-ed25519" && len(block.Args) != 2 {
    return fmt.Errorf("malformed ssh-ed25519 stanza: %d args", len(block.Args))
}

Type guard

func isWellFormedEd25519Block(b *age.Stanza) bool {
    return b != nil && b.Type == "ssh-ed25519" && len(b.Args) == 2
}

Try / catch

fileKey, err := identity.Unwrap(stanza)
if err != nil {
    if !errors.Is(err, age.ErrIncorrectIdentity) {
        return fmt.Errorf("corrupt age header: %w", err) // structural, not identity mismatch
    }
    return err // safe to try next identity
}

Prevention

When it happens

Trigger: Calling Unwrap with a stanza where Type == "ssh-ed25519" but len(Args) != 2 — edited, truncated, or non-standard-conformant headers; buggy third-party writers.

Common situations: Hand-edited age files; malformed-header fuzzing/security testing; files produced by non-compliant age implementations or damaged in transit.

Related errors


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