Merge pull request #367 from smallstep/cas
Support for CAS Interface and CloudCAS
This commit is contained in:
commit
647b9b4541
29 changed files with 3704 additions and 97 deletions
|
@ -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{
|
||||
|
|
62
cas/apiv1/extension.go
Normal file
62
cas/apiv1/extension.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
147
cas/apiv1/extension_test.go
Normal file
147
cas/apiv1/extension_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
50
cas/apiv1/options.go
Normal file
50
cas/apiv1/options.go
Normal file
|
@ -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()
|
||||
}
|
131
cas/apiv1/options_test.go
Normal file
131
cas/apiv1/options_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
29
cas/apiv1/registry.go
Normal file
29
cas/apiv1/registry.go
Normal file
|
@ -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
|
||||
}
|
90
cas/apiv1/registry_test.go
Normal file
90
cas/apiv1/registry_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
60
cas/apiv1/requests.go
Normal file
60
cas/apiv1/requests.go
Normal file
|
@ -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
|
||||
}
|
41
cas/apiv1/services.go
Normal file
41
cas/apiv1/services.go
Normal file
|
@ -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))
|
||||
}
|
23
cas/apiv1/services_test.go
Normal file
23
cas/apiv1/services_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
32
cas/cas.go
Normal file
32
cas/cas.go
Normal file
|
@ -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)
|
||||
}
|
60
cas/cas_test.go
Normal file
60
cas/cas_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
328
cas/cloudcas/certificate.go
Normal file
328
cas/cloudcas/certificate.go
Normal file
|
@ -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
|
||||
}
|
550
cas/cloudcas/certificate_test.go
Normal file
550
cas/cloudcas/certificate_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
289
cas/cloudcas/cloudcas.go
Normal file
289
cas/cloudcas/cloudcas.go
Normal file
|
@ -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
|
||||
|
||||
}
|
675
cas/cloudcas/cloudcas_test.go
Normal file
675
cas/cloudcas/cloudcas_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
115
cas/softcas/softcas.go
Normal file
115
cas/softcas/softcas.go
Normal file
|
@ -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
|
||||
}
|
345
cas/softcas/softcas_test.go
Normal file
345
cas/softcas/softcas_test.go
Normal file
|
@ -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
|
||||
|
|
23
db/db.go
23
db/db.go
|
@ -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
|
||||
|
|
218
docs/cas.md
Normal file
218
docs/cas.md
Normal file
|
@ -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
|
||||
```
|
17
go.mod
17
go.mod
|
@ -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
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…
Reference in a new issue