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 16 17
)

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

const (
	VALID              = ProofStatus("VALID")
	INVALID_CRYPTO     = ProofStatus("INVALID_CRYPTO")
18
	INVALID_TIMESTAMP  = ProofStatus("INVALID_TIMESTAMP")
19
	UNMATCHED_REQUEST  = ProofStatus("UNMATCHED_REQUEST")
20
	MISSING_ATTRIBUTES = ProofStatus("MISSING_ATTRIBUTES")
21 22 23 24

	// The contained attributes are currently expired, but it is not certain if they already were expired
	// during creation of the ABS.
	EXPIRED = 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 72 73 74 75 76 77 78 79 80
			ar.AttributeProofStatus = PRESENT
			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
			ar.AttributeProofStatus = INVALID_VALUE
		}
	}

	// 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 83 84 85 86 87 88 89 90
		ar.AttributeProofStatus = MISSING
	}
	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 104 105 106

		// 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)
		signatureProofResult.ProofStatus = MISSING_ATTRIBUTES
	}

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
		}

Tomas's avatar
Tomas committed
165
		id := cred.Attributes[k-2].GetAttributeTypeIdentifier(cred.Identifier())
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 230 231 232 233 234 235 236 237 238 239
				DisclosedId:    attrId,
				ProofStatus:    EXTRA,
			}
			returnDisjunctions = append(returnDisjunctions, &dummyDisj)
		}
	}

	return returnDisjunctions
}

// Check an gabi prooflist against a signature proofrequest
240
func checkProofWithRequest(configuration *Configuration, irmaSignature *SignedMessage, sigRequest *SignatureRequest) *SignatureProofResult {
Tomas's avatar
Tomas committed
241
	disclosed, err := ExtractDisclosedCredentials(configuration, irmaSignature.Signature)
242 243 244 245

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

252
	signatureProofResult := disclosed.createAndCheckSignatureProofResult(configuration, sigRequest)
253 254 255 256 257 258 259 260

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

	// If all disjunctions are satisfied, check if a credential is expired
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
	if irmaSignature.Timestamp == nil {
		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.
			signatureProofResult.ProofStatus = EXPIRED
			return signatureProofResult
		}
	} else {
		if disclosed.IsExpired(time.Unix(irmaSignature.Timestamp.Time, 0)) {
			// 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
			signatureProofResult.ProofStatus = INVALID_CRYPTO
			return signatureProofResult
		}
276 277 278 279 280 281 282 283
	}

	// All disjunctions satisfied and nothing expired, proof is valid!
	signatureProofResult.ProofStatus = VALID
	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 VerifySig(configuration *Configuration, irmaSignature *SignedMessage, sigRequest *SignatureRequest) *SignatureProofResult {
296 297 298 299 300 301 302 303 304 305 306
	// First check if this signature matches the request
	sigRequest.Timestamp = irmaSignature.Timestamp
	if !irmaSignature.MatchesNonceAndContext(sigRequest) {
		return &SignatureProofResult{
			ProofResult: &ProofResult{
				ProofStatus: UNMATCHED_REQUEST,
			},
		}
	}

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

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

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

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

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

	// Extract attributes and return result
Tomas's avatar
Tomas committed
345
	disclosed, err := ExtractDisclosedCredentials(configuration, irmaSignature.Signature)
346 347 348 349 350 351

	if err != nil {
		fmt.Println(err)
		return INVALID_CRYPTO, nil
	}

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

	return VALID, disclosed
363
}