diff --git a/authority/authority.go b/authority/authority.go index a0a80b62..3fdb67cc 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -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 { diff --git a/authority/authority_test.go b/authority/authority_test.go index 54de0040..8b003572 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -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) } }) } diff --git a/authority/config.go b/authority/config.go index 1d49f9a1..48d56952 100644 --- a/authority/config.go +++ b/authority/config.go @@ -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 diff --git a/authority/options.go b/authority/options.go index 9457f276..da5a8f88 100644 --- a/authority/options.go +++ b/authority/options.go @@ -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 } } diff --git a/authority/tls.go b/authority/tls.go index a5474d60..f22f4624 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -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 } diff --git a/authority/tls_test.go b/authority/tls_test.go index e96a4bd9..75f9e234 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -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{ diff --git a/cas/apiv1/extension.go b/cas/apiv1/extension.go new file mode 100644 index 00000000..bbe2525a --- /dev/null +++ b/cas/apiv1/extension.go @@ -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 + } + } +} diff --git a/cas/apiv1/extension_test.go b/cas/apiv1/extension_test.go new file mode 100644 index 00000000..7d6fe4dc --- /dev/null +++ b/cas/apiv1/extension_test.go @@ -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) + } + }) + } +} diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go new file mode 100644 index 00000000..fc674ab0 --- /dev/null +++ b/cas/apiv1/options.go @@ -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() +} diff --git a/cas/apiv1/options_test.go b/cas/apiv1/options_test.go new file mode 100644 index 00000000..91da8372 --- /dev/null +++ b/cas/apiv1/options_test.go @@ -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) + } + }) + } +} diff --git a/cas/apiv1/registry.go b/cas/apiv1/registry.go new file mode 100644 index 00000000..b74103b7 --- /dev/null +++ b/cas/apiv1/registry.go @@ -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 +} diff --git a/cas/apiv1/registry_test.go b/cas/apiv1/registry_test.go new file mode 100644 index 00000000..225efb7f --- /dev/null +++ b/cas/apiv1/registry_test.go @@ -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) + } + } + }) + } +} diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go new file mode 100644 index 00000000..2a233b8a --- /dev/null +++ b/cas/apiv1/requests.go @@ -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 +} diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go new file mode 100644 index 00000000..f41650d8 --- /dev/null +++ b/cas/apiv1/services.go @@ -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)) +} diff --git a/cas/apiv1/services_test.go b/cas/apiv1/services_test.go new file mode 100644 index 00000000..f9ab9042 --- /dev/null +++ b/cas/apiv1/services_test.go @@ -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) + } + }) + } +} diff --git a/cas/cas.go b/cas/cas.go new file mode 100644 index 00000000..3df83460 --- /dev/null +++ b/cas/cas.go @@ -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) +} diff --git a/cas/cas_test.go b/cas/cas_test.go new file mode 100644 index 00000000..a01e8dab --- /dev/null +++ b/cas/cas_test.go @@ -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) + } + }) + } +} diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go new file mode 100644 index 00000000..dc9584e3 --- /dev/null +++ b/cas/cloudcas/certificate.go @@ -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 +} diff --git a/cas/cloudcas/certificate_test.go b/cas/cloudcas/certificate_test.go new file mode 100644 index 00000000..4f30ea79 --- /dev/null +++ b/cas/cloudcas/certificate_test.go @@ -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) + } + }) + } +} diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go new file mode 100644 index 00000000..4866a797 --- /dev/null +++ b/cas/cloudcas/cloudcas.go @@ -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 + +} diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go new file mode 100644 index 00000000..f2b708f5 --- /dev/null +++ b/cas/cloudcas/cloudcas_test.go @@ -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) + } + }) + } +} diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go new file mode 100644 index 00000000..844c5c3c --- /dev/null +++ b/cas/softcas/softcas.go @@ -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 +} diff --git a/cas/softcas/softcas_test.go b/cas/softcas/softcas_test.go new file mode 100644 index 00000000..1fd8248a --- /dev/null +++ b/cas/softcas/softcas_test.go @@ -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) + } +} diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index d7726321..cab223a0 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -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 diff --git a/db/db.go b/db/db.go index f6a15d92..2643e577 100644 --- a/db/db.go +++ b/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 { diff --git a/db/simple.go b/db/simple.go index 05626497..0e5426ec 100644 --- a/db/simple.go +++ b/db/simple.go @@ -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 diff --git a/docs/cas.md b/docs/cas.md new file mode 100644 index 00000000..b2c6782e --- /dev/null +++ b/docs/cas.md @@ -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//locations//certificateAuthorities/" + } +} +``` + +* **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 +``` diff --git a/go.mod b/go.mod index 7960b247..156065ab 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 4ca187ef..92e79665 100644 --- a/go.sum +++ b/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=