golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/genbotcert/genbotcert.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Command genbotcert can both generate a CSR and private key for a LUCI bot 6 // and generate a certificate from a CSR. It accepts two arguments, the 7 // bot hostname, and the path to the CSR. If it only receives the hostname then 8 // it writes the PEM-encoded CSR to the current working directory along with 9 // a private key. If it receives both the hostname and CSR path then it 10 // validates that the hostname is what is what is expected in the CSR and 11 // generates a certificate. The certificate is written to the current working 12 // directory. 13 package main 14 15 import ( 16 "context" 17 "crypto/rand" 18 "crypto/rsa" 19 "crypto/x509" 20 "crypto/x509/pkix" 21 "encoding/pem" 22 "flag" 23 "fmt" 24 "log" 25 "os" 26 "strings" 27 "time" 28 29 privateca "cloud.google.com/go/security/privateca/apiv1" 30 "cloud.google.com/go/security/privateca/apiv1/privatecapb" 31 "golang.org/x/build/buildenv" 32 "google.golang.org/protobuf/types/known/durationpb" 33 ) 34 35 var ( 36 csrPath = flag.String("csr-path", "", "Path to the certificate signing request (required for certificate)") 37 botHostname = flag.String("bot-hostname", "", "Hostname for the bot (required)") 38 ) 39 40 func main() { 41 flag.Usage = func() { 42 fmt.Fprintln(os.Stderr, "Usage: genbotcert -bot-hostname <bot-hostname>") 43 flag.PrintDefaults() 44 } 45 flag.Parse() 46 if *botHostname == "" { 47 flag.Usage() 48 os.Exit(2) 49 } 50 ctx := context.Background() 51 var err error 52 if *csrPath == "" { 53 err = doMain(ctx, *botHostname) 54 } else { 55 err = generateCert(ctx, *botHostname, *csrPath) 56 } 57 if err != nil { 58 log.Fatalln(err) 59 } 60 } 61 62 func doMain(ctx context.Context, cn string) error { 63 key, err := rsa.GenerateKey(rand.Reader, 4096) 64 if err != nil { 65 return err 66 } 67 68 privPem := pem.EncodeToMemory( 69 &pem.Block{ 70 Type: "RSA PRIVATE KEY", 71 Bytes: x509.MarshalPKCS1PrivateKey(key), 72 }, 73 ) 74 if err := os.WriteFile(cn+".key", privPem, 0600); err != nil { 75 return err 76 } 77 78 subj := pkix.Name{ 79 CommonName: cn + ".bots.golang.org", 80 Organization: []string{"Google"}, 81 } 82 83 template := x509.CertificateRequest{ 84 Subject: subj, 85 DNSNames: []string{subj.CommonName}, 86 SignatureAlgorithm: x509.SHA256WithRSA, 87 } 88 89 csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, key) 90 if err != nil { 91 return err 92 } 93 csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) 94 if err := os.WriteFile(cn+".csr", csrPem, 0600); err != nil { 95 return err 96 } 97 98 fmt.Printf("Wrote CSR to %v.csr and key to %v.key\n", cn, cn) 99 return nil 100 } 101 102 func generateCert(ctx context.Context, hostname, csrPath string) error { 103 csr, err := os.ReadFile(csrPath) 104 if err != nil { 105 return fmt.Errorf("unable to read file %q: %s", csrPath, err) 106 } 107 // validate hostname 108 pb, _ := pem.Decode(csr) 109 cr, err := x509.ParseCertificateRequest(pb.Bytes) 110 if err != nil { 111 return fmt.Errorf("unable to parse certificate request: %w", err) 112 } 113 if cr.Subject.CommonName != fmt.Sprintf("%s.bots.golang.org", hostname) { 114 return fmt.Errorf("certificate signing request hostname does not match the expected hostname: expected %q, csr hostname: %q", hostname, strings.TrimSuffix(cr.Subject.CommonName, ".bots.golang.org")) 115 } 116 certID := fmt.Sprintf("%s-%d", hostname, time.Now().Unix()) // A unique name for the certificate. 117 caClient, err := privateca.NewCertificateAuthorityClient(ctx) 118 if err != nil { 119 return fmt.Errorf("NewCertificateAuthorityClient creation failed: %w", err) 120 } 121 defer caClient.Close() 122 fullCaPoolName := fmt.Sprintf("projects/%s/locations/%s/caPools/%s", buildenv.LUCIProduction.ProjectName, "us-central1", "default-pool") 123 // Create the CreateCertificateRequest. 124 // See https://pkg.go.dev/cloud.google.com/go/security/privateca/apiv1/privatecapb#CreateCertificateRequest. 125 req := &privatecapb.CreateCertificateRequest{ 126 Parent: fullCaPoolName, 127 CertificateId: certID, 128 Certificate: &privatecapb.Certificate{ 129 CertificateConfig: &privatecapb.Certificate_PemCsr{ 130 PemCsr: string(csr), 131 }, 132 Lifetime: &durationpb.Duration{ 133 Seconds: 315360000, // Seconds in 10 years. 134 }, 135 }, 136 IssuingCertificateAuthorityId: "luci-bot-ca", // The name of the certificate authority which issues the certificate. 137 } 138 resp, err := caClient.CreateCertificate(ctx, req) 139 if err != nil { 140 return fmt.Errorf("CreateCertificate failed: %w", err) 141 } 142 log.Printf("Certificate %s created", certID) 143 if err := os.WriteFile(certID+".cert", []byte(resp.PemCertificate), 0600); err != nil { 144 return fmt.Errorf("unable to write certificate to disk: %s", err) 145 } 146 fmt.Printf("Wrote certificate to %s.cert\n", certID) 147 return nil 148 }