summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/caddyauth/basicauth.go
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2020-10-31 10:51:05 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2020-10-31 10:51:05 -0600
commit937ec342010894f7a6239883b10f6a107ff82c9f (patch)
treee3dc861f6ac4e2675091b5c5b95d2ad3ffb96acb /modules/caddyhttp/caddyauth/basicauth.go
parent966d5e6b42fc6da3da8bd39dd6ceceb8f1da3999 (diff)
caddyauth: Prevent user enumeration by timing
Always follow the code path of hashing and comparing a plaintext password even if the account is not found by the given username; this ensures that similar CPU cycles are spent for both valid and invalid usernames. Thanks to @tylerlm for helping and looking into this!
Diffstat (limited to 'modules/caddyhttp/caddyauth/basicauth.go')
-rw-r--r--modules/caddyhttp/caddyauth/basicauth.go36
1 files changed, 33 insertions, 3 deletions
diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go
index 92e1683..d955773 100644
--- a/modules/caddyhttp/caddyauth/basicauth.go
+++ b/modules/caddyhttp/caddyauth/basicauth.go
@@ -52,11 +52,19 @@ type HTTPBasicAuth struct {
// memory for a longer time (this should not be a problem
// as long as your machine is not compromised, at which point
// all bets are off, since basicauth necessitates plaintext
- // passwords being received over the wire anyway).
+ // passwords being received over the wire anyway). Note that
+ // a cache hit does not mean it is a valid password.
HashCache *Cache `json:"hash_cache,omitempty"`
Accounts map[string]Account `json:"-"`
Hash Comparer `json:"-"`
+
+ // fakePassword is used when a given user is not found,
+ // so that timing side-channels can be mitigated: it gives
+ // us something to hash and compare even if the user does
+ // not exist, which should have similar timing as a user
+ // account that does exist.
+ fakePassword []byte
}
// CaddyModule returns the Caddy module information.
@@ -84,6 +92,14 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
return fmt.Errorf("hash is required")
}
+ // if supported, generate a fake password we can compare against if needed
+ if hasher, ok := hba.Hash.(Hasher); ok {
+ hba.fakePassword, err = hasher.Hash([]byte("antitiming"), []byte("fakesalt"))
+ if err != nil {
+ return fmt.Errorf("generating anti-timing password hash: %v", err)
+ }
+ }
+
repl := caddy.NewReplacer()
// load account list
@@ -132,8 +148,12 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
}
account, accountExists := hba.Accounts[username]
- // don't return early if account does not exist; we want
- // to try to avoid side-channels that leak existence
+ if !accountExists {
+ // don't return early if account does not exist; we want
+ // to try to avoid side-channels that leak existence, so
+ // we use a fake password to simulate realistic CPU cycles
+ account.password = hba.fakePassword
+ }
same, err := hba.correctPassword(account, []byte(plaintextPasswordStr))
if err != nil {
@@ -249,6 +269,16 @@ type Comparer interface {
Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error)
}
+// Hasher is a type that can generate a secure hash
+// given a plaintext and optional salt (for algorithms
+// that require a salt). Hashing modules which implement
+// this interface can be used with the hash-password
+// subcommand as well as benefitting from anti-timing
+// features.
+type Hasher interface {
+ Hash(plaintext, salt []byte) ([]byte, error)
+}
+
// Account contains a username, password, and salt (if applicable).
type Account struct {
// A user's username.