irmaconfig.go 41.7 KB
Newer Older
1
package irma
2
3

import (
4
	"crypto/rsa"
5
6
7
8
9
	"encoding/base64"
	"encoding/xml"
	"io/ioutil"
	"os"
	"path/filepath"
10
	"reflect"
11
	"regexp"
12
	"strconv"
13
	"time"
14

15
16
	"crypto/sha256"

17
18
19
20
	"fmt"

	"strings"

21
22
23
24
25
26
27
28
29
30
	"sort"

	"bytes"

	"encoding/hex"

	"crypto/ecdsa"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
31
	gobig "math/big"
32

33
	"github.com/dgrijalva/jwt-go"
34
	"github.com/go-errors/errors"
35
36
	"github.com/privacybydesign/gabi"
	"github.com/privacybydesign/gabi/big"
37
	"github.com/privacybydesign/irmago/internal/fs"
38
39
)

40
// Configuration keeps track of scheme managers, issuers, credential types and public keys,
41
// dezerializing them from an irma_configuration folder, and downloads and saves new ones on demand.
42
type Configuration struct {
43
44
45
	SchemeManagers  map[SchemeManagerIdentifier]*SchemeManager
	Issuers         map[IssuerIdentifier]*Issuer
	CredentialTypes map[CredentialTypeIdentifier]*CredentialType
46
	AttributeTypes  map[AttributeTypeIdentifier]*AttributeType
47

Sietse Ringers's avatar
Sietse Ringers committed
48
49
50
	// Path to the irma_configuration folder that this instance represents
	Path string

51
52
53
	// DisabledSchemeManagers keeps track of scheme managers that did not parse  succesfully
	// (i.e., invalid signature, parsing error), and the problem that occurred when parsing them
	DisabledSchemeManagers map[SchemeManagerIdentifier]*SchemeManagerError
54

55
56
	Warnings []string

57
	kssPublicKeys map[SchemeManagerIdentifier]map[int]*rsa.PublicKey
58
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
59
	privateKeys   map[IssuerIdentifier]*gabi.PrivateKey
60
	reverseHashes map[string]CredentialTypeIdentifier
61
	initialized   bool
62
	assets        string
63
	readOnly      bool
64
65
}

Sietse Ringers's avatar
Sietse Ringers committed
66
67
// ConfigurationFileHash encodes the SHA256 hash of an authenticated
// file under a scheme manager within the configuration folder.
68
69
type ConfigurationFileHash []byte

Sietse Ringers's avatar
Sietse Ringers committed
70
71
// SchemeManagerIndex is a (signed) list of files under a scheme manager
// along with their SHA266 hash
72
73
type SchemeManagerIndex map[string]ConfigurationFileHash

74
75
type SchemeManagerStatus string

76
77
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
78
	Status  SchemeManagerStatus
79
80
81
	Err     error
}

82
83
84
85
86
87
88
const (
	SchemeManagerStatusValid               = SchemeManagerStatus("Valid")
	SchemeManagerStatusUnprocessed         = SchemeManagerStatus("Unprocessed")
	SchemeManagerStatusInvalidIndex        = SchemeManagerStatus("InvalidIndex")
	SchemeManagerStatusInvalidSignature    = SchemeManagerStatus("InvalidSignature")
	SchemeManagerStatusParsingError        = SchemeManagerStatus("ParsingError")
	SchemeManagerStatusContentParsingError = SchemeManagerStatus("ContentParsingError")
89

90
91
	pubkeyPattern  = "%s/%s/%s/PublicKeys/*.xml"
	privkeyPattern = "%s/%s/%s/PrivateKeys/*.xml"
92
93
)

94
95
96
97
func (sme SchemeManagerError) Error() string {
	return fmt.Sprintf("Error parsing scheme manager %s: %s", sme.Manager.Name(), sme.Err.Error())
}

98
// newConfiguration returns a new configuration. After this
99
// ParseFolder() should be called to parse the specified path.
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
func NewConfiguration(path string) (*Configuration, error) {
	return newConfiguration(path, "")
}

func NewConfigurationReadOnly(path string) (*Configuration, error) {
	conf, err := newConfiguration(path, "")
	if err != nil {
		return nil, err
	}
	conf.readOnly = true
	return conf, nil
}

func NewConfigurationFromAssets(path, assets string) (*Configuration, error) {
	return newConfiguration(path, assets)
}

func newConfiguration(path string, assets string) (conf *Configuration, err error) {
118
	conf = &Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
119
		Path:   path,
120
		assets: assets,
121
	}
122

123
124
125
126
	if conf.assets != "" { // If an assets folder is specified, then it must exist
		if err = fs.AssertPathExists(conf.assets); err != nil {
			return nil, errors.WrapPrefix(err, "Nonexistent assets folder specified", 0)
		}
127
	}
128
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
129
130
		return nil, err
	}
131

132
133
134
	// Init all maps
	conf.clear()

135
136
137
	return
}

138
func (conf *Configuration) clear() {
139
140
141
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
142
	conf.AttributeTypes = make(map[AttributeTypeIdentifier]*AttributeType)
143
	conf.DisabledSchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManagerError)
144
	conf.kssPublicKeys = make(map[SchemeManagerIdentifier]map[int]*rsa.PublicKey)
145
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
146
	conf.privateKeys = make(map[IssuerIdentifier]*gabi.PrivateKey)
147
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
148
149
150
151
152
153
154
}

// ParseFolder populates the current Configuration by parsing the storage path,
// listing the containing scheme managers, issuers and credential types.
func (conf *Configuration) ParseFolder() (err error) {
	// Init all maps
	conf.clear()
155

156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
	// Copy any new or updated scheme managers out of the assets into storage
	if conf.assets != "" {
		err = iterateSubfolders(conf.assets, func(dir string) error {
			scheme := NewSchemeManagerIdentifier(filepath.Base(dir))
			uptodate, err := conf.isUpToDate(scheme)
			if err != nil {
				return err
			}
			if !uptodate {
				_, err = conf.CopyManagerFromAssets(scheme)
			}
			return err
		})
		if err != nil {
			return err
		}
	}

	// Parse scheme managers in storage
175
	var mgrerr *SchemeManagerError
Sietse Ringers's avatar
Sietse Ringers committed
176
	err = iterateSubfolders(conf.Path, func(dir string) error {
177
178
		manager := NewSchemeManager(filepath.Base(dir))
		err := conf.ParseSchemeManagerFolder(dir, manager)
179
180
		if err == nil {
			return nil // OK, do next scheme manager folder
181
		}
182
183
184
185
		// If there is an error, and it is of type SchemeManagerError, return nil
		// so as to continue parsing other managers.
		var ok bool
		if mgrerr, ok = err.(*SchemeManagerError); ok {
186
			conf.DisabledSchemeManagers[manager.Identifier()] = mgrerr
187
			return nil
188
		}
189
		return err // Not a SchemeManagerError? return it & halt parsing now
190
191
	})
	if err != nil {
192
		return
193
	}
194
	conf.initialized = true
195
196
197
	if mgrerr != nil {
		return mgrerr
	}
198
	return
199
200
}

201
202
203
204
205
206
207
// ParseOrRestoreFolder parses the irma_configuration folder, and when possible attempts to restore
// any broken scheme managers from their remote.
// Any error encountered during parsing is considered recoverable only if it is of type *SchemeManagerError;
// In this case the scheme in which it occured is downloaded from its remote and re-parsed.
// If any other error is encountered at any time, it is returned immediately.
// If no error is returned, parsing and possibly restoring has been succesfull, and there should be no
// disabled scheme managers.
208
209
func (conf *Configuration) ParseOrRestoreFolder() error {
	err := conf.ParseFolder()
210
211
212
	// Only in case of a *SchemeManagerError might we be able to recover
	if _, isSchemeMgrErr := err.(*SchemeManagerError); !isSchemeMgrErr {
		return err
213
	}
214
215
216
	if err != nil && (conf.assets == "" || conf.readOnly) {
		return err
	}
217
218

	for id := range conf.DisabledSchemeManagers {
219
220
221
222
223
224
225
226
227
		if err = conf.ReinstallSchemeManager(conf.SchemeManagers[id]); err == nil {
			continue
		}
		if _, err = conf.CopyManagerFromAssets(id); err != nil {
			return err // File system error, too serious, bail out now
		}
		name := id.String()
		if err = conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), NewSchemeManager(name)); err == nil {
			delete(conf.DisabledSchemeManagers, id)
228
		}
229
	}
230

231
232
233
	return err
}

234
// ParseSchemeManagerFolder parses the entire tree of the specified scheme manager
235
// If err != nil then a problem occured
236
func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
237
238
239
240
241
242
243
	// From this point, keep it in our map even if it has an error. The user must check either:
	// - manager.Status == SchemeManagerStatusValid, aka "VALID"
	// - or equivalently, manager.Valid == true
	// before using any scheme manager for anything, and handle accordingly
	conf.SchemeManagers[manager.Identifier()] = manager

	// Ensure we return a SchemeManagerError when any error occurs
244
245
	defer func() {
		if err != nil {
246
247
248
249
250
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
251
252
253
		}
	}()

254
255
	// Verify signature and read scheme manager description
	if err = conf.VerifySignature(manager.Identifier()); err != nil {
256
257
		return
	}
258
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
259
260
		manager.Status = SchemeManagerStatusInvalidIndex
		return
261
	}
262
263
264
265
266
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
267
268
269
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
270
	}
271
272
	if err = conf.checkScheme(manager, dir); err != nil {
		return
273
	}
274
275
276
277
278
279
280

	// Verify that all other files are validly signed
	err = conf.VerifySchemeManager(manager)
	if err != nil {
		manager.Status = SchemeManagerStatusInvalidSignature
		return
	}
281

282
	// Read timestamp indicating time of last modification
283
284
285
	ts, exists, err := readTimestamp(dir + "/timestamp")
	if err != nil || !exists {
		return errors.WrapPrefix(err, "Could not read scheme manager timestamp", 0)
286
	}
287
	manager.Timestamp = *ts
288

289
	// Parse contained issuers and credential types
290
	err = conf.parseIssuerFolders(manager, dir)
291
292
293
294
295
296
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
297
298
299
	return
}

300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// relativePath returns, given a outer path that contains the inner path,
// the relative path between outer an inner, which is such that
// outer/returnvalue refers to inner.
func relativePath(outer string, inner string) (string, error) {
	// Take Abs() of both paths to ensure that we don't fail on e.g.
	// outer = "./foo" and inner = "foo/bar"
	outerAbs, err := filepath.Abs(outer)
	if err != nil {
		return "", err
	}
	innerAbs, err := filepath.Abs(inner)
	if err != nil {
		return "", err
	}
	if !strings.HasPrefix(innerAbs, outerAbs) {
		return "", errors.New("inner path is not contained in outer path")
	}

	return innerAbs[len(outerAbs)+1:], nil
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
// PrivateKey returns the specified private key, or nil if not present in the Configuration.
func (conf *Configuration) PrivateKey(id IssuerIdentifier) (*gabi.PrivateKey, error) {
	if sk := conf.privateKeys[id]; sk != nil {
		return sk, nil
	}

	path := fmt.Sprintf(privkeyPattern, conf.Path, id.SchemeManagerIdentifier().Name(), id.Name())
	files, err := filepath.Glob(path)
	if err != nil {
		return nil, err
	}
	if len(files) == 0 {
		return nil, nil
	}

	// List private keys and get highest counter
	counters := make([]int, 0, len(files))
	for _, file := range files {
		filename := filepath.Base(file)
		count := filename[:len(filename)-4]
		i, err := strconv.Atoi(count)
		if err != nil {
			return nil, err
		}
		counters = append(counters, i)
	}
	sort.Ints(counters)
	counter := counters[len(counters)-1]

	// Read private key
	file := strings.Replace(path, "*", strconv.Itoa(counter), 1)
	sk, err := gabi.NewPrivateKeyFromFile(file)
	if err != nil {
		return nil, err
	}
	if int(sk.Counter) != counter {
		return nil, errors.Errorf("Private key %s of issuer %s has wrong <Counter>", file, id.String())
	}
	conf.privateKeys[id] = sk

	return sk, nil
}

364
365
// PublicKey returns the specified public key, or nil if not present in the Configuration.
func (conf *Configuration) PublicKey(id IssuerIdentifier, counter int) (*gabi.PublicKey, error) {
366
367
368
369
370
371
372
373
374
375
376
	var haveIssuer, haveKey bool
	var err error
	_, haveIssuer = conf.publicKeys[id]
	if haveIssuer {
		_, haveKey = conf.publicKeys[id][counter]
	}

	// If we have not seen this issuer or key before in conf.publicKeys,
	// try to parse the public key folder; new keys might have been put there since we last parsed it
	if !haveIssuer || !haveKey {
		if err = conf.parseKeysFolder(id); err != nil {
377
			return nil, err
378
379
		}
	}
380
	return conf.publicKeys[id][counter], nil
381
382
}

383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
// KeyshareServerKeyFunc returns a function that returns the public key with which to verify a keyshare server JWT,
// suitable for passing to jwt.Parse() and jwt.ParseWithClaims().
func (conf *Configuration) KeyshareServerKeyFunc(scheme SchemeManagerIdentifier) func(t *jwt.Token) (interface{}, error) {
	return func(t *jwt.Token) (i interface{}, e error) {
		var kid int
		if kidstr, ok := t.Header["kid"].(string); ok {
			var err error
			if kid, err = strconv.Atoi(kidstr); err != nil {
				return nil, err
			}
		}
		return conf.KeyshareServerPublicKey(scheme, kid)
	}
}

// KeyshareServerPublicKey returns the i'th public key of the specified scheme.
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
func (conf *Configuration) KeyshareServerPublicKey(scheme SchemeManagerIdentifier, i int) (*rsa.PublicKey, error) {
	if _, contains := conf.kssPublicKeys[scheme]; !contains {
		conf.kssPublicKeys[scheme] = make(map[int]*rsa.PublicKey)
	}
	if _, contains := conf.kssPublicKeys[scheme][i]; !contains {
		pkbts, err := ioutil.ReadFile(filepath.Join(conf.Path, scheme.Name(), fmt.Sprintf("kss-%d.pem", i)))
		if err != nil {
			return nil, err
		}
		pkblk, _ := pem.Decode(pkbts)
		genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
		if err != nil {
			return nil, err
		}
		pk, ok := genericPk.(*rsa.PublicKey)
		if !ok {
			return nil, errors.New("Invalid keyshare server public key")
		}
		conf.kssPublicKeys[scheme][i] = pk
	}
	return conf.kssPublicKeys[scheme][i], nil
}

422
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
423
	hash := sha256.Sum256([]byte(credid.String()))
424
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
425
426
}

427
428
429
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
430
431
432
433
	}
	return nil
}

434
// IsInitialized indicates whether this instance has successfully been initialized.
435
436
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
437
438
}

439
440
441
442
443
444
445
446
447
// Prune removes any invalid scheme managers and everything they own from this Configuration
func (conf *Configuration) Prune() {
	for _, manager := range conf.SchemeManagers {
		if !manager.Valid {
			_ = conf.RemoveSchemeManager(manager.Identifier(), false) // does not return errors
		}
	}
}

448
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
449
450
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
451
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
452
453
454
		if err != nil {
			return err
		}
455
456
		if !exists {
			return nil
457
		}
458
459
460
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
461

462
		if err = conf.checkIssuer(manager, issuer, dir); err != nil {
463
464
465
			return err
		}

466
		conf.Issuers[issuer.Identifier()] = issuer
467
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
468
		return conf.parseCredentialsFolder(manager, issuer, dir+"/Issues/")
469
470
471
	})
}

472
473
474
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
	name := id.String()
	for iss := range conf.Issuers {
		if iss.Root() == name {
			delete(conf.Issuers, iss)
		}
	}
	for iss := range conf.publicKeys {
		if iss.Root() == name {
			delete(conf.publicKeys, iss)
		}
	}
	for cred := range conf.CredentialTypes {
		if cred.Root() == name {
			delete(conf.CredentialTypes, cred)
		}
	}
491
492
493
494
	if !conf.readOnly {
		return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
	}
	return nil
495
496
}

497
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
498
499
func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
	manager := conf.SchemeManagers[issuerid.SchemeManagerIdentifier()]
500
	conf.publicKeys[issuerid] = map[int]*gabi.PublicKey{}
501
	path := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
502
503
504
505
506
507
508
509
510
511
	files, err := filepath.Glob(path)
	if err != nil {
		return err
	}

	for _, file := range files {
		filename := filepath.Base(file)
		count := filename[:len(filename)-4]
		i, err := strconv.Atoi(count)
		if err != nil {
512
			return err
513
		}
514
515
516
517
518
		relativepath, err := relativePath(conf.Path, file)
		if err != nil {
			return err
		}
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativepath)
519
		if err != nil || !found {
520
521
522
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
523
524
525
		if err != nil {
			return err
		}
526
527
528
		if int(pk.Counter) != i {
			return errors.Errorf("Public key %s of issuer %s has wrong <Counter>", file, issuerid.String())
		}
529
		pk.Issuer = issuerid.String()
530
		conf.publicKeys[issuerid][i] = pk
531
	}
532

533
534
535
	return nil
}

536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
func (conf *Configuration) PublicKeyIndices(issuerid IssuerIdentifier) (i []int, err error) {
	return conf.matchKeyPattern(issuerid, pubkeyPattern)
}

func (conf *Configuration) matchKeyPattern(issuerid IssuerIdentifier, pattern string) (i []int, err error) {
	pkpath := fmt.Sprintf(pattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
	files, err := filepath.Glob(pkpath)
	if err != nil {
		return
	}
	for _, file := range files {
		var count int
		base := filepath.Base(file)
		if count, err = strconv.Atoi(base[:len(base)-4]); err != nil {
			return
		}
		i = append(i, count)
	}
	sort.Ints(i)
	return
}

558
// parse $schememanager/$issuer/Issues/*/description.xml
559
560
561
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, issuer *Issuer, path string) error {
	var foundcred bool
	err := iterateSubfolders(path, func(dir string) error {
562
		cred := &CredentialType{}
563
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
564
565
566
		if err != nil {
			return err
		}
567
568
569
		if !exists {
			return nil
		}
570
		if err = conf.checkCredentialType(manager, issuer, cred, dir); err != nil {
571
572
573
574
			return err
		}
		foundcred = true
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
575
		credid := cred.Identifier()
576
577
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
578
		for index, attr := range cred.AttributeTypes {
579
580
581
582
			attr.Index = index
			attr.SchemeManagerID = cred.SchemeManagerID
			attr.IssuerID = cred.IssuerID
			attr.CredentialTypeID = cred.ID
583
			conf.AttributeTypes[attr.GetAttributeTypeIdentifier()] = attr
584
		}
585
586
		return nil
	})
587
588
589
590
591
592
	if !foundcred {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no credential types", issuer.Identifier().String()))
	}
	return err
}

593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
// iterateSubfolders iterates over the subfolders of the specified path,
// calling the specified handler each time. If anything goes wrong, or
// if the caller returns a non-nil error, an error is immediately returned.
func iterateSubfolders(path string, handler func(string) error) error {
	dirs, err := filepath.Glob(path + "/*")
	if err != nil {
		return err
	}

	for _, dir := range dirs {
		stat, err := os.Stat(dir)
		if err != nil {
			return err
		}
		if !stat.IsDir() {
			continue
		}
610
611
612
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
613
614
615
616
617
618
619
620
621
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

622
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
623
624
625
626
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

627
628
629
630
631
	relativepath, err := relativePath(conf.Path, path)
	if err != nil {
		return false, err
	}
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativepath)
632
633
634
	if !found {
		return false, nil
	}
635
636
637
638
	if err != nil {
		return true, err
	}

639
	err = xml.Unmarshal(bts, description)
640
641
642
643
644
645
	if err != nil {
		return true, err
	}

	return true, nil
}
646

647
648
649
650
651
// Contains checks if the configuration contains the specified credential type.
func (conf *Configuration) Contains(cred CredentialTypeIdentifier) bool {
	return conf.SchemeManagers[cred.IssuerIdentifier().SchemeManagerIdentifier()] != nil &&
		conf.Issuers[cred.IssuerIdentifier()] != nil &&
		conf.CredentialTypes[cred] != nil
652
}
653

654
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
655
	if conf.assets == "" || conf.readOnly {
656
657
		return true, nil
	}
658
659
660
661
	name := scheme.String()
	newTime, exists, err := readTimestamp(filepath.Join(conf.assets, name, "timestamp"))
	if err != nil || !exists {
		return true, errors.WrapPrefix(err, "Could not read asset timestamp of scheme "+name, 0)
662
	}
663
664
665
666
	// The storage version of the manager does not need to have a timestamp. If it does not, it is outdated.
	oldTime, exists, err := readTimestamp(filepath.Join(conf.Path, name, "timestamp"))
	if err != nil {
		return true, err
667
	}
668
	return exists && !newTime.After(*oldTime), nil
669
670
}

671
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
672
	if conf.assets == "" || conf.readOnly {
673
		return false, nil
674
	}
675
676
677
678
679
	// Remove old version; we want an exact copy of the assets version
	// not a merge of the assets version and the storage version
	name := scheme.String()
	if err := os.RemoveAll(filepath.Join(conf.Path, name)); err != nil {
		return false, err
680
	}
681
682
683
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
684
685
686
	)
}

687
688
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
689
func DownloadSchemeManager(url string) (*SchemeManager, error) {
690
691
692
693
694
695
696
697
698
	if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
		url = "https://" + url
	}
	if url[len(url)-1] == '/' {
		url = url[:len(url)-1]
	}
	if strings.HasSuffix(url, "/description.xml") {
		url = url[:len(url)-len("/description.xml")]
	}
699
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
700
701
702
	if err != nil {
		return nil, err
	}
703
	manager := NewSchemeManager("")
704
705
706
707
708
709
710
711
	if err = xml.Unmarshal(b, manager); err != nil {
		return nil, err
	}

	manager.URL = url // TODO?
	return manager, nil
}

Sietse Ringers's avatar
Sietse Ringers committed
712
713
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
714
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
715
	// Remove everything falling under the manager's responsibility
716
	for credid := range conf.CredentialTypes {
717
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
718
			delete(conf.CredentialTypes, credid)
719
720
		}
	}
721
	for issid := range conf.Issuers {
722
		if issid.SchemeManagerIdentifier() == id {
723
			delete(conf.Issuers, issid)
724
725
		}
	}
726
	for issid := range conf.publicKeys {
727
		if issid.SchemeManagerIdentifier() == id {
728
			delete(conf.publicKeys, issid)
729
730
		}
	}
731
	delete(conf.SchemeManagers, id)
732

733
	if fromStorage || !conf.readOnly {
Sietse Ringers's avatar
Sietse Ringers committed
734
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
735
736
	}
	return nil
737
738
}

739
func (conf *Configuration) ReinstallSchemeManager(manager *SchemeManager) (err error) {
740
741
742
743
	if conf.readOnly {
		return errors.New("cannot install scheme into a read-only configuration")
	}

744
745
746
747
748
749
750
751
752
	// Check if downloading stuff from the remote works before we uninstall the specified manager:
	// If we can't download anything we should keep the broken version
	manager, err = DownloadSchemeManager(manager.URL)
	if err != nil {
		return
	}
	if err = conf.DeleteSchemeManager(manager.Identifier()); err != nil {
		return
	}
753
	err = conf.InstallSchemeManager(manager, nil)
754
755
756
	return
}

757
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
758
// provided its signature is valid.
759
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager, publickey []byte) error {
760
761
762
763
	if conf.readOnly {
		return errors.New("cannot install scheme into a read-only configuration")
	}

764
	name := manager.ID
765
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
766
767
		return err
	}
768
769

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
770
	path := fmt.Sprintf("%s/%s", conf.Path, name)
771
772
773
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
774
775
776
777
778
779
780
781
	if publickey != nil {
		if err := fs.SaveFile(path+"/pk.pem", publickey); err != nil {
			return err
		}
	} else {
		if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
			return err
		}
782
	}
783
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
784
785
		return err
	}
786
787
788
789
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
790

791
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
792
793
794
795
796
}

// DownloadSchemeManagerSignature downloads, stores and verifies the latest version
// of the index file and signature of the specified manager.
func (conf *Configuration) DownloadSchemeManagerSignature(manager *SchemeManager) (err error) {
797
798
799
800
	if conf.readOnly {
		return errors.New("cannot download into a read-only configuration")
	}

801
	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
802
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
803
804
805
806
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
807
		return
808
809
	}
	if err = t.GetFile("index.sig", sig); err != nil {
810
		return
811
	}
812
	err = conf.VerifySignature(manager.Identifier())
813
	return
814
}
815

Sietse Ringers's avatar
Sietse Ringers committed
816
817
818
// Download downloads the issuers, credential types and public keys specified in set
// if the current Configuration does not already have them,  and checks their authenticity
// using the scheme manager index.
819
func (conf *Configuration) Download(session SessionRequest) (downloaded *IrmaIdentifierSet, err error) {
820
821
822
	if conf.readOnly {
		return nil, errors.New("cannot download into a read-only configuration")
	}
823
	managers := make(map[string]struct{}) // Managers that we must update
824
	downloaded = &IrmaIdentifierSet{
825
826
827
828
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
829

830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
	// Calculate which scheme managers must be updated
	if err = conf.checkIssuers(session.Identifiers(), managers); err != nil {
		return
	}
	if err = conf.checkCredentialTypes(session, managers); err != nil {
		return
	}

	// Update the scheme managers found above and parse them, if necessary
	for id := range managers {
		if err = conf.UpdateSchemeManager(NewSchemeManagerIdentifier(id), downloaded); err != nil {
			return
		}
	}
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
	return
}

850
func (conf *Configuration) checkCredentialTypes(session SessionRequest, managers map[string]struct{}) error {
851
852
853
854
855
856
857
858
	var disjunctions AttributeDisjunctionList
	var typ *CredentialType
	var contains bool

	switch s := session.(type) {
	case *IssuanceRequest:
		for _, credreq := range s.Credentials {
			// First check if we have this credential type
859
			typ, contains = conf.CredentialTypes[credreq.CredentialTypeID]
860
861
862
863
864
865
866
867
868
869
			if !contains {
				managers[credreq.CredentialTypeID.Root()] = struct{}{}
				continue
			}
			newAttrs := make(map[string]string)
			for k, v := range credreq.Attributes {
				newAttrs[k] = v
			}
			// For each of the attributes in the credentialtype, see if it is present; if so remove it from newAttrs
			// If not, check that it is optional; if not the credentialtype must be updated
870
			for _, attrtyp := range typ.AttributeTypes {
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
				_, contains = newAttrs[attrtyp.ID]
				if !contains && !attrtyp.IsOptional() {
					managers[credreq.CredentialTypeID.Root()] = struct{}{}
					break
				}
				delete(newAttrs, attrtyp.ID)
			}
			// If there is anything left in newAttrs, then these are attributes that are not in the credentialtype
			if len(newAttrs) > 0 {
				managers[credreq.CredentialTypeID.Root()] = struct{}{}
			}
		}
		disjunctions = s.Disclose
	case *DisclosureRequest:
		disjunctions = s.Content
	case *SignatureRequest:
		disjunctions = s.Content
	}

	for _, disjunction := range disjunctions {
		for _, attrid := range disjunction.Attributes {
			credid := attrid.CredentialTypeIdentifier()
			if typ, contains = conf.CredentialTypes[credid]; !contains {
				managers[credid.Root()] = struct{}{}
895
				continue
896
			}
Sietse Ringers's avatar
Sietse Ringers committed
897
			if !attrid.IsCredential() && !typ.ContainsAttribute(attrid) {
898
899
900
901
902
903
904
905
906
				managers[credid.Root()] = struct{}{}
			}
		}
	}

	return nil
}

func (conf *Configuration) checkIssuers(set *IrmaIdentifierSet, managers map[string]struct{}) error {
907
	for issid := range set.Issuers {
908
		if _, contains := conf.Issuers[issid]; !contains {
909
			managers[issid.Root()] = struct{}{}
910
		}
Sietse Ringers's avatar
Sietse Ringers committed
911
	}
912
913
914
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
915
			if err != nil {
916
				return err
Sietse Ringers's avatar
Sietse Ringers committed
917
918
			}
			if pk == nil {
919
				managers[issid.Root()] = struct{}{}
920
921
922
			}
		}
	}
923
	return nil
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
}

func (i SchemeManagerIndex) String() string {
	var paths []string
	var b bytes.Buffer

	for path := range i {
		paths = append(paths, path)
	}
	sort.Strings(paths)

	for _, path := range paths {
		b.WriteString(hex.EncodeToString(i[path]))
		b.WriteString(" ")
		b.WriteString(path)
		b.WriteString("\n")
	}

	return b.String()
}

Sietse Ringers's avatar
Sietse Ringers committed
945
// FromString populates this index by parsing the specified string.
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
func (i SchemeManagerIndex) FromString(s string) error {
	for j, line := range strings.Split(s, "\n") {
		if len(line) == 0 {
			continue
		}
		parts := strings.Split(line, " ")
		if len(parts) != 2 {
			return errors.Errorf("Scheme manager index line %d has incorrect amount of parts", j)
		}
		hash, err := hex.DecodeString(parts[0])
		if err != nil {
			return err
		}
		i[parts[1]] = hash
	}

	return nil
}

965
// parseIndex parses the index file of the specified manager.
966
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
967
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
968
	if err := fs.AssertPathExists(path); err != nil {
969
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
970
	}
Sietse Ringers's avatar
Sietse Ringers committed
971
	indexbts, err := ioutil.ReadFile(path)
972
	if err != nil {
973
		return nil, err
974
	}
975
976
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
977
978
}

979
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
980
	err := conf.VerifySignature(manager.Identifier())
981
982
983
984
	if err != nil {
		return err
	}

985
	var exists bool
986
	for file := range manager.index {
987
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
988
989
990
991
992
993
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
994
		// Don't care about the actual bytes
995
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
996
997
998
999
1000
1001
1002
			return err
		}
	}

	return nil
}

1003
1004
1005
// ReadAuthenticatedFile reads the file at the specified path
// and verifies its authenticity by checking that the file hash
// is present in the (signed) scheme manager index file.
1006
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
1007
	signedHash, ok := manager.index[filepath.ToSlash(path)]
1008
	if !ok {
1009
		return nil, false, nil
1010
1011
	}

Sietse Ringers's avatar
Sietse Ringers committed
1012
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
1013
	if err != nil {
1014
		return nil, true, err
1015
1016
1017
1018
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
1019
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
1020
	}
1021
	return bts, true, nil
1022
1023
1024
1025
1026
}

// VerifySignature verifies the signature on the scheme manager index file
// (which contains the SHA256 hashes of all files under this scheme manager,
// which are used for verifying file authenticity).
1027
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
	defer func() {
		if r := recover(); r != nil {
			if e, ok := r.(error); ok {
				err = errors.Errorf("Scheme manager index signature failed to verify: %s", e.Error())
			} else {
				err = errors.New("Scheme manager index signature failed to verify")
			}
		}
	}()

Sietse Ringers's avatar
Sietse Ringers committed
1038
	dir := filepath.Join(conf.Path, id.String())
1039
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
1040
		return errors.New("Missing scheme manager index file, signature, or public key")
1041
1042
1043
1044
1045
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
1046
		return err
1047
1048
1049
1050
1051
1052
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
1053
		return err
1054
	}
1055
	pk, err := ParsePemEcdsaPublicKey(pkbts)
1056
	if err != nil {
1057
		return err
1058
1059
1060
1061
1062
	}

	// Read and parse signature
	sig, err := ioutil.ReadFile(dir + "/index.sig")
	if err != nil {
1063
		return err
1064
	}
1065
	ints := make([]*gobig.Int, 0, 2)
1066
1067
1068
	_, err = asn1.Unmarshal(sig, &ints)

	// Verify signature
1069
1070
1071
1072
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
1073
}
1074

1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
func ParsePemEcdsaPublicKey(pkbts []byte) (*ecdsa.PublicKey, error) {
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
		return nil, err
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
		return nil, errors.New("Invalid scheme manager public key")
	}
	return pk, nil
}

1088
1089
1090
func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101

func (hash ConfigurationFileHash) Equal(other ConfigurationFileHash) bool {
	return bytes.Equal(hash, other)
}

// UpdateSchemeManager syncs the stored version within the irma_configuration directory
// with the remote version at the scheme manager's URL, downloading and storing
// new and modified files, according to the index files of both versions.
// It stores the identifiers of new or updated credential types or issuers in the second parameter.
// Note: any newly downloaded files are not yet parsed and inserted into conf.
func (conf *Configuration)