{"record":{"id":"dfb58194750eee3a","repo":"crowdsecurity/crowdsec","slug":"failed-to-derive-key-w","errorCode":null,"errorMessage":"failed to derive key: %w","messagePattern":"failed to derive key: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"pkg/appsec/challenge/crypto.go","lineNumber":93,"sourceCode":"\n// cookiePlaintextFixedHeaderLen is the size of the fixed-layout portion of\n// the plaintext header that precedes the protobuf envelope:\n//\n//\tnot_after_be8 (8) + flags_byte (1) + reason_len_be (2) = 11\n//\n// followed by reason_bytes (variable, 0..MaxAllowlistReasonLen).\nconst cookiePlaintextFixedHeaderLen = 8 + 1 + 2\n\n// cookieFlagAllowlisted marks a cookie minted by GrantChallengeCookie\n// (operator allowlist bypass) rather than by a real challenge submission.\nconst cookieFlagAllowlisted byte = 0x01\n\nfunc deriveKey(secret []byte) ([]byte, error) {\n\th := hkdf.New(sha256.New, secret, nil, []byte(hkdfInfo))\n\tkey := make([]byte, 32) // AES-256\n\n\tif _, err := h.Read(key); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to derive key: %w\", err)\n\t}\n\n\treturn key, nil\n}\n\n// sealCookieV0 produces a v0 cookie sealed under the long-lived master\n// cookie key. notAfter is the unix-seconds expiration; flags carries the\n// allowlist bit set by GrantChallengeCookie (0 for normal cookies);\n// reason is the operator-supplied allowlist reason (empty for normal\n// cookies). All three are prepended to the marshaled proto BEFORE\n// encryption so they are both confidential (not observable from the wire)\n// and authenticated (any tamper attempt invalidates the GCM tag).\n//\n// Returns ErrAllowlistReasonSize if reason exceeds MaxAllowlistReasonLen.\nfunc sealCookieV0(envelope *pb.ChallengeCookie, masterCookieKey []byte, notAfter int64, flags byte, reason string, aad []byte, maxCookieLen int) (string, error) {\n\tif maxCookieLen <= 0 {\n\t\tmaxCookieLen = MaxCookieLen\n\t}","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/crowdsecurity/crowdsec/blob/909b5157986a2b2c2163300fdaef5ed01289f7d2/pkg/appsec/challenge/crypto.go#L75-L111","documentation":"deriveKey expands the master secret into a 32-byte AES-256 key with HKDF-SHA256. This error means hkdf.Read failed while extracting the key — practically only when the input secret is empty/nil or the hash stream errors, which should never occur with a validated secret. Both sealCookieV0 and openCookieV0Bytes depend on it.","triggerScenarios":"Calling sealCookieV0 or openCookieV0Bytes (via openCookie/ValidCookie) with an empty or nil master cookie key, e.g. a ChallengeRuntime whose keyring was never initialized with a valid master secret.","commonSituations":"A mis-built runtime where WithMasterSecret was never applied; a keyring initialized from an unvalidated empty secret; programmatic use of the challenge package without going through BuildOptions.","solutions":["Ensure the runtime is built via NewChallengeRuntime with a valid master secret (>= 32 bytes) or its random fallback.","Check that BuildOptions was called and its error handled before constructing the runtime.","Log the wrapped hkdf error; an empty HKDF secret input is the usual root cause."],"exampleFix":"// before\nopts, _ := challenge.BuildOptions(cfg, logger)\nrt, err := challenge.NewChallengeRuntime(opts...)\n// after\nopts, err := challenge.BuildOptions(cfg, logger)\nif err != nil {\n    return fmt.Errorf(\"challenge config: %w\", err)\n}\nrt, err := challenge.NewChallengeRuntime(opts...)","handlingStrategy":"validation","validationCode":"if len(masterSecret) == 0 {\n    return errors.New(\"master secret is empty; configure master_secret or use NewChallengeRuntime\")\n}","typeGuard":null,"tryCatchPattern":"if err := errors.Is(err, hkdfErrFamily); err != nil {\n    // construct runtime via the standard constructor instead of manual keys\n}","preventionTips":["Always build the runtime through NewChallengeRuntime/BuildOptions, never with hand-made keys.","Verify ParseConfiguredSecret succeeded before installing a secret.","Add a startup smoke test that seals and opens a round-trip cookie."],"tags":["appsec","crypto","hkdf","key-derivation"],"backgroundTag":"key-derivation-failed","analyzedSha":"909b5157986a2b2c2163300fdaef5ed01289f7d2","analyzedAt":"2026-09-06T12:27:26.012Z","contentChangedAt":"2026-09-06T12:27:26.012Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}