irmaconfig.go 31.6 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
50
	Warnings []string

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

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

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

65
66
type SchemeManagerStatus string

67
68
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
69
	Status  SchemeManagerStatus
70
71
72
	Err     error
}

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

	pubkeyPattern = "%s/%s/%s/PublicKeys/*.xml"
82
83
)

84
85
86
87
func (sme SchemeManagerError) Error() string {
	return fmt.Sprintf("Error parsing scheme manager %s: %s", sme.Manager.Name(), sme.Err.Error())
}

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

96
97
98
99
	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)
		}
100
	}
101
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
102
103
		return nil, err
	}
104

105
106
107
	// Init all maps
	conf.clear()

108
109
110
	return
}

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

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

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

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

	for id := range conf.DisabledSchemeManagers {
186
187
188
189
190
191
192
193
194
		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)
195
		}
196
	}
197

198
199
200
	return err
}

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

221
222
	// Verify signature and read scheme manager description
	if err = conf.VerifySignature(manager.Identifier()); err != nil {
223
224
		return
	}
225
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
226
227
		manager.Status = SchemeManagerStatusInvalidIndex
		return
228
	}
229
230
231
232
233
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
234
235
236
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
237
	}
238
239
240
241
	if manager.XMLVersion < 7 {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Unsupported scheme manager description")
	}
242
243
244
	if filepath.Base(dir) != manager.ID {
		return errors.Errorf("Scheme %s has wrong directory name %s", manager.ID, filepath.Base(dir))
	}
245
246
247
248
249
250
251

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

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

260
	// Parse contained issuers and credential types
261
	err = conf.parseIssuerFolders(manager, dir)
262
263
264
265
266
267
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
268
269
270
	return
}

271
272
273
274
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

275
276
277
278
// 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{}
279
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
280
			return nil, err
281
282
		}
	}
283
	return conf.publicKeys[id][counter], nil
284
285
}

286
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
287
	hash := sha256.Sum256([]byte(credid.String()))
288
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
289
290
}

291
292
293
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
294
295
296
297
	}
	return nil
}

298
// IsInitialized indicates whether this instance has successfully been initialized.
299
300
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
301
302
}

303
304
305
306
307
308
309
310
311
// 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
		}
	}
}

312
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
313
314
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
315
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
316
317
318
		if err != nil {
			return err
		}
319
320
		if !exists {
			return nil
321
		}
322
323
324
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
325

326
		if err = conf.checkIssuer(manager, issuer, dir); err != nil {
327
328
329
			return err
		}

330
		conf.Issuers[issuer.Identifier()] = issuer
331
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
332
		return conf.parseCredentialsFolder(manager, issuer, dir+"/Issues/")
333
334
335
	})
}

336
337
338
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
	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)
		}
	}
355
356
357
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

358
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
359
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
360
	path := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
361
362
363
364
365
366
367
368
369
370
371
	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
372
		}
Sietse Ringers's avatar
Sietse Ringers committed
373
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
374
		if err != nil || !found {
375
376
377
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
378
379
380
		if err != nil {
			return err
		}
381
		pk.Issuer = issuerid.String()
382
		conf.publicKeys[issuerid][i] = pk
383
	}
384

385
386
387
	return nil
}

388
// parse $schememanager/$issuer/Issues/*/description.xml
389
390
391
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, issuer *Issuer, path string) error {
	var foundcred bool
	err := iterateSubfolders(path, func(dir string) error {
392
		cred := &CredentialType{}
393
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
394
395
396
		if err != nil {
			return err
		}
397
398
399
		if !exists {
			return nil
		}
400
		if err = conf.checkCredentialType(manager, issuer, cred, dir); err != nil {
401
402
403
404
			return err
		}
		foundcred = true
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
405
		credid := cred.Identifier()
406
407
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
408
409
		return nil
	})
410
411
412
413
414
415
	if !foundcred {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no credential types", issuer.Identifier().String()))
	}
	return err
}

416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
// 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
		}
433
434
435
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
436
437
438
439
440
441
442
443
444
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

445
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
446
447
448
449
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
450
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
451
452
453
	if !found {
		return false, nil
	}
454
455
456
457
	if err != nil {
		return true, err
	}

458
	err = xml.Unmarshal(bts, description)
459
460
461
462
463
464
	if err != nil {
		return true, err
	}

	return true, nil
}
465

466
467
468
469
470
// 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
471
}
472

473
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
474
475
476
	if conf.assets == "" {
		return true, nil
	}
477
478
479
480
	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)
481
	}
482
483
484
485
	// 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
486
	}
487
	return exists && !newTime.After(*oldTime), nil
488
489
}

490
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
491
	if conf.assets == "" {
492
		return false, nil
493
	}
494
495
496
497
498
	// 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
499
	}
500
501
502
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
503
504
505
	)
}

506
507
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
508
func DownloadSchemeManager(url string) (*SchemeManager, error) {
509
510
511
512
513
514
515
516
517
	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")]
	}
518
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
519
520
521
	if err != nil {
		return nil, err
	}
522
	manager := NewSchemeManager("")
523
524
525
526
527
528
529
530
	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
531
532
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
533
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
534
	// Remove everything falling under the manager's responsibility
535
	for credid := range conf.CredentialTypes {
536
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
537
			delete(conf.CredentialTypes, credid)
538
539
		}
	}
540
	for issid := range conf.Issuers {
541
		if issid.SchemeManagerIdentifier() == id {
542
			delete(conf.Issuers, issid)
543
544
		}
	}
545
	for issid := range conf.publicKeys {
546
		if issid.SchemeManagerIdentifier() == id {
547
			delete(conf.publicKeys, issid)
548
549
		}
	}
550
	delete(conf.SchemeManagers, id)
551
552

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
553
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
554
555
	}
	return nil
556
557
}

558
559
560
561
562
563
564
565
566
567
568
569
570
571
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
}

572
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
573
// provided its signature is valid.
574
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
575
	name := manager.ID
576
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
577
578
		return err
	}
579
580

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
581
	path := fmt.Sprintf("%s/%s", conf.Path, name)
582
583
584
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
585
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
586
587
		return err
	}
588
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
589
590
		return err
	}
591
592
593
594
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
595

596
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
597
598
599
600
601
602
}

// 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
603
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
604
605
606
607
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
608
		return
609
610
	}
	if err = t.GetFile("index.sig", sig); err != nil {
611
		return
612
	}
613
	err = conf.VerifySignature(manager.Identifier())
614
	return
615
}
616

Sietse Ringers's avatar
Sietse Ringers committed
617
618
619
// 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.
620
621
func (conf *Configuration) Download(session IrmaSession) (downloaded *IrmaIdentifierSet, err error) {
	managers := make(map[string]struct{}) // Managers that we must update
622
	downloaded = &IrmaIdentifierSet{
623
624
625
626
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
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
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
	// 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 {
704
	for issid := range set.Issuers {
705
		if _, contains := conf.Issuers[issid]; !contains {
706
			managers[issid.Root()] = struct{}{}
707
		}
Sietse Ringers's avatar
Sietse Ringers committed
708
	}
709
710
711
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
712
			if err != nil {
713
				return err
Sietse Ringers's avatar
Sietse Ringers committed
714
715
			}
			if pk == nil {
716
				managers[issid.Root()] = struct{}{}
717
718
719
			}
		}
	}
720
	return nil
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
}

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
742
// FromString populates this index by parsing the specified string.
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
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
}

762
// parseIndex parses the index file of the specified manager.
763
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
764
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
765
	if err := fs.AssertPathExists(path); err != nil {
766
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
767
	}
Sietse Ringers's avatar
Sietse Ringers committed
768
	indexbts, err := ioutil.ReadFile(path)
769
	if err != nil {
770
		return nil, err
771
	}
772
773
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
774
775
}

776
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
777
	err := conf.VerifySignature(manager.Identifier())
778
779
780
781
	if err != nil {
		return err
	}

782
	var exists bool
783
	for file := range manager.index {
784
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
785
786
787
788
789
790
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
791
		// Don't care about the actual bytes
792
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
793
794
795
796
797
798
799
			return err
		}
	}

	return nil
}

800
801
802
// 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.
803
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
804
	signedHash, ok := manager.index[path]
805
	if !ok {
806
		return nil, false, nil
807
808
	}

Sietse Ringers's avatar
Sietse Ringers committed
809
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
810
	if err != nil {
811
		return nil, true, err
812
813
814
815
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
816
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
817
	}
818
	return bts, true, nil
819
820
821
822
823
}

// 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).
824
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
825
826
827
828
829
830
831
832
833
834
	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
835
	dir := filepath.Join(conf.Path, id.String())
836
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
837
		return errors.New("Missing scheme manager index file, signature, or public key")
838
839
840
841
842
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
843
		return err
844
845
846
847
848
849
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
850
		return err
851
852
853
854
	}
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
855
		return err
856
857
858
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
859
		return errors.New("Invalid scheme manager public key")
860
861
862
863
864
	}

	// Read and parse signature
	sig, err := ioutil.ReadFile(dir + "/index.sig")
	if err != nil {
865
		return err
866
867
868
869
870
	}
	ints := make([]*big.Int, 0, 2)
	_, err = asn1.Unmarshal(sig, &ints)

	// Verify signature
871
872
873
874
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
875
}
876
877
878
879

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895

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

896
897
898
899
900
901
902
903
904
905
	// 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
	}
906
	if !manager.Timestamp.Before(*timestamp) {
907
908
909
		return nil
	}

910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
	// 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 {
928
		path := filepath.Join(conf.Path, filename)
929
		oldHash, known := manager.index[filename]
930
931
932
933
934
935
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
936
937
938
939
940
941
942
943
			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
944
		if err = transport.GetSignedFile(stripped, path, newHash); err != nil {
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
			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
}
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035

// Methods containing consistency checks on irma_configuration

func (conf *Configuration) checkIssuer(manager *SchemeManager, issuer *Issuer, dir string) error {
	// Check that the issuer has public keys
	issuerid := issuer.Identifier()
	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()
	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{})
	count := len(cred.Attributes)
	if count == 0 {
		return errors.Errorf("Credenial type %s has no attributes", name)
	}
	for i, attr := range cred.Attributes {
		index := i
		if attr.Index != nil {
			index = *attr.Index
		}
		if index >= count {
			conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has invalid attribute index at attribute %d", name, i))
		}
		indices[index] = struct{}{}
	}
	if len(indices) != count {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has invalid attribute ordering, check the index-tags", name))
	}
	return nil
}