verify.go 11.8 KB
Newer Older
1 2 3 4 5 6
package irma

import (
	"fmt"
	"math/big"
	"time"
7 8 9

	"github.com/go-errors/errors"
	"github.com/mhe/gabi"
10 11 12 13 14 15
)

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

const (
16 17 18 19 20
	ProofStatusValid             = ProofStatus("VALID")
	ProofStatusInvalidCrypto     = ProofStatus("INVALID_CRYPTO")
	ProofStatusInvalidTimestamp  = ProofStatus("INVALID_TIMESTAMP")
	ProofStatusUnmatchedRequest  = ProofStatus("UNMATCHED_REQUEST")
	ProofStatusMissingAttributes = ProofStatus("MISSING_ATTRIBUTES")
21 22 23

	// The contained attributes are currently expired, but it is not certain if they already were expired
	// during creation of the ABS.
24
	ProofStatusExpired = ProofStatus("EXPIRED")
25 26 27 28
)

// ProofResult is a result of a complete proof, containing all the disclosed attributes and corresponding request
type ProofResult struct {
29
	Disjunctions []*DisclosedAttributeDisjunction `json:"disjunctions"`
30 31 32 33
	ProofStatus  ProofStatus
}

type SignatureProofResult struct {
34
	*ProofResult
35
	Message string `json:"message"`
36 37 38 39 40
}

// DisclosedCredential contains raw disclosed credentials, without any extra parsing information
type DisclosedCredential struct {
	metadataAttribute *MetadataAttribute
Tomas's avatar
Tomas committed
41 42
	rawAttributes     map[AttributeTypeIdentifier]*string
	Attributes        map[AttributeTypeIdentifier]TranslatedString `json:"attributes"`
43 44
}

45 46 47 48 49 50
type DisclosedCredentialList []*DisclosedCredential

// Helper function to check if an attribute is satisfied against a list of disclosed attributes
// This is the case if:
// attribute is contained in disclosed AND if a value is present: equal to that value
// al can be nil if you don't want to include attribute status for proof
51
func (disclosed DisclosedCredentialList) isAttributeSatisfied(attributeId AttributeTypeIdentifier, requestedValue *string) (bool, *AttributeResult) {
52 53 54 55 56
	ar := AttributeResult{
		AttributeId: attributeId,
	}

	for _, cred := range disclosed {
57
		disclosedAttributeValue := cred.Attributes[attributeId]
58 59

		// Continue to next credential if requested attribute isn't disclosed in this credential
Tomas's avatar
Tomas committed
60
		if disclosedAttributeValue == nil || len(disclosedAttributeValue) == 0 {
61 62 63 64 65 66 67
			continue
		}

		// If this is the disclosed attribute, check if value matches
		// Attribute is satisfied if:
		// - Attribute is disclosed (i.e. not nil)
		// - Value is empty OR value equal to disclosedValue
Tomas's avatar
Tomas committed
68
		ar.AttributeValue = disclosedAttributeValue
69

Tomas's avatar
Tomas committed
70
		if requestedValue == nil || *cred.rawAttributes[attributeId] == *requestedValue {
71
			ar.AttributeProofStatus = AttributeProofStatusPresent
72 73 74 75
			return true, &ar
		} else {
			// If attribute is disclosed and present, but not equal to required value, mark it as invalid_value
			// We won't return true and continue searching in other disclosed attributes
76
			ar.AttributeProofStatus = AttributeProofStatusInvalidValue
77 78 79 80
		}
	}

	// If there is never a value assigned, then this attribute isn't disclosed, and thus missing
Tomas's avatar
Tomas committed
81
	if len(ar.AttributeValue) == 0 {
82
		ar.AttributeProofStatus = AttributeProofStatusMissing
83 84 85 86 87 88 89 90
	}
	return false, &ar
}

// Create a signature proof result and check disclosed credentials against a signature request
func (disclosed DisclosedCredentialList) createAndCheckSignatureProofResult(configuration *Configuration, sigRequest *SignatureRequest) *SignatureProofResult {
	signatureProofResult := SignatureProofResult{
		ProofResult: &ProofResult{},
91
		Message:     sigRequest.Message,
92 93 94
	}
	for _, content := range sigRequest.Content {
		isSatisfied, disjunction := content.SatisfyDisclosed(disclosed, configuration)
95
		signatureProofResult.Disjunctions = append(signatureProofResult.Disjunctions, disjunction)
96 97 98 99 100 101 102 103

		// If satisfied, continue to next one
		if isSatisfied {
			continue
		}

		// Else, set proof status to missing_attributes, but check other as well to add other disjunctions to result
		// (so user also knows attribute status of other disjunctions)
104
		signatureProofResult.ProofStatus = ProofStatusMissingAttributes
105 106
	}

107
	signatureProofResult.Disjunctions = addExtraAttributes(disclosed, signatureProofResult.ProofResult)
108 109 110
	return &signatureProofResult
}

111 112
// Returns true if one of the disclosed credentials is expired at the specified time
func (disclosed DisclosedCredentialList) IsExpired(t time.Time) bool {
113
	for _, cred := range disclosed {
114
		if cred.IsExpired(t) {
115 116 117 118 119 120
			return true
		}
	}
	return false
}

121
func (proofResult *ProofResult) ToAttributeResultList() AttributeResultList {
122 123
	var resultList AttributeResultList

124
	for _, v := range proofResult.Disjunctions {
125 126 127 128 129 130
		result := AttributeResult{
			AttributeValue:       v.DisclosedValue,
			AttributeId:          v.DisclosedId,
			AttributeProofStatus: v.ProofStatus,
		}

131
		resultList = append(resultList, &result)
132
	}
133
	return resultList
134 135 136 137
}

// Returns true if this attrId is present in one of the disjunctions
func (proofResult *ProofResult) ContainsAttribute(attrId AttributeTypeIdentifier) bool {
138
	for _, disj := range proofResult.Disjunctions {
139 140 141 142 143 144 145 146 147 148
		for _, attr := range disj.Attributes {
			if attr == attrId {
				return true
			}
		}
	}

	return false
}

149 150
func (cred *DisclosedCredential) IsExpired(t time.Time) bool {
	return cred.metadataAttribute.Expiry().Before(t)
151 152 153
}

func NewDisclosedCredentialFromADisclosed(aDisclosed map[int]*big.Int, configuration *Configuration) *DisclosedCredential {
Tomas's avatar
Tomas committed
154 155
	rawAttributes := make(map[AttributeTypeIdentifier]*string)
	attributes := make(map[AttributeTypeIdentifier]TranslatedString)
156 157 158 159 160 161 162 163 164

	metadata := MetadataFromInt(aDisclosed[1], configuration) // index 1 is metadata attribute
	cred := metadata.CredentialType()

	for k, v := range aDisclosed {
		if k < 2 {
			continue
		}

165
		id := cred.Attributes[k-2].GetAttributeTypeIdentifier()
166
		attributeValue := decodeAttribute(v, metadata.Version())
Tomas's avatar
Tomas committed
167 168
		rawAttributes[id] = attributeValue
		attributes[id] = translateAttribute(attributeValue)
169 170 171 172
	}

	return &DisclosedCredential{
		metadataAttribute: metadata,
Tomas's avatar
Tomas committed
173
		rawAttributes:     rawAttributes,
174 175 176 177
		Attributes:        attributes,
	}
}

178 179
func extractPublicKeys(configuration *Configuration, proofList gabi.ProofList) ([]*gabi.PublicKey, error) {
	var publicKeys = make([]*gabi.PublicKey, 0, len(proofList))
180

181
	for _, v := range proofList {
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
		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
			}
			publicKeys = append(publicKeys, publicKey)
		default:
			return nil, errors.New("Cannot extract public key, not a disclosure proofD!")
		}
	}
	return publicKeys, nil
}

Tomas's avatar
Tomas committed
198
func ExtractDisclosedCredentials(conf *Configuration, proofList gabi.ProofList) (DisclosedCredentialList, error) {
199
	var credentials = make(DisclosedCredentialList, 0, len(proofList))
200

201
	for _, v := range proofList {
202 203 204 205 206
		switch v.(type) {
		case *gabi.ProofD:
			proof := v.(*gabi.ProofD)
			cred := NewDisclosedCredentialFromADisclosed(proof.ADisclosed, conf)
			credentials = append(credentials, cred)
Tomas's avatar
Tomas committed
207
		case *gabi.ProofU: // nop
208 209 210 211 212 213 214 215 216
		default:
			return nil, errors.New("Cannot extract credentials from proof, not a disclosure proofD!")
		}
	}

	return credentials, nil
}

// Add extra disclosed attributes to an existing and checked ProofResult in 'dummy disjunctions'
217
func addExtraAttributes(disclosed DisclosedCredentialList, proofResult *ProofResult) []*DisclosedAttributeDisjunction {
218 219
	returnDisjunctions := make([]*DisclosedAttributeDisjunction, len(proofResult.Disjunctions))
	copy(returnDisjunctions, proofResult.Disjunctions)
220

221
	for _, cred := range disclosed {
222 223 224 225 226 227
		for attrId := range cred.Attributes {
			if proofResult.ContainsAttribute(attrId) {
				continue
			}

			dummyDisj := DisclosedAttributeDisjunction{
Tomas's avatar
Tomas committed
228
				DisclosedValue: cred.Attributes[attrId],
229
				DisclosedId:    attrId,
230
				ProofStatus:    AttributeProofStatusExtra,
231 232 233 234 235 236 237 238 239
			}
			returnDisjunctions = append(returnDisjunctions, &dummyDisj)
		}
	}

	return returnDisjunctions
}

// Check an gabi prooflist against a signature proofrequest
240 241
func (sm *SignedMessage) checkWithRequest(configuration *Configuration, sigRequest *SignatureRequest) *SignatureProofResult {
	disclosed, err := ExtractDisclosedCredentials(configuration, sm.Signature)
242 243 244 245

	if err != nil {
		fmt.Println(err)
		return &SignatureProofResult{
246
			ProofResult: &ProofResult{
247
				ProofStatus: ProofStatusInvalidCrypto,
248 249 250 251
			},
		}
	}

252
	signatureProofResult := disclosed.createAndCheckSignatureProofResult(configuration, sigRequest)
253 254 255

	// Return MISSING_ATTRIBUTES as proofstatus if one attribute is missing
	// This status takes priority over 'EXPIRED'
256
	if signatureProofResult.ProofStatus == ProofStatusMissingAttributes {
257 258 259 260
		return signatureProofResult
	}

	// If all disjunctions are satisfied, check if a credential is expired
261
	if sm.Timestamp == nil {
262 263 264 265
		if disclosed.IsExpired(time.Now()) {
			// At least one of the contained attributes has currently expired. We don't know the
			// creation time of the ABS so we can't ascertain that the attributes were still valid then.
			// Otherwise the signature is valid.
266
			signatureProofResult.ProofStatus = ProofStatusExpired
267 268 269
			return signatureProofResult
		}
	} else {
270
		if disclosed.IsExpired(time.Unix(sm.Timestamp.Time, 0)) {
271 272
			// The ABS contains attributes that were expired at the time of creation of the ABS.
			// This must not happen and in this case the signature is invalid
273
			signatureProofResult.ProofStatus = ProofStatusInvalidCrypto
274 275
			return signatureProofResult
		}
276 277 278
	}

	// All disjunctions satisfied and nothing expired, proof is valid!
279
	signatureProofResult.ProofStatus = ProofStatusValid
280 281 282 283
	return signatureProofResult
}

// Verify an IRMA proof cryptographically
284
func verify(configuration *Configuration, proofList gabi.ProofList, context *big.Int, nonce *big.Int, isSig bool) bool {
285 286 287 288 289 290 291 292 293 294
	// Extract public keys
	pks, err := extractPublicKeys(configuration, proofList)
	if err != nil {
		return false
	}

	return proofList.Verify(pks, context, nonce, true, isSig)
}

// Verify a signature proof and check if the attributes match the attributes in the original request
295
func (sm *SignedMessage) Verify(configuration *Configuration, sigRequest *SignatureRequest) *SignatureProofResult {
296
	// First check if this signature matches the request
297 298
	sigRequest.Timestamp = sm.Timestamp
	if !sm.MatchesNonceAndContext(sigRequest) {
299 300
		return &SignatureProofResult{
			ProofResult: &ProofResult{
301
				ProofStatus: ProofStatusUnmatchedRequest,
302 303 304 305 306
			},
		}
	}

	// Verify the timestamp
307 308
	if sm.Timestamp != nil {
		if err := sm.VerifyTimestamp(sigRequest.Message, configuration); err != nil {
309 310
			return &SignatureProofResult{
				ProofResult: &ProofResult{
311
					ProofStatus: ProofStatusInvalidTimestamp,
312 313 314
				},
			}
		}
315 316 317
	}

	// Now, cryptographically verify the signature
318
	if !verify(configuration, sm.Signature, sigRequest.GetContext(), sigRequest.GetNonce(), true) {
319
		return &SignatureProofResult{
320
			ProofResult: &ProofResult{
321
				ProofStatus: ProofStatusInvalidCrypto,
322 323 324 325 326
			},
		}
	}

	// Finally, check whether attribute values in proof satisfy the original signature request
327
	return sm.checkWithRequest(configuration, sigRequest)
328 329 330
}

// Verify a signature cryptographically, but do not check/compare with a signature request
331
func (sm *SignedMessage) VerifyWithoutRequest(configuration *Configuration) (ProofStatus, DisclosedCredentialList) {
332
	// First, verify the timestamp, if any
333 334
	if sm.Timestamp != nil {
		if err := sm.VerifyTimestamp(sm.Message, configuration); err != nil {
335
			return ProofStatusInvalidTimestamp, nil
336 337 338 339
		}
	}

	// Cryptographically verify the signature
340
	if !verify(configuration, sm.Signature, sm.Context, sm.GetNonce(), true) {
341
		return ProofStatusInvalidCrypto, nil
342 343 344
	}

	// Extract attributes and return result
345
	disclosed, err := ExtractDisclosedCredentials(configuration, sm.Signature)
346 347 348

	if err != nil {
		fmt.Println(err)
349
		return ProofStatusInvalidCrypto, nil
350 351
	}

352
	if sm.Timestamp == nil {
353
		if disclosed.IsExpired(time.Now()) {
354
			return ProofStatusExpired, disclosed
355 356
		}
	} else {
357
		if disclosed.IsExpired(time.Unix(sm.Timestamp.Time, 0)) {
358
			return ProofStatusInvalidCrypto, nil
359
		}
360 361
	}

362
	return ProofStatusValid, disclosed
363
}