gastownhall/beads · error

identity: reply authentication failed

Error message

identity: reply authentication failed

What it means

VerifyIdentReply computes the expected MAC (SignIdentReply over the canonical reply, secret, and nonce) and compares it to the reply's MAC using hmac.Equal. This error means both MACs decoded as hex but differ — the reply was tampered with, or was signed with a different secret or nonce than the request used.

Source

Thrown at internal/storage/dbproxy/identity/control.go:116

	return reply, nil
}

// VerifyIdentReply verifies the authenticated reply for the request nonce.
func VerifyIdentReply(reply IdentReply, secret, nonce string) error {
	got, err := hex.DecodeString(reply.MAC)
	if err != nil {
		return errors.New("identity: invalid reply MAC")
	}
	signed, err := SignIdentReply(reply, secret, nonce)
	if err != nil {
		return fmt.Errorf("identity: authenticate reply: %w", err)
	}
	want, err := hex.DecodeString(signed.MAC)
	if err != nil {
		return fmt.Errorf("identity: decode expected reply MAC: %w", err)
	}
	if !hmac.Equal(got, want) {
		return errors.New("identity: reply authentication failed")
	}
	return nil
}

func decodeIdentNonce(nonce string) ([]byte, error) {
	raw, err := hex.DecodeString(nonce)
	if err != nil || len(raw) != identNonceBytes {
		return nil, errors.New("identity: invalid request nonce")
	}
	return raw, nil
}

func canonicalIdentReply(reply IdentReply) ([]byte, error) {
	payload := struct {
		Schema      int    `json:"schema"`
		Role        string `json:"role"`
		RootID      string `json:"root_id"`
		UpstreamID  string `json:"upstream_id"`

View on GitHub (pinned to 71377f2769)

Solutions

  1. Confirm both sides read the same proxy secret (re-run secret rotation atomically and restart the proxy)
  2. Always generate a fresh nonce per Identify call and verify the reply for that exact nonce
  3. Treat as an authentication failure: reject the peer and do not retry with the same connection
  4. Check for stale proxy processes still running with an old secret after rotation

Example fix

// before
if err := identity.VerifyIdentReply(reply, secret, nonce); err != nil { return err }
// after
if err := identity.VerifyIdentReply(reply, freshNonce(), secret); err != nil {
    if strings.Contains(err.Error(), "authentication failed") { rotateSecret(); return ErrUntrustedPeer }
    return err
}
Defensive patterns

Strategy: type-guard

Type guard

func isAuthFailed(err error) bool { return strings.Contains(err.Error(), "reply authentication failed") }

Try / catch

if err := identity.VerifyIdentReply(reply, secret, nonce); err != nil {
    if isAuthFailed(err) { rotateSecretAndRedial(); return ErrUntrustedPeer }
    return err
}

Prevention

When it happens

Trigger: Calling VerifyIdentReply on a reply whose MAC decodes cleanly but does not match: wrong shared secret on the proxy side, a stale/reused nonce, mutated reply fields (role/root_id changed in transit), or a replay from a previous handshake.

Common situations: Secret rotation where the proxy still holds the old secret; a man-in-the-middle or rogue socket peer altering reply contents; reusing a captured reply for a new nonce; config drift between beads and its managed proxy.

Understand the failure class

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/0be5d215d4d8ae2c. Report an issue: GitHub.