smallstep-certificates/cas/cloudcas/cloudcas.go
Mariano Cano 10f6a901ec
Let the CA determine the RA lifetime
When the RA mode with StepCAS is used, let the CA decide which lifetime
the RA should get instead of requiring always 24h.

This commit also fixes linter warnings.

Related to #1094
2024-03-12 14:29:55 -07:00

742 lines
23 KiB
Go

package cloudcas
import (
"context"
"crypto/rand"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"regexp"
"strings"
"time"
privateca "cloud.google.com/go/security/privateca/apiv1"
pb "cloud.google.com/go/security/privateca/apiv1/privatecapb"
"github.com/google/uuid"
gax "github.com/googleapis/gax-go/v2"
"github.com/pkg/errors"
"github.com/smallstep/certificates/cas/apiv1"
"go.step.sm/crypto/x509util"
"google.golang.org/api/option"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
durationpb "google.golang.org/protobuf/types/known/durationpb"
)
func init() {
apiv1.Register(apiv1.CloudCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {
return New(ctx, opts)
})
}
var now = time.Now
// The actual regular expression that matches a certificate authority is:
//
// ^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/locations/[a-z0-9-]+/caPools/[a-zA-Z0-9-_]+/certificateAuthorities/[a-zA-Z0-9-_]+$
//
// But we will allow a more flexible one to fail if this changes.
var caRegexp = regexp.MustCompile("^projects/[^/]+/locations/[^/]+/caPools/[^/]+/certificateAuthorities/[^/]+$")
// CertificateAuthorityClient is the interface implemented by the Google CAS
// client.
type CertificateAuthorityClient interface {
CreateCertificate(ctx context.Context, req *pb.CreateCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error)
RevokeCertificate(ctx context.Context, req *pb.RevokeCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error)
GetCertificateAuthority(ctx context.Context, req *pb.GetCertificateAuthorityRequest, opts ...gax.CallOption) (*pb.CertificateAuthority, error)
CreateCertificateAuthority(ctx context.Context, req *pb.CreateCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.CreateCertificateAuthorityOperation, error)
FetchCertificateAuthorityCsr(ctx context.Context, req *pb.FetchCertificateAuthorityCsrRequest, opts ...gax.CallOption) (*pb.FetchCertificateAuthorityCsrResponse, error)
ActivateCertificateAuthority(ctx context.Context, req *pb.ActivateCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.ActivateCertificateAuthorityOperation, error)
EnableCertificateAuthority(ctx context.Context, req *pb.EnableCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.EnableCertificateAuthorityOperation, error)
GetCaPool(ctx context.Context, req *pb.GetCaPoolRequest, opts ...gax.CallOption) (*pb.CaPool, error)
CreateCaPool(ctx context.Context, req *pb.CreateCaPoolRequest, opts ...gax.CallOption) (*privateca.CreateCaPoolOperation, error)
}
// recocationCodeMap maps revocation reason codes from RFC 5280, to Google CAS
// revocation reasons. Revocation reason 7 is not used, and revocation reason 8
// (removeFromCRL) is not supported by Google CAS.
var revocationCodeMap = map[int]pb.RevocationReason{
0: pb.RevocationReason_REVOCATION_REASON_UNSPECIFIED,
1: pb.RevocationReason_KEY_COMPROMISE,
2: pb.RevocationReason_CERTIFICATE_AUTHORITY_COMPROMISE,
3: pb.RevocationReason_AFFILIATION_CHANGED,
4: pb.RevocationReason_SUPERSEDED,
5: pb.RevocationReason_CESSATION_OF_OPERATION,
6: pb.RevocationReason_CERTIFICATE_HOLD,
9: pb.RevocationReason_PRIVILEGE_WITHDRAWN,
10: pb.RevocationReason_ATTRIBUTE_AUTHORITY_COMPROMISE,
}
// caPoolTierMap contains the map between apiv1.Options.Tier and the pb type.
var caPoolTierMap = map[string]pb.CaPool_Tier{
"": pb.CaPool_DEVOPS,
"ENTERPRISE": pb.CaPool_ENTERPRISE,
"DEVOPS": pb.CaPool_DEVOPS,
}
// CloudCAS implements a Certificate Authority Service using Google Cloud CAS.
type CloudCAS struct {
client CertificateAuthorityClient
certificateAuthority string
project string
location string
caPool string
caPoolTier pb.CaPool_Tier
gcsBucket string
}
// newCertificateAuthorityClient creates the certificate authority client. This
// function is used for testing purposes.
var newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) {
var cloudOpts []option.ClientOption
if credentialsFile != "" {
cloudOpts = append(cloudOpts, option.WithCredentialsFile(credentialsFile))
}
client, err := privateca.NewCertificateAuthorityClient(ctx, cloudOpts...)
if err != nil {
return nil, errors.Wrap(err, "error creating client")
}
return client, nil
}
// New creates a new CertificateAuthorityService implementation using Google
// Cloud CAS.
func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) {
var caPoolTier pb.CaPool_Tier
if opts.IsCreator && opts.CertificateAuthority == "" {
switch {
case opts.Project == "":
return nil, errors.New("cloudCAS 'project' cannot be empty")
case opts.Location == "":
return nil, errors.New("cloudCAS 'location' cannot be empty")
case opts.CaPool == "":
return nil, errors.New("cloudCAS 'caPool' cannot be empty")
}
var ok bool
if caPoolTier, ok = caPoolTierMap[strings.ToUpper(opts.CaPoolTier)]; !ok {
return nil, errors.New("cloudCAS 'caPoolTier' is not a valid tier")
}
} else {
if opts.CertificateAuthority == "" {
return nil, errors.New("cloudCAS 'certificateAuthority' cannot be empty")
}
if !caRegexp.MatchString(opts.CertificateAuthority) {
return nil, errors.New("cloudCAS 'certificateAuthority' is not valid certificate authority resource")
}
// Extract project and location from CertificateAuthority
if parts := strings.Split(opts.CertificateAuthority, "/"); len(parts) == 8 {
if opts.Project == "" {
opts.Project = parts[1]
}
if opts.Location == "" {
opts.Location = parts[3]
}
if opts.CaPool == "" {
opts.CaPool = parts[5]
}
}
}
client, err := newCertificateAuthorityClient(ctx, opts.CredentialsFile)
if err != nil {
return nil, err
}
// GCSBucket is the the bucket name or empty for a managed bucket.
return &CloudCAS{
client: client,
certificateAuthority: opts.CertificateAuthority,
project: opts.Project,
location: opts.Location,
caPool: opts.CaPool,
gcsBucket: opts.GCSBucket,
caPoolTier: caPoolTier,
}, nil
}
// Type returns the type of this CertificateAuthorityService.
func (c *CloudCAS) Type() apiv1.Type {
return apiv1.CloudCAS
}
// GetCertificateAuthority returns the root certificate for the given
// certificate authority. It implements apiv1.CertificateAuthorityGetter
// interface.
func (c *CloudCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {
name := req.Name
if name == "" {
name = c.certificateAuthority
}
ctx, cancel := defaultContext()
defer cancel()
resp, err := c.client.GetCertificateAuthority(ctx, &pb.GetCertificateAuthorityRequest{
Name: name,
})
if err != nil {
return nil, errors.Wrap(err, "cloudCAS GetCertificateAuthority failed")
}
if len(resp.PemCaCertificates) == 0 {
return nil, errors.New("cloudCAS GetCertificateAuthority: PemCACertificate should not be empty")
}
// Last certificate in the chain is the root.
root, err := parseCertificate(resp.PemCaCertificates[len(resp.PemCaCertificates)-1])
if err != nil {
return nil, err
}
return &apiv1.GetCertificateAuthorityResponse{
RootCertificate: root,
}, nil
}
// CreateCertificate signs a new certificate using Google Cloud CAS.
func (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {
switch {
case req.Template == nil:
return nil, errors.New("createCertificateRequest `template` cannot be nil")
case req.Lifetime == 0:
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
}
cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID)
if err != nil {
return nil, err
}
return &apiv1.CreateCertificateResponse{
Certificate: cert,
CertificateChain: chain,
}, nil
}
// RenewCertificate renews the given certificate using Google Cloud CAS.
// Google's CAS does not support the renew operation, so this method uses
// CreateCertificate.
func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
switch {
case req.Template == nil:
return nil, errors.New("renewCertificateRequest `template` cannot be nil")
case req.Lifetime == 0:
return nil, errors.New("renewCertificateRequest `lifetime` cannot be 0")
}
cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID)
if err != nil {
return nil, err
}
return &apiv1.RenewCertificateResponse{
Certificate: cert,
CertificateChain: chain,
}, nil
}
// RevokeCertificate revokes a certificate using Google Cloud CAS.
func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
reason, ok := revocationCodeMap[req.ReasonCode]
switch {
case !ok:
return nil, errors.Errorf("revokeCertificate 'reasonCode=%d' is invalid or not supported", req.ReasonCode)
case req.Certificate == nil:
return nil, errors.New("revokeCertificateRequest `certificate` cannot be nil")
}
ext, ok := apiv1.FindCertificateAuthorityExtension(req.Certificate)
if !ok {
return nil, errors.New("error revoking certificate: certificate authority extension was not found")
}
var cae apiv1.CertificateAuthorityExtension
if _, err := asn1.Unmarshal(ext.Value, &cae); err != nil {
return nil, errors.Wrap(err, "error unmarshaling certificate authority extension")
}
ctx, cancel := defaultContext()
defer cancel()
certpb, err := c.client.RevokeCertificate(ctx, &pb.RevokeCertificateRequest{
Name: c.certificateAuthority + "/certificates/" + cae.CertificateID,
Reason: reason,
RequestId: req.RequestID,
})
if err != nil {
return nil, errors.Wrap(err, "cloudCAS RevokeCertificate failed")
}
cert, chain, err := getCertificateAndChain(certpb)
if err != nil {
return nil, err
}
return &apiv1.RevokeCertificateResponse{
Certificate: cert,
CertificateChain: chain,
}, nil
}
// CreateCertificateAuthority creates a new root or intermediate certificate
// using Google Cloud CAS.
func (c *CloudCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) {
switch {
case c.project == "":
return nil, errors.New("cloudCAS `project` cannot be empty")
case c.location == "":
return nil, errors.New("cloudCAS `location` cannot be empty")
case c.caPool == "":
return nil, errors.New("cloudCAS `caPool` cannot be empty")
case c.caPoolTier == 0:
return nil, errors.New("cloudCAS `caPoolTier` cannot be empty")
case req.Template == nil:
return nil, errors.New("createCertificateAuthorityRequest `template` cannot be nil")
case req.Lifetime == 0:
return nil, errors.New("createCertificateAuthorityRequest `lifetime` cannot be 0")
case req.Type == apiv1.IntermediateCA && req.Parent == nil:
return nil, errors.New("createCertificateAuthorityRequest `parent` cannot be nil")
case req.Type == apiv1.IntermediateCA && req.Parent.Name == "" && (req.Parent.Certificate == nil || req.Parent.Signer == nil):
return nil, errors.New("createCertificateAuthorityRequest `parent.name` cannot be empty")
}
var caType pb.CertificateAuthority_Type
switch req.Type {
case apiv1.RootCA:
caType = pb.CertificateAuthority_SELF_SIGNED
case apiv1.IntermediateCA:
caType = pb.CertificateAuthority_SUBORDINATE
default:
return nil, errors.Errorf("createCertificateAuthorityRequest `type=%d' is invalid or not supported", req.Type)
}
// Select key and signature algorithm to use
var err error
var keySpec *pb.CertificateAuthority_KeyVersionSpec
if req.CreateKey == nil {
if keySpec, err = createKeyVersionSpec(0, 0); err != nil {
return nil, errors.Wrap(err, "createCertificateAuthorityRequest `createKey` is not valid")
}
} else {
if keySpec, err = createKeyVersionSpec(req.CreateKey.SignatureAlgorithm, req.CreateKey.Bits); err != nil {
return nil, errors.Wrap(err, "createCertificateAuthorityRequest `createKey` is not valid")
}
}
// Normalize or generate id.
caID := normalizeCertificateAuthorityName(req.Name)
if caID == "" {
id, err := createCertificateID()
if err != nil {
return nil, err
}
caID = id
}
// Add CertificateAuthority extension
casExtension, err := apiv1.CreateCertificateAuthorityExtension(apiv1.CloudCAS, caID)
if err != nil {
return nil, err
}
req.Template.ExtraExtensions = append(req.Template.ExtraExtensions, casExtension)
// Create the caPool if necessary
parent, err := c.createCaPoolIfNecessary()
if err != nil {
return nil, err
}
// Prepare CreateCertificateAuthorityRequest
pbReq := &pb.CreateCertificateAuthorityRequest{
Parent: parent,
CertificateAuthorityId: caID,
RequestId: req.RequestID,
CertificateAuthority: &pb.CertificateAuthority{
Type: caType,
Config: &pb.CertificateConfig{
SubjectConfig: &pb.CertificateConfig_SubjectConfig{
Subject: createSubject(req.Template),
SubjectAltName: createSubjectAlternativeNames(req.Template),
},
X509Config: createX509Parameters(req.Template),
},
Lifetime: durationpb.New(req.Lifetime),
KeySpec: keySpec,
GcsBucket: c.gcsBucket,
Labels: map[string]string{},
},
}
// Create certificate authority.
ctx, cancel := defaultContext()
defer cancel()
resp, err := c.client.CreateCertificateAuthority(ctx, pbReq)
if err != nil {
return nil, errors.Wrap(err, "cloudCAS CreateCertificateAuthority failed")
}
// Wait for the long-running operation.
ctx, cancel = defaultInitiatorContext()
defer cancel()
ca, err := resp.Wait(ctx)
if err != nil {
return nil, errors.Wrap(err, "cloudCAS CreateCertificateAuthority failed")
}
// Sign Intermediate CAs with the parent.
if req.Type == apiv1.IntermediateCA {
ca, err = c.signIntermediateCA(parent, ca.Name, req)
if err != nil {
return nil, err
}
}
// Enable Certificate Authority.
ca, err = c.enableCertificateAuthority(ca)
if err != nil {
return nil, err
}
if len(ca.PemCaCertificates) == 0 {
return nil, errors.New("cloudCAS CreateCertificateAuthority failed: PemCaCertificates is empty")
}
cert, err := parseCertificate(ca.PemCaCertificates[0])
if err != nil {
return nil, err
}
var chain []*x509.Certificate
if pemChain := ca.PemCaCertificates[1:]; len(pemChain) > 0 {
chain = make([]*x509.Certificate, len(pemChain))
for i, s := range pemChain {
if chain[i], err = parseCertificate(s); err != nil {
return nil, err
}
}
}
return &apiv1.CreateCertificateAuthorityResponse{
Name: ca.Name,
Certificate: cert,
CertificateChain: chain,
}, nil
}
func (c *CloudCAS) createCaPoolIfNecessary() (string, error) {
ctx, cancel := defaultContext()
defer cancel()
pool, err := c.client.GetCaPool(ctx, &pb.GetCaPoolRequest{
Name: "projects/" + c.project + "/locations/" + c.location + "/caPools/" + c.caPool,
})
if err == nil {
return pool.Name, nil
}
if status.Code(err) != codes.NotFound {
return "", errors.Wrap(err, "cloudCAS GetCaPool failed")
}
// PublishCrl is only supported by the enterprise tier
var publishCrl bool
if c.caPoolTier == pb.CaPool_ENTERPRISE {
publishCrl = true
}
ctx, cancel = defaultContext()
defer cancel()
op, err := c.client.CreateCaPool(ctx, &pb.CreateCaPoolRequest{
Parent: "projects/" + c.project + "/locations/" + c.location,
CaPoolId: c.caPool,
CaPool: &pb.CaPool{
Tier: c.caPoolTier,
IssuancePolicy: nil,
PublishingOptions: &pb.CaPool_PublishingOptions{
PublishCaCert: true,
PublishCrl: publishCrl,
},
},
})
if err != nil {
return "", errors.Wrap(err, "cloudCAS CreateCaPool failed")
}
ctx, cancel = defaultInitiatorContext()
defer cancel()
pool, err = op.Wait(ctx)
if err != nil {
return "", errors.Wrap(err, "cloudCAS CreateCaPool failed")
}
return pool.Name, nil
}
func (c *CloudCAS) enableCertificateAuthority(ca *pb.CertificateAuthority) (*pb.CertificateAuthority, error) {
if ca.State == pb.CertificateAuthority_ENABLED {
return ca, nil
}
ctx, cancel := defaultContext()
defer cancel()
resp, err := c.client.EnableCertificateAuthority(ctx, &pb.EnableCertificateAuthorityRequest{
Name: ca.Name,
})
if err != nil {
return nil, errors.Wrap(err, "cloudCAS EnableCertificateAuthority failed")
}
ctx, cancel = defaultInitiatorContext()
defer cancel()
ca, err = resp.Wait(ctx)
if err != nil {
return nil, errors.Wrap(err, "cloudCAS EnableCertificateAuthority failed")
}
return ca, nil
}
func (c *CloudCAS) createCertificate(tpl *x509.Certificate, lifetime time.Duration, requestID string) (*x509.Certificate, []*x509.Certificate, error) {
// Removes the CAS extension if it exists.
apiv1.RemoveCertificateAuthorityExtension(tpl)
// Create new CAS extension with the certificate id.
id, err := createCertificateID()
if err != nil {
return nil, nil, err
}
casExtension, err := apiv1.CreateCertificateAuthorityExtension(apiv1.CloudCAS, id)
if err != nil {
return nil, nil, err
}
tpl.ExtraExtensions = append(tpl.ExtraExtensions, casExtension)
// Create and submit certificate
certConfig, err := createCertificateConfig(tpl)
if err != nil {
return nil, nil, err
}
ctx, cancel := defaultContext()
defer cancel()
cert, err := c.client.CreateCertificate(ctx, &pb.CreateCertificateRequest{
Parent: "projects/" + c.project + "/locations/" + c.location + "/caPools/" + c.caPool,
CertificateId: id,
Certificate: &pb.Certificate{
CertificateConfig: certConfig,
Lifetime: durationpb.New(lifetime),
Labels: map[string]string{},
},
IssuingCertificateAuthorityId: getResourceName(c.certificateAuthority),
RequestId: requestID,
})
if err != nil {
return nil, nil, errors.Wrap(err, "cloudCAS CreateCertificate failed")
}
// Return certificate and certificate chain
return getCertificateAndChain(cert)
}
func (c *CloudCAS) signIntermediateCA(parent, name string, req *apiv1.CreateCertificateAuthorityRequest) (*pb.CertificateAuthority, error) {
id, err := createCertificateID()
if err != nil {
return nil, err
}
// Fetch intermediate CSR
ctx, cancel := defaultInitiatorContext()
defer cancel()
csr, err := c.client.FetchCertificateAuthorityCsr(ctx, &pb.FetchCertificateAuthorityCsrRequest{
Name: name,
})
if err != nil {
return nil, errors.Wrap(err, "cloudCAS FetchCertificateAuthorityCsr failed")
}
// Sign the CSR with the ca.
var cert *pb.Certificate
if req.Parent.Certificate != nil && req.Parent.Signer != nil {
// Using a local certificate and key.
cr, err := parseCertificateRequest(csr.PemCsr)
if err != nil {
return nil, err
}
template, err := x509util.CreateCertificateTemplate(cr)
if err != nil {
return nil, err
}
t := now()
template.NotBefore = t.Add(-1 * req.Backdate)
template.NotAfter = t.Add(req.Lifetime)
// Sign certificate
crt, err := x509util.CreateCertificate(template, req.Parent.Certificate, template.PublicKey, req.Parent.Signer)
if err != nil {
return nil, err
}
// Build pb.Certificate for activaion
chain := []string{
encodeCertificate(req.Parent.Certificate),
}
for _, c := range req.Parent.CertificateChain {
chain = append(chain, encodeCertificate(c))
}
cert = &pb.Certificate{
PemCertificate: encodeCertificate(crt),
PemCertificateChain: chain,
}
} else {
// Using the parent in CloudCAS.
ctx, cancel = defaultInitiatorContext()
defer cancel()
cert, err = c.client.CreateCertificate(ctx, &pb.CreateCertificateRequest{
Parent: parent,
CertificateId: id,
Certificate: &pb.Certificate{
CertificateConfig: &pb.Certificate_PemCsr{
PemCsr: csr.PemCsr,
},
Lifetime: durationpb.New(req.Lifetime),
Labels: map[string]string{},
},
IssuingCertificateAuthorityId: getResourceName(req.Parent.Name),
RequestId: req.RequestID,
})
if err != nil {
return nil, errors.Wrap(err, "cloudCAS CreateCertificate failed")
}
}
// Activate the intermediate certificate.
ctx, cancel = defaultInitiatorContext()
defer cancel()
resp, err := c.client.ActivateCertificateAuthority(ctx, &pb.ActivateCertificateAuthorityRequest{
Name: name,
PemCaCertificate: cert.PemCertificate,
SubordinateConfig: &pb.SubordinateConfig{
SubordinateConfig: &pb.SubordinateConfig_PemIssuerChain{
PemIssuerChain: &pb.SubordinateConfig_SubordinateConfigChain{
PemCertificates: cert.PemCertificateChain,
},
},
},
RequestId: req.RequestID,
})
if err != nil {
return nil, errors.Wrap(err, "cloudCAS ActivateCertificateAuthority1 failed")
}
// Wait for the long-running operation.
ctx, cancel = defaultInitiatorContext()
defer cancel()
ca, err := resp.Wait(ctx)
if err != nil {
return nil, errors.Wrap(err, "cloudCAS ActivateCertificateAuthority failed")
}
return ca, nil
}
func defaultContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 15*time.Second)
}
func defaultInitiatorContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 60*time.Second)
}
func createCertificateID() (string, error) {
id, err := uuid.NewRandomFromReader(rand.Reader)
if err != nil {
return "", errors.Wrap(err, "error creating certificate id")
}
return id.String(), nil
}
func parseCertificate(pemCert string) (*x509.Certificate, error) {
block, _ := pem.Decode([]byte(pemCert))
if block == nil {
return nil, errors.New("error decoding certificate: not a valid PEM encoded block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing certificate")
}
return cert, nil
}
func parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) {
block, _ := pem.Decode([]byte(pemCsr))
if block == nil {
return nil, errors.New("error decoding certificate request: not a valid PEM encoded block")
}
cr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing certificate request")
}
return cr, nil
}
func encodeCertificate(cert *x509.Certificate) string {
return string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}))
}
func getCertificateAndChain(certpb *pb.Certificate) (*x509.Certificate, []*x509.Certificate, error) {
cert, err := parseCertificate(certpb.PemCertificate)
if err != nil {
return nil, nil, err
}
pemChain := certpb.PemCertificateChain[:len(certpb.PemCertificateChain)-1]
chain := make([]*x509.Certificate, len(pemChain))
for i := range pemChain {
chain[i], err = parseCertificate(pemChain[i])
if err != nil {
return nil, nil, err
}
}
return cert, chain, nil
}
// getResourceName returns the last part of a resource.
func getResourceName(name string) string {
parts := strings.Split(name, "/")
return parts[len(parts)-1]
}
// Normalize a certificate authority name to comply with [a-zA-Z0-9-_].
func normalizeCertificateAuthorityName(name string) string {
return strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
return r
case r >= 'A' && r <= 'Z':
return r
case r >= '0' && r <= '9':
return r
case r == '-':
return r
case r == '_':
return r
default:
return '-'
}
}, name)
}