Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
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!
Matthew Holt committed
Oct 31, 2020 at 10:51 UTC
937ec342010894f7a6239883b10f6a107ff82c9f
3 files changed
+47
-7
modules/caddyhttp/caddyauth/basicauth.go
+33
-3
@@ -52,11 +52,19 @@ type HTTPBasicAuth struct {
52
// memory for a longer time (this should not be a problem
53
// as long as your machine is not compromised, at which point
54
// all bets are off, since basicauth necessitates plaintext
55
- // passwords being received over the wire anyway).
55
+ // passwords being received over the wire anyway). Note that
56
+ // a cache hit does not mean it is a valid password.
57
HashCache *Cache `json:"hash_cache,omitempty"`
58
59
Accounts map[string]Account `json:"-"`
60
Hash Comparer `json:"-"`
61
+
62
+ // fakePassword is used when a given user is not found,
63
+ // so that timing side-channels can be mitigated: it gives
64
+ // us something to hash and compare even if the user does
65
+ // not exist, which should have similar timing as a user
66
+ // account that does exist.
67
+ fakePassword []byte
68
}
69
70
// CaddyModule returns the Caddy module information.
@@ -84,6 +92,14 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
92
return fmt.Errorf("hash is required")
93
}
94
95
+ // if supported, generate a fake password we can compare against if needed
96
+ if hasher, ok := hba.Hash.(Hasher); ok {
97
+ hba.fakePassword, err = hasher.Hash([]byte("antitiming"), []byte("fakesalt"))
98
+ if err != nil {
99
+ return fmt.Errorf("generating anti-timing password hash: %v", err)
100
+ }
101
+ }
102
+
103
repl := caddy.NewReplacer()
104
105
// load account list
@@ -132,8 +148,12 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
148
}
149
150
account, accountExists := hba.Accounts[username]
135
- // don't return early if account does not exist; we want
136
- // to try to avoid side-channels that leak existence
151
+ if !accountExists {
152
+ // don't return early if account does not exist; we want
153
+ // to try to avoid side-channels that leak existence, so
154
+ // we use a fake password to simulate realistic CPU cycles
155
+ account.password = hba.fakePassword
156
+ }
157
158
same, err := hba.correctPassword(account, []byte(plaintextPasswordStr))
159
if err != nil {
@@ -249,6 +269,16 @@ type Comparer interface {
269
Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error)
270
}
271
272
+// Hasher is a type that can generate a secure hash
273
+// given a plaintext and optional salt (for algorithms
274
+// that require a salt). Hashing modules which implement
275
+// this interface can be used with the hash-password
276
+// subcommand as well as benefitting from anti-timing
277
+// features.
278
+type Hasher interface {
279
+ Hash(plaintext, salt []byte) ([]byte, error)
280
+}
281
+
282
// Account contains a username, password, and salt (if applicable).
283
type Account struct {
284
// A user's username.
modules/caddyhttp/caddyauth/command.go
+2
-4
@@ -25,8 +25,6 @@ import (
25
26
"github.com/caddyserver/caddy/v2"
27
caddycmd "github.com/caddyserver/caddy/v2/cmd"
28
- "golang.org/x/crypto/bcrypt"
29
- "golang.org/x/crypto/scrypt"
28
"golang.org/x/crypto/ssh/terminal"
29
)
30
@@ -116,11 +114,11 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
114
var hash []byte
115
switch algorithm {
116
case "bcrypt":
119
- hash, err = bcrypt.GenerateFromPassword(plaintext, 14)
117
+ hash, err = BcryptHash{}.Hash(plaintext, nil)
118
case "scrypt":
119
def := ScryptHash{}
120
def.SetDefaults()
123
- hash, err = scrypt.Key(plaintext, salt, def.N, def.R, def.P, def.KeyLength)
121
+ hash, err = def.Hash(plaintext, salt)
122
default:
123
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
124
}
modules/caddyhttp/caddyauth/hashes.go
+12
@@ -50,6 +50,11 @@ func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) {
50
return true, nil
51
}
52
53
+// Hash hashes plaintext using a random salt.
54
+func (BcryptHash) Hash(plaintext, _ []byte) ([]byte, error) {
55
+ return bcrypt.GenerateFromPassword(plaintext, 14)
56
+}
57
+
58
// ScryptHash implements the scrypt KDF as a hash.
59
type ScryptHash struct {
60
// scrypt's N parameter. If unset or 0, a safe default is used.
@@ -113,6 +118,11 @@ func (s ScryptHash) Compare(hashed, plaintext, salt []byte) (bool, error) {
118
return false, nil
119
}
120
121
+// Hash hashes plaintext using the given salt.
122
+func (s ScryptHash) Hash(plaintext, salt []byte) ([]byte, error) {
123
+ return scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength)
124
+}
125
+
126
func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
127
return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1
128
}
@@ -121,5 +131,7 @@ func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
131
var (
132
_ Comparer = (*BcryptHash)(nil)
133
_ Comparer = (*ScryptHash)(nil)
134
+ _ Hasher = (*BcryptHash)(nil)
135
+ _ Hasher = (*ScryptHash)(nil)
136
_ caddy.Provisioner = (*ScryptHash)(nil)
137
)