JuliusBrussee/caveman · critical
githubapp: private key is not RSA
Error message
githubapp: private key is not RSA
What it means
Raised by parseRSAPrivateKey when the supplied PEM block decodes successfully but its DER payload is neither a PKCS#1 RSA key nor a PKCS#8 key. It fires after both x509.ParsePKCS1PrivateKey and x509.ParsePKCS8PrivateKey fail on block.Bytes, meaning the input is a well-formed PEM envelope around a non-RSA payload: typically an EC or Ed25519 key, a corrupted/truncated key body, or a certificate mistakenly passed where the GitHub App private key was expected. The input at fault is the private key bytes given to New (from the app's downloaded .pem or its KMS/openssl-converted equivalent), which must contain an RSA private key for GitHub App JWT signing.
Solutions
- Download the GitHub App's .pem from GitHub App settings — it is always RSA.
- If generating your own key, use openssl genrsa 2048 and verify with openssl rsa -in key.pem -check.
- Inspect the key header: 'BEGIN EC PRIVATE KEY' or EC-content PKCS#8 must be replaced by an RSA key.
Example fix
// before $ openssl genpkey -algorithm ED25519 -out app.key # non-RSA key // after $ openssl genrsa -out app.private-key.pem 2048 # RSA key for GitHub App
Defensive patterns
Strategy: validation
Validate before calling
func isRSAKey(s string) bool {
block, _ := pem.Decode([]byte(s))
if block == nil {
return false
}
if _, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
return true
}
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return false
}
_, ok := parsed.(*rsa.PrivateKey)
return ok
} Type guard
func isRSAKey(s string) bool {
block, _ := pem.Decode([]byte(s))
if block == nil {
return false
}
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return false
}
_, ok := parsed.(*rsa.PrivateKey)
return ok
} Try / catch
client, err := githubapp.New(pem)
if err != nil {
if strings.Contains(err.Error(), "is not RSA") {
return errors.New("GitHub App keys must be RSA; re-download the .pem from app settings")
}
return err
} Prevention
- Only use the .pem downloaded from the GitHub App settings page.
- When generating keys, use openssl genrsa (RSA), never genpkey with EC/Ed25519.
- Check the PEM header/algorithm before wiring a key into app config.
When it happens
Trigger: New() called with a PKCS#8 PEM containing an EC, Ed25519, or otherwise non-RSA private key.
Common situations: Generating a key with openssl genpkey -algorithm EC or ssh-keygen -t ed25519 and using it as a GitHub App key instead of the RSA key downloaded from GitHub; swapping in an unrelated non-RSA key from another service.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- githubapp: private key is neither PKCS#1 nor PKCS#8 RSA
- githubapp: private key is not valid PEM
- githubapp: sign app jwt
- githubapp: app id is required
- githubapp: base_url rejected by SSRF guard
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/c9d48c799d49271d.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/githubapp/githubapp.go:411
// parseRSAPrivateKey accepts a PKCS#1 ("RSA PRIVATE KEY") or PKCS#8
// ("PRIVATE KEY") PEM — GitHub Apps download PKCS#1, but Cloud KMS / openssl
// conversions emit PKCS#8, so we accept both.
func parseRSAPrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, fmt.Errorf("githubapp: private key is not valid PEM")
}
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
return key, nil
}
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("githubapp: private key is neither PKCS#1 nor PKCS#8 RSA: %w", err)
}
key, ok := parsed.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("githubapp: private key is not RSA")
}
return key, nil
}
// snippet trims an error body so we never echo a large/secret-bearing response.
func snippet(b []byte) string {
const max = 256
s := strings.TrimSpace(string(b))
if len(s) > max {
return s[:max] + "…"
}
return s
}
View on GitHub (pinned to 3ee70a1026)