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

import (
	"fmt"
	"time"

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

18
type (
Sietse Ringers's avatar
Sietse Ringers committed
19 20 21
	// 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.
22 23 24 25 26 27 28 29 30
	RevocationStorage struct {
		conf     *Configuration
		db       revStorage
		memdb    memRevStorage
		sqlMode  bool
		settings map[CredentialTypeIdentifier]*RevocationSetting

		Keys   RevocationKeys
		client RevocationClient
31 32
	}

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

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

Sietse Ringers's avatar
Sietse Ringers committed
44
	// RevocationSetting contains revocation settings for a given credential type.
45
	RevocationSetting struct {
46
		Mode                     RevocationMode `json:"mode" mapstructure:"mode"`
47
		PostURLs                 []string       `json:"post_urls" mapstructure:"post_urls"`
48
		ServerURL                string         `json:"server_url" mapstructure:"server_url"`
49
		MaxNonrevocationDuration uint           `json:"max_nonrev_duration" mapstructure:"max_nonrev_duration"` // in seconds, min 30
50

51 52
		// set to now whenever a new update is received, or when the RA indicates
		// there are no new updates. Thus it specifies up to what time our nonrevocation
53 54
		// guarantees lasts.
		updated time.Time
55 56
	}

Sietse Ringers's avatar
Sietse Ringers committed
57 58
	// RevocationMode specifies for a given credential type what revocation operations are
	// supported, and how the associated data is stored (SQL or memory).
59
	RevocationMode string
60
)
61

62 63 64 65 66
// Structs corresponding to SQL table rows. All of them end in Record.
type (
	AccumulatorRecord struct {
		*revocation.SignedAccumulator `gorm:"embedded"`
		CredType                      CredentialTypeIdentifier `gorm:"primary_key"`
67 68
	}

69 70 71
	EventRecord struct {
		*revocation.Event `gorm:"embedded"`
		CredType          CredentialTypeIdentifier `gorm:"primary_key"`
72 73 74 75
	}

	// IssuanceRecord contains information generated during issuance, needed for later revocation.
	IssuanceRecord struct {
76
		CredType   CredentialTypeIdentifier `gorm:"primary_key"`
77
		Key        string                   `gorm:"primary_key;column:revocationkey"`
78 79 80 81 82
		Attr       *big.Int
		Issued     int64
		ValidUntil int64
		RevokedAt  int64 // 0 if not currently revoked
	}
83 84 85 86 87 88

	// TODO
	TimeRecord struct {
		Index      uint64
		Start, End int64
	}
89
)
90

91
const (
Sietse Ringers's avatar
Sietse Ringers committed
92 93
	// RevocationModeRequestor is the default revocation mode in which only RevocationRecord instances
	// are consumed for issuance or verification. Uses an in-memory store.
94
	RevocationModeRequestor RevocationMode = ""
Sietse Ringers's avatar
Sietse Ringers committed
95 96

	// RevocationModeProxy indicates that this server
97 98
	// (1) allows fetching of revocation update messages from its database,
	// (2) relays all revocation updates it receives to the URLs configured in the containing
Sietse Ringers's avatar
Sietse Ringers committed
99
	// RevocationSetting struct.
100
	// Requires a SQL server to store and retrieve update messages from.
Sietse Ringers's avatar
Sietse Ringers committed
101 102
	RevocationModeProxy RevocationMode = "proxy"

103
	// RevocationModeServer indicates that this is a revocation server for a credential type.
Sietse Ringers's avatar
Sietse Ringers committed
104 105 106
	// 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
107
	// private key to be accessible, in order to revoke and to sign new revocation update messages.
108
	// In addition this mode exposes the same endpoints as RevocationModeProxy.
Sietse Ringers's avatar
Sietse Ringers committed
109 110
	RevocationModeServer RevocationMode = "server"

111
	// revocationUpdateCount specifies how many revocation events are attached to session requests
Sietse Ringers's avatar
Sietse Ringers committed
112 113
	// for the client to update its revocation state.
	revocationUpdateCount = 5
114

115 116 117
	// revocationMaxAccumulatorAge is the default maximum in seconds 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
118
	// against a 'younger' accumulator.
119
	revocationMaxAccumulatorAge uint = 5 * 60
120 121
)

Sietse Ringers's avatar
Sietse Ringers committed
122 123 124
// 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.
125
func (rs *RevocationStorage) EnableRevocation(typ CredentialTypeIdentifier, sk *revocation.PrivateKey) error {
126
	hasRecords, err := rs.db.HasRecords(typ, (*EventRecord)(nil))
127 128 129 130
	if err != nil {
		return err
	}
	if hasRecords {
131
		return errors.New("revocation event record table not empty")
132 133
	}

134
	update, err := revocation.NewAccumulator(sk)
135 136 137
	if err != nil {
		return err
	}
138

139
	if err = rs.addUpdate(rs.db, typ, update, true); err != nil {
140 141 142 143 144
		return err
	}
	return nil
}

Sietse Ringers's avatar
Sietse Ringers committed
145 146 147 148
// 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 {
149
		return rs.db.HasRecords(typ, (*EventRecord)(nil))
Sietse Ringers's avatar
Sietse Ringers committed
150 151 152 153 154
	} else {
		return rs.memdb.HasRecords(typ), nil
	}
}

155 156 157
// Revocation update message methods

// UpdateFrom returns all records that a client requires to update its revocation state if it is currently
158 159
// at the specified index, that is, all records whose end index is greater than or equal to
// the specified index.
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
func (rs *RevocationStorage) UpdateFrom(typ CredentialTypeIdentifier, index uint64) (*revocation.Update, error) {
	// Only requires SQL implementation
	var update *revocation.Update
	if err := rs.db.Transaction(func(tx revStorage) error {
		acc, _, err := rs.currentAccumulator(tx, typ)
		if err != nil {
			return err
		}
		var events []*EventRecord
		if err := tx.From(typ, "index", index, &events); err != nil {
			return err
		}
		update = rs.newUpdate(acc, events)
		return nil
	}); err != nil {
		return nil, err
	}
	return update, nil
178 179
}

180 181
func (rs *RevocationStorage) UpdateLatest(typ CredentialTypeIdentifier, count uint64) (*revocation.Update, error) {
	// TODO what should this function and UpdateFrom return when no records are found?
182
	if rs.sqlMode {
183 184 185 186 187 188 189
		var update *revocation.Update
		if err := rs.db.Transaction(func(tx revStorage) error {
			acc, _, err := rs.currentAccumulator(tx, typ)
			if err != nil {
				return err
			}
			var events []*EventRecord
190
			if err := tx.Latest(typ, "eventindex", count, &events); err != nil {
191 192 193 194 195
				return err
			}
			update = rs.newUpdate(acc, events)
			return nil
		}); err != nil {
196 197
			return nil, err
		}
198
		return update, nil
199
	} else {
200
		return rs.memdb.Latest(typ, count), nil
201 202 203
	}
}

204 205 206 207
func (*RevocationStorage) newUpdate(acc *revocation.SignedAccumulator, events []*EventRecord) *revocation.Update {
	updates := make([]*revocation.Event, len(events))
	for i := range events {
		updates[i] = events[i].Event
208
	}
209 210 211
	return &revocation.Update{
		SignedAccumulator: acc,
		Events:            updates,
212
	}
213 214
}

215
func (rs *RevocationStorage) AddUpdate(typ CredentialTypeIdentifier, record *revocation.Update) error {
216
	return rs.addUpdate(rs.db, typ, record, false)
217
}
218

219
func (rs *RevocationStorage) addUpdate(tx revStorage, typ CredentialTypeIdentifier, update *revocation.Update, create bool) error {
220
	// Unmarshal and verify the record against the appropriate public key
221
	pk, err := rs.Keys.PublicKey(typ.IssuerIdentifier(), update.SignedAccumulator.PKIndex)
222 223 224
	if err != nil {
		return err
	}
225
	if _, _, err = update.Verify(pk, 0); err != nil {
226 227 228
		return err
	}

229 230
	// Save record
	if rs.sqlMode {
231 232 233 234 235
		save := tx.Save
		if create {
			save = tx.Insert
		}
		if err = save(&AccumulatorRecord{SignedAccumulator: update.SignedAccumulator, CredType: typ}); err != nil {
236
			return err
237
		}
238 239 240 241 242
		for _, event := range update.Events {
			if err = tx.Insert(&EventRecord{Event: event, CredType: typ}); err != nil {
				return err
			}
		}
243
	} else {
244
		rs.memdb.Insert(typ, update)
245 246
	}

247
	s := rs.getSettings(typ)
248
	s.updated = time.Now()
249 250
	// POST record to listeners, if any, asynchroniously
	go rs.client.PostUpdate(typ, s.PostURLs, update)
251 252 253

	return nil
}
254

255 256 257 258
// Issuance records

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

261 262
func (rs *RevocationStorage) AddIssuanceRecord(r *IssuanceRecord) error {
	return rs.db.Insert(r)
263 264
}

265 266 267 268 269
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
270
	}
271 272
	return &r, nil
}
273

274 275
// Revocation methods

Sietse Ringers's avatar
Sietse Ringers committed
276 277
// 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,
278
// and updating the revocation database on disk.
279
func (rs *RevocationStorage) Revoke(typ CredentialTypeIdentifier, key string, sk *revocation.PrivateKey) error {
Sietse Ringers's avatar
Sietse Ringers committed
280 281
	if rs.getSettings(typ).Mode != RevocationModeServer {
		return errors.Errorf("cannot revoke %s", typ)
282
	}
283

284 285
	return rs.db.Transaction(func(tx revStorage) error {
		var err error
286
		issrecord := IssuanceRecord{}
287
		if err = tx.Get(typ, "revocationkey", key, &issrecord); err != nil {
288 289
			return err
		}
290 291
		issrecord.RevokedAt = time.Now().UnixNano()
		if err = tx.Save(&issrecord); err != nil {
292 293
			return err
		}
294
		return rs.revokeAttr(tx, typ, sk, issrecord.Attr)
295 296 297
	})
}

298
func (rs *RevocationStorage) revokeAttr(tx revStorage, typ CredentialTypeIdentifier, sk *revocation.PrivateKey, e *big.Int) error {
299
	_, cur, err := rs.currentAccumulator(tx, typ)
300 301 302 303 304 305
	if err != nil {
		return err
	}
	if cur == nil {
		return errors.Errorf("cannot revoke for type %s, not enabled yet", typ)
	}
306 307
	var parent EventRecord
	if err = rs.db.Last(typ, &parent); err != nil {
308 309
		return err
	}
310 311

	update, err := cur.Remove(sk, e, parent.Event)
312 313 314
	if err != nil {
		return err
	}
315
	if err = rs.addUpdate(tx, typ, update, false); err != nil {
316 317 318 319 320
		return err
	}
	return nil
}

321
// Accumulator methods
322

323 324 325 326 327
func (rs *RevocationStorage) currentAccumulator(tx revStorage, typ CredentialTypeIdentifier) (
	*revocation.SignedAccumulator, *revocation.Accumulator, error,
) {
	record := &AccumulatorRecord{}
	var err error
328
	if rs.sqlMode {
329
		if err = tx.Last(typ, record); err != nil {
330
			if gorm.IsRecordNotFoundError(err) {
331
				return nil, nil, nil
332
			}
333
		}
334
	} else {
335 336 337
		u := rs.memdb.Latest(typ, 0)
		if u == nil {
			return nil, nil, nil
338
		}
339
		record.SignedAccumulator = u.SignedAccumulator
340 341
	}

342
	pk, err := rs.Keys.PublicKey(typ.IssuerIdentifier(), record.PKIndex)
343
	if err != nil {
344
		return nil, nil, err
345
	}
346 347 348
	acc, err := record.UnmarshalVerify(pk)
	if err != nil {
		return nil, nil, err
349
	}
350
	return record.SignedAccumulator, acc, nil
351 352
}

353 354 355
// Methods to update from remote revocation server

func (rs *RevocationStorage) UpdateDB(typ CredentialTypeIdentifier) error {
356
	update, err := rs.client.FetchUpdateLatest(typ, revocationUpdateCount)
357
	if err != nil {
358
		return err
359
	}
360

361
	if err = rs.AddUpdate(typ, update); err != nil {
362 363 364 365 366 367
		return err
	}

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

Sietse Ringers's avatar
Sietse Ringers committed
370
func (rs *RevocationStorage) UpdateIfOld(typ CredentialTypeIdentifier) error {
371
	settings := rs.getSettings(typ)
372
	// update 10 seconds before the maximum, to stay below it
373
	if settings.updated.Before(time.Now().Add(time.Duration(-settings.MaxNonrevocationDuration+10) * time.Second)) {
374
		if err := rs.UpdateDB(typ); err != nil {
375 376 377 378 379 380
			return err
		}
	}
	return nil
}

381 382
// 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.
383 384 385 386 387 388 389 390 391
func (rs *RevocationStorage) SaveIssuanceRecord(typ CredentialTypeIdentifier, rec *IssuanceRecord, sk *gabi.PrivateKey) error {
	credtype := rs.conf.CredentialTypes[typ]
	if credtype == nil {
		return errors.New("unknown credential type")
	}
	if !credtype.SupportsRevocation() {
		return errors.New("cannot save issuance record: credential type does not support revocation")
	}

392
	// Just store it if we are the revocation server for this credential type
393 394
	settings := rs.getSettings(typ)
	if settings.Mode == RevocationModeServer {
395
		return rs.AddIssuanceRecord(rec)
396 397
	}

398
	// We have to send it, sign it first
399 400 401
	if settings.ServerURL == "" {
		return errors.New("cannot send issuance record: no server_url configured")
	}
402
	rsk, err := sk.RevocationKey()
403 404 405
	if err != nil {
		return err
	}
406
	return rs.client.PostIssuanceRecord(typ, rsk, rec, settings.ServerURL)
407 408
}

409 410
// Misscelaneous methods

411
func (rs *RevocationStorage) Load(debug bool, dbtype, connstr string, settings map[CredentialTypeIdentifier]*RevocationSetting) error {
412
	var t *CredentialTypeIdentifier
413

414 415
	for typ, s := range settings {
		switch s.Mode {
416 417 418 419
		case RevocationModeServer:
			if s.ServerURL != "" {
				return errors.New("server_url cannot be combined with server mode")
			}
420
			t = &typ
421 422 423
		case RevocationModeProxy:
			t = &typ
		case RevocationModeRequestor: // noop
424
		default:
425 426
			return errors.Errorf(`invalid revocation mode "%s" for %s (supported: "%s", "%s", "%s")`,
				s.Mode, typ, RevocationModeRequestor, RevocationModeServer, RevocationModeProxy)
427
		}
428
	}
429 430 431
	if t != nil && connstr == "" {
		return errors.Errorf("revocation mode for %s requires SQL database but no connection string given", *t)
	}
432 433 434 435 436 437 438

	if connstr == "" {
		Logger.Trace("Using memory revocation database")
		rs.memdb = newMemStorage()
		rs.sqlMode = false
	} else {
		Logger.Trace("Connecting to revocation SQL database")
439
		db, err := newSqlStorage(debug, dbtype, connstr)
440
		if err != nil {
441
			return err
442
		}
443 444
		rs.db = db
		rs.sqlMode = true
445
	}
446 447 448 449 450
	if settings != nil {
		rs.settings = settings
	} else {
		rs.settings = map[CredentialTypeIdentifier]*RevocationSetting{}
	}
451 452 453 454 455 456
	for id, settings := range rs.settings {
		if settings.MaxNonrevocationDuration != 0 && settings.MaxNonrevocationDuration < 30 {
			return errors.Errorf("max_nonrev_duration setting for %s must be at least 30 seconds, was %d",
				id, settings.MaxNonrevocationDuration)
		}
	}
457 458 459
	rs.client = RevocationClient{Conf: rs.conf}
	rs.Keys = RevocationKeys{Conf: rs.conf}
	return nil
460 461
}

462 463 464
func (rs *RevocationStorage) Close() error {
	if rs.db != nil {
		return rs.db.Close()
465 466 467 468
	}
	return nil
}

469
// SetRevocationUpdates retrieves the latest revocation records from the database, and attaches
470 471
// them to the request, for each credential type for which a nonrevocation proof is requested in
// b.Revocation.
472
func (rs *RevocationStorage) SetRevocationUpdates(b *BaseRequest) error {
473 474
	if len(b.Revocation) == 0 {
		return nil
475
	}
476
	var err error
477
	b.RevocationUpdates = make(map[CredentialTypeIdentifier]*revocation.Update, len(b.Revocation))
478
	for _, credid := range b.Revocation {
479 480 481
		if !rs.conf.CredentialTypes[credid].SupportsRevocation() {
			return errors.Errorf("cannot request nonrevocation proof for %s: revocation not enabled in scheme")
		}
Sietse Ringers's avatar
Sietse Ringers committed
482
		if err = rs.UpdateIfOld(credid); err != nil {
483 484 485 486 487 488 489 490 491 492 493 494
			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
			}
495
		}
496
		b.RevocationUpdates[credid], err = rs.UpdateLatest(credid, revocationUpdateCount)
497 498 499
		if err != nil {
			return err
		}
500
	}
501 502 503 504 505 506
	return nil
}

func (rs *RevocationStorage) getSettings(typ CredentialTypeIdentifier) *RevocationSetting {
	if rs.settings[typ] == nil {
		rs.settings[typ] = &RevocationSetting{}
507
	}
508 509 510 511 512
	s := rs.settings[typ]
	if s.MaxNonrevocationDuration == 0 {
		s.MaxNonrevocationDuration = revocationMaxAccumulatorAge
	}
	return s
513 514
}

515
func (client RevocationClient) PostUpdate(typ CredentialTypeIdentifier, urls []string, update *revocation.Update) {
516
	transport := NewHTTPTransport("")
517
	transport.Binary = true
518
	for _, url := range urls {
519 520
		err := transport.Post(fmt.Sprintf("%s/revocation/update/%s", url, typ.String()), nil, update)
		if err != nil {
521 522
			Logger.Warn("error sending revocation update", err)
		}
523
	}
524 525
}

526 527 528 529 530
func (client RevocationClient) PostIssuanceRecord(typ CredentialTypeIdentifier, sk *revocation.PrivateKey, rec *IssuanceRecord, url string) error {
	message, err := signed.MarshalSign(sk.ECDSA, rec)
	if err != nil {
		return err
	}
531
	return NewHTTPTransport(url).Post(
532
		fmt.Sprintf("revocation/issuancerecord/%s/%d", typ, sk.Counter), nil, []byte(message),
533 534 535 536
	)
}

// FetchRevocationRecords gets revocation update messages from the revocation server, of the specified index and greater.
537 538
func (client RevocationClient) FetchUpdateFrom(typ CredentialTypeIdentifier, index uint64) (*revocation.Update, error) {
	return client.fetchUpdate(typ, "updatefrom", index)
539
}
540

541 542 543 544 545
func (client RevocationClient) FetchUpdateLatest(typ CredentialTypeIdentifier, count uint64) (*revocation.Update, error) {
	return client.fetchUpdate(typ, "updatelatest", count)
}

func (client RevocationClient) fetchUpdate(typ CredentialTypeIdentifier, u string, i uint64) (*revocation.Update, error) {
546 547 548 549 550 551 552
	var (
		err       error
		errs      multierror.Error
		update    = &revocation.Update{}
		transport = NewHTTPTransport("")
	)
	transport.Binary = true
553 554
	for _, url := range client.Conf.CredentialTypes[typ].RevocationServers {
		transport.Server = url
555
		err = transport.Get(fmt.Sprintf("revocation/%s/%s/%d", u, typ, i), &update)
556
		if err == nil {
557
			return update, nil
558 559 560
		} else {
			errs.Errors = append(errs.Errors, err)
		}
561
	}
562
	return nil, errors.WrapPrefix(errs, "failed to download revocation update", 0)
563 564
}

565 566
func (rs RevocationKeys) PrivateKey(issid IssuerIdentifier) (*revocation.PrivateKey, error) {
	sk, err := rs.Conf.PrivateKey(issid)
567
	if err != nil {
568
		return nil, err
569 570
	}
	if sk == nil {
571
		return nil, errors.Errorf("unknown private key: %s", issid)
572
	}
573
	revsk, err := sk.RevocationKey()
574
	if err != nil {
575
		return nil, err
576
	}
577 578
	return revsk, nil
}
579

580 581
func (rs RevocationKeys) PublicKey(issid IssuerIdentifier, counter uint) (*revocation.PublicKey, error) {
	pk, err := rs.Conf.PublicKey(issid, int(counter))
582
	if err != nil {
583
		return nil, err
584
	}
585 586
	if pk == nil {
		return nil, errors.Errorf("unknown public key: %s-%d", issid, counter)
587
	}
588 589 590
	revpk, err := pk.RevocationKey()
	if err != nil {
		return nil, err
591
	}
592
	return revpk, nil
593
}