{"record":{"id":"03201b539879d6f8","repo":"JuliusBrussee/caveman","slug":"githubapp-private-key-is-not-valid-pem","errorCode":null,"errorMessage":"githubapp: private key is not valid PEM","messagePattern":"githubapp: private key is not valid PEM","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"shared/platform/githubapp/githubapp.go","lineNumber":400,"sourceCode":"\tresp, err := a.httpClient.Do(req)\n\tif err != nil {\n\t\treturn 0, nil, fmt.Errorf(\"githubapp: request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\traw, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))\n\tif err != nil {\n\t\treturn resp.StatusCode, nil, fmt.Errorf(\"githubapp: read response: %w\", err)\n\t}\n\treturn resp.StatusCode, raw, nil\n}\n\n// parseRSAPrivateKey accepts a PKCS#1 (\"RSA PRIVATE KEY\") or PKCS#8\n// (\"PRIVATE KEY\") PEM — GitHub Apps download PKCS#1, but Cloud KMS / openssl\n// conversions emit PKCS#8, so we accept both.\nfunc parseRSAPrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {\n\tblock, _ := pem.Decode(pemBytes)\n\tif block == nil {\n\t\treturn nil, fmt.Errorf(\"githubapp: private key is not valid PEM\")\n\t}\n\tif key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {\n\t\treturn key, nil\n\t}\n\tparsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"githubapp: private key is neither PKCS#1 nor PKCS#8 RSA: %w\", err)\n\t}\n\tkey, ok := parsed.(*rsa.PrivateKey)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"githubapp: private key is not RSA\")\n\t}\n\treturn key, nil\n}\n\n// snippet trims an error body so we never echo a large/secret-bearing response.\nfunc snippet(b []byte) string {\n\tconst max = 256","sourceCodeStart":382,"sourceCodeEnd":418,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/githubapp/githubapp.go#L382-L418","documentation":"parseRSAPrivateKey only accepts keys wrapped in a valid PEM block (PKCS#1 or PKCS#8). pem.Decode returned nil, meaning the input bytes contain no '-----BEGIN ...-----' armor at all. New() calls this when constructing a GitHub App client, so the app credentials could not be parsed.","triggerScenarios":"Calling New() with a private key that is raw base64 DER bytes without PEM armor, an empty/whitespace value, a key with stripped BEGIN/END lines, or a file path/placeholder instead of key contents.","commonSituations":"Env var contents lost their header lines through shell/secret-manager quoting; copying only the base64 body from the downloaded .pem; GITHUB_APP_KEY empty because the env var was never set; passing a filename instead of the file contents.","solutions":["Re-download the GitHub App private key (.pem) and use the full file including the '-----BEGIN RSA PRIVATE KEY-----' and '-----END RSA PRIVATE KEY-----' lines.","Load with os.ReadFile instead of pasting through env/config, or base64 the whole file and decode before use.","Verify before calling New() that the value is non-empty and starts with '-----BEGIN'.","If you have only DER bytes, wrap them in PEM (openssl rsa -inform DER -in key.der -out key.pem or pem.EncodeToMemory)."],"exampleFix":"// before\nkey := os.Getenv(\"GH_APP_KEY\") // raw base64 body, no PEM armor\nclient := githubapp.New(key)\n// after\npemBytes, _ := os.ReadFile(\"app.private-key.pem\") // full PEM with BEGIN/END lines\nclient := githubapp.New(string(pemBytes))","handlingStrategy":"validation","validationCode":"func looksLikePEM(b []byte) bool {\n\treturn bytes.HasPrefix(bytes.TrimSpace(b), []byte(\"-----BEGIN\"))\n}\nif !looksLikePEM([]byte(key)) {\n\treturn errors.New(\"GitHub App key is not PEM-encoded; missing -----BEGIN armor\")\n}","typeGuard":"func isPEMPrivateKey(s string) bool {\n\tblock, _ := pem.Decode([]byte(s))\n\treturn block != nil && strings.Contains(block.Type, \"PRIVATE KEY\")\n}","tryCatchPattern":"client, err := githubapp.New(rawKey)\nif err != nil {\n\tif strings.Contains(err.Error(), \"not valid PEM\") {\n\t\treturn fmt.Errorf(\"GH_APP_KEY missing PEM armor: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Store the key as a full PEM file and read it with os.ReadFile, never paste fragments.","Use base64 on the whole file and decode before use when passing through env/config.","Add a startup assertion that the key starts with '-----BEGIN'.","Keep BEGIN/END lines intact across shell quoting and secret-manager round-trips."],"tags":["github-app","pem","rsa","config"],"backgroundTag":"invalid-argument-format","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}