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 }