From 2b4b902975eafd1b76d06350d9f9d7210907e2c5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 23 Oct 2020 15:04:09 -0700 Subject: [PATCH] Add initial support for `step ca init` with cloud cas. Fixes smallstep/cli#363 --- cas/apiv1/options.go | 13 ++ cas/apiv1/requests.go | 77 ++++++++++ cas/apiv1/services.go | 7 + cas/cas.go | 36 ++++- cas/cloudcas/certificate.go | 44 ++++++ cas/cloudcas/cloudcas.go | 268 +++++++++++++++++++++++++++++++++- cas/cloudcas/cloudcas_test.go | 60 ++++++++ cas/softcas/softcas.go | 125 ++++++++++++++-- commands/onboard.go | 10 +- go.mod | 10 +- go.sum | 6 + pki/pki.go | 175 +++++++++++----------- 12 files changed, 717 insertions(+), 114 deletions(-) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 3ee1434c..e8072437 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "github.com/pkg/errors" + "github.com/smallstep/certificates/kms" ) // Options represents the configuration options used to select and configure the @@ -24,6 +25,18 @@ type Options struct { // They are configured in ca.json crt and key properties. Issuer *x509.Certificate `json:"-"` Signer crypto.Signer `json:"-"` + + // IsCreator is set to true when we're creating a certificate authority. Is + // used to skip some validations when initializing a CertificateAuthority. + IsCreator bool `json:"-"` + + // KeyManager is the KMS used to generate keys in SoftCAS. + KeyManager kms.KeyManager `json:"-"` + + // Project and Location are parameters used in CloudCAS to create a new + // certificate authority. + Project string `json:"-"` + Location string `json:"-"` } // Validate checks the fields in Options. diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index 2a233b8a..5305ff43 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -1,8 +1,53 @@ package apiv1 import ( + "crypto" "crypto/x509" "time" + + "github.com/smallstep/certificates/kms/apiv1" +) + +// CertificateAuthorityType indicates the type of Certificate Authority to +// create. +type CertificateAuthorityType int + +const ( + // RootCA is the type used to create a self-signed certificate suitable for + // use as a root CA. + RootCA CertificateAuthorityType = iota + 1 + + // IntermediateCA is the type used to create a subordinated certificate that + // can be used to sign additional leaf certificates. + IntermediateCA +) + +// SignatureAlgorithm used for cryptographic signing. +type SignatureAlgorithm int + +const ( + // Not specified. + UnspecifiedSignAlgorithm SignatureAlgorithm = iota + // RSASSA-PKCS1-v1_5 key and a SHA256 digest. + SHA256WithRSA + // RSASSA-PKCS1-v1_5 key and a SHA384 digest. + SHA384WithRSA + // RSASSA-PKCS1-v1_5 key and a SHA512 digest. + SHA512WithRSA + // RSASSA-PSS key with a SHA256 digest. + SHA256WithRSAPSS + // RSASSA-PSS key with a SHA384 digest. + SHA384WithRSAPSS + // RSASSA-PSS key with a SHA512 digest. + SHA512WithRSAPSS + // ECDSA on the NIST P-256 curve with a SHA256 digest. + ECDSAWithSHA256 + // ECDSA on the NIST P-384 curve with a SHA384 digest. + ECDSAWithSHA384 + // ECDSA on the NIST P-521 curve with a SHA512 digest. + ECDSAWithSHA512 + // EdDSA on Curve25519 with a SHA512 digest. + PureEd25519 ) // CreateCertificateRequest is the request used to sign a new certificate. @@ -58,3 +103,35 @@ type GetCertificateAuthorityRequest struct { type GetCertificateAuthorityResponse struct { RootCertificate *x509.Certificate } + +// CreateCertificateAuthorityRequest ... +// This is a work in progress +type CreateCertificateAuthorityRequest struct { + Name string + Type CertificateAuthorityType + Template *x509.Certificate + Lifetime time.Duration + Backdate time.Duration + RequestID string + Project string + Location string + + // Parent is the signer of the new CertificateAuthority. + Parent *CreateCertificateAuthorityResponse + + // CreateKey defines the KMS CreateKeyRequest to use when creating a new + // CertificateAuthority. If CreateKey is nil, a default algorithm will be + // used. + CreateKey *apiv1.CreateKeyRequest +} + +// CreateCertificateAuthorityResponse ... +// This is a work in progress +type CreateCertificateAuthorityResponse struct { + Name string + Certificate *x509.Certificate + CertificateChain []*x509.Certificate + PublicKey crypto.PublicKey + PrivateKey crypto.PrivateKey + Signer crypto.Signer +} diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index f41650d8..58a8f139 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -18,6 +18,13 @@ type CertificateAuthorityGetter interface { GetCertificateAuthority(req *GetCertificateAuthorityRequest) (*GetCertificateAuthorityResponse, error) } +// CertificateAuthorityCreator is an interface implamented by a +// CertificateAuthorityService that has a method to create a new certificate +// authority. +type CertificateAuthorityCreator interface { + CreateCertificateAuthority(req *CreateCertificateAuthorityRequest) (*CreateCertificateAuthorityResponse, error) +} + // Type represents the CAS type used. type Type string diff --git a/cas/cas.go b/cas/cas.go index 3df83460..0592fed5 100644 --- a/cas/cas.go +++ b/cas/cas.go @@ -6,14 +6,16 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" - - // Enable default implementation - _ "github.com/smallstep/certificates/cas/softcas" + "github.com/smallstep/certificates/cas/softcas" ) // CertificateAuthorityService is the interface implemented by all the CAS. type CertificateAuthorityService = apiv1.CertificateAuthorityService +// CertificateAuthorityCreator is the interface implemented by all CAS that can create a new authority. +type CertificateAuthorityCreator = apiv1.CertificateAuthorityCreator + +// New creates a new CertificateAuthorityService using the given options. func New(ctx context.Context, opts apiv1.Options) (CertificateAuthorityService, error) { if err := opts.Validate(); err != nil { return nil, err @@ -26,7 +28,33 @@ func New(ctx context.Context, opts apiv1.Options) (CertificateAuthorityService, fn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(t) if !ok { - return nil, errors.Errorf("unsupported kms type '%s'", t) + return nil, errors.Errorf("unsupported cas type '%s'", t) } return fn(ctx, opts) } + +// NewCreator creates a new CertificateAuthorityCreator using the given options. +func NewCreator(ctx context.Context, opts apiv1.Options) (CertificateAuthorityCreator, error) { + t := apiv1.Type(strings.ToLower(opts.Type)) + if t == apiv1.DefaultCAS { + t = apiv1.SoftCAS + } + if t == apiv1.SoftCAS { + return &softcas.SoftCAS{ + KeyManager: opts.KeyManager, + }, nil + } + + svc, err := New(ctx, opts) + if err != nil { + return nil, err + } + + creator, ok := svc.(CertificateAuthorityCreator) + if !ok { + + return nil, errors.Errorf("cas type '%s' does not implements CertificateAuthorityCreator", t) + } + + return creator, nil +} diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go index dc9584e3..51e221b4 100644 --- a/cas/cloudcas/certificate.go +++ b/cas/cloudcas/certificate.go @@ -8,8 +8,10 @@ import ( "crypto/x509/pkix" "encoding/asn1" "encoding/pem" + "fmt" "github.com/pkg/errors" + kmsapi "github.com/smallstep/certificates/kms/apiv1" pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -326,3 +328,45 @@ func findExtraExtension(cert *x509.Certificate, oid asn1.ObjectIdentifier) (pkix } return pkix.Extension{}, false } + +func createKeyVersionSpec(alg kmsapi.SignatureAlgorithm, bits int) (*pb.CertificateAuthority_KeyVersionSpec, error) { + switch alg { + case kmsapi.UnspecifiedSignAlgorithm, kmsapi.ECDSAWithSHA256: + return &pb.CertificateAuthority_KeyVersionSpec{ + KeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{ + Algorithm: pb.CertificateAuthority_EC_P256_SHA256, + }, + }, nil + case kmsapi.ECDSAWithSHA384: + return &pb.CertificateAuthority_KeyVersionSpec{ + KeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{ + Algorithm: pb.CertificateAuthority_EC_P384_SHA384, + }, + }, nil + case kmsapi.SHA256WithRSAPSS: + algo, err := getRSAPSSAlgorithm(bits) + if err != nil { + return nil, err + } + return &pb.CertificateAuthority_KeyVersionSpec{ + KeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{ + Algorithm: algo, + }, + }, nil + default: + return nil, fmt.Errorf("unknown or unsupported signature algorithm '%s'", bits) + } +} + +func getRSAPSSAlgorithm(bits int) (pb.CertificateAuthority_SignHashAlgorithm, error) { + switch bits { + case 0, 3072: + return pb.CertificateAuthority_RSA_PSS_3072_SHA_256, nil + case 2048: + return pb.CertificateAuthority_RSA_PSS_2048_SHA_256, nil + case 4096: + return pb.CertificateAuthority_RSA_PSS_4096_SHA_256, nil + default: + return 0, fmt.Errorf("unsupported RSA-PSS key size '%d'", bits) + } +} diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 8820304a..d0519777 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -6,6 +6,8 @@ import ( "crypto/x509" "encoding/asn1" "encoding/pem" + "regexp" + "strings" "time" privateca "cloud.google.com/go/security/privateca/apiv1beta1" @@ -24,12 +26,20 @@ func init() { }) } +// The actual regular expression that matches a certificate authority is: +// ^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/locations/[a-z0-9-]+/certificateAuthorities/[a-zA-Z0-9-_]+$ +// But we will allow a more flexible one to fail if this changes. +var caRegexp = regexp.MustCompile("^projects/[^/]+/locations/[^/]+/certificateAuthorities/[^/]+$") + // CertificateAuthorityClient is the interface implemented by the Google CAS // client. type CertificateAuthorityClient interface { CreateCertificate(ctx context.Context, req *pb.CreateCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) RevokeCertificate(ctx context.Context, req *pb.RevokeCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) GetCertificateAuthority(ctx context.Context, req *pb.GetCertificateAuthorityRequest, opts ...gax.CallOption) (*pb.CertificateAuthority, error) + CreateCertificateAuthority(ctx context.Context, req *pb.CreateCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.CreateCertificateAuthorityOperation, error) + FetchCertificateAuthorityCsr(ctx context.Context, req *pb.FetchCertificateAuthorityCsrRequest, opts ...gax.CallOption) (*pb.FetchCertificateAuthorityCsrResponse, error) + ActivateCertificateAuthority(ctx context.Context, req *pb.ActivateCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.ActivateCertificateAuthorityOperation, error) } // recocationCodeMap maps revocation reason codes from RFC 5280, to Google CAS @@ -51,6 +61,8 @@ var revocationCodeMap = map[int]pb.RevocationReason{ type CloudCAS struct { client CertificateAuthorityClient certificateAuthority string + project string + location string } // newCertificateAuthorityClient creates the certificate authority client. This @@ -70,8 +82,29 @@ var newCertificateAuthorityClient = func(ctx context.Context, credentialsFile st // 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") + if opts.IsCreator { + switch { + case opts.Project == "": + return nil, errors.New("cloudCAS 'project' cannot be empty") + case opts.Location == "": + return nil, errors.New("cloudCAS 'location' cannot be empty") + } + } else { + if opts.CertificateAuthority == "" { + return nil, errors.New("cloudCAS 'certificateAuthority' cannot be empty") + } + if !caRegexp.MatchString(opts.CertificateAuthority) { + return nil, errors.New("cloudCAS 'certificateAuthority' is not valid certificate authority resource") + } + // Extract project and location from CertificateAuthority + if parts := strings.Split(opts.CertificateAuthority, "/"); len(parts) == 6 { + if opts.Project == "" { + opts.Project = parts[1] + } + if opts.Location == "" { + opts.Location = parts[3] + } + } } client, err := newCertificateAuthorityClient(ctx, opts.CredentialsFile) @@ -82,6 +115,8 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { return &CloudCAS{ client: client, certificateAuthority: opts.CertificateAuthority, + project: opts.Project, + location: opts.Location, }, nil } @@ -101,6 +136,7 @@ func (c *CloudCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityReq Name: name, }) if err != nil { + println(name) return nil, errors.Wrap(err, "cloudCAS GetCertificateAuthority failed") } if len(resp.PemCaCertificates) == 0 { @@ -160,7 +196,7 @@ func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1. }, nil } -// RevokeCertificate a certificate using Google Cloud CAS. +// RevokeCertificate revokes a certificate using Google Cloud CAS. func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { reason, ok := revocationCodeMap[req.ReasonCode] switch { @@ -203,6 +239,140 @@ func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv }, nil } +// CreateCertificateAuthority creates a new root or intermediate certificate +// using Google Cloud CAS. +func (c *CloudCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) { + switch { + case c.project == "": + return nil, errors.New("cloudCAS `project` cannot be empty") + case c.location == "": + return nil, errors.New("cloudCAS `location` cannot be empty") + case req.Template == nil: + return nil, errors.New("createCertificateAuthorityRequest `template` cannot be nil") + case req.Lifetime == 0: + return nil, errors.New("createCertificateAuthorityRequest `lifetime` cannot be 0") + case req.Type == apiv1.IntermediateCA && req.Parent == nil: + return nil, errors.New("createCertificateAuthorityRequest `parent` cannot be nil") + case req.Type == apiv1.IntermediateCA && req.Parent.Name == "": + return nil, errors.New("createCertificateAuthorityRequest `parent.name` cannot be empty") + } + + // Select key and signature algorithm to use + var err error + var keySpec *pb.CertificateAuthority_KeyVersionSpec + if req.CreateKey == nil { + if keySpec, err = createKeyVersionSpec(0, 0); err != nil { + return nil, errors.Wrap(err, "createCertificateAuthorityRequest `createKey` is not valid") + } + } else { + if keySpec, err = createKeyVersionSpec(req.CreateKey.SignatureAlgorithm, req.CreateKey.Bits); err != nil { + return nil, errors.Wrap(err, "createCertificateAuthorityRequest `createKey` is not valid") + } + } + + // Normalize or generate id. + certificateAuthorityID := normalizeCertificateAuthorityName(req.Name) + if certificateAuthorityID == "" { + id, err := createCertificateID() + if err != nil { + return nil, err + } + certificateAuthorityID = id + } + + // Add CertificateAuthority extension + casExtension, err := apiv1.CreateCertificateAuthorityExtension(apiv1.CloudCAS, certificateAuthorityID) + if err != nil { + return nil, err + } + req.Template.ExtraExtensions = append(req.Template.ExtraExtensions, casExtension) + + // Prepare CreateCertificateAuthorityRequest + pbReq := &pb.CreateCertificateAuthorityRequest{ + Parent: "projects/" + c.project + "/locations/" + c.location, + CertificateAuthorityId: certificateAuthorityID, + RequestId: req.RequestID, + CertificateAuthority: &pb.CertificateAuthority{ + Type: pb.CertificateAuthority_TYPE_UNSPECIFIED, + Tier: pb.CertificateAuthority_ENTERPRISE, + Config: &pb.CertificateConfig{ + SubjectConfig: &pb.CertificateConfig_SubjectConfig{ + Subject: createSubject(req.Template), + CommonName: req.Template.Subject.CommonName, + }, + ReusableConfig: createReusableConfig(req.Template), + }, + Lifetime: durationpb.New(req.Lifetime), + KeySpec: keySpec, + IssuingOptions: &pb.CertificateAuthority_IssuingOptions{ + IncludeCaCertUrl: true, + IncludeCrlAccessUrl: true, + }, + Labels: map[string]string{}, + }, + } + + switch req.Type { + case apiv1.RootCA: + pbReq.CertificateAuthority.Type = pb.CertificateAuthority_SELF_SIGNED + case apiv1.IntermediateCA: + pbReq.CertificateAuthority.Type = pb.CertificateAuthority_SUBORDINATE + default: + return nil, errors.Errorf("createCertificateAuthorityRequest `type=%d' is invalid or not supported", req.Type) + } + + // Create certificate authority. + ctx, cancel := defaultContext() + defer cancel() + + resp, err := c.client.CreateCertificateAuthority(ctx, pbReq) + if err != nil { + return nil, errors.Wrap(err, "cloudCAS CreateCertificateAuthority failed") + } + + // Wait for the long-running operation. + ctx, cancel = defaultInitiatorContext() + defer cancel() + + ca, err := resp.Wait(ctx) + if err != nil { + return nil, errors.Wrap(err, "cloudCAS CreateCertificateAuthority failed") + } + + // Sign Intermediate CAs with the parent. + if req.Type == apiv1.IntermediateCA { + ca, err = c.signIntermediateCA(ca.Name, req) + if err != nil { + return nil, err + } + } + + if len(ca.PemCaCertificates) == 0 { + return nil, errors.New("cloudCAS CreateCertificateAuthority failed: PemCaCertificates is empty") + } + + cert, err := parseCertificate(ca.PemCaCertificates[0]) + if err != nil { + return nil, err + } + + var chain []*x509.Certificate + if pemChain := ca.PemCaCertificates[1:]; len(pemChain) > 0 { + chain = make([]*x509.Certificate, len(pemChain)) + for i, s := range pemChain { + if chain[i], err = parseCertificate(s); err != nil { + return nil, err + } + } + } + + return &apiv1.CreateCertificateAuthorityResponse{ + Name: ca.Name, + Certificate: cert, + CertificateChain: chain, + }, nil +} + func (c *CloudCAS) createCertificate(tpl *x509.Certificate, lifetime time.Duration, requestID string) (*x509.Certificate, []*x509.Certificate, error) { // Removes the CAS extension if it exists. apiv1.RemoveCertificateAuthorityExtension(tpl) @@ -245,10 +415,82 @@ func (c *CloudCAS) createCertificate(tpl *x509.Certificate, lifetime time.Durati return getCertificateAndChain(cert) } +func (c *CloudCAS) signIntermediateCA(name string, req *apiv1.CreateCertificateAuthorityRequest) (*pb.CertificateAuthority, error) { + id, err := createCertificateID() + if err != nil { + return nil, err + } + + // Fetch intermediate CSR + ctx, cancel := defaultInitiatorContext() + defer cancel() + + csr, err := c.client.FetchCertificateAuthorityCsr(ctx, &pb.FetchCertificateAuthorityCsrRequest{ + Name: name, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudCAS FetchCertificateAuthorityCsr failed") + } + + // Sign the CSR with the ca. + ctx, cancel = defaultInitiatorContext() + defer cancel() + + sign, err := c.client.CreateCertificate(ctx, &pb.CreateCertificateRequest{ + Parent: req.Parent.Name, + CertificateId: id, + Certificate: &pb.Certificate{ + // Name: "projects/" + c.project + "/locations/" + c.location + "/certificates/" + id, + CertificateConfig: &pb.Certificate_PemCsr{ + PemCsr: csr.PemCsr, + }, + Lifetime: durationpb.New(req.Lifetime), + Labels: map[string]string{}, + }, + RequestId: req.RequestID, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudCAS CreateCertificate failed") + } + + ctx, cancel = defaultInitiatorContext() + defer cancel() + resp, err := c.client.ActivateCertificateAuthority(ctx, &pb.ActivateCertificateAuthorityRequest{ + Name: name, + PemCaCertificate: sign.PemCertificate, + SubordinateConfig: &pb.SubordinateConfig{ + SubordinateConfig: &pb.SubordinateConfig_PemIssuerChain{ + PemIssuerChain: &pb.SubordinateConfig_SubordinateConfigChain{ + PemCertificates: sign.PemCertificateChain, + }, + }, + }, + RequestId: req.RequestID, + }) + if err != nil { + return nil, errors.Wrap(err, "cloudCAS ActivateCertificateAuthority1 failed") + } + + // Wait for the long-running operation. + ctx, cancel = defaultInitiatorContext() + defer cancel() + + ca, err := resp.Wait(ctx) + if err != nil { + return nil, errors.Wrap(err, "cloudCAS ActivateCertificateAuthority failed") + } + + return ca, nil +} + func defaultContext() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 15*time.Second) } +func defaultInitiatorContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 60*time.Second) +} + func createCertificateID() (string, error) { id, err := uuid.NewRandomFromReader(rand.Reader) if err != nil { @@ -287,3 +529,23 @@ func getCertificateAndChain(certpb *pb.Certificate) (*x509.Certificate, []*x509. return cert, chain, nil } + +// Normalize a certificate authority name to comply with [a-zA-Z0-9-_]. +func normalizeCertificateAuthorityName(name string) string { + return strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z': + return r + case r >= 'A' && r <= 'Z': + return r + case r >= '0' && r <= '9': + return r + case r == '-': + return r + case r == '_': + return r + default: + return '-' + } + }, name) +} diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index 38446325..16e4386d 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -5,6 +5,7 @@ import ( "context" "crypto/rand" "crypto/x509" + "crypto/x509/pkix" "encoding/asn1" "io" "os" @@ -673,3 +674,62 @@ func Test_getCertificateAndChain(t *testing.T) { }) } } + +func TestCloudCAS(t *testing.T) { + cas, err := New(context.Background(), apiv1.Options{ + Type: "cloudCAS", + CertificateAuthority: "projects/smallstep-cas-test/locations/us-west1", + CredentialsFile: "/Users/mariano/smallstep-cas-test-8a068f3e4540.json", + }) + if err != nil { + t.Fatal(err) + } + + // resp, err := cas.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{ + // Type: apiv1.RootCA, + // Template: &x509.Certificate{ + // Subject: pkix.Name{ + // CommonName: "Test Mariano Root CA", + // }, + // BasicConstraintsValid: true, + // IsCA: true, + // MaxPathLen: 1, + // MaxPathLenZero: false, + // KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + // }, + // Lifetime: time.Duration(30 * 24 * time.Hour), + // Project: "smallstep-cas-test", + // Location: "us-west1", + // }) + // if err != nil { + // t.Fatal(err) + // } + // debug(resp) + resp := &apiv1.CreateCertificateAuthorityResponse{ + Name: "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/9a593da4-61af-4426-a2f8-0650373b9c8e", + } + + resp, err = cas.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{ + Type: apiv1.IntermediateCA, + Template: &x509.Certificate{ + Subject: pkix.Name{ + Country: []string{"US"}, + CommonName: "Test Mariano Intermediate CA", + }, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 0, + MaxPathLenZero: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + }, + Lifetime: time.Duration(24 * time.Hour), + Parent: resp, + Project: "smallstep-cas-test", + Location: "us-west1", + }) + if err != nil { + t.Fatal(err) + } + // debug(resp) + t.Error("foo") +} diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 844c5c3c..16ae9547 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -4,10 +4,12 @@ import ( "context" "crypto" "crypto/x509" - "errors" "time" + "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" + "github.com/smallstep/certificates/kms" + kmsapi "github.com/smallstep/certificates/kms/apiv1" "go.step.sm/crypto/x509util" ) @@ -24,22 +26,26 @@ var now = func() time.Time { // 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 + Issuer *x509.Certificate + Signer crypto.Signer + KeyManager kms.KeyManager } // 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") + if !opts.IsCreator { + 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, + Issuer: opts.Issuer, + Signer: opts.Signer, + KeyManager: opts.KeyManager, }, nil } @@ -113,3 +119,102 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 }, }, nil } + +// CreateCertificateAuthority creates a root or an intermediate certificate. +func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) { + switch { + case req.Template == nil: + return nil, errors.New("createCertificateAuthorityRequest `template` cannot be nil") + case req.Lifetime == 0: + return nil, errors.New("createCertificateAuthorityRequest `lifetime` cannot be 0") + case req.Type == apiv1.IntermediateCA && req.Parent == nil: + return nil, errors.New("createCertificateAuthorityRequest `parent` cannot be nil") + case req.Type == apiv1.IntermediateCA && req.Parent.Certificate == nil: + return nil, errors.New("createCertificateAuthorityRequest `parent.template` cannot be nil") + case req.Type == apiv1.IntermediateCA && req.Parent.Signer == nil: + return nil, errors.New("createCertificateAuthorityRequest `parent.signer` cannot be nil") + } + + key, err := c.createKey(req.CreateKey) + if err != nil { + return nil, err + } + + signer, err := c.createSigner(&key.CreateSignerRequest) + if err != nil { + return nil, err + } + + t := now() + 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) + } + + var cert *x509.Certificate + switch req.Type { + case apiv1.RootCA: + cert, err = x509util.CreateCertificate(req.Template, req.Template, signer.Public(), signer) + if err != nil { + return nil, err + } + case apiv1.IntermediateCA: + cert, err = x509util.CreateCertificate(req.Template, req.Parent.Certificate, signer.Public(), req.Parent.Signer) + if err != nil { + return nil, err + } + default: + return nil, errors.Errorf("createCertificateAuthorityRequest `type=%d' is invalid or not supported", req.Type) + } + + // Add the parent + var chain []*x509.Certificate + if req.Parent != nil { + chain = append(chain, req.Parent.Certificate) + for _, crt := range req.Parent.CertificateChain { + chain = append(chain, crt) + } + } + + return &apiv1.CreateCertificateAuthorityResponse{ + Name: cert.Subject.CommonName, + Certificate: cert, + CertificateChain: chain, + PublicKey: key.PublicKey, + PrivateKey: key.PrivateKey, + Signer: signer, + }, nil +} + +// initializeKeyManager initiazes the default key manager if was not given. +func (c *SoftCAS) initializeKeyManager() (err error) { + if c.KeyManager == nil { + c.KeyManager, err = kms.New(context.Background(), kmsapi.Options{ + Type: string(kmsapi.DefaultKMS), + }) + } + return +} + +// createKey uses the configured kms to create a key. +func (c *SoftCAS) createKey(req *kmsapi.CreateKeyRequest) (*kmsapi.CreateKeyResponse, error) { + if err := c.initializeKeyManager(); err != nil { + return nil, err + } + if req == nil { + req = &kmsapi.CreateKeyRequest{ + SignatureAlgorithm: kmsapi.ECDSAWithSHA256, + } + } + return c.KeyManager.CreateKey(req) +} + +// createSigner uses the configured kms to create a singer +func (c *SoftCAS) createSigner(req *kmsapi.CreateSignerRequest) (crypto.Signer, error) { + if err := c.initializeKeyManager(); err != nil { + return nil, err + } + return c.KeyManager.CreateSigner(req) +} diff --git a/commands/onboard.go b/commands/onboard.go index f5dc422b..13c32304 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/ca" + "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/pki" "github.com/urfave/cli" "go.step.sm/cli-utils/command" @@ -162,7 +163,10 @@ func onboardAction(ctx *cli.Context) error { } func onboardPKI(config onboardingConfiguration) (*authority.Config, string, error) { - p, err := pki.New() + p, err := pki.New(apiv1.Options{ + Type: apiv1.SoftCAS, + IsCreator: true, + }) if err != nil { return nil, "", err } @@ -171,13 +175,13 @@ func onboardPKI(config onboardingConfiguration) (*authority.Config, string, erro p.SetDNSNames([]string{config.DNS}) ui.Println("Generating root certificate...") - rootCrt, rootKey, err := p.GenerateRootCertificate(config.Name+" Root CA", config.password) + root, err := p.GenerateRootCertificate(config.Name, config.Name, config.Name, config.password) if err != nil { return nil, "", err } ui.Println("Generating intermediate certificate...") - err = p.GenerateIntermediateCertificate(config.Name+" Intermediate CA", rootCrt, rootKey, config.password) + err = p.GenerateIntermediateCertificate(config.Name, config.Name, config.Name, root, config.password) if err != nil { return nil, "", err } diff --git a/go.mod b/go.mod index 872e0bb7..6f905db6 100644 --- a/go.mod +++ b/go.mod @@ -20,19 +20,15 @@ require ( github.com/urfave/cli v1.22.2 go.step.sm/cli-utils v0.1.0 go.step.sm/crypto v0.6.1 - golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de - golang.org/x/net v0.0.0-20200822124328-c89045814202 + golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 + golang.org/x/net v0.0.0-20201021035429-f5854403a974 google.golang.org/api v0.31.0 google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d 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 e69eb8e2..bb47a168 100644 --- a/go.sum +++ b/go.sum @@ -293,6 +293,8 @@ golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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= @@ -353,6 +355,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R 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/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -401,6 +405,8 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w 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/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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= diff --git a/pki/pki.go b/pki/pki.go index 4299a1f1..05f09b02 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -5,6 +5,7 @@ import ( "crypto" "crypto/sha256" "crypto/x509" + "crypto/x509/pkix" "encoding/hex" "encoding/json" "encoding/pem" @@ -31,7 +32,6 @@ import ( "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/pemutil" - "go.step.sm/crypto/x509util" "golang.org/x/crypto/ssh" ) @@ -148,6 +148,8 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) { // PKI represents the Public Key Infrastructure used by a certificate authority. type PKI struct { + casOptions apiv1.Options + caCreator apiv1.CertificateAuthorityCreator root, rootKey, rootFingerprint string intermediate, intermediateKey string sshHostPubKey, sshHostKey string @@ -160,11 +162,15 @@ type PKI struct { dnsNames []string caURL string enableSSH bool - authorityOptions *apiv1.Options } // New creates a new PKI configuration. -func New() (*PKI, error) { +func New(opts apiv1.Options) (*PKI, error) { + caCreator, err := cas.NewCreator(context.Background(), opts) + if err != nil { + return nil, err + } + public := GetPublicPath() private := GetSecretsPath() config := GetConfigPath() @@ -185,8 +191,9 @@ func New() (*PKI, error) { return s, errors.Wrapf(err, "error getting absolute path for %s", name) } - var err error p := &PKI{ + casOptions: opts, + caCreator: caCreator, provisioner: "step-cli", address: "127.0.0.1:9000", dnsNames: []string{"127.0.0.1"}, @@ -237,12 +244,6 @@ func (p *PKI) GetRootFingerprint() string { return p.rootFingerprint } -// SetAuthorityOptions sets the authority options object, these options are used -// to configure a registration authority. -func (p *PKI) SetAuthorityOptions(opts *apiv1.Options) { - p.authorityOptions = opts -} - // SetProvisioner sets the provisioner name of the OTT keys. func (p *PKI) SetProvisioner(s string) { p.provisioner = s @@ -275,37 +276,65 @@ func (p *PKI) GenerateKeyPairs(pass []byte) error { return nil } -// GenerateRootCertificate generates a root certificate with the given name. -func (p *PKI) GenerateRootCertificate(name string, pass []byte) (*x509.Certificate, interface{}, error) { - signer, err := generateDefaultKey() +// GenerateRootCertificate generates a root certificate with the given name +// and using the default key type. +func (p *PKI) GenerateRootCertificate(name, org, resource string, pass []byte) (*apiv1.CreateCertificateAuthorityResponse, error) { + resp, err := p.caCreator.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{ + Name: resource + "-Root-CA", + Type: apiv1.RootCA, + Lifetime: 10 * 365 * 24 * time.Hour, + CreateKey: nil, // use default + Template: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: name + " Root CA", + Organization: []string{org}, + }, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 1, + MaxPathLenZero: false, + }, + }) if err != nil { - return nil, nil, err + return nil, err } - cr, err := x509util.CreateCertificateRequest(name, []string{}, signer) - if err != nil { - return nil, nil, err + // PrivateKey will only be set if we have access to it (SoftCAS). + if err := p.WriteRootCertificate(resp.Certificate, resp.PrivateKey, pass); err != nil { + return nil, err } - data := x509util.CreateTemplateData(name, []string{}) - cert, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data)) - if err != nil { - return nil, nil, err - } + return resp, nil +} - template := cert.GetCertificate() - template.NotBefore = time.Now() - template.NotAfter = template.NotBefore.AddDate(10, 0, 0) - rootCrt, err := x509util.CreateCertificate(template, template, signer.Public(), signer) +// GenerateIntermediateCertificate generates an intermediate certificate with +// the given name and using the default key type. +func (p *PKI) GenerateIntermediateCertificate(name, org, resource string, parent *apiv1.CreateCertificateAuthorityResponse, pass []byte) error { + resp, err := p.caCreator.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{ + Name: resource + "-Intermediate-CA", + Type: apiv1.IntermediateCA, + Lifetime: 10 * 365 * 24 * time.Hour, + CreateKey: nil, // use default + Template: &x509.Certificate{ + Subject: pkix.Name{ + CommonName: name + " Intermediate CA", + Organization: []string{org}, + }, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 0, + MaxPathLenZero: true, + }, + Parent: parent, + }) if err != nil { - return nil, nil, err - } - - if err := p.WriteRootCertificate(rootCrt, signer, pass); err != nil { - return nil, nil, err + return err } - return rootCrt, signer, nil + p.casOptions.CertificateAuthority = resp.Name + return p.WriteIntermediateCertificate(resp.Certificate, resp.PrivateKey, pass) } // WriteRootCertificate writes to disk the given certificate and key. @@ -330,21 +359,33 @@ func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{ return nil } -// GetCertificateAuthority attempts to load the certificate authority from the -// RA. -func (p *PKI) GetCertificateAuthority() error { - ca, err := cas.New(context.Background(), *p.authorityOptions) - if err != nil { +// WriteIntermediateCertificate writes to disk the given certificate and key. +func (p *PKI) WriteIntermediateCertificate(crt *x509.Certificate, key interface{}, pass []byte) error { + if err := fileutil.WriteFile(p.intermediate, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + }), 0600); err != nil { return err } + if key != nil { + _, err := pemutil.Serialize(key, pemutil.WithPassword(pass), pemutil.ToFile(p.intermediateKey, 0600)) + if err != nil { + return err + } + } + return nil +} - srv, ok := ca.(apiv1.CertificateAuthorityGetter) +// GetCertificateAuthority attempts to load the certificate authority from the +// RA. +func (p *PKI) GetCertificateAuthority() error { + srv, ok := p.caCreator.(apiv1.CertificateAuthorityGetter) if !ok { return nil } resp, err := srv.GetCertificateAuthority(&apiv1.GetCertificateAuthorityRequest{ - Name: p.authorityOptions.CertificateAuthority, + Name: p.casOptions.CertificateAuthority, }) if err != nil { return err @@ -361,51 +402,6 @@ func (p *PKI) GetCertificateAuthority() error { return nil } -// GenerateIntermediateCertificate generates an intermediate certificate with -// the given name. -func (p *PKI) GenerateIntermediateCertificate(name string, rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error { - key, err := generateDefaultKey() - if err != nil { - return err - } - - cr, err := x509util.CreateCertificateRequest(name, []string{}, key) - if err != nil { - return err - } - - data := x509util.CreateTemplateData(name, []string{}) - cert, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultIntermediateTemplate, data)) - if err != nil { - return err - } - - template := cert.GetCertificate() - template.NotBefore = rootCrt.NotBefore - template.NotAfter = rootCrt.NotAfter - intermediateCrt, err := x509util.CreateCertificate(template, rootCrt, key.Public(), rootKey.(crypto.Signer)) - if err != nil { - return err - } - - return p.WriteIntermediateCertificate(intermediateCrt, key, pass) -} - -// WriteIntermediateCertificate writes to disk the given certificate and key. -func (p *PKI) WriteIntermediateCertificate(crt *x509.Certificate, key interface{}, pass []byte) error { - if err := fileutil.WriteFile(p.intermediate, pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: crt.Raw, - }), 0600); err != nil { - return err - } - _, err := pemutil.Serialize(key, pemutil.WithPassword(pass), pemutil.ToFile(p.intermediateKey, 0600)) - if err != nil { - return err - } - return nil -} - // GenerateSSHSigningKeys generates and encrypts a private key used for signing // SSH user certificates and a private key used for signing host certificates. func (p *PKI) GenerateSSHSigningKeys(password []byte) error { @@ -457,7 +453,7 @@ func (p *PKI) TellPKI() { func (p *PKI) tellPKI() { ui.Println() - if p.authorityOptions == nil || p.authorityOptions.Is(apiv1.SoftCAS) { + if p.casOptions.Is(apiv1.SoftCAS) { ui.PrintSelected("Root certificate", p.root) ui.PrintSelected("Root private key", p.rootKey) ui.PrintSelected("Root fingerprint", p.rootFingerprint) @@ -522,6 +518,11 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { EncryptedKey: key, } + var authorityOptions *apiv1.Options + if !p.casOptions.Is(apiv1.SoftCAS) { + authorityOptions = &p.casOptions + } + config := &authority.Config{ Root: []string{p.root}, FederatedRoots: []string{}, @@ -535,7 +536,7 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { DataSource: GetDBPath(), }, AuthorityConfig: &authority.AuthConfig{ - Options: p.authorityOptions, + Options: authorityOptions, DisableIssuedAtCheck: false, Provisioners: provisioner.List{prov}, }, @@ -642,7 +643,7 @@ func (p *PKI) Save(opt ...Option) error { ui.PrintSelected("Default configuration", p.defaults) ui.PrintSelected("Certificate Authority configuration", p.config) ui.Println() - if p.authorityOptions == nil || p.authorityOptions.Is(apiv1.SoftCAS) { + if p.casOptions.Is(apiv1.SoftCAS) { ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.") } else { ui.Println("Your registration authority is ready to go. To generate certificates for individual services see 'step help ca'.")