{"record":{"id":"a728306b1d0362d6","repo":"router-for-me/CLIProxyAPI","slug":"home-jwt-ca-fingerprint-is-required","errorCode":null,"errorMessage":"home jwt ca_fingerprint is required","messagePattern":"home jwt ca_fingerprint is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/home/certificate.go","lineNumber":101,"sourceCode":"\tparts := strings.Split(strings.TrimSpace(rawJWT), \".\")\n\tif len(parts) != 3 {\n\t\treturn claims, fmt.Errorf(\"home jwt is invalid\")\n\t}\n\tpayload, errDecode := decodeJWTPart(parts[1])\n\tif errDecode != nil {\n\t\treturn claims, errDecode\n\t}\n\tif errUnmarshal := json.Unmarshal(payload, &claims); errUnmarshal != nil {\n\t\treturn claims, errUnmarshal\n\t}\n\tif strings.TrimSpace(claims.CertificateID) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt certificate_id is required\")\n\t}\n\tif strings.TrimSpace(claims.ClusterID) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt cluster_id is required\")\n\t}\n\tif normalizeFingerprint(claims.CAFingerprint) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt ca_fingerprint is required\")\n\t}\n\tif strings.TrimSpace(claims.EnrollmentSecret) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt enrollment_secret is required\")\n\t}\n\tif strings.TrimSpace(claims.IP) == \"\" || claims.Port <= 0 {\n\t\treturn claims, fmt.Errorf(\"home jwt target address is invalid\")\n\t}\n\treturn claims, nil\n}\n\nfunc decodeJWTPart(part string) ([]byte, error) {\n\tif decoded, errDecode := base64.RawURLEncoding.DecodeString(part); errDecode == nil {\n\t\treturn decoded, nil\n\t}\n\treturn base64.URLEncoding.DecodeString(part)\n}\n\nfunc defaultCertificatePaths() (certificatePaths, error) {","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/home/certificate.go#L83-L119","documentation":"Claim-level validation in parseHomeJWTClaims: the ca_fingerprint claim is empty after normalization (normalizeFingerprint strips separators/whitespace). The fingerprint pins the expected home CA so the client can verify the CA certificate it receives during mTLS enrollment.","triggerScenarios":"JWT payload omits ca_fingerprint, or contains a value that is only colons/spaces (e.g. \":::\") so normalization yields an empty string.","commonSituations":"Hand-built test tokens skipping the fingerprint, coordinator versions that stopped embedding it, or a malformed fingerprint pasted with only separators.","solutions":["Get a fresh enrollment JWT that carries the real CA fingerprint (hex, with or without colons)","If self-minting, compute it from the CA cert: openssl x509 -in home-ca-crt.pem -noout -fingerprint -sha256 | cut -d= -f2","Decode the token payload and confirm the claim before retrying enrollment"],"exampleFix":"// before (JWT payload)\n{\"certificate_id\":\"c\",\"cluster_id\":\"k\",\"ca_fingerprint\":\"\"}\n\n// after (JWT payload)\n{\"certificate_id\":\"c\",\"cluster_id\":\"k\",\"ca_fingerprint\":\"AA:BB:CC:DD:...\"}","handlingStrategy":"validation","validationCode":"func fingerprintClaimOK(payload []byte) bool {\n    var c struct {\n        CAFingerprint string `json:\"ca_fingerprint\"`\n    }\n    return json.Unmarshal(payload, &c) == nil && strings.ReplaceAll(c.CAFingerprint, \":\", \"\") != \"\"\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Verify the ca_fingerprint claim is a real hex string, not just present","Keep the CA cert that matches the fingerprint available for cross-checking","Include claim-presence assertions in coordinator token-minting tests"],"tags":["jwt","authentication","mtls","enrollment","home","validation"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}