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  }