descriptions.go 27.4 KB
Newer Older
1
package irma
2
3
4

import (
	"encoding/xml"
Sietse Ringers's avatar
Sietse Ringers committed
5
	"fmt"
6
7
	"path/filepath"

Sietse Ringers's avatar
Sietse Ringers committed
8
	"github.com/go-errors/errors"
9
	"github.com/privacybydesign/irmago/internal/common"
10
11
)

12
13
14
// This file contains data types for scheme managers, issuers, credential types
// matching the XML files in irma_configuration.

15
// SchemeManager describes an issuer scheme and is the issuer equivalent to the RequestorScheme. The naming is legacy.
16
type SchemeManager struct {
17
18
	ID                string           `xml:"Id"`
	Name              TranslatedString `xml:"Name"`
19
20
	URL               string           `xml:"Url"`
	Contact           string           `xml:"contact"`
21
	Demo              bool             `xml:"Demo"` // Decides whether to download private keys
22
	Description       TranslatedString
23
	MinimumAppVersion SchemeAppVersion
24
25
26
	KeyshareServer    string
	KeyshareWebsite   string
	KeyshareAttribute string
27
	TimestampServer   string
28
29
	XMLVersion        int      `xml:"version,attr"`
	XMLName           xml.Name `xml:"SchemeManager"`
30

31
	Status    SchemeManagerStatus `xml:"-"`
32
33
	Timestamp Timestamp

34
35
	storagepath string
	index       SchemeManagerIndex
36
37
}

38
39
40
41
42
type SchemeAppVersion struct {
	Android int `xml:"Android"`
	IOS     int `xml:"iOS"`
}

43
44
// Issuer describes an issuer.
type Issuer struct {
45
46
47
48
49
	ID              string           `xml:"ID"`
	Name            TranslatedString `xml:"Name"`
	SchemeManagerID string           `xml:"SchemeManager"`
	ContactAddress  string
	ContactEMail    string
50
	DeprecatedSince Timestamp
51
	XMLVersion      int `xml:"version,attr"`
52
53
54
55
}

// CredentialType is a description of a credential type, specifying (a.o.) its name, issuer, and attributes.
type CredentialType struct {
56
57
58
59
60
61
62
63
64
65
66
	ID                    string           `xml:"CredentialID"`
	Name                  TranslatedString `xml:"Name"`
	IssuerID              string           `xml:"IssuerID"`
	SchemeManagerID       string           `xml:"SchemeManager"`
	IsSingleton           bool             `xml:"ShouldBeSingleton"`
	DisallowDelete        bool             `xml:"DisallowDelete"`
	Description           TranslatedString
	AttributeTypes        []*AttributeType `xml:"Attributes>Attribute" json:"-"`
	RevocationServers     []string         `xml:"RevocationServers>RevocationServer"`
	RevocationUpdateCount uint64
	RevocationUpdateSpeed uint64
Tomas's avatar
Tomas committed
67
68
69
70
71
72
73
74
	RevocationIndex       int      `xml:"-"`
	XMLVersion            int      `xml:"version,attr"`
	XMLName               xml.Name `xml:"IssueSpecification"`

	IssueURL     TranslatedString `xml:"IssueURL"`
	IsULIssueURL bool             `xml:"IsULIssueURL"`

	DeprecatedSince Timestamp
75

Sietse Ringers's avatar
Sietse Ringers committed
76
77
	Dependencies CredentialDependencies

Tomas's avatar
Tomas committed
78
79
80
	ForegroundColor         string
	BackgroundGradientStart string
	BackgroundGradientEnd   string
81
82
83
84
85
86
87

	IsInCredentialStore bool
	Category            TranslatedString
	FAQIntro            TranslatedString
	FAQPurpose          TranslatedString
	FAQContent          TranslatedString
	FAQHowto            TranslatedString
Sietse Ringers's avatar
Sietse Ringers committed
88
	FAQSummary          *TranslatedString
89
90
}

91
92
// AttributeType is a description of an attribute within a credential type.
type AttributeType struct {
93
	ID          string `xml:"id,attr"`
94
	Optional    string `xml:"optional,attr"  json:",omitempty"`
95
96
	Name        TranslatedString
	Description TranslatedString
97

98
99
	RandomBlind bool `xml:"randomblind,attr,optional" json:",omitempty"`

Tomas's avatar
Tomas committed
100
101
102
	Index        int    `xml:"-"`
	DisplayIndex *int   `xml:"displayIndex,attr" json:",omitempty"`
	DisplayHint  string `xml:"displayHint,attr"  json:",omitempty"`
103

104
105
	RevocationAttribute bool `xml:"revocation,attr" json:",omitempty"`

106
107
108
109
	// Taken from containing CredentialType
	CredentialTypeID string `xml:"-"`
	IssuerID         string `xml:"-"`
	SchemeManagerID  string `xml:"-"`
110
111
}

112
113
// CredentialDependencies contains dependencies on credential types, using condiscon:
// a conjunction of disjunctions of conjunctions of credential types.
Sietse Ringers's avatar
Sietse Ringers committed
114
115
type CredentialDependencies [][][]CredentialTypeIdentifier

116
117
118
119
// RequestorScheme describes verified requestors
type RequestorScheme struct {
	ID        RequestorSchemeIdentifier `json:"id"`
	URL       string                    `json:"url"`
120
	Demo      bool                      `json:"demo"`
121
122
123
	Status    SchemeManagerStatus       `json:"-"`
	Timestamp Timestamp                 `json:"-"`

124
125
126
	storagepath string
	index       SchemeManagerIndex
	requestors  []*RequestorInfo
127
128
129
130
}

// RequestorInfo describes a single verified requestor
type RequestorInfo struct {
131
132
133
134
135
136
	ID         RequestorIdentifier                    `json:"id"`
	Scheme     RequestorSchemeIdentifier              `json:"scheme"`
	Name       TranslatedString                       `json:"name"`
	Industry   *TranslatedString                      `json:"industry"`
	Hostnames  []string                               `json:"hostnames"`
	Logo       *string                                `json:"logo"`
137
	LogoPath   *string                                `json:"logoPath,omitempty"`
138
	ValidUntil *Timestamp                             `json:"valid_until"`
139
	Unverified bool                                   `json:"unverified"`
140
	Wizards    map[IssueWizardIdentifier]*IssueWizard `json:"wizards"`
141
142
143
144
145
}

// RequestorChunk is a number of verified requestors stored together. The RequestorScheme can consist of multiple such chunks
type RequestorChunk []*RequestorInfo

Sietse Ringers's avatar
Sietse Ringers committed
146
147
type (
	IssueWizard struct {
148
149
150
151
152
153
		ID                   IssueWizardIdentifier     `json:"id"`
		Title                TranslatedString          `json:"title"`
		Logo                 *string                   `json:"logo,omitempty"`     // SHA256 of the logo contents (which is the filename on disk)
		LogoPath             *string                   `json:"logoPath,omitempty"` // Full path to the logo set automatically during scheme parsing
		Issues               *CredentialTypeIdentifier `json:"issues,omitempty"`
		AllowOtherRequestors bool                      `json:"allowOtherRequestors"`
Sietse Ringers's avatar
Sietse Ringers committed
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169

		Info *TranslatedString `json:"info,omitempty"`
		FAQ  []IssueWizardQA   `json:"faq,omitempty"`

		Intro              *TranslatedString   `json:"intro,omitempty"`
		SuccessHeader      *TranslatedString   `json:"successHeader,omitempty"`
		SuccessText        *TranslatedString   `json:"successText,omitempty"`
		ExpandDependencies *bool               `json:"expandDependencies,omitempty"`
		Contents           IssueWizardContents `json:"contents"`
	}

	IssueWizardQA struct {
		Question TranslatedString `json:"question"`
		Answer   TranslatedString `json:"answer"`
	}

170
171
172
	// IssueWizardContents contains a condiscon (conjunction of disjunctions of conjunctions)
	// of issue wizard items, making it possible to present the user with different options
	// to complete the wizard.
Sietse Ringers's avatar
Sietse Ringers committed
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
	IssueWizardContents [][][]IssueWizardItem

	IssueWizardItem struct {
		Type       IssueWizardItemType       `json:"type"`
		Credential *CredentialTypeIdentifier `json:"credential,omitempty"`
		Header     *TranslatedString         `json:"header,omitempty"`
		Text       *TranslatedString         `json:"text,omitempty"`
		Label      *TranslatedString         `json:"label,omitempty"`
		SessionURL *string                   `json:"sessionUrl,omitempty"`
		URL        *TranslatedString         `json:"url,omitempty"`
		InApp      *bool                     `json:"inapp,omitempty"`
	}

	IssueWizardItemType string
)

const (
	IssueWizardItemTypeCredential IssueWizardItemType = "credential"
	IssueWizardItemTypeSession    IssueWizardItemType = "session"
	IssueWizardItemTypeWebsite    IssueWizardItemType = "website"
193
194

	maxWizardComplexity = 10
Sietse Ringers's avatar
Sietse Ringers committed
195
196
)

197
// Path returns a list of IssueWizardItems to be used as the wizard item order.
198
// If the ExpandDependencies boolean is set to false, the result of IssueWizardContents.ChoosePath
Sietse Ringers's avatar
Sietse Ringers committed
199
200
// is returned. If not set or set to true, this is augmented with all dependencies of all items
// in an executable order.
201
func (wizard IssueWizard) Path(conf *Configuration, creds CredentialInfoList) ([]IssueWizardItem, error) {
Sietse Ringers's avatar
Sietse Ringers committed
202
203
204
205
206
207
	// convert creds slice to map for easy lookup
	credsmap := map[CredentialTypeIdentifier]struct{}{}
	for _, cred := range creds {
		credsmap[cred.Identifier()] = struct{}{}
	}

208
	contents := wizard.Contents.ChoosePath(conf, credsmap)
Sietse Ringers's avatar
Sietse Ringers committed
209
210
	if wizard.ExpandDependencies != nil && !*wizard.ExpandDependencies {
		return contents, nil
211
212
	} else {
		return buildDependencyTree(contents, conf, credsmap)
Sietse Ringers's avatar
Sietse Ringers committed
213
	}
214
}
Sietse Ringers's avatar
Sietse Ringers committed
215

216
func buildDependencyTree(contents []IssueWizardItem, conf *Configuration, credsmap map[CredentialTypeIdentifier]struct{}) ([]IssueWizardItem, error) {
Sietse Ringers's avatar
Sietse Ringers committed
217
218
219
220
	// Each item in contents refers to a credential type that has dependencies, which may themselves
	// have dependencies. So each item has a tree of dependencies. We must return a list
	// containing all dependencies of each item in an executable order, i.e. item n in the
	// list depends only on items < n in the list. We do this as follows:
221
	// - by considering element n in items to be dependent on element n-1, we join all
Sietse Ringers's avatar
Sietse Ringers committed
222
223
224
225
	//   dependency trees into one
	// - of that tree, starting at the leaf nodes and iterating downwards toward the root,
	//   we put all items in the result list.

226
227
228
229
230
231
232
233
	// We assume here that if one wizard item depends on another, and that dependency is not defined
	// in the corresponding credential type issuer scheme, then they are put in the correct order in
	// the wizard in the requestor scheme: first a credential not depending on any other item in the
	// wizard, then an item that may depend on the first item, etc.
	// Below we iterate per level over the tree (root = level 0, its dependencies = level 1, their
	// dependencies = level 2, etc). Before that iteration, we don't yet know how many levels there
	// are. So the only logical starting point for this iteration is level 0, the root - i.e., the
	// last item of the contents slice. So we first reverse contents.
Sietse Ringers's avatar
Sietse Ringers committed
234
235
236
237
238
239
	reversed := make([]IssueWizardItem, 0, len(contents))
	byID := map[CredentialTypeIdentifier]IssueWizardItem{}
	skipped := 0
	for i := len(contents) - 1; i >= 0; i-- {
		item := contents[i]
		if item.Credential == nil {
240
241
			// If an item does not denote what credential it issues, we cannot take it into account -
			// just ignore it here and append it back to the end of the wizard just before returning.
Sietse Ringers's avatar
Sietse Ringers committed
242
243
244
245
246
247
248
249
			skipped++
			continue
		}
		reversed = append(reversed, contents[i])
		byID[*contents[i].Credential] = contents[i]
	}

	// Build a map containing per level of the dependency tree the (deduplicated) nodes at that level
250
	depTree := map[int]map[CredentialTypeIdentifier]struct{}{}
Sietse Ringers's avatar
Sietse Ringers committed
251
	for i, item := range reversed {
252
		populateDepTree(depTree, i, *item.Credential, conf, credentialDependencies{}, credsmap)
Sietse Ringers's avatar
Sietse Ringers committed
253
254
255
256
257
258
259
	}

	// Scanning horizontally, i.e. per level, we iterate across the tree, putting all
	// credential types that we come across in the result slice. This starts at the leaf nodes
	// that have no dependencies, and after that across the intermediate nodes whose dependencies
	// have been put in the result slice in previous iterations.
	var result []IssueWizardItem                         // to return
260
261
262
263
	resultMap := map[CredentialTypeIdentifier]struct{}{} // to keep track of credentials already put in the result slice
	for i := len(depTree) - 1; i >= 0; i-- {
		for id := range depTree[i] {
			if _, ok := resultMap[id]; ok {
Sietse Ringers's avatar
Sietse Ringers committed
264
265
				continue
			}
266
			resultMap[id] = struct{}{}
Sietse Ringers's avatar
Sietse Ringers committed
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
			if item, present := byID[id]; present {
				result = append(result, item)
			} else {
				current := id // create copy of loop variable to take address of
				result = append(result, IssueWizardItem{
					Type:       IssueWizardItemTypeCredential,
					Credential: &current,
				})
			}
		}
	}

	result = append(result, contents[len(contents)-skipped:]...)
	return result, nil
}

type credentialDependencies map[CredentialTypeIdentifier][]IssueWizardItem

285
// populateDepTree is a recursive function that populates a map containing per level of a tree the
Sietse Ringers's avatar
Sietse Ringers committed
286
// (deduplicated) nodes at that level.
287
288
func populateDepTree(
	depTree map[int]map[CredentialTypeIdentifier]struct{},
Sietse Ringers's avatar
Sietse Ringers committed
289
290
291
292
293
	level int,
	id CredentialTypeIdentifier,
	conf *Configuration,
	deps credentialDependencies,
	creds map[CredentialTypeIdentifier]struct{},
294
295
296
) {
	if depTree[level] == nil {
		depTree[level] = map[CredentialTypeIdentifier]struct{}{}
Sietse Ringers's avatar
Sietse Ringers committed
297
	}
298
	depTree[level][id] = struct{}{}
Sietse Ringers's avatar
Sietse Ringers committed
299
300

	for _, child := range deps.get(id, conf, creds) {
301
		populateDepTree(depTree, level+1, *child.Credential, conf, deps, creds)
Sietse Ringers's avatar
Sietse Ringers committed
302
303
304
	}
}

305
306
307
// get returns the credential dependencies of the specified credential. If not present in the map
// it caches them before returning; on later invocations for the same credential the cached output
// is returned.
Sietse Ringers's avatar
Sietse Ringers committed
308
309
func (deps credentialDependencies) get(id CredentialTypeIdentifier, conf *Configuration, creds map[CredentialTypeIdentifier]struct{}) []IssueWizardItem {
	if _, present := deps[id]; !present {
310
		deps[id] = conf.CredentialTypes[id].Dependencies.WizardContents().ChoosePath(conf, creds)
Sietse Ringers's avatar
Sietse Ringers committed
311
312
313
314
	}
	return deps[id]
}

315
// ChoosePath processes the wizard contents given the list of present credentials. Of each disjunction,
Sietse Ringers's avatar
Sietse Ringers committed
316
317
318
// either the first contained inner conjunction that is satisfied by the credential list is chosen;
// or if no such conjunction exists in the disjunction, the first conjunction is chosen.
// The result of doing this for all outer conjunctions is flattened and returned.
319
func (contents IssueWizardContents) ChoosePath(conf *Configuration, creds map[CredentialTypeIdentifier]struct{}) []IssueWizardItem {
Sietse Ringers's avatar
Sietse Ringers committed
320
321
322
323
324
325
326
	var choice []IssueWizardItem
	for _, discon := range contents {
		disconSatisfied := false
		for _, con := range discon {
			conSatisfied := true
			for _, item := range con {
				if item.Credential == nil {
327
328
329
330
					// If it is not known what credential this item will issue (if any), then we cannot
					// compare that credential to the list of present credentials to establish whether
					// or not this item is completed. So we cannot consider the item to be completed,
					// thus neither can we consider the containing conjunction as completed.
Sietse Ringers's avatar
Sietse Ringers committed
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
					conSatisfied = false
					break
				}
				if _, present := creds[*item.Credential]; !present {
					conSatisfied = false
					break
				}
			}
			if conSatisfied {
				choice = append(choice, con...)
				disconSatisfied = true
				break
			}
		}
		if !disconSatisfied {
			choice = append(choice, discon[0]...)
		}
	}

	return choice
}

func (wizard *IssueWizard) Validate(conf *Configuration) error {
	conf.validateTranslations(fmt.Sprintf("issue wizard %s", wizard.ID), wizard)

	if (wizard.SuccessHeader == nil) != (wizard.SuccessText == nil) {
		return errors.New("wizard contents must have success header and text either both specified, or both empty")
	}
359
360
361
362
363
	// validate that no possible content graph is too complex
	allRelevantPaths := wizard.Contents.buildValidationPaths(conf, map[CredentialTypeIdentifier]struct{}{})
	for _, contents := range allRelevantPaths {
		// validate expanded dependency tree if ExpandDependencies flag is set to true; otherwise validate current length
		if wizard.ExpandDependencies == nil || *wizard.ExpandDependencies {
364
365
366
			result, err := buildDependencyTree(contents, conf, map[CredentialTypeIdentifier]struct{}{})
			if err != nil {
				return err
367
368
369
			}

			if len(result) >= maxWizardComplexity {
370
				return errors.New("wizard too complex")
371
372
373
			}
		} else {
			if len(contents) >= maxWizardComplexity {
374
				return errors.New("wizard too complex")
375
376
377
378
379
			}
		}
	}

	// validate translations, IssueWizardItems and FAQSummaries of dependencies
380
	shouldBeLast := false
Sietse Ringers's avatar
Sietse Ringers committed
381
382
383
	for i, outer := range wizard.Contents {
		for j, middle := range outer {
			for k, item := range middle {
384
385
				// validate all items having no credential type of a wizard are at the end
				if item.Credential == nil {
386
					shouldBeLast = true
387
				} else if shouldBeLast {
388
					return errors.New("items having no credential type should come last")
389
390
				}

391
				if err := item.validate(conf); err != nil {
392
					return errors.Errorf("item %d.%d.%d: %w", i, j, k, err)
Sietse Ringers's avatar
Sietse Ringers committed
393
				}
394
				conf.validateTranslations(fmt.Sprintf("item %d.%d.%d", i, j, k), item)
Sietse Ringers's avatar
Sietse Ringers committed
395
396
397
			}
		}
	}
398
	conf.validateTranslations("issue wizard", wizard)
Sietse Ringers's avatar
Sietse Ringers committed
399
	for i, qa := range wizard.FAQ {
400
		conf.validateTranslations(fmt.Sprintf("QA %d", i), qa)
Sietse Ringers's avatar
Sietse Ringers committed
401
402
403
404
405
	}

	return nil
}

406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
func (contents IssueWizardContents) buildValidationPaths(conf *Configuration, creds map[CredentialTypeIdentifier]struct{}) [][]IssueWizardItem {
	var all [][]IssueWizardItem
	var choice []IssueWizardItem
	for _, discon := range contents {
		disconSatisfied := false

		for i, con := range discon {
			if i > 0 {
				if !userHasCreds(discon[i], creds) {
					// Copy from the original creds map to the target updatedCreds map
					updatedCreds := map[CredentialTypeIdentifier]struct{}{}
					for key, value := range creds {
						updatedCreds[key] = value
					}

					// check the scenario where the user already has the cards from this discon
					for _, item := range discon[i] {
						updatedCreds[*item.Credential] = struct{}{}
					}

426
					all = append(all, contents.buildValidationPaths(conf, updatedCreds)...)
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
				}
			}

			conSatisfied := true
			for _, item := range con {
				if item.Credential == nil {
					// If it is not known what credential this item will issue (if any), then we cannot
					// compare that credential to the list of present credentials to establish whether
					// or not this item is completed. So we cannot consider the item to be completed,
					// thus neither can we consider the containing conjunction as completed.
					conSatisfied = false
					break
				}
				if _, present := creds[*item.Credential]; !present {
					conSatisfied = false
					break
				}
			}
			if conSatisfied {
				choice = appendItems(choice, con)
				disconSatisfied = true
				break
			}
		}
		if !disconSatisfied {
			choice = appendItems(choice, discon[0])
		}
	}

	all = append(all, choice)

	return all
}

func userHasCreds(items []IssueWizardItem, creds map[CredentialTypeIdentifier]struct{}) bool {
	for _, val := range items {
463
464
465
466
		if val.Credential != nil {
			if _, ok := creds[*val.Credential]; !ok {
				return false
			}
467
468
469
470
471
472
473
474
		}
	}
	return true
}

// appendItems appends IssueWizardItems to IssueWizardItems and deduplicates
func appendItems(existing []IssueWizardItem, toBeAdded []IssueWizardItem) []IssueWizardItem {
	withDuplicates := append(existing, toBeAdded...)
475
476
477
478
479
480
481
	credsItemMap := make(map[*CredentialTypeIdentifier]IssueWizardItem)
	var itemsNoCreds []IssueWizardItem
	for _, i := range withDuplicates {
		if i.Credential != nil {
			credsItemMap[i.Credential] = i
		} else {
			itemsNoCreds = append(itemsNoCreds, i)
482
		}
483
484
	}

485
486
487
	var updated []IssueWizardItem
	for _, i := range credsItemMap {
		updated = append(updated, i)
488
	}
489
	updated = append(updated, itemsNoCreds...)
490

491
	return updated
492
493
}

494
func (item *IssueWizardItem) validate(conf *Configuration) error {
Sietse Ringers's avatar
Sietse Ringers committed
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
	if item.Type != IssueWizardItemTypeCredential &&
		item.Type != IssueWizardItemTypeSession &&
		item.Type != IssueWizardItemTypeWebsite {
		return errors.New("unsupported wizard item type")
	}
	if item.Type == IssueWizardItemTypeCredential {
		if item.Credential == nil {
			return errors.New("wizard item has type credential, but no credential specified")
		}
	} else {
		if item.Header == nil || item.Label == nil || item.Text == nil {
			return errors.New("wizard item missing required information")
		}
	}
	if item.Type == IssueWizardItemTypeSession && item.SessionURL == nil {
		return errors.New("wizard item has type session, but no session URL specified")
	}
	if item.Type == IssueWizardItemTypeWebsite && item.URL == nil {
		return errors.New("wizard item has type website, but no session URL specified")
	}

516
517
518
	if item.Credential == nil || conf.SchemeManagers[item.Credential.SchemeManagerIdentifier()] == nil {
		return nil
	}
519

520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
	// In `irma scheme verify` is run on a single requestor scheme, we cannot expect mentioned
	// credential types from other schemes to exist. So only require mentioned credential types
	// to exist if their containing scheme also exists
	if conf.CredentialTypes[*item.Credential] == nil {
		return errors.New("nonexisting credential type " + item.Credential.Name())
	}

	// The wizard item itself must either contain a text field or their its credential type must have a FAQSummary
	if item.Text != nil {
		if l := item.Text.validate(); len(l) > 0 {
			return errors.New("Wizard item text field incomplete for item with credential type: " + item.Credential.String())
		}
	} else {
		faqSummary := conf.CredentialTypes[*item.Credential].FAQSummary
		if faqSummary == nil {
			return errors.New("FAQSummary missing for wizard item with credential type: " + item.Credential.String())
		}
		if l := faqSummary.validate(); len(l) > 0 {
			return errors.New("FAQSummary missing for: " + item.Credential.String())
		}
	}

	// All dependencies of the the item and their dependencies must contain FAQSummaries
	if conf.CredentialTypes[*item.Credential].Dependencies != nil {
		depChain := DependencyChain{*item.Credential}
		if err := validateFAQSummary(*item.Credential, conf, depChain); err != nil {
			return err
547
548
549
550
551
552
		}
	}

	return nil
}

553
func validateFAQSummary(cred CredentialTypeIdentifier, conf *Configuration, validatedDeps DependencyChain) error {
554
	for _, outer := range conf.CredentialTypes[cred].Dependencies {
555
556
		for _, middle := range outer {
			for _, item := range middle {
557
558
559
560
561
				faqSummary := conf.CredentialTypes[item].FAQSummary
				updatedDeps := append(validatedDeps, item)

				if faqSummary == nil {
					return errors.New("FAQSummary missing for last item in chain: " + updatedDeps.String())
562
				}
563

564
565
				if l := faqSummary.validate(); len(l) > 0 {
					return errors.New("FAQSummary incomplete for last item in chain: " + updatedDeps.String())
566
567
568
				}

				if conf.CredentialTypes[item].Dependencies != nil {
569
					return validateFAQSummary(item, conf, updatedDeps)
570
571
572
573
574
				}
			}
		}
	}

Sietse Ringers's avatar
Sietse Ringers committed
575
576
577
	return nil
}

578
579
580
// NewRequestorInfo returns a Requestor with just the given hostname
func NewRequestorInfo(hostname string) *RequestorInfo {
	return &RequestorInfo{
581
582
583
		Name:       NewTranslatedString(&hostname),
		Hostnames:  []string{hostname},
		Unverified: true,
584
585
586
	}
}

587
588
func (ad AttributeType) GetAttributeTypeIdentifier() AttributeTypeIdentifier {
	return NewAttributeTypeIdentifier(fmt.Sprintf("%s.%s.%s.%s", ad.SchemeManagerID, ad.IssuerID, ad.CredentialTypeID, ad.ID))
589
590
}

591
func (ad AttributeType) IsOptional() bool {
592
593
594
	return ad.Optional == "true"
}

595
596
// Returns indices of random blind attributes within this credentialtype
// The indices coincide with indices of an AttributeList (metadataAttribute at index 0)
597
func (ct *CredentialType) RandomBlindAttributeIndices() []int {
598
599
600
	indices := []int{}
	for i, at := range ct.AttributeTypes {
		if at.RandomBlind {
601
			indices = append(indices, i+1)
602
603
604
605
606
		}
	}
	return indices
}

607
func (ct *CredentialType) attributeTypeIdentifiers(indices []int) (ids []string) {
608
609
610
611
612
613
614
615
616
617
	for i, at := range ct.AttributeTypes {
		for _, j := range indices {
			if i == j {
				ids = append(ids, at.ID)
			}
		}
	}
	return
}

618
619
func (ct *CredentialType) RandomBlindAttributeNames() []string {
	return ct.attributeTypeIdentifiers(ct.RandomBlindAttributeIndices())
620
621
}

622
func (ct *CredentialType) RevocationSupported() bool {
623
	return len(ct.RevocationServers) > 0
624
625
}

626
627
// ContainsAttribute tests whether the specified attribute is contained in this
// credentialtype.
628
func (ct *CredentialType) ContainsAttribute(ai AttributeTypeIdentifier) bool {
629
	if ai.CredentialTypeIdentifier().String() != ct.Identifier().String() {
630
631
		return false
	}
632
	for _, desc := range ct.AttributeTypes {
633
634
635
636
637
638
639
		if desc.ID == ai.Name() {
			return true
		}
	}
	return false
}

Sietse Ringers's avatar
Sietse Ringers committed
640
641
// IndexOf returns the index of the specified attribute if present,
// or an error (and -1) if not present.
642
643
644
645
func (ct CredentialType) IndexOf(ai AttributeTypeIdentifier) (int, error) {
	if ai.CredentialTypeIdentifier() != ct.Identifier() {
		return -1, errors.New("Wrong credential type")
	}
646
	for i, description := range ct.AttributeTypes {
647
648
649
650
651
652
653
		if description.ID == ai.Name() {
			return i, nil
		}
	}
	return -1, errors.New("Attribute identifier not found")
}

654
func (ct CredentialType) AttributeType(ai AttributeTypeIdentifier) *AttributeType {
655
656
657
658
	i, _ := ct.IndexOf(ai)
	if i == -1 {
		return nil
	}
659
	return ct.AttributeTypes[i]
660
661
}

Sietse Ringers's avatar
Sietse Ringers committed
662
663
// TranslatedString is a map of translated strings.
type TranslatedString map[string]string
664

665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
type xmlTranslation struct {
	XMLName xml.Name
	Text    string `xml:",chardata"`
}

type xmlTranslatedString struct {
	Translations []xmlTranslation `xml:",any"`
}

// MarshalXML implements xml.Marshaler.
func (ts *TranslatedString) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
	temp := &xmlTranslatedString{}
	for lang, text := range *ts {
		temp.Translations = append(temp.Translations,
			xmlTranslation{XMLName: xml.Name{Local: lang}, Text: text},
		)
	}
	return e.EncodeElement(temp, start)
}

Sietse Ringers's avatar
Sietse Ringers committed
685
686
687
688
689
690
691
// UnmarshalXML unmarshals an XML tag containing a string translated to multiple languages,
// for example: <Foo><en>Hello world</en><nl>Hallo wereld</nl></Foo>
// into a TranslatedString: { "en": "Hello world" , "nl": "Hallo wereld" }
func (ts *TranslatedString) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	if map[string]string(*ts) == nil {
		*ts = TranslatedString(make(map[string]string))
	}
692
	temp := &xmlTranslatedString{}
Sietse Ringers's avatar
Sietse Ringers committed
693
694
695
696
697
	if err := d.DecodeElement(temp, &start); err != nil {
		return err
	}
	for _, translation := range temp.Translations {
		(*ts)[translation.XMLName.Local] = translation.Text
698
	}
Sietse Ringers's avatar
Sietse Ringers committed
699
	return nil
700
701
}

702
703
func (ts *TranslatedString) validate() []string {
	var invalidLangs []string
704
705
	for _, lang := range validLangs {
		if text, exists := (*ts)[lang]; !exists || text == "" {
706
707
			invalidLangs = append(invalidLangs, lang)

708
709
		}
	}
710
	return invalidLangs
711
712
}

Sietse Ringers's avatar
Sietse Ringers committed
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
func (deps CredentialDependencies) WizardContents() IssueWizardContents {
	var contents IssueWizardContents
	for _, credDiscon := range deps {
		discon := make([][]IssueWizardItem, 0, len(credDiscon))
		for _, credCon := range credDiscon {
			con := make([]IssueWizardItem, 0, len(credCon))
			for i := range credCon {
				con = append(con, IssueWizardItem{Type: IssueWizardItemTypeCredential, Credential: &(credCon[i])})
			}
			discon = append(discon, con)
		}
		contents = append(contents, discon)
	}
	return contents
}

func (deps *CredentialDependencies) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	var temp struct {
731
732
		Or []struct {
			And []struct {
Sietse Ringers's avatar
Sietse Ringers committed
733
734
735
736
737
738
739
				Con []CredentialTypeIdentifier `xml:"CredentialType"`
			}
		}
	}
	if err := d.DecodeElement(&temp, &start); err != nil {
		return err
	}
740
741
742
	for _, discon := range temp.Or {
		t := make([][]CredentialTypeIdentifier, 0, len(discon.And))
		for _, con := range discon.And {
Sietse Ringers's avatar
Sietse Ringers committed
743
744
745
746
747
748
749
			t = append(t, con.Con)
		}
		*deps = append(*deps, t)
	}
	return nil
}

750
// Identifier returns the identifier of the specified credential type.
751
752
func (ct *CredentialType) Identifier() CredentialTypeIdentifier {
	return NewCredentialTypeIdentifier(ct.SchemeManagerID + "." + ct.IssuerID + "." + ct.ID)
753
754
755
}

// IssuerIdentifier returns the issuer identifier of the specified credential type.
756
757
func (ct *CredentialType) IssuerIdentifier() IssuerIdentifier {
	return NewIssuerIdentifier(ct.SchemeManagerID + "." + ct.IssuerID)
758
759
}

760
761
762
763
func (ct *CredentialType) SchemeManagerIdentifier() SchemeManagerIdentifier {
	return NewSchemeManagerIdentifier(ct.SchemeManagerID)
}

Sietse Ringers's avatar
Sietse Ringers committed
764
func (ct *CredentialType) Logo(conf *Configuration) string {
765
766
	scheme := conf.SchemeManagers[ct.SchemeManagerIdentifier()]
	path := filepath.Join(scheme.path(), ct.IssuerID, "Issues", ct.ID, "logo.png")
767
	exists, err := common.PathExists(path)
Sietse Ringers's avatar
Sietse Ringers committed
768
769
770
771
772
773
	if err != nil || !exists {
		return ""
	}
	return path
}

774
// Identifier returns the identifier of the specified issuer description.
775
776
777
778
func (id *Issuer) Identifier() IssuerIdentifier {
	return NewIssuerIdentifier(id.SchemeManagerID + "." + id.ID)
}

779
780
781
func (id *Issuer) SchemeManagerIdentifier() SchemeManagerIdentifier {
	return NewSchemeManagerIdentifier(id.SchemeManagerID)
}
782
783
784
785
786
787
788
789
790
791

func (ri *RequestorInfo) logoPath(scheme *RequestorScheme) string {
	if ri.Logo != nil {
		logoPath := filepath.Join(scheme.path(), "assets", *ri.Logo+".png")
		if exists, _ := common.PathExists(logoPath); exists {
			return logoPath
		}
	}
	return ""
}