734 lines
23 KiB
Go
734 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"
|
|
"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"
|
|
pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1"
|
|
"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
|
|
}
|
|
|
|
// 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)
|
|
}
|