irmaconfig.go 28.4 KB
Newer Older
1
package irma
2
3
4
5
6
7
8

import (
	"encoding/base64"
	"encoding/xml"
	"io/ioutil"
	"os"
	"path/filepath"
9
	"regexp"
10
11
	"strconv"

12
13
	"crypto/sha256"

14
15
16
17
	"fmt"

	"strings"

18
19
20
21
22
23
24
25
26
27
28
29
	"sort"

	"bytes"

	"encoding/hex"

	"crypto/ecdsa"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
	"math/big"

30
	"github.com/go-errors/errors"
31
	"github.com/mhe/gabi"
32
	"github.com/privacybydesign/irmago/internal/fs"
33
34
)

35
// Configuration keeps track of scheme managers, issuers, credential types and public keys,
36
// dezerializing them from an irma_configuration folder, and downloads and saves new ones on demand.
37
type Configuration struct {
38
39
40
	SchemeManagers  map[SchemeManagerIdentifier]*SchemeManager
	Issuers         map[IssuerIdentifier]*Issuer
	CredentialTypes map[CredentialTypeIdentifier]*CredentialType
41

Sietse Ringers's avatar
Sietse Ringers committed
42
43
44
	// Path to the irma_configuration folder that this instance represents
	Path string

45
46
47
	// 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
48

49
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
50
	reverseHashes map[string]CredentialTypeIdentifier
51
	initialized   bool
52
	assets        string
53
54
}

Sietse Ringers's avatar
Sietse Ringers committed
55
56
// ConfigurationFileHash encodes the SHA256 hash of an authenticated
// file under a scheme manager within the configuration folder.
57
58
type ConfigurationFileHash []byte

Sietse Ringers's avatar
Sietse Ringers committed
59
60
// SchemeManagerIndex is a (signed) list of files under a scheme manager
// along with their SHA266 hash
61
62
type SchemeManagerIndex map[string]ConfigurationFileHash

63
64
type SchemeManagerStatus string

65
66
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
67
	Status  SchemeManagerStatus
68
69
70
	Err     error
}

71
72
73
74
75
76
77
78
79
const (
	SchemeManagerStatusValid               = SchemeManagerStatus("Valid")
	SchemeManagerStatusUnprocessed         = SchemeManagerStatus("Unprocessed")
	SchemeManagerStatusInvalidIndex        = SchemeManagerStatus("InvalidIndex")
	SchemeManagerStatusInvalidSignature    = SchemeManagerStatus("InvalidSignature")
	SchemeManagerStatusParsingError        = SchemeManagerStatus("ParsingError")
	SchemeManagerStatusContentParsingError = SchemeManagerStatus("ContentParsingError")
)

80
81
82
83
func (sme SchemeManagerError) Error() string {
	return fmt.Sprintf("Error parsing scheme manager %s: %s", sme.Manager.Name(), sme.Err.Error())
}

84
// NewConfiguration returns a new configuration. After this
85
// ParseFolder() should be called to parse the specified path.
86
87
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
	conf = &Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
88
		Path:   path,
89
		assets: assets,
90
	}
91

92
93
94
95
	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)
		}
96
	}
97
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
98
99
		return nil, err
	}
100

101
102
103
	// Init all maps
	conf.clear()

104
105
106
	return
}

107
func (conf *Configuration) clear() {
108
109
110
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
111
	conf.DisabledSchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManagerError)
112
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
113
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
114
115
116
117
118
119
120
}

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

122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
	// 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
141
	var mgrerr *SchemeManagerError
Sietse Ringers's avatar
Sietse Ringers committed
142
	err = iterateSubfolders(conf.Path, func(dir string) error {
143
144
		manager := NewSchemeManager(filepath.Base(dir))
		err := conf.ParseSchemeManagerFolder(dir, manager)
145
146
		if err == nil {
			return nil // OK, do next scheme manager folder
147
		}
148
149
150
151
		// 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 {
152
			conf.DisabledSchemeManagers[manager.Identifier()] = mgrerr
153
			return nil
154
		}
155
		return err // Not a SchemeManagerError? return it & halt parsing now
156
157
	})
	if err != nil {
158
		return
159
	}
160
	conf.initialized = true
161
162
163
	if mgrerr != nil {
		return mgrerr
	}
164
	return
165
166
}

167
168
169
170
171
172
173
// 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.
174
175
func (conf *Configuration) ParseOrRestoreFolder() error {
	err := conf.ParseFolder()
176
177
178
	// Only in case of a *SchemeManagerError might we be able to recover
	if _, isSchemeMgrErr := err.(*SchemeManagerError); !isSchemeMgrErr {
		return err
179
	}
180
181
182
183
184
185
186
187
188
189

	err = nil
	for id := range conf.DisabledSchemeManagers {
		if reinstallErr := conf.ReinstallSchemeManager(conf.SchemeManagers[id]); reinstallErr != nil {
			// Again, we can recover only from a *SchemeManagerError, so bail out now otherwise
			if _, isSchemeMgrErr := reinstallErr.(*SchemeManagerError); !isSchemeMgrErr {
				return err
			}
			err = reinstallErr
		}
190
	}
191

192
193
194
	return err
}

195
// ParseSchemeManagerFolder parses the entire tree of the specified scheme manager
196
// If err != nil then a problem occured
197
func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
198
199
200
201
202
203
204
	// 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
205
206
	defer func() {
		if err != nil {
207
208
209
210
211
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
212
213
214
		}
	}()

215
216
	// Verify signature and read scheme manager description
	if err = conf.VerifySignature(manager.Identifier()); err != nil {
217
218
		return
	}
219
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
220
221
		manager.Status = SchemeManagerStatusInvalidIndex
		return
222
	}
223
224
225
226
227
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
228
229
230
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
231
	}
232
233
234
235
236
237
238
239
240
241
242
	if manager.XMLVersion < 7 {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Unsupported scheme manager description")
	}

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

244
	// Read timestamp indicating time of last modification
245
246
247
	ts, exists, err := readTimestamp(dir + "/timestamp")
	if err != nil || !exists {
		return errors.WrapPrefix(err, "Could not read scheme manager timestamp", 0)
248
	}
249
	manager.Timestamp = *ts
250

251
	// Parse contained issuers and credential types
252
	err = conf.parseIssuerFolders(manager, dir)
253
254
255
256
257
258
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
259
260
261
	return
}

262
263
264
265
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

266
267
268
269
// 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) {
	if _, contains := conf.publicKeys[id]; !contains {
		conf.publicKeys[id] = map[int]*gabi.PublicKey{}
270
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
271
			return nil, err
272
273
		}
	}
274
	return conf.publicKeys[id][counter], nil
275
276
}

277
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
278
	hash := sha256.Sum256([]byte(credid.String()))
279
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
280
281
}

282
283
284
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
285
286
287
288
	}
	return nil
}

289
// IsInitialized indicates whether this instance has successfully been initialized.
290
291
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
292
293
}

294
295
296
297
298
299
300
301
302
// 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
		}
	}
}

303
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
304
305
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
306
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
307
308
309
		if err != nil {
			return err
		}
310
311
		if !exists {
			return nil
312
		}
313
314
315
316
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
		conf.Issuers[issuer.Identifier()] = issuer
317
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
318
		return conf.parseCredentialsFolder(manager, dir+"/Issues/")
319
320
321
	})
}

322
323
324
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
	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)
		}
	}
341
342
343
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

344
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
345
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
Sietse Ringers's avatar
Sietse Ringers committed
346
	path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
347
348
349
350
351
352
353
354
355
356
357
	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 {
			continue
358
		}
Sietse Ringers's avatar
Sietse Ringers committed
359
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
360
		if err != nil || !found {
361
362
363
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
364
365
366
		if err != nil {
			return err
		}
367
		pk.Issuer = issuerid.String()
368
		conf.publicKeys[issuerid][i] = pk
369
	}
370

371
372
373
	return nil
}

374
// parse $schememanager/$issuer/Issues/*/description.xml
375
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, path string) error {
376
377
	return iterateSubfolders(path, func(dir string) error {
		cred := &CredentialType{}
378
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
379
380
381
		if err != nil {
			return err
		}
382
383
384
385
386
		if !exists {
			return nil
		}
		if cred.XMLVersion < 4 {
			return errors.New("Unsupported credential type description")
387
		}
388
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
389
390
391
		credid := cred.Identifier()
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
		return nil
	})
}

// 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
		}
413
414
415
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
416
417
418
419
420
421
422
423
424
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

425
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
426
427
428
429
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
430
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
431
432
433
	if !found {
		return false, nil
	}
434
435
436
437
	if err != nil {
		return true, err
	}

438
	err = xml.Unmarshal(bts, description)
439
440
441
442
443
444
	if err != nil {
		return true, err
	}

	return true, nil
}
445

446
447
448
449
450
// 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
451
}
452

453
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
454
455
456
	if conf.assets == "" {
		return true, nil
	}
457
458
459
460
	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)
461
	}
462
463
464
465
	// 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
466
	}
467
	return exists && !newTime.After(*oldTime), nil
468
469
}

470
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
471
	if conf.assets == "" {
472
		return false, nil
473
	}
474
475
476
477
478
	// 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
479
	}
480
481
482
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
483
484
485
	)
}

486
487
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
488
func DownloadSchemeManager(url string) (*SchemeManager, error) {
489
490
491
492
493
494
495
496
497
	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")]
	}
498
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
499
500
501
	if err != nil {
		return nil, err
	}
502
	manager := NewSchemeManager("")
503
504
505
506
507
508
509
510
	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
511
512
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
513
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
514
	// Remove everything falling under the manager's responsibility
515
	for credid := range conf.CredentialTypes {
516
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
517
			delete(conf.CredentialTypes, credid)
518
519
		}
	}
520
	for issid := range conf.Issuers {
521
		if issid.SchemeManagerIdentifier() == id {
522
			delete(conf.Issuers, issid)
523
524
		}
	}
525
	for issid := range conf.publicKeys {
526
		if issid.SchemeManagerIdentifier() == id {
527
			delete(conf.publicKeys, issid)
528
529
		}
	}
530
	delete(conf.SchemeManagers, id)
531
532

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
533
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
534
535
	}
	return nil
536
537
}

538
539
540
541
542
543
544
545
546
547
548
549
550
551
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
}

552
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
553
// provided its signature is valid.
554
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
555
	name := manager.ID
556
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
557
558
		return err
	}
559
560

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
561
	path := fmt.Sprintf("%s/%s", conf.Path, name)
562
563
564
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
565
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
566
567
		return err
	}
568
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
569
570
		return err
	}
571
572
573
574
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
575

576
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
577
578
579
580
581
582
}

// 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
583
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
584
585
586
587
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
588
		return
589
590
	}
	if err = t.GetFile("index.sig", sig); err != nil {
591
		return
592
	}
593
	err = conf.VerifySignature(manager.Identifier())
594
	return
595
}
596

Sietse Ringers's avatar
Sietse Ringers committed
597
598
599
// 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.
600
601
func (conf *Configuration) Download(session IrmaSession) (downloaded *IrmaIdentifierSet, err error) {
	managers := make(map[string]struct{}) // Managers that we must update
602
	downloaded = &IrmaIdentifierSet{
603
604
605
606
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
607

608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
	// 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
}

func (conf *Configuration) checkCredentialTypes(session IrmaSession, managers map[string]struct{}) error {
	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
			typ, contains = conf.CredentialTypes[*credreq.CredentialTypeID]
			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
			for _, attrtyp := range typ.Attributes {
				_, 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{}{}
			}
			if !typ.ContainsAttribute(attrid) {
				managers[credid.Root()] = struct{}{}
			}
		}
	}

	return nil
}

func (conf *Configuration) checkIssuers(set *IrmaIdentifierSet, managers map[string]struct{}) error {
684
	for issid := range set.Issuers {
685
		if _, contains := conf.Issuers[issid]; !contains {
686
			managers[issid.Root()] = struct{}{}
687
		}
Sietse Ringers's avatar
Sietse Ringers committed
688
	}
689
690
691
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
692
			if err != nil {
693
				return err
Sietse Ringers's avatar
Sietse Ringers committed
694
695
			}
			if pk == nil {
696
				managers[issid.Root()] = struct{}{}
697
698
699
			}
		}
	}
700
	return nil
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
}

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
722
// FromString populates this index by parsing the specified string.
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
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
}

742
// parseIndex parses the index file of the specified manager.
743
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
744
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
745
	if err := fs.AssertPathExists(path); err != nil {
746
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
747
	}
Sietse Ringers's avatar
Sietse Ringers committed
748
	indexbts, err := ioutil.ReadFile(path)
749
	if err != nil {
750
		return nil, err
751
	}
752
753
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
754
755
}

756
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
757
	err := conf.VerifySignature(manager.Identifier())
758
759
760
761
	if err != nil {
		return err
	}

762
	var exists bool
763
	for file := range manager.index {
764
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
765
766
767
768
769
770
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
771
		// Don't care about the actual bytes
772
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
773
774
775
776
777
778
779
			return err
		}
	}

	return nil
}

780
781
782
// 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.
783
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
784
	signedHash, ok := manager.index[path]
785
	if !ok {
786
		return nil, false, nil
787
788
	}

Sietse Ringers's avatar
Sietse Ringers committed
789
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
790
	if err != nil {
791
		return nil, true, err
792
793
794
795
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
796
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
797
	}
798
	return bts, true, nil
799
800
801
802
803
}

// 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).
804
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
805
806
807
808
809
810
811
812
813
814
	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
815
	dir := filepath.Join(conf.Path, id.String())
816
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
817
		return errors.New("Missing scheme manager index file, signature, or public key")
818
819
820
821
822
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
823
		return err
824
825
826
827
828
829
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
830
		return err
831
832
833
834
	}
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
835
		return err
836
837
838
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
839
		return errors.New("Invalid scheme manager public key")
840
841
842
843
844
	}

	// Read and parse signature
	sig, err := ioutil.ReadFile(dir + "/index.sig")
	if err != nil {
845
		return err
846
847
848
849
850
	}
	ints := make([]*big.Int, 0, 2)
	_, err = asn1.Unmarshal(sig, &ints)

	// Verify signature
851
852
853
854
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
855
}
856
857
858
859

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875

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)
	}

876
877
878
879
880
881
882
883
884
885
	// 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
	}
886
	if !manager.Timestamp.Before(*timestamp) {
887
888
889
		return nil
	}

890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
	// 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 {
908
		path := filepath.Join(conf.Path, filename)
909
		oldHash, known := manager.index[filename]
910
911
912
913
914
915
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
916
917
918
919
920
921
922
923
			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
924
		if err = transport.GetSignedFile(stripped, path, newHash); err != nil {
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
			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
}