Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WFE/nonce: Add NonceHMACKey field #7793

Merged
merged 11 commits into from
Nov 13, 2024
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
10 changes: 8 additions & 2 deletions test/integration/nonce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type nonceBalancerTestConfig struct {
GetNonceService *cmd.GRPCClientConfig
RedeemNonceService *cmd.GRPCClientConfig
NoncePrefixKey cmd.PasswordConfig
NonceHMACKey cmd.HMACKeyConfig
}
}

Expand All @@ -41,8 +42,13 @@ 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")
var rncKey []byte
rncKey, err = c.NotWFE.NonceHMACKey.Load()
if err != nil {
rncKeyString, err := c.NotWFE.NoncePrefixKey.Pass()
test.AssertNotError(t, err, "Failed to load nonceHMACKey or noncePrefixKey")
rncKey = []byte(rncKeyString)
}
jprenken marked this conversation as resolved.
Show resolved Hide resolved

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