Skip to content

Commit 3b42211

Browse files
committed
Add password-file flag to jwt sign and verify
1 parent 949a833 commit 3b42211

File tree

7 files changed

+91
-17
lines changed

7 files changed

+91
-17
lines changed

command/crypto/jwt/sign.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ string. When used with '--jwk' the <kid> value must match the **"kid"** member
185185
of the JWK. When used with **--jwks** (a JWK Set) the <kid> value must match
186186
the **"kid"** member of one of the JWKs in the JWK Set.`,
187187
},
188+
cli.StringFlag{
189+
Name: "password-file",
190+
Usage: `The path to the <file> containing the password to decrypt the key.`,
191+
},
188192
cli.BoolFlag{
189193
Name: "subtle",
190194
Hidden: true,
@@ -246,6 +250,9 @@ func signAction(ctx *cli.Context) error {
246250
if isSubtle {
247251
options = append(options, jose.WithSubtle(true))
248252
}
253+
if passwordFile := ctx.String("password-file"); len(passwordFile) > 0 {
254+
options = append(options, jose.WithPasswordFile(passwordFile))
255+
}
249256

250257
// Read key from --key or --jwks
251258
var jwk *jose.JSONWebKey

command/crypto/jwt/verify.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ a "kid" member the '--kid' flag can be used.`,
8282
The KID argument is a case-sensitive string. If the input JWS has a "kid"
8383
member its value must match <kid> or verification will fail.`,
8484
},
85+
cli.StringFlag{
86+
Name: "password-file",
87+
Usage: `The path to the <file> containing the password to decrypt the key.`,
88+
},
8589
cli.BoolFlag{
8690
Name: "subtle",
8791
Hidden: true,
@@ -172,6 +176,9 @@ func verifyAction(ctx *cli.Context) error {
172176
if !ctx.Bool("insecure") {
173177
options = append(options, jose.WithNoDefaults(true))
174178
}
179+
if passwordFile := ctx.String("password-file"); len(passwordFile) > 0 {
180+
options = append(options, jose.WithPasswordFile(passwordFile))
181+
}
175182

176183
// Read key from --key or --jwks
177184
var jwk *jose.JSONWebKey

jose/options.go

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,95 @@
11
package jose
22

3+
import (
4+
"github.com/smallstep/cli/utils"
5+
)
6+
37
type context struct {
48
use, alg, kid string
59
subtle, insecure bool
610
noDefaults bool
711
password []byte
812
}
913

10-
// apply the options to the context and returns it.
11-
func (ctx *context) apply(opts ...Option) *context {
14+
// apply the options to the context and returns an error if one of the options
15+
// fails.
16+
func (ctx *context) apply(opts ...Option) (*context, error) {
1217
for _, opt := range opts {
13-
opt(ctx)
18+
if err := opt(ctx); err != nil {
19+
return nil, err
20+
}
1421
}
15-
return ctx
22+
return ctx, nil
1623
}
1724

1825
// Option is the type used to add attributes to the context.
19-
type Option func(ctx *context)
26+
type Option func(ctx *context) error
2027

2128
// WithUse adds the use claim to the context.
2229
func WithUse(use string) Option {
23-
return func(ctx *context) {
30+
return func(ctx *context) error {
2431
ctx.use = use
32+
return nil
2533
}
2634
}
2735

2836
// WithAlg adds the alg claim to the context.
2937
func WithAlg(alg string) Option {
30-
return func(ctx *context) {
38+
return func(ctx *context) error {
3139
ctx.alg = alg
40+
return nil
3241
}
3342
}
3443

3544
// WithKid adds the kid property to the context.
3645
func WithKid(kid string) Option {
37-
return func(ctx *context) {
46+
return func(ctx *context) error {
3847
ctx.kid = kid
48+
return nil
3949
}
4050
}
4151

4252
// WithSubtle marks the context as subtle.
4353
func WithSubtle(subtle bool) Option {
44-
return func(ctx *context) {
54+
return func(ctx *context) error {
4555
ctx.subtle = subtle
56+
return nil
4657
}
4758
}
4859

4960
// WithInsecure marks the context as insecure.
5061
func WithInsecure(insecure bool) Option {
51-
return func(ctx *context) {
62+
return func(ctx *context) error {
5263
ctx.insecure = insecure
64+
return nil
5365
}
5466
}
5567

5668
// WithNoDefaults avoids that the parser loads defaults values, specially the
5769
// default algorithms.
5870
func WithNoDefaults(val bool) Option {
59-
return func(ctx *context) {
71+
return func(ctx *context) error {
6072
ctx.noDefaults = val
73+
return nil
6174
}
6275
}
6376

6477
// WithPassword is a method that adds the given password to the context.
6578
func WithPassword(pass []byte) Option {
66-
return func(ctx *context) {
79+
return func(ctx *context) error {
6780
ctx.password = pass
81+
return nil
82+
}
83+
}
84+
85+
// WithPasswordFile is a method that adds the password in a file to the context.
86+
func WithPasswordFile(filename string) Option {
87+
return func(ctx *context) error {
88+
b, err := utils.ReadPasswordFromFile(filename)
89+
if err != nil {
90+
return err
91+
}
92+
ctx.password = b
93+
return nil
6894
}
6995
}

jose/parse.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ const MaxDecryptTries = 3
3333
// it will return the raw data if it's not encrypted or the format is not
3434
// valid.
3535
func Decrypt(prompt string, data []byte, opts ...Option) ([]byte, error) {
36-
ctx := new(context).apply(opts...)
36+
ctx, err := new(context).apply(opts...)
37+
if err != nil {
38+
return nil, err
39+
}
3740

3841
enc, err := jose.ParseEncrypted(string(data))
3942
if err != nil {
@@ -64,7 +67,10 @@ func Decrypt(prompt string, data []byte, opts ...Option) ([]byte, error) {
6467
// password protected keys, it will ask the user for a password.
6568
// func ParseKey(filename, use, alg, kid string, subtle bool) (*JSONWebKey, error) {
6669
func ParseKey(filename string, opts ...Option) (*JSONWebKey, error) {
67-
ctx := new(context).apply(opts...)
70+
ctx, err := new(context).apply(opts...)
71+
if err != nil {
72+
return nil, err
73+
}
6874

6975
b, err := ioutil.ReadFile(filename)
7076
if err != nil {
@@ -143,7 +149,10 @@ func ReadJWKSet(filename string) ([]byte, error) {
143149
// a given file.
144150
// func ParseKeySet(filename, alg, kid string, isSubtle bool) (*jose.JSONWebKey, error) {
145151
func ParseKeySet(filename string, opts ...Option) (*jose.JSONWebKey, error) {
146-
ctx := new(context).apply(opts...)
152+
ctx, err := new(context).apply(opts...)
153+
if err != nil {
154+
return nil, err
155+
}
147156

148157
b, err := ReadJWKSet(filename)
149158
if err != nil {

jose/parse_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,15 @@ func TestGuessJWKAlgorithm(t *testing.T) {
207207
}
208208

209209
// With context
210-
ctx := new(context).apply(WithAlg(HS256))
210+
ctx, err := new(context).apply(WithAlg(HS256))
211+
assert.NoError(t, err)
211212
jwk := &JSONWebKey{Key: []byte("password")}
212213
guessJWKAlgorithm(ctx, jwk)
213214
assert.Equals(t, HS256, jwk.Algorithm)
214215

215216
// With algorithm set
216-
ctx = new(context).apply(WithAlg(HS256))
217+
ctx, err = new(context).apply(WithAlg(HS256))
218+
assert.NoError(t, err)
217219
jwk = &JSONWebKey{Key: []byte("password"), Algorithm: HS384}
218220
guessJWKAlgorithm(ctx, jwk)
219221
assert.Equals(t, HS384, jwk.Algorithm)

utils/read.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package utils
22

33
import (
44
"bufio"
5+
"bytes"
56
"fmt"
67
"io"
78
"io/ioutil"
89
"os"
910
"strings"
1011
"syscall"
12+
"unicode"
1113

1214
"github.com/pkg/errors"
1315
"github.com/smallstep/cli/crypto/randutil"
@@ -83,6 +85,17 @@ func ReadPasswordGenerate(prompt string) ([]byte, error) {
8385
return pass, errors.Wrap(err, "error reading password")
8486
}
8587

88+
// ReadPasswordFromFile reads and returns the password from the given filename.
89+
// The contents of the file will be trimmed at the right.
90+
func ReadPasswordFromFile(filename string) ([]byte, error) {
91+
password, err := ioutil.ReadFile(filename)
92+
if err != nil {
93+
return nil, errs.FileError(err, filename)
94+
}
95+
password = bytes.TrimRightFunc(password, unicode.IsSpace)
96+
return password, nil
97+
}
98+
8699
// ReadInput from stdin if something is detected or ask the user for an input
87100
// using the given prompt.
88101
func ReadInput(prompt string) ([]byte, error) {

utils/read_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ func TestReadFileStdin(t *testing.T) {
3838
require.True(t, bytes.Equal(content, b), "expected %s to equal %s", b, content)
3939
}
4040

41+
func TestReadPasswordFromFile(t *testing.T) {
42+
content := []byte("my-password-on-file\n")
43+
f, cleanup := newFile(t, content)
44+
defer cleanup()
45+
46+
b, err := ReadPasswordFromFile(f.Name())
47+
require.NoError(t, err)
48+
require.True(t, bytes.Equal([]byte("my-password-on-file"), b), "expected %s to equal %s", b, content)
49+
}
50+
4151
// Returns a temp file and a cleanup function to delete it.
4252
func newFile(t *testing.T, data []byte) (file *os.File, cleanup func()) {
4353
f, err := ioutil.TempFile("" /* dir */, "utils-read-test")

0 commit comments

Comments
 (0)