Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions cmd/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ var extensionsBuildWebBotAuthCmd = &cobra.Command{
url, _ := cmd.Flags().GetString("url")
keyPath, _ := cmd.Flags().GetString("key")
uploadName, _ := cmd.Flags().GetString("upload")

signatureAgentURL, _ := cmd.Flags().GetString("signature-agent")

Choose a reason for hiding this comment

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

perhaps we should add that kernel is explicitly add to the helper that Kernel is expecting a ed25519 key. This reason being that ConvertPEMToJWK validates that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is mentioned in the --key flag

extensionsBuildWebBotAuthCmd.Flags().String("key", "", "Path to Ed25519 private key file (JWK or PEM format)")

// Use upload name for extension name, or default to "web-bot-auth"
extensionName := "web-bot-auth"
if uploadName != "" {
Expand All @@ -444,11 +444,12 @@ var extensionsBuildWebBotAuthCmd = &cobra.Command{

// Build the extension
result, err := extensions.BuildWebBotAuth(cmd.Context(), extensions.ExtensionsBuildWebBotAuthInput{
Output: output,
HostURL: url,
KeyPath: keyPath,
ExtensionName: extensionName,
AutoUpload: uploadName != "",
Output: output,
HostURL: url,
KeyPath: keyPath,
ExtensionName: extensionName,
AutoUpload: uploadName != "",
SignatureAgentURL: signatureAgentURL,
})
if err != nil {
return err
Expand Down Expand Up @@ -489,4 +490,5 @@ func init() {
extensionsBuildWebBotAuthCmd.Flags().String("url", "http://127.0.0.1:10001", "Base URL for update.xml and policy templates")
extensionsBuildWebBotAuthCmd.Flags().String("key", "", "Path to Ed25519 private key file (JWK or PEM format)")
extensionsBuildWebBotAuthCmd.Flags().String("upload", "", "Upload extension to Kernel with specified name (e.g., --upload web-bot-auth)")
extensionsBuildWebBotAuthCmd.Flags().String("signature-agent", "", "URL of the signature agent")
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: the description is vague. consider something like: "Base URL of the signature agent (e.g., https://agent.example.com). Verifiers will look up /.well-known/http-message-signatures-directory at this URL."

}
63 changes: 50 additions & 13 deletions pkg/extensions/webbotauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,23 @@ import (
)

const (
defaultLocalhostURL = "http://localhost:8000"
defaultDirMode = 0755
defaultFileMode = 0644
// Current: v0.6.0 release (e3d76846b64be03ae00e2b9e53b697beab81541d) - Dec 19, 2025
webBotAuthCommit = "e3d76846b64be03ae00e2b9e53b697beab81541d"
webBotAuthDownloadURL = "https://github.com/cloudflare/web-bot-auth/archive/" + webBotAuthCommit + ".zip"
defaultLocalhostURL = "http://localhost:8000"
defaultDirMode = 0755
defaultFileMode = 0644
webBotAuthDownloadURL = "https://github.com/kernel/web-bot-auth/archive/refs/heads/main.zip"
downloadTimeout = 5 * time.Minute
// defaultWebBotAuthKey is the RFC9421 test key that works with Cloudflare's test site
// https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/
defaultWebBotAuthKey = `{"kty":"OKP","crv":"Ed25519","d":"n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU","x":"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"}`
)

type ExtensionsBuildWebBotAuthInput struct {
Output string
HostURL string
KeyPath string // Path to user's JWK or PEM file (optional, defaults to RFC9421 test key)
ExtensionName string // Name for the extension paths (defaults to "web-bot-auth")
AutoUpload bool // Whether the extension will be automatically uploaded after building
Output string
HostURL string
KeyPath string // Path to user's JWK or PEM file (optional, defaults to RFC9421 test key)
ExtensionName string // Name for the extension paths (defaults to "web-bot-auth")
AutoUpload bool // Whether the extension will be automatically uploaded after building
SignatureAgentURL string // URL of the signature agent
}

// BuildWebBotAuthOutput contains the result of building the extension
Expand Down Expand Up @@ -100,7 +99,7 @@ func BuildWebBotAuth(ctx context.Context, in ExtensionsBuildWebBotAuthInput) (*B
}

// Build extension
extensionID, err := buildWebBotAuthExtension(ctx, browserExtDir, in.HostURL, keyData, in.ExtensionName)
extensionID, err := buildWebBotAuthExtension(ctx, browserExtDir, in.HostURL, keyData, in.ExtensionName, in.SignatureAgentURL)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -219,7 +218,7 @@ func downloadAndExtractWebBotAuth(ctx context.Context) (browserExtDir string, cl

// buildWebBotAuthExtension modifies templates, builds the extension, and returns the extension ID
// extensionName is used for URL paths (e.g., "web-bot-auth") instead of the Chrome extension ID
func buildWebBotAuthExtension(ctx context.Context, browserExtDir, hostURL, keyData, extensionName string) (string, error) {
func buildWebBotAuthExtension(ctx context.Context, browserExtDir, hostURL, keyData, extensionName, signatureAgentURL string) (string, error) {
// Normalize hostURL by removing trailing slashes to prevent double slashes in URLs
hostURL = strings.TrimRight(hostURL, "/")

Expand Down Expand Up @@ -248,6 +247,14 @@ func buildWebBotAuthExtension(ctx context.Context, browserExtDir, hostURL, keyDa
}
pterm.Success.Println("Private key written successfully")

// Inject the JWK into background.ts (replacing the hardcoded test key)
pterm.Info.Println("Injecting custom JWK into background.ts...")
backgroundTsPath := filepath.Join(browserExtDir, "src", "background.ts")
if err := injectJWKIntoBackgroundTs(backgroundTsPath, keyData); err != nil {
return "", fmt.Errorf("failed to inject JWK: %w", err)
}
pterm.Success.Println("Custom JWK injected successfully")

// Modify template files
pterm.Info.Println("Modifying templates with host URL...")

Expand Down Expand Up @@ -293,6 +300,7 @@ func buildWebBotAuthExtension(ctx context.Context, browserExtDir, hostURL, keyDa
pterm.Info.Println("Building extension...")
npmBuild := exec.CommandContext(ctx, "npm", "run", "build:chrome")
npmBuild.Dir = browserExtDir
npmBuild.Env = append(os.Environ(), "SIGNATURE_AGENT_URL="+signatureAgentURL)
npmBuild.Stdout = os.Stdout
npmBuild.Stderr = os.Stderr
if err := npmBuild.Run(); err != nil {
Expand All @@ -303,6 +311,7 @@ func buildWebBotAuthExtension(ctx context.Context, browserExtDir, hostURL, keyDa
pterm.Info.Println("Bundling extension...")
npmBundle := exec.CommandContext(ctx, "npm", "run", "bundle:chrome")
npmBundle.Dir = browserExtDir
npmBundle.Env = append(os.Environ(), "SIGNATURE_AGENT_URL="+signatureAgentURL)
var bundleOutput bytes.Buffer
npmBundle.Stdout = io.MultiWriter(os.Stdout, &bundleOutput)
npmBundle.Stderr = os.Stderr
Expand Down Expand Up @@ -347,6 +356,34 @@ func buildWebBotAuthExtension(ctx context.Context, browserExtDir, hostURL, keyDa
return extensionID, nil
}

// injectJWKIntoBackgroundTs replaces the hardcoded test key import with the custom JWK
func injectJWKIntoBackgroundTs(backgroundTsPath, jwkData string) error {
content, err := os.ReadFile(backgroundTsPath)
if err != nil {
return fmt.Errorf("failed to read background.ts: %w", err)
}

contentStr := string(content)

// Replace the import line with an inline constant
// Find: import jwk from "../../rfc9421-keys/ed25519.json" assert { type: "json" };
// Replace with: const jwk = {your-jwk-here};
searchPattern := `import jwk from "../../rfc9421-keys/ed25519.json" assert { type: "json" };`
replacement := fmt.Sprintf("const jwk = %s;", jwkData)

if !strings.Contains(contentStr, searchPattern) {
return fmt.Errorf("could not find JWK import statement in background.ts")
}

contentStr = strings.Replace(contentStr, searchPattern, replacement, 1)
Comment on lines +377 to +387

Choose a reason for hiding this comment

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

This part seems kinda flakey. We are basically writing typescript code from Go to make a one line replacement.
Perhaps in later edits to https://github.com/kernel/web-bot-auth, we can maybe templatizes this file so that using https://pkg.go.dev/text/template in go we can easily replace the jwkData


if err := os.WriteFile(backgroundTsPath, []byte(contentStr), 0644); err != nil {
return fmt.Errorf("failed to write modified background.ts: %w", err)
}

return nil
}

// copyExtensionArtifacts copies built extension files to the output directory
func copyExtensionArtifacts(browserExtDir, outputDir string) error {
pterm.Info.Println("Copying extension files to output directory...")
Expand Down