diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78b59f4b8..10e7360d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,6 +58,7 @@ jobs: run: V=1 make ci - name: Codecov + if: matrix.go == '1.17' uses: codecov/codecov-action@v1.2.1 with: file: ./coverage.out # optional diff --git a/authority/export.go b/authority/export.go index d1096fa5c..14229823f 100644 --- a/authority/export.go +++ b/authority/export.go @@ -9,7 +9,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" - "go.step.sm/cli-utils/config" + "go.step.sm/cli-utils/step" "go.step.sm/linkedca" "google.golang.org/protobuf/types/known/structpb" ) @@ -245,7 +245,7 @@ func mustReadFileOrURI(fn string, m map[string][]byte) string { return "" } - stepPath := filepath.ToSlash(config.StepPath()) + stepPath := filepath.ToSlash(step.Path()) if !strings.HasSuffix(stepPath, "/") { stepPath += "/" } @@ -257,7 +257,7 @@ func mustReadFileOrURI(fn string, m map[string][]byte) string { panic(err) } if ok { - b, err := os.ReadFile(config.StepAbs(fn)) + b, err := os.ReadFile(step.Abs(fn)) if err != nil { panic(errors.Wrapf(err, "error reading %s", fn)) } diff --git a/authority/provisioners.go b/authority/provisioners.go index e394e7e92..5a0c354f9 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -13,7 +13,7 @@ import ( "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" - step "go.step.sm/cli-utils/config" + "go.step.sm/cli-utils/step" "go.step.sm/cli-utils/ui" "go.step.sm/crypto/jose" "go.step.sm/linkedca" @@ -238,6 +238,8 @@ func (a *Authority) RemoveProvisioner(ctx context.Context, id string) error { return nil } +// CreateFirstProvisioner creates and stores the first provisioner when using +// admin database provisioner storage. func CreateFirstProvisioner(ctx context.Context, db admin.DB, password string) (*linkedca.Provisioner, error) { if password == "" { pass, err := ui.PromptPasswordGenerate("Please enter the password to encrypt your first provisioner, leave empty and we'll generate one") @@ -287,6 +289,7 @@ func CreateFirstProvisioner(ctx context.Context, db admin.DB, password string) ( return p, nil } +// ValidateClaims validates the Claims type. func ValidateClaims(c *linkedca.Claims) error { if c == nil { return nil @@ -313,6 +316,7 @@ func ValidateClaims(c *linkedca.Claims) error { return nil } +// ValidateDurations validates the Durations type. func ValidateDurations(d *linkedca.Durations) error { var ( err error @@ -523,7 +527,7 @@ func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, * if p.X509.Template != "" { x509Template.Template = []byte(p.SSH.Template) } else if p.X509.TemplateFile != "" { - filename := step.StepAbs(p.X509.TemplateFile) + filename := step.Abs(p.X509.TemplateFile) if x509Template.Template, err = os.ReadFile(filename); err != nil { return nil, nil, errors.Wrap(err, "error reading x509 template") } @@ -539,7 +543,7 @@ func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, * if p.SSH.Template != "" { sshTemplate.Template = []byte(p.SSH.Template) } else if p.SSH.TemplateFile != "" { - filename := step.StepAbs(p.SSH.TemplateFile) + filename := step.Abs(p.SSH.TemplateFile) if sshTemplate.Template, err = os.ReadFile(filename); err != nil { return nil, nil, errors.Wrap(err, "error reading ssh template") } diff --git a/authority/ssh.go b/authority/ssh.go index 762319ae2..bef673bfe 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -101,6 +101,15 @@ func (a *Authority) GetSSHConfig(ctx context.Context, typ string, data map[strin if err != nil { return nil, err } + + // Backwards compatibility for version of the cli older than v0.18.0. + // Before v0.18.0 we were not passing any value for SSHTemplateVersionKey + // from the cli. + if o.Name == "step_includes.tpl" && data[templates.SSHTemplateVersionKey] == "" { + o.Type = templates.File + o.Path = strings.TrimPrefix(o.Path, "${STEPPATH}/") + } + output = append(output, o) } return output, nil diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 41df8576d..994d015fa 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -501,6 +501,32 @@ func TestAuthority_GetSSHConfig(t *testing.T) { {Name: "sshd_config.tpl", Type: templates.File, Comment: "#", Path: "/etc/ssh/sshd_config", Content: []byte("Match all\n\tTrustedUserCAKeys /etc/ssh/ca.pub\n\tHostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub\n\tHostKey /etc/ssh/ssh_host_ecdsa_key")}, } + tmplConfigUserIncludes := &templates.Templates{ + SSH: &templates.SSHTemplates{ + User: []templates.Template{ + {Name: "step_includes.tpl", Type: templates.PrependLine, TemplatePath: "./testdata/templates/step_includes.tpl", Path: "${STEPPATH}/ssh/includes", Comment: "#"}, + }, + }, + Data: map[string]interface{}{ + "Step": &templates.Step{ + SSH: templates.StepSSH{ + UserKey: user, + HostKey: host, + }, + }, + }, + } + + userOutputEmptyData := []templates.Output{ + {Name: "step_includes.tpl", Type: templates.File, Comment: "#", Path: "ssh/includes", Content: []byte("Include \"/ssh/config\"\n")}, + } + userOutputWithoutTemplateVersion := []templates.Output{ + {Name: "step_includes.tpl", Type: templates.File, Comment: "#", Path: "ssh/includes", Content: []byte("Include \"/home/user/.step/ssh/config\"\n")}, + } + userOutputWithTemplateVersion := []templates.Output{ + {Name: "step_includes.tpl", Type: templates.PrependLine, Comment: "#", Path: "${STEPPATH}/ssh/includes", Content: []byte("Include \"/home/user/.step/ssh/config\"\n")}, + } + tmplConfigErr := &templates.Templates{ SSH: &templates.SSHTemplates{ User: []templates.Template{ @@ -542,6 +568,9 @@ func TestAuthority_GetSSHConfig(t *testing.T) { {"host", fields{tmplConfig, nil, hostSigner}, args{"host", nil}, hostOutput, false}, {"userWithData", fields{tmplConfigWithUserData, userSigner, hostSigner}, args{"user", map[string]string{"StepPath": "/home/user/.step"}}, userOutputWithUserData, false}, {"hostWithData", fields{tmplConfigWithUserData, userSigner, hostSigner}, args{"host", map[string]string{"Certificate": "ssh_host_ecdsa_key-cert.pub", "Key": "ssh_host_ecdsa_key"}}, hostOutputWithUserData, false}, + {"userIncludesEmptyData", fields{tmplConfigUserIncludes, userSigner, hostSigner}, args{"user", nil}, userOutputEmptyData, false}, + {"userIncludesWithoutTemplateVersion", fields{tmplConfigUserIncludes, userSigner, hostSigner}, args{"user", map[string]string{"StepPath": "/home/user/.step"}}, userOutputWithoutTemplateVersion, false}, + {"userIncludesWithTemplateVersion", fields{tmplConfigUserIncludes, userSigner, hostSigner}, args{"user", map[string]string{"StepPath": "/home/user/.step", "StepSSHTemplateVersion": "v2"}}, userOutputWithTemplateVersion, false}, {"disabled", fields{tmplConfig, nil, nil}, args{"host", nil}, nil, true}, {"badType", fields{tmplConfig, userSigner, hostSigner}, args{"bad", nil}, nil, true}, {"userError", fields{tmplConfigErr, userSigner, hostSigner}, args{"user", nil}, nil, true}, diff --git a/authority/testdata/templates/step_includes.tpl b/authority/testdata/templates/step_includes.tpl new file mode 100644 index 000000000..8c481bd8f --- /dev/null +++ b/authority/testdata/templates/step_includes.tpl @@ -0,0 +1 @@ +{{- if or .User.GOOS "none" | eq "windows" }}Include "{{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config"{{- else }}Include "{{.User.StepPath}}/ssh/config"{{- end }} diff --git a/ca/client.go b/ca/client.go index df4561d84..b10c0f86a 100644 --- a/ca/client.go +++ b/ca/client.go @@ -28,7 +28,7 @@ import ( "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/ca/identity" "github.com/smallstep/certificates/errs" - "go.step.sm/cli-utils/config" + "go.step.sm/cli-utils/step" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/pemutil" @@ -225,7 +225,7 @@ func (o *clientOptions) getTransport(endpoint string) (tr http.RoundTripper, err return tr, nil } -// WithTransport adds a custom transport to the Client. It will fail if a +// WithTransport adds a custom transport to the Client. It will fail if a // previous option to create the transport has been configured. func WithTransport(tr http.RoundTripper) ClientOption { return func(o *clientOptions) error { @@ -237,6 +237,17 @@ func WithTransport(tr http.RoundTripper) ClientOption { } } +// WithInsecure adds a insecure transport that bypasses TLS verification. +func WithInsecure() ClientOption { + return func(o *clientOptions) error { + o.transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + return nil + } +} + // WithRootFile will create the transport using the given root certificate. It // will fail if a previous option to create the transport has been configured. func WithRootFile(filename string) ClientOption { @@ -1294,7 +1305,7 @@ func createCertificateRequest(commonName string, sans []string, key crypto.Priva // getRootCAPath returns the path where the root CA is stored based on the // STEPPATH environment variable. func getRootCAPath() string { - return filepath.Join(config.StepPath(), "certs", "root_ca.crt") + return filepath.Join(step.Path(), "certs", "root_ca.crt") } func readJSON(r io.ReadCloser, v interface{}) error { diff --git a/ca/identity/client.go b/ca/identity/client.go index 7d5dcfcbe..e38529caa 100644 --- a/ca/identity/client.go +++ b/ca/identity/client.go @@ -27,21 +27,22 @@ func (c *Client) ResolveReference(ref *url.URL) *url.URL { // $STEPPATH/config/defaults.json and the identity defined in // $STEPPATH/config/identity.json func LoadClient() (*Client, error) { - b, err := os.ReadFile(DefaultsFile) + defaultsFile := DefaultsFile() + b, err := os.ReadFile(defaultsFile) if err != nil { - return nil, errors.Wrapf(err, "error reading %s", DefaultsFile) + return nil, errors.Wrapf(err, "error reading %s", defaultsFile) } var defaults defaultsConfig if err := json.Unmarshal(b, &defaults); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling %s", DefaultsFile) + return nil, errors.Wrapf(err, "error unmarshaling %s", defaultsFile) } if err := defaults.Validate(); err != nil { - return nil, errors.Wrapf(err, "error validating %s", DefaultsFile) + return nil, errors.Wrapf(err, "error validating %s", defaultsFile) } caURL, err := url.Parse(defaults.CaURL) if err != nil { - return nil, errors.Wrapf(err, "error validating %s", DefaultsFile) + return nil, errors.Wrapf(err, "error validating %s", defaultsFile) } if caURL.Scheme == "" { caURL.Scheme = "https" @@ -52,7 +53,7 @@ func LoadClient() (*Client, error) { return nil, err } if err := identity.Validate(); err != nil { - return nil, errors.Wrapf(err, "error validating %s", IdentityFile) + return nil, errors.Wrapf(err, "error validating %s", IdentityFile()) } if kind := identity.Kind(); kind != MutualTLS { return nil, errors.Errorf("unsupported identity %s: only mTLS is currently supported", kind) diff --git a/ca/identity/client_test.go b/ca/identity/client_test.go index 0ed9b33b6..0f1234e9a 100644 --- a/ca/identity/client_test.go +++ b/ca/identity/client_test.go @@ -11,6 +11,12 @@ import ( "testing" ) +func returnInput(val string) func() string { + return func() string { + return val + } +} + func TestClient(t *testing.T) { oldIdentityFile := IdentityFile oldDefaultsFile := DefaultsFile @@ -19,8 +25,8 @@ func TestClient(t *testing.T) { DefaultsFile = oldDefaultsFile }() - IdentityFile = "testdata/config/identity.json" - DefaultsFile = "testdata/config/defaults.json" + IdentityFile = returnInput("testdata/config/identity.json") + DefaultsFile = returnInput("testdata/config/defaults.json") client, err := LoadClient() if err != nil { @@ -140,36 +146,36 @@ func TestLoadClient(t *testing.T) { wantErr bool }{ {"ok", func() { - IdentityFile = "testdata/config/identity.json" - DefaultsFile = "testdata/config/defaults.json" + IdentityFile = returnInput("testdata/config/identity.json") + DefaultsFile = returnInput("testdata/config/defaults.json") }, expected, false}, {"fail identity", func() { - IdentityFile = "testdata/config/missing.json" - DefaultsFile = "testdata/config/defaults.json" + IdentityFile = returnInput("testdata/config/missing.json") + DefaultsFile = returnInput("testdata/config/defaults.json") }, nil, true}, {"fail identity", func() { - IdentityFile = "testdata/config/fail.json" - DefaultsFile = "testdata/config/defaults.json" + IdentityFile = returnInput("testdata/config/fail.json") + DefaultsFile = returnInput("testdata/config/defaults.json") }, nil, true}, {"fail defaults", func() { - IdentityFile = "testdata/config/identity.json" - DefaultsFile = "testdata/config/missing.json" + IdentityFile = returnInput("testdata/config/identity.json") + DefaultsFile = returnInput("testdata/config/missing.json") }, nil, true}, {"fail defaults", func() { - IdentityFile = "testdata/config/identity.json" - DefaultsFile = "testdata/config/fail.json" + IdentityFile = returnInput("testdata/config/identity.json") + DefaultsFile = returnInput("testdata/config/fail.json") }, nil, true}, {"fail ca", func() { - IdentityFile = "testdata/config/identity.json" - DefaultsFile = "testdata/config/badca.json" + IdentityFile = returnInput("testdata/config/identity.json") + DefaultsFile = returnInput("testdata/config/badca.json") }, nil, true}, {"fail root", func() { - IdentityFile = "testdata/config/identity.json" - DefaultsFile = "testdata/config/badroot.json" + IdentityFile = returnInput("testdata/config/identity.json") + DefaultsFile = returnInput("testdata/config/badroot.json") }, nil, true}, {"fail type", func() { - IdentityFile = "testdata/config/badIdentity.json" - DefaultsFile = "testdata/config/defaults.json" + IdentityFile = returnInput("testdata/config/badIdentity.json") + DefaultsFile = returnInput("testdata/config/defaults.json") }, nil, true}, } for _, tt := range tests { diff --git a/ca/identity/identity.go b/ca/identity/identity.go index 4c6658506..8aa4b441f 100644 --- a/ca/identity/identity.go +++ b/ca/identity/identity.go @@ -15,7 +15,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/api" - "go.step.sm/cli-utils/config" + "go.step.sm/cli-utils/step" "go.step.sm/crypto/pemutil" ) @@ -38,11 +38,18 @@ const TunnelTLS Type = "tTLS" // DefaultLeeway is the duration for matching not before claims. const DefaultLeeway = 1 * time.Minute -// IdentityFile contains the location of the identity file. -var IdentityFile = filepath.Join(config.StepPath(), "config", "identity.json") +var ( + identityDir = step.IdentityPath + configDir = step.ConfigPath + + // IdentityFile contains a pointer to a function that outputs the location of + // the identity file. + IdentityFile = step.IdentityFile -// DefaultsFile contains the location of the defaults file. -var DefaultsFile = filepath.Join(config.StepPath(), "config", "defaults.json") + // DefaultsFile contains a prointer a function that outputs the location of the + // defaults configuration file. + DefaultsFile = step.DefaultsFile +) // Identity represents the identity file that can be used to authenticate with // the CA. @@ -73,23 +80,17 @@ func LoadIdentity(filename string) (*Identity, error) { // LoadDefaultIdentity loads the default identity. func LoadDefaultIdentity() (*Identity, error) { - return LoadIdentity(IdentityFile) + return LoadIdentity(IdentityFile()) } -// configDir and identityDir are used in WriteDefaultIdentity for testing -// purposes. -var ( - configDir = filepath.Join(config.StepPath(), "config") - identityDir = filepath.Join(config.StepPath(), "identity") -) - // WriteDefaultIdentity writes the given certificates and key and the // identity.json pointing to the new files. func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error { - if err := os.MkdirAll(configDir, 0700); err != nil { + if err := os.MkdirAll(configDir(), 0700); err != nil { return errors.Wrap(err, "error creating config directory") } + identityDir := identityDir() if err := os.MkdirAll(identityDir, 0700); err != nil { return errors.Wrap(err, "error creating identity directory") } @@ -126,7 +127,7 @@ func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) er }); err != nil { return errors.Wrap(err, "error writing identity json") } - if err := os.WriteFile(IdentityFile, buf.Bytes(), 0600); err != nil { + if err := os.WriteFile(IdentityFile(), buf.Bytes(), 0600); err != nil { return errors.Wrap(err, "error writing identity certificate") } @@ -135,7 +136,7 @@ func WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) er // WriteIdentityCertificate writes the identity certificate to disk. func WriteIdentityCertificate(certChain []api.Certificate) error { - filename := filepath.Join(identityDir, "identity.crt") + filename := filepath.Join(identityDir(), "identity.crt") return writeCertificate(filename, certChain) } @@ -318,7 +319,7 @@ func (i *Identity) Renew(client Renewer) error { return errors.Wrap(err, "error encoding identity certificate") } } - certFilename := filepath.Join(identityDir, "identity.crt") + certFilename := filepath.Join(identityDir(), "identity.crt") if err := os.WriteFile(certFilename, buf.Bytes(), 0600); err != nil { return errors.Wrap(err, "error writing identity certificate") } diff --git a/ca/identity/identity_test.go b/ca/identity/identity_test.go index ce64768cf..d3b1d541c 100644 --- a/ca/identity/identity_test.go +++ b/ca/identity/identity_test.go @@ -33,9 +33,9 @@ func TestLoadDefaultIdentity(t *testing.T) { want *Identity wantErr bool }{ - {"ok", func() { IdentityFile = "testdata/config/identity.json" }, expected, false}, - {"fail read", func() { IdentityFile = "testdata/config/missing.json" }, nil, true}, - {"fail unmarshal", func() { IdentityFile = "testdata/config/fail.json" }, nil, true}, + {"ok", func() { IdentityFile = returnInput("testdata/config/identity.json") }, expected, false}, + {"fail read", func() { IdentityFile = returnInput("testdata/config/missing.json") }, nil, true}, + {"fail unmarshal", func() { IdentityFile = returnInput("testdata/config/fail.json") }, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -217,9 +217,9 @@ func TestWriteDefaultIdentity(t *testing.T) { certChain = append(certChain, api.Certificate{Certificate: c}) } - configDir = filepath.Join(tmpDir, "config") - identityDir = filepath.Join(tmpDir, "identity") - IdentityFile = filepath.Join(tmpDir, "config", "identity.json") + configDir = returnInput(filepath.Join(tmpDir, "config")) + identityDir = returnInput(filepath.Join(tmpDir, "identity")) + IdentityFile = returnInput(filepath.Join(tmpDir, "config", "identity.json")) type args struct { certChain []api.Certificate @@ -233,27 +233,27 @@ func TestWriteDefaultIdentity(t *testing.T) { }{ {"ok", func() {}, args{certChain, key}, false}, {"fail mkdir config", func() { - configDir = filepath.Join(tmpDir, "identity", "identity.crt") - identityDir = filepath.Join(tmpDir, "identity") + configDir = returnInput(filepath.Join(tmpDir, "identity", "identity.crt")) + identityDir = returnInput(filepath.Join(tmpDir, "identity")) }, args{certChain, key}, true}, {"fail mkdir identity", func() { - configDir = filepath.Join(tmpDir, "config") - identityDir = filepath.Join(tmpDir, "identity", "identity.crt") + configDir = returnInput(filepath.Join(tmpDir, "config")) + identityDir = returnInput(filepath.Join(tmpDir, "identity", "identity.crt")) }, args{certChain, key}, true}, {"fail certificate", func() { - configDir = filepath.Join(tmpDir, "config") - identityDir = filepath.Join(tmpDir, "bad-dir") - os.MkdirAll(identityDir, 0600) + configDir = returnInput(filepath.Join(tmpDir, "config")) + identityDir = returnInput(filepath.Join(tmpDir, "bad-dir")) + os.MkdirAll(identityDir(), 0600) }, args{certChain, key}, true}, {"fail key", func() { - configDir = filepath.Join(tmpDir, "config") - identityDir = filepath.Join(tmpDir, "identity") + configDir = returnInput(filepath.Join(tmpDir, "config")) + identityDir = returnInput(filepath.Join(tmpDir, "identity")) }, args{certChain, "badKey"}, true}, {"fail write identity", func() { - configDir = filepath.Join(tmpDir, "bad-dir") - identityDir = filepath.Join(tmpDir, "identity") - IdentityFile = filepath.Join(configDir, "identity.json") - os.MkdirAll(configDir, 0600) + configDir = returnInput(filepath.Join(tmpDir, "bad-dir")) + identityDir = returnInput(filepath.Join(tmpDir, "identity")) + IdentityFile = returnInput(filepath.Join(configDir(), "identity.json")) + os.MkdirAll(configDir(), 0600) }, args{certChain, key}, true}, } @@ -377,7 +377,7 @@ func TestIdentity_Renew(t *testing.T) { } oldIdentityDir := identityDir - identityDir = "testdata/identity" + identityDir = returnInput("testdata/identity") defer func() { identityDir = oldIdentityDir os.RemoveAll(tmpDir) @@ -432,8 +432,8 @@ func TestIdentity_Renew(t *testing.T) { {"fail renew", func() {}, fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, args{fail}, true}, {"fail certificate", func() {}, fields{"mTLS", "testdata/certs/server.crt", "testdata/identity/identity_key"}, args{ok}, true}, {"fail write identity", func() { - identityDir = filepath.Join(tmpDir, "bad-dir") - os.MkdirAll(identityDir, 0600) + identityDir = returnInput(filepath.Join(tmpDir, "bad-dir")) + os.MkdirAll(identityDir(), 0600) }, fields{"mTLS", "testdata/identity/identity.crt", "testdata/identity/identity_key"}, args{ok}, true}, } for _, tt := range tests { diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 01d800d86..f40ddf5f6 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -21,7 +21,7 @@ import ( "github.com/urfave/cli" "go.step.sm/cli-utils/command" "go.step.sm/cli-utils/command/version" - "go.step.sm/cli-utils/config" + "go.step.sm/cli-utils/step" "go.step.sm/cli-utils/ui" "go.step.sm/cli-utils/usage" @@ -49,7 +49,7 @@ var ( ) func init() { - config.Set("Smallstep CA", Version, BuildTime) + step.Set("Smallstep CA", Version, BuildTime) authority.GlobalVersion.Version = Version rand.Seed(time.Now().UnixNano()) } @@ -115,7 +115,7 @@ func main() { app := cli.NewApp() app.Name = "step-ca" app.HelpName = "step-ca" - app.Version = config.Version() + app.Version = step.Version() app.Usage = "an online certificate authority for secure automated certificate management" app.UsageText = `**step-ca** [**--password-file**=] [**--ssh-host-password-file**=] [**--ssh-user-password-file**=] diff --git a/go.mod b/go.mod index c31dca60e..394eb1a47 100644 --- a/go.mod +++ b/go.mod @@ -29,10 +29,10 @@ require ( github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 - github.com/smallstep/nosql v0.3.8 + github.com/smallstep/nosql v0.3.9 github.com/urfave/cli v1.22.4 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 - go.step.sm/cli-utils v0.6.2 + go.step.sm/cli-utils v0.7.0 go.step.sm/crypto v0.13.0 go.step.sm/linkedca v0.7.0 golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 @@ -44,7 +44,8 @@ require ( gopkg.in/square/go-jose.v2 v2.6.0 ) -// replace github.com/smallstep/nosql => ../nosql -// replace go.step.sm/crypto => ../crypto -// replace go.step.sm/cli-utils => ../cli-utils -// replace go.step.sm/linkedca => ../linkedca +//replace github.com/smallstep/nosql => ../nosql + +//replace go.step.sm/crypto => ../crypto + +//replace go.step.sm/cli-utils => ../cli-utils diff --git a/go.sum b/go.sum index 1ee17532f..ede1fa22a 100644 --- a/go.sum +++ b/go.sum @@ -494,8 +494,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/nosql v0.3.8 h1:1/EWUbbEdz9ai0g9Fd09VekVjtxp+5+gIHpV2PdwW3o= -github.com/smallstep/nosql v0.3.8/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= +github.com/smallstep/nosql v0.3.9 h1:YPy5PR3PXClqmpFaVv0wfXDXDc7NXGBE1auyU2c87dc= +github.com/smallstep/nosql v0.3.9/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= 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/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -559,8 +559,8 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.step.sm/cli-utils v0.6.2 h1:ofa3G/EqE3dTDXmzoXHDZr18qJZoFsKSzbzuF+mxuZU= -go.step.sm/cli-utils v0.6.2/go.mod h1:0tZ8F2QwLgD6KbKj4nrQZhMakTasEAnOcW3Ekc5pnrA= +go.step.sm/cli-utils v0.7.0 h1:2GvY5Muid1yzp7YQbfCCS+gK3q7zlHjjLL5Z0DXz8ds= +go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/E= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.13.0 h1:mQuP9Uu2FNmqCJNO0OTbvolnYXzONy4wdUBtUVcP1s8= go.step.sm/crypto v0.13.0/go.mod h1:5YzQ85BujYBu6NH18jw7nFjwuRnDch35nLzH0ES5sKg= diff --git a/pki/pki.go b/pki/pki.go index 61e20b6bb..a8c298c72 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -29,9 +29,9 @@ import ( "github.com/smallstep/certificates/kms" kmsapi "github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/nosql" - "go.step.sm/cli-utils/config" "go.step.sm/cli-utils/errs" "go.step.sm/cli-utils/fileutil" + "go.step.sm/cli-utils/step" "go.step.sm/cli-utils/ui" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" @@ -87,44 +87,50 @@ const ( ) // GetDBPath returns the path where the file-system persistence is stored -// based on the STEPPATH environment variable. +// based on the $(step path). func GetDBPath() string { - return filepath.Join(config.StepPath(), dbPath) + return filepath.Join(step.Path(), dbPath) } // GetConfigPath returns the directory where the configuration files are stored -// based on the STEPPATH environment variable. +// based on the $(step path). func GetConfigPath() string { - return filepath.Join(config.StepPath(), configPath) + return filepath.Join(step.Path(), configPath) +} + +// GetProfileConfigPath returns the directory where the profile configuration +// files are stored based on the $(step path). +func GetProfileConfigPath() string { + return filepath.Join(step.ProfilePath(), configPath) } // GetPublicPath returns the directory where the public keys are stored based on -// the STEPPATH environment variable. +// the $(step path). func GetPublicPath() string { - return filepath.Join(config.StepPath(), publicPath) + return filepath.Join(step.Path(), publicPath) } // GetSecretsPath returns the directory where the private keys are stored based -// on the STEPPATH environment variable. +// on the $(step path). func GetSecretsPath() string { - return filepath.Join(config.StepPath(), privatePath) + return filepath.Join(step.Path(), privatePath) } // GetRootCAPath returns the path where the root CA is stored based on the -// STEPPATH environment variable. +// $(step path). func GetRootCAPath() string { - return filepath.Join(config.StepPath(), publicPath, "root_ca.crt") + return filepath.Join(step.Path(), publicPath, "root_ca.crt") } // GetOTTKeyPath returns the path where the one-time token key is stored based -// on the STEPPATH environment variable. +// on the $(step path). func GetOTTKeyPath() string { - return filepath.Join(config.StepPath(), privatePath, "ott_key") + return filepath.Join(step.Path(), privatePath, "ott_key") } // GetTemplatesPath returns the path where the templates are stored. func GetTemplatesPath() string { - return filepath.Join(config.StepPath(), templatesPath) + return filepath.Join(step.Path(), templatesPath) } // GetProvisioners returns the map of provisioners on the given CA. @@ -286,20 +292,22 @@ func WithKeyURIs(rootKey, intermediateKey, hostKey, userKey string) Option { // PKI represents the Public Key Infrastructure used by a certificate authority. type PKI struct { linkedca.Configuration - Defaults linkedca.Defaults - casOptions apiv1.Options - caService apiv1.CertificateAuthorityService - caCreator apiv1.CertificateAuthorityCreator - keyManager kmsapi.KeyManager - config string - defaults string - ottPublicKey *jose.JSONWebKey - ottPrivateKey *jose.JSONWebEncryption - options *options + Defaults linkedca.Defaults + casOptions apiv1.Options + caService apiv1.CertificateAuthorityService + caCreator apiv1.CertificateAuthorityCreator + keyManager kmsapi.KeyManager + config string + defaults string + profileDefaults string + ottPublicKey *jose.JSONWebKey + ottPrivateKey *jose.JSONWebEncryption + options *options } // New creates a new PKI configuration. func New(o apiv1.Options, opts ...Option) (*PKI, error) { + currentCtx := step.Contexts().GetCurrent() caService, err := cas.New(context.Background(), o) if err != nil { return nil, err @@ -358,6 +366,9 @@ func New(o apiv1.Options, opts ...Option) (*PKI, error) { cfg = GetConfigPath() // Create directories dirs := []string{public, private, cfg, GetTemplatesPath()} + if currentCtx != nil { + dirs = append(dirs, GetProfileConfigPath()) + } for _, name := range dirs { if _, err := os.Stat(name); os.IsNotExist(err) { if err = os.MkdirAll(name, 0700); err != nil { @@ -415,6 +426,10 @@ func New(o apiv1.Options, opts ...Option) (*PKI, error) { if p.defaults, err = getPath(cfg, "defaults.json"); err != nil { return nil, err } + if currentCtx != nil { + p.profileDefaults = currentCtx.ProfileDefaultsFile() + } + if p.config, err = getPath(cfg, "ca.json"); err != nil { return nil, err } @@ -944,6 +959,18 @@ func (p *PKI) Save(opt ...ConfigOption) error { if err = fileutil.WriteFile(p.defaults, b, 0644); err != nil { return errs.FileError(err, p.defaults) } + // If we're using contexts then write a blank object to the default profile + // configuration location. + if p.profileDefaults != "" { + if _, err := os.Stat(p.profileDefaults); os.IsNotExist(err) { + // Write with 0600 to be consistent with directories structure. + if err = fileutil.WriteFile(p.profileDefaults, []byte("{}"), 0600); err != nil { + return errs.FileError(err, p.profileDefaults) + } + } else if err != nil { + return errs.FileError(err, p.profileDefaults) + } + } // Generate and write templates if err := generateTemplates(cfg.Templates); err != nil { @@ -958,6 +985,9 @@ func (p *PKI) Save(opt ...ConfigOption) error { } ui.PrintSelected("Default configuration", p.defaults) + if p.profileDefaults != "" { + ui.PrintSelected("Default profile configuration", p.profileDefaults) + } ui.PrintSelected("Certificate Authority configuration", p.config) if p.options.deploymentType != LinkedDeployment { ui.Println() diff --git a/pki/templates.go b/pki/templates.go index 3506a96d6..0ccbed8b4 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -6,9 +6,9 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/templates" - "go.step.sm/cli-utils/config" "go.step.sm/cli-utils/errs" "go.step.sm/cli-utils/fileutil" + "go.step.sm/cli-utils/step" ) // getTemplates returns all the templates enabled @@ -44,7 +44,7 @@ func generateTemplates(t *templates.Templates) error { if !ok { return errors.Errorf("template %s does not exists", t.Name) } - if err := fileutil.WriteFile(config.StepAbs(t.TemplatePath), []byte(data), 0644); err != nil { + if err := fileutil.WriteFile(step.Abs(t.TemplatePath), []byte(data), 0644); err != nil { return err } } @@ -53,7 +53,7 @@ func generateTemplates(t *templates.Templates) error { if !ok { return errors.Errorf("template %s does not exists", t.Name) } - if err := fileutil.WriteFile(config.StepAbs(t.TemplatePath), []byte(data), 0644); err != nil { + if err := fileutil.WriteFile(step.Abs(t.TemplatePath), []byte(data), 0644); err != nil { return err } } diff --git a/templates/templates.go b/templates/templates.go index 4fd68ce95..2544b6e9c 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -9,8 +9,8 @@ import ( "github.com/Masterminds/sprig/v3" "github.com/pkg/errors" - "go.step.sm/cli-utils/config" "go.step.sm/cli-utils/fileutil" + "go.step.sm/cli-utils/step" ) // TemplateType defines how a template will be written in disk. @@ -19,6 +19,9 @@ type TemplateType string const ( // Snippet will mark a template as a part of a file. Snippet TemplateType = "snippet" + // PrependLine is a template for prepending a single line to a file. If the + // line already exists in the file it will be removed first. + PrependLine TemplateType = "prepend-line" // File will mark a templates as a full file. File TemplateType = "file" // Directory will mark a template as a directory. @@ -98,7 +101,7 @@ func (t *SSHTemplates) Validate() (err error) { return } -// Template represents on template file. +// Template represents a template file. type Template struct { *template.Template Name string `json:"name"` @@ -117,8 +120,8 @@ func (t *Template) Validate() error { return nil case t.Name == "": return errors.New("template name cannot be empty") - case t.Type != Snippet && t.Type != File && t.Type != Directory: - return errors.Errorf("invalid template type %s, it must be %s, %s, or %s", t.Type, Snippet, File, Directory) + case t.Type != Snippet && t.Type != File && t.Type != Directory && t.Type != PrependLine: + return errors.Errorf("invalid template type %s, it must be %s, %s, %s, or %s", t.Type, Snippet, PrependLine, File, Directory) case t.TemplatePath == "" && t.Type != Directory && len(t.Content) == 0: return errors.New("template template cannot be empty") case t.TemplatePath != "" && t.Type == Directory: @@ -131,7 +134,7 @@ func (t *Template) Validate() error { if t.TemplatePath != "" { // Check for file - st, err := os.Stat(config.StepAbs(t.TemplatePath)) + st, err := os.Stat(step.Abs(t.TemplatePath)) if err != nil { return errors.Wrapf(err, "error reading %s", t.TemplatePath) } @@ -165,7 +168,7 @@ func (t *Template) Load() error { if t.Template == nil && t.Type != Directory { switch { case t.TemplatePath != "": - filename := config.StepAbs(t.TemplatePath) + filename := step.Abs(t.TemplatePath) b, err := os.ReadFile(filename) if err != nil { return errors.Wrapf(err, "error reading %s", filename) @@ -246,7 +249,10 @@ type Output struct { // Write writes the Output to the filesystem as a directory, file or snippet. func (o *Output) Write() error { - path := config.StepAbs(o.Path) + // Replace ${STEPPATH} with the base step path. + o.Path = strings.ReplaceAll(o.Path, "${STEPPATH}", step.BasePath()) + + path := step.Abs(o.Path) if o.Type == Directory { return mkdir(path, 0700) } @@ -256,11 +262,17 @@ func (o *Output) Write() error { return err } - if o.Type == File { + switch o.Type { + case File: return fileutil.WriteFile(path, o.Content, 0600) + case Snippet: + return fileutil.WriteSnippet(path, o.Content, 0600) + case PrependLine: + return fileutil.PrependLine(path, o.Content, 0600) + default: + // Default to using a Snippet type if the type is not known. + return fileutil.WriteSnippet(path, o.Content, 0600) } - - return fileutil.WriteSnippet(path, o.Content, 0600) } func mkdir(path string, perm os.FileMode) error { diff --git a/templates/values.go b/templates/values.go index 972b1d55a..c33625273 100644 --- a/templates/values.go +++ b/templates/values.go @@ -4,6 +4,10 @@ import ( "golang.org/x/crypto/ssh" ) +// SSHTemplateVersionKey is a key that can be submitted by a client to select +// the template version that will be returned by the server. +var SSHTemplateVersionKey = "StepSSHTemplateVersion" + // Step represents the default variables available in the CA. type Step struct { SSH StepSSH @@ -22,16 +26,23 @@ type StepSSH struct { var DefaultSSHTemplates = SSHTemplates{ User: []Template{ { - Name: "include.tpl", + Name: "config.tpl", Type: Snippet, - TemplatePath: "templates/ssh/include.tpl", + TemplatePath: "templates/ssh/config.tpl", Path: "~/.ssh/config", Comment: "#", }, { - Name: "config.tpl", + Name: "step_includes.tpl", + Type: PrependLine, + TemplatePath: "templates/ssh/step_includes.tpl", + Path: "${STEPPATH}/ssh/includes", + Comment: "#", + }, + { + Name: "step_config.tpl", Type: File, - TemplatePath: "templates/ssh/config.tpl", + TemplatePath: "templates/ssh/step_config.tpl", Path: "ssh/config", Comment: "#", }, @@ -64,30 +75,43 @@ var DefaultSSHTemplates = SSHTemplates{ // DefaultSSHTemplateData contains the data of the default templates used on ssh. var DefaultSSHTemplateData = map[string]string{ - // include.tpl adds the step ssh config file. + // base_config.tpl adds the step ssh config file. // // Note: on windows `Include C:\...` is treated as a relative path. - "include.tpl": `Host * + "config.tpl": `Host * {{- if or .User.GOOS "none" | eq "windows" }} - Include "{{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config" +{{- if .User.StepBasePath }} + Include "{{ .User.StepBasePath | replace "\\" "/" | trimPrefix "C:" }}/ssh/includes" {{- else }} - Include "{{.User.StepPath}}/ssh/config" + Include "{{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/includes" +{{- end }} +{{- else }} +{{- if .User.StepBasePath }} + Include "{{.User.StepBasePath}}/ssh/includes" +{{- else }} + Include "{{.User.StepPath}}/ssh/includes" +{{- end }} {{- end }}`, + // includes.tpl adds the step ssh config file. + // + // Note: on windows `Include C:\...` is treated as a relative path. + "step_includes.tpl": `{{- if or .User.GOOS "none" | eq "windows" }}Include "{{ .User.StepPath | replace "\\" "/" | trimPrefix "C:" }}/ssh/config"{{- else }}Include "{{.User.StepPath}}/ssh/config"{{- end }}`, + // config.tpl is the step ssh config file, it includes the Match rule and // references the step known_hosts file. // // Note: on windows ProxyCommand requires the full path - "config.tpl": `Match exec "step ssh check-host %h" + "step_config.tpl": `Match exec "step ssh check-host{{- if .User.Context }} --context {{ .User.Context }}{{- end }} %h" {{- if .User.User }} User {{.User.User}} {{- end }} {{- if or .User.GOOS "none" | eq "windows" }} UserKnownHostsFile "{{.User.StepPath}}\ssh\known_hosts" - ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand %r %h %p + ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand{{- if .User.Context }} --context {{ .User.Context }}{{- end }} %r %h %p {{- else }} UserKnownHostsFile "{{.User.StepPath}}/ssh/known_hosts" - ProxyCommand step ssh proxycommand %r %h %p + ProxyCommand step ssh proxycommand{{- if .User.Context }} --context {{ .User.Context }}{{- end }} %r %h %p {{- end }} `,