session.go 11.4 KB
Newer Older
1
2
3
package cmd

import (
4
	"bufio"
5
6
	"fmt"
	"net/http"
7
	"os"
8
	"regexp"
9
	"strconv"
10
	"sync"
11
12

	"github.com/go-errors/errors"
13
	irma "github.com/privacybydesign/irmago"
14
	"github.com/privacybydesign/irmago/server"
Sietse Ringers's avatar
Sietse Ringers committed
15
	"github.com/privacybydesign/irmago/server/irmaserver"
16
	"github.com/sirupsen/logrus"
17
	"github.com/spf13/cobra"
18
	prefixed "github.com/x-cray/logrus-prefixed-formatter"
19
20
)

21
var (
Sietse Ringers's avatar
Sietse Ringers committed
22
23
	httpServer *http.Server
	irmaServer *irmaserver.Server
24
	defaulturl string
25
26

	logger = logrus.New()
27
28
)

29
30
31
32
// sessionCmd represents the session command
var sessionCmd = &cobra.Command{
	Use:   "session",
	Short: "Perform an IRMA disclosure, issuance or signature session",
33
34
35
36
37
38
39
40
	Long: `Perform an IRMA disclosure, issuance or signature session on the command line

Using either the builtin IRMA server library, or an external IRMA server (specify its URL
with --server), an IRMA session is started; the QR is printed in the terminal; and the session
result is printed when the session completes or fails.

A session request can either be constructed using the --disclose, --issue, and --sign together
with --message flags, or it can be specified as JSON to the --request flag.`,
41
42
	Example: `irma session --disclose irma-demo.MijnOverheid.root.BSN
irma session --sign irma-demo.MijnOverheid.root.BSN --message message
43
irma session --issue irma-demo.MijnOverheid.ageLower=yes,yes,yes,no --disclose irma-demo.MijnOverheid.root.BSN
44
irma session --request '{"type":"disclosing","content":[{"label":"BSN","attributes":["irma-demo.MijnOverheid.root.BSN"]}]}'
45
irma session --server http://localhost:8088 --authmethod token --key mytoken --disclose irma-demo.MijnOverheid.root.BSN`,
46
	Run: func(cmd *cobra.Command, args []string) {
47
		request, irmaconfig, err := configureSession(cmd)
48
49
50
		if err != nil {
			die("", err)
		}
51

52
		// Make sure we always run with latest configuration
53
54
55
56
57
58
		flags := cmd.Flags()
		disableUpdate, _ := flags.GetBool("disable-schemes-update")
		if !disableUpdate {
			if err = irmaconfig.UpdateSchemes(); err != nil {
				die("failed updating schemes", err)
			}
59
		}
60

61
		var result *server.SessionResult
62
		url, _ := cmd.Flags().GetString("url")
63
		serverurl, _ := cmd.Flags().GetString("server")
64
		noqr, _ := cmd.Flags().GetBool("noqr")
65
		pairing, _ := cmd.Flags().GetBool("pairing")
66

67
		if url != defaulturl && serverurl != "" {
68
69
70
			die("Failed to read configuration", errors.New("--url can't be combined with --server"))
		}

71
		if serverurl == "" {
72
			port, _ := flags.GetInt("port")
Sietse Ringers's avatar
Sietse Ringers committed
73
			privatekeysPath, _ := flags.GetString("privkeys")
74
			verbosity, _ := cmd.Flags().GetCount("verbose")
75
			result, err = libraryRequest(request, irmaconfig, url, port, privatekeysPath, noqr, verbosity, pairing)
76
		} else {
77
78
79
			authmethod, _ := flags.GetString("authmethod")
			key, _ := flags.GetString("key")
			name, _ := flags.GetString("name")
80
			result, err = serverRequest(request, serverurl, authmethod, key, name, noqr, pairing)
81
82
83
		}
		if err != nil {
			die("Session failed", err)
84
85
86
87
88
		}

		printSessionResult(result)

		// Done!
Sietse Ringers's avatar
Sietse Ringers committed
89
90
		if httpServer != nil {
			_ = httpServer.Close()
91
		}
92
93
94
	},
}

95
func libraryRequest(
96
	request irma.RequestorRequest,
97
	irmaconfig *irma.Configuration,
98
	url string,
99
100
101
	port int,
	privatekeysPath string,
	noqr bool,
102
	verbosity int,
103
	pairing bool,
104
) (*server.SessionResult, error) {
105
	if err := configureSessionServer(url, port, privatekeysPath, irmaconfig, verbosity); err != nil {
106
107
108
109
110
111
		return nil, err
	}
	startServer(port)

	// Start the session
	resultchan := make(chan *server.SessionResult)
112
	qr, requestorToken, _, err := irmaServer.StartSession(request, func(r *server.SessionResult) {
113
114
115
116
117
118
		resultchan <- r
	})
	if err != nil {
		return nil, errors.WrapPrefix(err, "IRMA session failed", 0)
	}

119
	// Enable pairing if necessary
120
	var sessionOptions *irma.SessionOptions
121
	if pairing {
122
		optionsRequest := irma.NewFrontendOptionsRequest()
123
		optionsRequest.PairingMethod = irma.PairingMethodPin
124
		if sessionOptions, err = irmaServer.SetFrontendOptions(requestorToken, &optionsRequest); err != nil {
125
			return nil, errors.WrapPrefix(err, "Failed to enable pairing", 0)
126
		}
127
128
	}

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

134
	if pairing {
135
		// Listen for session status
136
		statuschan, err := irmaServer.SessionStatus(requestorToken)
137
138
139
		if err != nil {
			return nil, errors.WrapPrefix(err, "Failed to start listening for session statuses", 0)
		}
140

141
142
		_, err = handlePairing(sessionOptions, statuschan, func() error {
			return irmaServer.PairingCompleted(requestorToken)
143
144
		})
		if err != nil {
145
			return nil, errors.WrapPrefix(err, "Failed to handle pairing", 0)
146
147
148
		}
	}

149
150
151
152
153
	// Wait for session to finish and then return session result
	return <-resultchan, nil
}

func serverRequest(
154
	request irma.RequestorRequest,
155
	serverurl, authmethod, key, name string,
156
	noqr bool,
157
	pairing bool,
158
159
160
161
) (*server.SessionResult, error) {
	logger.Debug("Server URL: ", serverurl)

	// Start session at server
162
	qr, frontendRequest, transport, err := postRequest(serverurl, request, name, authmethod, key)
163
	if err != nil {
164
165
166
		return nil, err
	}

167
	// Enable pairing if necessary
168
	var frontendTransport *irma.HTTPTransport
169
	sessionOptions := &irma.SessionOptions{}
170
	if pairing {
171
		frontendTransport = irma.NewHTTPTransport(qr.URL, false)
172
		frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendRequest.Authorization))
173
		optionsRequest := irma.NewFrontendOptionsRequest()
174
		optionsRequest.PairingMethod = irma.PairingMethodPin
175
		err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest)
176
		if err != nil {
177
			return nil, errors.WrapPrefix(err, "Failed to enable pairing", 0)
178
179
180
		}
	}

181
182
183
184
185
186
	// 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)
	}

187
	statuschan := make(chan irma.ServerStatus)
188
	errorchan := make(chan error)
189
	var wg sync.WaitGroup
190

191
	go irma.WaitStatus(transport, irma.ServerStatusInitialized, statuschan, errorchan)
192
193
194
195
196
197
	go func() {
		err := <-errorchan
		if err != nil {
			_ = server.LogFatal(err)
		}
	}()
198
199
200
201
202

	wg.Add(1)
	go func() {
		defer wg.Done()

203
		var status irma.ServerStatus
204
205
206
		if pairing {
			status, err = handlePairing(sessionOptions, statuschan, func() error {
				err = frontendTransport.Post("frontend/pairingcompleted", nil, nil)
207
				if err != nil {
208
					return errors.WrapPrefix(err, "Failed to complete pairing", 0)
209
				}
210
				return nil
211
212
			})
			if err != nil {
213
				err = errors.WrapPrefix(err, "Failed to handle pairing", 0)
214
215
216
				return
			}
		} else {
217
			// Wait until client connects if pairing is disabled
218
			status := <-statuschan
219
			if status != irma.ServerStatusConnected {
220
221
222
				err = errors.Errorf("Unexpected status: %s", status)
				return
			}
223
224
225
226
		}

		// Wait until client finishes
		status = <-statuschan
227
		if status != irma.ServerStatusCancelled && status != irma.ServerStatusDone {
228
229
230
231
			err = errors.Errorf("Unexpected status: %s", status)
			return
		}
	}()
232

233
234
235
	wg.Wait()
	if err != nil {
		return nil, err
236
237
238
239
	}

	// Retrieve session result
	result := &server.SessionResult{}
240
	if err := transport.Get("result", result); err != nil {
241
242
243
244
245
		return nil, errors.WrapPrefix(err, "Failed to get session result", 0)
	}
	return result, nil
}

246
func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string) (
247
	*irma.Qr, *irma.FrontendSessionRequest, *irma.HTTPTransport, error) {
248
249
	var (
		err       error
250
		pkg       = &server.SessionPackage{}
251
		transport = irma.NewHTTPTransport(serverurl, false)
252
253
254
255
	)

	switch authmethod {
	case "none":
256
		err = transport.Post("session", pkg, request)
257
	case "token":
258
		transport.SetHeader("Authorization", key)
259
		err = transport.Post("session", pkg, request)
260
	case "hmac", "rsa":
Sietse Ringers's avatar
Sietse Ringers committed
261
262
		var jwtstr string
		jwtstr, err = signRequest(request, name, authmethod, key)
Sietse Ringers's avatar
Sietse Ringers committed
263
		if err != nil {
264
			return nil, nil, nil, err
265
266
		}
		logger.Debug("Session request JWT: ", jwtstr)
267
		err = transport.Post("session", pkg, jwtstr)
268
	default:
269
		return nil, nil, nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)")
270
271
	}

272
	if err != nil {
273
		return nil, nil, nil, err
274
275
	}

Ivar Derksen's avatar
Ivar Derksen committed
276
	transport.Server += fmt.Sprintf("session/%s/", pkg.Token)
277
	return pkg.SessionPtr, pkg.FrontendRequest, transport, err
278
279
}

280
func handlePairing(options *irma.SessionOptions, statusChan chan irma.ServerStatus, completePairing func() error) (
281
	irma.ServerStatus, error) {
282
	errorChan := make(chan error)
283
	pairingStarted := false
284
285
	for {
		select {
Ivar Derksen's avatar
Ivar Derksen committed
286
		case status := <-statusChan:
287
			if status == irma.ServerStatusInitialized {
288
				continue
289
290
291
			} else if status == irma.ServerStatusPairing {
				pairingStarted = true
				go requestPairingPermission(options, completePairing, errorChan)
292
				continue
293
294
			} else if status == irma.ServerStatusConnected && !pairingStarted {
				fmt.Println("Pairing is not supported by the connected device.")
295
296
297
			}
			return status, nil
		case err := <-errorChan:
298
			return "", err
299
		}
300
	}
301
}
302

303
304
305
306
func requestPairingPermission(options *irma.SessionOptions, completePairing func() error, errorChan chan error) {
	if options.PairingMethod == irma.PairingMethodPin {
		fmt.Println("\nPairing code:", options.PairingCode)
		fmt.Println("Press Enter to confirm your device shows the same pairing code; otherwise press Ctrl-C.")
307
308
309
310
311
		_, err := bufio.NewReader(os.Stdin).ReadString('\n')
		if err != nil {
			errorChan <- err
			return
		}
312
		if err = completePairing(); err != nil {
313
314
315
			errorChan <- err
			return
		}
316
		fmt.Println("Pairing completed.")
317
318
319
		errorChan <- nil
		return
	}
320
	errorChan <- errors.Errorf("Pairing method %s is not supported", options.PairingMethod)
321
322
}

323
324
// Configuration functions

325
func configureSessionServer(url string, port int, privatekeysPath string, irmaconfig *irma.Configuration, verbosity int) error {
326
327
328
329
	// Replace "port" in url with actual port
	replace := "$1:" + strconv.Itoa(port)
	url = string(regexp.MustCompile("(https?://[^/]*):port").ReplaceAll([]byte(url), []byte(replace)))

330
	config := &server.Configuration{
331
332
		IrmaConfiguration:    irmaconfig,
		Logger:               logger,
333
		URL:                  url,
334
		DisableSchemesUpdate: true,
335
		Verbose:              verbosity,
336
337
338
339
340
	}
	if privatekeysPath != "" {
		config.IssuerPrivateKeysPath = privatekeysPath
	}

341
	var err error
Sietse Ringers's avatar
Sietse Ringers committed
342
343
	irmaServer, err = irmaserver.New(config)
	return err
344
345
}

346
func configureSession(cmd *cobra.Command) (irma.RequestorRequest, *irma.Configuration, error) {
347
348
	verbosity, _ := cmd.Flags().GetCount("verbose")
	logger.Level = server.Verbosity(verbosity)
349
	irma.SetLogger(logger)
350

351
352
353
354
	if localIPErr != nil {
		logger.Warn("Could not determine local IP address: ", localIPErr.Error())
	}

Sietse Ringers's avatar
Sietse Ringers committed
355
	return configureRequest(cmd)
356
}
357
358
359
360

func init() {
	RootCmd.AddCommand(sessionCmd)

361
362
	logger.Formatter = &prefixed.TextFormatter{FullTimestamp: true}

363
364
	if localIP != "" {
		defaulturl = "http://" + localIP + ":port"
365
366
	}

367
368
	flags := sessionCmd.Flags()
	flags.SortFlags = false
369
	flags.String("server", "", "External IRMA server to post request to (leave blank to use builtin library)")
370
	flags.StringP("url", "u", defaulturl, "external URL to which IRMA app connects (when not using --server), \":port\" being replaced by --port value")
371
	flags.IntP("port", "p", 48680, "port to listen at (when not using --server)")
372
	flags.Bool("noqr", false, "Print JSON instead of draw QR")
373
	flags.Bool("pairing", false, "Let IRMA app first pair, by entering the pairing code, before it can access the session")
Sietse Ringers's avatar
Sietse Ringers committed
374
375
	flags.StringP("request", "r", "", "JSON session request")
	flags.StringP("privkeys", "k", "", "path to private keys")
376
	flags.Bool("disable-schemes-update", false, "disable scheme updates")
377

Sietse Ringers's avatar
Sietse Ringers committed
378
	addRequestFlags(flags)
379

Sietse Ringers's avatar
Sietse Ringers committed
380
	flags.CountP("verbose", "v", "verbose (repeatable)")
381
}