Add initial support for step ca init with cloud cas.

Fixes smallstep/cli#363
This commit is contained in:
Mariano Cano 2020-10-23 15:04:09 -07:00
parent 3e6137110b
commit 5deca85b14
12 changed files with 720 additions and 117 deletions

View file

@ -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.

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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)
}

View file

@ -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
}

10
go.mod
View file

@ -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

6
go.sum
View file

@ -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=

View file

@ -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)
// 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
}
return resp, nil
}
// 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
return err
}
data := x509util.CreateTemplateData(name, []string{})
cert, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))
if err != nil {
return nil, nil, err
}
template := cert.GetCertificate()
template.NotBefore = time.Now()
template.NotAfter = template.NotBefore.AddDate(10, 0, 0)
rootCrt, err := x509util.CreateCertificate(template, template, signer.Public(), signer)
if err != nil {
return nil, nil, err
}
if err := p.WriteRootCertificate(rootCrt, signer, pass); err != nil {
return nil, nil, 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
}
// 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
}
// 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 {
return err
}
srv, ok := ca.(apiv1.CertificateAuthorityGetter)
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'.")