irmaconfig.go 10.9 KB
Newer Older
1
package irma
2
3
4
5
6
7
8
9
10

import (
	"encoding/base64"
	"encoding/xml"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"

11
12
	"crypto/sha256"

13
14
15
16
	"fmt"

	"strings"

17
	"github.com/credentials/irmago/internal/fs"
18
	"github.com/go-errors/errors"
19
20
21
	"github.com/mhe/gabi"
)

22
// Configuration keeps track of scheme managers, issuers, credential types and public keys,
23
// dezerializing them from an irma_configuration folder, and downloads and saves new ones on demand.
24
type Configuration struct {
25
26
27
	SchemeManagers  map[SchemeManagerIdentifier]*SchemeManager
	Issuers         map[IssuerIdentifier]*Issuer
	CredentialTypes map[CredentialTypeIdentifier]*CredentialType
28

29
	publicKeys    map[IssuerIdentifier]map[int]*gabi.PublicKey
30
	reverseHashes map[string]CredentialTypeIdentifier
31
	path          string
32
	initialized   bool
33
34
}

35
// NewConfiguration returns a new configuration. After this
36
// ParseFolder() should be called to parse the specified path.
37
38
func NewConfiguration(path string, assets string) (conf *Configuration, err error) {
	conf = &Configuration{
39
		path: path,
40
	}
41

42
	if err = fs.EnsureDirectoryExists(conf.path); err != nil {
43
44
		return nil, err
	}
45
	if assets != "" {
46
		if err = conf.Copy(assets, false); err != nil {
47
48
49
50
			return nil, err
		}
	}

51
52
53
	return
}

54
// ParseFolder populates the current Configuration by parsing the storage path,
55
// listing the containing scheme managers, issuers and credential types.
56
func (conf *Configuration) ParseFolder() error {
57
	// Init all maps
58
59
60
61
	conf.SchemeManagers = make(map[SchemeManagerIdentifier]*SchemeManager)
	conf.Issuers = make(map[IssuerIdentifier]*Issuer)
	conf.CredentialTypes = make(map[CredentialTypeIdentifier]*CredentialType)
	conf.publicKeys = make(map[IssuerIdentifier]map[int]*gabi.PublicKey)
62

63
	conf.reverseHashes = make(map[string]CredentialTypeIdentifier)
64

65
	err := iterateSubfolders(conf.path, func(dir string) error {
66
67
68
69
70
		manager := &SchemeManager{}
		exists, err := pathToDescription(dir+"/description.xml", manager)
		if err != nil {
			return err
		}
71
72
		if !exists {
			return nil
73
		}
74
75
76
77
78
		if manager.XMLVersion < 7 {
			return errors.New("Unsupported scheme manager description")
		}
		conf.SchemeManagers[manager.Identifier()] = manager
		return conf.parseIssuerFolders(dir)
79
80
81
82
	})
	if err != nil {
		return err
	}
83
	conf.initialized = true
84
85
86
	return nil
}

87
88
89
90
91
// PublicKey returns the specified public key, or nil if not present in the Configuration.
func (conf *Configuration) PublicKey(id IssuerIdentifier, counter int) (*gabi.PublicKey, error) {
	if _, contains := conf.publicKeys[id]; !contains {
		conf.publicKeys[id] = map[int]*gabi.PublicKey{}
		if err := conf.parseKeysFolder(id); err != nil {
92
			return nil, err
93
94
		}
	}
95
	return conf.publicKeys[id][counter], nil
96
97
}

98
func (conf *Configuration) addReverseHash(credid CredentialTypeIdentifier) {
99
	hash := sha256.Sum256([]byte(credid.String()))
100
	conf.reverseHashes[base64.StdEncoding.EncodeToString(hash[:16])] = credid
101
102
}

103
104
105
func (conf *Configuration) hashToCredentialType(hash []byte) *CredentialType {
	if str, exists := conf.reverseHashes[base64.StdEncoding.EncodeToString(hash)]; exists {
		return conf.CredentialTypes[str]
106
107
108
109
	}
	return nil
}

110
// IsInitialized indicates whether this instance has successfully been initialized.
111
112
func (conf *Configuration) IsInitialized() bool {
	return conf.initialized
113
114
}

115
func (conf *Configuration) parseIssuerFolders(path string) error {
116
117
118
119
120
121
	return iterateSubfolders(path, func(dir string) error {
		issuer := &Issuer{}
		exists, err := pathToDescription(dir+"/description.xml", issuer)
		if err != nil {
			return err
		}
122
123
		if !exists {
			return nil
124
		}
125
126
127
128
129
		if issuer.XMLVersion < 4 {
			return errors.New("Unsupported issuer description")
		}
		conf.Issuers[issuer.Identifier()] = issuer
		return conf.parseCredentialsFolder(dir + "/Issues/")
130
131
132
	})
}

133
// parse $schememanager/$issuer/PublicKeys/$i.xml for $i = 1, ...
134
135
func (conf *Configuration) parseKeysFolder(issuerid IssuerIdentifier) error {
	path := fmt.Sprintf("%s/%s/%s/PublicKeys/*.xml", conf.path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
136
137
138
139
140
141
142
143
144
145
146
	files, err := filepath.Glob(path)
	if err != nil {
		return err
	}

	for _, file := range files {
		filename := filepath.Base(file)
		count := filename[:len(filename)-4]
		i, err := strconv.Atoi(count)
		if err != nil {
			continue
147
148
149
150
151
		}
		pk, err := gabi.NewPublicKeyFromFile(file)
		if err != nil {
			return err
		}
152
		pk.Issuer = issuerid.String()
153
		conf.publicKeys[issuerid][i] = pk
154
	}
155

156
157
158
	return nil
}

159
// parse $schememanager/$issuer/Issues/*/description.xml
160
func (conf *Configuration) parseCredentialsFolder(path string) error {
161
162
163
164
165
166
	return iterateSubfolders(path, func(dir string) error {
		cred := &CredentialType{}
		exists, err := pathToDescription(dir+"/description.xml", cred)
		if err != nil {
			return err
		}
167
168
169
170
171
		if !exists {
			return nil
		}
		if cred.XMLVersion < 4 {
			return errors.New("Unsupported credential type description")
172
		}
173
174
175
		credid := cred.Identifier()
		conf.CredentialTypes[credid] = cred
		conf.addReverseHash(credid)
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
		return nil
	})
}

// iterateSubfolders iterates over the subfolders of the specified path,
// calling the specified handler each time. If anything goes wrong, or
// if the caller returns a non-nil error, an error is immediately returned.
func iterateSubfolders(path string, handler func(string) error) error {
	dirs, err := filepath.Glob(path + "/*")
	if err != nil {
		return err
	}

	for _, dir := range dirs {
		stat, err := os.Stat(dir)
		if err != nil {
			return err
		}
		if !stat.IsDir() {
			continue
		}
		err = handler(dir)
		if err != nil {
			return err
		}
	}

	return nil
}

func pathToDescription(path string, description interface{}) (bool, error) {
	if _, err := os.Stat(path); err != nil {
		return false, nil
	}

	file, err := os.Open(path)
	if err != nil {
		return true, err
	}
	defer file.Close()

	bytes, err := ioutil.ReadAll(file)
	if err != nil {
		return true, err
	}

	err = xml.Unmarshal(bytes, description)
	if err != nil {
		return true, err
	}

	return true, nil
}
229

230
231
232
233
234
// 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
235
}
236

237
238
func (conf *Configuration) Copy(source string, parse bool) error {
	if err := fs.EnsureDirectoryExists(conf.path); err != nil {
239
240
241
		return err
	}

242
	err := filepath.Walk(source, filepath.WalkFunc(
243
244
245
246
247
248
		func(path string, info os.FileInfo, err error) error {
			if path == source {
				return nil
			}
			subpath := path[len(source):]
			if info.IsDir() {
249
				if err := fs.EnsureDirectoryExists(conf.path + subpath); err != nil {
250
251
252
253
254
255
256
257
					return err
				}
			} else {
				srcfile, err := os.Open(path)
				if err != nil {
					return err
				}
				defer srcfile.Close()
Sietse Ringers's avatar
Sietse Ringers committed
258
259
260
261
				bytes, err := ioutil.ReadAll(srcfile)
				if err != nil {
					return err
				}
262
				if err := fs.SaveFile(conf.path+subpath, bytes); err != nil {
263
264
265
266
267
268
					return err
				}
			}
			return nil
		}),
	)
269
270
271
272
273

	if err != nil {
		return err
	}
	if parse {
274
		return conf.ParseFolder()
275
276
	}
	return nil
277
}
278

279
func (conf *Configuration) DownloadSchemeManager(url string) (*SchemeManager, error) {
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
	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")]
	}
	b, err := NewHTTPTransport(url).GetBytes("/description.xml")
	if err != nil {
		return nil, err
	}
	manager := &SchemeManager{}
	if err = xml.Unmarshal(b, manager); err != nil {
		return nil, err
	}

	manager.URL = url // TODO?
	return manager, nil
}

302
func (conf *Configuration) RemoveSchemeManager(id SchemeManagerIdentifier) error {
303
	// Remove everything falling under the manager's responsibility
304
	for credid := range conf.CredentialTypes {
305
		if credid.IssuerIdentifier().SchemeManagerIdentifier() == id {
306
			delete(conf.CredentialTypes, credid)
307
308
		}
	}
309
	for issid := range conf.Issuers {
310
		if issid.SchemeManagerIdentifier() == id {
311
			delete(conf.Issuers, issid)
312
313
		}
	}
314
	for issid := range conf.publicKeys {
315
		if issid.SchemeManagerIdentifier() == id {
316
			delete(conf.publicKeys, issid)
317
318
319
		}
	}
	// Remove from storage
320
	return os.RemoveAll(fmt.Sprintf("%s/%s", conf.path, id.String()))
321
322
323
	// or, remove above iterations and call .ParseFolder()?
}

324
func (conf *Configuration) AddSchemeManager(manager *SchemeManager) error {
325
	name := manager.ID
326
	if err := fs.EnsureDirectoryExists(fmt.Sprintf("%s/%s", conf.path, name)); err != nil {
327
328
329
330
331
332
		return err
	}
	b, err := xml.Marshal(manager)
	if err != nil {
		return err
	}
333
	if err := fs.SaveFile(fmt.Sprintf("%s/%s/description.xml", conf.path, name), b); err != nil {
334
335
		return err
	}
336
	conf.SchemeManagers[NewSchemeManagerIdentifier(name)] = manager
337
338
	return nil
}
339

340
func (conf *Configuration) Download(set *IrmaIdentifierSet) (*IrmaIdentifierSet, error) {
341
	var contains bool
342
343
344
345
346
347
348
	var err error
	downloaded := &IrmaIdentifierSet{
		SchemeManagers:  map[SchemeManagerIdentifier]struct{}{},
		Issuers:         map[IssuerIdentifier]struct{}{},
		CredentialTypes: map[CredentialTypeIdentifier]struct{}{},
	}

349
	for manid := range set.SchemeManagers {
350
		if _, contains = conf.SchemeManagers[manid]; !contains {
351
			return nil, errors.Errorf("Unknown scheme manager: %s", manid)
352
353
354
355
356
		}
	}

	transport := NewHTTPTransport("")
	for issid := range set.Issuers {
357
358
359
		if _, contains = conf.Issuers[issid]; !contains {
			url := conf.SchemeManagers[issid.SchemeManagerIdentifier()].URL + "/" + issid.Name()
			path := fmt.Sprintf("%s/%s/%s", conf.path, issid.SchemeManagerIdentifier().String(), issid.Name())
360
361
362
363
364
365
366
			if err = transport.GetFile(url+"/description.xml", path+"/description.xml"); err != nil {
				return nil, err
			}
			if transport.GetFile(url+"/logo.png", path+"/logo.png"); err != nil {
				return nil, err
			}
			downloaded.Issuers[issid] = struct{}{}
367
		}
Sietse Ringers's avatar
Sietse Ringers committed
368
369
370
	}
	for issid, list := range set.PublicKeys {
		for _, count := range list {
371
			pk, err := conf.PublicKey(issid, count)
Sietse Ringers's avatar
Sietse Ringers committed
372
373
374
375
376
377
			if err != nil {
				return nil, err
			}
			if pk == nil {
				manager := issid.SchemeManagerIdentifier()
				suffix := fmt.Sprintf("/%s/PublicKeys/%d.xml", issid.Name(), count)
378
379
				path := fmt.Sprintf("%s/%s/%s", conf.path, manager.String(), suffix)
				if transport.GetFile(conf.SchemeManagers[manager].URL+suffix, path); err != nil {
380
					return nil, err
381
				}
382
383
384
385
			}
		}
	}
	for credid := range set.CredentialTypes {
386
		if _, contains := conf.CredentialTypes[credid]; !contains {
387
388
			issuer := credid.IssuerIdentifier()
			manager := issuer.SchemeManagerIdentifier()
389
			local := fmt.Sprintf("%s/%s/%s/Issues", conf.path, manager.Name(), issuer.Name())
390
			if err := fs.EnsureDirectoryExists(local); err != nil {
391
				return nil, err
392
			}
393
			if transport.GetFile(
394
				fmt.Sprintf("%s/%s/Issues/%s/description.xml",
395
					conf.SchemeManagers[manager].URL, issuer.Name(), credid.Name()),
396
				fmt.Sprintf("%s/%s/description.xml", local, credid.Name()),
397
398
399
400
			); err != nil {
				return nil, err
			}
			downloaded.CredentialTypes[credid] = struct{}{}
401
402
403
		}
	}

404
	return downloaded, conf.ParseFolder()
405
}