/
helpers.go
435 lines (388 loc) · 13.5 KB
/
helpers.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
package irmaserver
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"reflect"
"time"
"github.com/alexandrevicenzi/go-sse"
"github.com/dgrijalva/jwt-go"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-errors/errors"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/common"
"github.com/privacybydesign/irmago/server"
"github.com/sirupsen/logrus"
)
// Session helpers
func (session *session) markAlive() {
session.lastActive = time.Now()
session.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Debugf("Session marked active, expiry delayed")
}
func (session *session) setStatus(status server.Status) {
session.conf.Logger.WithFields(logrus.Fields{"session": session.token, "prevStatus": session.prevStatus, "status": status}).
Info("Session status updated")
session.status = status
session.result.Status = status
session.sessions.update(session)
}
func (session *session) onUpdate() {
if session.sse == nil {
return
}
session.sse.SendMessage("session/"+session.clientToken,
sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)),
)
session.sse.SendMessage("session/"+session.token,
sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)),
)
}
func (session *session) fail(err server.Error, message string) *irma.RemoteError {
rerr := server.RemoteError(err, message)
session.setStatus(server.StatusCancelled)
session.result = &server.SessionResult{Err: rerr, Token: session.token, Status: server.StatusCancelled, Type: session.action}
return rerr
}
func (session *session) chooseProtocolVersion(minClient, maxClient *irma.ProtocolVersion) (*irma.ProtocolVersion, error) {
// Set minimum supported version to 2.5 if condiscon compatibility is required
minServer := minProtocolVersion
if !session.legacyCompatible {
minServer = &irma.ProtocolVersion{2, 5}
}
// Set minimum to 2.6 if nonrevocation is required
if len(session.request.Base().Revocation) > 0 {
minServer = &irma.ProtocolVersion{2, 6}
}
if minClient.AboveVersion(maxProtocolVersion) || maxClient.BelowVersion(minServer) || maxClient.BelowVersion(minClient) {
err := errors.Errorf("Protocol version negotiation failed, min=%s max=%s minServer=%s maxServer=%s", minClient.String(), maxClient.String(), minServer.String(), maxProtocolVersion.String())
server.LogWarning(err)
return nil, err
}
if maxClient.AboveVersion(maxProtocolVersion) {
return maxProtocolVersion, nil
} else {
return maxClient, nil
}
}
const retryTimeLimit = 10 * time.Second
// checkCache returns a previously cached response, for replaying against multiple requests from
// irmago's retryablehttp client, if:
// - the same was POSTed as last time
// - 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.
func (session *session) checkCache(message []byte) (int, []byte) {
if len(session.responseCache.response) == 0 ||
session.responseCache.sessionStatus != session.status ||
session.lastActive.Before(time.Now().Add(-retryTimeLimit)) ||
sha256.Sum256(session.responseCache.message) != sha256.Sum256(message) {
session.responseCache = responseCache{}
return 0, nil
}
return session.responseCache.status, session.responseCache.response
}
// Issuance helpers
func (session *session) computeWitness(sk *gabi.PrivateKey, cred *irma.CredentialRequest) (*revocation.Witness, error) {
id := cred.CredentialTypeID
credtyp := session.conf.IrmaConfiguration.CredentialTypes[id]
if !credtyp.RevocationSupported() || !session.request.Base().RevocationSupported() {
return nil, nil
}
// ensure the client always gets an up to date nonrevocation witness
rs := session.conf.IrmaConfiguration.Revocation
if err := rs.SyncDB(id); err != nil {
return nil, err
}
// Fetch latest revocation record, and then extract the current value of the accumulator
// from it to generate the witness from
updates, err := rs.UpdateLatest(id, 0, &cred.KeyCounter)
if err != nil {
return nil, err
}
u := updates[cred.KeyCounter]
if u == nil {
return nil, errors.Errorf("no revocation updates found for key %d", cred.KeyCounter)
}
sig := u.SignedAccumulator
pk, err := rs.Keys.PublicKey(id.IssuerIdentifier(), sig.PKCounter)
if err != nil {
return nil, err
}
acc, err := sig.UnmarshalVerify(pk)
if err != nil {
return nil, err
}
witness, err := sk.RevocationGenerateWitness(acc)
if err != nil {
return nil, err
}
witness.SignedAccumulator = sig // attach previously selected reocation record to the witness for the client
return witness, nil
}
func (session *session) computeAttributes(
sk *gabi.PrivateKey, cred *irma.CredentialRequest,
) ([]*big.Int, *revocation.Witness, error) {
id := cred.CredentialTypeID
witness, err := session.computeWitness(sk, cred)
if err != nil {
return nil, nil, err
}
var nonrevAttr *big.Int
if witness != nil {
nonrevAttr = witness.E
}
issuedAt := time.Now()
attributes, err := cred.AttributeList(session.conf.IrmaConfiguration, 0x03, nonrevAttr, issuedAt)
if err != nil {
return nil, nil, err
}
issrecord := &irma.IssuanceRecord{
CredType: id,
PKCounter: &sk.Counter,
Key: cred.RevocationKey,
Attr: (*irma.RevocationAttribute)(nonrevAttr),
Issued: attributes.SigningDate().UnixNano(),
ValidUntil: attributes.Expiry().UnixNano(),
}
if witness != nil {
err = session.conf.IrmaConfiguration.Revocation.SaveIssuanceRecord(id, issrecord, sk)
if err != nil {
return nil, nil, err
}
}
return attributes.Ints, witness, nil
}
func (s *Server) validateIssuanceRequest(request *irma.IssuanceRequest) error {
for _, cred := range request.Credentials {
// Check that we have the appropriate private key
iss := cred.CredentialTypeID.IssuerIdentifier()
privatekey, err := s.conf.IrmaConfiguration.PrivateKeys.Latest(iss)
if err != nil {
return err
}
if privatekey == nil {
return errors.Errorf("missing private key of issuer %s", iss.String())
}
pubkey, err := s.conf.IrmaConfiguration.PublicKey(iss, privatekey.Counter)
if err != nil {
return err
}
if pubkey == nil {
return errors.Errorf("missing public key of issuer %s", iss.String())
}
now := time.Now()
if now.Unix() > pubkey.ExpiryDate {
return errors.Errorf("cannot issue using expired public key %s-%d", iss.String(), privatekey.Counter)
}
cred.KeyCounter = privatekey.Counter
if s.conf.IrmaConfiguration.CredentialTypes[cred.CredentialTypeID].RevocationSupported() {
settings := s.conf.RevocationSettings[cred.CredentialTypeID]
if settings == nil || (settings.RevocationServerURL == "" && !settings.Server) {
return errors.Errorf("revocation enabled for %s but no revocation server configured", cred.CredentialTypeID)
}
if cred.RevocationKey == "" {
return errors.Errorf("revocation enabled for %s but no revocationKey specified", cred.CredentialTypeID)
}
}
// Check that the credential is consistent with irma_configuration
if err := cred.Validate(s.conf.IrmaConfiguration); err != nil {
return err
}
// Ensure the credential has an expiry date
defaultValidity := irma.Timestamp(time.Now().AddDate(0, 6, 0))
if cred.Validity == nil {
cred.Validity = &defaultValidity
}
if cred.Validity.Before(irma.Timestamp(now)) {
return errors.New("cannot issue expired credentials")
}
}
return nil
}
func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, scheme irma.SchemeManagerIdentifier) (*gabi.ProofP, error) {
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())
}
session.conf.Logger.Debug("Parsing keyshare ProofP JWT: ", str)
claims := &struct {
jwt.StandardClaims
ProofP *gabi.ProofP
}{}
token, err := jwt.ParseWithClaims(str, claims, session.conf.IrmaConfiguration.KeyshareServerKeyFunc(scheme))
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
}
// Other
func (s *Server) doResultCallback(result *server.SessionResult) {
url := s.GetRequest(result.Token).Base().CallbackURL
if url == "" {
return
}
server.DoResultCallback(url,
result,
s.conf.JwtIssuer,
s.GetRequest(result.Token).Base().ResultJwtValidity,
s.conf.JwtRSAPrivateKey,
)
}
func (s *Server) validateRequest(request irma.SessionRequest) error {
if _, err := s.conf.IrmaConfiguration.Download(request); err != nil {
return err
}
if err := request.Base().Validate(s.conf.IrmaConfiguration); err != nil {
return err
}
return request.Disclosure().Disclose.Validate(s.conf.IrmaConfiguration)
}
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
}
// purgeRequest logs the request excluding any attribute values.
func purgeRequest(request irma.RequestorRequest) irma.RequestorRequest {
// 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.
// As we do not know the precise type of request, we use reflection to create a new instance
// of the same type as request, into which we then unmarshal our copy.
cpy, err := copyObject(request)
if err != nil {
panic(err)
}
// Remove required attribute values from any attributes to be disclosed
_ = cpy.(irma.RequestorRequest).SessionRequest().Disclosure().Disclose.Iterate(
func(attr *irma.AttributeRequest) error {
attr.Value = nil
return nil
},
)
// Remove attribute values from attributes to be issued
if isreq, ok := cpy.(*irma.IdentityProviderRequest); ok {
for _, cred := range isreq.Request.Credentials {
cred.Attributes = nil
}
}
return cpy.(irma.RequestorRequest)
}
func eventServer(conf *server.Configuration) *sse.Server {
return sse.NewServer(&sse.Options{
ChannelNameFunc: func(r *http.Request) string {
ssectx := r.Context().Value("sse")
if ssectx == nil {
return ""
}
switch ssectx.(common.SSECtx).Component {
case server.ComponentSession:
return "session/" + ssectx.(common.SSECtx).Arg
case server.ComponentRevocation:
return "revocation/" + ssectx.(common.SSECtx).Arg
default:
return ""
}
},
Headers: map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Keep-Alive,X-Requested-With,Cache-Control,Content-Type,Last-Event-ID",
},
Logger: log.New(conf.Logger.WithField("type", "sse").WriterLevel(logrus.DebugLevel), "", 0),
})
}
func errorWriter(err *irma.RemoteError, writer func(w http.ResponseWriter, object interface{}, rerr *irma.RemoteError)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
writer(w, nil, err)
}
}
func (s *Server) cacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*session)
// Read r.Body, and then replace with a fresh ReadCloser for the next handler
var message []byte
var err error
if message, err = ioutil.ReadAll(r.Body); err != nil {
message = []byte("<failed to read body: " + err.Error() + ">")
}
_ = r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(message))
// if a cache is set and applicable, return it
status, output := session.checkCache(message)
if status > 0 && len(output) > 0 {
w.WriteHeader(status)
_, _ = w.Write(output)
return
}
// no cache set; perform request and record output
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
buf := new(bytes.Buffer)
ww.Tee(buf)
next.ServeHTTP(ww, r)
session.responseCache = responseCache{
message: message,
response: buf.Bytes(),
status: ww.Status(),
sessionStatus: session.status,
}
})
}
func (s *Server) sessionMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := chi.URLParam(r, "token")
session := s.sessions.clientGet(token)
if session == nil {
server.WriteError(w, server.ErrorSessionUnknown, "")
return
}
ctx := r.Context()
session.Lock()
session.locked = true
defer func() {
if session.prevStatus != session.status {
session.prevStatus = session.status
result := session.result
r := ctx.Value("sessionresult")
if r != nil {
*r.(*server.SessionResult) = *result
}
if session.status.Finished() {
if handler := s.handlers[result.Token]; handler != nil {
go handler(result)
delete(s.handlers, token)
}
}
}
if session.locked {
session.locked = false
session.Unlock()
}
}()
next.ServeHTTP(w, r.WithContext(context.WithValue(ctx, "session", session)))
})
}