irmaconfig.go 25 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
287
288
289
290
291
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

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

319
320
321
	return nil
}

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

	return nil
}

373
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
374
375
376
377
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
378
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
379
380
381
	if !found {
		return false, nil
	}
382
383
384
385
	if err != nil {
		return true, err
	}

386
	err = xml.Unmarshal(bts, description)
387
388
389
390
391
392
	if err != nil {
		return true, err
	}

	return true, nil
}
393

394
395
396
397
398
// 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
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
431
432
433
434
435
436
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
}

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

452
453
454
455
456
457
458
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
459
		filepath.Join(conf.Path, manager.ID),
460
461
462
463
	)
	return true
}

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

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
511
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
512
513
	}
	return nil
514
515
}

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

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

540
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
541
542
543
544
545
546
}

// 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
547
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
548
549
550
551
552
553
554
555
	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
	}

556
557
558
559
560
561
	defer func() {
		if err != nil {
			_ = conf.restoreManagerSignature(index, sig)
		}
	}()

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

578
579
580
581
582
583
584
585
586
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
}
587

588
589
func (conf *Configuration) restoreManagerSignature(index, sig string) error {
	if err := fs.Copy(index+".backup", index); err != nil {
590
591
		return err
	}
592
	if err := fs.Copy(sig+".backup", sig); err != nil {
593
594
595
596
597
		return err
	}
	return nil
}

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

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

631
632
633
	for id := range managers {
		if err = conf.UpdateSchemeManager(id, downloaded); err != nil {
			return
634
635
		}
	}
636

637
638
639
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
640
	return
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
}

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
662
// FromString populates this index by parsing the specified string.
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
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
}

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

696
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
697
698
699
700
701
702
703
704
	valid, err := conf.VerifySignature(manager.Identifier())
	if err != nil {
		return err
	}
	if !valid {
		return errors.New("Scheme manager signature was invalid")
	}

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

	return nil
}

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

Sietse Ringers's avatar
Sietse Ringers committed
731
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
732
	if err != nil {
733
		return nil, true, err
734
735
736
737
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
738
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
739
	}
740
	return bts, true, nil
741
742
743
744
745
}

// 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).
746
747
748
749
750
751
752
753
754
755
756
757
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
758
	dir := filepath.Join(conf.Path, id.String())
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
789
790
791
792
793
794
	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
795
}
796
797
798
799

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
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

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 {
835
		path := filepath.Join(conf.Path, filename)
836
		oldHash, known := manager.index[filename]
837
838
839
840
841
842
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
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
868
869
870
871
872
873
			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
}