Skip to content

Commit

Permalink
WFE/nonce: Add NonceHMACKey field (#7793)
Browse files Browse the repository at this point in the history
Add a new WFE & nonce config field, `NonceHMACKey`, which uses the new
`cmd.HMACKeyConfig` type. Deprecate the `NoncePrefixKey` config field.

Generalize the error message when validating `HMACKeyConfig` in
`config`.

Remove the deprecated `UseDerivablePrefix` config field, which is no
longer used anywhere.

Part of #7632
  • Loading branch information
jprenken authored Nov 13, 2024
1 parent 5be3e99 commit 0a27cba
Show file tree
Hide file tree
Showing 17 changed files with 77 additions and 53 deletions.
26 changes: 21 additions & 5 deletions cmd/boulder-wfe2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,23 @@ type Config struct {
// local and remote nonce-service instances.
RedeemNonceService *cmd.GRPCClientConfig `validate:"required"`

// NonceHMACKey is a path to a file containing an HMAC key which is a
// secret used for deriving the prefix of each nonce instance. It should
// contain 256 bits (32 bytes) of random data to be suitable as an
// HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
// multi-DC deployment this value should be the same across all
// boulder-wfe and nonce-service instances.
NonceHMACKey cmd.HMACKeyConfig `validate:"-"`

// NoncePrefixKey is a secret used for deriving the prefix of each nonce
// instance. It should contain 256 bits of random data to be suitable as
// an HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
// multi-DC deployment this value should be the same across all
// boulder-wfe and nonce-service instances.
//
// TODO(#7632) Update this to use the new HMACKeyConfig.
// TODO(#7632): Remove this.
//
// Deprecated: Use NonceHMACKey instead.
NoncePrefixKey cmd.PasswordConfig `validate:"-"`

// Chains is a list of lists of certificate filenames. Each inner list is
Expand Down Expand Up @@ -294,10 +304,16 @@ func main() {
cmd.Fail("'getNonceService' must be configured")
}

var noncePrefixKey string
if c.WFE.NoncePrefixKey.PasswordFile != "" {
noncePrefixKey, err = c.WFE.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load noncePrefixKey")
var noncePrefixKey []byte
if c.WFE.NonceHMACKey.KeyFile != "" {
noncePrefixKey, err = c.WFE.NonceHMACKey.Load()
cmd.FailOnError(err, "Failed to load nonceHMACKey file")
} else if c.WFE.NoncePrefixKey.PasswordFile != "" {
keyString, err := c.WFE.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load noncePrefixKey file")
noncePrefixKey = []byte(keyString)
} else {
cmd.Fail("NonceHMACKey KeyFile or NoncePrefixKey PasswordFile must be set")
}

getNonceConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, stats, clk)
Expand Down
2 changes: 1 addition & 1 deletion cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ func (hc *HMACKeyConfig) Load() ([]byte, error) {

if len(trimmed) != 32 {
return nil, fmt.Errorf(
"validating unpauseHMACKey, length must be 32 alphanumeric characters, got %d",
"validating HMAC key, length must be 32 alphanumeric characters, got %d",
len(trimmed),
)
}
Expand Down
38 changes: 23 additions & 15 deletions cmd/nonce-service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,32 @@ type Config struct {

MaxUsed int

// UseDerivablePrefix indicates whether to use a nonce prefix derived
// from the gRPC listening address. If this is false, the nonce prefix
// will be the value of the NoncePrefix field. If this is true, the
// NoncePrefixKey field is required.
// TODO(#6610): Remove this.
//
// Deprecated: this value is ignored, and treated as though it is always true.
UseDerivablePrefix bool `validate:"-"`
// NonceHMACKey is a path to a file containing an HMAC key which is a
// secret used for deriving the prefix of each nonce instance. It should
// contain 256 bits (32 bytes) of random data to be suitable as an
// HMAC-SHA256 key (e.g. the output of `openssl rand -hex 32`). In a
// multi-DC deployment this value should be the same across all
// boulder-wfe and nonce-service instances.
NonceHMACKey cmd.HMACKeyConfig `validate:"required_without_all=NoncePrefixKey,structonly"`

// NoncePrefixKey is a secret used for deriving the prefix of each nonce
// instance. It should contain 256 bits (32 bytes) of random data to be
// suitable as an HMAC-SHA256 key (e.g. the output of `openssl rand -hex
// 32`). In a multi-DC deployment this value should be the same across
// all boulder-wfe and nonce-service instances.
//
// TODO(#7632) Update this to use the new HMACKeyConfig.
NoncePrefixKey cmd.PasswordConfig `validate:"required"`
// TODO(#7632): Remove this and change `NonceHMACKey`'s validation to
// just `required.`
//
// Deprecated: Use NonceHMACKey instead.
NoncePrefixKey cmd.PasswordConfig `validate:"required_without_all=NonceHMACKey,structonly"`

Syslog cmd.SyslogConfig
OpenTelemetry cmd.OpenTelemetryConfig
}
}

func derivePrefix(key string, grpcAddr string) (string, error) {
func derivePrefix(key []byte, grpcAddr string) (string, error) {
host, port, err := net.SplitHostPort(grpcAddr)
if err != nil {
return "", fmt.Errorf("parsing gRPC listen address: %w", err)
Expand Down Expand Up @@ -84,12 +86,18 @@ func main() {
c.NonceService.DebugAddr = *debugAddr
}

if c.NonceService.NoncePrefixKey.PasswordFile == "" {
cmd.Fail("NoncePrefixKey PasswordFile must be set")
var key []byte
if c.NonceService.NonceHMACKey.KeyFile != "" {
key, err = c.NonceService.NonceHMACKey.Load()
cmd.FailOnError(err, "Failed to load 'nonceHMACKey' file.")
} else if c.NonceService.NoncePrefixKey.PasswordFile != "" {
keyString, err := c.NonceService.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load 'noncePrefixKey' file.")
key = []byte(keyString)
} else {
cmd.Fail("NonceHMACKey KeyFile or NoncePrefixKey PasswordFile must be set")
}

key, err := c.NonceService.NoncePrefixKey.Pass()
cmd.FailOnError(err, "Failed to load 'noncePrefixKey' file.")
noncePrefix, err := derivePrefix(key, c.NonceService.GRPC.Address)
cmd.FailOnError(err, "Failed to derive nonce prefix")

Expand Down
4 changes: 2 additions & 2 deletions grpc/noncebalancer/noncebalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var ErrNoBackendsMatchPrefix = status.New(codes.Unavailable, "no backends match
var errMissingPrefixCtxKey = errors.New("nonce.PrefixCtxKey value required in RPC context")
var errMissingHMACKeyCtxKey = errors.New("nonce.HMACKeyCtxKey value required in RPC context")
var errInvalidPrefixCtxKeyType = errors.New("nonce.PrefixCtxKey value in RPC context must be a string")
var errInvalidHMACKeyCtxKeyType = errors.New("nonce.HMACKeyCtxKey value in RPC context must be a string")
var errInvalidHMACKeyCtxKeyType = errors.New("nonce.HMACKeyCtxKey value in RPC context must be a byte slice")

// Balancer implements the base.PickerBuilder interface. It's used to create new
// balancer.Picker instances. It should only be used by nonce-service clients.
Expand Down Expand Up @@ -84,7 +84,7 @@ func (p *Picker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
// This should never happen.
return balancer.PickResult{}, errMissingHMACKeyCtxKey
}
hmacKey, ok := hmacKeyVal.(string)
hmacKey, ok := hmacKeyVal.([]byte)
if !ok {
// This should never happen.
return balancer.PickResult{}, errInvalidHMACKeyCtxKeyType
Expand Down
19 changes: 10 additions & 9 deletions grpc/noncebalancer/noncebalancer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import (
"context"
"testing"

"github.com/letsencrypt/boulder/nonce"
"github.com/letsencrypt/boulder/test"
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/base"
"google.golang.org/grpc/resolver"

"github.com/letsencrypt/boulder/nonce"
"github.com/letsencrypt/boulder/test"
)

func TestPickerPicksCorrectBackend(t *testing.T) {
_, p, subConns := setupTest(false)
prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, "Kala namak")
prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, []byte("Kala namak"))

testCtx := context.WithValue(context.Background(), nonce.PrefixCtxKey{}, "HNmOnt8w")
testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, prefix)
testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, []byte(prefix))
info := balancer.PickInfo{Ctx: testCtx}

gotPick, err := p.Pick(info)
Expand All @@ -26,9 +27,9 @@ func TestPickerPicksCorrectBackend(t *testing.T) {

func TestPickerMissingPrefixInCtx(t *testing.T) {
_, p, subConns := setupTest(false)
prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, "Kala namak")
prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, []byte("Kala namak"))

testCtx := context.WithValue(context.Background(), nonce.HMACKeyCtxKey{}, prefix)
testCtx := context.WithValue(context.Background(), nonce.HMACKeyCtxKey{}, []byte(prefix))
info := balancer.PickInfo{Ctx: testCtx}

gotPick, err := p.Pick(info)
Expand All @@ -40,7 +41,7 @@ func TestPickerInvalidPrefixInCtx(t *testing.T) {
_, p, _ := setupTest(false)

testCtx := context.WithValue(context.Background(), nonce.PrefixCtxKey{}, 9)
testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, "foobar")
testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, []byte("foobar"))
info := balancer.PickInfo{Ctx: testCtx}

gotPick, err := p.Pick(info)
Expand Down Expand Up @@ -73,10 +74,10 @@ func TestPickerInvalidHMACKeyInCtx(t *testing.T) {

func TestPickerNoMatchingSubConnAvailable(t *testing.T) {
_, p, subConns := setupTest(false)
prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, "Kala namak")
prefix := nonce.DerivePrefix(subConns[0].addrs[0].Addr, []byte("Kala namak"))

testCtx := context.WithValue(context.Background(), nonce.PrefixCtxKey{}, "rUsTrUin")
testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, prefix)
testCtx = context.WithValue(testCtx, nonce.HMACKeyCtxKey{}, []byte(prefix))
info := balancer.PickInfo{Ctx: testCtx}

gotPick, err := p.Pick(info)
Expand Down
4 changes: 2 additions & 2 deletions nonce/nonce.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ type HMACKeyCtxKey struct{}
// DerivePrefix derives a nonce prefix from the provided listening address and
// key. The prefix is derived by take the first 8 characters of the base64url
// encoded HMAC-SHA256 hash of the listening address using the provided key.
func DerivePrefix(grpcAddr, key string) string {
h := hmac.New(sha256.New, []byte(key))
func DerivePrefix(grpcAddr string, key []byte) string {
h := hmac.New(sha256.New, key)
h.Write([]byte(grpcAddr))
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))[:PrefixLen]
}
Expand Down
2 changes: 1 addition & 1 deletion nonce/nonce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,6 @@ func TestNoncePrefixValidation(t *testing.T) {
}

func TestDerivePrefix(t *testing.T) {
prefix := DerivePrefix("192.168.1.1:8080", "3b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f")
prefix := DerivePrefix("192.168.1.1:8080", []byte("3b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f"))
test.AssertEquals(t, prefix, "P9qQaK4o")
}
4 changes: 2 additions & 2 deletions test/config-next/nonce-a.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"NonceService": {
"maxUsed": 131072,
"noncePrefixKey": {
"passwordFile": "test/secrets/nonce_prefix_key"
"nonceHMACKey": {
"keyFile": "test/secrets/nonce_prefix_key"
},
"syslog": {
"stdoutLevel": 6,
Expand Down
4 changes: 2 additions & 2 deletions test/config-next/nonce-b.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"NonceService": {
"maxUsed": 131072,
"noncePrefixKey": {
"passwordFile": "test/secrets/nonce_prefix_key"
"nonceHMACKey": {
"keyFile": "test/secrets/nonce_prefix_key"
},
"syslog": {
"stdoutLevel": 6,
Expand Down
4 changes: 2 additions & 2 deletions test/config-next/wfe2.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
"noWaitForReady": true,
"hostOverride": "nonce.boulder"
},
"noncePrefixKey": {
"passwordFile": "test/secrets/nonce_prefix_key"
"nonceHMACKey": {
"keyFile": "test/secrets/nonce_prefix_key"
},
"chains": [
[
Expand Down
1 change: 0 additions & 1 deletion test/config/nonce-a.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"NonceService": {
"maxUsed": 131072,
"useDerivablePrefix": true,
"noncePrefixKey": {
"passwordFile": "test/secrets/nonce_prefix_key"
},
Expand Down
1 change: 0 additions & 1 deletion test/config/nonce-b.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"NonceService": {
"maxUsed": 131072,
"useDerivablePrefix": true,
"noncePrefixKey": {
"passwordFile": "test/secrets/nonce_prefix_key"
},
Expand Down
6 changes: 3 additions & 3 deletions test/integration/nonce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type nonceBalancerTestConfig struct {
TLS cmd.TLSConfig
GetNonceService *cmd.GRPCClientConfig
RedeemNonceService *cmd.GRPCClientConfig
NoncePrefixKey cmd.PasswordConfig
NonceHMACKey cmd.HMACKeyConfig
}
}

Expand All @@ -41,8 +41,8 @@ func TestNonceBalancer_NoBackendMatchingPrefix(t *testing.T) {
tlsConfig, err := c.NotWFE.TLS.Load(metrics.NoopRegisterer)
test.AssertNotError(t, err, "Could not load TLS config")

rncKey, err := c.NotWFE.NoncePrefixKey.Pass()
test.AssertNotError(t, err, "Failed to load noncePrefixKey")
rncKey, err := c.NotWFE.NonceHMACKey.Load()
test.AssertNotError(t, err, "Failed to load nonceHMACKey")

clk := clock.New()

Expand Down
4 changes: 2 additions & 2 deletions test/integration/testdata/nonce-client.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"noWaitForReady": true,
"hostOverride": "nonce.boulder"
},
"noncePrefixKey": {
"passwordFile": "test/secrets/nonce_prefix_key"
"nonceHMACKey": {
"keyFile": "test/secrets/nonce_prefix_key"
}
}
}
2 changes: 1 addition & 1 deletion test/secrets/nonce_prefix_key
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f
3b8c758dd85e113ea340ce0b3a99f389
4 changes: 2 additions & 2 deletions wfe2/wfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ type WebFrontEndImpl struct {
rnc nonce.Redeemer
// rncKey is the HMAC key used to derive the prefix of nonce backends used
// for nonce redemption.
rncKey string
rncKey []byte
accountGetter AccountGetter
log blog.Logger
clk clock.Clock
Expand Down Expand Up @@ -194,7 +194,7 @@ func NewWebFrontEndImpl(
sac sapb.StorageAuthorityReadOnlyClient,
gnc nonce.Getter,
rnc nonce.Redeemer,
rncKey string,
rncKey []byte,
accountGetter AccountGetter,
limiter *ratelimits.Limiter,
txnBuilder *ratelimits.TransactionBuilder,
Expand Down
5 changes: 3 additions & 2 deletions wfe2/wfe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) {
log := blog.NewMock()

// Use derived nonces.
noncePrefix := nonce.DerivePrefix("192.168.1.1:8080", "b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f")
rncKey := []byte("b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f")
noncePrefix := nonce.DerivePrefix("192.168.1.1:8080", rncKey)
nonceService, err := nonce.NewNonceService(metrics.NoopRegisterer, 100, noncePrefix)
test.AssertNotError(t, err, "making nonceService")

Expand Down Expand Up @@ -458,7 +459,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) {
mockSA,
gnc,
rnc,
"rncKey",
rncKey,
mockSA,
limiter,
txnBuilder,
Expand Down

0 comments on commit 0a27cba

Please sign in to comment.