{"record":{"id":"502c41a458c0a0b5","repo":"kubernetes/kops","slug":"incorrect-requesthash","errorCode":null,"errorMessage":"incorrect RequestHash","messagePattern":"incorrect RequestHash","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/bootstrap/pkibootstrap/pkiverifier/verifier.go","lineNumber":99,"sourceCode":"\n\ttokenData := &pkibootstrap.AuthTokenData{}\n\tif err := json.Unmarshal(token.Data, tokenData); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"unmarshalling authorization token data: %w\", err)\n\t}\n\n\t// Guard against replay attacks\n\tif tokenData.Audience != pkibootstrap.AudienceNodeAuthentication {\n\t\treturn nil, nil, fmt.Errorf(\"incorrect Audience\")\n\t}\n\ttimeSkew := math.Abs(time.Since(time.Unix(tokenData.Timestamp, 0)).Seconds())\n\tif timeSkew > float64(v.opt.MaxTimeSkew) {\n\t\treturn nil, nil, fmt.Errorf(\"incorrect Timestamp %v\", tokenData.Timestamp)\n\t}\n\n\t// Verify the token has signed the body content.\n\trequestHash := sha256.Sum256(body)\n\tif !bytes.Equal(requestHash[:], tokenData.RequestHash) {\n\t\treturn nil, nil, fmt.Errorf(\"incorrect RequestHash\")\n\t}\n\n\treturn token, tokenData, nil\n}\n\n// Can generate keys with\n// openssl ecparam -name prime256v1 -genkey -noout -out ec-priv-key.pem\n// openssl ec -in ec-priv-key.pem -pubout > ec-pub-key.pem\n// Note that golang doesn't support secp256k1: https://groups.google.com/g/golang-nuts/c/Mbkug5t3ZYA\n\nfunc (v *verifier) VerifyToken(ctx context.Context, rawRequest *http.Request, authToken string, body []byte) (*bootstrap.VerifyResult, error) {\n\t// Reminder: we shouldn't trust any data we get from the client until we've checked the signature (and even then...)\n\t// Thankfully the GCE SDK does seem to escape the parameters correctly, for example.\n\n\ttoken, tokenData, err := v.parseTokenData(pkibootstrap.AuthenticationTokenPrefix, authToken, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/kubernetes/kops/blob/4c8573c808a73d578c5eadc86d410646ea0b0d73/pkg/bootstrap/pkibootstrap/pkiverifier/verifier.go#L81-L117","documentation":"The SHA-256 hash of the HTTP request body received by kops-controller does not match the RequestHash claim inside the signed token. The token commits to the exact body bytes that were signed on the node; a mismatch means the body was altered in transit or the client hashed different bytes than it sent. This binds the token to one specific request, preventing body-substitution replay.","triggerScenarios":"parseTokenData (verifier.go:97-100) raises this when sha256.Sum256(body) != tokenData.RequestHash: a proxy/load-balancer rewrites or re-compresses the POST body, the client signs a different payload than the one sent (e.g. signs the marshaled struct then sends a re-marshaled/vendored variant), Content-Encoding/transformation middleware mutates the body, or the body was read and not rewound before VerifyToken is called.","commonSituations":"Ingress controllers or service meshes that transparently recompress (gzip) or normalize JSON bodies; client code that signs the canonical JSON but sends pretty-printed JSON; double-reading the request body in a handler middleware so the verifier sees empty bytes; version skew where the request schema changed between nodeup and kops-controller.","solutions":["Remove or bypass anything between the node and kops-controller that transforms the request body (recompression, JSON re-encoding, WAF rewriting).","On the client, sign exactly the []byte sent: pass the same body slice to both CreateToken and the HTTP request writer.","Check handler middleware: if the body is read before VerifyToken, reset it with io.Copy(body, r.Body) / r.Body = http.NoBody pattern or buffer-and-restore so the verifier hashes the original bytes.","Align nodeup and kops-controller versions so the request body format matches what the token was minted for.","Debug by sha256-summing the body the controller receives and comparing with the token's requestHash claim (base64-decode the Data field)."],"exampleFix":"// before: client signs one payload, sends another\npayload, _ := json.Marshal(req)\ntoken, _ := auth.CreateToken(payload)\nhttp.Post(url, \"application/json\", bytes.NewReader(prettyJSON))\n// after: sign and send the identical bytes\npayload, _ := json.Marshal(req)\ntoken, _ := auth.CreateToken(payload)\nhttp.Post(url, \"application/json\", bytes.NewReader(payload))","handlingStrategy":"validation","validationCode":"// On the client: verify the bytes you sign are exactly the bytes you send\npayload, err := json.Marshal(req)\nif err != nil {\n\treturn err\n}\ntoken, err := authenticator.CreateToken(payload)\nif err != nil {\n\treturn err\n}\nhash := sha256.Sum256(payload)\n_ = hash // log/hash compare after send if debugging; ensure the request body is bytes.NewReader(payload), not a re-marshaled copy","typeGuard":"func requestHashMatches(body []byte, d *pkibootstrap.AuthTokenData) bool {\n\th := sha256.Sum256(body)\n\treturn d != nil && bytes.Equal(h[:], d.RequestHash)\n}","tryCatchPattern":"result, err := verifier.VerifyToken(ctx, req, authToken, body)\nif err != nil {\n\tif strings.Contains(err.Error(), \"incorrect RequestHash\") {\n\t\t// body was mutated in transit or client signed different bytes; do not blind-retry\n\t\tklog.Errorf(\"bootstrap request hash mismatch: %v\", err)\n\t\treturn nil, fmt.Errorf(\"request body does not match signed token: %w\", err)\n\t}\n\treturn nil, err\n}","preventionTips":["On the client, marshal the request once and pass the identical []byte to both CreateToken and the HTTP writer.","Audit proxies/ingress/service meshes on the path to kops-controller for body recompression or rewriting; disable transformation.","In server middleware, if the request body is read early, restore it before calling VerifyToken.","Keep nodeup and kops-controller versions aligned so the signed schema matches the sent schema."],"tags":["go","authentication","pki","request-body","integrity"],"backgroundTag":"request-hash-mismatch","analyzedSha":"4c8573c808a73d578c5eadc86d410646ea0b0d73","analyzedAt":"2026-09-05T04:13:19.212Z","contentChangedAt":"2026-09-05T04:13:19.212Z","schemaVersion":2},"datasetVersion":"2026-09-12T07:17:12.445Z"}