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

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

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

72
73
type SchemeManagerStatus string

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

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

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

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

96
// NewConfiguration returns a new configuration. After this
97
// ParseFolder() should be called to parse the specified path.
98
99
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
	conf = &Configuration{
Sietse Ringers's avatar
Sietse Ringers committed
100
		Path:   path,
101
		assets: assets,
102
	}
103

104
105
106
107
	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)
		}
108
	}
109
	if err = fs.EnsureDirectoryExists(conf.Path); err != nil {
110
111
		return nil, err
	}
112

113
114
115
	// Init all maps
	conf.clear()

116
117
118
	return
}

119
func (conf *Configuration) clear() {
120
121
122
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
123
	conf.AttributeTypes = make(map[AttributeTypeIdentifier]*AttributeType)
124
	conf.DisabledSchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManagerError)
125
	conf.kssPublicKeys = make(map[SchemeManagerIdentifier]map[int]*rsa.PublicKey)
126
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
127
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
128
129
130
131
132
133
134
}

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

136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
	// 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
155
	var mgrerr *SchemeManagerError
Sietse Ringers's avatar
Sietse Ringers committed
156
	err = iterateSubfolders(conf.Path, func(dir string) error {
157
158
		manager := NewSchemeManager(filepath.Base(dir))
		err := conf.ParseSchemeManagerFolder(dir, manager)
159
160
		if err == nil {
			return nil // OK, do next scheme manager folder
161
		}
162
163
164
165
		// 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 {
166
			conf.DisabledSchemeManagers[manager.Identifier()] = mgrerr
167
			return nil
168
		}
169
		return err // Not a SchemeManagerError? return it & halt parsing now
170
171
	})
	if err != nil {
172
		return
173
	}
174
	conf.initialized = true
175
176
177
	if mgrerr != nil {
		return mgrerr
	}
178
	return
179
180
}

181
182
183
184
185
186
187
// 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.
188
189
func (conf *Configuration) ParseOrRestoreFolder() error {
	err := conf.ParseFolder()
190
191
192
	// Only in case of a *SchemeManagerError might we be able to recover
	if _, isSchemeMgrErr := err.(*SchemeManagerError); !isSchemeMgrErr {
		return err
193
	}
194
195

	for id := range conf.DisabledSchemeManagers {
196
197
198
199
200
201
202
203
204
		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)
205
		}
206
	}
207

208
209
210
	return err
}

211
// ParseSchemeManagerFolder parses the entire tree of the specified scheme manager
212
// If err != nil then a problem occured
213
func (conf *Configuration) ParseSchemeManagerFolder(dir string, manager *SchemeManager) (err error) {
214
215
216
217
218
219
220
	// 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
221
222
	defer func() {
		if err != nil {
223
224
225
226
227
			err = &SchemeManagerError{
				Manager: manager.Identifier(),
				Err:     err,
				Status:  manager.Status,
			}
228
229
230
		}
	}()

231
232
	// Verify signature and read scheme manager description
	if err = conf.VerifySignature(manager.Identifier()); err != nil {
233
234
		return
	}
235
	if manager.index, err = conf.parseIndex(filepath.Base(dir), manager); err != nil {
236
237
		manager.Status = SchemeManagerStatusInvalidIndex
		return
238
	}
239
240
241
242
243
	exists, err := conf.pathToDescription(manager, dir+"/description.xml", manager)
	if !exists {
		manager.Status = SchemeManagerStatusParsingError
		return errors.New("Scheme manager description not found")
	}
244
245
246
	if err != nil {
		manager.Status = SchemeManagerStatusParsingError
		return
247
	}
248
249
	if err = conf.checkScheme(manager, dir); err != nil {
		return
250
	}
251
252
253
254
255
256
257

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

259
	// Read timestamp indicating time of last modification
260
261
262
	ts, exists, err := readTimestamp(dir + "/timestamp")
	if err != nil || !exists {
		return errors.WrapPrefix(err, "Could not read scheme manager timestamp", 0)
263
	}
264
	manager.Timestamp = *ts
265

266
	// Parse contained issuers and credential types
267
	err = conf.parseIssuerFolders(manager, dir)
268
269
270
271
272
273
	if err != nil {
		manager.Status = SchemeManagerStatusContentParsingError
		return
	}
	manager.Status = SchemeManagerStatusValid
	manager.Valid = true
274
275
276
	return
}

277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// 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
296
297
}

298
299
// 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) {
300
301
302
303
304
305
306
307
308
309
310
	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 {
311
			return nil, err
312
313
		}
	}
314
	return conf.publicKeys[id][counter], nil
315
316
}

317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// 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.
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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
}

356
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
357
	hash := sha256.Sum256([]byte(credid.String()))
358
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
359
360
}

361
362
363
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
364
365
366
367
	}
	return nil
}

368
// IsInitialized indicates whether this instance has successfully been initialized.
369
370
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
371
372
}

373
374
375
376
377
378
379
380
381
// 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
		}
	}
}

382
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
383
384
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
385
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
386
387
388
		if err != nil {
			return err
		}
389
390
		if !exists {
			return nil
391
		}
392
393
394
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
395

396
		if err = conf.checkIssuer(manager, issuer, dir); err != nil {
397
398
399
			return err
		}

400
		conf.Issuers[issuer.Identifier()] = issuer
401
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
402
		return conf.parseCredentialsFolder(manager, issuer, dir+"/Issues/")
403
404
405
	})
}

406
407
408
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
	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)
		}
	}
425
426
427
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

428
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
429
430
func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
	manager := conf.SchemeManagers[issuerid.SchemeManagerIdentifier()]
431
	conf.publicKeys[issuerid] = map[int]*gabi.PublicKey{}
432
	path := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
433
434
435
436
437
438
439
440
441
442
	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 {
443
			return err
444
		}
445
446
447
448
449
		relativepath, err := relativePath(conf.Path, file)
		if err != nil {
			return err
		}
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativepath)
450
		if err != nil || !found {
451
452
453
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
454
455
456
		if err != nil {
			return err
		}
457
458
459
		if int(pk.Counter) != i {
			return errors.Errorf("Public key %s of issuer %s has wrong <Counter>", file, issuerid.String())
		}
460
		pk.Issuer = issuerid.String()
461
		conf.publicKeys[issuerid][i] = pk
462
	}
463

464
465
466
	return nil
}

467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
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
}

489
// parse $schememanager/$issuer/Issues/*/description.xml
490
491
492
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, issuer *Issuer, path string) error {
	var foundcred bool
	err := iterateSubfolders(path, func(dir string) error {
493
		cred := &CredentialType{}
494
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
495
496
497
		if err != nil {
			return err
		}
498
499
500
		if !exists {
			return nil
		}
501
		if err = conf.checkCredentialType(manager, issuer, cred, dir); err != nil {
502
503
504
505
			return err
		}
		foundcred = true
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
506
		credid := cred.Identifier()
507
508
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
509
		for index, attr := range cred.AttributeTypes {
510
511
512
513
			attr.Index = index
			attr.SchemeManagerID = cred.SchemeManagerID
			attr.IssuerID = cred.IssuerID
			attr.CredentialTypeID = cred.ID
514
			conf.AttributeTypes[attr.GetAttributeTypeIdentifier()] = attr
515
		}
516
517
		return nil
	})
518
519
520
521
522
523
	if !foundcred {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no credential types", issuer.Identifier().String()))
	}
	return err
}

524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
// 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
		}
541
542
543
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
544
545
546
547
548
549
550
551
552
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

553
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
554
555
556
557
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

558
559
560
561
562
	relativepath, err := relativePath(conf.Path, path)
	if err != nil {
		return false, err
	}
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativepath)
563
564
565
	if !found {
		return false, nil
	}
566
567
568
569
	if err != nil {
		return true, err
	}

570
	err = xml.Unmarshal(bts, description)
571
572
573
574
575
576
	if err != nil {
		return true, err
	}

	return true, nil
}
577

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

585
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
586
587
588
	if conf.assets == "" {
		return true, nil
	}
589
590
591
592
	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)
593
	}
594
595
596
597
	// 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
598
	}
599
	return exists && !newTime.After(*oldTime), nil
600
601
}

602
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
603
	if conf.assets == "" {
604
		return false, nil
605
	}
606
607
608
609
610
	// 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
611
	}
612
613
614
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
615
616
617
	)
}

618
619
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
620
func DownloadSchemeManager(url string) (*SchemeManager, error) {
621
622
623
624
625
626
627
628
629
	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")]
	}
630
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
631
632
633
	if err != nil {
		return nil, err
	}
634
	manager := NewSchemeManager("")
635
636
637
638
639
640
641
642
	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
643
644
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
645
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
646
	// Remove everything falling under the manager's responsibility
647
	for credid := range conf.CredentialTypes {
648
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
649
			delete(conf.CredentialTypes, credid)
650
651
		}
	}
652
	for issid := range conf.Issuers {
653
		if issid.SchemeManagerIdentifier() == id {
654
			delete(conf.Issuers, issid)
655
656
		}
	}
657
	for issid := range conf.publicKeys {
658
		if issid.SchemeManagerIdentifier() == id {
659
			delete(conf.publicKeys, issid)
660
661
		}
	}
662
	delete(conf.SchemeManagers, id)
663
664

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
665
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
666
667
	}
	return nil
668
669
}

670
671
672
673
674
675
676
677
678
679
func (conf *Configuration) ReinstallSchemeManager(manager *SchemeManager) (err error) {
	// 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
	}
680
	err = conf.InstallSchemeManager(manager, nil)
681
682
683
	return
}

684
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
685
// provided its signature is valid.
686
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager, publickey []byte) error {
687
	name := manager.ID
688
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
689
690
		return err
	}
691
692

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
693
	path := fmt.Sprintf("%s/%s", conf.Path, name)
694
695
696
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
697
698
699
700
701
702
703
704
	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
		}
705
	}
706
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
707
708
		return err
	}
709
710
711
712
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
713

714
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
715
716
717
718
719
720
}

// 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
721
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
722
723
724
725
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
726
		return
727
728
	}
	if err = t.GetFile("index.sig", sig); err != nil {
729
		return
730
	}
731
	err = conf.VerifySignature(manager.Identifier())
732
	return
733
}
734

Sietse Ringers's avatar
Sietse Ringers committed
735
736
737
// 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.
738
func (conf *Configuration) Download(session SessionRequest) (downloaded *IrmaIdentifierSet, err error) {
739
	managers := make(map[string]struct{}) // Managers that we must update
740
	downloaded = &IrmaIdentifierSet{
741
742
743
744
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
745

746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
	// 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
}

766
func (conf *Configuration) checkCredentialTypes(session SessionRequest, managers map[string]struct{}) error {
767
768
769
770
771
772
773
774
	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
775
			typ, contains = conf.CredentialTypes[credreq.CredentialTypeID]
776
777
778
779
780
781
782
783
784
785
			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
786
			for _, attrtyp := range typ.AttributeTypes {
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
				_, 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{}{}
811
				continue
812
			}
Sietse Ringers's avatar
Sietse Ringers committed
813
			if !attrid.IsCredential() && !typ.ContainsAttribute(attrid) {
814
815
816
817
818
819
820
821
822
				managers[credid.Root()] = struct{}{}
			}
		}
	}

	return nil
}

func (conf *Configuration) checkIssuers(set *IrmaIdentifierSet, managers map[string]struct{}) error {
823
	for issid := range set.Issuers {
824
		if _, contains := conf.Issuers[issid]; !contains {
825
			managers[issid.Root()] = struct{}{}
826
		}
Sietse Ringers's avatar
Sietse Ringers committed
827
	}
828
829
830
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
831
			if err != nil {
832
				return err
Sietse Ringers's avatar
Sietse Ringers committed
833
834
			}
			if pk == nil {
835
				managers[issid.Root()] = struct{}{}
836
837
838
			}
		}
	}
839
	return nil
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
}

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
861
// FromString populates this index by parsing the specified string.
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
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
}

881
// parseIndex parses the index file of the specified manager.
882
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
883
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
884
	if err := fs.AssertPathExists(path); err != nil {
885
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
886
	}
Sietse Ringers's avatar
Sietse Ringers committed
887
	indexbts, err := ioutil.ReadFile(path)
888
	if err != nil {
889
		return nil, err
890
	}
891
892
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
893
894
}

895
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
896
	err := conf.VerifySignature(manager.Identifier())
897
898
899
900
	if err != nil {
		return err
	}

901
	var exists bool
902
	for file := range manager.index {
903
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
904
905
906
907
908
909
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
910
		// Don't care about the actual bytes
911
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
912
913
914
915
916
917
918
			return err
		}
	}

	return nil
}

919
920
921
// 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.
922
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
923
	signedHash, ok := manager.index[filepath.ToSlash(path)]
924
	if !ok {
925
		return nil, false, nil
926
927
	}

Sietse Ringers's avatar
Sietse Ringers committed
928
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
929
	if err != nil {
930
		return nil, true, err
931
932
933
934
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
935
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
936
	}
937
	return bts, true, nil
938
939
940
941
942
}

// 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).
943
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
944
945
946
947
948
949
950
951
952
953
	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
954
	dir := filepath.Join(conf.Path, id.String())
955
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
956
		return errors.New("Missing scheme manager index file, signature, or public key")
957
958
959
960
961
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
962
		return err
963
964
965
966
967
968
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
969
		return err
970
	}
971
	pk, err := ParsePemEcdsaPublicKey(pkbts)
972
	if err != nil {
973
		return err
974
975
976
977
978
	}

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

	// Verify signature
985
986
987
988
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
989
}
990

991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
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
}

1004
1005
1006
func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022

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

1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
	// 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
	}
1033
	if !manager.Timestamp.Before(*timestamp) {
1034
1035
1036
		return nil
	}

1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
	// 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")

	// TODO: how to recover/fix local copy if err != nil below?
	for filename, newHash := range newIndex {
1055
		path := filepath.Join(conf.Path, filename)
1056
		oldHash, known := manager.index[filename]
1057
1058
1059
1060
1061
1062
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
1063
1064
1065
1066
1067
1068
1069
1070
			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
1071
		if err = transport.GetSignedFile(stripped, path, newHash); err != nil {
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
			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
}
1094
1095
1096
1097
1098

// Methods containing consistency checks on irma_configuration

func (conf *Configuration) checkIssuer(manager *SchemeManager, issuer *Issuer, dir string) error {
	issuerid := issuer.Identifier()
1099
1100
	conf.checkTranslations(fmt.Sprintf("Issuer %s", issuerid.String()), issuer)
	// Check that the issuer has public keys
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118