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 (
"context"
"crypto/subtle"
"encoding/csv"
"fmt"
"io"
"os"
"strings"
"github.com/k3s-io/k3s/pkg/nodepassword"
"k8s.io/klog/v2"
"k8s.io/apiserver/pkg/authentication/authenticator"
@ -38,7 +38,7 @@ type PasswordAuthenticator struct {
type userPasswordInfo struct {
info *user.DefaultInfo
password string
hash string
}
// NewCSV returns a PasswordAuthenticator, populated from a CSV file.
@ -65,9 +65,13 @@ func NewCSV(path string) (*PasswordAuthenticator, error) {
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))
}
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{
info: &user.DefaultInfo{Name: record[1], UID: record[2]},
password: record[0],
hash: hash,
}
if len(record) >= 4 {
obj.info.Groups = strings.Split(record[3], ",")
@ -88,7 +92,7 @@ func (a *PasswordAuthenticator) AuthenticatePassword(ctx context.Context, userna
if !ok {
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 &authenticator.Response{User: user.info}, true, nil

@ -18,8 +18,8 @@ import (
)
var (
// hasher provides the algorithm for generating and verifying hashes
hasher = hash.NewSCrypt()
// Hasher provides the algorithm for generating and verifying hashes
Hasher = hash.NewSCrypt()
)
func getSecretName(nodeName string) string {
@ -33,7 +33,7 @@ func verifyHash(secretClient coreclient.SecretClient, nodeName, pass string) err
return err
}
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 nil
@ -47,7 +47,7 @@ func Ensure(secretClient coreclient.SecretClient, nodeName, pass string) error {
return err
}
hash, err := hasher.CreateHash(pass)
hash, err := Hasher.CreateHash(pass)
if err != nil {
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
}
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 {
tokenPrefix := "::" + name + ":"
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")
}
password := strings.TrimSpace(string(passBytes))
if password != node.Password {
passHash, err := nodepassword.Hasher.CreateHash(strings.TrimSpace(string(passBytes)))
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)
}

Loading…
Cancel
Save