blob: b2495147f0a6ec1bc16e27264c022e8d0dd30abf [file] [log] [blame]
// 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/pem"
"flag"
"fmt"
"log"
"os"
"strings"
"time"
privateca "cloud.google.com/go/security/privateca/apiv1"
"cloud.google.com/go/security/privateca/apiv1/privatecapb"
"golang.org/x/build/buildenv"
"google.golang.org/protobuf/types/known/durationpb"
)
var (
csrPath = flag.String("csr-path", "", "Path to the certificate signing request (required for certificate)")
botHostname = flag.String("bot-hostname", "", "Hostname for the bot (required)")
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "Usage: genbotcert -bot-hostname <bot-hostname>")
flag.PrintDefaults()
}
flag.Parse()
if *botHostname == "" {
flag.Usage()
os.Exit(2)
}
ctx := context.Background()
var err error
if *csrPath == "" {
err = doMain(ctx, *botHostname)
} else {
err = generateCert(ctx, *botHostname, *csrPath)
}
if err != nil {
log.Fatalln(err)
}
}
func doMain(ctx 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 %v.csr and key to %v.key\n", cn, cn)
return nil
}
func generateCert(ctx context.Context, hostname, csrPath string) error {
csr, err := os.ReadFile(csrPath)
if err != nil {
return fmt.Errorf("unable to read file %q: %s", csrPath, err)
}
// validate hostname
pb, _ := pem.Decode(csr)
cr, err := x509.ParseCertificateRequest(pb.Bytes)
if err != nil {
return fmt.Errorf("unable to parse certificate request: %w", err)
}
if cr.Subject.CommonName != fmt.Sprintf("%s.bots.golang.org", hostname) {
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"))
}
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: %w", 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: %w", 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
}