irmaconfig.go 40.4 KB
Newer Older
1
package irma
2
3

import (
4
	"crypto/rsa"
5
6
7
8
9
	"encoding/base64"
	"encoding/xml"
	"io/ioutil"
	"os"
	"path/filepath"
10
	"reflect"
11
	"regexp"
12
	"strconv"
13
	"time"
14

15
16
	"crypto/sha256"

17
18
19
20
	"fmt"

	"strings"

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

	"bytes"

	"encoding/hex"

	"crypto/ecdsa"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
31
	gobig "math/big"
32

33
	"github.com/dgrijalva/jwt-go"
34
	"github.com/go-errors/errors"
35
36
	"github.com/privacybydesign/gabi"
	"github.com/privacybydesign/gabi/big"
37
	"github.com/privacybydesign/irmago/internal/fs"
38
39
)

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

Sietse Ringers's avatar
Sietse Ringers committed
48
49
50
	// Path to the irma_configuration folder that this instance represents
	Path string

51
52
53
	// 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
54

55
56
	Warnings []string

57
	kssPublicKeys map[SchemeManagerIdentifier]map[int]*rsa.PublicKey
58
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
59
	reverseHashes map[string]CredentialTypeIdentifier
60
	initialized   bool
61
	assets        string
62
	readOnly      bool
63
64
}

Sietse Ringers's avatar
Sietse Ringers committed
65
66
// ConfigurationFileHash encodes the SHA256 hash of an authenticated
// file under a scheme manager within the configuration folder.
67
68
type ConfigurationFileHash []byte

Sietse Ringers's avatar
Sietse Ringers committed
69
70
// SchemeManagerIndex is a (signed) list of files under a scheme manager
// along with their SHA266 hash
71
72
type SchemeManagerIndex map[string]ConfigurationFileHash

73
74
type SchemeManagerStatus string

75
76
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
77
	Status  SchemeManagerStatus
78
79
80
	Err     error
}

81
82
83
84
85
86
87
const (
	SchemeManagerStatusValid               = SchemeManagerStatus("Valid")
	SchemeManagerStatusUnprocessed         = SchemeManagerStatus("Unprocessed")
	SchemeManagerStatusInvalidIndex        = SchemeManagerStatus("InvalidIndex")
	SchemeManagerStatusInvalidSignature    = SchemeManagerStatus("InvalidSignature")
	SchemeManagerStatusParsingError        = SchemeManagerStatus("ParsingError")
	SchemeManagerStatusContentParsingError = SchemeManagerStatus("ContentParsingError")
88

89
90
	pubkeyPattern  = "%s/%s/%s/PublicKeys/*.xml"
	privkeyPattern = "%s/%s/%s/PrivateKeys/*.xml"
91
92
)

93
94
95
96
func (sme SchemeManagerError) Error() string {
	return fmt.Sprintf("Error parsing scheme manager %s: %s", sme.Manager.Name(), sme.Err.Error())
}

97
// newConfiguration returns a new configuration. After this
98
// ParseFolder() should be called to parse the specified path.
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
func NewConfiguration(path string) (*Configuration, error) {
	return newConfiguration(path, "")
}

func NewConfigurationReadOnly(path string) (*Configuration, error) {
	conf, err := newConfiguration(path, "")
	if err != nil {
		return nil, err
	}
	conf.readOnly = true
	return conf, nil
}

func NewConfigurationFromAssets(path, assets string) (*Configuration, error) {
	return newConfiguration(path, assets)
}

func newConfiguration(path string, assets string) (conf *Configuration, err error) {
117
	conf = &Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
118
		Path:   path,
119
		assets: assets,
120
	}
121

122
123
124
125
	if conf.assets != "" { // If an assets folder is specified, then it must exist
		if err = fs.AssertPathExists(conf.assets); err != nil {
			return nil, errors.WrapPrefix(err, "Nonexistent assets folder specified", 0)
		}
126
	}
127
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
128
129
		return nil, err
	}
130

131
132
133
	// Init all maps
	conf.clear()

134
135
136
	return
}

137
func (conf *Configuration) clear() {
138
139
140
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
141
	conf.AttributeTypes = make(map[AttributeTypeIdentifier]*AttributeType)
142
	conf.DisabledSchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManagerError)
143
	conf.kssPublicKeys = make(map[SchemeManagerIdentifier]map[int]*rsa.PublicKey)
144
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
145
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
146
147
148
149
150
151
152
}

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

154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
	// Copy any new or updated scheme managers out of the assets into storage
	if conf.assets != "" {
		err = iterateSubfolders(conf.assets, func(dir string) error {
			scheme := NewSchemeManagerIdentifier(filepath.Base(dir))
			uptodate, err := conf.isUpToDate(scheme)
			if err != nil {
				return err
			}
			if !uptodate {
				_, err = conf.CopyManagerFromAssets(scheme)
			}
			return err
		})
		if err != nil {
			return err
		}
	}

	// Parse scheme managers in storage
173
	var mgrerr *SchemeManagerError
Sietse Ringers's avatar
Sietse Ringers committed
174
	err = iterateSubfolders(conf.Path, func(dir string) error {
175
176
		manager := NewSchemeManager(filepath.Base(dir))
		err := conf.ParseSchemeManagerFolder(dir, manager)
177
178
		if err == nil {
			return nil // OK, do next scheme manager folder
179
		}
180
181
182
183
		// 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 {
184
			conf.DisabledSchemeManagers[manager.Identifier()] = mgrerr
185
			return nil
186
		}
187
		return err // Not a SchemeManagerError? return it & halt parsing now
188
189
	})
	if err != nil {
190
		return
191
	}
192
	conf.initialized = true
193
194
195
	if mgrerr != nil {
		return mgrerr
	}
196
	return
197
198
}

199
200
201
202
203
204
205
// ParseOrRestoreFolder parses the irma_configuration folder, and when possible attempts to restore
// any broken scheme managers from their remote.
// Any error encountered during parsing is considered recoverable only if it is of type *SchemeManagerError;
// In this case the scheme in which it occured is downloaded from its remote and re-parsed.
// If any other error is encountered at any time, it is returned immediately.
// If no error is returned, parsing and possibly restoring has been succesfull, and there should be no
// disabled scheme managers.
206
207
func (conf *Configuration) ParseOrRestoreFolder() error {
	err := conf.ParseFolder()
208
209
210
	// Only in case of a *SchemeManagerError might we be able to recover
	if _, isSchemeMgrErr := err.(*SchemeManagerError); !isSchemeMgrErr {
		return err
211
	}
212
213
214
	if err != nil && (conf.assets == "" || conf.readOnly) {
		return err
	}
215
216

	for id := range conf.DisabledSchemeManagers {
217
218
219
220
221
222
223
224
225
		if err = conf.ReinstallSchemeManager(conf.SchemeManagers[id]); err == nil {
			continue
		}
		if _, err = conf.CopyManagerFromAssets(id); err != nil {
			return err // File system error, too serious, bail out now
		}
		name := id.String()
		if err = conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), NewSchemeManager(name)); err == nil {
			delete(conf.DisabledSchemeManagers, id)
226
		}
227
	}
228

229
230
231
	return err
}

232
// ParseSchemeManagerFolder parses the entire tree of the specified scheme manager
233
// If err != nil then a problem occured
234
func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
235
236
237
238
239
240
241
	// 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
242
243
	defer func() {
		if err != nil {
244
245
246
247
248
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
249
250
251
		}
	}()

252
253
	// Verify signature and read scheme manager description
	if err = conf.VerifySignature(manager.Identifier()); err != nil {
254
255
		return
	}
256
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
257
258
		manager.Status = SchemeManagerStatusInvalidIndex
		return
259
	}
260
261
262
263
264
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
265
266
267
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
268
	}
269
270
	if err = conf.checkScheme(manager, dir); err != nil {
		return
271
	}
272
273
274
275
276
277
278

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

280
	// Read timestamp indicating time of last modification
281
282
283
	ts, exists, err := readTimestamp(dir + "/timestamp")
	if err != nil || !exists {
		return errors.WrapPrefix(err, "Could not read scheme manager timestamp", 0)
284
	}
285
	manager.Timestamp = *ts
286

287
	// Parse contained issuers and credential types
288
	err = conf.parseIssuerFolders(manager, dir)
289
290
291
292
293
294
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
295
296
297
	return
}

298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// relativePath returns, given a outer path that contains the inner path,
// the relative path between outer an inner, which is such that
// outer/returnvalue refers to inner.
func relativePath(outer string, inner string) (string, error) {
	// Take Abs() of both paths to ensure that we don't fail on e.g.
	// outer = "./foo" and inner = "foo/bar"
	outerAbs, err := filepath.Abs(outer)
	if err != nil {
		return "", err
	}
	innerAbs, err := filepath.Abs(inner)
	if err != nil {
		return "", err
	}
	if !strings.HasPrefix(innerAbs, outerAbs) {
		return "", errors.New("inner path is not contained in outer path")
	}

	return innerAbs[len(outerAbs)+1:], nil
317
318
}

319
320
// 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) {
321
322
323
324
325
326
327
328
329
330
331
	var haveIssuer, haveKey bool
	var err error
	_, haveIssuer = conf.publicKeys[id]
	if haveIssuer {
		_, haveKey = conf.publicKeys[id][counter]
	}

	// If we have not seen this issuer or key before in conf.publicKeys,
	// try to parse the public key folder; new keys might have been put there since we last parsed it
	if !haveIssuer || !haveKey {
		if err = conf.parseKeysFolder(id); err != nil {
332
			return nil, err
333
334
		}
	}
335
	return conf.publicKeys[id][counter], nil
336
337
}

338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
// KeyshareServerKeyFunc returns a function that returns the public key with which to verify a keyshare server JWT,
// suitable for passing to jwt.Parse() and jwt.ParseWithClaims().
func (conf *Configuration) KeyshareServerKeyFunc(scheme SchemeManagerIdentifier) func(t *jwt.Token) (interface{}, error) {
	return func(t *jwt.Token) (i interface{}, e error) {
		var kid int
		if kidstr, ok := t.Header["kid"].(string); ok {
			var err error
			if kid, err = strconv.Atoi(kidstr); err != nil {
				return nil, err
			}
		}
		return conf.KeyshareServerPublicKey(scheme, kid)
	}
}

// KeyshareServerPublicKey returns the i'th public key of the specified scheme.
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
func (conf *Configuration) KeyshareServerPublicKey(scheme SchemeManagerIdentifier, i int) (*rsa.PublicKey, error) {
	if _, contains := conf.kssPublicKeys[scheme]; !contains {
		conf.kssPublicKeys[scheme] = make(map[int]*rsa.PublicKey)
	}
	if _, contains := conf.kssPublicKeys[scheme][i]; !contains {
		pkbts, err := ioutil.ReadFile(filepath.Join(conf.Path, scheme.Name(), fmt.Sprintf("kss-%d.pem", i)))
		if err != nil {
			return nil, err
		}
		pkblk, _ := pem.Decode(pkbts)
		genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
		if err != nil {
			return nil, err
		}
		pk, ok := genericPk.(*rsa.PublicKey)
		if !ok {
			return nil, errors.New("Invalid keyshare server public key")
		}
		conf.kssPublicKeys[scheme][i] = pk
	}
	return conf.kssPublicKeys[scheme][i], nil
}

377
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
378
	hash := sha256.Sum256([]byte(credid.String()))
379
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
380
381
}

382
383
384
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
385
386
387
388
	}
	return nil
}

389
// IsInitialized indicates whether this instance has successfully been initialized.
390
391
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
392
393
}

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

403
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
404
405
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
406
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
407
408
409
		if err != nil {
			return err
		}
410
411
		if !exists {
			return nil
412
		}
413
414
415
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
416

417
		if err = conf.checkIssuer(manager, issuer, dir); err != nil {
418
419
420
			return err
		}

421
		conf.Issuers[issuer.Identifier()] = issuer
422
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
423
		return conf.parseCredentialsFolder(manager, issuer, dir+"/Issues/")
424
425
426
	})
}

427
428
429
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
	name := id.String()
	for iss := range conf.Issuers {
		if iss.Root() == name {
			delete(conf.Issuers, iss)
		}
	}
	for iss := range conf.publicKeys {
		if iss.Root() == name {
			delete(conf.publicKeys, iss)
		}
	}
	for cred := range conf.CredentialTypes {
		if cred.Root() == name {
			delete(conf.CredentialTypes, cred)
		}
	}
446
447
448
449
	if !conf.readOnly {
		return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
	}
	return nil
450
451
}

452
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
453
454
func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
	manager := conf.SchemeManagers[issuerid.SchemeManagerIdentifier()]
455
	conf.publicKeys[issuerid] = map[int]*gabi.PublicKey{}
456
	path := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
457
458
459
460
461
462
463
464
465
466
	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 {
467
			return err
468
		}
469
470
471
472
473
		relativepath, err := relativePath(conf.Path, file)
		if err != nil {
			return err
		}
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativepath)
474
		if err != nil || !found {
475
476
477
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
478
479
480
		if err != nil {
			return err
		}
481
482
483
		if int(pk.Counter) != i {
			return errors.Errorf("Public key %s of issuer %s has wrong <Counter>", file, issuerid.String())
		}
484
		pk.Issuer = issuerid.String()
485
		conf.publicKeys[issuerid][i] = pk
486
	}
487

488
489
490
	return nil
}

491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
func (conf *Configuration) PublicKeyIndices(issuerid IssuerIdentifier) (i []int, err error) {
	return conf.matchKeyPattern(issuerid, pubkeyPattern)
}

func (conf *Configuration) matchKeyPattern(issuerid IssuerIdentifier, pattern string) (i []int, err error) {
	pkpath := fmt.Sprintf(pattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
	files, err := filepath.Glob(pkpath)
	if err != nil {
		return
	}
	for _, file := range files {
		var count int
		base := filepath.Base(file)
		if count, err = strconv.Atoi(base[:len(base)-4]); err != nil {
			return
		}
		i = append(i, count)
	}
	sort.Ints(i)
	return
}

513
// parse $schememanager/$issuer/Issues/*/description.xml
514
515
516
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, issuer *Issuer, path string) error {
	var foundcred bool
	err := iterateSubfolders(path, func(dir string) error {
517
		cred := &CredentialType{}
518
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
519
520
521
		if err != nil {
			return err
		}
522
523
524
		if !exists {
			return nil
		}
525
		if err = conf.checkCredentialType(manager, issuer, cred, dir); err != nil {
526
527
528
529
			return err
		}
		foundcred = true
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
530
		credid := cred.Identifier()
531
532
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
533
		for index, attr := range cred.AttributeTypes {
534
535
536
537
			attr.Index = index
			attr.SchemeManagerID = cred.SchemeManagerID
			attr.IssuerID = cred.IssuerID
			attr.CredentialTypeID = cred.ID
538
			conf.AttributeTypes[attr.GetAttributeTypeIdentifier()] = attr
539
		}
540
541
		return nil
	})
542
543
544
545
546
547
	if !foundcred {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no credential types", issuer.Identifier().String()))
	}
	return err
}

548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
// 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
		}
565
566
567
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
568
569
570
571
572
573
574
575
576
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

577
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
578
579
580
581
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

582
583
584
585
586
	relativepath, err := relativePath(conf.Path, path)
	if err != nil {
		return false, err
	}
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativepath)
587
588
589
	if !found {
		return false, nil
	}
590
591
592
593
	if err != nil {
		return true, err
	}

594
	err = xml.Unmarshal(bts, description)
595
596
597
598
599
600
	if err != nil {
		return true, err
	}

	return true, nil
}
601

602
603
604
605
606
// 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
607
}
608

609
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
610
	if conf.assets == "" || conf.readOnly {
611
612
		return true, nil
	}
613
614
615
616
	name := scheme.String()
	newTime, exists, err := readTimestamp(filepath.Join(conf.assets, name, "timestamp"))
	if err != nil || !exists {
		return true, errors.WrapPrefix(err, "Could not read asset timestamp of scheme "+name, 0)
617
	}
618
619
620
621
	// The storage version of the manager does not need to have a timestamp. If it does not, it is outdated.
	oldTime, exists, err := readTimestamp(filepath.Join(conf.Path, name, "timestamp"))
	if err != nil {
		return true, err
622
	}
623
	return exists && !newTime.After(*oldTime), nil
624
625
}

626
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
627
	if conf.assets == "" || conf.readOnly {
628
		return false, nil
629
	}
630
631
632
633
634
	// Remove old version; we want an exact copy of the assets version
	// not a merge of the assets version and the storage version
	name := scheme.String()
	if err := os.RemoveAll(filepath.Join(conf.Path, name)); err != nil {
		return false, err
635
	}
636
637
638
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
639
640
641
	)
}

642
643
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
644
func DownloadSchemeManager(url string) (*SchemeManager, error) {
645
646
647
648
649
650
651
652
653
	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")]
	}
654
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
655
656
657
	if err != nil {
		return nil, err
	}
658
	manager := NewSchemeManager("")
659
660
661
662
663
664
665
666
	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
667
668
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
669
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
670
	// Remove everything falling under the manager's responsibility
671
	for credid := range conf.CredentialTypes {
672
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
673
			delete(conf.CredentialTypes, credid)
674
675
		}
	}
676
	for issid := range conf.Issuers {
677
		if issid.SchemeManagerIdentifier() == id {
678
			delete(conf.Issuers, issid)
679
680
		}
	}
681
	for issid := range conf.publicKeys {
682
		if issid.SchemeManagerIdentifier() == id {
683
			delete(conf.publicKeys, issid)
684
685
		}
	}
686
	delete(conf.SchemeManagers, id)
687

688
	if fromStorage || !conf.readOnly {
Sietse Ringers's avatar
Sietse Ringers committed
689
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
690
691
	}
	return nil
692
693
}

694
func (conf *Configuration) ReinstallSchemeManager(manager *SchemeManager) (err error) {
695
696
697
698
	if conf.readOnly {
		return errors.New("cannot install scheme into a read-only configuration")
	}

699
700
701
702
703
704
705
706
707
	// Check if downloading stuff from the remote works before we uninstall the specified manager:
	// If we can't download anything we should keep the broken version
	manager, err = DownloadSchemeManager(manager.URL)
	if err != nil {
		return
	}
	if err = conf.DeleteSchemeManager(manager.Identifier()); err != nil {
		return
	}
708
	err = conf.InstallSchemeManager(manager, nil)
709
710
711
	return
}

712
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
713
// provided its signature is valid.
714
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager, publickey []byte) error {
715
716
717
718
	if conf.readOnly {
		return errors.New("cannot install scheme into a read-only configuration")
	}

719
	name := manager.ID
720
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
721
722
		return err
	}
723
724

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
725
	path := fmt.Sprintf("%s/%s", conf.Path, name)
726
727
728
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
729
730
731
732
733
734
735
736
	if publickey != nil {
		if err := fs.SaveFile(path+"/pk.pem", publickey); err != nil {
			return err
		}
	} else {
		if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
			return err
		}
737
	}
738
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
739
740
		return err
	}
741
742
743
744
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
745

746
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
747
748
749
750
751
}

// 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) {
752
753
754
755
	if conf.readOnly {
		return errors.New("cannot download into a read-only configuration")
	}

756
	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
757
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
758
759
760
761
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
762
		return
763
764
	}
	if err = t.GetFile("index.sig", sig); err != nil {
765
		return
766
	}
767
	err = conf.VerifySignature(manager.Identifier())
768
	return
769
}
770

Sietse Ringers's avatar
Sietse Ringers committed
771
772
773
// 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.
774
func (conf *Configuration) Download(session SessionRequest) (downloaded *IrmaIdentifierSet, err error) {
775
776
777
	if conf.readOnly {
		return nil, errors.New("cannot download into a read-only configuration")
	}
778
	managers := make(map[string]struct{}) // Managers that we must update
779
	downloaded = &IrmaIdentifierSet{
780
781
782
783
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
784

785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
	// Calculate which scheme managers must be updated
	if err = conf.checkIssuers(session.Identifiers(), managers); err != nil {
		return
	}
	if err = conf.checkCredentialTypes(session, managers); err != nil {
		return
	}

	// Update the scheme managers found above and parse them, if necessary
	for id := range managers {
		if err = conf.UpdateSchemeManager(NewSchemeManagerIdentifier(id), downloaded); err != nil {
			return
		}
	}
	if !downloaded.Empty() {
		return downloaded, conf.ParseFolder()
	}
	return
}

805
func (conf *Configuration) checkCredentialTypes(session SessionRequest, managers map[string]struct{}) error {
806
807
808
809
810
811
812
813
	var disjunctions AttributeDisjunctionList
	var typ *CredentialType
	var contains bool

	switch s := session.(type) {
	case *IssuanceRequest:
		for _, credreq := range s.Credentials {
			// First check if we have this credential type
814
			typ, contains = conf.CredentialTypes[credreq.CredentialTypeID]
815
816
817
818
819
820
821
822
823
824
			if !contains {
				managers[credreq.CredentialTypeID.Root()] = struct{}{}
				continue
			}
			newAttrs := make(map[string]string)
			for k, v := range credreq.Attributes {
				newAttrs[k] = v
			}
			// For each of the attributes in the credentialtype, see if it is present; if so remove it from newAttrs
			// If not, check that it is optional; if not the credentialtype must be updated
825
			for _, attrtyp := range typ.AttributeTypes {
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
				_, contains = newAttrs[attrtyp.ID]
				if !contains && !attrtyp.IsOptional() {
					managers[credreq.CredentialTypeID.Root()] = struct{}{}
					break
				}
				delete(newAttrs, attrtyp.ID)
			}
			// If there is anything left in newAttrs, then these are attributes that are not in the credentialtype
			if len(newAttrs) > 0 {
				managers[credreq.CredentialTypeID.Root()] = struct{}{}
			}
		}
		disjunctions = s.Disclose
	case *DisclosureRequest:
		disjunctions = s.Content
	case *SignatureRequest:
		disjunctions = s.Content
	}

	for _, disjunction := range disjunctions {
		for _, attrid := range disjunction.Attributes {
			credid := attrid.CredentialTypeIdentifier()
			if typ, contains = conf.CredentialTypes[credid]; !contains {
				managers[credid.Root()] = struct{}{}
850
				continue
851
			}
Sietse Ringers's avatar
Sietse Ringers committed
852
			if !attrid.IsCredential() && !typ.ContainsAttribute(attrid) {
853
854
855
856
857
858
859
860
861
				managers[credid.Root()] = struct{}{}
			}
		}
	}

	return nil
}

func (conf *Configuration) checkIssuers(set *IrmaIdentifierSet, managers map[string]struct{}) error {
862
	for issid := range set.Issuers {
863
		if _, contains := conf.Issuers[issid]; !contains {
864
			managers[issid.Root()] = struct{}{}
865
		}
Sietse Ringers's avatar
Sietse Ringers committed
866
	}
867
868
869
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
870
			if err != nil {
871
				return err
Sietse Ringers's avatar
Sietse Ringers committed
872
873
			}
			if pk == nil {
874
				managers[issid.Root()] = struct{}{}
875
876
877
			}
		}
	}
878
	return nil
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
}

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
900
// FromString populates this index by parsing the specified string.
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
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
}

920
// parseIndex parses the index file of the specified manager.
921
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
922
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
923
	if err := fs.AssertPathExists(path); err != nil {
924
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
925
	}
Sietse Ringers's avatar
Sietse Ringers committed
926
	indexbts, err := ioutil.ReadFile(path)
927
	if err != nil {
928
		return nil, err
929
	}
930
931
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
932
933
}

934
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
935
	err := conf.VerifySignature(manager.Identifier())
936
937
938
939
	if err != nil {
		return err
	}

940
	var exists bool
941
	for file := range manager.index {
942
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
943
944
945
946
947
948
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
949
		// Don't care about the actual bytes
950
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
951
952
953
954
955
956
957
			return err
		}
	}

	return nil
}

958
959
960
// 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.
961
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
962
	signedHash, ok := manager.index[filepath.ToSlash(path)]
963
	if !ok {
964
		return nil, false, nil
965
966
	}

Sietse Ringers's avatar
Sietse Ringers committed
967
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
968
	if err != nil {
969
		return nil, true, err
970
971
972
973
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
974
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
975
	}
976
	return bts, true, nil
977
978
979
980
981
}

// 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).
982
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
983
984
985
986
987
988
989
990
991
992
	defer func() {
		if r := recover(); r != nil {
			if e, ok := r.(error); ok {
				err = errors.Errorf("Scheme manager index signature failed to verify: %s", e.Error())
			} else {
				err = errors.New("Scheme manager index signature failed to verify")
			}
		}
	}()

Sietse Ringers's avatar
Sietse Ringers committed
993
	dir := filepath.Join(conf.Path, id.String())
994
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
995
		return errors.New("Missing scheme manager index file, signature, or public key")
996
997
998
999
1000
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
1001
		return err
1002
1003
1004
1005
1006
1007
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
1008
		return err
1009
	}
1010
	pk, err := ParsePemEcdsaPublicKey(pkbts)
1011
	if err != nil {
1012
		return err
1013
1014
1015
1016
1017
	}

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

	// Verify signature
1024
1025
1026
1027
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
1028
}
1029

1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
func ParsePemEcdsaPublicKey(pkbts []byte) (*ecdsa.PublicKey, error) {
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
		return nil, err
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
		return nil, errors.New("Invalid scheme manager public key")
	}
	return pk, nil
}

1043
1044
1045
func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056

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) {
1057
1058
1059
	if conf.readOnly {
		return errors.New("cannot update a read-only configuration")
	}
1060
1061
1062
1063
1064
	manager, contains := conf.SchemeManagers[id]
	if !contains {
		return errors.Errorf("Cannot update unknown scheme manager %s", id)
	}

1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
	// Check remote timestamp and see if we have to do anything
	transport := NewHTTPTransport(manager.URL + "/")
	timestampBts, err := transport.GetBytes("timestamp")
	if err != nil {
		return err
	}
	timestamp, err := parseTimestamp(timestampBts)
	if err != nil {
		return err
	}
1075
	if !manager.Timestamp.Before(*timestamp) {
1076
1077
1078
		return nil
	}

1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
	// 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