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

import (
	"math/big"
	"time"
6 7 8

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

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

14 15 16
// Status is the proof status of a single attribute
type AttributeProofStatus string

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

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

28 29 30 31 32
	AttributeProofStatusPresent      = AttributeProofStatus("PRESENT")       // Attribute is disclosed and matches the value
	AttributeProofStatusExtra        = AttributeProofStatus("EXTRA")         // Attribute is disclosed, but wasn't requested in request
	AttributeProofStatusMissing      = AttributeProofStatus("MISSING")       // Attribute is NOT disclosed, but should be according to request
	AttributeProofStatusInvalidValue = AttributeProofStatus("INVALID_VALUE") // Attribute is disclosed, but has invalid value according to request
)
33 34 35 36

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

41 42
type DisclosedCredentialList []*DisclosedCredential

43 44 45 46
// VerificationResult is a result of verification of a SignedMessage or disclosure proof, containing all the disclosed attributes
type VerificationResult struct {
	Attributes []*DisclosedAttribute
	Status     ProofStatus
47 48
}

49 50 51 52 53
// DisclosedAttribute represents a disclosed attribute.
type DisclosedAttribute struct {
	Value      TranslatedString        `json:"value"` // Value of the disclosed attribute
	Identifier AttributeTypeIdentifier `json:"id"`
	Status     AttributeProofStatus    `json:"status"`
54 55
}

56 57
// Returns true if one of the disclosed credentials is expired at the specified time
func (disclosed DisclosedCredentialList) IsExpired(t time.Time) bool {
58
	for _, cred := range disclosed {
59
		if cred.IsExpired(t) {
60 61 62 63 64 65
			return true
		}
	}
	return false
}

66 67
func ExtractDisclosedCredentials(conf *Configuration, proofList gabi.ProofList) (DisclosedCredentialList, error) {
	var credentials = make(DisclosedCredentialList, 0, len(proofList))
68

69 70 71 72 73 74 75 76 77
	for _, v := range proofList {
		switch v.(type) {
		case *gabi.ProofD:
			proof := v.(*gabi.ProofD)
			cred := newDisclosedCredential(proof.ADisclosed, conf)
			credentials = append(credentials, cred)
		case *gabi.ProofU: // nop
		default:
			return nil, errors.New("Cannot extract credentials from proof, not a disclosure proofD")
78 79 80
		}
	}

81
	return credentials, nil
82 83
}

84 85
func (cred *DisclosedCredential) IsExpired(t time.Time) bool {
	return cred.metadataAttribute.Expiry().Before(t)
86 87
}

88
func newDisclosedCredential(aDisclosed map[int]*big.Int, configuration *Configuration) *DisclosedCredential {
Tomas's avatar
Tomas committed
89 90
	rawAttributes := make(map[AttributeTypeIdentifier]*string)
	attributes := make(map[AttributeTypeIdentifier]TranslatedString)
91 92 93 94 95 96 97 98 99

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

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

100
		id := cred.Attributes[k-2].GetAttributeTypeIdentifier()
101
		attributeValue := decodeAttribute(v, metadata.Version())
Tomas's avatar
Tomas committed
102 103
		rawAttributes[id] = attributeValue
		attributes[id] = translateAttribute(attributeValue)
104 105 106 107
	}

	return &DisclosedCredential{
		metadataAttribute: metadata,
Tomas's avatar
Tomas committed
108
		rawAttributes:     rawAttributes,
109 110 111 112
		Attributes:        attributes,
	}
}

113 114 115
// extractPublicKeys returns the public keys of each proof in the proofList, in the same order,
// for later use in verification of the proofList. If one of the proofs is not a ProofD
// an error is returned.
116 117
func extractPublicKeys(configuration *Configuration, proofList gabi.ProofList) ([]*gabi.PublicKey, error) {
	var publicKeys = make([]*gabi.PublicKey, 0, len(proofList))
118

119
	for _, v := range proofList {
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
		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
}

136 137 138 139 140 141
// verify an IRMA proofList cryptographically.
func verify(configuration *Configuration, proofList gabi.ProofList, context *big.Int, nonce *big.Int, isSig bool) bool {
	// Extract public keys
	pks, err := extractPublicKeys(configuration, proofList)
	if err != nil {
		return false
142 143
	}

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

147 148 149 150 151 152 153
func (disclosed DisclosedCredentialList) attributeList(configuration *Configuration, sigRequest *SignatureRequest) (bool, []*DisclosedAttribute) {
	var list []*DisclosedAttribute
	if sigRequest != nil {
		list = make([]*DisclosedAttribute, len(sigRequest.Content))
		for i := range list {
			list[i] = &DisclosedAttribute{
				Status: AttributeProofStatusMissing,
154 155 156 157
			}
		}
	}

158 159
	// attributes that have not yet been matched to one of the disjunctions of the request
	extraAttrs := map[AttributeTypeIdentifier]*DisclosedAttribute{}
160

161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
	for _, cred := range disclosed {
		for attrid, value := range cred.Attributes {
			attr := &DisclosedAttribute{
				Value:      value,
				Identifier: attrid,
				Status:     AttributeProofStatusExtra,
			}
			extraAttrs[attrid] = attr
			if sigRequest == nil {
				continue
			}
			for i, disjunction := range sigRequest.Content {
				if disjunction.attemptSatisfy(attrid, cred.rawAttributes[attrid]) {
					if disjunction.satisfied() {
						attr.Status = AttributeProofStatusPresent
					} else {
						attr.Status = AttributeProofStatusInvalidValue
					}
					list[i] = attr
					delete(extraAttrs, attrid)
				}
			}
183 184 185
		}
	}

186 187
	for _, attr := range extraAttrs {
		list = append(list, attr)
188 189
	}

190
	return sigRequest == nil || sigRequest.Content.satisfied(), list
191 192
}

193 194 195 196 197 198 199 200 201 202 203 204
// 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.
func (sm *SignedMessage) Verify(configuration *Configuration, request *SignatureRequest) (result *VerificationResult) {
	var message string
	result = &VerificationResult{}
205

206
	// First check if this signature matches the request
207 208 209 210 211
	if request != nil {
		request.Timestamp = sm.Timestamp
		if !sm.MatchesNonceAndContext(request) {
			result.Status = ProofStatusUnmatchedRequest
			return
212
		}
213 214 215 216 217
		// 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
218 219 220
	}

	// Verify the timestamp
221
	if sm.Timestamp != nil {
222 223 224
		if err := sm.VerifyTimestamp(message, configuration); err != nil {
			result.Status = ProofStatusInvalidTimestamp
			return
225
		}
226 227
	}

228
	// Now, cryptographically verify the IRMA disclosure proofs in the signature
229
	if !verify(configuration, sm.Signature, sm.Context, sm.GetNonce(), true) {
230 231
		result.Status = ProofStatusInvalidCrypto
		return
232 233
	}

234 235
	// Next extract the contained attributes from the signature, and match them to the signature request if present
	var allmatched bool
236
	disclosed, err := ExtractDisclosedCredentials(configuration, sm.Signature)
237
	if err != nil {
238 239 240 241 242 243 244 245 246 247
		result.Status = ProofStatusInvalidCrypto
		return
	}
	allmatched, result.Attributes = disclosed.attributeList(configuration, request)

	// Return MISSING_ATTRIBUTES as proofstatus if one of the disjunctions in the request (if present) is not satisfied
	// This status takes priority over 'EXPIRED'
	if !allmatched {
		result.Status = ProofStatusMissingAttributes
		return
248 249
	}

250
	// Check if a credential is expired
251
	if sm.Timestamp == nil {
252
		if disclosed.IsExpired(time.Now()) {
253 254 255 256 257
			// 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.
			result.Status = ProofStatusExpired
			return
258 259
		}
	} else {
260
		if disclosed.IsExpired(time.Unix(sm.Timestamp.Time, 0)) {
261 262 263 264
			// 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
			result.Status = ProofStatusInvalidCrypto
			return
265
		}
266 267
	}

268 269 270
	// All disjunctions satisfied and nothing expired, proof is valid!
	result.Status = ProofStatusValid
	return
271
}