revocation.go 17 KB
Newer Older
1
2
3
4
5
6
7
package irma

import (
	"fmt"
	"time"

	"github.com/go-errors/errors"
8
9
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
10
	"github.com/privacybydesign/gabi/big"
11
	"github.com/privacybydesign/gabi/revocation"
12
	"github.com/privacybydesign/gabi/signed"
13
14
)

15
type (
Sietse Ringers's avatar
Sietse Ringers committed
16
17
18
	// RevocationStorage stores and retrieves revocation-related data from and to a SQL database,
	// and offers a revocation API for all other irmago code, including a Revoke() method that
	// revokes an earlier issued credential.
19
20
21
22
23
24
25
26
27
	RevocationStorage struct {
		conf     *Configuration
		db       revStorage
		memdb    memRevStorage
		sqlMode  bool
		settings map[CredentialTypeIdentifier]*RevocationSetting

		Keys   RevocationKeys
		client RevocationClient
28
29
	}

Sietse Ringers's avatar
Sietse Ringers committed
30
	// RevocationClient offers an HTTP client to the revocation server endpoints.
31
32
33
34
	RevocationClient struct {
		Conf *Configuration
	}

Sietse Ringers's avatar
Sietse Ringers committed
35
36
	// RevocationKeys contains helper functions for retrieving revocation private and public keys
	// from an irma.Configuration instance.
37
38
39
40
	RevocationKeys struct {
		Conf *Configuration
	}

Sietse Ringers's avatar
Sietse Ringers committed
41
	// RevocationSetting contains revocation settings for a given credential type.
42
43
44
	RevocationSetting struct {
		Mode     RevocationMode `json:"mode"`
		PostURLs []string       `json:"post_urls" mapstructure:"post_urls"`
45
46
47
48
49

		// set to now whenever a new revocation record is received, or when the RA indicates
		// there are no new records. Thus it specifies up to what time our nonrevocation
		// guarantees lasts.
		updated time.Time
50
51
	}

Sietse Ringers's avatar
Sietse Ringers committed
52
53
	// RevocationMode specifies for a given credential type what revocation operations are
	// supported, and how the associated data is stored (SQL or memory).
54
	RevocationMode string
55

Sietse Ringers's avatar
Sietse Ringers committed
56
57
58
	// RevocationRecord contains a signed AccumulatorUpdate and associated information and is used
	// by clients, issuers and verifiers to update their revocation state, so that they can create
	// and verify nonrevocation proofs and witnesses.
59
60
	RevocationRecord struct {
		revocation.Record `gorm:"embedded"`
61
		PublicKeyIndex    uint
62
		CredType          CredentialTypeIdentifier `gorm:"primary_key"`
63
64
65
66
67
68
69
70
71
	}

	TimeRecord struct {
		Index      uint64
		Start, End int64
	}

	// IssuanceRecord contains information generated during issuance, needed for later revocation.
	IssuanceRecord struct {
72
73
		CredType   CredentialTypeIdentifier `gorm:"primary_key"`
		Key        string                   `gorm:"primary_key"`
74
75
76
77
78
		Attr       *big.Int
		Issued     int64
		ValidUntil int64
		RevokedAt  int64 // 0 if not currently revoked
	}
79
)
80

81
const (
Sietse Ringers's avatar
Sietse Ringers committed
82
83
	// RevocationModeRequestor is the default revocation mode in which only RevocationRecord instances
	// are consumed for issuance or verification. Uses an in-memory store.
84
	RevocationModeRequestor RevocationMode = ""
Sietse Ringers's avatar
Sietse Ringers committed
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

	// RevocationModeProxy indicates that this server
	// (1) allows fetching of RevocationRecord instances from its database,
	// (2) relays all RevocationRecord instances it receives to the URLs configured in the containing
	// RevocationSetting struct.
	// Requires a SQL server to store and retrieve RevocationRecord instances from.
	RevocationModeProxy RevocationMode = "proxy"

	// RevocationModeServer indicates that this is the revocation server for a credential type,
	// to which the credential type's RevocationServer URL should point.
	// IssuanceRecord instances are sent to this server, as well as revocation commands, through
	// revocation sessions or through the RevocationStorage.Revoke() method.
	// Requires a SQL server to store and retrieve all records from and requires the issuer's
	// private key to be accessible, in order to revoke and to sign new revocation records.
	RevocationModeServer RevocationMode = "server"

	// revocationUpdateCount specifies how many revocation records are attached to session requests
	// for the client to update its revocation state.
	revocationUpdateCount = 5
104
105
106
107
108
109

	// revocationMaxAccumulatorAge is the default maximum for the 'accumulator age', which we
	// define to be the amount of time since the last confirmation from the RA that the latest
	// accumulator that we know is still the latest one: clients should prove nonrevocation
	// against a 'younger' accumulator.
	revocationMaxAccumulatorAge = 5 * time.Minute
110
111
)

112
// Revocation record methods
113

Sietse Ringers's avatar
Sietse Ringers committed
114
115
116
// EnableRevocation creates an initial accumulator for a given credential type. This function is the
// only way to create such an initial accumulator and it must be called before anyone can use
// revocation for this credential type. Requires the issuer private key.
117
func (rs *RevocationStorage) EnableRevocation(typ CredentialTypeIdentifier, sk *revocation.PrivateKey) error {
118
119
120
121
122
123
124
125
	hasRecords, err := rs.db.HasRecords(typ, (*RevocationRecord)(nil))
	if err != nil {
		return err
	}
	if hasRecords {
		return errors.New("revocation record table not empty")
	}

126
127
128
129
	msg, acc, err := revocation.NewAccumulator(sk)
	if err != nil {
		return err
	}
130
131
	r := &RevocationRecord{
		Record: revocation.Record{
132
133
134
			Message:    msg,
			StartIndex: acc.Index,
			EndIndex:   acc.Index,
135
		},
136
137
		PublicKeyIndex: sk.Counter,
		CredType:       typ,
138
139
140
	}

	if err = rs.AddRevocationRecord(r); err != nil {
141
142
143
144
145
		return err
	}
	return nil
}

Sietse Ringers's avatar
Sietse Ringers committed
146
147
148
149
150
151
152
153
154
155
156
// RevocationEnabled returns whether or not revocation is enabled for the given credential type,
// by checking if any revocation record exists in the database.
func (rs *RevocationStorage) RevocationEnabled(typ CredentialTypeIdentifier) (bool, error) {
	if rs.sqlMode {
		return rs.db.HasRecords(typ, (*RevocationRecord)(nil))
	} else {
		return rs.memdb.HasRecords(typ), nil
	}
}

// RevocationRecords returns all records that a client requires to update its revocation state if it is currently
157
158
// at the specified index, that is, all records whose end index is greater than or equal to
// the specified index.
159
160
161
func (rs *RevocationStorage) RevocationRecords(typ CredentialTypeIdentifier, index uint64) ([]*RevocationRecord, error) {
	var records []*RevocationRecord
	return records, rs.db.From(typ, "end_index", index, &records)
162
163
}

164
165
166
167
168
169
170
171
func (rs *RevocationStorage) LatestRevocationRecords(typ CredentialTypeIdentifier, count uint64) ([]*RevocationRecord, error) {
	var records []*RevocationRecord
	if rs.sqlMode {
		if err := rs.db.Latest(typ, "end_index", count, &records); err != nil {
			return nil, err
		}
	} else {
		rs.memdb.Latest(typ, count, &records)
172
	}
173
174
	if len(records) == 0 {
		return nil, gorm.ErrRecordNotFound
175
	}
176
	return records, nil
177
178
}

179
func (rs *RevocationStorage) AddRevocationRecords(records []*RevocationRecord) error {
180
181
	var err error
	for _, r := range records {
182
		if err = rs.addRevocationRecord(rs.db, r, false); err != nil {
183
184
185
			return err
		}
	}
186
187
188
189
190
191

	if len(records) > 0 {
		// POST record to listeners, if any, asynchroniously
		go rs.client.PostRevocationRecords(rs.getSettings(records[0].CredType).PostURLs, records)
	}

192
193
194
	return nil
}

195
func (rs *RevocationStorage) AddRevocationRecord(record *RevocationRecord) error {
196
	return rs.addRevocationRecord(rs.db, record, true)
197
}
198

199
func (rs *RevocationStorage) addRevocationRecord(tx revStorage, record *RevocationRecord, post bool) error {
200
201
	// Unmarshal and verify the record against the appropriate public key
	pk, err := rs.Keys.PublicKey(record.CredType.IssuerIdentifier(), record.PublicKeyIndex)
202
203
204
	if err != nil {
		return err
	}
205
	_, err = record.UnmarshalVerify(pk)
206
	if err != nil {
207
208
209
		return err
	}

210
211
212
213
	// Save record
	if rs.sqlMode {
		if err = tx.Insert(record); err != nil {
			return err
214
		}
215
216
	} else {
		rs.memdb.Insert(record)
217
218
	}

219
220
	s := rs.getSettings(record.CredType)
	s.updated = time.Now()
221
222
223
224
	if post {
		// POST record to listeners, if any, asynchroniously
		go rs.client.PostRevocationRecords(s.PostURLs, []*RevocationRecord{record})
	}
225
226
227

	return nil
}
228

229
230
231
232
// Issuance records

func (rs *RevocationStorage) IssuanceRecordExists(typ CredentialTypeIdentifier, key []byte) (bool, error) {
	return rs.db.Exists(typ, "key", key, &IssuanceRecord{})
233
234
}

235
236
func (rs *RevocationStorage) AddIssuanceRecord(r *IssuanceRecord) error {
	return rs.db.Insert(r)
237
238
}

239
240
241
242
243
func (rs *RevocationStorage) IssuanceRecord(typ CredentialTypeIdentifier, key []byte) (*IssuanceRecord, error) {
	var r IssuanceRecord
	err := rs.db.Get(typ, "key", key, &r)
	if err != nil {
		return nil, err
244
	}
245
246
	return &r, nil
}
247

248
249
// Revocation methods

Sietse Ringers's avatar
Sietse Ringers committed
250
251
// Revoke revokes the credential specified by key if found within the current database,
// by updating its revocation time to now, removing its revocation attribute from the current accumulator,
252
// and updating the revocation database on disk.
253
func (rs *RevocationStorage) Revoke(typ CredentialTypeIdentifier, key string, sk *revocation.PrivateKey) error {
Sietse Ringers's avatar
Sietse Ringers committed
254
255
	if rs.getSettings(typ).Mode != RevocationModeServer {
		return errors.Errorf("cannot revoke %s", typ)
256
	}
257

258
259
260
261
262
263
264
265
266
267
	return rs.db.Transaction(func(tx revStorage) error {
		var err error
		cr := IssuanceRecord{}
		if err = tx.Get(typ, "key", key, &cr); err != nil {
			return err
		}
		cr.RevokedAt = time.Now().UnixNano()
		if err = tx.Save(&cr); err != nil {
			return err
		}
268
		return rs.revokeAttr(tx, typ, sk, cr.Attr)
269
270
271
	})
}

272
273
274
275
276
277
278
279
280
281
func (rs *RevocationStorage) revokeAttr(tx revStorage, typ CredentialTypeIdentifier, sk *revocation.PrivateKey, e *big.Int) error {
	cur, err := rs.currentAccumulator(tx, typ)
	if err != nil {
		return err
	}
	if cur == nil {
		return errors.Errorf("cannot revoke for type %s, not enabled yet", typ)
	}

	newAcc, err := cur.Remove(sk, e)
282
283
284
	if err != nil {
		return err
	}
285
	update := &revocation.AccumulatorUpdate{
286
287
288
289
290
291
292
293
294
		Accumulator: *newAcc,
		StartIndex:  newAcc.Index,
		Revoked:     []*big.Int{e},
		Time:        time.Now().UnixNano(),
	}
	updateMsg, err := signed.MarshalSign(sk.ECDSA, update)
	if err != nil {
		return err
	}
295
296
	record := &RevocationRecord{
		Record: revocation.Record{
297
298
299
			StartIndex: newAcc.Index,
			EndIndex:   newAcc.Index,
			Message:    updateMsg,
300
		},
301
302
		PublicKeyIndex: sk.Counter,
		CredType:       typ,
303
	}
304
	if err = rs.addRevocationRecord(tx, record, true); err != nil {
305
306
307
308
309
		return err
	}
	return nil
}

310
// Accumulator methods
311

312
313
func (rs *RevocationStorage) CurrentAccumulator(typ CredentialTypeIdentifier) (*revocation.Accumulator, error) {
	return rs.currentAccumulator(rs.db, typ)
314
315
}

316
317
func (rs *RevocationStorage) currentAccumulator(tx revStorage, typ CredentialTypeIdentifier) (rec *revocation.Accumulator, err error) {
	record := &RevocationRecord{}
318

319
320
321
322
323
	if rs.sqlMode {
		if err := tx.Last(typ, record); err != nil {
			if gorm.IsRecordNotFoundError(err) {
				return nil, nil
			}
324
325
			return nil, err
		}
326
327
328
329
330
331
332
	} else {
		var r []*RevocationRecord
		rs.memdb.Latest(typ, 1, &r)
		if len(r) == 0 {
			return nil, nil
		}
		record = r[0]
333
334
	}

335
	pk, err := rs.Keys.PublicKey(typ.IssuerIdentifier(), record.PublicKeyIndex)
336
337
338
	if err != nil {
		return nil, err
	}
339
340
	var u revocation.AccumulatorUpdate
	if err = signed.UnmarshalVerify(pk.ECDSA, record.Message, &u); err != nil {
341
342
		return nil, err
	}
343
	return &u.Accumulator, nil
344
345
}

346
347
348
349
// Methods to update from remote revocation server

func (rs *RevocationStorage) UpdateDB(typ CredentialTypeIdentifier) error {
	records, err := rs.client.FetchLatestRevocationRecords(typ, revocationUpdateCount)
350
	if err != nil {
351
		return err
352
	}
353
354
355
356
357
358
359
360

	if err = rs.AddRevocationRecords(records); err != nil {
		return err
	}

	// bump updated even if no new records were added
	rs.getSettings(typ).updated = time.Now()
	return nil
361
362
}

Sietse Ringers's avatar
Sietse Ringers committed
363
func (rs *RevocationStorage) UpdateIfOld(typ CredentialTypeIdentifier) error {
364
365
	// update 10 seconds before the maximum, to stay below it
	if rs.getSettings(typ).updated.Before(time.Now().Add(-revocationMaxAccumulatorAge + 10*time.Second)) {
366
		if err := rs.UpdateDB(typ); err != nil {
367
368
369
370
371
372
			return err
		}
	}
	return nil
}

373
374
375
376
377
378
379
380
// SaveIssuanceRecord either stores the issuance record locally, if we are the revocation server of
// the crecential type, or it signs and sends it to the remote revocation server.
func (rs *RevocationStorage) SaveIssuanceRecord(typ CredentialTypeIdentifier, rec *IssuanceRecord) error {
	// TODO store locally if appropriate?

	// Just store it if we are the revocation server for this credential type
	if rs.getSettings(typ).Mode == RevocationModeServer {
		return rs.AddIssuanceRecord(rec)
381
382
	}

383
384
385
386
387
388
389
390
391
	// We have to send it, sign it first
	credtype := rs.conf.CredentialTypes[typ]
	if credtype == nil {
		return errors.New("unknown credential type")
	}
	if credtype.RevocationServer == "" {
		return errors.New("credential type has no revocation server")
	}
	sk, err := rs.Keys.PrivateKey(typ.IssuerIdentifier())
392
393
394
	if err != nil {
		return err
	}
395
	message, err := signed.MarshalSign(sk.ECDSA, rec)
396
397
398
	if err != nil {
		return err
	}
399
400

	return rs.client.PostIssuanceRecord(typ, sk.Counter, message)
401
402
}

403
404
405
// Misscelaneous methods

func (rs *RevocationStorage) Load(debug bool, connstr string, settings map[CredentialTypeIdentifier]*RevocationSetting) error {
406
407
408
409
410
411
412
413
	var t *CredentialTypeIdentifier
	for typ, s := range settings {
		switch s.Mode {
		case RevocationModeServer, RevocationModeProxy:
			t = &typ
		default:
			return errors.Errorf("invalid revocation mode '%s' for %s (supported: %s, %s)",
				s.Mode, typ, RevocationModeServer, RevocationModeProxy)
414
		}
415
	}
416
417
418
	if t != nil && connstr == "" {
		return errors.Errorf("revocation mode for %s requires SQL database but no connection string given", *t)
	}
419
420
421
422
423
424
425
426

	if connstr == "" {
		Logger.Trace("Using memory revocation database")
		rs.memdb = newMemStorage()
		rs.sqlMode = false
	} else {
		Logger.Trace("Connecting to revocation SQL database")
		db, err := newSqlStorage(debug, connstr)
427
		if err != nil {
428
			return err
429
		}
430
431
		rs.db = db
		rs.sqlMode = true
432
	}
433
434
435
436
437
438
439
440
	if settings != nil {
		rs.settings = settings
	} else {
		rs.settings = map[CredentialTypeIdentifier]*RevocationSetting{}
	}
	rs.client = RevocationClient{Conf: rs.conf}
	rs.Keys = RevocationKeys{Conf: rs.conf}
	return nil
441
442
}

443
444
445
func (rs *RevocationStorage) Close() error {
	if rs.db != nil {
		return rs.db.Close()
446
447
448
449
	}
	return nil
}

450
451
452
453
454
455
// SetRevocationRecords retrieves the latest revocation records from the database, and attaches
// them to the request, for each credential type for which a nonrevocation proof is requested in
// b.Revocation.
func (rs *RevocationStorage) SetRevocationRecords(b *BaseRequest) error {
	if len(b.Revocation) == 0 {
		return nil
456
	}
457
458
459
	var err error
	b.RevocationUpdates = make(map[CredentialTypeIdentifier][]*RevocationRecord, len(b.Revocation))
	for _, credid := range b.Revocation {
Sietse Ringers's avatar
Sietse Ringers committed
460
		if err = rs.UpdateIfOld(credid); err != nil {
461
462
463
464
465
466
467
468
469
470
471
472
			updated := rs.getSettings(credid).updated
			if !updated.IsZero() {
				Logger.Warnf("failed to fetch revocation updates for %s, nonrevocation is guaranteed only until %s ago:",
					credid, time.Now().Sub(updated).String())
				Logger.Warn(err)
			} else {
				Logger.Errorf("revocation is disabled for %s: failed to fetch revocation updates and none are known locally", credid)
				Logger.Warn(err)
				// We can offer no nonrevocation guarantees at all while the requestor explicitly
				// asked for it; fail the session by returning an error
				return err
			}
473
474
475
476
477
		}
		b.RevocationUpdates[credid], err = rs.LatestRevocationRecords(credid, revocationUpdateCount)
		if err != nil {
			return err
		}
478
	}
479
480
481
482
483
484
	return nil
}

func (rs *RevocationStorage) getSettings(typ CredentialTypeIdentifier) *RevocationSetting {
	if rs.settings[typ] == nil {
		rs.settings[typ] = &RevocationSetting{}
485
	}
486
487
488
	return rs.settings[typ]
}

489
func (RevocationClient) PostRevocationRecords(urls []string, records []*RevocationRecord) {
490
	transport := NewHTTPTransport("")
491
492
	for _, url := range urls {
		if err := transport.Post(url+"/-/revocation/records", nil, &records); err != nil {
493
494
			Logger.Warn("error sending revocation update", err)
		}
495
	}
496
497
498
499
500
501
502
503
504
505
506
507
508
}

func (client RevocationClient) PostIssuanceRecord(typ CredentialTypeIdentifier, counter uint, message signed.Message) error {
	return NewHTTPTransport(client.Conf.CredentialTypes[typ].RevocationServer).Post(
		fmt.Sprintf("-/revocation/issuancerecord/%s/%d", typ, counter), nil, []byte(message),
	)
}

// FetchRevocationRecords gets revocation update messages from the revocation server, of the specified index and greater.
func (client RevocationClient) FetchRevocationRecords(typ CredentialTypeIdentifier, index uint64) ([]*RevocationRecord, error) {
	var records []*RevocationRecord
	err := NewHTTPTransport(client.Conf.CredentialTypes[typ].RevocationServer).
		Get(fmt.Sprintf("-/revocation/records/%s/%d", typ, index), &records)
509
	if err != nil {
510
		return nil, err
511
	}
512
513
	return records, nil
}
514

515
516
517
518
func (client RevocationClient) FetchLatestRevocationRecords(typ CredentialTypeIdentifier, count uint64) ([]*RevocationRecord, error) {
	var records []*RevocationRecord
	err := NewHTTPTransport(client.Conf.CredentialTypes[typ].RevocationServer).
		Get(fmt.Sprintf("-/revocation/latestrecords/%s/%d", typ, count), &records)
519
	if err != nil {
520
		return nil, err
521
	}
522
	return records, nil
523
524
}

525
526
func (rs RevocationKeys) PrivateKey(issid IssuerIdentifier) (*revocation.PrivateKey, error) {
	sk, err := rs.Conf.PrivateKey(issid)
527
	if err != nil {
528
		return nil, err
529
530
	}
	if sk == nil {
531
		return nil, errors.Errorf("unknown private key: %s", issid)
532
	}
533
	revsk, err := sk.RevocationKey()
534
	if err != nil {
535
		return nil, err
536
	}
537
538
	return revsk, nil
}
539

540
541
func (rs RevocationKeys) PublicKey(issid IssuerIdentifier, counter uint) (*revocation.PublicKey, error) {
	pk, err := rs.Conf.PublicKey(issid, int(counter))
542
	if err != nil {
543
		return nil, err
544
	}
545
546
	if pk == nil {
		return nil, errors.Errorf("unknown public key: %s-%d", issid, counter)
547
	}
548
549
550
	revpk, err := pk.RevocationKey()
	if err != nil {
		return nil, err
551
	}
552
	return revpk, nil
553
}