Switch to more efficient multi nonce-service design (#4308)

Basically a complete re-write/re-design of the forwarding concept introduced in
#4297 (sorry for the rapid churn here). Instead of nonce-services blindly
forwarding nonces around to each other in an attempt to find out who issued the
nonce we add an identifying prefix to each nonce generated by a service. The
WFEs then use this prefix to decide which nonce-service to ask to validate the
nonce.

This requires a slightly more complicated configuration at the WFE/2 end, but
overall I think ends up being a way cleaner, more understandable, easy to
reason about implementation. When configuring the WFE you need to provide two
forms of gRPC config:

* one gRPC config for retrieving nonces, this should be a DNS name that
resolves to all available nonce-services (or at least the ones you want to
retrieve nonces from locally, in a two DC setup you might only configure the
nonce-services that are in the same DC as the WFE instance). This allows
getting a nonce from any of the configured services and is load-balanced
transparently at the gRPC layer. 
* a map of nonce prefixes to gRPC configs, this maps each individual
nonce-service to it's prefix and allows the WFE instances to figure out which
nonce-service to ask to validate a nonce it has received (in a two DC setup
you'd want to configure this with all the nonce-services across both DCs so
that you can validate a nonce that was generated by a nonce-service in another
DC).

This balancing is implemented in the integration tests.

Given the current remote nonce code hasn't been deployed anywhere yet this
makes a number of hard breaking changes to both the existing nonce-service
code, and the forwarding code.

Fixes #4303.
This commit is contained in:
Roland Bracewell Shoemaker 2019-06-28 09:58:46 -07:00 committed by Daniel McCarney
parent 9094862051
commit af41bea99a
19 changed files with 283 additions and 270 deletions

View File

@ -41,9 +41,17 @@ type config struct {
TLS cmd.TLSConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
NonceService *cmd.GRPCClientConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
// GetNonceService contains a gRPC config for any nonce-service instances
// which we want to retrieve nonces from. In a multi-DC deployment this
// should refer to any local nonce-service instances.
GetNonceService *cmd.GRPCClientConfig
// RedeemNonceServices contains a map of nonce-service prefixes to
// gRPC configs we want to use to redeem nonces. In a multi-DC deployment
// this should contain all nonce-services from all DCs as we want to be
// able to redeem nonces generated at any DC.
RedeemNonceServices map[string]cmd.GRPCClientConfig
Features map[string]bool
@ -63,7 +71,7 @@ type config struct {
}
}
func setupWFE(c config, logger blog.Logger, stats metrics.Scope, clk clock.Clock) (core.RegistrationAuthority, core.StorageAuthority, noncepb.NonceServiceClient) {
func setupWFE(c config, logger blog.Logger, stats metrics.Scope, clk clock.Clock) (core.RegistrationAuthority, core.StorageAuthority, noncepb.NonceServiceClient, map[string]noncepb.NonceServiceClient) {
tlsConfig, err := c.WFE.TLS.Load()
cmd.FailOnError(err, "TLS config")
@ -77,13 +85,19 @@ func setupWFE(c config, logger blog.Logger, stats metrics.Scope, clk clock.Clock
sac := bgrpc.NewStorageAuthorityClient(sapb.NewStorageAuthorityClient(saConn))
var rns noncepb.NonceServiceClient
if c.WFE.NonceService != nil {
rnsConn, err := bgrpc.ClientSetup(c.WFE.NonceService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to Nonce service")
npm := map[string]noncepb.NonceServiceClient{}
if c.WFE.GetNonceService != nil {
rnsConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
rns = noncepb.NewNonceServiceClient(rnsConn)
for prefix, serviceConfig := range c.WFE.RedeemNonceServices {
conn, err := bgrpc.ClientSetup(&serviceConfig, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
npm[prefix] = noncepb.NewNonceServiceClient(conn)
}
}
return rac, sac, rns
return rac, sac, rns, npm
}
func main() {
@ -109,8 +123,8 @@ func main() {
kp, err := goodkey.NewKeyPolicy("") // don't load any weak keys
cmd.FailOnError(err, "Unable to create key policy")
rac, sac, rns := setupWFE(c, logger, scope, clk)
wfe, err := wfe.NewWebFrontEndImpl(scope, clk, kp, rns, logger)
rac, sac, rns, npm := setupWFE(c, logger, scope, clk)
wfe, err := wfe.NewWebFrontEndImpl(scope, clk, kp, rns, npm, logger)
cmd.FailOnError(err, "Unable to create WFE")
wfe.RA = rac
wfe.SA = sac

View File

@ -45,9 +45,17 @@ type config struct {
TLS cmd.TLSConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
NonceService *cmd.GRPCClientConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
// GetNonceService contains a gRPC config for any nonce-service instances
// which we want to retrieve nonces from. In a multi-DC deployment this
// should refer to any local nonce-service instances.
GetNonceService *cmd.GRPCClientConfig
// RedeemNonceServices contains a map of nonce-service prefixes to
// gRPC configs we want to use to redeem nonces. In a multi-DC deployment
// this should contain all nonce-services from all DCs as we want to be
// able to redeem nonces generated at any DC.
RedeemNonceServices map[string]cmd.GRPCClientConfig
// CertificateChains maps AIA issuer URLs to certificate filenames.
// Certificates are read into the chain in the order they are defined in the
@ -182,7 +190,7 @@ func loadCertificateChains(chainConfig map[string][]string) (map[string][]byte,
return results, nil
}
func setupWFE(c config, logger blog.Logger, stats metrics.Scope, clk clock.Clock) (core.RegistrationAuthority, core.StorageAuthority, noncepb.NonceServiceClient) {
func setupWFE(c config, logger blog.Logger, stats metrics.Scope, clk clock.Clock) (core.RegistrationAuthority, core.StorageAuthority, noncepb.NonceServiceClient, map[string]noncepb.NonceServiceClient) {
tlsConfig, err := c.WFE.TLS.Load()
cmd.FailOnError(err, "TLS config")
clientMetrics := bgrpc.NewClientMetrics(stats)
@ -195,13 +203,19 @@ func setupWFE(c config, logger blog.Logger, stats metrics.Scope, clk clock.Clock
sac := bgrpc.NewStorageAuthorityClient(sapb.NewStorageAuthorityClient(saConn))
var rns noncepb.NonceServiceClient
if c.WFE.NonceService != nil {
rnsConn, err := bgrpc.ClientSetup(c.WFE.NonceService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to Nonce service")
npm := map[string]noncepb.NonceServiceClient{}
if c.WFE.GetNonceService != nil {
rnsConn, err := bgrpc.ClientSetup(c.WFE.GetNonceService, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to get nonce service")
rns = noncepb.NewNonceServiceClient(rnsConn)
for prefix, serviceConfig := range c.WFE.RedeemNonceServices {
conn, err := bgrpc.ClientSetup(&serviceConfig, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to redeem nonce service")
npm[prefix] = noncepb.NewNonceServiceClient(conn)
}
}
return rac, sac, rns
return rac, sac, rns, npm
}
func main() {
@ -230,8 +244,8 @@ func main() {
kp, err := goodkey.NewKeyPolicy("") // don't load any weak keys
cmd.FailOnError(err, "Unable to create key policy")
rac, sac, rns := setupWFE(c, logger, scope, clk)
wfe, err := wfe2.NewWebFrontEndImpl(scope, clk, kp, certChains, rns, logger)
rac, sac, rns, npm := setupWFE(c, logger, scope, clk)
wfe, err := wfe2.NewWebFrontEndImpl(scope, clk, kp, certChains, rns, npm, logger)
cmd.FailOnError(err, "Unable to create WFE")
wfe.RA = rac
wfe.SA = sac

View File

@ -3,13 +3,10 @@ package main
import (
"context"
"flag"
"sync"
"time"
"github.com/letsencrypt/boulder/cmd"
corepb "github.com/letsencrypt/boulder/core/proto"
bgrpc "github.com/letsencrypt/boulder/grpc"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/nonce"
noncepb "github.com/letsencrypt/boulder/nonce/proto"
)
@ -17,68 +14,18 @@ import (
type config struct {
NonceService struct {
cmd.ServiceConfig
Syslog cmd.SyslogConfig
MaxUsed int
RemoteNonceServices []cmd.GRPCClientConfig
Syslog cmd.SyslogConfig
MaxUsed int
NoncePrefix string
}
}
type nonceServer struct {
inner *nonce.NonceService
remoteServices []noncepb.NonceServiceClient
log blog.Logger
}
func (ns *nonceServer) remoteRedeem(ctx context.Context, msg *noncepb.NonceMessage) bool {
deadline, ok := ctx.Deadline()
if !ok {
ns.log.Err("Context passed to remoteRedeem does not have a deadline")
return false
}
subCtx, cancel := context.WithDeadline(ctx, deadline.Add(-time.Millisecond*250))
defer cancel()
msg.Forwarded = true
results := make(chan bool, len(ns.remoteServices))
wg := new(sync.WaitGroup)
for _, remote := range ns.remoteServices {
wg.Add(1)
go func(r noncepb.NonceServiceClient) {
defer wg.Done()
resp, err := r.Redeem(subCtx, msg)
if err != nil {
ns.log.Errf("remote Redeem call failed: %s", err)
return
}
results <- resp.Valid
}(remote)
}
go func() {
wg.Wait()
close(results)
}()
for result := range results {
select {
case <-subCtx.Done():
return false
default:
if result {
return true
}
}
}
return false
inner *nonce.NonceService
}
func (ns *nonceServer) Redeem(ctx context.Context, msg *noncepb.NonceMessage) (*noncepb.ValidMessage, error) {
valid := ns.inner.Valid(msg.Nonce)
// If the nonce was not valid, we have configured remote nonce services,
// and this Redeem message wasn't forwarded, then forward it to the
// remote services
if !valid && len(ns.remoteServices) > 0 && !msg.Forwarded {
valid = ns.remoteRedeem(ctx, msg)
}
return &noncepb.ValidMessage{Valid: valid}, nil
return &noncepb.ValidMessage{Valid: ns.inner.Valid(msg.Nonce)}, nil
}
func (ns *nonceServer) Nonce(_ context.Context, _ *corepb.Empty) (*noncepb.NonceMessage, error) {
@ -92,6 +39,7 @@ func (ns *nonceServer) Nonce(_ context.Context, _ *corepb.Empty) (*noncepb.Nonce
func main() {
grpcAddr := flag.String("addr", "", "gRPC listen address override")
debugAddr := flag.String("debug-addr", "", "Debug server address override")
prefixOverride := flag.String("prefix", "", "Override the configured nonce prefix")
configFile := flag.String("config", "", "File path to the configuration file for this service")
flag.Parse()
@ -105,27 +53,21 @@ func main() {
if *debugAddr != "" {
c.NonceService.DebugAddr = *debugAddr
}
if *prefixOverride != "" {
c.NonceService.NoncePrefix = *prefixOverride
}
scope, logger := cmd.StatsAndLogging(c.NonceService.Syslog, c.NonceService.DebugAddr)
defer logger.AuditPanic()
logger.Info(cmd.VersionString())
ns, err := nonce.NewNonceService(scope, c.NonceService.MaxUsed)
ns, err := nonce.NewNonceService(scope, c.NonceService.MaxUsed, c.NonceService.NoncePrefix)
cmd.FailOnError(err, "Failed to initialize nonce service")
tlsConfig, err := c.NonceService.TLS.Load()
cmd.FailOnError(err, "tlsConfig config")
nonceServer := &nonceServer{inner: ns, log: logger}
if len(c.NonceService.RemoteNonceServices) > 0 {
clientMetrics := bgrpc.NewClientMetrics(scope)
clk := cmd.Clock()
for _, remoteNonceConfig := range c.NonceService.RemoteNonceServices {
rnsConn, err := bgrpc.ClientSetup(&remoteNonceConfig, tlsConfig, clientMetrics, clk)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to Nonce service")
nonceServer.remoteServices = append(nonceServer.remoteServices, noncepb.NewNonceServiceClient(rnsConn))
}
}
nonceServer := &nonceServer{inner: ns}
serverMetrics := bgrpc.NewServerMetrics(scope)
grpcSrv, l, err := bgrpc.NewServer(c.NonceService.GRPC, tlsConfig, serverMetrics, cmd.Clock())

View File

@ -1,115 +0,0 @@
package main
import (
"context"
"errors"
"testing"
"time"
corepb "github.com/letsencrypt/boulder/core/proto"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/nonce"
noncepb "github.com/letsencrypt/boulder/nonce/proto"
"github.com/letsencrypt/boulder/test"
"google.golang.org/grpc"
)
type workingRemote struct{ resp bool }
func (wr *workingRemote) Redeem(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
return &noncepb.ValidMessage{
Valid: wr.resp,
}, nil
}
func (wr *workingRemote) Nonce(ctx context.Context, in *corepb.Empty, opts ...grpc.CallOption) (*noncepb.NonceMessage, error) {
return nil, nil
}
type sleepingRemote struct{}
func (sr *sleepingRemote) Redeem(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
time.Sleep(time.Millisecond * 50)
return &noncepb.ValidMessage{
Valid: true,
}, nil
}
func (sr *sleepingRemote) Nonce(ctx context.Context, in *corepb.Empty, opts ...grpc.CallOption) (*noncepb.NonceMessage, error) {
return nil, nil
}
type brokenRemote struct{}
func (br *brokenRemote) Redeem(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
return nil, errors.New("BROKE!")
}
func (br *brokenRemote) Nonce(ctx context.Context, in *corepb.Empty, opts ...grpc.CallOption) (*noncepb.NonceMessage, error) {
return nil, nil
}
func TestRemoteRedeem(t *testing.T) {
l := blog.NewMock()
innerNs, err := nonce.NewNonceService(metrics.NewNoopScope(), 1)
test.AssertNotError(t, err, "NewNonceService failed")
ns := nonceServer{log: l, inner: innerNs}
// Working remote returning valid nonce message
ns.remoteServices = []noncepb.NonceServiceClient{
&workingRemote{resp: false},
&workingRemote{resp: true},
}
nonce := "asd"
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Hour))
resp, err := ns.Redeem(ctx, &noncepb.NonceMessage{Nonce: nonce, Forwarded: false})
cancel()
test.AssertNotError(t, err, "Redeem failed")
test.Assert(t, resp.Valid, "Redeem returned the wrong response")
// Working remotes returning invalid nonce message
ns.remoteServices = []noncepb.NonceServiceClient{
&workingRemote{resp: false},
&workingRemote{resp: false},
}
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Hour))
resp, err = ns.Redeem(ctx, &noncepb.NonceMessage{Nonce: nonce, Forwarded: false})
cancel()
test.AssertNotError(t, err, "Redeem failed")
test.Assert(t, !resp.Valid, "Redeem returned the wrong response")
// Sleeping remotes returns valid nonce message, but after 50ms, Redeem should return false
ns.remoteServices = []noncepb.NonceServiceClient{
&sleepingRemote{},
&sleepingRemote{},
}
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond))
resp, err = ns.Redeem(ctx, &noncepb.NonceMessage{Nonce: nonce, Forwarded: false})
cancel()
test.AssertNotError(t, err, "Redeem failed")
test.Assert(t, !resp.Valid, "Redeem returned the wrong response")
// Already forwarded message, Redeem should return false
ns.remoteServices = []noncepb.NonceServiceClient{
&workingRemote{resp: true},
&workingRemote{resp: true},
}
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Hour))
resp, err = ns.Redeem(ctx, &noncepb.NonceMessage{Nonce: nonce, Forwarded: true})
cancel()
test.AssertNotError(t, err, "Redeem failed")
test.Assert(t, !resp.Valid, "Redeem returned the wrong response")
// Broken remotes, Redeem should return false
ns.remoteServices = []noncepb.NonceServiceClient{
&brokenRemote{},
&brokenRemote{},
}
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond))
resp, err = ns.Redeem(ctx, &noncepb.NonceMessage{Nonce: nonce, Forwarded: false})
cancel()
test.AssertNotError(t, err, "Redeem failed")
test.Assert(t, !resp.Valid, "Redeem returned the wrong response")
}

View File

@ -23,6 +23,7 @@ services:
- publisher1.boulder
- ocsp-updater.boulder
- admin-revoker.boulder
- nonce1.boulder
rednet:
ipv4_address: 10.88.88.88
aliases:
@ -31,6 +32,7 @@ services:
- ra2.boulder
- va2.boulder
- publisher2.boulder
- nonce2.boulder
# Use sd-test-srv as a backup to Docker's embedded DNS server
# (https://docs.docker.com/config/containers/container-networking/#dns-services).
# If there's a name Docker's DNS server doesn't know about, it will

View File

@ -15,16 +15,19 @@ package nonce
import (
"container/heap"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"math/big"
"sync"
"time"
"github.com/letsencrypt/boulder/metrics"
noncepb "github.com/letsencrypt/boulder/nonce/proto"
)
const (
@ -43,6 +46,7 @@ type NonceService struct {
usedHeap *int64Heap
gcm cipher.AEAD
maxUsed int
prefix string
stats metrics.Scope
}
@ -65,8 +69,24 @@ func (h *int64Heap) Pop() interface{} {
}
// NewNonceService constructs a NonceService with defaults
func NewNonceService(scope metrics.Scope, maxUsed int) (*NonceService, error) {
func NewNonceService(scope metrics.Scope, maxUsed int, prefix string) (*NonceService, error) {
scope = scope.NewScope("NonceService")
// If a prefix is provided it must be four characters and valid
// base64. The prefix is required to be base64url as RFC8555
// section 6.5.1 requires that nonces use that encoding.
// As base64 operates on three byte binary segments we require
// the prefix to be three bytes (four characters) so that the
// bytes preceding the prefix wouldn't impact the encoding.
if prefix != "" {
if len(prefix) != 4 {
return nil, errors.New("nonce prefix must be 4 characters")
}
if _, err := base64.RawURLEncoding.DecodeString(prefix); err != nil {
return nil, errors.New("nonce prefix must be valid base64url")
}
}
key := make([]byte, 16)
if _, err := rand.Read(key); err != nil {
return nil, err
@ -92,6 +112,7 @@ func NewNonceService(scope metrics.Scope, maxUsed int) (*NonceService, error) {
usedHeap: &int64Heap{},
gcm: gcm,
maxUsed: maxUsed,
prefix: prefix,
stats: scope,
}, nil
}
@ -117,11 +138,24 @@ func (ns *NonceService) encrypt(counter int64) (string, error) {
ct := ns.gcm.Seal(nil, nonce, pt, nil)
copy(ret, nonce[4:])
copy(ret[8:], ct)
return base64.RawURLEncoding.EncodeToString(ret), nil
return ns.prefix + base64.RawURLEncoding.EncodeToString(ret), nil
}
func (ns *NonceService) decrypt(nonce string) (int64, error) {
decoded, err := base64.RawURLEncoding.DecodeString(nonce)
body := nonce
if ns.prefix != "" {
var prefix string
var err error
prefix, body, err = splitNonce(nonce)
if err != nil {
return 0, err
}
if ns.prefix != prefix {
return 0, fmt.Errorf("nonce contains invalid prefix: expected %q, got %q", ns.prefix, prefix)
}
}
decoded, err := base64.RawURLEncoding.DecodeString(body)
if err != nil {
return 0, err
}
@ -193,3 +227,28 @@ func (ns *NonceService) Valid(nonce string) bool {
ns.stats.Inc("Valid", 1)
return true
}
func splitNonce(nonce string) (string, string, error) {
if len(nonce) < 4 {
return "", "", errInvalidNonceLength
}
return nonce[:4], nonce[4:], nil
}
// RemoteRedeem checks the nonce prefix and routes the Redeem RPC
// to the associated remote nonce service
func RemoteRedeem(ctx context.Context, noncePrefixMap map[string]noncepb.NonceServiceClient, nonce string) (bool, error) {
prefix, _, err := splitNonce(nonce)
if err != nil {
return false, nil
}
nonceService, present := noncePrefixMap[prefix]
if !present {
return false, nil
}
resp, err := nonceService.Redeem(ctx, &noncepb.NonceMessage{Nonce: nonce})
if err != nil {
return false, err
}
return resp.Valid, nil
}

View File

@ -1,15 +1,20 @@
package nonce
import (
"context"
"errors"
"fmt"
"testing"
corepb "github.com/letsencrypt/boulder/core/proto"
"github.com/letsencrypt/boulder/metrics"
noncepb "github.com/letsencrypt/boulder/nonce/proto"
"github.com/letsencrypt/boulder/test"
"google.golang.org/grpc"
)
func TestValidNonce(t *testing.T) {
ns, err := NewNonceService(metrics.NewNoopScope(), 0)
ns, err := NewNonceService(metrics.NewNoopScope(), 0, "")
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
@ -17,7 +22,7 @@ func TestValidNonce(t *testing.T) {
}
func TestAlreadyUsed(t *testing.T) {
ns, err := NewNonceService(metrics.NewNoopScope(), 0)
ns, err := NewNonceService(metrics.NewNoopScope(), 0, "")
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
@ -26,7 +31,7 @@ func TestAlreadyUsed(t *testing.T) {
}
func TestRejectMalformed(t *testing.T) {
ns, err := NewNonceService(metrics.NewNoopScope(), 0)
ns, err := NewNonceService(metrics.NewNoopScope(), 0, "")
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
@ -34,15 +39,15 @@ func TestRejectMalformed(t *testing.T) {
}
func TestRejectShort(t *testing.T) {
ns, err := NewNonceService(metrics.NewNoopScope(), 0)
ns, err := NewNonceService(metrics.NewNoopScope(), 0, "")
test.AssertNotError(t, err, "Could not create nonce service")
test.Assert(t, !ns.Valid("aGkK"), "Accepted an invalid nonce")
}
func TestRejectUnknown(t *testing.T) {
ns1, err := NewNonceService(metrics.NewNoopScope(), 0)
ns1, err := NewNonceService(metrics.NewNoopScope(), 0, "")
test.AssertNotError(t, err, "Could not create nonce service")
ns2, err := NewNonceService(metrics.NewNoopScope(), 0)
ns2, err := NewNonceService(metrics.NewNoopScope(), 0, "")
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns1.Nonce()
@ -51,7 +56,7 @@ func TestRejectUnknown(t *testing.T) {
}
func TestRejectTooLate(t *testing.T) {
ns, err := NewNonceService(metrics.NewNoopScope(), 0)
ns, err := NewNonceService(metrics.NewNoopScope(), 0, "")
test.AssertNotError(t, err, "Could not create nonce service")
ns.latest = 2
@ -62,7 +67,7 @@ func TestRejectTooLate(t *testing.T) {
}
func TestRejectTooEarly(t *testing.T) {
ns, err := NewNonceService(metrics.NewNoopScope(), 0)
ns, err := NewNonceService(metrics.NewNoopScope(), 0, "")
test.AssertNotError(t, err, "Could not create nonce service")
n0, err := ns.Nonce()
@ -90,7 +95,7 @@ func TestRejectTooEarly(t *testing.T) {
}
func BenchmarkNonces(b *testing.B) {
ns, err := NewNonceService(metrics.NewNoopScope(), 0)
ns, err := NewNonceService(metrics.NewNoopScope(), 0, "")
if err != nil {
b.Fatal("creating nonce service", err)
}
@ -118,3 +123,90 @@ func BenchmarkNonces(b *testing.B) {
}
})
}
func TestNoncePrefixing(t *testing.T) {
ns, err := NewNonceService(metrics.NewNoopScope(), 0, "zinc")
test.AssertNotError(t, err, "Could not create nonce service")
n, err := ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
test.Assert(t, ns.Valid(n), "Valid nonce rejected")
n, err = ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
n = n[1:]
test.Assert(t, !ns.Valid(n), "Valid nonce with incorrect prefix accepted")
n, err = ns.Nonce()
test.AssertNotError(t, err, "Could not create nonce")
test.Assert(t, !ns.Valid(n[6:]), "Valid nonce without prefix accepted")
}
type malleableNonceClient struct {
redeem func(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error)
}
func (mnc *malleableNonceClient) Redeem(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
return mnc.redeem(ctx, in, opts...)
}
func (mnc *malleableNonceClient) Nonce(ctx context.Context, in *corepb.Empty, opts ...grpc.CallOption) (*noncepb.NonceMessage, error) {
return nil, errors.New("unimplemented")
}
func TestRemoteRedeem(t *testing.T) {
valid, err := RemoteRedeem(context.Background(), nil, "q")
test.AssertNotError(t, err, "RemoteRedeem failed")
test.Assert(t, !valid, "RemoteRedeem accepted an invalid nonce")
valid, err = RemoteRedeem(context.Background(), nil, "")
test.AssertNotError(t, err, "RemoteRedeem failed")
test.Assert(t, !valid, "RemoteRedeem accepted an empty nonce")
prefixMap := map[string]noncepb.NonceServiceClient{
"abcd": &malleableNonceClient{
redeem: func(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
return nil, errors.New("wrong one!")
},
},
"wxyz": &malleableNonceClient{
redeem: func(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
return &noncepb.ValidMessage{Valid: false}, nil
},
},
}
// Attempt to redeem a nonce with a prefix not in the prefix map, expect return false, nil
valid, err = RemoteRedeem(context.Background(), prefixMap, "asddCQEC")
test.AssertNotError(t, err, "RemoteRedeem failed")
test.Assert(t, !valid, "RemoteRedeem accepted nonce not in prefix map")
// Attempt to redeem a nonce with a prefix in the prefix map, remote returns error
// expect false, err
_, err = RemoteRedeem(context.Background(), prefixMap, "abcdbeef")
test.AssertError(t, err, "RemoteRedeem didn't return error when remote did")
// Attempt to redeem a nonce with a prefix in the prefix map, remote returns valid
// expect true, nil
valid, err = RemoteRedeem(context.Background(), prefixMap, "wxyzdead")
test.AssertNotError(t, err, "RemoteRedeem failed")
test.Assert(t, !valid, "RemoteRedeem didn't honor remote result")
// Attempt to redeem a nonce with a prefix in the prefix map, remote returns invalid
// expect false, nil
prefixMap["wxyz"] = &malleableNonceClient{
redeem: func(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) {
return &noncepb.ValidMessage{Valid: true}, nil
},
}
valid, err = RemoteRedeem(context.Background(), prefixMap, "wxyzdead")
test.AssertNotError(t, err, "RemoteRedeem failed")
test.Assert(t, valid, "RemoteRedeem didn't honor remote result")
}
func TestNoncePrefixValidation(t *testing.T) {
_, err := NewNonceService(metrics.NewNoopScope(), 0, "hey")
test.AssertError(t, err, "NewNonceService didn't fail with short prefix")
_, err = NewNonceService(metrics.NewNoopScope(), 0, "hey!")
test.AssertError(t, err, "NewNonceService didn't fail with invalid base64")
_, err = NewNonceService(metrics.NewNoopScope(), 0, "heyy")
test.AssertNotError(t, err, "NewNonceService failed with valid nonce prefix")
}

View File

@ -27,7 +27,6 @@ const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type NonceMessage struct {
Nonce string `protobuf:"bytes,1,opt,name=nonce,proto3" json:"nonce,omitempty"`
Forwarded bool `protobuf:"varint,2,opt,name=forwarded,proto3" json:"forwarded,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
@ -65,13 +64,6 @@ func (m *NonceMessage) GetNonce() string {
return ""
}
func (m *NonceMessage) GetForwarded() bool {
if m != nil {
return m.Forwarded
}
return false
}
type ValidMessage struct {
Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
@ -119,20 +111,18 @@ func init() {
func init() { proto.RegisterFile("nonce/proto/nonce.proto", fileDescriptor_9197b76ef104b424) }
var fileDescriptor_9197b76ef104b424 = []byte{
// 193 bytes of a gzipped FileDescriptorProto
// 169 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0xcb, 0xcf, 0x4b,
0x4e, 0xd5, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x07, 0xb3, 0xf5, 0xc0, 0x6c, 0x21, 0x56, 0x30,
0x47, 0x4a, 0x34, 0x39, 0xbf, 0x08, 0x26, 0x0d, 0x62, 0x42, 0x64, 0x95, 0x9c, 0xb8, 0x78, 0xfc,
0x47, 0x4a, 0x34, 0x39, 0xbf, 0x08, 0x26, 0x0d, 0x62, 0x42, 0x64, 0x95, 0x54, 0xb8, 0x78, 0xfc,
0x40, 0xf2, 0xbe, 0xa9, 0xc5, 0xc5, 0x89, 0xe9, 0xa9, 0x42, 0x22, 0x5c, 0x10, 0xf5, 0x12, 0x8c,
0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x10, 0x8e, 0x90, 0x0c, 0x17, 0x67, 0x5a, 0x7e, 0x51, 0x79, 0x62,
0x51, 0x4a, 0x6a, 0x8a, 0x04, 0x93, 0x02, 0xa3, 0x06, 0x47, 0x10, 0x42, 0x40, 0x49, 0x85, 0x8b,
0x27, 0x2c, 0x31, 0x27, 0x33, 0x05, 0xc9, 0x8c, 0x32, 0x10, 0x1f, 0x6c, 0x06, 0x47, 0x10, 0x84,
0x63, 0x54, 0x08, 0xb5, 0x29, 0x38, 0xb5, 0xa8, 0x2c, 0x33, 0x39, 0x55, 0x48, 0x9b, 0x8b, 0x15,
0xcc, 0x17, 0xe2, 0xd6, 0x03, 0xbb, 0xc7, 0x35, 0xb7, 0xa0, 0xa4, 0x52, 0x4a, 0x58, 0x0f, 0xe2,
0x76, 0x64, 0x47, 0x29, 0x31, 0x08, 0x99, 0x70, 0xb1, 0x05, 0xa5, 0xa6, 0xa4, 0xa6, 0xe6, 0x0a,
0x61, 0x53, 0x00, 0xd7, 0x85, 0xec, 0x0c, 0x25, 0x06, 0x27, 0xf6, 0x28, 0x56, 0xb0, 0x2f, 0x93,
0xd8, 0xc0, 0x94, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xd2, 0x89, 0x71, 0x61, 0x25, 0x01, 0x00,
0x00,
0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x10, 0x0e, 0x48, 0x55, 0x58, 0x62, 0x4e, 0x66, 0x0a, 0x92, 0xaa,
0x32, 0x10, 0x1f, 0xac, 0x8a, 0x23, 0x08, 0xc2, 0x31, 0x2a, 0x84, 0x9a, 0x15, 0x9c, 0x5a, 0x54,
0x96, 0x99, 0x9c, 0x2a, 0xa4, 0xcd, 0xc5, 0x0a, 0xe6, 0x0b, 0x71, 0xeb, 0x81, 0x6d, 0x74, 0xcd,
0x2d, 0x28, 0xa9, 0x94, 0x12, 0xd6, 0x83, 0xb8, 0x0e, 0xd9, 0x5a, 0x25, 0x06, 0x21, 0x13, 0x2e,
0xb6, 0xa0, 0xd4, 0x94, 0xd4, 0xd4, 0x5c, 0x21, 0x6c, 0x0a, 0xe0, 0xba, 0x90, 0x9d, 0xa1, 0xc4,
0xe0, 0xc4, 0x1e, 0xc5, 0x0a, 0xf6, 0x47, 0x12, 0x1b, 0x98, 0x32, 0x06, 0x04, 0x00, 0x00, 0xff,
0xff, 0x9a, 0xba, 0xd9, 0x37, 0x07, 0x01, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.

View File

@ -12,7 +12,6 @@ service NonceService {
message NonceMessage {
string nonce = 1;
bool forwarded = 2;
}
message ValidMessage {

View File

@ -1,6 +1,7 @@
{
"NonceService": {
"maxUsed": 131072,
"noncePrefix": "taro",
"syslog": {
"stdoutLevel": 6
},
@ -8,24 +9,13 @@
"grpc": {
"address": ":9101",
"clientNames": [
"wfe.boulder",
"nonce.boulder"
"wfe.boulder"
]
},
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/nonce.boulder/cert.pem",
"keyFile": "test/grpc-creds/nonce.boulder/key.pem"
},
"remoteNonceServices": [
{
"serverAddress": "nonce.boulder:9102",
"timeout": "15s"
},
{
"serverAddress": "nonce.boulder:9101",
"timeout": "15s"
}
]
}
}
}

View File

@ -30,10 +30,20 @@
"serverAddress": "sa.boulder:9095",
"timeout": "15s"
},
"nonceService": {
"getNonceService": {
"serverAddress": "nonce.boulder:9101",
"timeout": "15s"
},
"redeemNonceServices": {
"taro": {
"serverAddress": "nonce1.boulder:9101",
"timeout": "15s"
},
"zinc": {
"serverAddress": "nonce2.boulder:9101",
"timeout": "15s"
}
},
"features": {
"NewAuthorizationSchema": true
}

View File

@ -31,10 +31,20 @@
"serverAddress": "sa.boulder:9095",
"timeout": "15s"
},
"nonceService": {
"getNonceService": {
"serverAddress": "nonce.boulder:9101",
"timeout": "15s"
},
"redeemNonceServices": {
"taro": {
"serverAddress": "nonce1.boulder:9101",
"timeout": "15s"
},
"zinc": {
"serverAddress": "nonce2.boulder:9101",
"timeout": "15s"
}
},
"certificateChains": {
"http://boulder:4430/acme/issuer-cert": [ "test/test-ca2.pem" ],
"http://127.0.0.1:4000/acme/issuer-cert": [ "test/test-ca2.pem" ]

View File

@ -1,6 +1,7 @@
{
"NonceService": {
"maxUsed": 131072,
"noncePrefix": "taro",
"syslog": {
"stdoutLevel": 6
},

View File

@ -87,8 +87,8 @@ def start(race_detection, fakeclock=None, config_dir=default_config_dir):
[8006, './bin/ocsp-updater --config %s' % os.path.join(config_dir, "ocsp-updater.json")],
[8002, './bin/boulder-ra --config %s --addr ra1.boulder:9094 --debug-addr :8002' % os.path.join(config_dir, "ra.json")],
[8102, './bin/boulder-ra --config %s --addr ra2.boulder:9094 --debug-addr :8102' % os.path.join(config_dir, "ra.json")],
[8111, './bin/nonce-service --config %s' % os.path.join(config_dir, "nonce.json")],
[8112, './bin/nonce-service --config %s --addr nonce.boulder:9102 --debug-addr :8112' % os.path.join(config_dir, "nonce.json")],
[8111, './bin/nonce-service --config %s --addr nonce1.boulder:9101 --debug-addr :8111 --prefix taro' % os.path.join(config_dir, "nonce.json")],
[8112, './bin/nonce-service --config %s --addr nonce2.boulder:9101 --debug-addr :8112 --prefix zinc' % os.path.join(config_dir, "nonce.json")],
[4431, './bin/boulder-wfe2 --config %s' % os.path.join(config_dir, "wfe2.json")],
[4000, './bin/boulder-wfe --config %s' % os.path.join(config_dir, "wfe.json")],
])

View File

@ -94,6 +94,7 @@ type WebFrontEndImpl struct {
// Register of anti-replay nonces
nonceService *nonce.NonceService
remoteNonceService noncepb.NonceServiceClient
noncePrefixMap map[string]noncepb.NonceServiceClient
// Key policy.
keyPolicy goodkey.KeyPolicy
@ -116,6 +117,7 @@ func NewWebFrontEndImpl(
clk clock.Clock,
keyPolicy goodkey.KeyPolicy,
remoteNonceService noncepb.NonceServiceClient,
noncePrefixMap map[string]noncepb.NonceServiceClient,
logger blog.Logger,
) (WebFrontEndImpl, error) {
csrSignatureAlgs := prometheus.NewCounterVec(
@ -134,10 +136,11 @@ func NewWebFrontEndImpl(
keyPolicy: keyPolicy,
csrSignatureAlgs: csrSignatureAlgs,
remoteNonceService: remoteNonceService,
noncePrefixMap: noncePrefixMap,
}
if wfe.remoteNonceService == nil {
nonceService, err := nonce.NewNonceService(stats, 0)
nonceService, err := nonce.NewNonceService(stats, 0, "")
if err != nil {
return WebFrontEndImpl{}, err
}
@ -561,24 +564,24 @@ func (wfe *WebFrontEndImpl) verifyPOST(ctx context.Context, logEvent *web.Reques
logEvent.Payload = string(payload)
// Check that the request has a known anti-replay nonce
nonce := parsedJws.Signatures[0].Header.Nonce
if len(nonce) == 0 {
nonceStr := parsedJws.Signatures[0].Header.Nonce
if len(nonceStr) == 0 {
wfe.stats.Inc("Errors.JWSMissingNonce", 1)
return nil, nil, reg, probs.BadNonce("JWS has no anti-replay nonce")
}
var nonceValid bool
if wfe.remoteNonceService != nil {
validMsg, err := wfe.remoteNonceService.Redeem(ctx, &noncepb.NonceMessage{Nonce: nonce})
valid, err := nonce.RemoteRedeem(ctx, wfe.noncePrefixMap, nonceStr)
if err != nil {
return nil, nil, reg, probs.ServerInternal("failed to verify nonce validity: %s", err)
}
nonceValid = validMsg.Valid
nonceValid = valid
} else {
nonceValid = wfe.nonceService.Valid(nonce)
nonceValid = wfe.nonceService.Valid(nonceStr)
}
if !nonceValid {
wfe.stats.Inc("Errors.JWSInvalidNonce", 1)
return nil, nil, reg, probs.BadNonce("JWS has invalid anti-replay nonce %s", nonce)
return nil, nil, reg, probs.BadNonce("JWS has invalid anti-replay nonce %s", nonceStr)
}
// Check that the "resource" field is present and has the correct value

View File

@ -380,7 +380,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock) {
fc := clock.NewFake()
stats := metrics.NewNoopScope()
wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy, nil, blog.NewMock())
wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy, nil, nil, blog.NewMock())
test.AssertNotError(t, err, "Unable to create WFE")
wfe.SubscriberAgreementURL = agreementURL

View File

@ -19,7 +19,7 @@ import (
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
noncepb "github.com/letsencrypt/boulder/nonce/proto"
"github.com/letsencrypt/boulder/nonce"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/web"
)
@ -182,24 +182,23 @@ func (wfe *WebFrontEndImpl) validNonce(ctx context.Context, jws *jose.JSONWebSig
// validNonce is called after validPOSTRequest() and parseJWS() which
// defend against the incorrect number of signatures.
header := jws.Signatures[0].Header
nonce := header.Nonce
if len(nonce) == 0 {
if len(header.Nonce) == 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMissingNonce"}).Inc()
return probs.BadNonce("JWS has no anti-replay nonce")
}
var nonceValid bool
if wfe.remoteNonceService != nil {
validMsg, err := wfe.remoteNonceService.Redeem(ctx, &noncepb.NonceMessage{Nonce: nonce})
valid, err := nonce.RemoteRedeem(ctx, wfe.noncePrefixMap, header.Nonce)
if err != nil {
return probs.ServerInternal("failed to verify nonce validity: %s", err)
}
nonceValid = validMsg.Valid
nonceValid = valid
} else {
nonceValid = wfe.nonceService.Valid(nonce)
nonceValid = wfe.nonceService.Valid(header.Nonce)
}
if !nonceValid {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSInvalidNonce"}).Inc()
return probs.BadNonce("JWS has an invalid anti-replay nonce: %q", nonce)
return probs.BadNonce("JWS has an invalid anti-replay nonce: %q", header.Nonce)
}
return nil
}

View File

@ -101,6 +101,7 @@ type WebFrontEndImpl struct {
// Register of anti-replay nonces
nonceService *nonce.NonceService
remoteNonceService noncepb.NonceServiceClient
noncePrefixMap map[string]noncepb.NonceServiceClient
// Key policy.
keyPolicy goodkey.KeyPolicy
@ -122,6 +123,7 @@ func NewWebFrontEndImpl(
keyPolicy goodkey.KeyPolicy,
certificateChains map[string][]byte,
remoteNonceService noncepb.NonceServiceClient,
noncePrefixMap map[string]noncepb.NonceServiceClient,
logger blog.Logger,
) (WebFrontEndImpl, error) {
wfe := WebFrontEndImpl{
@ -132,10 +134,11 @@ func NewWebFrontEndImpl(
stats: initStats(scope),
scope: scope,
remoteNonceService: remoteNonceService,
noncePrefixMap: noncePrefixMap,
}
if wfe.remoteNonceService == nil {
nonceService, err := nonce.NewNonceService(scope, 0)
nonceService, err := nonce.NewNonceService(scope, 0, "")
if err != nil {
return WebFrontEndImpl{}, err
}

View File

@ -357,7 +357,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock) {
"http://localhost:4000/acme/issuer-cert": append([]byte{'\n'}, chainPEM...),
}
wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy, certChains, nil, blog.NewMock())
wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy, certChains, nil, nil, blog.NewMock())
test.AssertNotError(t, err, "Unable to create WFE")
wfe.SubscriberAgreementURL = agreementURL