Skip to content

Commit 45286f9

Browse files
committed
Allow a few global defaults to be pulled from the CA
- min-encryption-password-length - provisioner Enforce min-encryption-password-length, if set, in the 'step ssh certificate' command.
1 parent 30ac443 commit 45286f9

File tree

4 files changed

+88
-32
lines changed

4 files changed

+88
-32
lines changed

command/ssh/certificate.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ func certificateCommand() cli.Command {
4444
[**--console**] [**--no-password**] [**--insecure**] [**--force**] [**--x5c-cert**=<file>]
4545
[**--x5c-key**=<file>] [**--k8ssa-token-path**=<file>] [**--no-agent**]
4646
[**--kty**=<key-type>] [**--curve**=<curve>] [**--size**=<size>]
47-
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]`,
47+
[**--min-encryption-password-length**=<length>] [**--ca-url**=<uri>]
48+
[**--root**=<file>] [**--context**=<name>]`,
4849

4950
Description: `**step ssh certificate** command generates an SSH key pair and creates a
5051
certificate using [step certificates](https://github.com/smallstep/certificates).
@@ -202,6 +203,11 @@ $ step ssh certificate --kty OKP --curve Ed25519 mariano@work id_ed25519
202203
Name: "no-agent",
203204
Usage: "Do not add the generated certificate and associated private key to the SSH agent.",
204205
},
206+
cli.IntFlag{
207+
Name: "min-encryption-password-length",
208+
Usage: "Set minimum required length for password used to encrypt private key. The default value is '0'. Values <=0 are interpreted as if no minimum value is set.",
209+
Value: 0,
210+
},
205211
flags.CaConfig,
206212
flags.CaURL,
207213
flags.Root,
@@ -240,6 +246,7 @@ func certificateAction(ctx *cli.Context) error {
240246
noPassword := ctx.Bool("no-password")
241247
insecure := ctx.Bool("insecure")
242248
sshPrivKeyFile := ctx.String("private-key")
249+
minPasswordLength := ctx.Int("min-encryption-password-length")
243250
validAfter, validBefore, err := flags.ParseTimeDuration(ctx)
244251
if err != nil {
245252
return err
@@ -258,6 +265,8 @@ func certificateAction(ctx *cli.Context) error {
258265
switch {
259266
case noPassword && !insecure:
260267
return errs.RequiredInsecureFlag(ctx, "no-password")
268+
case noPassword && minPasswordLength > 0:
269+
return errs.IncompatibleFlagWithFlag(ctx, "noPassword", "minPasswordLen")
261270
case noPassword && passwordFile != "":
262271
return errs.IncompatibleFlagWithFlag(ctx, "no-password", "password-file")
263272
case token != "" && provisionerPasswordFile != "":
@@ -456,42 +465,43 @@ func certificateAction(ctx *cli.Context) error {
456465
// Private key (with password unless --no-password --insecure)
457466
opts := []pemutil.Options{
458467
pemutil.WithOpenSSH(true),
459-
pemutil.ToFile(keyFile, 0600),
468+
pemutil.ToFile(keyFile, 0o600),
460469
}
461470
switch {
462471
case noPassword && insecure:
463472
case passwordFile != "":
464-
opts = append(opts, pemutil.WithPasswordFile(passwordFile))
473+
opts = append(opts, pemutil.WithMinLengthPasswordFile(passwordFile, minPasswordLength))
465474
default:
466475
opts = append(opts, pemutil.WithPasswordPrompt("Please enter the password to encrypt the private key", func(s string) ([]byte, error) {
467-
return ui.PromptPassword(s, ui.WithValidateNotEmpty())
476+
return ui.PromptPassword(s, ui.WithValidateNotEmpty(), ui.WithValidateMinLength(minPasswordLength))
468477
}))
469478
}
479+
470480
_, err = pemutil.Serialize(priv, opts...)
471481
if err != nil {
472482
return err
473483
}
474484

475-
if err := utils.WriteFile(pubFile, marshalPublicKey(sshPub, subject), 0644); err != nil {
485+
if err := utils.WriteFile(pubFile, marshalPublicKey(sshPub, subject), 0o644); err != nil {
476486
return err
477487
}
478488
}
479489

480490
// Write certificate
481-
if err := utils.WriteFile(crtFile, marshalPublicKey(resp.Certificate, subject), 0644); err != nil {
491+
if err := utils.WriteFile(crtFile, marshalPublicKey(resp.Certificate, subject), 0o644); err != nil {
482492
return err
483493
}
484494

485495
// Write Add User keys and certs
486496
if isAddUser && resp.AddUserCertificate != nil {
487497
id := provisioner.SanitizeSSHUserPrincipal(subject) + "-provisioner"
488-
if _, err := pemutil.Serialize(auPriv, pemutil.WithOpenSSH(true), pemutil.ToFile(baseName+"-provisioner", 0600)); err != nil {
498+
if _, err := pemutil.Serialize(auPriv, pemutil.WithOpenSSH(true), pemutil.ToFile(baseName+"-provisioner", 0o600)); err != nil {
489499
return err
490500
}
491-
if err := utils.WriteFile(baseName+"-provisioner.pub", marshalPublicKey(sshAuPub, id), 0644); err != nil {
501+
if err := utils.WriteFile(baseName+"-provisioner.pub", marshalPublicKey(sshAuPub, id), 0o644); err != nil {
492502
return err
493503
}
494-
if err := utils.WriteFile(baseName+"-provisioner-cert.pub", marshalPublicKey(resp.AddUserCertificate, id), 0644); err != nil {
504+
if err := utils.WriteFile(baseName+"-provisioner-cert.pub", marshalPublicKey(resp.AddUserCertificate, id), 0o644); err != nil {
495505
return err
496506
}
497507
}

go.mod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ require (
1919
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262
2020
github.com/smallstep/certificates v0.28.2
2121
github.com/smallstep/certinfo v1.13.0
22-
github.com/smallstep/cli-utils v0.11.0
22+
github.com/smallstep/cli-utils v0.12.0
2323
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935
2424
github.com/smallstep/linkedca v0.23.0
2525
github.com/smallstep/truststore v0.13.0
@@ -28,7 +28,7 @@ require (
2828
github.com/stretchr/testify v1.10.0
2929
github.com/urfave/cli v1.22.16
3030
go.mozilla.org/pkcs7 v0.9.0
31-
go.step.sm/crypto v0.58.0
31+
go.step.sm/crypto v0.59.0
3232
golang.org/x/crypto v0.35.0
3333
golang.org/x/sys v0.30.0
3434
golang.org/x/term v0.29.0
@@ -131,12 +131,15 @@ require (
131131
go.opentelemetry.io/otel v1.34.0 // indirect
132132
go.opentelemetry.io/otel/metric v1.34.0 // indirect
133133
go.opentelemetry.io/otel/trace v1.34.0 // indirect
134+
go.uber.org/mock v0.5.0 // indirect
134135
golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 // indirect
136+
golang.org/x/mod v0.20.0 // indirect
135137
golang.org/x/net v0.35.0 // indirect
136138
golang.org/x/oauth2 v0.26.0 // indirect
137139
golang.org/x/sync v0.11.0 // indirect
138140
golang.org/x/text v0.22.0 // indirect
139141
golang.org/x/time v0.10.0 // indirect
142+
golang.org/x/tools v0.24.0 // indirect
140143
google.golang.org/api v0.221.0 // indirect
141144
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
142145
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ github.com/smallstep/certinfo v1.13.0 h1:iv/Fc1c8vke1asJZI7s3XoH7Wo/MY7znK0TlDUs
302302
github.com/smallstep/certinfo v1.13.0/go.mod h1:2pGT3T7r0s5f3BpJRi/j5K5akgvL3RfYXts5rDICkEA=
303303
github.com/smallstep/cli-utils v0.11.0 h1:XBKOEYFxbx3dal6P9mE58xXxBrdKNAp8J/CRMb7C9AI=
304304
github.com/smallstep/cli-utils v0.11.0/go.mod h1:VIQX+rhIUd4d8OBpIqpkQtBDq/lUlzqaGpCNbFeUuNM=
305+
github.com/smallstep/cli-utils v0.12.0 h1:EX3WBTpoCJGqIddiJnhYWYx+CGo8xH1o29tu5I+giss=
306+
github.com/smallstep/cli-utils v0.12.0/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20=
305307
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA=
306308
github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
307309
github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU=
@@ -377,6 +379,10 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC
377379
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
378380
go.step.sm/crypto v0.58.0 h1:PrrtM9fP8Y7A7193WnZDu2bqEZu7jc7M8+CCkDarMtw=
379381
go.step.sm/crypto v0.58.0/go.mod h1:yluOL5OqY7mXGGQ7JUmAv/6h8T8Ge3yXdlEESWHOqDQ=
382+
go.step.sm/crypto v0.58.1 h1:2PpEYTbytA3el9dW0gh9uJEe/CR/J6wS+x2vWYLG83M=
383+
go.step.sm/crypto v0.58.1/go.mod h1:yluOL5OqY7mXGGQ7JUmAv/6h8T8Ge3yXdlEESWHOqDQ=
384+
go.step.sm/crypto v0.59.0 h1:BDEbdyYJpp0yOSx7dmT+9git2/BXfkCsCzJKOj3v9RI=
385+
go.step.sm/crypto v0.59.0/go.mod h1:wz5S8Q0NI/x22TIWQxdGD+ioHa+ge2FbkcPmbXUzNao=
380386
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
381387
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
382388
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -398,6 +404,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
398404
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
399405
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
400406
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
407+
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
408+
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
401409
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
402410
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
403411
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -472,6 +480,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
472480
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
473481
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
474482
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
483+
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
484+
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
475485
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
476486
google.golang.org/api v0.221.0 h1:qzaJfLhDsbMeFee8zBRdt/Nc+xmOuafD/dbdgGfutOU=
477487
google.golang.org/api v0.221.0/go.mod h1:7sOU2+TL4TxUTdbi0gWgAIg7tH5qBXxoyhtL+9x3biQ=

utils/cautils/bootstrap.go

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import (
2525
)
2626

2727
type bootstrapAPIResponse struct {
28-
CaURL string `json:"url"`
29-
Fingerprint string `json:"fingerprint"`
30-
RedirectURL string `json:"redirect-url"`
28+
CaURL string `json:"url"`
29+
Fingerprint string `json:"fingerprint"`
30+
RedirectURL string `json:"redirect-url"`
31+
Provisioner string `json:"provisioner"`
32+
MinEncryptionPasswordLength int `json:"min-encryption-password-length"`
3133
}
3234

3335
// UseContext returns true if contexts should be used, false otherwise.
@@ -53,8 +55,22 @@ func WarnContext() {
5355
type bootstrapOption func(bc *bootstrapContext)
5456

5557
type bootstrapContext struct {
56-
defaultContextName string
57-
redirectURL string
58+
defaultContextName string
59+
redirectURL string
60+
provisioner string
61+
minEncryptionPasswordLength int
62+
}
63+
64+
func withProvisioner(provisioner string) bootstrapOption {
65+
return func(bc *bootstrapContext) {
66+
bc.provisioner = provisioner
67+
}
68+
}
69+
70+
func withMinEncryptionPasswordLength(minLength int) bootstrapOption {
71+
return func(bc *bootstrapContext) {
72+
bc.minEncryptionPasswordLength = minLength
73+
}
5874
}
5975

6076
func withDefaultContextValues(context string) bootstrapOption {
@@ -70,10 +86,12 @@ func withRedirectURL(r string) bootstrapOption {
7086
}
7187

7288
type bootstrapConfig struct {
73-
CA string `json:"ca-url"`
74-
Fingerprint string `json:"fingerprint"`
75-
Root string `json:"root"`
76-
Redirect string `json:"redirect-url"`
89+
CA string `json:"ca-url"`
90+
Fingerprint string `json:"fingerprint"`
91+
Root string `json:"root"`
92+
Redirect string `json:"redirect-url"`
93+
Provisioner string `json:"provisioner"`
94+
MinEncryptionPasswordLength int `json:"min-encryption-password-length"`
7795
}
7896

7997
func bootstrap(ctx *cli.Context, caURL, fingerprint string, opts ...bootstrapOption) error {
@@ -126,16 +144,16 @@ func bootstrap(ctx *cli.Context, caURL, fingerprint string, opts ...bootstrapOpt
126144
rootFile := pki.GetRootCAPath()
127145
configFile := step.DefaultsFile()
128146

129-
if err = os.MkdirAll(filepath.Dir(rootFile), 0700); err != nil {
147+
if err = os.MkdirAll(filepath.Dir(rootFile), 0o700); err != nil {
130148
return errs.FileError(err, rootFile)
131149
}
132150

133-
if err = os.MkdirAll(filepath.Dir(configFile), 0700); err != nil {
151+
if err = os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
134152
return errs.FileError(err, configFile)
135153
}
136154

137155
// Serialize root
138-
_, err = pemutil.Serialize(resp.RootPEM.Certificate, pemutil.ToFile(rootFile, 0600))
156+
_, err = pemutil.Serialize(resp.RootPEM.Certificate, pemutil.ToFile(rootFile, 0o600))
139157
if err != nil {
140158
return err
141159
}
@@ -148,12 +166,19 @@ func bootstrap(ctx *cli.Context, caURL, fingerprint string, opts ...bootstrapOpt
148166
}
149167

150168
// Serialize defaults.json
151-
b, err := json.MarshalIndent(bootstrapConfig{
169+
bootConf := bootstrapConfig{
152170
CA: caURL,
153171
Fingerprint: fingerprint,
154172
Root: pki.GetRootCAPath(),
155173
Redirect: bc.redirectURL,
156-
}, "", " ")
174+
}
175+
if bc.minEncryptionPasswordLength > 0 {
176+
bootConf.MinEncryptionPasswordLength = bc.minEncryptionPasswordLength
177+
}
178+
if bc.provisioner != "" {
179+
bootConf.Provisioner = bc.provisioner
180+
}
181+
b, err := json.MarshalIndent(bootConf, "", " ")
157182
if err != nil {
158183
return errors.Wrap(err, "error marshaling defaults.json")
159184
}
@@ -162,7 +187,7 @@ func bootstrap(ctx *cli.Context, caURL, fingerprint string, opts ...bootstrapOpt
162187
ctx.Set("fingerprint", fingerprint)
163188
ctx.Set("root", rootFile)
164189

165-
if err := utils.WriteFile(configFile, b, 0644); err != nil {
190+
if err := utils.WriteFile(configFile, b, 0o644); err != nil {
166191
return err
167192
}
168193

@@ -171,12 +196,12 @@ func bootstrap(ctx *cli.Context, caURL, fingerprint string, opts ...bootstrapOpt
171196
if step.Contexts().Enabled() {
172197
profileDefaultsFile := step.ProfileDefaultsFile()
173198

174-
if err := os.MkdirAll(filepath.Dir(profileDefaultsFile), 0700); err != nil {
199+
if err := os.MkdirAll(filepath.Dir(profileDefaultsFile), 0o700); err != nil {
175200
return errs.FileError(err, profileDefaultsFile)
176201
}
177202

178203
if _, err := os.Stat(profileDefaultsFile); os.IsNotExist(err) {
179-
if err := os.WriteFile(profileDefaultsFile, []byte("{}"), 0600); err != nil {
204+
if err := os.WriteFile(profileDefaultsFile, []byte("{}"), 0o600); err != nil {
180205
return errs.FileError(err, profileDefaultsFile)
181206
}
182207
ui.Printf("The profile configuration has been saved in %s.\n", profileDefaultsFile)
@@ -254,9 +279,17 @@ func BootstrapTeamAuthority(ctx *cli.Context, team, teamAuthority string) error
254279
r.RedirectURL = "https://smallstep.com/app/teams/sso/success"
255280
}
256281

257-
return bootstrap(ctx, r.CaURL, r.Fingerprint,
258-
withDefaultContextValues(teamAuthority+"."+team),
259-
withRedirectURL(r.RedirectURL))
282+
bootOpts := []bootstrapOption{
283+
withDefaultContextValues(teamAuthority + "." + team),
284+
withRedirectURL(r.RedirectURL),
285+
}
286+
if r.Provisioner != "" {
287+
bootOpts = append(bootOpts, withProvisioner(r.Provisioner))
288+
}
289+
if r.MinEncryptionPasswordLength > 0 {
290+
bootOpts = append(bootOpts, withMinEncryptionPasswordLength(r.MinEncryptionPasswordLength))
291+
}
292+
return bootstrap(ctx, r.CaURL, r.Fingerprint, bootOpts...)
260293
}
261294

262295
// BootstrapAuthority bootstraps an authority using only the caURL and fingerprint.
@@ -268,7 +301,7 @@ func BootstrapAuthority(ctx *cli.Context, caURL, fingerprint string) (err error)
268301
}
269302
}
270303

271-
var opts = []bootstrapOption{
304+
opts := []bootstrapOption{
272305
withDefaultContextValues(caHostname),
273306
}
274307

0 commit comments

Comments
 (0)