]> ruderich.org/simon Gitweb - nsscash/nsscash.git/commitdiff
nsscash: add "username"/"passsword" options for files
authorSimon Ruderich <simon@ruderich.org>
Sat, 3 Aug 2019 05:04:41 +0000 (07:04 +0200)
committerSimon Ruderich <simon@ruderich.org>
Sat, 3 Aug 2019 05:04:41 +0000 (07:04 +0200)
README
config.go
fetch.go
file.go
main_test.go

diff --git a/README b/README
index 4febddfaff66a3f16f662fe216b82e4ca3c5cd57..278f5a149ddb780ba44839a27a850f7b7b4b807a 100644 (file)
--- a/README
+++ b/README
@@ -152,6 +152,10 @@ keys are available:
   only certificates signed by this CA. Defaults to the system's certificate
   store when omitted.
 
+- `username`/`password`: Username and password sent via HTTP Basic-Auth to the
+  webserver. The configuration file must not be readable by other users when
+  this is used.
+
 - `path`: Path to store the retrieved file
 
 
index 8db49c1fe4875ebd959a084996280ea6f82cef09..18c6520222fd115a9fd091ebc15ce73f9cf5d0d3 100644 (file)
--- a/config.go
+++ b/config.go
@@ -19,6 +19,7 @@ package main
 
 import (
        "fmt"
+       "os"
 
        "github.com/BurntSushi/toml"
 )
@@ -33,6 +34,8 @@ type File struct {
        Url  string
        Path string
        CA   string
+       Username string
+       Password string
 
        body []byte // internally used by handleFiles()
 }
@@ -72,6 +75,13 @@ func LoadConfig(path string) (*Config, error) {
                return nil, fmt.Errorf("invalid fields used: %q", undecoded)
        }
 
+       f, err := os.Stat(path)
+       if err != nil {
+               return nil, err
+       }
+       perms := f.Mode().Perm()
+       unsafe := (perms & 0077) != 0 // readable by others
+
        if cfg.StatePath == "" {
                return nil, fmt.Errorf("statepath must not be empty")
        }
@@ -85,6 +95,12 @@ func LoadConfig(path string) (*Config, error) {
                        return nil, fmt.Errorf(
                                "file[%d].path must not be empty", i)
                }
+               if (f.Username != "" || f.Password != "") && unsafe {
+                       return nil, fmt.Errorf(
+                               "file[%d].username/passsword in use and "+
+                                       "unsafe permissions %v on %q",
+                               i, perms, path)
+               }
        }
 
        return &cfg, nil
index 491720a88786d0c363bc612048a6ce0e1f6b98b5..16e8d902a1d3f179e35a07851650243c61c17abf 100644 (file)
--- a/fetch.go
+++ b/fetch.go
@@ -36,11 +36,14 @@ func init() {
        clients[""] = &http.Client{}
 }
 
-func fetchIfModified(url, ca string, lastModified *time.Time) (int, []byte, error) {
+func fetchIfModified(url, user, pass, ca string, lastModified *time.Time) (int, []byte, error) {
        req, err := http.NewRequest("GET", url, nil)
        if err != nil {
                return 0, nil, err
        }
+       if user != "" || pass != "" {
+               req.SetBasicAuth(user, pass)
+       }
        if !lastModified.IsZero() {
                req.Header.Add("If-Modified-Since",
                        lastModified.UTC().Format(http.TimeFormat))
diff --git a/file.go b/file.go
index 07ac987cbec6d9fb128cd746e467461750f44882..e06a9bce335ccc4e4444e5f8ea9502d27df09eba 100644 (file)
--- a/file.go
+++ b/file.go
@@ -89,7 +89,8 @@ func fetchFile(file *File, state *State) error {
                t = zero // force download
        }
 
-       status, body, err := fetchIfModified(file.Url, file.CA, &t)
+       status, body, err := fetchIfModified(file.Url,
+               file.Username, file.Password, file.CA, &t)
        if err != nil {
                return err
        }
index ee79db903c3416a7327a04372b08f239f8bdbda6..181930f29da7034c7bac6d94b16b09ea7f11cd7d 100644 (file)
@@ -66,6 +66,12 @@ func mustNotExist(t *testing.T, paths ...string) {
        }
 }
 
+func hashAsHex(x []byte) string {
+       h := sha1.New()
+       h.Write(x)
+       return hex.EncodeToString(h.Sum(nil))
+}
+
 // mustHaveHash checks if the given path content has the given SHA-1 string
 // (in hex).
 func mustHaveHash(t *testing.T, path string, hash string) {
@@ -74,10 +80,7 @@ func mustHaveHash(t *testing.T, path string, hash string) {
                t.Fatal(err)
        }
 
-       h := sha1.New()
-       h.Write(x)
-       y := hex.EncodeToString(h.Sum(nil))
-
+       y := hashAsHex(x)
        if y != hash {
                t.Errorf("%q has unexpected hash %q", path, y)
        }
@@ -212,6 +215,7 @@ func TestMainFetch(t *testing.T) {
                fetchStateCannotWrite,
                fetchCannotDeploy,
                fetchSecondFetchFails,
+               fetchBasicAuth,
        }
 
        // HTTP tests
@@ -795,6 +799,87 @@ ca = "%[5]s"
        mustBeOld(t, passwdPath, groupPath)
 }
 
+func fetchBasicAuth(a args) {
+       t := a.t
+       mustWritePasswdConfig(t, a.url)
+       mustCreate(t, passwdPath)
+       mustHaveHash(t, passwdPath, "da39a3ee5e6b4b0d3255bfef95601890afd80709")
+
+       validUser := "username"
+       validPass := "password"
+
+       *a.handler = func(w http.ResponseWriter, r *http.Request) {
+               if r.URL.Path != "/passwd" {
+                       return
+               }
+
+               user, pass, ok := r.BasicAuth()
+               // NOTE: Do not use this in production because it permits
+               // attackers to determine the length of user/pass. Instead use
+               // hashes and subtle.ConstantTimeCompare().
+               if !ok || user != validUser || pass != validPass {
+                       w.Header().Set("WWW-Authenticate", `Basic realm="Test"`)
+                       w.WriteHeader(http.StatusUnauthorized)
+                       return
+               }
+
+               fmt.Fprintln(w, "root:x:0:0:root:/root:/bin/bash")
+               fmt.Fprintln(w, "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin")
+       }
+
+       t.Log("Missing authentication")
+
+       err := mainFetch(configPath)
+       mustBeErrorWithSubstring(t, err,
+               "status code 401")
+
+       mustNotExist(t, statePath, groupPath, plainPath)
+       mustBeOld(t, passwdPath)
+
+       t.Log("Unsafe config permissions")
+
+       mustWriteConfig(t, fmt.Sprintf(`
+statepath = "%[1]s"
+
+[[file]]
+type = "passwd"
+url = "%[2]s/passwd"
+path = "%[3]s"
+ca = "%[4]s"
+username = "%[5]s"
+password = "%[6]s"
+`, statePath, a.url, passwdPath, tlsCAPath, validUser, validPass))
+
+       err = os.Chmod(configPath, 0644)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       err = mainFetch(configPath)
+       mustBeErrorWithSubstring(t, err,
+               "file[0].username/passsword in use and unsafe permissions "+
+                       "-rw-r--r-- on \"testdata/config.toml\"")
+
+       mustNotExist(t, statePath, groupPath, plainPath)
+       mustBeOld(t, passwdPath)
+
+       t.Log("Working authentication")
+
+       err = os.Chmod(configPath, 0600)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       err = mainFetch(configPath)
+       if err != nil {
+               t.Error(err)
+       }
+
+       mustNotExist(t, plainPath, groupPath)
+       mustBeNew(t, passwdPath, statePath)
+       mustHaveHash(t, passwdPath, "bbb7db67469b111200400e2470346d5515d64c23")
+}
+
 func fetchInvalidCA(a args) {
        t := a.t