| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Command genbotcert can both generate a CSR and private key for a LUCI bot |
| // and generate a certificate from a CSR. It accepts two arguments, the |
| // bot hostname, and the path to the CSR. If it only receives the hostname then |
| // it writes the PEM-encoded CSR to the current working directory along with |
| // a private key. If it receives both the hostname and CSR path then it |
| // validates that the hostname is what is what is expected in the CSR and |
| // generates a certificate. The certificate is written to the current working |
| // directory. |
| package main |
| |
| import ( |
| "context" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/asn1" |
| "encoding/pem" |
| "flag" |
| "fmt" |
| "log" |
| "os" |
| "time" |
| |
| privateca "cloud.google.com/go/security/privateca/apiv1" |
| "cloud.google.com/go/security/privateca/apiv1/privatecapb" |
| "github.com/google/go-cmp/cmp" |
| "golang.org/x/build/buildenv" |
| "google.golang.org/protobuf/types/known/durationpb" |
| ) |
| |
| func main() { |
| var ( |
| botHostname = flag.String("bot-hostname", "", "Hostname for the bot (required)") |
| csrPath = flag.String("csr-path", "", "Path to the certificate signing request (required for certificate)") |
| ) |
| flag.Usage = func() { |
| fmt.Fprintln(os.Stderr, "Usage: genbotcert -bot-hostname <bot-hostname> [-csr-path <csr-path>]") |
| flag.PrintDefaults() |
| } |
| flag.Parse() |
| if *botHostname == "" { |
| flag.Usage() |
| os.Exit(2) |
| } |
| ctx := context.Background() |
| var err error |
| if *csrPath == "" { |
| err = createCSRAndKey(ctx, *botHostname) |
| } else { |
| err = generateCert(ctx, *csrPath, *botHostname) |
| } |
| if err != nil { |
| log.Fatalln(err) |
| } |
| } |
| |
| func createCSRAndKey(_ context.Context, cn string) error { |
| key, err := rsa.GenerateKey(rand.Reader, 4096) |
| if err != nil { |
| return err |
| } |
| |
| privPem := pem.EncodeToMemory( |
| &pem.Block{ |
| Type: "RSA PRIVATE KEY", |
| Bytes: x509.MarshalPKCS1PrivateKey(key), |
| }, |
| ) |
| if err := os.WriteFile(cn+".key", privPem, 0600); err != nil { |
| return err |
| } |
| |
| subj := pkix.Name{ |
| CommonName: cn + ".bots.golang.org", |
| Organization: []string{"Google"}, |
| } |
| |
| template := x509.CertificateRequest{ |
| Subject: subj, |
| DNSNames: []string{subj.CommonName}, |
| SignatureAlgorithm: x509.SHA256WithRSA, |
| } |
| |
| csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, key) |
| if err != nil { |
| return err |
| } |
| csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) |
| if err := os.WriteFile(cn+".csr", csrPem, 0600); err != nil { |
| return err |
| } |
| |
| fmt.Printf("Wrote CSR to %s.csr and key to %[1]s.key\n", cn) |
| return nil |
| } |
| |
| func generateCert(ctx context.Context, csrPath, hostname string) error { |
| csr, err := readAndCheckCSR(csrPath, hostname) |
| if err != nil { |
| return err |
| } |
| certID := fmt.Sprintf("%s-%d", hostname, time.Now().Unix()) // A unique name for the certificate. |
| caClient, err := privateca.NewCertificateAuthorityClient(ctx) |
| if err != nil { |
| return fmt.Errorf("NewCertificateAuthorityClient creation failed: %v", err) |
| } |
| defer caClient.Close() |
| fullCaPoolName := fmt.Sprintf("projects/%s/locations/%s/caPools/%s", buildenv.LUCIProduction.ProjectName, "us-central1", "default-pool") |
| // Create the CreateCertificateRequest. |
| // See https://pkg.go.dev/cloud.google.com/go/security/privateca/apiv1/privatecapb#CreateCertificateRequest. |
| req := &privatecapb.CreateCertificateRequest{ |
| Parent: fullCaPoolName, |
| CertificateId: certID, |
| Certificate: &privatecapb.Certificate{ |
| CertificateConfig: &privatecapb.Certificate_PemCsr{ |
| PemCsr: string(csr), |
| }, |
| Lifetime: &durationpb.Duration{ |
| Seconds: 315360000, // Seconds in 10 years. |
| }, |
| }, |
| IssuingCertificateAuthorityId: "luci-bot-ca", // The name of the certificate authority which issues the certificate. |
| } |
| resp, err := caClient.CreateCertificate(ctx, req) |
| if err != nil { |
| return fmt.Errorf("CreateCertificate failed: %v", err) |
| } |
| log.Printf("Certificate %s created", certID) |
| if err := os.WriteFile(certID+".cert", []byte(resp.PemCertificate), 0600); err != nil { |
| return fmt.Errorf("unable to write certificate to disk: %s", err) |
| } |
| fmt.Printf("Wrote certificate to %s.cert\n", certID) |
| return nil |
| } |
| |
| // readAndCheckCSR reads a .csr file located at csrPath, |
| // checks that it was generated by genbotcert for the right hostname, |
| // and returns the .csr file bytes if so. |
| func readAndCheckCSR(csrPath, hostname string) (csr []byte, _ error) { |
| csr, err := os.ReadFile(csrPath) |
| if err != nil { |
| return nil, fmt.Errorf("unable to read file %q: %s", csrPath, err) |
| } |
| |
| // Check that the CSR was generated by genbotcert |
| // with the right hostname. |
| pb, _ := pem.Decode(csr) |
| cr, err := x509.ParseCertificateRequest(pb.Bytes) |
| if err != nil { |
| return nil, fmt.Errorf("unable to parse certificate request: %v", err) |
| } |
| if err := cr.CheckSignature(); err != nil { |
| return nil, fmt.Errorf("error checking certificate request signature: %v", err) |
| } |
| want := x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: hostname + ".bots.golang.org", |
| Organization: []string{"Google"}, |
| Names: []pkix.AttributeTypeAndValue{ |
| {Type: asn1.ObjectIdentifier{2, 5, 4, 10}, Value: "Google"}, |
| {Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: hostname + ".bots.golang.org"}, |
| }, |
| }, |
| DNSNames: []string{hostname + ".bots.golang.org"}, |
| SignatureAlgorithm: x509.SHA256WithRSA, |
| } |
| if diff := cmp.Diff(want.Subject, cr.Subject); diff != "" { |
| return nil, fmt.Errorf("certificate request Subject mismatch (-want +got):\n%s", diff) |
| } |
| if diff := cmp.Diff(want.DNSNames, cr.DNSNames); diff != "" { |
| return nil, fmt.Errorf("certificate request DNSNames mismatch (-want +got):\n%s", diff) |
| } |
| if diff := cmp.Diff(want.SignatureAlgorithm, cr.SignatureAlgorithm); diff != "" { |
| return nil, fmt.Errorf("certificate request SignatureAlgorithm mismatch (-want +got):\n%s", diff) |
| } |
| |
| return csr, nil |
| } |