{"record":{"id":"33c2b91963977f08","repo":"slackhq/nebula","slug":"unable-to-unmarshal-private-key-w","errorCode":null,"errorMessage":"unable to unmarshal private key: %w","messagePattern":"unable to unmarshal private key: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"noiseutil/nist.go","lineNumber":51,"sourceCode":"\tif rng == nil {\n\t\trng = rand.Reader\n\t}\n\tprivkey, err := c.curve.GenerateKey(rng)\n\tif err != nil {\n\t\treturn noise.DHKey{}, err\n\t}\n\tpubkey := privkey.PublicKey()\n\treturn noise.DHKey{Private: privkey.Bytes(), Public: pubkey.Bytes()}, nil\n}\n\nfunc (c nistCurve) DH(privkey, pubkey []byte) ([]byte, error) {\n\tecdhPubKey, err := c.curve.NewPublicKey(pubkey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to unmarshal pubkey: %w\", err)\n\t}\n\tecdhPrivKey, err := c.curve.NewPrivateKey(privkey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to unmarshal private key: %w\", err)\n\t}\n\n\treturn ecdhPrivKey.ECDH(ecdhPubKey)\n}\n\nfunc (c nistCurve) DHLen() int {\n\t// NOTE: Noise Protocol specifies \"DHLen\" to represent two things:\n\t// - The size of the public key\n\t// - The return size of the DH() function\n\t// But for standard NIST ECDH, the sizes of these are different.\n\t// Luckily, the flynn/noise library actually only uses this DHLen()\n\t// value to represent the public key size, so that is what we are\n\t// returning here. The length of the DH() return bytes are unaffected by\n\t// this value here.\n\treturn c.pubLen\n}\nfunc (c nistCurve) DHName() string { return c.name }\n","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/noiseutil/nist.go#L33-L69","documentation":"This error is returned by nistCurve.DH when the local private key bytes cannot be parsed via ecdh.Curve.NewPrivateKey. It indicates the configured local private key is not a valid scalar for the NIST curve — wrong length, all zeros, or out of range. The underlying error is wrapped so the cause is preserved.","triggerScenarios":"Calling nistCurve.DH(privkey, pubkey) where privkey is empty, the wrong byte length for the curve, or numerically invalid as a private scalar for ecdh.Curve.NewPrivateKey.","commonSituations":"Hand-edited or base64-decoded config keys with missing/truncated bytes; a key generated for a different curve or algorithm; loading a PEM/DER private key file and passing the raw file bytes instead of the raw scalar.","solutions":["Regenerate the local key pair with the same library/curve and update the config","Ensure the private key is the raw scalar (32/48/66 bytes for P-256/P-384/P-521), not a PEM/DER/pkcs8 blob","Check config parsing: confirm base64 decoding produced the full key (no whitespace truncation)","Confirm the same curve is used for key generation and DH (e.g. P256 vs P384 mismatch)"],"exampleFix":"// before: passing a PEM block's raw bytes as the private key\npriv := pemBlock.Bytes\nshared, err := curve.DH(priv, pub)\n// after: decode to a raw scalar of correct size first\npriv, err := base64.StdEncoding.DecodeString(cfg.PrivateKey)\nif len(priv) != 32 { return nil, fmt.Errorf(\"bad P-256 private key length %d\", len(priv)) }\nshared, err := curve.DH(priv, pub)","handlingStrategy":"validation","validationCode":"func validNistPriv(curveName string, priv []byte) bool {\n    var n int\n    switch curveName {\n    case \"P256\": n = 32\n    case \"P384\": n = 48\n    case \"P521\": n = 66\n    default: return false\n    }\n    if len(priv) != n { return false }\n    for _, b := range priv { if b != 0 { return true } }\n    return false // all-zero scalar is invalid\n}","typeGuard":"func isPrivUnmarshalError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"unable to unmarshal private key\")\n}","tryCatchPattern":"shared, err := curve.DH(priv, pub)\nif err != nil {\n    if isPrivUnmarshalError(err) {\n        return fmt.Errorf(\"local private key invalid for curve; regenerate keys: %w\", err)\n    }\n    return err\n}","preventionTips":["Store raw scalars (correct fixed length) in config, not PEM/DER blobs","Regenerate keys with the same toolchain/curve used at runtime","Base64-decode and length-check keys at config-load time","Watch for config parsers trimming or truncating long key strings"],"tags":["go","noise-protocol","ecdh","key-parsing"],"backgroundTag":"invalid-private-key","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}