incusd: Use constant-time comparison for secrets

Compare cluster join tokens, certificate add tokens, image secrets and
operation secrets in constant time to avoid timing side-channels.

Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
This commit is contained in:
Stéphane Graber 2026-07-04 20:00:47 -04:00
parent 0c33952a14
commit 9c6002e864
No known key found for this signature in database
GPG Key ID: C638974D64792D67
4 changed files with 17 additions and 4 deletions

View File

@ -34,6 +34,7 @@ import (
"github.com/lxc/incus/v7/shared/api"
"github.com/lxc/incus/v7/shared/logger"
localtls "github.com/lxc/incus/v7/shared/tls"
"github.com/lxc/incus/v7/shared/util"
)
var certificatesCmd = APIEndpoint{
@ -342,7 +343,7 @@ func clusterMemberJoinTokenValid(s *state.State, r *http.Request, projectName st
continue
}
if opServerName == joinToken.ServerName && opSecret == joinToken.Secret {
if opServerName == joinToken.ServerName && util.CompareSecret(opSecret, joinToken.Secret) {
foundOp = op
break
}
@ -406,7 +407,7 @@ func certificateTokenValid(s *state.State, r *http.Request, addToken *api.Certif
continue
}
if opSecret == addToken.Secret {
if util.CompareSecret(opSecret, addToken.Secret) {
foundOp = op
break
}

View File

@ -3035,7 +3035,7 @@ func imageValidSecret(s *state.State, r *http.Request, projectName string, finge
continue
}
if opSecret == secret {
if util.CompareSecret(opSecret, secret) {
// Check if the operation is currently running (we allow access while expired).
if op.Status == api.Running.String() {
// Token is single-use, so cancel it now.

View File

@ -960,7 +960,7 @@ func operationWaitGet(d *Daemon, r *http.Request) response.Response {
// First check if the query is for a local operation from this node
op, err := operations.OperationGetInternal(id)
if err == nil {
if secret != "" && op.Metadata()["secret"] != secret {
if secret != "" && !util.CompareSecret(op.Metadata()["secret"], secret) {
return response.Forbidden(nil)
}

View File

@ -1,12 +1,24 @@
package util
import (
"crypto/subtle"
"errors"
"fmt"
"strconv"
"strings"
)
// CompareSecret compares an expected secret (typically read from operation metadata
// as an untyped value) against a provided one in constant time.
func CompareSecret(expected any, provided string) bool {
s, ok := expected.(string)
if !ok {
return false
}
return subtle.ConstantTimeCompare([]byte(s), []byte(provided)) == 1
}
// ParseUint32Range parses a uint32 range in the form "number" or "start-end".
// Returns the start number and the size of the range.
func ParseUint32Range(value string) (uint32, uint32, error) {