Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Available Commands:

Flags:
--app-id string The GitHub App to connect to. ($BATON_APP_ID)
--app-privatekey string Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set. ($BATON_APP_PRIVATEKEY)
--app-privatekey string Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set. Literal \n escape sequences are also accepted and unescaped before use, since this field can't hold newlines when entered through a form. ($BATON_APP_PRIVATEKEY)
--app-privatekey-path string Path to private key that is used to connect to the GitHub App. Ignored when app-privatekey is set. ($BATON_APP_PRIVATEKEY_PATH)
--client-id string The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID)
--client-secret string The client secret used to authenticate with ConductorOne ($BATON_CLIENT_SECRET)
Expand Down
2 changes: 1 addition & 1 deletion config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
{
"name": "app-privatekey",
"displayName": "GitHub App private key (PEM)",
"description": "Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set.",
"description": "Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set. Literal \\n escape sequences are also accepted and unescaped before use, since this field can't hold newlines when entered through a form.",
"isSecret": true,
"stringField": {}
},
Expand Down
4 changes: 3 additions & 1 deletion docs/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,9 @@ If you're using a GitHub app to set up the connector:

1. Enter your app ID into the **GitHub app ID** field.

1. Click **Choose file** and upload your private key file.
1. Provide the GitHub App's private key one of two ways:
- Click **Choose file** and upload your private key (`.pem`) file, or
- Paste the key's raw PEM contents into the **GitHub App private key (PEM)** field. Because this field only accepts a single line of text, replace every line break in the PEM with a literal `\n` (backslash, then the letter `n`) before pasting it in — the connector unescapes these back into real line breaks. If both fields are filled in, the pasted **GitHub App private key (PEM)** value takes precedence.

1. In the **Organization** field, enter the name of the GitHub organization associated with the GitHub app. **You must enter a single organization name in this field or the connector configuration will fail.**

Expand Down
5 changes: 4 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ var (
appPrivateKey = field.StringField(
"app-privatekey",
field.WithDisplayName("GitHub App private key (PEM)"),
field.WithDescription("Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set."),
field.WithDescription(
"Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set. "+
`Literal \n escape sequences are also accepted and unescaped before use, since this field can't hold newlines when entered through a form.`,
),
field.WithIsSecret(true),
)

Expand Down
35 changes: 35 additions & 0 deletions pkg/connector/app_privatekey_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package connector

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"strings"
"testing"

cfg "github.com/conductorone/baton-github/pkg/config"
Expand Down Expand Up @@ -39,3 +44,33 @@ func TestAppPrivateKeyPEM(t *testing.T) {
require.Error(t, err)
})
}

func TestLoadPrivateKeyFromString(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
der := x509.MarshalPKCS1PrivateKey(key)
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: der})
realNewlines := string(pemBytes)

shapes := map[string]string{
"real newlines": realNewlines,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: This map entry is one space short of gofmt alignment — its value starts at column 46 while every other entry (including the longest key on line 61) aligns at column 47. gofmt -w fixes it; worth doing so the file doesn't trip a formatting gate in verify.

Suggested change
"real newlines": realNewlines,
"real newlines": realNewlines,

"real CRLF": strings.ReplaceAll(realNewlines, "\n", "\r\n"),
"backslash-n escaped, as the docs instruct": strings.ReplaceAll(realNewlines, "\n", `\n`),
"backslash-n escaped, trailing space": strings.ReplaceAll(realNewlines, "\n", `\n`) + " ",
"backslash-r-backslash-n escaped (CRLF file)": strings.ReplaceAll(realNewlines, "\n", `\r\n`),
"backslash-r escaped (CR-only file)": strings.ReplaceAll(realNewlines, "\n", `\r`),
}
Comment on lines +55 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: every shape here is built from a PKCS#1 RSA PRIVATE KEY block, so loadPrivateKeyFromString's PKCS#8 branch — including the key.(*rsa.PrivateKey) type assertion and the "not an RSA private key" path — stays untested even though the existing TestAppPrivateKeyPEM fixtures use PRIVATE KEY armor. Adding one pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS8PrivateKey(key)}) shape would cover both armor types the function accepts. Low confidence that this matters in practice (GitHub App keys are PKCS#1), so purely a coverage nit.


for name, shape := range shapes {
t.Run("parses a PEM with "+name, func(t *testing.T) {
got, err := loadPrivateKeyFromString(shape)
require.NoError(t, err)
require.Equal(t, key.D, got.D)
})
}

t.Run("errors on garbage input", func(t *testing.T) {
_, err := loadPrivateKeyFromString("not a pem")
require.Error(t, err)
})
}
5 changes: 5 additions & 0 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,12 @@ func newGitHubGraphqlClient(ctx context.Context, instanceURL string, ts oauth2.T
return githubv4.NewClient(tc), nil
}

// escapedLineBreaks unescapes LF-, CRLF-, and CR-escaped line breaks (`\r\n`,
// `\n`, `\r`) to a real newline.
var escapedLineBreaks = strings.NewReplacer(`\r\n`, "\n", `\n`, "\n", `\r`, "\n")

func loadPrivateKeyFromString(p string) (*rsa.PrivateKey, error) {
p = escapedLineBreaks.Replace(p)
block, _ := pem.Decode([]byte(p))
if block == nil || (block.Type != "PRIVATE KEY" && block.Type != "RSA PRIVATE KEY") {
return nil, errors.New("invalid private key PEM format")
Expand Down
Loading