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