-
Notifications
You must be signed in to change notification settings - Fork 1
/
server.go
357 lines (290 loc) · 9.21 KB
/
server.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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/*
Copyright 2017 Continusec Pty Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package safeadmin
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/binary"
"log"
"math/big"
"time"
context "golang.org/x/net/context"
"sync"
"encoding/hex"
"github.com/continusec/safeadmin/pb"
"github.com/golang/protobuf/proto"
)
// SafeDumpServer is the main server object that can handle the business logic, for the defined gRPC service,
// regardless of actually protocol and persistence layer
type SafeDumpServer struct {
// Storage is a dumb layer that can store and load stuff
Storage SafeDumpPersistence
// MaxDecryptionPeriod is the maximum length of time the server will commit to being able to decrypt an object encrypted with it's certicates
MaxDecryptionPeriod time.Duration
// CertificationRotationPeriod is how often a fresh certificate is issued
CertificateRotationPeriod time.Duration
// OverrideDateChecks, if set, will skip date checks on TTL. This should only be used with breakglass tools that operate on the server directly
OverrideDateChecks bool
// PurgeOldKeys deletes old keys automatically (assuming cron is started)
PurgeOldKeys bool
// KeyRetentionPeriod is the period after a key expires that it will be kept anyway
// Only used if PurgeOldKeys is set
KeyRetentionPeriod time.Duration
// Cache
certLock sync.Mutex
cachedCurrentCert *pb.GetPublicCertResponse
// Cache
keysLock sync.Mutex
cachedKeys map[string]*keyCert // string is hex encoded spki
}
var (
keyCurrentCert = []byte("current-cert")
)
type keyCert struct {
Certificate *x509.Certificate
Key *rsa.PrivateKey
}
// generateNewCertificate creates a new certificate, but does not persist it.
// Returns the key/cert, as well as the time when it should next be rotated.
func (s *SafeDumpServer) generateNewCertificate() (*pb.KeyAndCert, time.Time, error) {
now := time.Now()
ttl := now.Add(s.MaxDecryptionPeriod + s.CertificateRotationPeriod)
// Generate a private key
pkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, time.Time{}, err
}
// Use a barebone template
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(0), // appears to be a required element
NotBefore: now.Add(-5 * time.Minute), // allow for client clock skew
NotAfter: ttl,
SignatureAlgorithm: x509.SHA256WithRSA,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &pkey.PublicKey, pkey)
if err != nil {
return nil, time.Time{}, err
}
return &pb.KeyAndCert{
Cert: &pb.GetPublicCertResponse{
Der: der,
Ttl: now.Add(s.CertificateRotationPeriod).Unix(),
},
Key: x509.MarshalPKCS1PrivateKey(pkey),
}, ttl, nil
}
// calcSPKI returns the SPKI SHA256 hash for the X509 DER ASN.1 encoded certificate
func calcSPKI(der []byte) ([]byte, error) {
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, err
}
hb := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
return hb[:], nil
}
// rotateCertificate will generate a new cert, then save. We don't care if there's race condition, worst case we just issue 2 certs which is fine.
func (s *SafeDumpServer) rotateCertificate(ctx context.Context) (*pb.GetPublicCertResponse, error) {
keyAndCert, keyTTL, err := s.generateNewCertificate()
if err != nil {
return nil, err
}
ksBo, err := proto.Marshal(keyAndCert)
if err != nil {
return nil, err
}
bo, err := proto.Marshal(keyAndCert.Cert)
if err != nil {
return nil, err
}
spki, err := calcSPKI(keyAndCert.Cert.Der)
if err != nil {
return nil, err
}
// First, save the key / cert. No big deal if we never use it, so no need to wrap in transaction with below
err = s.Storage.Save(ctx, spki, ksBo, keyTTL)
if err != nil {
return nil, err
}
// Then save out the current cert record
err = s.Storage.Save(ctx, keyCurrentCert, bo, time.Unix(keyAndCert.Cert.Ttl, 0))
if err != nil {
return nil, err
}
return keyAndCert.Cert, nil
}
// GetPublicCert is part of the service definition, it returns the current public certificate, rotating if necessary
func (s *SafeDumpServer) GetPublicCert(ctx context.Context, req *pb.GetPublicCertRequest) (*pb.GetPublicCertResponse, error) {
log.Println("Serving GetPublicCert request...")
s.certLock.Lock()
defer s.certLock.Unlock()
if s.cachedCurrentCert != nil && time.Now().Before(time.Unix(s.cachedCurrentCert.Ttl, 0)) {
return s.cachedCurrentCert, nil
}
bb, err := s.Storage.Load(ctx, keyCurrentCert)
switch err {
case nil:
var rv pb.GetPublicCertResponse
err = proto.Unmarshal(bb, &rv)
if err != nil {
log.Printf("Error unmarshaling current certificate: %s\n", err)
return nil, ErrInternalError
}
if time.Now().Before(time.Unix(rv.Ttl, 0)) {
s.cachedCurrentCert = &rv
return &rv, nil
}
// else, continue below
case ErrStorageKeyNotFound:
// continue below
default:
log.Printf("Error loading cached certificate: %s\n", err)
return nil, ErrInternalError
}
rv, err := s.rotateCertificate(ctx)
if err != nil {
log.Printf("Error rotating certificate: %s\n", err)
return nil, ErrInternalError
}
s.cachedCurrentCert = rv
return rv, nil
}
// loadKeyAndCert returns parsed DER cert, private key for spki
func (s *SafeDumpServer) loadKeyAndCert(ctx context.Context, spki []byte) (*keyCert, error) {
stringHash := hex.EncodeToString(spki)
s.keysLock.Lock()
defer s.keysLock.Unlock()
if s.cachedKeys == nil {
s.cachedKeys = make(map[string]*keyCert)
}
rv, ok := s.cachedKeys[stringHash]
if ok {
return rv, nil
}
bb, err := s.Storage.Load(ctx, spki)
if err != nil {
return nil, err
}
var ks pb.KeyAndCert
err = proto.Unmarshal(bb, &ks)
if err != nil {
return nil, err
}
pkey, err := x509.ParsePKCS1PrivateKey(ks.Key)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(ks.Cert.Der)
if err != nil {
return nil, err
}
rv = &keyCert{
Certificate: cert,
Key: pkey,
}
s.cachedKeys[stringHash] = rv
return rv, nil
}
// DecryptSecret finds the associated encryption key, and if all matches, decrypts it.
// Errors returned are ErrInvalidRequest, ErrInvalidDate, ErrInternalError, nil
func (s *SafeDumpServer) DecryptSecret(ctx context.Context, req *pb.DecryptSecretRequest) (*pb.DecryptSecretResponse, error) {
log.Println("Serving DecryptSecret request...")
// Do we have a header?
if req.Header == nil {
return nil, ErrInvalidRequest
}
now := time.Now()
headerTTL := time.Unix(req.Header.Ttl, 0)
// Is the TTL in the header before now?
if headerTTL.Before(now) {
if s.OverrideDateChecks {
// allow through if in break glass mode
} else {
return nil, ErrInvalidDate
}
}
// Load the key and cert
ks, err := s.loadKeyAndCert(ctx, req.Header.SpkiFingerprint)
if err != nil {
log.Printf("Error loading key and cert: %s\n", err)
return nil, ErrInternalError
}
// Verify the TTL is in the range for the cert
if headerTTL.After(ks.Certificate.NotAfter) {
if s.OverrideDateChecks {
// allow through if in break glass mode
} else {
return nil, ErrInvalidDate
}
}
if headerTTL.Before(ks.Certificate.NotBefore) {
if s.OverrideDateChecks {
// allow through if in break glass mode
} else {
return nil, ErrInvalidDate
}
}
// Finally, decrypt using our key
ttlb := make([]byte, 8)
binary.BigEndian.PutUint64(ttlb, uint64(req.Header.Ttl))
key, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, ks.Key, req.Header.EncryptedKey, ttlb)
if err != nil {
log.Printf("Error decrypting header: %s\n", err)
return nil, ErrInvalidRequest
}
return &pb.DecryptSecretResponse{Key: key}, nil
}
// Close is a no-op for the server, but is required per the interface definition
func (s *SafeDumpServer) Close() error {
return nil
}
// SourceName is used to key cache data. Ought not be called in this context
func (s *SafeDumpServer) SourceName() string {
return ""
}
// CronPurge should be called regularly
func (s *SafeDumpServer) CronPurge(ctx context.Context) error {
log.Println("Running purge cron...")
// First purge anything we have that is older than the retention period
if s.PurgeOldKeys {
cutoff := time.Now().Add(s.KeyRetentionPeriod)
log.Printf("Purging keys older than: %s\n", cutoff.Format(time.RFC3339))
err := s.Storage.Purge(ctx, cutoff)
if err != nil {
return err
}
}
// Clear out local cache
s.certLock.Lock()
s.cachedCurrentCert = nil
s.certLock.Unlock()
s.keysLock.Lock()
s.cachedKeys = nil
s.keysLock.Unlock()
log.Println("Cron complete")
return nil
}
// BeginPurgeCron will start go funcs the purge cron job
// We run as often as the CertificateRotationPeriod
func (s *SafeDumpServer) BeginPurgeCron() {
go func() {
for {
err := s.CronPurge(context.Background())
if err != nil {
log.Printf("Error running CronPurge: %s\n", err)
}
time.Sleep(s.CertificateRotationPeriod)
}
}()
}