Skip to content

Commit ed7ad1a

Browse files
committed
cli/command/registry: preserve all whitespace in secrets
Preserve all whitespace and treat the secret as an opaque value, leaving it to the registry to (in)validate. We still check for empty values in some places. This partially reverts a21a5f4, but checks for empty (whitespace-only) passwords without mutating the value. This better aligns with [NIST SP 800-63B §5.1.1.2], which describes that the value should be treated as opaque, preserving any other whitespace, including newlines. Note that trimming whitespace may still happen elsewhere (see [NIST SP 800-63B (revision 4) §3.1.1.2]); > Verifiers **MAY** make limited allowances for mistyping (e.g., removing > leading and trailing whitespace characters before verification, allowing > the verification of passwords with differing cases for the leading character) [NIST SP 800-63B §5.1.1.2]: https://pages.nist.gov/800-63-3/sp800-63b.html#memsecretver [NIST SP 800-63B (revision 4) §3.1.1.2]: https://pages.nist.gov/800-63-4/sp800-63b.html#passwordver Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent d7ca8d5 commit ed7ad1a

File tree

3 files changed

+94
-8
lines changed

3 files changed

+94
-8
lines changed

cli/command/registry.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
144144
}
145145
}
146146

147-
argPassword = strings.TrimSpace(argPassword)
148-
if argPassword == "" {
147+
isEmpty := strings.TrimSpace(argPassword) == ""
148+
if isEmpty {
149149
restoreInput, err := prompt.DisableInputEcho(cli.In())
150150
if err != nil {
151151
return registrytypes.AuthConfig{}, err

cli/command/registry/login.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"io"
8-
"strings"
98

109
"github.com/containerd/errdefs"
1110
"github.com/docker/cli/cli"
@@ -88,6 +87,42 @@ func verifyLoginFlags(flags *pflag.FlagSet, opts loginOptions) error {
8887
return nil
8988
}
9089

90+
// readSecretFromStdin reads the secret from r and returns it as a string.
91+
// It trims terminal line-endings (LF, CRLF, or CR), which may be added when
92+
// inputting interactively or piping input. The value is otherwise treated as
93+
// opaque, preserving any other whitespace, including newlines, per [NIST SP 800-63B §5.1.1.2].
94+
// Note that trimming whitespace may still happen elsewhere (see [NIST SP 800-63B (revision 4) §3.1.1.2]);
95+
//
96+
// > Verifiers **MAY** make limited allowances for mistyping (e.g., removing
97+
// > leading and trailing whitespace characters before verification, allowing
98+
// > the verification of passwords with differing cases for the leading character)
99+
//
100+
// [NIST SP 800-63B §5.1.1.2]: https://pages.nist.gov/800-63-3/sp800-63b.html#memsecretver
101+
// [NIST SP 800-63B (revision 4) §3.1.1.2]: https://pages.nist.gov/800-63-4/sp800-63b.html#passwordver
102+
func readSecretFromStdin(r io.Reader) (string, error) {
103+
b, err := io.ReadAll(r)
104+
if err != nil {
105+
return "", err
106+
}
107+
if len(b) == 0 {
108+
return "", nil
109+
}
110+
111+
n := len(b)
112+
switch b[n-1] {
113+
case '\n':
114+
if n >= 2 && b[n-2] == '\r' {
115+
b = b[:n-2]
116+
} else {
117+
b = b[:n-1]
118+
}
119+
case '\r':
120+
b = b[:n-1]
121+
}
122+
123+
return string(b), nil
124+
}
125+
91126
func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error {
92127
if opts.password != "" {
93128
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
@@ -97,14 +132,11 @@ func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error {
97132
if opts.user == "" {
98133
return errors.New("username is empty")
99134
}
100-
101-
contents, err := io.ReadAll(dockerCLI.In())
135+
p, err := readSecretFromStdin(dockerCLI.In())
102136
if err != nil {
103137
return err
104138
}
105-
106-
opts.password = strings.TrimSuffix(string(contents), "\n")
107-
opts.password = strings.TrimSuffix(opts.password, "\r")
139+
opts.password = p
108140
}
109141
return nil
110142
}

cli/command/registry/login_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package registry
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
@@ -287,6 +288,56 @@ func TestRunLogin(t *testing.T) {
287288
},
288289
},
289290
},
291+
{
292+
doc: "password with leading and trailing spaces",
293+
priorCredentials: map[string]configtypes.AuthConfig{},
294+
input: loginOptions{
295+
serverAddress: "reg1",
296+
user: "my-username",
297+
password: " my password with spaces ",
298+
},
299+
expectedCredentials: map[string]configtypes.AuthConfig{
300+
"reg1": {
301+
Username: "my-username",
302+
Password: " my password with spaces ",
303+
ServerAddress: "reg1",
304+
},
305+
},
306+
},
307+
{
308+
doc: "password stdin with line-endings",
309+
priorCredentials: map[string]configtypes.AuthConfig{},
310+
input: loginOptions{
311+
serverAddress: "reg1",
312+
user: "my-username",
313+
passwordStdin: true,
314+
password: " my password with spaces \r\n",
315+
},
316+
expectedCredentials: map[string]configtypes.AuthConfig{
317+
"reg1": {
318+
Username: "my-username",
319+
Password: " my password with spaces ",
320+
ServerAddress: "reg1",
321+
},
322+
},
323+
},
324+
{
325+
doc: "password stdin with multiple line-endings",
326+
priorCredentials: map[string]configtypes.AuthConfig{},
327+
input: loginOptions{
328+
serverAddress: "reg1",
329+
user: "my-username",
330+
passwordStdin: true,
331+
password: " my password\nwith spaces \r\n\r\n",
332+
},
333+
expectedCredentials: map[string]configtypes.AuthConfig{
334+
"reg1": {
335+
Username: "my-username",
336+
Password: " my password\nwith spaces \r\n",
337+
ServerAddress: "reg1",
338+
},
339+
},
340+
},
290341
}
291342

292343
for _, tc := range testCases {
@@ -295,6 +346,9 @@ func TestRunLogin(t *testing.T) {
295346
cfg := configfile.New(filepath.Join(tmpDir, "config.json"))
296347
cli := test.NewFakeCli(&fakeClient{})
297348
cli.SetConfigFile(cfg)
349+
if tc.input.passwordStdin {
350+
cli.SetIn(streams.NewIn(io.NopCloser(bytes.NewBufferString(tc.input.password))))
351+
}
298352

299353
for _, priorCred := range tc.priorCredentials {
300354
assert.NilError(t, cfg.GetCredentialsStore(priorCred.ServerAddress).Store(priorCred))

0 commit comments

Comments
 (0)