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

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

14
15
	"crypto/sha256"

16
17
18
19
	"fmt"

	"strings"

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

	"bytes"

	"encoding/hex"

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

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

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

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

47
48
49
	// 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
50

51
52
	Warnings []string

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

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

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

67
68
type SchemeManagerStatus string

69
70
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
71
	Status  SchemeManagerStatus
72
73
74
	Err     error
}

75
76
77
78
79
80
81
const (
	SchemeManagerStatusValid               = SchemeManagerStatus("Valid")
	SchemeManagerStatusUnprocessed         = SchemeManagerStatus("Unprocessed")
	SchemeManagerStatusInvalidIndex        = SchemeManagerStatus("InvalidIndex")
	SchemeManagerStatusInvalidSignature    = SchemeManagerStatus("InvalidSignature")
	SchemeManagerStatusParsingError        = SchemeManagerStatus("ParsingError")
	SchemeManagerStatusContentParsingError = SchemeManagerStatus("ContentParsingError")
82

83
84
	pubkeyPattern  = "%s/%s/%s/PublicKeys/*.xml"
	privkeyPattern = "%s/%s/%s/PrivateKeys/*.xml"
85
86
)

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

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

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

108
109
110
	// Init all maps
	conf.clear()

111
112
113
	return
}

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

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

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

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

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

201
202
203
	return err
}

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

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

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

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

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

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

274
275
// 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) {
276
277
278
279
280
281
282
283
284
285
286
	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 {
287
			return nil, err
288
289
		}
	}
290
	return conf.publicKeys[id][counter], nil
291
292
}

293
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
294
	hash := sha256.Sum256([]byte(credid.String()))
295
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
296
297
}

298
299
300
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
301
302
303
304
	}
	return nil
}

305
// IsInitialized indicates whether this instance has successfully been initialized.
306
307
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
308
309
}

310
311
312
313
314
315
316
317
318
// 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
		}
	}
}

319
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
320
321
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
322
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
323
324
325
		if err != nil {
			return err
		}
326
327
		if !exists {
			return nil
328
		}
329
330
331
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
332

333
		if err = conf.checkIssuer(manager, issuer, dir); err != nil {
334
335
336
			return err
		}

337
		conf.Issuers[issuer.Identifier()] = issuer
338
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
339
		return conf.parseCredentialsFolder(manager, issuer, dir+"/Issues/")
340
341
342
	})
}

343
344
345
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
	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)
		}
	}
362
363
364
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

365
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
366
367
func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
	manager := conf.SchemeManagers[issuerid.SchemeManagerIdentifier()]
368
	conf.publicKeys[issuerid] = map[int]*gabi.PublicKey{}
369
	path := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
370
371
372
373
374
375
376
377
378
379
	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 {
380
			return err
381
		}
Sietse Ringers's avatar
Sietse Ringers committed
382
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
383
		if err != nil || !found {
384
385
386
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
387
388
389
		if err != nil {
			return err
		}
390
391
392
		if int(pk.Counter) != i {
			return errors.Errorf("Public key %s of issuer %s has wrong <Counter>", file, issuerid.String())
		}
393
		pk.Issuer = issuerid.String()
394
		conf.publicKeys[issuerid][i] = pk
395
	}
396

397
398
399
	return nil
}

400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
func (conf *Configuration) 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
}

422
// parse $schememanager/$issuer/Issues/*/description.xml
423
424
425
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, issuer *Issuer, path string) error {
	var foundcred bool
	err := iterateSubfolders(path, func(dir string) error {
426
		cred := &CredentialType{}
427
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
428
429
430
		if err != nil {
			return err
		}
431
432
433
		if !exists {
			return nil
		}
434
		if err = conf.checkCredentialType(manager, issuer, cred, dir); err != nil {
435
436
437
438
			return err
		}
		foundcred = true
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
439
		credid := cred.Identifier()
440
441
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
442
443
		return nil
	})
444
445
446
447
448
449
	if !foundcred {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no credential types", issuer.Identifier().String()))
	}
	return err
}

450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
// 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
		}
467
468
469
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
470
471
472
473
474
475
476
477
478
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

479
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
480
481
482
483
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
484
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
485
486
487
	if !found {
		return false, nil
	}
488
489
490
491
	if err != nil {
		return true, err
	}

492
	err = xml.Unmarshal(bts, description)
493
494
495
496
497
498
	if err != nil {
		return true, err
	}

	return true, nil
}
499

500
501
502
503
504
// 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
505
}
506

507
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
508
509
510
	if conf.assets == "" {
		return true, nil
	}
511
512
513
514
	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)
515
	}
516
517
518
519
	// 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
520
	}
521
	return exists && !newTime.After(*oldTime), nil
522
523
}

524
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
525
	if conf.assets == "" {
526
		return false, nil
527
	}
528
529
530
531
532
	// 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
533
	}
534
535
536
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
537
538
539
	)
}

540
541
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
542
func DownloadSchemeManager(url string) (*SchemeManager, error) {
543
544
545
546
547
548
549
550
551
	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")]
	}
552
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
553
554
555
	if err != nil {
		return nil, err
	}
556
	manager := NewSchemeManager("")
557
558
559
560
561
562
563
564
	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
565
566
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
567
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
568
	// Remove everything falling under the manager's responsibility
569
	for credid := range conf.CredentialTypes {
570
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
571
			delete(conf.CredentialTypes, credid)
572
573
		}
	}
574
	for issid := range conf.Issuers {
575
		if issid.SchemeManagerIdentifier() == id {
576
			delete(conf.Issuers, issid)
577
578
		}
	}
579
	for issid := range conf.publicKeys {
580
		if issid.SchemeManagerIdentifier() == id {
581
			delete(conf.publicKeys, issid)
582
583
		}
	}
584
	delete(conf.SchemeManagers, id)
585
586

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
587
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
588
589
	}
	return nil
590
591
}

592
593
594
595
596
597
598
599
600
601
602
603
604
605
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
}

606
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
607
// provided its signature is valid.
608
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
609
	name := manager.ID
610
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
611
612
		return err
	}
613
614

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
615
	path := fmt.Sprintf("%s/%s", conf.Path, name)
616
617
618
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
619
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
620
621
		return err
	}
622
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
623
624
		return err
	}
625
626
627
628
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
629

630
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
631
632
633
634
635
636
}

// 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
637
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
638
639
640
641
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
642
		return
643
644
	}
	if err = t.GetFile("index.sig", sig); err != nil {
645
		return
646
	}
647
	err = conf.VerifySignature(manager.Identifier())
648
	return
649
}
650

Sietse Ringers's avatar
Sietse Ringers committed
651
652
653
// 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.
654
655
func (conf *Configuration) Download(session IrmaSession) (downloaded *IrmaIdentifierSet, err error) {
	managers := make(map[string]struct{}) // Managers that we must update
656
	downloaded = &IrmaIdentifierSet{
657
658
659
660
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
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
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
	// 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{}{}
727
				continue
728
729
730
731
732
733
734
735
736
737
738
			}
			if !typ.ContainsAttribute(attrid) {
				managers[credid.Root()] = struct{}{}
			}
		}
	}

	return nil
}

func (conf *Configuration) checkIssuers(set *IrmaIdentifierSet, managers map[string]struct{}) error {
739
	for issid := range set.Issuers {
740
		if _, contains := conf.Issuers[issid]; !contains {
741
			managers[issid.Root()] = struct{}{}
742
		}
Sietse Ringers's avatar
Sietse Ringers committed
743
	}
744
745
746
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
747
			if err != nil {
748
				return err
Sietse Ringers's avatar
Sietse Ringers committed
749
750
			}
			if pk == nil {
751
				managers[issid.Root()] = struct{}{}
752
753
754
			}
		}
	}
755
	return nil
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
}

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
777
// FromString populates this index by parsing the specified string.
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
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
}

797
// parseIndex parses the index file of the specified manager.
798
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
799
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
800
	if err := fs.AssertPathExists(path); err != nil {
801
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
802
	}
Sietse Ringers's avatar
Sietse Ringers committed
803
	indexbts, err := ioutil.ReadFile(path)
804
	if err != nil {
805
		return nil, err
806
	}
807
808
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
809
810
}

811
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
812
	err := conf.VerifySignature(manager.Identifier())
813
814
815
816
	if err != nil {
		return err
	}

817
	var exists bool
818
	for file := range manager.index {
819
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
820
821
822
823
824
825
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
826
		// Don't care about the actual bytes
827
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
828
829
830
831
832
833
834
			return err
		}
	}

	return nil
}

835
836
837
// 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.
838
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
839
	signedHash, ok := manager.index[filepath.ToSlash(path)]
840
	if !ok {
841
		return nil, false, nil
842
843
	}

Sietse Ringers's avatar
Sietse Ringers committed
844
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
845
	if err != nil {
846
		return nil, true, err
847
848
849
850
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
851
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
852
	}
853
	return bts, true, nil
854
855
856
857
858
}

// 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).
859
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
860
861
862
863
864
865
866
867
868
869
	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
870
	dir := filepath.Join(conf.Path, id.String())
871
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
872
		return errors.New("Missing scheme manager index file, signature, or public key")
873
874
875
876
877
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
878
		return err
879
880
881
882
883
884
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
885
		return err
886
887
888
889
	}
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
890
		return err
891
892
893
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
894
		return errors.New("Invalid scheme manager public key")
895
896
897
898
899
	}

	// Read and parse signature
	sig, err := ioutil.ReadFile(dir + "/index.sig")
	if err != nil {
900
		return err
901
902
903
904
905
	}
	ints := make([]*big.Int, 0, 2)
	_, err = asn1.Unmarshal(sig, &ints)

	// Verify signature
906
907
908
909
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
910
}
911
912
913
914

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930

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

931
932
933
934
935
936
937
938
939
940
	// 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
	}
941
	if !manager.Timestamp.Before(*timestamp) {
942
943
944
		return nil
	}

945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
	// 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 {
963
		path := filepath.Join(conf.Path, filename)
964
		oldHash, known := manager.index[filename]
965
966
967
968
969
970
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
971
972
973
974
975
976
977
978
			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
979
		if err = transport.GetSignedFile(stripped, path, newHash); err != nil {
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
			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
}
1002
1003
1004
1005
1006

// Methods containing consistency checks on irma_configuration

func (conf *Configuration) checkIssuer(manager *SchemeManager, issuer *Issuer, dir string) error {
	issuerid := issuer.Identifier()
1007
1008
	conf.checkTranslations(fmt.Sprintf("Issuer %s", issuerid.String()), issuer)
	// Check that the issuer has public keys
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
	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()
1032
	conf.checkTranslations(fmt.Sprintf("Credential type %s", credid.String()), cred)
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
	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 {
1059
		conf.checkTranslations(fmt.Sprintf("Attribute %s of credential type %s", attr.ID, cred.Identifier().String()), attr)
1060
		index := i
1061
1062
		if attr.DisplayIndex != nil {
			index = *attr.DisplayIndex
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
		}
		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
}
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109

func (conf *Configuration) checkScheme(scheme *SchemeManager, dir string) error {
	if scheme.XMLVersion < 7 {
		scheme.Status = SchemeManagerStatusParsingError
		return errors.New("Unsupported scheme manager description")
	}
	if filepath.Base(dir) != scheme.ID {
		scheme.Status = SchemeManagerStatusParsingError
		return errors.Errorf("Scheme %s has wrong directory name %s", scheme.ID, filepath.Base(dir))
	}
	conf.checkTranslations(fmt.Sprintf("Scheme %s", scheme.ID), scheme)
	return nil
}

// checkTranslations checks for each member of the interface o that is of type TranslatedString
// that it contains all necessary translations.
func (conf *Configuration) checkTranslations(file string, o interface{}) {
	langs := []string{"en", "nl"} // Hardcode these for now, TODO make configurable
	v := reflect.ValueOf(o)

	// Dereference in case of pointer or interface
	if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
		v = v.Elem()
	}

	for i := 0; i < v.NumField(); i++ {
		if v.Field(i).Type() == reflect.TypeOf(TranslatedString{}) {
			val := v.Field(i).Interface().(TranslatedString)
			for _, lang := range langs {
				if _, exists := val[lang]; !exists {
					conf.Warnings = append(conf.Warnings, fmt.Sprintf("%s misses %s translation in <%s> tag", file, lang, v.Type().Field(i).Name))
				}
			}
		}
	}
}
1110
1111
1112
1113
1114

func (conf *Configuration) CheckKeys() error {
	const expiryBoundary = int64(time.Hour/time.Second) * 24 * 31 // 1 month, TODO make configurable

	for issuerid := range conf.Issuers {
1115
		if err := conf.parseKeysFolder(issuerid); err != nil {
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
			return err
		}
		indices, err := conf.PublicKeyIndices(issuerid)
		if err != nil {
			return err
		}
		if len(indices) == 0 {
			continue
		}
		latest, err := conf.PublicKey(issuerid, indices[len(indices)-1])
		now := time.Now().Unix()
		if latest == nil || latest.ExpiryDate < now {
			conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no nonexpired public keys", issuerid.String()))
		}
		if latest != nil && latest.ExpiryDate > now && latest.ExpiryDate < now+expiryBoundary {
			conf.Warnings = append(conf.Warnings, fmt.Sprintf("Latest public key of issuer %s expires soon (at %s)",
				issuerid.String(), time.Unix(latest.ExpiryDate, 0).String()))
		}

		// Check private keys if any
		privkeypath := fmt.Sprintf(privkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
		privkeys, err := filepath.Glob(privkeypath)
		if err != nil {
			return err
		}
		for _, privkey := range privkeys {
			filename := filepath.Base(privkey)
			count, err := strconv.Atoi(filename[:len(filename)-4])
			if err != nil {
				return err
			}
			sk, err := gabi.NewPrivateKeyFromFile(privkey)
			if err != nil {
				return err
			}
			if int(sk.Counter) != count {
				return errors.Errorf("Private key %s of issuer %s has wrong <Counter>", filename, issuerid.String())
			}
			pk, err := conf.PublicKey(issuerid, count)
			if err != nil {
				return err
			}
			if pk == nil {
				return errors.Errorf("Private key %s of issuer %s has no corresponding public key", filename, issuerid.String())
			}
			if new(big.Int).Mul(sk.P, sk.Q).Cmp(pk.N) != 0 {
				return errors.Errorf("Private key %s of issuer %s does not belong to public key %s", filename, issuerid.String(), filename)
			}
		}
	}

	return nil
}