irmaconfig.go 25.1 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
	"strconv"
11
	"time"
12

13
14
	"crypto/sha256"

15
16
17
18
	"fmt"

	"strings"

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

	"bytes"

	"encoding/hex"

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

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

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

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

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

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

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

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

64
65
type SchemeManagerStatus string

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

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

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

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

Sietse Ringers's avatar
Sietse Ringers committed
93
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
94
95
		return nil, err
	}
96
97
98
99
100
	isUpToDate, err := conf.isUpToDate()
	if err != nil {
		return nil, err
	}
	if conf.assets != "" && !isUpToDate {
101
		if err = conf.CopyFromAssets(false); err != nil {
102
103
104
105
			return nil, err
		}
	}

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

109
110
111
	return
}

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

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

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

153
154
155
156
157
158
159
160
161
162
163
164
func (conf *Configuration) ParseOrRestoreFolder() error {
	err := conf.ParseFolder()
	var parse bool
	for id := range conf.DisabledSchemeManagers {
		parse = conf.CopyManagerFromAssets(id)
	}
	if parse {
		return conf.ParseFolder()
	}
	return err
}

165
// ParseSchemeManagerFolder parses the entire tree of the specified scheme manager
166
// If err != nil then a problem occured
167
func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
168
169
170
171
172
173
174
	// 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
175
176
	defer func() {
		if err != nil {
177
178
179
180
181
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
182
183
184
		}
	}()

185
186
187
188
189
	err = fs.AssertPathExists(dir + "/description.xml")
	if err != nil {
		return
	}

190
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
191
192
		manager.Status = SchemeManagerStatusInvalidIndex
		return
193
	}
194

195
	err = conf.VerifySchemeManager(manager)
196
197
198
199
200
	if err != nil {
		manager.Status = SchemeManagerStatusInvalidSignature
		return
	}

201
202
203
204
205
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
206
207
208
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
209
210
	}

211
212
213
214
215
	manager.Timestamp, err = readTimestamp(dir + "/timestamp")
	if err != nil {
		return errors.New("Could not read scheme manager timestamp")
	}

216
	if manager.XMLVersion < 7 {
217
		manager.Status = SchemeManagerStatusParsingError
218
		return errors.New("Unsupported scheme manager description")
219
	}
220

221
	err = conf.parseIssuerFolders(manager, dir)
222
223
224
225
226
227
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
228
229
230
	return
}

231
232
233
234
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

235
236
237
238
// 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{}
239
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
240
			return nil, err
241
242
		}
	}
243
	return conf.publicKeys[id][counter], nil
244
245
}

246
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
247
	hash := sha256.Sum256([]byte(credid.String()))
248
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
249
250
}

251
252
253
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
254
255
256
257
	}
	return nil
}

258
// IsInitialized indicates whether this instance has successfully been initialized.
259
260
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
261
262
}

263
264
265
266
267
268
269
270
271
// 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
		}
	}
}

272
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
273
274
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
275
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
276
277
278
		if err != nil {
			return err
		}
279
280
		if !exists {
			return nil
281
		}
282
283
284
285
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
		conf.Issuers[issuer.Identifier()] = issuer
286
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
287
		return conf.parseCredentialsFolder(manager, dir+"/Issues/")
288
289
290
	})
}

291
292
293
294
295
296
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

297
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
298
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
Sietse Ringers's avatar
Sietse Ringers committed
299
	path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
300
301
302
303
304
305
306
307
308
309
310
	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
311
		}
Sietse Ringers's avatar
Sietse Ringers committed
312
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
313
		if err != nil || !found {
314
315
316
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
317
318
319
		if err != nil {
			return err
		}
320
		pk.Issuer = issuerid.String()
321
		conf.publicKeys[issuerid][i] = pk
322
	}
323

324
325
326
	return nil
}

327
// parse $schememanager/$issuer/Issues/*/description.xml
328
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, path string) error {
329
330
	return iterateSubfolders(path, func(dir string) error {
		cred := &CredentialType{}
331
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
332
333
334
		if err != nil {
			return err
		}
335
336
337
338
339
		if !exists {
			return nil
		}
		if cred.XMLVersion < 4 {
			return errors.New("Unsupported credential type description")
340
		}
341
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
342
343
344
		credid := cred.Identifier()
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
		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
		}
366
367
368
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
369
370
371
372
373
374
375
376
377
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

378
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
379
380
381
382
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
383
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
384
385
386
	if !found {
		return false, nil
	}
387
388
389
390
	if err != nil {
		return true, err
	}

391
	err = xml.Unmarshal(bts, description)
392
393
394
395
396
397
	if err != nil {
		return true, err
	}

	return true, nil
}
398

399
400
401
402
403
// 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
404
}
405

406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
func (conf *Configuration) readTimestamp(path string) (timestamp *time.Time, exists bool, err error) {
	filename := filepath.Join(path, "timestamp")
	exists, err = fs.PathExists(filename)
	if err != nil || !exists {
		return
	}
	bts, err := ioutil.ReadFile(filename)
	if err != nil {
		return
	}
	i, err := strconv.ParseInt(string(bts), 10, 64)
	if err != nil {
		return
	}
	t := time.Unix(i, 0)
	return &t, true, nil
}

func (conf *Configuration) isUpToDate() (bool, error) {
	if conf.assets == "" {
		return true, nil
	}
	var err error
	newTime, exists, err := conf.readTimestamp(conf.assets)
	if err != nil {
		return false, err
	}
	if !exists {
		return false, errors.New("Timestamp in assets irma_configuration not found")
	}

	// conf.Path does not need to have a timestamp. If it does not, it is outdated
	oldTime, exists, err := conf.readTimestamp(conf.Path)
	return exists && !newTime.After(*oldTime), err
}

442
443
444
// CopyFromAssets recursively copies the directory tree from the assets folder
// into the directory of this Configuration.
func (conf *Configuration) CopyFromAssets(parse bool) error {
445
446
447
	if conf.assets == "" {
		return nil
	}
Sietse Ringers's avatar
Sietse Ringers committed
448
	if err := fs.CopyDirectory(conf.assets, conf.Path); err != nil {
449
450
451
		return err
	}
	if parse {
452
		return conf.ParseFolder()
453
454
	}
	return nil
455
}
456

457
458
459
460
461
462
463
func (conf *Configuration) CopyManagerFromAssets(managerID SchemeManagerIdentifier) bool {
	manager := conf.SchemeManagers[managerID]
	if conf.assets == "" {
		return false
	}
	_ = fs.CopyDirectory(
		filepath.Join(conf.assets, manager.ID),
Sietse Ringers's avatar
Sietse Ringers committed
464
		filepath.Join(conf.Path, manager.ID),
465
466
467
468
	)
	return true
}

469
470
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
471
func DownloadSchemeManager(url string) (*SchemeManager, error) {
472
473
474
475
476
477
478
479
480
	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")]
	}
481
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
482
483
484
	if err != nil {
		return nil, err
	}
485
	manager := NewSchemeManager("")
486
487
488
489
490
491
492
493
	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
494
495
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
496
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
497
	// Remove everything falling under the manager's responsibility
498
	for credid := range conf.CredentialTypes {
499
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
500
			delete(conf.CredentialTypes, credid)
501
502
		}
	}
503
	for issid := range conf.Issuers {
504
		if issid.SchemeManagerIdentifier() == id {
505
			delete(conf.Issuers, issid)
506
507
		}
	}
508
	for issid := range conf.publicKeys {
509
		if issid.SchemeManagerIdentifier() == id {
510
			delete(conf.publicKeys, issid)
511
512
		}
	}
513
	delete(conf.SchemeManagers, id)
514
515

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
516
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
517
518
	}
	return nil
519
520
}

521
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
522
// provided its signature is valid.
523
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
524
	name := manager.ID
525
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
526
527
		return err
	}
528
529

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
530
	path := fmt.Sprintf("%s/%s", conf.Path, name)
531
532
533
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
534
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
535
536
		return err
	}
537
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
538
539
		return err
	}
540
541
542
543
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
544

545
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
546
547
548
549
550
551
}

// 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
552
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
553
554
555
556
557
558
559
560
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	// Backup so we can restore last valid signature if the new signature is invalid
	if err := conf.backupManagerSignature(index, sig); err != nil {
		return err
	}

561
562
563
564
565
566
	defer func() {
		if err != nil {
			_ = conf.restoreManagerSignature(index, sig)
		}
	}()

567
	if err = t.GetFile("index", index); err != nil {
568
		return
569
570
	}
	if err = t.GetFile("index.sig", sig); err != nil {
571
		return
572
	}
Sietse Ringers's avatar
Sietse Ringers committed
573
574
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
575
		return
Sietse Ringers's avatar
Sietse Ringers committed
576
577
	}
	if !valid {
578
		err = errors.New("Scheme manager signature invalid")
Sietse Ringers's avatar
Sietse Ringers committed
579
	}
580
	return
581
}
582

583
584
585
586
587
588
589
590
591
func (conf *Configuration) backupManagerSignature(index, sig string) error {
	if err := fs.Copy(index, index+".backup"); err != nil {
		return err
	}
	if err := fs.Copy(sig, sig+".backup"); err != nil {
		return err
	}
	return nil
}
592

593
594
func (conf *Configuration) restoreManagerSignature(index, sig string) error {
	if err := fs.Copy(index+".backup", index); err != nil {
595
596
		return err
	}
597
	if err := fs.Copy(sig+".backup", sig); err != nil {
598
599
600
601
602
		return err
	}
	return nil
}

Sietse Ringers's avatar
Sietse Ringers committed
603
604
605
// 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.
606
607
func (conf *Configuration) Download(set *IrmaIdentifierSet) (downloaded *IrmaIdentifierSet, err error) {
	downloaded = &IrmaIdentifierSet{
608
609
610
611
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
612

613
	managers := make(map[SchemeManagerIdentifier]struct{})
614
	for issid := range set.Issuers {
615
616
		if _, contains := conf.Issuers[issid]; !contains {
			managers[issid.SchemeManagerIdentifier()] = struct{}{}
617
		}
Sietse Ringers's avatar
Sietse Ringers committed
618
	}
619
620
621
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
622
623
624
625
			if err != nil {
				return nil, err
			}
			if pk == nil {
626
				managers[issid.SchemeManagerIdentifier()] = struct{}{}
627
628
629
630
			}
		}
	}
	for credid := range set.CredentialTypes {
631
		if _, contains := conf.CredentialTypes[credid]; !contains {
632
			managers[credid.IssuerIdentifier().SchemeManagerIdentifier()] = struct{}{}
633
634
635
		}
	}

636
637
638
	for id := range managers {
		if err = conf.UpdateSchemeManager(id, downloaded); err != nil {
			return
639
640
		}
	}
641

642
643
644
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
645
	return
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
}

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
667
// FromString populates this index by parsing the specified string.
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
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
}

687
// parseIndex parses the index file of the specified manager.
688
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
689
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
690
	if err := fs.AssertPathExists(path); err != nil {
691
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
692
	}
Sietse Ringers's avatar
Sietse Ringers committed
693
	indexbts, err := ioutil.ReadFile(path)
694
	if err != nil {
695
		return nil, err
696
	}
697
698
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
699
700
}

701
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
702
703
704
705
706
707
708
709
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
		return err
	}
	if !valid {
		return errors.New("Scheme manager signature was invalid")
	}

710
	for file := range manager.index {
Sietse Ringers's avatar
Sietse Ringers committed
711
		exists, err := fs.PathExists(filepath.Join(conf.Path, file))
712
713
714
715
716
717
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
718
		// Don't care about the actual bytes
719
		if _, _, err := conf.ReadAuthenticatedFile(manager, file); err != nil {
720
721
722
723
724
725
726
			return err
		}
	}

	return nil
}

727
728
729
// 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.
730
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
731
	signedHash, ok := manager.index[path]
732
	if !ok {
733
		return nil, false, nil
734
735
	}

Sietse Ringers's avatar
Sietse Ringers committed
736
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
737
	if err != nil {
738
		return nil, true, err
739
740
741
742
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
743
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
744
	}
745
	return bts, true, nil
746
747
748
749
750
}

// 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).
751
752
753
754
755
756
757
758
759
760
761
762
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (valid bool, err error) {
	defer func() {
		if r := recover(); r != nil {
			valid = false
			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
763
	dir := filepath.Join(conf.Path, id.String())
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
		return false, errors.New("Missing scheme manager index file, signature, or public key")
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
		return false, err
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
		return false, err
	}
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
		return false, err
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
		return false, errors.New("Invalid scheme manager public key")
	}

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

	// Verify signature
	return ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]), nil
800
}
801
802
803
804

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839

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

	// 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")
	transport := NewHTTPTransport("")

	// TODO: how to recover/fix local copy if err != nil below?
	for filename, newHash := range newIndex {
840
		path := filepath.Join(conf.Path, filename)
841
		oldHash, known := manager.index[filename]
842
843
844
845
846
847
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
			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
		if err = transport.GetFile(manager.URL+"/"+stripped, path); err != nil {
			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
}