session.go 13.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
package cmd

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"strings"
10
	"time"
11

12
	"github.com/dgrijalva/jwt-go"
13
14
15
	"github.com/go-errors/errors"
	"github.com/mdp/qrterminal"
	"github.com/privacybydesign/irmago"
16
	"github.com/privacybydesign/irmago/internal/fs"
17
18
	"github.com/privacybydesign/irmago/server"
	"github.com/privacybydesign/irmago/server/irmarequestor"
19
	"github.com/sirupsen/logrus"
20
21
22
	"github.com/spf13/cobra"
)

23
24
25
26
27
28
29
const pollInterval = 1000 * time.Millisecond

var (
	irmaServer *http.Server
	logger     *logrus.Logger
)

30
31
32
33
34
35
// sessionCmd represents the session command
var sessionCmd = &cobra.Command{
	Use:   "session",
	Short: "Perform an IRMA disclosure, issuance or signature session",
	Example: `irma session --disclose irma-demo.MijnOverheid.root.BSN
irma session --sign irma-demo.MijnOverheid.root.BSN --message message
36
irma session --issue irma-demo.MijnOverheid.ageLower=yes,yes,yes,no --disclose irma-demo.MijnOverheid.root.BSN
37
38
irma session --request '{"type":"disclosing","content":[{"label":"BSN","attributes":["irma-demo.MijnOverheid.root.BSN"]}]}'
irma session --server http://localhost:48680 --authmethod token --key mytoken --disclose irma-demo.MijnOverheid.root.BSN`,
39
	Run: func(cmd *cobra.Command, args []string) {
40
		request, irmaconfig, err := configure(cmd)
41
42
43
44
		if err != nil {
			die("", err)
		}

45
46
		var result *server.SessionResult
		serverurl, _ := cmd.Flags().GetString("server")
47
		noqr, _ := cmd.Flags().GetBool("noqr")
48
		flags := cmd.Flags()
49
		if serverurl == "" {
50
51
			port, _ := flags.GetInt("port")
			privatekeysPath, _ := flags.GetString("privatekeys")
52
53
			result, err = libraryRequest(request, irmaconfig, port, privatekeysPath, noqr)
		} else {
54
55
56
57
			authmethod, _ := flags.GetString("authmethod")
			key, _ := flags.GetString("key")
			name, _ := flags.GetString("name")
			result, err = serverRequest(request, serverurl, authmethod, key, name, noqr)
58
59
60
		}
		if err != nil {
			die("Session failed", err)
61
62
63
64
65
		}

		printSessionResult(result)

		// Done!
66
67
68
		if irmaServer != nil {
			_ = irmaServer.Close()
		}
69
70
71
	},
}

72
func libraryRequest(
73
	request irma.RequestorRequest,
74
75
76
77
78
	irmaconfig *irma.Configuration,
	port int,
	privatekeysPath string,
	noqr bool,
) (*server.SessionResult, error) {
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
	if err := configureServer(port, privatekeysPath, irmaconfig); err != nil {
		return nil, err
	}
	startServer(port)

	// Start the session
	resultchan := make(chan *server.SessionResult)
	qr, _, err := irmarequestor.StartSession(request, func(r *server.SessionResult) {
		resultchan <- r
	})
	if err != nil {
		return nil, errors.WrapPrefix(err, "IRMA session failed", 0)
	}

	// Print QR code
	if err := printQr(qr, noqr); err != nil {
		return nil, errors.WrapPrefix(err, "Failed to print QR", 0)
	}

	// Wait for session to finish and then return session result
	return <-resultchan, nil
}

func serverRequest(
103
	request irma.RequestorRequest,
104
	serverurl, authmethod, key, name string,
105
106
107
108
109
	noqr bool,
) (*server.SessionResult, error) {
	logger.Debug("Server URL: ", serverurl)

	// Start session at server
110
111
	qr, transport, err := postRequest(serverurl, request, name, authmethod, key)
	if err != nil {
112
113
114
115
116
117
118
119
120
121
122
123
		return nil, err
	}

	// Print session QR
	logger.Debug("QR: ", prettyprint(qr))
	if err := printQr(qr, noqr); err != nil {
		return nil, errors.WrapPrefix(err, "Failed to print QR", 0)
	}

	statuschan := make(chan server.Status)

	// Wait untill client connects
124
	go poll(server.StatusInitialized, transport, statuschan)
125
126
127
128
129
130
	status := <-statuschan
	if status != server.StatusConnected {
		return nil, errors.Errorf("Unexpected status: %s", status)
	}

	// Wait untill client finishes
131
	go poll(server.StatusConnected, transport, statuschan)
132
133
134
135
136
137
138
	status = <-statuschan
	if status != server.StatusDone {
		return nil, errors.Errorf("Unexpected status: %s", status)
	}

	// Retrieve session result
	result := &server.SessionResult{}
139
	if err := transport.Get("result", result); err != nil {
140
141
142
143
144
		return nil, errors.WrapPrefix(err, "Failed to get session result", 0)
	}
	return result, nil
}

145
func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string) (*irma.Qr, *irma.HTTPTransport, error) {
146
147
148
149
150
151
152
153
154
155
	var (
		err       error
		sk        interface{}
		qr        = &irma.Qr{}
		transport = irma.NewHTTPTransport(serverurl)
	)

	switch authmethod {
	case "none":
		err = transport.Post("session", qr, request)
156
	case "token":
157
158
159
160
161
162
163
164
		transport.SetHeader("Authentication", key)
		err = transport.Post("session", qr, request)
	case "hmac", "rsa":
		var (
			jwtalg jwt.SigningMethod
			jwtstr string
			bts    []byte
		)
165
166
167
		// If the key refers to an existing file, use contents of the file as key
		if bts, err = fs.ReadKey("", key); err != nil {
			bts = []byte(key)
168
169
170
171
172
173
174
175
176
177
178
179
180
181
		}
		if authmethod == "hmac" {
			jwtalg = jwt.SigningMethodHS256
			if sk, err = fs.Base64Decode(bts); err != nil {
				return nil, nil, err
			}
		}
		if authmethod == "rsa" {
			jwtalg = jwt.SigningMethodRS256
			if sk, err = jwt.ParseRSAPrivateKeyFromPEM(bts); err != nil {
				return nil, nil, err
			}
		}

182
		if jwtstr, err = irma.SignRequestorRequest(request, jwtalg, sk, name); err != nil {
183
184
185
186
187
			return nil, nil, err
		}
		logger.Debug("Session request JWT: ", jwtstr)
		err = transport.Post("session", qr, jwtstr)
	default:
188
		return nil, nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)")
189
190
191
192
193
194
195
	}

	token := qr.URL[strings.LastIndex(qr.URL, "/")+1:]
	transport.Server += fmt.Sprintf("session/%s/", token)
	return qr, transport, err
}

196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// Configuration functions

func configureServer(port int, privatekeysPath string, irmaconfig *irma.Configuration) error {
	ip, err := server.LocalIP()
	if err != nil {
		return err
	}
	config := &server.Configuration{
		IrmaConfiguration: irmaconfig,
		Logger:            logger,
		URL:               "http://" + ip + ":" + strconv.Itoa(port),
	}
	if privatekeysPath != "" {
		config.IssuerPrivateKeysPath = privatekeysPath
	}

	return irmarequestor.Initialize(config)
}

215
func configure(cmd *cobra.Command) (irma.RequestorRequest, *irma.Configuration, error) {
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
	irmaconfigPath, err := cmd.Flags().GetString("irmaconf")
	if err != nil {
		return nil, nil, err
	}
	irmaconfig, err := irma.NewConfiguration(irmaconfigPath)
	if err != nil {
		return nil, nil, err
	}
	if err = irmaconfig.ParseFolder(); err != nil {
		return nil, nil, err
	}
	if len(irmaconfig.SchemeManagers) == 0 {
		if err = irmaconfig.DownloadDefaultSchemes(); err != nil {
			return nil, nil, err
		}
	}

	verbosity, _ := cmd.Flags().GetCount("verbose")
	logger = logrus.New()
	logger.Level = server.Verbosity(verbosity)
	logger.Formatter = &logrus.TextFormatter{FullTimestamp: true}
	irma.Logger = logger
	request, err := constructSessionRequest(cmd, irmaconfig)
	if err != nil {
		return nil, nil, err
	}

	logger.Debugf("Session request: %s", prettyprint(request))
244

245
246
247
248
249
250
	return request, irmaconfig, nil
}

// Helper functions

// poll recursively polls the session status until a status different from initialStatus is received.
251
func poll(initialStatus server.Status, transport *irma.HTTPTransport, statuschan chan server.Status) {
252
253
254
255
256
	// First we wait
	<-time.NewTimer(pollInterval).C

	// Get session status
	var status string
257
	if err := transport.Get("status", &status); err != nil {
258
259
260
261
262
263
		_ = server.LogFatal(err)
	}
	status = strings.Trim(status, `"`)

	// If the status has not yet changed, schedule another poll
	if server.Status(status) == initialStatus {
264
		go poll(initialStatus, transport, statuschan)
265
266
267
268
269
270
	} else {
		logger.Trace("Stopped polling, new status ", status)
		statuschan <- server.Status(status)
	}
}

271
func constructSessionRequest(cmd *cobra.Command, conf *irma.Configuration) (irma.RequestorRequest, error) {
272
273
274
275
	disclose, _ := cmd.Flags().GetStringArray("disclose")
	issue, _ := cmd.Flags().GetStringArray("issue")
	sign, _ := cmd.Flags().GetStringArray("sign")
	message, _ := cmd.Flags().GetString("message")
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
	jsonrequest, _ := cmd.Flags().GetString("request")

	if len(disclose) == 0 && len(issue) == 0 && len(sign) == 0 && message == "" {
		if jsonrequest == "" {
			return nil, errors.New("Provide either a complete session request using --request or construct one using the other flags")
		}
		request, err := server.ParseSessionRequest(jsonrequest)
		if err != nil {
			return nil, err
		}
		return request, nil
	}

	if jsonrequest != "" {
		return nil, errors.New("Provide either a complete session request using --request or construct one using the other flags")
	}
292
293
294
295
296
297
298
299
300
301
302
303
304

	if len(sign) != 0 {
		if len(disclose) != 0 {
			return nil, errors.New("cannot combine disclosure and signature sessions, use either --disclose or --sign")
		}
		if len(issue) != 0 {
			return nil, errors.New("cannot combine issuance and signature sessions, use either --issue or --sign")
		}
		if message == "" {
			return nil, errors.New("signature sessions require a message to be signed using --message")
		}
	}

305
	var request irma.RequestorRequest
306
307
308
309
310
	if len(disclose) != 0 {
		disjunctions, err := parseDisjunctions(disclose, conf)
		if err != nil {
			return nil, err
		}
311
312
313
314
		request = &irma.ServiceProviderRequest{
			Request: &irma.DisclosureRequest{
				BaseRequest: irma.BaseRequest{Type: irma.ActionDisclosing},
				Content:     disjunctions,
315
316
			},
		}
317

318
319
320
321
322
323
	}
	if len(sign) != 0 {
		disjunctions, err := parseDisjunctions(sign, conf)
		if err != nil {
			return nil, err
		}
324
325
326
327
328
		request = &irma.SignatureRequestorRequest{
			Request: &irma.SignatureRequest{
				DisclosureRequest: irma.DisclosureRequest{
					BaseRequest: irma.BaseRequest{Type: irma.ActionSigning},
					Content:     disjunctions,
329
				},
330
				Message: message,
331
332
333
334
335
336
337
338
339
340
341
342
			},
		}
	}
	if len(issue) != 0 {
		creds, err := parseCredentials(issue, conf)
		if err != nil {
			return nil, err
		}
		disjunctions, err := parseDisjunctions(disclose, conf)
		if err != nil {
			return nil, err
		}
343
344
345
346
347
348
349
		request = &irma.IdentityProviderRequest{
			Request: &irma.IssuanceRequest{
				BaseRequest: irma.BaseRequest{
					Type: irma.ActionIssuing,
				},
				Credentials: creds,
				Disclose:    disjunctions,
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
436
437
438
			},
		}
	}

	return request, nil
}

func parseCredentials(credentialsStr []string, conf *irma.Configuration) ([]*irma.CredentialRequest, error) {
	list := make([]*irma.CredentialRequest, 0, len(credentialsStr))

	for _, credStr := range credentialsStr {
		parts := strings.Split(credStr, "=")
		if len(parts) != 2 {
			return nil, errors.New("--issue argument must contain exactly 1 = sign")
		}
		credIdStr, attrsStr := parts[0], parts[1]
		credtype := conf.CredentialTypes[irma.NewCredentialTypeIdentifier(credIdStr)]
		if credtype == nil {
			return nil, errors.New("unknown credential type: " + credIdStr)
		}

		attrsSlice := strings.Split(attrsStr, ",")
		if len(attrsSlice) != len(credtype.AttributeTypes) {
			return nil, errors.Errorf("%d attributes required but %d provided for %s", len(credtype.AttributeTypes), len(attrsSlice), credIdStr)
		}

		attrs := make(map[string]string, len(attrsSlice))
		for i, typ := range credtype.AttributeTypes {
			attrs[typ.ID] = attrsSlice[i]
		}
		list = append(list, &irma.CredentialRequest{
			CredentialTypeID: irma.NewCredentialTypeIdentifier(credIdStr),
			Attributes:       attrs,
		})
	}

	return list, nil
}

func parseDisjunctions(disjunctionsStr []string, conf *irma.Configuration) (irma.AttributeDisjunctionList, error) {
	list := make(irma.AttributeDisjunctionList, 0, len(disjunctionsStr))
	for _, disjunctionStr := range disjunctionsStr {
		disjunction := &irma.AttributeDisjunction{}
		attrids := strings.Split(disjunctionStr, ",")
		for _, attridStr := range attrids {
			attrid := irma.NewAttributeTypeIdentifier(attridStr)
			if conf.AttributeTypes[attrid] == nil {
				return nil, errors.New("unknown attribute: " + attridStr)
			}
			disjunction.Attributes = append(disjunction.Attributes, attrid)
		}
		disjunction.Label = disjunction.Attributes[0].Name()
		list = append(list, disjunction)
	}
	return list, nil
}

func startServer(port int) {
	mux := http.NewServeMux()
	mux.HandleFunc("/", irmarequestor.HttpHandlerFunc())
	irmaServer = &http.Server{Addr: ":" + strconv.Itoa(port), Handler: mux}
	go func() {
		err := irmaServer.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			die("Failed to start server", err)
		}
	}()
}

func printQr(qr *irma.Qr, noqr bool) error {
	qrBts, err := json.Marshal(qr)
	if err != nil {
		return err
	}
	if noqr {
		fmt.Println(string(qrBts))
	} else {
		qrterminal.GenerateWithConfig(string(qrBts), qrterminal.Config{
			Level:     qrterminal.L,
			Writer:    os.Stdout,
			BlackChar: qrterminal.BLACK,
			WhiteChar: qrterminal.WHITE,
		})
	}
	return nil
}

func printSessionResult(result *server.SessionResult) {
	fmt.Println("Session result:")
439
	fmt.Println(prettyprint(result))
440
}
441
442
443
444
445
446
447
448
449

func init() {
	RootCmd.AddCommand(sessionCmd)

	flags := sessionCmd.Flags()
	flags.SortFlags = false
	flags.StringP("irmaconf", "i", defaultIrmaconfPath(), "path to irma_configuration")
	flags.StringP("privatekeys", "k", "", "path to private keys")
	flags.IntP("port", "p", 48680, "port to listen at")
450
	flags.Bool("noqr", false, "Print JSON instead of draw QR")
451
452
453
	flags.CountP("verbose", "v", "verbose (repeatable)")

	flags.StringP("server", "s", "", "Server to post request to (leave blank to use builtin library)")
454
	flags.StringP("authmethod", "a", "none", "Authentication method to server (none, token, rsa, hmac)")
455
456
	flags.String("key", "", "Key to sign request with")
	flags.String("name", "", "Requestor name")
457

458
	flags.StringP("request", "r", "", "JSON session request")
459
460
461
462
463
	flags.StringArray("disclose", nil, "Add an attribute disjunction (comma-separated)")
	flags.StringArray("issue", nil, "Add a credential to issue")
	flags.StringArray("sign", nil, "Add an attribute disjunction to signature session")
	flags.String("message", "", "Message to sign in signature session")
}