Merge pull request #402 from smallstep/ra-init

Add support for CloudCAS on step ca init
This commit is contained in:
Mariano Cano 2020-10-20 18:00:23 -07:00 committed by GitHub
commit 426f846974
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 102 additions and 43 deletions

View file

@ -156,8 +156,8 @@ func (a *Authority) init() error {
// Initialize the X.509 CA Service if it has not been set in the options. // Initialize the X.509 CA Service if it has not been set in the options.
if a.x509CAService == nil { if a.x509CAService == nil {
var options casapi.Options var options casapi.Options
if a.config.CAS != nil { if a.config.AuthorityConfig.Options != nil {
options = *a.config.CAS options = *a.config.AuthorityConfig.Options
} }
// Read intermediate and create X509 signer for default CAS. // Read intermediate and create X509 signer for default CAS.
@ -183,7 +183,7 @@ func (a *Authority) init() error {
// Get root certificate from CAS. // Get root certificate from CAS.
if srv, ok := a.x509CAService.(casapi.CertificateAuthorityGetter); ok { if srv, ok := a.x509CAService.(casapi.CertificateAuthorityGetter); ok {
resp, err := srv.GetCertificateAuthority(&casapi.GetCertificateAuthorityRequest{ resp, err := srv.GetCertificateAuthority(&casapi.GetCertificateAuthorityRequest{
Name: options.Certificateauthority, Name: options.CertificateAuthority,
}) })
if err != nil { if err != nil {
return err return err

View file

@ -55,7 +55,6 @@ type Config struct {
Address string `json:"address"` Address string `json:"address"`
DNSNames []string `json:"dnsNames"` DNSNames []string `json:"dnsNames"`
KMS *kms.Options `json:"kms,omitempty"` KMS *kms.Options `json:"kms,omitempty"`
CAS *cas.Options `json:"cas,omitempty"`
SSH *SSHConfig `json:"ssh,omitempty"` SSH *SSHConfig `json:"ssh,omitempty"`
Logger json.RawMessage `json:"logger,omitempty"` Logger json.RawMessage `json:"logger,omitempty"`
DB *db.Config `json:"db,omitempty"` DB *db.Config `json:"db,omitempty"`
@ -78,8 +77,11 @@ type ASN1DN struct {
CommonName string `json:"commonName,omitempty" step:"commonName"` CommonName string `json:"commonName,omitempty" step:"commonName"`
} }
// AuthConfig represents the configuration options for the authority. // AuthConfig represents the configuration options for the authority. An
// underlaying registration authority can also be configured using the
// cas.Options.
type AuthConfig struct { type AuthConfig struct {
*cas.Options
Provisioners provisioner.List `json:"provisioners"` Provisioners provisioner.List `json:"provisioners"`
Template *ASN1DN `json:"template,omitempty"` Template *ASN1DN `json:"template,omitempty"`
Claims *provisioner.Claims `json:"claims,omitempty"` Claims *provisioner.Claims `json:"claims,omitempty"`
@ -185,8 +187,11 @@ func (c *Config) Validate() error {
return errors.New("dnsNames cannot be empty") return errors.New("dnsNames cannot be empty")
} }
// The default CAS requires root, crt and key. // Options holds the RA/CAS configuration.
if c.CAS.Is(cas.SoftCAS) { ra := c.AuthorityConfig.Options
// The default RA/CAS requires root, crt and key.
if ra.Is(cas.SoftCAS) {
switch { switch {
case c.Root.HasEmpties(): case c.Root.HasEmpties():
return errors.New("root cannot be empty") return errors.New("root cannot be empty")
@ -225,8 +230,8 @@ func (c *Config) Validate() error {
return err return err
} }
// Validate CAS options, nil is ok. // Validate RA/CAS options, nil is ok.
if err := c.CAS.Validate(); err != nil { if err := ra.Validate(); err != nil {
return err return err
} }

View file

@ -18,7 +18,7 @@ type Options struct {
// CertificateAuthority reference. In CloudCAS the format is // CertificateAuthority reference. In CloudCAS the format is
// `projects/*/locations/*/certificateAuthorities/*`. // `projects/*/locations/*/certificateAuthorities/*`.
Certificateauthority string `json:"certificateAuthority"` CertificateAuthority string `json:"certificateAuthority"`
// Issuer and signer are the issuer certificate and signer used in SoftCAS. // Issuer and signer are the issuer certificate and signer used in SoftCAS.
// They are configured in ca.json crt and key properties. // They are configured in ca.json crt and key properties.

View file

@ -42,7 +42,7 @@ func TestOptions_Validate(t *testing.T) {
type fields struct { type fields struct {
Type string Type string
CredentialsFile string CredentialsFile string
Certificateauthority string CertificateAuthority string
Issuer *x509.Certificate Issuer *x509.Certificate
Signer crypto.Signer Signer crypto.Signer
} }
@ -69,7 +69,7 @@ func TestOptions_Validate(t *testing.T) {
o := &Options{ o := &Options{
Type: tt.fields.Type, Type: tt.fields.Type,
CredentialsFile: tt.fields.CredentialsFile, CredentialsFile: tt.fields.CredentialsFile,
Certificateauthority: tt.fields.Certificateauthority, CertificateAuthority: tt.fields.CertificateAuthority,
Issuer: tt.fields.Issuer, Issuer: tt.fields.Issuer,
Signer: tt.fields.Signer, Signer: tt.fields.Signer,
} }
@ -86,7 +86,7 @@ func TestOptions_Is(t *testing.T) {
type fields struct { type fields struct {
Type string Type string
CredentialsFile string CredentialsFile string
Certificateauthority string CertificateAuthority string
Issuer *x509.Certificate Issuer *x509.Certificate
Signer crypto.Signer Signer crypto.Signer
} }
@ -119,7 +119,7 @@ func TestOptions_Is(t *testing.T) {
o := &Options{ o := &Options{
Type: tt.fields.Type, Type: tt.fields.Type,
CredentialsFile: tt.fields.CredentialsFile, CredentialsFile: tt.fields.CredentialsFile,
Certificateauthority: tt.fields.Certificateauthority, CertificateAuthority: tt.fields.CertificateAuthority,
Issuer: tt.fields.Issuer, Issuer: tt.fields.Issuer,
Signer: tt.fields.Signer, Signer: tt.fields.Signer,
} }

View file

@ -70,7 +70,7 @@ var newCertificateAuthorityClient = func(ctx context.Context, credentialsFile st
// New creates a new CertificateAuthorityService implementation using Google // New creates a new CertificateAuthorityService implementation using Google
// Cloud CAS. // Cloud CAS.
func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) {
if opts.Certificateauthority == "" { if opts.CertificateAuthority == "" {
return nil, errors.New("cloudCAS 'certificateAuthority' cannot be empty") return nil, errors.New("cloudCAS 'certificateAuthority' cannot be empty")
} }
@ -81,7 +81,7 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) {
return &CloudCAS{ return &CloudCAS{
client: client, client: client,
certificateAuthority: opts.Certificateauthority, certificateAuthority: opts.CertificateAuthority,
}, nil }, nil
} }

View file

@ -174,20 +174,20 @@ func TestNew(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{"ok", args{context.Background(), apiv1.Options{ {"ok", args{context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CertificateAuthority: testAuthorityName,
}}, &CloudCAS{ }}, &CloudCAS{
client: &testClient{}, client: &testClient{},
certificateAuthority: testAuthorityName, certificateAuthority: testAuthorityName,
}, false}, }, false},
{"ok with credentials", args{context.Background(), apiv1.Options{ {"ok with credentials", args{context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/credentials.json", CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/credentials.json",
}}, &CloudCAS{ }}, &CloudCAS{
client: &testClient{credentialsFile: "testdata/credentials.json"}, client: &testClient{credentialsFile: "testdata/credentials.json"},
certificateAuthority: testAuthorityName, certificateAuthority: testAuthorityName,
}, false}, }, false},
{"fail certificate authority", args{context.Background(), apiv1.Options{}}, nil, true}, {"fail certificate authority", args{context.Background(), apiv1.Options{}}, nil, true},
{"fail with credentials", args{context.Background(), apiv1.Options{ {"fail with credentials", args{context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/error.json", CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/error.json",
}}, nil, true}, }}, nil, true},
} }
for _, tt := range tests { for _, tt := range tests {
@ -225,7 +225,7 @@ func TestNew_register(t *testing.T) {
} }
got, err := newFn(context.Background(), apiv1.Options{ got, err := newFn(context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/credentials.json", CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/credentials.json",
}) })
if err != nil { if err != nil {
t.Errorf("New() error = %v", err) t.Errorf("New() error = %v", err)
@ -255,10 +255,10 @@ func TestNew_real(t *testing.T) {
args args args args
wantErr bool wantErr bool
}{ }{
{"fail default credentials", true, args{context.Background(), apiv1.Options{Certificateauthority: testAuthorityName}}, true}, {"fail default credentials", true, args{context.Background(), apiv1.Options{CertificateAuthority: testAuthorityName}}, true},
{"fail certificate authority", false, args{context.Background(), apiv1.Options{}}, true}, {"fail certificate authority", false, args{context.Background(), apiv1.Options{}}, true},
{"fail with credentials", false, args{context.Background(), apiv1.Options{ {"fail with credentials", false, args{context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/missing.json", CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/missing.json",
}}, true}, }}, true},
} }
for _, tt := range tests { for _, tt := range tests {

View file

@ -1,14 +1,15 @@
# Certificate Authority Services # Registration Authorities
This document describes how to use a certificate authority service or CAS to This document describes how to use an external registration authority (RA), aka
sign X.509 certificates requests. certificate authority service (CAS) to sign X.509 certificates requests.
A CAS is a system that implements an API to sign certificate requests, the A CAS is a system that implements an API to sign certificate requests, the
difference between CAS and KMS is that the latter can sign any data, while CAS difference between CAS and KMS is that the latter can sign any data, while CAS
is intended to sign only X.509 certificates. is intended to sign only X.509 certificates.
`step-ca` defines an interface that can be implemented to support other `step-ca` defines an interface that can be implemented to support other
services, currently only CloudCAS and the default SoftCAS are implemented. registration authorities, currently only CloudCAS and the default SoftCAS are
implemented.
The `CertificateAuthorityService` is defined in the package The `CertificateAuthorityService` is defined in the package
`github.com/smallstep/certificates/cas/apiv1` and it is: `github.com/smallstep/certificates/cas/apiv1` and it is:
@ -123,15 +124,15 @@ or using `gcloud` CLI:
--reusable-config "subordinate-server-tls-pathlen-0" --reusable-config "subordinate-server-tls-pathlen-0"
``` ```
Not it's time to enable it in `step-ca` adding the new property `"cas"` must be added Now it's time to enable it in `step-ca` by adding some new files in the
to the `ca.json`. `"authority"` section of the `ca.json`.
```json ```json
{ {
"cas": { "authority": {
"type": "cloudCAS", "type": "cloudCAS",
"credentialsFile": "/path/to/credentials.json", "credentialsFile": "/path/to/credentials.json",
"certificateAuthority": "projects/<name>/locations/<loc>/certificateAuthorities/<ca-name>" "certificateAuthority": "projects/<name>/locations/<loc>/certificateAuthorities/<ca-name>",
} }
} }
``` ```
@ -161,12 +162,10 @@ need to configure `"root"`, and because the intermediate is in Google Cloud,
"type": "badger", "type": "badger",
"dataSource": "/home/jane/.step/db", "dataSource": "/home/jane/.step/db",
}, },
"cas": { "authority": {
"type": "cloudCAS", "type": "cloudCAS",
"credentialsFile": "/home/jane/.step/credentials.json", "credentialsFile": "/home/jane/.step/credentials.json",
"certificateAuthority": "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/prod-intermediate-ca" "certificateAuthority": "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/prod-intermediate-ca",
},
"authority": {
"provisioners": [ "provisioners": [
{ {
"type": "JWK", "type": "JWK",

View file

@ -1,6 +1,7 @@
package pki package pki
import ( import (
"context"
"crypto" "crypto"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
@ -20,6 +21,8 @@ import (
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas"
"github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db" "github.com/smallstep/certificates/db"
"github.com/smallstep/cli/config" "github.com/smallstep/cli/config"
"github.com/smallstep/cli/errs" "github.com/smallstep/cli/errs"
@ -157,6 +160,7 @@ type PKI struct {
dnsNames []string dnsNames []string
caURL string caURL string
enableSSH bool enableSSH bool
authorityOptions *apiv1.Options
} }
// New creates a new PKI configuration. // New creates a new PKI configuration.
@ -233,6 +237,12 @@ func (p *PKI) GetRootFingerprint() string {
return p.rootFingerprint 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. // SetProvisioner sets the provisioner name of the OTT keys.
func (p *PKI) SetProvisioner(s string) { func (p *PKI) SetProvisioner(s string) {
p.provisioner = s p.provisioner = s
@ -307,9 +317,11 @@ func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{
return err return err
} }
_, err := pemutil.Serialize(rootKey, pemutil.WithPassword(pass), pemutil.ToFile(p.rootKey, 0600)) if rootKey != nil {
if err != nil { _, err := pemutil.Serialize(rootKey, pemutil.WithPassword(pass), pemutil.ToFile(p.rootKey, 0600))
return err if err != nil {
return err
}
} }
sum := sha256.Sum256(rootCrt.Raw) sum := sha256.Sum256(rootCrt.Raw)
@ -318,6 +330,37 @@ func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{
return nil 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)
if !ok {
return nil
}
resp, err := srv.GetCertificateAuthority(&apiv1.GetCertificateAuthorityRequest{
Name: p.authorityOptions.CertificateAuthority,
})
if err != nil {
return err
}
if err := p.WriteRootCertificate(resp.RootCertificate, nil, nil); err != nil {
return err
}
// Issuer is in the RA
p.intermediate = ""
p.intermediateKey = ""
return nil
}
// GenerateIntermediateCertificate generates an intermediate certificate with // GenerateIntermediateCertificate generates an intermediate certificate with
// the given name. // the given name.
func (p *PKI) GenerateIntermediateCertificate(name string, rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error { func (p *PKI) GenerateIntermediateCertificate(name string, rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error {
@ -414,11 +457,18 @@ func (p *PKI) TellPKI() {
func (p *PKI) tellPKI() { func (p *PKI) tellPKI() {
ui.Println() ui.Println()
ui.PrintSelected("Root certificate", p.root) if p.authorityOptions == nil || p.authorityOptions.Is(apiv1.SoftCAS) {
ui.PrintSelected("Root private key", p.rootKey) ui.PrintSelected("Root certificate", p.root)
ui.PrintSelected("Root fingerprint", p.rootFingerprint) ui.PrintSelected("Root private key", p.rootKey)
ui.PrintSelected("Intermediate certificate", p.intermediate) ui.PrintSelected("Root fingerprint", p.rootFingerprint)
ui.PrintSelected("Intermediate private key", p.intermediateKey) ui.PrintSelected("Intermediate certificate", p.intermediate)
ui.PrintSelected("Intermediate private key", p.intermediateKey)
} else if p.rootFingerprint != "" {
ui.PrintSelected("Root certificate", p.root)
ui.PrintSelected("Root fingerprint", p.rootFingerprint)
} else {
ui.Printf(`{{ "%s" | red }} {{ "Root certificate:" | bold }} failed to retrieve it from RA`+"\n", ui.IconBad)
}
if p.enableSSH { if p.enableSSH {
ui.PrintSelected("SSH user root certificate", p.sshUserPubKey) ui.PrintSelected("SSH user root certificate", p.sshUserPubKey)
ui.PrintSelected("SSH user root private key", p.sshUserKey) ui.PrintSelected("SSH user root private key", p.sshUserKey)
@ -485,6 +535,7 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) {
DataSource: GetDBPath(), DataSource: GetDBPath(),
}, },
AuthorityConfig: &authority.AuthConfig{ AuthorityConfig: &authority.AuthConfig{
Options: p.authorityOptions,
DisableIssuedAtCheck: false, DisableIssuedAtCheck: false,
Provisioners: provisioner.List{prov}, Provisioners: provisioner.List{prov},
}, },
@ -591,7 +642,11 @@ func (p *PKI) Save(opt ...Option) error {
ui.PrintSelected("Default configuration", p.defaults) ui.PrintSelected("Default configuration", p.defaults)
ui.PrintSelected("Certificate Authority configuration", p.config) ui.PrintSelected("Certificate Authority configuration", p.config)
ui.Println() ui.Println()
ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.") if p.authorityOptions == nil || p.authorityOptions.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'.")
}
p.askFeedback() p.askFeedback()