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
- Re-acquire or re-encrypt the file; the stanza cannot be repaired reliably.
- Inspect the file's stanza args (count and decodability) before unwrapping.
- If you generate stanzas in your own tooling, emit exactly two args: ephemeral share and format.EncodeString(publicKey).
- 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
- Treat age file headers as immutable; never edit stanza args manually.
- Use binary-safe transfer to avoid header corruption.
- When emitting stanzas yourself, always include both ephemeral share and encoded public key args.
- Classify errors: only age.ErrIncorrectIdentity should trigger trying the next identity.
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
- invalid ssh-rsa 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/b1cfbdfb4458a191.
Report an issue: GitHub.