{"record":{"id":"e09816b03d3fb122","repo":"hyperledger/fabric","slug":"client-claimed-tls-hash-doesn-t-match-computed-tls","errorCode":null,"errorMessage":"client claimed TLS hash doesn't match computed TLS hash from gRPC stream","messagePattern":"client claimed TLS hash doesn't match computed TLS hash from gRPC stream","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"discovery/service.go","lineNumber":257,"sourceCode":"\t\treturn nil, errors.Wrap(err, \"failed parsing request\")\n\t}\n\tif req.Authentication == nil {\n\t\treturn nil, errors.New(\"access denied, no authentication info in request\")\n\t}\n\tif len(req.Authentication.ClientIdentity) == 0 {\n\t\treturn nil, errors.New(\"access denied, client identity wasn't supplied\")\n\t}\n\tif !tlsEnabled {\n\t\treturn req, nil\n\t}\n\tcomputedHash := certHashFromContext(ctx)\n\tif len(computedHash) == 0 {\n\t\treturn nil, errors.New(\"client didn't send a TLS certificate\")\n\t}\n\tif !bytes.Equal(computedHash, req.Authentication.ClientTlsCertHash) {\n\t\tclaimed := hex.EncodeToString(req.Authentication.ClientTlsCertHash)\n\t\tlogger.Warningf(\"client claimed TLS hash %s doesn't match computed TLS hash from gRPC stream %s\", claimed, hex.EncodeToString(computedHash))\n\t\treturn nil, errors.New(\"client claimed TLS hash doesn't match computed TLS hash from gRPC stream\")\n\t}\n\treturn req, nil\n}\n\nfunc validateCCQuery(ccQuery *discovery.ChaincodeQuery) error {\n\tif len(ccQuery.Interests) == 0 {\n\t\treturn errors.New(\"chaincode query must have at least one chaincode interest\")\n\t}\n\tfor _, interest := range ccQuery.Interests {\n\t\tif interest == nil {\n\t\t\treturn errors.New(\"chaincode interest is nil\")\n\t\t}\n\t\tif len(interest.Chaincodes) == 0 {\n\t\t\treturn errors.New(\"chaincode interest must contain at least one chaincode\")\n\t\t}\n\t\tfor _, cc := range interest.Chaincodes {\n\t\t\tif cc.Name == \"\" {\n\t\t\t\treturn errors.New(\"chaincode name in interest cannot be empty\")","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/hyperledger/fabric/blob/2736b63f8fd5932511d56fe68b7039d15977f7f6/discovery/service.go#L239-L275","documentation":"The client sent a TLS certificate and declared a ClientTlsCertHash in the request Authentication, but the hash computed from the actual certificate on the gRPC stream does not match the claimed hash. The server logs a warning with both hex-encoded hashes and rejects the request, guarding against identity/certificate spoofing.","triggerScenarios":"Calling Discover or TestValidateStructure where req.Authentication.ClientTlsCertHash differs from bytes computed by certHashFromContext over the stream's TLS certificate.","commonSituations":"Client computed the hash with a different algorithm or over the wrong DER encoding; client reconnected with a rotated/renewed certificate but reused an old hash; the request was built once and reused across connections with different certs; hash of an intermediate cert instead of the leaf.","solutions":["Recompute ClientTlsCertHash from the exact certificate the gRPC connection presents (SHA-256 over the DER-encoded leaf certificate) right before sending","Rebuild the discovery request per connection instead of caching it when certificates rotate","Log and compare the two hex hashes (the server warning shows both) to find which side is stale","Use an SDK helper that sets ClientTlsCertHash automatically from the TLS config"],"exampleFix":"// before\nreq.Authentication.ClientTlsCertHash = cachedHash // stale after cert rotation\n// after\nder, _ := x509.MarshalCertificate(leafCert) // DER bytes the conn presents\nsum := sha256.Sum256(der)\nreq.Authentication.ClientTlsCertHash = sum[:]","handlingStrategy":"validation","validationCode":"func computeCertHash(cert *x509.Certificate) []byte {\n    sum := sha256.Sum256(cert.Raw)\n    return sum[:]\n}\n// set req.Authentication.ClientTlsCertHash = computeCertHash(leafOfActiveConn) right before each send","typeGuard":"func hashMatches(conn *grpc.ClientConn, claimed []byte) bool {\n    state := conn.GetCredentials() // or compute from the conn's TLS state\n    return state != nil && bytes.Equal(computeCertHash(state.LeafCert), claimed)\n}","tryCatchPattern":"resp, err := client.Send(ctx, req)\nif err != nil {\n    if strings.Contains(err.Error(), \"claimed TLS hash doesn't match\") {\n        req.Authentication.ClientTlsCertHash = computeCertHash(currentConnLeafCert())\n        return client.Send(ctx, req) // rebuild and retry once\n    }\n    return err\n}","preventionTips":["Always compute the hash over the DER (cert.Raw) of the leaf certificate with SHA-256","Rebuild discovery requests after any TLS certificate rotation; never cache AuthInfo across connections","Generate the hash from the same tls.Config used to dial, not from a different cert file","Compare server-side warning log hashes with local values when debugging"],"tags":["tls","mtls","hash-mismatch","grpc","discovery","fabric"],"backgroundTag":"tls-cert-hash-mismatch","analyzedSha":"2736b63f8fd5932511d56fe68b7039d15977f7f6","analyzedAt":"2026-09-04T08:52:36.465Z","contentChangedAt":"2026-09-04T08:52:36.465Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}