Merge pull request #367 from smallstep/cas

Support for CAS Interface and CloudCAS
pull/395/head
Mariano Cano 4 years ago committed by GitHub
commit 647b9b4541
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,7 +2,6 @@ package authority
import (
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
@ -10,8 +9,11 @@ import (
"sync"
"time"
"github.com/smallstep/certificates/cas"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/kms"
kmsapi "github.com/smallstep/certificates/kms/apiv1"
@ -33,10 +35,9 @@ type Authority struct {
templates *templates.Templates
// X509 CA
x509CAService cas.CertificateAuthorityService
rootX509Certs []*x509.Certificate
federatedX509Certs []*x509.Certificate
x509Signer crypto.Signer
x509Issuer *x509.Certificate
certificates *sync.Map
// SSH CA
@ -106,9 +107,9 @@ func NewEmbedded(opts ...Option) (*Authority, error) {
return nil, errors.New("cannot create an authority without a configuration")
case len(a.rootX509Certs) == 0 && a.config.Root.HasEmpties():
return nil, errors.New("cannot create an authority without a root certificate")
case a.x509Issuer == nil && a.config.IntermediateCert == "":
case a.x509CAService == nil && a.config.IntermediateCert == "":
return nil, errors.New("cannot create an authority without an issuer certificate")
case a.x509Signer == nil && a.config.IntermediateKey == "":
case a.x509CAService == nil && a.config.IntermediateKey == "":
return nil, errors.New("cannot create an authority without an issuer signer")
}
@ -132,6 +133,14 @@ func (a *Authority) init() error {
var err error
// Initialize step-ca Database if it's not already initialized with WithDB.
// If a.config.DB is nil then a simple, barebones in memory DB will be used.
if a.db == nil {
if a.db, err = db.New(a.config.DB); err != nil {
return err
}
}
// Initialize key manager if it has not been set in the options.
if a.keyManager == nil {
var options kmsapi.Options
@ -144,12 +153,45 @@ func (a *Authority) init() error {
}
}
// Initialize step-ca Database if it's not already initialized with WithDB.
// If a.config.DB is nil then a simple, barebones in memory DB will be used.
if a.db == nil {
if a.db, err = db.New(a.config.DB); err != nil {
// Initialize the X.509 CA Service if it has not been set in the options.
if a.x509CAService == nil {
var options casapi.Options
if a.config.CAS != nil {
options = *a.config.CAS
}
// Read intermediate and create X509 signer for default CAS.
if options.Is(casapi.SoftCAS) {
options.Issuer, err = pemutil.ReadCertificate(a.config.IntermediateCert)
if err != nil {
return err
}
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: []byte(a.config.Password),
})
if err != nil {
return err
}
}
a.x509CAService, err = cas.New(context.Background(), options)
if err != nil {
return err
}
// Get root certificate from CAS.
if srv, ok := a.x509CAService.(casapi.CertificateAuthorityGetter); ok {
resp, err := srv.GetCertificateAuthority(&casapi.GetCertificateAuthorityRequest{
Name: options.Certificateauthority,
})
if err != nil {
return err
}
a.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate)
sum := sha256.Sum256(resp.RootCertificate.Raw)
log.Printf("Using root fingerprint '%s'", hex.EncodeToString(sum[:]))
}
}
// Read root certificates and store them in the certificates map.
@ -184,23 +226,6 @@ func (a *Authority) init() error {
a.certificates.Store(hex.EncodeToString(sum[:]), crt)
}
// Read intermediate and create X509 signer.
if a.x509Signer == nil {
crt, err := pemutil.ReadCertificate(a.config.IntermediateCert)
if err != nil {
return err
}
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: []byte(a.config.Password),
})
if err != nil {
return err
}
a.x509Signer = signer
a.x509Issuer = crt
}
// Decrypt and load SSH keys
var tmplVars templates.Step
if a.config.SSH != nil {

@ -143,8 +143,7 @@ func TestAuthorityNew(t *testing.T) {
assert.Equals(t, auth.rootX509Certs[0], root)
assert.True(t, auth.initOnce)
assert.NotNil(t, auth.x509Signer)
assert.NotNil(t, auth.x509Issuer)
assert.NotNil(t, auth.x509CAService)
for _, p := range tc.config.AuthorityConfig.Provisioners {
var _p provisioner.Interface
_p, ok = auth.provisioners.Load(p.GetID())
@ -256,8 +255,7 @@ func TestNewEmbedded(t *testing.T) {
if err == nil {
assert.True(t, got.initOnce)
assert.NotNil(t, got.rootX509Certs)
assert.NotNil(t, got.x509Signer)
assert.NotNil(t, got.x509Issuer)
assert.NotNil(t, got.x509CAService)
}
})
}

@ -9,6 +9,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
cas "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
kms "github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/templates"
@ -54,6 +55,7 @@ type Config struct {
Address string `json:"address"`
DNSNames []string `json:"dnsNames"`
KMS *kms.Options `json:"kms,omitempty"`
CAS *cas.Options `json:"cas,omitempty"`
SSH *SSHConfig `json:"ssh,omitempty"`
Logger json.RawMessage `json:"logger,omitempty"`
DB *db.Config `json:"db,omitempty"`
@ -179,19 +181,22 @@ func (c *Config) Validate() error {
case c.Address == "":
return errors.New("address cannot be empty")
case c.Root.HasEmpties():
return errors.New("root cannot be empty")
case c.IntermediateCert == "":
return errors.New("crt cannot be empty")
case c.IntermediateKey == "":
return errors.New("key cannot be empty")
case len(c.DNSNames) == 0:
return errors.New("dnsNames cannot be empty")
}
// The default CAS requires root, crt and key.
if c.CAS.Is(cas.SoftCAS) {
switch {
case c.Root.HasEmpties():
return errors.New("root cannot be empty")
case c.IntermediateCert == "":
return errors.New("crt cannot be empty")
case c.IntermediateKey == "":
return errors.New("key cannot be empty")
}
}
// Validate address (a port is required)
if _, _, err := net.SplitHostPort(c.Address); err != nil {
return errors.Errorf("invalid address %s", c.Address)
@ -220,6 +225,11 @@ func (c *Config) Validate() error {
return err
}
// Validate CAS options, nil is ok.
if err := c.CAS.Validate(); err != nil {
return err
}
// Validate ssh: nil is ok
if err := c.SSH.Validate(); err != nil {
return err

@ -8,6 +8,8 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/cas"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/kms"
"golang.org/x/crypto/ssh"
@ -92,8 +94,15 @@ func WithKeyManager(k kms.KeyManager) Option {
// WithX509Signer defines the signer used to sign X509 certificates.
func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {
return func(a *Authority) error {
a.x509Issuer = crt
a.x509Signer = s
srv, err := cas.New(context.Background(), casapi.Options{
Type: casapi.SoftCAS,
Issuer: crt,
Signer: s,
})
if err != nil {
return err
}
a.x509CAService = srv
return nil
}
}

@ -13,6 +13,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"go.step.sm/crypto/jose"
@ -144,20 +145,24 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
}
}
serverCert, err := x509util.CreateCertificate(leaf, a.x509Issuer, csr.PublicKey, a.x509Signer)
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
Template: leaf,
Lifetime: lifetime,
Backdate: signOpts.Backdate,
})
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error creating certificate", opts...)
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...)
}
if err = a.db.StoreCertificate(serverCert); err != nil {
if err = a.db.StoreCertificate(resp.Certificate); err != nil {
if err != db.ErrNotImplemented {
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error storing certificate in db", opts...)
}
}
return []*x509.Certificate{serverCert, a.x509Issuer}, nil
return append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...), nil
}
// Renew creates a new Certificate identical to the old certificate, except
@ -187,13 +192,12 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5
// Durations
backdate := a.config.AuthorityConfig.Backdate.Duration
duration := oldCert.NotAfter.Sub(oldCert.NotBefore)
now := time.Now().UTC()
lifetime := duration - backdate
// Create new certificate from previous values.
// Issuer, NotBefore, NotAfter and SubjectKeyId will be set by the CAS.
newCert := &x509.Certificate{
Issuer: a.x509Issuer.Subject,
Subject: oldCert.Subject,
NotBefore: now.Add(-1 * backdate),
NotAfter: now.Add(duration - backdate),
KeyUsage: oldCert.KeyUsage,
UnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions,
ExtKeyUsage: oldCert.ExtKeyUsage,
@ -228,10 +232,14 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5
}
// Copy all extensions except:
// 1. Authority Key Identifier - This one might be different if we rotate the intermediate certificate
// and it will cause a TLS bad certificate error.
// 2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier extension will be calculated
// for the new public key by NewLeafProfilewithTemplate()
//
// 1. Authority Key Identifier - This one might be different if we rotate
// the intermediate certificate and it will cause a TLS bad certificate
// error.
//
// 2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier
// extension will be calculated for the new public key by
// x509util.CreateCertificate()
for _, ext := range oldCert.Extensions {
if ext.Id.Equal(oidAuthorityKeyIdentifier) {
continue
@ -243,18 +251,22 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5
newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext)
}
serverCert, err := x509util.CreateCertificate(newCert, a.x509Issuer, newCert.PublicKey, a.x509Signer)
resp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{
Template: newCert,
Lifetime: lifetime,
Backdate: backdate,
})
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey", opts...)
}
if err = a.db.StoreCertificate(serverCert); err != nil {
if err = a.db.StoreCertificate(resp.Certificate); err != nil {
if err != db.ErrNotImplemented {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey; error storing certificate in db", opts...)
}
}
return []*x509.Certificate{serverCert, a.x509Issuer}, nil
return append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...), nil
}
// RevokeOptions are the options for the Revoke API.
@ -340,7 +352,30 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
err = a.db.RevokeSSH(rci)
} else { // default to revoke x509
} else {
// Revoke an X.509 certificate using CAS. If the certificate is not
// provided we will try to read it from the db. If the read fails we
// won't throw an error as it will be responsibility of the CAS
// implementation to require a certificate.
var revokedCert *x509.Certificate
if revokeOpts.Crt != nil {
revokedCert = revokeOpts.Crt
} else if rci.Serial != "" {
revokedCert, _ = a.db.GetCertificate(rci.Serial)
}
// CAS operation, note that SoftCAS (default) is a noop.
// The revoke happens when this is stored in the db.
_, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{
Certificate: revokedCert,
Reason: rci.Reason,
ReasonCode: rci.ReasonCode,
})
if err != nil {
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
}
// Save as revoked in the Db.
err = a.db.Revoke(rci)
}
switch err {
@ -390,30 +425,36 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
certTpl.NotBefore = now.Add(-1 * time.Minute)
certTpl.NotAfter = now.Add(24 * time.Hour)
cert, err := x509util.CreateCertificate(certTpl, a.x509Issuer, cr.PublicKey, a.x509Signer)
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
Template: certTpl,
Lifetime: 24 * time.Hour,
Backdate: 1 * time.Minute,
})
if err != nil {
return fatal(err)
}
// Generate PEM blocks to create tls.Certificate
crtPEM := pem.EncodeToMemory(&pem.Block{
pemBlocks := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
Bytes: resp.Certificate.Raw,
})
intermediatePEM, err := pemutil.Serialize(a.x509Issuer)
if err != nil {
return fatal(err)
for _, crt := range resp.CertificateChain {
pemBlocks = append(pemBlocks, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: crt.Raw,
})...)
}
keyPEM, err := pemutil.Serialize(priv)
if err != nil {
return fatal(err)
}
tlsCrt, err := tls.X509KeyPair(append(crtPEM, pem.EncodeToMemory(intermediatePEM)...), pem.EncodeToMemory(keyPEM))
tlsCrt, err := tls.X509KeyPair(pemBlocks, pem.EncodeToMemory(keyPEM))
if err != nil {
return fatal(err)
}
// Set leaf certificate
tlsCrt.Leaf = cert
tlsCrt.Leaf = resp.Certificate
return &tlsCrt, nil
}

@ -17,6 +17,8 @@ import (
"testing"
"time"
"github.com/smallstep/certificates/cas/softcas"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/provisioner"
@ -53,6 +55,14 @@ func (m *certificateDurationEnforcer) Enforce(cert *x509.Certificate) error {
return nil
}
func getDefaultIssuer(a *Authority) *x509.Certificate {
return a.x509CAService.(*softcas.SoftCAS).Issuer
}
func getDefaultSigner(a *Authority) crypto.Signer {
return a.x509CAService.(*softcas.SoftCAS).Signer
}
func generateCertificate(t *testing.T, commonName string, sans []string, opts ...interface{}) *x509.Certificate {
t.Helper()
@ -277,7 +287,7 @@ func TestAuthority_Sign(t *testing.T) {
},
"fail create cert": func(t *testing.T) *signTest {
_a := testAuthority(t)
_a.x509Signer = nil
_a.x509CAService.(*softcas.SoftCAS).Signer = nil
csr := getCSR(t, priv)
return &signTest{
auth: _a,
@ -539,17 +549,15 @@ ZYtQ9Ot36qc=
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"})
}
assert.Equals(t, leaf.Issuer, intermediate.Subject)
assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
assert.Equals(t, leaf.ExtKeyUsage,
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
assert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
issuer := getDefaultIssuer(a)
subjectKeyID, err := generateSubjectKeyID(pub)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId)
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
// Verify Provisioner OID
found := 0
@ -585,8 +593,7 @@ ZYtQ9Ot36qc=
}
}
assert.Equals(t, found, 1)
realIntermediate, err := x509.ParseCertificate(a.x509Issuer.Raw)
realIntermediate, err := x509.ParseCertificate(issuer.Raw)
assert.FatalError(t, err)
assert.Equals(t, intermediate, realIntermediate)
}
@ -614,17 +621,20 @@ func TestAuthority_Renew(t *testing.T) {
NotAfter: provisioner.NewTimeDuration(na1),
}
issuer := getDefaultIssuer(a)
signer := getDefaultSigner(a)
cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
withDefaultASN1DN(a.config.AuthorityConfig.Template),
withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),
withSigner(a.x509Issuer, a.x509Signer))
withSigner(issuer, signer))
certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
withDefaultASN1DN(a.config.AuthorityConfig.Template),
withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
withSigner(a.x509Issuer, a.x509Signer))
withSigner(issuer, signer))
type renewTest struct {
auth *Authority
@ -635,7 +645,7 @@ func TestAuthority_Renew(t *testing.T) {
tests := map[string]func() (*renewTest, error){
"fail/create-cert": func() (*renewTest, error) {
_a := testAuthority(t)
_a.x509Signer = nil
_a.x509CAService.(*softcas.SoftCAS).Signer = nil
return &renewTest{
auth: _a,
cert: cert,
@ -661,8 +671,8 @@ func TestAuthority_Renew(t *testing.T) {
intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner)
_a := testAuthority(t)
_a.x509Signer = intSigner
_a.x509Issuer = intCert
_a.x509CAService.(*softcas.SoftCAS).Issuer = intCert
_a.x509CAService.(*softcas.SoftCAS).Signer = intSigner
return &renewTest{
auth: _a,
cert: cert,
@ -729,8 +739,9 @@ func TestAuthority_Renew(t *testing.T) {
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
// We did not change the intermediate before renewing.
if a.x509Issuer.SerialNumber == tc.auth.x509Issuer.SerialNumber {
assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId)
authIssuer := getDefaultIssuer(tc.auth)
if issuer.SerialNumber == authIssuer.SerialNumber {
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.cert.Extensions {
//skip SubjectKeyIdentifier
@ -750,7 +761,7 @@ func TestAuthority_Renew(t *testing.T) {
}
} else {
// We did change the intermediate before renewing.
assert.Equals(t, leaf.AuthorityKeyId, tc.auth.x509Issuer.SubjectKeyId)
assert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.cert.Extensions {
//skip SubjectKeyIdentifier
@ -778,7 +789,7 @@ func TestAuthority_Renew(t *testing.T) {
}
}
realIntermediate, err := x509.ParseCertificate(tc.auth.x509Issuer.Raw)
realIntermediate, err := x509.ParseCertificate(authIssuer.Raw)
assert.FatalError(t, err)
assert.Equals(t, intermediate, realIntermediate)
}
@ -809,17 +820,20 @@ func TestAuthority_Rekey(t *testing.T) {
NotAfter: provisioner.NewTimeDuration(na1),
}
issuer := getDefaultIssuer(a)
signer := getDefaultSigner(a)
cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
withDefaultASN1DN(a.config.AuthorityConfig.Template),
withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),
withSigner(a.x509Issuer, a.x509Signer))
withSigner(issuer, signer))
certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
withDefaultASN1DN(a.config.AuthorityConfig.Template),
withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
withSigner(a.x509Issuer, a.x509Signer))
withSigner(issuer, signer))
type renewTest struct {
auth *Authority
@ -831,7 +845,7 @@ func TestAuthority_Rekey(t *testing.T) {
tests := map[string]func() (*renewTest, error){
"fail/create-cert": func() (*renewTest, error) {
_a := testAuthority(t)
_a.x509Signer = nil
_a.x509CAService.(*softcas.SoftCAS).Signer = nil
return &renewTest{
auth: _a,
cert: cert,
@ -864,8 +878,8 @@ func TestAuthority_Rekey(t *testing.T) {
intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner)
_a := testAuthority(t)
_a.x509Signer = intSigner
_a.x509Issuer = intCert
_a.x509CAService.(*softcas.SoftCAS).Issuer = intCert
_a.x509CAService.(*softcas.SoftCAS).Signer = intSigner
return &renewTest{
auth: _a,
cert: cert,
@ -942,8 +956,9 @@ func TestAuthority_Rekey(t *testing.T) {
}
// We did not change the intermediate before renewing.
if a.x509Issuer.SerialNumber == tc.auth.x509Issuer.SerialNumber {
assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId)
authIssuer := getDefaultIssuer(tc.auth)
if issuer.SerialNumber == authIssuer.SerialNumber {
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.cert.Extensions {
//skip SubjectKeyIdentifier
@ -963,7 +978,7 @@ func TestAuthority_Rekey(t *testing.T) {
}
} else {
// We did change the intermediate before renewing.
assert.Equals(t, leaf.AuthorityKeyId, tc.auth.x509Issuer.SubjectKeyId)
assert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.cert.Extensions {
//skip SubjectKeyIdentifier
@ -991,7 +1006,7 @@ func TestAuthority_Rekey(t *testing.T) {
}
}
realIntermediate, err := x509.ParseCertificate(tc.auth.x509Issuer.Raw)
realIntermediate, err := x509.ParseCertificate(authIssuer.Raw)
assert.FatalError(t, err)
assert.Equals(t, intermediate, realIntermediate)
}
@ -1107,6 +1122,9 @@ func TestAuthority_Revoke(t *testing.T) {
MUseToken: func(id, tok string) (bool, error) {
return true, nil
},
MGetCertificate: func(sn string) (*x509.Certificate, error) {
return nil, nil
},
Err: errors.New("force"),
}))
@ -1143,6 +1161,9 @@ func TestAuthority_Revoke(t *testing.T) {
MUseToken: func(id, tok string) (bool, error) {
return true, nil
},
MGetCertificate: func(sn string) (*x509.Certificate, error) {
return nil, nil
},
Err: db.ErrAlreadyExists,
}))
@ -1179,6 +1200,9 @@ func TestAuthority_Revoke(t *testing.T) {
MUseToken: func(id, tok string) (bool, error) {
return true, nil
},
MGetCertificate: func(sn string) (*x509.Certificate, error) {
return nil, errors.New("not found")
},
}))
cl := jwt.Claims{

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

@ -31,6 +31,10 @@ import (
// Experimental kms interfaces.
_ "github.com/smallstep/certificates/kms/yubikey"
// Enabled cas interfaces.
_ "github.com/smallstep/certificates/cas/cloudcas"
_ "github.com/smallstep/certificates/cas/softcas"
)
// commit and buildTime are filled in during build by the Makefile

@ -47,6 +47,7 @@ type AuthDB interface {
IsSSHRevoked(sn string) (bool, error)
Revoke(rci *RevokedCertificateInfo) error
RevokeSSH(rci *RevokedCertificateInfo) error
GetCertificate(serialNumber string) (*x509.Certificate, error)
StoreCertificate(crt *x509.Certificate) error
UseToken(id, tok string) (bool, error)
IsSSHHost(name string) (bool, error)
@ -187,6 +188,19 @@ func (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error {
}
}
// GetCertificate retrieves a certificate by the serial number.
func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) {
asn1Data, err := db.Get(certsTable, []byte(serialNumber))
if err != nil {
return nil, errors.Wrap(err, "database Get error")
}
cert, err := x509.ParseCertificate(asn1Data)
if err != nil {
return nil, errors.Wrapf(err, "error parsing certificate with serial number %s", serialNumber)
}
return cert, nil
}
// StoreCertificate stores a certificate PEM.
func (db *DB) StoreCertificate(crt *x509.Certificate) error {
if err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil {
@ -288,6 +302,7 @@ type MockAuthDB struct {
MIsSSHRevoked func(string) (bool, error)
MRevoke func(rci *RevokedCertificateInfo) error
MRevokeSSH func(rci *RevokedCertificateInfo) error
MGetCertificate func(serialNumber string) (*x509.Certificate, error)
MStoreCertificate func(crt *x509.Certificate) error
MUseToken func(id, tok string) (bool, error)
MIsSSHHost func(principal string) (bool, error)
@ -339,6 +354,14 @@ func (m *MockAuthDB) RevokeSSH(rci *RevokedCertificateInfo) error {
return m.Err
}
// GetCertificate mock.
func (m *MockAuthDB) GetCertificate(serialNumber string) (*x509.Certificate, error) {
if m.MGetCertificate != nil {
return m.MGetCertificate(serialNumber)
}
return m.Ret1.(*x509.Certificate), m.Err
}
// StoreCertificate mock.
func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error {
if m.MStoreCertificate != nil {

@ -46,6 +46,11 @@ func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error {
return ErrNotImplemented
}
// GetCertificate returns a "NotImplemented" error.
func (s *SimpleDB) GetCertificate(serialNumber string) (*x509.Certificate, error) {
return nil, ErrNotImplemented
}
// StoreCertificate returns a "NotImplemented" error.
func (s *SimpleDB) StoreCertificate(crt *x509.Certificate) error {
return ErrNotImplemented

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

@ -3,11 +3,12 @@ module github.com/smallstep/certificates
go 1.14
require (
cloud.google.com/go v0.51.0
cloud.google.com/go v0.65.1-0.20200904011802-3c2db50b5678
github.com/Masterminds/sprig/v3 v3.1.0
github.com/aws/aws-sdk-go v1.30.29
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-piv/piv-go v1.6.0
github.com/google/uuid v1.1.2
github.com/googleapis/gax-go/v2 v2.0.5
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
@ -21,13 +22,19 @@ require (
github.com/urfave/cli v1.22.2
go.step.sm/crypto v0.6.1
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
google.golang.org/api v0.15.0
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb
google.golang.org/grpc v1.26.0
golang.org/x/net v0.0.0-20200822124328-c89045814202
google.golang.org/api v0.31.0
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d
google.golang.org/grpc v1.32.0
google.golang.org/protobuf v1.25.0
gopkg.in/square/go-jose.v2 v2.5.1
// cloud.google.com/go/security/privateca/apiv1alpha1 v0.0.0
// google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1 v0.0.0
)
// replace github.com/smallstep/cli => ../cli
// replace github.com/smallstep/nosql => ../nosql
// replace go.step.sm/crypto => ../crypto
// replace cloud.google.com/go/security/privateca/apiv1alpha1 => ./pkg/cloud.google.com/go/security/privateca/apiv1alpha1
// replace google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1 => ./pkg/google.golang.org/genproto/googleapis/cloud/security/privateca/v1alpha1

216
go.sum

@ -5,12 +5,36 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM=
cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.65.1-0.20200904011802-3c2db50b5678 h1:5YqZUrIf2QELwPqw1kLpGIE0z0I++b7HhzSNKjZlIY0=
cloud.google.com/go v0.65.1-0.20200904011802-3c2db50b5678/go.mod h1:Ihp2NV3Qr9BWHCDNA8LXF9fZ1HGBl6Jx1xd7KP3nxkI=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw=
contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@ -68,6 +92,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@ -105,7 +130,9 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -119,7 +146,9 @@ github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
github.com/go-critic/go-critic v0.4.0 h1:sXD3pix0wDemuPuSlrXpJNNYXlUiKiysLrtPVQmxkzI=
github.com/go-critic/go-critic v0.4.0/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0=
@ -170,17 +199,34 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
@ -229,20 +275,33 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/monologue v0.0.0-20190606152607-4b11a32b5934/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk=
github.com/google/monologue v0.0.0-20191220140058-35abc9683a6c h1:0L/piDwninh6sjZ+vCZI7c6RA0R71ET8v1yinZzC9k8=
github.com/google/monologue v0.0.0-20191220140058-35abc9683a6c/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/trillian v1.2.2-0.20190612132142-05461f4df60a/go.mod h1:YPmUVn5NGwgnDUgqlVyFGMTgaWlnSvH7W5p+NdOG8UA=
github.com/google/trillian-examples v0.0.0-20190603134952-4e75ba15216c/go.mod h1:WgL3XZ3pA8/9cm7yxqWrZE6iZkESB2ItGxy5Fo6k2lk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@ -287,6 +346,7 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
@ -528,6 +588,10 @@ github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
@ -542,6 +606,9 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.step.sm/crypto v0.0.0-20200805202904-ec18b6df3cf0 h1:FymMl8TrXGxFf80BWpO0CnkSfLnw0BkDdRrhbMGf5zE=
go.step.sm/crypto v0.0.0-20200805202904-ec18b6df3cf0/go.mod h1:8VYxmvSKt5yOTBx3MGsD2Gk4F1Es/3FIxrjnfeYWE8U=
go.step.sm/crypto v0.1.1 h1:xg3kUS30hEnwgbxtKwq9a4MJaeiU616HSug60LU9B2E=
@ -568,6 +635,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
@ -576,7 +644,12 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -587,11 +660,18 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -610,23 +690,42 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -645,18 +744,38 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e h1:LwyF2AFISC9nVbS6MgzsaQNSUsRXI49GS+YQ5KX/QH0=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200828194041-157a740278f4 h1:kCCpuwSAoYJPkNc6x0xT9yTtV4oKtARo4RGBQWOfg9E=
golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -694,27 +813,73 @@ golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113232020-e2727e816f5a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200106190116-7be0a674c9fc/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200903185744-af4cc2cd812e h1:RvNtqusJ+6DJ07/by/M84a6/Dd17XU6n8QvhvknjJno=
golang.org/x/tools v0.0.0-20200903185744-af4cc2cd812e/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.31.0 h1:1w5Sz/puhxFo9lTtip2n47k7toB/U2nCqOKNHd3Yrbo=
google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -725,16 +890,63 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb h1:ADPHZzpzM4tk4V4S5cnCrr5SwzvlrPRmqqCuJDB8UTs=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -766,6 +978,8 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
@ -773,6 +987,8 @@ mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskX
mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw=
mvdan.cc/unparam v0.0.0-20191111180625-960b1ec0f2c2/go.mod h1:rCqoQrfAmpTX/h2APczwM7UymU/uvaOluiVPIYCSY/k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=

Loading…
Cancel
Save