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

109
// ParseFolder populates the current Configuration by parsing the storage path,
110
// listing the containing scheme managers, issuers and credential types.
111
func (conf *Configuration) ParseFolder() (err error) {
112
	// Init all maps
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
	var mgrerr *SchemeManagerError
Sietse Ringers's avatar
Sietse Ringers committed
121
	err = iterateSubfolders(conf.Path, func(dir string) error {
122
123
		manager := &SchemeManager{ID: filepath.Base(dir), Status: SchemeManagerStatusUnprocessed, Valid: false}
		err := conf.parseSchemeManagerFolder(dir, manager)
124
125
		if err == nil {
			return nil // OK, do next scheme manager folder
126
		}
127
128
129
130
		// 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 {
131
			conf.DisabledSchemeManagers[manager.Identifier()] = mgrerr
132
			return nil
133
		}
134
		return err // Not a SchemeManagerError? return it & halt parsing now
135
136
	})
	if err != nil {
137
		return
138
	}
139
	conf.initialized = true
140
141
142
	if mgrerr != nil {
		return mgrerr
	}
143
	return
144
145
}

146
147
148
149
150
151
152
153
154
155
156
157
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
}

158
159
// parseSchemeManagerFolder parses the entire tree of the specified scheme manager
// If err != nil then a problem occured
160
func (conf *Configuration) parseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
161
162
163
164
165
166
167
	// 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
168
169
	defer func() {
		if err != nil {
170
171
172
173
174
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
175
176
177
		}
	}()

178
179
180
181
182
	err = fs.AssertPathExists(dir + "/description.xml")
	if err != nil {
		return
	}

183
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
184
185
		manager.Status = SchemeManagerStatusInvalidIndex
		return
186
	}
187

188
	err = conf.VerifySchemeManager(manager)
189
190
191
192
193
	if err != nil {
		manager.Status = SchemeManagerStatusInvalidSignature
		return
	}

194
195
196
197
198
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
199
200
201
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
202
203
204
	}

	if manager.XMLVersion < 7 {
205
		manager.Status = SchemeManagerStatusParsingError
206
		return errors.New("Unsupported scheme manager description")
207
	}
208

209
	err = conf.parseIssuerFolders(manager, dir)
210
211
212
213
214
215
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
216
217
218
	return
}

219
220
221
222
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

223
224
225
226
// 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{}
227
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
228
			return nil, err
229
230
		}
	}
231
	return conf.publicKeys[id][counter], nil
232
233
}

234
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
235
	hash := sha256.Sum256([]byte(credid.String()))
236
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
237
238
}

239
240
241
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
242
243
244
245
	}
	return nil
}

246
// IsInitialized indicates whether this instance has successfully been initialized.
247
248
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
249
250
}

251
252
253
254
255
256
257
258
259
// 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
		}
	}
}

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

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

306
307
308
	return nil
}

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

	return nil
}

360
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
361
362
363
364
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
365
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
366
367
368
	if !found {
		return false, nil
	}
369
370
371
372
	if err != nil {
		return true, err
	}

373
	err = xml.Unmarshal(bts, description)
374
375
376
377
378
379
	if err != nil {
		return true, err
	}

	return true, nil
}
380

381
382
383
384
385
// 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
386
}
387

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

424
425
426
// CopyFromAssets recursively copies the directory tree from the assets folder
// into the directory of this Configuration.
func (conf *Configuration) CopyFromAssets(parse bool) error {
427
428
429
	if conf.assets == "" {
		return nil
	}
Sietse Ringers's avatar
Sietse Ringers committed
430
	if err := fs.CopyDirectory(conf.assets, conf.Path); err != nil {
431
432
433
		return err
	}
	if parse {
434
		return conf.ParseFolder()
435
436
	}
	return nil
437
}
438

439
440
441
442
443
444
445
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
446
		filepath.Join(conf.Path, manager.ID),
447
448
449
450
	)
	return true
}

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

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
498
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
499
500
	}
	return nil
501
502
}

Sietse Ringers's avatar
Sietse Ringers committed
503
504
// AddSchemeManager adds the specified scheme manager to this Configuration,
// provided its signature is valid.
505
func (conf *Configuration) AddSchemeManager(manager *SchemeManager) error {
506
	name := manager.ID
Sietse Ringers's avatar
Sietse Ringers committed
507
	if err := fs.EnsureDirectoryExists(fmt.Sprintf("%s/%s", conf.Path, name)); err != nil {
508
509
		return err
	}
510
511

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
512
	path := fmt.Sprintf("%s/%s", conf.Path, name)
513
514
515
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
516
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
517
518
		return err
	}
519
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
520
521
		return err
	}
522

Sietse Ringers's avatar
Sietse Ringers committed
523
	return conf.parseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
524
525
526
527
528
529
}

// 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
530
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
531
532
533
534
535
536
537
538
	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
	}

539
540
541
542
543
544
	defer func() {
		if err != nil {
			_ = conf.restoreManagerSignature(index, sig)
		}
	}()

545
	if err = t.GetFile("index", index); err != nil {
546
		return
547
548
	}
	if err = t.GetFile("index.sig", sig); err != nil {
549
		return
550
	}
Sietse Ringers's avatar
Sietse Ringers committed
551
552
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
553
		return
Sietse Ringers's avatar
Sietse Ringers committed
554
555
	}
	if !valid {
556
		err = errors.New("Scheme manager signature invalid")
Sietse Ringers's avatar
Sietse Ringers committed
557
	}
558
	return
559
}
560

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

571
572
func (conf *Configuration) restoreManagerSignature(index, sig string) error {
	if err := fs.Copy(index+".backup", index); err != nil {
573
574
		return err
	}
575
	if err := fs.Copy(sig+".backup", sig); err != nil {
576
577
578
579
580
		return err
	}
	return nil
}

Sietse Ringers's avatar
Sietse Ringers committed
581
582
583
// 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.
584
585
func (conf *Configuration) Download(set *IrmaIdentifierSet) (downloaded *IrmaIdentifierSet, err error) {
	downloaded = &IrmaIdentifierSet{
586
587
588
589
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
590

591
	managers := make(map[SchemeManagerIdentifier]struct{})
592
	for issid := range set.Issuers {
593
594
		if _, contains := conf.Issuers[issid]; !contains {
			managers[issid.SchemeManagerIdentifier()] = struct{}{}
595
		}
Sietse Ringers's avatar
Sietse Ringers committed
596
	}
597
598
599
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
600
601
602
603
			if err != nil {
				return nil, err
			}
			if pk == nil {
604
				managers[issid.SchemeManagerIdentifier()] = struct{}{}
605
606
607
608
			}
		}
	}
	for credid := range set.CredentialTypes {
609
		if _, contains := conf.CredentialTypes[credid]; !contains {
610
			managers[credid.IssuerIdentifier().SchemeManagerIdentifier()] = struct{}{}
611
612
613
		}
	}

614
615
616
	for id := range managers {
		if err = conf.UpdateSchemeManager(id, downloaded); err != nil {
			return
617
618
		}
	}
619

620
621
622
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
623
	return
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
}

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
645
// FromString populates this index by parsing the specified string.
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
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
}

665
// parseIndex parses the index file of the specified manager.
666
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
667
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
668
	if err := fs.AssertPathExists(path); err != nil {
669
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
670
	}
Sietse Ringers's avatar
Sietse Ringers committed
671
	indexbts, err := ioutil.ReadFile(path)
672
	if err != nil {
673
		return nil, err
674
	}
675
676
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
677
678
}

679
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
680
681
682
683
684
685
686
687
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
		return err
	}
	if !valid {
		return errors.New("Scheme manager signature was invalid")
	}

688
	for file := range manager.index {
Sietse Ringers's avatar
Sietse Ringers committed
689
		exists, err := fs.PathExists(filepath.Join(conf.Path, file))
690
691
692
693
694
695
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
696
		// Don't care about the actual bytes
697
		if _, _, err := conf.ReadAuthenticatedFile(manager, file); err != nil {
698
699
700
701
702
703
704
			return err
		}
	}

	return nil
}

705
706
707
// 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.
708
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
709
	signedHash, ok := manager.index[path]
710
	if !ok {
711
		return nil, false, nil
712
713
	}

Sietse Ringers's avatar
Sietse Ringers committed
714
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
715
	if err != nil {
716
		return nil, true, err
717
718
719
720
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
721
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
722
	}
723
	return bts, true, nil
724
725
726
727
728
}

// 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).
729
730
731
732
733
734
735
736
737
738
739
740
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
741
	dir := filepath.Join(conf.Path, id.String())
742
743
744
745
746
747
748
749
750
751
752
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
	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
778
}
779
780
781
782

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
783
784
785
786
787
788
789
790
791
792
793
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
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851

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 {
		oldHash, known := manager.index[filename]
		if known && oldHash.Equal(newHash) {
			continue // nothing to do, we already have this file
		}
		// Ensure that the folder in which to write the file exists
		path := filepath.Join(conf.Path, filename)
		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
}