@marcel91200 / caddy Mirror · read-only / commits / 937ec342
Fork 0

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 )