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 10
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
11
	"github.com/privacybydesign/gabi/big"
12
	"github.com/privacybydesign/gabi/revocation"
13
	"github.com/privacybydesign/gabi/signed"
14 15
)

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

		Keys   RevocationKeys
		client RevocationClient
29 30
	}

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

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

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

		// 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
53 54
	}

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

Sietse Ringers's avatar
Sietse Ringers committed
59 60 61
	// 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.
62 63 64
	RevocationRecord struct {
		revocation.Record `gorm:"embedded"`
		CredType          CredentialTypeIdentifier `gorm:"primary_key"`
65 66 67 68 69 70 71 72 73
	}

	TimeRecord struct {
		Index      uint64
		Start, End int64
	}

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

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

	// 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"

95
	// RevocationModeServer indicates that this is a revocation server for a credential type.
Sietse Ringers's avatar
Sietse Ringers committed
96 97 98 99
	// 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.
100
	// In addition this mode exposes the same endpoints as RevocationModeProxy.
Sietse Ringers's avatar
Sietse Ringers committed
101 102 103 104 105
	RevocationModeServer RevocationMode = "server"

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

107 108 109
	// 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
110
	// against a 'younger' accumulator.
111
	revocationMaxAccumulatorAge uint = 5 * 60
112 113
)

114
// Revocation record methods
115

Sietse Ringers's avatar
Sietse Ringers committed
116 117 118
// 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.
119
func (rs *RevocationStorage) EnableRevocation(typ CredentialTypeIdentifier, sk *revocation.PrivateKey) error {
120 121 122 123 124 125 126 127
	hasRecords, err := rs.db.HasRecords(typ, (*RevocationRecord)(nil))
	if err != nil {
		return err
	}
	if hasRecords {
		return errors.New("revocation record table not empty")
	}

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

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

Sietse Ringers's avatar
Sietse Ringers committed
148 149 150 151 152 153 154 155 156 157 158
// 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
159 160
// at the specified index, that is, all records whose end index is greater than or equal to
// the specified index.
161 162 163
func (rs *RevocationStorage) RevocationRecords(typ CredentialTypeIdentifier, index uint64) ([]*RevocationRecord, error) {
	var records []*RevocationRecord
	return records, rs.db.From(typ, "end_index", index, &records)
164 165
}

166 167 168 169 170 171 172 173
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)
174
	}
175 176
	if len(records) == 0 {
		return nil, gorm.ErrRecordNotFound
177
	}
178
	return records, nil
179 180
}

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

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

194 195 196
	return nil
}

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

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

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

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

	return nil
}
230

231 232 233 234
// Issuance records

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

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

241 242 243 244 245
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
246
	}
247 248
	return &r, nil
}
249

250 251
// Revocation methods

Sietse Ringers's avatar
Sietse Ringers committed
252 253
// 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,
254
// and updating the revocation database on disk.
255
func (rs *RevocationStorage) Revoke(typ CredentialTypeIdentifier, key string, sk *revocation.PrivateKey) error {
Sietse Ringers's avatar
Sietse Ringers committed
256 257
	if rs.getSettings(typ).Mode != RevocationModeServer {
		return errors.Errorf("cannot revoke %s", typ)
258
	}
259

260 261 262 263 264 265 266 267 268 269
	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
		}
270
		return rs.revokeAttr(tx, typ, sk, cr.Attr)
271 272 273
	})
}

274 275 276 277 278 279 280 281 282 283
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)
284 285 286
	if err != nil {
		return err
	}
287
	update := &revocation.AccumulatorUpdate{
288 289 290 291 292 293 294 295 296
		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
	}
297 298
	record := &RevocationRecord{
		Record: revocation.Record{
299 300 301 302
			StartIndex:     newAcc.Index,
			EndIndex:       newAcc.Index,
			PublicKeyIndex: sk.Counter,
			Message:        updateMsg,
303
		},
304
		CredType: typ,
305
	}
306
	if err = rs.addRevocationRecord(tx, record, true); err != nil {
307 308 309 310 311
		return err
	}
	return nil
}

312
// Accumulator methods
313

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

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

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

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

348 349 350 351
// Methods to update from remote revocation server

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

	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
363 364
}

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

376 377 378 379
// 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 {
	// Just store it if we are the revocation server for this credential type
380 381
	settings := rs.getSettings(typ)
	if settings.Mode == RevocationModeServer {
382
		return rs.AddIssuanceRecord(rec)
383 384
	}

385
	// We have to send it, sign it first
386 387 388
	if settings.ServerURL == "" {
		return errors.New("cannot send issuance record: no server_url configured")
	}
389 390 391 392
	credtype := rs.conf.CredentialTypes[typ]
	if credtype == nil {
		return errors.New("unknown credential type")
	}
393 394
	if !credtype.SupportsRevocation() {
		return errors.New("cannot send issuance record: credential type does not support revocation")
395 396
	}
	sk, err := rs.Keys.PrivateKey(typ.IssuerIdentifier())
397 398 399
	if err != nil {
		return err
	}
400
	message, err := signed.MarshalSign(sk.ECDSA, rec)
401 402 403
	if err != nil {
		return err
	}
404

405
	return rs.client.PostIssuanceRecord(typ, sk.Counter, message, settings.ServerURL)
406 407
}

408 409 410
// Misscelaneous methods

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

413 414
	for typ, s := range settings {
		switch s.Mode {
415 416 417 418
		case RevocationModeServer:
			if s.ServerURL != "" {
				return errors.New("server_url cannot be combined with server mode")
			}
419
			t = &typ
420 421 422
		case RevocationModeProxy:
			t = &typ
		case RevocationModeRequestor: // noop
423
		default:
424 425
			return errors.Errorf(`invalid revocation mode "%s" for %s (supported: "%s", "%s", "%s")`,
				s.Mode, typ, RevocationModeRequestor, RevocationModeServer, RevocationModeProxy)
426
		}
427
	}
428 429 430
	if t != nil && connstr == "" {
		return errors.Errorf("revocation mode for %s requires SQL database but no connection string given", *t)
	}
431 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")
		db, err := newSqlStorage(debug, connstr)
439
		if err != nil {
440
			return err
441
		}
442 443
		rs.db = db
		rs.sqlMode = true
444
	}
445 446 447 448 449
	if settings != nil {
		rs.settings = settings
	} else {
		rs.settings = map[CredentialTypeIdentifier]*RevocationSetting{}
	}
450 451 452 453 454 455
	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)
		}
	}
456 457 458
	rs.client = RevocationClient{Conf: rs.conf}
	rs.Keys = RevocationKeys{Conf: rs.conf}
	return nil
459 460
}

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

468 469 470 471 472 473
// 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
474
	}
475 476 477
	var err error
	b.RevocationUpdates = make(map[CredentialTypeIdentifier][]*RevocationRecord, len(b.Revocation))
	for _, credid := range b.Revocation {
478 479 480
		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
481
		if err = rs.UpdateIfOld(credid); err != nil {
482 483 484 485 486 487 488 489 490 491 492 493
			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
			}
494 495 496 497 498
		}
		b.RevocationUpdates[credid], err = rs.LatestRevocationRecords(credid, revocationUpdateCount)
		if err != nil {
			return err
		}
499
	}
500 501 502 503 504 505
	return nil
}

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

514
func (RevocationClient) PostRevocationRecords(urls []string, records []*RevocationRecord) {
515
	transport := NewHTTPTransport("")
516
	for _, url := range urls {
517
		if err := transport.Post(url+"/revocation/records", nil, &records); err != nil {
518 519
			Logger.Warn("error sending revocation update", err)
		}
520
	}
521 522
}

523 524
func (client RevocationClient) PostIssuanceRecord(typ CredentialTypeIdentifier, counter uint, message signed.Message, url string) error {
	return NewHTTPTransport(url).Post(
525
		fmt.Sprintf("revocation/issuancerecord/%s/%d", typ, counter), nil, []byte(message),
526 527 528 529 530 531
	)
}

// 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
532 533 534 535 536
	var err error
	var errs multierror.Error
	transport := NewHTTPTransport("")
	for _, url := range client.Conf.CredentialTypes[typ].RevocationServers {
		transport.Server = url
537
		err = transport.Get(fmt.Sprintf("revocation/records/%s/%d", typ, index), &records)
538 539 540 541 542
		if err == nil {
			return records, nil
		} else {
			errs.Errors = append(errs.Errors, err)
		}
543
	}
544
	return nil, errors.WrapPrefix(errs, "failed to download revocation records", 0)
545
}
546

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

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

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