Consistently use constant-time comparison of password hashes

As per https://github.com/golang/go/issues/47001 even subtle.ConstantTimeCompare should never be used with variable-length inputs, as it will return 0 if the lengths do not match. Switch to consistently using constant-time comparisons of hashes for password checks to avoid any possible side-channel leaks that could be combined with other vectors to discover password lengths.

Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
pull/7418/head
Brad Davidson 2 years ago committed by Brad Davidson
parent 9ec1789c21
commit 239021e759

@ -18,13 +18,13 @@ package passwordfile
import ( import (
"context" "context"
"crypto/subtle"
"encoding/csv" "encoding/csv"
"fmt" "fmt"
"io" "io"
"os" "os"
"strings" "strings"
"github.com/k3s-io/k3s/pkg/nodepassword"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticator"
@ -38,7 +38,7 @@ type PasswordAuthenticator struct {
type userPasswordInfo struct { type userPasswordInfo struct {
info *user.DefaultInfo info *user.DefaultInfo
password string hash string
} }
// NewCSV returns a PasswordAuthenticator, populated from a CSV file. // NewCSV returns a PasswordAuthenticator, populated from a CSV file.
@ -65,9 +65,13 @@ func NewCSV(path string) (*PasswordAuthenticator, error) {
if len(record) < 3 { if len(record) < 3 {
return nil, fmt.Errorf("password file '%s' must have at least 3 columns (password, user name, user uid), found %d", path, len(record)) return nil, fmt.Errorf("password file '%s' must have at least 3 columns (password, user name, user uid), found %d", path, len(record))
} }
hash, err := nodepassword.Hasher.CreateHash(record[0])
if err != nil {
return nil, fmt.Errorf("failed to hash password for username '%s' in password file '%s': %v", record[1], path, err)
}
obj := &userPasswordInfo{ obj := &userPasswordInfo{
info: &user.DefaultInfo{Name: record[1], UID: record[2]}, info: &user.DefaultInfo{Name: record[1], UID: record[2]},
password: record[0], hash: hash,
} }
if len(record) >= 4 { if len(record) >= 4 {
obj.info.Groups = strings.Split(record[3], ",") obj.info.Groups = strings.Split(record[3], ",")
@ -88,7 +92,7 @@ func (a *PasswordAuthenticator) AuthenticatePassword(ctx context.Context, userna
if !ok { if !ok {
return nil, false, nil return nil, false, nil
} }
if subtle.ConstantTimeCompare([]byte(user.password), []byte(password)) == 0 { if err := nodepassword.Hasher.VerifyHash(user.hash, password); err != nil {
return nil, false, nil return nil, false, nil
} }
return &authenticator.Response{User: user.info}, true, nil return &authenticator.Response{User: user.info}, true, nil

@ -18,8 +18,8 @@ import (
) )
var ( var (
// hasher provides the algorithm for generating and verifying hashes // Hasher provides the algorithm for generating and verifying hashes
hasher = hash.NewSCrypt() Hasher = hash.NewSCrypt()
) )
func getSecretName(nodeName string) string { func getSecretName(nodeName string) string {
@ -33,7 +33,7 @@ func verifyHash(secretClient coreclient.SecretClient, nodeName, pass string) err
return err return err
} }
if hash, ok := secret.Data["hash"]; ok { if hash, ok := secret.Data["hash"]; ok {
if err := hasher.VerifyHash(string(hash), pass); err != nil { if err := Hasher.VerifyHash(string(hash), pass); err != nil {
return errors.Wrapf(err, "unable to verify hash for node '%s'", nodeName) return errors.Wrapf(err, "unable to verify hash for node '%s'", nodeName)
} }
return nil return nil
@ -47,7 +47,7 @@ func Ensure(secretClient coreclient.SecretClient, nodeName, pass string) error {
return err return err
} }
hash, err := hasher.CreateHash(pass) hash, err := Hasher.CreateHash(pass)
if err != nil { if err != nil {
return errors.Wrapf(err, "unable to create hash for node '%s'", nodeName) return errors.Wrapf(err, "unable to create hash for node '%s'", nodeName)
} }

@ -60,14 +60,6 @@ func Read(file string) (*Passwd, error) {
return result, nil return result, nil
} }
func (p *Passwd) Check(name, pass string) (matches bool, exists bool) {
e, ok := p.names[name]
if !ok {
return false, false
}
return e.pass == pass, true
}
func (p *Passwd) EnsureUser(name, role, passwd string) error { func (p *Passwd) EnsureUser(name, role, passwd string) error {
tokenPrefix := "::" + name + ":" tokenPrefix := "::" + name + ":"
idx := strings.Index(passwd, tokenPrefix) idx := strings.Index(passwd, tokenPrefix)

@ -483,8 +483,12 @@ func verifyLocalPassword(ctx context.Context, config *Config, mu *sync.Mutex, de
return "", http.StatusInternalServerError, errors.Wrap(err, "unable to read node password file") return "", http.StatusInternalServerError, errors.Wrap(err, "unable to read node password file")
} }
password := strings.TrimSpace(string(passBytes)) passHash, err := nodepassword.Hasher.CreateHash(strings.TrimSpace(string(passBytes)))
if password != node.Password { if err != nil {
return "", http.StatusInternalServerError, errors.Wrap(err, "unable to hash node password file")
}
if err := nodepassword.Hasher.VerifyHash(passHash, node.Password); err != nil {
return "", http.StatusForbidden, errors.Wrapf(err, "unable to verify local password for node '%s'", node.Name) return "", http.StatusForbidden, errors.Wrapf(err, "unable to verify local password for node '%s'", node.Name)
} }

Loading…
Cancel
Save