irmaconfig.go 37.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
31
32
	"sort"

	"bytes"

	"encoding/hex"

	"crypto/ecdsa"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
	"math/big"

33
	"github.com/go-errors/errors"
34
	"github.com/mhe/gabi"
35
	"github.com/privacybydesign/irmago/internal/fs"
36
37
)

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

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

49
50
51
	// 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
52

53
54
	Warnings []string

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

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

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

70
71
type SchemeManagerStatus string

72
73
type SchemeManagerError struct {
	Manager SchemeManagerIdentifier
74
	Status  SchemeManagerStatus
75
76
77
	Err     error
}

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

86
87
	pubkeyPattern  = "%s/%s/%s/PublicKeys/*.xml"
	privkeyPattern = "%s/%s/%s/PrivateKeys/*.xml"
88
89
)

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

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

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

111
112
113
	// Init all maps
	conf.clear()

114
115
116
	return
}

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

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

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

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

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

206
207
208
	return err
}

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

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

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

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

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

275
276
277
278
func relativePath(absolute string, relative string) string {
	return relative[len(absolute)+1:]
}

279
280
281
// 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 {
282
		if err := conf.parseKeysFolder(conf.SchemeManagers[id.SchemeManagerIdentifier()], id); err != nil {
283
			return nil, err
284
285
		}
	}
286
	return conf.publicKeys[id][counter], nil
287
288
}

289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
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
}

312
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
313
	hash := sha256.Sum256([]byte(credid.String()))
314
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
315
316
}

317
318
319
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
320
321
322
323
	}
	return nil
}

324
// IsInitialized indicates whether this instance has successfully been initialized.
325
326
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
327
328
}

329
330
331
332
333
334
335
336
337
// 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
		}
	}
}

338
func (conf *Configuration) parseIssuerFolders(manager *SchemeManager, path string) error {
339
340
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
341
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", issuer)
342
343
344
		if err != nil {
			return err
		}
345
346
		if !exists {
			return nil
347
		}
348
349
350
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
351

352
		if err = conf.checkIssuer(manager, issuer, dir); err != nil {
353
354
355
			return err
		}

356
		conf.Issuers[issuer.Identifier()] = issuer
357
		issuer.Valid = conf.SchemeManagers[issuer.SchemeManagerIdentifier()].Valid
358
		return conf.parseCredentialsFolder(manager, issuer, dir+"/Issues/")
359
360
361
	})
}

362
363
364
func (conf *Configuration) DeleteSchemeManager(id SchemeManagerIdentifier) error {
	delete(conf.SchemeManagers, id)
	delete(conf.DisabledSchemeManagers, id)
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
	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)
		}
	}
381
382
383
	return os.RemoveAll(filepath.Join(conf.Path, id.Name()))
}

384
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
385
func (conf *Configuration) parseKeysFolder(manager *SchemeManager, issuerid IssuerIdentifier) error {
386
	conf.publicKeys[issuerid] = map[int]*gabi.PublicKey{}
387
	path := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
388
389
390
391
392
393
394
395
396
397
	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 {
398
			return err
399
		}
Sietse Ringers's avatar
Sietse Ringers committed
400
		bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, file))
401
		if err != nil || !found {
402
403
404
			return err
		}
		pk, err := gabi.NewPublicKeyFromBytes(bts)
405
406
407
		if err != nil {
			return err
		}
408
409
410
		if int(pk.Counter) != i {
			return errors.Errorf("Public key %s of issuer %s has wrong <Counter>", file, issuerid.String())
		}
411
		pk.Issuer = issuerid.String()
412
		conf.publicKeys[issuerid][i] = pk
413
	}
414

415
416
417
	return nil
}

418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
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
}

440
// parse $schememanager/$issuer/Issues/*/description.xml
441
442
443
func (conf *Configuration) parseCredentialsFolder(manager *SchemeManager, issuer *Issuer, path string) error {
	var foundcred bool
	err := iterateSubfolders(path, func(dir string) error {
444
		cred := &CredentialType{}
445
		exists, err := conf.pathToDescription(manager, dir+"/description.xml", cred)
446
447
448
		if err != nil {
			return err
		}
449
450
451
		if !exists {
			return nil
		}
452
		if err = conf.checkCredentialType(manager, issuer, cred, dir); err != nil {
453
454
455
456
			return err
		}
		foundcred = true
		cred.Valid = conf.SchemeManagers[cred.SchemeManagerIdentifier()].Valid
457
		credid := cred.Identifier()
458
459
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
460
461
462
463
464
465
466
		for index, attr := range cred.Attributes {
			attr.Index = index
			attr.SchemeManagerID = cred.SchemeManagerID
			attr.IssuerID = cred.IssuerID
			attr.CredentialTypeID = cred.ID
			conf.Attributes[attr.GetAttributeTypeIdentifier()] = attr
		}
467
468
		return nil
	})
469
470
471
472
473
474
	if !foundcred {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no credential types", issuer.Identifier().String()))
	}
	return err
}

475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
// 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
		}
492
493
494
		if strings.HasSuffix(dir, "/.git") {
			continue
		}
495
496
497
498
499
500
501
502
503
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

504
func (conf *Configuration) pathToDescription(manager *SchemeManager, path string, description interface{}) (bool, error) {
505
506
507
508
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

Sietse Ringers's avatar
Sietse Ringers committed
509
	bts, found, err := conf.ReadAuthenticatedFile(manager, relativePath(conf.Path, path))
510
511
512
	if !found {
		return false, nil
	}
513
514
515
516
	if err != nil {
		return true, err
	}

517
	err = xml.Unmarshal(bts, description)
518
519
520
521
522
523
	if err != nil {
		return true, err
	}

	return true, nil
}
524

525
526
527
528
529
// 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
530
}
531

532
func (conf *Configuration) isUpToDate(scheme SchemeManagerIdentifier) (bool, error) {
533
534
535
	if conf.assets == "" {
		return true, nil
	}
536
537
538
539
	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)
540
	}
541
542
543
544
	// 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
545
	}
546
	return exists && !newTime.After(*oldTime), nil
547
548
}

549
func (conf *Configuration) CopyManagerFromAssets(scheme SchemeManagerIdentifier) (bool, error) {
550
	if conf.assets == "" {
551
		return false, nil
552
	}
553
554
555
556
557
	// 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
558
	}
559
560
561
	return true, fs.CopyDirectory(
		filepath.Join(conf.assets, name),
		filepath.Join(conf.Path, name),
562
563
564
	)
}

565
566
// DownloadSchemeManager downloads and returns a scheme manager description.xml file
// from the specified URL.
567
func DownloadSchemeManager(url string) (*SchemeManager, error) {
568
569
570
571
572
573
574
575
576
	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")]
	}
577
	b, err := NewHTTPTransport(url).GetBytes("description.xml")
578
579
580
	if err != nil {
		return nil, err
	}
581
	manager := NewSchemeManager("")
582
583
584
585
586
587
588
589
	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
590
591
// RemoveSchemeManager removes the specified scheme manager and all associated issuers,
// public keys and credential types from this Configuration.
592
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier, fromStorage bool) error {
593
	// Remove everything falling under the manager's responsibility
594
	for credid := range conf.CredentialTypes {
595
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
596
			delete(conf.CredentialTypes, credid)
597
598
		}
	}
599
	for issid := range conf.Issuers {
600
		if issid.SchemeManagerIdentifier() == id {
601
			delete(conf.Issuers, issid)
602
603
		}
	}
604
	for issid := range conf.publicKeys {
605
		if issid.SchemeManagerIdentifier() == id {
606
			delete(conf.publicKeys, issid)
607
608
		}
	}
609
	delete(conf.SchemeManagers, id)
610
611

	if fromStorage {
Sietse Ringers's avatar
Sietse Ringers committed
612
		return os.RemoveAll(fmt.Sprintf("%s/%s", conf.Path, id.String()))
613
614
	}
	return nil
615
616
}

617
618
619
620
621
622
623
624
625
626
627
628
629
630
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
	}
	err = conf.InstallSchemeManager(manager)
	return
}

631
// InstallSchemeManager downloads and adds the specified scheme manager to this Configuration,
Sietse Ringers's avatar
Sietse Ringers committed
632
// provided its signature is valid.
633
func (conf *Configuration) InstallSchemeManager(manager *SchemeManager) error {
634
	name := manager.ID
635
	if err := fs.EnsureDirectoryExists(filepath.Join(conf.Path, name)); err != nil {
636
637
		return err
	}
638
639

	t := NewHTTPTransport(manager.URL)
Sietse Ringers's avatar
Sietse Ringers committed
640
	path := fmt.Sprintf("%s/%s", conf.Path, name)
641
642
643
	if err := t.GetFile("description.xml", path+"/description.xml"); err != nil {
		return err
	}
644
	if err := t.GetFile("pk.pem", path+"/pk.pem"); err != nil {
645
646
		return err
	}
647
	if err := conf.DownloadSchemeManagerSignature(manager); err != nil {
648
649
		return err
	}
650
651
652
653
	conf.SchemeManagers[manager.Identifier()] = manager
	if err := conf.UpdateSchemeManager(manager.Identifier(), nil); err != nil {
		return err
	}
654

655
	return conf.ParseSchemeManagerFolder(filepath.Join(conf.Path, name), manager)
656
657
658
659
660
661
}

// 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
662
	path := fmt.Sprintf("%s/%s", conf.Path, manager.ID)
663
664
665
666
	index := filepath.Join(path, "index")
	sig := filepath.Join(path, "index.sig")

	if err = t.GetFile("index", index); err != nil {
667
		return
668
669
	}
	if err = t.GetFile("index.sig", sig); err != nil {
670
		return
671
	}
672
	err = conf.VerifySignature(manager.Identifier())
673
	return
674
}
675

Sietse Ringers's avatar
Sietse Ringers committed
676
677
678
// 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.
679
func (conf *Configuration) Download(session SessionRequest) (downloaded *IrmaIdentifierSet, err error) {
680
	managers := make(map[string]struct{}) // Managers that we must update
681
	downloaded = &IrmaIdentifierSet{
682
683
684
685
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}
686

687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
	// 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
}

707
func (conf *Configuration) checkCredentialTypes(session SessionRequest, managers map[string]struct{}) error {
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
	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
			typ, contains = conf.CredentialTypes[*credreq.CredentialTypeID]
			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
			for _, attrtyp := range typ.Attributes {
				_, 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{}{}
			}
			if !typ.ContainsAttribute(attrid) {
				managers[credid.Root()] = struct{}{}
			}
		}
	}

	return nil
}

func (conf *Configuration) checkIssuers(set *IrmaIdentifierSet, managers map[string]struct{}) error {
763
	for issid := range set.Issuers {
764
		if _, contains := conf.Issuers[issid]; !contains {
765
			managers[issid.Root()] = struct{}{}
766
		}
Sietse Ringers's avatar
Sietse Ringers committed
767
	}
768
769
770
	for issid, keyids := range set.PublicKeys {
		for _, keyid := range keyids {
			pk, err := conf.PublicKey(issid, keyid)
Sietse Ringers's avatar
Sietse Ringers committed
771
			if err != nil {
772
				return err
Sietse Ringers's avatar
Sietse Ringers committed
773
774
			}
			if pk == nil {
775
				managers[issid.Root()] = struct{}{}
776
777
778
			}
		}
	}
779
	return nil
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
}

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
801
// FromString populates this index by parsing the specified string.
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
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
}

821
// parseIndex parses the index file of the specified manager.
822
func (conf *Configuration) parseIndex(name string, manager *SchemeManager) (SchemeManagerIndex, error) {
Sietse Ringers's avatar
Sietse Ringers committed
823
	path := filepath.Join(conf.Path, name, "index")
Sietse Ringers's avatar
Sietse Ringers committed
824
	if err := fs.AssertPathExists(path); err != nil {
825
		return nil, fmt.Errorf("Missing scheme manager index file; tried %s", path)
826
	}
Sietse Ringers's avatar
Sietse Ringers committed
827
	indexbts, err := ioutil.ReadFile(path)
828
	if err != nil {
829
		return nil, err
830
	}
831
832
	index := SchemeManagerIndex(make(map[string]ConfigurationFileHash))
	return index, index.FromString(string(indexbts))
833
834
}

835
func (conf *Configuration) VerifySchemeManager(manager *SchemeManager) error {
836
	err := conf.VerifySignature(manager.Identifier())
837
838
839
840
	if err != nil {
		return err
	}

841
	var exists bool
842
	for file := range manager.index {
843
		exists, err = fs.PathExists(filepath.Join(conf.Path, file))
844
845
846
847
848
849
		if err != nil {
			return err
		}
		if !exists {
			continue
		}
850
		// Don't care about the actual bytes
851
		if _, _, err = conf.ReadAuthenticatedFile(manager, file); err != nil {
852
853
854
855
856
857
858
			return err
		}
	}

	return nil
}

859
860
861
// 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.
862
func (conf *Configuration) ReadAuthenticatedFile(manager *SchemeManager, path string) ([]byte, bool, error) {
863
	signedHash, ok := manager.index[filepath.ToSlash(path)]
864
	if !ok {
865
		return nil, false, nil
866
867
	}

Sietse Ringers's avatar
Sietse Ringers committed
868
	bts, err := ioutil.ReadFile(filepath.Join(conf.Path, path))
869
	if err != nil {
870
		return nil, true, err
871
872
873
874
	}
	computedHash := sha256.Sum256(bts)

	if !bytes.Equal(computedHash[:], signedHash) {
875
		return nil, true, errors.Errorf("Hash of %s does not match scheme manager index", path)
876
	}
877
	return bts, true, nil
878
879
880
881
882
}

// 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).
883
func (conf *Configuration) VerifySignature(id SchemeManagerIdentifier) (err error) {
884
885
886
887
888
889
890
891
892
893
	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
894
	dir := filepath.Join(conf.Path, id.String())
895
	if err := fs.AssertPathExists(dir+"/index", dir+"/index.sig", dir+"/pk.pem"); err != nil {
896
		return errors.New("Missing scheme manager index file, signature, or public key")
897
898
899
900
901
	}

	// Read and hash index file
	indexbts, err := ioutil.ReadFile(dir + "/index")
	if err != nil {
902
		return err
903
904
905
906
907
908
	}
	indexhash := sha256.Sum256(indexbts)

	// Read and parse scheme manager public key
	pkbts, err := ioutil.ReadFile(dir + "/pk.pem")
	if err != nil {
909
		return err
910
911
912
913
	}
	pkblk, _ := pem.Decode(pkbts)
	genericPk, err := x509.ParsePKIXPublicKey(pkblk.Bytes)
	if err != nil {
914
		return err
915
916
917
	}
	pk, ok := genericPk.(*ecdsa.PublicKey)
	if !ok {
918
		return errors.New("Invalid scheme manager public key")
919
920
921
922
923
	}

	// Read and parse signature
	sig, err := ioutil.ReadFile(dir + "/index.sig")
	if err != nil {
924
		return err
925
926
927
928
929
	}
	ints := make([]*big.Int, 0, 2)
	_, err = asn1.Unmarshal(sig, &ints)

	// Verify signature
930
931
932
933
	if !ecdsa.Verify(pk, indexhash[:], ints[0], ints[1]) {
		return errors.New("Scheme manager signature was invalid")
	}
	return nil
934
}
935
936
937
938

func (hash ConfigurationFileHash) String() string {
	return hex.EncodeToString(hash)
}
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954

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

955
956
957
958
959
960
961
962
963
964
	// 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
	}
965
	if !manager.Timestamp.Before(*timestamp) {
966
967
968
		return nil
	}

969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
	// 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 {
987
		path := filepath.Join(conf.Path, filename)
988
		oldHash, known := manager.index[filename]
989
990
991
992
993
994
		var have bool
		have, err = fs.PathExists(path)
		if err != nil {
			return err
		}
		if known && have && oldHash.Equal(newHash) {
995
996
997
998
999
1000
1001
1002
			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
1003
		if err = transport.GetSignedFile(stripped, path, newHash); err != nil {
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
			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
}
1026
1027
1028
1029
1030

// Methods containing consistency checks on irma_configuration

func (conf *Configuration) checkIssuer(manager *SchemeManager, issuer *Issuer, dir string) error {
	issuerid := issuer.Identifier()
1031
1032
	conf.checkTranslations(fmt.Sprintf("Issuer %s", issuerid.String()), issuer)
	// Check that the issuer has public keys
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
	pkpath := fmt.Sprintf(pubkeyPattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
	files, err := filepath.Glob(pkpath)
	if err != nil {
		return err
	}
	if len(files) == 0 {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no public keys", issuerid.String()))
	}

	if filepath.Base(dir) != issuer.ID {
		return errors.Errorf("Issuer %s has wrong directory name %s", issuerid.String(), filepath.Base(dir))
	}
	if manager.ID != issuer.SchemeManagerID {
		return errors.Errorf("Issuer %s has wrong SchemeManager %s", issuerid.String(), issuer.SchemeManagerID)
	}
	if err = fs.AssertPathExists(dir + "/logo.png"); err != nil {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Issuer %s has no logo.png", issuerid.String()))
	}
	return nil
}

func (conf *Configuration) checkCredentialType(manager *SchemeManager, issuer *Issuer, cred *CredentialType, dir string) error {
	credid := cred.Identifier()
1056
	conf.checkTranslations(fmt.Sprintf("Credential type %s", credid.String()), cred)
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
	if cred.XMLVersion < 4 {
		return errors.New("Unsupported credential type description")
	}
	if cred.ID != filepath.Base(dir) {
		return errors.Errorf("Credential type %s has wrong directory name %s", credid.String(), filepath.Base(dir))
	}
	if cred.IssuerID != issuer.ID {
		return errors.Errorf("Credential type %s has wrong IssuerID %s", credid.String(), cred.IssuerID)
	}
	if cred.SchemeManagerID != manager.ID {
		return errors.Errorf("Credential type %s has wrong SchemeManager %s", credid.String(), cred.SchemeManagerID)
	}
	if err := fs.AssertPathExists(dir + "/logo.png"); err != nil {
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has no logo.png", credid.String()))
	}
	return conf.checkAttributes(cred)
}

func (conf *Configuration) checkAttributes(cred *CredentialType) error {
	name := cred.Identifier().String()
	indices := make(map[int]struct{})
	count := len(cred.Attributes)
	if count == 0 {
		return errors.Errorf("Credenial type %s has no attributes", name)
	}
	for i, attr := range cred.Attributes {
1083
		conf.checkTranslations(fmt.Sprintf("Attribute %s of credential type %s", attr.ID, cred.Identifier().String()), attr)
1084
		index := i
1085
1086
		if attr.DisplayIndex != nil {
			index = *attr.DisplayIndex
1087
1088
		}
		if index >= count {
1089
			conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has invalid attribute displayIndex at attribute %d", name, i))
1090
1091
1092
1093
		}
		indices[index] = struct{}{}
	}
	if len(indices) != count {
1094
		conf.Warnings = append(conf.Warnings, fmt.Sprintf("Credential type %s has invalid attribute ordering, check the displayIndex tags", name))
1095
1096
1097
	}
	return nil
}
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107

func (conf *Configuration) checkScheme(scheme *SchemeManager, dir string) error {
	if scheme.XMLVersion < 7 {
		scheme.Status = SchemeManagerStatusParsingError
		return errors.New("Unsupported scheme manager description")
	}
	if filepath.Base(dir) != scheme.ID {
		scheme.Status = SchemeManagerStatusParsingError
		return errors.Errorf("Scheme %s has wrong directory name %s", scheme.ID, filepath.Base(dir))
	}
1108
1109
1110
1111
1112
1113
	if scheme.KeyshareServer != "" {
		if err := fs.AssertPathExists(filepath.Join(dir, "kss-0.pem")); err != nil {
			scheme.Status = SchemeManagerStatusParsingError
			return errors.Errorf("Scheme %s has keyshare URL but no keyshare public key kss-0.pem", scheme.ID)
		}
	}
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
	conf.checkTranslations(fmt.Sprintf("Scheme %s", scheme.ID), scheme)
	return nil
}

// checkTranslations checks for each member of the interface o that is of type TranslatedString
// that it contains all necessary translations.
func (conf *Configuration) checkTranslations(file string, o interface{}) {
	langs := []string{"en", "nl"} // Hardcode these for now, TODO make configurable
	v := reflect.ValueOf(o)

	// Dereference in case of pointer or interface
	if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
		v = v.Elem()
	}

	for i := 0; i < v.