@@ -23,61 +23,91 @@ const (
2323 defaultFileMode = 0644
2424 webBotAuthDownloadURL = "https://github.com/cloudflare/web-bot-auth/archive/refs/heads/main.zip"
2525 downloadTimeout = 5 * time .Minute
26+ // defaultWebBotAuthKey is the RFC9421 test key that works with Cloudflare's test site
27+ // https://developers.cloudflare.com/bots/reference/bot-verification/web-bot-auth/
28+ defaultWebBotAuthKey = `{"kty":"OKP","crv":"Ed25519","d":"n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU","x":"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"}`
2629)
2730
28- type ExtensionsPrepareWebBotAuthInput struct {
31+ type ExtensionsBuildWebBotAuthInput struct {
2932 Output string
3033 HostURL string
34+ KeyPath string // Path to user's JWK file (optional, defaults to RFC9421 test key)
3135}
3236
33- func PrepareWebBotAuth (ctx context.Context , in ExtensionsPrepareWebBotAuthInput ) error {
37+ // BuildWebBotAuthOutput contains the result of building the extension
38+ type BuildWebBotAuthOutput struct {
39+ ExtensionID string
40+ OutputDir string
41+ }
42+
43+ func BuildWebBotAuth (ctx context.Context , in ExtensionsBuildWebBotAuthInput ) (* BuildWebBotAuthOutput , error ) {
3444 pterm .Info .Println ("Preparing web-bot-auth extension..." )
3545
3646 // Validate preconditions
3747 if err := validateToolDependencies (); err != nil {
38- return err
48+ return nil , err
3949 }
4050
4151 outputDir , err := filepath .Abs (in .Output )
4252 if err != nil {
43- return fmt .Errorf ("failed to resolve output path: %w" , err )
53+ return nil , fmt .Errorf ("failed to resolve output path: %w" , err )
4454 }
4555 if st , err := os .Stat (outputDir ); err == nil {
4656 if ! st .IsDir () {
47- return fmt .Errorf ("output path exists and is not a directory: %s" , outputDir )
57+ return nil , fmt .Errorf ("output path exists and is not a directory: %s" , outputDir )
4858 }
4959 entries , _ := os .ReadDir (outputDir )
5060 if len (entries ) > 0 {
51- return fmt .Errorf ("output directory must be empty: %s" , outputDir )
61+ return nil , fmt .Errorf ("output directory must be empty: %s" , outputDir )
5262 }
5363 } else {
5464 if err := os .MkdirAll (outputDir , defaultDirMode ); err != nil {
55- return fmt .Errorf ("failed to create output directory: %w" , err )
65+ return nil , fmt .Errorf ("failed to create output directory: %w" , err )
5666 }
5767 }
5868
5969 // Download and extract
6070 browserExtDir , cleanup , err := downloadAndExtractWebBotAuth (ctx )
6171 defer cleanup ()
6272 if err != nil {
63- return err
73+ return nil , err
74+ }
75+
76+ // Load key (custom or default)
77+ var jwkData string
78+ var usingDefaultKey bool
79+ if in .KeyPath != "" {
80+ pterm .Info .Printf ("Loading custom JWK from %s...\n " , in .KeyPath )
81+ keyBytes , err := os .ReadFile (in .KeyPath )
82+ if err != nil {
83+ return nil , fmt .Errorf ("failed to read key file: %w" , err )
84+ }
85+ jwkData = string (keyBytes )
86+ usingDefaultKey = false
87+ } else {
88+ pterm .Info .Println ("Using default RFC9421 test key (works with Cloudflare test site)..." )
89+ jwkData = defaultWebBotAuthKey
90+ usingDefaultKey = true
6491 }
6592
6693 // Build extension
67- extensionID , err := buildWebBotAuthExtension (ctx , browserExtDir , in .HostURL )
94+ extensionID , err := buildWebBotAuthExtension (ctx , browserExtDir , in .HostURL , jwkData )
6895 if err != nil {
69- return err
96+ return nil , err
7097 }
7198
7299 // Copy artifacts
73100 if err := copyExtensionArtifacts (browserExtDir , outputDir ); err != nil {
74- return err
101+ return nil , err
75102 }
76103
77104 // Display success message
78- displayWebBotAuthSuccess (outputDir , extensionID , in .HostURL )
105+ displayWebBotAuthSuccess (outputDir , extensionID , in .HostURL , usingDefaultKey )
79106
80- return nil
107+ return & BuildWebBotAuthOutput {
108+ ExtensionID : extensionID ,
109+ OutputDir : outputDir ,
110+ }, nil
81111}
82112
83113// extractExtensionID extracts the extension ID from npm bundle output
@@ -90,6 +120,7 @@ func extractExtensionID(output string) string {
90120 return ""
91121}
92122
123+
93124// validateToolDependencies checks for required tools (node and npm)
94125func validateToolDependencies () error {
95126 if _ , err := exec .LookPath ("node" ); err != nil {
@@ -179,10 +210,23 @@ func downloadAndExtractWebBotAuth(ctx context.Context) (browserExtDir string, cl
179210}
180211
181212// buildWebBotAuthExtension modifies templates, builds the extension, and returns the extension ID
182- func buildWebBotAuthExtension (ctx context.Context , browserExtDir , hostURL string ) (string , error ) {
213+ func buildWebBotAuthExtension (ctx context.Context , browserExtDir , hostURL , jwkData string ) (string , error ) {
183214 // Normalize hostURL by removing trailing slashes to prevent double slashes in URLs
184215 hostURL = strings .TrimRight (hostURL , "/" )
185216
217+ // Convert JWK to PEM and write to browserExtDir before building
218+ pterm .Info .Println ("Converting JWK to PEM format..." )
219+ pemData , err := util .JWKToPEM (jwkData )
220+ if err != nil {
221+ return "" , fmt .Errorf ("failed to convert JWK to PEM: %w" , err )
222+ }
223+
224+ privateKeyPath := filepath .Join (browserExtDir , "private_key.pem" )
225+ if err := os .WriteFile (privateKeyPath , pemData , 0600 ); err != nil {
226+ return "" , fmt .Errorf ("failed to write private key: %w" , err )
227+ }
228+ pterm .Success .Println ("Private key written successfully" )
229+
186230 // Modify template files
187231 pterm .Info .Println ("Modifying templates with host URL..." )
188232
@@ -337,27 +381,53 @@ func copyExtensionArtifacts(browserExtDir, outputDir string) error {
337381 pterm .Warning .Println ("No private_key.pem found - extension ID may change on rebuild" )
338382 }
339383
384+ // Copy policy directory (contains Chrome enterprise policy configuration)
385+ policySrc := filepath .Join (browserExtDir , "policy" )
386+ policyDst := filepath .Join (outputDir , "policy" )
387+ if _ , err := os .Stat (policySrc ); err == nil {
388+ if err := util .CopyDir (policySrc , policyDst ); err != nil {
389+ return fmt .Errorf ("failed to copy policy directory: %w" , err )
390+ }
391+ pterm .Info .Println ("Policy files copied (required for Chrome configuration)" )
392+ }
393+
340394 return nil
341395}
342396
343397// displayWebBotAuthSuccess displays success message and next steps
344- func displayWebBotAuthSuccess (outputDir , extensionID , hostURL string ) {
398+ func displayWebBotAuthSuccess (outputDir , extensionID , hostURL string , usingDefaultKey bool ) {
345399 pterm .Success .Println ("Web-bot-auth extension prepared successfully!" )
346400 pterm .Println ()
347401
348402 rows := pterm.TableData {{"Property" , "Value" }}
349403 rows = append (rows , []string {"Extension ID" , extensionID })
350404 rows = append (rows , []string {"Output directory" , outputDir })
351405 rows = append (rows , []string {"Host URL" , hostURL })
406+ if usingDefaultKey {
407+ rows = append (rows , []string {"Signing Key" , "RFC9421 test key (Cloudflare test site)" })
408+ } else {
409+ rows = append (rows , []string {"Signing Key" , "Custom JWK" })
410+ }
352411 table .PrintTableNoPad (rows , true )
353412
354413 pterm .Println ()
355414 pterm .Info .Println ("Next steps:" )
356415 pterm .Printf ("1. Upload using the extension ID as the name:\n " )
357416 pterm .Printf (" kernel extensions upload %s --name %s\n \n " , outputDir , extensionID )
358- pterm .Printf ("2. Use in your browser, or upload to a session:\n " )
359- pterm .Printf (" kernel browsers create --extension %s\n " , extensionID )
360- pterm .Printf (" or run kernel browsers extensions upload <session-id> %s\n \n " , outputDir )
361- pterm .Warning .Println ("⚠️ Private key saved to private_key.pem - keep it secure!" )
362- pterm .Info .Println (" It's automatically excluded when uploading via .gitignore" )
417+ pterm .Printf ("2. Use in your browser:\n " )
418+ pterm .Printf (" kernel browsers create --extension %s\n \n " , extensionID )
419+
420+ pterm .Println ()
421+ pterm .Info .Println (" For testing with Cloudflare's test site:" )
422+ pterm .Printf (" • Test URL: https://http-message-signatures-example.research.cloudflare.com\n " )
423+ pterm .Printf (" • Or: https://webbotauth.io/test\n " )
424+ pterm .Println ()
425+
426+ if usingDefaultKey {
427+ pterm .Info .Println ("Using default RFC9421 test key - compatible with Cloudflare test sites" )
428+ } else {
429+ pterm .Warning .Println ("⚠️ Private key saved to private_key.pem - keep it secure!" )
430+ pterm .Info .Println (" It's automatically excluded when uploading via .gitignore" )
431+ }
432+
363433}
0 commit comments