/
database.go
427 lines (372 loc) · 13.7 KB
/
database.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
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2015-2016 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Package asserts implements snappy assertions and a database
// abstraction for managing and holding them.
package asserts
import (
"errors"
"fmt"
"regexp"
"time"
)
// A Backstore stores assertions. It can store and retrieve assertions
// by type under unique primary key headers (whose names are available
// from assertType.PrimaryKey). Plus it supports searching by headers.
type Backstore interface {
// Put stores an assertion.
// It is responsible for checking that assert is newer than a
// previously stored revision with the same primary key headers.
Put(assertType *AssertionType, assert Assertion) error
// Get returns the assertion with the given unique key for its primary key headers.
// If none is present it returns ErrNotFound.
Get(assertType *AssertionType, key []string) (Assertion, error)
// Search returns assertions matching the given headers.
// It invokes foundCb for each found assertion.
Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion)) error
}
type nullBackstore struct{}
func (nbs nullBackstore) Put(t *AssertionType, a Assertion) error {
return fmt.Errorf("cannot store assertions without setting a proper assertion backstore implementation")
}
func (nbs nullBackstore) Get(t *AssertionType, k []string) (Assertion, error) {
return nil, ErrNotFound
}
func (nbs nullBackstore) Search(t *AssertionType, h map[string]string, f func(Assertion)) error {
return nil
}
// A KeypairManager is a manager and backstore for private/public key pairs.
type KeypairManager interface {
// Put stores the given private/public key pair for identity,
// making sure it can be later retrieved by authority-id and
// key id with Get().
// Trying to store a key with an already present key id should
// result in an error.
Put(authorityID string, privKey PrivateKey) error
// Get returns the private/public key pair with the given key id.
Get(authorityID, keyID string) (PrivateKey, error)
}
// TODO: for more flexibility plugging the keypair manager make PrivatKey private encoding methods optional, and add an explicit sign method.
// DatabaseConfig for an assertion database.
type DatabaseConfig struct {
// trusted account keys
TrustedKeys []*AccountKey
// backstore for assertions, left unset storing assertions will error
Backstore Backstore
// manager/backstore for keypairs, mandatory
KeypairManager KeypairManager
// assertion checkers used by Database.Check, left unset DefaultCheckers will be used which is recommended
Checkers []Checker
}
// Well-known errors
var (
ErrNotFound = errors.New("assertion not found")
)
// A RODatabase exposes read-only access to an assertion database.
type RODatabase interface {
// Find an assertion based on arbitrary headers.
// Provided headers must contain the primary key for the assertion type.
// It returns ErrNotFound if the assertion cannot be found.
Find(assertionType *AssertionType, headers map[string]string) (Assertion, error)
// FindMany finds assertions based on arbitrary headers.
// It returns ErrNotFound if no assertion can be found.
FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error)
}
// A Checker defines a check on an assertion considering aspects such as
// its signature, the signing key, and consistency with other
// assertions in the database.
type Checker func(assert Assertion, signature Signature, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error
// Database holds assertions and can be used to sign or check
// further assertions.
type Database struct {
bs Backstore
keypairMgr KeypairManager
trusted Backstore
backstores []Backstore
checkers []Checker
}
// OpenDatabase opens the assertion database based on the configuration.
func OpenDatabase(cfg *DatabaseConfig) (*Database, error) {
bs := cfg.Backstore
keypairMgr := cfg.KeypairManager
if bs == nil {
bs = nullBackstore{}
}
if keypairMgr == nil {
panic("database cannot be used without setting a keypair manager")
}
trustedBackstore := NewMemoryBackstore()
for _, accKey := range cfg.TrustedKeys {
err := trustedBackstore.Put(AccountKeyType, accKey)
if err != nil {
return nil, fmt.Errorf("error loading for use trusted account key %q for %q: %v", accKey.PublicKeyID(), accKey.AuthorityID(), err)
}
}
checkers := cfg.Checkers
if len(checkers) == 0 {
checkers = DefaultCheckers
}
dbCheckers := make([]Checker, len(checkers))
copy(dbCheckers, checkers)
return &Database{
bs: bs,
keypairMgr: keypairMgr,
trusted: trustedBackstore,
// order here is relevant, Find* precedence and
// findAccountKey depend on it, trusted should win over the
// general backstore!
backstores: []Backstore{trustedBackstore, bs},
checkers: dbCheckers,
}, nil
}
// GenerateKey generates a private/public key pair for identity and
// stores it returning its key id.
func (db *Database) GenerateKey(authorityID string) (keyID string, err error) {
// TODO: optionally delegate the whole thing to the keypair mgr
// TODO: support specifying different key types/algorithms
privKey, err := generatePrivateKey()
if err != nil {
return "", fmt.Errorf("failed to generate private key: %v", err)
}
pk := OpenPGPPrivateKey(privKey)
err = db.ImportKey(authorityID, pk)
if err != nil {
return "", err
}
return pk.PublicKey().ID(), nil
}
// ImportKey stores the given private/public key pair for identity.
func (db *Database) ImportKey(authorityID string, privKey PrivateKey) error {
return db.keypairMgr.Put(authorityID, privKey)
}
var (
// for sanity checking of fingerprint-like strings
fingerprintLike = regexp.MustCompile("^[0-9a-f]*$")
)
func (db *Database) safeGetPrivateKey(authorityID, keyID string) (PrivateKey, error) {
if keyID == "" {
return nil, fmt.Errorf("key id is empty")
}
if !fingerprintLike.MatchString(keyID) {
return nil, fmt.Errorf("key id contains unexpected chars: %q", keyID)
}
return db.keypairMgr.Get(authorityID, keyID)
}
// PublicKey returns the public key owned by authorityID that has the given key id.
func (db *Database) PublicKey(authorityID string, keyID string) (PublicKey, error) {
privKey, err := db.safeGetPrivateKey(authorityID, keyID)
if err != nil {
return nil, err
}
return privKey.PublicKey(), nil
}
// Sign assembles an assertion with the provided information and signs it
// with the private key from `headers["authority-id"]` that has the provided key id.
func (db *Database) Sign(assertType *AssertionType, headers map[string]string, body []byte, keyID string) (Assertion, error) {
authorityID, err := checkMandatory(headers, "authority-id")
if err != nil {
return nil, err
}
privKey, err := db.safeGetPrivateKey(authorityID, keyID)
if err != nil {
return nil, err
}
return assembleAndSign(assertType, headers, body, privKey)
}
// findAccountKey finds an AccountKey exactly by account id and key id.
func (db *Database) findAccountKey(authorityID, keyID string) (*AccountKey, error) {
key := []string{authorityID, keyID}
// consider trusted account keys then disk stored account keys
for _, bs := range db.backstores {
a, err := bs.Get(AccountKeyType, key)
if err == nil {
return a.(*AccountKey), nil
}
if err != ErrNotFound {
return nil, err
}
}
return nil, ErrNotFound
}
// Check tests whether the assertion is properly signed and consistent with all the stored knowledge.
func (db *Database) Check(assert Assertion) error {
_, signature := assert.Signature()
sig, err := decodeSignature(signature)
if err != nil {
return err
}
// TODO: later may need to consider type of assert to find candidate keys
accKey, err := db.findAccountKey(assert.AuthorityID(), sig.KeyID())
if err == ErrNotFound {
return fmt.Errorf("no matching public key %q for signature by %q", sig.KeyID(), assert.AuthorityID())
}
if err != nil {
return fmt.Errorf("error finding matching public key for signature: %v", err)
}
now := time.Now()
for _, checker := range db.checkers {
err := checker(assert, sig, accKey, db, now)
if err != nil {
return err
}
}
return nil
}
// Add persists the assertion after ensuring it is properly signed and consistent with all the stored knowledge.
// It will return an error when trying to add an older revision of the assertion than the one currently stored.
func (db *Database) Add(assert Assertion) error {
assertType := assert.Type()
err := db.Check(assert)
if err != nil {
return err
}
keyValues := make([]string, len(assertType.PrimaryKey))
for i, k := range assertType.PrimaryKey {
keyVal := assert.Header(k)
if keyVal == "" {
return fmt.Errorf("missing primary key header: %v", k)
}
keyValues[i] = keyVal
}
// assuming trusted account keys/assertions will be managed
// through the os snap this seems the safest policy until we
// know more/better
_, err = db.trusted.Get(assertType, keyValues)
if err != ErrNotFound {
return fmt.Errorf("cannot add %q assertion with primary key clashing with a trusted assertion: %v", assertType.Name, keyValues)
}
return db.bs.Put(assertType, assert)
}
func searchMatch(assert Assertion, expectedHeaders map[string]string) bool {
// check non-primary-key headers as well
for expectedKey, expectedValue := range expectedHeaders {
if assert.Header(expectedKey) != expectedValue {
return false
}
}
return true
}
// Find an assertion based on arbitrary headers.
// Provided headers must contain the primary key for the assertion type.
// It returns ErrNotFound if the assertion cannot be found.
func (db *Database) Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) {
err := checkAssertType(assertionType)
if err != nil {
return nil, err
}
keyValues := make([]string, len(assertionType.PrimaryKey))
for i, k := range assertionType.PrimaryKey {
keyVal := headers[k]
if keyVal == "" {
return nil, fmt.Errorf("must provide primary key: %v", k)
}
keyValues[i] = keyVal
}
var assert Assertion
for _, bs := range db.backstores {
a, err := bs.Get(assertionType, keyValues)
if err == nil {
assert = a
break
}
if err != ErrNotFound {
return nil, err
}
}
if assert == nil || !searchMatch(assert, headers) {
return nil, ErrNotFound
}
return assert, nil
}
// FindMany finds assertions based on arbitrary headers.
// It returns ErrNotFound if no assertion can be found.
func (db *Database) FindMany(assertionType *AssertionType, headers map[string]string) ([]Assertion, error) {
err := checkAssertType(assertionType)
if err != nil {
return nil, err
}
res := []Assertion{}
foundCb := func(assert Assertion) {
res = append(res, assert)
}
for _, bs := range db.backstores {
err = bs.Search(assertionType, headers, foundCb)
if err != nil {
return nil, err
}
}
if len(res) == 0 {
return nil, ErrNotFound
}
return res, nil
}
// assertion checkers
// CheckSigningKeyIsNotExpired checks that the signing key is not expired.
func CheckSigningKeyIsNotExpired(assert Assertion, signature Signature, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error {
if !signingKey.isKeyValidAt(checkTime) {
return fmt.Errorf("assertion is signed with expired public key %q from %q", signature.KeyID(), assert.AuthorityID())
}
return nil
}
// CheckSignature checks that the signature is valid.
func CheckSignature(assert Assertion, signature Signature, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error {
content, _ := assert.Signature()
err := signingKey.publicKey().verify(content, signature)
if err != nil {
return fmt.Errorf("failed signature verification: %v", err)
}
return nil
}
type timestamped interface {
Timestamp() time.Time
}
// CheckTimestampVsSigningKeyValidity verifies that the timestamp of
// the assertion is within the signing key validity.
func CheckTimestampVsSigningKeyValidity(assert Assertion, signature Signature, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error {
if tstamped, ok := assert.(timestamped); ok {
if !signingKey.isKeyValidAt(tstamped.Timestamp()) {
return fmt.Errorf("%s assertion timestamp outside of signing key validity", assert.Type().Name)
}
}
return nil
}
// XXX: keeping these in this form until we know better
// A consistencyChecker performs further checks based on the full
// assertion database knowledge and its own signing key.
type consistencyChecker interface {
checkConsistency(roDB RODatabase, signingKey *AccountKey) error
}
// CheckCrossConsistency verifies that the assertion is consistent with the other statements in the database.
func CheckCrossConsistency(assert Assertion, signature Signature, signingKey *AccountKey, roDB RODatabase, checkTime time.Time) error {
// see if the assertion requires further checks
if checker, ok := assert.(consistencyChecker); ok {
err := checker.checkConsistency(roDB, signingKey)
if err != nil {
return fmt.Errorf("%s assertion violates other knowledge: %v", assert.Type().Name, err)
}
}
return nil
}
// DefaultCheckers lists the default and recommended assertion
// checkers used by Database if none are specified in the
// DatabaseConfig.Checkers.
var DefaultCheckers = []Checker{
CheckSigningKeyIsNotExpired,
CheckSignature,
CheckTimestampVsSigningKeyValidity,
CheckCrossConsistency,
}