From 38fa780775af5a660c0682076784a62a27ba6e2d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 21 Sep 2020 15:27:20 -0700 Subject: [PATCH] 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