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.
if a.x509CAService == nil {
var options casapi.Options
if a.config.CAS != nil {
options = *a.config.CAS
if a.config.AuthorityConfig.Options != nil {
options = *a.config.AuthorityConfig.Options
}
// Read intermediate and create X509 signer for default CAS.
@ -183,7 +183,7 @@ func (a *Authority) init() error {
// Get root certificate from CAS.
if srv, ok := a.x509CAService.(casapi.CertificateAuthorityGetter); ok {
resp, err := srv.GetCertificateAuthority(&casapi.GetCertificateAuthorityRequest{
Name: options.Certificateauthority,
Name: options.CertificateAuthority,
})
if err != nil {
return err

View file

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

View file

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

View file

@ -42,7 +42,7 @@ func TestOptions_Validate(t *testing.T) {
type fields struct {
Type string
CredentialsFile string
Certificateauthority string
CertificateAuthority string
Issuer *x509.Certificate
Signer crypto.Signer
}
@ -69,7 +69,7 @@ func TestOptions_Validate(t *testing.T) {
o := &Options{
Type: tt.fields.Type,
CredentialsFile: tt.fields.CredentialsFile,
Certificateauthority: tt.fields.Certificateauthority,
CertificateAuthority: tt.fields.CertificateAuthority,
Issuer: tt.fields.Issuer,
Signer: tt.fields.Signer,
}
@ -86,7 +86,7 @@ func TestOptions_Is(t *testing.T) {
type fields struct {
Type string
CredentialsFile string
Certificateauthority string
CertificateAuthority string
Issuer *x509.Certificate
Signer crypto.Signer
}
@ -119,7 +119,7 @@ func TestOptions_Is(t *testing.T) {
o := &Options{
Type: tt.fields.Type,
CredentialsFile: tt.fields.CredentialsFile,
Certificateauthority: tt.fields.Certificateauthority,
CertificateAuthority: tt.fields.CertificateAuthority,
Issuer: tt.fields.Issuer,
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
// Cloud CAS.
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")
}
@ -81,7 +81,7 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) {
return &CloudCAS{
client: client,
certificateAuthority: opts.Certificateauthority,
certificateAuthority: opts.CertificateAuthority,
}, nil
}

View file

@ -174,20 +174,20 @@ func TestNew(t *testing.T) {
wantErr bool
}{
{"ok", args{context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName,
CertificateAuthority: testAuthorityName,
}}, &CloudCAS{
client: &testClient{},
certificateAuthority: testAuthorityName,
}, false},
{"ok with credentials", args{context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/credentials.json",
CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/credentials.json",
}}, &CloudCAS{
client: &testClient{credentialsFile: "testdata/credentials.json"},
certificateAuthority: testAuthorityName,
}, false},
{"fail certificate authority", args{context.Background(), apiv1.Options{}}, nil, true},
{"fail with credentials", args{context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/error.json",
CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/error.json",
}}, nil, true},
}
for _, tt := range tests {
@ -225,7 +225,7 @@ func TestNew_register(t *testing.T) {
}
got, err := newFn(context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/credentials.json",
CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/credentials.json",
})
if err != nil {
t.Errorf("New() error = %v", err)
@ -255,10 +255,10 @@ func TestNew_real(t *testing.T) {
args args
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 with credentials", false, args{context.Background(), apiv1.Options{
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/missing.json",
CertificateAuthority: testAuthorityName, CredentialsFile: "testdata/missing.json",
}}, true},
}
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
sign X.509 certificates requests.
This document describes how to use an external registration authority (RA), aka
certificate authority service (CAS) to sign X.509 certificates requests.
A CAS is a system that implements an API to sign certificate requests, the
difference between CAS and KMS is that the latter can sign any data, while CAS
is intended to sign only X.509 certificates.
`step-ca` defines an interface that can be implemented to support other
services, currently only CloudCAS and the default SoftCAS are implemented.
registration authorities, currently only CloudCAS and the default SoftCAS are
implemented.
The `CertificateAuthorityService` is defined in the package
`github.com/smallstep/certificates/cas/apiv1` and it is:
@ -123,15 +124,15 @@ or using `gcloud` CLI:
--reusable-config "subordinate-server-tls-pathlen-0"
```
Not it's time to enable it in `step-ca` adding the new property `"cas"` must be added
to the `ca.json`.
Now it's time to enable it in `step-ca` by adding some new files in the
`"authority"` section of the `ca.json`.
```json
{
"cas": {
"authority": {
"type": "cloudCAS",
"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",
"dataSource": "/home/jane/.step/db",
},
"cas": {
"authority": {
"type": "cloudCAS",
"credentialsFile": "/home/jane/.step/credentials.json",
"certificateAuthority": "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/prod-intermediate-ca"
},
"authority": {
"certificateAuthority": "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/prod-intermediate-ca",
"provisioners": [
{
"type": "JWK",

View file

@ -1,6 +1,7 @@
package pki
import (
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
@ -20,6 +21,8 @@ import (
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas"
"github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/cli/config"
"github.com/smallstep/cli/errs"
@ -157,6 +160,7 @@ type PKI struct {
dnsNames []string
caURL string
enableSSH bool
authorityOptions *apiv1.Options
}
// New creates a new PKI configuration.
@ -233,6 +237,12 @@ 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
@ -307,9 +317,11 @@ func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{
return err
}
_, err := pemutil.Serialize(rootKey, pemutil.WithPassword(pass), pemutil.ToFile(p.rootKey, 0600))
if err != nil {
return err
if rootKey != nil {
_, err := pemutil.Serialize(rootKey, pemutil.WithPassword(pass), pemutil.ToFile(p.rootKey, 0600))
if err != nil {
return err
}
}
sum := sha256.Sum256(rootCrt.Raw)
@ -318,6 +330,37 @@ 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 {
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
// the given name.
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() {
ui.Println()
ui.PrintSelected("Root certificate", p.root)
ui.PrintSelected("Root private key", p.rootKey)
ui.PrintSelected("Root fingerprint", p.rootFingerprint)
ui.PrintSelected("Intermediate certificate", p.intermediate)
ui.PrintSelected("Intermediate private key", p.intermediateKey)
if p.authorityOptions == nil || p.authorityOptions.Is(apiv1.SoftCAS) {
ui.PrintSelected("Root certificate", p.root)
ui.PrintSelected("Root private key", p.rootKey)
ui.PrintSelected("Root fingerprint", p.rootFingerprint)
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 {
ui.PrintSelected("SSH user root certificate", p.sshUserPubKey)
ui.PrintSelected("SSH user root private key", p.sshUserKey)
@ -485,6 +535,7 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) {
DataSource: GetDBPath(),
},
AuthorityConfig: &authority.AuthConfig{
Options: p.authorityOptions,
DisableIssuedAtCheck: false,
Provisioners: provisioner.List{prov},
},
@ -591,7 +642,11 @@ func (p *PKI) Save(opt ...Option) error {
ui.PrintSelected("Default configuration", p.defaults)
ui.PrintSelected("Certificate Authority configuration", p.config)
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()