irmaconfig.go 24.8 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
	}

	if manager.XMLVersion < 7 {
212
		manager.Status = SchemeManagerStatusParsingError
213
		return errors.New("Unsupported scheme manager description")
214
	}
215

216
	err = conf.parseIssuerFolders(manager, dir)
217
218
219
220
221
222
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
223
224
225
	return
}

226
227
228
229
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

230
231
232
233
// 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{}
234
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
235
			return nil, err
236
237
		}
	}
238
	return conf.publicKeys[id][counter], nil
239
240
}

241
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
242
	hash := sha256.Sum256([]byte(credid.String()))
243
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
244
245
}

246
247
248
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
249
250
251
252
	}
	return nil
}

253
// IsInitialized indicates whether this instance has successfully been initialized.
254
255
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
256
257
}

258
259
260
261
262
263
264
265
266
// 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
		}
	}
}

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

286
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
287
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
Sietse Ringers's avatar
Sietse Ringers committed
288
	path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
289
290
291
292
293
294
295
296
297
298
299
	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
300
		}
Sietse Ringers's avatar
Sietse Ringers committed
301
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
302
		if err != nil || !found {
303
304
305
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
306
307
308
		if err != nil {
			return err
		}
309
		pk.Issuer = issuerid.String()
310
		conf.publicKeys[issuerid][i] = pk
311
	}
312

313
314
315
	return nil
}

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

	return nil
}

367
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
368
369
370
371
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
372
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
373
374
375
	if !found {
		return false, nil
	}
376
377
378
379
	if err != nil {
		return true, err
	}

380
	err = xml.Unmarshal(bts, description)
381
382
383
384
385
386
	if err != nil {
		return true, err
	}

	return true, nil
}
387

388
389
390
391
392
// 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
393
}
394

395
396
397
398
399
400
401
402
403
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
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
}

431
432
433
// CopyFromAssets recursively copies the directory tree from the assets folder
// into the directory of this Configuration.
func (conf *Configuration) CopyFromAssets(parse bool) error {
434
435
436
	if conf.assets == "" {
		return nil
	}
Sietse Ringers's avatar
Sietse Ringers committed
437
	if err := fs.CopyDirectory(conf.assets, conf.Path); err != nil {
438
439
440
		return err
	}
	if parse {
441
		return conf.ParseFolder()
442
443
	}
	return nil
444
}
445

446
447
448
449
450
451
452
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
453
		filepath.Join(conf.Path, manager.ID),
454
455
456
457
	)
	return true
}

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

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
505
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
506
507
	}
	return nil
508
509
}

510
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
511
// provided its signature is valid.
512
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
513
	name := manager.ID
514
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
515
516
		return err
	}
517
518

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
519
	path := fmt.Sprintf("%s/%s", conf.Path, name)
520
521
522
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
523
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
524
525
		return err
	}
526
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
527
528
		return err
	}
529
530
531
532
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
533

534
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
535
536
537
538
539
540
}

// 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
541
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
542
543
544
545
546
547
548
549
	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
	}

550
551
552
553
554
555
	defer func() {
		if err != nil {
			_ = conf.restoreManagerSignature(index, sig)
		}
	}()

556
	if err = t.GetFile("index", index); err != nil {
557
		return
558
559
	}
	if err = t.GetFile("index.sig", sig); err != nil {
560
		return
561
	}
Sietse Ringers's avatar
Sietse Ringers committed
562
563
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
564
		return
Sietse Ringers's avatar
Sietse Ringers committed
565
566
	}
	if !valid {
567
		err = errors.New("Scheme manager signature invalid")
Sietse Ringers's avatar
Sietse Ringers committed
568
	}
569
	return
570
}
571

572
573
574
575
576
577
578
579
580
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
}
581

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

Sietse Ringers's avatar
Sietse Ringers committed
592
593
594
// 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.
595
596
func (conf *Configuration) Download(set *IrmaIdentifierSet) (downloaded *IrmaIdentifierSet, err error) {
	downloaded = &IrmaIdentifierSet{
597
598
599
600
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
601

602
	managers := make(map[SchemeManagerIdentifier]struct{})
603
	for issid := range set.Issuers {
604
605
		if _, contains := conf.Issuers[issid]; !contains {
			managers[issid.SchemeManagerIdentifier()] = struct{}{}
606
		}
Sietse Ringers's avatar
Sietse Ringers committed
607
	}
608
609
610
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
611
612
613
614
			if err != nil {
				return nil, err
			}
			if pk == nil {
615
				managers[issid.SchemeManagerIdentifier()] = struct{}{}
616
617
618
619
			}
		}
	}
	for credid := range set.CredentialTypes {
620
		if _, contains := conf.CredentialTypes[credid]; !contains {
621
			managers[credid.IssuerIdentifier().SchemeManagerIdentifier()] = struct{}{}
622
623
624
		}
	}

625
626
627
	for id := range managers {
		if err = conf.UpdateSchemeManager(id, downloaded); err != nil {
			return
628
629
		}
	}
630

631
632
633
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
634
	return
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
}

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
656
// FromString populates this index by parsing the specified string.
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
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
}

676
// parseIndex parses the index file of the specified manager.
677
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
678
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
679
	if err := fs.AssertPathExists(path); err != nil {
680
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
681
	}
Sietse Ringers's avatar
Sietse Ringers committed
682
	indexbts, err := ioutil.ReadFile(path)
683
	if err != nil {
684
		return nil, err
685
	}
686
687
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
688
689
}

690
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
691
692
693
694
695
696
697
698
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
		return err
	}
	if !valid {
		return errors.New("Scheme manager signature was invalid")
	}

699
	for file := range manager.index {
Sietse Ringers's avatar
Sietse Ringers committed
700
		exists, err := fs.PathExists(filepath.Join(conf.Path, file))
701
702
703
704
705
706
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
707
		// Don't care about the actual bytes
708
		if _, _, err := conf.ReadAuthenticatedFile(manager, file); err != nil {
709
710
711
712
713
714
715
			return err
		}
	}

	return nil
}

716
717
718
// 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.
719
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
720
	signedHash, ok := manager.index[path]
721
	if !ok {
722
		return nil, false, nil
723
724
	}

Sietse Ringers's avatar
Sietse Ringers committed
725
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
726
	if err != nil {
727
		return nil, true, err
728
729
730
731
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
732
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
733
	}
734
	return bts, true, nil
735
736
737
738
739
}

// 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).
740
741
742
743
744
745
746
747
748
749
750
751
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
752
	dir := filepath.Join(conf.Path, id.String())
753
754
755
756
757
758
759
760
761
762
763
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
	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
789
}
790
791
792
793

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828

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 {
829
		path := filepath.Join(conf.Path, filename)
830
		oldHash, known := manager.index[filename]
831
832
833
834
835
836
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
			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
}