irmaconfig.go 37.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/go-errors/errors"
34
35
	"github.com/privacybydesign/gabi"
	"github.com/privacybydesign/gabi/big"
36
	"github.com/privacybydesign/irmago/internal/fs"
37
38
)

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

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

50
51
52
	// 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
53

54
55
	Warnings []string

56
	kssPublicKeys map[SchemeManagerIdentifier]map[int]*rsa.PublicKey
57
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
58
	reverseHashes map[string]CredentialTypeIdentifier
59
	initialized   bool
60
	assets        string
61
62
}

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

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

71
72
type SchemeManagerStatus string

73
74
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
75
	Status  SchemeManagerStatus
76
77
78
	Err     error
}

79
80
81
82
83
84
85
const (
	SchemeManagerStatusValid               = SchemeManagerStatus("Valid")
	SchemeManagerStatusUnprocessed         = SchemeManagerStatus("Unprocessed")
	SchemeManagerStatusInvalidIndex        = SchemeManagerStatus("InvalidIndex")
	SchemeManagerStatusInvalidSignature    = SchemeManagerStatus("InvalidSignature")
	SchemeManagerStatusParsingError        = SchemeManagerStatus("ParsingError")
	SchemeManagerStatusContentParsingError = SchemeManagerStatus("ContentParsingError")
86

87
88
	pubkeyPattern  = "%s/%s/%s/PublicKeys/*.xml"
	privkeyPattern = "%s/%s/%s/PrivateKeys/*.xml"
89
90
)

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

95
// NewConfiguration returns a new configuration. After this
96
// ParseFolder() should be called to parse the specified path.
97
98
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
	conf = &Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
99
		Path:   path,
100
		assets: assets,
101
	}
102

103
104
105
106
	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)
		}
107
	}
108
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
109
110
		return nil, err
	}
111

112
113
114
	// Init all maps
	conf.clear()

115
116
117
	return
}

118
func (conf *Configuration) clear() {
119
120
121
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
122
	conf.AttributeTypes = make(map[AttributeTypeIdentifier]*AttributeType)
123
	conf.DisabledSchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManagerError)
124
	conf.kssPublicKeys = make(map[SchemeManagerIdentifier]map[int]*rsa.PublicKey)
125
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
126
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
127
128
129
130
131
132
133
}

// 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()
134

135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
	// 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
154
	var mgrerr *SchemeManagerError
Sietse Ringers's avatar
Sietse Ringers committed
155
	err = iterateSubfolders(conf.Path, func(dir string) error {
156
157
		manager := NewSchemeManager(filepath.Base(dir))
		err := conf.ParseSchemeManagerFolder(dir, manager)
158
159
		if err == nil {
			return nil // OK, do next scheme manager folder
160
		}
161
162
163
164
		// 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 {
165
			conf.DisabledSchemeManagers[manager.Identifier()] = mgrerr
166
			return nil
167
		}
168
		return err // Not a SchemeManagerError? return it & halt parsing now
169
170
	})
	if err != nil {
171
		return
172
	}
173
	conf.initialized = true
174
175
176
	if mgrerr != nil {
		return mgrerr
	}
177
	return
178
179
}

180
181
182
183
184
185
186
// 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.
187
188
func (conf *Configuration) ParseOrRestoreFolder() error {
	err := conf.ParseFolder()
189
190
191
	// Only in case of a *SchemeManagerError might we be able to recover
	if _, isSchemeMgrErr := err.(*SchemeManagerError); !isSchemeMgrErr {
		return err
192
	}
193
194

	for id := range conf.DisabledSchemeManagers {
195
196
197
198
199
200
201
202
203
		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)
204
		}
205
	}
206

207
208
209
	return err
}

210
// ParseSchemeManagerFolder parses the entire tree of the specified scheme manager
211
// If err != nil then a problem occured
212
func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
213
214
215
216
217
218
219
	// 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
220
221
	defer func() {
		if err != nil {
222
223
224
225
226
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
227
228
229
		}
	}()

230
231
	// Verify signature and read scheme manager description
	if err = conf.VerifySignature(manager.Identifier()); err != nil {
232
233
		return
	}
234
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
235
236
		manager.Status = SchemeManagerStatusInvalidIndex
		return
237
	}
238
239
240
241
242
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
243
244
245
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
246
	}
247
248
	if err = conf.checkScheme(manager, dir); err != nil {
		return
249
	}
250
251
252
253
254
255
256

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

258
	// Read timestamp indicating time of last modification
259
260
261
	ts, exists, err := readTimestamp(dir + "/timestamp")
	if err != nil || !exists {
		return errors.WrapPrefix(err, "Could not read scheme manager timestamp", 0)
262
	}
263
	manager.Timestamp = *ts
264

265
	// Parse contained issuers and credential types
266
	err = conf.parseIssuerFolders(manager, dir)
267
268
269
270
271
272
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
273
274
275
	return
}

276
277
278
279
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

280
281
// 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) {
282
283
284
285
286
287
288
289
290
291
292
	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 {
293
			return nil, err
294
295
		}
	}
296
	return conf.publicKeys[id][counter], nil
297
298
}

299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
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
}

322
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
323
	hash := sha256.Sum256([]byte(credid.String()))
324
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
325
326
}

327
328
329
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
330
331
332
333
	}
	return nil
}

334
// IsInitialized indicates whether this instance has successfully been initialized.
335
336
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
337
338
}

339
340
341
342
343
344
345
346
347
// 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
		}
	}
}

348
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
349
350
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
351
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
352
353
354
		if err != nil {
			return err
		}
355
356
		if !exists {
			return nil
357
		}
358
359
360
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
361

362
		if err = conf.checkIssuer(manager, issuer, dir); err != nil {
363
364
365
			return err
		}

366
		conf.Issuers[issuer.Identifier()] = issuer
367
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
368
		return conf.parseCredentialsFolder(manager, issuer, dir+"/Issues/")
369
370
371
	})
}

372
373
374
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
	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)
		}
	}
391
392
393
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

394
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
395
396
func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
	manager := conf.SchemeManagers[issuerid.SchemeManagerIdentifier()]
397
	conf.publicKeys[issuerid] = map[int]*gabi.PublicKey{}
398
	path := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
399
400
401
402
403
404
405
406
407
408
	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 {
409
			return err
410
		}
Sietse Ringers's avatar
Sietse Ringers committed
411
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
412
		if err != nil || !found {
413
414
415
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
416
417
418
		if err != nil {
			return err
		}
419
420
421
		if int(pk.Counter) != i {
			return errors.Errorf("Public key %s of issuer %s has wrong <Counter>", file, issuerid.String())
		}
422
		pk.Issuer = issuerid.String()
423
		conf.publicKeys[issuerid][i] = pk
424
	}
425

426
427
428
	return nil
}

429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
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
}

451
// parse $schememanager/$issuer/Issues/*/description.xml
452
453
454
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, issuer *Issuer, path string) error {
	var foundcred bool
	err := iterateSubfolders(path, func(dir string) error {
455
		cred := &CredentialType{}
456
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
457
458
459
		if err != nil {
			return err
		}
460
461
462
		if !exists {
			return nil
		}
463
		if err = conf.checkCredentialType(manager, issuer, cred, dir); err != nil {
464
465
466
467
			return err
		}
		foundcred = true
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
468
		credid := cred.Identifier()
469
470
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
471
		for index, attr := range cred.AttributeTypes {
472
473
474
475
			attr.Index = index
			attr.SchemeManagerID = cred.SchemeManagerID
			attr.IssuerID = cred.IssuerID
			attr.CredentialTypeID = cred.ID
476
			conf.AttributeTypes[attr.GetAttributeTypeIdentifier()] = attr
477
		}
478
479
		return nil
	})
480
481
482
483
484
485
	if !foundcred {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no credential types", issuer.Identifier().String()))
	}
	return err
}

486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
// 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
		}
503
504
505
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
506
507
508
509
510
511
512
513
514
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

515
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
516
517
518
519
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
520
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
521
522
523
	if !found {
		return false, nil
	}
524
525
526
527
	if err != nil {
		return true, err
	}

528
	err = xml.Unmarshal(bts, description)
529
530
531
532
533
534
	if err != nil {
		return true, err
	}

	return true, nil
}
535

536
537
538
539
540
// 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
541
}
542

543
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
544
545
546
	if conf.assets == "" {
		return true, nil
	}
547
548
549
550
	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)
551
	}
552
553
554
555
	// 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
556
	}
557
	return exists && !newTime.After(*oldTime), nil
558
559
}

560
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
561
	if conf.assets == "" {
562
		return false, nil
563
	}
564
565
566
567
568
	// 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
569
	}
570
571
572
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
573
574
575
	)
}

576
577
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
578
func DownloadSchemeManager(url string) (*SchemeManager, error) {
579
580
581
582
583
584
585
586
587
	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")]
	}
588
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
589
590
591
	if err != nil {
		return nil, err
	}
592
	manager := NewSchemeManager("")
593
594
595
596
597
598
599
600
	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
601
602
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
603
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
604
	// Remove everything falling under the manager's responsibility
605
	for credid := range conf.CredentialTypes {
606
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
607
			delete(conf.CredentialTypes, credid)
608
609
		}
	}
610
	for issid := range conf.Issuers {
611
		if issid.SchemeManagerIdentifier() == id {
612
			delete(conf.Issuers, issid)
613
614
		}
	}
615
	for issid := range conf.publicKeys {
616
		if issid.SchemeManagerIdentifier() == id {
617
			delete(conf.publicKeys, issid)
618
619
		}
	}
620
	delete(conf.SchemeManagers, id)
621
622

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
623
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
624
625
	}
	return nil
626
627
}

628
629
630
631
632
633
634
635
636
637
638
639
640
641
func (conf *Configuration) ReinstallSchemeManager(manager *SchemeManager) (err error) {
	// 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
	}
	err = conf.InstallSchemeManager(manager)
	return
}

642
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
643
// provided its signature is valid.
644
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
645
	name := manager.ID
646
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
647
648
		return err
	}
649
650

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
651
	path := fmt.Sprintf("%s/%s", conf.Path, name)
652
653
654
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
655
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
656
657
		return err
	}
658
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
659
660
		return err
	}
661
662
663
664
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
665

666
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
667
668
669
670
671
672
}

// 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) {
	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
673
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
674
675
676
677
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
678
		return
679
680
	}
	if err = t.GetFile("index.sig", sig); err != nil {
681
		return
682
	}
683
	err = conf.VerifySignature(manager.Identifier())
684
	return
685
}
686

Sietse Ringers's avatar
Sietse Ringers committed
687
688
689
// 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.
690
func (conf *Configuration) Download(session SessionRequest) (downloaded *IrmaIdentifierSet, err error) {
691
	managers := make(map[string]struct{}) // Managers that we must update
692
	downloaded = &IrmaIdentifierSet{
693
694
695
696
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
697

698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
	// 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
}

718
func (conf *Configuration) checkCredentialTypes(session SessionRequest, managers map[string]struct{}) error {
719
720
721
722
723
724
725
726
	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
727
			typ, contains = conf.CredentialTypes[credreq.CredentialTypeID]
728
729
730
731
732
733
734
735
736
737
			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
738
			for _, attrtyp := range typ.AttributeTypes {
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
				_, 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{}{}
763
				continue
764
			}
Sietse Ringers's avatar
Sietse Ringers committed
765
			if !attrid.IsCredential() && !typ.ContainsAttribute(attrid) {
766
767
768
769
770
771
772
773
774
				managers[credid.Root()] = struct{}{}
			}
		}
	}

	return nil
}

func (conf *Configuration) checkIssuers(set *IrmaIdentifierSet, managers map[string]struct{}) error {
775
	for issid := range set.Issuers {
776
		if _, contains := conf.Issuers[issid]; !contains {
777
			managers[issid.Root()] = struct{}{}
778
		}
Sietse Ringers's avatar
Sietse Ringers committed
779
	}
780
781
782
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
783
			if err != nil {
784
				return err
Sietse Ringers's avatar
Sietse Ringers committed
785
786
			}
			if pk == nil {
787
				managers[issid.Root()] = struct{}{}
788
789
790
			}
		}
	}
791
	return nil
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
}

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
813
// FromString populates this index by parsing the specified string.
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
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
}

833
// parseIndex parses the index file of the specified manager.
834
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
835
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
836
	if err := fs.AssertPathExists(path); err != nil {
837
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
838
	}
Sietse Ringers's avatar
Sietse Ringers committed
839
	indexbts, err := ioutil.ReadFile(path)
840
	if err != nil {
841
		return nil, err
842
	}
843
844
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
845
846
}

847
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
848
	err := conf.VerifySignature(manager.Identifier())
849
850
851
852
	if err != nil {
		return err
	}

853
	var exists bool
854
	for file := range manager.index {
855
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
856
857
858
859
860
861
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
862
		// Don't care about the actual bytes
863
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
864
865
866
867
868
869
870
			return err
		}
	}

	return nil
}

871
872
873
// 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.
874
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
875
	signedHash, ok := manager.index[filepath.ToSlash(path)]
876
	if !ok {
877
		return nil, false, nil
878
879
	}

Sietse Ringers's avatar
Sietse Ringers committed
880
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
881
	if err != nil {
882
		return nil, true, err
883
884
885
886
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
887
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
888
	}
889
	return bts, true, nil
890
891
892
893
894
}

// 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).
895
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
896
897
898
899
900
901
902
903
904
905
	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
906
	dir := filepath.Join(conf.Path, id.String())
907
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
908
		return errors.New("Missing scheme manager index file, signature, or public key")
909
910
911
912
913
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
914
		return err
915
916
917
918
919
920
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
921
		return err
922
923
924
925
	}
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
926
		return err
927
928
929
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
930
		return errors.New("Invalid scheme manager public key")
931
932
933
934
935
	}

	// Read and parse signature
	sig, err := ioutil.ReadFile(dir + "/index.sig")
	if err != nil {
936
		return err
937
	}
938
	ints := make([]*gobig.Int, 0, 2)
939
940
941
	_, err = asn1.Unmarshal(sig, &ints)

	// Verify signature
942
943
944
945
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
946
}
947
948
949
950

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966

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) UpdateSchemeManager(id SchemeManagerIdentifier, downloaded *IrmaIdentifierSet) (err error) {
	manager, contains := conf.SchemeManagers[id]
	if !contains {
		return errors.Errorf("Cannot update unknown scheme manager %s", id)
	}

967
968
969
970
971
972
973
974
975
976
	// Check remote timestamp and see if we have to do anything
	transport := NewHTTPTransport(manager.URL + "/")
	timestampBts, err := transport.GetBytes("timestamp")
	if err != nil {
		return err
	}
	timestamp, err := parseTimestamp(timestampBts)
	if err != nil {
		return err
	}
977
	if !manager.Timestamp.Before(*timestamp) {
978
979
980
		return nil
	}

981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
	// Download the new index and its signature, and check that the new index
	// is validly signed by the new signature
	// By aborting immediately in case of error, and restoring backup versions
	// of the index and signature, we leave our stored copy of the scheme manager
	// intact.
	if err = conf.DownloadSchemeManagerSignature(manager); err != nil {
		return
	}
	newIndex, err := conf.parseIndex(manager.ID, manager)
	if err != nil {
		return
	}

	issPattern := regexp.MustCompile("(.+)/(.+)/description\\.xml")
	credPattern := regexp.MustCompile("(.+)/(.+)/Issues/(.+)/description\\.xml")

	// TODO: how to recover/fix local copy if err != nil below?
	for filename, newHash := range newIndex {
999
		path := filepath.Join(conf.Path, filename)
1000
		oldHash, known := manager.index[filename]
1001
1002
1003
1004
1005
1006
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
1007
1008
1009
1010
1011
1012
1013
1014
			continue // nothing to do, we already have this file
		}
		// Ensure that the folder in which to write the file exists
		if err = os.MkdirAll(filepath.Dir(path), 0700); err != nil {
			return err
		}
		stripped := filename[len(manager.ID)+1:] // Scheme manager URL already ends with its name
		// Download the new file, store it in our own irma_configuration folder
1015
		if err = transport.GetSignedFile(stripped, path, newHash); err != nil {
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
			return
		}
		// See if the file is a credential type or issuer, and add it to the downloaded set if so
		if downloaded == nil {
			continue
		}
		var matches []string
		matches = issPattern.FindStringSubmatch(filename)
		if len(matches) == 3 {
			issid := NewIssuerIdentifier(fmt.Sprintf("%s.%s", matches[1], matches[2]))
			downloaded.Issuers[issid] = struct{}{}
		}
		matches = credPattern.FindStringSubmatch(filename)
		if len(matches) == 4 {
			credid := NewCredentialTypeIdentifier(fmt.Sprintf("%s.%s.%s", matches[1], matches[2], matches[3]))
			downloaded.CredentialTypes[credid] = struct{}{}
		}
	}

	manager.index = newIndex
	return
}
1038
1039
1040
1041
1042

// Methods containing consistency checks on irma_configuration

func (conf *Configuration) checkIssuer(manager *SchemeManager, issuer *Issuer, dir string) error {
	issuerid := issuer.Identifier()
1043
1044
	conf.checkTranslations(fmt.Sprintf("Issuer %s", issuerid.String()), issuer)
	// Check that the issuer has public keys
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
	pkpath := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
	files, err := filepath.Glob(pkpath)
	if err != nil {
		return err
	}
	if len(files) == 0 {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no public keys", issuerid.String()))
	}

	if filepath.Base(dir) != issuer.ID {
		return errors.Errorf("Issuer %s has wrong directory name %s", issuerid.String(), filepath.Base(dir))
	}
	if manager.ID != issuer.SchemeManagerID {
		return errors.Errorf("Issuer %s has wrong SchemeManager %s", issuerid.String(), issuer.SchemeManagerID)
	}
	if err = fs.AssertPathExists(dir + "/logo.png"); err != nil {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no logo.png", issuerid.String()))
	}
	return nil
}

func (conf *Configuration) checkCredentialType(manager *SchemeManager, issuer *Issuer, cred *CredentialType, dir string) error {
	credid := cred.Identifier()
1068
	conf.checkTranslations(fmt.Sprintf("Credential type %s", credid.String()), cred)
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
	if cred.XMLVersion < 4 {
		return errors.New("Unsupported credential type description")
	}
	if cred.ID != filepath.Base(dir) {
		return errors.Errorf("Credential type %s has wrong directory name %s", credid.String(), filepath.Base(dir))
	}
	if cred.IssuerID != issuer.ID {
		return errors.Errorf("Credential type %s has wrong IssuerID %s", credid.String(), cred.IssuerID)
	}
	if cred.SchemeManagerID != manager.ID {
		return errors.Errorf("Credential type %s has wrong SchemeManager %s", credid.String(), cred.SchemeManagerID)
	}
	if err := fs.AssertPathExists(dir + "/logo.png"); err != nil {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has no logo.png", credid.String()))
	}
	return conf.checkAttributes(cred)
}

func (conf *Configuration) checkAttributes(cred *CredentialType) error {
	name := cred.Identifier().String()
	indices := make(map[int]struct{})
1090
	count := len(cred.AttributeTypes)
1091
1092
1093
	if count == 0 {
		return errors.Errorf("Credenial type %s has no attributes", name)
	}
1094
	for i, attr := range cred.AttributeTypes {
1095
		conf.checkTranslations(fmt.Sprintf("Attribute %s of credential type %s", attr.ID, cred.Identifier().String()), attr)
1096
		index