hyperledger/fabric · error

client didn't send a TLS certificate

Error message

client didn't send a TLS certificate

What it means

The discovery service has TLS enabled and extracts the TLS certificate hash from the incoming gRPC stream context via certHashFromContext. An empty result means the client did not present any TLS certificate, so the server cannot verify the client's claimed ClientTlsCertHash. This is a mutual-TLS requirement failure.

Source

Thrown at discovery/service.go:252

	if request == nil {
		return nil, errors.New("nil request")
	}
	req, err := protoext.SignedRequestToRequest(request)
	if err != nil {
		return nil, errors.Wrap(err, "failed parsing request")
	}
	if req.Authentication == nil {
		return nil, errors.New("access denied, no authentication info in request")
	}
	if len(req.Authentication.ClientIdentity) == 0 {
		return nil, errors.New("access denied, client identity wasn't supplied")
	}
	if !tlsEnabled {
		return req, nil
	}
	computedHash := certHashFromContext(ctx)
	if len(computedHash) == 0 {
		return nil, errors.New("client didn't send a TLS certificate")
	}
	if !bytes.Equal(computedHash, req.Authentication.ClientTlsCertHash) {
		claimed := hex.EncodeToString(req.Authentication.ClientTlsCertHash)
		logger.Warningf("client claimed TLS hash %s doesn't match computed TLS hash from gRPC stream %s", claimed, hex.EncodeToString(computedHash))
		return nil, errors.New("client claimed TLS hash doesn't match computed TLS hash from gRPC stream")
	}
	return req, nil
}

func validateCCQuery(ccQuery *discovery.ChaincodeQuery) error {
	if len(ccQuery.Interests) == 0 {
		return errors.New("chaincode query must have at least one chaincode interest")
	}
	for _, interest := range ccQuery.Interests {
		if interest == nil {
			return errors.New("chaincode interest is nil")
		}
		if len(interest.Chaincodes) == 0 {

View on GitHub (pinned to 2736b63f8f)

Solutions

  1. Configure the client gRPC connection with TLS credentials that include the client certificate (grpc.WithTransportCredentials + credentials.NewTLS with Certificates set)
  2. Verify the client certificate chain is valid and actually sent (check with openssl s_client)
  3. Ensure any proxy between client and server forwards the client certificate
  4. Alternatively disable mutual TLS requirement on the server if your security model permits

Example fix

// before
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
// after
tlsCfg := &tls.Config{Certificates: []tls.Certificate{clientCert}, RootCAs: caPool}
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)))
Defensive patterns

Strategy: validation

Validate before calling

func ensureClientCert(tlsCfg *tls.Config) error {
    if tlsCfg == nil || len(tlsCfg.Certificates) == 0 {
        return errors.New("mutual TLS required: client certificate missing from TLS config")
    }
    return nil
}

Type guard

func hasClientCertificate(cfg *tls.Config) bool {
    return cfg != nil && len(cfg.Certificates) > 0
}

Try / catch

resp, err := client.Send(ctx, req)
if err != nil {
    if strings.Contains(err.Error(), "client didn't send a TLS certificate") {
        return fmt.Errorf("server requires mTLS; reconfigure dial options with client cert: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Discover or TestValidateStructure with TLS enabled on the server while the client connects without a client certificate (no mTLS), leaving certHashFromContext(ctx) empty.

Common situations: Client configured with one-way TLS only while the peer/discovery server requires mutual TLS; TLS credentials omitted from the dial options; a proxy/load balancer stripping the client cert; mismatch between client and server TLS settings after config change.

Understand the failure class

Related errors


AI-assisted analysis of hyperledger/fabric@2736b63f8f (2026-09-04). Data as JSON: /api/errors/6f92a91615eb4661. Report an issue: GitHub.