nats-io/nats-server · error

incomplete cert chain, got

Error message

incomplete cert chain, got %d, want at least %d

What it means

GetOCSPStatus requires at least a leaf and its issuer in the first verified chain to check the OCSP response signature. If the chain has fewer than 2 certificates, validation cannot proceed and this error reports the actual length found.

Solutions

  1. Ensure the client sends (or the server's cert pool supplies) the issuer certificate so the chain has at least leaf + issuer
  2. If using self-signed test certs, build a two-cert chain (root CA + leaf) instead of a single self-signed leaf
  3. Guard with a length check on VerifiedChains[0] before calling GetOCSPStatus

Example fix

// before
chain := [][]*x509.Certificate{{leaf}}
// after
chain := [][]*x509.Certificate{{leaf, caCert}}
Defensive patterns

Strategy: type-guard

Validate before calling

cs := conn.ConnectionState()
if len(cs.VerifiedChains) == 0 || len(cs.VerifiedChains[0]) < 2 {
    return errors.New("chain too short for OCSP check (need leaf + issuer)")
}

Type guard

func hasLeafAndIssuer(cs tls.ConnectionState) bool {
    return len(cs.VerifiedChains) > 0 && len(cs.VerifiedChains[0]) >= 2
}

Try / catch

resp, err := GetOCSPStatus(cs)
if err != nil {
    if strings.HasPrefix(err.Error(), "incomplete cert chain") {
        return nil, errOCSPNotPossible
    }
    return err
}

Prevention

When it happens

Trigger: Calling GetOCSPStatus with a ConnectionState whose VerifiedChains[0] contains only the leaf cert (len < 2) — e.g. a self-signed or directly trusted leaf with no issuer appended.

Common situations: Client presenting a self-signed certificate trusted as a root; minimal test chains built with only one cert; RootCAs pool configured so only the leaf verifies without issuer in chain.

Related errors


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/1d8c50c6a83e2745. Report an issue: GitHub.

Appendix: source

Thrown at internal/ocsp/ocsp.go:237

	if err != nil {
		t.Fatal(err)
	}

	block, _ := pem.Decode(data)
	if block == nil {
		t.Fatalf("failed to decode PEM %s", pemPath)
	}
	return block
}

func GetOCSPStatus(s tls.ConnectionState) (*ocsp.Response, error) {
	if len(s.VerifiedChains) == 0 {
		return nil, fmt.Errorf("missing TLS verified chains")
	}
	chain := s.VerifiedChains[0]

	if got, want := len(chain), 2; got < want {
		return nil, fmt.Errorf("incomplete cert chain, got %d, want at least %d", got, want)
	}
	leaf, issuer := chain[0], chain[1]

	resp, err := ocsp.ParseResponseForCert(s.OCSPResponse, leaf, issuer)
	if err != nil {
		return nil, fmt.Errorf("failed to parse OCSP response: %w", err)
	}
	if err := resp.CheckSignatureFrom(issuer); err != nil {
		return resp, err
	}
	return resp, nil
}

func SetOCSPStatus(t *testing.T, ocspURL, certPEM string, status int) {
	t.Helper()

	cert := parseCertPEM(t, certPEM)

View on GitHub (pinned to 3a66a489d2)