Merge pull request #633 from smallstep/linkedca

Linkedca
pull/682/head
Mariano Cano 3 years ago committed by GitHub
commit 9e57e4db2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

@ -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()),
)

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

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

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

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

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

@ -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,44 +295,96 @@ 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",
},
}
for _, fn := range opts {
fn(p)
}
if p.root, err = getPath(public, "root_ca.crt"); err != nil {
// 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
}
if p.rootKey, err = getPath(private, "root_ca_key"); err != nil {
rootKey, err := getPath(private, "root_ca_key")
if err != nil {
return nil, err
}
if p.intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil {
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 {
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 {
if p.Ssh.HostPublicKey, 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 {
if p.Ssh.UserPublicKey, 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 {
if p.Ssh.HostKey, err = getPath(private, "ssh_host_ca_key"); err != nil {
return nil, err
}
if p.sshUserKey, err = getPath(private, "ssh_user_ca_key"); err != nil {
if p.Ssh.UserKey, 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
}
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 youre 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.")
}
// 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()
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 youre 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.")
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")
}
prov := &provisioner.JWK{
Name: p.options.provisioner,
Type: "JWK",
Key: p.ottPublicKey,
EncryptedKey: key,
}
// Enable SSH authorization for default JWK provisioner
prov.Claims = &provisioner.Claims{
EnableSSHCA: &enableSSHCA,
provisioners = append(provisioners, prov)
// Add default ACME provisioner if enabled
if p.options.enableACME {
provisioners = append(provisioners, &provisioner.ACME{
Type: "ACME",
Name: "acme",
})
}
// Add default SSHPOP provisioner
sshpop := &provisioner.SSHPOP{
Type: "SSHPOP",
Name: "sshpop",
Claims: &provisioner.Claims{
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 {
p.tellPKI()
// Generate and write ca.json
config, err := p.GenerateConfig(opt...)
if err != nil {
func (p *PKI) Save(opt ...ConfigOption) error {
// Write generated files
if err := p.WriteFiles(); 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)
}
// Display the files written
p.tellPKI()
// Generate the CA URL.
if p.caURL == "" {
p.caURL = p.dnsNames[0]
var port string
_, port, err = net.SplitHostPort(p.address)
// Generate and write ca.json
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…
Cancel
Save