From 1b1f73dec6e19dd04f6bd38cdf06d7ca499aad20 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 8 Sep 2020 19:26:32 -0700 Subject: [PATCH 01/29] Early attempt to develop a CAS interface. --- authority/authority.go | 16 +++ authority/config.go | 7 + authority/tls.go | 19 ++- cas/apiv1/options.go | 32 +++++ cas/apiv1/registry.go | 27 ++++ cas/apiv1/requests.go | 26 ++++ cas/apiv1/services.go | 22 +++ cas/cas.go | 32 +++++ cas/cloudcas/certificate.go | 268 ++++++++++++++++++++++++++++++++++++ cas/cloudcas/cloudcas.go | 110 +++++++++++++++ cas/softcas/softcas.go | 50 +++++++ cmd/step-ca/main.go | 4 + go.mod | 11 +- go.sum | 211 ++++++++++++++++++++++++++++ 14 files changed, 827 insertions(+), 8 deletions(-) create mode 100644 cas/apiv1/options.go create mode 100644 cas/apiv1/registry.go create mode 100644 cas/apiv1/requests.go create mode 100644 cas/apiv1/services.go create mode 100644 cas/cas.go create mode 100644 cas/cloudcas/certificate.go create mode 100644 cas/cloudcas/cloudcas.go create mode 100644 cas/softcas/softcas.go diff --git a/authority/authority.go b/authority/authority.go index a0a80b62..d7a450d4 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -10,8 +10,11 @@ import ( "sync" "time" + "github.com/smallstep/certificates/cas" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/kms" kmsapi "github.com/smallstep/certificates/kms/apiv1" @@ -33,6 +36,7 @@ type Authority struct { templates *templates.Templates // X509 CA + x509CAService cas.CertificateAuthorityService rootX509Certs []*x509.Certificate federatedX509Certs []*x509.Certificate x509Signer crypto.Signer @@ -144,6 +148,18 @@ func (a *Authority) init() error { } } + // Initialize the X.509 CA Service if it has not been set in the options + if a.x509CAService == nil { + var options casapi.Options + if a.config.CAS != nil { + options = *a.config.CAS + } + a.x509CAService, err = cas.New(context.Background(), options) + if err != nil { + return nil + } + } + // Initialize step-ca Database if it's not already initialized with WithDB. // If a.config.DB is nil then a simple, barebones in memory DB will be used. if a.db == nil { diff --git a/authority/config.go b/authority/config.go index 1d49f9a1..1f8374f8 100644 --- a/authority/config.go +++ b/authority/config.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + cas "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" kms "github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/certificates/templates" @@ -54,6 +55,7 @@ type Config struct { Address string `json:"address"` DNSNames []string `json:"dnsNames"` KMS *kms.Options `json:"kms,omitempty"` + CAS *cas.Options `json:"cas,omitempty"` SSH *SSHConfig `json:"ssh,omitempty"` Logger json.RawMessage `json:"logger,omitempty"` DB *db.Config `json:"db,omitempty"` @@ -220,6 +222,11 @@ func (c *Config) Validate() error { return err } + // Validate CAS options, nil is ok. + if err := c.CAS.Validate(); err != nil { + return err + } + // Validate ssh: nil is ok if err := c.SSH.Validate(); err != nil { return err diff --git a/authority/tls.go b/authority/tls.go index a5474d60..cc290839 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" @@ -144,11 +145,23 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } } - serverCert, err := x509util.CreateCertificate(leaf, a.x509Issuer, csr.PublicKey, a.x509Signer) + lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(-1 * signOpts.Backdate)) + resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ + Template: leaf, + Issuer: a.x509Issuer, + Signer: a.x509Signer, + Lifetime: lifetime, + }) if err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, - "authority.Sign; error creating certificate", opts...) + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...) } + serverCert := resp.Certificate + + // serverCert, err := x509util.CreateCertificate(leaf, a.x509Issuer, csr.PublicKey, a.x509Signer) + // if err != nil { + // return nil, errs.Wrap(http.StatusInternalServerError, err, + // "authority.Sign; error creating certificate", opts...) + // } if err = a.db.StoreCertificate(serverCert); err != nil { if err != db.ErrNotImplemented { diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go new file mode 100644 index 00000000..b3da527c --- /dev/null +++ b/cas/apiv1/options.go @@ -0,0 +1,32 @@ +package apiv1 + +import ( + "strings" + + "github.com/pkg/errors" +) + +// Options represents the configuration options used to select and configure the +// CertificateAuthorityService (CAS) to use. +type Options struct { + // The type of the CAS to use. + Type string `json:"type"` + + // Path to the credentials file used in CloudCAS + CredentialsFile string `json:"credentialsFile"` +} + +// Validate checks the fields in Options. +func (o *Options) Validate() error { + if o == nil { + return nil + } + + switch Type(strings.ToLower(o.Type)) { + case DefaultCAS, SoftCAS, CloudCAS: + default: + return errors.Errorf("unsupported kms type %s", o.Type) + } + + return nil +} diff --git a/cas/apiv1/registry.go b/cas/apiv1/registry.go new file mode 100644 index 00000000..9c4c96ee --- /dev/null +++ b/cas/apiv1/registry.go @@ -0,0 +1,27 @@ +package apiv1 + +import ( + "context" + "sync" +) + +var registry = new(sync.Map) + +// CertificateAuthorityServiceNewFunc is the type that represents the method to initialize a new +// CertificateAuthorityService. +type CertificateAuthorityServiceNewFunc func(ctx context.Context, opts Options) (CertificateAuthorityService, error) + +// Register adds to the registry a method to create a KeyManager of type t. +func Register(t Type, fn CertificateAuthorityServiceNewFunc) { + registry.Store(t, fn) +} + +// LoadCertificateAuthorityServiceNewFunc returns the function initialize a KayManager. +func LoadCertificateAuthorityServiceNewFunc(t Type) (CertificateAuthorityServiceNewFunc, bool) { + v, ok := registry.Load(t) + if !ok { + return nil, false + } + fn, ok := v.(CertificateAuthorityServiceNewFunc) + return fn, ok +} diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go new file mode 100644 index 00000000..b3d12a08 --- /dev/null +++ b/cas/apiv1/requests.go @@ -0,0 +1,26 @@ +package apiv1 + +import ( + "crypto" + "crypto/x509" + "time" +) + +type CreateCertificateRequest struct { + Template *x509.Certificate + Issuer *x509.Certificate + Signer crypto.Signer + Lifetime time.Duration + + RequestID string +} +type CreateCertificateResponse struct { + Certificate *x509.Certificate + CertificateChain []*x509.Certificate +} + +type RenewCertificateRequest struct{} +type RenewCertificateResponse struct{} + +type RevokeCertificateRequest struct{} +type RevokeCertificateResponse struct{} diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go new file mode 100644 index 00000000..08660d3a --- /dev/null +++ b/cas/apiv1/services.go @@ -0,0 +1,22 @@ +package apiv1 + +// CertificateAuthorityService is the interface implemented to support external +// certificate authorities. +type CertificateAuthorityService interface { + CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) + RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error) + RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) +} + +// Type represents the KMS type used. +type Type string + +// +const ( + // DefaultCAS is a CertificateAuthorityService using software. + DefaultCAS = "" + // SoftCAS is a CertificateAuthorityService using software. + SoftCAS = "softcas" + // CloudCAS is a CertificateAuthorityService using Google Cloud CAS. + CloudCAS = "cloudcas" +) diff --git a/cas/cas.go b/cas/cas.go new file mode 100644 index 00000000..3df83460 --- /dev/null +++ b/cas/cas.go @@ -0,0 +1,32 @@ +package cas + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/cas/apiv1" + + // Enable default implementation + _ "github.com/smallstep/certificates/cas/softcas" +) + +// CertificateAuthorityService is the interface implemented by all the CAS. +type CertificateAuthorityService = apiv1.CertificateAuthorityService + +func New(ctx context.Context, opts apiv1.Options) (CertificateAuthorityService, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + + t := apiv1.Type(strings.ToLower(opts.Type)) + if t == apiv1.DefaultCAS { + t = apiv1.SoftCAS + } + + fn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(t) + if !ok { + return nil, errors.Errorf("unsupported kms type '%s'", t) + } + return fn(ctx, opts) +} diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go new file mode 100644 index 00000000..5c7f5bd0 --- /dev/null +++ b/cas/cloudcas/certificate.go @@ -0,0 +1,268 @@ +package cloudcas + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + + "github.com/pkg/errors" + pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" +) + +var ( + oidExtensionSubjectAltName = []int{2, 5, 29, 17} +) + +var ( + oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0} + oidExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} + oidExtKeyUsageClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} + oidExtKeyUsageCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3} + oidExtKeyUsageEmailProtection = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4} + oidExtKeyUsageIPSECEndSystem = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5} + oidExtKeyUsageIPSECTunnel = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6} + oidExtKeyUsageIPSECUser = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7} + oidExtKeyUsageTimeStamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} + oidExtKeyUsageOCSPSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9} + oidExtKeyUsageMicrosoftServerGatedCrypto = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3} + oidExtKeyUsageNetscapeServerGatedCrypto = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1} + oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22} + oidExtKeyUsageMicrosoftKernelCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1} +) + +const ( + nameTypeEmail = 1 + nameTypeDNS = 2 + nameTypeURI = 6 + nameTypeIP = 7 +) + +func createCertificateConfig(tpl *x509.Certificate) (*pb.Certificate_Config, error) { + pk, err := createPublicKey(tpl.PublicKey) + if err != nil { + return nil, err + } + + config := &pb.CertificateConfig{ + SubjectConfig: &pb.CertificateConfig_SubjectConfig{ + Subject: createSubject(tpl), + CommonName: tpl.Subject.CommonName, + SubjectAltName: createSubjectAlternativeNames(tpl), + }, + ReusableConfig: createReusableConfig(tpl), + PublicKey: pk, + } + return &pb.Certificate_Config{ + Config: config, + }, nil +} + +func createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) { + pk := new(pb.PublicKey) + switch key := key.(type) { + case *ecdsa.PublicKey: + asn1Bytes, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return nil, errors.Wrap(err, "error marshaling public key") + } + return &pb.PublicKey{ + Type: pb.PublicKey_PEM_RSA_KEY, + Key: pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: asn1Bytes, + }), + }, nil + case *rsa.PublicKey: + return &pb.PublicKey{ + Type: pb.PublicKey_PEM_RSA_KEY, + Key: pem.EncodeToMemory(&pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(key), + }), + }, nil + default: + return nil, errors.Errorf("unsupported public key type: %T", key) + } + + return pk, nil +} + +func createSubject(cert *x509.Certificate) *pb.Subject { + sub := cert.Subject + ret := new(pb.Subject) + if len(sub.Country) > 0 { + ret.CountryCode = sub.Country[0] + } + if len(sub.Organization) > 0 { + ret.Organization = sub.Organization[0] + } + if len(sub.OrganizationalUnit) > 0 { + ret.OrganizationalUnit = sub.OrganizationalUnit[0] + } + if len(sub.Locality) > 0 { + ret.Locality = sub.Locality[0] + } + if len(sub.Province) > 0 { + ret.Province = sub.Province[0] + } + if len(sub.StreetAddress) > 0 { + ret.StreetAddress = sub.StreetAddress[0] + } + if len(sub.PostalCode) > 0 { + ret.PostalCode = sub.PostalCode[0] + } + return ret +} + +func createSubjectAlternativeNames(cert *x509.Certificate) *pb.SubjectAltNames { + ret := new(pb.SubjectAltNames) + ret.DnsNames = cert.DNSNames + ret.EmailAddresses = cert.EmailAddresses + if n := len(cert.IPAddresses); n > 0 { + ret.IpAddresses = make([]string, n) + for i, ip := range cert.IPAddresses { + ret.IpAddresses[i] = ip.String() + } + } + if n := len(cert.URIs); n > 0 { + ret.Uris = make([]string, n) + for i, u := range cert.URIs { + ret.Uris[i] = u.String() + } + } + + // Add extra SANs coming from the extensions + if ext, ok := findExtraExtension(cert, oidExtensionSubjectAltName); ok { + ret.CustomSans = []*pb.X509Extension{{ + ObjectId: createObjectID(ext.Id), + Critical: ext.Critical, + Value: ext.Value, + }} + } + return ret +} + +func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper { + var unknownEKUs []*pb.ObjectId + var ekuOptions = &pb.KeyUsage_ExtendedKeyUsageOptions{} + for _, eku := range cert.ExtKeyUsage { + switch eku { + case x509.ExtKeyUsageAny: + unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageAny)) + case x509.ExtKeyUsageServerAuth: + ekuOptions.ServerAuth = true + case x509.ExtKeyUsageClientAuth: + ekuOptions.ClientAuth = true + case x509.ExtKeyUsageCodeSigning: + ekuOptions.CodeSigning = true + case x509.ExtKeyUsageEmailProtection: + ekuOptions.EmailProtection = true + case x509.ExtKeyUsageIPSECEndSystem: + unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECEndSystem)) + case x509.ExtKeyUsageIPSECTunnel: + unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECTunnel)) + case x509.ExtKeyUsageIPSECUser: + unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECUser)) + case x509.ExtKeyUsageTimeStamping: + ekuOptions.TimeStamping = true + case x509.ExtKeyUsageOCSPSigning: + ekuOptions.OcspSigning = true + case x509.ExtKeyUsageMicrosoftServerGatedCrypto: + unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftServerGatedCrypto)) + case x509.ExtKeyUsageNetscapeServerGatedCrypto: + unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageNetscapeServerGatedCrypto)) + case x509.ExtKeyUsageMicrosoftCommercialCodeSigning: + unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftCommercialCodeSigning)) + case x509.ExtKeyUsageMicrosoftKernelCodeSigning: + unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftKernelCodeSigning)) + } + } + + for _, oid := range cert.UnknownExtKeyUsage { + unknownEKUs = append(unknownEKUs, createObjectID(oid)) + } + + policyIDs := make([]*pb.ObjectId, len(cert.PolicyIdentifiers)) + for i, oid := range cert.PolicyIdentifiers { + policyIDs[i] = createObjectID(oid) + } + + var caOptions *pb.ReusableConfigValues_CaOptions + if cert.BasicConstraintsValid { + var maxPathLength *wrapperspb.Int32Value + switch { + case cert.MaxPathLenZero: + maxPathLength = wrapperspb.Int32(0) + case cert.MaxPathLen > 0: + maxPathLength = wrapperspb.Int32(int32(cert.MaxPathLen)) + default: + maxPathLength = nil + } + + caOptions = &pb.ReusableConfigValues_CaOptions{ + IsCa: wrapperspb.Bool(cert.IsCA), + MaxIssuerPathLength: maxPathLength, + } + } + + extraExtensions := make([]*pb.X509Extension, len(cert.ExtraExtensions)) + for i, ext := range cert.ExtraExtensions { + extraExtensions[i] = &pb.X509Extension{ + ObjectId: createObjectID(ext.Id), + Critical: ext.Critical, + Value: ext.Value, + } + } + + values := &pb.ReusableConfigValues{ + KeyUsage: &pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + DigitalSignature: cert.KeyUsage&x509.KeyUsageDigitalSignature == 1, + ContentCommitment: cert.KeyUsage&x509.KeyUsageContentCommitment == 1, + KeyEncipherment: cert.KeyUsage&x509.KeyUsageKeyEncipherment == 1, + DataEncipherment: cert.KeyUsage&x509.KeyUsageDataEncipherment == 1, + KeyAgreement: cert.KeyUsage&x509.KeyUsageKeyAgreement == 1, + CertSign: cert.KeyUsage&x509.KeyUsageCertSign == 1, + CrlSign: cert.KeyUsage&x509.KeyUsageCRLSign == 1, + EncipherOnly: cert.KeyUsage&x509.KeyUsageEncipherOnly == 1, + DecipherOnly: cert.KeyUsage&x509.KeyUsageDecipherOnly == 1, + }, + ExtendedKeyUsage: ekuOptions, + UnknownExtendedKeyUsages: unknownEKUs, + }, + CaOptions: caOptions, + PolicyIds: policyIDs, + AiaOcspServers: cert.OCSPServer, + AdditionalExtensions: extraExtensions, + } + + return &pb.ReusableConfigWrapper{ + ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{ + ReusableConfigValues: values, + }, + } +} + +func createObjectID(oid asn1.ObjectIdentifier) *pb.ObjectId { + ret := make([]int32, len(oid)) + for i, v := range oid { + ret[i] = int32(v) + } + return &pb.ObjectId{ + ObjectIdPath: ret, + } +} + +func findExtraExtension(cert *x509.Certificate, oid asn1.ObjectIdentifier) (pkix.Extension, bool) { + for _, ext := range cert.ExtraExtensions { + if ext.Id.Equal(oid) { + return ext, true + } + } + return pkix.Extension{}, false +} diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go new file mode 100644 index 00000000..6dcd0c2f --- /dev/null +++ b/cas/cloudcas/cloudcas.go @@ -0,0 +1,110 @@ +package cloudcas + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + privateca "cloud.google.com/go/security/privateca/apiv1beta1" + "github.com/pkg/errors" + "github.com/smallstep/certificates/cas/apiv1" + privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" + 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) + }) +} + +// CloudCAS implements a Certificate Authority Service using Google Cloud CAS. +type CloudCAS struct { + client *privateca.CertificateAuthorityClient + certificateAuthority string +} + +type caClient interface{} + +// New creates a new CertificateAuthorityService implementation using Google +// Cloud CAS. +func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { + client, err := privateca.NewCertificateAuthorityClient(ctx) + if err != nil { + return nil, errors.Wrap(err, "error creating client") + } + + return &CloudCAS{ + client: client, + certificateAuthority: "", + }, 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") + } + + certConfig, err := createCertificateConfig(req.Template) + if err != nil { + return nil, err + } + + ctx, cancel := defaultContext() + defer cancel() + + certpb, err := c.client.CreateCertificate(ctx, &privatecapb.CreateCertificateRequest{ + Parent: c.certificateAuthority, + CertificateId: "", + Certificate: &privatecapb.Certificate{ + CertificateConfig: certConfig, + Lifetime: durationpb.New(req.Lifetime), + Labels: map[string]string{}, + }, + RequestId: req.RequestID, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudCAS CreateCertificate failed") + } + + cert, err := parseCertificate(certpb.PemCertificate) + if err != nil { + return nil, err + } + + return &apiv1.CreateCertificateResponse{ + Certificate: cert, + }, nil +} + +func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// RevokeCertificate a certificate using Google Cloud CAS. +func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { + + return nil, fmt.Errorf("not implemented") +} + +func defaultContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 15*time.Second) +} + +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 +} diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go new file mode 100644 index 00000000..b7e5ddfc --- /dev/null +++ b/cas/softcas/softcas.go @@ -0,0 +1,50 @@ +package softcas + +import ( + "context" + "crypto/x509" + "fmt" + + "github.com/smallstep/certificates/cas/apiv1" + "go.step.sm/crypto/x509util" +) + +func init() { + apiv1.Register(apiv1.SoftCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) { + return New(ctx, opts) + }) +} + +// SoftCAS implements a Certificate Authority Service using Golang crypto. +// This is the default CAS used in step-ca. +type SoftCAS struct{} + +// New creates a new CertificateAuthorityService implementation using Golang +// crypto. +func New(ctx context.Context, opts apiv1.Options) (*SoftCAS, error) { + return &SoftCAS{}, nil +} + +// CreateCertificate signs a new certificate using Golang crypto. +func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { + cert, err := x509util.CreateCertificate(req.Template, req.Issuer, req.Template.PublicKey, req.Signer) + if err != nil { + return nil, err + } + + return &apiv1.CreateCertificateResponse{ + Certificate: cert, + CertificateChain: []*x509.Certificate{ + req.Issuer, + }, + }, nil +} + +func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { + return nil, fmt.Errorf("not implemented") +} + +// RevokeCertificate revokes the given certificate in step-ca. +func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { + return nil, fmt.Errorf("not implemented") +} diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 2151d80f..87297f83 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -31,6 +31,10 @@ import ( // Experimental kms interfaces. _ "github.com/smallstep/certificates/kms/yubikey" + + // Enabled cas interfaces. + _ "github.com/smallstep/certificates/cas/cloudcas" + _ "github.com/smallstep/certificates/cas/softcas" ) // commit and buildTime are filled in during build by the Makefile diff --git a/go.mod b/go.mod index a71d716c..4b7fc282 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/smallstep/certificates go 1.14 require ( - cloud.google.com/go v0.51.0 + cloud.google.com/go v0.65.1-0.20200904011802-3c2db50b5678 github.com/Masterminds/sprig/v3 v3.1.0 github.com/aws/aws-sdk-go v1.30.29 github.com/go-chi/chi v4.0.2+incompatible @@ -21,10 +21,11 @@ require ( github.com/urfave/cli v1.22.2 go.step.sm/crypto v0.6.0 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de - golang.org/x/net v0.0.0-20200202094626-16171245cfb2 - google.golang.org/api v0.15.0 - google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb - google.golang.org/grpc v1.26.0 + golang.org/x/net v0.0.0-20200822124328-c89045814202 + google.golang.org/api v0.31.0 + google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d + google.golang.org/grpc v1.31.1 + google.golang.org/protobuf v1.25.0 gopkg.in/square/go-jose.v2 v2.5.1 ) diff --git a/go.sum b/go.sum index 8b390caa..d07b0546 100644 --- a/go.sum +++ b/go.sum @@ -5,12 +5,36 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM= cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.65.1-0.20200904011802-3c2db50b5678 h1:5YqZUrIf2QELwPqw1kLpGIE0z0I++b7HhzSNKjZlIY0= +cloud.google.com/go v0.65.1-0.20200904011802-3c2db50b5678/go.mod h1:Ihp2NV3Qr9BWHCDNA8LXF9fZ1HGBl6Jx1xd7KP3nxkI= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -68,6 +92,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -105,7 +130,9 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -119,7 +146,9 @@ github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= github.com/go-critic/go-critic v0.4.0 h1:sXD3pix0wDemuPuSlrXpJNNYXlUiKiysLrtPVQmxkzI= github.com/go-critic/go-critic v0.4.0/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= @@ -168,17 +197,34 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= @@ -227,14 +273,25 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/monologue v0.0.0-20190606152607-4b11a32b5934/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk= github.com/google/monologue v0.0.0-20191220140058-35abc9683a6c h1:0L/piDwninh6sjZ+vCZI7c6RA0R71ET8v1yinZzC9k8= github.com/google/monologue v0.0.0-20191220140058-35abc9683a6c/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/trillian v1.2.2-0.20190612132142-05461f4df60a/go.mod h1:YPmUVn5NGwgnDUgqlVyFGMTgaWlnSvH7W5p+NdOG8UA= github.com/google/trillian-examples v0.0.0-20190603134952-4e75ba15216c/go.mod h1:WgL3XZ3pA8/9cm7yxqWrZE6iZkESB2ItGxy5Fo6k2lk= @@ -285,6 +342,7 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= @@ -526,6 +584,10 @@ github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= @@ -540,6 +602,9 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.step.sm/crypto v0.0.0-20200805202904-ec18b6df3cf0 h1:FymMl8TrXGxFf80BWpO0CnkSfLnw0BkDdRrhbMGf5zE= go.step.sm/crypto v0.0.0-20200805202904-ec18b6df3cf0/go.mod h1:8VYxmvSKt5yOTBx3MGsD2Gk4F1Es/3FIxrjnfeYWE8U= go.step.sm/crypto v0.1.1 h1:xg3kUS30hEnwgbxtKwq9a4MJaeiU616HSug60LU9B2E= @@ -566,6 +631,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= @@ -574,7 +640,12 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -585,11 +656,18 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -608,23 +686,42 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -643,18 +740,38 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e h1:LwyF2AFISC9nVbS6MgzsaQNSUsRXI49GS+YQ5KX/QH0= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4 h1:kCCpuwSAoYJPkNc6x0xT9yTtV4oKtARo4RGBQWOfg9E= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -692,27 +809,72 @@ golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113232020-e2727e816f5a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200106190116-7be0a674c9fc/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200903185744-af4cc2cd812e h1:RvNtqusJ+6DJ07/by/M84a6/Dd17XU6n8QvhvknjJno= +golang.org/x/tools v0.0.0-20200903185744-af4cc2cd812e/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.31.0 h1:1w5Sz/puhxFo9lTtip2n47k7toB/U2nCqOKNHd3Yrbo= +google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -723,16 +885,61 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb h1:ADPHZzpzM4tk4V4S5cnCrr5SwzvlrPRmqqCuJDB8UTs= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -764,6 +971,8 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= @@ -771,6 +980,8 @@ mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskX mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= mvdan.cc/unparam v0.0.0-20191111180625-960b1ec0f2c2/go.mod h1:rCqoQrfAmpTX/h2APczwM7UymU/uvaOluiVPIYCSY/k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4= From c8d9cb0a1deeb0c4970fcab4fedcec5688ad5c7f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 10 Sep 2020 16:19:18 -0700 Subject: [PATCH 02/29] Complete cloudcas using CAS v1beta1. --- cas/apiv1/extension.go | 57 ++++++++++++ cas/apiv1/options.go | 8 ++ cas/apiv1/requests.go | 34 +++++-- cas/apiv1/services.go | 28 +++++- cas/cloudcas/certificate.go | 58 +++++++++--- cas/cloudcas/cloudcas.go | 175 ++++++++++++++++++++++++++++++------ go.mod | 9 +- go.sum | 5 ++ 8 files changed, 318 insertions(+), 56 deletions(-) create mode 100644 cas/apiv1/extension.go diff --git a/cas/apiv1/extension.go b/cas/apiv1/extension.go new file mode 100644 index 00000000..66da15a0 --- /dev/null +++ b/cas/apiv1/extension.go @@ -0,0 +1,57 @@ +package apiv1 + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + + "github.com/pkg/errors" +) + +// CertificateAuthorityExtension is type used to encode the certificate +// authority extension. +type CertificateAuthorityExtension struct { + Type string + CertificateID string `asn1:"optional,omitempty"` + KeyValuePairs []string `asn1:"optional,omitempty"` +} + +// CreateCertificateAuthorityExtension returns a X.509 extension that shows the +// CAS type, id and a list of optional key value pairs. +func CreateCertificateAuthorityExtension(typ Type, certificateID string, keyValuePairs ...string) (pkix.Extension, error) { + b, err := asn1.Marshal(CertificateAuthorityExtension{ + Type: typ.String(), + CertificateID: certificateID, + KeyValuePairs: keyValuePairs, + }) + if err != nil { + return pkix.Extension{}, errors.Wrapf(err, "error marshaling certificate id extension") + } + return pkix.Extension{ + Id: oidStepCertificateAuthority, + Critical: false, + Value: b, + }, nil +} + +// FindCertificateAuthorityExtension returns the certificate authority extension +// from a signed certificate. +func FindCertificateAuthorityExtension(cert *x509.Certificate) (pkix.Extension, bool) { + for _, ext := range cert.Extensions { + if ext.Id.Equal(oidStepCertificateAuthority) { + return ext, true + } + } + return pkix.Extension{}, false +} + +// RemoveCertificateAuthorityExtension removes the certificate authority +// extension from a certificate template. +func RemoveCertificateAuthorityExtension(cert *x509.Certificate) { + for i, ext := range cert.ExtraExtensions { + if ext.Id.Equal(oidStepCertificateAuthority) { + cert.ExtraExtensions = append(cert.ExtraExtensions[:i], cert.ExtraExtensions[i+1:]...) + return + } + } +} diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index b3da527c..7118472e 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -30,3 +30,11 @@ func (o *Options) Validate() error { return nil } + +// HasType returns if the options have the given type. +func (o *Options) HasType(t Type) bool { + if o == nil { + return SoftCAS == t.String() + } + return Type(o.Type).String() == t.String() +} diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index b3d12a08..bc6770be 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -7,11 +7,11 @@ import ( ) type CreateCertificateRequest struct { - Template *x509.Certificate - Issuer *x509.Certificate - Signer crypto.Signer - Lifetime time.Duration - + Template *x509.Certificate + Issuer *x509.Certificate + Signer crypto.Signer + Lifetime time.Duration + Backdate time.Duration RequestID string } type CreateCertificateResponse struct { @@ -19,8 +19,24 @@ type CreateCertificateResponse struct { CertificateChain []*x509.Certificate } -type RenewCertificateRequest struct{} -type RenewCertificateResponse struct{} +type RenewCertificateRequest struct { + Template *x509.Certificate + Issuer *x509.Certificate + Signer crypto.Signer + Lifetime time.Duration + Backdate time.Duration + RequestID string +} +type RenewCertificateResponse struct { + Certificate *x509.Certificate + CertificateChain []*x509.Certificate +} -type RevokeCertificateRequest struct{} -type RevokeCertificateResponse struct{} +// RevokeCertificateRequest is the request used to revoke a certificate. +type RevokeCertificateRequest struct { + Certificate *x509.Certificate +} +type RevokeCertificateResponse struct { + Certificate *x509.Certificate + CertificateChain []*x509.Certificate +} diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 08660d3a..f3bc6b16 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -1,5 +1,15 @@ package apiv1 +import ( + "encoding/asn1" + "strings" +) + +var ( + oidStepRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} + oidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...) +) + // CertificateAuthorityService is the interface implemented to support external // certificate authorities. type CertificateAuthorityService interface { @@ -11,12 +21,24 @@ type CertificateAuthorityService interface { // Type represents the KMS type used. type Type string -// const ( // DefaultCAS is a CertificateAuthorityService using software. DefaultCAS = "" // SoftCAS is a CertificateAuthorityService using software. - SoftCAS = "softcas" + SoftCAS = "SoftCAS" // CloudCAS is a CertificateAuthorityService using Google Cloud CAS. - CloudCAS = "cloudcas" + CloudCAS = "CloudCAS" ) + +// String returns the given type as a string. All the letters will be lowercase. +func (t Type) String() string { + if t == "" { + return SoftCAS + } + for _, s := range []string{SoftCAS, CloudCAS} { + if strings.EqualFold(s, string(t)) { + return s + } + } + return string(t) +} diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go index 5c7f5bd0..31da2c7d 100644 --- a/cas/cloudcas/certificate.go +++ b/cas/cloudcas/certificate.go @@ -63,7 +63,6 @@ func createCertificateConfig(tpl *x509.Certificate) (*pb.Certificate_Config, err } func createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) { - pk := new(pb.PublicKey) switch key := key.(type) { case *ecdsa.PublicKey: asn1Bytes, err := x509.MarshalPKIXPublicKey(key) @@ -88,8 +87,6 @@ func createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) { default: return nil, errors.Errorf("unsupported public key type: %T", key) } - - return pk, nil } func createSubject(cert *x509.Certificate) *pb.Subject { @@ -138,12 +135,43 @@ func createSubjectAlternativeNames(cert *x509.Certificate) *pb.SubjectAltNames { // Add extra SANs coming from the extensions if ext, ok := findExtraExtension(cert, oidExtensionSubjectAltName); ok { - ret.CustomSans = []*pb.X509Extension{{ - ObjectId: createObjectID(ext.Id), - Critical: ext.Critical, - Value: ext.Value, - }} + var rawValues []asn1.RawValue + if _, err := asn1.Unmarshal(ext.Value, &rawValues); err == nil { + var newValues []asn1.RawValue + for _, v := range rawValues { + switch v.Tag { + case nameTypeDNS: + if len(ret.DnsNames) == 0 { + newValues = append(newValues, v) + } + case nameTypeEmail: + if len(ret.EmailAddresses) == 0 { + newValues = append(newValues, v) + } + case nameTypeIP: + if len(ret.IpAddresses) == 0 { + newValues = append(newValues, v) + } + case nameTypeURI: + if len(ret.Uris) == 0 { + newValues = append(newValues, v) + } + default: + newValues = append(newValues, v) + } + } + if len(newValues) > 0 { + if b, err := asn1.Marshal(newValues); err == nil { + ret.CustomSans = []*pb.X509Extension{{ + ObjectId: createObjectID(ext.Id), + Critical: ext.Critical, + Value: b, + }} + } + } + } } + return ret } @@ -210,12 +238,14 @@ func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper { } } - extraExtensions := make([]*pb.X509Extension, len(cert.ExtraExtensions)) - for i, ext := range cert.ExtraExtensions { - extraExtensions[i] = &pb.X509Extension{ - ObjectId: createObjectID(ext.Id), - Critical: ext.Critical, - Value: ext.Value, + var extraExtensions []*pb.X509Extension + for _, ext := range cert.ExtraExtensions { + if !ext.Id.Equal(oidExtensionSubjectAltName) { + extraExtensions = append(extraExtensions, &pb.X509Extension{ + ObjectId: createObjectID(ext.Id), + Critical: ext.Critical, + Value: ext.Value, + }) } } diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 6dcd0c2f..52608658 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -3,14 +3,18 @@ package cloudcas import ( "context" "crypto/x509" + "encoding/asn1" + "encoding/json" "encoding/pem" "fmt" "time" privateca "cloud.google.com/go/security/privateca/apiv1beta1" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" - privatecapb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" + "google.golang.org/api/option" + pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" durationpb "google.golang.org/protobuf/types/known/durationpb" ) @@ -20,6 +24,16 @@ func init() { }) } +func debug(v interface{}) { + b, _ := json.MarshalIndent(v, "", " ") + fmt.Println(string(b)) +} + +var ( + stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} + stepOIDCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 2)...) +) + // CloudCAS implements a Certificate Authority Service using Google Cloud CAS. type CloudCAS struct { client *privateca.CertificateAuthorityClient @@ -31,14 +45,19 @@ type caClient interface{} // New creates a new CertificateAuthorityService implementation using Google // Cloud CAS. func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { - client, err := privateca.NewCertificateAuthorityClient(ctx) + var cloudOpts []option.ClientOption + if opts.CredentialsFile != "" { + cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile)) + } + + client, err := privateca.NewCertificateAuthorityClient(ctx, cloudOpts...) if err != nil { return nil, errors.Wrap(err, "error creating client") } return &CloudCAS{ client: client, - certificateAuthority: "", + certificateAuthority: "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/Smallstep-Test-Intermediate-CA", }, nil } @@ -51,52 +70,131 @@ func (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv return nil, errors.New("createCertificateRequest `lifetime` cannot be 0") } - certConfig, err := createCertificateConfig(req.Template) - if err != nil { - return nil, err - } - - ctx, cancel := defaultContext() - defer cancel() - - certpb, err := c.client.CreateCertificate(ctx, &privatecapb.CreateCertificateRequest{ - Parent: c.certificateAuthority, - CertificateId: "", - Certificate: &privatecapb.Certificate{ - CertificateConfig: certConfig, - Lifetime: durationpb.New(req.Lifetime), - Labels: map[string]string{}, - }, - RequestId: req.RequestID, - }) - if err != nil { - return nil, errors.Wrap(err, "cloudCAS CreateCertificate failed") - } - - cert, err := parseCertificate(certpb.PemCertificate) + cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID) if err != nil { return nil, err } return &apiv1.CreateCertificateResponse{ - Certificate: cert, + 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) { - return nil, fmt.Errorf("not implemented") + switch { + case req.Template == nil: + return nil, errors.New("renewCertificate `template` cannot be nil") + case req.Lifetime == 0: + return nil, errors.New("renewCertificate `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 a certificate using Google Cloud CAS. func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { + if req.Certificate == nil { + return nil, errors.New("revokeCertificate `certificate` cannot be nil") + } - return nil, fmt.Errorf("not implemented") + 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, &ext); 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: pb.RevocationReason_REVOCATION_REASON_UNSPECIFIED, + }) + 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 +} + +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: c.certificateAuthority, + CertificateId: id, + Certificate: &pb.Certificate{ + CertificateConfig: certConfig, + Lifetime: durationpb.New(lifetime), + Labels: map[string]string{}, + }, + RequestId: requestID, + }) + if err != nil { + return nil, nil, errors.Wrap(err, "cloudCAS CreateCertificate failed") + } + + // Return certificate and certificate chain + return getCertificateAndChain(cert) } func defaultContext() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 15*time.Second) } +func createCertificateID() (string, error) { + id, err := uuid.NewRandom() + 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 { @@ -108,3 +206,22 @@ func parseCertificate(pemCert string) (*x509.Certificate, error) { } return cert, nil } + +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 + +} diff --git a/go.mod b/go.mod index 4b7fc282..f489d6d7 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.14 require ( cloud.google.com/go v0.65.1-0.20200904011802-3c2db50b5678 + github.com/Masterminds/sprig/v3 v3.1.0 github.com/aws/aws-sdk-go v1.30.29 github.com/go-chi/chi v4.0.2+incompatible github.com/go-piv/piv-go v1.5.0 + github.com/google/uuid v1.1.2 github.com/googleapis/gax-go/v2 v2.0.5 github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect @@ -24,11 +26,16 @@ require ( golang.org/x/net v0.0.0-20200822124328-c89045814202 google.golang.org/api v0.31.0 google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d - google.golang.org/grpc v1.31.1 + google.golang.org/grpc v1.32.0 google.golang.org/protobuf v1.25.0 gopkg.in/square/go-jose.v2 v2.5.1 + // cloud.google.com/go/security/privateca/apiv1alpha1 v0.0.0 + // google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1 v0.0.0 ) // replace github.com/smallstep/cli => ../cli // replace github.com/smallstep/nosql => ../nosql // replace go.step.sm/crypto => ../crypto + +// replace cloud.google.com/go/security/privateca/apiv1alpha1 => ./pkg/cloud.google.com/go/security/privateca/apiv1alpha1 +// replace google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1 => ./pkg/google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1 diff --git a/go.sum b/go.sum index d07b0546..60b99c45 100644 --- a/go.sum +++ b/go.sum @@ -298,6 +298,8 @@ github.com/google/trillian-examples v0.0.0-20190603134952-4e75ba15216c/go.mod h1 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -911,6 +913,7 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200910191746-8ad3c7ee2cd1 h1:Oi/dETbxPPblvoi4hgkzJun62A4dwuBsTM0UcZYpN3U= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -929,6 +932,8 @@ google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From aad8f9e5827849203c86061afc88680f727fe3d6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 10 Sep 2020 19:09:46 -0700 Subject: [PATCH 03/29] Pass issuer and signer to softCAS options. Remove commented code and initialize CAS properly. Minor fixes in CloudCAS. --- authority/authority.go | 64 ++++++++++++++++++++++----------- authority/config.go | 2 +- authority/tls.go | 67 ++++++++++++++++++---------------- cas/apiv1/extension.go | 5 +++ cas/apiv1/options.go | 28 ++++++++++----- cas/apiv1/registry.go | 8 +++-- cas/apiv1/requests.go | 5 --- cas/apiv1/services.go | 23 ++++-------- cas/cloudcas/cloudcas.go | 12 ++++--- cas/softcas/softcas.go | 78 +++++++++++++++++++++++++++++++++------- 10 files changed, 191 insertions(+), 101 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index d7a450d4..0fc7a71a 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -148,18 +148,6 @@ func (a *Authority) init() error { } } - // Initialize the X.509 CA Service if it has not been set in the options - if a.x509CAService == nil { - var options casapi.Options - if a.config.CAS != nil { - options = *a.config.CAS - } - a.x509CAService, err = cas.New(context.Background(), options) - if err != nil { - return nil - } - } - // Initialize step-ca Database if it's not already initialized with WithDB. // If a.config.DB is nil then a simple, barebones in memory DB will be used. if a.db == nil { @@ -206,15 +194,51 @@ func (a *Authority) init() error { if err != nil { return err } - signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ - SigningKey: a.config.IntermediateKey, - Password: []byte(a.config.Password), - }) - if err != nil { - return err - } - a.x509Signer = signer a.x509Issuer = crt + + // Read signer only is the CAS is the default one. + if a.config.CAS.HasType(casapi.SoftCAS) { + signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: []byte(a.config.Password), + }) + if err != nil { + return err + } + a.x509Signer = signer + } + } + + // Initialize the X.509 CA Service if it has not been set in the options + if a.x509CAService == nil { + var options casapi.Options + if a.config.CAS != nil { + options = *a.config.CAS + } + + // Set issuer and signer for default CAS. + if options.HasType(casapi.SoftCAS) { + crt, err := pemutil.ReadCertificate(a.config.IntermediateCert) + if err != nil { + return err + } + + signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: []byte(a.config.Password), + }) + if err != nil { + return err + } + + options.Issuer = crt + options.Signer = signer + } + + a.x509CAService, err = cas.New(context.Background(), options) + if err != nil { + return nil + } } // Decrypt and load SSH keys diff --git a/authority/config.go b/authority/config.go index 1f8374f8..168b360d 100644 --- a/authority/config.go +++ b/authority/config.go @@ -187,7 +187,7 @@ func (c *Config) Validate() error { case c.IntermediateCert == "": return errors.New("crt cannot be empty") - case c.IntermediateKey == "": + case c.IntermediateKey == "" && c.CAS.HasType(cas.SoftCAS): return errors.New("key cannot be empty") case len(c.DNSNames) == 0: diff --git a/authority/tls.go b/authority/tls.go index cc290839..dfb9b583 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -145,32 +145,24 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } } - lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(-1 * signOpts.Backdate)) + lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ Template: leaf, - Issuer: a.x509Issuer, - Signer: a.x509Signer, Lifetime: lifetime, + Backdate: signOpts.Backdate, }) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...) } - serverCert := resp.Certificate - // serverCert, err := x509util.CreateCertificate(leaf, a.x509Issuer, csr.PublicKey, a.x509Signer) - // if err != nil { - // return nil, errs.Wrap(http.StatusInternalServerError, err, - // "authority.Sign; error creating certificate", opts...) - // } - - if err = a.db.StoreCertificate(serverCert); err != nil { + if err = a.db.StoreCertificate(resp.Certificate); err != nil { if err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error storing certificate in db", opts...) } } - return []*x509.Certificate{serverCert, a.x509Issuer}, nil + return append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...), nil } // Renew creates a new Certificate identical to the old certificate, except @@ -200,13 +192,12 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 // Durations backdate := a.config.AuthorityConfig.Backdate.Duration duration := oldCert.NotAfter.Sub(oldCert.NotBefore) - now := time.Now().UTC() + lifetime := duration - backdate + // Create new certificate from previous values. + // Issuer, NotBefore, NotAfter and SubjectKeyId will be set by the CAS. newCert := &x509.Certificate{ - Issuer: a.x509Issuer.Subject, Subject: oldCert.Subject, - NotBefore: now.Add(-1 * backdate), - NotAfter: now.Add(duration - backdate), KeyUsage: oldCert.KeyUsage, UnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions, ExtKeyUsage: oldCert.ExtKeyUsage, @@ -241,10 +232,14 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 } // Copy all extensions except: - // 1. Authority Key Identifier - This one might be different if we rotate the intermediate certificate - // and it will cause a TLS bad certificate error. - // 2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier extension will be calculated - // for the new public key by NewLeafProfilewithTemplate() + // + // 1. Authority Key Identifier - This one might be different if we rotate + // the intermediate certificate and it will cause a TLS bad certificate + // error. + // + // 2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier + // extension will be calculated for the new public key by + // x509util.CreateCertificate() for _, ext := range oldCert.Extensions { if ext.Id.Equal(oidAuthorityKeyIdentifier) { continue @@ -256,18 +251,22 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext) } - serverCert, err := x509util.CreateCertificate(newCert, a.x509Issuer, newCert.PublicKey, a.x509Signer) + resp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{ + Template: newCert, + Lifetime: lifetime, + Backdate: backdate, + }) if err != nil { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey", opts...) } - if err = a.db.StoreCertificate(serverCert); err != nil { + if err = a.db.StoreCertificate(resp.Certificate); err != nil { if err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey; error storing certificate in db", opts...) } } - return []*x509.Certificate{serverCert, a.x509Issuer}, nil + return append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...), nil } // RevokeOptions are the options for the Revoke API. @@ -403,30 +402,36 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { certTpl.NotBefore = now.Add(-1 * time.Minute) certTpl.NotAfter = now.Add(24 * time.Hour) - cert, err := x509util.CreateCertificate(certTpl, a.x509Issuer, cr.PublicKey, a.x509Signer) + resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ + Template: certTpl, + Lifetime: 24 * time.Hour, + Backdate: 1 * time.Minute, + }) if err != nil { return fatal(err) } // Generate PEM blocks to create tls.Certificate - crtPEM := pem.EncodeToMemory(&pem.Block{ + pemBlocks := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", - Bytes: cert.Raw, + Bytes: resp.Certificate.Raw, }) - intermediatePEM, err := pemutil.Serialize(a.x509Issuer) - if err != nil { - return fatal(err) + for _, crt := range resp.CertificateChain { + pemBlocks = append(pemBlocks, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + })...) } keyPEM, err := pemutil.Serialize(priv) if err != nil { return fatal(err) } - tlsCrt, err := tls.X509KeyPair(append(crtPEM, pem.EncodeToMemory(intermediatePEM)...), pem.EncodeToMemory(keyPEM)) + tlsCrt, err := tls.X509KeyPair(pemBlocks, pem.EncodeToMemory(keyPEM)) if err != nil { return fatal(err) } // Set leaf certificate - tlsCrt.Leaf = cert + tlsCrt.Leaf = resp.Certificate return &tlsCrt, nil } diff --git a/cas/apiv1/extension.go b/cas/apiv1/extension.go index 66da15a0..de341fbb 100644 --- a/cas/apiv1/extension.go +++ b/cas/apiv1/extension.go @@ -8,6 +8,11 @@ import ( "github.com/pkg/errors" ) +var ( + oidStepRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} + oidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...) +) + // CertificateAuthorityExtension is type used to encode the certificate // authority extension. type CertificateAuthorityExtension struct { diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 7118472e..05b83db2 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -1,7 +1,8 @@ package apiv1 import ( - "strings" + "crypto" + "crypto/x509" "github.com/pkg/errors" ) @@ -14,27 +15,36 @@ type Options struct { // Path to the credentials file used in CloudCAS CredentialsFile string `json:"credentialsFile"` + + // CertificateAuthority reference. In CloudCAS the format is + // `projects/*/locations/*/certificateAuthorities/*`. + Certificateauthority string `json:"certificateAuthority"` + + // Issuer and signer are the issuer certificate and signer used in SoftCAS. + // They are configured in ca.json crt and key properties. + Issuer *x509.Certificate `json:"-"` + Signer crypto.Signer `json:"-"` } // Validate checks the fields in Options. func (o *Options) Validate() error { + var typ Type if o == nil { - return nil + typ = Type(SoftCAS) + } else { + typ = Type(o.Type) } - - switch Type(strings.ToLower(o.Type)) { - case DefaultCAS, SoftCAS, CloudCAS: - default: - return errors.Errorf("unsupported kms type %s", o.Type) + // Check that the type can be loaded. + if _, ok := LoadCertificateAuthorityServiceNewFunc(typ); !ok { + return errors.Errorf("unsupported cas type %s", typ) } - return nil } // HasType returns if the options have the given type. func (o *Options) HasType(t Type) bool { if o == nil { - return SoftCAS == t.String() + return t.String() == SoftCAS } return Type(o.Type).String() == t.String() } diff --git a/cas/apiv1/registry.go b/cas/apiv1/registry.go index 9c4c96ee..b74103b7 100644 --- a/cas/apiv1/registry.go +++ b/cas/apiv1/registry.go @@ -5,7 +5,9 @@ import ( "sync" ) -var registry = new(sync.Map) +var ( + registry = new(sync.Map) +) // CertificateAuthorityServiceNewFunc is the type that represents the method to initialize a new // CertificateAuthorityService. @@ -13,12 +15,12 @@ type CertificateAuthorityServiceNewFunc func(ctx context.Context, opts Options) // Register adds to the registry a method to create a KeyManager of type t. func Register(t Type, fn CertificateAuthorityServiceNewFunc) { - registry.Store(t, fn) + registry.Store(t.String(), fn) } // LoadCertificateAuthorityServiceNewFunc returns the function initialize a KayManager. func LoadCertificateAuthorityServiceNewFunc(t Type) (CertificateAuthorityServiceNewFunc, bool) { - v, ok := registry.Load(t) + v, ok := registry.Load(t.String()) if !ok { return nil, false } diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index bc6770be..9527e16c 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -1,15 +1,12 @@ package apiv1 import ( - "crypto" "crypto/x509" "time" ) type CreateCertificateRequest struct { Template *x509.Certificate - Issuer *x509.Certificate - Signer crypto.Signer Lifetime time.Duration Backdate time.Duration RequestID string @@ -21,8 +18,6 @@ type CreateCertificateResponse struct { type RenewCertificateRequest struct { Template *x509.Certificate - Issuer *x509.Certificate - Signer crypto.Signer Lifetime time.Duration Backdate time.Duration RequestID string diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index f3bc6b16..5c6de1c3 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -1,15 +1,9 @@ package apiv1 import ( - "encoding/asn1" "strings" ) -var ( - oidStepRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} - oidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...) -) - // CertificateAuthorityService is the interface implemented to support external // certificate authorities. type CertificateAuthorityService interface { @@ -18,27 +12,24 @@ type CertificateAuthorityService interface { RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) } -// Type represents the KMS type used. +// Type represents the CAS type used. type Type string const ( // DefaultCAS is a CertificateAuthorityService using software. DefaultCAS = "" // SoftCAS is a CertificateAuthorityService using software. - SoftCAS = "SoftCAS" + SoftCAS = "softcas" // CloudCAS is a CertificateAuthorityService using Google Cloud CAS. - CloudCAS = "CloudCAS" + CloudCAS = "cloudcas" ) -// String returns the given type as a string. All the letters will be lowercase. +// String returns a string from the type. It will always return the lower case +// version of the Type, as we need a standard type to compare and use as the +// registry key. func (t Type) String() string { if t == "" { return SoftCAS } - for _, s := range []string{SoftCAS, CloudCAS} { - if strings.EqualFold(s, string(t)) { - return s - } - } - return string(t) + return strings.ToLower(string(t)) } diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 52608658..0a7a064d 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -45,6 +45,10 @@ type caClient interface{} // New creates a new CertificateAuthorityService implementation using Google // Cloud CAS. func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { + if opts.Certificateauthority == "" { + return nil, errors.New("cloudCAS 'certificateAuthority' cannot be empty") + } + var cloudOpts []option.ClientOption if opts.CredentialsFile != "" { cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile)) @@ -57,7 +61,7 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { return &CloudCAS{ client: client, - certificateAuthority: "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/Smallstep-Test-Intermediate-CA", + certificateAuthority: opts.Certificateauthority, }, nil } @@ -87,9 +91,9 @@ func (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { switch { case req.Template == nil: - return nil, errors.New("renewCertificate `template` cannot be nil") + return nil, errors.New("renewCertificateRequest `template` cannot be nil") case req.Lifetime == 0: - return nil, errors.New("renewCertificate `lifetime` cannot be 0") + return nil, errors.New("renewCertificateRequest `lifetime` cannot be 0") } cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID) @@ -106,7 +110,7 @@ func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1. // RevokeCertificate a certificate using Google Cloud CAS. func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { if req.Certificate == nil { - return nil, errors.New("revokeCertificate `certificate` cannot be nil") + return nil, errors.New("revokeCertificateRequest `certificate` cannot be nil") } ext, ok := apiv1.FindCertificateAuthorityExtension(req.Certificate) diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index b7e5ddfc..751913a0 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -2,8 +2,11 @@ package softcas import ( "context" + "crypto" "crypto/x509" + "errors" "fmt" + "time" "github.com/smallstep/certificates/cas/apiv1" "go.step.sm/crypto/x509util" @@ -15,19 +18,47 @@ func init() { }) } -// SoftCAS implements a Certificate Authority Service using Golang crypto. -// This is the default CAS used in step-ca. -type SoftCAS struct{} - -// New creates a new CertificateAuthorityService implementation using Golang -// crypto. -func New(ctx context.Context, opts apiv1.Options) (*SoftCAS, error) { - return &SoftCAS{}, nil +var now = func() time.Time { + return time.Now() } -// CreateCertificate signs a new certificate using Golang crypto. +// SoftCAS implements a Certificate Authority Service using Golang or KMS +// crypto. This is the default CAS used in step-ca. +type SoftCAS struct { + Issuer *x509.Certificate + Signer crypto.Signer +} + +// New creates a new CertificateAuthorityService implementation using Golang or KMS +// crypto. +func New(ctx context.Context, opts apiv1.Options) (*SoftCAS, error) { + switch { + case opts.Issuer == nil: + return nil, errors.New("softCAS 'issuer' cannot be nil") + case opts.Signer == nil: + return nil, errors.New("softCAS 'signer' cannot be nil") + } + return &SoftCAS{ + Issuer: opts.Issuer, + Signer: opts.Signer, + }, nil +} + +// CreateCertificate signs a new certificate using Golang or KMS crypto. func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { - cert, err := x509util.CreateCertificate(req.Template, req.Issuer, req.Template.PublicKey, req.Signer) + 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") + } + + t := now() + req.Template.NotBefore = t.Add(-1 * req.Backdate) + req.Template.NotAfter = t.Add(req.Lifetime) + req.Template.Issuer = c.Issuer.Subject + + cert, err := x509util.CreateCertificate(req.Template, c.Issuer, req.Template.PublicKey, c.Signer) if err != nil { return nil, err } @@ -35,13 +66,36 @@ func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 return &apiv1.CreateCertificateResponse{ Certificate: cert, CertificateChain: []*x509.Certificate{ - req.Issuer, + c.Issuer, }, }, nil } +// RenewCertificate signs the given certificate template using Golang or KMS crypto. func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { - return nil, fmt.Errorf("not implemented") + 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") + } + + t := now() + req.Template.NotBefore = t.Add(-1 * req.Backdate) + req.Template.NotAfter = t.Add(req.Lifetime) + req.Template.Issuer = c.Issuer.Subject + + cert, err := x509util.CreateCertificate(req.Template, c.Issuer, req.Template.PublicKey, c.Signer) + if err != nil { + return nil, err + } + + return &apiv1.RenewCertificateResponse{ + Certificate: cert, + CertificateChain: []*x509.Certificate{ + c.Issuer, + }, + }, nil } // RevokeCertificate revokes the given certificate in step-ca. From bd8dd9da41fc953c62135c36582ff7ad001bdf0d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 10 Sep 2020 19:13:17 -0700 Subject: [PATCH 04/29] Do not read issuer and signer twice. --- authority/authority.go | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 0fc7a71a..a51e986f 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -218,21 +218,8 @@ func (a *Authority) init() error { // Set issuer and signer for default CAS. if options.HasType(casapi.SoftCAS) { - crt, err := pemutil.ReadCertificate(a.config.IntermediateCert) - if err != nil { - return err - } - - signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ - SigningKey: a.config.IntermediateKey, - Password: []byte(a.config.Password), - }) - if err != nil { - return err - } - - options.Issuer = crt - options.Signer = signer + options.Issuer = a.x509Issuer + options.Signer = a.x509Signer } a.x509CAService, err = cas.New(context.Background(), options) From 8eff4e77a893e16d381427ae60075c223c86d584 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 14 Sep 2020 19:12:49 -0700 Subject: [PATCH 05/29] Comment request structs. --- cas/apiv1/requests.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index 9527e16c..3f7349ca 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -5,23 +5,29 @@ import ( "time" ) +// CreateCertificateRequest is the request used to sign a new certificate. type CreateCertificateRequest struct { Template *x509.Certificate Lifetime time.Duration Backdate time.Duration RequestID string } + +// CreateCertificateResponse is the response to a create certificate request. type CreateCertificateResponse struct { Certificate *x509.Certificate CertificateChain []*x509.Certificate } +// RenewCertificateRequest is the request used to re-sign a certificate. type RenewCertificateRequest struct { Template *x509.Certificate Lifetime time.Duration Backdate time.Duration RequestID string } + +// RenewCertificateResponse is the response to a renew certificate request. type RenewCertificateResponse struct { Certificate *x509.Certificate CertificateChain []*x509.Certificate @@ -30,7 +36,12 @@ type RenewCertificateResponse struct { // RevokeCertificateRequest is the request used to revoke a certificate. type RevokeCertificateRequest struct { Certificate *x509.Certificate + Reason string + ReasonCode int + RequestID string } + +// RevokeCertificateResponse is the response to a revoke certificate request. type RevokeCertificateResponse struct { Certificate *x509.Certificate CertificateChain []*x509.Certificate From 01e6495f43ca8adb8d80cdd01e5d75e62055d3e7 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 14 Sep 2020 19:13:40 -0700 Subject: [PATCH 06/29] Add most of cloudcas unit tests and minor fixes. --- cas/apiv1/extension_test.go | 56 +++ cas/cloudcas/certificate.go | 8 +- cas/cloudcas/certificate_test.go | 213 +++++++++++ cas/cloudcas/cloudcas.go | 67 +++- cas/cloudcas/cloudcas_test.go | 581 +++++++++++++++++++++++++++++++ 5 files changed, 902 insertions(+), 23 deletions(-) create mode 100644 cas/apiv1/extension_test.go create mode 100644 cas/cloudcas/certificate_test.go create mode 100644 cas/cloudcas/cloudcas_test.go diff --git a/cas/apiv1/extension_test.go b/cas/apiv1/extension_test.go new file mode 100644 index 00000000..113e3de1 --- /dev/null +++ b/cas/apiv1/extension_test.go @@ -0,0 +1,56 @@ +package apiv1 + +import ( + "crypto/x509/pkix" + "fmt" + "reflect" + "testing" +) + +func TestCreateCertificateAuthorityExtension(t *testing.T) { + type args struct { + typ Type + certificateID string + keyValuePairs []string + } + tests := []struct { + name string + args args + want pkix.Extension + wantErr bool + }{ + {"ok", args{Type(CloudCAS), "1ac75689-cd3f-482e-a695-8a13daf39dc4", nil}, pkix.Extension{ + Id: oidStepCertificateAuthority, + Critical: false, + Value: []byte{ + 0x30, 0x30, 0x13, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x63, 0x61, 0x73, 0x13, 0x24, 0x31, 0x61, + 0x63, 0x37, 0x35, 0x36, 0x38, 0x39, 0x2d, 0x63, 0x64, 0x33, 0x66, 0x2d, 0x34, 0x38, 0x32, 0x65, + 0x2d, 0x61, 0x36, 0x39, 0x35, 0x2d, 0x38, 0x61, 0x31, 0x33, 0x64, 0x61, 0x66, 0x33, 0x39, 0x64, + 0x63, 0x34, + }, + }, false}, + {"ok", args{Type(CloudCAS), "1ac75689-cd3f-482e-a695-8a13daf39dc4", []string{"foo", "bar"}}, pkix.Extension{ + Id: oidStepCertificateAuthority, + Critical: false, + Value: []byte{ + 0x30, 0x3c, 0x13, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x63, 0x61, 0x73, 0x13, 0x24, 0x31, 0x61, + 0x63, 0x37, 0x35, 0x36, 0x38, 0x39, 0x2d, 0x63, 0x64, 0x33, 0x66, 0x2d, 0x34, 0x38, 0x32, 0x65, + 0x2d, 0x61, 0x36, 0x39, 0x35, 0x2d, 0x38, 0x61, 0x31, 0x33, 0x64, 0x61, 0x66, 0x33, 0x39, 0x64, + 0x63, 0x34, 0x30, 0x0a, 0x13, 0x03, 0x66, 0x6f, 0x6f, 0x13, 0x03, 0x62, 0x61, 0x72, + }, + }, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CreateCertificateAuthorityExtension(tt.args.typ, tt.args.certificateID, tt.args.keyValuePairs...) + if (err != nil) != tt.wantErr { + t.Errorf("CreateCertificateAuthorityExtension() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CreateCertificateAuthorityExtension() = %v, want %v", got, tt.want) + fmt.Printf("%x\n", got.Value) + } + }) + } +} diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go index 31da2c7d..6c19adbf 100644 --- a/cas/cloudcas/certificate.go +++ b/cas/cloudcas/certificate.go @@ -70,7 +70,7 @@ func createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) { return nil, errors.Wrap(err, "error marshaling public key") } return &pb.PublicKey{ - Type: pb.PublicKey_PEM_RSA_KEY, + Type: pb.PublicKey_PEM_EC_KEY, Key: pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: asn1Bytes, @@ -215,9 +215,9 @@ func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper { unknownEKUs = append(unknownEKUs, createObjectID(oid)) } - policyIDs := make([]*pb.ObjectId, len(cert.PolicyIdentifiers)) - for i, oid := range cert.PolicyIdentifiers { - policyIDs[i] = createObjectID(oid) + var policyIDs []*pb.ObjectId + for _, oid := range cert.PolicyIdentifiers { + policyIDs = append(policyIDs, createObjectID(oid)) } var caOptions *pb.ReusableConfigValues_CaOptions diff --git a/cas/cloudcas/certificate_test.go b/cas/cloudcas/certificate_test.go new file mode 100644 index 00000000..4c0505c1 --- /dev/null +++ b/cas/cloudcas/certificate_test.go @@ -0,0 +1,213 @@ +package cloudcas + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "reflect" + "testing" + + pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" +) + +var ( + testLeafPrivateKey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlNnX2+xfjX +a1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czgPw== +-----END PUBLIC KEY----- +` + testRSACertificate = `-----BEGIN CERTIFICATE----- +MIICozCCAkmgAwIBAgIRANNhMpODj7ThgviZCoF6kj8wCgYIKoZIzj0EAwIwKjEo +MCYGA1UEAxMfR29vZ2xlIENBUyBUZXN0IEludGVybWVkaWF0ZSBDQTAeFw0yMDA5 +MTUwMTUxMDdaFw0zMDA5MTMwMTUxMDNaMB0xGzAZBgNVBAMTEnRlc3Quc21hbGxz +dGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANPRjuIlsP5Z +672syAsHlbILFabG/xmrlsO0UdcLo4Yjf9WPAFA+7q+CsVDFh4dQbMv96fsHtdYP +E9wlWyMqYG+5E8QT2i0WNFEoYcXOGZuXdyD/TA5Aucu1RuYLrZXQrXWDnvaWOgvr +EZ6s9VsPCzzkL8KBejIMQIMY0KXEJfB/HgXZNn8V2trZkWT5CzxbcOF3s3UC1Z6F +Ja6zjpxhSyRkqgknJxv6yK4t7HEwdhrDI8uyxJYHPQWKNRjWecHWE9E+MtoS7D08 +mTh8qlAKoBbkGolR2nJSXffU09F3vSg+MIfjPiRqjf6394cQ3T9D5yZK//rCrxWU +8KKBQMEmdKcCAwEAAaOBkTCBjjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQffuoYvH1+IF1cipl35gXJxSJE +SjAfBgNVHSMEGDAWgBRIOVqyLDSlErJLuWWEvRm5UU1r1TAdBgNVHREEFjAUghJ0 +ZXN0LnNtYWxsc3RlcC5jb20wCgYIKoZIzj0EAwIDSAAwRQIhAL9AAw/LVLvvxBkM +sJnHd+RIk7ZblkgcArwpIS2+Z5xNAiBtUED4zyimz9b4aQiXdw4IMd2CKxVyW8eE +6x1vSZMvzQ== +-----END CERTIFICATE-----` + testRSAPublicKey = `-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEA09GO4iWw/lnrvazICweVsgsVpsb/GauWw7RR1wujhiN/1Y8AUD7u +r4KxUMWHh1Bsy/3p+we11g8T3CVbIypgb7kTxBPaLRY0UShhxc4Zm5d3IP9MDkC5 +y7VG5gutldCtdYOe9pY6C+sRnqz1Ww8LPOQvwoF6MgxAgxjQpcQl8H8eBdk2fxXa +2tmRZPkLPFtw4XezdQLVnoUlrrOOnGFLJGSqCScnG/rIri3scTB2GsMjy7LElgc9 +BYo1GNZ5wdYT0T4y2hLsPTyZOHyqUAqgFuQaiVHaclJd99TT0Xe9KD4wh+M+JGqN +/rf3hxDdP0PnJkr/+sKvFZTwooFAwSZ0pwIDAQAB +-----END RSA PUBLIC KEY----- +` +) + +func Test_createCertificateConfig(t *testing.T) { + cert := mustParseCertificate(t, testLeafCertificate) + type args struct { + tpl *x509.Certificate + } + tests := []struct { + name string + args args + want *pb.Certificate_Config + wantErr bool + }{ + {"ok", args{cert}, &pb.Certificate_Config{ + Config: &pb.CertificateConfig{ + SubjectConfig: &pb.CertificateConfig_SubjectConfig{ + Subject: &pb.Subject{}, + CommonName: "test.smallstep.com", + SubjectAltName: &pb.SubjectAltNames{ + DnsNames: []string{"test.smallstep.com"}, + }, + }, + ReusableConfig: &pb.ReusableConfigWrapper{ + ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{ + ReusableConfigValues: &pb.ReusableConfigValues{ + KeyUsage: &pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + DigitalSignature: true, + }, + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{ + ClientAuth: true, + ServerAuth: true, + }, + }, + }, + }, + }, + PublicKey: &pb.PublicKey{ + Type: pb.PublicKey_PEM_EC_KEY, + Key: []byte(testLeafPrivateKey), + }, + }, + }, false}, + {"fail", args{&x509.Certificate{}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := createCertificateConfig(tt.args.tpl) + if (err != nil) != tt.wantErr { + t.Errorf("createCertificateConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("createCertificateConfig() = %v, want %v", got.Config.ReusableConfig, tt.want.Config.ReusableConfig) + } + }) + } +} + +func Test_createPublicKey(t *testing.T) { + edpub, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + ecCert := mustParseCertificate(t, testLeafCertificate) + rsaCert := mustParseCertificate(t, testRSACertificate) + type args struct { + key crypto.PublicKey + } + tests := []struct { + name string + args args + want *pb.PublicKey + wantErr bool + }{ + {"ok ec", args{ecCert.PublicKey}, &pb.PublicKey{ + Type: pb.PublicKey_PEM_EC_KEY, + Key: []byte(testLeafPrivateKey), + }, false}, + {"ok rsa", args{rsaCert.PublicKey}, &pb.PublicKey{ + Type: pb.PublicKey_PEM_RSA_KEY, + Key: []byte(testRSAPublicKey), + }, false}, + {"fail ed25519", args{edpub}, nil, true}, + {"fail ec marshal", args{&ecdsa.PublicKey{ + Curve: &elliptic.CurveParams{Name: "FOO", BitSize: 256}, + X: ecCert.PublicKey.(*ecdsa.PublicKey).X, + Y: ecCert.PublicKey.(*ecdsa.PublicKey).Y, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := createPublicKey(tt.args.key) + if (err != nil) != tt.wantErr { + t.Errorf("createPublicKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("createPublicKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_createSubject(t *testing.T) { + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + args args + want *pb.Subject + }{ + {"ok empty", args{&x509.Certificate{}}, &pb.Subject{}}, + {"ok all", args{&x509.Certificate{ + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{"Smallstep Labs"}, + OrganizationalUnit: []string{"Engineering"}, + Locality: []string{"San Francisco"}, + Province: []string{"California"}, + StreetAddress: []string{"1 A St."}, + PostalCode: []string{"12345"}, + SerialNumber: "1234567890", + CommonName: "test.smallstep.com", + }, + }}, &pb.Subject{ + CountryCode: "US", + Organization: "Smallstep Labs", + OrganizationalUnit: "Engineering", + Locality: "San Francisco", + Province: "California", + StreetAddress: "1 A St.", + PostalCode: "12345", + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createSubject(tt.args.cert); !reflect.DeepEqual(got, tt.want) { + t.Errorf("createSubject() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_createSubjectAlternativeNames(t *testing.T) { + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + args args + want *pb.SubjectAltNames + }{ + {"ok empty", args{&x509.Certificate{}}, &pb.SubjectAltNames{}}, + // TODO + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createSubjectAlternativeNames(tt.args.cert); !reflect.DeepEqual(got, tt.want) { + t.Errorf("createSubjectAlternativeNames() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 0a7a064d..e6aa9a49 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -2,15 +2,15 @@ package cloudcas import ( "context" + "crypto/rand" "crypto/x509" "encoding/asn1" - "encoding/json" "encoding/pem" - "fmt" "time" privateca "cloud.google.com/go/security/privateca/apiv1beta1" "github.com/google/uuid" + gax "github.com/googleapis/gax-go/v2" "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" "google.golang.org/api/option" @@ -24,9 +24,11 @@ func init() { }) } -func debug(v interface{}) { - b, _ := json.MarshalIndent(v, "", " ") - fmt.Println(string(b)) +// 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) } var ( @@ -34,13 +36,40 @@ var ( stepOIDCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 2)...) ) +// 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, +} + // CloudCAS implements a Certificate Authority Service using Google Cloud CAS. type CloudCAS struct { - client *privateca.CertificateAuthorityClient + client CertificateAuthorityClient certificateAuthority string } -type caClient interface{} +// 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. @@ -49,14 +78,9 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { return nil, errors.New("cloudCAS 'certificateAuthority' cannot be empty") } - var cloudOpts []option.ClientOption - if opts.CredentialsFile != "" { - cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile)) - } - - client, err := privateca.NewCertificateAuthorityClient(ctx, cloudOpts...) + client, err := newCertificateAuthorityClient(ctx, opts.CredentialsFile) if err != nil { - return nil, errors.Wrap(err, "error creating client") + return nil, err } return &CloudCAS{ @@ -109,7 +133,11 @@ func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1. // RevokeCertificate a certificate using Google Cloud CAS. func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { - if req.Certificate == nil { + 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") } @@ -119,7 +147,7 @@ func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv } var cae apiv1.CertificateAuthorityExtension - if _, err := asn1.Unmarshal(ext.Value, &ext); err != nil { + if _, err := asn1.Unmarshal(ext.Value, &cae); err != nil { return nil, errors.Wrap(err, "error unmarshaling certificate authority extension") } @@ -127,8 +155,9 @@ func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv defer cancel() certpb, err := c.client.RevokeCertificate(ctx, &pb.RevokeCertificateRequest{ - Name: c.certificateAuthority + "/certificates/" + cae.CertificateID, - Reason: pb.RevocationReason_REVOCATION_REASON_UNSPECIFIED, + Name: c.certificateAuthority + "/certificates/" + cae.CertificateID, + Reason: reason, + RequestId: req.RequestID, }) if err != nil { return nil, errors.Wrap(err, "cloudCAS RevokeCertificate failed") @@ -192,7 +221,7 @@ func defaultContext() (context.Context, context.CancelFunc) { } func createCertificateID() (string, error) { - id, err := uuid.NewRandom() + id, err := uuid.NewRandomFromReader(rand.Reader) if err != nil { return "", errors.Wrap(err, "error creating certificate id") } diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go new file mode 100644 index 00000000..56579ce6 --- /dev/null +++ b/cas/cloudcas/cloudcas_test.go @@ -0,0 +1,581 @@ +package cloudcas + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/x509" + "encoding/asn1" + "io" + "os" + "reflect" + "testing" + "time" + + "github.com/google/uuid" + gax "github.com/googleapis/gax-go/v2" + "github.com/pkg/errors" + "github.com/smallstep/certificates/cas/apiv1" + pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" +) + +var ( + errTest = errors.New("test error") + testAuthorityName = "projects/test-project/locations/us-west1/certificateAuthorities/test-ca" + testCertificateName = "projects/test-project/locations/us-west1/certificateAuthorities/test-ca/certificates/test-certificate" + testRootCertificate = `-----BEGIN CERTIFICATE----- +MIIBhjCCAS2gAwIBAgIQLbKTuXau4+t3KFbGpJJAADAKBggqhkjOPQQDAjAiMSAw +HgYDVQQDExdHb29nbGUgQ0FTIFRlc3QgUm9vdCBDQTAeFw0yMDA5MTQyMjQ4NDla +Fw0zMDA5MTIyMjQ4NDlaMCIxIDAeBgNVBAMTF0dvb2dsZSBDQVMgVGVzdCBSb290 +IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYKGgQ3/0D7+oBTc0CXoYfSC6 +M8hOqLsmzBapPZSYpfwjgEsjdNU84jdrYmW1zF1+p+MrL4c7qJv9NLo/picCuqNF +MEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE +FFVn9V7Qymd7cUJh9KAhnUDAQL5YMAoGCCqGSM49BAMCA0cAMEQCIA4LzttYoT3u +8TYgSrvFT+Z+cklfi4UrPBU6aSbcUaW2AiAPfaqbyccQT3CxMVyHg+xZZjAirZp8 +lAeA/T4FxAonHA== +-----END CERTIFICATE-----` + testIntermediateCertificate = `-----BEGIN CERTIFICATE----- +MIIBsDCCAVagAwIBAgIQOb91kHxWKVzSJ9ESW1ViVzAKBggqhkjOPQQDAjAiMSAw +HgYDVQQDExdHb29nbGUgQ0FTIFRlc3QgUm9vdCBDQTAeFw0yMDA5MTQyMjQ4NDla +Fw0zMDA5MTIyMjQ4NDlaMCoxKDAmBgNVBAMTH0dvb2dsZSBDQVMgVGVzdCBJbnRl +cm1lZGlhdGUgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASUHN1cNyId4Ei/ +4MxD5VrZFc51P50caMUdDZVrPveidChBYCU/9IM6vnRlZHx2HLjQ0qAvqHwY3rT0 +xc7n+PfCo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAd +BgNVHQ4EFgQUSDlasiw0pRKyS7llhL0ZuVFNa9UwHwYDVR0jBBgwFoAUVWf1XtDK +Z3txQmH0oCGdQMBAvlgwCgYIKoZIzj0EAwIDSAAwRQIgMmsLcoC4KriXw+s+cZx2 +bJMf6Mx/WESj31buJJhpzY0CIQCBUa/JtvS3nyce/4DF5tK2v49/NWHREgqAaZ57 +DcYyHQ== +-----END CERTIFICATE-----` + testLeafCertificate = `-----BEGIN CERTIFICATE----- +MIIB1jCCAX2gAwIBAgIQQfOn+COMeuD8VYF1TiDkEzAKBggqhkjOPQQDAjAqMSgw +JgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx +NDIyNTE1NVoXDTMwMDkxMjIyNTE1MlowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0 +ZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlN +nX2+xfjXa1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czg +P6OBkTCBjjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMB0GA1UdDgQWBBSYPbu4Tmm7Zze/hCePeZH1Avoj+jAfBgNVHSMEGDAW +gBRIOVqyLDSlErJLuWWEvRm5UU1r1TAdBgNVHREEFjAUghJ0ZXN0LnNtYWxsc3Rl +cC5jb20wCgYIKoZIzj0EAwIDRwAwRAIgY+nTc+RHn31/BOhht4JpxCmJPHxqFT3S +ojnictBudV0CIB87ipY5HV3c8FLVEzTA0wFwdDZvQraQYsthwbg2kQFb +-----END CERTIFICATE-----` + testSignedCertificate = `-----BEGIN CERTIFICATE----- +MIIB/DCCAaKgAwIBAgIQHHFuGMz0cClfde5kqP5prTAKBggqhkjOPQQDAjAqMSgw +JgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx +NTAwMDQ0M1oXDTMwMDkxMzAwMDQ0MFowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0 +ZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMqNCiXMvbn74LsHzRv+8 +17m9vEzH6RHrg3m82e0uEc36+fZWV/zJ9SKuONmnl5VP79LsjL5SVH0RDj73U2XO +DKOBtjCBszAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMB0GA1UdDgQWBBRTA2cTs7PCNjnps/+T0dS8diqv0DAfBgNVHSMEGDAW +gBRIOVqyLDSlErJLuWWEvRm5UU1r1TBCBgwrBgEEAYKkZMYoQAIEMjAwEwhjbG91 +ZGNhcxMkZDhkMThhNjgtNTI5Ni00YWYzLWFlNGItMmY4NzdkYTNmYmQ5MAoGCCqG +SM49BAMCA0gAMEUCIGxl+pqJ50WYWUqK2l4V1FHoXSi0Nht5kwTxFxnWZu1xAiEA +zemu3bhWLFaGg3s8i+HTEhw4RqkHP74vF7AVYp88bAw= +-----END CERTIFICATE-----` +) + +type testClient struct { + credentialsFile string + certificate *pb.Certificate + err error +} + +func newTestClient(credentialsFile string) (CertificateAuthorityClient, error) { + if credentialsFile == "testdata/error.json" { + return nil, errTest + } + return &testClient{ + credentialsFile: credentialsFile, + }, nil +} + +func okTestClient() *testClient { + return &testClient{ + credentialsFile: "testdata/credentials.json", + certificate: &pb.Certificate{ + Name: testCertificateName, + PemCertificate: testSignedCertificate, + PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, + }, + } +} + +func failTestClient() *testClient { + return &testClient{ + credentialsFile: "testdata/credentials.json", + err: errTest, + } +} + +func badTestClient() *testClient { + return &testClient{ + credentialsFile: "testdata/credentials.json", + certificate: &pb.Certificate{ + Name: testCertificateName, + PemCertificate: "not a pem cert", + PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, + }, + } +} + +func setTeeReader(t *testing.T, w *bytes.Buffer) { + t.Helper() + reader := rand.Reader + t.Cleanup(func() { + rand.Reader = reader + }) + rand.Reader = io.TeeReader(reader, w) +} + +func (c *testClient) CreateCertificate(ctx context.Context, req *pb.CreateCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) { + return c.certificate, c.err +} + +func (c *testClient) RevokeCertificate(ctx context.Context, req *pb.RevokeCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) { + return c.certificate, c.err +} + +func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate { + t.Helper() + crt, err := parseCertificate(pemCert) + if err != nil { + t.Fatal(err) + } + return crt +} + +func TestNew(t *testing.T) { + tmp := newCertificateAuthorityClient + newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) { + return newTestClient(credentialsFile) + } + t.Cleanup(func() { + newCertificateAuthorityClient = tmp + }) + + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + args args + want *CloudCAS + wantErr bool + }{ + {"ok", args{context.Background(), apiv1.Options{ + Certificateauthority: testAuthorityName, + }}, &CloudCAS{ + client: &testClient{}, + certificateAuthority: testAuthorityName, + }, false}, + {"ok with credentials", args{context.Background(), apiv1.Options{ + Certificateauthority: testAuthorityName, CredentialsFile: "testdata/credentials.json", + }}, &CloudCAS{ + client: &testClient{credentialsFile: "testdata/credentials.json"}, + certificateAuthority: testAuthorityName, + }, false}, + {"fail certificate authority", args{context.Background(), apiv1.Options{}}, nil, true}, + {"fail with credentials", args{context.Background(), apiv1.Options{ + Certificateauthority: testAuthorityName, CredentialsFile: "testdata/error.json", + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNew_real(t *testing.T) { + if v, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS"); ok { + os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS") + t.Cleanup(func() { + os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", v) + }) + } + + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"fail default credentials", args{context.Background(), apiv1.Options{Certificateauthority: testAuthorityName}}, true}, + {"fail certificate authority", args{context.Background(), apiv1.Options{}}, true}, + {"fail with credentials", args{context.Background(), apiv1.Options{ + Certificateauthority: testAuthorityName, CredentialsFile: "testdata/missing.json", + }}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCloudCAS_CreateCertificate(t *testing.T) { + type fields struct { + client CertificateAuthorityClient + certificateAuthority string + } + type args struct { + req *apiv1.CreateCertificateRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.CreateCertificateResponse + wantErr bool + }{ + {"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ + Template: mustParseCertificate(t, testLeafCertificate), + Lifetime: 24 * time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: mustParseCertificate(t, testSignedCertificate), + CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, + }, false}, + {"fail Template", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ + Lifetime: 24 * time.Hour, + }}, nil, true}, + {"fail Lifetime", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ + Template: mustParseCertificate(t, testLeafCertificate), + }}, nil, true}, + {"fail CreateCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ + Template: mustParseCertificate(t, testLeafCertificate), + Lifetime: 24 * time.Hour, + }}, nil, true}, + {"fail Certificate", fields{badTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ + Template: mustParseCertificate(t, testLeafCertificate), + Lifetime: 24 * time.Hour, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CloudCAS{ + client: tt.fields.client, + certificateAuthority: tt.fields.certificateAuthority, + } + got, err := c.CreateCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("CloudCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CloudCAS.CreateCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCloudCAS_createCertificate(t *testing.T) { + leaf := mustParseCertificate(t, testLeafCertificate) + signed := mustParseCertificate(t, testSignedCertificate) + chain := []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)} + + type fields struct { + client CertificateAuthorityClient + certificateAuthority string + } + type args struct { + tpl *x509.Certificate + lifetime time.Duration + requestID string + } + tests := []struct { + name string + fields fields + args args + want *x509.Certificate + want1 []*x509.Certificate + wantErr bool + }{ + {"ok", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, signed, chain, false}, + {"fail CertificateConfig", fields{okTestClient(), testAuthorityName}, args{&x509.Certificate{}, 24 * time.Hour, "request-id"}, nil, nil, true}, + {"fail CreateCertificate", fields{failTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true}, + {"fail ParseCertificates", fields{badTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true}, + {"fail create id", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true}, + } + + // Pre-calulate rand.Random + buf := new(bytes.Buffer) + setTeeReader(t, buf) + for i := 0; i < len(tests)-1; i++ { + _, err := uuid.NewRandomFromReader(rand.Reader) + if err != nil { + t.Fatal(err) + } + } + rand.Reader = buf + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CloudCAS{ + client: tt.fields.client, + certificateAuthority: tt.fields.certificateAuthority, + } + got, got1, err := c.createCertificate(tt.args.tpl, tt.args.lifetime, tt.args.requestID) + if (err != nil) != tt.wantErr { + t.Errorf("CloudCAS.createCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CloudCAS.createCertificate() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("CloudCAS.createCertificate() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestCloudCAS_RenewCertificate(t *testing.T) { + type fields struct { + client CertificateAuthorityClient + certificateAuthority string + } + type args struct { + req *apiv1.RenewCertificateRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.RenewCertificateResponse + wantErr bool + }{ + {"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ + Template: mustParseCertificate(t, testLeafCertificate), + Lifetime: 24 * time.Hour, + }}, &apiv1.RenewCertificateResponse{ + Certificate: mustParseCertificate(t, testSignedCertificate), + CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, + }, false}, + {"fail Template", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ + Lifetime: 24 * time.Hour, + }}, nil, true}, + {"fail Lifetime", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ + Template: mustParseCertificate(t, testLeafCertificate), + }}, nil, true}, + {"fail CreateCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ + Template: mustParseCertificate(t, testLeafCertificate), + Lifetime: 24 * time.Hour, + }}, nil, true}, + {"fail Certificate", fields{badTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ + Template: mustParseCertificate(t, testLeafCertificate), + Lifetime: 24 * time.Hour, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CloudCAS{ + client: tt.fields.client, + certificateAuthority: tt.fields.certificateAuthority, + } + got, err := c.RenewCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("CloudCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CloudCAS.RenewCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCloudCAS_RevokeCertificate(t *testing.T) { + badExtensionCert := mustParseCertificate(t, testSignedCertificate) + for i, ext := range badExtensionCert.Extensions { + if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 2}) { + badExtensionCert.Extensions[i].Value = []byte("bad-data") + } + } + + type fields struct { + client CertificateAuthorityClient + certificateAuthority string + } + type args struct { + req *apiv1.RevokeCertificateRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.RevokeCertificateResponse + wantErr bool + }{ + {"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + Certificate: mustParseCertificate(t, testSignedCertificate), + ReasonCode: 1, + }}, &apiv1.RevokeCertificateResponse{ + Certificate: mustParseCertificate(t, testSignedCertificate), + CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, + }, false}, + {"fail Extension", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + Certificate: mustParseCertificate(t, testLeafCertificate), + ReasonCode: 1, + }}, nil, true}, + {"fail Extension Value", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + Certificate: badExtensionCert, + ReasonCode: 1, + }}, nil, true}, + {"fail Certificate", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + ReasonCode: 2, + }}, nil, true}, + {"fail ReasonCode", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + Certificate: mustParseCertificate(t, testSignedCertificate), + ReasonCode: 100, + }}, nil, true}, + {"fail ReasonCode 7", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + Certificate: mustParseCertificate(t, testSignedCertificate), + ReasonCode: 7, + }}, nil, true}, + {"fail ReasonCode 8", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + Certificate: mustParseCertificate(t, testSignedCertificate), + ReasonCode: 8, + }}, nil, true}, + {"fail RevokeCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + Certificate: mustParseCertificate(t, testSignedCertificate), + ReasonCode: 1, + }}, nil, true}, + {"fail ParseCertificate", fields{badTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ + Certificate: mustParseCertificate(t, testSignedCertificate), + ReasonCode: 1, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CloudCAS{ + client: tt.fields.client, + certificateAuthority: tt.fields.certificateAuthority, + } + got, err := c.RevokeCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("CloudCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CloudCAS.RevokeCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_createCertificateID(t *testing.T) { + buf := new(bytes.Buffer) + setTeeReader(t, buf) + uuid, err := uuid.NewRandomFromReader(rand.Reader) + if err != nil { + t.Fatal(err) + } + rand.Reader = buf + + tests := []struct { + name string + want string + wantErr bool + }{ + {"ok", uuid.String(), false}, + {"fail", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := createCertificateID() + if (err != nil) != tt.wantErr { + t.Errorf("createCertificateID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("createCertificateID() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseCertificate(t *testing.T) { + type args struct { + pemCert string + } + tests := []struct { + name string + args args + want *x509.Certificate + wantErr bool + }{ + {"ok", args{testLeafCertificate}, mustParseCertificate(t, testLeafCertificate), false}, + {"ok intermediate", args{testIntermediateCertificate}, mustParseCertificate(t, testIntermediateCertificate), false}, + {"fail pem", args{"not pem"}, nil, true}, + {"fail parseCertificate", args{"-----BEGIN CERTIFICATE-----\nZm9vYmFyCg==\n-----END CERTIFICATE-----\n"}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseCertificate(tt.args.pemCert) + if (err != nil) != tt.wantErr { + t.Errorf("parseCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getCertificateAndChain(t *testing.T) { + type args struct { + certpb *pb.Certificate + } + tests := []struct { + name string + args args + want *x509.Certificate + want1 []*x509.Certificate + wantErr bool + }{ + {"ok", args{&pb.Certificate{ + Name: testCertificateName, + PemCertificate: testSignedCertificate, + PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, + }}, mustParseCertificate(t, testSignedCertificate), []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, false}, + {"fail PemCertificate", args{&pb.Certificate{ + Name: testCertificateName, + PemCertificate: "foobar", + PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, + }}, nil, nil, true}, + {"fail PemCertificateChain", args{&pb.Certificate{ + Name: testCertificateName, + PemCertificate: testSignedCertificate, + PemCertificateChain: []string{"foobar", testRootCertificate}, + }}, nil, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := getCertificateAndChain(tt.args.certpb) + if (err != nil) != tt.wantErr { + t.Errorf("getCertificateAndChain() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getCertificateAndChain() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("getCertificateAndChain() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} From f7d066fca8e2df0a766bb61fde4bc857318e169e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Sep 2020 15:19:59 -0700 Subject: [PATCH 07/29] Fix key usages. --- cas/cloudcas/certificate.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go index 6c19adbf..819bc2d7 100644 --- a/cas/cloudcas/certificate.go +++ b/cas/cloudcas/certificate.go @@ -138,6 +138,7 @@ func createSubjectAlternativeNames(cert *x509.Certificate) *pb.SubjectAltNames { var rawValues []asn1.RawValue if _, err := asn1.Unmarshal(ext.Value, &rawValues); err == nil { var newValues []asn1.RawValue + for _, v := range rawValues { switch v.Tag { case nameTypeDNS: @@ -252,15 +253,15 @@ func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper { values := &pb.ReusableConfigValues{ KeyUsage: &pb.KeyUsage{ BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ - DigitalSignature: cert.KeyUsage&x509.KeyUsageDigitalSignature == 1, - ContentCommitment: cert.KeyUsage&x509.KeyUsageContentCommitment == 1, - KeyEncipherment: cert.KeyUsage&x509.KeyUsageKeyEncipherment == 1, - DataEncipherment: cert.KeyUsage&x509.KeyUsageDataEncipherment == 1, - KeyAgreement: cert.KeyUsage&x509.KeyUsageKeyAgreement == 1, - CertSign: cert.KeyUsage&x509.KeyUsageCertSign == 1, - CrlSign: cert.KeyUsage&x509.KeyUsageCRLSign == 1, - EncipherOnly: cert.KeyUsage&x509.KeyUsageEncipherOnly == 1, - DecipherOnly: cert.KeyUsage&x509.KeyUsageDecipherOnly == 1, + DigitalSignature: cert.KeyUsage&x509.KeyUsageDigitalSignature > 0, + ContentCommitment: cert.KeyUsage&x509.KeyUsageContentCommitment > 0, + KeyEncipherment: cert.KeyUsage&x509.KeyUsageKeyEncipherment > 0, + DataEncipherment: cert.KeyUsage&x509.KeyUsageDataEncipherment > 0, + KeyAgreement: cert.KeyUsage&x509.KeyUsageKeyAgreement > 0, + CertSign: cert.KeyUsage&x509.KeyUsageCertSign > 0, + CrlSign: cert.KeyUsage&x509.KeyUsageCRLSign > 0, + EncipherOnly: cert.KeyUsage&x509.KeyUsageEncipherOnly > 0, + DecipherOnly: cert.KeyUsage&x509.KeyUsageDecipherOnly > 0, }, ExtendedKeyUsage: ekuOptions, UnknownExtendedKeyUsages: unknownEKUs, From 144ffe73dd35313d646d85b3a906a64dde292c32 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Sep 2020 17:23:11 -0700 Subject: [PATCH 08/29] Complete unit tests for Google CAS. --- cas/cloudcas/certificate.go | 69 +++++-- cas/cloudcas/certificate_test.go | 343 ++++++++++++++++++++++++++++++- cas/cloudcas/cloudcas_test.go | 33 +++ 3 files changed, 423 insertions(+), 22 deletions(-) diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go index 819bc2d7..c29eee60 100644 --- a/cas/cloudcas/certificate.go +++ b/cas/cloudcas/certificate.go @@ -15,9 +15,27 @@ import ( ) var ( - oidExtensionSubjectAltName = []int{2, 5, 29, 17} + oidExtensionSubjectKeyID = []int{2, 5, 29, 14} + oidExtensionKeyUsage = []int{2, 5, 29, 15} + oidExtensionExtendedKeyUsage = []int{2, 5, 29, 37} + oidExtensionAuthorityKeyID = []int{2, 5, 29, 35} + oidExtensionBasicConstraints = []int{2, 5, 29, 19} + oidExtensionSubjectAltName = []int{2, 5, 29, 17} + oidExtensionCertificatePolicies = []int{2, 5, 29, 32} + oidExtensionAuthorityInfoAccess = []int{1, 3, 6, 1, 5, 5, 7, 1, 1} ) +var extraExtensions = [...]asn1.ObjectIdentifier{ + oidExtensionSubjectKeyID, // Added by CAS + oidExtensionKeyUsage, // Added in CertificateConfig.ReusableConfig + oidExtensionExtendedKeyUsage, // Added in CertificateConfig.ReusableConfig + oidExtensionAuthorityKeyID, // Added by CAS + oidExtensionBasicConstraints, // Added in CertificateConfig.ReusableConfig + oidExtensionSubjectAltName, // Added in CertificateConfig.SubjectConfig.SubjectAltName + oidExtensionCertificatePolicies, // Added in CertificateConfig.ReusableConfig + oidExtensionAuthorityInfoAccess, // Added in CertificateConfig.ReusableConfig and by CAS +} + var ( oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0} oidExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} @@ -140,24 +158,28 @@ func createSubjectAlternativeNames(cert *x509.Certificate) *pb.SubjectAltNames { var newValues []asn1.RawValue for _, v := range rawValues { - switch v.Tag { - case nameTypeDNS: - if len(ret.DnsNames) == 0 { + if v.Class == asn1.ClassContextSpecific { + switch v.Tag { + case nameTypeDNS: + if len(ret.DnsNames) == 0 { + newValues = append(newValues, v) + } + case nameTypeEmail: + if len(ret.EmailAddresses) == 0 { + newValues = append(newValues, v) + } + case nameTypeIP: + if len(ret.IpAddresses) == 0 { + newValues = append(newValues, v) + } + case nameTypeURI: + if len(ret.Uris) == 0 { + newValues = append(newValues, v) + } + default: newValues = append(newValues, v) } - case nameTypeEmail: - if len(ret.EmailAddresses) == 0 { - newValues = append(newValues, v) - } - case nameTypeIP: - if len(ret.IpAddresses) == 0 { - newValues = append(newValues, v) - } - case nameTypeURI: - if len(ret.Uris) == 0 { - newValues = append(newValues, v) - } - default: + } else { newValues = append(newValues, v) } } @@ -241,7 +263,7 @@ func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper { var extraExtensions []*pb.X509Extension for _, ext := range cert.ExtraExtensions { - if !ext.Id.Equal(oidExtensionSubjectAltName) { + if isExtraExtension(ext.Id) { extraExtensions = append(extraExtensions, &pb.X509Extension{ ObjectId: createObjectID(ext.Id), Critical: ext.Critical, @@ -279,6 +301,17 @@ func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper { } } +// isExtraExtension returns true if the extension oid is not managed in a +// different way. +func isExtraExtension(oid asn1.ObjectIdentifier) bool { + for _, id := range extraExtensions { + if id.Equal(oid) { + return false + } + } + return true +} + func createObjectID(oid asn1.ObjectIdentifier) *pb.ObjectId { ret := make([]int32, len(oid)) for i, v := range oid { diff --git a/cas/cloudcas/certificate_test.go b/cas/cloudcas/certificate_test.go index 4c0505c1..d967c1dd 100644 --- a/cas/cloudcas/certificate_test.go +++ b/cas/cloudcas/certificate_test.go @@ -8,14 +8,18 @@ import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" + "net" + "net/url" "reflect" "testing" pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" ) var ( - testLeafPrivateKey = `-----BEGIN PUBLIC KEY----- + testLeafPublicKey = `-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlNnX2+xfjX a1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czgPw== -----END PUBLIC KEY----- @@ -85,7 +89,7 @@ func Test_createCertificateConfig(t *testing.T) { }, PublicKey: &pb.PublicKey{ Type: pb.PublicKey_PEM_EC_KEY, - Key: []byte(testLeafPrivateKey), + Key: []byte(testLeafPublicKey), }, }, }, false}, @@ -123,7 +127,7 @@ func Test_createPublicKey(t *testing.T) { }{ {"ok ec", args{ecCert.PublicKey}, &pb.PublicKey{ Type: pb.PublicKey_PEM_EC_KEY, - Key: []byte(testLeafPrivateKey), + Key: []byte(testLeafPublicKey), }, false}, {"ok rsa", args{rsaCert.PublicKey}, &pb.PublicKey{ Type: pb.PublicKey_PEM_RSA_KEY, @@ -192,6 +196,21 @@ func Test_createSubject(t *testing.T) { } func Test_createSubjectAlternativeNames(t *testing.T) { + marshalRawValues := func(rawValues []asn1.RawValue) []byte { + b, err := asn1.Marshal(rawValues) + if err != nil { + t.Fatal(err) + } + return b + } + + uri := func(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + t.Fatal(err) + } + return u + } type args struct { cert *x509.Certificate } @@ -201,7 +220,64 @@ func Test_createSubjectAlternativeNames(t *testing.T) { want *pb.SubjectAltNames }{ {"ok empty", args{&x509.Certificate{}}, &pb.SubjectAltNames{}}, - // TODO + {"ok dns", args{&x509.Certificate{DNSNames: []string{ + "doe.com", "doe.org", + }}}, &pb.SubjectAltNames{DnsNames: []string{"doe.com", "doe.org"}}}, + {"ok emails", args{&x509.Certificate{EmailAddresses: []string{ + "john@doe.com", "jane@doe.com", + }}}, &pb.SubjectAltNames{EmailAddresses: []string{"john@doe.com", "jane@doe.com"}}}, + {"ok ips", args{&x509.Certificate{IPAddresses: []net.IP{ + net.ParseIP("127.0.0.1"), net.ParseIP("1.2.3.4"), + net.ParseIP("::1"), net.ParseIP("2001:0db8:85a3:a0b:12f0:8a2e:0370:7334"), net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + }}}, &pb.SubjectAltNames{IpAddresses: []string{"127.0.0.1", "1.2.3.4", "::1", "2001:db8:85a3:a0b:12f0:8a2e:370:7334", "2001:db8:85a3::8a2e:370:7334"}}}, + {"ok uris", args{&x509.Certificate{URIs: []*url.URL{ + uri("mailto:john@doe.com"), uri("https://john@doe.com/hello"), + }}}, &pb.SubjectAltNames{Uris: []string{"mailto:john@doe.com", "https://john@doe.com/hello"}}}, + {"ok extensions", args{&x509.Certificate{ + ExtraExtensions: []pkix.Extension{{ + Id: []int{2, 5, 29, 17}, Critical: true, Value: []byte{ + 0x30, 0x48, 0x82, 0x0b, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x81, + 0x0c, 0x6a, 0x61, 0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x87, 0x04, 0x01, + 0x02, 0x03, 0x04, 0x87, 0x10, 0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x0a, 0x0b, 0x12, 0xf0, 0x8a, + 0x2e, 0x03, 0x70, 0x73, 0x34, 0x86, 0x13, 0x6d, 0x61, 0x69, 0x6c, 0x74, 0x6f, 0x3a, 0x6a, 0x61, + 0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, + }, + }}, + }}, &pb.SubjectAltNames{ + CustomSans: []*pb.X509Extension{{ + ObjectId: &pb.ObjectId{ObjectIdPath: []int32{2, 5, 29, 17}}, + Critical: true, + Value: []byte{ + 0x30, 0x48, 0x82, 0x0b, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x81, + 0x0c, 0x6a, 0x61, 0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x87, 0x04, 0x01, + 0x02, 0x03, 0x04, 0x87, 0x10, 0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x0a, 0x0b, 0x12, 0xf0, 0x8a, + 0x2e, 0x03, 0x70, 0x73, 0x34, 0x86, 0x13, 0x6d, 0x61, 0x69, 0x6c, 0x74, 0x6f, 0x3a, 0x6a, 0x61, + 0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, + }, + }}, + }}, + {"ok extra extensions", args{&x509.Certificate{ + DNSNames: []string{"doe.com"}, + ExtraExtensions: []pkix.Extension{{ + Id: []int{2, 5, 29, 17}, Critical: true, Value: marshalRawValues([]asn1.RawValue{ + {Class: asn1.ClassApplication, Tag: 2, IsCompound: true, Bytes: []byte{}}, + {Class: asn1.ClassContextSpecific, Tag: nameTypeDNS, Bytes: []byte("doe.com")}, + {Class: asn1.ClassContextSpecific, Tag: nameTypeEmail, Bytes: []byte("jane@doe.com")}, + {Class: asn1.ClassContextSpecific, Tag: 8, Bytes: []byte("foo.bar")}, + }), + }}, + }}, &pb.SubjectAltNames{ + DnsNames: []string{"doe.com"}, + CustomSans: []*pb.X509Extension{{ + ObjectId: &pb.ObjectId{ObjectIdPath: []int32{2, 5, 29, 17}}, + Critical: true, + Value: marshalRawValues([]asn1.RawValue{ + {Class: asn1.ClassApplication, Tag: 2, IsCompound: true, Bytes: []byte{}}, + {Class: asn1.ClassContextSpecific, Tag: nameTypeEmail, Bytes: []byte("jane@doe.com")}, + {Class: asn1.ClassContextSpecific, Tag: 8, Bytes: []byte("foo.bar")}, + }), + }}, + }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -211,3 +287,262 @@ func Test_createSubjectAlternativeNames(t *testing.T) { }) } } + +func Test_createReusableConfig(t *testing.T) { + withKU := func(ku *pb.KeyUsage) *pb.ReusableConfigWrapper { + if ku.BaseKeyUsage == nil { + ku.BaseKeyUsage = &pb.KeyUsage_KeyUsageOptions{} + } + if ku.ExtendedKeyUsage == nil { + ku.ExtendedKeyUsage = &pb.KeyUsage_ExtendedKeyUsageOptions{} + } + return &pb.ReusableConfigWrapper{ + ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{ + ReusableConfigValues: &pb.ReusableConfigValues{ + KeyUsage: ku, + }, + }, + } + } + withRCV := func(rcv *pb.ReusableConfigValues) *pb.ReusableConfigWrapper { + if rcv.KeyUsage == nil { + rcv.KeyUsage = &pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{}, + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{}, + } + } + return &pb.ReusableConfigWrapper{ + ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{ + ReusableConfigValues: rcv, + }, + } + } + + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + args args + want *pb.ReusableConfigWrapper + }{ + {"keyUsageDigitalSignature", args{&x509.Certificate{ + KeyUsage: x509.KeyUsageDigitalSignature, + }}, &pb.ReusableConfigWrapper{ + ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{ + ReusableConfigValues: &pb.ReusableConfigValues{ + KeyUsage: &pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + DigitalSignature: true, + }, + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{}, + UnknownExtendedKeyUsages: nil, + }, + CaOptions: nil, + PolicyIds: nil, + AiaOcspServers: nil, + AdditionalExtensions: nil, + }, + }, + }}, + // KeyUsage + {"KeyUsageDigitalSignature", args{&x509.Certificate{KeyUsage: x509.KeyUsageDigitalSignature}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + DigitalSignature: true, + }, + })}, + {"KeyUsageContentCommitment", args{&x509.Certificate{KeyUsage: x509.KeyUsageContentCommitment}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + ContentCommitment: true, + }, + })}, + {"KeyUsageKeyEncipherment", args{&x509.Certificate{KeyUsage: x509.KeyUsageKeyEncipherment}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + KeyEncipherment: true, + }, + })}, + {"KeyUsageDataEncipherment", args{&x509.Certificate{KeyUsage: x509.KeyUsageDataEncipherment}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + DataEncipherment: true, + }, + })}, + {"KeyUsageKeyAgreement", args{&x509.Certificate{KeyUsage: x509.KeyUsageKeyAgreement}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + KeyAgreement: true, + }, + })}, + {"KeyUsageCertSign", args{&x509.Certificate{KeyUsage: x509.KeyUsageCertSign}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + CertSign: true, + }, + })}, + {"KeyUsageCRLSign", args{&x509.Certificate{KeyUsage: x509.KeyUsageCRLSign}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + CrlSign: true, + }, + })}, + {"KeyUsageEncipherOnly", args{&x509.Certificate{KeyUsage: x509.KeyUsageEncipherOnly}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + EncipherOnly: true, + }, + })}, + {"KeyUsageDecipherOnly", args{&x509.Certificate{KeyUsage: x509.KeyUsageDecipherOnly}}, withKU(&pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + DecipherOnly: true, + }, + })}, + // ExtKeyUsage + {"ExtKeyUsageAny", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{2, 5, 29, 37, 0}}}, + })}, + {"ExtKeyUsageServerAuth", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}}, withKU(&pb.KeyUsage{ + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{ + ServerAuth: true, + }, + })}, + {"ExtKeyUsageClientAuth", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}}, withKU(&pb.KeyUsage{ + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{ + ClientAuth: true, + }, + })}, + {"ExtKeyUsageCodeSigning", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}}}, withKU(&pb.KeyUsage{ + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{ + CodeSigning: true, + }, + })}, + {"ExtKeyUsageEmailProtection", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}}}, withKU(&pb.KeyUsage{ + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{ + EmailProtection: true, + }, + })}, + {"ExtKeyUsageIPSECEndSystem", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECEndSystem}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 5}}}, + })}, + {"ExtKeyUsageIPSECTunnel", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECTunnel}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 6}}}, + })}, + {"ExtKeyUsageIPSECUser", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECUser}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 7}}}, + })}, + {"ExtKeyUsageTimeStamping", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}}}, withKU(&pb.KeyUsage{ + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{ + TimeStamping: true, + }, + })}, + {"ExtKeyUsageOCSPSigning", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning}}}, withKU(&pb.KeyUsage{ + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{ + OcspSigning: true, + }, + })}, + {"ExtKeyUsageMicrosoftServerGatedCrypto", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftServerGatedCrypto}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}}}, + })}, + {"ExtKeyUsageNetscapeServerGatedCrypto", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageNetscapeServerGatedCrypto}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{2, 16, 840, 1, 113730, 4, 1}}}, + })}, + {"ExtKeyUsageMicrosoftCommercialCodeSigning", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftCommercialCodeSigning}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}}}, + })}, + {"ExtKeyUsageMicrosoftKernelCodeSigning", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftKernelCodeSigning}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}}}, + })}, + // UnknownExtendedKeyUsages + {"UnknownExtKeyUsage", args{&x509.Certificate{UnknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 2, 3, 4}, {4, 3, 2, 1}}}}, withKU(&pb.KeyUsage{ + UnknownExtendedKeyUsages: []*pb.ObjectId{ + {ObjectIdPath: []int32{1, 2, 3, 4}}, + {ObjectIdPath: []int32{4, 3, 2, 1}}, + }, + })}, + // BasicCre + {"BasicConstraintsCAMax0", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 0, MaxPathLenZero: true}}, withRCV(&pb.ReusableConfigValues{ + CaOptions: &pb.ReusableConfigValues_CaOptions{ + IsCa: wrapperspb.Bool(true), + MaxIssuerPathLength: wrapperspb.Int32(0), + }, + })}, + {"BasicConstraintsCAMax1", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 1, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{ + CaOptions: &pb.ReusableConfigValues_CaOptions{ + IsCa: wrapperspb.Bool(true), + MaxIssuerPathLength: wrapperspb.Int32(1), + }, + })}, + {"BasicConstraintsCANoMax", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: -1, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{ + CaOptions: &pb.ReusableConfigValues_CaOptions{ + IsCa: wrapperspb.Bool(true), + MaxIssuerPathLength: nil, + }, + })}, + {"BasicConstraintsCANoMax0", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{ + CaOptions: &pb.ReusableConfigValues_CaOptions{ + IsCa: wrapperspb.Bool(true), + MaxIssuerPathLength: nil, + }, + })}, + {"BasicConstraintsNoCA", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: false, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{ + CaOptions: &pb.ReusableConfigValues_CaOptions{ + IsCa: wrapperspb.Bool(false), + MaxIssuerPathLength: nil, + }, + })}, + {"BasicConstraintsNoValid", args{&x509.Certificate{BasicConstraintsValid: false, IsCA: false, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{ + CaOptions: nil, + })}, + // PolicyIdentifiers + {"PolicyIdentifiers", args{&x509.Certificate{PolicyIdentifiers: []asn1.ObjectIdentifier{{1, 2, 3, 4}, {4, 3, 2, 1}}}}, withRCV(&pb.ReusableConfigValues{ + PolicyIds: []*pb.ObjectId{ + {ObjectIdPath: []int32{1, 2, 3, 4}}, + {ObjectIdPath: []int32{4, 3, 2, 1}}, + }, + })}, + // OCSPServer + {"OCPServers", args{&x509.Certificate{OCSPServer: []string{"https://oscp.doe.com", "https://doe.com/ocsp"}}}, withRCV(&pb.ReusableConfigValues{ + AiaOcspServers: []string{"https://oscp.doe.com", "https://doe.com/ocsp"}, + })}, + // Extensions + {"Extensions", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{ + {Id: []int{1, 2, 3, 4}, Critical: true, Value: []byte("foobar")}, + {Id: []int{2, 5, 29, 17}, Critical: true, Value: []byte("SANs")}, + {Id: []int{4, 3, 2, 1}, Critical: false, Value: []byte("zoobar")}, + }}}, withRCV(&pb.ReusableConfigValues{ + AdditionalExtensions: []*pb.X509Extension{ + {ObjectId: &pb.ObjectId{ObjectIdPath: []int32{1, 2, 3, 4}}, Critical: true, Value: []byte("foobar")}, + {ObjectId: &pb.ObjectId{ObjectIdPath: []int32{4, 3, 2, 1}}, Critical: false, Value: []byte("zoobar")}, + }, + })}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createReusableConfig(tt.args.cert); !reflect.DeepEqual(got, tt.want) { + t.Errorf("createReusableConfig() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isExtraExtension(t *testing.T) { + type args struct { + oid asn1.ObjectIdentifier + } + tests := []struct { + name string + args args + want bool + }{ + {"oidExtensionSubjectKeyID", args{oidExtensionSubjectKeyID}, false}, + {"oidExtensionKeyUsage", args{oidExtensionKeyUsage}, false}, + {"oidExtensionExtendedKeyUsage", args{oidExtensionExtendedKeyUsage}, false}, + {"oidExtensionAuthorityKeyID", args{oidExtensionAuthorityKeyID}, false}, + {"oidExtensionBasicConstraints", args{oidExtensionBasicConstraints}, false}, + {"oidExtensionSubjectAltName", args{oidExtensionSubjectAltName}, false}, + {"oidExtensionCertificatePolicies", args{oidExtensionCertificatePolicies}, false}, + {"oidExtensionAuthorityInfoAccess", args{oidExtensionAuthorityInfoAccess}, false}, + {"other", args{[]int{1, 2, 3, 4}}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isExtraExtension(tt.args.oid); got != tt.want { + t.Errorf("isExtraExtension() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index 56579ce6..ca15e27f 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -193,6 +193,39 @@ func TestNew(t *testing.T) { } } +func TestNew_register(t *testing.T) { + tmp := newCertificateAuthorityClient + newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) { + return newTestClient(credentialsFile) + } + t.Cleanup(func() { + newCertificateAuthorityClient = tmp + }) + + want := &CloudCAS{ + client: &testClient{credentialsFile: "testdata/credentials.json"}, + certificateAuthority: testAuthorityName, + } + + newFn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.CloudCAS) + if !ok { + t.Error("apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.CloudCAS) was not found") + return + } + + got, err := newFn(context.Background(), apiv1.Options{ + Certificateauthority: testAuthorityName, CredentialsFile: "testdata/credentials.json", + }) + if err != nil { + t.Errorf("New() error = %v", err) + return + } + if !reflect.DeepEqual(got, want) { + t.Errorf("New() = %v, want %v", got, want) + } + +} + func TestNew_real(t *testing.T) { if v, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS"); ok { os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS") From e17ce39e3aed928bc1f1dde17d902f2c9272b2dc Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Sep 2020 18:14:03 -0700 Subject: [PATCH 09/29] Add support for Revoke using CAS. --- authority/tls.go | 23 ++++++++++++++++++++++- db/db.go | 23 +++++++++++++++++++++++ db/simple.go | 5 +++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/authority/tls.go b/authority/tls.go index dfb9b583..7405c1dc 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -352,7 +352,28 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod { err = a.db.RevokeSSH(rci) - } else { // default to revoke x509 + } else { + // Revoke an X.509 certificate using CAS. If the certificate is not + // provided we will try to read it from the db. + var revokedCert *x509.Certificate + if revokeOpts.Crt != nil { + revokedCert = revokeOpts.Crt + } else if rci.Serial != "" { + revokedCert, _ = a.db.GetCertificate(rci.Serial) + } + + // CAS operation, note that SoftCAS (default) is a noop. + // The revoke happens when this is stored in the db. + _, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{ + Certificate: revokedCert, + Reason: rci.Reason, + ReasonCode: rci.ReasonCode, + }) + if err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + } + + // Save as revoked in the Db. err = a.db.Revoke(rci) } switch err { diff --git a/db/db.go b/db/db.go index f6a15d92..77db7e97 100644 --- a/db/db.go +++ b/db/db.go @@ -47,6 +47,7 @@ type AuthDB interface { IsSSHRevoked(sn string) (bool, error) Revoke(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error + GetCertificate(serialNumber string) (*x509.Certificate, error) StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) IsSSHHost(name string) (bool, error) @@ -187,6 +188,19 @@ func (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error { } } +// GetCertificate retrieves a certificate by the serial number. +func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) { + ans1Data, err := db.Get(certsTable, []byte(serialNumber)) + if err != nil { + return nil, errors.Wrap(err, "database Get error") + } + cert, err := x509.ParseCertificate(ans1Data) + if err != nil { + return nil, errors.Wrapf(err, "error parsing certificate with serial number %s", serialNumber) + } + return cert, nil +} + // StoreCertificate stores a certificate PEM. func (db *DB) StoreCertificate(crt *x509.Certificate) error { if err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil { @@ -288,6 +302,7 @@ type MockAuthDB struct { MIsSSHRevoked func(string) (bool, error) MRevoke func(rci *RevokedCertificateInfo) error MRevokeSSH func(rci *RevokedCertificateInfo) error + MGetCertificate func(serialNumber string) (*x509.Certificate, error) MStoreCertificate func(crt *x509.Certificate) error MUseToken func(id, tok string) (bool, error) MIsSSHHost func(principal string) (bool, error) @@ -339,6 +354,14 @@ func (m *MockAuthDB) RevokeSSH(rci *RevokedCertificateInfo) error { return m.Err } +// GetCertificate mock. +func (m *MockAuthDB) GetCertificate(serialNumber string) (*x509.Certificate, error) { + if m.MGetCertificate != nil { + return m.MGetCertificate(serialNumber) + } + return m.Ret1.(*x509.Certificate), m.Err +} + // StoreCertificate mock. func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error { if m.MStoreCertificate != nil { diff --git a/db/simple.go b/db/simple.go index 05626497..0e5426ec 100644 --- a/db/simple.go +++ b/db/simple.go @@ -46,6 +46,11 @@ func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error { return ErrNotImplemented } +// GetCertificate returns a "NotImplemented" error. +func (s *SimpleDB) GetCertificate(serialNumber string) (*x509.Certificate, error) { + return nil, ErrNotImplemented +} + // StoreCertificate returns a "NotImplemented" error. func (s *SimpleDB) StoreCertificate(crt *x509.Certificate) error { return ErrNotImplemented From 1550a21f6894399bbad73ece4226ad4cedaed480 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Sep 2020 18:14:21 -0700 Subject: [PATCH 10/29] Fix unit tests. --- authority/tls_test.go | 21 ++++++++++++++++++--- cas/softcas/softcas.go | 20 +++++++++++++++----- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/authority/tls_test.go b/authority/tls_test.go index e96a4bd9..9d8c6226 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -17,6 +17,8 @@ import ( "testing" "time" + "github.com/smallstep/certificates/cas/softcas" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/provisioner" @@ -277,7 +279,7 @@ func TestAuthority_Sign(t *testing.T) { }, "fail create cert": func(t *testing.T) *signTest { _a := testAuthority(t) - _a.x509Signer = nil + _a.x509CAService.(*softcas.SoftCAS).Signer = nil csr := getCSR(t, priv) return &signTest{ auth: _a, @@ -635,7 +637,7 @@ func TestAuthority_Renew(t *testing.T) { tests := map[string]func() (*renewTest, error){ "fail/create-cert": func() (*renewTest, error) { _a := testAuthority(t) - _a.x509Signer = nil + _a.x509CAService.(*softcas.SoftCAS).Signer = nil return &renewTest{ auth: _a, cert: cert, @@ -661,6 +663,8 @@ func TestAuthority_Renew(t *testing.T) { intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner) _a := testAuthority(t) + _a.x509CAService.(*softcas.SoftCAS).Issuer = intCert + _a.x509CAService.(*softcas.SoftCAS).Signer = intSigner _a.x509Signer = intSigner _a.x509Issuer = intCert return &renewTest{ @@ -831,7 +835,7 @@ func TestAuthority_Rekey(t *testing.T) { tests := map[string]func() (*renewTest, error){ "fail/create-cert": func() (*renewTest, error) { _a := testAuthority(t) - _a.x509Signer = nil + _a.x509CAService.(*softcas.SoftCAS).Signer = nil return &renewTest{ auth: _a, cert: cert, @@ -864,6 +868,8 @@ func TestAuthority_Rekey(t *testing.T) { intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner) _a := testAuthority(t) + _a.x509CAService.(*softcas.SoftCAS).Issuer = intCert + _a.x509CAService.(*softcas.SoftCAS).Signer = intSigner _a.x509Signer = intSigner _a.x509Issuer = intCert return &renewTest{ @@ -1107,6 +1113,9 @@ func TestAuthority_Revoke(t *testing.T) { MUseToken: func(id, tok string) (bool, error) { return true, nil }, + MGetCertificate: func(sn string) (*x509.Certificate, error) { + return nil, nil + }, Err: errors.New("force"), })) @@ -1143,6 +1152,9 @@ func TestAuthority_Revoke(t *testing.T) { MUseToken: func(id, tok string) (bool, error) { return true, nil }, + MGetCertificate: func(sn string) (*x509.Certificate, error) { + return nil, nil + }, Err: db.ErrAlreadyExists, })) @@ -1179,6 +1191,9 @@ func TestAuthority_Revoke(t *testing.T) { MUseToken: func(id, tok string) (bool, error) { return true, nil }, + MGetCertificate: func(sn string) (*x509.Certificate, error) { + return nil, errors.New("not found") + }, })) cl := jwt.Claims{ diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 751913a0..b0ce19ee 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -5,7 +5,6 @@ import ( "crypto" "crypto/x509" "errors" - "fmt" "time" "github.com/smallstep/certificates/cas/apiv1" @@ -54,8 +53,12 @@ func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 } t := now() - req.Template.NotBefore = t.Add(-1 * req.Backdate) - req.Template.NotAfter = t.Add(req.Lifetime) + if req.Template.NotBefore.IsZero() { + req.Template.NotBefore = t.Add(-1 * req.Backdate) + } + if req.Template.NotAfter.IsZero() { + req.Template.NotAfter = t.Add(req.Lifetime) + } req.Template.Issuer = c.Issuer.Subject cert, err := x509util.CreateCertificate(req.Template, c.Issuer, req.Template.PublicKey, c.Signer) @@ -98,7 +101,14 @@ func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.R }, nil } -// RevokeCertificate revokes the given certificate in step-ca. +// RevokeCertificate revokes the given certificate in step-ca. In SoftCAS this +// operation is a no-op as the actual revoke will happen when we store the entry +// in the db. func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { - return nil, fmt.Errorf("not implemented") + return &apiv1.RevokeCertificateResponse{ + Certificate: req.Certificate, + CertificateChain: []*x509.Certificate{ + c.Issuer, + }, + }, nil } From e146b3fe160e69956962efc33c2a2fb549c6f951 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Sep 2020 19:37:02 -0700 Subject: [PATCH 11/29] Add Unit tests for softcas. --- cas/softcas/softcas.go | 1 + cas/softcas/softcas_test.go | 345 ++++++++++++++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 cas/softcas/softcas_test.go diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index b0ce19ee..844c5c3c 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -53,6 +53,7 @@ func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 } t := now() + // Provisioners can also set specific values. if req.Template.NotBefore.IsZero() { req.Template.NotBefore = t.Add(-1 * req.Backdate) } diff --git a/cas/softcas/softcas_test.go b/cas/softcas/softcas_test.go new file mode 100644 index 00000000..1fd8248a --- /dev/null +++ b/cas/softcas/softcas_test.go @@ -0,0 +1,345 @@ +package softcas + +import ( + "bytes" + "context" + "crypto" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "io" + "math/big" + "reflect" + "testing" + "time" + + "go.step.sm/crypto/pemutil" + "go.step.sm/crypto/x509util" + + "github.com/smallstep/certificates/cas/apiv1" +) + +var ( + testIntermediatePem = `-----BEGIN CERTIFICATE----- +MIIBPjCB8aADAgECAhAk4aPIlsVvQg3gveApc3mIMAUGAytlcDAeMRwwGgYDVQQD +ExNTbWFsbHN0ZXAgVW5pdCBUZXN0MB4XDTIwMDkxNjAyMDgwMloXDTMwMDkxNDAy +MDgwMlowHjEcMBoGA1UEAxMTU21hbGxzdGVwIFVuaXQgVGVzdDAqMAUGAytlcAMh +ANLs3JCzECR29biut0NDsaLnh0BGij5eJx6VkdJPfS/ko0UwQzAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUup5qpZFMAFdgK7RB +xNzmUaQM8YwwBQYDK2VwA0EAAwcW25E/6bchyKwp3RRK1GXiPMDCc+hsTJxuOLWy +YM7ga829dU8X4pRcEEAcBndqCED/502excjEK7U9vCkFCg== +-----END CERTIFICATE-----` + + testIntermediateKeyPem = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEII9ZckcrDKlbhZKR0jp820Uz6mOMLFsq2JhI+Tl7WJwH +-----END PRIVATE KEY-----` +) + +var ( + testIssuer = mustIssuer() + testSigner = mustSigner() + testTemplate = &x509.Certificate{ + Subject: pkix.Name{CommonName: "test.smallstep.com"}, + DNSNames: []string{"test.smallstep.com"}, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + PublicKey: mustSigner().Public(), + SerialNumber: big.NewInt(1234), + } + testNow = time.Now() + testSignedTemplate = mustSign(testTemplate, testNow, testNow.Add(24*time.Hour)) +) + +func mockNow(t *testing.T) { + tmp := now + now = func() time.Time { + return testNow + } + t.Cleanup(func() { + now = tmp + }) +} + +func mustIssuer() *x509.Certificate { + v, err := pemutil.Parse([]byte(testIntermediatePem)) + if err != nil { + panic(err) + } + return v.(*x509.Certificate) +} + +func mustSigner() crypto.Signer { + v, err := pemutil.Parse([]byte(testIntermediateKeyPem)) + if err != nil { + panic(err) + } + return v.(crypto.Signer) +} + +func mustSign(template *x509.Certificate, notBefore, notAfter time.Time) *x509.Certificate { + tmpl := *template + tmpl.NotBefore = notBefore + tmpl.NotAfter = notAfter + tmpl.Issuer = testIssuer.Subject + cert, err := x509util.CreateCertificate(&tmpl, testIssuer, tmpl.PublicKey, testSigner) + if err != nil { + panic(err) + } + return cert +} + +func setTeeReader(t *testing.T, w *bytes.Buffer) { + t.Helper() + reader := rand.Reader + t.Cleanup(func() { + rand.Reader = reader + }) + rand.Reader = io.TeeReader(reader, w) +} + +func TestNew(t *testing.T) { + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + args args + want *SoftCAS + wantErr bool + }{ + {"ok", args{context.Background(), apiv1.Options{Issuer: testIssuer, Signer: testSigner}}, &SoftCAS{Issuer: testIssuer, Signer: testSigner}, false}, + {"fail no issuer", args{context.Background(), apiv1.Options{Signer: testSigner}}, nil, true}, + {"fail no signer", args{context.Background(), apiv1.Options{Issuer: testIssuer}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNew_register(t *testing.T) { + newFn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.SoftCAS) + if !ok { + t.Error("apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.SoftCAS) was not found") + return + } + + want := &SoftCAS{ + Issuer: testIssuer, + Signer: testSigner, + } + + got, err := newFn(context.Background(), apiv1.Options{Issuer: testIssuer, Signer: testSigner}) + if err != nil { + t.Errorf("New() error = %v", err) + return + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("New() = %v, want %v", got, want) + } +} + +func TestSoftCAS_CreateCertificate(t *testing.T) { + mockNow(t) + // Set rand.Reader to EOF + buf := new(bytes.Buffer) + setTeeReader(t, buf) + rand.Reader = buf + + tmplNotBefore := *testTemplate + tmplNotBefore.NotBefore = testNow + + tmplNotAfter := *testTemplate + tmplNotAfter.NotAfter = testNow.Add(24 * time.Hour) + + tmplWithLifetime := *testTemplate + tmplWithLifetime.NotBefore = testNow + tmplWithLifetime.NotAfter = testNow.Add(24 * time.Hour) + + tmplNoSerial := *testTemplate + tmplNoSerial.SerialNumber = nil + + type fields struct { + Issuer *x509.Certificate + Signer crypto.Signer + } + type args struct { + req *apiv1.CreateCertificateRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.CreateCertificateResponse + wantErr bool + }{ + {"ok", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{ + Template: testTemplate, Lifetime: 24 * time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: testSignedTemplate, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, + {"ok with notBefore", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{ + Template: &tmplNotBefore, Lifetime: 24 * time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: testSignedTemplate, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, + {"ok with notBefore+notAfter", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{ + Template: &tmplWithLifetime, Lifetime: 24 * time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: testSignedTemplate, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, + {"fail template", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true}, + {"fail lifetime", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{Template: testTemplate}}, nil, true}, + {"fail CreateCertificate", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{ + Template: &tmplNoSerial, + Lifetime: 24 * time.Hour, + }}, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SoftCAS{ + Issuer: tt.fields.Issuer, + Signer: tt.fields.Signer, + } + got, err := c.CreateCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SoftCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SoftCAS.CreateCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSoftCAS_RenewCertificate(t *testing.T) { + mockNow(t) + + // Set rand.Reader to EOF + buf := new(bytes.Buffer) + setTeeReader(t, buf) + rand.Reader = buf + + tmplNoSerial := *testTemplate + tmplNoSerial.SerialNumber = nil + + type fields struct { + Issuer *x509.Certificate + Signer crypto.Signer + } + type args struct { + req *apiv1.RenewCertificateRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.RenewCertificateResponse + wantErr bool + }{ + {"ok", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{ + Template: testTemplate, Lifetime: 24 * time.Hour, + }}, &apiv1.RenewCertificateResponse{ + Certificate: testSignedTemplate, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, + {"fail template", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true}, + {"fail lifetime", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{Template: testTemplate}}, nil, true}, + {"fail CreateCertificate", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{ + Template: &tmplNoSerial, + Lifetime: 24 * time.Hour, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SoftCAS{ + Issuer: tt.fields.Issuer, + Signer: tt.fields.Signer, + } + got, err := c.RenewCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SoftCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SoftCAS.RenewCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSoftCAS_RevokeCertificate(t *testing.T) { + type fields struct { + Issuer *x509.Certificate + Signer crypto.Signer + } + type args struct { + req *apiv1.RevokeCertificateRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.RevokeCertificateResponse + wantErr bool + }{ + {"ok", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{ + Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}}, + Reason: "test reason", + ReasonCode: 1, + }}, &apiv1.RevokeCertificateResponse{ + Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}}, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, + {"ok no cert", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{ + Reason: "test reason", + ReasonCode: 1, + }}, &apiv1.RevokeCertificateResponse{ + Certificate: nil, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, + {"ok empty", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{}}, &apiv1.RevokeCertificateResponse{ + Certificate: nil, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SoftCAS{ + Issuer: tt.fields.Issuer, + Signer: tt.fields.Signer, + } + got, err := c.RevokeCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("SoftCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SoftCAS.RevokeCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_now(t *testing.T) { + t0 := time.Now() + t1 := now() + if t1.Sub(t0) > time.Second { + t.Errorf("now() = %s, want ~%s", t1, t0) + } +} From 8957e5e5a24bc46b70d6b90120cebdd80dae1ef4 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Sep 2020 12:34:42 -0700 Subject: [PATCH 12/29] Add missing tests --- cas/apiv1/extension_test.go | 95 +++++++++++++++++++++++++- cas/apiv1/options_test.go | 131 ++++++++++++++++++++++++++++++++++++ cas/apiv1/registry_test.go | 90 +++++++++++++++++++++++++ cas/apiv1/services_test.go | 23 +++++++ cas/cas_test.go | 60 +++++++++++++++++ 5 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 cas/apiv1/options_test.go create mode 100644 cas/apiv1/registry_test.go create mode 100644 cas/apiv1/services_test.go create mode 100644 cas/cas_test.go diff --git a/cas/apiv1/extension_test.go b/cas/apiv1/extension_test.go index 113e3de1..7d6fe4dc 100644 --- a/cas/apiv1/extension_test.go +++ b/cas/apiv1/extension_test.go @@ -1,8 +1,8 @@ package apiv1 import ( + "crypto/x509" "crypto/x509/pkix" - "fmt" "reflect" "testing" ) @@ -49,7 +49,98 @@ func TestCreateCertificateAuthorityExtension(t *testing.T) { } if !reflect.DeepEqual(got, tt.want) { t.Errorf("CreateCertificateAuthorityExtension() = %v, want %v", got, tt.want) - fmt.Printf("%x\n", got.Value) + } + }) + } +} + +func TestFindCertificateAuthorityExtension(t *testing.T) { + expected := pkix.Extension{ + Id: oidStepCertificateAuthority, + Value: []byte("fake data"), + } + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + args args + want pkix.Extension + want1 bool + }{ + {"first", args{&x509.Certificate{Extensions: []pkix.Extension{ + expected, + {Id: []int{1, 2, 3, 4}}, + }}}, expected, true}, + {"last", args{&x509.Certificate{Extensions: []pkix.Extension{ + {Id: []int{1, 2, 3, 4}}, + {Id: []int{2, 3, 4, 5}}, + expected, + }}}, expected, true}, + {"fail", args{&x509.Certificate{Extensions: []pkix.Extension{ + {Id: []int{1, 2, 3, 4}}, + }}}, pkix.Extension{}, false}, + {"fail ExtraExtensions", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{ + expected, + {Id: []int{1, 2, 3, 4}}, + }}}, pkix.Extension{}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := FindCertificateAuthorityExtension(tt.args.cert) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("FindCertificateAuthorityExtension() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("FindCertificateAuthorityExtension() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestRemoveCertificateAuthorityExtension(t *testing.T) { + caExt := pkix.Extension{ + Id: oidStepCertificateAuthority, + Value: []byte("fake data"), + } + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + args args + want *x509.Certificate + }{ + {"first", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{ + caExt, + {Id: []int{1, 2, 3, 4}}, + }}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{ + {Id: []int{1, 2, 3, 4}}, + }}}, + {"last", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{ + {Id: []int{1, 2, 3, 4}}, + caExt, + }}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{ + {Id: []int{1, 2, 3, 4}}, + }}}, + {"missing", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{ + {Id: []int{1, 2, 3, 4}}, + }}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{ + {Id: []int{1, 2, 3, 4}}, + }}}, + {"extensions", args{&x509.Certificate{Extensions: []pkix.Extension{ + caExt, + {Id: []int{1, 2, 3, 4}}, + }}}, &x509.Certificate{Extensions: []pkix.Extension{ + caExt, + {Id: []int{1, 2, 3, 4}}, + }}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + RemoveCertificateAuthorityExtension(tt.args.cert) + if !reflect.DeepEqual(tt.args.cert, tt.want) { + t.Errorf("RemoveCertificateAuthorityExtension() cert = %v, want %v", tt.args.cert, tt.want) } }) } diff --git a/cas/apiv1/options_test.go b/cas/apiv1/options_test.go new file mode 100644 index 00000000..ddf26f7f --- /dev/null +++ b/cas/apiv1/options_test.go @@ -0,0 +1,131 @@ +package apiv1 + +import ( + "context" + "crypto" + "crypto/x509" + "sync" + "testing" +) + +type testCAS struct { + name string +} + +func (t *testCAS) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { + return nil, nil +} + +func (t *testCAS) RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error) { + return nil, nil +} + +func (t *testCAS) RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) { + return nil, nil +} + +func mockRegister(t *testing.T) { + t.Helper() + Register(SoftCAS, func(ctx context.Context, opts Options) (CertificateAuthorityService, error) { + return &testCAS{name: SoftCAS}, nil + }) + Register(CloudCAS, func(ctx context.Context, opts Options) (CertificateAuthorityService, error) { + return &testCAS{name: CloudCAS}, nil + }) + t.Cleanup(func() { + registry = new(sync.Map) + }) +} + +func TestOptions_Validate(t *testing.T) { + mockRegister(t) + type fields struct { + Type string + CredentialsFile string + Certificateauthority string + Issuer *x509.Certificate + Signer crypto.Signer + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"empty", fields{}, false}, + {"SoftCAS", fields{SoftCAS, "", "", nil, nil}, false}, + {"CloudCAS", fields{CloudCAS, "", "", nil, nil}, false}, + {"softcas", fields{"softcas", "", "", nil, nil}, false}, + {"CLOUDCAS", fields{"CLOUDCAS", "", "", nil, nil}, false}, + {"fail", fields{"FailCAS", "", "", nil, nil}, true}, + } + t.Run("nil", func(t *testing.T) { + var o *Options + if err := o.Validate(); err != nil { + t.Errorf("Options.Validate() error = %v, wantErr %v", err, false) + } + }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &Options{ + Type: tt.fields.Type, + CredentialsFile: tt.fields.CredentialsFile, + Certificateauthority: tt.fields.Certificateauthority, + Issuer: tt.fields.Issuer, + Signer: tt.fields.Signer, + } + if err := o.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestOptions_HasType(t *testing.T) { + mockRegister(t) + + type fields struct { + Type string + CredentialsFile string + Certificateauthority string + Issuer *x509.Certificate + Signer crypto.Signer + } + type args struct { + t Type + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + {"empty", fields{}, args{}, true}, + {"SoftCAS", fields{SoftCAS, "", "", nil, nil}, args{"SoftCAS"}, true}, + {"CloudCAS", fields{CloudCAS, "", "", nil, nil}, args{"CloudCAS"}, true}, + {"softcas", fields{"softcas", "", "", nil, nil}, args{SoftCAS}, true}, + {"CLOUDCAS", fields{"CLOUDCAS", "", "", nil, nil}, args{CloudCAS}, true}, + {"UnknownCAS", fields{"UnknownCAS", "", "", nil, nil}, args{"UnknownCAS"}, true}, + {"fail", fields{CloudCAS, "", "", nil, nil}, args{"SoftCAS"}, false}, + {"fail", fields{SoftCAS, "", "", nil, nil}, args{"CloudCAS"}, false}, + } + t.Run("nil", func(t *testing.T) { + var o *Options + if got := o.HasType(SoftCAS); got != true { + t.Errorf("Options.HasType() = %v, want %v", got, true) + } + }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &Options{ + Type: tt.fields.Type, + CredentialsFile: tt.fields.CredentialsFile, + Certificateauthority: tt.fields.Certificateauthority, + Issuer: tt.fields.Issuer, + Signer: tt.fields.Signer, + } + if got := o.HasType(tt.args.t); got != tt.want { + t.Errorf("Options.HasType() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cas/apiv1/registry_test.go b/cas/apiv1/registry_test.go new file mode 100644 index 00000000..ce510d13 --- /dev/null +++ b/cas/apiv1/registry_test.go @@ -0,0 +1,90 @@ +package apiv1 + +import ( + "context" + "fmt" + "reflect" + "sync" + "testing" +) + +func TestRegister(t *testing.T) { + t.Cleanup(func() { + registry = new(sync.Map) + }) + type args struct { + t Type + fn CertificateAuthorityServiceNewFunc + } + tests := []struct { + name string + args args + want CertificateAuthorityService + wantErr bool + }{ + {"ok", args{"TestCAS", func(ctx context.Context, opts Options) (CertificateAuthorityService, error) { + return &testCAS{}, nil + }}, &testCAS{}, false}, + {"error", args{"ErrorCAS", func(ctx context.Context, opts Options) (CertificateAuthorityService, error) { + return nil, fmt.Errorf("an error") + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Register(tt.args.t, tt.args.fn) + fmt.Println(registry) + fn, ok := registry.LoadAndDelete(tt.args.t.String()) + if !ok { + t.Errorf("Register() failed") + return + } + got, err := fn.(CertificateAuthorityServiceNewFunc)(context.Background(), Options{}) + if (err != nil) != tt.wantErr { + t.Errorf("CertificateAuthorityServiceNewFunc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CertificateAuthorityServiceNewFunc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLoadCertificateAuthorityServiceNewFunc(t *testing.T) { + mockRegister(t) + type args struct { + t Type + } + tests := []struct { + name string + args args + want CertificateAuthorityService + wantOk bool + }{ + {"default", args{""}, &testCAS{name: SoftCAS}, true}, + {"SoftCAS", args{"SoftCAS"}, &testCAS{name: SoftCAS}, true}, + {"CloudCAS", args{"CloudCAS"}, &testCAS{name: CloudCAS}, true}, + {"softcas", args{"softcas"}, &testCAS{name: SoftCAS}, true}, + {"cloudcas", args{"cloudcas"}, &testCAS{name: CloudCAS}, true}, + {"FailCAS", args{"FailCAS"}, nil, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fn, ok := LoadCertificateAuthorityServiceNewFunc(tt.args.t) + if ok != tt.wantOk { + t.Errorf("LoadCertificateAuthorityServiceNewFunc() ok = %v, want %v", ok, tt.wantOk) + return + } + if ok { + got, err := fn(context.Background(), Options{}) + if err != nil { + t.Errorf("CertificateAuthorityServiceNewFunc() error = %v", err) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CertificateAuthorityServiceNewFunc() = %v, want %v", got, tt.want) + } + } + }) + } +} diff --git a/cas/apiv1/services_test.go b/cas/apiv1/services_test.go new file mode 100644 index 00000000..f9ab9042 --- /dev/null +++ b/cas/apiv1/services_test.go @@ -0,0 +1,23 @@ +package apiv1 + +import "testing" + +func TestType_String(t *testing.T) { + tests := []struct { + name string + t Type + want string + }{ + {"default", "", "softcas"}, + {"SoftCAS", SoftCAS, "softcas"}, + {"CloudCAS", CloudCAS, "cloudcas"}, + {"UnknownCAS", "UnknownCAS", "unknowncas"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.t.String(); got != tt.want { + t.Errorf("Type.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cas/cas_test.go b/cas/cas_test.go new file mode 100644 index 00000000..a01e8dab --- /dev/null +++ b/cas/cas_test.go @@ -0,0 +1,60 @@ +package cas + +import ( + "context" + "crypto/ed25519" + "crypto/x509" + "crypto/x509/pkix" + "reflect" + "testing" + + "github.com/smallstep/certificates/cas/softcas" + + "github.com/smallstep/certificates/cas/apiv1" +) + +func TestNew(t *testing.T) { + expected := &softcas.SoftCAS{ + Issuer: &x509.Certificate{Subject: pkix.Name{CommonName: "Test Issuer"}}, + Signer: ed25519.PrivateKey{}, + } + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + args args + want CertificateAuthorityService + wantErr bool + }{ + {"ok default", args{context.Background(), apiv1.Options{ + Issuer: &x509.Certificate{Subject: pkix.Name{CommonName: "Test Issuer"}}, + Signer: ed25519.PrivateKey{}, + }}, expected, false}, + {"ok softcas", args{context.Background(), apiv1.Options{ + Type: "softcas", + Issuer: &x509.Certificate{Subject: pkix.Name{CommonName: "Test Issuer"}}, + Signer: ed25519.PrivateKey{}, + }}, expected, false}, + {"ok SoftCAS", args{context.Background(), apiv1.Options{ + Type: "SoftCAS", + Issuer: &x509.Certificate{Subject: pkix.Name{CommonName: "Test Issuer"}}, + Signer: ed25519.PrivateKey{}, + }}, expected, false}, + {"fail empty", args{context.Background(), apiv1.Options{}}, (*softcas.SoftCAS)(nil), true}, + {"fail type", args{context.Background(), apiv1.Options{Type: "FailCAS"}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %#v, want %v", got, tt.want) + } + }) + } +} From f2dd5c48cc2a90fe955dff0c99c8ee864922bd6b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Sep 2020 12:41:43 -0700 Subject: [PATCH 13/29] Fix linting errors. --- cas/cloudcas/certificate.go | 6 ------ cas/cloudcas/cloudcas.go | 5 ----- cas/cloudcas/cloudcas_test.go | 2 +- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go index c29eee60..4bbec946 100644 --- a/cas/cloudcas/certificate.go +++ b/cas/cloudcas/certificate.go @@ -38,15 +38,9 @@ var extraExtensions = [...]asn1.ObjectIdentifier{ var ( oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0} - oidExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} - oidExtKeyUsageClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} - oidExtKeyUsageCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3} - oidExtKeyUsageEmailProtection = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4} oidExtKeyUsageIPSECEndSystem = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5} oidExtKeyUsageIPSECTunnel = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6} oidExtKeyUsageIPSECUser = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7} - oidExtKeyUsageTimeStamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} - oidExtKeyUsageOCSPSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9} oidExtKeyUsageMicrosoftServerGatedCrypto = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3} oidExtKeyUsageNetscapeServerGatedCrypto = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1} oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22} diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index e6aa9a49..f8679fe8 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -31,11 +31,6 @@ type CertificateAuthorityClient interface { RevokeCertificate(ctx context.Context, req *pb.RevokeCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) } -var ( - stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64} - stepOIDCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 2)...) -) - // 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. diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index ca15e27f..1b2770af 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -343,7 +343,7 @@ func TestCloudCAS_createCertificate(t *testing.T) { {"fail create id", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true}, } - // Pre-calulate rand.Random + // Pre-calculate rand.Random buf := new(bytes.Buffer) setTeeReader(t, buf) for i := 0; i < len(tests)-1; i++ { From 60515d92c5d88d6ef74e787a33af08522716b340 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Sep 2020 13:31:26 -0700 Subject: [PATCH 14/29] Remove unnecessary properties. --- authority/authority.go | 45 ++++++++++-------------------- authority/authority_test.go | 6 ++-- authority/options.go | 13 +++++++-- authority/tls_test.go | 55 +++++++++++++++++++++---------------- 4 files changed, 60 insertions(+), 59 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index a51e986f..5d8f9b04 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -2,7 +2,6 @@ package authority import ( "context" - "crypto" "crypto/sha256" "crypto/x509" "encoding/hex" @@ -39,8 +38,6 @@ type Authority struct { x509CAService cas.CertificateAuthorityService rootX509Certs []*x509.Certificate federatedX509Certs []*x509.Certificate - x509Signer crypto.Signer - x509Issuer *x509.Certificate certificates *sync.Map // SSH CA @@ -110,9 +107,9 @@ func NewEmbedded(opts ...Option) (*Authority, error) { return nil, errors.New("cannot create an authority without a configuration") case len(a.rootX509Certs) == 0 && a.config.Root.HasEmpties(): return nil, errors.New("cannot create an authority without a root certificate") - case a.x509Issuer == nil && a.config.IntermediateCert == "": + case a.x509CAService == nil && a.config.IntermediateCert == "": return nil, errors.New("cannot create an authority without an issuer certificate") - case a.x509Signer == nil && a.config.IntermediateKey == "": + case a.x509CAService == nil && a.config.IntermediateKey == "": return nil, errors.New("cannot create an authority without an issuer signer") } @@ -188,38 +185,26 @@ func (a *Authority) init() error { a.certificates.Store(hex.EncodeToString(sum[:]), crt) } - // Read intermediate and create X509 signer. - if a.x509Signer == nil { - crt, err := pemutil.ReadCertificate(a.config.IntermediateCert) - if err != nil { - return err - } - a.x509Issuer = crt - - // Read signer only is the CAS is the default one. - if a.config.CAS.HasType(casapi.SoftCAS) { - signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ - SigningKey: a.config.IntermediateKey, - Password: []byte(a.config.Password), - }) - if err != nil { - return err - } - a.x509Signer = signer - } - } - - // Initialize the X.509 CA Service if it has not been set in the options + // Initialize the X.509 CA Service if it has not been set in the options. if a.x509CAService == nil { var options casapi.Options if a.config.CAS != nil { options = *a.config.CAS } - // Set issuer and signer for default CAS. + // Read intermediate and create X509 signer for default CAS. if options.HasType(casapi.SoftCAS) { - options.Issuer = a.x509Issuer - options.Signer = a.x509Signer + options.Issuer, err = pemutil.ReadCertificate(a.config.IntermediateCert) + if err != nil { + return err + } + options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: []byte(a.config.Password), + }) + if err != nil { + return err + } } a.x509CAService, err = cas.New(context.Background(), options) diff --git a/authority/authority_test.go b/authority/authority_test.go index 54de0040..8b003572 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -143,8 +143,7 @@ func TestAuthorityNew(t *testing.T) { assert.Equals(t, auth.rootX509Certs[0], root) assert.True(t, auth.initOnce) - assert.NotNil(t, auth.x509Signer) - assert.NotNil(t, auth.x509Issuer) + assert.NotNil(t, auth.x509CAService) for _, p := range tc.config.AuthorityConfig.Provisioners { var _p provisioner.Interface _p, ok = auth.provisioners.Load(p.GetID()) @@ -256,8 +255,7 @@ func TestNewEmbedded(t *testing.T) { if err == nil { assert.True(t, got.initOnce) assert.NotNil(t, got.rootX509Certs) - assert.NotNil(t, got.x509Signer) - assert.NotNil(t, got.x509Issuer) + assert.NotNil(t, got.x509CAService) } }) } diff --git a/authority/options.go b/authority/options.go index 9457f276..da5a8f88 100644 --- a/authority/options.go +++ b/authority/options.go @@ -8,6 +8,8 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/cas" + casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/kms" "golang.org/x/crypto/ssh" @@ -92,8 +94,15 @@ func WithKeyManager(k kms.KeyManager) Option { // WithX509Signer defines the signer used to sign X509 certificates. func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option { return func(a *Authority) error { - a.x509Issuer = crt - a.x509Signer = s + srv, err := cas.New(context.Background(), casapi.Options{ + Type: casapi.SoftCAS, + Issuer: crt, + Signer: s, + }) + if err != nil { + return err + } + a.x509CAService = srv return nil } } diff --git a/authority/tls_test.go b/authority/tls_test.go index 9d8c6226..75f9e234 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -55,6 +55,14 @@ func (m *certificateDurationEnforcer) Enforce(cert *x509.Certificate) error { return nil } +func getDefaultIssuer(a *Authority) *x509.Certificate { + return a.x509CAService.(*softcas.SoftCAS).Issuer +} + +func getDefaultSigner(a *Authority) crypto.Signer { + return a.x509CAService.(*softcas.SoftCAS).Signer +} + func generateCertificate(t *testing.T, commonName string, sans []string, opts ...interface{}) *x509.Certificate { t.Helper() @@ -541,17 +549,15 @@ ZYtQ9Ot36qc= assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"}) } assert.Equals(t, leaf.Issuer, intermediate.Subject) - assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256) assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA) - assert.Equals(t, leaf.ExtKeyUsage, - []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) + assert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) + issuer := getDefaultIssuer(a) subjectKeyID, err := generateSubjectKeyID(pub) assert.FatalError(t, err) assert.Equals(t, leaf.SubjectKeyId, subjectKeyID) - - assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId) + assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId) // Verify Provisioner OID found := 0 @@ -587,8 +593,7 @@ ZYtQ9Ot36qc= } } assert.Equals(t, found, 1) - - realIntermediate, err := x509.ParseCertificate(a.x509Issuer.Raw) + realIntermediate, err := x509.ParseCertificate(issuer.Raw) assert.FatalError(t, err) assert.Equals(t, intermediate, realIntermediate) } @@ -616,17 +621,20 @@ func TestAuthority_Renew(t *testing.T) { NotAfter: provisioner.NewTimeDuration(na1), } + issuer := getDefaultIssuer(a) + signer := getDefaultSigner(a) + cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), - withSigner(a.x509Issuer, a.x509Signer)) + withSigner(issuer, signer)) certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID), - withSigner(a.x509Issuer, a.x509Signer)) + withSigner(issuer, signer)) type renewTest struct { auth *Authority @@ -665,8 +673,6 @@ func TestAuthority_Renew(t *testing.T) { _a := testAuthority(t) _a.x509CAService.(*softcas.SoftCAS).Issuer = intCert _a.x509CAService.(*softcas.SoftCAS).Signer = intSigner - _a.x509Signer = intSigner - _a.x509Issuer = intCert return &renewTest{ auth: _a, cert: cert, @@ -733,8 +739,9 @@ func TestAuthority_Renew(t *testing.T) { assert.Equals(t, leaf.SubjectKeyId, subjectKeyID) // We did not change the intermediate before renewing. - if a.x509Issuer.SerialNumber == tc.auth.x509Issuer.SerialNumber { - assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId) + authIssuer := getDefaultIssuer(tc.auth) + if issuer.SerialNumber == authIssuer.SerialNumber { + assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { //skip SubjectKeyIdentifier @@ -754,7 +761,7 @@ func TestAuthority_Renew(t *testing.T) { } } else { // We did change the intermediate before renewing. - assert.Equals(t, leaf.AuthorityKeyId, tc.auth.x509Issuer.SubjectKeyId) + assert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { //skip SubjectKeyIdentifier @@ -782,7 +789,7 @@ func TestAuthority_Renew(t *testing.T) { } } - realIntermediate, err := x509.ParseCertificate(tc.auth.x509Issuer.Raw) + realIntermediate, err := x509.ParseCertificate(authIssuer.Raw) assert.FatalError(t, err) assert.Equals(t, intermediate, realIntermediate) } @@ -813,17 +820,20 @@ func TestAuthority_Rekey(t *testing.T) { NotAfter: provisioner.NewTimeDuration(na1), } + issuer := getDefaultIssuer(a) + signer := getDefaultSigner(a) + cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID), - withSigner(a.x509Issuer, a.x509Signer)) + withSigner(issuer, signer)) certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"}, withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()), withDefaultASN1DN(a.config.AuthorityConfig.Template), withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID), - withSigner(a.x509Issuer, a.x509Signer)) + withSigner(issuer, signer)) type renewTest struct { auth *Authority @@ -870,8 +880,6 @@ func TestAuthority_Rekey(t *testing.T) { _a := testAuthority(t) _a.x509CAService.(*softcas.SoftCAS).Issuer = intCert _a.x509CAService.(*softcas.SoftCAS).Signer = intSigner - _a.x509Signer = intSigner - _a.x509Issuer = intCert return &renewTest{ auth: _a, cert: cert, @@ -948,8 +956,9 @@ func TestAuthority_Rekey(t *testing.T) { } // We did not change the intermediate before renewing. - if a.x509Issuer.SerialNumber == tc.auth.x509Issuer.SerialNumber { - assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId) + authIssuer := getDefaultIssuer(tc.auth) + if issuer.SerialNumber == authIssuer.SerialNumber { + assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { //skip SubjectKeyIdentifier @@ -969,7 +978,7 @@ func TestAuthority_Rekey(t *testing.T) { } } else { // We did change the intermediate before renewing. - assert.Equals(t, leaf.AuthorityKeyId, tc.auth.x509Issuer.SubjectKeyId) + assert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId) // Compare extensions: they can be in a different order for _, ext1 := range tc.cert.Extensions { //skip SubjectKeyIdentifier @@ -997,7 +1006,7 @@ func TestAuthority_Rekey(t *testing.T) { } } - realIntermediate, err := x509.ParseCertificate(tc.auth.x509Issuer.Raw) + realIntermediate, err := x509.ParseCertificate(authIssuer.Raw) assert.FatalError(t, err) assert.Equals(t, intermediate, realIntermediate) } From 91aa1e87f105b5cda9d129aae38aa9faa416d2e8 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Sep 2020 13:51:49 -0700 Subject: [PATCH 15/29] Do not use go 1.15 methods. --- cas/apiv1/registry_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas/apiv1/registry_test.go b/cas/apiv1/registry_test.go index ce510d13..225efb7f 100644 --- a/cas/apiv1/registry_test.go +++ b/cas/apiv1/registry_test.go @@ -33,7 +33,7 @@ func TestRegister(t *testing.T) { t.Run(tt.name, func(t *testing.T) { Register(tt.args.t, tt.args.fn) fmt.Println(registry) - fn, ok := registry.LoadAndDelete(tt.args.t.String()) + fn, ok := registry.Load(tt.args.t.String()) if !ok { t.Errorf("Register() failed") return From 884a6f5dd03aef8cc866b906d062ef988d624210 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Sep 2020 14:03:26 -0700 Subject: [PATCH 16/29] Skip test on CI. --- cas/cloudcas/cloudcas_test.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index 1b2770af..4b1a721b 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -239,18 +239,22 @@ func TestNew_real(t *testing.T) { opts apiv1.Options } tests := []struct { - name string - args args - wantErr bool + name string + skipOnCI bool + args args + wantErr bool }{ - {"fail default credentials", args{context.Background(), apiv1.Options{Certificateauthority: testAuthorityName}}, true}, - {"fail certificate authority", args{context.Background(), apiv1.Options{}}, true}, - {"fail with credentials", args{context.Background(), apiv1.Options{ + {"fail default credentials", true, args{context.Background(), apiv1.Options{Certificateauthority: testAuthorityName}}, true}, + {"fail certificate authority", false, args{context.Background(), apiv1.Options{}}, true}, + {"fail with credentials", false, args{context.Background(), apiv1.Options{ Certificateauthority: testAuthorityName, CredentialsFile: "testdata/missing.json", }}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + if tt.skipOnCI && os.Getenv("CI") == "true" { + t.SkipNow() + } _, err := New(tt.args.ctx, tt.args.opts) if (err != nil) != tt.wantErr { t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) From fa099f2ae28d38f3daefec7c9a262d86ff5ea654 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Sep 2020 15:11:25 -0700 Subject: [PATCH 17/29] Change method name. --- cas/apiv1/options.go | 4 ++-- cas/apiv1/options_test.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 05b83db2..fc674ab0 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -41,8 +41,8 @@ func (o *Options) Validate() error { return nil } -// HasType returns if the options have the given type. -func (o *Options) HasType(t Type) bool { +// Is returns if the options have the given type. +func (o *Options) Is(t Type) bool { if o == nil { return t.String() == SoftCAS } diff --git a/cas/apiv1/options_test.go b/cas/apiv1/options_test.go index ddf26f7f..91da8372 100644 --- a/cas/apiv1/options_test.go +++ b/cas/apiv1/options_test.go @@ -80,7 +80,7 @@ func TestOptions_Validate(t *testing.T) { } } -func TestOptions_HasType(t *testing.T) { +func TestOptions_Is(t *testing.T) { mockRegister(t) type fields struct { @@ -110,8 +110,8 @@ func TestOptions_HasType(t *testing.T) { } t.Run("nil", func(t *testing.T) { var o *Options - if got := o.HasType(SoftCAS); got != true { - t.Errorf("Options.HasType() = %v, want %v", got, true) + if got := o.Is(SoftCAS); got != true { + t.Errorf("Options.Is() = %v, want %v", got, true) } }) for _, tt := range tests { @@ -123,8 +123,8 @@ func TestOptions_HasType(t *testing.T) { Issuer: tt.fields.Issuer, Signer: tt.fields.Signer, } - if got := o.HasType(tt.args.t); got != tt.want { - t.Errorf("Options.HasType() = %v, want %v", got, tt.want) + if got := o.Is(tt.args.t); got != tt.want { + t.Errorf("Options.Is() = %v, want %v", got, tt.want) } }) } From 38fa780775af5a660c0682076784a62a27ba6e2d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Sep 2020 15:27:20 -0700 Subject: [PATCH 18/29] Add interface to get root certificate from CAS. This change makes easier the configuration of cloudCAS as it does not require to configure the root or intermediate certificate in the ca.json. CloudCAS will get the root certificate using the configured certificateAuthority. --- authority/authority.go | 75 ++++++++++++++++++++--------------- authority/config.go | 21 +++++----- cas/apiv1/requests.go | 12 ++++++ cas/apiv1/services.go | 6 +++ cas/cloudcas/cloudcas.go | 34 ++++++++++++++++ cas/cloudcas/cloudcas_test.go | 63 +++++++++++++++++++++++++++-- 6 files changed, 167 insertions(+), 44 deletions(-) diff --git a/authority/authority.go b/authority/authority.go index 5d8f9b04..0721f40f 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -133,6 +133,14 @@ func (a *Authority) init() error { var err error + // Initialize step-ca Database if it's not already initialized with WithDB. + // If a.config.DB is nil then a simple, barebones in memory DB will be used. + if a.db == nil { + if a.db, err = db.New(a.config.DB); err != nil { + return err + } + } + // Initialize key manager if it has not been set in the options. if a.keyManager == nil { var options kmsapi.Options @@ -145,12 +153,43 @@ func (a *Authority) init() error { } } - // Initialize step-ca Database if it's not already initialized with WithDB. - // If a.config.DB is nil then a simple, barebones in memory DB will be used. - if a.db == nil { - if a.db, err = db.New(a.config.DB); err != nil { + // Initialize the X.509 CA Service if it has not been set in the options. + if a.x509CAService == nil { + var options casapi.Options + if a.config.CAS != nil { + options = *a.config.CAS + } + + // Read intermediate and create X509 signer for default CAS. + if options.Is(casapi.SoftCAS) { + options.Issuer, err = pemutil.ReadCertificate(a.config.IntermediateCert) + if err != nil { + return err + } + options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: []byte(a.config.Password), + }) + if err != nil { + return err + } + } + + a.x509CAService, err = cas.New(context.Background(), options) + if err != nil { return err } + + // Get root certificate from CAS. + if srv, ok := a.x509CAService.(casapi.CertificateAuthorityGetter); ok { + resp, err := srv.GetCertificateAuthority(&casapi.GetCertificateAuthorityRequest{ + Name: options.Certificateauthority, + }) + if err != nil { + return err + } + a.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate) + } } // Read root certificates and store them in the certificates map. @@ -185,34 +224,6 @@ func (a *Authority) init() error { a.certificates.Store(hex.EncodeToString(sum[:]), crt) } - // Initialize the X.509 CA Service if it has not been set in the options. - if a.x509CAService == nil { - var options casapi.Options - if a.config.CAS != nil { - options = *a.config.CAS - } - - // Read intermediate and create X509 signer for default CAS. - if options.HasType(casapi.SoftCAS) { - options.Issuer, err = pemutil.ReadCertificate(a.config.IntermediateCert) - if err != nil { - return err - } - options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ - SigningKey: a.config.IntermediateKey, - Password: []byte(a.config.Password), - }) - if err != nil { - return err - } - } - - a.x509CAService, err = cas.New(context.Background(), options) - if err != nil { - return nil - } - } - // Decrypt and load SSH keys var tmplVars templates.Step if a.config.SSH != nil { diff --git a/authority/config.go b/authority/config.go index 168b360d..48d56952 100644 --- a/authority/config.go +++ b/authority/config.go @@ -181,19 +181,22 @@ func (c *Config) Validate() error { case c.Address == "": return errors.New("address cannot be empty") - case c.Root.HasEmpties(): - return errors.New("root cannot be empty") - - case c.IntermediateCert == "": - return errors.New("crt cannot be empty") - - case c.IntermediateKey == "" && c.CAS.HasType(cas.SoftCAS): - return errors.New("key cannot be empty") - case len(c.DNSNames) == 0: return errors.New("dnsNames cannot be empty") } + // The default CAS requires root, crt and key. + if c.CAS.Is(cas.SoftCAS) { + switch { + case c.Root.HasEmpties(): + return errors.New("root cannot be empty") + case c.IntermediateCert == "": + return errors.New("crt cannot be empty") + case c.IntermediateKey == "": + return errors.New("key cannot be empty") + } + } + // Validate address (a port is required) if _, _, err := net.SplitHostPort(c.Address); err != nil { return errors.Errorf("invalid address %s", c.Address) diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index 3f7349ca..2a233b8a 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -46,3 +46,15 @@ type RevokeCertificateResponse struct { Certificate *x509.Certificate CertificateChain []*x509.Certificate } + +// GetCertificateAuthorityRequest is the request used to get the root +// certificate from a CAS. +type GetCertificateAuthorityRequest struct { + Name string +} + +// GetCertificateAuthorityResponse is the response that contains +// the root certificate. +type GetCertificateAuthorityResponse struct { + RootCertificate *x509.Certificate +} diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 5c6de1c3..f41650d8 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -12,6 +12,12 @@ type CertificateAuthorityService interface { RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) } +// CertificateAuthorityGetter is an interface implemented by a +// CertificateAuthorityService that has a method to get the root certificate. +type CertificateAuthorityGetter interface { + GetCertificateAuthority(req *GetCertificateAuthorityRequest) (*GetCertificateAuthorityResponse, error) +} + // Type represents the CAS type used. type Type string diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index f8679fe8..4866a797 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -29,6 +29,7 @@ func init() { 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) } // recocationCodeMap maps revocation reason codes from RFC 5280, to Google CAS @@ -84,6 +85,39 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { }, 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 { diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index 4b1a721b..f2b708f5 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -74,9 +74,10 @@ zemu3bhWLFaGg3s8i+HTEhw4RqkHP74vF7AVYp88bAw= ) type testClient struct { - credentialsFile string - certificate *pb.Certificate - err error + credentialsFile string + certificate *pb.Certificate + certificateAuthority *pb.CertificateAuthority + err error } func newTestClient(credentialsFile string) (CertificateAuthorityClient, error) { @@ -96,6 +97,9 @@ func okTestClient() *testClient { PemCertificate: testSignedCertificate, PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, }, + certificateAuthority: &pb.CertificateAuthority{ + PemCaCertificates: []string{testIntermediateCertificate, testRootCertificate}, + }, } } @@ -114,6 +118,9 @@ func badTestClient() *testClient { PemCertificate: "not a pem cert", PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, }, + certificateAuthority: &pb.CertificateAuthority{ + PemCaCertificates: []string{testIntermediateCertificate, "not a pem cert"}, + }, } } @@ -134,6 +141,10 @@ func (c *testClient) RevokeCertificate(ctx context.Context, req *pb.RevokeCertif return c.certificate, c.err } +func (c *testClient) GetCertificateAuthority(ctx context.Context, req *pb.GetCertificateAuthorityRequest, opts ...gax.CallOption) (*pb.CertificateAuthority, error) { + return c.certificateAuthority, c.err +} + func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate { t.Helper() crt, err := parseCertificate(pemCert) @@ -263,6 +274,52 @@ func TestNew_real(t *testing.T) { } } +func TestCloudCAS_GetCertificateAuthority(t *testing.T) { + root := mustParseCertificate(t, testRootCertificate) + type fields struct { + client CertificateAuthorityClient + certificateAuthority string + } + type args struct { + req *apiv1.GetCertificateAuthorityRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.GetCertificateAuthorityResponse + wantErr bool + }{ + {"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, &apiv1.GetCertificateAuthorityResponse{ + RootCertificate: root, + }, false}, + {"ok with name", fields{okTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{ + Name: testCertificateName, + }}, &apiv1.GetCertificateAuthorityResponse{ + RootCertificate: root, + }, false}, + {"fail GetCertificateAuthority", fields{failTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true}, + {"fail bad root", fields{badTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true}, + {"fail no pems", fields{&testClient{certificateAuthority: &pb.CertificateAuthority{}}, testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CloudCAS{ + client: tt.fields.client, + certificateAuthority: tt.fields.certificateAuthority, + } + got, err := c.GetCertificateAuthority(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("CloudCAS.GetCertificateAuthority() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CloudCAS.GetCertificateAuthority() = %v, want %v", got, tt.want) + } + }) + } +} + func TestCloudCAS_CreateCertificate(t *testing.T) { type fields struct { client CertificateAuthorityClient From 8e6d7accf859a3287a6832c904fe35727e6db323 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Sep 2020 17:09:46 -0700 Subject: [PATCH 19/29] Do not add the CRL distribution points extension. This extension is added by CloudCAS. --- cas/cloudcas/certificate.go | 34 +++++++++++++++++--------------- cas/cloudcas/certificate_test.go | 4 +++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go index 4bbec946..dc9584e3 100644 --- a/cas/cloudcas/certificate.go +++ b/cas/cloudcas/certificate.go @@ -15,25 +15,27 @@ import ( ) var ( - oidExtensionSubjectKeyID = []int{2, 5, 29, 14} - oidExtensionKeyUsage = []int{2, 5, 29, 15} - oidExtensionExtendedKeyUsage = []int{2, 5, 29, 37} - oidExtensionAuthorityKeyID = []int{2, 5, 29, 35} - oidExtensionBasicConstraints = []int{2, 5, 29, 19} - oidExtensionSubjectAltName = []int{2, 5, 29, 17} - oidExtensionCertificatePolicies = []int{2, 5, 29, 32} - oidExtensionAuthorityInfoAccess = []int{1, 3, 6, 1, 5, 5, 7, 1, 1} + oidExtensionSubjectKeyID = []int{2, 5, 29, 14} + oidExtensionKeyUsage = []int{2, 5, 29, 15} + oidExtensionExtendedKeyUsage = []int{2, 5, 29, 37} + oidExtensionAuthorityKeyID = []int{2, 5, 29, 35} + oidExtensionBasicConstraints = []int{2, 5, 29, 19} + oidExtensionSubjectAltName = []int{2, 5, 29, 17} + oidExtensionCRLDistributionPoints = []int{2, 5, 29, 31} + oidExtensionCertificatePolicies = []int{2, 5, 29, 32} + oidExtensionAuthorityInfoAccess = []int{1, 3, 6, 1, 5, 5, 7, 1, 1} ) var extraExtensions = [...]asn1.ObjectIdentifier{ - oidExtensionSubjectKeyID, // Added by CAS - oidExtensionKeyUsage, // Added in CertificateConfig.ReusableConfig - oidExtensionExtendedKeyUsage, // Added in CertificateConfig.ReusableConfig - oidExtensionAuthorityKeyID, // Added by CAS - oidExtensionBasicConstraints, // Added in CertificateConfig.ReusableConfig - oidExtensionSubjectAltName, // Added in CertificateConfig.SubjectConfig.SubjectAltName - oidExtensionCertificatePolicies, // Added in CertificateConfig.ReusableConfig - oidExtensionAuthorityInfoAccess, // Added in CertificateConfig.ReusableConfig and by CAS + oidExtensionSubjectKeyID, // Added by CAS + oidExtensionKeyUsage, // Added in CertificateConfig.ReusableConfig + oidExtensionExtendedKeyUsage, // Added in CertificateConfig.ReusableConfig + oidExtensionAuthorityKeyID, // Added by CAS + oidExtensionBasicConstraints, // Added in CertificateConfig.ReusableConfig + oidExtensionSubjectAltName, // Added in CertificateConfig.SubjectConfig.SubjectAltName + oidExtensionCRLDistributionPoints, // Added by CAS + oidExtensionCertificatePolicies, // Added in CertificateConfig.ReusableConfig + oidExtensionAuthorityInfoAccess, // Added in CertificateConfig.ReusableConfig and by CAS } var ( diff --git a/cas/cloudcas/certificate_test.go b/cas/cloudcas/certificate_test.go index d967c1dd..4f30ea79 100644 --- a/cas/cloudcas/certificate_test.go +++ b/cas/cloudcas/certificate_test.go @@ -501,8 +501,9 @@ func Test_createReusableConfig(t *testing.T) { // Extensions {"Extensions", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{ {Id: []int{1, 2, 3, 4}, Critical: true, Value: []byte("foobar")}, - {Id: []int{2, 5, 29, 17}, Critical: true, Value: []byte("SANs")}, + {Id: []int{2, 5, 29, 17}, Critical: true, Value: []byte("SANs")}, // {Id: []int{4, 3, 2, 1}, Critical: false, Value: []byte("zoobar")}, + {Id: []int{2, 5, 29, 31}, Critical: false, Value: []byte("CRL Distribution points")}, }}}, withRCV(&pb.ReusableConfigValues{ AdditionalExtensions: []*pb.X509Extension{ {ObjectId: &pb.ObjectId{ObjectIdPath: []int32{1, 2, 3, 4}}, Critical: true, Value: []byte("foobar")}, @@ -534,6 +535,7 @@ func Test_isExtraExtension(t *testing.T) { {"oidExtensionAuthorityKeyID", args{oidExtensionAuthorityKeyID}, false}, {"oidExtensionBasicConstraints", args{oidExtensionBasicConstraints}, false}, {"oidExtensionSubjectAltName", args{oidExtensionSubjectAltName}, false}, + {"oidExtensionCRLDistributionPoints", args{oidExtensionCRLDistributionPoints}, false}, {"oidExtensionCertificatePolicies", args{oidExtensionCertificatePolicies}, false}, {"oidExtensionAuthorityInfoAccess", args{oidExtensionAuthorityInfoAccess}, false}, {"other", args{[]int{1, 2, 3, 4}}, true}, From 072adc906e712d126b5802ed6737ad482555709e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 22 Sep 2020 13:23:48 -0700 Subject: [PATCH 20/29] Print root fingerprint for CloudCAS. --- authority/authority.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/authority/authority.go b/authority/authority.go index 0721f40f..3fdb67cc 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -189,6 +189,8 @@ func (a *Authority) init() error { return err } a.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate) + sum := sha256.Sum256(resp.RootCertificate.Raw) + log.Printf("Using root fingerprint '%s'", hex.EncodeToString(sum[:])) } } From 42ce78ed43c08320ad420e299cac46dc64b0e82a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 22 Sep 2020 13:32:48 -0700 Subject: [PATCH 21/29] Add initial docs for CAS. --- docs/cas.md | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 docs/cas.md diff --git a/docs/cas.md b/docs/cas.md new file mode 100644 index 00000000..b1f41c02 --- /dev/null +++ b/docs/cas.md @@ -0,0 +1,212 @@ +# Certificate Management Services + +This document describes how to use a certificate management service or CAS to +sign X.509 certificates requests. + +A CAS is a system that implements an API to sign certificate requests, the +difference between CAS and KMS is that the latter can sign any data, while CAS +is intended to sign only X.509 certificates. + +`step-ca` defines an interface that can be implemented to support other +services, currently only CloudCAS and the default SoftCAS are implemented. + +The `CertificateAuthorityService` is defined in the package +`github.com/smallstep/certificates/cas/apiv1` and it is: + +```go +type CertificateAuthorityService interface { + CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) + RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error) + RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) +} +``` + +The same package defines another interface that is used to get the root +certificates from the CAS: + +```go +type CertificateAuthorityGetter interface { + GetCertificateAuthority(req *GetCertificateAuthorityRequest) (*GetCertificateAuthorityResponse, error) +} +``` + +## SoftCAS + +SoftCAS is the default implementation supported by `step-ca`. No special +configurations are required to enable it. + +SoftCAS generally uses certificates and keys in the filesystem, but a KMS can +also be used instead of a key file for signing certificates. See [KMS](kms.md) +for more information. + +## CloudCAS + +CloudCAS is the implementation of the `CertificateAuthorityService` and +`CertificateAuthorityGetter` interfaces using [Google's Certificate Authority +Service](https://cloud.google.com/certificate-authority-service/). + +Before enabling CloudCAS in `step-ca` you do some steps in Google Cloud Console +or using `gcloud` CLI: + +1. Create or define a project to use. Let's say the name is `smallstep-cas-test`. +2. Create the KMS keyring and keys for root and intermediate certificates: + + ```sh + # Create key ring + gcloud kms keyrings create kr1 --location us-west1 + # Create key for Root certificate + gcloud kms keys create k1 \ + --location us-west1 \ + --keyring kr1 \ + --purpose asymmetric-signing \ + --default-algorithm ec-sign-p256-sha256 \ + --protection-level software + # Create key for Intermediate certicate + gcloud kms keys create k2 \ + --location us-west1 \ + --keyring kr1 \ + --purpose asymmetric-signing \ + --default-algorithm ec-sign-p256-sha256 \ + --protection-level software + + # Put the resource name for version 1 of the new KMS keys into a shell variable. + # This will be used in the other instructions below. + KMS_ROOT_KEY_VERSION=$(gcloud kms keys versions describe 1 --key k1 --keyring kr1 --location us-west1 --format "value(name)") + KMS_INTERMEDIATE_KEY_VERSION=$(gcloud kms keys versions describe 1 --key k2 --keyring kr1 --location us-west1 --format "value(name)") + ``` + +3. Enable the CA service API. You can do it on the console or running: + + ```sh + gcloud services enable privateca.googleapis.com + ``` + +4. Configure IAM. Create a service account using Google Console or running: + + ```sh + # Create service account + gcloud iam service-accounts create step-ca-sa \ + --project smallstep-cas-test \ + --description "Step-CA Service Account" \ + --display-name "Step-CA Service Account" + # Add permissions to use the privateca API + gcloud projects add-iam-policy-binding smallstep-cas-test \ + --member=serviceAccount:step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \ + --role=roles/privateca.caManager + # Download the credentials.file + gcloud iam service-accounts keys create credentials.json \ + --iam-account step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com + ``` + +5. Create a Root CA. You can do this on the console or running: + + ```sh + gcloud alpha privateca roots create prod-root-ca \ + --kms-key-version "$KMS_ROOT_KEY_VERSION" \ + --subject "CN=Example Root CA, O=Example LLC" \ + --max-chain-length 2 + ``` + +6. Create an Intermediate CA. You can do this on the console or running: + + ```sh + gcloud alpha privateca subordinates create prod-intermediate-ca \ + --issuer prod-root-ca --issuer-location us-west1 \ + --kms-key-version "$KMS_INTERMEDIATE_KEY_VERSION" \ + --subject "CN=Example Intermediate CA, O=Example LLC" \ + --reusable-config "subordinate-server-tls-pathlen-0" + ``` + +Not it's time to enable it in `step-ca` adding the new property `"cas"` must be added +to the `ca.json`. + +```json +{ + "cas": { + "type": "cloudCAS", + "credentialsFile": "/path/to/credentials.json", + "certificateAuthority": "projects//locations//certificateAuthorities/" + } +} +``` + +* **type** defines the name of the CAS to use, _cloudCAS_ must be used to enable it. +* **credentialsFile** defines the path to a Google Cloud credential file with + access to Google's Certificate AuthorityService. We created this file before + in step 4. Instead of setting this property, the environment variable + `GOOGLE_APPLICATION_CREDENTIALS` can be pointed to the file to use. Or if the + `step-ca` is running in Google Cloud, the default service account in the + machine can also be used. +* **certificateAuthority** defines the Google Cloud resource to the intermediate + (or subordinated) certificate to use. We created this resource in step 6. + +As we said before, the CloudCAS implementation in `step-ca` also defines the +interface `CertificateAuthorityGetter`, this allows `step-ca` to automatically +download the root certificate from Cloud CAS. In the `ca.json` now you don't +need to configure `"root"`, and because the intermediate is in Google Cloud, +`"crt"` and `"key"` are no needed. A full `ca.json` can look like: + +```json +{ + "address": ":443", + "dnsNames": ["ca.example.com"], + "logger": {"format": "text"}, + "db": { + "type": "badger", + "dataSource": "/home/jane/.step/db", + }, + "cas": { + "type": "cloudCAS", + "credentialsFile": "/home/jane/.step/credentials.json", + "certificateAuthority": "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/prod-intermediate-ca" + }, + "authority": { + "provisioners": [ + { + "type": "JWK", + "name": "jane@example.com", + "key": { + "use": "sig", + "kty": "EC", + "kid": "ehFT9BkVOY5k_eIiMax0ZxVZCe2hlDVkMwZ2Y78av4s", + "crv": "P-256", + "alg": "ES256", + "x": "GtEftN0_ED1lNc2SEUJDXV9EMi7JY-kqINPIEQJIkjM", + "y": "8HYFdNe1MbWcbclF-hU1L80SCmMcZQI6vZfTOXfPOjg" + }, + "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiSjBSWnY5UFZrM3JKRUJkem5RbExzZyJ9.Fiwvo-RIKU5G6v5udeCT1nlX87ElxrocP2FcgNs3AqEz5OH9H4suew.NmzUJR_9xv8ynQC8.dqOveA_G5kn5lxjxnEZoJCystnJMVYLkZ_8CVzfJQhYchbZfNk_-FKdIuQxeWWBzvmomsILFNtLOIUoqSt30qk83lFyGQWN8Ke2bK5DhuwojF7RI_UqkMyiKP0F28Z4ZFhfQP5D2ZT_stoFaMlU8eak0-T8MOiBIfdAJTWM9x2DN-68mtUBuL5z5eU8bqsxELnjGauD_GHTdnduOosmYsw8vp_PmffTTwqUzDFH1RhkeSmRFRZntAizZMGYkxLamquHI3Jvuqiv4eeJ3yLqh3Ppyo_mVQKnxM7P9TyTxcvLkb2dB3K-cItl1fpsz92cy8euKsKG8n5-hKFRyPfY.j7jBN7nUwatoSsIZuNIwHA" + } + ] + }, + "tls": { + "cipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" + ], + "minVersion": 1.2, + "maxVersion": 1.3, + "renegotiation": false + } +} +``` + +The we only need to run `step-ca` as usual, but this time, the CA will print the +root fingerprint too: + +```sh +$ step-ca /home/jane/.step/config/ca.json +2020/09/22 13:17:15 Using root fingerprint '3ef16343cf0952eedbe2b843066bb798fa7a7bceb16aa285e8b0399f661b28b7' +2020/09/22 13:17:15 Serving HTTPS on :9000 ... +``` + +We will need to bootstrap once our environment using the printed fingerprint: + +```sh +step ca bootstrap --ca-url https://ca.example.com --fingerprint 3ef16343cf0952eedbe2b843066bb798fa7a7bceb16aa285e8b0399f661b28b7 +``` + +And now we can sign sign a certificate as always: + +```sh +step ca certificate test.example.com test.crt test.key +``` From 066c7ee10b1bc10824dcf2b6c6c3ca3a75a6192a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Sep 2020 12:37:29 -0700 Subject: [PATCH 22/29] Fix iam permissions. --- docs/cas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cas.md b/docs/cas.md index b1f41c02..002e42d5 100644 --- a/docs/cas.md +++ b/docs/cas.md @@ -92,7 +92,7 @@ or using `gcloud` CLI: # Add permissions to use the privateca API gcloud projects add-iam-policy-binding smallstep-cas-test \ --member=serviceAccount:step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \ - --role=roles/privateca.caManager + --role=roles/privateca.certificateRequester # Download the credentials.file gcloud iam service-accounts keys create credentials.json \ --iam-account step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com From 52d857a30228742787174834b8cf612f9bac88ff Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Sep 2020 12:43:25 -0700 Subject: [PATCH 23/29] Update CloudCAS instructions. --- docs/cas.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/cas.md b/docs/cas.md index 002e42d5..3318fb04 100644 --- a/docs/cas.md +++ b/docs/cas.md @@ -85,23 +85,24 @@ or using `gcloud` CLI: ```sh # Create service account - gcloud iam service-accounts create step-ca-sa \ + gcloud iam service-accounts create mariano-ca-sa \ --project smallstep-cas-test \ - --description "Step-CA Service Account" \ - --display-name "Step-CA Service Account" + --description "Mariano-CA Service Account" \ + --display-name "mariano-CA Service Account" # Add permissions to use the privateca API gcloud projects add-iam-policy-binding smallstep-cas-test \ - --member=serviceAccount:step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \ + --member=serviceAccount:mariano-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \ + --role=roles/privateca.caManager \ --role=roles/privateca.certificateRequester # Download the credentials.file gcloud iam service-accounts keys create credentials.json \ - --iam-account step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com + --iam-account mariano-ca-sa@smallstep-cas-test.iam.gserviceaccount.com ``` 5. Create a Root CA. You can do this on the console or running: ```sh - gcloud alpha privateca roots create prod-root-ca \ + gcloud beta privateca roots create prod-root-ca \ --kms-key-version "$KMS_ROOT_KEY_VERSION" \ --subject "CN=Example Root CA, O=Example LLC" \ --max-chain-length 2 @@ -110,7 +111,7 @@ or using `gcloud` CLI: 6. Create an Intermediate CA. You can do this on the console or running: ```sh - gcloud alpha privateca subordinates create prod-intermediate-ca \ + gcloud beta privateca subordinates create prod-intermediate-ca \ --issuer prod-root-ca --issuer-location us-west1 \ --kms-key-version "$KMS_INTERMEDIATE_KEY_VERSION" \ --subject "CN=Example Intermediate CA, O=Example LLC" \ From 7d779e12db2984ca05cfca9f91f17725afa9544f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 24 Sep 2020 12:45:19 -0700 Subject: [PATCH 24/29] Change service account name. --- docs/cas.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/cas.md b/docs/cas.md index 3318fb04..f84db8bb 100644 --- a/docs/cas.md +++ b/docs/cas.md @@ -85,18 +85,18 @@ or using `gcloud` CLI: ```sh # Create service account - gcloud iam service-accounts create mariano-ca-sa \ + gcloud iam service-accounts create step-ca-sa \ --project smallstep-cas-test \ - --description "Mariano-CA Service Account" \ - --display-name "mariano-CA Service Account" + --description "Step-CA Service Account" \ + --display-name "Step-CA Service Account" # Add permissions to use the privateca API gcloud projects add-iam-policy-binding smallstep-cas-test \ - --member=serviceAccount:mariano-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \ + --member=serviceAccount:step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \ --role=roles/privateca.caManager \ --role=roles/privateca.certificateRequester # Download the credentials.file gcloud iam service-accounts keys create credentials.json \ - --iam-account mariano-ca-sa@smallstep-cas-test.iam.gserviceaccount.com + --iam-account step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com ``` 5. Create a Root CA. You can do this on the console or running: From 3f55f22b2e6404e151e6c678cf8aeb8420e67d2d Mon Sep 17 00:00:00 2001 From: Carl Tashian Date: Tue, 29 Sep 2020 15:24:15 -0700 Subject: [PATCH 25/29] Update cas.md Added `--location` flag to a couple of the commands --- docs/cas.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/cas.md b/docs/cas.md index f84db8bb..f51f31d8 100644 --- a/docs/cas.md +++ b/docs/cas.md @@ -103,6 +103,7 @@ or using `gcloud` CLI: ```sh gcloud beta privateca roots create prod-root-ca \ + --location us-west1 \ --kms-key-version "$KMS_ROOT_KEY_VERSION" \ --subject "CN=Example Root CA, O=Example LLC" \ --max-chain-length 2 @@ -112,7 +113,9 @@ or using `gcloud` CLI: ```sh gcloud beta privateca subordinates create prod-intermediate-ca \ - --issuer prod-root-ca --issuer-location us-west1 \ + --location us-west1 \ + --issuer prod-root-ca \ + --issuer-location us-west1 \ --kms-key-version "$KMS_INTERMEDIATE_KEY_VERSION" \ --subject "CN=Example Intermediate CA, O=Example LLC" \ --reusable-config "subordinate-server-tls-pathlen-0" From 329f401e58153ad0af5fe3a819168cfb1004f87f Mon Sep 17 00:00:00 2001 From: Carl Tashian Date: Tue, 29 Sep 2020 15:46:53 -0700 Subject: [PATCH 26/29] Update cas.md Needed to run two commands to set up IAM roles because passing `--role` twice only uses the second value passed. --- docs/cas.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/cas.md b/docs/cas.md index f51f31d8..b2c6782e 100644 --- a/docs/cas.md +++ b/docs/cas.md @@ -92,7 +92,9 @@ or using `gcloud` CLI: # Add permissions to use the privateca API gcloud projects add-iam-policy-binding smallstep-cas-test \ --member=serviceAccount:step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \ - --role=roles/privateca.caManager \ + --role=roles/privateca.caManager + gcloud projects add-iam-policy-binding smallstep-cas-test \ + --member=serviceAccount:step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \ --role=roles/privateca.certificateRequester # Download the credentials.file gcloud iam service-accounts keys create credentials.json \ From 8381e9bd17d4900a0cf55d10b0522e8bbdcb4d63 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 5 Oct 2020 17:20:22 -0700 Subject: [PATCH 27/29] Fix typos. --- cas/apiv1/extension.go | 2 +- db/db.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cas/apiv1/extension.go b/cas/apiv1/extension.go index de341fbb..bbe2525a 100644 --- a/cas/apiv1/extension.go +++ b/cas/apiv1/extension.go @@ -13,7 +13,7 @@ var ( oidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...) ) -// CertificateAuthorityExtension is type used to encode the certificate +// CertificateAuthorityExtension type is used to encode the certificate // authority extension. type CertificateAuthorityExtension struct { Type string diff --git a/db/db.go b/db/db.go index 77db7e97..2643e577 100644 --- a/db/db.go +++ b/db/db.go @@ -190,11 +190,11 @@ func (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error { // GetCertificate retrieves a certificate by the serial number. func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) { - ans1Data, err := db.Get(certsTable, []byte(serialNumber)) + asn1Data, err := db.Get(certsTable, []byte(serialNumber)) if err != nil { return nil, errors.Wrap(err, "database Get error") } - cert, err := x509.ParseCertificate(ans1Data) + cert, err := x509.ParseCertificate(asn1Data) if err != nil { return nil, errors.Wrapf(err, "error parsing certificate with serial number %s", serialNumber) } From d64427487daad7b3370381f8c062f3c1f5c625e9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 5 Oct 2020 17:39:44 -0700 Subject: [PATCH 28/29] Add comment about the missing error check. --- authority/tls.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/authority/tls.go b/authority/tls.go index 7405c1dc..faa0ebf8 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -354,7 +354,9 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error err = a.db.RevokeSSH(rci) } else { // Revoke an X.509 certificate using CAS. If the certificate is not - // provided we will try to read it from the db. + // provided we will try to read it from the db. If the read fails we + // won't throw an error as it will be responsability of the CAS + // implementation to require a certificate. var revokedCert *x509.Certificate if revokeOpts.Crt != nil { revokedCert = revokeOpts.Crt From 3e0ab8fba78831859c4ef632b7f0d9fcdd0f65fb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 5 Oct 2020 18:00:50 -0700 Subject: [PATCH 29/29] Fix typo. --- authority/tls.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authority/tls.go b/authority/tls.go index faa0ebf8..f22f4624 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -355,7 +355,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error } else { // Revoke an X.509 certificate using CAS. If the certificate is not // provided we will try to read it from the db. If the read fails we - // won't throw an error as it will be responsability of the CAS + // won't throw an error as it will be responsibility of the CAS // implementation to require a certificate. var revokedCert *x509.Certificate if revokeOpts.Crt != nil {