forked from TrueCloudLab/certificates
commit
9e57e4db2c
26 changed files with 2252 additions and 505 deletions
|
@ -7,39 +7,40 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/cas"
|
||||
"github.com/smallstep/certificates/scep"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||
"github.com/smallstep/certificates/authority/administrator"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"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"
|
||||
kmsapi "github.com/smallstep/certificates/kms/apiv1"
|
||||
"github.com/smallstep/certificates/kms/sshagentkms"
|
||||
"github.com/smallstep/certificates/scep"
|
||||
"github.com/smallstep/certificates/templates"
|
||||
"github.com/smallstep/nosql"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/linkedca"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Authority implements the Certificate Authority internal interface.
|
||||
type Authority struct {
|
||||
config *config.Config
|
||||
keyManager kms.KeyManager
|
||||
provisioners *provisioner.Collection
|
||||
admins *administrator.Collection
|
||||
db db.AuthDB
|
||||
adminDB admin.DB
|
||||
templates *templates.Templates
|
||||
config *config.Config
|
||||
keyManager kms.KeyManager
|
||||
provisioners *provisioner.Collection
|
||||
admins *administrator.Collection
|
||||
db db.AuthDB
|
||||
adminDB admin.DB
|
||||
templates *templates.Templates
|
||||
linkedCAToken string
|
||||
|
||||
// X509 CA
|
||||
x509CAService cas.CertificateAuthorityService
|
||||
|
@ -205,6 +206,11 @@ func (a *Authority) init() error {
|
|||
|
||||
var err error
|
||||
|
||||
// Automatically enable admin for all linked cas.
|
||||
if a.linkedCAToken != "" {
|
||||
a.config.AuthorityConfig.EnableAdmin = true
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
@ -442,10 +448,24 @@ func (a *Authority) init() error {
|
|||
// Initialize step-ca Admin Database if it's not already initialized using
|
||||
// WithAdminDB.
|
||||
if a.adminDB == nil {
|
||||
// Check if AuthConfig already exists
|
||||
a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
if a.linkedCAToken == "" {
|
||||
// Check if AuthConfig already exists
|
||||
a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Use the linkedca client as the admindb.
|
||||
client, err := newLinkedCAClient(a.linkedCAToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If authorityId is configured make sure it matches the one in the token
|
||||
if id := a.config.AuthorityConfig.AuthorityID; id != "" && !strings.EqualFold(id, client.authorityID) {
|
||||
return errors.New("error initializing linkedca: token authority and configured authority do not match")
|
||||
}
|
||||
client.Run()
|
||||
a.adminDB = client
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -453,7 +473,7 @@ func (a *Authority) init() error {
|
|||
if err != nil {
|
||||
return admin.WrapErrorISE(err, "error loading provisioners to initialize authority")
|
||||
}
|
||||
if len(provs) == 0 {
|
||||
if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") {
|
||||
// Create First Provisioner
|
||||
prov, err := CreateFirstProvisioner(context.Background(), a.adminDB, a.config.Password)
|
||||
if err != nil {
|
||||
|
@ -527,6 +547,9 @@ func (a *Authority) CloseForReload() {
|
|||
if err := a.keyManager.Close(); err != nil {
|
||||
log.Printf("error closing the key manager: %v", err)
|
||||
}
|
||||
if client, ok := a.adminDB.(*linkedCaClient); ok {
|
||||
client.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// requiresDecrypter returns whether the Authority
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -273,10 +274,19 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
|
|||
//
|
||||
// TODO(mariano): should we authorize by default?
|
||||
func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
|
||||
var err error
|
||||
var isRevoked bool
|
||||
var opts = []interface{}{errs.WithKeyVal("serialNumber", cert.SerialNumber.String())}
|
||||
|
||||
// Check the passive revocation table.
|
||||
isRevoked, err := a.db.IsRevoked(cert.SerialNumber.String())
|
||||
serial := cert.SerialNumber.String()
|
||||
if lca, ok := a.adminDB.(interface {
|
||||
IsRevoked(string) (bool, error)
|
||||
}); ok {
|
||||
isRevoked, err = lca.IsRevoked(serial)
|
||||
} else {
|
||||
isRevoked, err = a.db.IsRevoked(serial)
|
||||
}
|
||||
if err != nil {
|
||||
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
|
||||
}
|
||||
|
@ -294,6 +304,28 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// authorizeSSHCertificate returns an error if the given certificate is revoked.
|
||||
func (a *Authority) authorizeSSHCertificate(ctx context.Context, cert *ssh.Certificate) error {
|
||||
var err error
|
||||
var isRevoked bool
|
||||
|
||||
serial := strconv.FormatUint(cert.Serial, 10)
|
||||
if lca, ok := a.adminDB.(interface {
|
||||
IsSSHRevoked(string) (bool, error)
|
||||
}); ok {
|
||||
isRevoked, err = lca.IsSSHRevoked(serial)
|
||||
} else {
|
||||
isRevoked, err = a.db.IsSSHRevoked(serial)
|
||||
}
|
||||
if err != nil {
|
||||
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHCertificate", errs.WithKeyVal("serialNumber", serial))
|
||||
}
|
||||
if isRevoked {
|
||||
return errs.Unauthorized("authority.authorizeSSHCertificate: certificate has been revoked", errs.WithKeyVal("serialNumber", serial))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// authorizeSSHSign loads the provisioner from the token, checks that it has not
|
||||
// been used again and calls the provisioner AuthorizeSSHSign method. Returns a
|
||||
// list of methods to apply to the signing flow.
|
||||
|
|
|
@ -75,6 +75,7 @@ type ASN1DN struct {
|
|||
Locality string `json:"locality,omitempty"`
|
||||
Province string `json:"province,omitempty"`
|
||||
StreetAddress string `json:"streetAddress,omitempty"`
|
||||
SerialNumber string `json:"serialNumber,omitempty"`
|
||||
CommonName string `json:"commonName,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -83,8 +84,9 @@ type ASN1DN struct {
|
|||
// cas.Options.
|
||||
type AuthConfig struct {
|
||||
*cas.Options
|
||||
AuthorityID string `json:"authorityID,omitempty"`
|
||||
Provisioners provisioner.List `json:"provisioners"`
|
||||
AuthorityID string `json:"authorityId,omitempty"`
|
||||
DeploymentType string `json:"deploymentType,omitempty"`
|
||||
Provisioners provisioner.List `json:"provisioners,omitempty"`
|
||||
Admins []*linkedca.Admin `json:"-"`
|
||||
Template *ASN1DN `json:"template,omitempty"`
|
||||
Claims *provisioner.Claims `json:"claims,omitempty"`
|
||||
|
@ -188,9 +190,10 @@ func (c *Config) Validate() error {
|
|||
switch {
|
||||
case c.Address == "":
|
||||
return errors.New("address cannot be empty")
|
||||
|
||||
case len(c.DNSNames) == 0:
|
||||
return errors.New("dnsNames cannot be empty")
|
||||
case c.AuthorityConfig == nil:
|
||||
return errors.New("authority cannot be nil")
|
||||
}
|
||||
|
||||
// Options holds the RA/CAS configuration.
|
||||
|
@ -222,7 +225,7 @@ func (c *Config) Validate() error {
|
|||
c.TLS.MaxVersion = DefaultTLSOptions.MaxVersion
|
||||
}
|
||||
if c.TLS.MinVersion == 0 {
|
||||
c.TLS.MinVersion = c.TLS.MaxVersion
|
||||
c.TLS.MinVersion = DefaultTLSOptions.MinVersion
|
||||
}
|
||||
if c.TLS.MinVersion > c.TLS.MaxVersion {
|
||||
return errors.New("tls minVersion cannot exceed tls maxVersion")
|
||||
|
|
|
@ -15,8 +15,9 @@ var (
|
|||
// DefaultTLSRenegotiation default TLS connection renegotiation policy.
|
||||
DefaultTLSRenegotiation = false // Never regnegotiate.
|
||||
// DefaultTLSCipherSuites specifies default step ciphersuite(s).
|
||||
// These are TLS 1.0 - 1.2 cipher suites.
|
||||
DefaultTLSCipherSuites = CipherSuites{
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
}
|
||||
// ApprovedTLSCipherSuites smallstep approved ciphersuites.
|
||||
|
@ -26,25 +27,21 @@ var (
|
|||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
|
||||
}
|
||||
// DefaultTLSOptions represents the default TLS version as well as the cipher
|
||||
// suites used in the TLS certificates.
|
||||
DefaultTLSOptions = TLSOptions{
|
||||
CipherSuites: CipherSuites{
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
},
|
||||
MinVersion: 1.2,
|
||||
MaxVersion: 1.2,
|
||||
Renegotiation: false,
|
||||
CipherSuites: DefaultTLSCipherSuites,
|
||||
MinVersion: DefaultTLSMinVersion,
|
||||
MaxVersion: DefaultTLSMaxVersion,
|
||||
Renegotiation: DefaultTLSRenegotiation,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -119,27 +116,38 @@ func (c CipherSuites) Value() []uint16 {
|
|||
|
||||
// cipherSuites has the list of supported cipher suites.
|
||||
var cipherSuites = map[string]uint16{
|
||||
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
|
||||
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
// TLS 1.0 - 1.2 cipher suites.
|
||||
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
|
||||
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
|
||||
// TLS 1.3 cipher sutes.
|
||||
"TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256,
|
||||
"TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384,
|
||||
"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
|
||||
// Legacy names.
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
}
|
||||
|
||||
// TLSOptions represents the TLS options that can be specified on *tls.Config
|
||||
|
|
284
authority/export.go
Normal file
284
authority/export.go
Normal file
|
@ -0,0 +1,284 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/cli-utils/config"
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// Export creates a linkedca configuration form the current ca.json and loaded
|
||||
// authorities.
|
||||
//
|
||||
// Note that export will not export neither the pki password nor the certificate
|
||||
// issuer password.
|
||||
func (a *Authority) Export() (c *linkedca.Configuration, err error) {
|
||||
// Recover from panics
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
|
||||
files := make(map[string][]byte)
|
||||
|
||||
// The exported configuration should not include the password in it.
|
||||
c = &linkedca.Configuration{
|
||||
Version: "1.0",
|
||||
Root: mustReadFilesOrURIs(a.config.Root, files),
|
||||
FederatedRoots: mustReadFilesOrURIs(a.config.FederatedRoots, files),
|
||||
Intermediate: mustReadFileOrURI(a.config.IntermediateCert, files),
|
||||
IntermediateKey: mustReadFileOrURI(a.config.IntermediateKey, files),
|
||||
Address: a.config.Address,
|
||||
InsecureAddress: a.config.InsecureAddress,
|
||||
DnsNames: a.config.DNSNames,
|
||||
Db: mustMarshalToStruct(a.config.DB),
|
||||
Logger: mustMarshalToStruct(a.config.Logger),
|
||||
Monitoring: mustMarshalToStruct(a.config.Monitoring),
|
||||
Authority: &linkedca.Authority{
|
||||
Id: a.config.AuthorityConfig.AuthorityID,
|
||||
EnableAdmin: a.config.AuthorityConfig.EnableAdmin,
|
||||
DisableIssuedAtCheck: a.config.AuthorityConfig.DisableIssuedAtCheck,
|
||||
Backdate: mustDuration(a.config.AuthorityConfig.Backdate),
|
||||
DeploymentType: a.config.AuthorityConfig.DeploymentType,
|
||||
},
|
||||
Files: files,
|
||||
}
|
||||
|
||||
// SSH
|
||||
if v := a.config.SSH; v != nil {
|
||||
c.Ssh = &linkedca.SSH{
|
||||
HostKey: mustReadFileOrURI(v.HostKey, files),
|
||||
UserKey: mustReadFileOrURI(v.UserKey, files),
|
||||
AddUserPrincipal: v.AddUserPrincipal,
|
||||
AddUserCommand: v.AddUserCommand,
|
||||
}
|
||||
for _, k := range v.Keys {
|
||||
typ, ok := linkedca.SSHPublicKey_Type_value[strings.ToUpper(k.Type)]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unsupported ssh key type %s", k.Type)
|
||||
}
|
||||
c.Ssh.Keys = append(c.Ssh.Keys, &linkedca.SSHPublicKey{
|
||||
Type: linkedca.SSHPublicKey_Type(typ),
|
||||
Federated: k.Federated,
|
||||
Key: mustMarshalToStruct(k),
|
||||
})
|
||||
}
|
||||
if b := v.Bastion; b != nil {
|
||||
c.Ssh.Bastion = &linkedca.Bastion{
|
||||
Hostname: b.Hostname,
|
||||
User: b.User,
|
||||
Port: b.Port,
|
||||
Command: b.Command,
|
||||
Flags: b.Flags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KMS
|
||||
if v := a.config.KMS; v != nil {
|
||||
var typ int32
|
||||
var ok bool
|
||||
if v.Type == "" {
|
||||
typ = int32(linkedca.KMS_SOFTKMS)
|
||||
} else {
|
||||
typ, ok = linkedca.KMS_Type_value[strings.ToUpper(v.Type)]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unsupported kms type %s", v.Type)
|
||||
}
|
||||
}
|
||||
c.Kms = &linkedca.KMS{
|
||||
Type: linkedca.KMS_Type(typ),
|
||||
CredentialsFile: v.CredentialsFile,
|
||||
Uri: v.URI,
|
||||
Pin: v.Pin,
|
||||
ManagementKey: v.ManagementKey,
|
||||
Region: v.Region,
|
||||
Profile: v.Profile,
|
||||
}
|
||||
}
|
||||
|
||||
// Authority
|
||||
// cas options
|
||||
if v := a.config.AuthorityConfig.Options; v != nil {
|
||||
c.Authority.Type = 0
|
||||
c.Authority.CertificateAuthority = v.CertificateAuthority
|
||||
c.Authority.CertificateAuthorityFingerprint = v.CertificateAuthorityFingerprint
|
||||
c.Authority.CredentialsFile = v.CredentialsFile
|
||||
if iss := v.CertificateIssuer; iss != nil {
|
||||
typ, ok := linkedca.CertificateIssuer_Type_value[strings.ToUpper(iss.Type)]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unknown certificate issuer type %s", iss.Type)
|
||||
}
|
||||
// The exported certificate issuer should not include the password.
|
||||
c.Authority.CertificateIssuer = &linkedca.CertificateIssuer{
|
||||
Type: linkedca.CertificateIssuer_Type(typ),
|
||||
Provisioner: iss.Provisioner,
|
||||
Certificate: mustReadFileOrURI(iss.Certificate, files),
|
||||
Key: mustReadFileOrURI(iss.Key, files),
|
||||
}
|
||||
}
|
||||
}
|
||||
// admins
|
||||
for {
|
||||
list, cursor := a.admins.Find("", 100)
|
||||
c.Authority.Admins = append(c.Authority.Admins, list...)
|
||||
if cursor == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
// provisioners
|
||||
for {
|
||||
list, cursor := a.provisioners.Find("", 100)
|
||||
for _, p := range list {
|
||||
lp, err := ProvisionerToLinkedca(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Authority.Provisioners = append(c.Authority.Provisioners, lp)
|
||||
}
|
||||
if cursor == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
// global claims
|
||||
c.Authority.Claims = claimsToLinkedca(a.config.AuthorityConfig.Claims)
|
||||
// Distinguished names template
|
||||
if v := a.config.AuthorityConfig.Template; v != nil {
|
||||
c.Authority.Template = &linkedca.DistinguishedName{
|
||||
Country: v.Country,
|
||||
Organization: v.Organization,
|
||||
OrganizationalUnit: v.OrganizationalUnit,
|
||||
Locality: v.Locality,
|
||||
Province: v.Province,
|
||||
StreetAddress: v.StreetAddress,
|
||||
SerialNumber: v.SerialNumber,
|
||||
CommonName: v.CommonName,
|
||||
}
|
||||
}
|
||||
|
||||
// TLS
|
||||
if v := a.config.TLS; v != nil {
|
||||
c.Tls = &linkedca.TLS{
|
||||
MinVersion: v.MinVersion.String(),
|
||||
MaxVersion: v.MaxVersion.String(),
|
||||
Renegotiation: v.Renegotiation,
|
||||
}
|
||||
for _, cs := range v.CipherSuites.Value() {
|
||||
c.Tls.CipherSuites = append(c.Tls.CipherSuites, linkedca.TLS_CiperSuite(cs))
|
||||
}
|
||||
}
|
||||
|
||||
// Templates
|
||||
if v := a.config.Templates; v != nil {
|
||||
c.Templates = &linkedca.ConfigTemplates{
|
||||
Ssh: &linkedca.SSHConfigTemplate{},
|
||||
Data: mustMarshalToStruct(v.Data),
|
||||
}
|
||||
// Remove automatically loaded vars
|
||||
if c.Templates.Data != nil && c.Templates.Data.Fields != nil {
|
||||
delete(c.Templates.Data.Fields, "Step")
|
||||
}
|
||||
for _, t := range v.SSH.Host {
|
||||
typ, ok := linkedca.ConfigTemplate_Type_value[strings.ToUpper(string(t.Type))]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unsupported template type %s", t.Type)
|
||||
}
|
||||
c.Templates.Ssh.Hosts = append(c.Templates.Ssh.Hosts, &linkedca.ConfigTemplate{
|
||||
Type: linkedca.ConfigTemplate_Type(typ),
|
||||
Name: t.Name,
|
||||
Template: mustReadFileOrURI(t.TemplatePath, files),
|
||||
Path: t.Path,
|
||||
Comment: t.Comment,
|
||||
Requires: t.RequiredData,
|
||||
Content: t.Content,
|
||||
})
|
||||
}
|
||||
for _, t := range v.SSH.User {
|
||||
typ, ok := linkedca.ConfigTemplate_Type_value[strings.ToUpper(string(t.Type))]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unsupported template type %s", t.Type)
|
||||
}
|
||||
c.Templates.Ssh.Users = append(c.Templates.Ssh.Users, &linkedca.ConfigTemplate{
|
||||
Type: linkedca.ConfigTemplate_Type(typ),
|
||||
Name: t.Name,
|
||||
Template: mustReadFileOrURI(t.TemplatePath, files),
|
||||
Path: t.Path,
|
||||
Comment: t.Comment,
|
||||
Requires: t.RequiredData,
|
||||
Content: t.Content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func mustDuration(d *provisioner.Duration) string {
|
||||
if d == nil || d.Duration == 0 {
|
||||
return ""
|
||||
}
|
||||
return d.String()
|
||||
}
|
||||
|
||||
func mustMarshalToStruct(v interface{}) *structpb.Struct {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "error marshaling %T", v))
|
||||
}
|
||||
var r *structpb.Struct
|
||||
if err := json.Unmarshal(b, &r); err != nil {
|
||||
panic(errors.Wrapf(err, "error unmarshaling %T", v))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func mustReadFileOrURI(fn string, m map[string][]byte) string {
|
||||
if fn == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
stepPath := filepath.ToSlash(config.StepPath())
|
||||
if !strings.HasSuffix(stepPath, "/") {
|
||||
stepPath += "/"
|
||||
}
|
||||
|
||||
fn = strings.TrimPrefix(filepath.ToSlash(fn), stepPath)
|
||||
|
||||
ok, err := isFilename(fn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if ok {
|
||||
b, err := ioutil.ReadFile(config.StepAbs(fn))
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "error reading %s", fn))
|
||||
}
|
||||
m[fn] = b
|
||||
return fn
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
func mustReadFilesOrURIs(fns []string, m map[string][]byte) []string {
|
||||
var result []string
|
||||
for _, fn := range fns {
|
||||
result = append(result, mustReadFileOrURI(fn, m))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isFilename(fn string) (bool, error) {
|
||||
u, err := url.Parse(fn)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "error parsing %s", fn)
|
||||
}
|
||||
return u.Scheme == "" || u.Scheme == "file", nil
|
||||
}
|
490
authority/linkedca.go
Normal file
490
authority/linkedca.go
Normal file
|
@ -0,0 +1,490 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/tlsutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
const uuidPattern = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
|
||||
|
||||
type linkedCaClient struct {
|
||||
renewer *tlsutil.Renewer
|
||||
client linkedca.MajordomoClient
|
||||
authorityID string
|
||||
}
|
||||
|
||||
type linkedCAClaims struct {
|
||||
jose.Claims
|
||||
SANs []string `json:"sans"`
|
||||
SHA string `json:"sha"`
|
||||
}
|
||||
|
||||
func newLinkedCAClient(token string) (*linkedCaClient, error) {
|
||||
tok, err := jose.ParseSigned(token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing token")
|
||||
}
|
||||
|
||||
var claims linkedCAClaims
|
||||
if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing token")
|
||||
}
|
||||
// Validate claims
|
||||
if len(claims.Audience) != 1 {
|
||||
return nil, errors.New("error parsing token: invalid aud claim")
|
||||
}
|
||||
if claims.SHA == "" {
|
||||
return nil, errors.New("error parsing token: invalid sha claim")
|
||||
}
|
||||
// Get linkedCA endpoint from audience.
|
||||
u, err := url.Parse(claims.Audience[0])
|
||||
if err != nil {
|
||||
return nil, errors.New("error parsing token: invalid aud claim")
|
||||
}
|
||||
// Get authority from SANs
|
||||
authority, err := getAuthority(claims.SANs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create csr to login with
|
||||
signer, err := keyutil.GenerateDefaultSigner()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csr, err := x509util.CreateCertificateRequest(claims.Subject, claims.SANs, signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get and verify root certificate
|
||||
root, err := getRootCertificate(u.Host, claims.SHA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(root)
|
||||
|
||||
// Login with majordomo and get certificates
|
||||
cert, tlsConfig, err := login(authority, token, csr, signer, u.Host, pool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start TLS renewer and set the GetClientCertificate callback to it.
|
||||
renewer, err := tlsutil.NewRenewer(cert, tlsConfig, func() (*tls.Certificate, *tls.Config, error) {
|
||||
return login(authority, token, csr, signer, u.Host, pool)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.GetClientCertificate = renewer.GetClientCertificate
|
||||
|
||||
// Start mTLS client
|
||||
conn, err := grpc.Dial(u.Host, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error connecting %s", u.Host)
|
||||
}
|
||||
|
||||
return &linkedCaClient{
|
||||
renewer: renewer,
|
||||
client: linkedca.NewMajordomoClient(conn),
|
||||
authorityID: authority,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) Run() {
|
||||
c.renewer.Run()
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) Stop() {
|
||||
c.renewer.Stop()
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
|
||||
resp, err := c.client.CreateProvisioner(ctx, &linkedca.CreateProvisionerRequest{
|
||||
Type: prov.Type,
|
||||
Name: prov.Name,
|
||||
Details: prov.Details,
|
||||
Claims: prov.Claims,
|
||||
X509Template: prov.X509Template,
|
||||
SshTemplate: prov.SshTemplate,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating provisioner")
|
||||
}
|
||||
prov.Id = resp.Id
|
||||
prov.AuthorityId = resp.AuthorityId
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) {
|
||||
resp, err := c.client.GetProvisioner(ctx, &linkedca.GetProvisionerRequest{
|
||||
Id: id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error getting provisioners")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) {
|
||||
resp, err := c.client.GetConfiguration(ctx, &linkedca.ConfigurationRequest{
|
||||
AuthorityId: c.authorityID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error getting provisioners")
|
||||
}
|
||||
return resp.Provisioners, nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
|
||||
_, err := c.client.UpdateProvisioner(ctx, &linkedca.UpdateProvisionerRequest{
|
||||
Id: prov.Id,
|
||||
Name: prov.Name,
|
||||
Details: prov.Details,
|
||||
Claims: prov.Claims,
|
||||
X509Template: prov.X509Template,
|
||||
SshTemplate: prov.SshTemplate,
|
||||
})
|
||||
return errors.Wrap(err, "error updating provisioner")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) DeleteProvisioner(ctx context.Context, id string) error {
|
||||
_, err := c.client.DeleteProvisioner(ctx, &linkedca.DeleteProvisionerRequest{
|
||||
Id: id,
|
||||
})
|
||||
return errors.Wrap(err, "error deleting provisioner")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) CreateAdmin(ctx context.Context, adm *linkedca.Admin) error {
|
||||
resp, err := c.client.CreateAdmin(ctx, &linkedca.CreateAdminRequest{
|
||||
Subject: adm.Subject,
|
||||
ProvisionerId: adm.ProvisionerId,
|
||||
Type: adm.Type,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating admin")
|
||||
}
|
||||
adm.Id = resp.Id
|
||||
adm.AuthorityId = resp.AuthorityId
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) {
|
||||
resp, err := c.client.GetAdmin(ctx, &linkedca.GetAdminRequest{
|
||||
Id: id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error getting admins")
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) {
|
||||
resp, err := c.client.GetConfiguration(ctx, &linkedca.ConfigurationRequest{
|
||||
AuthorityId: c.authorityID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error getting admins")
|
||||
}
|
||||
return resp.Admins, nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) UpdateAdmin(ctx context.Context, adm *linkedca.Admin) error {
|
||||
_, err := c.client.UpdateAdmin(ctx, &linkedca.UpdateAdminRequest{
|
||||
Id: adm.Id,
|
||||
Type: adm.Type,
|
||||
})
|
||||
return errors.Wrap(err, "error updating admin")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) DeleteAdmin(ctx context.Context, id string) error {
|
||||
_, err := c.client.DeleteAdmin(ctx, &linkedca.DeleteAdminRequest{
|
||||
Id: id,
|
||||
})
|
||||
return errors.Wrap(err, "error deleting admin")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) StoreCertificateChain(fullchain ...*x509.Certificate) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_, err := c.client.PostCertificate(ctx, &linkedca.CertificateRequest{
|
||||
PemCertificate: serializeCertificateChain(fullchain[0]),
|
||||
PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
|
||||
})
|
||||
return errors.Wrap(err, "error posting certificate")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) StoreRenewedCertificate(parent *x509.Certificate, fullchain ...*x509.Certificate) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_, err := c.client.PostCertificate(ctx, &linkedca.CertificateRequest{
|
||||
PemCertificate: serializeCertificateChain(fullchain[0]),
|
||||
PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
|
||||
PemParentCertificate: serializeCertificateChain(parent),
|
||||
})
|
||||
return errors.Wrap(err, "error posting certificate")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) StoreSSHCertificate(crt *ssh.Certificate) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{
|
||||
Certificate: string(ssh.MarshalAuthorizedKey(crt)),
|
||||
})
|
||||
return errors.Wrap(err, "error posting ssh certificate")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) Revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_, err := c.client.RevokeCertificate(ctx, &linkedca.RevokeCertificateRequest{
|
||||
Serial: rci.Serial,
|
||||
PemCertificate: serializeCertificate(crt),
|
||||
Reason: rci.Reason,
|
||||
ReasonCode: linkedca.RevocationReasonCode(rci.ReasonCode),
|
||||
Passive: true,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "error revoking certificate")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) RevokeSSH(ssh *ssh.Certificate, rci *db.RevokedCertificateInfo) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_, err := c.client.RevokeSSHCertificate(ctx, &linkedca.RevokeSSHCertificateRequest{
|
||||
Serial: rci.Serial,
|
||||
Certificate: serializeSSHCertificate(ssh),
|
||||
Reason: rci.Reason,
|
||||
ReasonCode: linkedca.RevocationReasonCode(rci.ReasonCode),
|
||||
Passive: true,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "error revoking ssh certificate")
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) IsRevoked(serial string) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
resp, err := c.client.GetCertificateStatus(ctx, &linkedca.GetCertificateStatusRequest{
|
||||
Serial: serial,
|
||||
})
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "error getting certificate status")
|
||||
}
|
||||
return resp.Status != linkedca.RevocationStatus_ACTIVE, nil
|
||||
}
|
||||
|
||||
func (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
resp, err := c.client.GetSSHCertificateStatus(ctx, &linkedca.GetSSHCertificateStatusRequest{
|
||||
Serial: serial,
|
||||
})
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "error getting certificate status")
|
||||
}
|
||||
return resp.Status != linkedca.RevocationStatus_ACTIVE, nil
|
||||
}
|
||||
|
||||
func serializeCertificate(crt *x509.Certificate) string {
|
||||
if crt == nil {
|
||||
return ""
|
||||
}
|
||||
return string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: crt.Raw,
|
||||
}))
|
||||
}
|
||||
|
||||
func serializeCertificateChain(fullchain ...*x509.Certificate) string {
|
||||
var chain string
|
||||
for _, crt := range fullchain {
|
||||
chain += string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: crt.Raw,
|
||||
}))
|
||||
}
|
||||
return chain
|
||||
}
|
||||
|
||||
func serializeSSHCertificate(crt *ssh.Certificate) string {
|
||||
if crt == nil {
|
||||
return ""
|
||||
}
|
||||
return string(ssh.MarshalAuthorizedKey(crt))
|
||||
}
|
||||
|
||||
func getAuthority(sans []string) (string, error) {
|
||||
for _, s := range sans {
|
||||
if strings.HasPrefix(s, "urn:smallstep:authority:") {
|
||||
if regexp.MustCompile(uuidPattern).MatchString(s[24:]) {
|
||||
return s[24:], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("error parsing token: invalid sans claim")
|
||||
}
|
||||
|
||||
// getRootCertificate creates an insecure majordomo client and returns the
|
||||
// verified root certificate.
|
||||
func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error connecting %s", endpoint)
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client := linkedca.NewMajordomoClient(conn)
|
||||
resp, err := client.GetRootCertificate(ctx, &linkedca.GetRootCertificateRequest{
|
||||
Fingerprint: fingerprint,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting root certificate: %w", err)
|
||||
}
|
||||
|
||||
var block *pem.Block
|
||||
b := []byte(resp.PemCertificate)
|
||||
for len(b) > 0 {
|
||||
block, b = pem.Decode(b)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing certificate: %w", err)
|
||||
}
|
||||
|
||||
// verify the sha256
|
||||
sum := sha256.Sum256(cert.Raw)
|
||||
if !strings.EqualFold(fingerprint, hex.EncodeToString(sum[:])) {
|
||||
return nil, fmt.Errorf("error verifying certificate: SHA256 fingerprint does not match")
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error getting root certificate: certificate not found")
|
||||
}
|
||||
|
||||
// login creates a new majordomo client with just the root ca pool and returns
|
||||
// the signed certificate and tls configuration.
|
||||
func login(authority, token string, csr *x509.CertificateRequest, signer crypto.PrivateKey, endpoint string, rootCAs *x509.CertPool) (*tls.Certificate, *tls.Config, error) {
|
||||
// Connect to majordomo
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
})))
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "error connecting %s", endpoint)
|
||||
}
|
||||
|
||||
// Login to get the signed certificate
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
client := linkedca.NewMajordomoClient(conn)
|
||||
resp, err := client.Login(ctx, &linkedca.LoginRequest{
|
||||
AuthorityId: authority,
|
||||
Token: token,
|
||||
PemCertificateRequest: string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csr.Raw,
|
||||
})),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "error logging in %s", endpoint)
|
||||
}
|
||||
|
||||
// Parse login response
|
||||
var block *pem.Block
|
||||
var bundle []*x509.Certificate
|
||||
rest := []byte(resp.PemCertificateChain)
|
||||
for {
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
return nil, nil, errors.New("error decoding login response: pemCertificateChain is not a certificate bundle")
|
||||
}
|
||||
crt, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "error parsing login response")
|
||||
}
|
||||
bundle = append(bundle, crt)
|
||||
}
|
||||
if len(bundle) == 0 {
|
||||
return nil, nil, errors.New("error decoding login response: pemCertificateChain should not be empty")
|
||||
}
|
||||
|
||||
// Build tls.Certificate with PemCertificate and intermediates in the
|
||||
// PemCertificateChain
|
||||
cert := &tls.Certificate{
|
||||
PrivateKey: signer,
|
||||
}
|
||||
rest = []byte(resp.PemCertificate)
|
||||
for {
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type == "CERTIFICATE" {
|
||||
leaf, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "error parsing pemCertificate")
|
||||
}
|
||||
cert.Certificate = append(cert.Certificate, block.Bytes)
|
||||
cert.Leaf = leaf
|
||||
}
|
||||
}
|
||||
|
||||
// Add intermediates to the tls.Certificate
|
||||
last := len(bundle) - 1
|
||||
for i := 0; i < last; i++ {
|
||||
cert.Certificate = append(cert.Certificate, bundle[i].Raw)
|
||||
}
|
||||
|
||||
// Add root to the pool if it's not there yet
|
||||
rootCAs.AddCert(bundle[last])
|
||||
|
||||
return cert, &tls.Config{
|
||||
RootCAs: rootCAs,
|
||||
}, nil
|
||||
}
|
|
@ -196,6 +196,15 @@ func WithAdminDB(db admin.DB) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithLinkedCAToken is an option to set the authentication token used to enable
|
||||
// linked ca.
|
||||
func WithLinkedCAToken(token string) Option {
|
||||
return func(a *Authority) error {
|
||||
a.linkedCAToken = token
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {
|
||||
var block *pem.Block
|
||||
var certs []*x509.Certificate
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"go.step.sm/crypto/jose"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -30,7 +29,6 @@ type SSHPOP struct {
|
|||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
db db.AuthDB
|
||||
claimer *Claimer
|
||||
audiences Audiences
|
||||
sshPubKeys *SSHKeys
|
||||
|
@ -102,7 +100,6 @@ func (p *SSHPOP) Init(config Config) error {
|
|||
}
|
||||
|
||||
p.audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
||||
p.db = config.DB
|
||||
p.sshPubKeys = config.SSHKeys
|
||||
return nil
|
||||
}
|
||||
|
@ -110,6 +107,8 @@ func (p *SSHPOP) Init(config Config) error {
|
|||
// authorizeToken performs common jwt authorization actions and returns the
|
||||
// claims for case specific downstream parsing.
|
||||
// e.g. a Sign request will auth/validate different fields than a Revoke request.
|
||||
//
|
||||
// Checking for certificate revocation has been moved to the authority package.
|
||||
func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayload, error) {
|
||||
sshCert, jwt, err := ExtractSSHPOPCert(token)
|
||||
if err != nil {
|
||||
|
@ -117,14 +116,6 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
|
|||
"sshpop.authorizeToken; error extracting sshpop header from token")
|
||||
}
|
||||
|
||||
// Check for revocation.
|
||||
if isRevoked, err := p.db.IsSSHRevoked(strconv.FormatUint(sshCert.Serial, 10)); err != nil {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err,
|
||||
"sshpop.authorizeToken; error checking checking sshpop cert revocation")
|
||||
} else if isRevoked {
|
||||
return nil, errs.Unauthorized("sshpop.authorizeToken; sshpop certificate is revoked")
|
||||
}
|
||||
|
||||
// Check validity period of the certificate.
|
||||
n := time.Now()
|
||||
if sshCert.ValidAfter != 0 && time.Unix(int64(sshCert.ValidAfter), 0).After(n) {
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
|
@ -83,52 +82,9 @@ func TestSSHPOP_authorizeToken(t *testing.T) {
|
|||
err: errors.New("sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "),
|
||||
}
|
||||
},
|
||||
"fail/error-revoked-db-check": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, errors.New("force")
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateSSHPOPToken(p, cert, jwk)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: tok,
|
||||
code: http.StatusInternalServerError,
|
||||
err: errors.New("sshpop.authorizeToken; error checking checking sshpop cert revocation: force"),
|
||||
}
|
||||
},
|
||||
"fail/cert-already-revoked": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return true, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateSSHPOPToken(p, cert, jwk)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
p: p,
|
||||
token: tok,
|
||||
code: http.StatusUnauthorized,
|
||||
err: errors.New("sshpop.authorizeToken; sshpop certificate is revoked"),
|
||||
}
|
||||
},
|
||||
"fail/cert-not-yet-valid": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{
|
||||
CertType: ssh.UserCert,
|
||||
ValidAfter: uint64(time.Now().Add(time.Minute).Unix()),
|
||||
|
@ -146,11 +102,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) {
|
|||
"fail/cert-past-validity": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{
|
||||
CertType: ssh.UserCert,
|
||||
ValidBefore: uint64(time.Now().Add(-time.Minute).Unix()),
|
||||
|
@ -168,11 +119,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) {
|
|||
"fail/no-signer-found": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateSSHPOPToken(p, cert, jwk)
|
||||
|
@ -187,11 +133,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) {
|
|||
"fail/error-parsing-claims-bad-sig": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, _, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
otherJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
|
@ -208,11 +149,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) {
|
|||
"fail/invalid-claims-issuer": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("foo", "bar", testAudiences.Sign[0], "",
|
||||
|
@ -228,11 +164,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) {
|
|||
"fail/invalid-audience": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("foo", p.GetName(), "invalid-aud", "",
|
||||
|
@ -248,11 +179,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) {
|
|||
"fail/empty-subject": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "",
|
||||
|
@ -268,11 +194,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) {
|
|||
"ok": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateSSHPOPToken(p, cert, jwk)
|
||||
|
@ -330,11 +251,6 @@ func TestSSHPOP_AuthorizeSSHRevoke(t *testing.T) {
|
|||
"fail/subject-not-equal-serial": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRevoke[0], "",
|
||||
|
@ -350,11 +266,6 @@ func TestSSHPOP_AuthorizeSSHRevoke(t *testing.T) {
|
|||
"ok": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.UserCert}, sshSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRevoke[0], "",
|
||||
|
@ -419,11 +330,6 @@ func TestSSHPOP_AuthorizeSSHRenew(t *testing.T) {
|
|||
"fail/not-host-cert": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRenew[0], "",
|
||||
|
@ -439,11 +345,6 @@ func TestSSHPOP_AuthorizeSSHRenew(t *testing.T) {
|
|||
"ok": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRenew[0], "",
|
||||
|
@ -511,11 +412,6 @@ func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) {
|
|||
"fail/not-host-cert": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRekey[0], "",
|
||||
|
@ -531,11 +427,6 @@ func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) {
|
|||
"ok": func(t *testing.T) test {
|
||||
p, err := generateSSHPOP()
|
||||
assert.FatalError(t, err)
|
||||
p.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner)
|
||||
assert.FatalError(t, err)
|
||||
tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRekey[0], "",
|
||||
|
|
|
@ -4,12 +4,17 @@ import (
|
|||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
step "go.step.sm/cli-utils/config"
|
||||
"go.step.sm/cli-utils/ui"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/linkedca"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
@ -234,6 +239,14 @@ func (a *Authority) RemoveProvisioner(ctx context.Context, id string) error {
|
|||
}
|
||||
|
||||
func CreateFirstProvisioner(ctx context.Context, db admin.DB, password string) (*linkedca.Provisioner, error) {
|
||||
if password == "" {
|
||||
pass, err := ui.PromptPasswordGenerate("Please enter the password to encrypt your first provisioner, leave empty and we'll generate one")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
password = string(pass)
|
||||
}
|
||||
|
||||
jwk, jwe, err := jose.GenerateDefaultKeyPair([]byte(password))
|
||||
if err != nil {
|
||||
return nil, admin.WrapErrorISE(err, "error generating JWK key pair")
|
||||
|
@ -398,6 +411,13 @@ func durationsToCertificates(d *linkedca.Durations) (min, max, def *provisioner.
|
|||
return
|
||||
}
|
||||
|
||||
func durationsToLinkedca(d *provisioner.Duration) string {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
return d.Duration.String()
|
||||
}
|
||||
|
||||
// claimsToCertificates converts the linkedca provisioner claims type to the
|
||||
// certifictes claims type.
|
||||
func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) {
|
||||
|
@ -438,6 +458,109 @@ func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) {
|
|||
return pc, nil
|
||||
}
|
||||
|
||||
func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
disableRenewal := config.DefaultDisableRenewal
|
||||
if c.DisableRenewal != nil {
|
||||
disableRenewal = *c.DisableRenewal
|
||||
}
|
||||
|
||||
lc := &linkedca.Claims{
|
||||
DisableRenewal: disableRenewal,
|
||||
}
|
||||
|
||||
if c.DefaultTLSDur != nil || c.MinTLSDur != nil || c.MaxTLSDur != nil {
|
||||
lc.X509 = &linkedca.X509Claims{
|
||||
Enabled: true,
|
||||
Durations: &linkedca.Durations{
|
||||
Default: durationsToLinkedca(c.DefaultTLSDur),
|
||||
Min: durationsToLinkedca(c.MinTLSDur),
|
||||
Max: durationsToLinkedca(c.MaxTLSDur),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if c.EnableSSHCA != nil && *c.EnableSSHCA {
|
||||
lc.Ssh = &linkedca.SSHClaims{
|
||||
Enabled: true,
|
||||
}
|
||||
if c.DefaultUserSSHDur != nil || c.MinUserSSHDur != nil || c.MaxUserSSHDur != nil {
|
||||
lc.Ssh.UserDurations = &linkedca.Durations{
|
||||
Default: durationsToLinkedca(c.DefaultUserSSHDur),
|
||||
Min: durationsToLinkedca(c.MinUserSSHDur),
|
||||
Max: durationsToLinkedca(c.MaxUserSSHDur),
|
||||
}
|
||||
}
|
||||
if c.DefaultHostSSHDur != nil || c.MinHostSSHDur != nil || c.MaxHostSSHDur != nil {
|
||||
lc.Ssh.HostDurations = &linkedca.Durations{
|
||||
Default: durationsToLinkedca(c.DefaultHostSSHDur),
|
||||
Min: durationsToLinkedca(c.MinHostSSHDur),
|
||||
Max: durationsToLinkedca(c.MaxHostSSHDur),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lc
|
||||
}
|
||||
|
||||
func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *linkedca.Template, error) {
|
||||
var err error
|
||||
var x509Template, sshTemplate *linkedca.Template
|
||||
|
||||
if p == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
if p.X509 != nil && p.X509.HasTemplate() {
|
||||
x509Template = &linkedca.Template{
|
||||
Template: nil,
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
if p.X509.Template != "" {
|
||||
x509Template.Template = []byte(p.SSH.Template)
|
||||
} else if p.X509.TemplateFile != "" {
|
||||
filename := step.StepAbs(p.X509.TemplateFile)
|
||||
if x509Template.Template, err = ioutil.ReadFile(filename); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "error reading x509 template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.SSH != nil && p.SSH.HasTemplate() {
|
||||
sshTemplate = &linkedca.Template{
|
||||
Template: nil,
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
if p.SSH.Template != "" {
|
||||
sshTemplate.Template = []byte(p.SSH.Template)
|
||||
} else if p.SSH.TemplateFile != "" {
|
||||
filename := step.StepAbs(p.SSH.TemplateFile)
|
||||
if sshTemplate.Template, err = ioutil.ReadFile(filename); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "error reading ssh template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return x509Template, sshTemplate, nil
|
||||
}
|
||||
|
||||
func provisionerPEMToLinkedca(b []byte) [][]byte {
|
||||
var roots [][]byte
|
||||
var block *pem.Block
|
||||
for {
|
||||
if block, b = pem.Decode(b); block == nil {
|
||||
break
|
||||
}
|
||||
roots = append(roots, pem.EncodeToMemory(block))
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
// ProvisionerToCertificates converts the linkedca provisioner type to the certificates provisioner
|
||||
// interface.
|
||||
func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, error) {
|
||||
|
@ -448,7 +571,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
|
|||
|
||||
details := p.Details.GetData()
|
||||
if details == nil {
|
||||
return nil, fmt.Errorf("provisioner does not have any details")
|
||||
return nil, errors.New("provisioner does not have any details")
|
||||
}
|
||||
|
||||
options := optionsToCertificates(p)
|
||||
|
@ -457,7 +580,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
|
|||
case *linkedca.ProvisionerDetails_JWK:
|
||||
jwk := new(jose.JSONWebKey)
|
||||
if err := json.Unmarshal(d.JWK.PublicKey, &jwk); err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrap(err, "error unmarshaling public key")
|
||||
}
|
||||
return &provisioner.JWK{
|
||||
ID: p.Id,
|
||||
|
@ -588,6 +711,233 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
|
|||
}
|
||||
}
|
||||
|
||||
// ProvisionerToLinkedca converts a provisioner.Interface to a
|
||||
// linkedca.Provisioner type.
|
||||
func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, error) {
|
||||
switch p := p.(type) {
|
||||
case *provisioner.JWK:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
publicKey, err := json.Marshal(p.Key)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error marshaling key")
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_JWK,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_JWK{
|
||||
JWK: &linkedca.JWKProvisioner{
|
||||
PublicKey: publicKey,
|
||||
EncryptedPrivateKey: []byte(p.EncryptedKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
case *provisioner.OIDC:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_OIDC,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_OIDC{
|
||||
OIDC: &linkedca.OIDCProvisioner{
|
||||
ClientId: p.ClientID,
|
||||
ClientSecret: p.ClientSecret,
|
||||
ConfigurationEndpoint: p.ConfigurationEndpoint,
|
||||
Admins: p.Admins,
|
||||
Domains: p.Domains,
|
||||
Groups: p.Groups,
|
||||
ListenAddress: p.ListenAddress,
|
||||
TenantId: p.TenantID,
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
case *provisioner.GCP:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_GCP,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_GCP{
|
||||
GCP: &linkedca.GCPProvisioner{
|
||||
ServiceAccounts: p.ServiceAccounts,
|
||||
ProjectIds: p.ProjectIDs,
|
||||
DisableCustomSans: p.DisableCustomSANs,
|
||||
DisableTrustOnFirstUse: p.DisableTrustOnFirstUse,
|
||||
InstanceAge: p.InstanceAge.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
case *provisioner.AWS:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_AWS,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_AWS{
|
||||
AWS: &linkedca.AWSProvisioner{
|
||||
Accounts: p.Accounts,
|
||||
DisableCustomSans: p.DisableCustomSANs,
|
||||
DisableTrustOnFirstUse: p.DisableTrustOnFirstUse,
|
||||
InstanceAge: p.InstanceAge.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
case *provisioner.Azure:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_AZURE,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_Azure{
|
||||
Azure: &linkedca.AzureProvisioner{
|
||||
TenantId: p.TenantID,
|
||||
ResourceGroups: p.ResourceGroups,
|
||||
Audience: p.Audience,
|
||||
DisableCustomSans: p.DisableCustomSANs,
|
||||
DisableTrustOnFirstUse: p.DisableTrustOnFirstUse,
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
case *provisioner.ACME:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_ACME,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_ACME{
|
||||
ACME: &linkedca.ACMEProvisioner{
|
||||
ForceCn: p.ForceCN,
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
case *provisioner.X5C:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_X5C,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_X5C{
|
||||
X5C: &linkedca.X5CProvisioner{
|
||||
Roots: provisionerPEMToLinkedca(p.Roots),
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
case *provisioner.K8sSA:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_K8SSA,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_K8SSA{
|
||||
K8SSA: &linkedca.K8SSAProvisioner{
|
||||
PublicKeys: provisionerPEMToLinkedca(p.PubKeys),
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
case *provisioner.SSHPOP:
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_SSHPOP,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_SSHPOP{
|
||||
SSHPOP: &linkedca.SSHPOPProvisioner{},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
}, nil
|
||||
case *provisioner.SCEP:
|
||||
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &linkedca.Provisioner{
|
||||
Id: p.ID,
|
||||
Type: linkedca.Provisioner_SCEP,
|
||||
Name: p.GetName(),
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_SCEP{
|
||||
SCEP: &linkedca.SCEPProvisioner{
|
||||
ForceCn: p.ForceCN,
|
||||
Challenge: p.GetChallengePassword(),
|
||||
Capabilities: p.Capabilities,
|
||||
MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength),
|
||||
},
|
||||
},
|
||||
},
|
||||
Claims: claimsToLinkedca(p.Claims),
|
||||
X509Template: x509Template,
|
||||
SshTemplate: sshTemplate,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("provisioner %s not implemented", p.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func parseInstanceAge(age string) (provisioner.Duration, error) {
|
||||
var instanceAge provisioner.Duration
|
||||
if age != "" {
|
||||
|
|
|
@ -239,7 +239,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
|
|||
}
|
||||
}
|
||||
|
||||
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
||||
if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db")
|
||||
}
|
||||
|
||||
|
@ -249,7 +249,11 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
|
|||
// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.
|
||||
func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) {
|
||||
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
|
||||
return nil, errs.BadRequest("rewnewSSH: cannot renew certificate without validity period")
|
||||
return nil, errs.BadRequest("renewSSH: cannot renew certificate without validity period")
|
||||
}
|
||||
|
||||
if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backdate := a.config.AuthorityConfig.Backdate.Duration
|
||||
|
@ -294,7 +298,7 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss
|
|||
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
|
||||
}
|
||||
|
||||
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
||||
if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db")
|
||||
}
|
||||
|
||||
|
@ -319,6 +323,10 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub
|
|||
return nil, errs.BadRequest("rekeySSH; cannot rekey certificate without validity period")
|
||||
}
|
||||
|
||||
if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backdate := a.config.AuthorityConfig.Backdate.Duration
|
||||
duration := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second
|
||||
now := time.Now()
|
||||
|
@ -369,13 +377,23 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub
|
|||
}
|
||||
}
|
||||
|
||||
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
||||
if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db")
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (a *Authority) storeSSHCertificate(cert *ssh.Certificate) error {
|
||||
type sshCertificateStorer interface {
|
||||
StoreSSHCertificate(crt *ssh.Certificate) error
|
||||
}
|
||||
if s, ok := a.adminDB.(sshCertificateStorer); ok {
|
||||
return s.StoreSSHCertificate(cert)
|
||||
}
|
||||
return a.db.StoreSSHCertificate(cert)
|
||||
}
|
||||
|
||||
// IsValidForAddUser checks if a user provisioner certificate can be issued to
|
||||
// the given certificate.
|
||||
func IsValidForAddUser(cert *ssh.Certificate) error {
|
||||
|
@ -451,7 +469,7 @@ func (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subje
|
|||
}
|
||||
cert.Signature = sig
|
||||
|
||||
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
||||
if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
|
||||
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error storing certificate in db")
|
||||
}
|
||||
|
||||
|
|
|
@ -750,6 +750,11 @@ func TestAuthority_RekeySSH(t *testing.T) {
|
|||
now := time.Now().UTC()
|
||||
|
||||
a := testAuthority(t)
|
||||
a.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
|
||||
type test struct {
|
||||
auth *Authority
|
||||
|
@ -763,6 +768,56 @@ func TestAuthority_RekeySSH(t *testing.T) {
|
|||
code int
|
||||
}
|
||||
tests := map[string]func(t *testing.T) *test{
|
||||
"fail/is-revoked": func(t *testing.T) *test {
|
||||
auth := testAuthority(t)
|
||||
auth.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return true, nil
|
||||
},
|
||||
}
|
||||
return &test{
|
||||
auth: auth,
|
||||
userSigner: signer,
|
||||
hostSigner: signer,
|
||||
cert: &ssh.Certificate{
|
||||
Serial: 1234567890,
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
CertType: ssh.UserCert,
|
||||
ValidPrincipals: []string{"foo", "bar"},
|
||||
KeyId: "foo",
|
||||
},
|
||||
key: pub,
|
||||
signOpts: []provisioner.SignOption{},
|
||||
err: errors.New("authority.authorizeSSHCertificate: certificate has been revoked"),
|
||||
code: http.StatusUnauthorized,
|
||||
}
|
||||
},
|
||||
"fail/is-revoked-error": func(t *testing.T) *test {
|
||||
auth := testAuthority(t)
|
||||
auth.db = &db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, errors.New("an error")
|
||||
},
|
||||
}
|
||||
return &test{
|
||||
auth: auth,
|
||||
userSigner: signer,
|
||||
hostSigner: signer,
|
||||
cert: &ssh.Certificate{
|
||||
Serial: 1234567890,
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
CertType: ssh.UserCert,
|
||||
ValidPrincipals: []string{"foo", "bar"},
|
||||
KeyId: "foo",
|
||||
},
|
||||
key: pub,
|
||||
signOpts: []provisioner.SignOption{},
|
||||
err: errors.New("authority.authorizeSSHCertificate: an error"),
|
||||
code: http.StatusInternalServerError,
|
||||
}
|
||||
},
|
||||
"fail/opts-type": func(t *testing.T) *test {
|
||||
return &test{
|
||||
userSigner: signer,
|
||||
|
@ -831,6 +886,9 @@ func TestAuthority_RekeySSH(t *testing.T) {
|
|||
"fail/db-store": func(t *testing.T) *test {
|
||||
return &test{
|
||||
auth: testAuthority(t, WithDatabase(&db.MockAuthDB{
|
||||
MIsSSHRevoked: func(sn string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
MStoreSSHCertificate: func(cert *ssh.Certificate) error {
|
||||
return errors.New("force")
|
||||
},
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// GetTLSOptions returns the tls options configured.
|
||||
|
@ -36,7 +37,6 @@ func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc {
|
|||
if def == nil {
|
||||
return errors.New("default ASN1DN template cannot be nil")
|
||||
}
|
||||
|
||||
if len(crt.Subject.Country) == 0 && def.Country != "" {
|
||||
crt.Subject.Country = append(crt.Subject.Country, def.Country)
|
||||
}
|
||||
|
@ -55,7 +55,12 @@ func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc {
|
|||
if len(crt.Subject.StreetAddress) == 0 && def.StreetAddress != "" {
|
||||
crt.Subject.StreetAddress = append(crt.Subject.StreetAddress, def.StreetAddress)
|
||||
}
|
||||
|
||||
if len(crt.Subject.SerialNumber) == 0 && def.SerialNumber != "" {
|
||||
crt.Subject.SerialNumber = def.SerialNumber
|
||||
}
|
||||
if len(crt.Subject.CommonName) == 0 && def.CommonName != "" {
|
||||
crt.Subject.CommonName = def.CommonName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -280,9 +285,15 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5
|
|||
// `StoreCertificate(...*x509.Certificate) error` instead of just
|
||||
// `StoreCertificate(*x509.Certificate) error`.
|
||||
func (a *Authority) storeCertificate(fullchain []*x509.Certificate) error {
|
||||
if s, ok := a.db.(interface {
|
||||
type certificateChainStorer interface {
|
||||
StoreCertificateChain(...*x509.Certificate) error
|
||||
}); ok {
|
||||
}
|
||||
// Store certificate in linkedca
|
||||
if s, ok := a.adminDB.(certificateChainStorer); ok {
|
||||
return s.StoreCertificateChain(fullchain...)
|
||||
}
|
||||
// Store certificate in local db
|
||||
if s, ok := a.db.(certificateChainStorer); ok {
|
||||
return s.StoreCertificateChain(fullchain...)
|
||||
}
|
||||
return a.db.StoreCertificate(fullchain[0])
|
||||
|
@ -293,9 +304,15 @@ func (a *Authority) storeCertificate(fullchain []*x509.Certificate) error {
|
|||
//
|
||||
// TODO: at some point we should implement this in the standard implementation.
|
||||
func (a *Authority) storeRenewedCertificate(oldCert *x509.Certificate, fullchain []*x509.Certificate) error {
|
||||
if s, ok := a.db.(interface {
|
||||
type renewedCertificateChainStorer interface {
|
||||
StoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error
|
||||
}); ok {
|
||||
}
|
||||
// Store certificate in linkedca
|
||||
if s, ok := a.adminDB.(renewedCertificateChainStorer); ok {
|
||||
return s.StoreRenewedCertificate(oldCert, fullchain...)
|
||||
}
|
||||
// Store certificate in local db
|
||||
if s, ok := a.db.(renewedCertificateChainStorer); ok {
|
||||
return s.StoreRenewedCertificate(oldCert, fullchain...)
|
||||
}
|
||||
return a.db.StoreCertificate(fullchain[0])
|
||||
|
@ -381,7 +398,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
|||
}
|
||||
|
||||
if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
|
||||
err = a.db.RevokeSSH(rci)
|
||||
err = a.revokeSSH(nil, rci)
|
||||
} 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
|
||||
|
@ -408,7 +425,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
|||
}
|
||||
|
||||
// Save as revoked in the Db.
|
||||
err = a.db.Revoke(rci)
|
||||
err = a.revoke(revokedCert, rci)
|
||||
}
|
||||
switch err {
|
||||
case nil:
|
||||
|
@ -423,6 +440,24 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
|||
}
|
||||
}
|
||||
|
||||
func (a *Authority) revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {
|
||||
if lca, ok := a.adminDB.(interface {
|
||||
Revoke(*x509.Certificate, *db.RevokedCertificateInfo) error
|
||||
}); ok {
|
||||
return lca.Revoke(crt, rci)
|
||||
}
|
||||
return a.db.Revoke(rci)
|
||||
}
|
||||
|
||||
func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateInfo) error {
|
||||
if lca, ok := a.adminDB.(interface {
|
||||
RevokeSSH(*ssh.Certificate, *db.RevokedCertificateInfo) error
|
||||
}); ok {
|
||||
return lca.RevokeSSH(crt, rci)
|
||||
}
|
||||
return a.db.Revoke(rci)
|
||||
}
|
||||
|
||||
// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
|
||||
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
|
||||
fatal := func(err error) (*tls.Certificate, error) {
|
||||
|
|
13
ca/ca.go
13
ca/ca.go
|
@ -30,6 +30,7 @@ import (
|
|||
|
||||
type options struct {
|
||||
configFile string
|
||||
linkedCAToken string
|
||||
password []byte
|
||||
issuerPassword []byte
|
||||
database db.AuthDB
|
||||
|
@ -75,6 +76,13 @@ func WithDatabase(db db.AuthDB) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithLinkedCAToken sets the token used to authenticate with the linkedca.
|
||||
func WithLinkedCAToken(token string) Option {
|
||||
return func(o *options) {
|
||||
o.linkedCAToken = token
|
||||
}
|
||||
}
|
||||
|
||||
// CA is the type used to build the complete certificate authority. It builds
|
||||
// the HTTP server, set ups the middlewares and the HTTP handlers.
|
||||
type CA struct {
|
||||
|
@ -111,6 +119,10 @@ func (ca *CA) Init(config *config.Config) (*CA, error) {
|
|||
}
|
||||
|
||||
var opts []authority.Option
|
||||
if ca.opts.linkedCAToken != "" {
|
||||
opts = append(opts, authority.WithLinkedCAToken(ca.opts.linkedCAToken))
|
||||
}
|
||||
|
||||
if ca.opts.database != nil {
|
||||
opts = append(opts, authority.WithDatabase(ca.opts.database))
|
||||
}
|
||||
|
@ -326,6 +338,7 @@ func (ca *CA) Reload() error {
|
|||
newCA, err := New(config,
|
||||
WithPassword(ca.opts.password),
|
||||
WithIssuerPassword(ca.opts.issuerPassword),
|
||||
WithLinkedCAToken(ca.opts.linkedCAToken),
|
||||
WithConfigFile(ca.opts.configFile),
|
||||
WithDatabase(ca.auth.GetDatabase()),
|
||||
)
|
||||
|
|
7
ca/testdata/ca.json
vendored
7
ca/testdata/ca.json
vendored
|
@ -9,12 +9,11 @@
|
|||
"logger": {"format": "text"},
|
||||
"tls": {
|
||||
"minVersion": 1.2,
|
||||
"maxVersion": 1.2,
|
||||
"maxVersion": 1.3,
|
||||
"renegotiation": false,
|
||||
"cipherSuites": [
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
|
||||
]
|
||||
},
|
||||
"authority": {
|
||||
|
|
|
@ -38,10 +38,17 @@ type Options struct {
|
|||
CertificateChain []*x509.Certificate `json:"-"`
|
||||
Signer crypto.Signer `json:"-"`
|
||||
|
||||
// IsCreator is set to true when we're creating a certificate authority. Is
|
||||
// used to skip some validations when initializing a CertificateAuthority.
|
||||
// IsCreator is set to true when we're creating a certificate authority. It
|
||||
// is used to skip some validations when initializing a
|
||||
// CertificateAuthority. This option is used on SoftCAS and CloudCAS.
|
||||
IsCreator bool `json:"-"`
|
||||
|
||||
// IsCAGetter is set to true when we're just using the
|
||||
// CertificateAuthorityGetter interface to retrieve the root certificate. It
|
||||
// is used to skip some validations when initializing a
|
||||
// CertificateAuthority. This option is used on StepCAS.
|
||||
IsCAGetter bool `json:"-"`
|
||||
|
||||
// KeyManager is the KMS used to generate keys in SoftCAS.
|
||||
KeyManager kms.KeyManager `json:"-"`
|
||||
|
||||
|
|
|
@ -47,10 +47,13 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Create configured issuer
|
||||
iss, err := newStepIssuer(caURL, client, opts.CertificateIssuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var iss stepIssuer
|
||||
// Create configured issuer unless we only want to use GetCertificateAuthority.
|
||||
// This avoid the request for the password if not provided.
|
||||
if !opts.IsCAGetter {
|
||||
if iss, err = newStepIssuer(caURL, client, opts.CertificateIssuer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &StepCAS{
|
||||
|
|
|
@ -411,6 +411,19 @@ func TestNew(t *testing.T) {
|
|||
client: client,
|
||||
fingerprint: testRootFingerprint,
|
||||
}, false},
|
||||
{"ok ca getter", args{context.TODO(), apiv1.Options{
|
||||
IsCAGetter: true,
|
||||
CertificateAuthority: caURL.String(),
|
||||
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||
Type: "jwk",
|
||||
Provisioner: "ra@doe.org",
|
||||
},
|
||||
}}, &StepCAS{
|
||||
iss: nil,
|
||||
client: client,
|
||||
fingerprint: testRootFingerprint,
|
||||
}, false},
|
||||
{"fail authority", args{context.TODO(), apiv1.Options{
|
||||
CertificateAuthority: "",
|
||||
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||
|
|
|
@ -8,11 +8,13 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/pki"
|
||||
"github.com/urfave/cli"
|
||||
"go.step.sm/cli-utils/errs"
|
||||
)
|
||||
|
@ -38,6 +40,11 @@ certificate issuer private key used in the RA mode.`,
|
|||
Name: "resolver",
|
||||
Usage: "address of a DNS resolver to be used instead of the default.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "token",
|
||||
Usage: "token used to enable the linked ca.",
|
||||
EnvVar: "STEP_CA_TOKEN",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -46,6 +53,7 @@ func appAction(ctx *cli.Context) error {
|
|||
passFile := ctx.String("password-file")
|
||||
issuerPassFile := ctx.String("issuer-password-file")
|
||||
resolver := ctx.String("resolver")
|
||||
token := ctx.String("token")
|
||||
|
||||
// If zero cmd line args show help, if >1 cmd line args show error.
|
||||
if ctx.NArg() == 0 {
|
||||
|
@ -61,6 +69,18 @@ func appAction(ctx *cli.Context) error {
|
|||
fatal(err)
|
||||
}
|
||||
|
||||
if config.AuthorityConfig != nil {
|
||||
if token == "" && strings.EqualFold(config.AuthorityConfig.DeploymentType, pki.LinkedDeployment.String()) {
|
||||
return errors.New(`'step-ca' requires the '--token' flag for linked deploy type.
|
||||
|
||||
To get a linked authority token:
|
||||
1. Log in or create a Certificate Manager account at ` + "\033[1mhttps://u.step.sm/linked\033[0m" + `
|
||||
2. Add a new authority and select "Link a step-ca instance"
|
||||
3. Follow instructions in browser to start 'step-ca' using the '--token' flag
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
var password []byte
|
||||
if passFile != "" {
|
||||
if password, err = ioutil.ReadFile(passFile); err != nil {
|
||||
|
@ -88,7 +108,8 @@ func appAction(ctx *cli.Context) error {
|
|||
srv, err := ca.New(config,
|
||||
ca.WithConfigFile(configFile),
|
||||
ca.WithPassword(password),
|
||||
ca.WithIssuerPassword(issuerPassword))
|
||||
ca.WithIssuerPassword(issuerPassword),
|
||||
ca.WithLinkedCAToken(token))
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
|
113
commands/export.go
Normal file
113
commands/export.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"go.step.sm/cli-utils/command"
|
||||
"go.step.sm/cli-utils/errs"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command.Register(cli.Command{
|
||||
Name: "export",
|
||||
Usage: "export the current configuration of step-ca",
|
||||
UsageText: "**step-ca export** <config>",
|
||||
Action: exportAction,
|
||||
Description: `**step-ca export** exports the current configuration of step-ca.
|
||||
|
||||
Note that neither the PKI password nor the certificate issuer password will be
|
||||
included in the export file.
|
||||
|
||||
## POSITIONAL ARGUMENTS
|
||||
|
||||
<config>
|
||||
: The ca.json that contains the step-ca configuration.
|
||||
|
||||
## EXAMPLES
|
||||
|
||||
Export the current configuration:
|
||||
'''
|
||||
$ step-ca export $(step path)/config/ca.json
|
||||
'''`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "password-file",
|
||||
Usage: `path to the <file> containing the password to decrypt the
|
||||
intermediate private key.`,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "issuer-password-file",
|
||||
Usage: `path to the <file> containing the password to decrypt the
|
||||
certificate issuer private key used in the RA mode.`,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func exportAction(ctx *cli.Context) error {
|
||||
if err := errs.NumberOfArguments(ctx, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFile := ctx.Args().Get(0)
|
||||
passwordFile := ctx.String("password-file")
|
||||
issuerPasswordFile := ctx.String("issuer-password-file")
|
||||
|
||||
config, err := config.LoadConfiguration(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := config.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if passwordFile != "" {
|
||||
b, err := ioutil.ReadFile(passwordFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading %s", passwordFile)
|
||||
}
|
||||
config.Password = string(bytes.TrimRightFunc(b, unicode.IsSpace))
|
||||
}
|
||||
if issuerPasswordFile != "" {
|
||||
b, err := ioutil.ReadFile(issuerPasswordFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error reading %s", issuerPasswordFile)
|
||||
}
|
||||
if config.AuthorityConfig.CertificateIssuer != nil {
|
||||
config.AuthorityConfig.CertificateIssuer.Password = string(bytes.TrimRightFunc(b, unicode.IsSpace))
|
||||
}
|
||||
}
|
||||
|
||||
auth, err := authority.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
export, err := auth.Export()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := protojson.Marshal(export)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error marshaling export")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.Indent(&buf, b, "", "\t"); err != nil {
|
||||
return errors.Wrap(err, "error indenting export")
|
||||
}
|
||||
|
||||
fmt.Println(buf.String())
|
||||
return nil
|
||||
}
|
|
@ -163,17 +163,21 @@ func onboardAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
func onboardPKI(config onboardingConfiguration) (*config.Config, string, error) {
|
||||
var opts = []pki.Option{
|
||||
pki.WithAddress(config.Address),
|
||||
pki.WithDNSNames([]string{config.DNS}),
|
||||
pki.WithProvisioner("admin"),
|
||||
}
|
||||
|
||||
p, err := pki.New(apiv1.Options{
|
||||
Type: apiv1.SoftCAS,
|
||||
IsCreator: true,
|
||||
})
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
p.SetAddress(config.Address)
|
||||
p.SetDNSNames([]string{config.DNS})
|
||||
|
||||
// Generate pki
|
||||
ui.Println("Generating root certificate...")
|
||||
root, err := p.GenerateRootCertificate(config.Name, config.Name, config.Name, config.password)
|
||||
if err != nil {
|
||||
|
@ -186,8 +190,12 @@ func onboardPKI(config onboardingConfiguration) (*config.Config, string, error)
|
|||
return nil, "", err
|
||||
}
|
||||
|
||||
// Write files to disk
|
||||
if err = p.WriteFiles(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Generate provisioner
|
||||
p.SetProvisioner("admin")
|
||||
ui.Println("Generating admin provisioner...")
|
||||
if err = p.GenerateKeyPairs(config.password); err != nil {
|
||||
return nil, "", err
|
||||
|
|
20
go.mod
20
go.mod
|
@ -29,20 +29,20 @@ require (
|
|||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1
|
||||
go.step.sm/cli-utils v0.4.1
|
||||
go.step.sm/crypto v0.9.0
|
||||
go.step.sm/linkedca v0.0.0-20210611183751-27424aae8d25
|
||||
go.step.sm/linkedca v0.5.0
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420
|
||||
golang.org/x/net v0.0.0-20210716203947-853a461950ff
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
google.golang.org/api v0.47.0
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c
|
||||
google.golang.org/grpc v1.38.0
|
||||
google.golang.org/protobuf v1.26.0
|
||||
google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492
|
||||
google.golang.org/grpc v1.39.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
)
|
||||
|
||||
//replace github.com/smallstep/nosql => ../nosql
|
||||
|
||||
//replace go.step.sm/crypto => ../crypto
|
||||
|
||||
//replace go.step.sm/cli-utils => ../cli-utils
|
||||
// replace github.com/smallstep/nosql => ../nosql
|
||||
// replace go.step.sm/crypto => ../crypto
|
||||
// replace go.step.sm/cli-utils => ../cli-utils
|
||||
// replace go.step.sm/linkedca => ../linkedca
|
||||
|
||||
replace go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 => github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568
|
||||
|
|
29
go.sum
29
go.sum
|
@ -71,6 +71,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
|||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
|
@ -113,6 +114,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
|
@ -155,6 +157,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
|
@ -274,6 +277,7 @@ github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd
|
|||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
@ -435,6 +439,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
|
|||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
|
@ -516,12 +521,13 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.step.sm/cli-utils v0.4.1 h1:QztRUhGYjOPM1I2Nmi7V6XejQyVtcESmo+sbegxvX7Q=
|
||||
go.step.sm/cli-utils v0.4.1/go.mod h1:hWYVOSlw8W9Pd+BwIbs/aftVVMRms3EG7Q2qLRwc0WA=
|
||||
go.step.sm/crypto v0.9.0 h1:q2AllTSnVj4NRtyEPkGW2ohArLmbGbe6ZAL/VIOKDzA=
|
||||
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
|
||||
go.step.sm/linkedca v0.0.0-20210611183751-27424aae8d25 h1:ncJqviWswJT19IdnfOYQGKG1zL7IDy4lAJz1PuM3fgw=
|
||||
go.step.sm/linkedca v0.0.0-20210611183751-27424aae8d25/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
|
||||
go.step.sm/linkedca v0.5.0 h1:oZVRSpElM7lAL1XN2YkjdHwI/oIZ+1ULOnuqYPM6xjY=
|
||||
go.step.sm/linkedca v0.5.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
|
@ -620,8 +626,9 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210716203947-853a461950ff h1:j2EK/QoxYNBsXI4R7fQkkRUk8y6wnOBI+6hgPdP/6Ds=
|
||||
golang.org/x/net v0.0.0-20210716203947-853a461950ff/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -702,8 +709,9 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -772,6 +780,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
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=
|
||||
|
@ -834,6 +843,7 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
|
|||
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-20200513103714-09dca8ec2884/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=
|
||||
|
@ -851,8 +861,9 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D
|
|||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 h1:7yQQsvnwjfEahbNNEKcBHv3mR+HnB1ctGY/z1JXzx8M=
|
||||
google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
|
@ -871,6 +882,7 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
|
|||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
|
@ -878,8 +890,9 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
|
|||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI=
|
||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
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=
|
||||
|
@ -892,8 +905,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
|||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
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=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -911,6 +925,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
|
|||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
|
|
154
pki/helm.go
Normal file
154
pki/helm.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"io"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
authconfig "github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
type helmVariables struct {
|
||||
*linkedca.Configuration
|
||||
Defaults *linkedca.Defaults
|
||||
Password string
|
||||
SSH struct {
|
||||
Enabled bool
|
||||
}
|
||||
TLS authconfig.TLSOptions
|
||||
Provisioners []provisioner.Interface
|
||||
}
|
||||
|
||||
func (p *PKI) WriteHelmTemplate(w io.Writer) error {
|
||||
tmpl, err := template.New("helm").Funcs(sprig.TxtFuncMap()).Parse(helmTemplate)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error writing helm template")
|
||||
}
|
||||
|
||||
// Delete ssh section if it is not enabled
|
||||
if !p.options.enableSSH {
|
||||
p.Ssh = nil
|
||||
}
|
||||
|
||||
// Convert provisioner to ca.json
|
||||
provisioners := make([]provisioner.Interface, len(p.Authority.Provisioners))
|
||||
for i, p := range p.Authority.Provisioners {
|
||||
pp, err := authority.ProvisionerToCertificates(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provisioners[i] = pp
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, helmVariables{
|
||||
Configuration: &p.Configuration,
|
||||
Defaults: &p.Defaults,
|
||||
Password: "",
|
||||
TLS: authconfig.DefaultTLSOptions,
|
||||
Provisioners: provisioners,
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "error executing helm template")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const helmTemplate = `# Helm template
|
||||
inject:
|
||||
enabled: true
|
||||
# Config contains the configuration files ca.json and defaults.json
|
||||
config:
|
||||
files:
|
||||
ca.json:
|
||||
root: {{ first .Root }}
|
||||
federateRoots: []
|
||||
crt: {{ .Intermediate }}
|
||||
key: {{ .IntermediateKey }}
|
||||
{{- if .SSH.Enabled }}
|
||||
ssh:
|
||||
hostKey: {{ .Ssh.HostKey }}
|
||||
userKey: {{ .Ssh.UserKey }}
|
||||
{{- end }}
|
||||
address: {{ .Address }}
|
||||
dnsNames:
|
||||
{{- range .DnsNames }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
logger:
|
||||
format: json
|
||||
db:
|
||||
type: badger
|
||||
dataSource: /home/step/db
|
||||
authority:
|
||||
provisioners:
|
||||
{{- range .Provisioners }}
|
||||
- {{ . | toJson }}
|
||||
{{- end }}
|
||||
tls:
|
||||
cipherSuites:
|
||||
{{- range .TLS.CipherSuites }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
minVersion: {{ .TLS.MinVersion }}
|
||||
maxVersion: {{ .TLS.MaxVersion }}
|
||||
renegotiation: {{ .TLS.Renegotiation }}
|
||||
|
||||
defaults.json:
|
||||
ca-url: {{ .Defaults.CaUrl }}
|
||||
ca-config: {{ .Defaults.CaConfig }}
|
||||
fingerprint: {{ .Defaults.Fingerprint }}
|
||||
root: {{ .Defaults.Root }}
|
||||
|
||||
# Certificates contains the root and intermediate certificate and
|
||||
# optionally the SSH host and user public keys
|
||||
certificates:
|
||||
# intermediate_ca contains the text of the intermediate CA Certificate
|
||||
intermediate_ca: |
|
||||
{{- index .Files .Intermediate | toString | nindent 6 }}
|
||||
|
||||
# root_ca contains the text of the root CA Certificate
|
||||
root_ca: |
|
||||
{{- first .Root | index .Files | toString | nindent 6 }}
|
||||
|
||||
{{- if .Ssh }}
|
||||
# ssh_host_ca contains the text of the public ssh key for the SSH root CA
|
||||
ssh_host_ca: {{ index .Files .Ssh.HostPublicKey | toString }}
|
||||
|
||||
# ssh_user_ca contains the text of the public ssh key for the SSH root CA
|
||||
ssh_user_ca: {{ index .Files .Ssh.UserPublicKey | toString }}
|
||||
{{- end }}
|
||||
|
||||
# Secrets contains the root and intermediate keys and optionally the SSH
|
||||
# private keys
|
||||
secrets:
|
||||
# ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key
|
||||
# This value must be base64 encoded.
|
||||
ca_password: {{ .Password | b64enc }}
|
||||
provisioner_password: {{ .Password | b64enc}}
|
||||
|
||||
x509:
|
||||
# intermediate_ca_key contains the contents of your encrypted intermediate CA key
|
||||
intermediate_ca_key: |
|
||||
{{- index .Files .IntermediateKey | toString | nindent 8 }}
|
||||
|
||||
# root_ca_key contains the contents of your encrypted root CA key
|
||||
# Note that this value can be omitted without impacting the functionality of step-certificates
|
||||
# If supplied, this should be encrypted using a unique password that is not used for encrypting
|
||||
# the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.
|
||||
root_ca_key: |
|
||||
{{- first .RootKey | index .Files | toString | nindent 8 }}
|
||||
|
||||
{{- if .Ssh }}
|
||||
ssh:
|
||||
# ssh_host_ca_key contains the contents of your encrypted SSH Host CA key
|
||||
host_ca_key: |
|
||||
{{- index .Files .Ssh.HostKey | toString | nindent 8 }}
|
||||
|
||||
# ssh_user_ca_key contains the contents of your encrypted SSH User CA key
|
||||
user_ca_key: |
|
||||
{{- index .Files .Ssh.UserKey | toString | nindent 8 }}
|
||||
{{- end }}
|
||||
`
|
775
pki/pki.go
775
pki/pki.go
|
@ -10,21 +10,23 @@ import (
|
|||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"html"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
admindb "github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||
authconfig "github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/cas"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
"go.step.sm/cli-utils/config"
|
||||
"go.step.sm/cli-utils/errs"
|
||||
"go.step.sm/cli-utils/fileutil"
|
||||
|
@ -32,9 +34,40 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/linkedca"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// DeploymentType defines witch type of deployment a user is initializing
|
||||
type DeploymentType int
|
||||
|
||||
const (
|
||||
// StandaloneDeployment is a deployment where all the components like keys,
|
||||
// provisioners, admins, certificates and others are managed by the user.
|
||||
StandaloneDeployment DeploymentType = iota
|
||||
// LinkedDeployment is a deployment where the keys are managed by the user,
|
||||
// but provisioners, admins and the record of certificates are managed in
|
||||
// the cloud.
|
||||
LinkedDeployment
|
||||
// HostedDeployment is a deployment where all the components are managed in
|
||||
// the cloud by smallstep.com/certificate-manager.
|
||||
HostedDeployment
|
||||
)
|
||||
|
||||
// String returns the string version of the deployment type.
|
||||
func (d DeploymentType) String() string {
|
||||
switch d {
|
||||
case StandaloneDeployment:
|
||||
return "standalone"
|
||||
case LinkedDeployment:
|
||||
return "linked"
|
||||
case HostedDeployment:
|
||||
return "hosted"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// ConfigPath is the directory name under the step path where the configuration
|
||||
// files will be stored.
|
||||
|
@ -134,43 +167,125 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) {
|
|||
return resp.Key, nil
|
||||
}
|
||||
|
||||
type options struct {
|
||||
provisioner string
|
||||
pkiOnly bool
|
||||
enableACME bool
|
||||
enableSSH bool
|
||||
enableAdmin bool
|
||||
noDB bool
|
||||
isHelm bool
|
||||
deploymentType DeploymentType
|
||||
}
|
||||
|
||||
// Option is the type of a configuration option on the pki constructor.
|
||||
type Option func(p *PKI)
|
||||
|
||||
// WithAddress sets the listen address of step-ca.
|
||||
func WithAddress(s string) Option {
|
||||
return func(p *PKI) {
|
||||
p.Address = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithCaURL sets the default ca-url of step-ca.
|
||||
func WithCaURL(s string) Option {
|
||||
return func(p *PKI) {
|
||||
p.Defaults.CaUrl = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithDNSNames sets the SANs of step-ca.
|
||||
func WithDNSNames(s []string) Option {
|
||||
return func(p *PKI) {
|
||||
p.DnsNames = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithProvisioner defines the name of the default provisioner.
|
||||
func WithProvisioner(s string) Option {
|
||||
return func(p *PKI) {
|
||||
p.options.provisioner = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithPKIOnly will only generate the PKI without the step-ca config files.
|
||||
func WithPKIOnly() Option {
|
||||
return func(p *PKI) {
|
||||
p.options.pkiOnly = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithACME enables acme provisioner in step-ca.
|
||||
func WithACME() Option {
|
||||
return func(p *PKI) {
|
||||
p.options.enableACME = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithSSH enables ssh in step-ca.
|
||||
func WithSSH() Option {
|
||||
return func(p *PKI) {
|
||||
p.options.enableSSH = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdmin enables the admin api in step-ca.
|
||||
func WithAdmin() Option {
|
||||
return func(p *PKI) {
|
||||
p.options.enableAdmin = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoDB disables the db in step-ca.
|
||||
func WithNoDB() Option {
|
||||
return func(p *PKI) {
|
||||
p.options.noDB = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithHelm configures the pki to create a helm values.yaml.
|
||||
func WithHelm() Option {
|
||||
return func(p *PKI) {
|
||||
p.options.isHelm = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeploymentType defines the deployment type of step-ca.
|
||||
func WithDeploymentType(dt DeploymentType) Option {
|
||||
return func(p *PKI) {
|
||||
p.options.deploymentType = dt
|
||||
}
|
||||
}
|
||||
|
||||
// PKI represents the Public Key Infrastructure used by a certificate authority.
|
||||
type PKI struct {
|
||||
casOptions apiv1.Options
|
||||
caCreator apiv1.CertificateAuthorityCreator
|
||||
root, rootKey, rootFingerprint string
|
||||
intermediate, intermediateKey string
|
||||
sshHostPubKey, sshHostKey string
|
||||
sshUserPubKey, sshUserKey string
|
||||
config, defaults string
|
||||
ottPublicKey *jose.JSONWebKey
|
||||
ottPrivateKey *jose.JSONWebEncryption
|
||||
provisioner string
|
||||
address string
|
||||
dnsNames []string
|
||||
caURL string
|
||||
enableSSH bool
|
||||
linkedca.Configuration
|
||||
Defaults linkedca.Defaults
|
||||
casOptions apiv1.Options
|
||||
caService apiv1.CertificateAuthorityService
|
||||
caCreator apiv1.CertificateAuthorityCreator
|
||||
config string
|
||||
defaults string
|
||||
ottPublicKey *jose.JSONWebKey
|
||||
ottPrivateKey *jose.JSONWebEncryption
|
||||
options *options
|
||||
}
|
||||
|
||||
// New creates a new PKI configuration.
|
||||
func New(opts apiv1.Options) (*PKI, error) {
|
||||
caCreator, err := cas.NewCreator(context.Background(), opts)
|
||||
func New(o apiv1.Options, opts ...Option) (*PKI, error) {
|
||||
caService, err := cas.New(context.Background(), o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
public := GetPublicPath()
|
||||
private := GetSecretsPath()
|
||||
config := GetConfigPath()
|
||||
|
||||
// Create directories
|
||||
dirs := []string{public, private, config, GetTemplatesPath()}
|
||||
for _, name := range dirs {
|
||||
if _, err := os.Stat(name); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(name, 0700); err != nil {
|
||||
return nil, errs.FileError(err, name)
|
||||
}
|
||||
var caCreator apiv1.CertificateAuthorityCreator
|
||||
if o.IsCreator {
|
||||
creator, ok := caService.(apiv1.CertificateAuthorityCreator)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("cas type '%s' does not implements CertificateAuthorityCreator", o.Type)
|
||||
}
|
||||
caCreator = creator
|
||||
}
|
||||
|
||||
// get absolute path for dir/name
|
||||
|
@ -180,45 +295,97 @@ func New(opts apiv1.Options) (*PKI, error) {
|
|||
}
|
||||
|
||||
p := &PKI{
|
||||
casOptions: opts,
|
||||
caCreator: caCreator,
|
||||
provisioner: "step-cli",
|
||||
address: "127.0.0.1:9000",
|
||||
dnsNames: []string{"127.0.0.1"},
|
||||
Configuration: linkedca.Configuration{
|
||||
Address: "127.0.0.1:9000",
|
||||
DnsNames: []string{"127.0.0.1"},
|
||||
Ssh: &linkedca.SSH{},
|
||||
Authority: &linkedca.Authority{},
|
||||
Files: make(map[string][]byte),
|
||||
},
|
||||
casOptions: o,
|
||||
caCreator: caCreator,
|
||||
caService: caService,
|
||||
options: &options{
|
||||
provisioner: "step-cli",
|
||||
},
|
||||
}
|
||||
if p.root, err = getPath(public, "root_ca.crt"); err != nil {
|
||||
return nil, err
|
||||
for _, fn := range opts {
|
||||
fn(p)
|
||||
}
|
||||
if p.rootKey, err = getPath(private, "root_ca_key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.intermediateKey, err = getPath(private, "intermediate_ca_key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.sshHostPubKey, err = getPath(public, "ssh_host_ca_key.pub"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.sshUserPubKey, err = getPath(public, "ssh_user_ca_key.pub"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.sshHostKey, err = getPath(private, "ssh_host_ca_key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.sshUserKey, err = getPath(private, "ssh_user_ca_key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(config) > 0 {
|
||||
if p.config, err = getPath(config, "ca.json"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.defaults, err = getPath(config, "defaults.json"); err != nil {
|
||||
return nil, err
|
||||
|
||||
// Use /home/step as the step path in helm configurations.
|
||||
// Use the current step path when creating pki in files.
|
||||
var public, private, config string
|
||||
if p.options.isHelm {
|
||||
public = "/home/step/certs"
|
||||
private = "/home/step/secrets"
|
||||
config = "/home/step/config"
|
||||
} else {
|
||||
public = GetPublicPath()
|
||||
private = GetSecretsPath()
|
||||
config = GetConfigPath()
|
||||
// Create directories
|
||||
dirs := []string{public, private, config, GetTemplatesPath()}
|
||||
for _, name := range dirs {
|
||||
if _, err := os.Stat(name); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(name, 0700); err != nil {
|
||||
return nil, errs.FileError(err, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.Defaults.CaUrl == "" {
|
||||
p.Defaults.CaUrl = p.DnsNames[0]
|
||||
_, port, err := net.SplitHostPort(p.Address)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing %s", p.Address)
|
||||
}
|
||||
if port == "443" {
|
||||
p.Defaults.CaUrl = fmt.Sprintf("https://%s", p.Defaults.CaUrl)
|
||||
} else {
|
||||
p.Defaults.CaUrl = fmt.Sprintf("https://%s:%s", p.Defaults.CaUrl, port)
|
||||
}
|
||||
}
|
||||
|
||||
root, err := getPath(public, "root_ca.crt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rootKey, err := getPath(private, "root_ca_key")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Root = []string{root}
|
||||
p.RootKey = []string{rootKey}
|
||||
p.Defaults.Root = root
|
||||
|
||||
if p.Intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.IntermediateKey, err = getPath(private, "intermediate_ca_key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.Ssh.HostPublicKey, err = getPath(public, "ssh_host_ca_key.pub"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.Ssh.UserPublicKey, err = getPath(public, "ssh_user_ca_key.pub"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.Ssh.HostKey, err = getPath(private, "ssh_host_ca_key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.Ssh.UserKey, err = getPath(private, "ssh_user_ca_key"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.defaults, err = getPath(config, "defaults.json"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.config, err = getPath(config, "ca.json"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Defaults.CaConfig = p.config
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
|
@ -229,27 +396,7 @@ func (p *PKI) GetCAConfigPath() string {
|
|||
|
||||
// GetRootFingerprint returns the root fingerprint.
|
||||
func (p *PKI) GetRootFingerprint() string {
|
||||
return p.rootFingerprint
|
||||
}
|
||||
|
||||
// SetProvisioner sets the provisioner name of the OTT keys.
|
||||
func (p *PKI) SetProvisioner(s string) {
|
||||
p.provisioner = s
|
||||
}
|
||||
|
||||
// SetAddress sets the listening address of the CA.
|
||||
func (p *PKI) SetAddress(s string) {
|
||||
p.address = s
|
||||
}
|
||||
|
||||
// SetDNSNames sets the dns names of the CA.
|
||||
func (p *PKI) SetDNSNames(s []string) {
|
||||
p.dnsNames = s
|
||||
}
|
||||
|
||||
// SetCAURL sets the ca-url to use in the defaults.json.
|
||||
func (p *PKI) SetCAURL(s string) {
|
||||
p.caURL = s
|
||||
return p.Defaults.Fingerprint
|
||||
}
|
||||
|
||||
// GenerateKeyPairs generates the key pairs used by the certificate authority.
|
||||
|
@ -261,6 +408,28 @@ func (p *PKI) GenerateKeyPairs(pass []byte) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Add JWK provisioner to the configuration.
|
||||
publicKey, err := json.Marshal(p.ottPublicKey)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error marshaling public key")
|
||||
}
|
||||
encryptedKey, err := p.ottPrivateKey.CompactSerialize()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error serializing private key")
|
||||
}
|
||||
p.Authority.Provisioners = append(p.Authority.Provisioners, &linkedca.Provisioner{
|
||||
Type: linkedca.Provisioner_JWK,
|
||||
Name: p.options.provisioner,
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_JWK{
|
||||
JWK: &linkedca.JWKProvisioner{
|
||||
PublicKey: publicKey,
|
||||
EncryptedPrivateKey: []byte(encryptedKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -296,6 +465,21 @@ func (p *PKI) GenerateRootCertificate(name, org, resource string, pass []byte) (
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// WriteRootCertificate writes to the buffer the given certificate and key if given.
|
||||
func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error {
|
||||
p.Files[p.Root[0]] = encodeCertificate(rootCrt)
|
||||
if rootKey != nil {
|
||||
var err error
|
||||
p.Files[p.RootKey[0]], err = encodePrivateKey(rootKey, pass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sum := sha256.Sum256(rootCrt.Raw)
|
||||
p.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:]))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateIntermediateCertificate generates an intermediate certificate with
|
||||
// the given name and using the default key type.
|
||||
func (p *PKI) GenerateIntermediateCertificate(name, org, resource string, parent *apiv1.CreateCertificateAuthorityResponse, pass []byte) error {
|
||||
|
@ -322,46 +506,9 @@ func (p *PKI) GenerateIntermediateCertificate(name, org, resource string, parent
|
|||
}
|
||||
|
||||
p.casOptions.CertificateAuthority = resp.Name
|
||||
return p.WriteIntermediateCertificate(resp.Certificate, resp.PrivateKey, pass)
|
||||
}
|
||||
|
||||
// WriteRootCertificate writes to disk the given certificate and key.
|
||||
func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error {
|
||||
if err := fileutil.WriteFile(p.root, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: rootCrt.Raw,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rootKey != nil {
|
||||
_, err := pemutil.Serialize(rootKey, pemutil.WithPassword(pass), pemutil.ToFile(p.rootKey, 0600))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(rootCrt.Raw)
|
||||
p.rootFingerprint = strings.ToLower(hex.EncodeToString(sum[:]))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteIntermediateCertificate writes to disk the given certificate and key.
|
||||
func (p *PKI) WriteIntermediateCertificate(crt *x509.Certificate, key interface{}, pass []byte) error {
|
||||
if err := fileutil.WriteFile(p.intermediate, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: crt.Raw,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if key != nil {
|
||||
_, err := pemutil.Serialize(key, pemutil.WithPassword(pass), pemutil.ToFile(p.intermediateKey, 0600))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
p.Files[p.Intermediate] = encodeCertificate(resp.Certificate)
|
||||
p.Files[p.IntermediateKey], err = encodePrivateKey(resp.PrivateKey, pass)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateCertificateAuthorityResponse returns a
|
||||
|
@ -379,7 +526,7 @@ func (p *PKI) CreateCertificateAuthorityResponse(cert *x509.Certificate, key cry
|
|||
// GetCertificateAuthority attempts to load the certificate authority from the
|
||||
// RA.
|
||||
func (p *PKI) GetCertificateAuthority() error {
|
||||
srv, ok := p.caCreator.(apiv1.CertificateAuthorityGetter)
|
||||
srv, ok := p.caService.(apiv1.CertificateAuthorityGetter)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
@ -396,8 +543,8 @@ func (p *PKI) GetCertificateAuthority() error {
|
|||
}
|
||||
|
||||
// Issuer is in the RA
|
||||
p.intermediate = ""
|
||||
p.intermediateKey = ""
|
||||
p.Intermediate = ""
|
||||
p.IntermediateKey = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -405,8 +552,8 @@ func (p *PKI) GetCertificateAuthority() error {
|
|||
// GenerateSSHSigningKeys generates and encrypts a private key used for signing
|
||||
// SSH user certificates and a private key used for signing host certificates.
|
||||
func (p *PKI) GenerateSSHSigningKeys(password []byte) error {
|
||||
var pubNames = []string{p.sshHostPubKey, p.sshUserPubKey}
|
||||
var privNames = []string{p.sshHostKey, p.sshUserKey}
|
||||
var pubNames = []string{p.Ssh.HostPublicKey, p.Ssh.UserPublicKey}
|
||||
var privNames = []string{p.Ssh.HostKey, p.Ssh.UserKey}
|
||||
for i := 0; i < 2; i++ {
|
||||
pub, priv, err := keyutil.GenerateDefaultKeyPair()
|
||||
if err != nil {
|
||||
|
@ -419,57 +566,65 @@ func (p *PKI) GenerateSSHSigningKeys(password []byte) error {
|
|||
if err != nil {
|
||||
return errors.Wrapf(err, "error converting public key")
|
||||
}
|
||||
_, err = pemutil.Serialize(priv, pemutil.WithFilename(privNames[i]), pemutil.WithPassword(password))
|
||||
p.Files[pubNames[i]] = ssh.MarshalAuthorizedKey(sshKey)
|
||||
p.Files[privNames[i]], err = encodePrivateKey(priv, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = fileutil.WriteFile(pubNames[i], ssh.MarshalAuthorizedKey(sshKey), 0600); err != nil {
|
||||
}
|
||||
p.options.enableSSH = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteFiles writes on disk the previously generated files.
|
||||
func (p *PKI) WriteFiles() error {
|
||||
for fn, b := range p.Files {
|
||||
if err := fileutil.WriteFile(fn, b, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.enableSSH = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PKI) askFeedback() {
|
||||
ui.Println()
|
||||
ui.Printf("\033[1mFEEDBACK\033[0m %s %s\n",
|
||||
html.UnescapeString("&#"+strconv.Itoa(128525)+";"),
|
||||
html.UnescapeString("&#"+strconv.Itoa(127867)+";"))
|
||||
ui.Println(" The \033[1mstep\033[0m utility is not instrumented for usage statistics. It does not")
|
||||
ui.Println(" phone home. But your feedback is extremely valuable. Any information you")
|
||||
ui.Println(" can provide regarding how you’re using `step` helps. Please send us a")
|
||||
ui.Println(" sentence or two, good or bad: \033[1mfeedback@smallstep.com\033[0m or join")
|
||||
ui.Println(" \033[1mhttps://github.com/smallstep/certificates/discussions\033[0m.")
|
||||
}
|
||||
ui.Println("\033[1mFEEDBACK\033[0m 😍 🍻")
|
||||
ui.Println(" The \033[1mstep\033[0m utility is not instrumented for usage statistics. It does not phone")
|
||||
ui.Println(" home. But your feedback is extremely valuable. Any information you can provide")
|
||||
ui.Println(" regarding how you’re using `step` helps. Please send us a sentence or two,")
|
||||
ui.Println(" good or bad at \033[1mfeedback@smallstep.com\033[0m or join GitHub Discussions")
|
||||
ui.Println(" \033[1mhttps://github.com/smallstep/certificates/discussions\033[0m and our Discord ")
|
||||
ui.Println(" \033[1mhttps://u.step.sm/discord\033[0m.")
|
||||
|
||||
// TellPKI outputs the locations of public and private keys generated
|
||||
// generated for a new PKI. Generally this will consist of a root certificate
|
||||
// and key and an intermediate certificate and key.
|
||||
func (p *PKI) TellPKI() {
|
||||
p.tellPKI()
|
||||
p.askFeedback()
|
||||
if p.options.deploymentType == LinkedDeployment {
|
||||
ui.Println()
|
||||
ui.Println("\033[1mNEXT STEPS\033[0m")
|
||||
ui.Println(" 1. Log in or create a Certificate Manager account at \033[1mhttps://u.step.sm/linked\033[0m")
|
||||
ui.Println(" 2. Add a new authority and select \"Link a step-ca instance\"")
|
||||
ui.Println(" 3. Follow instructions in browser to start `step-ca` using the `--token` flag")
|
||||
ui.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PKI) tellPKI() {
|
||||
ui.Println()
|
||||
if p.casOptions.Is(apiv1.SoftCAS) {
|
||||
ui.PrintSelected("Root certificate", p.root)
|
||||
ui.PrintSelected("Root private key", p.rootKey)
|
||||
ui.PrintSelected("Root fingerprint", p.rootFingerprint)
|
||||
ui.PrintSelected("Intermediate certificate", p.intermediate)
|
||||
ui.PrintSelected("Intermediate private key", p.intermediateKey)
|
||||
} else if p.rootFingerprint != "" {
|
||||
ui.PrintSelected("Root certificate", p.root)
|
||||
ui.PrintSelected("Root fingerprint", p.rootFingerprint)
|
||||
ui.PrintSelected("Root certificate", p.Root[0])
|
||||
ui.PrintSelected("Root private key", p.RootKey[0])
|
||||
ui.PrintSelected("Root fingerprint", p.Defaults.Fingerprint)
|
||||
ui.PrintSelected("Intermediate certificate", p.Intermediate)
|
||||
ui.PrintSelected("Intermediate private key", p.IntermediateKey)
|
||||
} else if p.Defaults.Fingerprint != "" {
|
||||
ui.PrintSelected("Root certificate", p.Root[0])
|
||||
ui.PrintSelected("Root fingerprint", p.Defaults.Fingerprint)
|
||||
} else {
|
||||
ui.Printf(`{{ "%s" | red }} {{ "Root certificate:" | bold }} failed to retrieve it from RA`+"\n", ui.IconBad)
|
||||
}
|
||||
if p.enableSSH {
|
||||
ui.PrintSelected("SSH user root certificate", p.sshUserPubKey)
|
||||
ui.PrintSelected("SSH user root private key", p.sshUserKey)
|
||||
ui.PrintSelected("SSH host root certificate", p.sshHostPubKey)
|
||||
ui.PrintSelected("SSH host root private key", p.sshHostKey)
|
||||
if p.options.enableSSH {
|
||||
ui.PrintSelected("SSH user public key", p.Ssh.UserPublicKey)
|
||||
ui.PrintSelected("SSH user private key", p.Ssh.UserKey)
|
||||
ui.PrintSelected("SSH host public key", p.Ssh.HostPublicKey)
|
||||
ui.PrintSelected("SSH host private key", p.Ssh.HostKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -480,176 +635,230 @@ type caDefaults struct {
|
|||
Root string `json:"root"`
|
||||
}
|
||||
|
||||
// Option is the type for modifiers over the auth config object.
|
||||
type Option func(c *authconfig.Config) error
|
||||
|
||||
// WithDefaultDB is a configuration modifier that adds a default DB stanza to
|
||||
// the authority config.
|
||||
func WithDefaultDB() Option {
|
||||
return func(c *authconfig.Config) error {
|
||||
c.DB = &db.Config{
|
||||
Type: "badger",
|
||||
DataSource: GetDBPath(),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutDB is a configuration modifier that adds a default DB stanza to
|
||||
// the authority config.
|
||||
func WithoutDB() Option {
|
||||
return func(c *authconfig.Config) error {
|
||||
c.DB = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// ConfigOption is the type for modifiers over the auth config object.
|
||||
type ConfigOption func(c *authconfig.Config) error
|
||||
|
||||
// GenerateConfig returns the step certificates configuration.
|
||||
func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
|
||||
key, err := p.ottPrivateKey.CompactSerialize()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error serializing private key")
|
||||
}
|
||||
|
||||
prov := &provisioner.JWK{
|
||||
Name: p.provisioner,
|
||||
Type: "JWK",
|
||||
Key: p.ottPublicKey,
|
||||
EncryptedKey: key,
|
||||
}
|
||||
|
||||
func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {
|
||||
var authorityOptions *apiv1.Options
|
||||
if !p.casOptions.Is(apiv1.SoftCAS) {
|
||||
authorityOptions = &p.casOptions
|
||||
}
|
||||
|
||||
config := &authconfig.Config{
|
||||
Root: []string{p.root},
|
||||
FederatedRoots: []string{},
|
||||
IntermediateCert: p.intermediate,
|
||||
IntermediateKey: p.intermediateKey,
|
||||
Address: p.address,
|
||||
DNSNames: p.dnsNames,
|
||||
Root: p.Root,
|
||||
FederatedRoots: p.FederatedRoots,
|
||||
IntermediateCert: p.Intermediate,
|
||||
IntermediateKey: p.IntermediateKey,
|
||||
Address: p.Address,
|
||||
DNSNames: p.DnsNames,
|
||||
Logger: []byte(`{"format": "text"}`),
|
||||
DB: &db.Config{
|
||||
Type: "badger",
|
||||
Type: "badgerv2",
|
||||
DataSource: GetDBPath(),
|
||||
},
|
||||
AuthorityConfig: &authconfig.AuthConfig{
|
||||
Options: authorityOptions,
|
||||
DisableIssuedAtCheck: false,
|
||||
Provisioners: provisioner.List{prov},
|
||||
},
|
||||
TLS: &authconfig.TLSOptions{
|
||||
MinVersion: authconfig.DefaultTLSMinVersion,
|
||||
MaxVersion: authconfig.DefaultTLSMaxVersion,
|
||||
Renegotiation: authconfig.DefaultTLSRenegotiation,
|
||||
CipherSuites: authconfig.DefaultTLSCipherSuites,
|
||||
EnableAdmin: false,
|
||||
},
|
||||
TLS: &authconfig.DefaultTLSOptions,
|
||||
Templates: p.getTemplates(),
|
||||
}
|
||||
if p.enableSSH {
|
||||
enableSSHCA := true
|
||||
config.SSH = &authconfig.SSHConfig{
|
||||
HostKey: p.sshHostKey,
|
||||
UserKey: p.sshUserKey,
|
||||
|
||||
// Add linked as a deployment type to detect it on start and provide a
|
||||
// message if the token is not given.
|
||||
if p.options.deploymentType == LinkedDeployment {
|
||||
config.AuthorityConfig.DeploymentType = LinkedDeployment.String()
|
||||
}
|
||||
|
||||
// On standalone deployments add the provisioners to either the ca.json or
|
||||
// the database.
|
||||
var provisioners []provisioner.Interface
|
||||
if p.options.deploymentType == StandaloneDeployment {
|
||||
key, err := p.ottPrivateKey.CompactSerialize()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error serializing private key")
|
||||
}
|
||||
// Enable SSH authorization for default JWK provisioner
|
||||
prov.Claims = &provisioner.Claims{
|
||||
EnableSSHCA: &enableSSHCA,
|
||||
|
||||
prov := &provisioner.JWK{
|
||||
Name: p.options.provisioner,
|
||||
Type: "JWK",
|
||||
Key: p.ottPublicKey,
|
||||
EncryptedKey: key,
|
||||
}
|
||||
// Add default SSHPOP provisioner
|
||||
sshpop := &provisioner.SSHPOP{
|
||||
Type: "SSHPOP",
|
||||
Name: "sshpop",
|
||||
Claims: &provisioner.Claims{
|
||||
provisioners = append(provisioners, prov)
|
||||
|
||||
// Add default ACME provisioner if enabled
|
||||
if p.options.enableACME {
|
||||
provisioners = append(provisioners, &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "acme",
|
||||
})
|
||||
}
|
||||
|
||||
if p.options.enableSSH {
|
||||
enableSSHCA := true
|
||||
config.SSH = &authconfig.SSHConfig{
|
||||
HostKey: p.Ssh.HostKey,
|
||||
UserKey: p.Ssh.UserKey,
|
||||
}
|
||||
// Enable SSH authorization for default JWK provisioner
|
||||
prov.Claims = &provisioner.Claims{
|
||||
EnableSSHCA: &enableSSHCA,
|
||||
},
|
||||
}
|
||||
|
||||
// Add default SSHPOP provisioner
|
||||
provisioners = append(provisioners, &provisioner.SSHPOP{
|
||||
Type: "SSHPOP",
|
||||
Name: "sshpop",
|
||||
Claims: &provisioner.Claims{
|
||||
EnableSSHCA: &enableSSHCA,
|
||||
},
|
||||
})
|
||||
}
|
||||
config.AuthorityConfig.Provisioners = append(config.AuthorityConfig.Provisioners, sshpop)
|
||||
}
|
||||
|
||||
// Apply configuration modifiers
|
||||
for _, o := range opt {
|
||||
if err = o(config); err != nil {
|
||||
if err := o(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Set authority.enableAdmin to true
|
||||
if p.options.enableAdmin {
|
||||
config.AuthorityConfig.EnableAdmin = true
|
||||
}
|
||||
|
||||
if p.options.deploymentType == StandaloneDeployment {
|
||||
if !config.AuthorityConfig.EnableAdmin {
|
||||
config.AuthorityConfig.Provisioners = provisioners
|
||||
} else {
|
||||
// At this moment this code path is never used because `step ca
|
||||
// init` will always set enableAdmin to false for a standalone
|
||||
// deployment. Once we move `step beta` commands out of the beta we
|
||||
// should probably default to this route.
|
||||
//
|
||||
// Note that we might want to be able to define the database as a
|
||||
// flag in `step ca init` so we can write to the proper place.
|
||||
db, err := db.New(config.DB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminDB, err := admindb.New(db.(nosql.DB), admin.DefaultAuthorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Add all the provisioners to the db.
|
||||
var adminID string
|
||||
for i, p := range provisioners {
|
||||
prov, err := authority.ProvisionerToLinkedca(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := adminDB.CreateProvisioner(context.Background(), prov); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i == 0 {
|
||||
adminID = prov.Id
|
||||
}
|
||||
}
|
||||
// Add the first provisioner as an admin.
|
||||
if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{
|
||||
AuthorityId: admin.DefaultAuthorityID,
|
||||
Subject: "step",
|
||||
Type: linkedca.Admin_SUPER_ADMIN,
|
||||
ProvisionerId: adminID,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Save stores the pki on a json file that will be used as the certificate
|
||||
// authority configuration.
|
||||
func (p *PKI) Save(opt ...Option) error {
|
||||
func (p *PKI) Save(opt ...ConfigOption) error {
|
||||
// Write generated files
|
||||
if err := p.WriteFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Display the files written
|
||||
p.tellPKI()
|
||||
|
||||
// Generate and write ca.json
|
||||
config, err := p.GenerateConfig(opt...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling %s", p.config)
|
||||
}
|
||||
if err = fileutil.WriteFile(p.config, b, 0644); err != nil {
|
||||
return errs.FileError(err, p.config)
|
||||
}
|
||||
|
||||
// Generate the CA URL.
|
||||
if p.caURL == "" {
|
||||
p.caURL = p.dnsNames[0]
|
||||
var port string
|
||||
_, port, err = net.SplitHostPort(p.address)
|
||||
if !p.options.pkiOnly {
|
||||
config, err := p.GenerateConfig(opt...)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing %s", p.address)
|
||||
return err
|
||||
}
|
||||
if port == "443" {
|
||||
p.caURL = fmt.Sprintf("https://%s", p.caURL)
|
||||
} else {
|
||||
p.caURL = fmt.Sprintf("https://%s:%s", p.caURL, port)
|
||||
|
||||
b, err := json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling %s", p.config)
|
||||
}
|
||||
if err = fileutil.WriteFile(p.config, b, 0644); err != nil {
|
||||
return errs.FileError(err, p.config)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and write defaults.json
|
||||
defaults := &caDefaults{
|
||||
Root: p.root,
|
||||
CAConfig: p.config,
|
||||
CAUrl: p.caURL,
|
||||
Fingerprint: p.rootFingerprint,
|
||||
}
|
||||
b, err = json.MarshalIndent(defaults, "", "\t")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling %s", p.defaults)
|
||||
}
|
||||
if err = fileutil.WriteFile(p.defaults, b, 0644); err != nil {
|
||||
return errs.FileError(err, p.defaults)
|
||||
}
|
||||
// Generate and write defaults.json
|
||||
defaults := &caDefaults{
|
||||
Root: p.Defaults.Root,
|
||||
CAConfig: p.Defaults.CaConfig,
|
||||
CAUrl: p.Defaults.CaUrl,
|
||||
Fingerprint: p.Defaults.Fingerprint,
|
||||
}
|
||||
b, err = json.MarshalIndent(defaults, "", "\t")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling %s", p.defaults)
|
||||
}
|
||||
if err = fileutil.WriteFile(p.defaults, b, 0644); err != nil {
|
||||
return errs.FileError(err, p.defaults)
|
||||
}
|
||||
|
||||
// Generate and write templates
|
||||
if err := generateTemplates(config.Templates); err != nil {
|
||||
return err
|
||||
}
|
||||
// Generate and write templates
|
||||
if err := generateTemplates(config.Templates); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.DB != nil {
|
||||
ui.PrintSelected("Database folder", config.DB.DataSource)
|
||||
}
|
||||
if config.Templates != nil {
|
||||
ui.PrintSelected("Templates folder", GetTemplatesPath())
|
||||
}
|
||||
if config.DB != nil {
|
||||
ui.PrintSelected("Database folder", config.DB.DataSource)
|
||||
}
|
||||
if config.Templates != nil {
|
||||
ui.PrintSelected("Templates folder", GetTemplatesPath())
|
||||
}
|
||||
|
||||
ui.PrintSelected("Default configuration", p.defaults)
|
||||
ui.PrintSelected("Certificate Authority configuration", p.config)
|
||||
ui.Println()
|
||||
if p.casOptions.Is(apiv1.SoftCAS) {
|
||||
ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.")
|
||||
} else {
|
||||
ui.Println("Your registration authority is ready to go. To generate certificates for individual services see 'step help ca'.")
|
||||
ui.PrintSelected("Default configuration", p.defaults)
|
||||
ui.PrintSelected("Certificate Authority configuration", p.config)
|
||||
if p.options.deploymentType != LinkedDeployment {
|
||||
ui.Println()
|
||||
if p.casOptions.Is(apiv1.SoftCAS) {
|
||||
ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.")
|
||||
} else {
|
||||
ui.Println("Your registration authority is ready to go. To generate certificates for individual services see 'step help ca'.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.askFeedback()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeCertificate(c *x509.Certificate) []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: c.Raw,
|
||||
})
|
||||
}
|
||||
|
||||
func encodePrivateKey(key crypto.PrivateKey, pass []byte) ([]byte, error) {
|
||||
block, err := pemutil.Serialize(key, pemutil.WithPassword(pass))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pem.EncodeToMemory(block), nil
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
// getTemplates returns all the templates enabled
|
||||
func (p *PKI) getTemplates() *templates.Templates {
|
||||
if !p.enableSSH {
|
||||
if !p.options.enableSSH {
|
||||
return nil
|
||||
}
|
||||
return &templates.Templates{
|
||||
|
|
Loading…
Reference in a new issue