github.com/letsencrypt/boulder@v0.20251208.0/nonce/nonce.go (about)

     1  // Package nonce implements a service for generating and redeeming nonces.
     2  // To generate a nonce, it encrypts a monotonically increasing counter (latest)
     3  // using an authenticated cipher. To redeem a nonce, it checks that the nonce
     4  // decrypts to a valid integer between the earliest and latest counter values,
     5  // and that it's not on the cross-off list. To avoid a constantly growing cross-off
     6  // list, the nonce service periodically retires the oldest counter values by
     7  // finding the lowest counter value in the cross-off list, deleting it, and setting
     8  // "earliest" to its value. To make this efficient, the cross-off list is represented
     9  // two ways: Once as a map, for quick lookup of a given value, and once as a heap,
    10  // to quickly find the lowest value.
    11  // The MaxUsed value determines how long a generated nonce can be used before it
    12  // is forgotten. To calculate that period, divide the MaxUsed value by average
    13  // redemption rate (valid POSTs per second).
    14  package nonce
    15  
    16  import (
    17  	"container/heap"
    18  	"context"
    19  	"crypto/aes"
    20  	"crypto/cipher"
    21  	"crypto/hmac"
    22  	"crypto/rand"
    23  	"crypto/sha256"
    24  	"encoding/base64"
    25  	"errors"
    26  	"fmt"
    27  	"math/big"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/prometheus/client_golang/prometheus"
    32  	"github.com/prometheus/client_golang/prometheus/promauto"
    33  	"google.golang.org/grpc"
    34  	"google.golang.org/protobuf/types/known/emptypb"
    35  
    36  	berrors "github.com/letsencrypt/boulder/errors"
    37  	noncepb "github.com/letsencrypt/boulder/nonce/proto"
    38  )
    39  
    40  const (
    41  	// PrefixLen is the character length of a nonce prefix.
    42  	PrefixLen = 8
    43  
    44  	// NonceLen is the character length of a nonce, excluding the prefix.
    45  	NonceLen       = 32
    46  	defaultMaxUsed = 65536
    47  )
    48  
    49  var errInvalidNonceLength = errors.New("invalid nonce length")
    50  
    51  // PrefixCtxKey is exported for use as a key in a context.Context.
    52  type PrefixCtxKey struct{}
    53  
    54  // HMACKeyCtxKey is exported for use as a key in a context.Context.
    55  type HMACKeyCtxKey struct{}
    56  
    57  // DerivePrefix derives a nonce prefix from the provided listening address and
    58  // key. The prefix is derived by take the first 8 characters of the base64url
    59  // encoded HMAC-SHA256 hash of the listening address using the provided key.
    60  func DerivePrefix(grpcAddr string, key []byte) string {
    61  	h := hmac.New(sha256.New, key)
    62  	h.Write([]byte(grpcAddr))
    63  	return base64.RawURLEncoding.EncodeToString(h.Sum(nil))[:PrefixLen]
    64  }
    65  
    66  // NonceService generates, cancels, and tracks Nonces.
    67  type NonceService struct {
    68  	noncepb.UnsafeNonceServiceServer
    69  	mu               sync.Mutex
    70  	latest           int64
    71  	earliest         int64
    72  	used             map[int64]bool
    73  	usedHeap         *int64Heap
    74  	gcm              cipher.AEAD
    75  	maxUsed          int
    76  	prefix           string
    77  	nonceCreates     prometheus.Counter
    78  	nonceEarliest    prometheus.Gauge
    79  	nonceLatest      prometheus.Gauge
    80  	nonceRedeems     *prometheus.CounterVec
    81  	nonceAges        *prometheus.HistogramVec
    82  	nonceHeapLatency prometheus.Histogram
    83  }
    84  
    85  type int64Heap []int64
    86  
    87  func (h int64Heap) Len() int           { return len(h) }
    88  func (h int64Heap) Less(i, j int) bool { return h[i] < h[j] }
    89  func (h int64Heap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
    90  
    91  func (h *int64Heap) Push(x any) {
    92  	*h = append(*h, x.(int64))
    93  }
    94  
    95  func (h *int64Heap) Pop() any {
    96  	old := *h
    97  	n := len(old)
    98  	x := old[n-1]
    99  	*h = old[0 : n-1]
   100  	return x
   101  }
   102  
   103  // NewNonceService constructs a NonceService with defaults
   104  func NewNonceService(stats prometheus.Registerer, maxUsed int, prefix string) (*NonceService, error) {
   105  	// If a prefix is provided it must be eight characters and valid base64. The
   106  	// prefix is required to be base64url as RFC8555 section 6.5.1 requires that
   107  	// nonces use that encoding. As base64 operates on three byte binary segments
   108  	// we require the prefix to be six bytes (eight characters) so that the bytes
   109  	// preceding the prefix wouldn't impact the encoding.
   110  	if prefix != "" {
   111  		if len(prefix) != PrefixLen {
   112  			return nil, fmt.Errorf(
   113  				"nonce prefix must be %d characters, not %d",
   114  				PrefixLen,
   115  				len(prefix),
   116  			)
   117  		}
   118  		if _, err := base64.RawURLEncoding.DecodeString(prefix); err != nil {
   119  			return nil, errors.New("nonce prefix must be valid base64url")
   120  		}
   121  	}
   122  
   123  	key := make([]byte, 16)
   124  	if _, err := rand.Read(key); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	c, err := aes.NewCipher(key)
   129  	if err != nil {
   130  		panic("Failure in NewCipher: " + err.Error())
   131  	}
   132  	gcm, err := cipher.NewGCM(c)
   133  	if err != nil {
   134  		panic("Failure in NewGCM: " + err.Error())
   135  	}
   136  
   137  	if maxUsed <= 0 {
   138  		maxUsed = defaultMaxUsed
   139  	}
   140  
   141  	nonceCreates := promauto.With(stats).NewCounter(prometheus.CounterOpts{
   142  		Name: "nonce_creates",
   143  		Help: "A counter of nonces generated",
   144  	})
   145  	nonceEarliest := promauto.With(stats).NewGauge(prometheus.GaugeOpts{
   146  		Name: "nonce_earliest",
   147  		Help: "A gauge with the current earliest valid nonce value",
   148  	})
   149  	nonceLatest := promauto.With(stats).NewGauge(prometheus.GaugeOpts{
   150  		Name: "nonce_latest",
   151  		Help: "A gauge with the current latest valid nonce value",
   152  	})
   153  	nonceRedeems := promauto.With(stats).NewCounterVec(prometheus.CounterOpts{
   154  		Name: "nonce_redeems",
   155  		Help: "A counter of nonce validations labelled by result",
   156  	}, []string{"result", "error"})
   157  	nonceAges := promauto.With(stats).NewHistogramVec(prometheus.HistogramOpts{
   158  		Name:    "nonce_ages",
   159  		Help:    "A histogram of nonce ages at the time they were (attempted to be) redeemed, expressed as fractions of the valid nonce window",
   160  		Buckets: []float64{-0.01, 0, .1, .2, .3, .4, .5, .6, .7, .8, .9, 1, 1.1, 1.2, 1.5, 2, 5},
   161  	}, []string{"result"})
   162  	nonceHeapLatency := promauto.With(stats).NewHistogram(prometheus.HistogramOpts{
   163  		Name: "nonce_heap_latency",
   164  		Help: "A histogram of latencies of heap pop operations",
   165  	})
   166  
   167  	return &NonceService{
   168  		earliest:         0,
   169  		latest:           0,
   170  		used:             make(map[int64]bool, maxUsed),
   171  		usedHeap:         &int64Heap{},
   172  		gcm:              gcm,
   173  		maxUsed:          maxUsed,
   174  		prefix:           prefix,
   175  		nonceCreates:     nonceCreates,
   176  		nonceEarliest:    nonceEarliest,
   177  		nonceLatest:      nonceLatest,
   178  		nonceRedeems:     nonceRedeems,
   179  		nonceAges:        nonceAges,
   180  		nonceHeapLatency: nonceHeapLatency,
   181  	}, nil
   182  }
   183  
   184  func (ns *NonceService) encrypt(counter int64) (string, error) {
   185  	// Generate a nonce with upper 4 bytes zero
   186  	nonce := make([]byte, 12)
   187  	for i := range 4 {
   188  		nonce[i] = 0
   189  	}
   190  	_, err := rand.Read(nonce[4:])
   191  	if err != nil {
   192  		return "", err
   193  	}
   194  
   195  	// Encode counter to plaintext
   196  	pt := make([]byte, 8)
   197  	ctr := big.NewInt(counter)
   198  	pad := 8 - len(ctr.Bytes())
   199  	copy(pt[pad:], ctr.Bytes())
   200  
   201  	// Encrypt
   202  	ret := make([]byte, NonceLen)
   203  	ct := ns.gcm.Seal(nil, nonce, pt, nil)
   204  	copy(ret, nonce[4:])
   205  	copy(ret[8:], ct)
   206  
   207  	return ns.prefix + base64.RawURLEncoding.EncodeToString(ret), nil
   208  }
   209  
   210  func (ns *NonceService) decrypt(nonce string) (int64, error) {
   211  	body := nonce
   212  	if ns.prefix != "" {
   213  		var prefix string
   214  		var err error
   215  		prefix, body, err = ns.splitNonce(nonce)
   216  		if err != nil {
   217  			return 0, err
   218  		}
   219  		if ns.prefix != prefix {
   220  			return 0, fmt.Errorf("nonce contains invalid prefix: expected %q, got %q", ns.prefix, prefix)
   221  		}
   222  	}
   223  	decoded, err := base64.RawURLEncoding.DecodeString(body)
   224  	if err != nil {
   225  		return 0, err
   226  	}
   227  	if len(decoded) != NonceLen {
   228  		return 0, errInvalidNonceLength
   229  	}
   230  
   231  	n := make([]byte, 12)
   232  	for i := range 4 {
   233  		n[i] = 0
   234  	}
   235  	copy(n[4:], decoded[:8])
   236  
   237  	pt, err := ns.gcm.Open(nil, n, decoded[8:], nil)
   238  	if err != nil {
   239  		return 0, err
   240  	}
   241  
   242  	ctr := big.NewInt(0)
   243  	ctr.SetBytes(pt)
   244  	return ctr.Int64(), nil
   245  }
   246  
   247  // nonce provides a new Nonce.
   248  func (ns *NonceService) nonce() (string, error) {
   249  	ns.mu.Lock()
   250  	ns.latest++
   251  	latest := ns.latest
   252  	ns.mu.Unlock()
   253  	ns.nonceCreates.Inc()
   254  	ns.nonceLatest.Set(float64(latest))
   255  	return ns.encrypt(latest)
   256  }
   257  
   258  // valid determines whether the provided Nonce string is valid, returning
   259  // true if so.
   260  func (ns *NonceService) valid(nonce string) error {
   261  	c, err := ns.decrypt(nonce)
   262  	if err != nil {
   263  		ns.nonceRedeems.WithLabelValues("invalid", "decrypt").Inc()
   264  		return berrors.BadNonceError("unable to decrypt nonce: %s", err)
   265  	}
   266  
   267  	ns.mu.Lock()
   268  	defer ns.mu.Unlock()
   269  
   270  	// age represents how "far back" in the valid nonce window this nonce is.
   271  	// If it is very recent, then the numerator is very small and the age is close
   272  	// to zero. If it is old but still valid, the numerator is slightly smaller
   273  	// than the denominator, and the age is close to one. If it is too old, then
   274  	// the age is greater than one. If it is magically too new (i.e. greater than
   275  	// the largest nonce we've actually handed out), then the age is negative.
   276  	age := float64(ns.latest-c) / float64(ns.latest-ns.earliest)
   277  
   278  	if c > ns.latest { // i.e. age < 0
   279  		ns.nonceRedeems.WithLabelValues("invalid", "too high").Inc()
   280  		ns.nonceAges.WithLabelValues("invalid").Observe(age)
   281  		return berrors.BadNonceError("nonce greater than highest dispensed nonce: %d > %d", c, ns.latest)
   282  	}
   283  
   284  	if c <= ns.earliest { // i.e. age >= 1
   285  		ns.nonceRedeems.WithLabelValues("invalid", "too low").Inc()
   286  		ns.nonceAges.WithLabelValues("invalid").Observe(age)
   287  		return berrors.BadNonceError("nonce less than lowest eligible nonce: %d < %d", c, ns.earliest)
   288  	}
   289  
   290  	if ns.used[c] {
   291  		ns.nonceRedeems.WithLabelValues("invalid", "already used").Inc()
   292  		ns.nonceAges.WithLabelValues("invalid").Observe(age)
   293  		return berrors.BadNonceError("nonce already marked as used: %d in [%d]used", c, len(ns.used))
   294  	}
   295  
   296  	ns.used[c] = true
   297  	heap.Push(ns.usedHeap, c)
   298  	if len(ns.used) > ns.maxUsed {
   299  		s := time.Now()
   300  		ns.earliest = heap.Pop(ns.usedHeap).(int64)
   301  		ns.nonceEarliest.Set(float64(ns.earliest))
   302  		ns.nonceHeapLatency.Observe(time.Since(s).Seconds())
   303  		delete(ns.used, ns.earliest)
   304  	}
   305  
   306  	ns.nonceRedeems.WithLabelValues("valid", "").Inc()
   307  	ns.nonceAges.WithLabelValues("valid").Observe(age)
   308  	return nil
   309  }
   310  
   311  // splitNonce splits a nonce into a prefix and a body.
   312  func (ns *NonceService) splitNonce(nonce string) (string, string, error) {
   313  	if len(nonce) < PrefixLen {
   314  		return "", "", errInvalidNonceLength
   315  	}
   316  	return nonce[:PrefixLen], nonce[PrefixLen:], nil
   317  }
   318  
   319  // Redeem accepts a nonce from a gRPC client and redeems it using the inner nonce service.
   320  func (ns *NonceService) Redeem(ctx context.Context, msg *noncepb.NonceMessage) (*noncepb.ValidMessage, error) {
   321  	err := ns.valid(msg.Nonce)
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  	return &noncepb.ValidMessage{Valid: true}, nil
   326  }
   327  
   328  // Nonce generates a nonce and sends it to a gRPC client.
   329  func (ns *NonceService) Nonce(_ context.Context, _ *emptypb.Empty) (*noncepb.NonceMessage, error) {
   330  	nonce, err := ns.nonce()
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	return &noncepb.NonceMessage{Nonce: nonce}, nil
   335  }
   336  
   337  // Getter is an interface for an RPC client that can get a nonce.
   338  type Getter interface {
   339  	Nonce(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*noncepb.NonceMessage, error)
   340  }
   341  
   342  // Redeemer is an interface for an RPC client that can redeem a nonce.
   343  type Redeemer interface {
   344  	Redeem(ctx context.Context, in *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error)
   345  }
   346  
   347  // NewGetter returns a new noncepb.NonceServiceClient which can only be used to
   348  // get nonces.
   349  func NewGetter(cc grpc.ClientConnInterface) Getter {
   350  	return noncepb.NewNonceServiceClient(cc)
   351  }
   352  
   353  // NewRedeemer returns a new noncepb.NonceServiceClient which can only be used
   354  // to redeem nonces.
   355  func NewRedeemer(cc grpc.ClientConnInterface) Redeemer {
   356  	return noncepb.NewNonceServiceClient(cc)
   357  }