helpers.go 9.57 KB
Newer Older
1
package servercore
Sietse Ringers's avatar
Sietse Ringers committed
2
3

import (
4
	"crypto/sha256"
5
	"encoding/json"
6
	"fmt"
7
	"net/http"
8
	"reflect"
Sietse Ringers's avatar
Sietse Ringers committed
9
10
11
12
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/go-errors/errors"
13
	"github.com/privacybydesign/gabi"
14
15
	"github.com/privacybydesign/gabi/big"
	"github.com/privacybydesign/gabi/revocation"
Sietse Ringers's avatar
Sietse Ringers committed
16
	"github.com/privacybydesign/irmago"
Sietse Ringers's avatar
Sietse Ringers committed
17
	"github.com/privacybydesign/irmago/server"
18
	"github.com/sirupsen/logrus"
19
	"gopkg.in/antage/eventsource.v1"
Sietse Ringers's avatar
Sietse Ringers committed
20
21
22
23
24
25
)

// Session helpers

func (session *session) markAlive() {
	session.lastActive = time.Now()
26
	session.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Debugf("Session marked active, expiry delayed")
Sietse Ringers's avatar
Sietse Ringers committed
27
28
}

Sietse Ringers's avatar
Sietse Ringers committed
29
func (session *session) setStatus(status server.Status) {
30
	session.conf.Logger.WithFields(logrus.Fields{"session": session.token, "prevStatus": session.prevStatus, "status": status}).
31
		Info("Session status updated")
Sietse Ringers's avatar
Sietse Ringers committed
32
	session.status = status
Sietse Ringers's avatar
Sietse Ringers committed
33
	session.result.Status = status
34
	session.sessions.update(session)
35
36
37
38
}

func (session *session) onUpdate() {
	if session.evtSource != nil {
39
		session.conf.Logger.WithFields(logrus.Fields{"session": session.token, "status": session.status}).
40
			Debug("Sending status to SSE listeners")
41
42
		// We send JSON like the other APIs, so quote
		session.evtSource.SendEventMessage(fmt.Sprintf(`"%s"`, session.status), "", "")
43
	}
Sietse Ringers's avatar
Sietse Ringers committed
44
45
}

Sietse Ringers's avatar
Sietse Ringers committed
46
47
48
func (session *session) fail(err server.Error, message string) *irma.RemoteError {
	rerr := server.RemoteError(err, message)
	session.setStatus(server.StatusCancelled)
49
	session.result = &server.SessionResult{Err: rerr, Token: session.token, Status: server.StatusCancelled, Type: session.action}
Sietse Ringers's avatar
Sietse Ringers committed
50
51
52
	return rerr
}

53
const retryTimeLimit = 10 * time.Second
54
55
56
57

// checkCache returns a previously cached response, for replaying against multiple requests from
// irmago's retryablehttp client, if:
// - the same was POSTed as last time
58
59
// - last time was not more than 10 seconds ago (retryablehttp client gives up before this)
// - the session status is what it is expected to be when receiving the request for a second time.
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
func (session *session) checkCache(message []byte, expectedStatus server.Status) (int, []byte) {
	if len(session.responseCache.response) > 0 {
		if session.responseCache.sessionStatus != expectedStatus {
			// don't replay a cache value that was set in a previous session state
			session.responseCache = responseCache{}
			return 0, nil
		}
		if sha256.Sum256(session.responseCache.message) != sha256.Sum256(message) ||
			session.lastActive.Before(time.Now().Add(-retryTimeLimit)) ||
			session.status != expectedStatus {
			return server.JsonResponse(nil, session.fail(server.ErrorUnexpectedRequest, ""))
		}
		return session.responseCache.status, session.responseCache.response
	}
	return 0, nil
}

Sietse Ringers's avatar
Sietse Ringers committed
77
78
// Issuance helpers

79
func (session *session) issuanceHandleRevocation(
80
81
	cred *irma.CredentialRequest, attributes *irma.AttributeList, sk *gabi.PrivateKey,
) (witness *revocation.Witness, nonrevAttr *big.Int, err error) {
82
83
	id := cred.CredentialTypeID
	if !session.conf.IrmaConfiguration.CredentialTypes[id].SupportsRevocation() {
84
85
		return
	}
86
87

	// ensure the client always gets an up to date nonrevocation witness
88
	if settings, ok := session.conf.RevocationSettings[id]; !ok || settings.Mode != irma.RevocationModeServer {
89
		if err = session.conf.IrmaConfiguration.Revocation.UpdateDB(id); err != nil {
90
91
92
			return
		}
	}
93

94
	rs := session.conf.IrmaConfiguration.Revocation
95

96
97
	// Fetch latest revocation record, and then extract the current value of the accumulator
	// from it to generate the witness from
98
	updates, err := rs.UpdateLatest(id, 0)
99
	if err != nil {
100
101
		return
	}
102
103
104
105
	u := updates[uint(cred.KeyCounter)]
	if u == nil {
		return nil, nil, errors.Errorf("no revocation updates found for key %d", cred.KeyCounter)
	}
106
107
	sig := u.SignedAccumulator
	pk, err := rs.Keys.PublicKey(id.IssuerIdentifier(), sig.PKIndex)
108
109
110
	if err != nil {
		return nil, nil, err
	}
111
	acc, err := sig.UnmarshalVerify(pk)
112
113
114
115
	if err != nil {
		return nil, nil, err
	}

116
	if witness, err = sk.RevocationGenerateWitness(acc); err != nil {
117
118
		return
	}
119

120
	witness.SignedAccumulator = sig // attach previously selected reocation record to the witness for the client
121
	nonrevAttr = witness.E
122
	issrecord := &irma.IssuanceRecord{
123
		CredType:   id,
124
		PKIndex:    sk.Counter,
125
		Key:        cred.RevocationKey,
126
		Attr:       (*irma.RevocationAttribute)(nonrevAttr),
127
128
129
		Issued:     time.Now().UnixNano(), // or (floored) cred issuance time?
		ValidUntil: attributes.Expiry().UnixNano(),
	}
130
	err = session.conf.IrmaConfiguration.Revocation.SaveIssuanceRecord(id, issrecord, sk)
131
132
133
	return
}

134
func (s *Server) validateIssuanceRequest(request *irma.IssuanceRequest) error {
Sietse Ringers's avatar
Sietse Ringers committed
135
136
137
	for _, cred := range request.Credentials {
		// Check that we have the appropriate private key
		iss := cred.CredentialTypeID.IssuerIdentifier()
138
		privatekey, err := s.conf.IrmaConfiguration.PrivateKeyLatest(iss)
139
140
141
142
		if err != nil {
			return err
		}
		if privatekey == nil {
143
			return errors.Errorf("missing private key of issuer %s", iss.String())
Sietse Ringers's avatar
Sietse Ringers committed
144
		}
145
		pubkey, err := s.conf.IrmaConfiguration.PublicKey(iss, int(privatekey.Counter))
Sietse Ringers's avatar
Sietse Ringers committed
146
147
148
149
		if err != nil {
			return err
		}
		if pubkey == nil {
150
			return errors.Errorf("missing public key of issuer %s", iss.String())
Sietse Ringers's avatar
Sietse Ringers committed
151
152
153
		}
		cred.KeyCounter = int(privatekey.Counter)

154
155
156
157
		// Check that the credential is consistent with irma_configuration
		if err := cred.Validate(s.conf.IrmaConfiguration); err != nil {
			return err
		}
158

159
		if s.conf.IrmaConfiguration.CredentialTypes[cred.CredentialTypeID].SupportsRevocation() {
160
			enabled, err := s.conf.IrmaConfiguration.Revocation.RevocationEnabled(cred.CredentialTypeID)
161
162
163
			if err != nil {
				return err
			}
164
			if !enabled {
165
166
167
168
				s.conf.Logger.WithFields(logrus.Fields{"cred": cred.CredentialTypeID}).Warn("revocation supported in scheme but not enabled")
			}
		}

Sietse Ringers's avatar
Sietse Ringers committed
169
		// Ensure the credential has an expiry date
170
		defaultValidity := irma.Timestamp(time.Now().AddDate(0, 6, 0))
Sietse Ringers's avatar
Sietse Ringers committed
171
172
173
174
175
176
177
178
179
180
181
		if cred.Validity == nil {
			cred.Validity = &defaultValidity
		}
		if cred.Validity.Before(irma.Timestamp(time.Now())) {
			return errors.New("cannot issue expired credentials")
		}
	}

	return nil
}

Sietse Ringers's avatar
Sietse Ringers committed
182
func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, scheme irma.SchemeManagerIdentifier) (*gabi.ProofP, error) {
Sietse Ringers's avatar
Sietse Ringers committed
183
184
185
186
187
188
189
190
191
	if session.kssProofs == nil {
		session.kssProofs = make(map[irma.SchemeManagerIdentifier]*gabi.ProofP)
	}

	if _, contains := session.kssProofs[scheme]; !contains {
		str, contains := commitments.ProofPjwts[scheme.Name()]
		if !contains {
			return nil, errors.Errorf("no keyshare proof included for scheme %s", scheme.Name())
		}
192
		session.conf.Logger.Debug("Parsing keyshare ProofP JWT: ", str)
Sietse Ringers's avatar
Sietse Ringers committed
193
194
195
196
		claims := &struct {
			jwt.StandardClaims
			ProofP *gabi.ProofP
		}{}
197
		token, err := jwt.ParseWithClaims(str, claims, session.conf.IrmaConfiguration.KeyshareServerKeyFunc(scheme))
Sietse Ringers's avatar
Sietse Ringers committed
198
199
200
201
202
203
204
205
206
207
208
209
		if err != nil {
			return nil, err
		}
		if !token.Valid {
			return nil, errors.Errorf("invalid keyshare proof included for scheme %s", scheme.Name())
		}
		session.kssProofs[scheme] = claims.ProofP
	}

	return session.kssProofs[scheme], nil
}

210
211
212
213
214
215
216
var eventHeaders = [][]byte{[]byte("Access-Control-Allow-Origin: *")}

func (session *session) eventSource() eventsource.EventSource {
	if session.evtSource != nil {
		return session.evtSource
	}

217
	session.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Debug("Making server sent event source")
218
219
220
221
	session.evtSource = eventsource.New(nil, func(_ *http.Request) [][]byte { return eventHeaders })
	return session.evtSource
}

Sietse Ringers's avatar
Sietse Ringers committed
222
223
// Other

224
225
226
227
228
func (session *session) chooseProtocolVersion(minClient, maxClient *irma.ProtocolVersion) (*irma.ProtocolVersion, error) {
	// Set our minimum supported version to 2.5 if condiscon compatibility is required
	minServer := minProtocolVersion
	if !session.legacyCompatible {
		minServer = &irma.ProtocolVersion{2, 5}
Sietse Ringers's avatar
Sietse Ringers committed
229
	}
230
231
232
233
234

	if minClient.AboveVersion(maxProtocolVersion) || maxClient.BelowVersion(minServer) || maxClient.BelowVersion(minClient) {
		return nil, server.LogWarning(errors.Errorf("Protocol version negotiation failed, min=%s max=%s minServer=%s maxServer=%s", minClient.String(), maxClient.String(), minServer.String(), maxProtocolVersion.String()))
	}
	if maxClient.AboveVersion(maxProtocolVersion) {
Sietse Ringers's avatar
Sietse Ringers committed
235
236
		return maxProtocolVersion, nil
	} else {
237
		return maxClient, nil
Sietse Ringers's avatar
Sietse Ringers committed
238
239
	}
}
240

241
242
243
244
245
246
247
248
249
250
251
252
func copyObject(i interface{}) (interface{}, error) {
	cpy := reflect.New(reflect.TypeOf(i).Elem()).Interface()
	bts, err := json.Marshal(i)
	if err != nil {
		return nil, err
	}
	if err = json.Unmarshal(bts, cpy); err != nil {
		return nil, err
	}
	return cpy, nil
}

253
254
// purgeRequest logs the request excluding any attribute values.
func purgeRequest(request irma.RequestorRequest) irma.RequestorRequest {
255
256
257
258
	// We want to log as much as possible of the request, but no attribute values.
	// We cannot just remove them from the request parameter as that would break the calling code.
	// So we create a deep copy of the request from which we can then safely remove whatever we want to.
	// Ugly hack alert: the easiest way to do this seems to be to convert it to JSON and then back.
259
	// As we do not know the precise type of request, we use reflection to create a new instance
260
	// of the same type as request, into which we then unmarshal our copy.
261
262
263
264
	cpy, err := copyObject(request)
	if err != nil {
		panic(err)
	}
265
266

	// Remove required attribute values from any attributes to be disclosed
267
268
269
270
271
272
273
	_ = cpy.(irma.RequestorRequest).SessionRequest().Disclosure().Disclose.Iterate(
		func(attr *irma.AttributeRequest) error {
			attr.Value = nil
			return nil
		},
	)

274
	// Remove attribute values from attributes to be issued
275
276
	if isreq, ok := cpy.(*irma.IdentityProviderRequest); ok {
		for _, cred := range isreq.Request.Credentials {
277
278
279
			cred.Attributes = nil
		}
	}
280
281

	return cpy.(irma.RequestorRequest)
282
}