verify.go 14.3 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
	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"`
40
41
}

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

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

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

53
	for _, v := range pl {
54
55
56
57
58
59
60
61
		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
			}
62
63
64
			if publicKey == nil {
				return nil, ErrorMissingPublicKey
			}
65
66
			publicKeys = append(publicKeys, publicKey)
		default:
67
			return nil, errors.New("Cannot extract public key, not a disclosure proofD")
68
69
70
71
72
		}
	}
	return publicKeys, nil
}

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 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
}

func extractAttribute(pl gabi.ProofList, index *DisclosedAttributeIndex, conf *Configuration) (*DisclosedAttribute, *string, error) {
	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
	return parseAttribute(index.AttributeIndex, metadata, proofd.ADisclosed[index.AttributeIndex])
}

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

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

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

133
134
135
	// 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 {
136
137
		schemeID := NewIssuerIdentifier(publickeys[i].Issuer).SchemeManagerIdentifier()
		if !configuration.SchemeManagers[schemeID].Distributed() {
138
			keyshareServers[i] = "." // dummy value: no IRMA scheme will ever have this name
139
		} else {
140
			keyshareServers[i] = schemeID.Name()
141
142
143
		}
	}

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

148
149
150
	// 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
151
152
153
154
155
156
157
158
159
160
	singletons := map[CredentialTypeIdentifier]bool{}
	for _, proof := range pl {
		proofd, ok := proof.(*gabi.ProofD)
		if !ok {
			continue
		}
		typ := MetadataFromInt(proofd.ADisclosed[1], configuration).CredentialType()
		if typ == nil {
			return false, errors.New("Received unknown credential type")
		}
161
		id := typ.Identifier()
162
		if typ.IsSingleton {
163
164
			if !singletons[id] { // Seen for the first time
				singletons[id] = true
165
166
167
168
			} else { // Seen for the second time
				return false, nil
			}
		}
169
170
171
172
173

		// 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,
174
		// OR a newer one included in the proofs itself.
175
176
177
178
179
180
181
		r := revRecords[id]
		if len(r) == 0 { // no nonrevocation proof was requested for this credential
			return true, nil
		}
		if !proofd.HasNonRevocationProof() {
			return false, nil
		}
182
		if proofd.NonRevocationProof.Accumulator.Index < r[len(r)-1].EndIndex {
183
184
			return false, errors.New("nonrevocation proof used wrong accumulator")
		}
185
186
187
	}

	return true, nil
188
}
189

190
191
192
193
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)
194
		if !ok {
195
			continue
196
		}
197
198
199
200
		disclosed[i] = map[int]struct{}{}
		for j := range proofd.ADisclosed {
			if j <= 1 {
				continue
201
			}
202
			disclosed[i][j] = struct{}{}
203
204
205
		}
	}

206
207
	for i, set := range d.Indices {
		if len(condiscon) <= i {
208
209
			continue
		}
210
211
212
213
		for _, index := range set {
			delete(disclosed[index.CredentialIndex], index.AttributeIndex)
		}
	}
214

215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
	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.
func (d *Disclosure) DisclosedAttributes(configuration *Configuration, condiscon AttributeConDisCon) (bool, [][]*DisclosedAttribute, error) {
	complete, list, err := condiscon.Satisfy(d, configuration)
	if err != nil {
		return false, nil, err
	}

	var extra []*DisclosedAttribute
	indices := d.extraIndices(condiscon)
	for _, index := range indices {
		attr, _, err := extractAttribute(d.Proofs, index, configuration)
		if err != nil {
			return false, nil, err
242
		}
243
244
245
246
247
		attr.Status = AttributeProofStatusExtra
		extra = append(extra, attr)
	}
	if len(extra) > 0 {
		list = append(list, extra)
248
	}
249

250
	return complete, list, nil
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
}

func parseAttribute(index int, metadata *MetadataAttribute, attr *big.Int) (*DisclosedAttribute, *string, error) {
	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())
	}
268
269
270
271
	status := AttributeProofStatusPresent
	if attrval == nil {
		status = AttributeProofStatusNull
	}
272
	return &DisclosedAttribute{
273
274
275
276
277
		Identifier:   attrid,
		RawValue:     attrval,
		Value:        NewTranslatedString(attrval),
		Status:       status,
		IssuanceTime: Timestamp(metadata.SigningDate()),
278
	}, attrval, nil
279
280
}

281
func (d *Disclosure) VerifyAgainstRequest(
282
	configuration *Configuration,
283
	request SessionRequest,
284
285
	context, nonce *big.Int,
	publickeys []*gabi.PublicKey,
286
	validAt *time.Time,
287
	issig bool,
288
) ([][]*DisclosedAttribute, ProofStatus, error) {
289
	var required AttributeConDisCon
290
	var revRecords map[CredentialTypeIdentifier][]*revocation.Record
291
292
293
294
295
296
297
	if request != nil {
		revRecords = request.Base().RevocationUpdates
		required = request.Disclosure().Disclose
	}

	// Cryptographically verify all included IRMA proofs
	valid, err := ProofList(d.Proofs).VerifyProofs(configuration, context, nonce, publickeys, revRecords, issig)
298
299
	if !valid || err != nil {
		return nil, ProofStatusInvalid, err
300
301
302
	}

	// Next extract the contained attributes from the proofs, and match them to the signature request if present
303
	allmatched, list, err := d.DisclosedAttributes(configuration, required)
304
	if err != nil {
305
		return nil, ProofStatusInvalid, err
306
307
308
309
	}

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

313
314
315
316
317
	// Check that all credentials were unexpired
	if expired := ProofList(d.Proofs).Expired(configuration, validAt); expired {
		return list, ProofStatusExpired, nil
	}

318
319
320
	return list, ProofStatusValid, nil
}

321
func (d *Disclosure) Verify(configuration *Configuration, request *DisclosureRequest) ([][]*DisclosedAttribute, ProofStatus, error) {
322
	return d.VerifyAgainstRequest(configuration, request, request.GetContext(), request.GetNonce(nil), nil, nil, false)
323
324
}

325
326
327
328
329
330
331
332
333
// 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.
334
func (sm *SignedMessage) Verify(configuration *Configuration, request *SignatureRequest) ([][]*DisclosedAttribute, ProofStatus, error) {
335
	var message string
336

337
338
339
340
	if len(sm.Signature) == 0 {
		return nil, ProofStatusInvalid, nil
	}

341
	// First check if this signature matches the request
342
343
	if request != nil {
		if !sm.MatchesNonceAndContext(request) {
344
			return nil, ProofStatusUnmatchedRequest, nil
345
		}
346
347
348
349
350
		// 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
351
352
	}

353
	// Next, verify the timestamp so we can safely use its time
354
	t := time.Now()
355
	if sm.Timestamp != nil {
356
357
358
		if err := sm.VerifyTimestamp(message, configuration); err != nil {
			return nil, ProofStatusInvalidTimestamp, nil
		}
359
		t = time.Unix(sm.Timestamp.Time, 0)
360
361
	}

362
363
364
365
366
	// 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
367
	}
368
	return sm.Disclosure().VerifyAgainstRequest(configuration, r, sm.Context, sm.GetNonce(), nil, &t, true)
369
}
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405

// 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,
406
			Value:      NewTranslatedString(&value),
407
408
409
410
411
412
			Status:     AttributeProofStatusPresent,
		}
	}

	return disclosedAttributes, nil
}