Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 21b92ef

Browse files
authored
feat: add cache abstraction for fetching signing keys (#14777)
- Adds the database implementation for fetching and caching keys used for JWT signing. It's been merged into the `keyrotate` pkg and renamed to `cryptokeys` since they're coupled concepts.
1 parent f7ddbb7 commit 21b92ef

File tree

18 files changed

+1060
-178
lines changed

18 files changed

+1060
-178
lines changed

coderd/apidoc/docs.go

+36-34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+32-30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/cryptokeys/dbkeycache.go

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package cryptokeys
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
8+
"golang.org/x/xerrors"
9+
10+
"cdr.dev/slog"
11+
"github.com/coder/coder/v2/coderd/database"
12+
"github.com/coder/coder/v2/coderd/database/db2sdk"
13+
"github.com/coder/coder/v2/codersdk"
14+
"github.com/coder/quartz"
15+
)
16+
17+
// never represents the maximum value for a time.Duration.
18+
const never = 1<<63 - 1
19+
20+
// DBCache implements Keycache for callers with access to the database.
21+
type DBCache struct {
22+
db database.Store
23+
feature database.CryptoKeyFeature
24+
logger slog.Logger
25+
clock quartz.Clock
26+
27+
// The following are initialized by NewDBCache.
28+
keysMu sync.RWMutex
29+
keys map[int32]database.CryptoKey
30+
latestKey database.CryptoKey
31+
timer *quartz.Timer
32+
// invalidateAt is the time at which the keys cache should be invalidated.
33+
invalidateAt time.Time
34+
closed bool
35+
}
36+
37+
type DBCacheOption func(*DBCache)
38+
39+
func WithDBCacheClock(clock quartz.Clock) DBCacheOption {
40+
return func(d *DBCache) {
41+
d.clock = clock
42+
}
43+
}
44+
45+
// NewDBCache creates a new DBCache. Close should be called to
46+
// release resources associated with its internal timer.
47+
func NewDBCache(logger slog.Logger, db database.Store, feature database.CryptoKeyFeature, opts ...func(*DBCache)) *DBCache {
48+
d := &DBCache{
49+
db: db,
50+
feature: feature,
51+
clock: quartz.NewReal(),
52+
logger: logger,
53+
}
54+
55+
for _, opt := range opts {
56+
opt(d)
57+
}
58+
59+
d.timer = d.clock.AfterFunc(never, d.clear)
60+
61+
return d
62+
}
63+
64+
// Verifying returns the CryptoKey with the given sequence number, provided that
65+
// it is neither deleted nor has breached its deletion date. It should only be
66+
// used for verifying or decrypting payloads. To sign/encrypt call Signing.
67+
func (d *DBCache) Verifying(ctx context.Context, sequence int32) (codersdk.CryptoKey, error) {
68+
d.keysMu.RLock()
69+
if d.closed {
70+
d.keysMu.RUnlock()
71+
return codersdk.CryptoKey{}, ErrClosed
72+
}
73+
74+
now := d.clock.Now()
75+
key, ok := d.keys[sequence]
76+
d.keysMu.RUnlock()
77+
if ok {
78+
return checkKey(key, now)
79+
}
80+
81+
d.keysMu.Lock()
82+
defer d.keysMu.Unlock()
83+
84+
if d.closed {
85+
return codersdk.CryptoKey{}, ErrClosed
86+
}
87+
88+
key, ok = d.keys[sequence]
89+
if ok {
90+
return checkKey(key, now)
91+
}
92+
93+
err := d.fetch(ctx)
94+
if err != nil {
95+
return codersdk.CryptoKey{}, xerrors.Errorf("fetch: %w", err)
96+
}
97+
98+
key, ok = d.keys[sequence]
99+
if !ok {
100+
return codersdk.CryptoKey{}, ErrKeyNotFound
101+
}
102+
103+
return checkKey(key, now)
104+
}
105+
106+
// Signing returns the latest valid key for signing. A valid key is one that is
107+
// both past its start time and before its deletion time.
108+
func (d *DBCache) Signing(ctx context.Context) (codersdk.CryptoKey, error) {
109+
d.keysMu.RLock()
110+
111+
if d.closed {
112+
d.keysMu.RUnlock()
113+
return codersdk.CryptoKey{}, ErrClosed
114+
}
115+
116+
latest := d.latestKey
117+
d.keysMu.RUnlock()
118+
119+
now := d.clock.Now()
120+
if latest.CanSign(now) {
121+
return db2sdk.CryptoKey(latest), nil
122+
}
123+
124+
d.keysMu.Lock()
125+
defer d.keysMu.Unlock()
126+
127+
if d.closed {
128+
return codersdk.CryptoKey{}, ErrClosed
129+
}
130+
131+
if d.latestKey.CanSign(now) {
132+
return db2sdk.CryptoKey(d.latestKey), nil
133+
}
134+
135+
// Refetch all keys for this feature so we can find the latest valid key.
136+
err := d.fetch(ctx)
137+
if err != nil {
138+
return codersdk.CryptoKey{}, xerrors.Errorf("fetch: %w", err)
139+
}
140+
141+
return db2sdk.CryptoKey(d.latestKey), nil
142+
}
143+
144+
// clear invalidates the cache. This forces the subsequent call to fetch fresh keys.
145+
func (d *DBCache) clear() {
146+
now := d.clock.Now("DBCache", "clear")
147+
d.keysMu.Lock()
148+
defer d.keysMu.Unlock()
149+
// Check if we raced with a fetch. It's possible that the timer fired and we
150+
// lost the race to the mutex. We want to avoid invalidating
151+
// a cache that was just refetched.
152+
if now.Before(d.invalidateAt) {
153+
return
154+
}
155+
d.keys = nil
156+
d.latestKey = database.CryptoKey{}
157+
}
158+
159+
// fetch fetches all keys for the given feature and determines the latest key.
160+
// It must be called while holding the keysMu lock.
161+
func (d *DBCache) fetch(ctx context.Context) error {
162+
keys, err := d.db.GetCryptoKeysByFeature(ctx, d.feature)
163+
if err != nil {
164+
return xerrors.Errorf("get crypto keys by feature: %w", err)
165+
}
166+
167+
now := d.clock.Now()
168+
_ = d.timer.Reset(time.Minute * 10)
169+
d.invalidateAt = now.Add(time.Minute * 10)
170+
171+
cache := make(map[int32]database.CryptoKey)
172+
var latest database.CryptoKey
173+
for _, key := range keys {
174+
cache[key.Sequence] = key
175+
if key.CanSign(now) && key.Sequence > latest.Sequence {
176+
latest = key
177+
}
178+
}
179+
180+
if len(cache) == 0 {
181+
return ErrKeyNotFound
182+
}
183+
184+
if !latest.CanSign(now) {
185+
return ErrKeyInvalid
186+
}
187+
188+
d.keys, d.latestKey = cache, latest
189+
return nil
190+
}
191+
192+
func checkKey(key database.CryptoKey, now time.Time) (codersdk.CryptoKey, error) {
193+
if !key.CanVerify(now) {
194+
return codersdk.CryptoKey{}, ErrKeyInvalid
195+
}
196+
197+
return db2sdk.CryptoKey(key), nil
198+
}
199+
200+
func (d *DBCache) Close() {
201+
d.keysMu.Lock()
202+
defer d.keysMu.Unlock()
203+
204+
if d.closed {
205+
return
206+
}
207+
208+
d.timer.Stop()
209+
d.closed = true
210+
}

0 commit comments

Comments
 (0)