verify.go 15.4 KB
Newer Older
1
2
3
package irma

import (
4
	"crypto/rsa"
5
	"time"
6

7
	"github.com/dgrijalva/jwt-go"
8
	"github.com/go-errors/errors"
9
10
	"github.com/privacybydesign/gabi"
	"github.com/privacybydesign/gabi/big"
11
	"github.com/privacybydesign/gabi/revocation"
12
13
14
15
16
)

// ProofStatus is the status of the complete proof
type ProofStatus string

17
18
19
// Status is the proof status of a single attribute
type AttributeProofStatus string

20
const (
21
	ProofStatusValid             = ProofStatus("VALID")              // Proof is valid
22
	ProofStatusInvalid           = ProofStatus("INVALID")            // Proof is invalid
23
24
25
	ProofStatusInvalidTimestamp  = ProofStatus("INVALID_TIMESTAMP")  // Attribute-based signature had invalid timestamp
	ProofStatusUnmatchedRequest  = ProofStatus("UNMATCHED_REQUEST")  // Proof does not correspond to a specified request
	ProofStatusMissingAttributes = ProofStatus("MISSING_ATTRIBUTES") // Proof does not contain all requested attributes
26
	ProofStatusExpired           = ProofStatus("EXPIRED")            // Attributes were expired at proof creation time (now, or according to timestamp in case of abs)
27

28
29
30
	AttributeProofStatusPresent = AttributeProofStatus("PRESENT") // Attribute is disclosed and matches the value
	AttributeProofStatusExtra   = AttributeProofStatus("EXTRA")   // Attribute is disclosed, but wasn't requested in request
	AttributeProofStatusNull    = AttributeProofStatus("NULL")    // Attribute is disclosed but is null
31
)
32

33
34
// DisclosedAttribute represents a disclosed attribute.
type DisclosedAttribute struct {
35
36
37
38
39
40
	RawValue         *string                 `json:"rawvalue"`
	Value            TranslatedString        `json:"value"` // Value of the disclosed attribute
	Identifier       AttributeTypeIdentifier `json:"id"`
	Status           AttributeProofStatus    `json:"status"`
	IssuanceTime     Timestamp               `json:"issuancetime"`
	NotRevokedBefore *Timestamp              `json:"notrevokedbefore,omitempty"`
41
42
}

43
44
// ProofList is a gabi.ProofList with some extra methods.
type ProofList gabi.ProofList
45

46
47
var ErrorMissingPublicKey = errors.New("Missing public key")

48
// ExtractPublicKeys returns the public keys of each proof in the proofList, in the same order,
49
50
// for later use in verification of the proofList. If one of the proofs is not a ProofD
// an error is returned.
51
func (pl ProofList) ExtractPublicKeys(configuration *Configuration) ([]*gabi.PublicKey, error) {
52
	var publicKeys = make([]*gabi.PublicKey, 0, len(pl))
53

54
	for _, v := range pl {
55
56
57
58
59
60
61
62
		switch v.(type) {
		case *gabi.ProofD:
			proof := v.(*gabi.ProofD)
			metadata := MetadataFromInt(proof.ADisclosed[1], configuration) // index 1 is metadata attribute
			publicKey, err := metadata.PublicKey()
			if err != nil {
				return nil, err
			}
63
64
65
			if publicKey == nil {
				return nil, ErrorMissingPublicKey
			}
66
67
			publicKeys = append(publicKeys, publicKey)
		default:
68
			return nil, errors.New("Cannot extract public key, not a disclosure proofD")
69
70
71
72
73
		}
	}
	return publicKeys, nil
}

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Expired returns true if any of the contained disclosure proofs is specified at the specified time,
// or now, when the specified time is nil.
func (pl ProofList) Expired(configuration *Configuration, t *time.Time) bool {
	if t == nil {
		temp := time.Now()
		t = &temp
	}
	for _, proof := range pl {
		proofd, ok := proof.(*gabi.ProofD)
		if !ok {
			continue
		}
		metadata := MetadataFromInt(proofd.ADisclosed[1], configuration) // index 1 is metadata attribute
		if metadata.Expiry().Before(*t) {
			return true
		}
	}
	return false
}

94
func extractAttribute(pl gabi.ProofList, index *DisclosedAttributeIndex, notrevoked *time.Time, conf *Configuration) (*DisclosedAttribute, *string, error) {
95
96
97
98
99
100
101
102
103
104
105
	if len(pl) < index.CredentialIndex {
		return nil, nil, errors.New("Credential index out of range")
	}
	proofd, ok := pl[index.CredentialIndex].(*gabi.ProofD)
	if !ok {
		// If with the index the user told us to look for the required attribute at this specific location,
		// and the proof here is not a disclosure proof, then reject
		return nil, nil, errors.New("ProofList contained proof of invalid type")
	}

	metadata := MetadataFromInt(proofd.ADisclosed[1], conf) // index 1 is metadata attribute
106
	return parseAttribute(index.AttributeIndex, metadata, proofd.ADisclosed[index.AttributeIndex], notrevoked)
107
108
}

109
// VerifyProofs verifies the proofs cryptographically.
110
111
112
113
func (pl ProofList) VerifyProofs(
	configuration *Configuration,
	context *big.Int, nonce *big.Int,
	publickeys []*gabi.PublicKey,
114
	revRecords map[CredentialTypeIdentifier]*revocation.Update,
115
	isSig bool,
116
) (bool, map[int]*time.Time, error) {
117
118
	// Empty proof lists are allowed (if consistent with the session request, which is checked elsewhere)
	if len(pl) == 0 {
119
		return true, nil, nil
120
121
	}

122
123
124
125
	if publickeys == nil {
		var err error
		publickeys, err = pl.ExtractPublicKeys(configuration)
		if err != nil {
126
			return false, nil, err
127
128
129
130
		}
	}

	if len(pl) != len(publickeys) {
131
		return false, nil, errors.New("Insufficient public keys to verify the proofs")
132
	}
133

134
135
136
	// Compute slice to inform gabi of which proofs should be verified to share the same secret key
	keyshareServers := make([]string, len(pl))
	for i := range pl {
137
138
		schemeID := NewIssuerIdentifier(publickeys[i].Issuer).SchemeManagerIdentifier()
		if !configuration.SchemeManagers[schemeID].Distributed() {
139
			keyshareServers[i] = "." // dummy value: no IRMA scheme will ever have this name
140
		} else {
141
			keyshareServers[i] = schemeID.Name()
142
143
144
		}
	}

145
	if !gabi.ProofList(pl).Verify(publickeys, context, nonce, isSig, keyshareServers) {
146
		return false, nil, nil
147
148
	}

149
150
151
	// Perform per-proof verifications for each proof:
	// - verify that any singleton credential occurs at most once in the prooflist
	// - verify that all required nonrevocation proofs are present
152
	singletons := map[CredentialTypeIdentifier]bool{}
153
	revocationtime := map[int]*time.Time{} // per proof, stores up to what time it is known to be not revoked
154
	for i, proof := range pl {
155
156
157
158
159
160
		proofd, ok := proof.(*gabi.ProofD)
		if !ok {
			continue
		}
		typ := MetadataFromInt(proofd.ADisclosed[1], configuration).CredentialType()
		if typ == nil {
161
			return false, nil, errors.New("Received unknown credential type")
162
		}
163
		id := typ.Identifier()
164
		if typ.IsSingleton {
165
166
			if !singletons[id] { // Seen for the first time
				singletons[id] = true
167
			} else { // Seen for the second time
168
				return false, nil, nil
169
170
			}
		}
171
172
173
174
175

		// The cryptographic validity of all included nonrevocation proofs has already been checked
		// by ProofList.Verify() above, so all that remains here is to check if all expected
		// nonrevocation proofs are present, and against the expected accumulator value:
		// the last one in the update message set we provided along with the session request,
176
		// OR a newer one included in the proofs itself.
177
		r := revRecords[id]
178
		if r == nil { // no nonrevocation proof was requested for this credential
179
			return true, nil, nil
180
181
		}
		if !proofd.HasNonRevocationProof() {
182
			return false, nil, nil
183
		}
184
185
186
187
188
189
190
191
192
193
194
195
196

		sig := proofd.NonRevocationProof.SignedAccumulator
		pk, err := RevocationKeys{configuration}.PublicKey(typ.IssuerIdentifier(), sig.PKIndex)
		if err != nil {
			return false, nil, nil
		}
		acc, err := proofd.NonRevocationProof.SignedAccumulator.UnmarshalVerify(pk)
		if err != nil {
			return false, nil, nil
		}

		ours, theirs := r.Events[len(r.Events)-1].Index, acc.Index
		if ours > theirs {
197
198
199
			return false, nil, errors.New("nonrevocation proof used wrong accumulator")
		}
		if ours == theirs {
200
			settings := configuration.Revocation.getSettings(id)
201
			if uint(time.Now().Sub(settings.updated).Seconds()) > settings.MaxNonrevocationDuration {
202
				revocationtime[i] = &settings.updated
203
			}
204
		}
205
206
	}

207
	return true, revocationtime, nil
208
}
209

210
211
212
213
func (d *Disclosure) extraIndices(condiscon AttributeConDisCon) []*DisclosedAttributeIndex {
	disclosed := make([]map[int]struct{}, len(d.Proofs))
	for i, proof := range d.Proofs {
		proofd, ok := proof.(*gabi.ProofD)
214
		if !ok {
215
			continue
216
		}
217
218
219
220
		disclosed[i] = map[int]struct{}{}
		for j := range proofd.ADisclosed {
			if j <= 1 {
				continue
221
			}
222
			disclosed[i][j] = struct{}{}
223
224
225
		}
	}

226
227
	for i, set := range d.Indices {
		if len(condiscon) <= i {
228
229
			continue
		}
230
231
232
233
		for _, index := range set {
			delete(disclosed[index.CredentialIndex], index.AttributeIndex)
		}
	}
234

235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
	var extra []*DisclosedAttributeIndex
	for i, attrs := range disclosed {
		for j := range attrs {
			extra = append(extra, &DisclosedAttributeIndex{CredentialIndex: i, AttributeIndex: j})
		}
	}

	return extra
}

// DisclosedAttributes returns a slice containing for each item in the conjunction the disclosed
// attributes that are present in the proof list. If a non-empty and non-nil AttributeDisjunctionList
// is included, then the first attributes in the returned slice match with the disjunction list in
// the disjunction list. The first return parameter of this function indicates whether or not all
// disjunctions (if present) are satisfied.
250
251
252
func (d *Disclosure) DisclosedAttributes(configuration *Configuration, condiscon AttributeConDisCon, revtimes map[int]*time.Time) (bool, [][]*DisclosedAttribute, error) {
	if revtimes == nil {
		revtimes = map[int]*time.Time{}
253
	}
254
	complete, list, err := condiscon.Satisfy(d, revtimes, configuration)
255
256
257
258
259
260
261
	if err != nil {
		return false, nil, err
	}

	var extra []*DisclosedAttribute
	indices := d.extraIndices(condiscon)
	for _, index := range indices {
262
		attr, _, err := extractAttribute(d.Proofs, index, revtimes[index.CredentialIndex], configuration)
263
264
		if err != nil {
			return false, nil, err
265
		}
266
267
268
269
270
		attr.Status = AttributeProofStatusExtra
		extra = append(extra, attr)
	}
	if len(extra) > 0 {
		list = append(list, extra)
271
	}
272

273
	return complete, list, nil
274
275
}

276
func parseAttribute(index int, metadata *MetadataAttribute, attr *big.Int, notrevoked *time.Time) (*DisclosedAttribute, *string, error) {
277
278
279
280
281
282
283
284
285
286
287
288
289
290
	var attrid AttributeTypeIdentifier
	var attrval *string
	credtype := metadata.CredentialType()
	if credtype == nil {
		return nil, nil, errors.New("ProofList contained a disclosure proof of an unkown credential type")
	}
	if index == 1 {
		attrid = NewAttributeTypeIdentifier(credtype.Identifier().String())
		p := "present"
		attrval = &p
	} else {
		attrid = credtype.AttributeTypes[index-2].GetAttributeTypeIdentifier()
		attrval = decodeAttribute(attr, metadata.Version())
	}
291
292
293
294
	status := AttributeProofStatusPresent
	if attrval == nil {
		status = AttributeProofStatusNull
	}
295
	return &DisclosedAttribute{
296
297
298
299
300
301
		Identifier:       attrid,
		RawValue:         attrval,
		Value:            NewTranslatedString(attrval),
		Status:           status,
		IssuanceTime:     Timestamp(metadata.SigningDate()),
		NotRevokedBefore: (*Timestamp)(notrevoked),
302
	}, attrval, nil
303
304
}

305
func (d *Disclosure) VerifyAgainstRequest(
306
	configuration *Configuration,
307
	request SessionRequest,
308
309
	context, nonce *big.Int,
	publickeys []*gabi.PublicKey,
310
	validAt *time.Time,
311
	issig bool,
312
) ([][]*DisclosedAttribute, ProofStatus, error) {
313
	var required AttributeConDisCon
314
	var revupdates map[CredentialTypeIdentifier]*revocation.Update
315
	if request != nil {
316
		revupdates = request.Base().RevocationUpdates
317
318
319
320
		required = request.Disclosure().Disclose
	}

	// Cryptographically verify all included IRMA proofs
321
	valid, revtimes, err := ProofList(d.Proofs).VerifyProofs(configuration, context, nonce, publickeys, revupdates, issig)
322
323
	if !valid || err != nil {
		return nil, ProofStatusInvalid, err
324
325
326
	}

	// Next extract the contained attributes from the proofs, and match them to the signature request if present
327
	allmatched, list, err := d.DisclosedAttributes(configuration, required, revtimes)
328
	if err != nil {
329
		return nil, ProofStatusInvalid, err
330
331
332
333
	}

	// Return MISSING_ATTRIBUTES as proofstatus if one of the disjunctions in the request (if present) is not satisfied
	if !allmatched {
334
335
336
		return list, ProofStatusMissingAttributes, nil
	}

337
338
339
340
341
	// Check that all credentials were unexpired
	if expired := ProofList(d.Proofs).Expired(configuration, validAt); expired {
		return list, ProofStatusExpired, nil
	}

342
343
344
	return list, ProofStatusValid, nil
}

345
func (d *Disclosure) Verify(configuration *Configuration, request *DisclosureRequest) ([][]*DisclosedAttribute, ProofStatus, error) {
346
	return d.VerifyAgainstRequest(configuration, request, request.GetContext(), request.GetNonce(nil), nil, nil, false)
347
348
}

349
350
351
352
353
354
355
356
357
// Verify the attribute-based signature, optionally against a corresponding signature request. If the request is present
// (i.e. not nil), then the first attributes in the returned result match with the disjunction list in the request
// (that is, the i'th attribute in the result should satisfy the i'th disjunction in the request). If the request is not
// fully satisfied in this fasion, the Status of the result is ProofStatusMissingAttributes. Any remaining attributes
// (i.e. not asked for by the request) are also included in the result, after the attributes that match disjunctions
// in the request.
//
// The signature request is optional; if it is nil then the attribute-based signature is still verified, and all
// containing attributes returned in the result.
358
func (sm *SignedMessage) Verify(configuration *Configuration, request *SignatureRequest) ([][]*DisclosedAttribute, ProofStatus, error) {
359
	var message string
360

361
362
363
364
	if len(sm.Signature) == 0 {
		return nil, ProofStatusInvalid, nil
	}

365
	// First check if this signature matches the request
366
367
	if request != nil {
		if !sm.MatchesNonceAndContext(request) {
368
			return nil, ProofStatusUnmatchedRequest, nil
369
		}
370
371
372
373
374
		// If there is a request, then the signed message must be that of the request
		message = request.Message
	} else {
		// If not, we just verify that the signed message is a valid signature over its contained message
		message = sm.Message
375
376
	}

377
	// Next, verify the timestamp so we can safely use its time
378
	t := time.Now()
379
	if sm.Timestamp != nil {
380
381
382
		if err := sm.VerifyTimestamp(message, configuration); err != nil {
			return nil, ProofStatusInvalidTimestamp, nil
		}
383
		t = time.Unix(sm.Timestamp.Time, 0)
384
385
	}

386
387
388
389
390
	// Finally, cryptographically verify the IRMA disclosure proofs in the signature
	// and verify that it satisfies the signature request, if present
	var r SessionRequest // wrapper for request to avoid avoid https://golang.org/doc/faq#nil_error
	if request != nil {
		r = request
391
	}
392
	return sm.Disclosure().VerifyAgainstRequest(configuration, r, sm.Context, sm.GetNonce(), nil, &t, true)
393
}
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429

// ExpiredError indicates that something (e.g. a JWT) has expired.
type ExpiredError struct {
	Err error // underlying error
}

func (e ExpiredError) Error() string {
	return "irmago: expired (" + e.Err.Error() + ")"
}

// ParseApiServerJwt verifies and parses a JWT as returned by an irma_api_server after a disclosure request into a key-value pair.
func ParseApiServerJwt(inputJwt string, signingKey *rsa.PublicKey) (map[AttributeTypeIdentifier]*DisclosedAttribute, error) {
	claims := struct {
		jwt.StandardClaims
		Attributes map[AttributeTypeIdentifier]string `json:"attributes"`
	}{}
	_, err := jwt.ParseWithClaims(inputJwt, claims, func(token *jwt.Token) (interface{}, error) {
		return signingKey, nil
	})
	if err != nil {
		if err, ok := err.(*jwt.ValidationError); ok && (err.Errors&jwt.ValidationErrorExpired) != 0 {
			return nil, ExpiredError{err}
		} else {
			return nil, err
		}
	}

	if claims.Subject != "disclosure_result" {
		return nil, errors.New("JWT is not a disclosure result")
	}

	disclosedAttributes := make(map[AttributeTypeIdentifier]*DisclosedAttribute, len(claims.Attributes))
	for id, value := range claims.Attributes {
		disclosedAttributes[id] = &DisclosedAttribute{
			Identifier: id,
			RawValue:   &value,
430
			Value:      NewTranslatedString(&value),
431
432
433
434
435
436
			Status:     AttributeProofStatusPresent,
		}
	}

	return disclosedAttributes, nil
}