commit
647b9b4541
@ -0,0 +1,62 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
oidStepRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}
|
||||
oidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...)
|
||||
)
|
||||
|
||||
// CertificateAuthorityExtension type is used to encode the certificate
|
||||
// authority extension.
|
||||
type CertificateAuthorityExtension struct {
|
||||
Type string
|
||||
CertificateID string `asn1:"optional,omitempty"`
|
||||
KeyValuePairs []string `asn1:"optional,omitempty"`
|
||||
}
|
||||
|
||||
// CreateCertificateAuthorityExtension returns a X.509 extension that shows the
|
||||
// CAS type, id and a list of optional key value pairs.
|
||||
func CreateCertificateAuthorityExtension(typ Type, certificateID string, keyValuePairs ...string) (pkix.Extension, error) {
|
||||
b, err := asn1.Marshal(CertificateAuthorityExtension{
|
||||
Type: typ.String(),
|
||||
CertificateID: certificateID,
|
||||
KeyValuePairs: keyValuePairs,
|
||||
})
|
||||
if err != nil {
|
||||
return pkix.Extension{}, errors.Wrapf(err, "error marshaling certificate id extension")
|
||||
}
|
||||
return pkix.Extension{
|
||||
Id: oidStepCertificateAuthority,
|
||||
Critical: false,
|
||||
Value: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FindCertificateAuthorityExtension returns the certificate authority extension
|
||||
// from a signed certificate.
|
||||
func FindCertificateAuthorityExtension(cert *x509.Certificate) (pkix.Extension, bool) {
|
||||
for _, ext := range cert.Extensions {
|
||||
if ext.Id.Equal(oidStepCertificateAuthority) {
|
||||
return ext, true
|
||||
}
|
||||
}
|
||||
return pkix.Extension{}, false
|
||||
}
|
||||
|
||||
// RemoveCertificateAuthorityExtension removes the certificate authority
|
||||
// extension from a certificate template.
|
||||
func RemoveCertificateAuthorityExtension(cert *x509.Certificate) {
|
||||
for i, ext := range cert.ExtraExtensions {
|
||||
if ext.Id.Equal(oidStepCertificateAuthority) {
|
||||
cert.ExtraExtensions = append(cert.ExtraExtensions[:i], cert.ExtraExtensions[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateCertificateAuthorityExtension(t *testing.T) {
|
||||
type args struct {
|
||||
typ Type
|
||||
certificateID string
|
||||
keyValuePairs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want pkix.Extension
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{Type(CloudCAS), "1ac75689-cd3f-482e-a695-8a13daf39dc4", nil}, pkix.Extension{
|
||||
Id: oidStepCertificateAuthority,
|
||||
Critical: false,
|
||||
Value: []byte{
|
||||
0x30, 0x30, 0x13, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x63, 0x61, 0x73, 0x13, 0x24, 0x31, 0x61,
|
||||
0x63, 0x37, 0x35, 0x36, 0x38, 0x39, 0x2d, 0x63, 0x64, 0x33, 0x66, 0x2d, 0x34, 0x38, 0x32, 0x65,
|
||||
0x2d, 0x61, 0x36, 0x39, 0x35, 0x2d, 0x38, 0x61, 0x31, 0x33, 0x64, 0x61, 0x66, 0x33, 0x39, 0x64,
|
||||
0x63, 0x34,
|
||||
},
|
||||
}, false},
|
||||
{"ok", args{Type(CloudCAS), "1ac75689-cd3f-482e-a695-8a13daf39dc4", []string{"foo", "bar"}}, pkix.Extension{
|
||||
Id: oidStepCertificateAuthority,
|
||||
Critical: false,
|
||||
Value: []byte{
|
||||
0x30, 0x3c, 0x13, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x63, 0x61, 0x73, 0x13, 0x24, 0x31, 0x61,
|
||||
0x63, 0x37, 0x35, 0x36, 0x38, 0x39, 0x2d, 0x63, 0x64, 0x33, 0x66, 0x2d, 0x34, 0x38, 0x32, 0x65,
|
||||
0x2d, 0x61, 0x36, 0x39, 0x35, 0x2d, 0x38, 0x61, 0x31, 0x33, 0x64, 0x61, 0x66, 0x33, 0x39, 0x64,
|
||||
0x63, 0x34, 0x30, 0x0a, 0x13, 0x03, 0x66, 0x6f, 0x6f, 0x13, 0x03, 0x62, 0x61, 0x72,
|
||||
},
|
||||
}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := CreateCertificateAuthorityExtension(tt.args.typ, tt.args.certificateID, tt.args.keyValuePairs...)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CreateCertificateAuthorityExtension() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CreateCertificateAuthorityExtension() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindCertificateAuthorityExtension(t *testing.T) {
|
||||
expected := pkix.Extension{
|
||||
Id: oidStepCertificateAuthority,
|
||||
Value: []byte("fake data"),
|
||||
}
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want pkix.Extension
|
||||
want1 bool
|
||||
}{
|
||||
{"first", args{&x509.Certificate{Extensions: []pkix.Extension{
|
||||
expected,
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}}, expected, true},
|
||||
{"last", args{&x509.Certificate{Extensions: []pkix.Extension{
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
{Id: []int{2, 3, 4, 5}},
|
||||
expected,
|
||||
}}}, expected, true},
|
||||
{"fail", args{&x509.Certificate{Extensions: []pkix.Extension{
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}}, pkix.Extension{}, false},
|
||||
{"fail ExtraExtensions", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{
|
||||
expected,
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}}, pkix.Extension{}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := FindCertificateAuthorityExtension(tt.args.cert)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("FindCertificateAuthorityExtension() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("FindCertificateAuthorityExtension() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveCertificateAuthorityExtension(t *testing.T) {
|
||||
caExt := pkix.Extension{
|
||||
Id: oidStepCertificateAuthority,
|
||||
Value: []byte("fake data"),
|
||||
}
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *x509.Certificate
|
||||
}{
|
||||
{"first", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{
|
||||
caExt,
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}},
|
||||
{"last", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
caExt,
|
||||
}}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}},
|
||||
{"missing", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}},
|
||||
{"extensions", args{&x509.Certificate{Extensions: []pkix.Extension{
|
||||
caExt,
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}}, &x509.Certificate{Extensions: []pkix.Extension{
|
||||
caExt,
|
||||
{Id: []int{1, 2, 3, 4}},
|
||||
}}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
RemoveCertificateAuthorityExtension(tt.args.cert)
|
||||
if !reflect.DeepEqual(tt.args.cert, tt.want) {
|
||||
t.Errorf("RemoveCertificateAuthorityExtension() cert = %v, want %v", tt.args.cert, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Options represents the configuration options used to select and configure the
|
||||
// CertificateAuthorityService (CAS) to use.
|
||||
type Options struct {
|
||||
// The type of the CAS to use.
|
||||
Type string `json:"type"`
|
||||
|
||||
// Path to the credentials file used in CloudCAS
|
||||
CredentialsFile string `json:"credentialsFile"`
|
||||
|
||||
// CertificateAuthority reference. In CloudCAS the format is
|
||||
// `projects/*/locations/*/certificateAuthorities/*`.
|
||||
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.
|
||||
Issuer *x509.Certificate `json:"-"`
|
||||
Signer crypto.Signer `json:"-"`
|
||||
}
|
||||
|
||||
// Validate checks the fields in Options.
|
||||
func (o *Options) Validate() error {
|
||||
var typ Type
|
||||
if o == nil {
|
||||
typ = Type(SoftCAS)
|
||||
} else {
|
||||
typ = Type(o.Type)
|
||||
}
|
||||
// Check that the type can be loaded.
|
||||
if _, ok := LoadCertificateAuthorityServiceNewFunc(typ); !ok {
|
||||
return errors.Errorf("unsupported cas type %s", typ)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Is returns if the options have the given type.
|
||||
func (o *Options) Is(t Type) bool {
|
||||
if o == nil {
|
||||
return t.String() == SoftCAS
|
||||
}
|
||||
return Type(o.Type).String() == t.String()
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testCAS struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (t *testCAS) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t *testCAS) RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t *testCAS) RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func mockRegister(t *testing.T) {
|
||||
t.Helper()
|
||||
Register(SoftCAS, func(ctx context.Context, opts Options) (CertificateAuthorityService, error) {
|
||||
return &testCAS{name: SoftCAS}, nil
|
||||
})
|
||||
Register(CloudCAS, func(ctx context.Context, opts Options) (CertificateAuthorityService, error) {
|
||||
return &testCAS{name: CloudCAS}, nil
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
registry = new(sync.Map)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOptions_Validate(t *testing.T) {
|
||||
mockRegister(t)
|
||||
type fields struct {
|
||||
Type string
|
||||
CredentialsFile string
|
||||
Certificateauthority string
|
||||
Issuer *x509.Certificate
|
||||
Signer crypto.Signer
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{"empty", fields{}, false},
|
||||
{"SoftCAS", fields{SoftCAS, "", "", nil, nil}, false},
|
||||
{"CloudCAS", fields{CloudCAS, "", "", nil, nil}, false},
|
||||
{"softcas", fields{"softcas", "", "", nil, nil}, false},
|
||||
{"CLOUDCAS", fields{"CLOUDCAS", "", "", nil, nil}, false},
|
||||
{"fail", fields{"FailCAS", "", "", nil, nil}, true},
|
||||
}
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
var o *Options
|
||||
if err := o.Validate(); err != nil {
|
||||
t.Errorf("Options.Validate() error = %v, wantErr %v", err, false)
|
||||
}
|
||||
})
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &Options{
|
||||
Type: tt.fields.Type,
|
||||
CredentialsFile: tt.fields.CredentialsFile,
|
||||
Certificateauthority: tt.fields.Certificateauthority,
|
||||
Issuer: tt.fields.Issuer,
|
||||
Signer: tt.fields.Signer,
|
||||
}
|
||||
if err := o.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions_Is(t *testing.T) {
|
||||
mockRegister(t)
|
||||
|
||||
type fields struct {
|
||||
Type string
|
||||
CredentialsFile string
|
||||
Certificateauthority string
|
||||
Issuer *x509.Certificate
|
||||
Signer crypto.Signer
|
||||
}
|
||||
type args struct {
|
||||
t Type
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"empty", fields{}, args{}, true},
|
||||
{"SoftCAS", fields{SoftCAS, "", "", nil, nil}, args{"SoftCAS"}, true},
|
||||
{"CloudCAS", fields{CloudCAS, "", "", nil, nil}, args{"CloudCAS"}, true},
|
||||
{"softcas", fields{"softcas", "", "", nil, nil}, args{SoftCAS}, true},
|
||||
{"CLOUDCAS", fields{"CLOUDCAS", "", "", nil, nil}, args{CloudCAS}, true},
|
||||
{"UnknownCAS", fields{"UnknownCAS", "", "", nil, nil}, args{"UnknownCAS"}, true},
|
||||
{"fail", fields{CloudCAS, "", "", nil, nil}, args{"SoftCAS"}, false},
|
||||
{"fail", fields{SoftCAS, "", "", nil, nil}, args{"CloudCAS"}, false},
|
||||
}
|
||||
t.Run("nil", func(t *testing.T) {
|
||||
var o *Options
|
||||
if got := o.Is(SoftCAS); got != true {
|
||||
t.Errorf("Options.Is() = %v, want %v", got, true)
|
||||
}
|
||||
})
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &Options{
|
||||
Type: tt.fields.Type,
|
||||
CredentialsFile: tt.fields.CredentialsFile,
|
||||
Certificateauthority: tt.fields.Certificateauthority,
|
||||
Issuer: tt.fields.Issuer,
|
||||
Signer: tt.fields.Signer,
|
||||
}
|
||||
if got := o.Is(tt.args.t); got != tt.want {
|
||||
t.Errorf("Options.Is() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
registry = new(sync.Map)
|
||||
)
|
||||
|
||||
// CertificateAuthorityServiceNewFunc is the type that represents the method to initialize a new
|
||||
// CertificateAuthorityService.
|
||||
type CertificateAuthorityServiceNewFunc func(ctx context.Context, opts Options) (CertificateAuthorityService, error)
|
||||
|
||||
// Register adds to the registry a method to create a KeyManager of type t.
|
||||
func Register(t Type, fn CertificateAuthorityServiceNewFunc) {
|
||||
registry.Store(t.String(), fn)
|
||||
}
|
||||
|
||||
// LoadCertificateAuthorityServiceNewFunc returns the function initialize a KayManager.
|
||||
func LoadCertificateAuthorityServiceNewFunc(t Type) (CertificateAuthorityServiceNewFunc, bool) {
|
||||
v, ok := registry.Load(t.String())
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
fn, ok := v.(CertificateAuthorityServiceNewFunc)
|
||||
return fn, ok
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
registry = new(sync.Map)
|
||||
})
|
||||
type args struct {
|
||||
t Type
|
||||
fn CertificateAuthorityServiceNewFunc
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want CertificateAuthorityService
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{"TestCAS", func(ctx context.Context, opts Options) (CertificateAuthorityService, error) {
|
||||
return &testCAS{}, nil
|
||||
}}, &testCAS{}, false},
|
||||
{"error", args{"ErrorCAS", func(ctx context.Context, opts Options) (CertificateAuthorityService, error) {
|
||||
return nil, fmt.Errorf("an error")
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
Register(tt.args.t, tt.args.fn)
|
||||
fmt.Println(registry)
|
||||
fn, ok := registry.Load(tt.args.t.String())
|
||||
if !ok {
|
||||
t.Errorf("Register() failed")
|
||||
return
|
||||
}
|
||||
got, err := fn.(CertificateAuthorityServiceNewFunc)(context.Background(), Options{})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CertificateAuthorityServiceNewFunc() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CertificateAuthorityServiceNewFunc() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCertificateAuthorityServiceNewFunc(t *testing.T) {
|
||||
mockRegister(t)
|
||||
type args struct {
|
||||
t Type
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want CertificateAuthorityService
|
||||
wantOk bool
|
||||
}{
|
||||
{"default", args{""}, &testCAS{name: SoftCAS}, true},
|
||||
{"SoftCAS", args{"SoftCAS"}, &testCAS{name: SoftCAS}, true},
|
||||
{"CloudCAS", args{"CloudCAS"}, &testCAS{name: CloudCAS}, true},
|
||||
{"softcas", args{"softcas"}, &testCAS{name: SoftCAS}, true},
|
||||
{"cloudcas", args{"cloudcas"}, &testCAS{name: CloudCAS}, true},
|
||||
{"FailCAS", args{"FailCAS"}, nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fn, ok := LoadCertificateAuthorityServiceNewFunc(tt.args.t)
|
||||
if ok != tt.wantOk {
|
||||
t.Errorf("LoadCertificateAuthorityServiceNewFunc() ok = %v, want %v", ok, tt.wantOk)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
got, err := fn(context.Background(), Options{})
|
||||
if err != nil {
|
||||
t.Errorf("CertificateAuthorityServiceNewFunc() error = %v", err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CertificateAuthorityServiceNewFunc() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateCertificateRequest is the request used to sign a new certificate.
|
||||
type CreateCertificateRequest struct {
|
||||
Template *x509.Certificate
|
||||
Lifetime time.Duration
|
||||
Backdate time.Duration
|
||||
RequestID string
|
||||
}
|
||||
|
||||
// CreateCertificateResponse is the response to a create certificate request.
|
||||
type CreateCertificateResponse struct {
|
||||
Certificate *x509.Certificate
|
||||
CertificateChain []*x509.Certificate
|
||||
}
|
||||
|
||||
// RenewCertificateRequest is the request used to re-sign a certificate.
|
||||
type RenewCertificateRequest struct {
|
||||
Template *x509.Certificate
|
||||
Lifetime time.Duration
|
||||
Backdate time.Duration
|
||||
RequestID string
|
||||
}
|
||||
|
||||
// RenewCertificateResponse is the response to a renew certificate request.
|
||||
type RenewCertificateResponse struct {
|
||||
Certificate *x509.Certificate
|
||||
CertificateChain []*x509.Certificate
|
||||
}
|
||||
|
||||
// RevokeCertificateRequest is the request used to revoke a certificate.
|
||||
type RevokeCertificateRequest struct {
|
||||
Certificate *x509.Certificate
|
||||
Reason string
|
||||
ReasonCode int
|
||||
RequestID string
|
||||
}
|
||||
|
||||
// RevokeCertificateResponse is the response to a revoke certificate request.
|
||||
type RevokeCertificateResponse struct {
|
||||
Certificate *x509.Certificate
|
||||
CertificateChain []*x509.Certificate
|
||||
}
|
||||
|
||||
// GetCertificateAuthorityRequest is the request used to get the root
|
||||
// certificate from a CAS.
|
||||
type GetCertificateAuthorityRequest struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// GetCertificateAuthorityResponse is the response that contains
|
||||
// the root certificate.
|
||||
type GetCertificateAuthorityResponse struct {
|
||||
RootCertificate *x509.Certificate
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CertificateAuthorityService is the interface implemented to support external
|
||||
// certificate authorities.
|
||||
type CertificateAuthorityService interface {
|
||||
CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error)
|
||||
RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error)
|
||||
RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error)
|
||||
}
|
||||
|
||||
// CertificateAuthorityGetter is an interface implemented by a
|
||||
// CertificateAuthorityService that has a method to get the root certificate.
|
||||
type CertificateAuthorityGetter interface {
|
||||
GetCertificateAuthority(req *GetCertificateAuthorityRequest) (*GetCertificateAuthorityResponse, error)
|
||||
}
|
||||
|
||||
// Type represents the CAS type used.
|
||||
type Type string
|
||||
|
||||
const (
|
||||
// DefaultCAS is a CertificateAuthorityService using software.
|
||||
DefaultCAS = ""
|
||||
// SoftCAS is a CertificateAuthorityService using software.
|
||||
SoftCAS = "softcas"
|
||||
// CloudCAS is a CertificateAuthorityService using Google Cloud CAS.
|
||||
CloudCAS = "cloudcas"
|
||||
)
|
||||
|
||||
// String returns a string from the type. It will always return the lower case
|
||||
// version of the Type, as we need a standard type to compare and use as the
|
||||
// registry key.
|
||||
func (t Type) String() string {
|
||||
if t == "" {
|
||||
return SoftCAS
|
||||
}
|
||||
return strings.ToLower(string(t))
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package apiv1
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestType_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
t Type
|
||||
want string
|
||||
}{
|
||||
{"default", "", "softcas"},
|
||||
{"SoftCAS", SoftCAS, "softcas"},
|
||||
{"CloudCAS", CloudCAS, "cloudcas"},
|
||||
{"UnknownCAS", "UnknownCAS", "unknowncas"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.t.String(); got != tt.want {
|
||||
t.Errorf("Type.String() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package cas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
|
||||
// Enable default implementation
|
||||
_ "github.com/smallstep/certificates/cas/softcas"
|
||||
)
|
||||
|
||||
// CertificateAuthorityService is the interface implemented by all the CAS.
|
||||
type CertificateAuthorityService = apiv1.CertificateAuthorityService
|
||||
|
||||
func New(ctx context.Context, opts apiv1.Options) (CertificateAuthorityService, error) {
|
||||
if err := opts.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := apiv1.Type(strings.ToLower(opts.Type))
|
||||
if t == apiv1.DefaultCAS {
|
||||
t = apiv1.SoftCAS
|
||||
}
|
||||
|
||||
fn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(t)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unsupported kms type '%s'", t)
|
||||
}
|
||||
return fn(ctx, opts)
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package cas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/smallstep/certificates/cas/softcas"
|
||||
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
expected := &softcas.SoftCAS{
|
||||
Issuer: &x509.Certificate{Subject: pkix.Name{CommonName: "Test Issuer"}},
|
||||
Signer: ed25519.PrivateKey{},
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
opts apiv1.Options
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want CertificateAuthorityService
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok default", args{context.Background(), apiv1.Options{
|
||||
Issuer: &x509.Certificate{Subject: pkix.Name{CommonName: "Test Issuer"}},
|
||||
Signer: ed25519.PrivateKey{},
|
||||
}}, expected, false},
|
||||
{"ok softcas", args{context.Background(), apiv1.Options{
|
||||
Type: "softcas",
|
||||
Issuer: &x509.Certificate{Subject: pkix.Name{CommonName: "Test Issuer"}},
|
||||
Signer: ed25519.PrivateKey{},
|
||||
}}, expected, false},
|
||||
{"ok SoftCAS", args{context.Background(), apiv1.Options{
|
||||
Type: "SoftCAS",
|
||||
Issuer: &x509.Certificate{Subject: pkix.Name{CommonName: "Test Issuer"}},
|
||||
Signer: ed25519.PrivateKey{},
|
||||
}}, expected, false},
|
||||
{"fail empty", args{context.Background(), apiv1.Options{}}, (*softcas.SoftCAS)(nil), true},
|
||||
{"fail type", args{context.Background(), apiv1.Options{Type: "FailCAS"}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := New(tt.args.ctx, tt.args.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("New() = %#v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,328 @@
|
||||
package cloudcas
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1"
|
||||
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
var (
|
||||
oidExtensionSubjectKeyID = []int{2, 5, 29, 14}
|
||||
oidExtensionKeyUsage = []int{2, 5, 29, 15}
|
||||
oidExtensionExtendedKeyUsage = []int{2, 5, 29, 37}
|
||||
oidExtensionAuthorityKeyID = []int{2, 5, 29, 35}
|
||||
oidExtensionBasicConstraints = []int{2, 5, 29, 19}
|
||||
oidExtensionSubjectAltName = []int{2, 5, 29, 17}
|
||||
oidExtensionCRLDistributionPoints = []int{2, 5, 29, 31}
|
||||
oidExtensionCertificatePolicies = []int{2, 5, 29, 32}
|
||||
oidExtensionAuthorityInfoAccess = []int{1, 3, 6, 1, 5, 5, 7, 1, 1}
|
||||
)
|
||||
|
||||
var extraExtensions = [...]asn1.ObjectIdentifier{
|
||||
oidExtensionSubjectKeyID, // Added by CAS
|
||||
oidExtensionKeyUsage, // Added in CertificateConfig.ReusableConfig
|
||||
oidExtensionExtendedKeyUsage, // Added in CertificateConfig.ReusableConfig
|
||||
oidExtensionAuthorityKeyID, // Added by CAS
|
||||
oidExtensionBasicConstraints, // Added in CertificateConfig.ReusableConfig
|
||||
oidExtensionSubjectAltName, // Added in CertificateConfig.SubjectConfig.SubjectAltName
|
||||
oidExtensionCRLDistributionPoints, // Added by CAS
|
||||
oidExtensionCertificatePolicies, // Added in CertificateConfig.ReusableConfig
|
||||
oidExtensionAuthorityInfoAccess, // Added in CertificateConfig.ReusableConfig and by CAS
|
||||
}
|
||||
|
||||
var (
|
||||
oidExtKeyUsageAny = asn1.ObjectIdentifier{2, 5, 29, 37, 0}
|
||||
oidExtKeyUsageIPSECEndSystem = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5}
|
||||
oidExtKeyUsageIPSECTunnel = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6}
|
||||
oidExtKeyUsageIPSECUser = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7}
|
||||
oidExtKeyUsageMicrosoftServerGatedCrypto = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}
|
||||
oidExtKeyUsageNetscapeServerGatedCrypto = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1}
|
||||
oidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}
|
||||
oidExtKeyUsageMicrosoftKernelCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}
|
||||
)
|
||||
|
||||
const (
|
||||
nameTypeEmail = 1
|
||||
nameTypeDNS = 2
|
||||
nameTypeURI = 6
|
||||
nameTypeIP = 7
|
||||
)
|
||||
|
||||
func createCertificateConfig(tpl *x509.Certificate) (*pb.Certificate_Config, error) {
|
||||
pk, err := createPublicKey(tpl.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &pb.CertificateConfig{
|
||||
SubjectConfig: &pb.CertificateConfig_SubjectConfig{
|
||||
Subject: createSubject(tpl),
|
||||
CommonName: tpl.Subject.CommonName,
|
||||
SubjectAltName: createSubjectAlternativeNames(tpl),
|
||||
},
|
||||
ReusableConfig: createReusableConfig(tpl),
|
||||
PublicKey: pk,
|
||||
}
|
||||
return &pb.Certificate_Config{
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) {
|
||||
switch key := key.(type) {
|
||||
case *ecdsa.PublicKey:
|
||||
asn1Bytes, err := x509.MarshalPKIXPublicKey(key)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error marshaling public key")
|
||||
}
|
||||
return &pb.PublicKey{
|
||||
Type: pb.PublicKey_PEM_EC_KEY,
|
||||
Key: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: asn1Bytes,
|
||||
}),
|
||||
}, nil
|
||||
case *rsa.PublicKey:
|
||||
return &pb.PublicKey{
|
||||
Type: pb.PublicKey_PEM_RSA_KEY,
|
||||
Key: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: x509.MarshalPKCS1PublicKey(key),
|
||||
}),
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported public key type: %T", key)
|
||||
}
|
||||
}
|
||||
|
||||
func createSubject(cert *x509.Certificate) *pb.Subject {
|
||||
sub := cert.Subject
|
||||
ret := new(pb.Subject)
|
||||
if len(sub.Country) > 0 {
|
||||
ret.CountryCode = sub.Country[0]
|
||||
}
|
||||
if len(sub.Organization) > 0 {
|
||||
ret.Organization = sub.Organization[0]
|
||||
}
|
||||
if len(sub.OrganizationalUnit) > 0 {
|
||||
ret.OrganizationalUnit = sub.OrganizationalUnit[0]
|
||||
}
|
||||
if len(sub.Locality) > 0 {
|
||||
ret.Locality = sub.Locality[0]
|
||||
}
|
||||
if len(sub.Province) > 0 {
|
||||
ret.Province = sub.Province[0]
|
||||
}
|
||||
if len(sub.StreetAddress) > 0 {
|
||||
ret.StreetAddress = sub.StreetAddress[0]
|
||||
}
|
||||
if len(sub.PostalCode) > 0 {
|
||||
ret.PostalCode = sub.PostalCode[0]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func createSubjectAlternativeNames(cert *x509.Certificate) *pb.SubjectAltNames {
|
||||
ret := new(pb.SubjectAltNames)
|
||||
ret.DnsNames = cert.DNSNames
|
||||
ret.EmailAddresses = cert.EmailAddresses
|
||||
if n := len(cert.IPAddresses); n > 0 {
|
||||
ret.IpAddresses = make([]string, n)
|
||||
for i, ip := range cert.IPAddresses {
|
||||
ret.IpAddresses[i] = ip.String()
|
||||
}
|
||||
}
|
||||
if n := len(cert.URIs); n > 0 {
|
||||
ret.Uris = make([]string, n)
|
||||
for i, u := range cert.URIs {
|
||||
ret.Uris[i] = u.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Add extra SANs coming from the extensions
|
||||
if ext, ok := findExtraExtension(cert, oidExtensionSubjectAltName); ok {
|
||||
var rawValues []asn1.RawValue
|
||||
if _, err := asn1.Unmarshal(ext.Value, &rawValues); err == nil {
|
||||
var newValues []asn1.RawValue
|
||||
|
||||
for _, v := range rawValues {
|
||||
if v.Class == asn1.ClassContextSpecific {
|
||||
switch v.Tag {
|
||||
case nameTypeDNS:
|
||||
if len(ret.DnsNames) == 0 {
|
||||
newValues = append(newValues, v)
|
||||
}
|
||||
case nameTypeEmail:
|
||||
if len(ret.EmailAddresses) == 0 {
|
||||
newValues = append(newValues, v)
|
||||
}
|
||||
case nameTypeIP:
|
||||
if len(ret.IpAddresses) == 0 {
|
||||
newValues = append(newValues, v)
|
||||
}
|
||||
case nameTypeURI:
|
||||
if len(ret.Uris) == 0 {
|
||||
newValues = append(newValues, v)
|
||||
}
|
||||
default:
|
||||
newValues = append(newValues, v)
|
||||
}
|
||||
} else {
|
||||
newValues = append(newValues, v)
|
||||
}
|
||||
}
|
||||
if len(newValues) > 0 {
|
||||
if b, err := asn1.Marshal(newValues); err == nil {
|
||||
ret.CustomSans = []*pb.X509Extension{{
|
||||
ObjectId: createObjectID(ext.Id),
|
||||
Critical: ext.Critical,
|
||||
Value: b,
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper {
|
||||
var unknownEKUs []*pb.ObjectId
|
||||
var ekuOptions = &pb.KeyUsage_ExtendedKeyUsageOptions{}
|
||||
for _, eku := range cert.ExtKeyUsage {
|
||||
switch eku {
|
||||
case x509.ExtKeyUsageAny:
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageAny))
|
||||
case x509.ExtKeyUsageServerAuth:
|
||||
ekuOptions.ServerAuth = true
|
||||
case x509.ExtKeyUsageClientAuth:
|
||||
ekuOptions.ClientAuth = true
|
||||
case x509.ExtKeyUsageCodeSigning:
|
||||
ekuOptions.CodeSigning = true
|
||||
case x509.ExtKeyUsageEmailProtection:
|
||||
ekuOptions.EmailProtection = true
|
||||
case x509.ExtKeyUsageIPSECEndSystem:
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECEndSystem))
|
||||
case x509.ExtKeyUsageIPSECTunnel:
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECTunnel))
|
||||
case x509.ExtKeyUsageIPSECUser:
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECUser))
|
||||
case x509.ExtKeyUsageTimeStamping:
|
||||
ekuOptions.TimeStamping = true
|
||||
case x509.ExtKeyUsageOCSPSigning:
|
||||
ekuOptions.OcspSigning = true
|
||||
case x509.ExtKeyUsageMicrosoftServerGatedCrypto:
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftServerGatedCrypto))
|
||||
case x509.ExtKeyUsageNetscapeServerGatedCrypto:
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageNetscapeServerGatedCrypto))
|
||||
case x509.ExtKeyUsageMicrosoftCommercialCodeSigning:
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftCommercialCodeSigning))
|
||||
case x509.ExtKeyUsageMicrosoftKernelCodeSigning:
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftKernelCodeSigning))
|
||||
}
|
||||
}
|
||||
|
||||
for _, oid := range cert.UnknownExtKeyUsage {
|
||||
unknownEKUs = append(unknownEKUs, createObjectID(oid))
|
||||
}
|
||||
|
||||
var policyIDs []*pb.ObjectId
|
||||
for _, oid := range cert.PolicyIdentifiers {
|
||||
policyIDs = append(policyIDs, createObjectID(oid))
|
||||
}
|
||||
|
||||
var caOptions *pb.ReusableConfigValues_CaOptions
|
||||
if cert.BasicConstraintsValid {
|
||||
var maxPathLength *wrapperspb.Int32Value
|
||||
switch {
|
||||
case cert.MaxPathLenZero:
|
||||
maxPathLength = wrapperspb.Int32(0)
|
||||
case cert.MaxPathLen > 0:
|
||||
maxPathLength = wrapperspb.Int32(int32(cert.MaxPathLen))
|
||||
default:
|
||||
maxPathLength = nil
|
||||
}
|
||||
|
||||
caOptions = &pb.ReusableConfigValues_CaOptions{
|
||||
IsCa: wrapperspb.Bool(cert.IsCA),
|
||||
MaxIssuerPathLength: maxPathLength,
|
||||
}
|
||||
}
|
||||
|
||||
var extraExtensions []*pb.X509Extension
|
||||
for _, ext := range cert.ExtraExtensions {
|
||||
if isExtraExtension(ext.Id) {
|
||||
extraExtensions = append(extraExtensions, &pb.X509Extension{
|
||||
ObjectId: createObjectID(ext.Id),
|
||||
Critical: ext.Critical,
|
||||
Value: ext.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
values := &pb.ReusableConfigValues{
|
||||
KeyUsage: &pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
DigitalSignature: cert.KeyUsage&x509.KeyUsageDigitalSignature > 0,
|
||||
ContentCommitment: cert.KeyUsage&x509.KeyUsageContentCommitment > 0,
|
||||
KeyEncipherment: cert.KeyUsage&x509.KeyUsageKeyEncipherment > 0,
|
||||
DataEncipherment: cert.KeyUsage&x509.KeyUsageDataEncipherment > 0,
|
||||
KeyAgreement: cert.KeyUsage&x509.KeyUsageKeyAgreement > 0,
|
||||
CertSign: cert.KeyUsage&x509.KeyUsageCertSign > 0,
|
||||
CrlSign: cert.KeyUsage&x509.KeyUsageCRLSign > 0,
|
||||
EncipherOnly: cert.KeyUsage&x509.KeyUsageEncipherOnly > 0,
|
||||
DecipherOnly: cert.KeyUsage&x509.KeyUsageDecipherOnly > 0,
|
||||
},
|
||||
ExtendedKeyUsage: ekuOptions,
|
||||
UnknownExtendedKeyUsages: unknownEKUs,
|
||||
},
|
||||
CaOptions: caOptions,
|
||||
PolicyIds: policyIDs,
|
||||
AiaOcspServers: cert.OCSPServer,
|
||||
AdditionalExtensions: extraExtensions,
|
||||
}
|
||||
|
||||
return &pb.ReusableConfigWrapper{
|
||||
ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{
|
||||
ReusableConfigValues: values,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// isExtraExtension returns true if the extension oid is not managed in a
|
||||
// different way.
|
||||
func isExtraExtension(oid asn1.ObjectIdentifier) bool {
|
||||
for _, id := range extraExtensions {
|
||||
if id.Equal(oid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func createObjectID(oid asn1.ObjectIdentifier) *pb.ObjectId {
|
||||
ret := make([]int32, len(oid))
|
||||
for i, v := range oid {
|
||||
ret[i] = int32(v)
|
||||
}
|
||||
return &pb.ObjectId{
|
||||
ObjectIdPath: ret,
|
||||
}
|
||||
}
|
||||
|
||||
func findExtraExtension(cert *x509.Certificate, oid asn1.ObjectIdentifier) (pkix.Extension, bool) {
|
||||
for _, ext := range cert.ExtraExtensions {
|
||||
if ext.Id.Equal(oid) {
|
||||
return ext, true
|
||||
}
|
||||
}
|
||||
return pkix.Extension{}, false
|
||||
}
|
@ -0,0 +1,550 @@
|
||||
package cloudcas
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1"
|
||||
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
var (
|
||||
testLeafPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlNnX2+xfjX
|
||||
a1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czgPw==
|
||||
-----END PUBLIC KEY-----
|
||||
`
|
||||
testRSACertificate = `-----BEGIN CERTIFICATE-----
|
||||
MIICozCCAkmgAwIBAgIRANNhMpODj7ThgviZCoF6kj8wCgYIKoZIzj0EAwIwKjEo
|
||||
MCYGA1UEAxMfR29vZ2xlIENBUyBUZXN0IEludGVybWVkaWF0ZSBDQTAeFw0yMDA5
|
||||
MTUwMTUxMDdaFw0zMDA5MTMwMTUxMDNaMB0xGzAZBgNVBAMTEnRlc3Quc21hbGxz
|
||||
dGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANPRjuIlsP5Z
|
||||
672syAsHlbILFabG/xmrlsO0UdcLo4Yjf9WPAFA+7q+CsVDFh4dQbMv96fsHtdYP
|
||||
E9wlWyMqYG+5E8QT2i0WNFEoYcXOGZuXdyD/TA5Aucu1RuYLrZXQrXWDnvaWOgvr
|
||||
EZ6s9VsPCzzkL8KBejIMQIMY0KXEJfB/HgXZNn8V2trZkWT5CzxbcOF3s3UC1Z6F
|
||||
Ja6zjpxhSyRkqgknJxv6yK4t7HEwdhrDI8uyxJYHPQWKNRjWecHWE9E+MtoS7D08
|
||||
mTh8qlAKoBbkGolR2nJSXffU09F3vSg+MIfjPiRqjf6394cQ3T9D5yZK//rCrxWU
|
||||
8KKBQMEmdKcCAwEAAaOBkTCBjjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYI
|
||||
KwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQffuoYvH1+IF1cipl35gXJxSJE
|
||||
SjAfBgNVHSMEGDAWgBRIOVqyLDSlErJLuWWEvRm5UU1r1TAdBgNVHREEFjAUghJ0
|
||||
ZXN0LnNtYWxsc3RlcC5jb20wCgYIKoZIzj0EAwIDSAAwRQIhAL9AAw/LVLvvxBkM
|
||||
sJnHd+RIk7ZblkgcArwpIS2+Z5xNAiBtUED4zyimz9b4aQiXdw4IMd2CKxVyW8eE
|
||||
6x1vSZMvzQ==
|
||||
-----END CERTIFICATE-----`
|
||||
testRSAPublicKey = `-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEA09GO4iWw/lnrvazICweVsgsVpsb/GauWw7RR1wujhiN/1Y8AUD7u
|
||||
r4KxUMWHh1Bsy/3p+we11g8T3CVbIypgb7kTxBPaLRY0UShhxc4Zm5d3IP9MDkC5
|
||||
y7VG5gutldCtdYOe9pY6C+sRnqz1Ww8LPOQvwoF6MgxAgxjQpcQl8H8eBdk2fxXa
|
||||
2tmRZPkLPFtw4XezdQLVnoUlrrOOnGFLJGSqCScnG/rIri3scTB2GsMjy7LElgc9
|
||||
BYo1GNZ5wdYT0T4y2hLsPTyZOHyqUAqgFuQaiVHaclJd99TT0Xe9KD4wh+M+JGqN
|
||||
/rf3hxDdP0PnJkr/+sKvFZTwooFAwSZ0pwIDAQAB
|
||||
-----END RSA PUBLIC KEY-----
|
||||
`
|
||||
)
|
||||
|
||||
func Test_createCertificateConfig(t *testing.T) {
|
||||
cert := mustParseCertificate(t, testLeafCertificate)
|
||||
type args struct {
|
||||
tpl *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *pb.Certificate_Config
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{cert}, &pb.Certificate_Config{
|
||||
Config: &pb.CertificateConfig{
|
||||
SubjectConfig: &pb.CertificateConfig_SubjectConfig{
|
||||
Subject: &pb.Subject{},
|
||||
CommonName: "test.smallstep.com",
|
||||
SubjectAltName: &pb.SubjectAltNames{
|
||||
DnsNames: []string{"test.smallstep.com"},
|
||||
},
|
||||
},
|
||||
ReusableConfig: &pb.ReusableConfigWrapper{
|
||||
ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{
|
||||
ReusableConfigValues: &pb.ReusableConfigValues{
|
||||
KeyUsage: &pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
DigitalSignature: true,
|
||||
},
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{
|
||||
ClientAuth: true,
|
||||
ServerAuth: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PublicKey: &pb.PublicKey{
|
||||
Type: pb.PublicKey_PEM_EC_KEY,
|
||||
Key: []byte(testLeafPublicKey),
|
||||
},
|
||||
},
|
||||
}, false},
|
||||
{"fail", args{&x509.Certificate{}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := createCertificateConfig(tt.args.tpl)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("createCertificateConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("createCertificateConfig() = %v, want %v", got.Config.ReusableConfig, tt.want.Config.ReusableConfig)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createPublicKey(t *testing.T) {
|
||||
edpub, _, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ecCert := mustParseCertificate(t, testLeafCertificate)
|
||||
rsaCert := mustParseCertificate(t, testRSACertificate)
|
||||
type args struct {
|
||||
key crypto.PublicKey
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *pb.PublicKey
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok ec", args{ecCert.PublicKey}, &pb.PublicKey{
|
||||
Type: pb.PublicKey_PEM_EC_KEY,
|
||||
Key: []byte(testLeafPublicKey),
|
||||
}, false},
|
||||
{"ok rsa", args{rsaCert.PublicKey}, &pb.PublicKey{
|
||||
Type: pb.PublicKey_PEM_RSA_KEY,
|
||||
Key: []byte(testRSAPublicKey),
|
||||
}, false},
|
||||
{"fail ed25519", args{edpub}, nil, true},
|
||||
{"fail ec marshal", args{&ecdsa.PublicKey{
|
||||
Curve: &elliptic.CurveParams{Name: "FOO", BitSize: 256},
|
||||
X: ecCert.PublicKey.(*ecdsa.PublicKey).X,
|
||||
Y: ecCert.PublicKey.(*ecdsa.PublicKey).Y,
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := createPublicKey(tt.args.key)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("createPublicKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("createPublicKey() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createSubject(t *testing.T) {
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *pb.Subject
|
||||
}{
|
||||
{"ok empty", args{&x509.Certificate{}}, &pb.Subject{}},
|
||||
{"ok all", args{&x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
Organization: []string{"Smallstep Labs"},
|
||||
OrganizationalUnit: []string{"Engineering"},
|
||||
Locality: []string{"San Francisco"},
|
||||
Province: []string{"California"},
|
||||
StreetAddress: []string{"1 A St."},
|
||||
PostalCode: []string{"12345"},
|
||||
SerialNumber: "1234567890",
|
||||
CommonName: "test.smallstep.com",
|
||||
},
|
||||
}}, &pb.Subject{
|
||||
CountryCode: "US",
|
||||
Organization: "Smallstep Labs",
|
||||
OrganizationalUnit: "Engineering",
|
||||
Locality: "San Francisco",
|
||||
Province: "California",
|
||||
StreetAddress: "1 A St.",
|
||||
PostalCode: "12345",
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := createSubject(tt.args.cert); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("createSubject() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createSubjectAlternativeNames(t *testing.T) {
|
||||
marshalRawValues := func(rawValues []asn1.RawValue) []byte {
|
||||
b, err := asn1.Marshal(rawValues)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
uri := func(s string) *url.URL {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *pb.SubjectAltNames
|
||||
}{
|
||||
{"ok empty", args{&x509.Certificate{}}, &pb.SubjectAltNames{}},
|
||||
{"ok dns", args{&x509.Certificate{DNSNames: []string{
|
||||
"doe.com", "doe.org",
|
||||
}}}, &pb.SubjectAltNames{DnsNames: []string{"doe.com", "doe.org"}}},
|
||||
{"ok emails", args{&x509.Certificate{EmailAddresses: []string{
|
||||
"john@doe.com", "jane@doe.com",
|
||||
}}}, &pb.SubjectAltNames{EmailAddresses: []string{"john@doe.com", "jane@doe.com"}}},
|
||||
{"ok ips", args{&x509.Certificate{IPAddresses: []net.IP{
|
||||
net.ParseIP("127.0.0.1"), net.ParseIP("1.2.3.4"),
|
||||
net.ParseIP("::1"), net.ParseIP("2001:0db8:85a3:a0b:12f0:8a2e:0370:7334"), net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
||||
}}}, &pb.SubjectAltNames{IpAddresses: []string{"127.0.0.1", "1.2.3.4", "::1", "2001:db8:85a3:a0b:12f0:8a2e:370:7334", "2001:db8:85a3::8a2e:370:7334"}}},
|
||||
{"ok uris", args{&x509.Certificate{URIs: []*url.URL{
|
||||
uri("mailto:john@doe.com"), uri("https://john@doe.com/hello"),
|
||||
}}}, &pb.SubjectAltNames{Uris: []string{"mailto:john@doe.com", "https://john@doe.com/hello"}}},
|
||||
{"ok extensions", args{&x509.Certificate{
|
||||
ExtraExtensions: []pkix.Extension{{
|
||||
Id: []int{2, 5, 29, 17}, Critical: true, Value: []byte{
|
||||
0x30, 0x48, 0x82, 0x0b, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x81,
|
||||
0x0c, 0x6a, 0x61, 0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x87, 0x04, 0x01,
|
||||
0x02, 0x03, 0x04, 0x87, 0x10, 0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x0a, 0x0b, 0x12, 0xf0, 0x8a,
|
||||
0x2e, 0x03, 0x70, 0x73, 0x34, 0x86, 0x13, 0x6d, 0x61, 0x69, 0x6c, 0x74, 0x6f, 0x3a, 0x6a, 0x61,
|
||||
0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
},
|
||||
}},
|
||||
}}, &pb.SubjectAltNames{
|
||||
CustomSans: []*pb.X509Extension{{
|
||||
ObjectId: &pb.ObjectId{ObjectIdPath: []int32{2, 5, 29, 17}},
|
||||
Critical: true,
|
||||
Value: []byte{
|
||||
0x30, 0x48, 0x82, 0x0b, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x81,
|
||||
0x0c, 0x6a, 0x61, 0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x87, 0x04, 0x01,
|
||||
0x02, 0x03, 0x04, 0x87, 0x10, 0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x0a, 0x0b, 0x12, 0xf0, 0x8a,
|
||||
0x2e, 0x03, 0x70, 0x73, 0x34, 0x86, 0x13, 0x6d, 0x61, 0x69, 0x6c, 0x74, 0x6f, 0x3a, 0x6a, 0x61,
|
||||
0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
},
|
||||
}},
|
||||
}},
|
||||
{"ok extra extensions", args{&x509.Certificate{
|
||||
DNSNames: []string{"doe.com"},
|
||||
ExtraExtensions: []pkix.Extension{{
|
||||
Id: []int{2, 5, 29, 17}, Critical: true, Value: marshalRawValues([]asn1.RawValue{
|
||||
{Class: asn1.ClassApplication, Tag: 2, IsCompound: true, Bytes: []byte{}},
|
||||
{Class: asn1.ClassContextSpecific, Tag: nameTypeDNS, Bytes: []byte("doe.com")},
|
||||
{Class: asn1.ClassContextSpecific, Tag: nameTypeEmail, Bytes: []byte("jane@doe.com")},
|
||||
{Class: asn1.ClassContextSpecific, Tag: 8, Bytes: []byte("foo.bar")},
|
||||
}),
|
||||
}},
|
||||
}}, &pb.SubjectAltNames{
|
||||
DnsNames: []string{"doe.com"},
|
||||
CustomSans: []*pb.X509Extension{{
|
||||
ObjectId: &pb.ObjectId{ObjectIdPath: []int32{2, 5, 29, 17}},
|
||||
Critical: true,
|
||||
Value: marshalRawValues([]asn1.RawValue{
|
||||
{Class: asn1.ClassApplication, Tag: 2, IsCompound: true, Bytes: []byte{}},
|
||||
{Class: asn1.ClassContextSpecific, Tag: nameTypeEmail, Bytes: []byte("jane@doe.com")},
|
||||
{Class: asn1.ClassContextSpecific, Tag: 8, Bytes: []byte("foo.bar")},
|
||||
}),
|
||||
}},
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := createSubjectAlternativeNames(tt.args.cert); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("createSubjectAlternativeNames() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createReusableConfig(t *testing.T) {
|
||||
withKU := func(ku *pb.KeyUsage) *pb.ReusableConfigWrapper {
|
||||
if ku.BaseKeyUsage == nil {
|
||||
ku.BaseKeyUsage = &pb.KeyUsage_KeyUsageOptions{}
|
||||
}
|
||||
if ku.ExtendedKeyUsage == nil {
|
||||
ku.ExtendedKeyUsage = &pb.KeyUsage_ExtendedKeyUsageOptions{}
|
||||
}
|
||||
return &pb.ReusableConfigWrapper{
|
||||
ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{
|
||||
ReusableConfigValues: &pb.ReusableConfigValues{
|
||||
KeyUsage: ku,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
withRCV := func(rcv *pb.ReusableConfigValues) *pb.ReusableConfigWrapper {
|
||||
if rcv.KeyUsage == nil {
|
||||
rcv.KeyUsage = &pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{},
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{},
|
||||
}
|
||||
}
|
||||
return &pb.ReusableConfigWrapper{
|
||||
ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{
|
||||
ReusableConfigValues: rcv,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *pb.ReusableConfigWrapper
|
||||
}{
|
||||
{"keyUsageDigitalSignature", args{&x509.Certificate{
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
}}, &pb.ReusableConfigWrapper{
|
||||
ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{
|
||||
ReusableConfigValues: &pb.ReusableConfigValues{
|
||||
KeyUsage: &pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
DigitalSignature: true,
|
||||
},
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{},
|
||||
UnknownExtendedKeyUsages: nil,
|
||||
},
|
||||
CaOptions: nil,
|
||||
PolicyIds: nil,
|
||||
AiaOcspServers: nil,
|
||||
AdditionalExtensions: nil,
|
||||
},
|
||||
},
|
||||
}},
|
||||
// KeyUsage
|
||||
{"KeyUsageDigitalSignature", args{&x509.Certificate{KeyUsage: x509.KeyUsageDigitalSignature}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
DigitalSignature: true,
|
||||
},
|
||||
})},
|
||||
{"KeyUsageContentCommitment", args{&x509.Certificate{KeyUsage: x509.KeyUsageContentCommitment}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
ContentCommitment: true,
|
||||
},
|
||||
})},
|
||||
{"KeyUsageKeyEncipherment", args{&x509.Certificate{KeyUsage: x509.KeyUsageKeyEncipherment}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
KeyEncipherment: true,
|
||||
},
|
||||
})},
|
||||
{"KeyUsageDataEncipherment", args{&x509.Certificate{KeyUsage: x509.KeyUsageDataEncipherment}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
DataEncipherment: true,
|
||||
},
|
||||
})},
|
||||
{"KeyUsageKeyAgreement", args{&x509.Certificate{KeyUsage: x509.KeyUsageKeyAgreement}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
KeyAgreement: true,
|
||||
},
|
||||
})},
|
||||
{"KeyUsageCertSign", args{&x509.Certificate{KeyUsage: x509.KeyUsageCertSign}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
CertSign: true,
|
||||
},
|
||||
})},
|
||||
{"KeyUsageCRLSign", args{&x509.Certificate{KeyUsage: x509.KeyUsageCRLSign}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
CrlSign: true,
|
||||
},
|
||||
})},
|
||||
{"KeyUsageEncipherOnly", args{&x509.Certificate{KeyUsage: x509.KeyUsageEncipherOnly}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
EncipherOnly: true,
|
||||
},
|
||||
})},
|
||||
{"KeyUsageDecipherOnly", args{&x509.Certificate{KeyUsage: x509.KeyUsageDecipherOnly}}, withKU(&pb.KeyUsage{
|
||||
BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{
|
||||
DecipherOnly: true,
|
||||
},
|
||||
})},
|
||||
// ExtKeyUsage
|
||||
{"ExtKeyUsageAny", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{2, 5, 29, 37, 0}}},
|
||||
})},
|
||||
{"ExtKeyUsageServerAuth", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}}, withKU(&pb.KeyUsage{
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{
|
||||
ServerAuth: true,
|
||||
},
|
||||
})},
|
||||
{"ExtKeyUsageClientAuth", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}}, withKU(&pb.KeyUsage{
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{
|
||||
ClientAuth: true,
|
||||
},
|
||||
})},
|
||||
{"ExtKeyUsageCodeSigning", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}}}, withKU(&pb.KeyUsage{
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{
|
||||
CodeSigning: true,
|
||||
},
|
||||
})},
|
||||
{"ExtKeyUsageEmailProtection", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}}}, withKU(&pb.KeyUsage{
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{
|
||||
EmailProtection: true,
|
||||
},
|
||||
})},
|
||||
{"ExtKeyUsageIPSECEndSystem", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECEndSystem}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 5}}},
|
||||
})},
|
||||
{"ExtKeyUsageIPSECTunnel", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECTunnel}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 6}}},
|
||||
})},
|
||||
{"ExtKeyUsageIPSECUser", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECUser}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 7}}},
|
||||
})},
|
||||
{"ExtKeyUsageTimeStamping", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}}}, withKU(&pb.KeyUsage{
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{
|
||||
TimeStamping: true,
|
||||
},
|
||||
})},
|
||||
{"ExtKeyUsageOCSPSigning", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning}}}, withKU(&pb.KeyUsage{
|
||||
ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{
|
||||
OcspSigning: true,
|
||||
},
|
||||
})},
|
||||
{"ExtKeyUsageMicrosoftServerGatedCrypto", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftServerGatedCrypto}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}}},
|
||||
})},
|
||||
{"ExtKeyUsageNetscapeServerGatedCrypto", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageNetscapeServerGatedCrypto}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{2, 16, 840, 1, 113730, 4, 1}}},
|
||||
})},
|
||||
{"ExtKeyUsageMicrosoftCommercialCodeSigning", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftCommercialCodeSigning}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}}},
|
||||
})},
|
||||
{"ExtKeyUsageMicrosoftKernelCodeSigning", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftKernelCodeSigning}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}}},
|
||||
})},
|
||||
// UnknownExtendedKeyUsages
|
||||
{"UnknownExtKeyUsage", args{&x509.Certificate{UnknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 2, 3, 4}, {4, 3, 2, 1}}}}, withKU(&pb.KeyUsage{
|
||||
UnknownExtendedKeyUsages: []*pb.ObjectId{
|
||||
{ObjectIdPath: []int32{1, 2, 3, 4}},
|
||||
{ObjectIdPath: []int32{4, 3, 2, 1}},
|
||||
},
|
||||
})},
|
||||
// BasicCre
|
||||
{"BasicConstraintsCAMax0", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 0, MaxPathLenZero: true}}, withRCV(&pb.ReusableConfigValues{
|
||||
CaOptions: &pb.ReusableConfigValues_CaOptions{
|
||||
IsCa: wrapperspb.Bool(true),
|
||||
MaxIssuerPathLength: wrapperspb.Int32(0),
|
||||
},
|
||||
})},
|
||||
{"BasicConstraintsCAMax1", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 1, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{
|
||||
CaOptions: &pb.ReusableConfigValues_CaOptions{
|
||||
IsCa: wrapperspb.Bool(true),
|
||||
MaxIssuerPathLength: wrapperspb.Int32(1),
|
||||
},
|
||||
})},
|
||||
{"BasicConstraintsCANoMax", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: -1, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{
|
||||
CaOptions: &pb.ReusableConfigValues_CaOptions{
|
||||
IsCa: wrapperspb.Bool(true),
|
||||
MaxIssuerPathLength: nil,
|
||||
},
|
||||
})},
|
||||
{"BasicConstraintsCANoMax0", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{
|
||||
CaOptions: &pb.ReusableConfigValues_CaOptions{
|
||||
IsCa: wrapperspb.Bool(true),
|
||||
MaxIssuerPathLength: nil,
|
||||
},
|
||||
})},
|
||||
{"BasicConstraintsNoCA", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: false, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{
|
||||
CaOptions: &pb.ReusableConfigValues_CaOptions{
|
||||
IsCa: wrapperspb.Bool(false),
|
||||
MaxIssuerPathLength: nil,
|
||||
},
|
||||
})},
|
||||
{"BasicConstraintsNoValid", args{&x509.Certificate{BasicConstraintsValid: false, IsCA: false, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.ReusableConfigValues{
|
||||
CaOptions: nil,
|
||||
})},
|
||||
// PolicyIdentifiers
|
||||
{"PolicyIdentifiers", args{&x509.Certificate{PolicyIdentifiers: []asn1.ObjectIdentifier{{1, 2, 3, 4}, {4, 3, 2, 1}}}}, withRCV(&pb.ReusableConfigValues{
|
||||
PolicyIds: []*pb.ObjectId{
|
||||
{ObjectIdPath: []int32{1, 2, 3, 4}},
|
||||
{ObjectIdPath: []int32{4, 3, 2, 1}},
|
||||
},
|
||||
})},
|
||||
// OCSPServer
|
||||
{"OCPServers", args{&x509.Certificate{OCSPServer: []string{"https://oscp.doe.com", "https://doe.com/ocsp"}}}, withRCV(&pb.ReusableConfigValues{
|
||||
AiaOcspServers: []string{"https://oscp.doe.com", "https://doe.com/ocsp"},
|
||||
})},
|
||||
// Extensions
|
||||
{"Extensions", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{
|
||||
{Id: []int{1, 2, 3, 4}, Critical: true, Value: []byte("foobar")},
|
||||
{Id: []int{2, 5, 29, 17}, Critical: true, Value: []byte("SANs")}, //
|
||||
{Id: []int{4, 3, 2, 1}, Critical: false, Value: []byte("zoobar")},
|
||||
{Id: []int{2, 5, 29, 31}, Critical: false, Value: []byte("CRL Distribution points")},
|
||||
}}}, withRCV(&pb.ReusableConfigValues{
|
||||
AdditionalExtensions: []*pb.X509Extension{
|
||||
{ObjectId: &pb.ObjectId{ObjectIdPath: []int32{1, 2, 3, 4}}, Critical: true, Value: []byte("foobar")},
|
||||
{ObjectId: &pb.ObjectId{ObjectIdPath: []int32{4, 3, 2, 1}}, Critical: false, Value: []byte("zoobar")},
|
||||
},
|
||||
})},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := createReusableConfig(tt.args.cert); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("createReusableConfig() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isExtraExtension(t *testing.T) {
|
||||
type args struct {
|
||||
oid asn1.ObjectIdentifier
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{"oidExtensionSubjectKeyID", args{oidExtensionSubjectKeyID}, false},
|
||||
{"oidExtensionKeyUsage", args{oidExtensionKeyUsage}, false},
|
||||
{"oidExtensionExtendedKeyUsage", args{oidExtensionExtendedKeyUsage}, false},
|
||||
{"oidExtensionAuthorityKeyID", args{oidExtensionAuthorityKeyID}, false},
|
||||
{"oidExtensionBasicConstraints", args{oidExtensionBasicConstraints}, false},
|
||||
{"oidExtensionSubjectAltName", args{oidExtensionSubjectAltName}, false},
|
||||
{"oidExtensionCRLDistributionPoints", args{oidExtensionCRLDistributionPoints}, false},
|
||||
{"oidExtensionCertificatePolicies", args{oidExtensionCertificatePolicies}, false},
|
||||
{"oidExtensionAuthorityInfoAccess", args{oidExtensionAuthorityInfoAccess}, false},
|
||||
{"other", args{[]int{1, 2, 3, 4}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isExtraExtension(tt.args.oid); got != tt.want {
|
||||
t.Errorf("isExtraExtension() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,289 @@
|
||||
package cloudcas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"time"
|
||||
|
||||
privateca "cloud.google.com/go/security/privateca/apiv1beta1"
|
||||
"github.com/google/uuid"
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
"google.golang.org/api/option"
|
||||
pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1"
|
||||
durationpb "google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apiv1.Register(apiv1.CloudCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {
|
||||
return New(ctx, opts)
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// recocationCodeMap maps revocation reason codes from RFC 5280, to Google CAS
|
||||
// revocation reasons. Revocation reason 7 is not used, and revocation reason 8
|
||||
// (removeFromCRL) is not supported by Google CAS.
|
||||
var revocationCodeMap = map[int]pb.RevocationReason{
|
||||
0: pb.RevocationReason_REVOCATION_REASON_UNSPECIFIED,
|
||||
1: pb.RevocationReason_KEY_COMPROMISE,
|
||||
2: pb.RevocationReason_CERTIFICATE_AUTHORITY_COMPROMISE,
|
||||
3: pb.RevocationReason_AFFILIATION_CHANGED,
|
||||
4: pb.RevocationReason_SUPERSEDED,
|
||||
5: pb.RevocationReason_CESSATION_OF_OPERATION,
|
||||
6: pb.RevocationReason_CERTIFICATE_HOLD,
|
||||
9: pb.RevocationReason_PRIVILEGE_WITHDRAWN,
|
||||
10: pb.RevocationReason_ATTRIBUTE_AUTHORITY_COMPROMISE,
|
||||
}
|
||||
|
||||
// CloudCAS implements a Certificate Authority Service using Google Cloud CAS.
|
||||
type CloudCAS struct {
|
||||
client CertificateAuthorityClient
|
||||
certificateAuthority string
|
||||
}
|
||||
|
||||
// newCertificateAuthorityClient creates the certificate authority client. This
|
||||
// function is used for testing purposes.
|
||||
var newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) {
|
||||
var cloudOpts []option.ClientOption
|
||||
if credentialsFile != "" {
|
||||
cloudOpts = append(cloudOpts, option.WithCredentialsFile(credentialsFile))
|
||||
}
|
||||
client, err := privateca.NewCertificateAuthorityClient(ctx, cloudOpts...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error creating client")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
client, err := newCertificateAuthorityClient(ctx, opts.CredentialsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CloudCAS{
|
||||
client: client,
|
||||
certificateAuthority: opts.Certificateauthority,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCertificateAuthority returns the root certificate for the given
|
||||
// certificate authority. It implements apiv1.CertificateAuthorityGetter
|
||||
// interface.
|
||||
func (c *CloudCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {
|
||||
name := req.Name
|
||||
if name == "" {
|
||||
name = c.certificateAuthority
|
||||
}
|
||||
|
||||
ctx, cancel := defaultContext()
|
||||
defer cancel()
|
||||
|
||||
resp, err := c.client.GetCertificateAuthority(ctx, &pb.GetCertificateAuthorityRequest{
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cloudCAS GetCertificateAuthority failed")
|
||||
}
|
||||
if len(resp.PemCaCertificates) == 0 {
|
||||
return nil, errors.New("cloudCAS GetCertificateAuthority: PemCACertificate should not be empty")
|
||||
}
|
||||
|
||||
// Last certificate in the chain is the root.
|
||||
root, err := parseCertificate(resp.PemCaCertificates[len(resp.PemCaCertificates)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apiv1.GetCertificateAuthorityResponse{
|
||||
RootCertificate: root,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateCertificate signs a new certificate using Google Cloud CAS.
|
||||
func (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {
|
||||
switch {
|
||||
case req.Template == nil:
|
||||
return nil, errors.New("createCertificateRequest `template` cannot be nil")
|
||||
case req.Lifetime == 0:
|
||||
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
|
||||
}
|
||||
|
||||
cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apiv1.CreateCertificateResponse{
|
||||
Certificate: cert,
|
||||
CertificateChain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RenewCertificate renews the given certificate using Google Cloud CAS.
|
||||
// Google's CAS does not support the renew operation, so this method uses
|
||||
// CreateCertificate.
|
||||
func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
|
||||
switch {
|
||||
case req.Template == nil:
|
||||
return nil, errors.New("renewCertificateRequest `template` cannot be nil")
|
||||
case req.Lifetime == 0:
|
||||
return nil, errors.New("renewCertificateRequest `lifetime` cannot be 0")
|
||||
}
|
||||
|
||||
cert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apiv1.RenewCertificateResponse{
|
||||
Certificate: cert,
|
||||
CertificateChain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RevokeCertificate a certificate using Google Cloud CAS.
|
||||
func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
|
||||
reason, ok := revocationCodeMap[req.ReasonCode]
|
||||
switch {
|
||||
case !ok:
|
||||
return nil, errors.Errorf("revokeCertificate 'reasonCode=%d' is invalid or not supported", req.ReasonCode)
|
||||
case req.Certificate == nil:
|
||||
return nil, errors.New("revokeCertificateRequest `certificate` cannot be nil")
|
||||
}
|
||||
|
||||
ext, ok := apiv1.FindCertificateAuthorityExtension(req.Certificate)
|
||||
if !ok {
|
||||
return nil, errors.New("error revoking certificate: certificate authority extension was not found")
|
||||
}
|
||||
|
||||
var cae apiv1.CertificateAuthorityExtension
|
||||
if _, err := asn1.Unmarshal(ext.Value, &cae); err != nil {
|
||||
return nil, errors.Wrap(err, "error unmarshaling certificate authority extension")
|
||||
}
|
||||
|
||||
ctx, cancel := defaultContext()
|
||||
defer cancel()
|
||||
|
||||
certpb, err := c.client.RevokeCertificate(ctx, &pb.RevokeCertificateRequest{
|
||||
Name: c.certificateAuthority + "/certificates/" + cae.CertificateID,
|
||||
Reason: reason,
|
||||
RequestId: req.RequestID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cloudCAS RevokeCertificate failed")
|
||||
}
|
||||
|
||||
cert, chain, err := getCertificateAndChain(certpb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apiv1.RevokeCertificateResponse{
|
||||
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)
|
||||
|
||||
// Create new CAS extension with the certificate id.
|
||||
id, err := createCertificateID()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
casExtension, err := apiv1.CreateCertificateAuthorityExtension(apiv1.CloudCAS, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
tpl.ExtraExtensions = append(tpl.ExtraExtensions, casExtension)
|
||||
|
||||
// Create and submit certificate
|
||||
certConfig, err := createCertificateConfig(tpl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := defaultContext()
|
||||
defer cancel()
|
||||
|
||||
cert, err := c.client.CreateCertificate(ctx, &pb.CreateCertificateRequest{
|
||||
Parent: c.certificateAuthority,
|
||||
CertificateId: id,
|
||||
Certificate: &pb.Certificate{
|
||||
CertificateConfig: certConfig,
|
||||
Lifetime: durationpb.New(lifetime),
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
RequestId: requestID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "cloudCAS CreateCertificate failed")
|
||||
}
|
||||
|
||||
// Return certificate and certificate chain
|
||||
return getCertificateAndChain(cert)
|
||||
}
|
||||
|
||||
func defaultContext() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), 15*time.Second)
|
||||
}
|
||||
|
||||
func createCertificateID() (string, error) {
|
||||
id, err := uuid.NewRandomFromReader(rand.Reader)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error creating certificate id")
|
||||
}
|
||||
return id.String(), nil
|
||||
}
|
||||
|
||||
func parseCertificate(pemCert string) (*x509.Certificate, error) {
|
||||
block, _ := pem.Decode([]byte(pemCert))
|
||||
if block == nil {
|
||||
return nil, errors.New("error decoding certificate: not a valid PEM encoded block")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing certificate")
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func getCertificateAndChain(certpb *pb.Certificate) (*x509.Certificate, []*x509.Certificate, error) {
|
||||
cert, err := parseCertificate(certpb.PemCertificate)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pemChain := certpb.PemCertificateChain[:len(certpb.PemCertificateChain)-1]
|
||||
chain := make([]*x509.Certificate, len(pemChain))
|
||||
for i := range pemChain {
|
||||
chain[i], err = parseCertificate(pemChain[i])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return cert, chain, nil
|
||||
|
||||
}
|
@ -0,0 +1,675 @@
|
||||
package cloudcas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
gax "github.com/googleapis/gax-go/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1"
|
||||
)
|
||||
|
||||
var (
|
||||
errTest = errors.New("test error")
|
||||
testAuthorityName = "projects/test-project/locations/us-west1/certificateAuthorities/test-ca"
|
||||
testCertificateName = "projects/test-project/locations/us-west1/certificateAuthorities/test-ca/certificates/test-certificate"
|
||||
testRootCertificate = `-----BEGIN CERTIFICATE-----
|
||||
MIIBhjCCAS2gAwIBAgIQLbKTuXau4+t3KFbGpJJAADAKBggqhkjOPQQDAjAiMSAw
|
||||
HgYDVQQDExdHb29nbGUgQ0FTIFRlc3QgUm9vdCBDQTAeFw0yMDA5MTQyMjQ4NDla
|
||||
Fw0zMDA5MTIyMjQ4NDlaMCIxIDAeBgNVBAMTF0dvb2dsZSBDQVMgVGVzdCBSb290
|
||||
IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYKGgQ3/0D7+oBTc0CXoYfSC6
|
||||
M8hOqLsmzBapPZSYpfwjgEsjdNU84jdrYmW1zF1+p+MrL4c7qJv9NLo/picCuqNF
|
||||
MEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE
|
||||
FFVn9V7Qymd7cUJh9KAhnUDAQL5YMAoGCCqGSM49BAMCA0cAMEQCIA4LzttYoT3u
|
||||
8TYgSrvFT+Z+cklfi4UrPBU6aSbcUaW2AiAPfaqbyccQT3CxMVyHg+xZZjAirZp8
|
||||
lAeA/T4FxAonHA==
|
||||
-----END CERTIFICATE-----`
|
||||
testIntermediateCertificate = `-----BEGIN CERTIFICATE-----
|
||||
MIIBsDCCAVagAwIBAgIQOb91kHxWKVzSJ9ESW1ViVzAKBggqhkjOPQQDAjAiMSAw
|
||||
HgYDVQQDExdHb29nbGUgQ0FTIFRlc3QgUm9vdCBDQTAeFw0yMDA5MTQyMjQ4NDla
|
||||
Fw0zMDA5MTIyMjQ4NDlaMCoxKDAmBgNVBAMTH0dvb2dsZSBDQVMgVGVzdCBJbnRl
|
||||
cm1lZGlhdGUgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASUHN1cNyId4Ei/
|
||||
4MxD5VrZFc51P50caMUdDZVrPveidChBYCU/9IM6vnRlZHx2HLjQ0qAvqHwY3rT0
|
||||
xc7n+PfCo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAd
|
||||
BgNVHQ4EFgQUSDlasiw0pRKyS7llhL0ZuVFNa9UwHwYDVR0jBBgwFoAUVWf1XtDK
|
||||
Z3txQmH0oCGdQMBAvlgwCgYIKoZIzj0EAwIDSAAwRQIgMmsLcoC4KriXw+s+cZx2
|
||||
bJMf6Mx/WESj31buJJhpzY0CIQCBUa/JtvS3nyce/4DF5tK2v49/NWHREgqAaZ57
|
||||
DcYyHQ==
|
||||
-----END CERTIFICATE-----`
|
||||
testLeafCertificate = `-----BEGIN CERTIFICATE-----
|
||||
MIIB1jCCAX2gAwIBAgIQQfOn+COMeuD8VYF1TiDkEzAKBggqhkjOPQQDAjAqMSgw
|
||||
JgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx
|
||||
NDIyNTE1NVoXDTMwMDkxMjIyNTE1MlowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0
|
||||
ZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlN
|
||||
nX2+xfjXa1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czg
|
||||
P6OBkTCBjjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
|
||||
AQUFBwMCMB0GA1UdDgQWBBSYPbu4Tmm7Zze/hCePeZH1Avoj+jAfBgNVHSMEGDAW
|
||||
gBRIOVqyLDSlErJLuWWEvRm5UU1r1TAdBgNVHREEFjAUghJ0ZXN0LnNtYWxsc3Rl
|
||||
cC5jb20wCgYIKoZIzj0EAwIDRwAwRAIgY+nTc+RHn31/BOhht4JpxCmJPHxqFT3S
|
||||
ojnictBudV0CIB87ipY5HV3c8FLVEzTA0wFwdDZvQraQYsthwbg2kQFb
|
||||
-----END CERTIFICATE-----`
|
||||
testSignedCertificate = `-----BEGIN CERTIFICATE-----
|
||||
MIIB/DCCAaKgAwIBAgIQHHFuGMz0cClfde5kqP5prTAKBggqhkjOPQQDAjAqMSgw
|
||||
JgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx
|
||||
NTAwMDQ0M1oXDTMwMDkxMzAwMDQ0MFowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0
|
||||
ZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMqNCiXMvbn74LsHzRv+8
|
||||
17m9vEzH6RHrg3m82e0uEc36+fZWV/zJ9SKuONmnl5VP79LsjL5SVH0RDj73U2XO
|
||||
DKOBtjCBszAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
|
||||
AQUFBwMCMB0GA1UdDgQWBBRTA2cTs7PCNjnps/+T0dS8diqv0DAfBgNVHSMEGDAW
|
||||
gBRIOVqyLDSlErJLuWWEvRm5UU1r1TBCBgwrBgEEAYKkZMYoQAIEMjAwEwhjbG91
|
||||
ZGNhcxMkZDhkMThhNjgtNTI5Ni00YWYzLWFlNGItMmY4NzdkYTNmYmQ5MAoGCCqG
|
||||
SM49BAMCA0gAMEUCIGxl+pqJ50WYWUqK2l4V1FHoXSi0Nht5kwTxFxnWZu1xAiEA
|
||||
zemu3bhWLFaGg3s8i+HTEhw4RqkHP74vF7AVYp88bAw=
|
||||
-----END CERTIFICATE-----`
|
||||
)
|
||||
|
||||
type testClient struct {
|
||||
credentialsFile string
|
||||
certificate *pb.Certificate
|
||||
certificateAuthority *pb.CertificateAuthority
|
||||
err error
|
||||
}
|
||||
|
||||
func newTestClient(credentialsFile string) (CertificateAuthorityClient, error) {
|
||||
if credentialsFile == "testdata/error.json" {
|
||||
return nil, errTest
|
||||
}
|
||||
return &testClient{
|
||||
credentialsFile: credentialsFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func okTestClient() *testClient {
|
||||
return &testClient{
|
||||
credentialsFile: "testdata/credentials.json",
|
||||
certificate: &pb.Certificate{
|
||||
Name: testCertificateName,
|
||||
PemCertificate: testSignedCertificate,
|
||||
PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},
|
||||
},
|
||||
certificateAuthority: &pb.CertificateAuthority{
|
||||
PemCaCertificates: []string{testIntermediateCertificate, testRootCertificate},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func failTestClient() *testClient {
|
||||
return &testClient{
|
||||
credentialsFile: "testdata/credentials.json",
|
||||
err: errTest,
|
||||
}
|
||||
}
|
||||
|
||||
func badTestClient() *testClient {
|
||||
return &testClient{
|
||||
credentialsFile: "testdata/credentials.json",
|
||||
certificate: &pb.Certificate{
|
||||
Name: testCertificateName,
|
||||
PemCertificate: "not a pem cert",
|
||||
PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},
|
||||
},
|
||||
certificateAuthority: &pb.CertificateAuthority{
|
||||
PemCaCertificates: []string{testIntermediateCertificate, "not a pem cert"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func setTeeReader(t *testing.T, w *bytes.Buffer) {
|
||||
t.Helper()
|
||||
reader := rand.Reader
|
||||
t.Cleanup(func() {
|
||||
rand.Reader = reader
|
||||
})
|
||||
rand.Reader = io.TeeReader(reader, w)
|
||||
}
|
||||
|
||||
func (c *testClient) CreateCertificate(ctx context.Context, req *pb.CreateCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) {
|
||||
return c.certificate, c.err
|
||||
}
|
||||
|
||||
func (c *testClient) RevokeCertificate(ctx context.Context, req *pb.RevokeCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) {
|
||||
return c.certificate, c.err
|
||||
}
|
||||
|
||||
func (c *testClient) GetCertificateAuthority(ctx context.Context, req *pb.GetCertificateAuthorityRequest, opts ...gax.CallOption) (*pb.CertificateAuthority, error) {
|
||||
return c.certificateAuthority, c.err
|
||||
}
|
||||
|
||||
func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate {
|
||||
t.Helper()
|
||||
crt, err := parseCertificate(pemCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return crt
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tmp := newCertificateAuthorityClient
|
||||
newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) {
|
||||
return newTestClient(credentialsFile)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newCertificateAuthorityClient = tmp
|
||||
})
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
opts apiv1.Options
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *CloudCAS
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{context.Background(), apiv1.Options{
|
||||
Certificateauthority: testAuthorityName,
|
||||
}}, &CloudCAS{
|
||||
client: &testClient{},
|
||||
certificateAuthority: testAuthorityName,
|
||||
}, false},
|
||||
{"ok with credentials", args{context.Background(), apiv1.Options{
|
||||
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",
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := New(tt.args.ctx, tt.args.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_register(t *testing.T) {
|
||||
tmp := newCertificateAuthorityClient
|
||||
newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) {
|
||||
return newTestClient(credentialsFile)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
newCertificateAuthorityClient = tmp
|
||||
})
|
||||
|
||||
want := &CloudCAS{
|
||||
client: &testClient{credentialsFile: "testdata/credentials.json"},
|
||||
certificateAuthority: testAuthorityName,
|
||||
}
|
||||
|
||||
newFn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.CloudCAS)
|
||||
if !ok {
|
||||
t.Error("apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.CloudCAS) was not found")
|
||||
return
|
||||
}
|
||||
|
||||
got, err := newFn(context.Background(), apiv1.Options{
|
||||
Certificateauthority: testAuthorityName, CredentialsFile: "testdata/credentials.json",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("New() error = %v", err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("New() = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNew_real(t *testing.T) {
|
||||
if v, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS"); ok {
|
||||
os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS")
|
||||
t.Cleanup(func() {
|
||||
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", v)
|
||||
})
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
opts apiv1.Options
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
skipOnCI bool
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"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",
|
||||
}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.skipOnCI && os.Getenv("CI") == "true" {
|
||||
t.SkipNow()
|
||||
}
|
||||
_, err := New(tt.args.ctx, tt.args.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudCAS_GetCertificateAuthority(t *testing.T) {
|
||||
root := mustParseCertificate(t, testRootCertificate)
|
||||
type fields struct {
|
||||
client CertificateAuthorityClient
|
||||
certificateAuthority string
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.GetCertificateAuthorityRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *apiv1.GetCertificateAuthorityResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, &apiv1.GetCertificateAuthorityResponse{
|
||||
RootCertificate: root,
|
||||
}, false},
|
||||
{"ok with name", fields{okTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{
|
||||
Name: testCertificateName,
|
||||
}}, &apiv1.GetCertificateAuthorityResponse{
|
||||
RootCertificate: root,
|
||||
}, false},
|
||||
{"fail GetCertificateAuthority", fields{failTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true},
|
||||
{"fail bad root", fields{badTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true},
|
||||
{"fail no pems", fields{&testClient{certificateAuthority: &pb.CertificateAuthority{}}, testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &CloudCAS{
|
||||
client: tt.fields.client,
|
||||
certificateAuthority: tt.fields.certificateAuthority,
|
||||
}
|
||||
got, err := c.GetCertificateAuthority(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudCAS.GetCertificateAuthority() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CloudCAS.GetCertificateAuthority() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudCAS_CreateCertificate(t *testing.T) {
|
||||
type fields struct {
|
||||
client CertificateAuthorityClient
|
||||
certificateAuthority string
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.CreateCertificateRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *apiv1.CreateCertificateResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{
|
||||
Template: mustParseCertificate(t, testLeafCertificate),
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, &apiv1.CreateCertificateResponse{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)},
|
||||
}, false},
|
||||
{"fail Template", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, nil, true},
|
||||
{"fail Lifetime", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{
|
||||
Template: mustParseCertificate(t, testLeafCertificate),
|
||||
}}, nil, true},
|
||||
{"fail CreateCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{
|
||||
Template: mustParseCertificate(t, testLeafCertificate),
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, nil, true},
|
||||
{"fail Certificate", fields{badTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{
|
||||
Template: mustParseCertificate(t, testLeafCertificate),
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &CloudCAS{
|
||||
client: tt.fields.client,
|
||||
certificateAuthority: tt.fields.certificateAuthority,
|
||||
}
|
||||
got, err := c.CreateCertificate(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CloudCAS.CreateCertificate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudCAS_createCertificate(t *testing.T) {
|
||||
leaf := mustParseCertificate(t, testLeafCertificate)
|
||||
signed := mustParseCertificate(t, testSignedCertificate)
|
||||
chain := []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}
|
||||
|
||||
type fields struct {
|
||||
client CertificateAuthorityClient
|
||||
certificateAuthority string
|
||||
}
|
||||
type args struct {
|
||||
tpl *x509.Certificate
|
||||
lifetime time.Duration
|
||||
requestID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *x509.Certificate
|
||||
want1 []*x509.Certificate
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, signed, chain, false},
|
||||
{"fail CertificateConfig", fields{okTestClient(), testAuthorityName}, args{&x509.Certificate{}, 24 * time.Hour, "request-id"}, nil, nil, true},
|
||||
{"fail CreateCertificate", fields{failTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true},
|
||||
{"fail ParseCertificates", fields{badTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true},
|
||||
{"fail create id", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true},
|
||||
}
|
||||
|
||||
// Pre-calculate rand.Random
|
||||
buf := new(bytes.Buffer)
|
||||
setTeeReader(t, buf)
|
||||
for i := 0; i < len(tests)-1; i++ {
|
||||
_, err := uuid.NewRandomFromReader(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
rand.Reader = buf
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &CloudCAS{
|
||||
client: tt.fields.client,
|
||||
certificateAuthority: tt.fields.certificateAuthority,
|
||||
}
|
||||
got, got1, err := c.createCertificate(tt.args.tpl, tt.args.lifetime, tt.args.requestID)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudCAS.createCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CloudCAS.createCertificate() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if !reflect.DeepEqual(got1, tt.want1) {
|
||||
t.Errorf("CloudCAS.createCertificate() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudCAS_RenewCertificate(t *testing.T) {
|
||||
type fields struct {
|
||||
client CertificateAuthorityClient
|
||||
certificateAuthority string
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.RenewCertificateRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *apiv1.RenewCertificateResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{
|
||||
Template: mustParseCertificate(t, testLeafCertificate),
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, &apiv1.RenewCertificateResponse{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)},
|
||||
}, false},
|
||||
{"fail Template", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, nil, true},
|
||||
{"fail Lifetime", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{
|
||||
Template: mustParseCertificate(t, testLeafCertificate),
|
||||
}}, nil, true},
|
||||
{"fail CreateCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{
|
||||
Template: mustParseCertificate(t, testLeafCertificate),
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, nil, true},
|
||||
{"fail Certificate", fields{badTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{
|
||||
Template: mustParseCertificate(t, testLeafCertificate),
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &CloudCAS{
|
||||
client: tt.fields.client,
|
||||
certificateAuthority: tt.fields.certificateAuthority,
|
||||
}
|
||||
got, err := c.RenewCertificate(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CloudCAS.RenewCertificate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudCAS_RevokeCertificate(t *testing.T) {
|
||||
badExtensionCert := mustParseCertificate(t, testSignedCertificate)
|
||||
for i, ext := range badExtensionCert.Extensions {
|
||||
if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 2}) {
|
||||
badExtensionCert.Extensions[i].Value = []byte("bad-data")
|
||||
}
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
client CertificateAuthorityClient
|
||||
certificateAuthority string
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.RevokeCertificateRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *apiv1.RevokeCertificateResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
ReasonCode: 1,
|
||||
}}, &apiv1.RevokeCertificateResponse{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)},
|
||||
}, false},
|
||||
{"fail Extension", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: mustParseCertificate(t, testLeafCertificate),
|
||||
ReasonCode: 1,
|
||||
}}, nil, true},
|
||||
{"fail Extension Value", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: badExtensionCert,
|
||||
ReasonCode: 1,
|
||||
}}, nil, true},
|
||||
{"fail Certificate", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
ReasonCode: 2,
|
||||
}}, nil, true},
|
||||
{"fail ReasonCode", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
ReasonCode: 100,
|
||||
}}, nil, true},
|
||||
{"fail ReasonCode 7", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
ReasonCode: 7,
|
||||
}}, nil, true},
|
||||
{"fail ReasonCode 8", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
ReasonCode: 8,
|
||||
}}, nil, true},
|
||||
{"fail RevokeCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
ReasonCode: 1,
|
||||
}}, nil, true},
|
||||
{"fail ParseCertificate", fields{badTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: mustParseCertificate(t, testSignedCertificate),
|
||||
ReasonCode: 1,
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &CloudCAS{
|
||||
client: tt.fields.client,
|
||||
certificateAuthority: tt.fields.certificateAuthority,
|
||||
}
|
||||
got, err := c.RevokeCertificate(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CloudCAS.RevokeCertificate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createCertificateID(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
setTeeReader(t, buf)
|
||||
uuid, err := uuid.NewRandomFromReader(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rand.Reader = buf
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", uuid.String(), false},
|
||||
{"fail", "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := createCertificateID()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("createCertificateID() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("createCertificateID() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseCertificate(t *testing.T) {
|
||||
type args struct {
|
||||
pemCert string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *x509.Certificate
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{testLeafCertificate}, mustParseCertificate(t, testLeafCertificate), false},
|
||||
{"ok intermediate", args{testIntermediateCertificate}, mustParseCertificate(t, testIntermediateCertificate), false},
|
||||
{"fail pem", args{"not pem"}, nil, true},
|
||||
{"fail parseCertificate", args{"-----BEGIN CERTIFICATE-----\nZm9vYmFyCg==\n-----END CERTIFICATE-----\n"}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseCertificate(tt.args.pemCert)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseCertificate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getCertificateAndChain(t *testing.T) {
|
||||
type args struct {
|
||||
certpb *pb.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *x509.Certificate
|
||||
want1 []*x509.Certificate
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{&pb.Certificate{
|
||||
Name: testCertificateName,
|
||||
PemCertificate: testSignedCertificate,
|
||||
PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},
|
||||
}}, mustParseCertificate(t, testSignedCertificate), []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, false},
|
||||
{"fail PemCertificate", args{&pb.Certificate{
|
||||
Name: testCertificateName,
|
||||
PemCertificate: "foobar",
|
||||
PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},
|
||||
}}, nil, nil, true},
|
||||
{"fail PemCertificateChain", args{&pb.Certificate{
|
||||
Name: testCertificateName,
|
||||
PemCertificate: testSignedCertificate,
|
||||
PemCertificateChain: []string{"foobar", testRootCertificate},
|
||||
}}, nil, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := getCertificateAndChain(tt.args.certpb)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getCertificateAndChain() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getCertificateAndChain() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if !reflect.DeepEqual(got1, tt.want1) {
|
||||
t.Errorf("getCertificateAndChain() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package softcas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apiv1.Register(apiv1.SoftCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {
|
||||
return New(ctx, opts)
|
||||
})
|
||||
}
|
||||
|
||||
var now = func() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
return &SoftCAS{
|
||||
Issuer: opts.Issuer,
|
||||
Signer: opts.Signer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateCertificate signs a new certificate using Golang or KMS crypto.
|
||||
func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {
|
||||
switch {
|
||||
case req.Template == nil:
|
||||
return nil, errors.New("createCertificateRequest `template` cannot be nil")
|
||||
case req.Lifetime == 0:
|
||||
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
|
||||
}
|
||||
|
||||
t := now()
|
||||
// Provisioners can also set specific values.
|
||||
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)
|
||||
}
|
||||
req.Template.Issuer = c.Issuer.Subject
|
||||
|
||||
cert, err := x509util.CreateCertificate(req.Template, c.Issuer, req.Template.PublicKey, c.Signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apiv1.CreateCertificateResponse{
|
||||
Certificate: cert,
|
||||
CertificateChain: []*x509.Certificate{
|
||||
c.Issuer,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RenewCertificate signs the given certificate template using Golang or KMS crypto.
|
||||
func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
|
||||
switch {
|
||||
case req.Template == nil:
|
||||
return nil, errors.New("createCertificateRequest `template` cannot be nil")
|
||||
case req.Lifetime == 0:
|
||||
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
|
||||
}
|
||||
|
||||
t := now()
|
||||
req.Template.NotBefore = t.Add(-1 * req.Backdate)
|
||||
req.Template.NotAfter = t.Add(req.Lifetime)
|
||||
req.Template.Issuer = c.Issuer.Subject
|
||||
|
||||
cert, err := x509util.CreateCertificate(req.Template, c.Issuer, req.Template.PublicKey, c.Signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apiv1.RenewCertificateResponse{
|
||||
Certificate: cert,
|
||||
CertificateChain: []*x509.Certificate{
|
||||
c.Issuer,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RevokeCertificate revokes the given certificate in step-ca. In SoftCAS this
|
||||
// operation is a no-op as the actual revoke will happen when we store the entry
|
||||
// in the db.
|
||||
func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
|
||||
return &apiv1.RevokeCertificateResponse{
|
||||
Certificate: req.Certificate,
|
||||
CertificateChain: []*x509.Certificate{
|
||||
c.Issuer,
|
||||
},
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,345 @@
|
||||
package softcas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"io"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
)
|
||||
|
||||
var (
|
||||
testIntermediatePem = `-----BEGIN CERTIFICATE-----
|
||||
MIIBPjCB8aADAgECAhAk4aPIlsVvQg3gveApc3mIMAUGAytlcDAeMRwwGgYDVQQD
|
||||
ExNTbWFsbHN0ZXAgVW5pdCBUZXN0MB4XDTIwMDkxNjAyMDgwMloXDTMwMDkxNDAy
|
||||
MDgwMlowHjEcMBoGA1UEAxMTU21hbGxzdGVwIFVuaXQgVGVzdDAqMAUGAytlcAMh
|
||||
ANLs3JCzECR29biut0NDsaLnh0BGij5eJx6VkdJPfS/ko0UwQzAOBgNVHQ8BAf8E
|
||||
BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUup5qpZFMAFdgK7RB
|
||||
xNzmUaQM8YwwBQYDK2VwA0EAAwcW25E/6bchyKwp3RRK1GXiPMDCc+hsTJxuOLWy
|
||||
YM7ga829dU8X4pRcEEAcBndqCED/502excjEK7U9vCkFCg==
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
testIntermediateKeyPem = `-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEII9ZckcrDKlbhZKR0jp820Uz6mOMLFsq2JhI+Tl7WJwH
|
||||
-----END PRIVATE KEY-----`
|
||||
)
|
||||
|
||||
var (
|
||||
testIssuer = mustIssuer()
|
||||
testSigner = mustSigner()
|
||||
testTemplate = &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "test.smallstep.com"},
|
||||
DNSNames: []string{"test.smallstep.com"},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
PublicKey: mustSigner().Public(),
|
||||
SerialNumber: big.NewInt(1234),
|
||||
}
|
||||
testNow = time.Now()
|
||||
testSignedTemplate = mustSign(testTemplate, testNow, testNow.Add(24*time.Hour))
|
||||
)
|
||||
|
||||
func mockNow(t *testing.T) {
|
||||
tmp := now
|
||||
now = func() time.Time {
|
||||
return testNow
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
now = tmp
|
||||
})
|
||||
}
|
||||
|
||||
func mustIssuer() *x509.Certificate {
|
||||
v, err := pemutil.Parse([]byte(testIntermediatePem))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v.(*x509.Certificate)
|
||||
}
|
||||
|
||||
func mustSigner() crypto.Signer {
|
||||
v, err := pemutil.Parse([]byte(testIntermediateKeyPem))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v.(crypto.Signer)
|
||||
}
|
||||
|
||||
func mustSign(template *x509.Certificate, notBefore, notAfter time.Time) *x509.Certificate {
|
||||
tmpl := *template
|
||||
tmpl.NotBefore = notBefore
|
||||
tmpl.NotAfter = notAfter
|
||||
tmpl.Issuer = testIssuer.Subject
|
||||
cert, err := x509util.CreateCertificate(&tmpl, testIssuer, tmpl.PublicKey, testSigner)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cert
|
||||
}
|
||||
|
||||
func setTeeReader(t *testing.T, w *bytes.Buffer) {
|
||||
t.Helper()
|
||||
reader := rand.Reader
|
||||
t.Cleanup(func() {
|
||||
rand.Reader = reader
|
||||
})
|
||||
rand.Reader = io.TeeReader(reader, w)
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
opts apiv1.Options
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *SoftCAS
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{context.Background(), apiv1.Options{Issuer: testIssuer, Signer: testSigner}}, &SoftCAS{Issuer: testIssuer, Signer: testSigner}, false},
|
||||
{"fail no issuer", args{context.Background(), apiv1.Options{Signer: testSigner}}, nil, true},
|
||||
{"fail no signer", args{context.Background(), apiv1.Options{Issuer: testIssuer}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := New(tt.args.ctx, tt.args.opts)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_register(t *testing.T) {
|
||||
newFn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.SoftCAS)
|
||||
if !ok {
|
||||
t.Error("apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.SoftCAS) was not found")
|
||||
return
|
||||
}
|
||||
|
||||
want := &SoftCAS{
|
||||
Issuer: testIssuer,
|
||||
Signer: testSigner,
|
||||
}
|
||||
|
||||
got, err := newFn(context.Background(), apiv1.Options{Issuer: testIssuer, Signer: testSigner})
|
||||
if err != nil {
|
||||
t.Errorf("New() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("New() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoftCAS_CreateCertificate(t *testing.T) {
|
||||
mockNow(t)
|
||||
// Set rand.Reader to EOF
|
||||
buf := new(bytes.Buffer)
|
||||
setTeeReader(t, buf)
|
||||
rand.Reader = buf
|
||||
|
||||
tmplNotBefore := *testTemplate
|
||||
tmplNotBefore.NotBefore = testNow
|
||||
|
||||
tmplNotAfter := *testTemplate
|
||||
tmplNotAfter.NotAfter = testNow.Add(24 * time.Hour)
|
||||
|
||||
tmplWithLifetime := *testTemplate
|
||||
tmplWithLifetime.NotBefore = testNow
|
||||
tmplWithLifetime.NotAfter = testNow.Add(24 * time.Hour)
|
||||
|
||||
tmplNoSerial := *testTemplate
|
||||
tmplNoSerial.SerialNumber = nil
|
||||
|
||||
type fields struct {
|
||||
Issuer *x509.Certificate
|
||||
Signer crypto.Signer
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.CreateCertificateRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *apiv1.CreateCertificateResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{
|
||||
Template: testTemplate, Lifetime: 24 * time.Hour,
|
||||
}}, &apiv1.CreateCertificateResponse{
|
||||
Certificate: testSignedTemplate,
|
||||
CertificateChain: []*x509.Certificate{testIssuer},
|
||||
}, false},
|
||||
{"ok with notBefore", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{
|
||||
Template: &tmplNotBefore, Lifetime: 24 * time.Hour,
|
||||
}}, &apiv1.CreateCertificateResponse{
|
||||
Certificate: testSignedTemplate,
|
||||
CertificateChain: []*x509.Certificate{testIssuer},
|
||||
}, false},
|
||||
{"ok with notBefore+notAfter", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{
|
||||
Template: &tmplWithLifetime, Lifetime: 24 * time.Hour,
|
||||
}}, &apiv1.CreateCertificateResponse{
|
||||
Certificate: testSignedTemplate,
|
||||
CertificateChain: []*x509.Certificate{testIssuer},
|
||||
}, false},
|
||||
{"fail template", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true},
|
||||
{"fail lifetime", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{Template: testTemplate}}, nil, true},
|
||||
{"fail CreateCertificate", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{
|
||||
Template: &tmplNoSerial,
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, nil, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &SoftCAS{
|
||||
Issuer: tt.fields.Issuer,
|
||||
Signer: tt.fields.Signer,
|
||||
}
|
||||
got, err := c.CreateCertificate(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SoftCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SoftCAS.CreateCertificate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoftCAS_RenewCertificate(t *testing.T) {
|
||||
mockNow(t)
|
||||
|
||||
// Set rand.Reader to EOF
|
||||
buf := new(bytes.Buffer)
|
||||
setTeeReader(t, buf)
|
||||
rand.Reader = buf
|
||||
|
||||
tmplNoSerial := *testTemplate
|
||||
tmplNoSerial.SerialNumber = nil
|
||||
|
||||
type fields struct {
|
||||
Issuer *x509.Certificate
|
||||
Signer crypto.Signer
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.RenewCertificateRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *apiv1.RenewCertificateResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{
|
||||
Template: testTemplate, Lifetime: 24 * time.Hour,
|
||||
}}, &apiv1.RenewCertificateResponse{
|
||||
Certificate: testSignedTemplate,
|
||||
CertificateChain: []*x509.Certificate{testIssuer},
|
||||
}, false},
|
||||
{"fail template", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true},
|
||||
{"fail lifetime", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{Template: testTemplate}}, nil, true},
|
||||
{"fail CreateCertificate", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{
|
||||
Template: &tmplNoSerial,
|
||||
Lifetime: 24 * time.Hour,
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &SoftCAS{
|
||||
Issuer: tt.fields.Issuer,
|
||||
Signer: tt.fields.Signer,
|
||||
}
|
||||
got, err := c.RenewCertificate(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SoftCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SoftCAS.RenewCertificate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSoftCAS_RevokeCertificate(t *testing.T) {
|
||||
type fields struct {
|
||||
Issuer *x509.Certificate
|
||||
Signer crypto.Signer
|
||||
}
|
||||
type args struct {
|
||||
req *apiv1.RevokeCertificateRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *apiv1.RevokeCertificateResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{
|
||||
Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}},
|
||||
Reason: "test reason",
|
||||
ReasonCode: 1,
|
||||
}}, &apiv1.RevokeCertificateResponse{
|
||||
Certificate: &x509.Certificate{Subject: pkix.Name{CommonName: "fake"}},
|
||||
CertificateChain: []*x509.Certificate{testIssuer},
|
||||
}, false},
|
||||
{"ok no cert", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{
|
||||
Reason: "test reason",
|
||||
ReasonCode: 1,
|
||||
}}, &apiv1.RevokeCertificateResponse{
|
||||
Certificate: nil,
|
||||
CertificateChain: []*x509.Certificate{testIssuer},
|
||||
}, false},
|
||||
{"ok empty", fields{testIssuer, testSigner}, args{&apiv1.RevokeCertificateRequest{}}, &apiv1.RevokeCertificateResponse{
|
||||
Certificate: nil,
|
||||
CertificateChain: []*x509.Certificate{testIssuer},
|
||||
}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &SoftCAS{
|
||||
Issuer: tt.fields.Issuer,
|
||||
Signer: tt.fields.Signer,
|
||||
}
|
||||
got, err := c.RevokeCertificate(tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SoftCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SoftCAS.RevokeCertificate() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_now(t *testing.T) {
|
||||
t0 := time.Now()
|
||||
t1 := now()
|
||||
if t1.Sub(t0) > time.Second {
|
||||
t.Errorf("now() = %s, want ~%s", t1, t0)
|
||||
}
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
# Certificate Management Services
|
||||
|
||||
This document describes how to use a certificate management service or 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.
|
||||
|
||||
The `CertificateAuthorityService` is defined in the package
|
||||
`github.com/smallstep/certificates/cas/apiv1` and it is:
|
||||
|
||||
```go
|
||||
type CertificateAuthorityService interface {
|
||||
CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error)
|
||||
RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error)
|
||||
RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error)
|
||||
}
|
||||
```
|
||||
|
||||
The same package defines another interface that is used to get the root
|
||||
certificates from the CAS:
|
||||
|
||||
```go
|
||||
type CertificateAuthorityGetter interface {
|
||||
GetCertificateAuthority(req *GetCertificateAuthorityRequest) (*GetCertificateAuthorityResponse, error)
|
||||
}
|
||||
```
|
||||
|
||||
## SoftCAS
|
||||
|
||||
SoftCAS is the default implementation supported by `step-ca`. No special
|
||||
configurations are required to enable it.
|
||||
|
||||
SoftCAS generally uses certificates and keys in the filesystem, but a KMS can
|
||||
also be used instead of a key file for signing certificates. See [KMS](kms.md)
|
||||
for more information.
|
||||
|
||||
## CloudCAS
|
||||
|
||||
CloudCAS is the implementation of the `CertificateAuthorityService` and
|
||||
`CertificateAuthorityGetter` interfaces using [Google's Certificate Authority
|
||||
Service](https://cloud.google.com/certificate-authority-service/).
|
||||
|
||||
Before enabling CloudCAS in `step-ca` you do some steps in Google Cloud Console
|
||||
or using `gcloud` CLI:
|
||||
|
||||
1. Create or define a project to use. Let's say the name is `smallstep-cas-test`.
|
||||
2. Create the KMS keyring and keys for root and intermediate certificates:
|
||||
|
||||
```sh
|
||||
# Create key ring
|
||||
gcloud kms keyrings create kr1 --location us-west1
|
||||
# Create key for Root certificate
|
||||
gcloud kms keys create k1 \
|
||||
--location us-west1 \
|
||||
--keyring kr1 \
|
||||
--purpose asymmetric-signing \
|
||||
--default-algorithm ec-sign-p256-sha256 \
|
||||
--protection-level software
|
||||
# Create key for Intermediate certicate
|
||||
gcloud kms keys create k2 \
|
||||
--location us-west1 \
|
||||
--keyring kr1 \
|
||||
--purpose asymmetric-signing \
|
||||
--default-algorithm ec-sign-p256-sha256 \
|
||||
--protection-level software
|
||||
|
||||
# Put the resource name for version 1 of the new KMS keys into a shell variable.
|
||||
# This will be used in the other instructions below.
|
||||
KMS_ROOT_KEY_VERSION=$(gcloud kms keys versions describe 1 --key k1 --keyring kr1 --location us-west1 --format "value(name)")
|
||||
KMS_INTERMEDIATE_KEY_VERSION=$(gcloud kms keys versions describe 1 --key k2 --keyring kr1 --location us-west1 --format "value(name)")
|
||||
```
|
||||
|
||||
3. Enable the CA service API. You can do it on the console or running:
|
||||
|
||||
```sh
|
||||
gcloud services enable privateca.googleapis.com
|
||||
```
|
||||
|
||||
4. Configure IAM. Create a service account using Google Console or running:
|
||||
|
||||
```sh
|
||||
# Create service account
|
||||
gcloud iam service-accounts create step-ca-sa \
|
||||
--project smallstep-cas-test \
|
||||
--description "Step-CA Service Account" \
|
||||
--display-name "Step-CA Service Account"
|
||||
# Add permissions to use the privateca API
|
||||
gcloud projects add-iam-policy-binding smallstep-cas-test \
|
||||
--member=serviceAccount:step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \
|
||||
--role=roles/privateca.caManager
|
||||
gcloud projects add-iam-policy-binding smallstep-cas-test \
|
||||
--member=serviceAccount:step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com \
|
||||
--role=roles/privateca.certificateRequester
|
||||
# Download the credentials.file
|
||||
gcloud iam service-accounts keys create credentials.json \
|
||||
--iam-account step-ca-sa@smallstep-cas-test.iam.gserviceaccount.com
|
||||
```
|
||||
|
||||
5. Create a Root CA. You can do this on the console or running:
|
||||
|
||||
```sh
|
||||
gcloud beta privateca roots create prod-root-ca \
|
||||
--location us-west1 \
|
||||
--kms-key-version "$KMS_ROOT_KEY_VERSION" \
|
||||
--subject "CN=Example Root CA, O=Example LLC" \
|
||||
--max-chain-length 2
|
||||
```
|
||||
|
||||
6. Create an Intermediate CA. You can do this on the console or running:
|
||||
|
||||
```sh
|
||||
gcloud beta privateca subordinates create prod-intermediate-ca \
|
||||
--location us-west1 \
|
||||
--issuer prod-root-ca \
|
||||
--issuer-location us-west1 \
|
||||
--kms-key-version "$KMS_INTERMEDIATE_KEY_VERSION" \
|
||||
--subject "CN=Example Intermediate CA, O=Example LLC" \
|
||||
--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`.
|
||||
|
||||
```json
|
||||
{
|
||||
"cas": {
|
||||
"type": "cloudCAS",
|
||||
"credentialsFile": "/path/to/credentials.json",
|
||||
"certificateAuthority": "projects/<name>/locations/<loc>/certificateAuthorities/<ca-name>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **type** defines the name of the CAS to use, _cloudCAS_ must be used to enable it.
|
||||
* **credentialsFile** defines the path to a Google Cloud credential file with
|
||||
access to Google's Certificate AuthorityService. We created this file before
|
||||
in step 4. Instead of setting this property, the environment variable
|
||||
`GOOGLE_APPLICATION_CREDENTIALS` can be pointed to the file to use. Or if the
|
||||
`step-ca` is running in Google Cloud, the default service account in the
|
||||
machine can also be used.
|
||||
* **certificateAuthority** defines the Google Cloud resource to the intermediate
|
||||
(or subordinated) certificate to use. We created this resource in step 6.
|
||||
|
||||
As we said before, the CloudCAS implementation in `step-ca` also defines the
|
||||
interface `CertificateAuthorityGetter`, this allows `step-ca` to automatically
|
||||
download the root certificate from Cloud CAS. In the `ca.json` now you don't
|
||||
need to configure `"root"`, and because the intermediate is in Google Cloud,
|
||||
`"crt"` and `"key"` are no needed. A full `ca.json` can look like:
|
||||
|
||||
```json
|
||||
{
|
||||
"address": ":443",
|
||||
"dnsNames": ["ca.example.com"],
|
||||
"logger": {"format": "text"},
|
||||
"db": {
|
||||
"type": "badger",
|
||||
"dataSource": "/home/jane/.step/db",
|
||||
},
|
||||
"cas": {
|
||||
"type": "cloudCAS",
|
||||
"credentialsFile": "/home/jane/.step/credentials.json",
|
||||
"certificateAuthority": "projects/smallstep-cas-test/locations/us-west1/certificateAuthorities/prod-intermediate-ca"
|
||||
},
|
||||
"authority": {
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "JWK",
|
||||
"name": "jane@example.com",
|
||||
"key": {
|
||||
"use": "sig",
|
||||
"kty": "EC",
|
||||
"kid": "ehFT9BkVOY5k_eIiMax0ZxVZCe2hlDVkMwZ2Y78av4s",
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"x": "GtEftN0_ED1lNc2SEUJDXV9EMi7JY-kqINPIEQJIkjM",
|
||||
"y": "8HYFdNe1MbWcbclF-hU1L80SCmMcZQI6vZfTOXfPOjg"
|
||||
},
|
||||
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiSjBSWnY5UFZrM3JKRUJkem5RbExzZyJ9.Fiwvo-RIKU5G6v5udeCT1nlX87ElxrocP2FcgNs3AqEz5OH9H4suew.NmzUJR_9xv8ynQC8.dqOveA_G5kn5lxjxnEZoJCystnJMVYLkZ_8CVzfJQhYchbZfNk_-FKdIuQxeWWBzvmomsILFNtLOIUoqSt30qk83lFyGQWN8Ke2bK5DhuwojF7RI_UqkMyiKP0F28Z4ZFhfQP5D2ZT_stoFaMlU8eak0-T8MOiBIfdAJTWM9x2DN-68mtUBuL5z5eU8bqsxELnjGauD_GHTdnduOosmYsw8vp_PmffTTwqUzDFH1RhkeSmRFRZntAizZMGYkxLamquHI3Jvuqiv4eeJ3yLqh3Ppyo_mVQKnxM7P9TyTxcvLkb2dB3K-cItl1fpsz92cy8euKsKG8n5-hKFRyPfY.j7jBN7nUwatoSsIZuNIwHA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tls": {
|
||||
"cipherSuites": [
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
|
||||
],
|
||||
"minVersion": 1.2,
|
||||
"maxVersion": 1.3,
|
||||
"renegotiation": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The we only need to run `step-ca` as usual, but this time, the CA will print the
|
||||
root fingerprint too:
|
||||
|
||||
```sh
|
||||
$ step-ca /home/jane/.step/config/ca.json
|
||||
2020/09/22 13:17:15 Using root fingerprint '3ef16343cf0952eedbe2b843066bb798fa7a7bceb16aa285e8b0399f661b28b7'
|
||||
2020/09/22 13:17:15 Serving HTTPS on :9000 ...
|
||||
```
|
||||
|
||||
We will need to bootstrap once our environment using the printed fingerprint:
|
||||
|
||||
```sh
|
||||
step ca bootstrap --ca-url https://ca.example.com --fingerprint 3ef16343cf0952eedbe2b843066bb798fa7a7bceb16aa285e8b0399f661b28b7
|
||||
```
|
||||
|
||||
And now we can sign sign a certificate as always:
|
||||
|
||||
```sh
|
||||
step ca certificate test.example.com test.crt test.key
|
||||
```
|
Loading…
Reference in New Issue