Skip to content

Commit b9eb377

Browse files
committed
[FABC-905] Add passfile to server config file template
Adds support for bootstrapping server using a password file. The password file will take precedence over password specified in config or flag. Signed-off-by: Tiffany Harris <[email protected]>
1 parent d510ff3 commit b9eb377

File tree

8 files changed

+169
-42
lines changed

8 files changed

+169
-42
lines changed

cmd/fabric-ca-server/config.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"io/ioutil"
1111
"os"
1212
"path/filepath"
13+
"regexp"
1314
"strings"
1415

1516
"github.com/cloudflare/cfssl/log"
@@ -160,6 +161,7 @@ registry:
160161
identities:
161162
- name: <<<ADMIN>>>
162163
pass: <<<ADMINPW>>>
164+
passFile: <<<ADMINPASSFILE>>>
163165
type: client
164166
affiliation: ""
165167
attrs:
@@ -619,7 +621,7 @@ func (s *ServerCmd) configInit() (err error) {
619621
}
620622

621623
func (s *ServerCmd) createDefaultConfigFile() error {
622-
var user, pass string
624+
var user, pass, upFile string
623625
// If LDAP is enabled, authentication of enrollment requests are performed
624626
// by using LDAP authentication; therefore, no bootstrap username and password
625627
// are required.
@@ -631,6 +633,7 @@ func (s *ServerCmd) createDefaultConfigFile() error {
631633
// bootstrap administrator. Other identities can be dynamically registered.
632634
// Create the default config, but only if they provided this bootstrap
633635
// username and password.
636+
upFile = s.myViper.GetString("bootfile")
634637
up := s.myViper.GetString("boot")
635638
if up == "" {
636639
return errors.New("The '-b user:pass' option is required")
@@ -644,11 +647,12 @@ func (s *ServerCmd) createDefaultConfigFile() error {
644647
}
645648
user = ups[0]
646649
pass = ups[1]
650+
647651
if len(user) >= 1024 {
648652
return errors.Errorf("The identity name must be less than 1024 characters: '%s'", user)
649653
}
650-
if len(pass) == 0 {
651-
return errors.New("An empty password in the '-b user:pass' option is not permitted")
654+
if len(pass) == 0 && upFile == "" {
655+
return errors.New("An empty password in the '-b user:pass' and '-f passfile' option is not permitted")
652656
}
653657
}
654658

@@ -662,6 +666,13 @@ func (s *ServerCmd) createDefaultConfigFile() error {
662666
// Do string subtitution to get the default config
663667
cfg := strings.Replace(defaultCfgTemplate, "<<<VERSION>>>", metadata.Version, 1)
664668
cfg = strings.Replace(cfg, "<<<ADMIN>>>", user, 1)
669+
// When not provided, remove passfile from template
670+
if upFile != "" && !util.FileExists(upFile) {
671+
re := regexp.MustCompile("(?m)[\r\n]+^.*ADMINPASSFILE.*$")
672+
cfg = re.ReplaceAllString(cfg, "")
673+
} else {
674+
cfg = strings.Replace(cfg, "<<<ADMINPASSFILE>>>", upFile, 1)
675+
}
665676
cfg = strings.Replace(cfg, "<<<ADMINPW>>>", pass, 1)
666677
cfg = strings.Replace(cfg, "<<<MYHOST>>>", myhost, 1)
667678
purl := s.myViper.GetString("intermediate.parentserver.url")

cmd/fabric-ca-server/main_test.go

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"os"
1515
"path"
1616
"path/filepath"
17-
"regexp"
1817
"testing"
1918

2019
"github.com/cloudflare/cfssl/log"
@@ -27,25 +26,15 @@ import (
2726
)
2827

2928
const (
30-
initYaml = "i.yaml"
3129
startYaml = "s.yaml"
3230
ldapTestDir = "ldapTestDir"
3331
)
3432

3533
var (
3634
longUserName = util.RandomString(1025)
37-
)
38-
39-
var (
4035
longFileName = util.RandomString(261)
4136
)
4237

43-
// Create a config element in unexpected format
44-
var badSyntaxYaml = "bad.yaml"
45-
46-
// Unsupported file type
47-
var unsupportedFileType = "config.txt"
48-
4938
type TestData struct {
5039
input []string // input
5140
expected string // expected result
@@ -69,19 +58,6 @@ func checkTest(in *TestData, t *testing.T) {
6958
}
7059
}
7160

72-
// errorTest validates error cases
73-
func errorTest(in *TestData, t *testing.T) {
74-
err := RunMain(in.input)
75-
if err != nil {
76-
matched, _ := regexp.MatchString(in.expected, err.Error())
77-
if !matched {
78-
t.Errorf("FAILED:\n \tin: %v;\n \tout: %v;\n \texpected: %v\n", in.input, err.Error(), in.expected)
79-
}
80-
} else {
81-
t.Errorf("FAILED:\n \tin: %v;\n \tout: <nil>\n \texpected: %v\n", in.input, in.expected)
82-
}
83-
}
84-
8561
func TestMain(m *testing.M) {
8662
os.Setenv("FABRIC_CA_SERVER_OPERATIONS_LISTENADDRESS", "localhost:0")
8763
defer os.Unsetenv("FABRIC_CA_SERVER_OPERATIONS_LISTENADDRESS")
@@ -99,9 +75,22 @@ func TestNoArguments(t *testing.T) {
9975

10076
func TestErrors(t *testing.T) {
10177
os.Unsetenv(homeEnvVar)
78+
initYaml := "i.yaml"
79+
// Create a config element in unexpected format
80+
badSyntaxYaml := "bad.yaml"
81+
// Unsupported file type
82+
unsupportedFileType := "config.txt"
10283
_ = ioutil.WriteFile(badSyntaxYaml, []byte("signing: true\n"), 0644)
103-
104-
errorCases := []TestData{
84+
defer func() {
85+
os.Remove(badSyntaxYaml)
86+
os.Remove(unsupportedFileType)
87+
os.Remove(startYaml)
88+
}()
89+
90+
tests := []struct {
91+
cmd []string
92+
expected string
93+
}{
10594
{[]string{cmdName, "init", "-c", initYaml}, "option is required"},
10695
{[]string{cmdName, "init", "-c", initYaml, "-n", "acme.com", "-b", "user::"}, "Failed to read"},
10796
{[]string{cmdName, "init", "-b", "user:pass", "-n", "acme.com", "ca.key"}, "Unrecognized arguments found"},
@@ -118,20 +107,45 @@ func TestErrors(t *testing.T) {
118107
{[]string{cmdName, "start", "-c", startYaml, "-b", "user:pass", "ca.key"}, "Unrecognized arguments found"},
119108
}
120109

121-
for _, e := range errorCases {
122-
errorTest(&e, t)
123-
_ = os.Remove(initYaml)
110+
for _, tt := range tests {
111+
tt := tt
112+
t.Run(tt.expected, func(t *testing.T) {
113+
err := RunMain(tt.cmd)
114+
assert.Error(t, err, tt.expected)
115+
os.Remove(initYaml)
116+
})
124117
}
125118
}
126119

127120
func TestOneTimePass(t *testing.T) {
128-
testDir := "oneTimePass"
129-
os.RemoveAll(testDir)
130-
defer os.RemoveAll(testDir)
131-
// Test with "-b" option
132-
err := RunMain([]string{cmdName, "init", "-b", "admin:adminpw", "--registry.maxenrollments", "1", "-H", testDir})
133-
if err != nil {
134-
t.Fatalf("Failed to init server with one time passwords: %s", err)
121+
tests := []struct {
122+
testName string
123+
cmd []string
124+
}{
125+
{
126+
testName: "When identity has user and pass",
127+
cmd: []string{cmdName, "init", "-b", "admin:adminpw", "--registry.maxenrollments", "1", "-H", os.TempDir()},
128+
},
129+
{
130+
testName: "When identity has user and passfile",
131+
cmd: []string{cmdName, "init", "-b", "admin:adminpw", "-f", "pwFile", "--registry.maxenrollments", "1", "-H", os.TempDir()},
132+
},
133+
{
134+
testName: "When identity has user, pass, and passfile",
135+
cmd: []string{cmdName, "init", "-b", "admin:adminpw", "-f", "pwFile", "--registry.maxenrollments", "1", "-H", os.TempDir()},
136+
},
137+
}
138+
139+
for _, tt := range tests {
140+
t.Run(tt.testName, func(t *testing.T) {
141+
defer os.Remove(filepath.Join(os.TempDir(), "pwFile"))
142+
143+
err := ioutil.WriteFile(filepath.Join(os.TempDir(), "pwFile"), []byte("mypassword\n"), 0666)
144+
assert.NoError(t, err, "Failed to create passfile")
145+
146+
err = RunMain(tt.cmd)
147+
assert.NoError(t, err, "Failed to init server with one time passwords")
148+
})
135149
}
136150
}
137151

@@ -426,11 +440,8 @@ func checkConfigAndDBLoc(t *testing.T, args TestData, cfgFile string, dsFile str
426440
func TestClean(t *testing.T) {
427441
defYaml := util.GetDefaultConfigFile(cmdName)
428442
os.Remove(defYaml)
429-
os.Remove(initYaml)
430443
os.Remove(startYaml)
431-
os.Remove(badSyntaxYaml)
432444
os.Remove(fmt.Sprintf("/tmp/%s.yaml", longFileName))
433-
os.Remove(unsupportedFileType)
434445
os.Remove("ca-key.pem")
435446
os.Remove("ca-cert.pem")
436447
os.Remove("IssuerSecretKey")

cmd/fabric-ca-server/servercmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ func (s *ServerCmd) registerFlags() {
154154
pflags.StringVarP(&s.homeDirectory, "home", "H", "", fmt.Sprintf("Server's home directory (default \"%s\")", filepath.Dir(cfg)))
155155
util.FlagString(s.myViper, pflags, "boot", "b", "",
156156
"The user:pass for bootstrap admin which is required to build default config file")
157+
util.FlagString(s.myViper, pflags, "bootfile", "f", "",
158+
"Password file for bootstrap admin which is required to build the default config file")
157159

158160
// Register flags for all tagged and exported fields in the config
159161
s.cfg = &lib.ServerConfig{}

docs/source/servercli.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Fabric-CA Server's CLI
1818
Flags:
1919
--address string Listening address of fabric-ca-server (default "0.0.0.0")
2020
-b, --boot string The user:pass for bootstrap admin which is required to build default config file
21+
-f, --bootfile string Password file for bootstrap admin which is required to build the default config file
2122
--ca.certfile string PEM-encoded CA certificate file (default "ca-cert.pem")
2223
--ca.chainfile string PEM-encoded CA chain file (default "ca-chain.pem")
2324
--ca.keyfile string PEM-encoded CA key file

docs/source/serverconfig.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ Fabric-CA Server's Configuration File
133133
identities:
134134
- name: <<<adminUserName>>>
135135
pass: <<<adminPassword>>>
136+
passFile:
136137
type: client
137138
affiliation: ""
138139
attrs:

lib/ca.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"path"
2020
"path/filepath"
2121
"strconv"
22+
"strings"
2223
"sync"
2324
"time"
2425

@@ -825,6 +826,17 @@ func (ca *CA) addIdentity(id *CAConfigIdentity, errIfFound bool) error {
825826
return err
826827
}
827828

829+
// A password file takes precedence over password
830+
if id.PassFile != "" {
831+
passBytes, err := util.ReadFile(id.PassFile)
832+
if err != nil {
833+
return errors.WithMessage(err, fmt.Sprintf("Failed to read password from '%s'", id.PassFile))
834+
}
835+
// Remove the newline from reading file
836+
id.Pass = string(passBytes[:])
837+
id.Pass = strings.TrimSuffix(id.Pass, "\n")
838+
}
839+
828840
rec := cadbuser.Info{
829841
Name: id.Name,
830842
Pass: id.Pass,

lib/ca_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package lib
88

99
import (
1010
"crypto/x509"
11+
"errors"
1112
"fmt"
1213
"io/ioutil"
1314
"os"
@@ -351,6 +352,93 @@ func TestCAgetUserAttrValue(t *testing.T) {
351352
CAclean(ca, t)
352353
}
353354

355+
func TestCAAddIdentity(t *testing.T) {
356+
tests := []struct {
357+
testName string
358+
id *CAConfigIdentity
359+
}{
360+
{
361+
testName: "When identity has user and pass",
362+
id: &CAConfigIdentity{
363+
Name: "admin1",
364+
Pass: "adminpw",
365+
},
366+
},
367+
{
368+
testName: "When identity has user and passfile",
369+
id: &CAConfigIdentity{
370+
Name: "admin2",
371+
PassFile: filepath.Join(os.TempDir(), "adminpwfile"),
372+
},
373+
},
374+
{
375+
testName: "When identity has user, pass, and passfile",
376+
id: &CAConfigIdentity{
377+
Name: "admin3",
378+
Pass: "adminpw",
379+
PassFile: filepath.Join(os.TempDir(), "adminpwfile"),
380+
},
381+
},
382+
}
383+
384+
for _, tt := range tests {
385+
t.Run(tt.testName, func(t *testing.T) {
386+
testDirClean(t)
387+
388+
if tt.id.PassFile != "" {
389+
err := ioutil.WriteFile(tt.id.PassFile, []byte("mypassword\n"), 0666)
390+
assert.NoError(t, err, "Failed to create passfile")
391+
defer os.Remove(tt.id.PassFile)
392+
}
393+
394+
cfg = CAConfig{}
395+
cfg.Registry = CAConfigRegistry{MaxEnrollments: 10}
396+
ca, err := newCA(configFile, &cfg, &srv, false)
397+
assert.NoError(t, err, "Failed creating newCA")
398+
399+
err = ca.addIdentity(tt.id, true)
400+
assert.NoError(t, err, "Failed to add new identity")
401+
402+
CAclean(ca, t)
403+
})
404+
}
405+
}
406+
407+
func TestCAAddIdentityFailure(t *testing.T) {
408+
tests := []struct {
409+
testName string
410+
newID *CAConfigIdentity
411+
expectedErr error
412+
}{
413+
{
414+
testName: "When passfile does not exist",
415+
newID: &CAConfigIdentity{
416+
Name: "admin",
417+
Pass: "adminpw",
418+
PassFile: "fakepassfile",
419+
},
420+
expectedErr: errors.New("Failed to read password from 'fakepassfile'"),
421+
},
422+
}
423+
424+
for _, tt := range tests {
425+
t.Run(tt.testName, func(t *testing.T) {
426+
testDirClean(t)
427+
428+
cfg = CAConfig{}
429+
cfg.Registry = CAConfigRegistry{
430+
MaxEnrollments: 10,
431+
}
432+
ca, err := newCA(configFile, &cfg, &srv, false)
433+
assert.NoError(t, err, "Failed creating newCA")
434+
435+
err = ca.addIdentity(tt.newID, true)
436+
assert.Error(t, err, tt.expectedErr)
437+
CAclean(ca, t)
438+
})
439+
}
440+
}
441+
354442
func TestCAaddIdentity(t *testing.T) {
355443
testDirClean(t)
356444
id := &CAConfigIdentity{

lib/caconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ type CAConfigRegistry struct {
148148
type CAConfigIdentity struct {
149149
Name string `mask:"username"`
150150
Pass string `mask:"password"`
151+
PassFile string
151152
Type string
152153
Affiliation string
153154
MaxEnrollments int

0 commit comments

Comments
 (0)