Merge pull request #312 from smallstep/cert-templates

Certificate flexibility
This commit is contained in:
Mariano Cano 2020-08-14 11:23:11 -07:00 committed by GitHub
commit 93b532ecff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1274 additions and 669 deletions

View file

@ -14,7 +14,7 @@ SRC=$(shell find . -type f -name '*.go' -not -path "./vendor/*")
GOOS_OVERRIDE ?=
OUTPUT_ROOT=output/
all: lint build test
all: lint test build
.PHONY: all

View file

@ -18,6 +18,7 @@ type Provisioner interface {
AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)
GetName() string
DefaultTLSCertDuration() time.Duration
GetOptions() *provisioner.Options
}
// MockProvisioner for testing
@ -27,6 +28,7 @@ type MockProvisioner struct {
MgetName func() string
MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error)
MdefaultTLSCertDuration func() time.Duration
MgetOptions func() *provisioner.Options
}
// GetName mock
@ -53,6 +55,13 @@ func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration {
return m.Mret1.(time.Duration)
}
func (m *MockProvisioner) GetOptions() *provisioner.Options {
if m.MgetOptions != nil {
return m.MgetOptions()
}
return m.Mret1.(*provisioner.Options)
}
// ContextKey is the key type for storing and searching for ACME request
// essentials in the context of a request.
type ContextKey string
@ -125,7 +134,7 @@ func ProvisionerFromContext(ctx context.Context) (Provisioner, error) {
// SignAuthority is the interface implemented by a CA authority.
type SignAuthority interface {
Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
LoadProvisionerByID(string) (provisioner.Interface, error)
}

View file

@ -11,6 +11,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/nosql"
"go.step.sm/crypto/x509util"
)
var defaultOrderExpiry = time.Hour * 24
@ -299,23 +300,23 @@ func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAut
orderNames = uniqueLowerNames(orderNames)
// Validate identifier names against CSR alternative names.
//
// Note that with certificate templates we are not going to check for the
// absence of other SANs as they will only be set if the templates allows
// them.
if len(csr.DNSNames) != len(orderNames) {
return nil, BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly: CSR names = %v, Order names = %v", csr.DNSNames, orderNames))
}
sans := make([]x509util.SubjectAlternativeName, len(csr.DNSNames))
for i := range csr.DNSNames {
if csr.DNSNames[i] != orderNames[i] {
return nil, BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly: CSR names = %v, Order names = %v", csr.DNSNames, orderNames))
}
}
if len(csr.IPAddresses) > 0 {
return nil, BadCSRErr(errors.Errorf("CSR contains IP Address SANs, but should only contain DNS Names"))
}
if len(csr.EmailAddresses) > 0 {
return nil, BadCSRErr(errors.Errorf("CSR contains Email Address SANs, but should only contain DNS Names"))
}
if len(csr.URIs) > 0 {
return nil, BadCSRErr(errors.Errorf("CSR contains URI SANs, but should only contain DNS Names"))
sans[i] = x509util.SubjectAlternativeName{
Type: x509util.DNSType,
Value: csr.DNSNames[i],
}
}
// Get authorizations from the ACME provisioner.
@ -325,8 +326,19 @@ func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAut
return nil, ServerInternalErr(errors.Wrapf(err, "error retrieving authorization options from ACME provisioner"))
}
// Template data
data := x509util.NewTemplateData()
data.SetCommonName(csr.Subject.CommonName)
data.Set(x509util.SANsKey, sans)
templateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data)
if err != nil {
return nil, ServerInternalErr(errors.Wrapf(err, "error creating template options from ACME provisioner"))
}
signOps = append(signOps, templateOptions)
// Create and store a new certificate.
certChain, err := auth.Sign(csr, provisioner.Options{
certChain, err := auth.Sign(csr, provisioner.SignOptions{
NotBefore: provisioner.NewTimeDuration(o.NotBefore),
NotAfter: provisioner.NewTimeDuration(o.NotAfter),
}, signOps...)

View file

@ -895,13 +895,13 @@ func TestOrderUpdateStatus(t *testing.T) {
}
type mockSignAuth struct {
sign func(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
sign func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
loadProvisionerByID func(string) (provisioner.Interface, error)
ret1, ret2 interface{}
err error
}
func (m *mockSignAuth) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
func (m *mockSignAuth) Sign(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
if m.sign != nil {
return m.sign(csr, signOpts, extraOpts...)
} else if m.err != nil {
@ -1066,13 +1066,13 @@ func TestOrderFinalize(t *testing.T) {
Subject: pkix.Name{
CommonName: "",
},
DNSNames: []string{"acme.example.com", "step.example.com"},
// DNSNames: []string{"acme.example.com", "step.example.com"},
IPAddresses: []net.IP{net.ParseIP("1.1.1.1")},
}
return test{
o: o,
csr: csr,
err: BadCSRErr(errors.Errorf("CSR contains IP Address SANs, but should only contain DNS Names")),
err: BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly")),
}
},
"fail/ready/no-emailAddresses": func(t *testing.T) test {
@ -1084,13 +1084,13 @@ func TestOrderFinalize(t *testing.T) {
Subject: pkix.Name{
CommonName: "",
},
DNSNames: []string{"acme.example.com", "step.example.com"},
// DNSNames: []string{"acme.example.com", "step.example.com"},
EmailAddresses: []string{"max@smallstep.com", "mariano@smallstep.com"},
}
return test{
o: o,
csr: csr,
err: BadCSRErr(errors.Errorf("CSR contains Email Address SANs, but should only contain DNS Names")),
err: BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly")),
}
},
"fail/ready/no-URIs": func(t *testing.T) test {
@ -1104,13 +1104,13 @@ func TestOrderFinalize(t *testing.T) {
Subject: pkix.Name{
CommonName: "",
},
DNSNames: []string{"acme.example.com", "step.example.com"},
URIs: []*url.URL{u},
// DNSNames: []string{"acme.example.com", "step.example.com"},
URIs: []*url.URL{u},
}
return test{
o: o,
csr: csr,
err: BadCSRErr(errors.Errorf("CSR contains URI SANs, but should only contain DNS Names")),
err: BadCSRErr(errors.Errorf("CSR names do not match identifiers exactly")),
}
},
"fail/ready/provisioner-auth-sign-error": func(t *testing.T) test {
@ -1262,8 +1262,8 @@ func TestOrderFinalize(t *testing.T) {
res: clone,
csr: csr,
sa: &mockSignAuth{
sign: func(csr *x509.CertificateRequest, pops provisioner.Options, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, len(signOps), 5)
sign: func(csr *x509.CertificateRequest, pops provisioner.SignOptions, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, len(signOps), 6)
return []*x509.Certificate{crt, inter}, nil
},
},
@ -1311,8 +1311,8 @@ func TestOrderFinalize(t *testing.T) {
res: &clone,
csr: csr,
sa: &mockSignAuth{
sign: func(csr *x509.CertificateRequest, pops provisioner.Options, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, len(signOps), 5)
sign: func(csr *x509.CertificateRequest, pops provisioner.SignOptions, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, len(signOps), 6)
return []*x509.Certificate{crt, inter}, nil
},
},
@ -1358,8 +1358,8 @@ func TestOrderFinalize(t *testing.T) {
res: &clone,
csr: csr,
sa: &mockSignAuth{
sign: func(csr *x509.CertificateRequest, pops provisioner.Options, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, len(signOps), 5)
sign: func(csr *x509.CertificateRequest, pops provisioner.SignOptions, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, len(signOps), 6)
return []*x509.Certificate{crt, inter}, nil
},
},

View file

@ -34,7 +34,7 @@ type Authority interface {
AuthorizeSign(ott string) ([]provisioner.SignOption, error)
GetTLSOptions() *tlsutil.TLSOptions
Root(shasum string) (*x509.Certificate, error)
Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)

View file

@ -550,7 +550,7 @@ type mockAuthority struct {
authorizeSign func(ott string) ([]provisioner.SignOption, error)
getTLSOptions func() *tlsutil.TLSOptions
root func(shasum string) (*x509.Certificate, error)
sign func(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
@ -560,7 +560,7 @@ type mockAuthority struct {
getEncryptedKey func(kid string) (string, error)
getRoots func() ([]*x509.Certificate, error)
getFederation func() ([]*x509.Certificate, error)
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
rekeySSH func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
@ -599,7 +599,7 @@ func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) {
return m.ret1.(*x509.Certificate), m.err
}
func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
if m.sign != nil {
return m.sign(cr, opts, signOpts...)
}
@ -669,7 +669,7 @@ func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) {
return m.ret1.([]*x509.Certificate), m.err
}
func (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
func (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
if m.signSSH != nil {
return m.signSSH(ctx, key, opts, signOpts...)
}

View file

@ -2,6 +2,7 @@ package api
import (
"crypto/tls"
"encoding/json"
"net/http"
"github.com/smallstep/certificates/authority/provisioner"
@ -11,10 +12,11 @@ import (
// SignRequest is the request body for a certificate signature request.
type SignRequest struct {
CsrPEM CertificateRequest `json:"csr"`
OTT string `json:"ott"`
NotAfter TimeDuration `json:"notAfter"`
NotBefore TimeDuration `json:"notBefore"`
CsrPEM CertificateRequest `json:"csr"`
OTT string `json:"ott"`
NotAfter TimeDuration `json:"notAfter"`
NotBefore TimeDuration `json:"notBefore"`
TemplateData json.RawMessage `json:"templateData"`
}
// Validate checks the fields of the SignRequest and returns nil if they are ok
@ -58,9 +60,10 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) {
return
}
opts := provisioner.Options{
NotBefore: body.NotBefore,
NotAfter: body.NotAfter,
opts := provisioner.SignOptions{
NotBefore: body.NotBefore,
NotAfter: body.NotAfter,
TemplateData: body.TemplateData,
}
signOpts, err := h.Authority.AuthorizeSign(body.OTT)

View file

@ -19,7 +19,7 @@ import (
// SSHAuthority is the interface implemented by a SSH CA authority.
type SSHAuthority interface {
SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
@ -274,7 +274,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) {
}
}
opts := provisioner.SSHOptions{
opts := provisioner.SignSSHOptions{
CertType: body.CertType,
KeyID: body.KeyID,
Principals: body.Principals,
@ -322,7 +322,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) {
NotAfter: time.Unix(int64(cert.ValidBefore), 0),
})
certChain, err := h.Authority.Sign(cr, provisioner.Options{}, signOpts...)
certChain, err := h.Authority.Sign(cr, provisioner.SignOptions{}, signOpts...)
if err != nil {
WriteError(w, errs.ForbiddenErr(err))
return

View file

@ -319,13 +319,13 @@ func Test_caHandler_SSHSign(t *testing.T) {
authorizeSign: func(ott string) ([]provisioner.SignOption, error) {
return []provisioner.SignOption{}, tt.authErr
},
signSSH: func(ctx context.Context, key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
signSSH: func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
return tt.signCert, tt.signErr
},
signSSHAddUser: func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) {
return tt.addUserCert, tt.addUserErr
},
sign: func(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
sign: func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
return tt.tlsSignCerts, tt.tlsSignErr
},
}).(*caHandler)

View file

@ -283,7 +283,7 @@ func TestNewEmbedded_Sign(t *testing.T) {
csr, err := x509.ParseCertificateRequest(cr)
assert.FatalError(t, err)
cert, err := a.Sign(csr, provisioner.Options{})
cert, err := a.Sign(csr, provisioner.SignOptions{})
assert.FatalError(t, err)
assert.Equals(t, []string{"foo.bar.zar"}, cert[0].DNSNames)
assert.Equals(t, crt, cert[1])

View file

@ -449,7 +449,7 @@ func TestAuthority_authorizeSign(t *testing.T) {
}
} else {
if assert.Nil(t, tc.err) {
assert.Len(t, 6, got)
assert.Len(t, 7, got)
}
}
})
@ -830,17 +830,17 @@ func TestAuthority_authorizeRenew(t *testing.T) {
}
func generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &provisioner.SSHOptions{
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &provisioner.SignSSHOptions{
CertType: "user",
Principals: []string{"name"},
}, jwk)
}
type stepPayload struct {
SSH *provisioner.SSHOptions `json:"ssh,omitempty"`
SSH *provisioner.SignSSHOptions `json:"ssh,omitempty"`
}
func generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *provisioner.SSHOptions, jwk *jose.JSONWebKey) (string, error) {
func generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *provisioner.SignSSHOptions, jwk *jose.JSONWebKey) (string, error) {
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),

View file

@ -13,10 +13,11 @@ import (
// provisioning flow.
type ACME struct {
*base
Type string `json:"type"`
Name string `json:"name"`
Claims *Claims `json:"claims,omitempty"`
ForceCN bool `json:"forceCN,omitempty"`
Type string `json:"type"`
Name string `json:"name"`
ForceCN bool `json:"forceCN,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
claimer *Claimer
}
@ -45,6 +46,11 @@ func (p *ACME) GetEncryptedKey() (string, string, bool) {
return "", "", false
}
// GetOptions returns the configured provisioner options.
func (p *ACME) GetOptions() *Options {
return p.Options
}
// DefaultTLSCertDuration returns the default TLS cert duration enforced by
// the provisioner.
func (p *ACME) DefaultTLSCertDuration() time.Duration {

View file

@ -18,6 +18,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
"go.step.sm/crypto/x509util"
)
// awsIssuer is the string used as issuer in the generated tokens.
@ -150,6 +151,7 @@ type AWS struct {
IMDSVersions []string `json:"imdsVersions"`
InstanceAge Duration `json:"instanceAge,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
claimer *Claimer
config *awsConfig
audiences Audiences
@ -310,22 +312,38 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}
doc := payload.document
// Template options
data := x509util.NewTemplateData()
data.SetCommonName(payload.Claims.Subject)
if v, err := unsafeParseSigned(token); err == nil {
data.SetToken(v)
}
// Enforce known CN and default DNS and IP if configured.
// By default we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU.
var so []SignOption
if p.DisableCustomSANs {
so = append(so, dnsNamesValidator([]string{
fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region),
}))
dnsName := fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region)
so = append(so, dnsNamesValidator([]string{dnsName}))
so = append(so, ipAddressesValidator([]net.IP{
net.ParseIP(doc.PrivateIP),
}))
so = append(so, emailAddressesValidator(nil))
so = append(so, urisValidator(nil))
// Template options
data.SetSANs([]string{dnsName, doc.PrivateIP})
}
templateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultIIDLeafTemplate)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSign")
}
return append(so,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
@ -577,7 +595,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
}
// Default to cert type to host
defaults := SSHOptions{
defaults := SignSSHOptions{
CertType: SSHHostCert,
Principals: principals,
}

View file

@ -595,11 +595,11 @@ func TestAWS_AuthorizeSign(t *testing.T) {
code int
wantErr bool
}{
{"ok", p1, args{t1, "foo.local"}, 5, http.StatusOK, false},
{"ok", p2, args{t2, "instance-id"}, 9, http.StatusOK, false},
{"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 9, http.StatusOK, false},
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 9, http.StatusOK, false},
{"ok", p1, args{t4, "instance-id"}, 5, http.StatusOK, false},
{"ok", p1, args{t1, "foo.local"}, 6, http.StatusOK, false},
{"ok", p2, args{t2, "instance-id"}, 10, http.StatusOK, false},
{"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 10, http.StatusOK, false},
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 10, http.StatusOK, false},
{"ok", p1, args{t4, "instance-id"}, 6, http.StatusOK, false},
{"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true},
{"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true},
{"fail subject", p1, args{token: failSubject}, 0, http.StatusUnauthorized, true},
@ -629,6 +629,7 @@ func TestAWS_AuthorizeSign(t *testing.T) {
assert.Len(t, tt.wantLen, got)
for _, o := range got {
switch v := o.(type) {
case certificateOptionsFunc:
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeAWS))
assert.Equals(t, v.Name, tt.aws.GetName())
@ -701,51 +702,51 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) {
assert.FatalError(t, err)
hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedHostOptions := &SSHOptions{
expectedHostOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
expectedHostOptionsIP := &SSHOptions{
expectedHostOptionsIP := &SignSSHOptions{
CertType: "host", Principals: []string{"127.0.0.1"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
expectedHostOptionsHostname := &SSHOptions{
expectedHostOptionsHostname := &SignSSHOptions{
CertType: "host", Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
expectedCustomOptions := &SSHOptions{
expectedCustomOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"foo.local"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
type args struct {
token string
sshOpts SSHOptions
sshOpts SignSSHOptions
key interface{}
}
tests := []struct {
name string
aws *AWS
args args
expected *SSHOptions
expected *SignSSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principal-ip", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1"}}, pub}, expectedHostOptionsIP, http.StatusOK, false, false},
{"ok-principal-hostname", p1, args{t1, SSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptionsHostname, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-custom", p2, args{t2, SSHOptions{Principals: []string{"foo.local"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p3, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"ok", p1, args{t1, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SignSSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SignSSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principal-ip", p1, args{t1, SignSSHOptions{Principals: []string{"127.0.0.1"}}, pub}, expectedHostOptionsIP, http.StatusOK, false, false},
{"ok-principal-hostname", p1, args{t1, SignSSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptionsHostname, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SignSSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-custom", p2, args{t2, SignSSHOptions{Principals: []string{"foo.local"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SignSSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SignSSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SignSSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p3, args{"foo", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -15,6 +15,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
"go.step.sm/crypto/x509util"
)
// azureOIDCBaseURL is the base discovery url for Microsoft Azure tokens.
@ -90,6 +91,7 @@ type Azure struct {
DisableCustomSANs bool `json:"disableCustomSANs"`
DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
claimer *Claimer
config *azureConfig
oidcConfig openIDConfiguration
@ -276,6 +278,13 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
}
}
// Template options
data := x509util.NewTemplateData()
data.SetCommonName(name)
if v, err := unsafeParseSigned(token); err == nil {
data.SetToken(v)
}
// Enforce known common name and default DNS if configured.
// By default we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU.
@ -287,9 +296,18 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
so = append(so, ipAddressesValidator(nil))
so = append(so, emailAddressesValidator(nil))
so = append(so, urisValidator(nil))
// Enforce SANs in the template.
data.SetSANs([]string{name})
}
templateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultIIDLeafTemplate)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSign")
}
return append(so,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
@ -332,7 +350,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
}
// Default to host + known hostnames
defaults := SSHOptions{
defaults := SignSSHOptions{
CertType: SSHHostCert,
Principals: principals,
}

View file

@ -431,9 +431,9 @@ func TestAzure_AuthorizeSign(t *testing.T) {
code int
wantErr bool
}{
{"ok", p1, args{t1}, 4, http.StatusOK, false},
{"ok", p2, args{t2}, 9, http.StatusOK, false},
{"ok", p1, args{t11}, 4, http.StatusOK, false},
{"ok", p1, args{t1}, 5, http.StatusOK, false},
{"ok", p2, args{t2}, 10, http.StatusOK, false},
{"ok", p1, args{t11}, 5, http.StatusOK, false},
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
@ -458,6 +458,7 @@ func TestAzure_AuthorizeSign(t *testing.T) {
assert.Len(t, tt.wantLen, got)
for _, o := range got {
switch v := o.(type) {
case certificateOptionsFunc:
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeAzure))
assert.Equals(t, v.Name, tt.azure.GetName())
@ -570,41 +571,41 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) {
assert.FatalError(t, err)
hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedHostOptions := &SSHOptions{
expectedHostOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"virtualMachine"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
expectedCustomOptions := &SSHOptions{
expectedCustomOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"foo.bar"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
type args struct {
token string
sshOpts SSHOptions
sshOpts SignSSHOptions
key interface{}
}
tests := []struct {
name string
azure *Azure
args args
expected *SSHOptions
expected *SignSSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-custom", p2, args{t2, SSHOptions{Principals: []string{"foo.bar"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p3, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"ok", p1, args{t1, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SignSSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SignSSHOptions{Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SignSSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-custom", p2, args{t2, SignSSHOptions{Principals: []string{"foo.bar"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SignSSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SignSSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SignSSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p3, args{"foo", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -16,6 +16,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
"go.step.sm/crypto/x509util"
)
// gcpCertsURL is the url that serves Google OAuth2 public keys.
@ -84,6 +85,7 @@ type GCP struct {
DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"`
InstanceAge Duration `json:"instanceAge,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
claimer *Claimer
config *gcpConfig
keyStore *keyStore
@ -215,6 +217,14 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}
ce := claims.Google.ComputeEngine
// Template options
data := x509util.NewTemplateData()
data.SetCommonName(ce.InstanceName)
if v, err := unsafeParseSigned(token); err == nil {
data.SetToken(v)
}
// Enforce known common name and default DNS if configured.
// By default we we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU.
@ -231,9 +241,18 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
so = append(so, ipAddressesValidator(nil))
so = append(so, emailAddressesValidator(nil))
so = append(so, urisValidator(nil))
// Template SANs
data.SetSANs([]string{dnsName1, dnsName2})
}
templateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultIIDLeafTemplate)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "gcp.AuthorizeSign")
}
return append(so,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),
@ -375,7 +394,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
}
// Default to host + known hostnames
defaults := SSHOptions{
defaults := SignSSHOptions{
CertType: SSHHostCert,
Principals: principals,
}

View file

@ -515,9 +515,9 @@ func TestGCP_AuthorizeSign(t *testing.T) {
code int
wantErr bool
}{
{"ok", p1, args{t1}, 4, http.StatusOK, false},
{"ok", p2, args{t2}, 9, http.StatusOK, false},
{"ok", p3, args{t3}, 4, http.StatusOK, false},
{"ok", p1, args{t1}, 5, http.StatusOK, false},
{"ok", p2, args{t2}, 10, http.StatusOK, false},
{"ok", p3, args{t3}, 5, http.StatusOK, false},
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
{"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true},
@ -547,6 +547,7 @@ func TestGCP_AuthorizeSign(t *testing.T) {
assert.Len(t, tt.wantLen, got)
for _, o := range got {
switch v := o.(type) {
case certificateOptionsFunc:
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeGCP))
assert.Equals(t, v.Name, tt.gcp.GetName())
@ -622,51 +623,51 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
assert.FatalError(t, err)
hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedHostOptions := &SSHOptions{
expectedHostOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
expectedHostOptionsPrincipal1 := &SSHOptions{
expectedHostOptionsPrincipal1 := &SignSSHOptions{
CertType: "host", Principals: []string{"instance-name.c.project-id.internal"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
expectedHostOptionsPrincipal2 := &SSHOptions{
expectedHostOptionsPrincipal2 := &SignSSHOptions{
CertType: "host", Principals: []string{"instance-name.zone.c.project-id.internal"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
expectedCustomOptions := &SSHOptions{
expectedCustomOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"foo.bar", "bar.foo"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
type args struct {
token string
sshOpts SSHOptions
sshOpts SignSSHOptions
key interface{}
}
tests := []struct {
name string
gcp *GCP
args args
expected *SSHOptions
expected *SignSSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principal1", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, http.StatusOK, false, false},
{"ok-principal2", p1, args{t1, SSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-custom", p2, args{t2, SSHOptions{Principals: []string{"foo.bar", "bar.foo"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p3, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"ok", p1, args{t1, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SignSSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SignSSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principal1", p1, args{t1, SignSSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, http.StatusOK, false, false},
{"ok-principal2", p1, args{t1, SignSSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SignSSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-custom", p2, args{t2, SignSSHOptions{Principals: []string{"foo.bar", "bar.foo"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SignSSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SignSSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SignSSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p3, args{"foo", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -9,6 +9,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
"go.step.sm/crypto/x509util"
)
// jwtPayload extends jwt.Claims with step attributes.
@ -19,7 +20,7 @@ type jwtPayload struct {
}
type stepPayload struct {
SSH *SSHOptions `json:"ssh,omitempty"`
SSH *SignSSHOptions `json:"ssh,omitempty"`
}
// JWK is the default provisioner, an entity that can sign tokens necessary for
@ -31,6 +32,7 @@ type JWK struct {
Key *jose.JSONWebKey `json:"key"`
EncryptedKey string `json:"encryptedKey,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
claimer *Claimer
audiences Audiences
}
@ -151,7 +153,19 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
claims.SANs = []string{claims.Subject}
}
// Certificate templates
data := x509util.CreateTemplateData(claims.Subject, claims.SANs)
if v, err := unsafeParseSigned(token); err == nil {
data.SetToken(v)
}
templateOptions, err := TemplateOptions(p.Options, data)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSign")
}
return []SignOption{
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),

View file

@ -295,9 +295,10 @@ func TestJWK_AuthorizeSign(t *testing.T) {
}
} else {
if assert.NotNil(t, got) {
assert.Len(t, 6, got)
assert.Len(t, 7, got)
for _, o := range got {
switch v := o.(type) {
case certificateOptionsFunc:
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeJWK))
assert.Equals(t, v.Name, tt.prov.GetName())
@ -403,41 +404,41 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
userDuration := p1.claimer.DefaultUserSSHCertDuration()
hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedUserOptions := &SSHOptions{
expectedUserOptions := &SignSSHOptions{
CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)),
}
expectedHostOptions := &SSHOptions{
expectedHostOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"smallstep.com"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
type args struct {
token string
sshOpts SSHOptions
sshOpts SignSSHOptions
key interface{}
}
tests := []struct {
name string
prov *JWK
args args
expected *SSHOptions
expected *SignSSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"user", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false},
{"user-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"host", p1, args{t2, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-type", p1, args{t2, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-principals", p1, args{t2, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-options", p1, args{t2, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedUserOptions, http.StatusUnauthorized, true, false},
{"fail-signature", p1, args{failSig, SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
{"rail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
{"user", p1, args{t1, SignSSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-rsa2048", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false},
{"user-type", p1, args{t1, SignSSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-principals", p1, args{t1, SignSSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-options", p1, args{t1, SignSSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"host", p1, args{t2, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-type", p1, args{t2, SignSSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-principals", p1, args{t2, SignSSHOptions{Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-options", p1, args{t2, SignSSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"fail-sshCA-disabled", p2, args{"foo", SignSSHOptions{}, pub}, expectedUserOptions, http.StatusUnauthorized, true, false},
{"fail-signature", p1, args{failSig, SignSSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
{"rail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -486,72 +487,72 @@ func TestJWK_AuthorizeSign_SSHOptions(t *testing.T) {
userDuration := p1.claimer.DefaultUserSSHCertDuration()
hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedUserOptions := &SSHOptions{
expectedUserOptions := &SignSSHOptions{
CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)),
}
expectedHostOptions := &SSHOptions{
expectedHostOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"smallstep.com"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
type args struct {
sub, iss, aud string
iat time.Time
tokSSHOpts *SSHOptions
userSSHOpts *SSHOptions
tokSSHOpts *SignSSHOptions
userSSHOpts *SignSSHOptions
jwk *jose.JSONWebKey
}
tests := []struct {
name string
prov *JWK
args args
expected *SSHOptions
expected *SignSSHOptions
wantErr bool
wantSignErr bool
}{
{"ok-user", p1, args{sub, iss, aud, iat, &SSHOptions{CertType: "user", Principals: []string{"name"}}, &SSHOptions{}, jwk}, expectedUserOptions, false, false},
{"ok-host", p1, args{sub, iss, aud, iat, &SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, &SSHOptions{}, jwk}, expectedHostOptions, false, false},
{"ok-user-opts", p1, args{sub, iss, aud, iat, &SSHOptions{}, &SSHOptions{CertType: "user", Principals: []string{"name"}}, jwk}, expectedUserOptions, false, false},
{"ok-host-opts", p1, args{sub, iss, aud, iat, &SSHOptions{}, &SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, jwk}, expectedHostOptions, false, false},
{"ok-user-mixed", p1, args{sub, iss, aud, iat, &SSHOptions{CertType: "user"}, &SSHOptions{Principals: []string{"name"}}, jwk}, expectedUserOptions, false, false},
{"ok-host-mixed", p1, args{sub, iss, aud, iat, &SSHOptions{Principals: []string{"smallstep.com"}}, &SSHOptions{CertType: "host"}, jwk}, expectedHostOptions, false, false},
{"ok-user-validAfter", p1, args{sub, iss, aud, iat, &SSHOptions{
{"ok-user", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, &SignSSHOptions{}, jwk}, expectedUserOptions, false, false},
{"ok-host", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, &SignSSHOptions{}, jwk}, expectedHostOptions, false, false},
{"ok-user-opts", p1, args{sub, iss, aud, iat, &SignSSHOptions{}, &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, jwk}, expectedUserOptions, false, false},
{"ok-host-opts", p1, args{sub, iss, aud, iat, &SignSSHOptions{}, &SignSSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, jwk}, expectedHostOptions, false, false},
{"ok-user-mixed", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: "user"}, &SignSSHOptions{Principals: []string{"name"}}, jwk}, expectedUserOptions, false, false},
{"ok-host-mixed", p1, args{sub, iss, aud, iat, &SignSSHOptions{Principals: []string{"smallstep.com"}}, &SignSSHOptions{CertType: "host"}, jwk}, expectedHostOptions, false, false},
{"ok-user-validAfter", p1, args{sub, iss, aud, iat, &SignSSHOptions{
CertType: "user", Principals: []string{"name"},
}, &SSHOptions{
}, &SignSSHOptions{
ValidAfter: NewTimeDuration(tm.Add(-time.Hour)),
}, jwk}, &SSHOptions{
}, jwk}, &SignSSHOptions{
CertType: "user", Principals: []string{"name"}, ValidAfter: NewTimeDuration(tm.Add(-time.Hour)), ValidBefore: NewTimeDuration(tm.Add(userDuration - time.Hour)),
}, false, false},
{"ok-user-validBefore", p1, args{sub, iss, aud, iat, &SSHOptions{
{"ok-user-validBefore", p1, args{sub, iss, aud, iat, &SignSSHOptions{
CertType: "user", Principals: []string{"name"},
}, &SSHOptions{
}, &SignSSHOptions{
ValidBefore: NewTimeDuration(tm.Add(time.Hour)),
}, jwk}, &SSHOptions{
}, jwk}, &SignSSHOptions{
CertType: "user", Principals: []string{"name"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(time.Hour)),
}, false, false},
{"ok-user-validAfter-validBefore", p1, args{sub, iss, aud, iat, &SSHOptions{
{"ok-user-validAfter-validBefore", p1, args{sub, iss, aud, iat, &SignSSHOptions{
CertType: "user", Principals: []string{"name"},
}, &SSHOptions{
}, &SignSSHOptions{
ValidAfter: NewTimeDuration(tm.Add(10 * time.Minute)), ValidBefore: NewTimeDuration(tm.Add(time.Hour)),
}, jwk}, &SSHOptions{
}, jwk}, &SignSSHOptions{
CertType: "user", Principals: []string{"name"}, ValidAfter: NewTimeDuration(tm.Add(10 * time.Minute)), ValidBefore: NewTimeDuration(tm.Add(time.Hour)),
}, false, false},
{"ok-user-match", p1, args{sub, iss, aud, iat, &SSHOptions{
{"ok-user-match", p1, args{sub, iss, aud, iat, &SignSSHOptions{
CertType: "user", Principals: []string{"name"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(1 * time.Hour)),
}, &SSHOptions{
}, &SignSSHOptions{
CertType: "user", Principals: []string{"name"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(1 * time.Hour)),
}, jwk}, &SSHOptions{
}, jwk}, &SignSSHOptions{
CertType: "user", Principals: []string{"name"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(time.Hour)),
}, false, false},
{"fail-certType", p1, args{sub, iss, aud, iat, &SSHOptions{CertType: "user", Principals: []string{"name"}}, &SSHOptions{CertType: "host"}, jwk}, nil, false, true},
{"fail-principals", p1, args{sub, iss, aud, iat, &SSHOptions{CertType: "user", Principals: []string{"name"}}, &SSHOptions{Principals: []string{"root"}}, jwk}, nil, false, true},
{"fail-validAfter", p1, args{sub, iss, aud, iat, &SSHOptions{CertType: "user", Principals: []string{"name"}, ValidAfter: NewTimeDuration(tm)}, &SSHOptions{ValidAfter: NewTimeDuration(tm.Add(time.Hour))}, jwk}, nil, false, true},
{"fail-validBefore", p1, args{sub, iss, aud, iat, &SSHOptions{CertType: "user", Principals: []string{"name"}, ValidBefore: NewTimeDuration(tm.Add(time.Hour))}, &SSHOptions{ValidBefore: NewTimeDuration(tm.Add(10 * time.Hour))}, jwk}, nil, false, true},
{"fail-subject", p1, args{"", iss, aud, iat, &SSHOptions{CertType: "user", Principals: []string{"name"}}, &SSHOptions{}, jwk}, nil, true, false},
{"fail-issuer", p1, args{sub, "invalid", aud, iat, &SSHOptions{CertType: "user", Principals: []string{"name"}}, &SSHOptions{}, jwk}, nil, true, false},
{"fail-audience", p1, args{sub, iss, "invalid", iat, &SSHOptions{CertType: "user", Principals: []string{"name"}}, &SSHOptions{}, jwk}, nil, true, false},
{"fail-expired", p1, args{sub, iss, aud, iat.Add(-6 * time.Minute), &SSHOptions{CertType: "user", Principals: []string{"name"}}, &SSHOptions{}, jwk}, nil, true, false},
{"fail-notBefore", p1, args{sub, iss, aud, iat.Add(5 * time.Minute), &SSHOptions{CertType: "user", Principals: []string{"name"}}, &SSHOptions{}, jwk}, nil, true, false},
{"fail-certType", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, &SignSSHOptions{CertType: "host"}, jwk}, nil, false, true},
{"fail-principals", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, &SignSSHOptions{Principals: []string{"root"}}, jwk}, nil, false, true},
{"fail-validAfter", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: "user", Principals: []string{"name"}, ValidAfter: NewTimeDuration(tm)}, &SignSSHOptions{ValidAfter: NewTimeDuration(tm.Add(time.Hour))}, jwk}, nil, false, true},
{"fail-validBefore", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: "user", Principals: []string{"name"}, ValidBefore: NewTimeDuration(tm.Add(time.Hour))}, &SignSSHOptions{ValidBefore: NewTimeDuration(tm.Add(10 * time.Hour))}, jwk}, nil, false, true},
{"fail-subject", p1, args{"", iss, aud, iat, &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, &SignSSHOptions{}, jwk}, nil, true, false},
{"fail-issuer", p1, args{sub, "invalid", aud, iat, &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, &SignSSHOptions{}, jwk}, nil, true, false},
{"fail-audience", p1, args{sub, iss, "invalid", iat, &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, &SignSSHOptions{}, jwk}, nil, true, false},
{"fail-expired", p1, args{sub, iss, aud, iat.Add(-6 * time.Minute), &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, &SignSSHOptions{}, jwk}, nil, true, false},
{"fail-notBefore", p1, args{sub, iss, aud, iat.Add(5 * time.Minute), &SignSSHOptions{CertType: "user", Principals: []string{"name"}}, &SignSSHOptions{}, jwk}, nil, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -560,7 +561,7 @@ func TestJWK_AuthorizeSign_SSHOptions(t *testing.T) {
if got, err := tt.prov.AuthorizeSSHSign(context.Background(), token); (err != nil) != tt.wantErr {
t.Errorf("JWK.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
} else if !tt.wantErr && assert.NotNil(t, got) {
var opts SSHOptions
var opts SignSSHOptions
if tt.args.userSSHOpts != nil {
opts = *tt.args.userSSHOpts
}

View file

@ -3,6 +3,7 @@ package provisioner
import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
@ -12,7 +13,7 @@ import (
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ed25519"
"go.step.sm/crypto/x509util"
)
// NOTE: There can be at most one kubernetes service account provisioner configured
@ -40,10 +41,11 @@ type k8sSAPayload struct {
// entity trusted to make signature requests.
type K8sSA struct {
*base
Type string `json:"type"`
Name string `json:"name"`
Claims *Claims `json:"claims,omitempty"`
PubKeys []byte `json:"publicKeys,omitempty"`
Type string `json:"type"`
Name string `json:"name"`
PubKeys []byte `json:"publicKeys,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
claimer *Claimer
audiences Audiences
//kauthn kauthn.AuthenticationV1Interface
@ -204,11 +206,27 @@ func (p *K8sSA) AuthorizeRevoke(ctx context.Context, token string) error {
// AuthorizeSign validates the given token.
func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
if _, err := p.authorizeToken(token, p.audiences.Sign); err != nil {
claims, err := p.authorizeToken(token, p.audiences.Sign)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeSign")
}
// Add some values to use in custom templates.
data := x509util.NewTemplateData()
data.SetCommonName(claims.ServiceAccountName)
if v, err := unsafeParseSigned(token); err == nil {
data.SetToken(v)
}
// Certificate templates: on K8sSA the default template is the certificate
// request.
templateOptions, err := CustomTemplateOptions(p.Options, data, x509util.CertificateRequestTemplate)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeSign")
}
return []SignOption{
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeK8sSA, p.Name, ""),
profileDefaultDuration(p.claimer.DefaultTLSCertDuration()),

View file

@ -274,6 +274,7 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
tot := 0
for _, o := range opts {
switch v := o.(type) {
case certificateOptionsFunc:
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeK8sSA))
assert.Equals(t, v.Name, tc.p.GetName())
@ -290,7 +291,7 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
}
tot++
}
assert.Equals(t, tot, 4)
assert.Equals(t, tot, 5)
}
}
}

View file

@ -14,6 +14,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
"go.step.sm/crypto/x509util"
)
// openIDConfiguration contains the necessary properties in the
@ -63,6 +64,7 @@ type OIDC struct {
Groups []string `json:"groups,omitempty"`
ListenAddress string `json:"listenAddress,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
configuration openIDConfiguration
keyStore *keyStore
claimer *Claimer
@ -72,10 +74,12 @@ type OIDC struct {
// IsAdmin returns true if the given email is in the Admins allowlist, false
// otherwise.
func (o *OIDC) IsAdmin(email string) bool {
email = sanitizeEmail(email)
for _, e := range o.Admins {
if email == sanitizeEmail(e) {
return true
if email != "" {
email = sanitizeEmail(email)
for _, e := range o.Admins {
if email == sanitizeEmail(e) {
return true
}
}
}
return false
@ -203,13 +207,8 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
return errs.Unauthorized("validatePayload: failed to validate oidc token payload: invalid azp")
}
// Enforce an email claim
if p.Email == "" {
return errs.Unauthorized("validatePayload: failed to validate oidc token payload: email not found")
}
// Validate domains (case-insensitive)
if !o.IsAdmin(p.Email) && len(o.Domains) > 0 {
if p.Email != "" && len(o.Domains) > 0 && !o.IsAdmin(p.Email) {
email := sanitizeEmail(p.Email)
var found bool
for _, d := range o.Domains {
@ -301,20 +300,49 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSign")
}
so := []SignOption{
// Certificate templates
sans := []string{}
if claims.Email != "" {
sans = append(sans, claims.Email)
}
// Add uri SAN with iss#sub if issuer is a URL with schema.
//
// According to https://openid.net/specs/openid-connect-core-1_0.html the
// iss value is a case sensitive URL using the https scheme that contains
// scheme, host, and optionally, port number and path components and no
// query or fragment components.
if iss, err := url.Parse(claims.Issuer); err == nil && iss.Scheme != "" {
iss.Fragment = claims.Subject
sans = append(sans, iss.String())
}
data := x509util.CreateTemplateData(claims.Subject, sans)
if v, err := unsafeParseSigned(token); err == nil {
data.SetToken(v)
}
// Use the default template unless no-templates are configured and email is
// an admin, in that case we will use the CR template.
defaultTemplate := x509util.DefaultLeafTemplate
if !o.Options.GetX509Options().HasTemplate() && o.IsAdmin(claims.Email) {
defaultTemplate = x509util.CertificateRequestTemplate
}
templateOptions, err := CustomTemplateOptions(o.Options, data, defaultTemplate)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSign")
}
return []SignOption{
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
profileDefaultDuration(o.claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},
newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()),
}
// Admins should be able to authorize any SAN
if o.IsAdmin(claims.Email) {
return so, nil
}
return append(so, emailOnlyIdentity(claims.Email)), nil
}, nil
}
// AuthorizeRenew returns an error if the renewal is disabled.
@ -337,6 +365,10 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHSign")
}
// Enforce an email claim
if claims.Email == "" {
return nil, errs.Unauthorized("oidc.AuthorizeSSHSign: failed to validate oidc token payload: email not found")
}
signOptions := []SignOption{
// set the key id to the token email
sshCertKeyIDModifier(claims.Email),
@ -348,7 +380,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHSign")
}
defaults := SSHOptions{
defaults := SignSSHOptions{
CertType: SSHUserCert,
Principals: iden.Usernames,
}

View file

@ -179,12 +179,12 @@ func TestOIDC_authorizeToken(t *testing.T) {
assert.FatalError(t, err)
t4, err := generateToken("subject", issuer, p3.ClientID, "foo@smallstep.com", []string{}, time.Now(), &keys.Keys[2])
assert.FatalError(t, err)
// Invalid email
failEmail, err := generateToken("subject", issuer, p3.ClientID, "", []string{}, time.Now(), &keys.Keys[2])
assert.FatalError(t, err)
failDomain, err := generateToken("subject", issuer, p3.ClientID, "name@example.com", []string{}, time.Now(), &keys.Keys[2])
t5, err := generateToken("subject", issuer, p3.ClientID, "", []string{}, time.Now(), &keys.Keys[2])
assert.FatalError(t, err)
// Invalid email
failDomain, err := generateToken("subject", issuer, p3.ClientID, "name@example.com", []string{}, time.Now(), &keys.Keys[2])
assert.FatalError(t, err)
// Invalid tokens
parts := strings.Split(t1, ".")
key, err := generateJSONWebKey()
@ -226,7 +226,7 @@ func TestOIDC_authorizeToken(t *testing.T) {
{"ok tenantid", p2, args{t2}, http.StatusOK, tenantIssuer, false},
{"ok admin", p3, args{t3}, http.StatusOK, issuer, false},
{"ok domain", p3, args{t4}, http.StatusOK, issuer, false},
{"fail-email", p3, args{failEmail}, http.StatusUnauthorized, "", true},
{"ok no email", p3, args{t5}, http.StatusOK, issuer, false},
{"fail-domain", p3, args{failDomain}, http.StatusUnauthorized, "", true},
{"fail-key", p1, args{failKey}, http.StatusUnauthorized, "", true},
{"fail-token", p1, args{failTok}, http.StatusUnauthorized, "", true},
@ -290,8 +290,8 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
// Admin email not in domains
okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
// Invalid email
failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
// No email
noEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
type args struct {
@ -306,7 +306,8 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
}{
{"ok1", p1, args{t1}, http.StatusOK, false},
{"admin", p3, args{okAdmin}, http.StatusOK, false},
{"fail-email", p3, args{failEmail}, http.StatusUnauthorized, true},
{"no-email", p3, args{noEmail}, http.StatusOK, false},
{"bad-token", p3, args{"foobar"}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -323,12 +324,13 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
} else {
if assert.NotNil(t, got) {
if tt.name == "admin" {
assert.Len(t, 4, got)
assert.Len(t, 5, got)
} else {
assert.Len(t, 5, got)
}
for _, o := range got {
switch v := o.(type) {
case certificateOptionsFunc:
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeOIDC))
assert.Equals(t, v.Name, tt.prov.GetName())
@ -514,7 +516,7 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
// Admin email not in domains
okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
// Invalid email
// Empty email
failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
@ -532,64 +534,64 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
userDuration := p1.claimer.DefaultUserSSHCertDuration()
hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedUserOptions := &SSHOptions{
expectedUserOptions := &SignSSHOptions{
CertType: "user", Principals: []string{"name", "name@smallstep.com"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)),
}
expectedAdminOptions := &SSHOptions{
expectedAdminOptions := &SignSSHOptions{
CertType: "user", Principals: []string{"root", "root@example.com"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)),
}
expectedHostOptions := &SSHOptions{
expectedHostOptions := &SignSSHOptions{
CertType: "host", Principals: []string{"smallstep.com"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
type args struct {
token string
sshOpts SSHOptions
sshOpts SignSSHOptions
key interface{}
}
tests := []struct {
name string
prov *OIDC
args args
expected *SSHOptions
expected *SignSSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false},
{"ok-user", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"name"},
{"ok", p1, args{t1, SignSSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false},
{"ok-user", p1, args{t1, SignSSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SignSSHOptions{Principals: []string{"name"}}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"ok-principals-getIdentity", p4, args{okGetIdentityToken, SSHOptions{Principals: []string{"mariano"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"mariano"},
{"ok-principals-getIdentity", p4, args{okGetIdentityToken, SignSSHOptions{Principals: []string{"mariano"}}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"mariano"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"ok-emptyPrincipals-getIdentity", p4, args{okGetIdentityToken, SSHOptions{}, pub},
&SSHOptions{CertType: "user", Principals: []string{"max", "mariano"},
{"ok-emptyPrincipals-getIdentity", p4, args{okGetIdentityToken, SignSSHOptions{}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"max", "mariano"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"name"},
{"ok-options", p1, args{t1, SignSSHOptions{CertType: "user", Principals: []string{"name"}}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"admin", p3, args{okAdmin, SSHOptions{}, pub}, expectedAdminOptions, http.StatusOK, false, false},
{"admin-user", p3, args{okAdmin, SSHOptions{CertType: "user"}, pub}, expectedAdminOptions, http.StatusOK, false, false},
{"admin-principals", p3, args{okAdmin, SSHOptions{Principals: []string{"root"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"root"},
{"admin", p3, args{okAdmin, SignSSHOptions{}, pub}, expectedAdminOptions, http.StatusOK, false, false},
{"admin-user", p3, args{okAdmin, SignSSHOptions{CertType: "user"}, pub}, expectedAdminOptions, http.StatusOK, false, false},
{"admin-principals", p3, args{okAdmin, SignSSHOptions{Principals: []string{"root"}}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"root"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"admin-options", p3, args{okAdmin, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"name"},
{"admin-options", p3, args{okAdmin, SignSSHOptions{CertType: "user", Principals: []string{"name"}}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"admin-host", p3, args{okAdmin, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub},
{"admin-host", p3, args{okAdmin, SignSSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub},
expectedHostOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
{"fail-user-host", p1, args{t1, SSHOptions{CertType: "host"}, pub}, nil, http.StatusOK, false, true},
{"fail-user-principals", p1, args{t1, SSHOptions{Principals: []string{"root"}}, pub}, nil, http.StatusOK, false, true},
{"fail-email", p3, args{failEmail, SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
{"fail-getIdentity", p5, args{failGetIdentityToken, SSHOptions{}, pub}, nil, http.StatusInternalServerError, true, false},
{"fail-sshCA-disabled", p6, args{"foo", SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
{"fail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
{"fail-user-host", p1, args{t1, SignSSHOptions{CertType: "host"}, pub}, nil, http.StatusOK, false, true},
{"fail-user-principals", p1, args{t1, SignSSHOptions{Principals: []string{"root"}}, pub}, nil, http.StatusOK, false, true},
{"fail-email", p3, args{failEmail, SignSSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
{"fail-getIdentity", p5, args{failGetIdentityToken, SignSSHOptions{}, pub}, nil, http.StatusInternalServerError, true, false},
{"fail-sshCA-disabled", p6, args{"foo", SignSSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -0,0 +1,136 @@
package provisioner
import (
"encoding/json"
"strings"
"github.com/pkg/errors"
"github.com/smallstep/cli/jose"
"go.step.sm/crypto/x509util"
)
// CertificateOptions is an interface that returns a list of options passed when
// creating a new certificate.
type CertificateOptions interface {
Options(SignOptions) []x509util.Option
}
type certificateOptionsFunc func(SignOptions) []x509util.Option
func (fn certificateOptionsFunc) Options(so SignOptions) []x509util.Option {
return fn(so)
}
// Options are a collection of custom options that can be added to
// each provisioner.
type Options struct {
X509 *X509Options `json:"x509,omitempty"`
}
// GetX509Options returns the X.509Options
func (o *Options) GetX509Options() *X509Options {
if o == nil {
return nil
}
return o.X509
}
// X509Options contains specific options for X.509 certificates.
type X509Options struct {
// Template contains a X.509 certificate template. It can be a JSON template
// escaped in a string or it can be also encoded in base64.
Template string `json:"template"`
// TemplateFile points to a file containing a X.509 certificate template.
TemplateFile string `json:"templateFile"`
// TemplateData is a JSON object with variables that can be used in custom
// templates.
TemplateData json.RawMessage `json:"templateData"`
}
// HasTemplate returns true if a template is defined in the provisioner options.
func (o *X509Options) HasTemplate() bool {
return o != nil && (o.Template != "" || o.TemplateFile != "")
}
// TemplateOptions generates a CertificateOptions with the template and data
// defined in the ProvisionerOptions, the provisioner generated data, and the
// user data provided in the request. If no template has been provided,
// x509util.DefaultLeafTemplate will be used.
func TemplateOptions(o *Options, data x509util.TemplateData) (CertificateOptions, error) {
return CustomTemplateOptions(o, data, x509util.DefaultLeafTemplate)
}
// CustomTemplateOptions generates a CertificateOptions with the template, data
// defined in the ProvisionerOptions, the provisioner generated data and the
// user data provided in the request. If no template has been provided in the
// ProvisionerOptions, the given template will be used.
func CustomTemplateOptions(o *Options, data x509util.TemplateData, defaultTemplate string) (CertificateOptions, error) {
opts := o.GetX509Options()
if data == nil {
data = x509util.NewTemplateData()
}
if opts != nil {
// Add template data if any.
if len(opts.TemplateData) > 0 {
if err := json.Unmarshal(opts.TemplateData, &data); err != nil {
return nil, errors.Wrap(err, "error unmarshaling template data")
}
}
}
return certificateOptionsFunc(func(so SignOptions) []x509util.Option {
// We're not provided user data without custom templates.
if !opts.HasTemplate() {
return []x509util.Option{
x509util.WithTemplate(defaultTemplate, data),
}
}
// Add user provided data.
if len(so.TemplateData) > 0 {
userObject := make(map[string]interface{})
if err := json.Unmarshal(so.TemplateData, &userObject); err != nil {
data.SetUserData(map[string]interface{}{})
} else {
data.SetUserData(userObject)
}
}
// Load a template from a file if Template is not defined.
if opts.Template == "" && opts.TemplateFile != "" {
return []x509util.Option{
x509util.WithTemplateFile(opts.TemplateFile, data),
}
}
// Load a template from the Template fields
// 1. As a JSON in a string.
template := strings.TrimSpace(opts.Template)
if strings.HasPrefix(template, "{") {
return []x509util.Option{
x509util.WithTemplate(template, data),
}
}
// 2. As a base64 encoded JSON.
return []x509util.Option{
x509util.WithTemplateBase64(template, data),
}
}), nil
}
// unsafeParseSigned parses the given token and returns all the claims without
// verifying the signature of the token.
func unsafeParseSigned(s string) (map[string]interface{}, error) {
token, err := jose.ParseSigned(s)
if err != nil {
return nil, err
}
claims := make(map[string]interface{})
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
return nil, err
}
return claims, nil
}

View file

@ -0,0 +1,260 @@
package provisioner
import (
"bytes"
"crypto/x509"
"encoding/json"
"reflect"
"testing"
"github.com/smallstep/cli/crypto/pemutil"
"go.step.sm/crypto/x509util"
)
func parseCertificateRequest(t *testing.T, filename string) *x509.CertificateRequest {
t.Helper()
v, err := pemutil.Read(filename)
if err != nil {
t.Fatal(err)
}
csr, ok := v.(*x509.CertificateRequest)
if !ok {
t.Fatalf("%s is not a certificate request", filename)
}
return csr
}
func TestOptions_GetX509Options(t *testing.T) {
type fields struct {
o *Options
}
tests := []struct {
name string
fields fields
want *X509Options
}{
{"ok", fields{&Options{X509: &X509Options{Template: "foo"}}}, &X509Options{Template: "foo"}},
{"nil", fields{&Options{}}, nil},
{"nilOptions", fields{nil}, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.fields.o.GetX509Options(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Options.GetX509Options() = %v, want %v", got, tt.want)
}
})
}
}
func TestProvisionerX509Options_HasTemplate(t *testing.T) {
type fields struct {
Template string
TemplateFile string
TemplateData json.RawMessage
}
tests := []struct {
name string
fields fields
want bool
}{
{"template", fields{Template: "the template"}, true},
{"templateFile", fields{TemplateFile: "the template file"}, true},
{"false", fields{}, false},
{"falseWithTemplateData", fields{TemplateData: []byte(`{"foo":"bar"}`)}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &X509Options{
Template: tt.fields.Template,
TemplateFile: tt.fields.TemplateFile,
TemplateData: tt.fields.TemplateData,
}
if got := o.HasTemplate(); got != tt.want {
t.Errorf("ProvisionerOptions.HasTemplate() = %v, want %v", got, tt.want)
}
})
}
}
func TestTemplateOptions(t *testing.T) {
csr := parseCertificateRequest(t, "testdata/certs/ecdsa.csr")
data := x509util.TemplateData{
x509util.SubjectKey: x509util.Subject{
CommonName: "foobar",
},
x509util.SANsKey: []x509util.SubjectAlternativeName{
{Type: "dns", Value: "foo.com"},
},
}
type args struct {
o *Options
data x509util.TemplateData
}
tests := []struct {
name string
args args
want x509util.Options
wantErr bool
}{
{"ok", args{nil, data}, x509util.Options{
CertBuffer: bytes.NewBufferString(`{
"subject": {"commonName":"foobar"},
"sans": [{"type":"dns","value":"foo.com"}],
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["serverAuth", "clientAuth"]
}`)}, false},
{"okCustomTemplate", args{&Options{X509: &X509Options{Template: x509util.DefaultIIDLeafTemplate}}, data}, x509util.Options{
CertBuffer: bytes.NewBufferString(`{
"subject": {"commonName":"foo"},
"sans": [{"type":"dns","value":"foo.com"}],
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["serverAuth", "clientAuth"]
}`)}, false},
{"fail", args{&Options{X509: &X509Options{TemplateData: []byte(`{"badJSON`)}}, data}, x509util.Options{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cof, err := TemplateOptions(tt.args.o, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("TemplateOptions() error = %v, wantErr %v", err, tt.wantErr)
return
}
var opts x509util.Options
if cof != nil {
for _, fn := range cof.Options(SignOptions{}) {
if err := fn(csr, &opts); err != nil {
t.Errorf("x509util.Options() error = %v", err)
return
}
}
}
if !reflect.DeepEqual(opts, tt.want) {
t.Errorf("x509util.Option = %v, want %v", opts, tt.want)
}
})
}
}
func TestCustomTemplateOptions(t *testing.T) {
csr := parseCertificateRequest(t, "testdata/certs/ecdsa.csr")
csrCertificate := `{"version":0,"subject":{"commonName":"foo"},"dnsNames":["foo"],"emailAddresses":null,"ipAddresses":null,"uris":null,"extensions":[{"id":"2.5.29.17","critical":false,"value":"MAWCA2Zvbw=="}]}`
data := x509util.TemplateData{
x509util.SubjectKey: x509util.Subject{
CommonName: "foobar",
},
x509util.SANsKey: []x509util.SubjectAlternativeName{
{Type: "dns", Value: "foo.com"},
},
}
type args struct {
o *Options
data x509util.TemplateData
defaultTemplate string
userOptions SignOptions
}
tests := []struct {
name string
args args
want x509util.Options
wantErr bool
}{
{"ok", args{nil, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{
CertBuffer: bytes.NewBufferString(`{
"subject": {"commonName":"foobar"},
"sans": [{"type":"dns","value":"foo.com"}],
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["serverAuth", "clientAuth"]
}`)}, false},
{"okIID", args{nil, data, x509util.DefaultIIDLeafTemplate, SignOptions{}}, x509util.Options{
CertBuffer: bytes.NewBufferString(`{
"subject": {"commonName":"foo"},
"sans": [{"type":"dns","value":"foo.com"}],
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["serverAuth", "clientAuth"]
}`)}, false},
{"okNoData", args{&Options{}, nil, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{
CertBuffer: bytes.NewBufferString(`{
"subject": null,
"sans": null,
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["serverAuth", "clientAuth"]
}`)}, false},
{"okTemplateData", args{&Options{X509: &X509Options{TemplateData: []byte(`{"foo":"bar"}`)}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{
CertBuffer: bytes.NewBufferString(`{
"subject": {"commonName":"foobar"},
"sans": [{"type":"dns","value":"foo.com"}],
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["serverAuth", "clientAuth"]
}`)}, false},
{"okTemplate", args{&Options{X509: &X509Options{Template: "{{ toJson .Insecure.CR }}"}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{
CertBuffer: bytes.NewBufferString(csrCertificate)}, false},
{"okFile", args{&Options{X509: &X509Options{TemplateFile: "./testdata/templates/cr.tpl"}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{
CertBuffer: bytes.NewBufferString(csrCertificate)}, false},
{"okBase64", args{&Options{X509: &X509Options{Template: "e3sgdG9Kc29uIC5JbnNlY3VyZS5DUiB9fQ=="}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{
CertBuffer: bytes.NewBufferString(csrCertificate)}, false},
{"okUserOptions", args{&Options{X509: &X509Options{Template: `{"foo": "{{.Insecure.User.foo}}"}`}}, data, x509util.DefaultLeafTemplate, SignOptions{TemplateData: []byte(`{"foo":"bar"}`)}}, x509util.Options{
CertBuffer: bytes.NewBufferString(`{"foo": "bar"}`),
}, false},
{"okBadUserOptions", args{&Options{X509: &X509Options{Template: `{"foo": "{{.Insecure.User.foo}}"}`}}, data, x509util.DefaultLeafTemplate, SignOptions{TemplateData: []byte(`{"badJSON"}`)}}, x509util.Options{
CertBuffer: bytes.NewBufferString(`{"foo": "<no value>"}`),
}, false},
{"fail", args{&Options{X509: &X509Options{TemplateData: []byte(`{"badJSON`)}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{}, true},
{"failTemplateData", args{&Options{X509: &X509Options{TemplateData: []byte(`{"badJSON}`)}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cof, err := CustomTemplateOptions(tt.args.o, tt.args.data, tt.args.defaultTemplate)
if (err != nil) != tt.wantErr {
t.Errorf("CustomTemplateOptions() error = %v, wantErr %v", err, tt.wantErr)
return
}
var opts x509util.Options
if cof != nil {
for _, fn := range cof.Options(tt.args.userOptions) {
if err := fn(csr, &opts); err != nil {
t.Errorf("x509util.Options() error = %v", err)
return
}
}
}
if !reflect.DeepEqual(opts, tt.want) {
t.Errorf("x509util.Option = %v, want %v", opts, tt.want)
}
})
}
}
func Test_unsafeParseSigned(t *testing.T) {
okToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYW5lQGRvZS5jb20iLCJpc3MiOiJodHRwczovL2RvZS5jb20iLCJqdGkiOiI4ZmYzMjQ4MS1mZDVmLTRlMmUtOTZkZi05MDhjMTI3Yzg1ZjciLCJpYXQiOjE1OTUzNjAwMjgsImV4cCI6MTU5NTM2MzYyOH0.aid8UuhFucJOFHXaob9zpNtVvhul9ulTGsA52mU6XIw"
type args struct {
s string
}
tests := []struct {
name string
args args
want map[string]interface{}
wantErr bool
}{
{"ok", args{okToken}, map[string]interface{}{
"sub": "jane@doe.com",
"iss": "https://doe.com",
"jti": "8ff32481-fd5f-4e2e-96df-908c127c85f7",
"iat": float64(1595360028),
"exp": float64(1595363628),
}, false},
{"failToken", args{"foobar"}, nil, true},
{"failPayload", args{"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ew.aid8UuhFucJOFHXaob9zpNtVvhul9ulTGsA52mU6XIw"}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := unsafeParseSigned(tt.args.s)
if (err != nil) != tt.wantErr {
t.Errorf("unsafeParseSigned() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("unsafeParseSigned() = \n%v, want \n%v", got, tt.want)
}
})
}
}

View file

@ -245,7 +245,7 @@ func (l *List) UnmarshalJSON(data []byte) error {
continue
}
if err := json.Unmarshal(data, p); err != nil {
return errors.Errorf("error unmarshaling provisioner")
return errors.Wrap(err, "error unmarshaling provisioner")
}
*l = append(*l, p)
}

View file

@ -2,65 +2,77 @@ package provisioner
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"net"
"net/url"
"reflect"
"time"
"github.com/pkg/errors"
"github.com/smallstep/cli/crypto/x509util"
"golang.org/x/crypto/ed25519"
"go.step.sm/crypto/x509util"
)
// Options contains the options that can be passed to the Sign method. Backdate
// DefaultCertValidity is the default validity for a certificate if none is specified.
const DefaultCertValidity = 24 * time.Hour
// SignOptions contains the options that can be passed to the Sign method. Backdate
// is automatically filled and can only be configured in the CA.
type Options struct {
NotAfter TimeDuration `json:"notAfter"`
NotBefore TimeDuration `json:"notBefore"`
Backdate time.Duration `json:"-"`
type SignOptions struct {
NotAfter TimeDuration `json:"notAfter"`
NotBefore TimeDuration `json:"notBefore"`
TemplateData json.RawMessage `json:"templateData"`
Backdate time.Duration `json:"-"`
}
// SignOption is the interface used to collect all extra options used in the
// Sign method.
type SignOption interface{}
// CertificateValidator is the interface used to validate a X.509 certificate.
// CertificateValidator is an interface used to validate a given X.509 certificate.
type CertificateValidator interface {
SignOption
Valid(cert *x509.Certificate, o Options) error
Valid(cert *x509.Certificate, opts SignOptions) error
}
// CertificateRequestValidator is the interface used to validate a X.509
// certificate request.
// CertificateRequestValidator is an interface used to validate a given X.509 certificate request.
type CertificateRequestValidator interface {
SignOption
Valid(req *x509.CertificateRequest) error
Valid(cr *x509.CertificateRequest) error
}
// ProfileModifier is the interface used to add custom options to the profile
// constructor. The options are used to modify the final certificate.
type ProfileModifier interface {
SignOption
Option(o Options) x509util.WithOption
// CertificateModifier is an interface used to modify a given X.509 certificate.
// Types implementing this interface will be validated with a
// CertificateValidator.
type CertificateModifier interface {
Modify(cert *x509.Certificate, opts SignOptions) error
}
// CertificateEnforcer is the interface used to modify a certificate after
// validation.
// CertificateEnforcer is an interface used to modify a given X.509 certificate.
// Types implemented this interface will NOT be validated with a
// CertificateValidator.
type CertificateEnforcer interface {
SignOption
Enforce(cert *x509.Certificate) error
}
// profileWithOption is a wrapper against x509util.WithOption to conform the
// interface.
type profileWithOption x509util.WithOption
// CertificateModifierFunc allows to create simple certificate modifiers just
// with a function.
type CertificateModifierFunc func(cert *x509.Certificate, opts SignOptions) error
func (v profileWithOption) Option(Options) x509util.WithOption {
return x509util.WithOption(v)
// Modify implements CertificateModifier and just calls the defined function.
func (fn CertificateModifierFunc) Modify(cert *x509.Certificate, opts SignOptions) error {
return fn(cert, opts)
}
// CertificateEnforcerFunc allows to create simple certificate enforcer just
// with a function.
type CertificateEnforcerFunc func(cert *x509.Certificate) error
// Modify implements CertificateEnforcer and just calls the defined function.
func (fn CertificateEnforcerFunc) Enforce(cert *x509.Certificate) error {
return fn(cert)
}
// emailOnlyIdentity is a CertificateRequestValidator that checks that the only
@ -254,27 +266,30 @@ func (eee ExtraExtsEnforcer) Enforce(cert *x509.Certificate) error {
return nil
}
// profileDefaultDuration is a wrapper against x509util.WithOption to conform
// the SignOption interface.
// profileDefaultDuration is a modifier that sets the certificate
// duration.
type profileDefaultDuration time.Duration
func (v profileDefaultDuration) Option(so Options) x509util.WithOption {
func (v profileDefaultDuration) Modify(cert *x509.Certificate, so SignOptions) error {
var backdate time.Duration
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = now()
backdate = -1 * so.Backdate
}
notAfter := so.NotAfter.RelativeTime(notBefore)
return func(p x509util.Profile) error {
fn := x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v))
if err := fn(p); err != nil {
return err
if notAfter.IsZero() {
if v != 0 {
notAfter = notBefore.Add(time.Duration(v))
} else {
notAfter = notBefore.Add(DefaultCertValidity)
}
crt := p.Subject()
crt.NotBefore = crt.NotBefore.Add(backdate)
return nil
}
cert.NotBefore = notBefore.Add(backdate)
cert.NotAfter = notAfter
return nil
}
// profileLimitDuration is an x509 profile option that modifies an x509 validity
@ -286,40 +301,37 @@ type profileLimitDuration struct {
// Option returns an x509util option that limits the validity period of a
// certificate to one that is superficially imposed.
func (v profileLimitDuration) Option(so Options) x509util.WithOption {
return func(p x509util.Profile) error {
var backdate time.Duration
n := now()
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = n
backdate = -1 * so.Backdate
}
if notBefore.Before(v.notBefore) {
return errors.Errorf("requested certificate notBefore (%s) is before "+
"the active validity window of the provisioning credential (%s)",
notBefore, v.notBefore)
}
notAfter := so.NotAfter.RelativeTime(notBefore)
if notAfter.After(v.notAfter) {
return errors.Errorf("requested certificate notAfter (%s) is after "+
"the expiration of the provisioning credential (%s)",
notAfter, v.notAfter)
}
if notAfter.IsZero() {
t := notBefore.Add(v.def)
if t.After(v.notAfter) {
notAfter = v.notAfter
} else {
notAfter = t
}
}
crt := p.Subject()
crt.NotBefore = notBefore.Add(backdate)
crt.NotAfter = notAfter
return nil
func (v profileLimitDuration) Modify(cert *x509.Certificate, so SignOptions) error {
var backdate time.Duration
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = now()
backdate = -1 * so.Backdate
}
if notBefore.Before(v.notBefore) {
return errors.Errorf("requested certificate notBefore (%s) is before "+
"the active validity window of the provisioning credential (%s)",
notBefore, v.notBefore)
}
notAfter := so.NotAfter.RelativeTime(notBefore)
if notAfter.After(v.notAfter) {
return errors.Errorf("requested certificate notAfter (%s) is after "+
"the expiration of the provisioning credential (%s)",
notAfter, v.notAfter)
}
if notAfter.IsZero() {
t := notBefore.Add(v.def)
if t.After(v.notAfter) {
notAfter = v.notAfter
} else {
notAfter = t
}
}
cert.NotBefore = notBefore.Add(backdate)
cert.NotAfter = notAfter
return nil
}
// validityValidator validates the certificate validity settings.
@ -335,7 +347,7 @@ func newValidityValidator(min, max time.Duration) *validityValidator {
// Valid validates the certificate validity settings (notBefore/notAfter) and
// and total duration.
func (v *validityValidator) Valid(cert *x509.Certificate, o Options) error {
func (v *validityValidator) Valid(cert *x509.Certificate, o SignOptions) error {
var (
na = cert.NotAfter.Truncate(time.Second)
nb = cert.NotBefore.Truncate(time.Second)
@ -385,22 +397,21 @@ func newForceCNOption(forceCN bool) *forceCNOption {
return &forceCNOption{forceCN}
}
func (o *forceCNOption) Option(Options) x509util.WithOption {
return func(p x509util.Profile) error {
if !o.ForceCN {
// Forcing CN is disabled, do nothing to certificate
return nil
}
crt := p.Subject()
if crt.Subject.CommonName == "" {
if len(crt.DNSNames) > 0 {
crt.Subject.CommonName = crt.DNSNames[0]
} else {
return errors.New("Cannot force CN, DNSNames is empty")
}
}
func (o *forceCNOption) Modify(cert *x509.Certificate, _ SignOptions) error {
if !o.ForceCN {
// Forcing CN is disabled, do nothing to certificate
return nil
}
if cert.Subject.CommonName == "" {
if len(cert.DNSNames) > 0 {
cert.Subject.CommonName = cert.DNSNames[0]
} else {
return errors.New("Cannot force CN, DNSNames is empty")
}
}
return nil
}
type provisionerExtensionOption struct {
@ -419,23 +430,20 @@ func newProvisionerExtensionOption(typ Type, name, credentialID string, keyValue
}
}
func (o *provisionerExtensionOption) Option(Options) x509util.WithOption {
return func(p x509util.Profile) error {
crt := p.Subject()
ext, err := createProvisionerExtension(o.Type, o.Name, o.CredentialID, o.KeyValuePairs...)
if err != nil {
return err
}
// Prepend the provisioner extension. In the auth.Sign code we will
// force the resulting certificate to only have one extension, the
// first stepOIDProvisioner that is found in the ExtraExtensions.
// A client could pass a csr containing a malicious stepOIDProvisioner
// ExtraExtension. If we were to append (rather than prepend) the correct
// stepOIDProvisioner extension, then the resulting certificate would
// contain the malicious extension, rather than the one applied by step-ca.
crt.ExtraExtensions = append([]pkix.Extension{ext}, crt.ExtraExtensions...)
return nil
func (o *provisionerExtensionOption) Modify(cert *x509.Certificate, _ SignOptions) error {
ext, err := createProvisionerExtension(o.Type, o.Name, o.CredentialID, o.KeyValuePairs...)
if err != nil {
return err
}
// Prepend the provisioner extension. In the auth.Sign code we will
// force the resulting certificate to only have one extension, the
// first stepOIDProvisioner that is found in the ExtraExtensions.
// A client could pass a csr containing a malicious stepOIDProvisioner
// ExtraExtension. If we were to append (rather than prepend) the correct
// stepOIDProvisioner extension, then the resulting certificate would
// contain the malicious extension, rather than the one applied by step-ca.
cert.ExtraExtensions = append([]pkix.Extension{ext}, cert.ExtraExtensions...)
return nil
}
func createProvisionerExtension(typ int, name, credentialID string, keyValuePairs ...string) (pkix.Extension, error) {
@ -454,8 +462,3 @@ func createProvisionerExtension(typ int, name, credentialID string, keyValuePair
Value: b,
}, nil
}
func init() {
// Avoid dead-code warning in profileWithOption
_ = profileWithOption(nil)
}

View file

@ -13,7 +13,6 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/x509util"
)
func Test_emailOnlyIdentity_Valid(t *testing.T) {
@ -400,7 +399,7 @@ func Test_ExtraExtsEnforcer_Enforce(t *testing.T) {
func Test_validityValidator_Valid(t *testing.T) {
type test struct {
cert *x509.Certificate
opts Options
opts SignOptions
vv *validityValidator
err error
}
@ -409,7 +408,7 @@ func Test_validityValidator_Valid(t *testing.T) {
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotAfter: time.Now().Add(-5 * time.Minute)},
opts: Options{},
opts: SignOptions{},
err: errors.New("notAfter cannot be in the past"),
}
},
@ -418,7 +417,7 @@ func Test_validityValidator_Valid(t *testing.T) {
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotBefore: time.Now().Add(10 * time.Minute),
NotAfter: time.Now().Add(5 * time.Minute)},
opts: Options{},
opts: SignOptions{},
err: errors.New("notAfter cannot be before notBefore"),
}
},
@ -428,7 +427,7 @@ func Test_validityValidator_Valid(t *testing.T) {
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotBefore: n,
NotAfter: n.Add(3 * time.Minute)},
opts: Options{},
opts: SignOptions{},
err: errors.New("is less than the authorized minimum certificate duration of "),
}
},
@ -438,7 +437,7 @@ func Test_validityValidator_Valid(t *testing.T) {
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotBefore: n,
NotAfter: n.Add(5 * time.Minute)},
opts: Options{},
opts: SignOptions{},
}
},
"fail/duration-too-great": func() test {
@ -465,7 +464,7 @@ func Test_validityValidator_Valid(t *testing.T) {
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: cert,
opts: Options{Backdate: time.Second},
opts: SignOptions{Backdate: time.Second},
}
},
"ok/duration-exact-max-with-backdate": func() test {
@ -476,7 +475,7 @@ func Test_validityValidator_Valid(t *testing.T) {
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: cert,
opts: Options{Backdate: backdate},
opts: SignOptions{Backdate: backdate},
}
},
}
@ -497,7 +496,7 @@ func Test_validityValidator_Valid(t *testing.T) {
func Test_forceCN_Option(t *testing.T) {
type test struct {
so Options
so SignOptions
fcn forceCNOption
cert *x509.Certificate
valid func(*x509.Certificate)
@ -508,7 +507,7 @@ func Test_forceCN_Option(t *testing.T) {
"ok/CN-not-forced": func() test {
return test{
fcn: forceCNOption{false},
so: Options{},
so: SignOptions{},
cert: &x509.Certificate{
Subject: pkix.Name{},
DNSNames: []string{"acme.example.com", "step.example.com"},
@ -521,7 +520,7 @@ func Test_forceCN_Option(t *testing.T) {
"ok/CN-forced-and-set": func() test {
return test{
fcn: forceCNOption{true},
so: Options{},
so: SignOptions{},
cert: &x509.Certificate{
Subject: pkix.Name{
CommonName: "Some Common Name",
@ -536,7 +535,7 @@ func Test_forceCN_Option(t *testing.T) {
"ok/CN-forced-and-not-set": func() test {
return test{
fcn: forceCNOption{true},
so: Options{},
so: SignOptions{},
cert: &x509.Certificate{
Subject: pkix.Name{},
DNSNames: []string{"acme.example.com", "step.example.com"},
@ -549,7 +548,7 @@ func Test_forceCN_Option(t *testing.T) {
"fail/CN-forced-and-empty-DNSNames": func() test {
return test{
fcn: forceCNOption{true},
so: Options{},
so: SignOptions{},
cert: &x509.Certificate{
Subject: pkix.Name{},
DNSNames: []string{},
@ -562,15 +561,13 @@ func Test_forceCN_Option(t *testing.T) {
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
prof := &x509util.Leaf{}
prof.SetSubject(tt.cert)
if err := tt.fcn.Option(tt.so)(prof); err != nil {
if err := tt.fcn.Modify(tt.cert, tt.so); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
} else {
if assert.Nil(t, tt.err) {
tt.valid(prof.Subject())
tt.valid(tt.cert)
}
}
})
@ -579,7 +576,7 @@ func Test_forceCN_Option(t *testing.T) {
func Test_profileDefaultDuration_Option(t *testing.T) {
type test struct {
so Options
so SignOptions
pdd profileDefaultDuration
cert *x509.Certificate
valid func(*x509.Certificate)
@ -588,7 +585,7 @@ func Test_profileDefaultDuration_Option(t *testing.T) {
"ok/notBefore-notAfter-duration-empty": func() test {
return test{
pdd: profileDefaultDuration(0),
so: Options{},
so: SignOptions{},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
n := now()
@ -604,7 +601,7 @@ func Test_profileDefaultDuration_Option(t *testing.T) {
nb := time.Now().Add(5 * time.Minute).UTC()
return test{
pdd: profileDefaultDuration(0),
so: Options{NotBefore: NewTimeDuration(nb)},
so: SignOptions{NotBefore: NewTimeDuration(nb)},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, nb)
@ -616,7 +613,7 @@ func Test_profileDefaultDuration_Option(t *testing.T) {
d := 4 * time.Hour
return test{
pdd: profileDefaultDuration(d),
so: Options{Backdate: time.Second},
so: SignOptions{Backdate: time.Second},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
n := now()
@ -632,7 +629,7 @@ func Test_profileDefaultDuration_Option(t *testing.T) {
na := now().Add(10 * time.Minute).UTC()
return test{
pdd: profileDefaultDuration(0),
so: Options{NotAfter: NewTimeDuration(na)},
so: SignOptions{NotAfter: NewTimeDuration(na)},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
n := now()
@ -649,7 +646,7 @@ func Test_profileDefaultDuration_Option(t *testing.T) {
d := 4 * time.Hour
return test{
pdd: profileDefaultDuration(d),
so: Options{NotBefore: NewTimeDuration(nb), NotAfter: NewTimeDuration(na)},
so: SignOptions{NotBefore: NewTimeDuration(nb), NotAfter: NewTimeDuration(na)},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, nb)
@ -661,10 +658,9 @@ func Test_profileDefaultDuration_Option(t *testing.T) {
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
prof := &x509util.Leaf{}
prof.SetSubject(tt.cert)
assert.FatalError(t, tt.pdd.Option(tt.so)(prof), "unexpected error")
tt.valid(prof.Subject())
assert.FatalError(t, tt.pdd.Modify(tt.cert, tt.so), "unexpected error")
time.Sleep(1 * time.Nanosecond)
tt.valid(tt.cert)
})
}
}
@ -702,10 +698,8 @@ func Test_newProvisionerExtension_Option(t *testing.T) {
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
prof := &x509util.Leaf{}
prof.SetSubject(tt.cert)
assert.FatalError(t, newProvisionerExtensionOption(TypeJWK, "foo", "bar", "baz", "zap").Option(Options{})(prof))
tt.valid(prof.Subject())
assert.FatalError(t, newProvisionerExtensionOption(TypeJWK, "foo", "bar", "baz", "zap").Modify(tt.cert, SignOptions{}))
tt.valid(tt.cert)
})
}
}
@ -716,7 +710,7 @@ func Test_profileLimitDuration_Option(t *testing.T) {
type test struct {
pld profileLimitDuration
so Options
so SignOptions
cert *x509.Certificate
valid func(*x509.Certificate)
err error
@ -727,7 +721,7 @@ func Test_profileLimitDuration_Option(t *testing.T) {
assert.FatalError(t, err)
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notBefore: n.Add(8 * time.Hour)},
so: Options{NotBefore: d},
so: SignOptions{NotBefore: d},
cert: new(x509.Certificate),
err: errors.New("requested certificate notBefore ("),
}
@ -737,7 +731,7 @@ func Test_profileLimitDuration_Option(t *testing.T) {
assert.FatalError(t, err)
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d},
so: SignOptions{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d},
cert: new(x509.Certificate),
err: errors.New("requested certificate notAfter ("),
}
@ -747,7 +741,7 @@ func Test_profileLimitDuration_Option(t *testing.T) {
assert.FatalError(t, err)
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d, Backdate: 1 * time.Minute},
so: SignOptions{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d, Backdate: 1 * time.Minute},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))
@ -758,7 +752,7 @@ func Test_profileLimitDuration_Option(t *testing.T) {
"ok/valid-notAfter-nil-limit-over-default": func() test {
return test{
pld: profileLimitDuration{def: 1 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), Backdate: 1 * time.Minute},
so: SignOptions{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), Backdate: 1 * time.Minute},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))
@ -769,7 +763,7 @@ func Test_profileLimitDuration_Option(t *testing.T) {
"ok/valid-notAfter-nil-limit-under-default": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), Backdate: 1 * time.Minute},
so: SignOptions{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), Backdate: 1 * time.Minute},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))
@ -780,7 +774,7 @@ func Test_profileLimitDuration_Option(t *testing.T) {
"ok/over-limit-with-backdate": func() test {
return test{
pld: profileLimitDuration{def: 24 * time.Hour, notAfter: n.Add(6 * time.Hour)},
so: Options{Backdate: 1 * time.Minute},
so: SignOptions{Backdate: 1 * time.Minute},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, n.Add(-time.Minute))
@ -791,7 +785,7 @@ func Test_profileLimitDuration_Option(t *testing.T) {
"ok/under-limit-with-backdate": func() test {
return test{
pld: profileLimitDuration{def: 24 * time.Hour, notAfter: n.Add(30 * time.Hour)},
so: Options{Backdate: 1 * time.Minute},
so: SignOptions{Backdate: 1 * time.Minute},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, n.Add(-time.Minute))
@ -803,15 +797,13 @@ func Test_profileLimitDuration_Option(t *testing.T) {
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
prof := &x509util.Leaf{}
prof.SetSubject(tt.cert)
if err := tt.pld.Option(tt.so)(prof); err != nil {
if err := tt.pld.Modify(tt.cert, tt.so); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
} else {
if assert.Nil(t, tt.err) {
tt.valid(prof.Subject())
tt.valid(tt.cert)
}
}
})

View file

@ -30,20 +30,20 @@ type SSHCertModifier interface {
// to modify the SSH certificate.
type SSHCertOptionModifier interface {
SignOption
Option(o SSHOptions) SSHCertModifier
Option(o SignSSHOptions) SSHCertModifier
}
// SSHCertValidator is the interface used to validate an SSH certificate.
type SSHCertValidator interface {
SignOption
Valid(cert *ssh.Certificate, opts SSHOptions) error
Valid(cert *ssh.Certificate, opts SignSSHOptions) error
}
// SSHCertOptionsValidator is the interface used to validate the custom
// options used to modify the SSH certificate.
type SSHCertOptionsValidator interface {
SignOption
Valid(got SSHOptions) error
Valid(got SignSSHOptions) error
}
// sshModifierFunc is an adapter to allow the use of ordinary functions as SSH
@ -54,8 +54,8 @@ func (f sshModifierFunc) Modify(cert *ssh.Certificate) error {
return f(cert)
}
// SSHOptions contains the options that can be passed to the SignSSH method.
type SSHOptions struct {
// SignSSHOptions contains the options that can be passed to the SignSSH method.
type SignSSHOptions struct {
CertType string `json:"certType"`
KeyID string `json:"keyID"`
Principals []string `json:"principals"`
@ -65,12 +65,12 @@ type SSHOptions struct {
}
// Type returns the uint32 representation of the CertType.
func (o SSHOptions) Type() uint32 {
func (o SignSSHOptions) Type() uint32 {
return sshCertTypeUInt32(o.CertType)
}
// Modify implements SSHCertModifier and sets the SSHOption in the ssh.Certificate.
func (o SSHOptions) Modify(cert *ssh.Certificate) error {
func (o SignSSHOptions) Modify(cert *ssh.Certificate) error {
switch o.CertType {
case "": // ignore
case SSHUserCert:
@ -100,7 +100,7 @@ func (o SSHOptions) Modify(cert *ssh.Certificate) error {
// match compares two SSHOptions and return an error if they don't match. It
// ignores zero values.
func (o SSHOptions) match(got SSHOptions) error {
func (o SignSSHOptions) match(got SignSSHOptions) error {
if o.CertType != "" && got.CertType != "" && o.CertType != got.CertType {
return errors.Errorf("ssh certificate type does not match - got %v, want %v", got.CertType, o.CertType)
}
@ -165,7 +165,7 @@ func (m sshCertValidBeforeModifier) Modify(cert *ssh.Certificate) error {
// sshCertDefaultsModifier implements a SSHCertModifier that
// modifies the certificate with the given options if they are not set.
type sshCertDefaultsModifier SSHOptions
type sshCertDefaultsModifier SignSSHOptions
// Modify implements the SSHCertModifier interface.
func (m sshCertDefaultsModifier) Modify(cert *ssh.Certificate) error {
@ -215,7 +215,7 @@ type sshDefaultDuration struct {
*Claimer
}
func (m *sshDefaultDuration) Option(o SSHOptions) SSHCertModifier {
func (m *sshDefaultDuration) Option(o SignSSHOptions) SSHCertModifier {
return sshModifierFunc(func(cert *ssh.Certificate) error {
d, err := m.DefaultSSHCertDuration(cert.CertType)
if err != nil {
@ -248,7 +248,7 @@ type sshLimitDuration struct {
NotAfter time.Time
}
func (m *sshLimitDuration) Option(o SSHOptions) SSHCertModifier {
func (m *sshLimitDuration) Option(o SignSSHOptions) SSHCertModifier {
if m.NotAfter.IsZero() {
defaultDuration := &sshDefaultDuration{m.Claimer}
return defaultDuration.Option(o)
@ -297,12 +297,12 @@ func (m *sshLimitDuration) Option(o SSHOptions) SSHCertModifier {
// sshCertOptionsValidator validates the user SSHOptions with the ones
// usually present in the token.
type sshCertOptionsValidator SSHOptions
type sshCertOptionsValidator SignSSHOptions
// Valid implements SSHCertOptionsValidator and returns nil if both
// SSHOptions match.
func (v sshCertOptionsValidator) Valid(got SSHOptions) error {
want := SSHOptions(v)
func (v sshCertOptionsValidator) Valid(got SignSSHOptions) error {
want := SignSSHOptions(v)
return want.match(got)
}
@ -310,7 +310,7 @@ type sshCertValidityValidator struct {
*Claimer
}
func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate, opts SSHOptions) error {
func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate, opts SignSSHOptions) error {
switch {
case cert.ValidAfter == 0:
return errors.New("ssh certificate validAfter cannot be 0")
@ -355,7 +355,7 @@ func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate, opts SSHOptions)
type sshCertDefaultValidator struct{}
// Valid returns an error if the given certificate does not contain the necessary fields.
func (v *sshCertDefaultValidator) Valid(cert *ssh.Certificate, o SSHOptions) error {
func (v *sshCertDefaultValidator) Valid(cert *ssh.Certificate, o SignSSHOptions) error {
switch {
case len(cert.Nonce) == 0:
return errors.New("ssh certificate nonce cannot be empty")
@ -390,7 +390,7 @@ func (v *sshCertDefaultValidator) Valid(cert *ssh.Certificate, o SSHOptions) err
type sshDefaultPublicKeyValidator struct{}
// Valid checks that certificate request common name matches the one configured.
func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SSHOptions) error {
func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SignSSHOptions) error {
if cert.Key == nil {
return errors.New("ssh certificate key cannot be nil")
}
@ -420,7 +420,7 @@ func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, o SSHOptions)
type sshCertKeyIDValidator string
// Valid returns an error if the given certificate does not contain the necessary fields.
func (v sshCertKeyIDValidator) Valid(cert *ssh.Certificate, o SSHOptions) error {
func (v sshCertKeyIDValidator) Valid(cert *ssh.Certificate, o SignSSHOptions) error {
if string(v) != cert.KeyId {
return errors.Errorf("invalid ssh certificate KeyId; want %s, but got %s", string(v), cert.KeyId)
}

View file

@ -28,7 +28,7 @@ func TestSSHOptions_Type(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := SSHOptions{
o := SignSSHOptions{
CertType: tt.fields.CertType,
}
if got := o.Type(); got != tt.want {
@ -40,7 +40,7 @@ func TestSSHOptions_Type(t *testing.T) {
func TestSSHOptions_Modify(t *testing.T) {
type test struct {
so *SSHOptions
so *SignSSHOptions
cert *ssh.Certificate
valid func(*ssh.Certificate)
err error
@ -48,21 +48,21 @@ func TestSSHOptions_Modify(t *testing.T) {
tests := map[string](func() test){
"fail/unexpected-cert-type": func() test {
return test{
so: &SSHOptions{CertType: "foo"},
so: &SignSSHOptions{CertType: "foo"},
cert: new(ssh.Certificate),
err: errors.Errorf("ssh certificate has an unknown type - foo"),
}
},
"fail/validAfter-greater-validBefore": func() test {
return test{
so: &SSHOptions{CertType: "user"},
so: &SignSSHOptions{CertType: "user"},
cert: &ssh.Certificate{ValidAfter: uint64(15), ValidBefore: uint64(10)},
err: errors.Errorf("ssh certificate valid after cannot be greater than valid before"),
}
},
"ok/user-cert": func() test {
return test{
so: &SSHOptions{CertType: "user"},
so: &SignSSHOptions{CertType: "user"},
cert: new(ssh.Certificate),
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.CertType, uint32(ssh.UserCert))
@ -71,7 +71,7 @@ func TestSSHOptions_Modify(t *testing.T) {
},
"ok/host-cert": func() test {
return test{
so: &SSHOptions{CertType: "host"},
so: &SignSSHOptions{CertType: "host"},
cert: new(ssh.Certificate),
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.CertType, uint32(ssh.HostCert))
@ -81,7 +81,7 @@ func TestSSHOptions_Modify(t *testing.T) {
"ok": func() test {
va := time.Now().Add(5 * time.Minute)
vb := time.Now().Add(1 * time.Hour)
so := &SSHOptions{CertType: "host", KeyID: "foo", Principals: []string{"foo", "bar"},
so := &SignSSHOptions{CertType: "host", KeyID: "foo", Principals: []string{"foo", "bar"},
ValidAfter: NewTimeDuration(va), ValidBefore: NewTimeDuration(vb)}
return test{
so: so,
@ -114,43 +114,43 @@ func TestSSHOptions_Modify(t *testing.T) {
func TestSSHOptions_Match(t *testing.T) {
type test struct {
so SSHOptions
cmp SSHOptions
so SignSSHOptions
cmp SignSSHOptions
err error
}
tests := map[string](func() test){
"fail/cert-type": func() test {
return test{
so: SSHOptions{CertType: "foo"},
cmp: SSHOptions{CertType: "bar"},
so: SignSSHOptions{CertType: "foo"},
cmp: SignSSHOptions{CertType: "bar"},
err: errors.Errorf("ssh certificate type does not match - got bar, want foo"),
}
},
"fail/pricipals": func() test {
return test{
so: SSHOptions{Principals: []string{"foo"}},
cmp: SSHOptions{Principals: []string{"bar"}},
so: SignSSHOptions{Principals: []string{"foo"}},
cmp: SignSSHOptions{Principals: []string{"bar"}},
err: errors.Errorf("ssh certificate principals does not match - got [bar], want [foo]"),
}
},
"fail/validAfter": func() test {
return test{
so: SSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute))},
cmp: SSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(5 * time.Minute))},
so: SignSSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute))},
cmp: SignSSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(5 * time.Minute))},
err: errors.Errorf("ssh certificate valid after does not match"),
}
},
"fail/validBefore": func() test {
return test{
so: SSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(1 * time.Minute))},
cmp: SSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute))},
so: SignSSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(1 * time.Minute))},
cmp: SignSSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute))},
err: errors.Errorf("ssh certificate valid before does not match"),
}
},
"ok/original-empty": func() test {
return test{
so: SSHOptions{},
cmp: SSHOptions{
so: SignSSHOptions{},
cmp: SignSSHOptions{
CertType: "foo",
Principals: []string{"foo"},
ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute)),
@ -160,8 +160,8 @@ func TestSSHOptions_Match(t *testing.T) {
},
"ok/cmp-empty": func() test {
return test{
cmp: SSHOptions{},
so: SSHOptions{
cmp: SignSSHOptions{},
so: SignSSHOptions{
CertType: "foo",
Principals: []string{"foo"},
ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute)),
@ -174,13 +174,13 @@ func TestSSHOptions_Match(t *testing.T) {
va := NewTimeDuration(n.Add(1 * time.Minute))
vb := NewTimeDuration(n.Add(5 * time.Minute))
return test{
cmp: SSHOptions{
cmp: SignSSHOptions{
CertType: "foo",
Principals: []string{"foo"},
ValidAfter: va,
ValidBefore: vb,
},
so: SSHOptions{
so: SignSSHOptions{
CertType: "foo",
Principals: []string{"foo"},
ValidAfter: va,
@ -330,7 +330,7 @@ func Test_sshCertDefaultsModifier_Modify(t *testing.T) {
n := time.Now()
va := NewTimeDuration(n.Add(1 * time.Minute))
vb := NewTimeDuration(n.Add(5 * time.Minute))
so := SSHOptions{
so := SignSSHOptions{
Principals: []string{"foo", "bar"},
CertType: "host",
ValidAfter: va,
@ -349,7 +349,7 @@ func Test_sshCertDefaultsModifier_Modify(t *testing.T) {
},
"ok/no-changes": func() test {
n := time.Now()
so := SSHOptions{
so := SignSSHOptions{
Principals: []string{"foo", "bar"},
CertType: "host",
ValidAfter: NewTimeDuration(n.Add(15 * time.Minute)),
@ -659,7 +659,7 @@ func Test_sshCertDefaultValidator_Valid(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := v.Valid(tt.cert, SSHOptions{}); err != nil {
if err := v.Valid(tt.cert, SignSSHOptions{}); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
@ -678,31 +678,31 @@ func Test_sshCertValidityValidator(t *testing.T) {
tests := []struct {
name string
cert *ssh.Certificate
opts SSHOptions
opts SignSSHOptions
err error
}{
{
"fail/validAfter-0",
&ssh.Certificate{CertType: ssh.UserCert},
SSHOptions{},
SignSSHOptions{},
errors.New("ssh certificate validAfter cannot be 0"),
},
{
"fail/validBefore-in-past",
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(-time.Minute).Unix())},
SSHOptions{},
SignSSHOptions{},
errors.New("ssh certificate validBefore cannot be in the past"),
},
{
"fail/validBefore-before-validAfter",
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Add(5 * time.Minute).Unix()), ValidBefore: uint64(now().Add(3 * time.Minute).Unix())},
SSHOptions{},
SignSSHOptions{},
errors.New("ssh certificate validBefore cannot be before validAfter"),
},
{
"fail/cert-type-not-set",
&ssh.Certificate{ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(10 * time.Minute).Unix())},
SSHOptions{},
SignSSHOptions{},
errors.New("ssh certificate type has not been set"),
},
{
@ -712,7 +712,7 @@ func Test_sshCertValidityValidator(t *testing.T) {
ValidAfter: uint64(now().Unix()),
ValidBefore: uint64(now().Add(10 * time.Minute).Unix()),
},
SSHOptions{},
SignSSHOptions{},
errors.New("unknown ssh certificate type 3"),
},
{
@ -722,7 +722,7 @@ func Test_sshCertValidityValidator(t *testing.T) {
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(4 * time.Minute).Unix()),
},
SSHOptions{Backdate: time.Second},
SignSSHOptions{Backdate: time.Second},
errors.New("requested duration of 4m0s is less than minimum accepted duration for selected provisioner of 5m0s"),
},
{
@ -732,7 +732,7 @@ func Test_sshCertValidityValidator(t *testing.T) {
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(5 * time.Minute).Unix()),
},
SSHOptions{Backdate: time.Second},
SignSSHOptions{Backdate: time.Second},
nil,
},
{
@ -742,7 +742,7 @@ func Test_sshCertValidityValidator(t *testing.T) {
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(48 * time.Hour).Unix()),
},
SSHOptions{Backdate: time.Second},
SignSSHOptions{Backdate: time.Second},
errors.New("requested duration of 48h0m0s is greater than maximum accepted duration for selected provisioner of 24h0m1s"),
},
{
@ -752,7 +752,7 @@ func Test_sshCertValidityValidator(t *testing.T) {
ValidAfter: uint64(n.Unix()),
ValidBefore: uint64(n.Add(24*time.Hour + time.Second).Unix()),
},
SSHOptions{Backdate: time.Second},
SignSSHOptions{Backdate: time.Second},
nil,
},
{
@ -762,7 +762,7 @@ func Test_sshCertValidityValidator(t *testing.T) {
ValidAfter: uint64(now().Unix()),
ValidBefore: uint64(now().Add(8 * time.Hour).Unix()),
},
SSHOptions{Backdate: time.Second},
SignSSHOptions{Backdate: time.Second},
nil,
},
}
@ -908,7 +908,7 @@ func Test_sshValidityModifier(t *testing.T) {
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
if err := tt.svm.Option(SSHOptions{}).Modify(tt.cert); err != nil {
if err := tt.svm.Option(SignSSHOptions{}).Modify(tt.cert); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
@ -962,7 +962,7 @@ func Test_sshDefaultDuration_Option(t *testing.T) {
Claimer *Claimer
}
type args struct {
o SSHOptions
o SignSSHOptions
cert *ssh.Certificate
}
tests := []struct {
@ -972,26 +972,26 @@ func Test_sshDefaultDuration_Option(t *testing.T) {
want *ssh.Certificate
wantErr bool
}{
{"user", fields{newClaimer(nil)}, args{SSHOptions{}, &ssh.Certificate{CertType: ssh.UserCert}},
{"user", fields{newClaimer(nil)}, args{SignSSHOptions{}, &ssh.Certificate{CertType: ssh.UserCert}},
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(0), ValidBefore: unix(16 * time.Hour)}, false},
{"host", fields{newClaimer(nil)}, args{SSHOptions{}, &ssh.Certificate{CertType: ssh.HostCert}},
{"host", fields{newClaimer(nil)}, args{SignSSHOptions{}, &ssh.Certificate{CertType: ssh.HostCert}},
&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(0), ValidBefore: unix(30 * 24 * time.Hour)}, false},
{"user claim", fields{newClaimer(&Claims{DefaultUserSSHDur: &Duration{1 * time.Hour}})}, args{SSHOptions{}, &ssh.Certificate{CertType: ssh.UserCert}},
{"user claim", fields{newClaimer(&Claims{DefaultUserSSHDur: &Duration{1 * time.Hour}})}, args{SignSSHOptions{}, &ssh.Certificate{CertType: ssh.UserCert}},
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(0), ValidBefore: unix(1 * time.Hour)}, false},
{"host claim", fields{newClaimer(&Claims{DefaultHostSSHDur: &Duration{1 * time.Hour}})}, args{SSHOptions{}, &ssh.Certificate{CertType: ssh.HostCert}},
{"host claim", fields{newClaimer(&Claims{DefaultHostSSHDur: &Duration{1 * time.Hour}})}, args{SignSSHOptions{}, &ssh.Certificate{CertType: ssh.HostCert}},
&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(0), ValidBefore: unix(1 * time.Hour)}, false},
{"user backdate", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert}},
{"user backdate", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert}},
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(16 * time.Hour)}, false},
{"host backdate", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert}},
{"host backdate", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert}},
&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(30 * 24 * time.Hour)}, false},
{"user validAfter", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(1 * time.Hour)}},
{"user validAfter", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(1 * time.Hour)}},
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(time.Hour), ValidBefore: unix(17 * time.Hour)}, false},
{"user validBefore", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidBefore: unix(1 * time.Hour)}},
{"user validBefore", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidBefore: unix(1 * time.Hour)}},
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(time.Hour)}, false},
{"host validAfter validBefore", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}},
{"host validAfter validBefore", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}},
&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}, false},
{"fail zero", fields{newClaimer(nil)}, args{SSHOptions{}, &ssh.Certificate{}}, &ssh.Certificate{}, true},
{"fail type", fields{newClaimer(nil)}, args{SSHOptions{}, &ssh.Certificate{CertType: 3}}, &ssh.Certificate{CertType: 3}, true},
{"fail zero", fields{newClaimer(nil)}, args{SignSSHOptions{}, &ssh.Certificate{}}, &ssh.Certificate{}, true},
{"fail type", fields{newClaimer(nil)}, args{SignSSHOptions{}, &ssh.Certificate{CertType: 3}}, &ssh.Certificate{CertType: 3}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -1015,7 +1015,7 @@ func Test_sshLimitDuration_Option(t *testing.T) {
NotAfter time.Time
}
type args struct {
o SSHOptions
o SignSSHOptions
}
tests := []struct {
name string

View file

@ -10,7 +10,7 @@ import (
"golang.org/x/crypto/ssh"
)
func validateSSHCertificate(cert *ssh.Certificate, opts *SSHOptions) error {
func validateSSHCertificate(cert *ssh.Certificate, opts *SignSSHOptions) error {
switch {
case cert == nil:
return fmt.Errorf("certificate is nil")
@ -39,7 +39,7 @@ func validateSSHCertificate(cert *ssh.Certificate, opts *SSHOptions) error {
}
}
func signSSHCertificate(key crypto.PublicKey, opts SSHOptions, signOpts []SignOption, signKey crypto.Signer) (*ssh.Certificate, error) {
func signSSHCertificate(key crypto.PublicKey, opts SignSSHOptions, signOpts []SignOption, signKey crypto.Signer) (*ssh.Certificate, error) {
pub, err := ssh.NewPublicKey(key)
if err != nil {
return nil, err

View file

@ -0,0 +1 @@
{{ toJson .Insecure.CR }}

View file

@ -825,20 +825,20 @@ func generateK8sSAToken(jwk *jose.JSONWebKey, claims *k8sSAPayload, tokOpts ...t
}
func generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SSHOptions{
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SignSSHOptions{
CertType: "user",
Principals: []string{"name"},
}, jwk)
}
func generateSimpleSSHHostToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SSHOptions{
return generateSSHToken("subject@localhost", iss, aud, time.Now(), &SignSSHOptions{
CertType: "host",
Principals: []string{"smallstep.com"},
}, jwk)
}
func generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *SSHOptions, jwk *jose.JSONWebKey) (string, error) {
func generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *SignSSHOptions, jwk *jose.JSONWebKey) (string, error) {
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),

View file

@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
"go.step.sm/crypto/x509util"
)
// x5cPayload extends jwt.Claims with step attributes.
@ -24,10 +25,11 @@ type x5cPayload struct {
// signature requests.
type X5C struct {
*base
Type string `json:"type"`
Name string `json:"name"`
Roots []byte `json:"roots"`
Claims *Claims `json:"claims,omitempty"`
Type string `json:"type"`
Name string `json:"name"`
Roots []byte `json:"roots"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
claimer *Claimer
audiences Audiences
rootPool *x509.CertPool
@ -193,7 +195,19 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
claims.SANs = []string{claims.Subject}
}
// Certificate templates
data := x509util.CreateTemplateData(claims.Subject, claims.SANs)
if v, err := unsafeParseSigned(token); err == nil {
data.SetToken(v)
}
templateOptions, err := TemplateOptions(p.Options, data)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSign")
}
return []SignOption{
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeX5C, p.Name, ""),
profileLimitDuration{p.claimer.DefaultTLSCertDuration(),

View file

@ -463,9 +463,10 @@ func TestX5C_AuthorizeSign(t *testing.T) {
} else {
if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) {
assert.Equals(t, len(opts), 6)
assert.Equals(t, len(opts), 7)
for _, o := range opts {
switch v := o.(type) {
case certificateOptionsFunc:
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeX5C))
assert.Equals(t, v.Name, tc.p.GetName())
@ -695,7 +696,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
Audience: []string{testAudiences.SSHSign[0]},
},
Step: &stepPayload{SSH: &SSHOptions{
Step: &stepPayload{SSH: &SignSSHOptions{
CertType: SSHHostCert,
Principals: []string{"max", "mariano", "alan"},
ValidAfter: TimeDuration{d: 5 * time.Minute},
@ -727,7 +728,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
Audience: []string{testAudiences.SSHSign[0]},
},
Step: &stepPayload{SSH: &SSHOptions{}},
Step: &stepPayload{SSH: &SignSSHOptions{}},
}
tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))
assert.FatalError(t, err)
@ -758,7 +759,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
case sshCertOptionsValidator:
tc.claims.Step.SSH.ValidAfter.t = time.Time{}
tc.claims.Step.SSH.ValidBefore.t = time.Time{}
assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH)
assert.Equals(t, SignSSHOptions(v), *tc.claims.Step.SSH)
case sshCertKeyIDModifier:
assert.Equals(t, string(v), "foo")
case sshCertTypeModifier:
@ -770,7 +771,7 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
case sshCertValidBeforeModifier:
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix())
case sshCertDefaultsModifier:
assert.Equals(t, SSHOptions(v), SSHOptions{CertType: SSHUserCert})
assert.Equals(t, SignSSHOptions(v), SignSSHOptions{CertType: SSHUserCert})
case *sshLimitDuration:
assert.Equals(t, v.Claimer, tc.p.claimer)
assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter)

View file

@ -204,7 +204,7 @@ func (a *Authority) GetSSHBastion(ctx context.Context, user string, hostname str
}
// SignSSH creates a signed SSH certificate with the given public key and options.
func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
var mods []provisioner.SSHCertModifier
var validators []provisioner.SSHCertValidator
@ -453,7 +453,7 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub
// Apply validators from provisioner.
for _, v := range validators {
if err := v.Valid(cert, provisioner.SSHOptions{Backdate: backdate}); err != nil {
if err := v.Valid(cert, provisioner.SignSSHOptions{Backdate: backdate}); err != nil {
return nil, errs.Wrap(http.StatusForbidden, err, "rekeySSH")
}
}

View file

@ -62,7 +62,7 @@ func (m sshTestCertModifier) Modify(cert *ssh.Certificate) error {
type sshTestCertValidator string
func (v sshTestCertValidator) Valid(crt *ssh.Certificate, opts provisioner.SSHOptions) error {
func (v sshTestCertValidator) Valid(crt *ssh.Certificate, opts provisioner.SignSSHOptions) error {
if v == "" {
return nil
}
@ -71,7 +71,7 @@ func (v sshTestCertValidator) Valid(crt *ssh.Certificate, opts provisioner.SSHOp
type sshTestOptionsValidator string
func (v sshTestOptionsValidator) Valid(opts provisioner.SSHOptions) error {
func (v sshTestOptionsValidator) Valid(opts provisioner.SignSSHOptions) error {
if v == "" {
return nil
}
@ -80,7 +80,7 @@ func (v sshTestOptionsValidator) Valid(opts provisioner.SSHOptions) error {
type sshTestOptionsModifier string
func (m sshTestOptionsModifier) Option(opts provisioner.SSHOptions) provisioner.SSHCertModifier {
func (m sshTestOptionsModifier) Option(opts provisioner.SignSSHOptions) provisioner.SSHCertModifier {
return sshTestCertModifier(string(m))
}
@ -109,7 +109,7 @@ func TestAuthority_SignSSH(t *testing.T) {
}
type args struct {
key ssh.PublicKey
opts provisioner.SSHOptions
opts provisioner.SignSSHOptions
signOpts []provisioner.SignOption
}
type want struct {
@ -125,27 +125,27 @@ func TestAuthority_SignSSH(t *testing.T) {
want want
wantErr bool
}{
{"ok-user", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions}}, want{CertType: ssh.UserCert}, false},
{"ok-host", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{hostOptions}}, want{CertType: ssh.HostCert}, false},
{"ok-opts-type-user", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-type-host", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert}, false},
{"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false},
{"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false},
{"ok-opts-valid-after", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false},
{"ok-opts-valid-before", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false},
{"ok-cert-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false},
{"ok-cert-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false},
{"fail-opts-type", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "foo"}, []provisioner.SignOption{}}, want{}, true},
{"fail-cert-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("an error")}}, want{}, true},
{"fail-cert-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("an error")}}, want{}, true},
{"fail-opts-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("an error")}}, want{}, true},
{"fail-opts-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("an error")}}, want{}, true},
{"fail-bad-sign-options", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, "wrong type"}}, want{}, true},
{"fail-no-user-key", fields{nil, signer}, args{pub, provisioner.SSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{}, true},
{"fail-no-host-key", fields{signer, nil}, args{pub, provisioner.SSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{}, true},
{"fail-bad-type", fields{signer, nil}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{sshTestModifier{CertType: 0}}}, want{}, true},
{"ok-user", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions}}, want{CertType: ssh.UserCert}, false},
{"ok-host", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostOptions}}, want{CertType: ssh.HostCert}, false},
{"ok-opts-type-user", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-type-host", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert}, false},
{"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false},
{"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false},
{"ok-opts-valid-after", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false},
{"ok-opts-valid-before", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false},
{"ok-cert-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false},
{"ok-cert-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false},
{"fail-opts-type", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "foo"}, []provisioner.SignOption{}}, want{}, true},
{"fail-cert-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("an error")}}, want{}, true},
{"fail-cert-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("an error")}}, want{}, true},
{"fail-opts-validator", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("an error")}}, want{}, true},
{"fail-opts-modifier", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("an error")}}, want{}, true},
{"fail-bad-sign-options", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userOptions, "wrong type"}}, want{}, true},
{"fail-no-user-key", fields{nil, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{}, true},
{"fail-no-host-key", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{}, true},
{"fail-bad-type", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{sshTestModifier{CertType: 0}}}, want{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -18,8 +18,9 @@ import (
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util"
x509legacy "github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose"
"go.step.sm/crypto/x509util"
)
// GetTLSOptions returns the tls options configured.
@ -30,12 +31,11 @@ func (a *Authority) GetTLSOptions() *tlsutil.TLSOptions {
var oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
var oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
return func(p x509util.Profile) error {
func withDefaultASN1DN(def *x509legacy.ASN1DN) provisioner.CertificateModifierFunc {
return func(crt *x509.Certificate, opts provisioner.SignOptions) error {
if def == nil {
return errors.New("default ASN1DN template cannot be nil")
}
crt := p.Subject()
if len(crt.Subject.Country) == 0 && def.Country != "" {
crt.Subject.Country = append(crt.Subject.Country, def.Country)
@ -61,67 +61,95 @@ func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
}
// Sign creates a signed certificate from a certificate signing request.
func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
var (
opts = []interface{}{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)}
mods = []x509util.WithOption{withDefaultASN1DN(a.config.AuthorityConfig.Template)}
certValidators = []provisioner.CertificateValidator{}
forcedModifiers = []provisioner.CertificateEnforcer{provisioner.ExtraExtsEnforcer{}}
certOptions []x509util.Option
certValidators []provisioner.CertificateValidator
certModifiers []provisioner.CertificateModifier
certEnforcers []provisioner.CertificateEnforcer
)
opts := []interface{}{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)}
if err := csr.CheckSignature(); err != nil {
return nil, errs.Wrap(http.StatusBadRequest, err, "authority.Sign; invalid certificate request", opts...)
}
// Set backdate with the configured value
signOpts.Backdate = a.config.AuthorityConfig.Backdate.Duration
for _, op := range extraOpts {
switch k := op.(type) {
case provisioner.CertificateValidator:
certValidators = append(certValidators, k)
// Adds new options to NewCertificate
case provisioner.CertificateOptions:
certOptions = append(certOptions, k.Options(signOpts)...)
// Validate tie given certificate request.
case provisioner.CertificateRequestValidator:
if err := k.Valid(csr); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
case provisioner.ProfileModifier:
mods = append(mods, k.Option(signOpts))
// Validates the unsigned certificate template.
case provisioner.CertificateValidator:
certValidators = append(certValidators, k)
// Modifies a certificate before validating it.
case provisioner.CertificateModifier:
certModifiers = append(certModifiers, k)
// Modifies a certificate after validating it.
case provisioner.CertificateEnforcer:
forcedModifiers = append(forcedModifiers, k)
certEnforcers = append(certEnforcers, k)
default:
return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...)
}
}
if err := csr.CheckSignature(); err != nil {
return nil, errs.Wrap(http.StatusBadRequest, err, "authority.Sign; invalid certificate request", opts...)
}
leaf, err := x509util.NewLeafProfileWithCSR(csr, a.x509Issuer, a.x509Signer, mods...)
cert, err := x509util.NewCertificate(csr, certOptions...)
if err != nil {
if _, ok := err.(*x509util.TemplateError); ok {
return nil, errs.NewErr(http.StatusBadRequest, err,
errs.WithMessage(err.Error()),
errs.WithKeyVal("csr", csr),
errs.WithKeyVal("signOptions", signOpts),
)
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...)
}
// Certificate validation
// Certificate modifiers before validation
leaf := cert.GetCertificate()
// Set default subject
if err := withDefaultASN1DN(a.config.AuthorityConfig.Template).Modify(leaf, signOpts); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
for _, m := range certModifiers {
if err := m.Modify(leaf, signOpts); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
}
// Certificate validation.
for _, v := range certValidators {
if err := v.Valid(leaf.Subject(), signOpts); err != nil {
if err := v.Valid(leaf, signOpts); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
}
// Certificate modifiers after validation
for _, m := range forcedModifiers {
if err := m.Enforce(leaf.Subject()); err != nil {
for _, m := range certEnforcers {
if err := m.Enforce(leaf); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
}
crtBytes, err := leaf.CreateCertificate()
serverCert, err := x509util.CreateCertificate(leaf, a.x509Issuer, csr.PublicKey, a.x509Signer)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error creating new leaf certificate", opts...)
}
serverCert, err := x509.ParseCertificate(crtBytes)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error parsing new leaf certificate", opts...)
"authority.Sign; error creating certificate", opts...)
}
if err = a.db.StoreCertificate(serverCert); err != nil {
@ -217,7 +245,7 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5
newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext)
}
leaf, err := x509util.NewLeafProfileWithTemplate(newCert, a.x509Issuer, a.x509Signer)
leaf, err := x509legacy.NewLeafProfileWithTemplate(newCert, a.x509Issuer, a.x509Signer)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey", opts...)
}
@ -343,8 +371,8 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
profile, err := x509util.NewLeafProfile("Step Online CA", a.x509Issuer, a.x509Signer,
x509util.WithHosts(strings.Join(a.config.DNSNames, ",")))
profile, err := x509legacy.NewLeafProfile("Step Online CA", a.x509Issuer, a.x509Signer,
x509legacy.WithHosts(strings.Join(a.config.DNSNames, ",")))
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate")
}

View file

@ -95,6 +95,22 @@ func setExtraExtsCSR(exts []pkix.Extension) func(*x509.CertificateRequest) {
}
}
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
b, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, errors.Wrap(err, "error marshaling public key")
}
info := struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}{}
if _, err = asn1.Unmarshal(b, &info); err != nil {
return nil, errors.Wrap(err, "error unmarshaling public key")
}
hash := sha1.Sum(info.SubjectPublicKey.Bytes)
return hash[:], nil
}
type basicConstraints struct {
IsCA bool `asn1:"optional"`
MaxPathLen int `asn1:"optional,default:-1"`
@ -116,9 +132,10 @@ func TestAuthority_Sign(t *testing.T) {
}
nb := time.Now()
signOpts := provisioner.Options{
signOpts := provisioner.SignOptions{
NotBefore: provisioner.NewTimeDuration(nb),
NotAfter: provisioner.NewTimeDuration(nb.Add(time.Minute * 5)),
Backdate: 1 * time.Minute,
}
// Create a token to get test extra opts.
@ -134,7 +151,7 @@ func TestAuthority_Sign(t *testing.T) {
type signTest struct {
auth *Authority
csr *x509.CertificateRequest
signOpts provisioner.Options
signOpts provisioner.SignOptions
extraOpts []provisioner.SignOption
notBefore time.Time
notAfter time.Time
@ -176,7 +193,7 @@ func TestAuthority_Sign(t *testing.T) {
extraOpts: extraOpts,
signOpts: signOpts,
err: errors.New("authority.Sign: default ASN1DN template cannot be nil"),
code: http.StatusInternalServerError,
code: http.StatusUnauthorized,
}
},
"fail create cert": func(t *testing.T) *signTest {
@ -188,13 +205,13 @@ func TestAuthority_Sign(t *testing.T) {
csr: csr,
extraOpts: extraOpts,
signOpts: signOpts,
err: errors.New("authority.Sign; error creating new leaf certificate"),
err: errors.New("authority.Sign; error creating certificate"),
code: http.StatusInternalServerError,
}
},
"fail provisioner duration claim": func(t *testing.T) *signTest {
csr := getCSR(t, priv)
_signOpts := provisioner.Options{
_signOpts := provisioner.SignOptions{
NotBefore: provisioner.NewTimeDuration(nb),
NotAfter: provisioner.NewTimeDuration(nb.Add(time.Hour * 25)),
}
@ -263,6 +280,33 @@ ZYtQ9Ot36qc=
code: http.StatusInternalServerError,
}
},
"fail custom template": func(t *testing.T) *signTest {
csr := getCSR(t, priv)
testAuthority := testAuthority(t)
p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
if !ok {
t.Fatal("provisioner not found")
}
p.(*provisioner.JWK).Options = &provisioner.Options{
X509: &provisioner.X509Options{Template: `{{ fail "fail message" }}`},
}
testExtraOpts, err := testAuthority.Authorize(ctx, token)
assert.FatalError(t, err)
testAuthority.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
return &signTest{
auth: testAuthority,
csr: csr,
extraOpts: testExtraOpts,
signOpts: signOpts,
err: errors.New("fail message"),
code: http.StatusBadRequest,
}
},
"ok": func(t *testing.T) *signTest {
csr := getCSR(t, priv)
_a := testAuthority(t)
@ -313,6 +357,39 @@ ZYtQ9Ot36qc=
notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second),
}
},
"ok with custom template": func(t *testing.T) *signTest {
csr := getCSR(t, priv)
testAuthority := testAuthority(t)
testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
if !ok {
t.Fatal("provisioner not found")
}
p.(*provisioner.JWK).Options = &provisioner.Options{
X509: &provisioner.X509Options{Template: `{
"subject": {{toJson .Subject}},
"dnsNames": {{ toJson .Insecure.CR.DNSNames }},
"keyUsage": ["digitalSignature"],
"extKeyUsage": ["serverAuth","clientAuth"]
}`},
}
testExtraOpts, err := testAuthority.Authorize(ctx, token)
assert.FatalError(t, err)
testAuthority.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
return &signTest{
auth: testAuthority,
csr: csr,
extraOpts: testExtraOpts,
signOpts: signOpts,
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
}
},
}
for name, genTestCase := range tests {
@ -357,9 +434,9 @@ ZYtQ9Ot36qc=
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"})
kid, err := generateSubjectKeyID(pub)
subjectKeyID, err := generateSubjectKeyID(pub)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, kid)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
assert.Equals(t, leaf.AuthorityKeyId, a.x509Issuer.SubjectKeyId)
@ -396,31 +473,6 @@ ZYtQ9Ot36qc=
}
}
// subjectPublicKeyInfo is a PKIX public key structure defined in RFC 5280.
type subjectPublicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
// generateSubjectKeyID generates the key identifier according the the RFC 5280
// section 4.2.1.2.
//
// The keyIdentifier is composed of the 160-bit SHA-1 hash of the value of the
// BIT STRING subjectPublicKey (excluding the tag, length, and number of unused
// bits).
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
b, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, errors.Wrap(err, "error marshaling public key")
}
var info subjectPublicKeyInfo
if _, err = asn1.Unmarshal(b, &info); err != nil {
return nil, errors.Wrap(err, "error unmarshaling public key")
}
hash := sha1.Sum(info.SubjectPublicKey.Bytes)
return hash[:], nil
}
func TestAuthority_Renew(t *testing.T) {
pub, _, err := keys.GenerateDefaultKeyPair()
assert.FatalError(t, err)
@ -435,17 +487,24 @@ func TestAuthority_Renew(t *testing.T) {
CommonName: "renew",
}
certModToWithOptions := func(m provisioner.CertificateModifierFunc) x509util.WithOption {
return func(p x509util.Profile) error {
crt := p.Subject()
return m.Modify(crt, provisioner.SignOptions{})
}
}
now := time.Now().UTC()
nb1 := now.Add(-time.Minute * 7)
na1 := now
so := &provisioner.Options{
so := &provisioner.SignOptions{
NotBefore: provisioner.NewTimeDuration(nb1),
NotAfter: provisioner.NewTimeDuration(na1),
}
leaf, err := x509util.NewLeafProfile("renew", a.x509Issuer, a.x509Signer,
x509util.WithNotBeforeAfterDuration(so.NotBefore.Time(), so.NotAfter.Time(), 0),
withDefaultASN1DN(a.config.AuthorityConfig.Template),
certModToWithOptions(withDefaultASN1DN(a.config.AuthorityConfig.Template)),
x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"),
withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID))
assert.FatalError(t, err)
@ -456,7 +515,7 @@ func TestAuthority_Renew(t *testing.T) {
leafNoRenew, err := x509util.NewLeafProfile("norenew", a.x509Issuer, a.x509Signer,
x509util.WithNotBeforeAfterDuration(so.NotBefore.Time(), so.NotAfter.Time(), 0),
withDefaultASN1DN(a.config.AuthorityConfig.Template),
certModToWithOptions(withDefaultASN1DN(a.config.AuthorityConfig.Template)),
x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"),
withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
)
@ -576,12 +635,9 @@ func TestAuthority_Renew(t *testing.T) {
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"})
// Test Public Key and SubjectKeyId
assert.Equals(t, leaf.PublicKey, cert.PublicKey)
kid, err := generateSubjectKeyID(cert.PublicKey)
subjectKeyID, err := generateSubjectKeyID(pub)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, kid)
assert.Equals(t, leaf.SubjectKeyId, cert.SubjectKeyId)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
// We did not change the intermediate before renewing.
if a.x509Issuer.SerialNumber == tc.auth.x509Issuer.SerialNumber {
@ -658,17 +714,24 @@ func TestAuthority_Rekey(t *testing.T) {
CommonName: "renew",
}
certModToWithOptions := func(m provisioner.CertificateModifierFunc) x509util.WithOption {
return func(p x509util.Profile) error {
crt := p.Subject()
return m.Modify(crt, provisioner.SignOptions{})
}
}
now := time.Now().UTC()
nb1 := now.Add(-time.Minute * 7)
na1 := now
so := &provisioner.Options{
so := &provisioner.SignOptions{
NotBefore: provisioner.NewTimeDuration(nb1),
NotAfter: provisioner.NewTimeDuration(na1),
}
leaf, err := x509util.NewLeafProfile("renew", a.x509Issuer, a.x509Signer,
x509util.WithNotBeforeAfterDuration(so.NotBefore.Time(), so.NotAfter.Time(), 0),
withDefaultASN1DN(a.config.AuthorityConfig.Template),
certModToWithOptions(withDefaultASN1DN(a.config.AuthorityConfig.Template)),
x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"),
withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID))
assert.FatalError(t, err)
@ -679,7 +742,7 @@ func TestAuthority_Rekey(t *testing.T) {
leafNoRenew, err := x509util.NewLeafProfile("norenew", a.x509Issuer, a.x509Signer,
x509util.WithNotBeforeAfterDuration(so.NotBefore.Time(), so.NotAfter.Time(), 0),
withDefaultASN1DN(a.config.AuthorityConfig.Template),
certModToWithOptions(withDefaultASN1DN(a.config.AuthorityConfig.Template)),
x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"),
withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
)
@ -814,9 +877,9 @@ func TestAuthority_Rekey(t *testing.T) {
}
assert.Equals(t, leaf.PublicKey, expectedPK)
kid, err := generateSubjectKeyID(expectedPK)
subjectKeyID, err := generateSubjectKeyID(expectedPK)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, kid)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
if tc.pk == nil {
assert.Equals(t, leaf.SubjectKeyId, cert.SubjectKeyId)
}

View file

@ -34,31 +34,6 @@ import (
"gopkg.in/square/go-jose.v2/jwt"
)
// subjectPublicKeyInfo is a PKIX public key structure defined in RFC 5280.
type subjectPublicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
// generateSubjectKeyID generates the key identifier according the the RFC 5280
// section 4.2.1.2.
//
// The keyIdentifier is composed of the 160-bit SHA-1 hash of the value of the
// BIT STRING subjectPublicKey (excluding the tag, length, and number of unused
// bits).
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
b, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, errors.Wrap(err, "error marshaling public key")
}
var info subjectPublicKeyInfo
if _, err = asn1.Unmarshal(b, &info); err != nil {
return nil, errors.Wrap(err, "error unmarshaling public key")
}
hash := sha1.Sum(info.SubjectPublicKey.Bytes)
return hash[:], nil
}
type ClosingBuffer struct {
*bytes.Buffer
}
@ -79,6 +54,22 @@ func getCSR(priv interface{}) (*x509.CertificateRequest, error) {
return x509.ParseCertificateRequest(csrBytes)
}
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
b, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, errors.Wrap(err, "error marshaling public key")
}
info := struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}{}
if _, err = asn1.Unmarshal(b, &info); err != nil {
return nil, errors.Wrap(err, "error unmarshaling public key")
}
hash := sha1.Sum(info.SubjectPublicKey.Bytes)
return hash[:], nil
}
func TestMain(m *testing.M) {
DisableIdentity = true
os.Exit(m.Run())
@ -326,9 +317,9 @@ ZEp7knvU2psWRw==
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"})
kid, err := generateSubjectKeyID(pub)
subjectKeyID, err := generateSubjectKeyID(pub)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, kid)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
assert.Equals(t, leaf.AuthorityKeyId, intermediateIdentity.Crt.SubjectKeyId)
@ -667,10 +658,9 @@ func TestCARenew(t *testing.T) {
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
assert.Equals(t, leaf.DNSNames, []string{"funk"})
kid, err := generateSubjectKeyID(pub)
subjectKeyID, err := generateSubjectKeyID(pub)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, kid)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
assert.Equals(t, leaf.AuthorityKeyId, intermediateIdentity.Crt.SubjectKeyId)
realIntermediate, err := x509.ParseCertificate(intermediateIdentity.Crt.Raw)

View file

@ -133,7 +133,7 @@ func (p *Provisioner) SSHToken(certType, keyID string, principals []string) (str
token.WithIssuer(p.name),
token.WithAudience(p.sshAudience),
token.WithValidity(notBefore, notAfter),
token.WithSSH(provisioner.SSHOptions{
token.WithSSH(provisioner.SignSSHOptions{
CertType: certType,
Principals: principals,
KeyID: keyID,

View file

@ -56,7 +56,7 @@ func (c *Client) getClientTLSConfig(ctx context.Context, sign *api.SignResponse,
return nil, nil, err
}
// Use mutable tls.Config on renew
tr.DialTLS = c.buildDialTLS(tlsCtx) //nolint:deprecated
tr.DialTLS = c.buildDialTLS(tlsCtx) // nolint:staticcheck
// tr.DialTLSContext = c.buildDialTLSContext(tlsCtx)
renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk)
@ -108,7 +108,7 @@ func (c *Client) GetServerTLSConfig(ctx context.Context, sign *api.SignResponse,
return nil, err
}
// Use mutable tls.Config on renew
tr.DialTLS = c.buildDialTLS(tlsCtx) //nolint:deprecated
tr.DialTLS = c.buildDialTLS(tlsCtx) // nolint:staticcheck
// tr.DialTLSContext = c.buildDialTLSContext(tlsCtx)
renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk)

7
go.mod
View file

@ -4,13 +4,11 @@ go 1.13
require (
cloud.google.com/go v0.51.0
github.com/Masterminds/sprig/v3 v3.0.0
github.com/Masterminds/sprig/v3 v3.1.0
github.com/aws/aws-sdk-go v1.30.29
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-piv/piv-go v1.5.0
github.com/googleapis/gax-go/v2 v2.0.5
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/newrelic/go-agent v2.15.0+incompatible
github.com/pkg/errors v0.9.1
github.com/rs/xid v1.2.1
@ -19,7 +17,8 @@ require (
github.com/smallstep/cli v0.14.7-rc.1.0.20200727165646-eb4e97335f2d
github.com/smallstep/nosql v0.3.0
github.com/urfave/cli v1.22.2
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
go.step.sm/crypto v0.0.0-20200805202904-ec18b6df3cf0
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
google.golang.org/api v0.15.0
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb

62
go.sum
View file

@ -28,8 +28,12 @@ github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITg
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk=
github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g=
github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U=
github.com/Masterminds/sprig/v3 v3.1.0 h1:j7GpgZ7PdFqNsmncycTHsLmVPf5/3wJtlgW9TNDYD9Y=
github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA=
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
@ -274,10 +278,14 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@ -472,10 +480,7 @@ github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.14.5/go.mod h1:zzpB8wMz967gL8FmK6zvCNB4pDVwFDKjPg1diTVc1h8=
github.com/smallstep/certinfo v1.3.0/go.mod h1:1gQJekdPwPvUwFWGTi7bZELmQT09cxC9wJ0VBkBNiwU=
github.com/smallstep/cli v0.14.5 h1:avA6q9h2aIbAQa/vTFV5psjJ1mg8NQliKC+RsFIC778=
github.com/smallstep/cli v0.14.5/go.mod h1:mRFuqC3cGwQESBGJvog4o76jZZZ7bMjkE+hAnq2QyR8=
github.com/smallstep/cli v0.14.6 h1:xc9rawDKB70Vgvg10gfQAh9EpDWS3k1O002J5bApqUk=
github.com/smallstep/cli v0.14.6/go.mod h1:Gs9mXwk5tIBlSISt1tvCAMso7eEAiJRyNgR/6JsoojI=
github.com/smallstep/cli v0.14.7-rc.1.0.20200727165646-eb4e97335f2d h1:bDnvzyEXzAMO5in8QHRrQCEUopiXjFVKHiR8c0m7Iww=
github.com/smallstep/cli v0.14.7-rc.1.0.20200727165646-eb4e97335f2d/go.mod h1:7aWHk7WwJMpEP4PYyav86FMpaI9vuA0uJRliUAqCwxg=
github.com/smallstep/nosql v0.3.0 h1:V1X5vfDsDt89499h3jZFUlR4VnnsYYs5tXaQZ0w8z5U=
@ -486,69 +491,51 @@ github.com/smallstep/zcrypto v0.0.0-20200203191936-fbc32cf76bce/go.mod h1:+F24VU
github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f/go.mod h1:GeHHT7sJDI9ti3oEaFnvx1F4N8n3ZSw2YM1+sbEoxc4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs=
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q=
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/tommy-muehle/go-mnd v1.1.1 h1:4D0wuPKjOTiK2garzuPGGvm4zZ/wLYDOH8TJSABC7KU=
github.com/tommy-muehle/go-mnd v1.1.1/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
github.com/ultraware/funlen v0.0.2 h1:Av96YVBwwNSe4MLR7iI/BIa3VyI7/djnto/pK3Uxbdo=
github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/uudashr/gocognit v1.0.1 h1:MoG2fZ0b/Eo7NXoIwCVFLG5JED3qgQz5/NEE+rOsjPs=
github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
@ -556,7 +543,6 @@ github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOV
github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
@ -568,24 +554,21 @@ github.com/zmap/zlint v0.0.0-20190516161541-9047d02cf65a/go.mod h1:xwLbce0UzBXp4
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
go.etcd.io/etcd v3.3.18+incompatible h1:5aomL5mqoKHxw6NG+oYgsowk8tU8aOalo2IdZxdWHkw=
go.etcd.io/etcd v3.3.18+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.step.sm/crypto v0.0.0-20200805202904-ec18b6df3cf0 h1:FymMl8TrXGxFf80BWpO0CnkSfLnw0BkDdRrhbMGf5zE=
go.step.sm/crypto v0.0.0-20200805202904-ec18b6df3cf0/go.mod h1:8VYxmvSKt5yOTBx3MGsD2Gk4F1Es/3FIxrjnfeYWE8U=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM=
go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E=
go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -593,15 +576,13 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -616,7 +597,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@ -640,11 +620,9 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -659,7 +637,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180202135801-37707fdb30a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -672,7 +649,6 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -681,7 +657,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -689,14 +664,12 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e h1:LwyF2AFISC9nVbS6MgzsaQNSUsRXI49GS+YQ5KX/QH0=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -736,7 +709,6 @@ golang.org/x/tools v0.0.0-20191113232020-e2727e816f5a/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200106190116-7be0a674c9fc h1:MR2F33ipDGog0C4eMhU6u9o3q6c3dvYis2aG6Jl12Wg=
golang.org/x/tools v0.0.0-20200106190116-7be0a674c9fc/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
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=
@ -750,7 +722,6 @@ google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
@ -781,13 +752,11 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.1 h1:GyboHr4UqMiLUybYjd22ZjQIKEJEpgtLXtuGbR21Oho=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A=
@ -795,7 +764,6 @@ gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -806,21 +774,15 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw=
mvdan.cc/unparam v0.0.0-20191111180625-960b1ec0f2c2 h1:K7wru2CfJGumS5hkiguQ0Rb9ebKM2Jo8s5d4Jm9lFaM=
mvdan.cc/unparam v0.0.0-20191111180625-960b1ec0f2c2/go.mod h1:rCqoQrfAmpTX/h2APczwM7UymU/uvaOluiVPIYCSY/k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
sourcegraph.com/sqs/pbtypes v1.0.0 h1:f7lAwqviDEGvON4kRv0o5V7FT/IQK+tbkF664XMbP3o=
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=