From 61a8f6bb029aa7199b9e5e13aeaab50566a61afa Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 9 Jun 2020 15:42:24 +0200 Subject: [PATCH 01/77] Feat: introduce binding support to protect against shoulder surfing --- go.mod | 2 + go.sum | 11 +++ internal/common/common.go | 16 +++- internal/sessiontest/handlers_test.go | 8 ++ internal/sessiontest/legacy_test.go | 34 ++++++++ internal/sessiontest/main_test.go | 63 +++++++++++++- internal/sessiontest/requestor_test.go | 23 +++--- internal/sessiontest/session_test.go | 29 ++++++- irma/cmd/request.go | 61 ++------------ irma/cmd/session.go | 59 +++++++++---- irmaclient/client.go | 6 +- irmaclient/handlers.go | 3 + irmaclient/irmaclient_test.go | 2 +- irmaclient/session.go | 74 ++++++++++++++--- messages.go | 13 ++- requests.go | 15 ++++ server/api.go | 61 +++++++++++--- server/errors.go | 2 + server/irmac/irmac.go | 12 +-- server/irmaserver/api.go | 94 ++++++++++++--------- server/irmaserver/handle.go | 97 +++++++++++++++------- server/irmaserver/helpers.go | 109 +++++++++++++++++++++++-- server/irmaserver/sessions.go | 53 ++++++------ server/requestorserver/server.go | 21 ++--- server/wait_status.go | 91 +++++++++++++++++++++ 25 files changed, 731 insertions(+), 228 deletions(-) create mode 100644 server/wait_status.go diff --git a/go.mod b/go.mod index cdef2dc52..1c1bc988e 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-retryablehttp v0.6.2 + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jasonlvhit/gocron v0.0.0-20180312192515-54194c9749d4 github.com/jinzhu/gorm v1.9.12 github.com/lib/pq v1.3.0 // indirect @@ -26,6 +27,7 @@ require ( github.com/onsi/ginkgo v1.12.0 // indirect github.com/onsi/gomega v1.9.0 // indirect github.com/privacybydesign/gabi v0.0.0-20210409092845-6113e0d3ec81 + github.com/pelletier/go-toml v1.2.0 // indirect github.com/sietseringers/cobra v1.0.1-0.20200909200314-c50c3838234b github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50 github.com/sietseringers/pflag v1.0.4-0.20200909193609-0cde7e893819 diff --git a/go.sum b/go.sum index 8b555d02c..38b760478 100644 --- a/go.sum +++ b/go.sum @@ -175,6 +175,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= @@ -274,8 +275,13 @@ github.com/spf13/afero v1.2.0 h1:O9FblXGxoTc51M+cqr74Bm2Tmt4PvkA5iu/j8HrkNuY= github.com/spf13/afero v1.2.0/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -292,6 +298,7 @@ github.com/timshannon/bolthold v0.0.0-20180829183128-83840edea944/go.mod h1:jUig github.com/timshannon/bolthold v0.0.0-20190812165541-a85bcc049a2e h1:FC5JjwU5y5ZBR/vH8LhmPman3k5dep45jRyCpR1VDVQ= github.com/timshannon/bolthold v0.0.0-20190812165541-a85bcc049a2e/go.mod h1:jUigdmrbdCxcIDEFrq82t4X9805XZfwFZoYUap0ET/U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= @@ -299,6 +306,7 @@ github.com/x448/float16 v0.8.3/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -350,6 +358,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -425,6 +434,7 @@ google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBr google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -440,6 +450,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/internal/common/common.go b/internal/common/common.go index c31a1fdf1..8a374f978 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -27,6 +27,8 @@ var ForceHTTPS = true const ( sessionChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" sessionTokenLength = 20 + bindingChars = "0123456789" + bindingTokenLength = 4 ) // AssertPathExists returns nil only if it has been successfully @@ -273,15 +275,23 @@ type SSECtx struct { } func NewSessionToken() string { - r := make([]byte, sessionTokenLength) + return newToken(sessionTokenLength, sessionChars) +} + +func NewBindingCode() string { + return newToken(bindingTokenLength, bindingChars) +} + +func newToken(count int, characterSet string) string { + r := make([]byte, count) _, err := rand.Read(r) if err != nil { panic(err) } - b := make([]byte, sessionTokenLength) + b := make([]byte, count) for i := range b { - b[i] = sessionChars[r[i]%byte(len(sessionChars))] + b[i] = characterSet[r[i]%byte(len(characterSet))] } return string(b) } diff --git a/internal/sessiontest/handlers_test.go b/internal/sessiontest/handlers_test.go index afa305121..3058c19e8 100644 --- a/internal/sessiontest/handlers_test.go +++ b/internal/sessiontest/handlers_test.go @@ -82,6 +82,7 @@ type TestHandler struct { expectedServerName *irma.RequestorInfo wait time.Duration result string + bindingCodeChan chan string } func (th TestHandler) KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier) { @@ -150,6 +151,13 @@ func (th TestHandler) RequestSchemeManagerPermission(manager *irma.SchemeManager func (th TestHandler) RequestPin(remainingAttempts int, callback irmaclient.PinHandler) { callback(true, "12345") } +func (th TestHandler) BindingRequired(bindingCode string) { + if th.bindingCodeChan != nil { + th.bindingCodeChan <- bindingCode + return + } + th.Failure(&irma.SessionError{ErrorType: irma.ErrorType("Binding required")}) +} type SessionResult struct { Err error diff --git a/internal/sessiontest/legacy_test.go b/internal/sessiontest/legacy_test.go index 0eef65f39..f09ec7f79 100644 --- a/internal/sessiontest/legacy_test.go +++ b/internal/sessiontest/legacy_test.go @@ -1,6 +1,7 @@ package sessiontest import ( + "github.com/privacybydesign/irmago/irmaclient" "testing" irma "github.com/privacybydesign/irmago" @@ -34,3 +35,36 @@ func TestSessionUsingLegacyStorage(t *testing.T) { // Test whether credential is still there after the storage has been reloaded sessionHelper(t, getDisclosureRequest(idRoot), "verification", client) } + +func TestWithoutBindingSupport(t *testing.T) { + defer irmaclient.SetMaxVersion(nil) + irmaclient.SetMaxVersion(&irma.ProtocolVersion{Major: 2, Minor: 6}) + + t.Run("TestSigningSession", TestSigningSession) + t.Run("TestDisclosureSession", TestDisclosureSession) + t.Run("TestNoAttributeDisclosureSession", TestNoAttributeDisclosureSession) + t.Run("TestEmptyDisclosure", TestEmptyDisclosure) + t.Run("TestIssuanceSession", TestIssuanceSession) + t.Run("TestMultipleIssuanceSession", TestMultipleIssuanceSession) + t.Run("TestDefaultCredentialValidity", TestDefaultCredentialValidity) + t.Run("TestIssuanceDisclosureEmptyAttributes", TestIssuanceDisclosureEmptyAttributes) + t.Run("TestIssuanceOptionalZeroLengthAttributes", TestIssuanceOptionalZeroLengthAttributes) + t.Run("TestIssuanceOptionalSetAttributes", TestIssuanceOptionalSetAttributes) + t.Run("TestIssuanceSameAttributesNotSingleton", TestIssuanceSameAttributesNotSingleton) + t.Run("TestIssuanceBinding", TestIssuanceBinding) + t.Run("TestLargeAttribute", TestLargeAttribute) + t.Run("TestIssuanceSingletonCredential", TestIssuanceSingletonCredential) + t.Run("TestUnsatisfiableDisclosureSession", TestUnsatisfiableDisclosureSession) + t.Run("TestAttributeByteEncoding", TestAttributeByteEncoding) + t.Run("TestOutdatedClientIrmaConfiguration", TestOutdatedClientIrmaConfiguration) + t.Run("TestDisclosureNewAttributeUpdateSchemeManager", TestDisclosureNewAttributeUpdateSchemeManager) + t.Run("TestIssueNewAttributeUpdateSchemeManager", TestIssueNewAttributeUpdateSchemeManager) + t.Run("TestIssueOptionalAttributeUpdateSchemeManager", TestIssueOptionalAttributeUpdateSchemeManager) + t.Run("TestIssueNewCredTypeUpdateSchemeManager", TestIssueNewCredTypeUpdateSchemeManager) + t.Run("TestDisclosureNewCredTypeUpdateSchemeManager", TestDisclosureNewCredTypeUpdateSchemeManager) + t.Run("TestDisclosureNonexistingCredTypeUpdateSchemeManager", TestDisclosureNonexistingCredTypeUpdateSchemeManager) + t.Run("TestStaticQRSession", TestStaticQRSession) + t.Run("TestIssuedCredentialIsStored", TestIssuedCredentialIsStored) + t.Run("TestPOSTSizeLimit", TestPOSTSizeLimit) + // TestOptionsChangeAfterConnected is not necessary since the extra check is always skipped for legacy versions. +} diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index d5227a3b2..d5a8e3ff6 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -120,7 +120,7 @@ func getMultipleIssuanceRequest() *irma.IssuanceRequest { var TestType = "irmaserver-jwt" -func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) *server.SessionPackage { +func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) (*server.SessionPackage, irma.FrontendToken) { var ( sesPkg server.SessionPackage err error @@ -141,7 +141,7 @@ func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) } require.NoError(t, err) - return &sesPkg + return &sesPkg, sesPkg.FrontendToken } func getJwt(t *testing.T, request irma.SessionRequest, sessiontype string, alg jwt.SigningMethod) string { @@ -201,7 +201,7 @@ func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string defer StopRequestorServer() } - sesPkg := startSession(t, request, sessiontype) + sesPkg, _ := startSession(t, request, sessiontype) c := make(chan *SessionResult) h := &TestHandler{t: t, c: c, client: client, expectedServerName: expectedRequestorInfo(t, client.Configuration)} @@ -220,6 +220,63 @@ func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string return resJwt } +func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, sessiontype string, client *irmaclient.Client, + bindingCheck func(t *testing.T, transport *irma.HTTPTransport)) { + if client == nil { + var handler *TestClientHandler + client, handler = parseStorage(t) + defer test.ClearTestStorage(t, handler.storage) + } + + if TestType == "irmaserver" || TestType == "irmaserver-jwt" || TestType == "irmaserver-hmac-jwt" { + StartRequestorServer(JwtServerConfiguration) + defer StopRequestorServer() + } + + sesPkg, frontendToken := startSession(t, request, sessiontype) + optionsRequest := irma.NewOptionsRequest() + optionsRequest.EnableBinding = true + options := &server.SessionOptions{} + transport := irma.NewHTTPTransport(sesPkg.SessionPtr.URL, false) + transport.SetHeader(irma.AuthorizationHeader, frontendToken) + err := transport.Post("options", options, optionsRequest) + require.NoError(t, err) + require.Equal(t, true, options.BindingEnabled) + require.Equal(t, false, options.BindingCompleted) + + c := make(chan *SessionResult) + cBindingCode := make(chan string) + h := &TestHandler{ + t: t, + c: c, + client: client, + expectedServerName: expectedRequestorInfo(t, client.Configuration), + bindingCodeChan: cBindingCode, + } + + qrjson, err := json.Marshal(sesPkg.SessionPtr) + require.NoError(t, err) + client.NewSession(string(qrjson), h) + + // Binding can only be done from protocol version 2.7 + if _, max := client.GetSupportedVersions(); max.Above(2, 6) { + bindingCode := <-cBindingCode + bindingCheck(t, transport) + require.Equal(t, options.BindingCode, bindingCode) + optionsRequest = irma.NewOptionsRequest() + optionsRequest.BindingCompleted = true + options = &server.SessionOptions{} + err = transport.Post("options", options, optionsRequest) + require.NoError(t, err) + require.Equal(t, true, options.BindingEnabled) + require.Equal(t, true, options.BindingCompleted) + } + + if result := <-c; result != nil { + require.NoError(t, result.Err) + } +} + func expectedRequestorInfo(t *testing.T, conf *irma.Configuration) *irma.RequestorInfo { if common.ForceHTTPS { return irma.NewRequestorInfo("localhost") diff --git a/internal/sessiontest/requestor_test.go b/internal/sessiontest/requestor_test.go index b0bf6ef68..b41d89d1e 100644 --- a/internal/sessiontest/requestor_test.go +++ b/internal/sessiontest/requestor_test.go @@ -60,7 +60,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien clientChan := make(chan *SessionResult, 2) serverChan := make(chan *server.SessionResult) - qr, token, err := irmaServer.StartSession(request, func(result *server.SessionResult) { + qr, backendToken, _, err := irmaServer.StartSession(request, func(result *server.SessionResult) { serverChan <- result }) require.NoError(t, err) @@ -68,18 +68,16 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien var h irmaclient.Handler requestor := expectedRequestorInfo(t, client.Configuration) if opts&sessionOptionUnsatisfiableRequest > 0 { - h = &UnsatisfiableTestHandler{TestHandler: TestHandler{t, clientChan, client, requestor, 0, ""}} + h = &UnsatisfiableTestHandler{TestHandler: TestHandler{t, clientChan, client, requestor, 0, "", nil}} } else { var wait time.Duration = 0 if opts&sessionOptionClientWait > 0 { wait = 2 * time.Second } - h = &TestHandler{t, clientChan, client, requestor, wait, ""} + h = &TestHandler{t, clientChan, client, requestor, wait, "", nil} } - j, err := json.Marshal(qr) - require.NoError(t, err) - dismisser := client.NewSession(string(j), h) + clientToken, dismisser := client.NewQrSession(qr, h) clientResult := <-clientChan if opts&sessionOptionIgnoreError == 0 && clientResult != nil { require.NoError(t, clientResult.Err) @@ -91,7 +89,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien } serverResult := <-serverChan - require.Equal(t, token, serverResult.Token) + require.Equal(t, backendToken, serverResult.Token) if opts&sessionOptionRetryPost > 0 { req, err := http.NewRequest(http.MethodPost, @@ -100,6 +98,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien ) require.NoError(t, err) req.Header.Add("Content-Type", "application/json") + req.Header.Add(irma.AuthorizationHeader, clientToken) res, err := new(http.Client).Do(req) require.NoError(t, err) require.True(t, res.StatusCode < 300) @@ -114,7 +113,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien func TestRequestorInvalidRequest(t *testing.T) { StartIrmaServer(t, false, "") defer StopIrmaServer() - _, _, err := irmaServer.StartSession(irma.NewDisclosureRequest( + _, _, _, err := irmaServer.StartSession(irma.NewDisclosureRequest( irma.NewAttributeTypeIdentifier("irma-demo.RU.foo.bar"), irma.NewAttributeTypeIdentifier("irma-demo.baz.qux.abc"), ), nil) @@ -124,7 +123,7 @@ func TestRequestorInvalidRequest(t *testing.T) { func TestRequestorDoubleGET(t *testing.T) { StartIrmaServer(t, false, "") defer StopIrmaServer() - qr, _, err := irmaServer.StartSession(irma.NewDisclosureRequest( + qr, _, _, err := irmaServer.StartSession(irma.NewDisclosureRequest( irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID"), ), nil) require.NoError(t, err) @@ -378,12 +377,12 @@ func TestClientDeveloperMode(t *testing.T) { // Try to start another session with our non-https server issuanceRequest = getNameIssuanceRequest() - qr, _, err := irmaServer.StartSession(issuanceRequest, nil) + qr, _, _, err := irmaServer.StartSession(issuanceRequest, nil) require.NoError(t, err) c := make(chan *SessionResult, 1) j, err := json.Marshal(qr) require.NoError(t, err) - client.NewSession(string(j), &TestHandler{t, c, client, nil, 0, ""}) + client.NewSession(string(j), &TestHandler{t, c, client, nil, 0, "", nil}) result := <-c // Check that it failed with an appropriate error message @@ -464,6 +463,6 @@ func TestIssueExpiredKey(t *testing.T) { // server aborts issuance sessions in case of expired public keys expireKey(t, irmaServerConfiguration.IrmaConfiguration) - _, _, err := irmaServer.StartSession(getIssuanceRequest(true), nil) + _, _, _, err := irmaServer.StartSession(getIssuanceRequest(true), nil) require.Error(t, err) } diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index d77aef937..621ceb05b 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -136,6 +136,12 @@ func TestIssuanceSameAttributesNotSingleton(t *testing.T) { require.Equal(t, prevLen+1, len(client.CredentialInfoList())) } +func TestIssuanceBinding(t *testing.T) { + id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") + request := getCombinedIssuanceRequest(id) + sessionHelperWithBinding(t, request, "issue", nil, func(*testing.T, *irma.HTTPTransport) {}) +} + func TestLargeAttribute(t *testing.T) { client, handler := parseStorage(t) defer test.ClearTestStorage(t, handler.storage) @@ -366,7 +372,7 @@ func TestIssueOptionalAttributeUpdateSchemeManager(t *testing.T) { serverChan := make(chan *server.SessionResult) StartIrmaServer(t, false, "") // Run a server with old configuration (level is non-optional) - _, _, err := irmaServer.StartSession(issuanceRequest, func(result *server.SessionResult) { + _, _, _, err := irmaServer.StartSession(issuanceRequest, func(result *server.SessionResult) { serverChan <- result }) expectedError := &irma.RequiredAttributeMissingError{ @@ -389,7 +395,7 @@ func TestIssueOptionalAttributeUpdateSchemeManager(t *testing.T) { _, err = client.Configuration.Download(issuanceRequest) require.NoError(t, err) require.True(t, client.Configuration.CredentialTypes[credid].AttributeType(attrid).IsOptional()) - _, _, err = irmaServer.StartSession(issuanceRequest, func(result *server.SessionResult) { + _, _, _, err = irmaServer.StartSession(issuanceRequest, func(result *server.SessionResult) { serverChan <- result }) require.NoError(t, err) @@ -488,7 +494,7 @@ func TestStaticQRSession(t *testing.T) { c := make(chan *SessionResult) // Perform session - client.NewSession(string(bts), &TestHandler{t, c, client, requestor, 0, ""}) + client.NewSession(string(bts), &TestHandler{t, c, client, requestor, 0, "", nil}) if result := <-c; result != nil { require.NoError(t, result.Err) } @@ -538,7 +544,7 @@ func TestBlindIssuanceSession(t *testing.T) { StartIrmaServer(t, false, "") defer StopIrmaServer() - _, _, err := irmaServer.StartSession(request, nil) + _, _, _, err := irmaServer.StartSession(request, nil) require.EqualError(t, err, "Error type: randomblind\nDescription: randomblind attribute cannot be set in credential request\nStatus code: 0") // Make the request valid @@ -629,3 +635,18 @@ func TestChainedSessions(t *testing.T) { require.NoError(t, errors.New("newly issued credential not found in client")) } + +func TestOptionsChangeAfterConnected(t *testing.T) { + id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") + request := getCombinedIssuanceRequest(id) + + check := func(t *testing.T, transport *irma.HTTPTransport) { + request := irma.NewOptionsRequest() + result := &server.SessionOptions{} + err := transport.Post("options", result, request) + require.NoError(t, err) + require.NotEqual(t, request.EnableBinding, result.BindingEnabled) + } + + sessionHelperWithBinding(t, request, "issue", nil, check) +} diff --git a/irma/cmd/request.go b/irma/cmd/request.go index 422207ac4..0136b2f6b 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -3,11 +3,6 @@ package cmd import ( "encoding/json" "fmt" - "net/http" - "os" - "strconv" - "strings" - "time" "github.com/dgrijalva/jwt-go" "github.com/go-errors/errors" @@ -16,8 +11,11 @@ import ( "github.com/privacybydesign/irmago/internal/common" "github.com/privacybydesign/irmago/server" "github.com/sietseringers/cobra" - sseclient "github.com/sietseringers/go-sse" "github.com/sietseringers/pflag" + "net/http" + "os" + "strconv" + "strings" ) // requestCmd represents the request command @@ -112,52 +110,6 @@ func configureRequest(cmd *cobra.Command) (irma.RequestorRequest, *irma.Configur // Helper functions -func wait(initialStatus server.Status, transport *irma.HTTPTransport, statuschan chan server.Status) { - events := make(chan *sseclient.Event) - - go func() { - for { - if e := <-events; e != nil && e.Type != "open" { - status := server.Status(strings.Trim(string(e.Data), `"`)) - statuschan <- status - if status.Finished() { - return - } - } - } - }() - - if err := sseclient.Notify(nil, transport.Server+"statusevents", true, events); err != nil { - fmt.Println("SSE failed, fallback to polling", err) - close(events) - poll(initialStatus, transport, statuschan) - return - } -} - -// poll recursively polls the session status until a final status is received. -func poll(initialStatus server.Status, transport *irma.HTTPTransport, statuschan chan server.Status) { - // First we wait - <-time.NewTimer(pollInterval).C - - // Get session status - var s string - if err := transport.Get("status", &s); err != nil { - _ = server.LogFatal(err) - } - status := server.Status(strings.Trim(s, `"`)) - - // report if status changed - if status != initialStatus { - statuschan <- status - } - - if status.Finished() { - return - } - go poll(status, transport, statuschan) -} - func constructSessionRequest(cmd *cobra.Command, conf *irma.Configuration) (irma.RequestorRequest, error) { disclose, _ := cmd.Flags().GetStringArray("disclose") issue, _ := cmd.Flags().GetStringArray("issue") @@ -318,7 +270,7 @@ func startServer(port int) { }() } -func printQr(qr *irma.Qr, noqr bool) error { +func printQr(qr *irma.Qr, noqr bool, options *server.SessionOptions) error { qrBts, err := json.Marshal(qr) if err != nil { return err @@ -333,6 +285,9 @@ func printQr(qr *irma.Qr, noqr bool) error { WhiteChar: qrterminal.WHITE, }) } + if options.BindingEnabled { + fmt.Println("\nBinding code:", options.BindingCode) + } return nil } diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 36457b17a..3c4a50cbc 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -6,7 +6,6 @@ import ( "regexp" "strconv" "sync" - "time" "github.com/go-errors/errors" irma "github.com/privacybydesign/irmago" @@ -17,8 +16,6 @@ import ( prefixed "github.com/x-cray/logrus-prefixed-formatter" ) -const pollInterval = 1000 * time.Millisecond - var ( httpServer *http.Server irmaServer *irmaserver.Server @@ -63,6 +60,7 @@ irma session --server http://localhost:8088 --authmethod token --key mytoken --d url, _ := cmd.Flags().GetString("url") serverurl, _ := cmd.Flags().GetString("server") noqr, _ := cmd.Flags().GetBool("noqr") + binding, _ := cmd.Flags().GetBool("binding") if url != defaulturl && serverurl != "" { die("Failed to read configuration", errors.New("--url can't be combined with --server")) @@ -72,12 +70,12 @@ irma session --server http://localhost:8088 --authmethod token --key mytoken --d port, _ := flags.GetInt("port") privatekeysPath, _ := flags.GetString("privkeys") verbosity, _ := cmd.Flags().GetCount("verbose") - result, err = libraryRequest(request, irmaconfig, url, port, privatekeysPath, noqr, verbosity) + result, err = libraryRequest(request, irmaconfig, url, port, privatekeysPath, noqr, verbosity, binding) } else { authmethod, _ := flags.GetString("authmethod") key, _ := flags.GetString("key") name, _ := flags.GetString("name") - result, err = serverRequest(request, serverurl, authmethod, key, name, noqr) + result, err = serverRequest(request, serverurl, authmethod, key, name, noqr, binding) } if err != nil { die("Session failed", err) @@ -100,6 +98,7 @@ func libraryRequest( privatekeysPath string, noqr bool, verbosity int, + binding bool, ) (*server.SessionResult, error) { if err := configureSessionServer(url, port, privatekeysPath, irmaconfig, verbosity); err != nil { return nil, err @@ -108,15 +107,24 @@ func libraryRequest( // Start the session resultchan := make(chan *server.SessionResult) - qr, _, err := irmaServer.StartSession(request, func(r *server.SessionResult) { + qr, backendToken, _, err := irmaServer.StartSession(request, func(r *server.SessionResult) { resultchan <- r }) if err != nil { return nil, errors.WrapPrefix(err, "IRMA session failed", 0) } + // Currently, the default session options are the same in all conditions, + // so do only fetch them when a change is requested + sessionOptions := &server.SessionOptions{} + if binding { + optionsRequest := irma.NewOptionsRequest() + optionsRequest.EnableBinding = true + sessionOptions = irmaServer.SetOptions(backendToken, &optionsRequest) + } + // Print QR code - if err := printQr(qr, noqr); err != nil { + if err := printQr(qr, noqr, sessionOptions); err != nil { return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } @@ -128,25 +136,33 @@ func serverRequest( request irma.RequestorRequest, serverurl, authmethod, key, name string, noqr bool, + binding bool, ) (*server.SessionResult, error) { logger.Debug("Server URL: ", serverurl) // Start session at server - qr, transport, err := postRequest(serverurl, request, name, authmethod, key) + qr, sessionOptions, transport, err := postRequest(serverurl, request, name, authmethod, key, binding) if err != nil { return nil, err } // Print session QR logger.Debug("QR: ", prettyprint(qr)) - if err := printQr(qr, noqr); err != nil { + if err := printQr(qr, noqr, sessionOptions); err != nil { return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } statuschan := make(chan server.Status) + errorchan := make(chan error) var wg sync.WaitGroup - go wait(server.StatusInitialized, transport, statuschan) + go server.WaitStatus(transport, server.StatusInitialized, statuschan, errorchan) + go func() { + err := <-errorchan + if err != nil { + _ = server.LogFatal(err) + } + }() wg.Add(1) go func() { @@ -180,7 +196,8 @@ func serverRequest( return result, nil } -func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string) (*irma.Qr, *irma.HTTPTransport, error) { +func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string, binding bool) ( + *irma.Qr, *server.SessionOptions, *irma.HTTPTransport, error) { var ( err error pkg = &server.SessionPackage{} @@ -197,17 +214,26 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth var jwtstr string jwtstr, err = signRequest(request, name, authmethod, key) if err != nil { - return nil, nil, err + return nil, nil, nil, err } logger.Debug("Session request JWT: ", jwtstr) err = transport.Post("session", pkg, jwtstr) default: - return nil, nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)") + return nil, nil, nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)") } - token := pkg.Token - transport.Server += fmt.Sprintf("session/%s/", token) - return pkg.SessionPtr, transport, err + if err != nil { + return nil, nil, transport, err + } + + backendToken := pkg.Token + transport.Server += fmt.Sprintf("session/%s/", backendToken) + sessionOptions := &server.SessionOptions{} + if binding { + transport.SetHeader(irma.AuthorizationHeader, pkg.FrontendToken) + err = transport.Post(pkg.SessionPtr.URL+"/options", &irma.OptionsRequest{EnableBinding: true}, &sessionOptions) + } + return pkg.SessionPtr, sessionOptions, transport, err } // Configuration functions @@ -260,6 +286,7 @@ func init() { flags.StringP("url", "u", defaulturl, "external URL to which IRMA app connects (when not using --server), \":port\" being replaced by --port value") flags.IntP("port", "p", 48680, "port to listen at (when not using --server)") flags.Bool("noqr", false, "Print JSON instead of draw QR") + flags.Bool("binding", false, "Enable extra binding step between server and IRMA app") flags.StringP("request", "r", "", "JSON session request") flags.StringP("privkeys", "k", "", "path to private keys") flags.Bool("disable-schemes-update", false, "disable scheme updates") diff --git a/irmaclient/client.go b/irmaclient/client.go index ff7a898bd..52938f7c8 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -1109,7 +1109,7 @@ func (client *Client) keyshareEnrollWorker(managerID irma.SchemeManagerIdentifie // If the session succeeds or fails, the keyshare server is stored to disk or // removed from the client by the keyshareEnrollmentHandler. client.keyshareServers[managerID] = kss - client.newQrSession(qr, &keyshareEnrollmentHandler{ + client.NewQrSession(qr, &keyshareEnrollmentHandler{ client: client, pin: pin, kss: kss, @@ -1273,6 +1273,10 @@ func (client *Client) ConfigurationUpdated(downloaded *irma.IrmaIdentifierSet) e return nil } +func (client *Client) GetSupportedVersions() (min, max *irma.ProtocolVersion) { + return minVersion, maxVersion +} + func (cc *credCandidate) Present() bool { return cc.Hash != "" } diff --git a/irmaclient/handlers.go b/irmaclient/handlers.go index 885d3c78e..0c2e32400 100644 --- a/irmaclient/handlers.go +++ b/irmaclient/handlers.go @@ -83,3 +83,6 @@ func (h *keyshareEnrollmentHandler) KeyshareEnrollmentMissing(manager irma.Schem func (h *keyshareEnrollmentHandler) ClientReturnURLSet(clientReturnURL string) { h.fail(errors.New("Keyshare enrollment session unexpectedly found an external return url")) } +func (h *keyshareEnrollmentHandler) BindingRequired(bindingCode string) { + h.fail(errors.New("Keyshare enrollment session unexpectedly found an binding required")) +} diff --git a/irmaclient/irmaclient_test.go b/irmaclient/irmaclient_test.go index da806e994..ad8b5ab6b 100644 --- a/irmaclient/irmaclient_test.go +++ b/irmaclient/irmaclient_test.go @@ -124,7 +124,7 @@ func TestCandidates(t *testing.T) { // but we should also get the option to get another value request := irma.NewDisclosureRequest(attrtype) disjunction := request.Disclose[0] - request.ProtocolVersion = &irma.ProtocolVersion{Major: 2, Minor: 6} + request.ProtocolVersion = &irma.ProtocolVersion{Major: 2, Minor: 7} attrs, satisfiable, err := client.candidatesDisCon(request, disjunction) require.NoError(t, err) require.True(t, satisfiable) diff --git a/irmaclient/session.go b/irmaclient/session.go index 166ee5aee..95f73a35d 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/privacybydesign/irmago/server" + "github.com/bwesterb/go-atum" "github.com/go-errors/errors" "github.com/privacybydesign/gabi" @@ -35,6 +37,8 @@ type Handler interface { Cancelled() Failure(err *irma.SessionError) + BindingRequired(bindingCode string) + KeyshareBlocked(manager irma.SchemeManagerIdentifier, duration int) KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier) KeyshareEnrollmentMissing(manager irma.SchemeManagerIdentifier) @@ -110,12 +114,22 @@ var supportedVersions = map[int][]int{ 4, // old protocol with legacy session requests 5, // introduces condiscon feature 6, // introduces nonrevocation proofs - 7, // introduces chained sessions + 7, // introduces chained sessions and session binding }, } var minVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][0]} var maxVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][len(supportedVersions[2])-1]} +const clientTokenChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +func SetMaxVersion(version *irma.ProtocolVersion) { + if version == nil { + maxVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][len(supportedVersions[2])-1]} + } else { + maxVersion = version + } +} + // Session constructors // NewSession starts a new IRMA session, given (along with a handler to pass feedback to) a session request. @@ -129,7 +143,8 @@ func (client *Client) NewSession(sessionrequest string, handler Handler) Session handler.Failure(&irma.SessionError{ErrorType: irma.ErrorInvalidRequest, Err: err}) return nil } - return client.newQrSession(qr, handler) + _, dismisser := client.NewQrSession(qr, handler) + return dismisser } sigRequest := &irma.SignatureRequest{} @@ -177,20 +192,20 @@ func (client *Client) newManualSession(request irma.SessionRequest, handler Hand return session } -// newQrSession creates and starts a new interactive IRMA session -func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { +// NewQrSession creates and starts a new interactive IRMA session +func (client *Client) NewQrSession(qr *irma.Qr, handler Handler) (irma.ClientToken, *session) { if qr.Type == irma.ActionRedirect { newqr := &irma.Qr{} transport := irma.NewHTTPTransport("", !client.Preferences.DeveloperMode) if err := transport.Post(qr.URL, newqr, struct{}{}); err != nil { handler.Failure(&irma.SessionError{ErrorType: irma.ErrorTransport, Err: errors.Wrap(err, 0)}) - return nil + return "", nil } if newqr.Type == irma.ActionRedirect { // explicitly avoid infinite recursion handler.Failure(&irma.SessionError{ErrorType: irma.ErrorInvalidRequest, Err: errors.New("infinite static QR recursion")}) - return nil + return "", nil } - return client.newQrSession(newqr, handler) + return client.NewQrSession(newqr, handler) } client.PauseJobs() @@ -228,29 +243,44 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { fallthrough default: session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownAction, Info: string(session.Action)}) - return nil + return "", nil } + clientToken := common.NewSessionToken() session.transport.SetHeader(irma.MinVersionHeader, min.String()) session.transport.SetHeader(irma.MaxVersionHeader, maxVersion.String()) + session.transport.SetHeader(irma.AuthorizationHeader, clientToken) if !strings.HasSuffix(session.ServerURL, "/") { session.ServerURL += "/" } go session.getSessionInfo() - return session + return clientToken, session } // Core session methods // getSessionInfo retrieves the first message in the IRMA protocol (only in interactive sessions) +// If needed, it also handles binding. func (session *session) getSessionInfo() { defer session.recoverFromPanic() session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating) // Get the first IRMA protocol message and parse it - err := session.transport.Get("", session.request) + info := &server.SessionInfo{ + Request: session.request, // As request is an interface it need to be initialized with a specific instance. + } + err := session.transport.Get("", info) + if err != nil { + session.fail(err.(*irma.SessionError)) + return + } + + // Check whether binding is needed, and if so, wait for it to be completed + if info.Options.BindingEnabled { + err = session.handleBinding(info.Options.BindingCode) + } if err != nil { session.fail(err.(*irma.SessionError)) return @@ -259,6 +289,28 @@ func (session *session) getSessionInfo() { session.processSessionInfo() } +func (session *session) handleBinding(bindingCode string) error { + session.Handler.BindingRequired(bindingCode) + + statuschan := make(chan server.Status) + errorchan := make(chan error) + + go server.WaitStatusChanged(session.transport, server.StatusInitialized, statuschan, errorchan) + select { + case status := <-statuschan: + if status == server.StatusBindingCompleted { + return session.transport.Get("request", session.request) + } else { + return errors.New("Server finished session without completing binding") + } + case <-errorchan: + return &irma.SessionError{ + ErrorType: irma.ErrorServerResponse, + Info: "Binding aborted by server", + } + } +} + func requestorInfo(serverURL string, conf *irma.Configuration) *irma.RequestorInfo { if serverURL == "" { return nil @@ -530,7 +582,7 @@ func (session *session) sendResponse(message interface{}) { session.finish(false) if serverResponse != nil && serverResponse.NextSession != nil { - session.next = session.client.newQrSession(serverResponse.NextSession, session.Handler) + _, session.next = session.client.NewQrSession(serverResponse.NextSession, session.Handler) session.next.implicitDisclosure = session.choice.Attributes } else { session.Handler.Success(string(messageJson)) diff --git a/messages.go b/messages.go index aab755cd4..d6e7a3824 100644 --- a/messages.go +++ b/messages.go @@ -19,8 +19,9 @@ import ( type Status string const ( - MinVersionHeader = "X-IRMA-MinProtocolVersion" - MaxVersionHeader = "X-IRMA-MaxProtocolVersion" + MinVersionHeader = "X-IRMA-MinProtocolVersion" + MaxVersionHeader = "X-IRMA-MaxProtocolVersion" + AuthorizationHeader = "Authorization" ) // ProtocolVersion encodes the IRMA protocol version of an IRMA session. @@ -164,6 +165,14 @@ type Qr struct { Type Action `json:"irmaqr"` } +// Tokens to identify a session from the perspective of the different agents +type BackendToken = string +type ClientToken = string +type FrontendToken = string + +// Authorization headers +type ClientAuthorization = string + // Statuses const ( StatusConnected = Status("connected") diff --git a/requests.go b/requests.go index 942f9e61d..bf9b04ad6 100644 --- a/requests.go +++ b/requests.go @@ -22,6 +22,7 @@ const ( LDContextSignatureRequest = "https://irma.app/ld/request/signature/v2" LDContextIssuanceRequest = "https://irma.app/ld/request/issuance/v2" LDContextRevocationRequest = "https://irma.app/ld/request/revocation/v1" + LDContextOptionsRequest = "https://irma.app/ld/request/options/v1" DefaultJwtValidity = 120 ) @@ -210,6 +211,13 @@ type AttributeRequest struct { NotNull bool `json:"notNull,omitempty"` } +// An OptionsRequest asks for a options change of a particular session. +type OptionsRequest struct { + LDContext string `json:"@context,omitempty"` + EnableBinding bool `json:"enableBinding,omitempty"` + BindingCompleted bool `json:"bindingCompleted,omitempty"` +} + type RevocationRequest struct { LDContext string `json:"@context,omitempty"` CredentialType CredentialTypeIdentifier `json:"type"` @@ -1092,3 +1100,10 @@ func SignRequestorRequest(request RequestorRequest, alg jwt.SigningMethod, key i func NewAttributeRequest(attr string) AttributeRequest { return AttributeRequest{Type: NewAttributeTypeIdentifier(attr)} } + +// NewOptionsRequest returns a new options request initialized with default values for each option +func NewOptionsRequest() OptionsRequest { + return OptionsRequest{ + LDContext: LDContextOptionsRequest, + } +} diff --git a/server/api.go b/server/api.go index b723e949e..dccf0cb79 100644 --- a/server/api.go +++ b/server/api.go @@ -25,9 +25,22 @@ import ( var Logger *logrus.Logger = logrus.StandardLogger() +const LDContextSessionInfo = "https://irma.app/ld/request/info/v1" + type SessionPackage struct { - SessionPtr *irma.Qr `json:"sessionPtr"` - Token string `json:"token"` + SessionPtr *irma.Qr `json:"sessionPtr"` + Token irma.BackendToken `json:"token"` + FrontendToken irma.FrontendToken `json:"frontendToken"` +} + +// SessionInfo contains all information irmaclient needs to know to initiate a session. +// Session request is stored as RawMessage since it is not used internally and otherwise we would +// have to make a manual JSON marshal function with type checking since SessionRequest is an interface. +type SessionInfo struct { + LDContext string `json:"@context,omitempty"` + ProtocolVersion *irma.ProtocolVersion `json:"protocolVersion,omitempty"` + Options *SessionOptions `json:"options,omitempty"` + Request irma.SessionRequest `json:"request,omitempty"` } // SessionResult contains session information such as the session status, type, possible errors, @@ -35,7 +48,7 @@ type SessionPackage struct { type SessionResult struct { Token string `json:"token"` Status Status `json:"status"` - Type irma.Action `json:"type"'` + Type irma.Action `json:"type"` ProofStatus irma.ProofStatus `json:"proofStatus,omitempty"` Disclosed [][]*irma.DisclosedAttribute `json:"disclosed,omitempty"` Signature *irma.SignedMessage `json:"signature,omitempty"` @@ -45,6 +58,12 @@ type SessionResult struct { LegacySession bool `json:"-"` // true if request was started with legacy (i.e. pre-condiscon) session request } +type SessionOptions struct { + BindingEnabled bool `json:"bindingEnabled"` + BindingCode string `json:"bindingCode,omitempty"` + BindingCompleted bool `json:"bindingCompleted,omitempty"` +} + // SessionHandler is a function that can handle a session result // once an IRMA session has completed. type SessionHandler func(*SessionResult) @@ -67,14 +86,6 @@ type LegacySessionResult struct { Err *irma.RemoteError `json:"error,omitempty"` } -const ( - StatusInitialized Status = "INITIALIZED" // The session has been started and is waiting for the client - StatusConnected Status = "CONNECTED" // The client has retrieved the session request, we wait for its response - StatusCancelled Status = "CANCELLED" // The session is cancelled, possibly due to an error - StatusDone Status = "DONE" // The session has completed successfully - StatusTimeout Status = "TIMEOUT" // Session timed out -) - const ( ComponentRevocation = "revocation" ComponentSession = "session" @@ -87,6 +98,15 @@ const ( WriteTimeout = 2 * ReadTimeout ) +const ( + StatusInitialized Status = "INITIALIZED" // The session has been started and is waiting for the client + StatusBindingCompleted Status = "BINDING_COMPLETED" // The client has bound and can retrieve the request + StatusConnected Status = "CONNECTED" // The client has retrieved the session request, we wait for its response + StatusCancelled Status = "CANCELLED" // The session is cancelled, possibly due to an error + StatusDone Status = "DONE" // The session has completed successfully + StatusTimeout Status = "TIMEOUT" // Session timed out +) + // Remove this when dropping support for legacy pre-condiscon session requests func (r *SessionResult) Legacy() *LegacySessionResult { var disclosed []*irma.DisclosedAttribute @@ -530,3 +550,22 @@ func LogMiddleware(typ string, opts LogOptions) func(next http.Handler) http.Han }) } } + +func (info *SessionInfo) UnmarshalJSON(data []byte) error { + // Marshal in alias first to prevent infinite recursion + type Alias SessionInfo + err := json.Unmarshal(data, &struct{ *Alias }{(*Alias)(info)}) + if err == nil && info.LDContext == LDContextSessionInfo { + return nil + } + + // For legacy sessions initialize session info by hand using the fetched request + err = json.Unmarshal(data, info.Request) + if err != nil { + return err + } + info.LDContext = LDContextSessionInfo + info.ProtocolVersion = info.Request.Base().ProtocolVersion + info.Options = &SessionOptions{} + return nil +} diff --git a/server/errors.go b/server/errors.go index f35b5a3d4..9265481bf 100644 --- a/server/errors.go +++ b/server/errors.go @@ -19,6 +19,8 @@ var ( ErrorAttributesWrong Error = Error{Type: "ATTRIBUTES_WRONG", Status: 400, Description: "Specified attribute(s) do not belong to this credential type or missing attributes"} ErrorCannotIssue Error = Error{Type: "CANNOT_ISSUE", Status: 500, Description: "Cannot issue this credential"} + ErrorClientUnauthorized Error = Error{Type: "UNAUTHORIZED", Status: 403, Description: "You are not authorized to access the session"} + ErrorBindingRequired Error = Error{Type: "BINDING_REQUIRED", Status: 403, Description: "Binding is required first"} ErrorIssuanceFailed Error = Error{Type: "ISSUANCE_FAILED", Status: 500, Description: "Failed to create credential(s)"} ErrorInvalidProofs Error = Error{Type: "INVALID_PROOFS", Status: 400, Description: "Invalid secret key commitments and/or disclosure proofs"} ErrorAttributesMissing Error = Error{Type: "ATTRIBUTES_MISSING", Status: 400, Description: "Not all requested-for attributes were present"} diff --git a/server/irmac/irmac.go b/server/irmac/irmac.go index 60980e73f..ef51ed147 100644 --- a/server/irmac/irmac.go +++ b/server/irmac/irmac.go @@ -49,9 +49,10 @@ func Initialize(IrmaConfiguration *C.char) *C.char { func StartSession(requestString *C.char) (r *C.char) { // Create struct for return information result := struct { - IrmaQr string - Token string - Error string + IrmaQr string + BackendToken string + FrontendToken string + Error string }{} defer func() { j, _ := json.Marshal(result) @@ -65,7 +66,7 @@ func StartSession(requestString *C.char) (r *C.char) { } // Run the actual core function - qr, token, err := s.StartSession(C.GoString(requestString), nil) + qr, backendToken, frontendToken, err := s.StartSession(C.GoString(requestString), nil) // And properly return the result if err != nil { @@ -79,7 +80,8 @@ func StartSession(requestString *C.char) (r *C.char) { } // return actual results result.IrmaQr = string(qrJson) - result.Token = token + result.BackendToken = backendToken + result.FrontendToken = frontendToken return } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 2ed2b004f..201bbdd02 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -109,16 +109,22 @@ func (s *Server) HandlerFunc() http.HandlerFunc { r.NotFound(errorWriter(notfound, server.WriteResponse)) r.MethodNotAllowed(errorWriter(notallowed, server.WriteResponse)) - r.Route("/session/{token}", func(r chi.Router) { + r.Route("/session/{backendToken}", func(r chi.Router) { r.Use(s.sessionMiddleware) r.Delete("/", s.handleSessionDelete) r.Get("/status", s.handleSessionStatus) r.Get("/statusevents", s.handleSessionStatusEvents) + r.Post("/options", s.handleSessionOptionsPost) r.Group(func(r chi.Router) { + r.Use(s.authenticationMiddleware) r.Use(s.cacheMiddleware) r.Get("/", s.handleSessionGet) - r.Post("/commitments", s.handleSessionCommitments) - r.Post("/proofs", s.handleSessionProofs) + r.Group(func(r chi.Router) { + r.Use(s.bindingMiddleware) + r.Get("/request", s.handleSessionGetRequest) + r.Post("/commitments", s.handleSessionCommitments) + r.Post("/proofs", s.handleSessionProofs) + }) }) }) r.Post("/session/{name}", s.handleStaticMessage) @@ -149,24 +155,27 @@ func (s *Server) Stop() { } // StartSession starts an IRMA session, running the handler on completion, if specified. -// The session token (the second return parameter) can be used in GetSessionResult() -// and CancelSession(). +// The session backendToken (the second return parameter) can be used in GetSessionResult() +// and CancelSession(). The session frontendToken (the third return parameter) is needed +// by frontend clients (i.e. browser libraries) to POST to the '/options' endpoint of the IRMA protocol. // The request parameter can be an irma.RequestorRequest, or an irma.SessionRequest, or a // ([]byte or string) JSON representation of one of those (for more details, see server.ParseSessionRequest().) -func StartSession(request interface{}, handler server.SessionHandler) (*irma.Qr, string, error) { +func StartSession(request interface{}, handler server.SessionHandler, +) (*irma.Qr, irma.BackendToken, irma.FrontendToken, error) { return s.StartSession(request, handler) } -func (s *Server) StartSession(req interface{}, handler server.SessionHandler) (*irma.Qr, string, error) { +func (s *Server) StartSession(req interface{}, handler server.SessionHandler, +) (*irma.Qr, irma.BackendToken, irma.FrontendToken, error) { rrequest, err := server.ParseSessionRequest(req) if err != nil { - return nil, "", err + return nil, "", "", err } request := rrequest.SessionRequest() action := request.Action() if err := s.validateRequest(request); err != nil { - return nil, "", err + return nil, "", "", err } if action == irma.ActionIssuing { // Include the AttributeTypeIdentifiers of random blind attributes to each CredentialRequest. @@ -177,66 +186,77 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler) (* } if err := s.validateIssuanceRequest(request.(*irma.IssuanceRequest)); err != nil { - return nil, "", err + return nil, "", "", err } } request.Base().DevelopmentMode = !s.conf.Production session := s.newSession(action, rrequest) - s.conf.Logger.WithFields(logrus.Fields{"action": action, "session": session.token}).Infof("Session started") + s.conf.Logger.WithFields(logrus.Fields{"action": action, "session": session.backendToken}).Infof("Session started") if s.conf.Logger.IsLevelEnabled(logrus.DebugLevel) { - s.conf.Logger.WithFields(logrus.Fields{"session": session.token, "clienttoken": session.clientToken}).Info("Session request: ", server.ToJson(rrequest)) + s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken, "clienttoken": session.clientToken}).Info("Session request: ", server.ToJson(rrequest)) } else { - s.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Info("Session request (purged of attribute values): ", server.ToJson(purgeRequest(rrequest))) + s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Info("Session request (purged of attribute values): ", server.ToJson(purgeRequest(rrequest))) } if handler != nil { - s.handlers[session.token] = handler + s.handlers[session.backendToken] = handler } return &irma.Qr{ Type: action, URL: s.conf.URL + "session/" + session.clientToken, - }, session.token, nil + }, session.backendToken, session.frontendToken, nil } // GetSessionResult retrieves the result of the specified IRMA session. -func GetSessionResult(token string) *server.SessionResult { - return s.GetSessionResult(token) +func GetSessionResult(backendToken irma.BackendToken) *server.SessionResult { + return s.GetSessionResult(backendToken) } -func (s *Server) GetSessionResult(token string) *server.SessionResult { - session := s.sessions.get(token) +func (s *Server) GetSessionResult(backendToken irma.BackendToken) *server.SessionResult { + session := s.sessions.get(backendToken) if session == nil { - s.conf.Logger.Warn("Session result requested of unknown session ", token) + s.conf.Logger.Warn("Session result requested of unknown session ", backendToken) return nil } return session.result } // GetRequest retrieves the request submitted by the requestor that started the specified IRMA session. -func GetRequest(token string) irma.RequestorRequest { +func GetRequest(token irma.BackendToken) irma.RequestorRequest { return s.GetRequest(token) } -func (s *Server) GetRequest(token string) irma.RequestorRequest { - session := s.sessions.get(token) +func (s *Server) GetRequest(backendToken irma.BackendToken) irma.RequestorRequest { + session := s.sessions.get(backendToken) if session == nil { - s.conf.Logger.Warn("Session request requested of unknown session ", token) + s.conf.Logger.Warn("Session request requested of unknown session ", backendToken) return nil } return session.rrequest } // CancelSession cancels the specified IRMA session. -func CancelSession(token string) error { - return s.CancelSession(token) +func CancelSession(backendToken irma.BackendToken) error { + return s.CancelSession(backendToken) } -func (s *Server) CancelSession(token string) error { - session := s.sessions.get(token) +func (s *Server) CancelSession(backendToken irma.BackendToken) error { + session := s.sessions.get(backendToken) if session == nil { - return server.LogError(errors.Errorf("can't cancel unknown session %s", token)) + return server.LogError(errors.Errorf("can't cancel unknown session %s", backendToken)) } session.handleDelete() return nil } +// Requests a change of the session options at the server. +// Returns the updated options struct. Invalid requested options are ignored. +// Options that are not specified in the request, keep their old value. +func SetOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) *server.SessionOptions { + return s.SetOptions(backendToken, request) +} +func (s *Server) SetOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) *server.SessionOptions { + session := s.sessions.get(backendToken) + return session.updateOptions(request) +} + // Revoke revokes the earlier issued credential specified by key. (Can only be used if this server // is the revocation server for the specified credential type and if the corresponding // issuer private key is present in the server configuration.) @@ -249,10 +269,10 @@ func (s *Server) Revoke(credid irma.CredentialTypeIdentifier, key string, issued // SubscribeServerSentEvents subscribes the HTTP client to server sent events on status updates // of the specified IRMA session. -func SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, token string, requestor bool) error { - return s.SubscribeServerSentEvents(w, r, token, requestor) +func SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, backendToken irma.BackendToken, requestor bool) error { + return s.SubscribeServerSentEvents(w, r, backendToken, requestor) } -func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, token string, requestor bool) error { +func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, backendToken irma.BackendToken, requestor bool) error { if !s.conf.EnableSSE { server.WriteResponse(w, nil, &irma.RemoteError{ Status: 500, @@ -265,15 +285,15 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques var session *session if requestor { - session = s.sessions.get(token) + session = s.sessions.get(backendToken) } else { - session = s.sessions.clientGet(token) + session = s.sessions.clientGet(backendToken) } if session == nil { - return server.LogError(errors.Errorf("can't subscribe to server sent events of unknown session %s", token)) + return server.LogError(errors.Errorf("can't subscribe to server sent events of unknown session %s", backendToken)) } if session.status.Finished() { - return server.LogError(errors.Errorf("can't subscribe to server sent events of finished session %s", token)) + return server.LogError(errors.Errorf("can't subscribe to server sent events of finished session %s", backendToken)) } // The EventSource.onopen Javascript callback is not consistently called across browsers (Chrome yes, Firefox+Safari no). @@ -284,7 +304,7 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques // event to just the webclient currently listening. (Thus the handler of this "open" event must be idempotent.) go func() { time.Sleep(200 * time.Millisecond) - s.serverSentEvents.SendMessage("session/"+token, sse.NewMessage("", "", "open")) + s.serverSentEvents.SendMessage("session/"+backendToken, sse.NewMessage("", "", "open")) }() s.serverSentEvents.ServeHTTP(w, r) return nil diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 0b24977b8..4d416b61c 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -29,23 +29,24 @@ func (session *session) handleDelete() { } session.markAlive() - session.result = &server.SessionResult{Token: session.token, Status: server.StatusCancelled, Type: session.action} + session.result = &server.SessionResult{Token: session.backendToken, Status: server.StatusCancelled, Type: session.action} session.setStatus(server.StatusCancelled) } -func (session *session) handleGetRequest(min, max *irma.ProtocolVersion) (irma.SessionRequest, *irma.RemoteError) { - if session.status != server.StatusInitialized { - return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session already started") +func (session *session) handleGetRequest(min, max *irma.ProtocolVersion) ( + *server.SessionInfo, *irma.SessionRequest, *irma.RemoteError) { + if session.status != server.StatusInitialized && session.status != server.StatusBindingCompleted { + return nil, nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session already started") } session.markAlive() - logger := session.conf.Logger.WithFields(logrus.Fields{"session": session.token}) + logger := session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}) // we include the latest revocation updates for the client here, as opposed to when the session // was started, so that the client always gets the very latest revocation records var err error if err = session.conf.IrmaConfiguration.Revocation.SetRevocationUpdates(session.request.Base()); err != nil { - return nil, session.fail(server.ErrorRevocation, err.Error()) + return nil, nil, session.fail(server.ErrorRevocation, err.Error()) } // Handle legacy clients that do not support condiscon, by attempting to convert the condiscon @@ -57,7 +58,7 @@ func (session *session) handleGetRequest(min, max *irma.ProtocolVersion) (irma.S } if session.version, err = session.chooseProtocolVersion(min, max); err != nil { - return nil, session.fail(server.ErrorProtocolVersion, "") + return nil, nil, session.fail(server.ErrorProtocolVersion, "") } logger.WithFields(logrus.Fields{"version": session.version.String()}).Debugf("Protocol version negotiated") session.request.Base().ProtocolVersion = session.version @@ -67,31 +68,36 @@ func (session *session) handleGetRequest(min, max *irma.ProtocolVersion) (irma.S if session.version.Below(2, 5) { logger.Info("Returning legacy session format") legacy.Base().ProtocolVersion = session.version - return legacy, nil + return nil, &legacy, nil } - // In case of issuance requests, strip revocation keys from []CredentialRequest - isreq, issuing := session.request.(*irma.IssuanceRequest) - if !issuing { - return session.request, nil - } - cpy, err := copyObject(isreq) - if err != nil { - return nil, session.fail(server.ErrorRevocation, err.Error()) - } - for _, cred := range cpy.(*irma.IssuanceRequest).Credentials { - cred.RevocationSupported = cred.RevocationKey != "" - cred.RevocationKey = "" + if session.version.Below(2, 7) { + // These versions do not support binding, so force binding completion to prevent problems. + if session.options.BindingEnabled { + session.options.BindingCompleted = true + } + request, rerr := session.getRequest() + return nil, &request, rerr } - return cpy.(*irma.IssuanceRequest), nil + info, rerr := session.getInfo() + return info, nil, rerr } func (session *session) handleGetStatus() (server.Status, *irma.RemoteError) { return session.status, nil } +func (session *session) handlePostOptions(optionsRequest *irma.OptionsRequest, token irma.FrontendToken) ( + *server.SessionOptions, *irma.RemoteError) { + if token != session.frontendToken { + return nil, server.RemoteError(server.ErrorUnauthorized, "") + } + return session.updateOptions(optionsRequest), nil +} + func (session *session) handlePostSignature(signature *irma.SignedMessage) (*irma.ServerSessionResponse, *irma.RemoteError) { - if session.status != server.StatusConnected { + // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. + if session.version == nil || session.version.Below(2, 7) && session.status != server.StatusConnected { return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") } session.markAlive() @@ -122,7 +128,8 @@ func (session *session) handlePostSignature(signature *irma.SignedMessage) (*irm } func (session *session) handlePostDisclosure(disclosure *irma.Disclosure) (*irma.ServerSessionResponse, *irma.RemoteError) { - if session.status != server.StatusConnected { + // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. + if session.version == nil || session.version.Below(2, 7) && session.status != server.StatusConnected { return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") } session.markAlive() @@ -153,7 +160,8 @@ func (session *session) handlePostDisclosure(disclosure *irma.Disclosure) (*irma } func (session *session) handlePostCommitments(commitments *irma.IssueCommitmentMessage) (*irma.ServerSessionResponse, *irma.RemoteError) { - if session.status != server.StatusConnected { + // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. + if session.version == nil || session.version.Below(2, 7) && session.status != server.StatusConnected { return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") } session.markAlive() @@ -306,7 +314,7 @@ func (s *Server) startNext(session *session, res *irma.ServerSessionResponse) er if next == nil { return nil } - qr, token, err := s.StartSession(next, nil) + qr, token, _, err := s.StartSession(next, nil) if err != nil { return err } @@ -418,8 +426,41 @@ func (s *Server) handleSessionGet(w http.ResponseWriter, r *http.Request) { return } session := r.Context().Value("session").(*session) - res, err := session.handleGetRequest(&min, &max) - server.WriteResponse(w, res, err) + // When session binding is supported by all clients, the legacy support can be removed + res, legacyRes, err := session.handleGetRequest(&min, &max) + if legacyRes != nil { + server.WriteResponse(w, legacyRes, err) + } else { + server.WriteResponse(w, res, err) + } +} + +func (s *Server) handleSessionGetRequest(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session) + if session.options.BindingEnabled && !session.options.BindingCompleted { + server.WriteError(w, server.ErrorUnauthorized, "Binding required") + return + } + request, err := session.getRequest() + server.WriteResponse(w, request, err) +} + +func (s *Server) handleSessionOptionsPost(w http.ResponseWriter, r *http.Request) { + optionsRequest := &irma.OptionsRequest{} + bts, err := ioutil.ReadAll(r.Body) + if err != nil { + server.WriteError(w, server.ErrorMalformedInput, err.Error()) + return + } + err = irma.UnmarshalValidate(bts, optionsRequest) + if err != nil { + server.WriteError(w, server.ErrorMalformedInput, err.Error()) + return + } + frontendToken := r.Header.Get(irma.AuthorizationHeader) + session := r.Context().Value("session").(*session) + res, rerr := session.handlePostOptions(optionsRequest, frontendToken) + server.WriteResponse(w, res, rerr) } func (s *Server) handleStaticMessage(w http.ResponseWriter, r *http.Request) { @@ -428,7 +469,7 @@ func (s *Server) handleStaticMessage(w http.ResponseWriter, r *http.Request) { server.WriteResponse(w, nil, server.RemoteError(server.ErrorInvalidRequest, "unknown static session")) return } - qr, _, err := s.StartSession(rrequest, s.doResultCallback) + qr, _, _, err := s.StartSession(rrequest, s.doResultCallback) if err != nil { server.WriteResponse(w, nil, server.RemoteError(server.ErrorMalformedInput, err.Error())) return diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index a67b2d82f..b18d0dce6 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -31,11 +31,11 @@ import ( func (session *session) markAlive() { session.lastActive = time.Now() - session.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Debugf("Session marked active, expiry delayed") + session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).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}). + session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken, "prevStatus": session.prevStatus, "status": status}). Info("Session status updated") session.status = status session.result.Status = status @@ -49,15 +49,33 @@ func (session *session) onUpdate() { session.sse.SendMessage("session/"+session.clientToken, sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)), ) - session.sse.SendMessage("session/"+session.token, + session.sse.SendMessage("session/"+session.backendToken, sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)), ) } +// Checks whether requested options are valid in the current session context. +func (session *session) updateOptions(request *irma.OptionsRequest) *server.SessionOptions { + if session.status == server.StatusInitialized { + session.options.BindingEnabled = request.EnableBinding + if request.EnableBinding { + session.options.BindingCode = common.NewBindingCode() + } else { + session.options.BindingCode = "" + } + } else if session.status == server.StatusConnected { + if session.options.BindingEnabled && !session.options.BindingCompleted && request.BindingCompleted { + session.options.BindingCompleted = true + session.status = server.StatusBindingCompleted + } + } + return &session.options +} + 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} + session.result = &server.SessionResult{Err: rerr, Token: session.backendToken, Status: server.StatusCancelled, Type: session.action} return rerr } @@ -95,8 +113,9 @@ const retryTimeLimit = 10 * time.Second // - 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 || +func (session *session) checkCache(endpoint string, message []byte) (int, []byte) { + if session.responseCache.endpoint != endpoint || + 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) { @@ -267,6 +286,40 @@ func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, sche return session.kssProofs[scheme], nil } +func (session *session) getInfo() (*server.SessionInfo, *irma.RemoteError) { + info := server.SessionInfo{ + LDContext: server.LDContextSessionInfo, + ProtocolVersion: session.version, + Options: &session.options, + } + + if !session.options.BindingEnabled || session.options.BindingCompleted { + request, rerr := session.getRequest() + if rerr != nil { + return nil, rerr + } + info.Request = request + } + return &info, nil +} + +func (session *session) getRequest() (irma.SessionRequest, *irma.RemoteError) { + // In case of issuance requests, strip revocation keys from []CredentialRequest + isreq, issuing := session.request.(*irma.IssuanceRequest) + if !issuing { + return session.request, nil + } + cpy, err := copyObject(isreq) + if err != nil { + return nil, session.fail(server.ErrorRevocation, err.Error()) + } + for _, cred := range cpy.(*irma.IssuanceRequest).Credentials { + cred.RevocationSupported = cred.RevocationKey != "" + cred.RevocationKey = "" + } + return cpy.(*irma.IssuanceRequest), nil +} + // Other func (s *Server) doResultCallback(result *server.SessionResult) { @@ -389,7 +442,7 @@ func (s *Server) cacheMiddleware(next http.Handler) http.Handler { r.Body = ioutil.NopCloser(bytes.NewBuffer(message)) // if a cache is set and applicable, return it - status, output := session.checkCache(message) + status, output := session.checkCache(r.URL.Path, message) if status > 0 && len(output) > 0 { w.WriteHeader(status) _, _ = w.Write(output) @@ -403,6 +456,7 @@ func (s *Server) cacheMiddleware(next http.Handler) http.Handler { next.ServeHTTP(ww, r) session.responseCache = responseCache{ + endpoint: r.URL.Path, message: message, response: buf.Bytes(), status: ww.Status(), @@ -413,7 +467,7 @@ func (s *Server) cacheMiddleware(next http.Handler) http.Handler { func (s *Server) sessionMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := chi.URLParam(r, "token") + token := chi.URLParam(r, "backendToken") session := s.sessions.clientGet(token) if session == nil { server.WriteError(w, server.ErrorSessionUnknown, "") @@ -447,3 +501,42 @@ func (s *Server) sessionMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r.WithContext(context.WithValue(ctx, "session", session))) }) } + +func (s *Server) authenticationMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session) + + authorization := r.Header.Get(irma.AuthorizationHeader) + if authorization == "" && session.version != nil { + // Protocol versions below 2.7 use legacy authentication handled in handleSessionGet and cacheMiddleware. + if session.version.Below(2, 7) { + next.ServeHTTP(w, r) + } else { + server.WriteError(w, server.ErrorClientUnauthorized, "No authorization header provided") + } + return + } + + if session.clientAuth == "" { + session.clientAuth = authorization + } else if session.clientAuth != authorization { + server.WriteError(w, server.ErrorClientUnauthorized, "") + return + } + + next.ServeHTTP(w, r) + }) +} + +func (s *Server) bindingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session) + + if session.options.BindingEnabled && !session.options.BindingCompleted { + server.WriteError(w, server.ErrorBindingRequired, "") + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index 9c7138273..a619bda56 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -20,19 +20,22 @@ type session struct { locked bool action irma.Action - token string - clientToken string + backendToken irma.BackendToken + clientToken irma.ClientToken + frontendToken irma.FrontendToken version *irma.ProtocolVersion rrequest irma.RequestorRequest request irma.SessionRequest legacyCompatible bool // if the request is convertible to pre-condiscon format implicitDisclosure irma.AttributeConDisCon + options server.SessionOptions status server.Status prevStatus server.Status sse *sse.Server responseCache responseCache + clientAuth irma.ClientAuthorization lastActive time.Time result *server.SessionResult @@ -43,6 +46,7 @@ type session struct { } type responseCache struct { + endpoint string message []byte response []byte status int @@ -90,7 +94,7 @@ func (s *memorySessionStore) clientGet(t string) *session { func (s *memorySessionStore) add(session *session) { s.Lock() defer s.Unlock() - s.requestor[session.token] = session + s.requestor[session.backendToken] = session s.client[session.clientToken] = session } @@ -103,7 +107,7 @@ func (s *memorySessionStore) stop() { defer s.Unlock() for _, session := range s.requestor { if session.sse != nil { - session.sse.CloseChannel("session/" + session.token) + session.sse.CloseChannel("session/" + session.backendToken) session.sse.CloseChannel("session/" + session.clientToken) } } @@ -129,11 +133,11 @@ func (s *memorySessionStore) deleteExpired() { if session.lastActive.Add(timeout).Before(time.Now()) { if !session.status.Finished() { - s.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Infof("Session expired") + s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Infof("Session expired") session.markAlive() session.setStatus(server.StatusTimeout) } else { - s.conf.Logger.WithFields(logrus.Fields{"session": session.token}).Infof("Deleting session") + s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Infof("Deleting session") expired = append(expired, token) } } @@ -145,7 +149,7 @@ func (s *memorySessionStore) deleteExpired() { for _, token := range expired { session := s.requestor[token] if session.sse != nil { - session.sse.CloseChannel("session/" + session.token) + session.sse.CloseChannel("session/" + session.backendToken) session.sse.CloseChannel("session/" + session.clientToken) } delete(s.client, session.clientToken) @@ -157,39 +161,42 @@ func (s *memorySessionStore) deleteExpired() { var one *big.Int = big.NewInt(1) func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) *session { - token := common.NewSessionToken() clientToken := common.NewSessionToken() + backendToken := common.NewSessionToken() + frontendToken := common.NewSessionToken() base := request.SessionRequest().Base() if s.conf.AugmentClientReturnURL && base.AugmentReturnURL && base.ClientReturnURL != "" { if strings.Contains(base.ClientReturnURL, "?") { - base.ClientReturnURL += "&token=" + token + base.ClientReturnURL += "&token=" + backendToken } else { - base.ClientReturnURL += "?token=" + token + base.ClientReturnURL += "?token=" + backendToken } } ses := &session{ - action: action, - rrequest: request, - request: request.SessionRequest(), - lastActive: time.Now(), - token: token, - clientToken: clientToken, - status: server.StatusInitialized, - prevStatus: server.StatusInitialized, - conf: s.conf, - sessions: s.sessions, - sse: s.serverSentEvents, + action: action, + rrequest: request, + request: request.SessionRequest(), + options: server.SessionOptions{}, + lastActive: time.Now(), + backendToken: backendToken, + clientToken: clientToken, + frontendToken: frontendToken, + status: server.StatusInitialized, + prevStatus: server.StatusInitialized, + conf: s.conf, + sessions: s.sessions, + sse: s.serverSentEvents, result: &server.SessionResult{ LegacySession: request.SessionRequest().Base().Legacy(), - Token: token, + Token: backendToken, Type: action, Status: server.StatusInitialized, }, } - s.conf.Logger.WithFields(logrus.Fields{"session": ses.token}).Debug("New session started") + s.conf.Logger.WithFields(logrus.Fields{"session": ses.backendToken}).Debug("New session started") nonce, _ := gabi.GenerateNonce() base.Nonce = nonce base.Context = one diff --git a/server/requestorserver/server.go b/server/requestorserver/server.go index b6df38b23..147886ccc 100644 --- a/server/requestorserver/server.go +++ b/server/requestorserver/server.go @@ -317,13 +317,13 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleStatusEvents(w http.ResponseWriter, r *http.Request) { - token := chi.URLParam(r, "token") - s.conf.Logger.WithFields(logrus.Fields{"session": token}).Debug("new client subscribed to server sent events") + backendToken := chi.URLParam(r, "backendToken") + s.conf.Logger.WithFields(logrus.Fields{"session": backendToken}).Debug("new client subscribed to server sent events") r = r.WithContext(context.WithValue(r.Context(), "sse", common.SSECtx{ Component: server.ComponentSession, - Arg: token, + Arg: backendToken, })) - if err := s.irmaserv.SubscribeServerSentEvents(w, r, token, true); err != nil { + if err := s.irmaserv.SubscribeServerSentEvents(w, r, backendToken, true); err != nil { server.WriteResponse(w, nil, &irma.RemoteError{ Status: server.ErrorUnsupported.Status, ErrorName: string(server.ErrorUnsupported.Type), @@ -387,8 +387,8 @@ func (s *Server) handleJwtProofs(w http.ResponseWriter, r *http.Request) { return } - sessiontoken := chi.URLParam(r, "token") - res := s.irmaserv.GetSessionResult(sessiontoken) + backendToken := chi.URLParam(r, "token") + res := s.irmaserv.GetSessionResult(backendToken) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") return @@ -413,7 +413,7 @@ func (s *Server) handleJwtProofs(w http.ResponseWriter, r *http.Request) { claims["iss"] = s.conf.JwtIssuer } claims["status"] = res.ProofStatus - validity := s.irmaserv.GetRequest(sessiontoken).Base().ResultJwtValidity + validity := s.irmaserv.GetRequest(backendToken).Base().ResultJwtValidity if validity != 0 { claims["exp"] = time.Now().Unix() + int64(validity) } @@ -518,15 +518,16 @@ func (s *Server) createSession(w http.ResponseWriter, requestor string, rrequest } // Everything is authenticated and parsed, we're good to go! - qr, token, err := s.irmaserv.StartSession(rrequest, s.doResultCallback) + qr, backendToken, frontendToken, err := s.irmaserv.StartSession(rrequest, s.doResultCallback) if err != nil { server.WriteError(w, server.ErrorInvalidRequest, err.Error()) return } server.WriteJson(w, server.SessionPackage{ - SessionPtr: qr, - Token: token, + SessionPtr: qr, + Token: backendToken, + FrontendToken: frontendToken, }) } diff --git a/server/wait_status.go b/server/wait_status.go new file mode 100644 index 000000000..10297e08c --- /dev/null +++ b/server/wait_status.go @@ -0,0 +1,91 @@ +package server + +import ( + "github.com/privacybydesign/irmago" + sseclient "github.com/sietseringers/go-sse" + "strings" + "time" +) + +const pollInterval = 1000 * time.Millisecond + +func WaitStatus(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { + if err := subscribeSSE(transport, statuschan, errorchan); err != nil { + go poll(transport, initialStatus, statuschan, errorchan) + } +} + +func WaitStatusChanged(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { + if err := subscribeSSE(transport, statuschan, errorchan); err != nil { + go pollUntilChange(transport, initialStatus, statuschan, errorchan) + } +} + +// Start listening for server-sent events +func subscribeSSE(transport *irma.HTTPTransport, statuschan chan Status, errorchan chan error) error { + + events := make(chan *sseclient.Event) + go func() { + for { + if e := <-events; e != nil && e.Type != "open" { + status := Status(strings.Trim(string(e.Data), `"`)) + statuschan <- status + if status.Finished() { + errorchan <- nil + return + } + } + } + }() + + err := sseclient.Notify(nil, transport.Server+"statusevents", true, events) + if err != nil { + close(events) + } + return err +} + +// poll recursively polls the session status until a final status is received. +func poll(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { + go func() { + for { + statuschanPolling := make(chan Status) + errorchanPolling := make(chan error) + go pollUntilChange(transport, initialStatus, statuschanPolling, errorchanPolling) + select { + case status := <-statuschanPolling: + statuschan <- status + if status.Finished() { + errorchan <- nil + return + } + break + case err := <-errorchanPolling: + errorchan <- err + return + } + } + }() +} + +func pollUntilChange(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { + // First we wait + <-time.NewTimer(pollInterval).C + + // Get session status + var s string + if err := transport.Get("status", &s); err != nil { + errorchan <- err + return + } + status := Status(strings.Trim(s, `"`)) + + // report if status changed + if status != initialStatus { + statuschan <- status + errorchan <- nil + return + } + + go pollUntilChange(transport, status, statuschan, errorchan) +} From 7ff0ab01557c86e620dd8f95c42922da62a01114 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 9 Jun 2020 17:41:47 +0200 Subject: [PATCH 02/77] Refactor: minor code style changes --- internal/common/common.go | 6 +++--- internal/sessiontest/legacy_test.go | 2 +- internal/sessiontest/main_test.go | 4 ++-- internal/sessiontest/session_test.go | 4 +++- irma/cmd/request.go | 8 ++++---- irmaclient/session.go | 5 +---- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/internal/common/common.go b/internal/common/common.go index 8a374f978..60cf6317c 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -27,8 +27,8 @@ var ForceHTTPS = true const ( sessionChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" sessionTokenLength = 20 - bindingChars = "0123456789" - bindingTokenLength = 4 + bindingCodeChars = "0123456789" + bindingCodeLength = 4 ) // AssertPathExists returns nil only if it has been successfully @@ -279,7 +279,7 @@ func NewSessionToken() string { } func NewBindingCode() string { - return newToken(bindingTokenLength, bindingChars) + return newToken(bindingCodeLength, bindingCodeChars) } func newToken(count int, characterSet string) string { diff --git a/internal/sessiontest/legacy_test.go b/internal/sessiontest/legacy_test.go index f09ec7f79..12b96224e 100644 --- a/internal/sessiontest/legacy_test.go +++ b/internal/sessiontest/legacy_test.go @@ -66,5 +66,5 @@ func TestWithoutBindingSupport(t *testing.T) { t.Run("TestStaticQRSession", TestStaticQRSession) t.Run("TestIssuedCredentialIsStored", TestIssuedCredentialIsStored) t.Run("TestPOSTSizeLimit", TestPOSTSizeLimit) - // TestOptionsChangeAfterConnected is not necessary since the extra check is always skipped for legacy versions. + // TestDisableBindingAfterClientConnected is not necessary since the extra check is always skipped for legacy versions. } diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index d5a8e3ff6..545353b1a 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -221,7 +221,7 @@ func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string } func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, sessiontype string, client *irmaclient.Client, - bindingCheck func(t *testing.T, transport *irma.HTTPTransport)) { + additionalCheck func(t *testing.T, transport *irma.HTTPTransport)) { if client == nil { var handler *TestClientHandler client, handler = parseStorage(t) @@ -261,7 +261,7 @@ func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, session // Binding can only be done from protocol version 2.7 if _, max := client.GetSupportedVersions(); max.Above(2, 6) { bindingCode := <-cBindingCode - bindingCheck(t, transport) + additionalCheck(t, transport) require.Equal(t, options.BindingCode, bindingCode) optionsRequest = irma.NewOptionsRequest() optionsRequest.BindingCompleted = true diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index 621ceb05b..5becba0a0 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -636,7 +636,8 @@ func TestChainedSessions(t *testing.T) { require.NoError(t, errors.New("newly issued credential not found in client")) } -func TestOptionsChangeAfterConnected(t *testing.T) { +func TestDisableBindingAfterClientConnected(t *testing.T) { + // When client is already connected, the frontend may not change the binding setting anymore. id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") request := getCombinedIssuanceRequest(id) @@ -645,6 +646,7 @@ func TestOptionsChangeAfterConnected(t *testing.T) { result := &server.SessionOptions{} err := transport.Post("options", result, request) require.NoError(t, err) + // The request may not have been accepted, so the result must differ. require.NotEqual(t, request.EnableBinding, result.BindingEnabled) } diff --git a/irma/cmd/request.go b/irma/cmd/request.go index 0136b2f6b..a097d78fa 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -3,6 +3,10 @@ package cmd import ( "encoding/json" "fmt" + "net/http" + "os" + "strconv" + "strings" "github.com/dgrijalva/jwt-go" "github.com/go-errors/errors" @@ -12,10 +16,6 @@ import ( "github.com/privacybydesign/irmago/server" "github.com/sietseringers/cobra" "github.com/sietseringers/pflag" - "net/http" - "os" - "strconv" - "strings" ) // requestCmd represents the request command diff --git a/irmaclient/session.go b/irmaclient/session.go index 95f73a35d..0010e32d9 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -33,12 +33,11 @@ type PinHandler func(proceed bool, pin string) type Handler interface { StatusUpdate(action irma.Action, status irma.Status) ClientReturnURLSet(clientReturnURL string) + BindingRequired(bindingCode string) Success(result string) Cancelled() Failure(err *irma.SessionError) - BindingRequired(bindingCode string) - KeyshareBlocked(manager irma.SchemeManagerIdentifier, duration int) KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier) KeyshareEnrollmentMissing(manager irma.SchemeManagerIdentifier) @@ -120,8 +119,6 @@ var supportedVersions = map[int][]int{ var minVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][0]} var maxVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][len(supportedVersions[2])-1]} -const clientTokenChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - func SetMaxVersion(version *irma.ProtocolVersion) { if version == nil { maxVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][len(supportedVersions[2])-1]} From 234a219c7840ae3ae1fdea5e7c1f59a94e607cb5 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 10 Jun 2020 11:00:16 +0200 Subject: [PATCH 03/77] Added more tests for binding and fixed usage variable name clientAuth --- internal/sessiontest/legacy_test.go | 1 + internal/sessiontest/main_test.go | 51 ++++++++++++++++---------- internal/sessiontest/requestor_test.go | 4 +- internal/sessiontest/session_test.go | 32 ++++++++++++++++ irmaclient/session.go | 8 ++-- 5 files changed, 71 insertions(+), 25 deletions(-) diff --git a/internal/sessiontest/legacy_test.go b/internal/sessiontest/legacy_test.go index 12b96224e..7cbb88f11 100644 --- a/internal/sessiontest/legacy_test.go +++ b/internal/sessiontest/legacy_test.go @@ -66,5 +66,6 @@ func TestWithoutBindingSupport(t *testing.T) { t.Run("TestStaticQRSession", TestStaticQRSession) t.Run("TestIssuedCredentialIsStored", TestIssuedCredentialIsStored) t.Run("TestPOSTSizeLimit", TestPOSTSizeLimit) + t.Run("TestDisableBinding", TestDisableBinding) // TestDisableBindingAfterClientConnected is not necessary since the extra check is always skipped for legacy versions. } diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 545353b1a..b00e6feea 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -233,16 +233,8 @@ func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, session defer StopRequestorServer() } - sesPkg, frontendToken := startSession(t, request, sessiontype) - optionsRequest := irma.NewOptionsRequest() - optionsRequest.EnableBinding = true - options := &server.SessionOptions{} - transport := irma.NewHTTPTransport(sesPkg.SessionPtr.URL, false) - transport.SetHeader(irma.AuthorizationHeader, frontendToken) - err := transport.Post("options", options, optionsRequest) - require.NoError(t, err) - require.Equal(t, true, options.BindingEnabled) - require.Equal(t, false, options.BindingCompleted) + sesPtr, frontendToken := startSession(t, request, sessiontype) + frontendTransport, bindingCode := enableBinding(t, sesPtr.SessionPtr, frontendToken) c := make(chan *SessionResult) cBindingCode := make(chan string) @@ -254,19 +246,26 @@ func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, session bindingCodeChan: cBindingCode, } - qrjson, err := json.Marshal(sesPkg.SessionPtr) - require.NoError(t, err) - client.NewSession(string(qrjson), h) + clientAuth, _ := client.NewQrSession(sesPtr.SessionPtr, h) // Binding can only be done from protocol version 2.7 if _, max := client.GetSupportedVersions(); max.Above(2, 6) { - bindingCode := <-cBindingCode - additionalCheck(t, transport) - require.Equal(t, options.BindingCode, bindingCode) - optionsRequest = irma.NewOptionsRequest() + enteredBindingCode := <-cBindingCode + require.Equal(t, bindingCode, enteredBindingCode) + + // Check whether access to request endpoint is denied as long as binding is not finished + transport := irma.NewHTTPTransport(sesPtr.SessionPtr.URL, false) + transport.SetHeader(irma.AuthorizationHeader, clientAuth) + err := transport.Get("request", struct{}{}) + require.Error(t, err) + require.Equal(t, 403, err.(*irma.SessionError).RemoteStatus) + + additionalCheck(t, frontendTransport) + + optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingCompleted = true - options = &server.SessionOptions{} - err = transport.Post("options", options, optionsRequest) + options := &server.SessionOptions{} + err = frontendTransport.Post("options", options, optionsRequest) require.NoError(t, err) require.Equal(t, true, options.BindingEnabled) require.Equal(t, true, options.BindingCompleted) @@ -277,6 +276,20 @@ func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, session } } +func enableBinding(t *testing.T, qr *irma.Qr, frontendToken irma.FrontendToken) (*irma.HTTPTransport, string) { + optionsRequest := irma.NewOptionsRequest() + optionsRequest.EnableBinding = true + options := &server.SessionOptions{} + transport := irma.NewHTTPTransport(qr.URL, false) + transport.SetHeader(irma.AuthorizationHeader, frontendToken) + err := transport.Post("options", options, optionsRequest) + + require.NoError(t, err) + require.Equal(t, true, options.BindingEnabled) + require.Equal(t, false, options.BindingCompleted) + return transport, options.BindingCode +} + func expectedRequestorInfo(t *testing.T, conf *irma.Configuration) *irma.RequestorInfo { if common.ForceHTTPS { return irma.NewRequestorInfo("localhost") diff --git a/internal/sessiontest/requestor_test.go b/internal/sessiontest/requestor_test.go index b41d89d1e..9b402474a 100644 --- a/internal/sessiontest/requestor_test.go +++ b/internal/sessiontest/requestor_test.go @@ -77,7 +77,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien h = &TestHandler{t, clientChan, client, requestor, wait, "", nil} } - clientToken, dismisser := client.NewQrSession(qr, h) + clientAuth, dismisser := client.NewQrSession(qr, h) clientResult := <-clientChan if opts&sessionOptionIgnoreError == 0 && clientResult != nil { require.NoError(t, clientResult.Err) @@ -98,7 +98,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien ) require.NoError(t, err) req.Header.Add("Content-Type", "application/json") - req.Header.Add(irma.AuthorizationHeader, clientToken) + req.Header.Add(irma.AuthorizationHeader, clientAuth) res, err := new(http.Client).Do(req) require.NoError(t, err) require.True(t, res.StatusCode < 300) diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index 5becba0a0..7db5ae2cb 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -636,6 +636,38 @@ func TestChainedSessions(t *testing.T) { require.NoError(t, errors.New("newly issued credential not found in client")) } +func TestDisableBinding(t *testing.T) { + var handler *TestClientHandler + client, handler := parseStorage(t) + defer test.ClearTestStorage(t, handler.storage) + + StartRequestorServer(JwtServerConfiguration) + defer StopRequestorServer() + + id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") + request := getCombinedIssuanceRequest(id) + sesPkg, frontendToken := startSession(t, request, "issue") + transport, _ := enableBinding(t, sesPkg.SessionPtr, frontendToken) + + // Disable binding again + optionsRequest := irma.NewOptionsRequest() + options := &server.SessionOptions{} + optionsRequest.EnableBinding = false + err := transport.Post("options", options, optionsRequest) + require.NoError(t, err) + require.Equal(t, optionsRequest.EnableBinding, options.BindingEnabled) + + c := make(chan *SessionResult) + h := &TestHandler{t: t, c: c, client: client, expectedServerName: expectedRequestorInfo(t, client.Configuration)} + qrjson, err := json.Marshal(sesPkg.SessionPtr) + require.NoError(t, err) + client.NewSession(string(qrjson), h) + + if result := <-c; result != nil { + require.NoError(t, result.Err) + } +} + func TestDisableBindingAfterClientConnected(t *testing.T) { // When client is already connected, the frontend may not change the binding setting anymore. id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") diff --git a/irmaclient/session.go b/irmaclient/session.go index 0010e32d9..f011fd445 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -190,7 +190,7 @@ func (client *Client) newManualSession(request irma.SessionRequest, handler Hand } // NewQrSession creates and starts a new interactive IRMA session -func (client *Client) NewQrSession(qr *irma.Qr, handler Handler) (irma.ClientToken, *session) { +func (client *Client) NewQrSession(qr *irma.Qr, handler Handler) (irma.ClientAuthorization, *session) { if qr.Type == irma.ActionRedirect { newqr := &irma.Qr{} transport := irma.NewHTTPTransport("", !client.Preferences.DeveloperMode) @@ -243,16 +243,16 @@ func (client *Client) NewQrSession(qr *irma.Qr, handler Handler) (irma.ClientTok return "", nil } - clientToken := common.NewSessionToken() + clientAuth := common.NewSessionToken() session.transport.SetHeader(irma.MinVersionHeader, min.String()) session.transport.SetHeader(irma.MaxVersionHeader, maxVersion.String()) - session.transport.SetHeader(irma.AuthorizationHeader, clientToken) + session.transport.SetHeader(irma.AuthorizationHeader, clientAuth) if !strings.HasSuffix(session.ServerURL, "/") { session.ServerURL += "/" } go session.getSessionInfo() - return clientToken, session + return clientAuth, session } // Core session methods From c754c0d9a3365a2ffb8abd5aefdde56ba160e7be Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 10 Jun 2020 12:03:04 +0200 Subject: [PATCH 04/77] Make irma session fully support binding --- irma/cmd/request.go | 6 +++++ irma/cmd/session.go | 56 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/irma/cmd/request.go b/irma/cmd/request.go index a097d78fa..07b181820 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -1,6 +1,7 @@ package cmd import ( + "bufio" "encoding/json" "fmt" "net/http" @@ -287,6 +288,11 @@ func printQr(qr *irma.Qr, noqr bool, options *server.SessionOptions) error { } if options.BindingEnabled { fmt.Println("\nBinding code:", options.BindingCode) + fmt.Print("Press Enter to confirm your device is connected; otherwise press ctrl-C: ") + if _, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil { + return err + } + } return nil } diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 3c4a50cbc..bea4b8996 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -128,6 +128,15 @@ func libraryRequest( return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } + if sessionOptions.BindingEnabled { + optionsRequest := irma.NewOptionsRequest() + optionsRequest.BindingCompleted = true + sessionOptions = irmaServer.SetOptions(backendToken, &optionsRequest) + if !sessionOptions.BindingCompleted { + return nil, errors.New("Binding could not be completed") + } + } + // Wait for session to finish and then return session result return <-resultchan, nil } @@ -141,17 +150,44 @@ func serverRequest( logger.Debug("Server URL: ", serverurl) // Start session at server - qr, sessionOptions, transport, err := postRequest(serverurl, request, name, authmethod, key, binding) + qr, frontendToken, transport, err := postRequest(serverurl, request, name, authmethod, key) if err != nil { return nil, err } + // Enable binding if necessary + frontendTransport := irma.NewHTTPTransport(qr.URL, false) + frontendTransport.SetHeader(irma.AuthorizationHeader, frontendToken) + sessionOptions := &server.SessionOptions{} + if binding { + optionsRequest := irma.NewOptionsRequest() + optionsRequest.EnableBinding = true + err = frontendTransport.Post("options", sessionOptions, optionsRequest) + if err != nil { + return nil, errors.WrapPrefix(err, "Failed to enable binding", 0) + } + } + // Print session QR logger.Debug("QR: ", prettyprint(qr)) if err := printQr(qr, noqr, sessionOptions); err != nil { return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } + if sessionOptions.BindingEnabled { + optionsRequest := irma.NewOptionsRequest() + optionsRequest.BindingCompleted = true + sessionOptions := &server.SessionOptions{} + err = frontendTransport.Post("options", sessionOptions, optionsRequest) + if err != nil { + return nil, errors.WrapPrefix(err, "Failed to complete binding", 0) + } else if !sessionOptions.BindingCompleted { + return nil, errors.New("Failed to complete binding for unknown reason") + } + } else if binding { + return nil, errors.New("Session aborted") + } + statuschan := make(chan server.Status) errorchan := make(chan error) var wg sync.WaitGroup @@ -196,8 +232,8 @@ func serverRequest( return result, nil } -func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string, binding bool) ( - *irma.Qr, *server.SessionOptions, *irma.HTTPTransport, error) { +func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string) ( + *irma.Qr, irma.FrontendToken, *irma.HTTPTransport, error) { var ( err error pkg = &server.SessionPackage{} @@ -214,26 +250,22 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth var jwtstr string jwtstr, err = signRequest(request, name, authmethod, key) if err != nil { - return nil, nil, nil, err + return nil, "", nil, err } logger.Debug("Session request JWT: ", jwtstr) err = transport.Post("session", pkg, jwtstr) default: - return nil, nil, nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)") + return nil, "", nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)") } if err != nil { - return nil, nil, transport, err + return nil, "", transport, err } backendToken := pkg.Token transport.Server += fmt.Sprintf("session/%s/", backendToken) - sessionOptions := &server.SessionOptions{} - if binding { - transport.SetHeader(irma.AuthorizationHeader, pkg.FrontendToken) - err = transport.Post(pkg.SessionPtr.URL+"/options", &irma.OptionsRequest{EnableBinding: true}, &sessionOptions) - } - return pkg.SessionPtr, sessionOptions, transport, err + + return pkg.SessionPtr, pkg.FrontendToken, transport, err } // Configuration functions From 4b316ca0281083c194e947f7e9c37b62c3f9cf28 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 10 Jun 2020 21:58:09 +0200 Subject: [PATCH 05/77] Added support for responding to binding message in irma session --- irma/cmd/request.go | 46 +++++++++++++++++--- irma/cmd/session.go | 84 ++++++++++++++++++++++++------------ server/irmaserver/helpers.go | 6 ++- server/wait_status.go | 5 ++- 4 files changed, 102 insertions(+), 39 deletions(-) diff --git a/irma/cmd/request.go b/irma/cmd/request.go index 07b181820..42e3ae376 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -271,7 +271,7 @@ func startServer(port int) { }() } -func printQr(qr *irma.Qr, noqr bool, options *server.SessionOptions) error { +func printQr(qr *irma.Qr, noqr bool) error { qrBts, err := json.Marshal(qr) if err != nil { return err @@ -286,15 +286,47 @@ func printQr(qr *irma.Qr, noqr bool, options *server.SessionOptions) error { WhiteChar: qrterminal.WHITE, }) } - if options.BindingEnabled { - fmt.Println("\nBinding code:", options.BindingCode) - fmt.Print("Press Enter to confirm your device is connected; otherwise press ctrl-C: ") - if _, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil { - return err + return nil +} + +func handleBinding(options *server.SessionOptions, statusChan chan server.Status, completeBinding func() bool) ( + server.Status, error) { + fmt.Println("\nBinding code:", options.BindingCode) + + errorChan := make(chan error) + go func() { + for { + fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") + _, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err == nil { + if completeBinding() { + fmt.Println("Binding completed.") + return + } + fmt.Println("No connected device was found, please try again.") + } else { + errorChan <- err + return + } } + }() + status := server.StatusInitialized + for { + select { + case status = <-statusChan: + if status == server.StatusInitialized || status == server.StatusConnected { + // Continue until status is other than StatusInitialized or StatusConnected + break + } + if status != server.StatusBindingCompleted && status != server.StatusTimeout { + fmt.Println("Binding was not supported by the connected device.") + } + return status, nil + case err := <-errorChan: + return status, err + } } - return nil } func printSessionResult(result *server.SessionResult) { diff --git a/irma/cmd/session.go b/irma/cmd/session.go index bea4b8996..9d9849183 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -6,6 +6,7 @@ import ( "regexp" "strconv" "sync" + "time" "github.com/go-errors/errors" irma "github.com/privacybydesign/irmago" @@ -124,16 +125,36 @@ func libraryRequest( } // Print QR code - if err := printQr(qr, noqr, sessionOptions); err != nil { + if err := printQr(qr, noqr); err != nil { return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } if sessionOptions.BindingEnabled { - optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingCompleted = true - sessionOptions = irmaServer.SetOptions(backendToken, &optionsRequest) - if !sessionOptions.BindingCompleted { - return nil, errors.New("Binding could not be completed") + // Listen for session status + statuschan := make(chan server.Status) + go func() { + var status server.Status + for { + newStatus := irmaServer.GetSessionResult(backendToken).Status + if newStatus != status { + status = newStatus + statuschan <- status + if status.Finished() { + return + } + } + time.Sleep(500 * time.Millisecond) + } + }() + + _, err = handleBinding(sessionOptions, statuschan, func() bool { + optionsRequest := irma.NewOptionsRequest() + optionsRequest.BindingCompleted = true + sessionOptions := irmaServer.SetOptions(backendToken, &optionsRequest) + return sessionOptions.BindingCompleted + }) + if err != nil { + return nil, errors.WrapPrefix(err, "Failed to handle binding", 0) } } @@ -165,29 +186,17 @@ func serverRequest( err = frontendTransport.Post("options", sessionOptions, optionsRequest) if err != nil { return nil, errors.WrapPrefix(err, "Failed to enable binding", 0) + } else if !sessionOptions.BindingEnabled { + return nil, errors.New("Binding is supported by server but could not be enabled for unknown reason") } } // Print session QR logger.Debug("QR: ", prettyprint(qr)) - if err := printQr(qr, noqr, sessionOptions); err != nil { + if err := printQr(qr, noqr); err != nil { return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } - if sessionOptions.BindingEnabled { - optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingCompleted = true - sessionOptions := &server.SessionOptions{} - err = frontendTransport.Post("options", sessionOptions, optionsRequest) - if err != nil { - return nil, errors.WrapPrefix(err, "Failed to complete binding", 0) - } else if !sessionOptions.BindingCompleted { - return nil, errors.New("Failed to complete binding for unknown reason") - } - } else if binding { - return nil, errors.New("Session aborted") - } - statuschan := make(chan server.Status) errorchan := make(chan error) var wg sync.WaitGroup @@ -204,15 +213,34 @@ func serverRequest( go func() { defer wg.Done() - // Wait until client connects - status := <-statuschan - if status != server.StatusConnected { - err = errors.Errorf("Unexpected status: %s", status) - return + var status server.Status + if binding { + status, err = handleBinding(sessionOptions, statuschan, func() bool { + optionsRequest := irma.NewOptionsRequest() + optionsRequest.BindingCompleted = true + sessionOptions := &server.SessionOptions{} + err = frontendTransport.Post("options", sessionOptions, optionsRequest) + if err != nil { + err = errors.WrapPrefix(err, "Failed to complete binding", 0) + } + return sessionOptions.BindingCompleted + }) + if err != nil { + err = errors.WrapPrefix(err, "Failed to handle binding", 0) + return + } + } else { + // Wait until client connects if binding is disabled + status := <-statuschan + if status != server.StatusConnected { + err = errors.Errorf("Unexpected status: %s", status) + return + } + + // Wait until client finishes + status = <-statuschan } - // Wait until client finishes - status = <-statuschan if status != server.StatusCancelled && status != server.StatusDone { err = errors.Errorf("Unexpected status: %s", status) return diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index b18d0dce6..85d9eb344 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -56,7 +56,7 @@ func (session *session) onUpdate() { // Checks whether requested options are valid in the current session context. func (session *session) updateOptions(request *irma.OptionsRequest) *server.SessionOptions { - if session.status == server.StatusInitialized { + if session.status == server.StatusInitialized && !request.BindingCompleted { session.options.BindingEnabled = request.EnableBinding if request.EnableBinding { session.options.BindingCode = common.NewBindingCode() @@ -66,7 +66,9 @@ func (session *session) updateOptions(request *irma.OptionsRequest) *server.Sess } else if session.status == server.StatusConnected { if session.options.BindingEnabled && !session.options.BindingCompleted && request.BindingCompleted { session.options.BindingCompleted = true - session.status = server.StatusBindingCompleted + if session.version.Above(2, 6) { + session.status = server.StatusBindingCompleted + } } } return &session.options diff --git a/server/wait_status.go b/server/wait_status.go index 10297e08c..5b3eacfe8 100644 --- a/server/wait_status.go +++ b/server/wait_status.go @@ -48,12 +48,13 @@ func subscribeSSE(transport *irma.HTTPTransport, statuschan chan Status, errorch // poll recursively polls the session status until a final status is received. func poll(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { go func() { + status := initialStatus for { statuschanPolling := make(chan Status) errorchanPolling := make(chan error) - go pollUntilChange(transport, initialStatus, statuschanPolling, errorchanPolling) + go pollUntilChange(transport, status, statuschanPolling, errorchanPolling) select { - case status := <-statuschanPolling: + case status = <-statuschanPolling: statuschan <- status if status.Finished() { errorchan <- nil From 44d39c4308c86c35a9ce5bffd3e8e74a68033ee0 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 10 Jun 2020 23:05:06 +0200 Subject: [PATCH 06/77] Introduce binding as a separate server status --- irma/cmd/request.go | 48 +++++++++++++++++------------------- irma/cmd/session.go | 6 ++--- irmaclient/session.go | 4 +-- server/api.go | 12 ++++----- server/irmaserver/handle.go | 15 +++++++---- server/irmaserver/helpers.go | 6 ++--- 6 files changed, 46 insertions(+), 45 deletions(-) diff --git a/irma/cmd/request.go b/irma/cmd/request.go index 42e3ae376..ba20123be 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -291,36 +291,34 @@ func printQr(qr *irma.Qr, noqr bool) error { func handleBinding(options *server.SessionOptions, statusChan chan server.Status, completeBinding func() bool) ( server.Status, error) { - fmt.Println("\nBinding code:", options.BindingCode) - errorChan := make(chan error) - go func() { - for { - fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") - _, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err == nil { - if completeBinding() { - fmt.Println("Binding completed.") - return - } - fmt.Println("No connected device was found, please try again.") - } else { - errorChan <- err - return - } - } - }() - status := server.StatusInitialized + bindingStarted := false for { select { case status = <-statusChan: - if status == server.StatusInitialized || status == server.StatusConnected { - // Continue until status is other than StatusInitialized or StatusConnected - break - } - if status != server.StatusBindingCompleted && status != server.StatusTimeout { - fmt.Println("Binding was not supported by the connected device.") + if status == server.StatusInitialized { + continue + } else if status == server.StatusBinding { + bindingStarted = true + go func() { + fmt.Println("\nBinding code:", options.BindingCode) + fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") + _, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err == nil { + if completeBinding() { + fmt.Println("Binding completed.") + } else { + errorChan <- errors.New("Failed to complete binding") + } + } else { + errorChan <- err + return + } + }() + continue + } else if status == server.StatusConnected && !bindingStarted { + fmt.Println("Binding is not supported by the connected device.") } return status, nil case err := <-errorChan: diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 9d9849183..2decfa937 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -236,11 +236,11 @@ func serverRequest( err = errors.Errorf("Unexpected status: %s", status) return } - - // Wait until client finishes - status = <-statuschan } + // Wait until client finishes + status = <-statuschan + if status != server.StatusCancelled && status != server.StatusDone { err = errors.Errorf("Unexpected status: %s", status) return diff --git a/irmaclient/session.go b/irmaclient/session.go index f011fd445..583ba2a52 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -292,10 +292,10 @@ func (session *session) handleBinding(bindingCode string) error { statuschan := make(chan server.Status) errorchan := make(chan error) - go server.WaitStatusChanged(session.transport, server.StatusInitialized, statuschan, errorchan) + go server.WaitStatusChanged(session.transport, server.StatusBinding, statuschan, errorchan) select { case status := <-statuschan: - if status == server.StatusBindingCompleted { + if status == server.StatusConnected { return session.transport.Get("request", session.request) } else { return errors.New("Server finished session without completing binding") diff --git a/server/api.go b/server/api.go index dccf0cb79..13a71dcb1 100644 --- a/server/api.go +++ b/server/api.go @@ -99,12 +99,12 @@ const ( ) const ( - StatusInitialized Status = "INITIALIZED" // The session has been started and is waiting for the client - StatusBindingCompleted Status = "BINDING_COMPLETED" // The client has bound and can retrieve the request - StatusConnected Status = "CONNECTED" // The client has retrieved the session request, we wait for its response - StatusCancelled Status = "CANCELLED" // The session is cancelled, possibly due to an error - StatusDone Status = "DONE" // The session has completed successfully - StatusTimeout Status = "TIMEOUT" // Session timed out + StatusInitialized Status = "INITIALIZED" // The session has been started and is waiting for the client + StatusBinding Status = "BINDING" // The client is binding, waiting for the frontend to accept + StatusConnected Status = "CONNECTED" // The client has retrieved the session request, we wait for its response + StatusCancelled Status = "CANCELLED" // The session is cancelled, possibly due to an error + StatusDone Status = "DONE" // The session has completed successfully + StatusTimeout Status = "TIMEOUT" // Session timed out ) // Remove this when dropping support for legacy pre-condiscon session requests diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 4d416b61c..eb1168000 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -35,7 +35,8 @@ func (session *session) handleDelete() { func (session *session) handleGetRequest(min, max *irma.ProtocolVersion) ( *server.SessionInfo, *irma.SessionRequest, *irma.RemoteError) { - if session.status != server.StatusInitialized && session.status != server.StatusBindingCompleted { + // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. + if session.status != server.StatusInitialized { return nil, nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session already started") } @@ -63,7 +64,11 @@ func (session *session) handleGetRequest(min, max *irma.ProtocolVersion) ( logger.WithFields(logrus.Fields{"version": session.version.String()}).Debugf("Protocol version negotiated") session.request.Base().ProtocolVersion = session.version - session.setStatus(server.StatusConnected) + if session.options.BindingEnabled && session.version.Above(2, 6) { + session.setStatus(server.StatusBinding) + } else { + session.setStatus(server.StatusConnected) + } if session.version.Below(2, 5) { logger.Info("Returning legacy session format") @@ -97,7 +102,7 @@ func (session *session) handlePostOptions(optionsRequest *irma.OptionsRequest, t func (session *session) handlePostSignature(signature *irma.SignedMessage) (*irma.ServerSessionResponse, *irma.RemoteError) { // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. - if session.version == nil || session.version.Below(2, 7) && session.status != server.StatusConnected { + if session.status != server.StatusConnected { return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") } session.markAlive() @@ -129,7 +134,7 @@ func (session *session) handlePostSignature(signature *irma.SignedMessage) (*irm func (session *session) handlePostDisclosure(disclosure *irma.Disclosure) (*irma.ServerSessionResponse, *irma.RemoteError) { // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. - if session.version == nil || session.version.Below(2, 7) && session.status != server.StatusConnected { + if session.status != server.StatusConnected { return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") } session.markAlive() @@ -161,7 +166,7 @@ func (session *session) handlePostDisclosure(disclosure *irma.Disclosure) (*irma func (session *session) handlePostCommitments(commitments *irma.IssueCommitmentMessage) (*irma.ServerSessionResponse, *irma.RemoteError) { // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. - if session.version == nil || session.version.Below(2, 7) && session.status != server.StatusConnected { + if session.status != server.StatusConnected { return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") } session.markAlive() diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 85d9eb344..a07f7a149 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -63,12 +63,10 @@ func (session *session) updateOptions(request *irma.OptionsRequest) *server.Sess } else { session.options.BindingCode = "" } - } else if session.status == server.StatusConnected { + } else if session.status == server.StatusBinding { if session.options.BindingEnabled && !session.options.BindingCompleted && request.BindingCompleted { session.options.BindingCompleted = true - if session.version.Above(2, 6) { - session.status = server.StatusBindingCompleted - } + session.setStatus(server.StatusConnected) } } return &session.options From f3a401d24bc12ed1b1456f10f2cae71dedaa9a84 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 10 Jun 2020 23:44:14 +0200 Subject: [PATCH 07/77] Minor fixes for shoulder surfing feature --- internal/common/common.go | 6 +++--- irma/cmd/session.go | 5 ++--- irmaclient/session.go | 13 ++++++++++--- server/api.go | 18 +++++++++--------- server/irmaserver/handle.go | 4 ++-- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/internal/common/common.go b/internal/common/common.go index 60cf6317c..3bd8454d0 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -275,14 +275,14 @@ type SSECtx struct { } func NewSessionToken() string { - return newToken(sessionTokenLength, sessionChars) + return newRandomString(sessionTokenLength, sessionChars) } func NewBindingCode() string { - return newToken(bindingCodeLength, bindingCodeChars) + return newRandomString(bindingCodeLength, bindingCodeChars) } -func newToken(count int, characterSet string) string { +func newRandomString(count int, characterSet string) string { r := make([]byte, count) _, err := rand.Read(r) if err != nil { diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 2decfa937..2ec148de8 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -129,7 +129,7 @@ func libraryRequest( return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } - if sessionOptions.BindingEnabled { + if binding { // Listen for session status statuschan := make(chan server.Status) go func() { @@ -240,7 +240,6 @@ func serverRequest( // Wait until client finishes status = <-statuschan - if status != server.StatusCancelled && status != server.StatusDone { err = errors.Errorf("Unexpected status: %s", status) return @@ -346,7 +345,7 @@ func init() { flags.StringP("url", "u", defaulturl, "external URL to which IRMA app connects (when not using --server), \":port\" being replaced by --port value") flags.IntP("port", "p", 48680, "port to listen at (when not using --server)") flags.Bool("noqr", false, "Print JSON instead of draw QR") - flags.Bool("binding", false, "Enable extra binding step between server and IRMA app") + flags.Bool("binding", false, "Enable explicit binding between server and IRMA app") flags.StringP("request", "r", "", "JSON session request") flags.StringP("privkeys", "k", "", "path to private keys") flags.Bool("disable-schemes-update", false, "disable scheme updates") diff --git a/irmaclient/session.go b/irmaclient/session.go index 583ba2a52..301d5b919 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -243,10 +243,16 @@ func (client *Client) NewQrSession(qr *irma.Qr, handler Handler) (irma.ClientAut return "", nil } - clientAuth := common.NewSessionToken() session.transport.SetHeader(irma.MinVersionHeader, min.String()) session.transport.SetHeader(irma.MaxVersionHeader, maxVersion.String()) - session.transport.SetHeader(irma.AuthorizationHeader, clientAuth) + + // From protocol version 2.7 also a authorization header must be included. + clientAuth := "" + if maxVersion.Above(2, 6) { + clientAuth = common.NewSessionToken() + session.transport.SetHeader(irma.AuthorizationHeader, clientAuth) + } + if !strings.HasSuffix(session.ServerURL, "/") { session.ServerURL += "/" } @@ -268,13 +274,14 @@ func (session *session) getSessionInfo() { info := &server.SessionInfo{ Request: session.request, // As request is an interface it need to be initialized with a specific instance. } + // UnmarshalJSON of SessionInfo takes into account legacy protocols, so we do not have to check that here. err := session.transport.Get("", info) if err != nil { session.fail(err.(*irma.SessionError)) return } - // Check whether binding is needed, and if so, wait for it to be completed + // Check whether binding is needed, and if so, wait for it to be completed. if info.Options.BindingEnabled { err = session.handleBinding(info.Options.BindingCode) } diff --git a/server/api.go b/server/api.go index 13a71dcb1..142a4d1de 100644 --- a/server/api.go +++ b/server/api.go @@ -86,6 +86,15 @@ type LegacySessionResult struct { Err *irma.RemoteError `json:"error,omitempty"` } +const ( + StatusInitialized Status = "INITIALIZED" // The session has been started and is waiting for the client + StatusBinding Status = "BINDING" // The client is binding, waiting for the frontend to accept + StatusConnected Status = "CONNECTED" // The client has retrieved the session request, we wait for its response + StatusCancelled Status = "CANCELLED" // The session is cancelled, possibly due to an error + StatusDone Status = "DONE" // The session has completed successfully + StatusTimeout Status = "TIMEOUT" // Session timed out +) + const ( ComponentRevocation = "revocation" ComponentSession = "session" @@ -98,15 +107,6 @@ const ( WriteTimeout = 2 * ReadTimeout ) -const ( - StatusInitialized Status = "INITIALIZED" // The session has been started and is waiting for the client - StatusBinding Status = "BINDING" // The client is binding, waiting for the frontend to accept - StatusConnected Status = "CONNECTED" // The client has retrieved the session request, we wait for its response - StatusCancelled Status = "CANCELLED" // The session is cancelled, possibly due to an error - StatusDone Status = "DONE" // The session has completed successfully - StatusTimeout Status = "TIMEOUT" // Session timed out -) - // Remove this when dropping support for legacy pre-condiscon session requests func (r *SessionResult) Legacy() *LegacySessionResult { var disclosed []*irma.DisclosedAttribute diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index eb1168000..4b89244e4 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -33,7 +33,7 @@ func (session *session) handleDelete() { session.setStatus(server.StatusCancelled) } -func (session *session) handleGetRequest(min, max *irma.ProtocolVersion) ( +func (session *session) handleGetInfo(min, max *irma.ProtocolVersion) ( *server.SessionInfo, *irma.SessionRequest, *irma.RemoteError) { // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. if session.status != server.StatusInitialized { @@ -432,7 +432,7 @@ func (s *Server) handleSessionGet(w http.ResponseWriter, r *http.Request) { } session := r.Context().Value("session").(*session) // When session binding is supported by all clients, the legacy support can be removed - res, legacyRes, err := session.handleGetRequest(&min, &max) + res, legacyRes, err := session.handleGetInfo(&min, &max) if legacyRes != nil { server.WriteResponse(w, legacyRes, err) } else { From e9561e6ab781f91f3cc1980d2de1ba95fa40926a Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 8 Jul 2020 17:24:17 +0200 Subject: [PATCH 08/77] Changed outline of frontend endpoints for binding --- internal/sessiontest/main_test.go | 10 ++---- internal/sessiontest/session_test.go | 8 ++--- irma/cmd/request.go | 7 ++-- irma/cmd/session.go | 24 ++++++------- requests.go | 5 ++- server/api.go | 9 ++--- server/irmaserver/api.go | 29 ++++++++++++---- server/irmaserver/handle.go | 51 +++++++++++----------------- server/irmaserver/helpers.go | 46 +++++++++++++++++++------ server/irmaserver/sessions.go | 2 +- 10 files changed, 105 insertions(+), 86 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index b00e6feea..4674bace4 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -262,13 +262,8 @@ func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, session additionalCheck(t, frontendTransport) - optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingCompleted = true - options := &server.SessionOptions{} - err = frontendTransport.Post("options", options, optionsRequest) + err = frontendTransport.Post("frontend/bindingcompleted", nil, nil) require.NoError(t, err) - require.Equal(t, true, options.BindingEnabled) - require.Equal(t, true, options.BindingCompleted) } if result := <-c; result != nil { @@ -282,11 +277,10 @@ func enableBinding(t *testing.T, qr *irma.Qr, frontendToken irma.FrontendToken) options := &server.SessionOptions{} transport := irma.NewHTTPTransport(qr.URL, false) transport.SetHeader(irma.AuthorizationHeader, frontendToken) - err := transport.Post("options", options, optionsRequest) + err := transport.Post("frontend/options", options, optionsRequest) require.NoError(t, err) require.Equal(t, true, options.BindingEnabled) - require.Equal(t, false, options.BindingCompleted) return transport, options.BindingCode } diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index 7db5ae2cb..008118b2b 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -653,7 +653,7 @@ func TestDisableBinding(t *testing.T) { optionsRequest := irma.NewOptionsRequest() options := &server.SessionOptions{} optionsRequest.EnableBinding = false - err := transport.Post("options", options, optionsRequest) + err := transport.Post("frontend/options", options, optionsRequest) require.NoError(t, err) require.Equal(t, optionsRequest.EnableBinding, options.BindingEnabled) @@ -676,10 +676,8 @@ func TestDisableBindingAfterClientConnected(t *testing.T) { check := func(t *testing.T, transport *irma.HTTPTransport) { request := irma.NewOptionsRequest() result := &server.SessionOptions{} - err := transport.Post("options", result, request) - require.NoError(t, err) - // The request may not have been accepted, so the result must differ. - require.NotEqual(t, request.EnableBinding, result.BindingEnabled) + err := transport.Post("frontend/options", result, request) + require.Error(t, err) } sessionHelperWithBinding(t, request, "issue", nil, check) diff --git a/irma/cmd/request.go b/irma/cmd/request.go index ba20123be..e759a75cc 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -289,7 +289,7 @@ func printQr(qr *irma.Qr, noqr bool) error { return nil } -func handleBinding(options *server.SessionOptions, statusChan chan server.Status, completeBinding func() bool) ( +func handleBinding(options *server.SessionOptions, statusChan chan server.Status, completeBinding func() error) ( server.Status, error) { errorChan := make(chan error) status := server.StatusInitialized @@ -306,10 +306,11 @@ func handleBinding(options *server.SessionOptions, statusChan chan server.Status fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") _, err := bufio.NewReader(os.Stdin).ReadString('\n') if err == nil { - if completeBinding() { + err = completeBinding() + if err == nil { fmt.Println("Binding completed.") } else { - errorChan <- errors.New("Failed to complete binding") + errorChan <- err } } else { errorChan <- err diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 2ec148de8..0992e2c69 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -121,7 +121,9 @@ func libraryRequest( if binding { optionsRequest := irma.NewOptionsRequest() optionsRequest.EnableBinding = true - sessionOptions = irmaServer.SetOptions(backendToken, &optionsRequest) + if sessionOptions, err = irmaServer.SetFrontendOptions(backendToken, &optionsRequest); err != nil { + return nil, errors.WrapPrefix(err, "IRMA enable binding failed", 0) + } } // Print QR code @@ -147,11 +149,8 @@ func libraryRequest( } }() - _, err = handleBinding(sessionOptions, statuschan, func() bool { - optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingCompleted = true - sessionOptions := irmaServer.SetOptions(backendToken, &optionsRequest) - return sessionOptions.BindingCompleted + _, err = handleBinding(sessionOptions, statuschan, func() error { + return irmaServer.BindingCompleted(backendToken) }) if err != nil { return nil, errors.WrapPrefix(err, "Failed to handle binding", 0) @@ -183,7 +182,7 @@ func serverRequest( if binding { optionsRequest := irma.NewOptionsRequest() optionsRequest.EnableBinding = true - err = frontendTransport.Post("options", sessionOptions, optionsRequest) + err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) if err != nil { return nil, errors.WrapPrefix(err, "Failed to enable binding", 0) } else if !sessionOptions.BindingEnabled { @@ -215,15 +214,12 @@ func serverRequest( var status server.Status if binding { - status, err = handleBinding(sessionOptions, statuschan, func() bool { - optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingCompleted = true - sessionOptions := &server.SessionOptions{} - err = frontendTransport.Post("options", sessionOptions, optionsRequest) + status, err = handleBinding(sessionOptions, statuschan, func() error { + err = frontendTransport.Post("frontend/bindingcompleted", nil, nil) if err != nil { - err = errors.WrapPrefix(err, "Failed to complete binding", 0) + return errors.WrapPrefix(err, "Failed to complete binding", 0) } - return sessionOptions.BindingCompleted + return nil }) if err != nil { err = errors.WrapPrefix(err, "Failed to handle binding", 0) diff --git a/requests.go b/requests.go index bf9b04ad6..2c32ee1d2 100644 --- a/requests.go +++ b/requests.go @@ -213,9 +213,8 @@ type AttributeRequest struct { // An OptionsRequest asks for a options change of a particular session. type OptionsRequest struct { - LDContext string `json:"@context,omitempty"` - EnableBinding bool `json:"enableBinding,omitempty"` - BindingCompleted bool `json:"bindingCompleted,omitempty"` + LDContext string `json:"@context,omitempty"` + EnableBinding bool `json:"enableBinding,omitempty"` } type RevocationRequest struct { diff --git a/server/api.go b/server/api.go index 142a4d1de..066c87d6e 100644 --- a/server/api.go +++ b/server/api.go @@ -26,6 +26,7 @@ import ( var Logger *logrus.Logger = logrus.StandardLogger() const LDContextSessionInfo = "https://irma.app/ld/request/info/v1" +const LDContextSessionOptions = "https://irma.app/ld/options/v1" type SessionPackage struct { SessionPtr *irma.Qr `json:"sessionPtr"` @@ -59,9 +60,9 @@ type SessionResult struct { } type SessionOptions struct { - BindingEnabled bool `json:"bindingEnabled"` - BindingCode string `json:"bindingCode,omitempty"` - BindingCompleted bool `json:"bindingCompleted,omitempty"` + LDContext string `json:"@context,omitempty"` + BindingEnabled bool `json:"bindingEnabled"` + BindingCode string `json:"bindingCode,omitempty"` } // SessionHandler is a function that can handle a session result @@ -566,6 +567,6 @@ func (info *SessionInfo) UnmarshalJSON(data []byte) error { } info.LDContext = LDContextSessionInfo info.ProtocolVersion = info.Request.Base().ProtocolVersion - info.Options = &SessionOptions{} + info.Options = &SessionOptions{LDContext: LDContextSessionOptions} return nil } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 201bbdd02..db0c5dea8 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -114,7 +114,11 @@ func (s *Server) HandlerFunc() http.HandlerFunc { r.Delete("/", s.handleSessionDelete) r.Get("/status", s.handleSessionStatus) r.Get("/statusevents", s.handleSessionStatusEvents) - r.Post("/options", s.handleSessionOptionsPost) + r.Route("/frontend", func(r chi.Router) { + r.Use(s.frontendMiddleware) + r.Post("/options", s.handleFrontendOptionsPost) + r.Post("/bindingcompleted", s.handleFrontendBindingCompleted) + }) r.Group(func(r chi.Router) { r.Use(s.authenticationMiddleware) r.Use(s.cacheMiddleware) @@ -246,15 +250,26 @@ func (s *Server) CancelSession(backendToken irma.BackendToken) error { return nil } -// Requests a change of the session options at the server. -// Returns the updated options struct. Invalid requested options are ignored. +// Requests a change of the session frontend options at the server. +// Returns the updated options struct. Frontend options can only be +// changed the irma client has not connected yet. Otherwise an error is returned. // Options that are not specified in the request, keep their old value. -func SetOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) *server.SessionOptions { - return s.SetOptions(backendToken, request) +func SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*server.SessionOptions, error) { + return s.SetFrontendOptions(backendToken, request) +} +func (s *Server) SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*server.SessionOptions, error) { + session := s.sessions.get(backendToken) + return session.updateFrontendOptions(request) +} + +// Complete binding between the irma client and the frontend. Returns +// an error when no client is actually connected. +func BindingCompleted(backendToken irma.BackendToken) error { + return s.BindingCompleted(backendToken) } -func (s *Server) SetOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) *server.SessionOptions { +func (s *Server) BindingCompleted(backendToken irma.BackendToken) error { session := s.sessions.get(backendToken) - return session.updateOptions(request) + return session.bindingCompleted() } // Revoke revokes the earlier issued credential specified by key. (Can only be used if this server diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 4b89244e4..b5fa2d976 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -77,10 +77,7 @@ func (session *session) handleGetInfo(min, max *irma.ProtocolVersion) ( } if session.version.Below(2, 7) { - // These versions do not support binding, so force binding completion to prevent problems. - if session.options.BindingEnabled { - session.options.BindingCompleted = true - } + // These versions do not support binding, so the request is always returned immediately. request, rerr := session.getRequest() return nil, &request, rerr } @@ -92,19 +89,7 @@ func (session *session) handleGetStatus() (server.Status, *irma.RemoteError) { return session.status, nil } -func (session *session) handlePostOptions(optionsRequest *irma.OptionsRequest, token irma.FrontendToken) ( - *server.SessionOptions, *irma.RemoteError) { - if token != session.frontendToken { - return nil, server.RemoteError(server.ErrorUnauthorized, "") - } - return session.updateOptions(optionsRequest), nil -} - func (session *session) handlePostSignature(signature *irma.SignedMessage) (*irma.ServerSessionResponse, *irma.RemoteError) { - // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. - if session.status != server.StatusConnected { - return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") - } session.markAlive() var err error @@ -133,10 +118,6 @@ func (session *session) handlePostSignature(signature *irma.SignedMessage) (*irm } func (session *session) handlePostDisclosure(disclosure *irma.Disclosure) (*irma.ServerSessionResponse, *irma.RemoteError) { - // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. - if session.status != server.StatusConnected { - return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") - } session.markAlive() var err error @@ -165,12 +146,7 @@ func (session *session) handlePostDisclosure(disclosure *irma.Disclosure) (*irma } func (session *session) handlePostCommitments(commitments *irma.IssueCommitmentMessage) (*irma.ServerSessionResponse, *irma.RemoteError) { - // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. - if session.status != server.StatusConnected { - return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session not yet started or already finished") - } session.markAlive() - request := session.request.(*irma.IssuanceRequest) discloseCount := len(commitments.Proofs) - len(request.Credentials) @@ -442,15 +418,15 @@ func (s *Server) handleSessionGet(w http.ResponseWriter, r *http.Request) { func (s *Server) handleSessionGetRequest(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - if session.options.BindingEnabled && !session.options.BindingCompleted { - server.WriteError(w, server.ErrorUnauthorized, "Binding required") + if session.version.Below(2, 7) { + server.WriteError(w, server.ErrorUnexpectedRequest, "Endpoint is not support in used protocol version") return } request, err := session.getRequest() server.WriteResponse(w, request, err) } -func (s *Server) handleSessionOptionsPost(w http.ResponseWriter, r *http.Request) { +func (s *Server) handleFrontendOptionsPost(w http.ResponseWriter, r *http.Request) { optionsRequest := &irma.OptionsRequest{} bts, err := ioutil.ReadAll(r.Body) if err != nil { @@ -462,10 +438,23 @@ func (s *Server) handleSessionOptionsPost(w http.ResponseWriter, r *http.Request server.WriteError(w, server.ErrorMalformedInput, err.Error()) return } - frontendToken := r.Header.Get(irma.AuthorizationHeader) + session := r.Context().Value("session").(*session) - res, rerr := session.handlePostOptions(optionsRequest, frontendToken) - server.WriteResponse(w, res, rerr) + res, err := session.updateFrontendOptions(optionsRequest) + if err != nil { + server.WriteError(w, server.ErrorUnexpectedRequest, err.Error()) + return + } + server.WriteResponse(w, res, nil) +} + +func (s *Server) handleFrontendBindingCompleted(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session) + if err := session.bindingCompleted(); err != nil { + server.WriteError(w, server.ErrorUnexpectedRequest, err.Error()) + return + } + w.WriteHeader(http.StatusNoContent) } func (s *Server) handleStaticMessage(w http.ResponseWriter, r *http.Request) { diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index a07f7a149..83bed7710 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -55,21 +55,26 @@ func (session *session) onUpdate() { } // Checks whether requested options are valid in the current session context. -func (session *session) updateOptions(request *irma.OptionsRequest) *server.SessionOptions { - if session.status == server.StatusInitialized && !request.BindingCompleted { +func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*server.SessionOptions, error) { + if session.status == server.StatusInitialized { session.options.BindingEnabled = request.EnableBinding if request.EnableBinding { session.options.BindingCode = common.NewBindingCode() } else { session.options.BindingCode = "" } - } else if session.status == server.StatusBinding { - if session.options.BindingEnabled && !session.options.BindingCompleted && request.BindingCompleted { - session.options.BindingCompleted = true - session.setStatus(server.StatusConnected) - } + return &session.options, nil + } + return nil, errors.New("Frontend options cannot be updated when client is already connected") +} + +// Complete the binding process of frontend and irma client +func (session *session) bindingCompleted() error { + if session.status == server.StatusBinding { + session.setStatus(server.StatusConnected) + return nil } - return &session.options + return errors.New("Binding was not enabled") } func (session *session) fail(err server.Error, message string) *irma.RemoteError { @@ -293,7 +298,7 @@ func (session *session) getInfo() (*server.SessionInfo, *irma.RemoteError) { Options: &session.options, } - if !session.options.BindingEnabled || session.options.BindingCompleted { + if !session.options.BindingEnabled { request, rerr := session.getRequest() if rerr != nil { return nil, rerr @@ -428,6 +433,19 @@ func errorWriter(err *irma.RemoteError, writer func(w http.ResponseWriter, objec } } +func (s *Server) frontendMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session) + frontendToken := r.Header.Get(irma.AuthorizationHeader) + + if frontendToken != session.frontendToken { + server.WriteError(w, server.ErrorUnauthorized, "") + return + } + next.ServeHTTP(w, r) + }) +} + 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) @@ -517,6 +535,8 @@ func (s *Server) authenticationMiddleware(next http.Handler) http.Handler { return } + // If the client connects for the first time, we grant access and make sure + // only that exact client can connect to this session in future requests. if session.clientAuth == "" { session.clientAuth = authorization } else if session.clientAuth != authorization { @@ -532,11 +552,17 @@ func (s *Server) bindingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - if session.options.BindingEnabled && !session.options.BindingCompleted { + if session.options.BindingEnabled && session.status == server.StatusBinding { server.WriteError(w, server.ErrorBindingRequired, "") return } + // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. + if session.status != server.StatusConnected { + server.WriteError(w, server.ErrorUnexpectedRequest, "Session not yet started or already finished") + return + } + next.ServeHTTP(w, r) }) } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index a619bda56..72fdb6826 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -178,7 +178,7 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) * action: action, rrequest: request, request: request.SessionRequest(), - options: server.SessionOptions{}, + options: server.SessionOptions{LDContext: server.LDContextSessionOptions}, lastActive: time.Now(), backendToken: backendToken, clientToken: clientToken, From 97a06a4ba857be651255bab509ee531af7530ad9 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 9 Jul 2020 09:57:47 +0200 Subject: [PATCH 09/77] Added binding method to option request --- internal/sessiontest/main_test.go | 2 +- internal/sessiontest/session_test.go | 4 ++-- irma/cmd/session.go | 4 ++-- requests.go | 12 +++++++++--- server/irmaserver/helpers.go | 10 ++++++++-- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 4674bace4..cc2e897d4 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -273,7 +273,7 @@ func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, session func enableBinding(t *testing.T, qr *irma.Qr, frontendToken irma.FrontendToken) (*irma.HTTPTransport, string) { optionsRequest := irma.NewOptionsRequest() - optionsRequest.EnableBinding = true + optionsRequest.BindingMethod = irma.BindingMethodPin options := &server.SessionOptions{} transport := irma.NewHTTPTransport(qr.URL, false) transport.SetHeader(irma.AuthorizationHeader, frontendToken) diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index 008118b2b..e36e11df0 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -652,10 +652,10 @@ func TestDisableBinding(t *testing.T) { // Disable binding again optionsRequest := irma.NewOptionsRequest() options := &server.SessionOptions{} - optionsRequest.EnableBinding = false + optionsRequest.BindingMethod = irma.BindingMethodNone err := transport.Post("frontend/options", options, optionsRequest) require.NoError(t, err) - require.Equal(t, optionsRequest.EnableBinding, options.BindingEnabled) + require.Equal(t, false, options.BindingEnabled) c := make(chan *SessionResult) h := &TestHandler{t: t, c: c, client: client, expectedServerName: expectedRequestorInfo(t, client.Configuration)} diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 0992e2c69..52b34d256 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -120,7 +120,7 @@ func libraryRequest( sessionOptions := &server.SessionOptions{} if binding { optionsRequest := irma.NewOptionsRequest() - optionsRequest.EnableBinding = true + optionsRequest.BindingMethod = irma.BindingMethodPin if sessionOptions, err = irmaServer.SetFrontendOptions(backendToken, &optionsRequest); err != nil { return nil, errors.WrapPrefix(err, "IRMA enable binding failed", 0) } @@ -181,7 +181,7 @@ func serverRequest( sessionOptions := &server.SessionOptions{} if binding { optionsRequest := irma.NewOptionsRequest() - optionsRequest.EnableBinding = true + optionsRequest.BindingMethod = irma.BindingMethodPin err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) if err != nil { return nil, errors.WrapPrefix(err, "Failed to enable binding", 0) diff --git a/requests.go b/requests.go index 2c32ee1d2..1fad6fb5b 100644 --- a/requests.go +++ b/requests.go @@ -211,10 +211,15 @@ type AttributeRequest struct { NotNull bool `json:"notNull,omitempty"` } +type BindingMethod string + +const BindingMethodNone = "none" +const BindingMethodPin = "pin" + // An OptionsRequest asks for a options change of a particular session. type OptionsRequest struct { - LDContext string `json:"@context,omitempty"` - EnableBinding bool `json:"enableBinding,omitempty"` + LDContext string `json:"@context,omitempty"` + BindingMethod BindingMethod `json:"bindingMethod"` } type RevocationRequest struct { @@ -1103,6 +1108,7 @@ func NewAttributeRequest(attr string) AttributeRequest { // NewOptionsRequest returns a new options request initialized with default values for each option func NewOptionsRequest() OptionsRequest { return OptionsRequest{ - LDContext: LDContextOptionsRequest, + LDContext: LDContextOptionsRequest, + BindingMethod: BindingMethodNone, } } diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 83bed7710..8ad05acca 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -57,8 +57,14 @@ func (session *session) onUpdate() { // Checks whether requested options are valid in the current session context. func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*server.SessionOptions, error) { if session.status == server.StatusInitialized { - session.options.BindingEnabled = request.EnableBinding - if request.EnableBinding { + if request.BindingMethod == irma.BindingMethodNone { + session.options.BindingEnabled = false + } else if request.BindingMethod == irma.BindingMethodPin { + session.options.BindingEnabled = true + } else { + return nil, errors.New("Binding method unknown") + } + if session.options.BindingEnabled { session.options.BindingCode = common.NewBindingCode() } else { session.options.BindingCode = "" From ccea19e542fdac618d5e923fff4cd042be6f4029 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 9 Jul 2020 18:11:35 +0200 Subject: [PATCH 10/77] Added binding method to session options struct --- internal/sessiontest/main_test.go | 1 - internal/sessiontest/session_test.go | 1 - irma/cmd/request.go | 19 ++++++++++++------- irma/cmd/session.go | 2 -- irmaclient/session.go | 2 +- requests.go | 6 ++++-- server/api.go | 11 +++++++---- server/irmaserver/handle.go | 2 +- server/irmaserver/helpers.go | 16 +++++++--------- server/irmaserver/sessions.go | 11 +++++++---- 10 files changed, 39 insertions(+), 32 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index cc2e897d4..2ebc05931 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -280,7 +280,6 @@ func enableBinding(t *testing.T, qr *irma.Qr, frontendToken irma.FrontendToken) err := transport.Post("frontend/options", options, optionsRequest) require.NoError(t, err) - require.Equal(t, true, options.BindingEnabled) return transport, options.BindingCode } diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index e36e11df0..ad743f5af 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -655,7 +655,6 @@ func TestDisableBinding(t *testing.T) { optionsRequest.BindingMethod = irma.BindingMethodNone err := transport.Post("frontend/options", options, optionsRequest) require.NoError(t, err) - require.Equal(t, false, options.BindingEnabled) c := make(chan *SessionResult) h := &TestHandler{t: t, c: c, client: client, expectedServerName: expectedRequestorInfo(t, client.Configuration)} diff --git a/irma/cmd/request.go b/irma/cmd/request.go index e759a75cc..35d45186e 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -302,18 +302,23 @@ func handleBinding(options *server.SessionOptions, statusChan chan server.Status } else if status == server.StatusBinding { bindingStarted = true go func() { - fmt.Println("\nBinding code:", options.BindingCode) - fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") - _, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err == nil { - err = completeBinding() + if options.BindingMethod == irma.BindingMethodPin { + fmt.Println("\nBinding code:", options.BindingCode) + fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") + _, err := bufio.NewReader(os.Stdin).ReadString('\n') if err == nil { - fmt.Println("Binding completed.") + err = completeBinding() + if err == nil { + fmt.Println("Binding completed.") + } else { + errorChan <- err + } } else { errorChan <- err + return } } else { - errorChan <- err + errorChan <- errors.Errorf("Binding method %s is not supported", options.BindingMethod) return } }() diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 52b34d256..b48919148 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -185,8 +185,6 @@ func serverRequest( err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) if err != nil { return nil, errors.WrapPrefix(err, "Failed to enable binding", 0) - } else if !sessionOptions.BindingEnabled { - return nil, errors.New("Binding is supported by server but could not be enabled for unknown reason") } } diff --git a/irmaclient/session.go b/irmaclient/session.go index 301d5b919..c8d3bc039 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -282,7 +282,7 @@ func (session *session) getSessionInfo() { } // Check whether binding is needed, and if so, wait for it to be completed. - if info.Options.BindingEnabled { + if info.Options.BindingMethod != irma.BindingMethodNone { err = session.handleBinding(info.Options.BindingCode) } if err != nil { diff --git a/requests.go b/requests.go index 1fad6fb5b..a32515d1c 100644 --- a/requests.go +++ b/requests.go @@ -213,8 +213,10 @@ type AttributeRequest struct { type BindingMethod string -const BindingMethodNone = "none" -const BindingMethodPin = "pin" +const ( + BindingMethodNone = "none" + BindingMethodPin = "pin" +) // An OptionsRequest asks for a options change of a particular session. type OptionsRequest struct { diff --git a/server/api.go b/server/api.go index 066c87d6e..b4a7ffd67 100644 --- a/server/api.go +++ b/server/api.go @@ -60,9 +60,9 @@ type SessionResult struct { } type SessionOptions struct { - LDContext string `json:"@context,omitempty"` - BindingEnabled bool `json:"bindingEnabled"` - BindingCode string `json:"bindingCode,omitempty"` + LDContext string `json:"@context,omitempty"` + BindingMethod irma.BindingMethod `json:"bindingMethod"` + BindingCode string `json:"bindingCode,omitempty"` } // SessionHandler is a function that can handle a session result @@ -567,6 +567,9 @@ func (info *SessionInfo) UnmarshalJSON(data []byte) error { } info.LDContext = LDContextSessionInfo info.ProtocolVersion = info.Request.Base().ProtocolVersion - info.Options = &SessionOptions{LDContext: LDContextSessionOptions} + info.Options = &SessionOptions{ + LDContext: LDContextSessionOptions, + BindingMethod: irma.BindingMethodNone, + } return nil } diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index b5fa2d976..adc3769d0 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -64,7 +64,7 @@ func (session *session) handleGetInfo(min, max *irma.ProtocolVersion) ( logger.WithFields(logrus.Fields{"version": session.version.String()}).Debugf("Protocol version negotiated") session.request.Base().ProtocolVersion = session.version - if session.options.BindingEnabled && session.version.Above(2, 6) { + if session.options.BindingMethod != irma.BindingMethodNone && session.version.Above(2, 6) { session.setStatus(server.StatusBinding) } else { session.setStatus(server.StatusConnected) diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 8ad05acca..7bf0f1703 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -58,17 +58,15 @@ func (session *session) onUpdate() { func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*server.SessionOptions, error) { if session.status == server.StatusInitialized { if request.BindingMethod == irma.BindingMethodNone { - session.options.BindingEnabled = false + session.options.BindingMethod = irma.BindingMethodNone + session.options.BindingCode = "" } else if request.BindingMethod == irma.BindingMethodPin { - session.options.BindingEnabled = true - } else { - return nil, errors.New("Binding method unknown") - } - if session.options.BindingEnabled { + session.options.BindingMethod = irma.BindingMethodPin session.options.BindingCode = common.NewBindingCode() } else { - session.options.BindingCode = "" + return nil, errors.New("Binding method unknown") } + return &session.options, nil } return nil, errors.New("Frontend options cannot be updated when client is already connected") @@ -304,7 +302,7 @@ func (session *session) getInfo() (*server.SessionInfo, *irma.RemoteError) { Options: &session.options, } - if !session.options.BindingEnabled { + if session.options.BindingMethod == irma.BindingMethodNone { request, rerr := session.getRequest() if rerr != nil { return nil, rerr @@ -558,7 +556,7 @@ func (s *Server) bindingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - if session.options.BindingEnabled && session.status == server.StatusBinding { + if session.status == server.StatusBinding { server.WriteError(w, server.ErrorBindingRequired, "") return } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index 72fdb6826..ce9bff665 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -175,10 +175,13 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) * } ses := &session{ - action: action, - rrequest: request, - request: request.SessionRequest(), - options: server.SessionOptions{LDContext: server.LDContextSessionOptions}, + action: action, + rrequest: request, + request: request.SessionRequest(), + options: server.SessionOptions{ + LDContext: server.LDContextSessionOptions, + BindingMethod: irma.BindingMethodNone, + }, lastActive: time.Now(), backendToken: backendToken, clientToken: clientToken, From 00d7ea7ee1d6c6d73cbac0886847185a82da838c Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 13 Jul 2020 10:22:34 +0200 Subject: [PATCH 11/77] Improve comment to explain session state check --- server/irmaserver/handle.go | 3 ++- server/irmaserver/helpers.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index adc3769d0..a092dd62b 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -35,7 +35,8 @@ func (session *session) handleDelete() { func (session *session) handleGetInfo(min, max *irma.ProtocolVersion) ( *server.SessionInfo, *irma.SessionRequest, *irma.RemoteError) { - // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. + // Check whether session is in the right state when protocol version is below 2.7. + // For newer versions the authenticationMiddleware makes this extra check unnecessary. if session.status != server.StatusInitialized { return nil, nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session already started") } diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 7bf0f1703..a0e604ea4 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -561,7 +561,8 @@ func (s *Server) bindingMiddleware(next http.Handler) http.Handler { return } - // Add check for sessions below version 2.7, for other sessions this is checked in authenticationMiddleware. + // Check whether session is in the right state when protocol version is below 2.7. + // For newer versions the authenticationMiddleware makes this extra check unnecessary. if session.status != server.StatusConnected { server.WriteError(w, server.ErrorUnexpectedRequest, "Session not yet started or already finished") return From 758bd0f17d4d919c978c1cde194f04320718f8a6 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 27 Jul 2020 17:23:58 +0200 Subject: [PATCH 12/77] Simplify unmarshalling of SessionInfo struct --- server/api.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/api.go b/server/api.go index b4a7ffd67..46b342c1a 100644 --- a/server/api.go +++ b/server/api.go @@ -553,9 +553,9 @@ func LogMiddleware(typ string, opts LogOptions) func(next http.Handler) http.Han } func (info *SessionInfo) UnmarshalJSON(data []byte) error { - // Marshal in alias first to prevent infinite recursion - type Alias SessionInfo - err := json.Unmarshal(data, &struct{ *Alias }{(*Alias)(info)}) + // Unmarshal in alias first to prevent infinite recursion + type alias SessionInfo + err := json.Unmarshal(data, (*alias)(info)) if err == nil && info.LDContext == LDContextSessionInfo { return nil } From 0b223ac3878d1a3e4e3771bd686ff8a01d20a65d Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 28 Jul 2020 14:57:18 +0200 Subject: [PATCH 13/77] Some code refactors and improved error handling --- internal/sessiontest/main_test.go | 2 +- irmaclient/client.go | 2 +- irmaclient/handlers.go | 2 +- irmaclient/session.go | 7 ++++--- server/api.go | 16 +++++++--------- server/irmaserver/handle.go | 2 +- server/irmaserver/helpers.go | 6 +++--- 7 files changed, 18 insertions(+), 19 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 2ebc05931..28e16afea 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -249,7 +249,7 @@ func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, session clientAuth, _ := client.NewQrSession(sesPtr.SessionPtr, h) // Binding can only be done from protocol version 2.7 - if _, max := client.GetSupportedVersions(); max.Above(2, 6) { + if _, max := client.SupportedVersions(); max.Above(2, 6) { enteredBindingCode := <-cBindingCode require.Equal(t, bindingCode, enteredBindingCode) diff --git a/irmaclient/client.go b/irmaclient/client.go index 52938f7c8..f542d1922 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -1273,7 +1273,7 @@ func (client *Client) ConfigurationUpdated(downloaded *irma.IrmaIdentifierSet) e return nil } -func (client *Client) GetSupportedVersions() (min, max *irma.ProtocolVersion) { +func (client *Client) SupportedVersions() (min, max *irma.ProtocolVersion) { return minVersion, maxVersion } diff --git a/irmaclient/handlers.go b/irmaclient/handlers.go index 0c2e32400..a0416d2c4 100644 --- a/irmaclient/handlers.go +++ b/irmaclient/handlers.go @@ -84,5 +84,5 @@ func (h *keyshareEnrollmentHandler) ClientReturnURLSet(clientReturnURL string) { h.fail(errors.New("Keyshare enrollment session unexpectedly found an external return url")) } func (h *keyshareEnrollmentHandler) BindingRequired(bindingCode string) { - h.fail(errors.New("Keyshare enrollment session unexpectedly found an binding required")) + h.fail(errors.New("Keyshare enrollment session failed: session binding required")) } diff --git a/irmaclient/session.go b/irmaclient/session.go index c8d3bc039..22bf076f3 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -271,10 +271,10 @@ func (session *session) getSessionInfo() { session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating) // Get the first IRMA protocol message and parse it - info := &server.SessionInfo{ + info := &server.ClientRequest{ Request: session.request, // As request is an interface it need to be initialized with a specific instance. } - // UnmarshalJSON of SessionInfo takes into account legacy protocols, so we do not have to check that here. + // UnmarshalJSON of ClientRequest takes into account legacy protocols, so we do not have to check that here. err := session.transport.Get("", info) if err != nil { session.fail(err.(*irma.SessionError)) @@ -307,10 +307,11 @@ func (session *session) handleBinding(bindingCode string) error { } else { return errors.New("Server finished session without completing binding") } - case <-errorchan: + case err := <-errorchan: return &irma.SessionError{ ErrorType: irma.ErrorServerResponse, Info: "Binding aborted by server", + Err: err, } } } diff --git a/server/api.go b/server/api.go index 46b342c1a..be9babf34 100644 --- a/server/api.go +++ b/server/api.go @@ -25,7 +25,7 @@ import ( var Logger *logrus.Logger = logrus.StandardLogger() -const LDContextSessionInfo = "https://irma.app/ld/request/info/v1" +const LDContextClientRequest = "https://irma.app/ld/request/client/v1" const LDContextSessionOptions = "https://irma.app/ld/options/v1" type SessionPackage struct { @@ -34,10 +34,8 @@ type SessionPackage struct { FrontendToken irma.FrontendToken `json:"frontendToken"` } -// SessionInfo contains all information irmaclient needs to know to initiate a session. -// Session request is stored as RawMessage since it is not used internally and otherwise we would -// have to make a manual JSON marshal function with type checking since SessionRequest is an interface. -type SessionInfo struct { +// ClientRequest contains all information irmaclient needs to know to initiate a session. +type ClientRequest struct { LDContext string `json:"@context,omitempty"` ProtocolVersion *irma.ProtocolVersion `json:"protocolVersion,omitempty"` Options *SessionOptions `json:"options,omitempty"` @@ -552,11 +550,11 @@ func LogMiddleware(typ string, opts LogOptions) func(next http.Handler) http.Han } } -func (info *SessionInfo) UnmarshalJSON(data []byte) error { +func (info *ClientRequest) UnmarshalJSON(data []byte) error { // Unmarshal in alias first to prevent infinite recursion - type alias SessionInfo + type alias ClientRequest err := json.Unmarshal(data, (*alias)(info)) - if err == nil && info.LDContext == LDContextSessionInfo { + if err == nil && info.LDContext == LDContextClientRequest { return nil } @@ -565,7 +563,7 @@ func (info *SessionInfo) UnmarshalJSON(data []byte) error { if err != nil { return err } - info.LDContext = LDContextSessionInfo + info.LDContext = LDContextClientRequest info.ProtocolVersion = info.Request.Base().ProtocolVersion info.Options = &SessionOptions{ LDContext: LDContextSessionOptions, diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index a092dd62b..986862ed0 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -34,7 +34,7 @@ func (session *session) handleDelete() { } func (session *session) handleGetInfo(min, max *irma.ProtocolVersion) ( - *server.SessionInfo, *irma.SessionRequest, *irma.RemoteError) { + *server.ClientRequest, *irma.SessionRequest, *irma.RemoteError) { // Check whether session is in the right state when protocol version is below 2.7. // For newer versions the authenticationMiddleware makes this extra check unnecessary. if session.status != server.StatusInitialized { diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index a0e604ea4..5114459a3 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -295,9 +295,9 @@ func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, sche return session.kssProofs[scheme], nil } -func (session *session) getInfo() (*server.SessionInfo, *irma.RemoteError) { - info := server.SessionInfo{ - LDContext: server.LDContextSessionInfo, +func (session *session) getInfo() (*server.ClientRequest, *irma.RemoteError) { + info := server.ClientRequest{ + LDContext: server.LDContextClientRequest, ProtocolVersion: session.version, Options: &session.options, } From 0526c38b88e32c2c5f6ce71f64594dc384bd9616 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 29 Jul 2020 14:02:41 +0200 Subject: [PATCH 14/77] Code refactors in irma cmd package --- irma/cmd/request.go | 6 +++--- irma/cmd/session.go | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/irma/cmd/request.go b/irma/cmd/request.go index 35d45186e..8893f8690 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -308,11 +308,11 @@ func handleBinding(options *server.SessionOptions, statusChan chan server.Status _, err := bufio.NewReader(os.Stdin).ReadString('\n') if err == nil { err = completeBinding() - if err == nil { - fmt.Println("Binding completed.") - } else { + if err != nil { errorChan <- err + return } + fmt.Println("Binding completed.") } else { errorChan <- err return diff --git a/irma/cmd/session.go b/irma/cmd/session.go index b48919148..d1363c0da 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -176,10 +176,11 @@ func serverRequest( } // Enable binding if necessary - frontendTransport := irma.NewHTTPTransport(qr.URL, false) - frontendTransport.SetHeader(irma.AuthorizationHeader, frontendToken) - sessionOptions := &server.SessionOptions{} + var frontendTransport *irma.HTTPTransport + var sessionOptions *server.SessionOptions if binding { + frontendTransport = irma.NewHTTPTransport(qr.URL, false) + frontendTransport.SetHeader(irma.AuthorizationHeader, frontendToken) optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = irma.BindingMethodPin err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) From 34f66b9f682eb76da79e8957d3dbd5e4eb0b3dd5 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 30 Jul 2020 11:03:50 +0200 Subject: [PATCH 15/77] Refactored sessiontest to better integrate binding support --- internal/sessiontest/handlers_test.go | 4 + internal/sessiontest/legacy_test.go | 1 - internal/sessiontest/main_test.go | 107 +++++++++++-------------- internal/sessiontest/requestor_test.go | 28 +++---- internal/sessiontest/session_test.go | 72 +++++++---------- irmaclient/client.go | 2 +- irmaclient/session.go | 18 ++--- 7 files changed, 101 insertions(+), 131 deletions(-) diff --git a/internal/sessiontest/handlers_test.go b/internal/sessiontest/handlers_test.go index 3058c19e8..b3c83a6f5 100644 --- a/internal/sessiontest/handlers_test.go +++ b/internal/sessiontest/handlers_test.go @@ -83,6 +83,8 @@ type TestHandler struct { wait time.Duration result string bindingCodeChan chan string + dismisser *irmaclient.SessionDismisser + frontendTransport *irma.HTTPTransport } func (th TestHandler) KeyshareEnrollmentIncomplete(manager irma.SchemeManagerIdentifier) { @@ -152,6 +154,8 @@ func (th TestHandler) RequestPin(remainingAttempts int, callback irmaclient.PinH callback(true, "12345") } func (th TestHandler) BindingRequired(bindingCode string) { + // Send binding code via channel to calling test. This is done such that + // calling tests can detect whether this handler wasn't called. if th.bindingCodeChan != nil { th.bindingCodeChan <- bindingCode return diff --git a/internal/sessiontest/legacy_test.go b/internal/sessiontest/legacy_test.go index 7cbb88f11..613e3c0b8 100644 --- a/internal/sessiontest/legacy_test.go +++ b/internal/sessiontest/legacy_test.go @@ -67,5 +67,4 @@ func TestWithoutBindingSupport(t *testing.T) { t.Run("TestIssuedCredentialIsStored", TestIssuedCredentialIsStored) t.Run("TestPOSTSizeLimit", TestPOSTSizeLimit) t.Run("TestDisableBinding", TestDisableBinding) - // TestDisableBindingAfterClientConnected is not necessary since the extra check is always skipped for legacy versions. } diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 28e16afea..94fae3a70 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -6,8 +6,10 @@ import ( "io/ioutil" "os" "path/filepath" + "reflect" "testing" "time" + "unsafe" jwt "github.com/dgrijalva/jwt-go" irma "github.com/privacybydesign/irmago" @@ -189,39 +191,14 @@ func getJwt(t *testing.T, request irma.SessionRequest, sessiontype string, alg j return j } -func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string, client *irmaclient.Client) string { - if client == nil { - var handler *TestClientHandler - client, handler = parseStorage(t) - defer test.ClearTestStorage(t, handler.storage) - } - - if TestType == "irmaserver" || TestType == "irmaserver-jwt" || TestType == "irmaserver-hmac-jwt" { - StartRequestorServer(JwtServerConfiguration) - defer StopRequestorServer() - } - - sesPkg, _ := startSession(t, request, sessiontype) - - c := make(chan *SessionResult) - h := &TestHandler{t: t, c: c, client: client, expectedServerName: expectedRequestorInfo(t, client.Configuration)} - qrjson, err := json.Marshal(sesPkg.SessionPtr) - require.NoError(t, err) - client.NewSession(string(qrjson), h) - - if result := <-c; result != nil { - require.NoError(t, result.Err) - } - - var resJwt string - err = irma.NewHTTPTransport("http://localhost:48682/session/"+sesPkg.Token, false).Get("result-jwt", &resJwt) - require.NoError(t, err) - - return resJwt -} - -func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, sessiontype string, client *irmaclient.Client, - additionalCheck func(t *testing.T, transport *irma.HTTPTransport)) { +func sessionHelperWithFrontendOptions( + t *testing.T, + request irma.SessionRequest, + sessiontype string, + client *irmaclient.Client, + frontendOptionsHandler func(handler *TestHandler), + bindingHandler func(handler *TestHandler), +) string { if client == nil { var handler *TestClientHandler client, handler = parseStorage(t) @@ -233,54 +210,62 @@ func sessionHelperWithBinding(t *testing.T, request irma.SessionRequest, session defer StopRequestorServer() } - sesPtr, frontendToken := startSession(t, request, sessiontype) - frontendTransport, bindingCode := enableBinding(t, sesPtr.SessionPtr, frontendToken) + sesPkg, frontendToken := startSession(t, request, sessiontype) c := make(chan *SessionResult) - cBindingCode := make(chan string) + bindingCodeChan := make(chan string) h := &TestHandler{ t: t, c: c, client: client, expectedServerName: expectedRequestorInfo(t, client.Configuration), - bindingCodeChan: cBindingCode, + bindingCodeChan: bindingCodeChan, } - clientAuth, _ := client.NewQrSession(sesPtr.SessionPtr, h) - - // Binding can only be done from protocol version 2.7 - if _, max := client.SupportedVersions(); max.Above(2, 6) { - enteredBindingCode := <-cBindingCode - require.Equal(t, bindingCode, enteredBindingCode) - - // Check whether access to request endpoint is denied as long as binding is not finished - transport := irma.NewHTTPTransport(sesPtr.SessionPtr.URL, false) - transport.SetHeader(irma.AuthorizationHeader, clientAuth) - err := transport.Get("request", struct{}{}) - require.Error(t, err) - require.Equal(t, 403, err.(*irma.SessionError).RemoteStatus) + if frontendOptionsHandler != nil || bindingHandler != nil { + h.frontendTransport = irma.NewHTTPTransport(sesPkg.SessionPtr.URL, false) + h.frontendTransport.SetHeader(irma.AuthorizationHeader, frontendToken) + } + if frontendOptionsHandler != nil { + frontendOptionsHandler(h) + } - additionalCheck(t, frontendTransport) + bts, err := json.Marshal(sesPkg.SessionPtr) + require.NoError(t, err) + dismisser := client.NewSession(string(bts), h) - err = frontendTransport.Post("frontend/bindingcompleted", nil, nil) - require.NoError(t, err) + if bindingHandler != nil { + h.dismisser = &dismisser + bindingHandler(h) } if result := <-c; result != nil { require.NoError(t, result.Err) } + + var resJwt string + err = irma.NewHTTPTransport("http://localhost:48682/session/"+sesPkg.Token, false).Get("result-jwt", &resJwt) + require.NoError(t, err) + + return resJwt } -func enableBinding(t *testing.T, qr *irma.Qr, frontendToken irma.FrontendToken) (*irma.HTTPTransport, string) { +func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string, client *irmaclient.Client) string { + return sessionHelperWithFrontendOptions(t, request, sessiontype, client, nil, nil) +} + +func extractTransportFromDismisser(dismisser *irmaclient.SessionDismisser) *irma.HTTPTransport { + rct := reflect.ValueOf(dismisser).Elem().Elem().Elem().FieldByName("transport") + return reflect.NewAt(rct.Type(), unsafe.Pointer(rct.UnsafeAddr())).Elem().Interface().(*irma.HTTPTransport) +} + +func setBindingMethod(method irma.BindingMethod, handler *TestHandler) string { optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingMethod = irma.BindingMethodPin + optionsRequest.BindingMethod = method options := &server.SessionOptions{} - transport := irma.NewHTTPTransport(qr.URL, false) - transport.SetHeader(irma.AuthorizationHeader, frontendToken) - err := transport.Post("frontend/options", options, optionsRequest) - - require.NoError(t, err) - return transport, options.BindingCode + err := handler.frontendTransport.Post("frontend/options", options, optionsRequest) + require.NoError(handler.t, err) + return options.BindingCode } func expectedRequestorInfo(t *testing.T, conf *irma.Configuration) *irma.RequestorInfo { diff --git a/internal/sessiontest/requestor_test.go b/internal/sessiontest/requestor_test.go index 9b402474a..0c2985fef 100644 --- a/internal/sessiontest/requestor_test.go +++ b/internal/sessiontest/requestor_test.go @@ -1,10 +1,7 @@ package sessiontest import ( - "bytes" "encoding/json" - "io/ioutil" - "net/http" "reflect" "testing" "time" @@ -68,16 +65,19 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien var h irmaclient.Handler requestor := expectedRequestorInfo(t, client.Configuration) if opts&sessionOptionUnsatisfiableRequest > 0 { - h = &UnsatisfiableTestHandler{TestHandler: TestHandler{t, clientChan, client, requestor, 0, "", nil}} + h = &UnsatisfiableTestHandler{TestHandler: TestHandler{t, clientChan, client, requestor, 0, "", nil, nil, nil}} } else { var wait time.Duration = 0 if opts&sessionOptionClientWait > 0 { wait = 2 * time.Second } - h = &TestHandler{t, clientChan, client, requestor, wait, "", nil} + h = &TestHandler{t, clientChan, client, requestor, wait, "", nil, nil, nil} } - clientAuth, dismisser := client.NewQrSession(qr, h) + bts, err := json.Marshal(qr) + require.NoError(t, err) + dismisser := client.NewSession(string(bts), h) + clientResult := <-clientChan if opts&sessionOptionIgnoreError == 0 && clientResult != nil { require.NoError(t, clientResult.Err) @@ -92,17 +92,9 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien require.Equal(t, backendToken, serverResult.Token) if opts&sessionOptionRetryPost > 0 { - req, err := http.NewRequest(http.MethodPost, - qr.URL+"/proofs", - bytes.NewBuffer([]byte(h.(*TestHandler).result)), - ) - require.NoError(t, err) - req.Header.Add("Content-Type", "application/json") - req.Header.Add(irma.AuthorizationHeader, clientAuth) - res, err := new(http.Client).Do(req) - require.NoError(t, err) - require.True(t, res.StatusCode < 300) - _, err = ioutil.ReadAll(res.Body) + clientTransport := extractTransportFromDismisser(&dismisser) + var result string + err := clientTransport.Post("proofs", &result, h.(*TestHandler).result) require.NoError(t, err) } @@ -382,7 +374,7 @@ func TestClientDeveloperMode(t *testing.T) { c := make(chan *SessionResult, 1) j, err := json.Marshal(qr) require.NoError(t, err) - client.NewSession(string(j), &TestHandler{t, c, client, nil, 0, "", nil}) + client.NewSession(string(j), &TestHandler{t, c, client, nil, 0, "", nil, nil, nil}) result := <-c // Check that it failed with an appropriate error message diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index ad743f5af..7bdf61688 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -139,7 +139,32 @@ func TestIssuanceSameAttributesNotSingleton(t *testing.T) { func TestIssuanceBinding(t *testing.T) { id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") request := getCombinedIssuanceRequest(id) - sessionHelperWithBinding(t, request, "issue", nil, func(*testing.T, *irma.HTTPTransport) {}) + + var bindingCode string + frontendOptionsHandler := func(handler *TestHandler) { + bindingCode = setBindingMethod(irma.BindingMethodPin, handler) + } + bindingHandler := func(handler *TestHandler) { + if _, max := handler.client.SupportedVersions(); max.Above(2, 6) { + require.Equal(t, bindingCode, <-handler.bindingCodeChan) + + // Check whether access to request endpoint is denied as long as binding is not finished + clientTransport := extractTransportFromDismisser(handler.dismisser) + err := clientTransport.Get("request", struct{}{}) + require.Error(t, err) + require.Equal(t, 403, err.(*irma.SessionError).RemoteStatus) + + // Check whether binding cannot be disabled again after client is connected. + request := irma.NewOptionsRequest() + result := &server.SessionOptions{} + err = handler.frontendTransport.Post("frontend/options", result, request) + require.Error(t, err) + + err = handler.frontendTransport.Post("frontend/bindingcompleted", nil, nil) + require.NoError(handler.t, err) + } + } + sessionHelperWithFrontendOptions(t, request, "issue", nil, frontendOptionsHandler, bindingHandler) } func TestLargeAttribute(t *testing.T) { @@ -494,7 +519,7 @@ func TestStaticQRSession(t *testing.T) { c := make(chan *SessionResult) // Perform session - client.NewSession(string(bts), &TestHandler{t, c, client, requestor, 0, "", nil}) + client.NewSession(string(bts), &TestHandler{t, c, client, requestor, 0, "", nil, nil, nil}) if result := <-c; result != nil { require.NoError(t, result.Err) } @@ -637,47 +662,12 @@ func TestChainedSessions(t *testing.T) { } func TestDisableBinding(t *testing.T) { - var handler *TestClientHandler - client, handler := parseStorage(t) - defer test.ClearTestStorage(t, handler.storage) - - StartRequestorServer(JwtServerConfiguration) - defer StopRequestorServer() - id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") request := getCombinedIssuanceRequest(id) - sesPkg, frontendToken := startSession(t, request, "issue") - transport, _ := enableBinding(t, sesPkg.SessionPtr, frontendToken) - - // Disable binding again - optionsRequest := irma.NewOptionsRequest() - options := &server.SessionOptions{} - optionsRequest.BindingMethod = irma.BindingMethodNone - err := transport.Post("frontend/options", options, optionsRequest) - require.NoError(t, err) - c := make(chan *SessionResult) - h := &TestHandler{t: t, c: c, client: client, expectedServerName: expectedRequestorInfo(t, client.Configuration)} - qrjson, err := json.Marshal(sesPkg.SessionPtr) - require.NoError(t, err) - client.NewSession(string(qrjson), h) - - if result := <-c; result != nil { - require.NoError(t, result.Err) + frontendOptionsHandler := func(handler *TestHandler) { + _ = setBindingMethod(irma.BindingMethodPin, handler) + _ = setBindingMethod(irma.BindingMethodNone, handler) } -} - -func TestDisableBindingAfterClientConnected(t *testing.T) { - // When client is already connected, the frontend may not change the binding setting anymore. - id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") - request := getCombinedIssuanceRequest(id) - - check := func(t *testing.T, transport *irma.HTTPTransport) { - request := irma.NewOptionsRequest() - result := &server.SessionOptions{} - err := transport.Post("frontend/options", result, request) - require.Error(t, err) - } - - sessionHelperWithBinding(t, request, "issue", nil, check) + sessionHelperWithFrontendOptions(t, request, "issue", nil, frontendOptionsHandler, nil) } diff --git a/irmaclient/client.go b/irmaclient/client.go index f542d1922..d1ab9afa8 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -1109,7 +1109,7 @@ func (client *Client) keyshareEnrollWorker(managerID irma.SchemeManagerIdentifie // If the session succeeds or fails, the keyshare server is stored to disk or // removed from the client by the keyshareEnrollmentHandler. client.keyshareServers[managerID] = kss - client.NewQrSession(qr, &keyshareEnrollmentHandler{ + client.newQrSession(qr, &keyshareEnrollmentHandler{ client: client, pin: pin, kss: kss, diff --git a/irmaclient/session.go b/irmaclient/session.go index 22bf076f3..3341bd7bb 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -140,7 +140,7 @@ func (client *Client) NewSession(sessionrequest string, handler Handler) Session handler.Failure(&irma.SessionError{ErrorType: irma.ErrorInvalidRequest, Err: err}) return nil } - _, dismisser := client.NewQrSession(qr, handler) + dismisser := client.newQrSession(qr, handler) return dismisser } @@ -189,20 +189,20 @@ func (client *Client) newManualSession(request irma.SessionRequest, handler Hand return session } -// NewQrSession creates and starts a new interactive IRMA session -func (client *Client) NewQrSession(qr *irma.Qr, handler Handler) (irma.ClientAuthorization, *session) { +// newQrSession creates and starts a new interactive IRMA session +func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { if qr.Type == irma.ActionRedirect { newqr := &irma.Qr{} transport := irma.NewHTTPTransport("", !client.Preferences.DeveloperMode) if err := transport.Post(qr.URL, newqr, struct{}{}); err != nil { handler.Failure(&irma.SessionError{ErrorType: irma.ErrorTransport, Err: errors.Wrap(err, 0)}) - return "", nil + return nil } if newqr.Type == irma.ActionRedirect { // explicitly avoid infinite recursion handler.Failure(&irma.SessionError{ErrorType: irma.ErrorInvalidRequest, Err: errors.New("infinite static QR recursion")}) - return "", nil + return nil } - return client.NewQrSession(newqr, handler) + return client.newQrSession(newqr, handler) } client.PauseJobs() @@ -240,7 +240,7 @@ func (client *Client) NewQrSession(qr *irma.Qr, handler Handler) (irma.ClientAut fallthrough default: session.fail(&irma.SessionError{ErrorType: irma.ErrorUnknownAction, Info: string(session.Action)}) - return "", nil + return nil } session.transport.SetHeader(irma.MinVersionHeader, min.String()) @@ -258,7 +258,7 @@ func (client *Client) NewQrSession(qr *irma.Qr, handler Handler) (irma.ClientAut } go session.getSessionInfo() - return clientAuth, session + return session } // Core session methods @@ -587,7 +587,7 @@ func (session *session) sendResponse(message interface{}) { session.finish(false) if serverResponse != nil && serverResponse.NextSession != nil { - _, session.next = session.client.NewQrSession(serverResponse.NextSession, session.Handler) + session.next = session.client.newQrSession(serverResponse.NextSession, session.Handler) session.next.implicitDisclosure = session.choice.Attributes } else { session.Handler.Success(string(messageJson)) From e3fe42de9a8233f26400920b45d8077b9b08a824 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 14:00:52 +0200 Subject: [PATCH 16/77] Refactor type of irma.BackendToken --- messages.go | 2 +- server/api.go | 4 ++-- server/irmac/irmac.go | 11 ++++++----- server/irmaserver/api.go | 26 ++++++++++++------------ server/irmaserver/helpers.go | 8 ++++---- server/irmaserver/sessions.go | 34 ++++++++++++++++---------------- server/requestorserver/server.go | 14 ++++++------- 7 files changed, 50 insertions(+), 49 deletions(-) diff --git a/messages.go b/messages.go index d6e7a3824..70b2d64af 100644 --- a/messages.go +++ b/messages.go @@ -166,7 +166,7 @@ type Qr struct { } // Tokens to identify a session from the perspective of the different agents -type BackendToken = string +type BackendToken string type ClientToken = string type FrontendToken = string diff --git a/server/api.go b/server/api.go index be9babf34..6b8558999 100644 --- a/server/api.go +++ b/server/api.go @@ -45,7 +45,7 @@ type ClientRequest struct { // SessionResult contains session information such as the session status, type, possible errors, // and disclosed attributes or attribute-based signature if appropriate to the session type. type SessionResult struct { - Token string `json:"token"` + Token irma.BackendToken `json:"token"` Status Status `json:"status"` Type irma.Action `json:"type"` ProofStatus irma.ProofStatus `json:"proofStatus,omitempty"` @@ -76,7 +76,7 @@ type LogOptions struct { // Remove this when dropping support for legacy pre-condiscon session requests type LegacySessionResult struct { - Token string `json:"token"` + Token irma.BackendToken `json:"token"` Status Status `json:"status"` Type irma.Action `json:"type"` ProofStatus irma.ProofStatus `json:"proofStatus,omitempty"` diff --git a/server/irmac/irmac.go b/server/irmac/irmac.go index ef51ed147..0ac2ffa27 100644 --- a/server/irmac/irmac.go +++ b/server/irmac/irmac.go @@ -8,6 +8,7 @@ import ( "context" "encoding/base64" "encoding/json" + irma "github.com/privacybydesign/irmago" "io/ioutil" "net/http" "net/http/httptest" @@ -80,8 +81,8 @@ func StartSession(requestString *C.char) (r *C.char) { } // return actual results result.IrmaQr = string(qrJson) - result.BackendToken = backendToken - result.FrontendToken = frontendToken + result.BackendToken = string(backendToken) + result.FrontendToken = string(frontendToken) return } @@ -93,7 +94,7 @@ func GetSessionResult(token *C.char) *C.char { } // Run the actual core function - result := s.GetSessionResult(C.GoString(token)) + result := s.GetSessionResult(irma.BackendToken(C.GoString(token))) // And properly return results if result == nil { @@ -115,7 +116,7 @@ func GetRequest(token *C.char) *C.char { } // Run the core function - result := s.GetRequest(C.GoString(token)) + result := s.GetRequest(irma.BackendToken(C.GoString(token))) // And properly return results if result == nil { @@ -137,7 +138,7 @@ func CancelSession(token *C.char) *C.char { } // Run the core function - err := s.CancelSession(C.GoString(token)) + err := s.CancelSession(irma.BackendToken(C.GoString(token))) if err != nil { return C.CString(err.Error()) diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index db0c5dea8..e8cbaee98 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -23,7 +23,7 @@ type Server struct { sessions sessionStore scheduler *gocron.Scheduler stopScheduler chan bool - handlers map[string]server.SessionHandler + handlers map[irma.BackendToken]server.SessionHandler serverSentEvents *sse.Server } @@ -50,11 +50,11 @@ func New(conf *server.Configuration) (*Server, error) { conf: conf, scheduler: gocron.NewScheduler(), sessions: &memorySessionStore{ - requestor: make(map[string]*session), - client: make(map[string]*session), + requestor: make(map[irma.BackendToken]*session), + client: make(map[irma.ClientToken]*session), conf: conf, }, - handlers: make(map[string]server.SessionHandler), + handlers: make(map[irma.BackendToken]server.SessionHandler), serverSentEvents: e, } @@ -109,7 +109,7 @@ func (s *Server) HandlerFunc() http.HandlerFunc { r.NotFound(errorWriter(notfound, server.WriteResponse)) r.MethodNotAllowed(errorWriter(notallowed, server.WriteResponse)) - r.Route("/session/{backendToken}", func(r chi.Router) { + r.Route("/session/{clientToken}", func(r chi.Router) { r.Use(s.sessionMiddleware) r.Delete("/", s.handleSessionDelete) r.Get("/status", s.handleSessionStatus) @@ -284,10 +284,10 @@ func (s *Server) Revoke(credid irma.CredentialTypeIdentifier, key string, issued // SubscribeServerSentEvents subscribes the HTTP client to server sent events on status updates // of the specified IRMA session. -func SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, backendToken irma.BackendToken, requestor bool) error { - return s.SubscribeServerSentEvents(w, r, backendToken, requestor) +func SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, token string, requestor bool) error { + return s.SubscribeServerSentEvents(w, r, token, requestor) } -func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, backendToken irma.BackendToken, requestor bool) error { +func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, token string, requestor bool) error { if !s.conf.EnableSSE { server.WriteResponse(w, nil, &irma.RemoteError{ Status: 500, @@ -300,15 +300,15 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques var session *session if requestor { - session = s.sessions.get(backendToken) + session = s.sessions.get(irma.BackendToken(token)) } else { - session = s.sessions.clientGet(backendToken) + session = s.sessions.clientGet(irma.ClientToken(token)) } if session == nil { - return server.LogError(errors.Errorf("can't subscribe to server sent events of unknown session %s", backendToken)) + return server.LogError(errors.Errorf("can't subscribe to server sent events of unknown session %s", token)) } if session.status.Finished() { - return server.LogError(errors.Errorf("can't subscribe to server sent events of finished session %s", backendToken)) + return server.LogError(errors.Errorf("can't subscribe to server sent events of finished session %s", token)) } // The EventSource.onopen Javascript callback is not consistently called across browsers (Chrome yes, Firefox+Safari no). @@ -319,7 +319,7 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques // event to just the webclient currently listening. (Thus the handler of this "open" event must be idempotent.) go func() { time.Sleep(200 * time.Millisecond) - s.serverSentEvents.SendMessage("session/"+backendToken, sse.NewMessage("", "", "open")) + s.serverSentEvents.SendMessage("session/"+token, sse.NewMessage("", "", "open")) }() s.serverSentEvents.ServeHTTP(w, r) return nil diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 5114459a3..50b33621a 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -46,10 +46,10 @@ func (session *session) onUpdate() { if session.sse == nil { return } - session.sse.SendMessage("session/"+session.clientToken, + session.sse.SendMessage("session/"+string(session.clientToken), sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)), ) - session.sse.SendMessage("session/"+session.backendToken, + session.sse.SendMessage("session/"+string(session.backendToken), sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)), ) } @@ -489,7 +489,7 @@ func (s *Server) cacheMiddleware(next http.Handler) http.Handler { func (s *Server) sessionMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := chi.URLParam(r, "backendToken") + token := irma.ClientToken(chi.URLParam(r, "clientToken")) session := s.sessions.clientGet(token) if session == nil { server.WriteError(w, server.ErrorSessionUnknown, "") @@ -510,7 +510,7 @@ func (s *Server) sessionMiddleware(next http.Handler) http.Handler { if session.status.Finished() { if handler := s.handlers[result.Token]; handler != nil { go handler(result) - delete(s.handlers, token) + delete(s.handlers, result.Token) } } } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index ce9bff665..a6e3492fe 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -54,8 +54,8 @@ type responseCache struct { } type sessionStore interface { - get(token string) *session - clientGet(token string) *session + get(token irma.BackendToken) *session + clientGet(token irma.ClientToken) *session add(session *session) update(session *session) deleteExpired() @@ -66,8 +66,8 @@ type memorySessionStore struct { sync.RWMutex conf *server.Configuration - requestor map[string]*session - client map[string]*session + requestor map[irma.BackendToken]*session + client map[irma.ClientToken]*session } const ( @@ -79,13 +79,13 @@ var ( maxProtocolVersion = irma.NewVersion(2, 7) ) -func (s *memorySessionStore) get(t string) *session { +func (s *memorySessionStore) get(t irma.BackendToken) *session { s.RLock() defer s.RUnlock() return s.requestor[t] } -func (s *memorySessionStore) clientGet(t string) *session { +func (s *memorySessionStore) clientGet(t irma.ClientToken) *session { s.RLock() defer s.RUnlock() return s.client[t] @@ -107,8 +107,8 @@ func (s *memorySessionStore) stop() { defer s.Unlock() for _, session := range s.requestor { if session.sse != nil { - session.sse.CloseChannel("session/" + session.backendToken) - session.sse.CloseChannel("session/" + session.clientToken) + session.sse.CloseChannel("session/" + string(session.backendToken)) + session.sse.CloseChannel("session/" + string(session.clientToken)) } } } @@ -117,13 +117,13 @@ func (s *memorySessionStore) deleteExpired() { // First check which sessions have expired // We don't need a write lock for this yet, so postpone that for actual deleting s.RLock() - toCheck := make(map[string]*session, len(s.requestor)) + toCheck := make(map[irma.BackendToken]*session, len(s.requestor)) for token, session := range s.requestor { toCheck[token] = session } s.RUnlock() - expired := make([]string, 0, len(toCheck)) + expired := make([]irma.BackendToken, 0, len(toCheck)) for token, session := range toCheck { session.Lock() timeout := maxSessionLifetime @@ -149,8 +149,8 @@ func (s *memorySessionStore) deleteExpired() { for _, token := range expired { session := s.requestor[token] if session.sse != nil { - session.sse.CloseChannel("session/" + session.backendToken) - session.sse.CloseChannel("session/" + session.clientToken) + session.sse.CloseChannel("session/" + string(session.backendToken)) + session.sse.CloseChannel("session/" + string(session.clientToken)) } delete(s.client, session.clientToken) delete(s.requestor, token) @@ -161,16 +161,16 @@ func (s *memorySessionStore) deleteExpired() { var one *big.Int = big.NewInt(1) func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) *session { - clientToken := common.NewSessionToken() - backendToken := common.NewSessionToken() - frontendToken := common.NewSessionToken() + clientToken := irma.ClientToken(common.NewSessionToken()) + backendToken := irma.BackendToken(common.NewSessionToken()) + frontendToken := irma.FrontendToken(common.NewSessionToken()) base := request.SessionRequest().Base() if s.conf.AugmentClientReturnURL && base.AugmentReturnURL && base.ClientReturnURL != "" { if strings.Contains(base.ClientReturnURL, "?") { - base.ClientReturnURL += "&token=" + backendToken + base.ClientReturnURL += "&token=" + string(backendToken) } else { - base.ClientReturnURL += "?token=" + backendToken + base.ClientReturnURL += "?token=" + string(backendToken) } } diff --git a/server/requestorserver/server.go b/server/requestorserver/server.go index 147886ccc..4ffc41d4d 100644 --- a/server/requestorserver/server.go +++ b/server/requestorserver/server.go @@ -208,7 +208,7 @@ func (s *Server) Handler() http.Handler { // Server routes r.Route("/session", func(r chi.Router) { r.Post("/", s.handleCreateSession) - r.Route("/{token}", func(r chi.Router) { + r.Route("/{backendToken}", func(r chi.Router) { r.Delete("/", s.handleDelete) r.Get("/status", s.handleStatus) r.Get("/statusevents", s.handleStatusEvents) @@ -308,7 +308,7 @@ func (s *Server) handleRevocation(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { - res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token")) + res := s.irmaserv.GetSessionResult(irma.BackendToken(chi.URLParam(r, "backendToken"))) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") return @@ -333,14 +333,14 @@ func (s *Server) handleStatusEvents(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) { - err := s.irmaserv.CancelSession(chi.URLParam(r, "token")) + err := s.irmaserv.CancelSession(irma.BackendToken(chi.URLParam(r, "backendToken"))) if err != nil { server.WriteError(w, server.ErrorSessionUnknown, "") } } func (s *Server) handleResult(w http.ResponseWriter, r *http.Request) { - res := s.irmaserv.GetSessionResult(chi.URLParam(r, "token")) + res := s.irmaserv.GetSessionResult(irma.BackendToken(chi.URLParam(r, "backendToken"))) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") return @@ -359,8 +359,8 @@ func (s *Server) handleJwtResult(w http.ResponseWriter, r *http.Request) { return } - sessiontoken := chi.URLParam(r, "token") - res := s.irmaserv.GetSessionResult(sessiontoken) + backendToken := irma.BackendToken(chi.URLParam(r, "backendToken")) + res := s.irmaserv.GetSessionResult(backendToken) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") return @@ -387,7 +387,7 @@ func (s *Server) handleJwtProofs(w http.ResponseWriter, r *http.Request) { return } - backendToken := chi.URLParam(r, "token") + backendToken := irma.BackendToken(chi.URLParam(r, "backendToken")) res := s.irmaserv.GetSessionResult(backendToken) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") From edbd13e25209964058ab4695f360fdaf83e85cb6 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 14:07:11 +0200 Subject: [PATCH 17/77] Refactor type of irma.ClientToken --- messages.go | 2 +- server/irmaserver/api.go | 2 +- server/irmaserver/handle.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/messages.go b/messages.go index 70b2d64af..c099aab81 100644 --- a/messages.go +++ b/messages.go @@ -167,7 +167,7 @@ type Qr struct { // Tokens to identify a session from the perspective of the different agents type BackendToken string -type ClientToken = string +type ClientToken string type FrontendToken = string // Authorization headers diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index e8cbaee98..154719ced 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -207,7 +207,7 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, } return &irma.Qr{ Type: action, - URL: s.conf.URL + "session/" + session.clientToken, + URL: s.conf.URL + "session/" + string(session.clientToken), }, session.backendToken, session.frontendToken, nil } diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 986862ed0..8395c9b7c 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -384,9 +384,9 @@ func (s *Server) handleSessionStatusEvents(w http.ResponseWriter, r *http.Reques session.Unlock() r = r.WithContext(context.WithValue(r.Context(), "sse", common.SSECtx{ Component: server.ComponentSession, - Arg: session.clientToken, + Arg: string(session.clientToken), })) - if err := s.SubscribeServerSentEvents(w, r, session.clientToken, false); err != nil { + if err := s.SubscribeServerSentEvents(w, r, string(session.clientToken), false); err != nil { server.WriteError(w, server.ErrorUnknown, err.Error()) return } From 6117adde39cf9d1ba7cc4cc525359693387eb99f Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 14:12:42 +0200 Subject: [PATCH 18/77] Refactor type of irma.FrontendToken --- internal/sessiontest/main_test.go | 2 +- irma/cmd/session.go | 2 +- messages.go | 2 +- server/irmaserver/helpers.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 94fae3a70..d25012a9b 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -224,7 +224,7 @@ func sessionHelperWithFrontendOptions( if frontendOptionsHandler != nil || bindingHandler != nil { h.frontendTransport = irma.NewHTTPTransport(sesPkg.SessionPtr.URL, false) - h.frontendTransport.SetHeader(irma.AuthorizationHeader, frontendToken) + h.frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendToken)) } if frontendOptionsHandler != nil { frontendOptionsHandler(h) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index d1363c0da..98115c91b 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -180,7 +180,7 @@ func serverRequest( var sessionOptions *server.SessionOptions if binding { frontendTransport = irma.NewHTTPTransport(qr.URL, false) - frontendTransport.SetHeader(irma.AuthorizationHeader, frontendToken) + frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendToken)) optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = irma.BindingMethodPin err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) diff --git a/messages.go b/messages.go index c099aab81..b588edb77 100644 --- a/messages.go +++ b/messages.go @@ -168,7 +168,7 @@ type Qr struct { // Tokens to identify a session from the perspective of the different agents type BackendToken string type ClientToken string -type FrontendToken = string +type FrontendToken string // Authorization headers type ClientAuthorization = string diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 50b33621a..0dce7425f 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -440,7 +440,7 @@ func errorWriter(err *irma.RemoteError, writer func(w http.ResponseWriter, objec func (s *Server) frontendMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - frontendToken := r.Header.Get(irma.AuthorizationHeader) + frontendToken := irma.FrontendToken(r.Header.Get(irma.AuthorizationHeader)) if frontendToken != session.frontendToken { server.WriteError(w, server.ErrorUnauthorized, "") From 627b1b61eae727aad12282fc0a9f97e6047070d9 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 14:47:30 +0200 Subject: [PATCH 19/77] Include authenticationMiddleware in handleSessionGet --- messages.go | 2 +- server/irmaserver/api.go | 1 - server/irmaserver/handle.go | 24 +++++++++++++++-------- server/irmaserver/helpers.go | 37 +++++++----------------------------- 4 files changed, 24 insertions(+), 40 deletions(-) diff --git a/messages.go b/messages.go index b588edb77..53ded2b71 100644 --- a/messages.go +++ b/messages.go @@ -171,7 +171,7 @@ type ClientToken string type FrontendToken string // Authorization headers -type ClientAuthorization = string +type ClientAuthorization string // Statuses const ( diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 154719ced..47fdafabc 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -120,7 +120,6 @@ func (s *Server) HandlerFunc() http.HandlerFunc { r.Post("/bindingcompleted", s.handleFrontendBindingCompleted) }) r.Group(func(r chi.Router) { - r.Use(s.authenticationMiddleware) r.Use(s.cacheMiddleware) r.Get("/", s.handleSessionGet) r.Group(func(r chi.Router) { diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 8395c9b7c..9b0015108 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -33,10 +33,9 @@ func (session *session) handleDelete() { session.setStatus(server.StatusCancelled) } -func (session *session) handleGetInfo(min, max *irma.ProtocolVersion) ( +func (session *session) handleGetInfo(min, max *irma.ProtocolVersion, clientAuth irma.ClientAuthorization) ( *server.ClientRequest, *irma.SessionRequest, *irma.RemoteError) { - // Check whether session is in the right state when protocol version is below 2.7. - // For newer versions the authenticationMiddleware makes this extra check unnecessary. + if session.status != server.StatusInitialized { return nil, nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session already started") } @@ -44,9 +43,20 @@ func (session *session) handleGetInfo(min, max *irma.ProtocolVersion) ( session.markAlive() logger := session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}) + var err error + if session.version, err = session.chooseProtocolVersion(min, max); err != nil { + return nil, nil, session.fail(server.ErrorProtocolVersion, "") + } + + // Protocol versions below 2.7 don't include an authorization header. Therefore skip the authorization + // header presence check if a lower version is used. + if clientAuth == "" && session.version.Above(2, 6) { + return nil, nil, server.RemoteError(server.ErrorClientUnauthorized, "No authorization header provided") + } + session.clientAuth = clientAuth + // we include the latest revocation updates for the client here, as opposed to when the session // was started, so that the client always gets the very latest revocation records - var err error if err = session.conf.IrmaConfiguration.Revocation.SetRevocationUpdates(session.request.Base()); err != nil { return nil, nil, session.fail(server.ErrorRevocation, err.Error()) } @@ -59,9 +69,6 @@ func (session *session) handleGetInfo(min, max *irma.ProtocolVersion) ( logger.Info("Using condiscon: backwards compatibility with legacy IRMA apps is disabled") } - if session.version, err = session.chooseProtocolVersion(min, max); err != nil { - return nil, nil, session.fail(server.ErrorProtocolVersion, "") - } logger.WithFields(logrus.Fields{"version": session.version.String()}).Debugf("Protocol version negotiated") session.request.Base().ProtocolVersion = session.version @@ -408,8 +415,9 @@ func (s *Server) handleSessionGet(w http.ResponseWriter, r *http.Request) { return } session := r.Context().Value("session").(*session) + clientAuth := irma.ClientAuthorization(r.Header.Get(irma.AuthorizationHeader)) // When session binding is supported by all clients, the legacy support can be removed - res, legacyRes, err := session.handleGetInfo(&min, &max) + res, legacyRes, err := session.handleGetInfo(&min, &max, clientAuth) if legacyRes != nil { server.WriteResponse(w, legacyRes, err) } else { diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 0dce7425f..de78294d1 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -524,34 +524,6 @@ func (s *Server) sessionMiddleware(next http.Handler) http.Handler { }) } -func (s *Server) authenticationMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value("session").(*session) - - authorization := r.Header.Get(irma.AuthorizationHeader) - if authorization == "" && session.version != nil { - // Protocol versions below 2.7 use legacy authentication handled in handleSessionGet and cacheMiddleware. - if session.version.Below(2, 7) { - next.ServeHTTP(w, r) - } else { - server.WriteError(w, server.ErrorClientUnauthorized, "No authorization header provided") - } - return - } - - // If the client connects for the first time, we grant access and make sure - // only that exact client can connect to this session in future requests. - if session.clientAuth == "" { - session.clientAuth = authorization - } else if session.clientAuth != authorization { - server.WriteError(w, server.ErrorClientUnauthorized, "") - return - } - - next.ServeHTTP(w, r) - }) -} - func (s *Server) bindingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) @@ -561,12 +533,17 @@ func (s *Server) bindingMiddleware(next http.Handler) http.Handler { return } - // Check whether session is in the right state when protocol version is below 2.7. - // For newer versions the authenticationMiddleware makes this extra check unnecessary. + // Endpoints behind the bindingMiddleware can only be accessed when the client is already connected + // and includes the right authorization header to prove we still talk to the same client as before. if session.status != server.StatusConnected { server.WriteError(w, server.ErrorUnexpectedRequest, "Session not yet started or already finished") return } + clientAuth := irma.ClientAuthorization(r.Header.Get(irma.AuthorizationHeader)) + if session.clientAuth != clientAuth { + server.WriteError(w, server.ErrorClientUnauthorized, "") + return + } next.ServeHTTP(w, r) }) From 4e2759333267ec5a78dcfa33983ea48d33b686d5 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 14:58:08 +0200 Subject: [PATCH 20/77] Improved error handling in irmaclient's session.go --- irmaclient/session.go | 11 +++++------ messages.go | 2 ++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/irmaclient/session.go b/irmaclient/session.go index 3341bd7bb..d8f9fd32b 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -283,11 +283,10 @@ func (session *session) getSessionInfo() { // Check whether binding is needed, and if so, wait for it to be completed. if info.Options.BindingMethod != irma.BindingMethodNone { - err = session.handleBinding(info.Options.BindingCode) - } - if err != nil { - session.fail(err.(*irma.SessionError)) - return + if err = session.handleBinding(info.Options.BindingCode); err != nil { + session.fail(err.(*irma.SessionError)) + return + } } session.processSessionInfo() @@ -305,7 +304,7 @@ func (session *session) handleBinding(bindingCode string) error { if status == server.StatusConnected { return session.transport.Get("request", session.request) } else { - return errors.New("Server finished session without completing binding") + return &irma.SessionError{ErrorType: irma.ErrorBindingRejected} } case err := <-errorchan: return &irma.SessionError{ diff --git a/messages.go b/messages.go index 53ded2b71..372da4257 100644 --- a/messages.go +++ b/messages.go @@ -206,6 +206,8 @@ const ( ErrorCrypto = ErrorType("crypto") // Error involving revocation or nonrevocation proofs ErrorRevocation = ErrorType("revocation") + // Our binding attempt was rejected by the server + ErrorBindingRejected = ErrorType("bindingRejected") // Server rejected our response (second IRMA message) ErrorRejected = ErrorType("rejected") // (De)serializing of a message failed From 9d6f34581379c204a5472ac13bc6f0e4c6a0cc82 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 15:26:58 +0200 Subject: [PATCH 21/77] Let handleGetInfo return an interface instead of two values --- server/irmaserver/handle.go | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 9b0015108..55c720edc 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -34,10 +34,10 @@ func (session *session) handleDelete() { } func (session *session) handleGetInfo(min, max *irma.ProtocolVersion, clientAuth irma.ClientAuthorization) ( - *server.ClientRequest, *irma.SessionRequest, *irma.RemoteError) { + interface{}, *irma.RemoteError) { if session.status != server.StatusInitialized { - return nil, nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session already started") + return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session already started") } session.markAlive() @@ -45,20 +45,20 @@ func (session *session) handleGetInfo(min, max *irma.ProtocolVersion, clientAuth var err error if session.version, err = session.chooseProtocolVersion(min, max); err != nil { - return nil, nil, session.fail(server.ErrorProtocolVersion, "") + return nil, session.fail(server.ErrorProtocolVersion, "") } // Protocol versions below 2.7 don't include an authorization header. Therefore skip the authorization // header presence check if a lower version is used. if clientAuth == "" && session.version.Above(2, 6) { - return nil, nil, server.RemoteError(server.ErrorClientUnauthorized, "No authorization header provided") + return nil, server.RemoteError(server.ErrorClientUnauthorized, "No authorization header provided") } session.clientAuth = clientAuth // we include the latest revocation updates for the client here, as opposed to when the session // was started, so that the client always gets the very latest revocation records if err = session.conf.IrmaConfiguration.Revocation.SetRevocationUpdates(session.request.Base()); err != nil { - return nil, nil, session.fail(server.ErrorRevocation, err.Error()) + return nil, session.fail(server.ErrorRevocation, err.Error()) } // Handle legacy clients that do not support condiscon, by attempting to convert the condiscon @@ -81,16 +81,16 @@ func (session *session) handleGetInfo(min, max *irma.ProtocolVersion, clientAuth if session.version.Below(2, 5) { logger.Info("Returning legacy session format") legacy.Base().ProtocolVersion = session.version - return nil, &legacy, nil + return &legacy, nil } if session.version.Below(2, 7) { // These versions do not support binding, so the request is always returned immediately. request, rerr := session.getRequest() - return nil, &request, rerr + return &request, rerr } info, rerr := session.getInfo() - return info, nil, rerr + return info, rerr } func (session *session) handleGetStatus() (server.Status, *irma.RemoteError) { @@ -417,12 +417,8 @@ func (s *Server) handleSessionGet(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) clientAuth := irma.ClientAuthorization(r.Header.Get(irma.AuthorizationHeader)) // When session binding is supported by all clients, the legacy support can be removed - res, legacyRes, err := session.handleGetInfo(&min, &max, clientAuth) - if legacyRes != nil { - server.WriteResponse(w, legacyRes, err) - } else { - server.WriteResponse(w, res, err) - } + res, err := session.handleGetInfo(&min, &max, clientAuth) + server.WriteResponse(w, res, err) } func (s *Server) handleSessionGetRequest(w http.ResponseWriter, r *http.Request) { From 92c4e4348e087f313b02e57e1973ba4eee7c6125 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 15:29:50 +0200 Subject: [PATCH 22/77] Do an early return in updateFrontendOptions --- server/irmaserver/helpers.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index de78294d1..3f32356bc 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -56,20 +56,20 @@ func (session *session) onUpdate() { // Checks whether requested options are valid in the current session context. func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*server.SessionOptions, error) { - if session.status == server.StatusInitialized { - if request.BindingMethod == irma.BindingMethodNone { - session.options.BindingMethod = irma.BindingMethodNone - session.options.BindingCode = "" - } else if request.BindingMethod == irma.BindingMethodPin { - session.options.BindingMethod = irma.BindingMethodPin - session.options.BindingCode = common.NewBindingCode() - } else { - return nil, errors.New("Binding method unknown") - } - - return &session.options, nil + if session.status != server.StatusInitialized { + return nil, errors.New("Frontend options cannot be updated when client is already connected") + } + if request.BindingMethod == irma.BindingMethodNone { + session.options.BindingMethod = irma.BindingMethodNone + session.options.BindingCode = "" + } else if request.BindingMethod == irma.BindingMethodPin { + session.options.BindingMethod = irma.BindingMethodPin + session.options.BindingCode = common.NewBindingCode() + } else { + return nil, errors.New("Binding method unknown") } - return nil, errors.New("Frontend options cannot be updated when client is already connected") + + return &session.options, nil } // Complete the binding process of frontend and irma client From 8a1e2bd635d0ace624220983c01a43fb401e4785 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 15:37:32 +0200 Subject: [PATCH 23/77] Remove duplicate assignment to binding method field --- server/irmaserver/helpers.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 3f32356bc..d21840370 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -60,14 +60,13 @@ func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*se return nil, errors.New("Frontend options cannot be updated when client is already connected") } if request.BindingMethod == irma.BindingMethodNone { - session.options.BindingMethod = irma.BindingMethodNone session.options.BindingCode = "" } else if request.BindingMethod == irma.BindingMethodPin { - session.options.BindingMethod = irma.BindingMethodPin session.options.BindingCode = common.NewBindingCode() } else { return nil, errors.New("Binding method unknown") } + session.options.BindingMethod = request.BindingMethod return &session.options, nil } From bc8963ee2942cb57fd8d839142ec68d85519b883 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 3 Aug 2020 15:41:10 +0200 Subject: [PATCH 24/77] Improved checkCache documentation --- server/irmaserver/helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index d21840370..ee8253519 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -118,7 +118,7 @@ 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 +// - the same body was POSTed to the same endpoint 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(endpoint string, message []byte) (int, []byte) { From 075b1296def5e2d7f711386bc82ab2ae09527c35 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 4 Aug 2020 11:25:21 +0200 Subject: [PATCH 25/77] Cancel sse channel when closing it prematurely --- server/wait_status.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/server/wait_status.go b/server/wait_status.go index 5b3eacfe8..32e5d472f 100644 --- a/server/wait_status.go +++ b/server/wait_status.go @@ -1,6 +1,7 @@ package server import ( + "context" "github.com/privacybydesign/irmago" sseclient "github.com/sietseringers/go-sse" "strings" @@ -10,39 +11,45 @@ import ( const pollInterval = 1000 * time.Millisecond func WaitStatus(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { - if err := subscribeSSE(transport, statuschan, errorchan); err != nil { + if err := subscribeSSE(transport, statuschan, errorchan, false); err != nil { go poll(transport, initialStatus, statuschan, errorchan) } } func WaitStatusChanged(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { - if err := subscribeSSE(transport, statuschan, errorchan); err != nil { + if err := subscribeSSE(transport, statuschan, errorchan, true); err != nil { go pollUntilChange(transport, initialStatus, statuschan, errorchan) } } // Start listening for server-sent events -func subscribeSSE(transport *irma.HTTPTransport, statuschan chan Status, errorchan chan error) error { +func subscribeSSE(transport *irma.HTTPTransport, statuschan chan Status, errorchan chan error, untilNextOnly bool) error { + ctx, cancel := context.WithCancel(context.Background()) events := make(chan *sseclient.Event) + cancelled := false go func() { for { - if e := <-events; e != nil && e.Type != "open" { + e := <-events + if e != nil && e.Type != "open" { status := Status(strings.Trim(string(e.Data), `"`)) statuschan <- status - if status.Finished() { + if untilNextOnly || status.Finished() { errorchan <- nil + cancelled = true + cancel() return } } } }() - err := sseclient.Notify(nil, transport.Server+"statusevents", true, events) - if err != nil { + err := sseclient.Notify(ctx, transport.Server+"statusevents", true, events) + if !cancelled { close(events) + return err } - return err + return nil } // poll recursively polls the session status until a final status is received. From 599fe63c8090e02514d3e11efb7aafbefadf90ad Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 4 Aug 2020 11:43:46 +0200 Subject: [PATCH 26/77] Move handleBinding to session.go + fix session options marshalling --- irma/cmd/request.go | 45 ---------------------------------------- irma/cmd/session.go | 50 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/irma/cmd/request.go b/irma/cmd/request.go index 8893f8690..83fb3996e 100644 --- a/irma/cmd/request.go +++ b/irma/cmd/request.go @@ -1,7 +1,6 @@ package cmd import ( - "bufio" "encoding/json" "fmt" "net/http" @@ -289,50 +288,6 @@ func printQr(qr *irma.Qr, noqr bool) error { return nil } -func handleBinding(options *server.SessionOptions, statusChan chan server.Status, completeBinding func() error) ( - server.Status, error) { - errorChan := make(chan error) - status := server.StatusInitialized - bindingStarted := false - for { - select { - case status = <-statusChan: - if status == server.StatusInitialized { - continue - } else if status == server.StatusBinding { - bindingStarted = true - go func() { - if options.BindingMethod == irma.BindingMethodPin { - fmt.Println("\nBinding code:", options.BindingCode) - fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") - _, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err == nil { - err = completeBinding() - if err != nil { - errorChan <- err - return - } - fmt.Println("Binding completed.") - } else { - errorChan <- err - return - } - } else { - errorChan <- errors.Errorf("Binding method %s is not supported", options.BindingMethod) - return - } - }() - continue - } else if status == server.StatusConnected && !bindingStarted { - fmt.Println("Binding is not supported by the connected device.") - } - return status, nil - case err := <-errorChan: - return status, err - } - } -} - func printSessionResult(result *server.SessionResult) { fmt.Println("Session result:") fmt.Println(prettyprint(result)) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 98115c91b..e5ecd9663 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -1,8 +1,10 @@ package cmd import ( + "bufio" "fmt" "net/http" + "os" "regexp" "strconv" "sync" @@ -117,7 +119,7 @@ func libraryRequest( // Currently, the default session options are the same in all conditions, // so do only fetch them when a change is requested - sessionOptions := &server.SessionOptions{} + var sessionOptions *server.SessionOptions if binding { optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = irma.BindingMethodPin @@ -177,7 +179,7 @@ func serverRequest( // Enable binding if necessary var frontendTransport *irma.HTTPTransport - var sessionOptions *server.SessionOptions + sessionOptions := &server.SessionOptions{} if binding { frontendTransport = irma.NewHTTPTransport(qr.URL, false) frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendToken)) @@ -290,6 +292,50 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth return pkg.SessionPtr, pkg.FrontendToken, transport, err } +func handleBinding(options *server.SessionOptions, statusChan chan server.Status, completeBinding func() error) ( + server.Status, error) { + errorChan := make(chan error) + status := server.StatusInitialized + bindingStarted := false + for { + select { + case status = <-statusChan: + if status == server.StatusInitialized { + continue + } else if status == server.StatusBinding { + bindingStarted = true + go func() { + if options.BindingMethod == irma.BindingMethodPin { + fmt.Println("\nBinding code:", options.BindingCode) + fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") + _, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err == nil { + err = completeBinding() + if err != nil { + errorChan <- err + return + } + fmt.Println("Binding completed.") + } else { + errorChan <- err + return + } + } else { + errorChan <- errors.Errorf("Binding method %s is not supported", options.BindingMethod) + return + } + }() + continue + } else if status == server.StatusConnected && !bindingStarted { + fmt.Println("Binding is not supported by the connected device.") + } + return status, nil + case err := <-errorChan: + return status, err + } + } +} + // Configuration functions func configureSessionServer(url string, port int, privatekeysPath string, irmaconfig *irma.Configuration, verbosity int) error { From f8c6cad5c31050851b4f0af5ba66b09177201c02 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 4 Aug 2020 11:59:25 +0200 Subject: [PATCH 27/77] Split off request binding permission in separate function --- irma/cmd/session.go | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index e5ecd9663..8bc6587f8 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -304,27 +304,7 @@ func handleBinding(options *server.SessionOptions, statusChan chan server.Status continue } else if status == server.StatusBinding { bindingStarted = true - go func() { - if options.BindingMethod == irma.BindingMethodPin { - fmt.Println("\nBinding code:", options.BindingCode) - fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") - _, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err == nil { - err = completeBinding() - if err != nil { - errorChan <- err - return - } - fmt.Println("Binding completed.") - } else { - errorChan <- err - return - } - } else { - errorChan <- errors.Errorf("Binding method %s is not supported", options.BindingMethod) - return - } - }() + go requestBindingPermission(options, completeBinding, errorChan) continue } else if status == server.StatusConnected && !bindingStarted { fmt.Println("Binding is not supported by the connected device.") @@ -336,6 +316,26 @@ func handleBinding(options *server.SessionOptions, statusChan chan server.Status } } +func requestBindingPermission(options *server.SessionOptions, completeBinding func() error, errorChan chan error) { + if options.BindingMethod == irma.BindingMethodPin { + fmt.Println("\nBinding code:", options.BindingCode) + fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") + _, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + errorChan <- err + return + } + if err = completeBinding(); err != nil { + errorChan <- err + return + } + fmt.Println("Binding completed.") + errorChan <- nil + return + } + errorChan <- errors.Errorf("Binding method %s is not supported", options.BindingMethod) +} + // Configuration functions func configureSessionServer(url string, port int, privatekeysPath string, irmaconfig *irma.Configuration, verbosity int) error { From 7319dc38805312c6056859c381650ff3cf961c08 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 4 Aug 2020 12:10:52 +0200 Subject: [PATCH 28/77] Improved request binding permission instruction --- irma/cmd/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 8bc6587f8..fe6ec5812 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -319,7 +319,7 @@ func handleBinding(options *server.SessionOptions, statusChan chan server.Status func requestBindingPermission(options *server.SessionOptions, completeBinding func() error, errorChan chan error) { if options.BindingMethod == irma.BindingMethodPin { fmt.Println("\nBinding code:", options.BindingCode) - fmt.Println("Press Enter to confirm your device is connected; otherwise press Ctrl-C.") + fmt.Println("Press Enter to confirm your device shows the same binding code; otherwise press Ctrl-C.") _, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil { errorChan <- err From 1a650f3bc2661aaabea513a217d3fbbface71c7c Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 4 Aug 2020 13:45:24 +0200 Subject: [PATCH 29/77] Refactor: Move ClientRequest to irma package --- internal/sessiontest/main_test.go | 2 +- internal/sessiontest/session_test.go | 2 +- irmaclient/session.go | 2 +- requests.go | 41 ++++++++++++++++++++++++++++ server/api.go | 39 -------------------------- server/irmaserver/api.go | 4 +-- server/irmaserver/helpers.go | 8 +++--- server/irmaserver/sessions.go | 6 ++-- 8 files changed, 53 insertions(+), 51 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index d25012a9b..6976bcdbb 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -262,7 +262,7 @@ func extractTransportFromDismisser(dismisser *irmaclient.SessionDismisser) *irma func setBindingMethod(method irma.BindingMethod, handler *TestHandler) string { optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = method - options := &server.SessionOptions{} + options := &irma.SessionOptions{} err := handler.frontendTransport.Post("frontend/options", options, optionsRequest) require.NoError(handler.t, err) return options.BindingCode diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index 7bdf61688..ae4e20bd4 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -156,7 +156,7 @@ func TestIssuanceBinding(t *testing.T) { // Check whether binding cannot be disabled again after client is connected. request := irma.NewOptionsRequest() - result := &server.SessionOptions{} + result := &irma.SessionOptions{} err = handler.frontendTransport.Post("frontend/options", result, request) require.Error(t, err) diff --git a/irmaclient/session.go b/irmaclient/session.go index d8f9fd32b..a8c20ea8a 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -271,7 +271,7 @@ func (session *session) getSessionInfo() { session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating) // Get the first IRMA protocol message and parse it - info := &server.ClientRequest{ + info := &irma.ClientRequest{ Request: session.request, // As request is an interface it need to be initialized with a specific instance. } // UnmarshalJSON of ClientRequest takes into account legacy protocols, so we do not have to check that here. diff --git a/requests.go b/requests.go index a32515d1c..aae95c8c8 100644 --- a/requests.go +++ b/requests.go @@ -23,6 +23,8 @@ const ( LDContextIssuanceRequest = "https://irma.app/ld/request/issuance/v2" LDContextRevocationRequest = "https://irma.app/ld/request/revocation/v1" LDContextOptionsRequest = "https://irma.app/ld/request/options/v1" + LDContextClientRequest = "https://irma.app/ld/request/client/v1" + LDContextSessionOptions = "https://irma.app/ld/options/v1" DefaultJwtValidity = 120 ) @@ -238,6 +240,20 @@ type NonRevocationRequest struct { type NonRevocationParameters map[CredentialTypeIdentifier]*NonRevocationRequest +type SessionOptions struct { + LDContext string `json:"@context,omitempty"` + BindingMethod BindingMethod `json:"bindingMethod"` + BindingCode string `json:"bindingCode,omitempty"` +} + +// ClientRequest contains all information irmaclient needs to know to initiate a session. +type ClientRequest struct { + LDContext string `json:"@context,omitempty"` + ProtocolVersion *ProtocolVersion `json:"protocolVersion,omitempty"` + Options *SessionOptions `json:"options,omitempty"` + Request SessionRequest `json:"request,omitempty"` +} + func (choice *DisclosureChoice) Validate() error { if choice == nil { return nil @@ -1114,3 +1130,28 @@ func NewOptionsRequest() OptionsRequest { BindingMethod: BindingMethodNone, } } + +func (info *ClientRequest) UnmarshalJSON(data []byte) error { + // Unmarshal in alias first to prevent infinite recursion + type alias ClientRequest + err := json.Unmarshal(data, (*alias)(info)) + if err != nil { + return err + } + if info.LDContext == LDContextClientRequest { + return nil + } + + // For legacy sessions initialize session info by hand using the fetched request + err = json.Unmarshal(data, info.Request) + if err != nil { + return err + } + info.LDContext = LDContextClientRequest + info.ProtocolVersion = info.Request.Base().ProtocolVersion + info.Options = &SessionOptions{ + LDContext: LDContextSessionOptions, + BindingMethod: BindingMethodNone, + } + return nil +} diff --git a/server/api.go b/server/api.go index 6b8558999..1ba11aa38 100644 --- a/server/api.go +++ b/server/api.go @@ -25,23 +25,12 @@ import ( var Logger *logrus.Logger = logrus.StandardLogger() -const LDContextClientRequest = "https://irma.app/ld/request/client/v1" -const LDContextSessionOptions = "https://irma.app/ld/options/v1" - type SessionPackage struct { SessionPtr *irma.Qr `json:"sessionPtr"` Token irma.BackendToken `json:"token"` FrontendToken irma.FrontendToken `json:"frontendToken"` } -// ClientRequest contains all information irmaclient needs to know to initiate a session. -type ClientRequest struct { - LDContext string `json:"@context,omitempty"` - ProtocolVersion *irma.ProtocolVersion `json:"protocolVersion,omitempty"` - Options *SessionOptions `json:"options,omitempty"` - Request irma.SessionRequest `json:"request,omitempty"` -} - // SessionResult contains session information such as the session status, type, possible errors, // and disclosed attributes or attribute-based signature if appropriate to the session type. type SessionResult struct { @@ -57,12 +46,6 @@ type SessionResult struct { LegacySession bool `json:"-"` // true if request was started with legacy (i.e. pre-condiscon) session request } -type SessionOptions struct { - LDContext string `json:"@context,omitempty"` - BindingMethod irma.BindingMethod `json:"bindingMethod"` - BindingCode string `json:"bindingCode,omitempty"` -} - // SessionHandler is a function that can handle a session result // once an IRMA session has completed. type SessionHandler func(*SessionResult) @@ -549,25 +532,3 @@ func LogMiddleware(typ string, opts LogOptions) func(next http.Handler) http.Han }) } } - -func (info *ClientRequest) UnmarshalJSON(data []byte) error { - // Unmarshal in alias first to prevent infinite recursion - type alias ClientRequest - err := json.Unmarshal(data, (*alias)(info)) - if err == nil && info.LDContext == LDContextClientRequest { - return nil - } - - // For legacy sessions initialize session info by hand using the fetched request - err = json.Unmarshal(data, info.Request) - if err != nil { - return err - } - info.LDContext = LDContextClientRequest - info.ProtocolVersion = info.Request.Base().ProtocolVersion - info.Options = &SessionOptions{ - LDContext: LDContextSessionOptions, - BindingMethod: irma.BindingMethodNone, - } - return nil -} diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 47fdafabc..65a62c8d5 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -253,10 +253,10 @@ func (s *Server) CancelSession(backendToken irma.BackendToken) error { // Returns the updated options struct. Frontend options can only be // changed the irma client has not connected yet. Otherwise an error is returned. // Options that are not specified in the request, keep their old value. -func SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*server.SessionOptions, error) { +func SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { return s.SetFrontendOptions(backendToken, request) } -func (s *Server) SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*server.SessionOptions, error) { +func (s *Server) SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { session := s.sessions.get(backendToken) return session.updateFrontendOptions(request) } diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index ee8253519..04060e463 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -55,7 +55,7 @@ func (session *session) onUpdate() { } // Checks whether requested options are valid in the current session context. -func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*server.SessionOptions, error) { +func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*irma.SessionOptions, error) { if session.status != server.StatusInitialized { return nil, errors.New("Frontend options cannot be updated when client is already connected") } @@ -294,9 +294,9 @@ func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, sche return session.kssProofs[scheme], nil } -func (session *session) getInfo() (*server.ClientRequest, *irma.RemoteError) { - info := server.ClientRequest{ - LDContext: server.LDContextClientRequest, +func (session *session) getInfo() (*irma.ClientRequest, *irma.RemoteError) { + info := irma.ClientRequest{ + LDContext: irma.LDContextClientRequest, ProtocolVersion: session.version, Options: &session.options, } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index a6e3492fe..5944c8620 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -29,7 +29,7 @@ type session struct { legacyCompatible bool // if the request is convertible to pre-condiscon format implicitDisclosure irma.AttributeConDisCon - options server.SessionOptions + options irma.SessionOptions status server.Status prevStatus server.Status sse *sse.Server @@ -178,8 +178,8 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) * action: action, rrequest: request, request: request.SessionRequest(), - options: server.SessionOptions{ - LDContext: server.LDContextSessionOptions, + options: irma.SessionOptions{ + LDContext: irma.LDContextSessionOptions, BindingMethod: irma.BindingMethodNone, }, lastActive: time.Now(), From 1fbedbe6f99ad34e2b0f39dc21716bd06c41c8a8 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 4 Aug 2020 14:32:40 +0200 Subject: [PATCH 30/77] Small refactors and improvement of comments --- internal/sessiontest/handlers_test.go | 2 +- internal/sessiontest/main_test.go | 8 ++++---- internal/sessiontest/requestor_test.go | 6 +++--- internal/sessiontest/session_test.go | 2 +- irma/cmd/session.go | 11 +++++------ irmaclient/session.go | 4 ++-- server/irmaserver/api.go | 4 ++-- server/irmaserver/handle.go | 3 +-- server/irmaserver/helpers.go | 5 +++-- server/wait_status.go | 9 +++++---- 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/internal/sessiontest/handlers_test.go b/internal/sessiontest/handlers_test.go index b3c83a6f5..229922ce3 100644 --- a/internal/sessiontest/handlers_test.go +++ b/internal/sessiontest/handlers_test.go @@ -155,7 +155,7 @@ func (th TestHandler) RequestPin(remainingAttempts int, callback irmaclient.PinH } func (th TestHandler) BindingRequired(bindingCode string) { // Send binding code via channel to calling test. This is done such that - // calling tests can detect whether this handler wasn't called. + // calling tests can detect when this handler is skipped unexpectedly. if th.bindingCodeChan != nil { th.bindingCodeChan <- bindingCode return diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 6976bcdbb..18c35a56c 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -230,9 +230,9 @@ func sessionHelperWithFrontendOptions( frontendOptionsHandler(h) } - bts, err := json.Marshal(sesPkg.SessionPtr) + qrjson, err := json.Marshal(sesPkg.SessionPtr) require.NoError(t, err) - dismisser := client.NewSession(string(bts), h) + dismisser := client.NewSession(string(qrjson), h) if bindingHandler != nil { h.dismisser = &dismisser @@ -244,7 +244,7 @@ func sessionHelperWithFrontendOptions( } var resJwt string - err = irma.NewHTTPTransport("http://localhost:48682/session/"+sesPkg.Token, false).Get("result-jwt", &resJwt) + err = irma.NewHTTPTransport("http://localhost:48682/session/"+string(sesPkg.Token), false).Get("result-jwt", &resJwt) require.NoError(t, err) return resJwt @@ -254,7 +254,7 @@ func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string return sessionHelperWithFrontendOptions(t, request, sessiontype, client, nil, nil) } -func extractTransportFromDismisser(dismisser *irmaclient.SessionDismisser) *irma.HTTPTransport { +func extractClientTransport(dismisser *irmaclient.SessionDismisser) *irma.HTTPTransport { rct := reflect.ValueOf(dismisser).Elem().Elem().Elem().FieldByName("transport") return reflect.NewAt(rct.Type(), unsafe.Pointer(rct.UnsafeAddr())).Elem().Interface().(*irma.HTTPTransport) } diff --git a/internal/sessiontest/requestor_test.go b/internal/sessiontest/requestor_test.go index 0c2985fef..70abd8cb7 100644 --- a/internal/sessiontest/requestor_test.go +++ b/internal/sessiontest/requestor_test.go @@ -74,9 +74,9 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien h = &TestHandler{t, clientChan, client, requestor, wait, "", nil, nil, nil} } - bts, err := json.Marshal(qr) + j, err := json.Marshal(qr) require.NoError(t, err) - dismisser := client.NewSession(string(bts), h) + dismisser := client.NewSession(string(j), h) clientResult := <-clientChan if opts&sessionOptionIgnoreError == 0 && clientResult != nil { @@ -92,7 +92,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien require.Equal(t, backendToken, serverResult.Token) if opts&sessionOptionRetryPost > 0 { - clientTransport := extractTransportFromDismisser(&dismisser) + clientTransport := extractClientTransport(&dismisser) var result string err := clientTransport.Post("proofs", &result, h.(*TestHandler).result) require.NoError(t, err) diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index ae4e20bd4..f4fbefac6 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -149,7 +149,7 @@ func TestIssuanceBinding(t *testing.T) { require.Equal(t, bindingCode, <-handler.bindingCodeChan) // Check whether access to request endpoint is denied as long as binding is not finished - clientTransport := extractTransportFromDismisser(handler.dismisser) + clientTransport := extractClientTransport(handler.dismisser) err := clientTransport.Get("request", struct{}{}) require.Error(t, err) require.Equal(t, 403, err.(*irma.SessionError).RemoteStatus) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index fe6ec5812..ce59c135b 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -117,9 +117,8 @@ func libraryRequest( return nil, errors.WrapPrefix(err, "IRMA session failed", 0) } - // Currently, the default session options are the same in all conditions, - // so do only fetch them when a change is requested - var sessionOptions *server.SessionOptions + // Enable binding if necessary + var sessionOptions *irma.SessionOptions if binding { optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = irma.BindingMethodPin @@ -179,7 +178,7 @@ func serverRequest( // Enable binding if necessary var frontendTransport *irma.HTTPTransport - sessionOptions := &server.SessionOptions{} + sessionOptions := &irma.SessionOptions{} if binding { frontendTransport = irma.NewHTTPTransport(qr.URL, false) frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendToken)) @@ -292,7 +291,7 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth return pkg.SessionPtr, pkg.FrontendToken, transport, err } -func handleBinding(options *server.SessionOptions, statusChan chan server.Status, completeBinding func() error) ( +func handleBinding(options *irma.SessionOptions, statusChan chan server.Status, completeBinding func() error) ( server.Status, error) { errorChan := make(chan error) status := server.StatusInitialized @@ -316,7 +315,7 @@ func handleBinding(options *server.SessionOptions, statusChan chan server.Status } } -func requestBindingPermission(options *server.SessionOptions, completeBinding func() error, errorChan chan error) { +func requestBindingPermission(options *irma.SessionOptions, completeBinding func() error, errorChan chan error) { if options.BindingMethod == irma.BindingMethodPin { fmt.Println("\nBinding code:", options.BindingCode) fmt.Println("Press Enter to confirm your device shows the same binding code; otherwise press Ctrl-C.") diff --git a/irmaclient/session.go b/irmaclient/session.go index a8c20ea8a..99e6dfdb2 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -246,7 +246,7 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { session.transport.SetHeader(irma.MinVersionHeader, min.String()) session.transport.SetHeader(irma.MaxVersionHeader, maxVersion.String()) - // From protocol version 2.7 also a authorization header must be included. + // From protocol version 2.7 also an authorization header must be included. clientAuth := "" if maxVersion.Above(2, 6) { clientAuth = common.NewSessionToken() @@ -272,7 +272,7 @@ func (session *session) getSessionInfo() { // Get the first IRMA protocol message and parse it info := &irma.ClientRequest{ - Request: session.request, // As request is an interface it need to be initialized with a specific instance. + Request: session.request, // As request is an interface, it need to be initialized with a specific instance. } // UnmarshalJSON of ClientRequest takes into account legacy protocols, so we do not have to check that here. err := session.transport.Get("", info) diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 65a62c8d5..1a5f06b65 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -250,8 +250,8 @@ func (s *Server) CancelSession(backendToken irma.BackendToken) error { } // Requests a change of the session frontend options at the server. -// Returns the updated options struct. Frontend options can only be -// changed the irma client has not connected yet. Otherwise an error is returned. +// Returns the updated session options struct. Frontend options can only be +// changed when the client is not connected yet. Otherwise an error is returned. // Options that are not specified in the request, keep their old value. func SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { return s.SetFrontendOptions(backendToken, request) diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 55c720edc..014a180f8 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -85,7 +85,7 @@ func (session *session) handleGetInfo(min, max *irma.ProtocolVersion, clientAuth } if session.version.Below(2, 7) { - // These versions do not support binding, so the request is always returned immediately. + // These versions do not support the ClientRequest format, so send the SessionRequest. request, rerr := session.getRequest() return &request, rerr } @@ -416,7 +416,6 @@ func (s *Server) handleSessionGet(w http.ResponseWriter, r *http.Request) { } session := r.Context().Value("session").(*session) clientAuth := irma.ClientAuthorization(r.Header.Get(irma.AuthorizationHeader)) - // When session binding is supported by all clients, the legacy support can be removed res, err := session.handleGetInfo(&min, &max, clientAuth) server.WriteResponse(w, res, err) } diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 04060e463..c99845dbe 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -66,8 +66,8 @@ func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*ir } else { return nil, errors.New("Binding method unknown") } - session.options.BindingMethod = request.BindingMethod + session.options.BindingMethod = request.BindingMethod return &session.options, nil } @@ -119,6 +119,7 @@ const retryTimeLimit = 10 * time.Second // checkCache returns a previously cached response, for replaying against multiple requests from // irmago's retryablehttp client, if: // - the same body was POSTed to the same endpoint as last time +// - the body is not empty // - 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(endpoint string, message []byte) (int, []byte) { @@ -533,7 +534,7 @@ func (s *Server) bindingMiddleware(next http.Handler) http.Handler { } // Endpoints behind the bindingMiddleware can only be accessed when the client is already connected - // and includes the right authorization header to prove we still talk to the same client as before. + // and the request includes the right authorization header to prove we still talk to the same client as before. if session.status != server.StatusConnected { server.WriteError(w, server.ErrorUnexpectedRequest, "Session not yet started or already finished") return diff --git a/server/wait_status.go b/server/wait_status.go index 32e5d472f..b1e8e1eee 100644 --- a/server/wait_status.go +++ b/server/wait_status.go @@ -45,11 +45,12 @@ func subscribeSSE(transport *irma.HTTPTransport, statuschan chan Status, errorch }() err := sseclient.Notify(ctx, transport.Server+"statusevents", true, events) - if !cancelled { - close(events) - return err + // When sse was cancelled, an error is expected to be returned. The channels are already closed then. + if cancelled { + return nil } - return nil + close(events) + return err } // poll recursively polls the session status until a final status is received. From e79b13591ad8436e2e93b41522a8889a0a41b240 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 5 Aug 2020 11:17:53 +0200 Subject: [PATCH 31/77] Minor improvements --- internal/sessiontest/handlers_test.go | 2 +- internal/sessiontest/main_test.go | 3 +-- irmaclient/session.go | 3 +-- server/irmaserver/handle.go | 6 +++--- server/irmaserver/helpers.go | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/sessiontest/handlers_test.go b/internal/sessiontest/handlers_test.go index 229922ce3..55179868a 100644 --- a/internal/sessiontest/handlers_test.go +++ b/internal/sessiontest/handlers_test.go @@ -155,7 +155,7 @@ func (th TestHandler) RequestPin(remainingAttempts int, callback irmaclient.PinH } func (th TestHandler) BindingRequired(bindingCode string) { // Send binding code via channel to calling test. This is done such that - // calling tests can detect when this handler is skipped unexpectedly. + // calling tests can detect it when this handler is skipped unexpectedly. if th.bindingCodeChan != nil { th.bindingCodeChan <- bindingCode return diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 18c35a56c..8798df6bc 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -213,16 +213,15 @@ func sessionHelperWithFrontendOptions( sesPkg, frontendToken := startSession(t, request, sessiontype) c := make(chan *SessionResult) - bindingCodeChan := make(chan string) h := &TestHandler{ t: t, c: c, client: client, expectedServerName: expectedRequestorInfo(t, client.Configuration), - bindingCodeChan: bindingCodeChan, } if frontendOptionsHandler != nil || bindingHandler != nil { + h.bindingCodeChan = make(chan string) h.frontendTransport = irma.NewHTTPTransport(sesPkg.SessionPtr.URL, false) h.frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendToken)) } diff --git a/irmaclient/session.go b/irmaclient/session.go index 99e6dfdb2..994e8bce0 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -140,8 +140,7 @@ func (client *Client) NewSession(sessionrequest string, handler Handler) Session handler.Failure(&irma.SessionError{ErrorType: irma.ErrorInvalidRequest, Err: err}) return nil } - dismisser := client.newQrSession(qr, handler) - return dismisser + return client.newQrSession(qr, handler) } sigRequest := &irma.SignatureRequest{} diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 014a180f8..b790fc99b 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -33,7 +33,7 @@ func (session *session) handleDelete() { session.setStatus(server.StatusCancelled) } -func (session *session) handleGetInfo(min, max *irma.ProtocolVersion, clientAuth irma.ClientAuthorization) ( +func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, clientAuth irma.ClientAuthorization) ( interface{}, *irma.RemoteError) { if session.status != server.StatusInitialized { @@ -89,7 +89,7 @@ func (session *session) handleGetInfo(min, max *irma.ProtocolVersion, clientAuth request, rerr := session.getRequest() return &request, rerr } - info, rerr := session.getInfo() + info, rerr := session.getClientRequest() return info, rerr } @@ -416,7 +416,7 @@ func (s *Server) handleSessionGet(w http.ResponseWriter, r *http.Request) { } session := r.Context().Value("session").(*session) clientAuth := irma.ClientAuthorization(r.Header.Get(irma.AuthorizationHeader)) - res, err := session.handleGetInfo(&min, &max, clientAuth) + res, err := session.handleGetClientRequest(&min, &max, clientAuth) server.WriteResponse(w, res, err) } diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index c99845dbe..a729da3de 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -295,7 +295,7 @@ func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, sche return session.kssProofs[scheme], nil } -func (session *session) getInfo() (*irma.ClientRequest, *irma.RemoteError) { +func (session *session) getClientRequest() (*irma.ClientRequest, *irma.RemoteError) { info := irma.ClientRequest{ LDContext: irma.LDContextClientRequest, ProtocolVersion: session.version, From 4ad743d47c42a379de4aa0e9987b9c351663082b Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 5 Aug 2020 11:36:35 +0200 Subject: [PATCH 32/77] Return error in getClientRequest and getRequest instead of RemoteError --- server/irmaserver/handle.go | 20 +++++++++++++++----- server/irmaserver/helpers.go | 12 ++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index b790fc99b..231ba3aa4 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -86,11 +86,17 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c if session.version.Below(2, 7) { // These versions do not support the ClientRequest format, so send the SessionRequest. - request, rerr := session.getRequest() - return &request, rerr + request, err := session.getRequest() + if err != nil { + return nil, session.fail(server.ErrorRevocation, err.Error()) + } + return &request, nil + } + info, err := session.getClientRequest() + if err != nil { + return nil, session.fail(server.ErrorRevocation, err.Error()) } - info, rerr := session.getClientRequest() - return info, rerr + return info, nil } func (session *session) handleGetStatus() (server.Status, *irma.RemoteError) { @@ -426,8 +432,12 @@ func (s *Server) handleSessionGetRequest(w http.ResponseWriter, r *http.Request) server.WriteError(w, server.ErrorUnexpectedRequest, "Endpoint is not support in used protocol version") return } + var rerr *irma.RemoteError request, err := session.getRequest() - server.WriteResponse(w, request, err) + if err != nil { + rerr = session.fail(server.ErrorRevocation, err.Error()) + } + server.WriteResponse(w, request, rerr) } func (s *Server) handleFrontendOptionsPost(w http.ResponseWriter, r *http.Request) { diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index a729da3de..b469274f6 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -295,7 +295,7 @@ func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, sche return session.kssProofs[scheme], nil } -func (session *session) getClientRequest() (*irma.ClientRequest, *irma.RemoteError) { +func (session *session) getClientRequest() (*irma.ClientRequest, error) { info := irma.ClientRequest{ LDContext: irma.LDContextClientRequest, ProtocolVersion: session.version, @@ -303,16 +303,16 @@ func (session *session) getClientRequest() (*irma.ClientRequest, *irma.RemoteErr } if session.options.BindingMethod == irma.BindingMethodNone { - request, rerr := session.getRequest() - if rerr != nil { - return nil, rerr + request, err := session.getRequest() + if err != nil { + return nil, err } info.Request = request } return &info, nil } -func (session *session) getRequest() (irma.SessionRequest, *irma.RemoteError) { +func (session *session) getRequest() (irma.SessionRequest, error) { // In case of issuance requests, strip revocation keys from []CredentialRequest isreq, issuing := session.request.(*irma.IssuanceRequest) if !issuing { @@ -320,7 +320,7 @@ func (session *session) getRequest() (irma.SessionRequest, *irma.RemoteError) { } cpy, err := copyObject(isreq) if err != nil { - return nil, session.fail(server.ErrorRevocation, err.Error()) + return nil, err } for _, cred := range cpy.(*irma.IssuanceRequest).Credentials { cred.RevocationSupported = cred.RevocationKey != "" From 5889b0bd4af0dd87f8d550a1504b803569a131cc Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 3 Sep 2020 18:06:20 +0200 Subject: [PATCH 33/77] Rename server.Status type and move to irma package --- internal/sessiontest/handlers_test.go | 4 ++-- internal/sessiontest/requestor_test.go | 4 ++-- internal/sessiontest/revocation_test.go | 2 +- irma/cmd/session.go | 26 +++++++++++----------- irmaclient/handlers.go | 2 +- irmaclient/session.go | 24 ++++++++++---------- messages.go | 29 ++++++++++++++++++++----- server/api.go | 20 ++--------------- server/irmaserver/handle.go | 20 ++++++++--------- server/irmaserver/helpers.go | 16 +++++++------- server/irmaserver/sessions.go | 16 +++++++------- verify.go | 2 +- server/wait_status.go => wait_status.go | 19 ++++++++-------- 13 files changed, 91 insertions(+), 93 deletions(-) rename server/wait_status.go => wait_status.go (70%) diff --git a/internal/sessiontest/handlers_test.go b/internal/sessiontest/handlers_test.go index 55179868a..dc54ba017 100644 --- a/internal/sessiontest/handlers_test.go +++ b/internal/sessiontest/handlers_test.go @@ -99,7 +99,7 @@ func (th TestHandler) KeyshareEnrollmentMissing(manager irma.SchemeManagerIdenti func (th TestHandler) KeyshareEnrollmentDeleted(manager irma.SchemeManagerIdentifier) { th.Failure(&irma.SessionError{Err: errors.Errorf("Keyshare enrollment deleted for %s", manager.String())}) } -func (th TestHandler) StatusUpdate(action irma.Action, status irma.Status) {} +func (th TestHandler) StatusUpdate(action irma.Action, status irma.ClientStatus) {} func (th *TestHandler) Success(result string) { th.result = result th.c <- nil @@ -209,7 +209,7 @@ type ManualTestHandler struct { action irma.Action } -func (th *ManualTestHandler) StatusUpdate(action irma.Action, status irma.Status) { +func (th *ManualTestHandler) StatusUpdate(action irma.Action, status irma.ClientStatus) { th.action = action } diff --git a/internal/sessiontest/requestor_test.go b/internal/sessiontest/requestor_test.go index 70abd8cb7..336868ac3 100644 --- a/internal/sessiontest/requestor_test.go +++ b/internal/sessiontest/requestor_test.go @@ -217,7 +217,7 @@ func TestRequestorCombinedSessionMultipleAttributes(t *testing.T) { ] }`), &ir)) - require.Equal(t, server.StatusDone, requestorSessionHelper(t, &ir, nil).Status) + require.Equal(t, irma.ServerStatusDone, requestorSessionHelper(t, &ir, nil).Status) } func testRequestorIssuance(t *testing.T, keyshare bool, client *irmaclient.Client) { @@ -451,7 +451,7 @@ func TestIssueExpiredKey(t *testing.T) { expireKey(t, client.Configuration) result = requestorSessionHelper(t, getIssuanceRequest(true), client, sessionOptionReuseServer, sessionOptionIgnoreError) require.Nil(t, result.Err) - require.Equal(t, server.StatusCancelled, result.Status) + require.Equal(t, irma.ServerStatusCancelled, result.Status) // server aborts issuance sessions in case of expired public keys expireKey(t, irmaServerConfiguration.IrmaConfiguration) diff --git a/internal/sessiontest/revocation_test.go b/internal/sessiontest/revocation_test.go index b39ebf075..a552c6d02 100644 --- a/internal/sessiontest/revocation_test.go +++ b/internal/sessiontest/revocation_test.go @@ -236,7 +236,7 @@ func TestRevocationAll(t *testing.T) { stopRevocationServer() result := revocationSession(t, client, nil, sessionOptionIgnoreError) - require.Equal(t, server.StatusCancelled, result.Status) + require.Equal(t, irma.ServerStatusCancelled, result.Status) require.NotNil(t, result.Err) require.Equal(t, result.Err.ErrorName, string(server.ErrorRevocation.Type)) }) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index ce59c135b..5235062e8 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -134,9 +134,9 @@ func libraryRequest( if binding { // Listen for session status - statuschan := make(chan server.Status) + statuschan := make(chan irma.ServerStatus) go func() { - var status server.Status + var status irma.ServerStatus for { newStatus := irmaServer.GetSessionResult(backendToken).Status if newStatus != status { @@ -196,11 +196,11 @@ func serverRequest( return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } - statuschan := make(chan server.Status) + statuschan := make(chan irma.ServerStatus) errorchan := make(chan error) var wg sync.WaitGroup - go server.WaitStatus(transport, server.StatusInitialized, statuschan, errorchan) + go irma.WaitStatus(transport, irma.ServerStatusInitialized, statuschan, errorchan) go func() { err := <-errorchan if err != nil { @@ -212,7 +212,7 @@ func serverRequest( go func() { defer wg.Done() - var status server.Status + var status irma.ServerStatus if binding { status, err = handleBinding(sessionOptions, statuschan, func() error { err = frontendTransport.Post("frontend/bindingcompleted", nil, nil) @@ -228,7 +228,7 @@ func serverRequest( } else { // Wait until client connects if binding is disabled status := <-statuschan - if status != server.StatusConnected { + if status != irma.ServerStatusConnected { err = errors.Errorf("Unexpected status: %s", status) return } @@ -236,7 +236,7 @@ func serverRequest( // Wait until client finishes status = <-statuschan - if status != server.StatusCancelled && status != server.StatusDone { + if status != irma.ServerStatusCancelled && status != irma.ServerStatusDone { err = errors.Errorf("Unexpected status: %s", status) return } @@ -291,21 +291,21 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth return pkg.SessionPtr, pkg.FrontendToken, transport, err } -func handleBinding(options *irma.SessionOptions, statusChan chan server.Status, completeBinding func() error) ( - server.Status, error) { +func handleBinding(options *irma.SessionOptions, statusChan chan irma.ServerStatus, completeBinding func() error) ( + irma.ServerStatus, error) { errorChan := make(chan error) - status := server.StatusInitialized + status := irma.ServerStatusInitialized bindingStarted := false for { select { case status = <-statusChan: - if status == server.StatusInitialized { + if status == irma.ServerStatusInitialized { continue - } else if status == server.StatusBinding { + } else if status == irma.ServerStatusBinding { bindingStarted = true go requestBindingPermission(options, completeBinding, errorChan) continue - } else if status == server.StatusConnected && !bindingStarted { + } else if status == irma.ServerStatusConnected && !bindingStarted { fmt.Println("Binding is not supported by the connected device.") } return status, nil diff --git a/irmaclient/handlers.go b/irmaclient/handlers.go index a0416d2c4..930927769 100644 --- a/irmaclient/handlers.go +++ b/irmaclient/handlers.go @@ -53,7 +53,7 @@ func (h *keyshareEnrollmentHandler) fail(err error) { } // Not interested, ingore -func (h *keyshareEnrollmentHandler) StatusUpdate(action irma.Action, status irma.Status) {} +func (h *keyshareEnrollmentHandler) StatusUpdate(action irma.Action, status irma.ClientStatus) {} // The methods below should never be called, so we let each of them fail the session func (h *keyshareEnrollmentHandler) RequestVerificationPermission(request *irma.DisclosureRequest, satisfiable bool, candidates [][]DisclosureCandidates, ServerName *irma.RequestorInfo, callback PermissionHandler) { diff --git a/irmaclient/session.go b/irmaclient/session.go index 994e8bce0..db5d5a81d 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -8,8 +8,6 @@ import ( "strings" "time" - "github.com/privacybydesign/irmago/server" - "github.com/bwesterb/go-atum" "github.com/go-errors/errors" "github.com/privacybydesign/gabi" @@ -31,7 +29,7 @@ type PinHandler func(proceed bool, pin string) // A Handler contains callbacks for communication to the user. type Handler interface { - StatusUpdate(action irma.Action, status irma.Status) + StatusUpdate(action irma.Action, status irma.ClientStatus) ClientReturnURLSet(clientReturnURL string) BindingRequired(bindingCode string) Success(result string) @@ -182,7 +180,7 @@ func (client *Client) newManualSession(request irma.SessionRequest, handler Hand prepRevocation: make(chan error), } client.sessions.add(session) - session.Handler.StatusUpdate(session.Action, irma.StatusManualStarted) + session.Handler.StatusUpdate(session.Action, irma.ClientStatusManualStarted) session.processSessionInfo() return session @@ -223,7 +221,7 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { } client.sessions.add(session) - session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating) + session.Handler.StatusUpdate(session.Action, irma.ClientStatusCommunicating) min := minVersion // Check if the action is one of the supported types @@ -267,7 +265,7 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { func (session *session) getSessionInfo() { defer session.recoverFromPanic() - session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating) + session.Handler.StatusUpdate(session.Action, irma.ClientStatusCommunicating) // Get the first IRMA protocol message and parse it info := &irma.ClientRequest{ @@ -294,13 +292,13 @@ func (session *session) getSessionInfo() { func (session *session) handleBinding(bindingCode string) error { session.Handler.BindingRequired(bindingCode) - statuschan := make(chan server.Status) + statuschan := make(chan irma.ServerStatus) errorchan := make(chan error) - go server.WaitStatusChanged(session.transport, server.StatusBinding, statuschan, errorchan) + go irma.WaitStatusChanged(session.transport, irma.ServerStatusBinding, statuschan, errorchan) select { case status := <-statuschan: - if status == server.StatusConnected { + if status == irma.ServerStatusConnected { return session.transport.Get("request", session.request) } else { return &irma.SessionError{ErrorType: irma.ErrorBindingRejected} @@ -439,7 +437,7 @@ func (session *session) requestPermission() { return } - session.Handler.StatusUpdate(session.Action, irma.StatusConnected) + session.Handler.StatusUpdate(session.Action, irma.ClientStatusConnected) // Ask for permission to execute the session switch session.Action { @@ -478,7 +476,7 @@ func (session *session) doSession(proceed bool, choice *irma.DisclosureChoice) { session.fail(&irma.SessionError{ErrorType: irma.ErrorRequiredAttributeMissing, Err: err}) return } - session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating) + session.Handler.StatusUpdate(session.Action, irma.ClientStatusCommunicating) // wait for revocation preparation to finish err := <-session.prepRevocation @@ -825,11 +823,11 @@ func (session *session) KeyshareError(manager *irma.SchemeManagerIdentifier, err } func (session *session) KeysharePin() { - session.Handler.StatusUpdate(session.Action, irma.StatusConnected) + session.Handler.StatusUpdate(session.Action, irma.ClientStatusConnected) } func (session *session) KeysharePinOK() { - session.Handler.StatusUpdate(session.Action, irma.StatusCommunicating) + session.Handler.StatusUpdate(session.Action, irma.ClientStatusCommunicating) } func (s sessions) remove(token string) { diff --git a/messages.go b/messages.go index 372da4257..464c76dca 100644 --- a/messages.go +++ b/messages.go @@ -15,8 +15,11 @@ import ( "github.com/privacybydesign/gabi" ) -// Status encodes the status of an IRMA session (e.g., connected). -type Status string +// ClientStatus encodes the client status of an IRMA session (e.g., connected). +type ClientStatus string + +// ServerStatus encodes the server status of an IRMA session (e.g., CONNECTED). +type ServerStatus string const ( MinVersionHeader = "X-IRMA-MinProtocolVersion" @@ -173,11 +176,21 @@ type FrontendToken string // Authorization headers type ClientAuthorization string -// Statuses +// Client statuses +const ( + ClientStatusConnected = ClientStatus("connected") + ClientStatusCommunicating = ClientStatus("communicating") + ClientStatusManualStarted = ClientStatus("manualStarted") +) + +// Server statuses const ( - StatusConnected = Status("connected") - StatusCommunicating = Status("communicating") - StatusManualStarted = Status("manualStarted") + ServerStatusInitialized ServerStatus = "INITIALIZED" // The session has been started and is waiting for the client + ServerStatusBinding ServerStatus = "BINDING" // The client is binding, waiting for the frontend to accept + ServerStatusConnected ServerStatus = "CONNECTED" // The client has retrieved the session request, we wait for its response + ServerStatusCancelled ServerStatus = "CANCELLED" // The session is cancelled, possibly due to an error + ServerStatusDone ServerStatus = "DONE" // The session has completed successfully + ServerStatusTimeout ServerStatus = "TIMEOUT" // Session timed out ) // Actions @@ -360,6 +373,10 @@ func (qr *Qr) Validate() (err error) { return nil } +func (status ServerStatus) Finished() bool { + return status == ServerStatusDone || status == ServerStatusCancelled || status == ServerStatusTimeout +} + type ServerSessionResponse struct { ProofStatus ProofStatus `json:"proofStatus"` IssueSignatures []*gabi.IssueSignatureMessage `json:"sigs,omitempty"` diff --git a/server/api.go b/server/api.go index 1ba11aa38..002120ed4 100644 --- a/server/api.go +++ b/server/api.go @@ -35,7 +35,7 @@ type SessionPackage struct { // and disclosed attributes or attribute-based signature if appropriate to the session type. type SessionResult struct { Token irma.BackendToken `json:"token"` - Status Status `json:"status"` + Status irma.ServerStatus `json:"status"` Type irma.Action `json:"type"` ProofStatus irma.ProofStatus `json:"proofStatus,omitempty"` Disclosed [][]*irma.DisclosedAttribute `json:"disclosed,omitempty"` @@ -50,9 +50,6 @@ type SessionResult struct { // once an IRMA session has completed. type SessionHandler func(*SessionResult) -// Status is the status of an IRMA session. -type Status string - type LogOptions struct { Response, Headers, From, EncodeBinary bool } @@ -60,7 +57,7 @@ type LogOptions struct { // Remove this when dropping support for legacy pre-condiscon session requests type LegacySessionResult struct { Token irma.BackendToken `json:"token"` - Status Status `json:"status"` + Status irma.ServerStatus `json:"status"` Type irma.Action `json:"type"` ProofStatus irma.ProofStatus `json:"proofStatus,omitempty"` Disclosed []*irma.DisclosedAttribute `json:"disclosed,omitempty"` @@ -68,15 +65,6 @@ type LegacySessionResult struct { Err *irma.RemoteError `json:"error,omitempty"` } -const ( - StatusInitialized Status = "INITIALIZED" // The session has been started and is waiting for the client - StatusBinding Status = "BINDING" // The client is binding, waiting for the frontend to accept - StatusConnected Status = "CONNECTED" // The client has retrieved the session request, we wait for its response - StatusCancelled Status = "CANCELLED" // The session is cancelled, possibly due to an error - StatusDone Status = "DONE" // The session has completed successfully - StatusTimeout Status = "TIMEOUT" // Session timed out -) - const ( ComponentRevocation = "revocation" ComponentSession = "session" @@ -98,10 +86,6 @@ func (r *SessionResult) Legacy() *LegacySessionResult { return &LegacySessionResult{r.Token, r.Status, r.Type, r.ProofStatus, disclosed, r.Signature, r.Err} } -func (status Status) Finished() bool { - return status == StatusDone || status == StatusCancelled || status == StatusTimeout -} - // RemoteError converts an error and an explaining message to an *irma.RemoteError. func RemoteError(err Error, message string) *irma.RemoteError { var stack string diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 231ba3aa4..bda8e7a25 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -29,14 +29,14 @@ func (session *session) handleDelete() { } session.markAlive() - session.result = &server.SessionResult{Token: session.backendToken, Status: server.StatusCancelled, Type: session.action} - session.setStatus(server.StatusCancelled) + session.result = &server.SessionResult{Token: session.backendToken, Status: irma.ServerStatusCancelled, Type: session.action} + session.setStatus(irma.ServerStatusCancelled) } func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, clientAuth irma.ClientAuthorization) ( interface{}, *irma.RemoteError) { - if session.status != server.StatusInitialized { + if session.status != irma.ServerStatusInitialized { return nil, server.RemoteError(server.ErrorUnexpectedRequest, "Session already started") } @@ -73,9 +73,9 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c session.request.Base().ProtocolVersion = session.version if session.options.BindingMethod != irma.BindingMethodNone && session.version.Above(2, 6) { - session.setStatus(server.StatusBinding) + session.setStatus(irma.ServerStatusBinding) } else { - session.setStatus(server.StatusConnected) + session.setStatus(irma.ServerStatusConnected) } if session.version.Below(2, 5) { @@ -99,7 +99,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c return info, nil } -func (session *session) handleGetStatus() (server.Status, *irma.RemoteError) { +func (session *session) handleGetStatus() (irma.ServerStatus, *irma.RemoteError) { return session.status, nil } @@ -116,7 +116,7 @@ func (session *session) handlePostSignature(signature *irma.SignedMessage) (*irm session.result.Disclosed, session.result.ProofStatus, err = signature.Verify(session.conf.IrmaConfiguration, request) if err == nil { - session.setStatus(server.StatusDone) + session.setStatus(irma.ServerStatusDone) } else { if err == irma.ErrMissingPublicKey { rerr = session.fail(server.ErrorUnknownPublicKey, err.Error()) @@ -143,7 +143,7 @@ func (session *session) handlePostDisclosure(disclosure *irma.Disclosure) (*irma session.result.Disclosed, session.result.ProofStatus, err = disclosure.Verify(session.conf.IrmaConfiguration, request) if err == nil { - session.setStatus(server.StatusDone) + session.setStatus(irma.ServerStatusDone) } else { if err == irma.ErrMissingPublicKey { rerr = session.fail(server.ErrorUnknownPublicKey, err.Error()) @@ -236,7 +236,7 @@ func (session *session) handlePostCommitments(commitments *irma.IssueCommitmentM sigs = append(sigs, sig) } - session.setStatus(server.StatusDone) + session.setStatus(irma.ServerStatusDone) return &irma.ServerSessionResponse{ SessionType: irma.ActionIssuing, ProtocolVersion: session.version, @@ -251,7 +251,7 @@ func (session *session) nextSession() (irma.RequestorRequest, irma.AttributeConD return nil, nil, nil } url := base.NextSession.URL - if session.result.Status != server.StatusDone || + if session.result.Status != irma.ServerStatusDone || session.result.ProofStatus != irma.ProofStatusValid || session.result.Err != nil { return nil, nil, errors.New("session in invalid state") diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index b469274f6..3b54a8b48 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -34,7 +34,7 @@ func (session *session) markAlive() { session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Debugf("Session marked active, expiry delayed") } -func (session *session) setStatus(status server.Status) { +func (session *session) setStatus(status irma.ServerStatus) { session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken, "prevStatus": session.prevStatus, "status": status}). Info("Session status updated") session.status = status @@ -56,7 +56,7 @@ func (session *session) onUpdate() { // Checks whether requested options are valid in the current session context. func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*irma.SessionOptions, error) { - if session.status != server.StatusInitialized { + if session.status != irma.ServerStatusInitialized { return nil, errors.New("Frontend options cannot be updated when client is already connected") } if request.BindingMethod == irma.BindingMethodNone { @@ -73,8 +73,8 @@ func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*ir // Complete the binding process of frontend and irma client func (session *session) bindingCompleted() error { - if session.status == server.StatusBinding { - session.setStatus(server.StatusConnected) + if session.status == irma.ServerStatusBinding { + session.setStatus(irma.ServerStatusConnected) return nil } return errors.New("Binding was not enabled") @@ -82,8 +82,8 @@ func (session *session) bindingCompleted() error { 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.backendToken, Status: server.StatusCancelled, Type: session.action} + session.setStatus(irma.ServerStatusCancelled) + session.result = &server.SessionResult{Err: rerr, Token: session.backendToken, Status: irma.ServerStatusCancelled, Type: session.action} return rerr } @@ -528,14 +528,14 @@ func (s *Server) bindingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - if session.status == server.StatusBinding { + if session.status == irma.ServerStatusBinding { server.WriteError(w, server.ErrorBindingRequired, "") return } // Endpoints behind the bindingMiddleware can only be accessed when the client is already connected // and the request includes the right authorization header to prove we still talk to the same client as before. - if session.status != server.StatusConnected { + if session.status != irma.ServerStatusConnected { server.WriteError(w, server.ErrorUnexpectedRequest, "Session not yet started or already finished") return } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index 5944c8620..a2f122c8d 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -30,8 +30,8 @@ type session struct { implicitDisclosure irma.AttributeConDisCon options irma.SessionOptions - status server.Status - prevStatus server.Status + status irma.ServerStatus + prevStatus irma.ServerStatus sse *sse.Server responseCache responseCache @@ -50,7 +50,7 @@ type responseCache struct { message []byte response []byte status int - sessionStatus server.Status + sessionStatus irma.ServerStatus } type sessionStore interface { @@ -127,7 +127,7 @@ func (s *memorySessionStore) deleteExpired() { for token, session := range toCheck { session.Lock() timeout := maxSessionLifetime - if session.status == server.StatusInitialized && session.rrequest.Base().ClientTimeout != 0 { + if session.status == irma.ServerStatusInitialized && session.rrequest.Base().ClientTimeout != 0 { timeout = time.Duration(session.rrequest.Base().ClientTimeout) * time.Second } @@ -135,7 +135,7 @@ func (s *memorySessionStore) deleteExpired() { if !session.status.Finished() { s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Infof("Session expired") session.markAlive() - session.setStatus(server.StatusTimeout) + session.setStatus(irma.ServerStatusTimeout) } else { s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Infof("Deleting session") expired = append(expired, token) @@ -186,8 +186,8 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) * backendToken: backendToken, clientToken: clientToken, frontendToken: frontendToken, - status: server.StatusInitialized, - prevStatus: server.StatusInitialized, + status: irma.ServerStatusInitialized, + prevStatus: irma.ServerStatusInitialized, conf: s.conf, sessions: s.sessions, sse: s.serverSentEvents, @@ -195,7 +195,7 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) * LegacySession: request.SessionRequest().Base().Legacy(), Token: backendToken, Type: action, - Status: server.StatusInitialized, + Status: irma.ServerStatusInitialized, }, } diff --git a/verify.go b/verify.go index dc34d3902..4329d7dad 100644 --- a/verify.go +++ b/verify.go @@ -394,7 +394,7 @@ func (d *Disclosure) Verify(configuration *Configuration, request *DisclosureReq // 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 +// fully satisfied in this fashion, 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. // diff --git a/server/wait_status.go b/wait_status.go similarity index 70% rename from server/wait_status.go rename to wait_status.go index b1e8e1eee..87810e757 100644 --- a/server/wait_status.go +++ b/wait_status.go @@ -1,8 +1,7 @@ -package server +package irma import ( "context" - "github.com/privacybydesign/irmago" sseclient "github.com/sietseringers/go-sse" "strings" "time" @@ -10,20 +9,20 @@ import ( const pollInterval = 1000 * time.Millisecond -func WaitStatus(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { +func WaitStatus(transport *HTTPTransport, initialStatus ServerStatus, statuschan chan ServerStatus, errorchan chan error) { if err := subscribeSSE(transport, statuschan, errorchan, false); err != nil { go poll(transport, initialStatus, statuschan, errorchan) } } -func WaitStatusChanged(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { +func WaitStatusChanged(transport *HTTPTransport, initialStatus ServerStatus, statuschan chan ServerStatus, errorchan chan error) { if err := subscribeSSE(transport, statuschan, errorchan, true); err != nil { go pollUntilChange(transport, initialStatus, statuschan, errorchan) } } // Start listening for server-sent events -func subscribeSSE(transport *irma.HTTPTransport, statuschan chan Status, errorchan chan error, untilNextOnly bool) error { +func subscribeSSE(transport *HTTPTransport, statuschan chan ServerStatus, errorchan chan error, untilNextOnly bool) error { ctx, cancel := context.WithCancel(context.Background()) events := make(chan *sseclient.Event) @@ -32,7 +31,7 @@ func subscribeSSE(transport *irma.HTTPTransport, statuschan chan Status, errorch for { e := <-events if e != nil && e.Type != "open" { - status := Status(strings.Trim(string(e.Data), `"`)) + status := ServerStatus(strings.Trim(string(e.Data), `"`)) statuschan <- status if untilNextOnly || status.Finished() { errorchan <- nil @@ -54,11 +53,11 @@ func subscribeSSE(transport *irma.HTTPTransport, statuschan chan Status, errorch } // poll recursively polls the session status until a final status is received. -func poll(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { +func poll(transport *HTTPTransport, initialStatus ServerStatus, statuschan chan ServerStatus, errorchan chan error) { go func() { status := initialStatus for { - statuschanPolling := make(chan Status) + statuschanPolling := make(chan ServerStatus) errorchanPolling := make(chan error) go pollUntilChange(transport, status, statuschanPolling, errorchanPolling) select { @@ -77,7 +76,7 @@ func poll(transport *irma.HTTPTransport, initialStatus Status, statuschan chan S }() } -func pollUntilChange(transport *irma.HTTPTransport, initialStatus Status, statuschan chan Status, errorchan chan error) { +func pollUntilChange(transport *HTTPTransport, initialStatus ServerStatus, statuschan chan ServerStatus, errorchan chan error) { // First we wait <-time.NewTimer(pollInterval).C @@ -87,7 +86,7 @@ func pollUntilChange(transport *irma.HTTPTransport, initialStatus Status, status errorchan <- err return } - status := Status(strings.Trim(s, `"`)) + status := ServerStatus(strings.Trim(s, `"`)) // report if status changed if status != initialStatus { From 3e6a02eb7d5d27624814dadb162358f1ef98e6b2 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 11:29:19 +0200 Subject: [PATCH 34/77] Use reflection to set maxVersion on irmaclient in tests --- internal/sessiontest/legacy_test.go | 8 +++++--- internal/sessiontest/main_test.go | 14 ++++++++++++++ internal/sessiontest/session_test.go | 2 +- irmaclient/client.go | 10 ++++++---- irmaclient/irmaclient_test.go | 2 +- irmaclient/session.go | 18 ++++-------------- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/internal/sessiontest/legacy_test.go b/internal/sessiontest/legacy_test.go index 613e3c0b8..89c3d3cab 100644 --- a/internal/sessiontest/legacy_test.go +++ b/internal/sessiontest/legacy_test.go @@ -1,7 +1,6 @@ package sessiontest import ( - "github.com/privacybydesign/irmago/irmaclient" "testing" irma "github.com/privacybydesign/irmago" @@ -37,8 +36,11 @@ func TestSessionUsingLegacyStorage(t *testing.T) { } func TestWithoutBindingSupport(t *testing.T) { - defer irmaclient.SetMaxVersion(nil) - irmaclient.SetMaxVersion(&irma.ProtocolVersion{Major: 2, Minor: 6}) + defaultMaxVersion := maxClientVersion + defer func() { + maxClientVersion = defaultMaxVersion + }() + maxClientVersion = &irma.ProtocolVersion{Major: 2, Minor: 6} t.Run("TestSigningSession", TestSigningSession) t.Run("TestDisclosureSession", TestDisclosureSession) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 8798df6bc..d4fa1519f 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -20,6 +20,9 @@ import ( "github.com/stretchr/testify/require" ) +// Defines the maximum protocol version of an irmaclient in tests +var maxClientVersion = &irma.ProtocolVersion{Major: 2, Minor: 7} + func TestMain(m *testing.M) { // Create HTTP server for scheme managers test.StartSchemeManagerHttpServer() @@ -46,6 +49,12 @@ func parseExistingStorage(t *testing.T, storage string) (*irmaclient.Client, *Te handler, ) require.NoError(t, err) + + // Set max version we want to test on + version := extractClientMaxVersion(client) + version.Major = maxClientVersion.Major + version.Minor = maxClientVersion.Minor + client.SetPreferences(irmaclient.Preferences{DeveloperMode: true}) return client, handler } @@ -258,6 +267,11 @@ func extractClientTransport(dismisser *irmaclient.SessionDismisser) *irma.HTTPTr return reflect.NewAt(rct.Type(), unsafe.Pointer(rct.UnsafeAddr())).Elem().Interface().(*irma.HTTPTransport) } +func extractClientMaxVersion(client *irmaclient.Client) *irma.ProtocolVersion { + rmv := reflect.ValueOf(client).Elem().FieldByName("maxVersion") + return reflect.NewAt(rmv.Type(), unsafe.Pointer(rmv.UnsafeAddr())).Elem().Interface().(*irma.ProtocolVersion) +} + func setBindingMethod(method irma.BindingMethod, handler *TestHandler) string { optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = method diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index f4fbefac6..eef0dda9f 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -145,7 +145,7 @@ func TestIssuanceBinding(t *testing.T) { bindingCode = setBindingMethod(irma.BindingMethodPin, handler) } bindingHandler := func(handler *TestHandler) { - if _, max := handler.client.SupportedVersions(); max.Above(2, 6) { + if extractClientMaxVersion(handler.client).Above(2, 6) { require.Equal(t, bindingCode, <-handler.bindingCodeChan) // Check whether access to request endpoint is denied as long as binding is not finished diff --git a/irmaclient/client.go b/irmaclient/client.go index d1ab9afa8..b8c4df24e 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -56,6 +56,10 @@ type Client struct { // Legacy storage needed when client has not updated to the new storage yet fileStorage fileStorage + // Versions the client supports + minVersion *irma.ProtocolVersion + maxVersion *irma.ProtocolVersion + // Other state Preferences Preferences Configuration *irma.Configuration @@ -158,6 +162,8 @@ func New( attributes: make(map[irma.CredentialTypeIdentifier][]*irma.AttributeList), irmaConfigurationPath: irmaConfigurationPath, handler: handler, + minVersion: &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][0]}, + maxVersion: &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][len(supportedVersions[2])-1]}, } client.Configuration, err = irma.NewConfiguration( @@ -1273,10 +1279,6 @@ func (client *Client) ConfigurationUpdated(downloaded *irma.IrmaIdentifierSet) e return nil } -func (client *Client) SupportedVersions() (min, max *irma.ProtocolVersion) { - return minVersion, maxVersion -} - func (cc *credCandidate) Present() bool { return cc.Hash != "" } diff --git a/irmaclient/irmaclient_test.go b/irmaclient/irmaclient_test.go index ad8b5ab6b..21c800dbe 100644 --- a/irmaclient/irmaclient_test.go +++ b/irmaclient/irmaclient_test.go @@ -232,7 +232,7 @@ func TestCandidateConjunctionOrder(t *testing.T) { ) req := &irma.DisclosureRequest{ - BaseRequest: irma.BaseRequest{ProtocolVersion: maxVersion}, + BaseRequest: irma.BaseRequest{ProtocolVersion: client.maxVersion}, Disclose: cdc, } diff --git a/irmaclient/session.go b/irmaclient/session.go index db5d5a81d..02194bb30 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -114,16 +114,6 @@ var supportedVersions = map[int][]int{ 7, // introduces chained sessions and session binding }, } -var minVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][0]} -var maxVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][len(supportedVersions[2])-1]} - -func SetMaxVersion(version *irma.ProtocolVersion) { - if version == nil { - maxVersion = &irma.ProtocolVersion{Major: 2, Minor: supportedVersions[2][len(supportedVersions[2])-1]} - } else { - maxVersion = version - } -} // Session constructors @@ -174,7 +164,7 @@ func (client *Client) newManualSession(request irma.SessionRequest, handler Hand Action: action, Handler: handler, client: client, - Version: minVersion, + Version: client.minVersion, request: request, done: doneChannel, prepRevocation: make(chan error), @@ -222,7 +212,7 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { client.sessions.add(session) session.Handler.StatusUpdate(session.Action, irma.ClientStatusCommunicating) - min := minVersion + min := client.minVersion // Check if the action is one of the supported types switch session.Action { @@ -241,11 +231,11 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { } session.transport.SetHeader(irma.MinVersionHeader, min.String()) - session.transport.SetHeader(irma.MaxVersionHeader, maxVersion.String()) + session.transport.SetHeader(irma.MaxVersionHeader, client.maxVersion.String()) // From protocol version 2.7 also an authorization header must be included. clientAuth := "" - if maxVersion.Above(2, 6) { + if client.maxVersion.Above(2, 6) { clientAuth = common.NewSessionToken() session.transport.SetHeader(irma.AuthorizationHeader, clientAuth) } From fde6f412bb3fc866ff42c625efd2e14c0244c52c Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 11:38:28 +0200 Subject: [PATCH 35/77] Remove pointer usage for SessionDismisser in tests --- internal/sessiontest/handlers_test.go | 2 +- internal/sessiontest/main_test.go | 7 +++---- internal/sessiontest/requestor_test.go | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/sessiontest/handlers_test.go b/internal/sessiontest/handlers_test.go index dc54ba017..3d9f8bf00 100644 --- a/internal/sessiontest/handlers_test.go +++ b/internal/sessiontest/handlers_test.go @@ -83,7 +83,7 @@ type TestHandler struct { wait time.Duration result string bindingCodeChan chan string - dismisser *irmaclient.SessionDismisser + dismisser irmaclient.SessionDismisser frontendTransport *irma.HTTPTransport } diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index d4fa1519f..4458f988b 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -240,10 +240,9 @@ func sessionHelperWithFrontendOptions( qrjson, err := json.Marshal(sesPkg.SessionPtr) require.NoError(t, err) - dismisser := client.NewSession(string(qrjson), h) + h.dismisser = client.NewSession(string(qrjson), h) if bindingHandler != nil { - h.dismisser = &dismisser bindingHandler(h) } @@ -262,8 +261,8 @@ func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string return sessionHelperWithFrontendOptions(t, request, sessiontype, client, nil, nil) } -func extractClientTransport(dismisser *irmaclient.SessionDismisser) *irma.HTTPTransport { - rct := reflect.ValueOf(dismisser).Elem().Elem().Elem().FieldByName("transport") +func extractClientTransport(dismisser irmaclient.SessionDismisser) *irma.HTTPTransport { + rct := reflect.ValueOf(dismisser).Elem().FieldByName("transport") return reflect.NewAt(rct.Type(), unsafe.Pointer(rct.UnsafeAddr())).Elem().Interface().(*irma.HTTPTransport) } diff --git a/internal/sessiontest/requestor_test.go b/internal/sessiontest/requestor_test.go index 336868ac3..a9c3d28f1 100644 --- a/internal/sessiontest/requestor_test.go +++ b/internal/sessiontest/requestor_test.go @@ -92,7 +92,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien require.Equal(t, backendToken, serverResult.Token) if opts&sessionOptionRetryPost > 0 { - clientTransport := extractClientTransport(&dismisser) + clientTransport := extractClientTransport(dismisser) var result string err := clientTransport.Post("proofs", &result, h.(*TestHandler).result) require.NoError(t, err) From acdb670913f188ce9dc1b998eabaf8e79f255c8c Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 12:19:13 +0200 Subject: [PATCH 36/77] Refactor BackendToken to RequestorToken --- internal/sessiontest/requestor_test.go | 4 +- irma/cmd/session.go | 12 ++--- messages.go | 2 +- server/api.go | 10 ++-- server/irmac/irmac.go | 18 +++---- server/irmaserver/api.go | 68 +++++++++++++------------- server/irmaserver/handle.go | 4 +- server/irmaserver/helpers.go | 8 +-- server/irmaserver/sessions.go | 50 +++++++++---------- server/requestorserver/server.go | 30 ++++++------ 10 files changed, 103 insertions(+), 103 deletions(-) diff --git a/internal/sessiontest/requestor_test.go b/internal/sessiontest/requestor_test.go index a9c3d28f1..46b9e72bf 100644 --- a/internal/sessiontest/requestor_test.go +++ b/internal/sessiontest/requestor_test.go @@ -57,7 +57,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien clientChan := make(chan *SessionResult, 2) serverChan := make(chan *server.SessionResult) - qr, backendToken, _, err := irmaServer.StartSession(request, func(result *server.SessionResult) { + qr, requestorToken, _, err := irmaServer.StartSession(request, func(result *server.SessionResult) { serverChan <- result }) require.NoError(t, err) @@ -89,7 +89,7 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien } serverResult := <-serverChan - require.Equal(t, backendToken, serverResult.Token) + require.Equal(t, requestorToken, serverResult.Token) if opts&sessionOptionRetryPost > 0 { clientTransport := extractClientTransport(dismisser) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 5235062e8..532b747ed 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -110,7 +110,7 @@ func libraryRequest( // Start the session resultchan := make(chan *server.SessionResult) - qr, backendToken, _, err := irmaServer.StartSession(request, func(r *server.SessionResult) { + qr, requestorToken, _, err := irmaServer.StartSession(request, func(r *server.SessionResult) { resultchan <- r }) if err != nil { @@ -122,7 +122,7 @@ func libraryRequest( if binding { optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = irma.BindingMethodPin - if sessionOptions, err = irmaServer.SetFrontendOptions(backendToken, &optionsRequest); err != nil { + if sessionOptions, err = irmaServer.SetFrontendOptions(requestorToken, &optionsRequest); err != nil { return nil, errors.WrapPrefix(err, "IRMA enable binding failed", 0) } } @@ -138,7 +138,7 @@ func libraryRequest( go func() { var status irma.ServerStatus for { - newStatus := irmaServer.GetSessionResult(backendToken).Status + newStatus := irmaServer.GetSessionResult(requestorToken).Status if newStatus != status { status = newStatus statuschan <- status @@ -151,7 +151,7 @@ func libraryRequest( }() _, err = handleBinding(sessionOptions, statuschan, func() error { - return irmaServer.BindingCompleted(backendToken) + return irmaServer.BindingCompleted(requestorToken) }) if err != nil { return nil, errors.WrapPrefix(err, "Failed to handle binding", 0) @@ -285,8 +285,8 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth return nil, "", transport, err } - backendToken := pkg.Token - transport.Server += fmt.Sprintf("session/%s/", backendToken) + requestorToken := pkg.Token + transport.Server += fmt.Sprintf("session/%s/", requestorToken) return pkg.SessionPtr, pkg.FrontendToken, transport, err } diff --git a/messages.go b/messages.go index 464c76dca..c1fc758bf 100644 --- a/messages.go +++ b/messages.go @@ -169,7 +169,7 @@ type Qr struct { } // Tokens to identify a session from the perspective of the different agents -type BackendToken string +type RequestorToken string type ClientToken string type FrontendToken string diff --git a/server/api.go b/server/api.go index 002120ed4..761b489b8 100644 --- a/server/api.go +++ b/server/api.go @@ -26,15 +26,15 @@ import ( var Logger *logrus.Logger = logrus.StandardLogger() type SessionPackage struct { - SessionPtr *irma.Qr `json:"sessionPtr"` - Token irma.BackendToken `json:"token"` - FrontendToken irma.FrontendToken `json:"frontendToken"` + SessionPtr *irma.Qr `json:"sessionPtr"` + Token irma.RequestorToken `json:"token"` + FrontendToken irma.FrontendToken `json:"frontendToken"` } // SessionResult contains session information such as the session status, type, possible errors, // and disclosed attributes or attribute-based signature if appropriate to the session type. type SessionResult struct { - Token irma.BackendToken `json:"token"` + Token irma.RequestorToken `json:"token"` Status irma.ServerStatus `json:"status"` Type irma.Action `json:"type"` ProofStatus irma.ProofStatus `json:"proofStatus,omitempty"` @@ -56,7 +56,7 @@ type LogOptions struct { // Remove this when dropping support for legacy pre-condiscon session requests type LegacySessionResult struct { - Token irma.BackendToken `json:"token"` + Token irma.RequestorToken `json:"token"` Status irma.ServerStatus `json:"status"` Type irma.Action `json:"type"` ProofStatus irma.ProofStatus `json:"proofStatus,omitempty"` diff --git a/server/irmac/irmac.go b/server/irmac/irmac.go index 0ac2ffa27..f5a48a635 100644 --- a/server/irmac/irmac.go +++ b/server/irmac/irmac.go @@ -50,10 +50,10 @@ func Initialize(IrmaConfiguration *C.char) *C.char { func StartSession(requestString *C.char) (r *C.char) { // Create struct for return information result := struct { - IrmaQr string - BackendToken string - FrontendToken string - Error string + IrmaQr string + RequestorToken string + FrontendToken string + Error string }{} defer func() { j, _ := json.Marshal(result) @@ -67,7 +67,7 @@ func StartSession(requestString *C.char) (r *C.char) { } // Run the actual core function - qr, backendToken, frontendToken, err := s.StartSession(C.GoString(requestString), nil) + qr, requestorToken, frontendToken, err := s.StartSession(C.GoString(requestString), nil) // And properly return the result if err != nil { @@ -81,7 +81,7 @@ func StartSession(requestString *C.char) (r *C.char) { } // return actual results result.IrmaQr = string(qrJson) - result.BackendToken = string(backendToken) + result.RequestorToken = string(requestorToken) result.FrontendToken = string(frontendToken) return } @@ -94,7 +94,7 @@ func GetSessionResult(token *C.char) *C.char { } // Run the actual core function - result := s.GetSessionResult(irma.BackendToken(C.GoString(token))) + result := s.GetSessionResult(irma.RequestorToken(C.GoString(token))) // And properly return results if result == nil { @@ -116,7 +116,7 @@ func GetRequest(token *C.char) *C.char { } // Run the core function - result := s.GetRequest(irma.BackendToken(C.GoString(token))) + result := s.GetRequest(irma.RequestorToken(C.GoString(token))) // And properly return results if result == nil { @@ -138,7 +138,7 @@ func CancelSession(token *C.char) *C.char { } // Run the core function - err := s.CancelSession(irma.BackendToken(C.GoString(token))) + err := s.CancelSession(irma.RequestorToken(C.GoString(token))) if err != nil { return C.CString(err.Error()) diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 1a5f06b65..978a80467 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -23,7 +23,7 @@ type Server struct { sessions sessionStore scheduler *gocron.Scheduler stopScheduler chan bool - handlers map[irma.BackendToken]server.SessionHandler + handlers map[irma.RequestorToken]server.SessionHandler serverSentEvents *sse.Server } @@ -50,11 +50,11 @@ func New(conf *server.Configuration) (*Server, error) { conf: conf, scheduler: gocron.NewScheduler(), sessions: &memorySessionStore{ - requestor: make(map[irma.BackendToken]*session), + requestor: make(map[irma.RequestorToken]*session), client: make(map[irma.ClientToken]*session), conf: conf, }, - handlers: make(map[irma.BackendToken]server.SessionHandler), + handlers: make(map[irma.RequestorToken]server.SessionHandler), serverSentEvents: e, } @@ -158,17 +158,17 @@ func (s *Server) Stop() { } // StartSession starts an IRMA session, running the handler on completion, if specified. -// The session backendToken (the second return parameter) can be used in GetSessionResult() +// The session requestorToken (the second return parameter) can be used in GetSessionResult() // and CancelSession(). The session frontendToken (the third return parameter) is needed // by frontend clients (i.e. browser libraries) to POST to the '/options' endpoint of the IRMA protocol. // The request parameter can be an irma.RequestorRequest, or an irma.SessionRequest, or a // ([]byte or string) JSON representation of one of those (for more details, see server.ParseSessionRequest().) func StartSession(request interface{}, handler server.SessionHandler, -) (*irma.Qr, irma.BackendToken, irma.FrontendToken, error) { +) (*irma.Qr, irma.RequestorToken, irma.FrontendToken, error) { return s.StartSession(request, handler) } func (s *Server) StartSession(req interface{}, handler server.SessionHandler, -) (*irma.Qr, irma.BackendToken, irma.FrontendToken, error) { +) (*irma.Qr, irma.RequestorToken, irma.FrontendToken, error) { rrequest, err := server.ParseSessionRequest(req) if err != nil { return nil, "", "", err @@ -195,55 +195,55 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, request.Base().DevelopmentMode = !s.conf.Production session := s.newSession(action, rrequest) - s.conf.Logger.WithFields(logrus.Fields{"action": action, "session": session.backendToken}).Infof("Session started") + s.conf.Logger.WithFields(logrus.Fields{"action": action, "session": session.requestorToken}).Infof("Session started") if s.conf.Logger.IsLevelEnabled(logrus.DebugLevel) { - s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken, "clienttoken": session.clientToken}).Info("Session request: ", server.ToJson(rrequest)) + s.conf.Logger.WithFields(logrus.Fields{"session": session.requestorToken, "clienttoken": session.clientToken}).Info("Session request: ", server.ToJson(rrequest)) } else { - s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Info("Session request (purged of attribute values): ", server.ToJson(purgeRequest(rrequest))) + s.conf.Logger.WithFields(logrus.Fields{"session": session.requestorToken}).Info("Session request (purged of attribute values): ", server.ToJson(purgeRequest(rrequest))) } if handler != nil { - s.handlers[session.backendToken] = handler + s.handlers[session.requestorToken] = handler } return &irma.Qr{ Type: action, URL: s.conf.URL + "session/" + string(session.clientToken), - }, session.backendToken, session.frontendToken, nil + }, session.requestorToken, session.frontendToken, nil } // GetSessionResult retrieves the result of the specified IRMA session. -func GetSessionResult(backendToken irma.BackendToken) *server.SessionResult { - return s.GetSessionResult(backendToken) +func GetSessionResult(requestorToken irma.RequestorToken) *server.SessionResult { + return s.GetSessionResult(requestorToken) } -func (s *Server) GetSessionResult(backendToken irma.BackendToken) *server.SessionResult { - session := s.sessions.get(backendToken) +func (s *Server) GetSessionResult(requestorToken irma.RequestorToken) *server.SessionResult { + session := s.sessions.get(requestorToken) if session == nil { - s.conf.Logger.Warn("Session result requested of unknown session ", backendToken) + s.conf.Logger.Warn("Session result requested of unknown session ", requestorToken) return nil } return session.result } // GetRequest retrieves the request submitted by the requestor that started the specified IRMA session. -func GetRequest(token irma.BackendToken) irma.RequestorRequest { +func GetRequest(token irma.RequestorToken) irma.RequestorRequest { return s.GetRequest(token) } -func (s *Server) GetRequest(backendToken irma.BackendToken) irma.RequestorRequest { - session := s.sessions.get(backendToken) +func (s *Server) GetRequest(requestorToken irma.RequestorToken) irma.RequestorRequest { + session := s.sessions.get(requestorToken) if session == nil { - s.conf.Logger.Warn("Session request requested of unknown session ", backendToken) + s.conf.Logger.Warn("Session request requested of unknown session ", requestorToken) return nil } return session.rrequest } // CancelSession cancels the specified IRMA session. -func CancelSession(backendToken irma.BackendToken) error { - return s.CancelSession(backendToken) +func CancelSession(requestorToken irma.RequestorToken) error { + return s.CancelSession(requestorToken) } -func (s *Server) CancelSession(backendToken irma.BackendToken) error { - session := s.sessions.get(backendToken) +func (s *Server) CancelSession(requestorToken irma.RequestorToken) error { + session := s.sessions.get(requestorToken) if session == nil { - return server.LogError(errors.Errorf("can't cancel unknown session %s", backendToken)) + return server.LogError(errors.Errorf("can't cancel unknown session %s", requestorToken)) } session.handleDelete() return nil @@ -253,21 +253,21 @@ func (s *Server) CancelSession(backendToken irma.BackendToken) error { // Returns the updated session options struct. Frontend options can only be // changed when the client is not connected yet. Otherwise an error is returned. // Options that are not specified in the request, keep their old value. -func SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { - return s.SetFrontendOptions(backendToken, request) +func SetFrontendOptions(requestorToken irma.RequestorToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { + return s.SetFrontendOptions(requestorToken, request) } -func (s *Server) SetFrontendOptions(backendToken irma.BackendToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { - session := s.sessions.get(backendToken) +func (s *Server) SetFrontendOptions(requestorToken irma.RequestorToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { + session := s.sessions.get(requestorToken) return session.updateFrontendOptions(request) } // Complete binding between the irma client and the frontend. Returns // an error when no client is actually connected. -func BindingCompleted(backendToken irma.BackendToken) error { - return s.BindingCompleted(backendToken) +func BindingCompleted(requestorToken irma.RequestorToken) error { + return s.BindingCompleted(requestorToken) } -func (s *Server) BindingCompleted(backendToken irma.BackendToken) error { - session := s.sessions.get(backendToken) +func (s *Server) BindingCompleted(requestorToken irma.RequestorToken) error { + session := s.sessions.get(requestorToken) return session.bindingCompleted() } @@ -299,7 +299,7 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques var session *session if requestor { - session = s.sessions.get(irma.BackendToken(token)) + session = s.sessions.get(irma.RequestorToken(token)) } else { session = s.sessions.clientGet(irma.ClientToken(token)) } diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index bda8e7a25..a29e1a956 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -29,7 +29,7 @@ func (session *session) handleDelete() { } session.markAlive() - session.result = &server.SessionResult{Token: session.backendToken, Status: irma.ServerStatusCancelled, Type: session.action} + session.result = &server.SessionResult{Token: session.requestorToken, Status: irma.ServerStatusCancelled, Type: session.action} session.setStatus(irma.ServerStatusCancelled) } @@ -41,7 +41,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c } session.markAlive() - logger := session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}) + logger := session.conf.Logger.WithFields(logrus.Fields{"session": session.requestorToken}) var err error if session.version, err = session.chooseProtocolVersion(min, max); err != nil { diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 3b54a8b48..d59260e02 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -31,11 +31,11 @@ import ( func (session *session) markAlive() { session.lastActive = time.Now() - session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Debugf("Session marked active, expiry delayed") + session.conf.Logger.WithFields(logrus.Fields{"session": session.requestorToken}).Debugf("Session marked active, expiry delayed") } func (session *session) setStatus(status irma.ServerStatus) { - session.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken, "prevStatus": session.prevStatus, "status": status}). + session.conf.Logger.WithFields(logrus.Fields{"session": session.requestorToken, "prevStatus": session.prevStatus, "status": status}). Info("Session status updated") session.status = status session.result.Status = status @@ -49,7 +49,7 @@ func (session *session) onUpdate() { session.sse.SendMessage("session/"+string(session.clientToken), sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)), ) - session.sse.SendMessage("session/"+string(session.backendToken), + session.sse.SendMessage("session/"+string(session.requestorToken), sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)), ) } @@ -83,7 +83,7 @@ func (session *session) bindingCompleted() error { func (session *session) fail(err server.Error, message string) *irma.RemoteError { rerr := server.RemoteError(err, message) session.setStatus(irma.ServerStatusCancelled) - session.result = &server.SessionResult{Err: rerr, Token: session.backendToken, Status: irma.ServerStatusCancelled, Type: session.action} + session.result = &server.SessionResult{Err: rerr, Token: session.requestorToken, Status: irma.ServerStatusCancelled, Type: session.action} return rerr } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index a2f122c8d..063496a05 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -20,7 +20,7 @@ type session struct { locked bool action irma.Action - backendToken irma.BackendToken + requestorToken irma.RequestorToken clientToken irma.ClientToken frontendToken irma.FrontendToken version *irma.ProtocolVersion @@ -54,7 +54,7 @@ type responseCache struct { } type sessionStore interface { - get(token irma.BackendToken) *session + get(token irma.RequestorToken) *session clientGet(token irma.ClientToken) *session add(session *session) update(session *session) @@ -66,7 +66,7 @@ type memorySessionStore struct { sync.RWMutex conf *server.Configuration - requestor map[irma.BackendToken]*session + requestor map[irma.RequestorToken]*session client map[irma.ClientToken]*session } @@ -79,7 +79,7 @@ var ( maxProtocolVersion = irma.NewVersion(2, 7) ) -func (s *memorySessionStore) get(t irma.BackendToken) *session { +func (s *memorySessionStore) get(t irma.RequestorToken) *session { s.RLock() defer s.RUnlock() return s.requestor[t] @@ -94,7 +94,7 @@ func (s *memorySessionStore) clientGet(t irma.ClientToken) *session { func (s *memorySessionStore) add(session *session) { s.Lock() defer s.Unlock() - s.requestor[session.backendToken] = session + s.requestor[session.requestorToken] = session s.client[session.clientToken] = session } @@ -107,7 +107,7 @@ func (s *memorySessionStore) stop() { defer s.Unlock() for _, session := range s.requestor { if session.sse != nil { - session.sse.CloseChannel("session/" + string(session.backendToken)) + session.sse.CloseChannel("session/" + string(session.requestorToken)) session.sse.CloseChannel("session/" + string(session.clientToken)) } } @@ -117,13 +117,13 @@ func (s *memorySessionStore) deleteExpired() { // First check which sessions have expired // We don't need a write lock for this yet, so postpone that for actual deleting s.RLock() - toCheck := make(map[irma.BackendToken]*session, len(s.requestor)) + toCheck := make(map[irma.RequestorToken]*session, len(s.requestor)) for token, session := range s.requestor { toCheck[token] = session } s.RUnlock() - expired := make([]irma.BackendToken, 0, len(toCheck)) + expired := make([]irma.RequestorToken, 0, len(toCheck)) for token, session := range toCheck { session.Lock() timeout := maxSessionLifetime @@ -133,11 +133,11 @@ func (s *memorySessionStore) deleteExpired() { if session.lastActive.Add(timeout).Before(time.Now()) { if !session.status.Finished() { - s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Infof("Session expired") + s.conf.Logger.WithFields(logrus.Fields{"session": session.requestorToken}).Infof("Session expired") session.markAlive() session.setStatus(irma.ServerStatusTimeout) } else { - s.conf.Logger.WithFields(logrus.Fields{"session": session.backendToken}).Infof("Deleting session") + s.conf.Logger.WithFields(logrus.Fields{"session": session.requestorToken}).Infof("Deleting session") expired = append(expired, token) } } @@ -149,7 +149,7 @@ func (s *memorySessionStore) deleteExpired() { for _, token := range expired { session := s.requestor[token] if session.sse != nil { - session.sse.CloseChannel("session/" + string(session.backendToken)) + session.sse.CloseChannel("session/" + string(session.requestorToken)) session.sse.CloseChannel("session/" + string(session.clientToken)) } delete(s.client, session.clientToken) @@ -162,15 +162,15 @@ var one *big.Int = big.NewInt(1) func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) *session { clientToken := irma.ClientToken(common.NewSessionToken()) - backendToken := irma.BackendToken(common.NewSessionToken()) + requestorToken := irma.RequestorToken(common.NewSessionToken()) frontendToken := irma.FrontendToken(common.NewSessionToken()) base := request.SessionRequest().Base() if s.conf.AugmentClientReturnURL && base.AugmentReturnURL && base.ClientReturnURL != "" { if strings.Contains(base.ClientReturnURL, "?") { - base.ClientReturnURL += "&token=" + string(backendToken) + base.ClientReturnURL += "&token=" + string(requestorToken) } else { - base.ClientReturnURL += "?token=" + string(backendToken) + base.ClientReturnURL += "?token=" + string(requestorToken) } } @@ -182,24 +182,24 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) * LDContext: irma.LDContextSessionOptions, BindingMethod: irma.BindingMethodNone, }, - lastActive: time.Now(), - backendToken: backendToken, - clientToken: clientToken, - frontendToken: frontendToken, - status: irma.ServerStatusInitialized, - prevStatus: irma.ServerStatusInitialized, - conf: s.conf, - sessions: s.sessions, - sse: s.serverSentEvents, + lastActive: time.Now(), + requestorToken: requestorToken, + clientToken: clientToken, + frontendToken: frontendToken, + status: irma.ServerStatusInitialized, + prevStatus: irma.ServerStatusInitialized, + conf: s.conf, + sessions: s.sessions, + sse: s.serverSentEvents, result: &server.SessionResult{ LegacySession: request.SessionRequest().Base().Legacy(), - Token: backendToken, + Token: requestorToken, Type: action, Status: irma.ServerStatusInitialized, }, } - s.conf.Logger.WithFields(logrus.Fields{"session": ses.backendToken}).Debug("New session started") + s.conf.Logger.WithFields(logrus.Fields{"session": ses.requestorToken}).Debug("New session started") nonce, _ := gabi.GenerateNonce() base.Nonce = nonce base.Context = one diff --git a/server/requestorserver/server.go b/server/requestorserver/server.go index 4ffc41d4d..80d3af2b0 100644 --- a/server/requestorserver/server.go +++ b/server/requestorserver/server.go @@ -208,7 +208,7 @@ func (s *Server) Handler() http.Handler { // Server routes r.Route("/session", func(r chi.Router) { r.Post("/", s.handleCreateSession) - r.Route("/{backendToken}", func(r chi.Router) { + r.Route("/{requestorToken}", func(r chi.Router) { r.Delete("/", s.handleDelete) r.Get("/status", s.handleStatus) r.Get("/statusevents", s.handleStatusEvents) @@ -308,7 +308,7 @@ func (s *Server) handleRevocation(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { - res := s.irmaserv.GetSessionResult(irma.BackendToken(chi.URLParam(r, "backendToken"))) + res := s.irmaserv.GetSessionResult(irma.RequestorToken(chi.URLParam(r, "requestorToken"))) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") return @@ -317,13 +317,13 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleStatusEvents(w http.ResponseWriter, r *http.Request) { - backendToken := chi.URLParam(r, "backendToken") - s.conf.Logger.WithFields(logrus.Fields{"session": backendToken}).Debug("new client subscribed to server sent events") + requestorToken := chi.URLParam(r, "requestorToken") + s.conf.Logger.WithFields(logrus.Fields{"session": requestorToken}).Debug("new client subscribed to server sent events") r = r.WithContext(context.WithValue(r.Context(), "sse", common.SSECtx{ Component: server.ComponentSession, - Arg: backendToken, + Arg: requestorToken, })) - if err := s.irmaserv.SubscribeServerSentEvents(w, r, backendToken, true); err != nil { + if err := s.irmaserv.SubscribeServerSentEvents(w, r, requestorToken, true); err != nil { server.WriteResponse(w, nil, &irma.RemoteError{ Status: server.ErrorUnsupported.Status, ErrorName: string(server.ErrorUnsupported.Type), @@ -333,14 +333,14 @@ func (s *Server) handleStatusEvents(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) { - err := s.irmaserv.CancelSession(irma.BackendToken(chi.URLParam(r, "backendToken"))) + err := s.irmaserv.CancelSession(irma.RequestorToken(chi.URLParam(r, "requestorToken"))) if err != nil { server.WriteError(w, server.ErrorSessionUnknown, "") } } func (s *Server) handleResult(w http.ResponseWriter, r *http.Request) { - res := s.irmaserv.GetSessionResult(irma.BackendToken(chi.URLParam(r, "backendToken"))) + res := s.irmaserv.GetSessionResult(irma.RequestorToken(chi.URLParam(r, "requestorToken"))) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") return @@ -359,8 +359,8 @@ func (s *Server) handleJwtResult(w http.ResponseWriter, r *http.Request) { return } - backendToken := irma.BackendToken(chi.URLParam(r, "backendToken")) - res := s.irmaserv.GetSessionResult(backendToken) + requestorToken := irma.RequestorToken(chi.URLParam(r, "requestorToken")) + res := s.irmaserv.GetSessionResult(requestorToken) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") return @@ -387,8 +387,8 @@ func (s *Server) handleJwtProofs(w http.ResponseWriter, r *http.Request) { return } - backendToken := irma.BackendToken(chi.URLParam(r, "backendToken")) - res := s.irmaserv.GetSessionResult(backendToken) + requestorToken := irma.RequestorToken(chi.URLParam(r, "requestorToken")) + res := s.irmaserv.GetSessionResult(requestorToken) if res == nil { server.WriteError(w, server.ErrorSessionUnknown, "") return @@ -413,7 +413,7 @@ func (s *Server) handleJwtProofs(w http.ResponseWriter, r *http.Request) { claims["iss"] = s.conf.JwtIssuer } claims["status"] = res.ProofStatus - validity := s.irmaserv.GetRequest(backendToken).Base().ResultJwtValidity + validity := s.irmaserv.GetRequest(requestorToken).Base().ResultJwtValidity if validity != 0 { claims["exp"] = time.Now().Unix() + int64(validity) } @@ -518,7 +518,7 @@ func (s *Server) createSession(w http.ResponseWriter, requestor string, rrequest } // Everything is authenticated and parsed, we're good to go! - qr, backendToken, frontendToken, err := s.irmaserv.StartSession(rrequest, s.doResultCallback) + qr, requestorToken, frontendToken, err := s.irmaserv.StartSession(rrequest, s.doResultCallback) if err != nil { server.WriteError(w, server.ErrorInvalidRequest, err.Error()) return @@ -526,7 +526,7 @@ func (s *Server) createSession(w http.ResponseWriter, requestor string, rrequest server.WriteJson(w, server.SessionPackage{ SessionPtr: qr, - Token: backendToken, + Token: requestorToken, FrontendToken: frontendToken, }) } From 736c713bf5f6e1de4b60b194b9a0a3c94f3a71fb Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 12:35:53 +0200 Subject: [PATCH 37/77] Refactor FrontendToken to FrontendAuthorization --- internal/sessiontest/main_test.go | 8 ++++---- irma/cmd/session.go | 8 ++++---- messages.go | 2 +- server/api.go | 6 +++--- server/irmac/irmac.go | 6 +++--- server/irmaserver/api.go | 8 ++++---- server/irmaserver/helpers.go | 4 ++-- server/irmaserver/sessions.go | 6 +++--- server/requestorserver/server.go | 8 ++++---- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 4458f988b..78f87ae4c 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -131,7 +131,7 @@ func getMultipleIssuanceRequest() *irma.IssuanceRequest { var TestType = "irmaserver-jwt" -func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) (*server.SessionPackage, irma.FrontendToken) { +func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) (*server.SessionPackage, irma.FrontendAuthorization) { var ( sesPkg server.SessionPackage err error @@ -152,7 +152,7 @@ func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) } require.NoError(t, err) - return &sesPkg, sesPkg.FrontendToken + return &sesPkg, sesPkg.FrontendAuth } func getJwt(t *testing.T, request irma.SessionRequest, sessiontype string, alg jwt.SigningMethod) string { @@ -219,7 +219,7 @@ func sessionHelperWithFrontendOptions( defer StopRequestorServer() } - sesPkg, frontendToken := startSession(t, request, sessiontype) + sesPkg, frontendAuth := startSession(t, request, sessiontype) c := make(chan *SessionResult) h := &TestHandler{ @@ -232,7 +232,7 @@ func sessionHelperWithFrontendOptions( if frontendOptionsHandler != nil || bindingHandler != nil { h.bindingCodeChan = make(chan string) h.frontendTransport = irma.NewHTTPTransport(sesPkg.SessionPtr.URL, false) - h.frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendToken)) + h.frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendAuth)) } if frontendOptionsHandler != nil { frontendOptionsHandler(h) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 532b747ed..1d82f845e 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -171,7 +171,7 @@ func serverRequest( logger.Debug("Server URL: ", serverurl) // Start session at server - qr, frontendToken, transport, err := postRequest(serverurl, request, name, authmethod, key) + qr, frontendAuth, transport, err := postRequest(serverurl, request, name, authmethod, key) if err != nil { return nil, err } @@ -181,7 +181,7 @@ func serverRequest( sessionOptions := &irma.SessionOptions{} if binding { frontendTransport = irma.NewHTTPTransport(qr.URL, false) - frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendToken)) + frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendAuth)) optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = irma.BindingMethodPin err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) @@ -256,7 +256,7 @@ func serverRequest( } func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string) ( - *irma.Qr, irma.FrontendToken, *irma.HTTPTransport, error) { + *irma.Qr, irma.FrontendAuthorization, *irma.HTTPTransport, error) { var ( err error pkg = &server.SessionPackage{} @@ -288,7 +288,7 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth requestorToken := pkg.Token transport.Server += fmt.Sprintf("session/%s/", requestorToken) - return pkg.SessionPtr, pkg.FrontendToken, transport, err + return pkg.SessionPtr, pkg.FrontendAuth, transport, err } func handleBinding(options *irma.SessionOptions, statusChan chan irma.ServerStatus, completeBinding func() error) ( diff --git a/messages.go b/messages.go index c1fc758bf..979b392f4 100644 --- a/messages.go +++ b/messages.go @@ -171,10 +171,10 @@ type Qr struct { // Tokens to identify a session from the perspective of the different agents type RequestorToken string type ClientToken string -type FrontendToken string // Authorization headers type ClientAuthorization string +type FrontendAuthorization string // Client statuses const ( diff --git a/server/api.go b/server/api.go index 761b489b8..83225415c 100644 --- a/server/api.go +++ b/server/api.go @@ -26,9 +26,9 @@ import ( var Logger *logrus.Logger = logrus.StandardLogger() type SessionPackage struct { - SessionPtr *irma.Qr `json:"sessionPtr"` - Token irma.RequestorToken `json:"token"` - FrontendToken irma.FrontendToken `json:"frontendToken"` + SessionPtr *irma.Qr `json:"sessionPtr"` + Token irma.RequestorToken `json:"token"` + FrontendAuth irma.FrontendAuthorization `json:"frontendAuth"` } // SessionResult contains session information such as the session status, type, possible errors, diff --git a/server/irmac/irmac.go b/server/irmac/irmac.go index f5a48a635..46bad0fbe 100644 --- a/server/irmac/irmac.go +++ b/server/irmac/irmac.go @@ -52,7 +52,7 @@ func StartSession(requestString *C.char) (r *C.char) { result := struct { IrmaQr string RequestorToken string - FrontendToken string + FrontendAuth string Error string }{} defer func() { @@ -67,7 +67,7 @@ func StartSession(requestString *C.char) (r *C.char) { } // Run the actual core function - qr, requestorToken, frontendToken, err := s.StartSession(C.GoString(requestString), nil) + qr, requestorToken, frontendAuth, err := s.StartSession(C.GoString(requestString), nil) // And properly return the result if err != nil { @@ -82,7 +82,7 @@ func StartSession(requestString *C.char) (r *C.char) { // return actual results result.IrmaQr = string(qrJson) result.RequestorToken = string(requestorToken) - result.FrontendToken = string(frontendToken) + result.FrontendAuth = string(frontendAuth) return } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 978a80467..e9b943ae2 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -159,16 +159,16 @@ func (s *Server) Stop() { // StartSession starts an IRMA session, running the handler on completion, if specified. // The session requestorToken (the second return parameter) can be used in GetSessionResult() -// and CancelSession(). The session frontendToken (the third return parameter) is needed +// and CancelSession(). The session's frontendAuth (the third return parameter) is needed // by frontend clients (i.e. browser libraries) to POST to the '/options' endpoint of the IRMA protocol. // The request parameter can be an irma.RequestorRequest, or an irma.SessionRequest, or a // ([]byte or string) JSON representation of one of those (for more details, see server.ParseSessionRequest().) func StartSession(request interface{}, handler server.SessionHandler, -) (*irma.Qr, irma.RequestorToken, irma.FrontendToken, error) { +) (*irma.Qr, irma.RequestorToken, irma.FrontendAuthorization, error) { return s.StartSession(request, handler) } func (s *Server) StartSession(req interface{}, handler server.SessionHandler, -) (*irma.Qr, irma.RequestorToken, irma.FrontendToken, error) { +) (*irma.Qr, irma.RequestorToken, irma.FrontendAuthorization, error) { rrequest, err := server.ParseSessionRequest(req) if err != nil { return nil, "", "", err @@ -207,7 +207,7 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, return &irma.Qr{ Type: action, URL: s.conf.URL + "session/" + string(session.clientToken), - }, session.requestorToken, session.frontendToken, nil + }, session.requestorToken, session.frontendAuth, nil } // GetSessionResult retrieves the result of the specified IRMA session. diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index d59260e02..be2a4dc25 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -440,9 +440,9 @@ func errorWriter(err *irma.RemoteError, writer func(w http.ResponseWriter, objec func (s *Server) frontendMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - frontendToken := irma.FrontendToken(r.Header.Get(irma.AuthorizationHeader)) + frontendAuth := irma.FrontendAuthorization(r.Header.Get(irma.AuthorizationHeader)) - if frontendToken != session.frontendToken { + if frontendAuth != session.frontendAuth { server.WriteError(w, server.ErrorUnauthorized, "") return } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index 063496a05..a1f80aab1 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -22,7 +22,7 @@ type session struct { action irma.Action requestorToken irma.RequestorToken clientToken irma.ClientToken - frontendToken irma.FrontendToken + frontendAuth irma.FrontendAuthorization version *irma.ProtocolVersion rrequest irma.RequestorRequest request irma.SessionRequest @@ -163,7 +163,7 @@ var one *big.Int = big.NewInt(1) func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) *session { clientToken := irma.ClientToken(common.NewSessionToken()) requestorToken := irma.RequestorToken(common.NewSessionToken()) - frontendToken := irma.FrontendToken(common.NewSessionToken()) + frontendAuth := irma.FrontendAuthorization(common.NewSessionToken()) base := request.SessionRequest().Base() if s.conf.AugmentClientReturnURL && base.AugmentReturnURL && base.ClientReturnURL != "" { @@ -185,7 +185,7 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) * lastActive: time.Now(), requestorToken: requestorToken, clientToken: clientToken, - frontendToken: frontendToken, + frontendAuth: frontendAuth, status: irma.ServerStatusInitialized, prevStatus: irma.ServerStatusInitialized, conf: s.conf, diff --git a/server/requestorserver/server.go b/server/requestorserver/server.go index 80d3af2b0..3dba59551 100644 --- a/server/requestorserver/server.go +++ b/server/requestorserver/server.go @@ -518,16 +518,16 @@ func (s *Server) createSession(w http.ResponseWriter, requestor string, rrequest } // Everything is authenticated and parsed, we're good to go! - qr, requestorToken, frontendToken, err := s.irmaserv.StartSession(rrequest, s.doResultCallback) + qr, requestorToken, frontendAuth, err := s.irmaserv.StartSession(rrequest, s.doResultCallback) if err != nil { server.WriteError(w, server.ErrorInvalidRequest, err.Error()) return } server.WriteJson(w, server.SessionPackage{ - SessionPtr: qr, - Token: requestorToken, - FrontendToken: frontendToken, + SessionPtr: qr, + Token: requestorToken, + FrontendAuth: frontendAuth, }) } From a056cf3eb0221b19a8503fcc33bd1aa5cd20a23c Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 12:41:11 +0200 Subject: [PATCH 38/77] Improve error handling of SetFrontendOptions and BindingCompleted --- server/irmaserver/api.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index e9b943ae2..3e027bcba 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -258,6 +258,9 @@ func SetFrontendOptions(requestorToken irma.RequestorToken, request *irma.Option } func (s *Server) SetFrontendOptions(requestorToken irma.RequestorToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { session := s.sessions.get(requestorToken) + if session == nil { + return nil, server.LogError(errors.Errorf("can't set frontend options of unknown session %s", requestorToken)) + } return session.updateFrontendOptions(request) } @@ -268,6 +271,9 @@ func BindingCompleted(requestorToken irma.RequestorToken) error { } func (s *Server) BindingCompleted(requestorToken irma.RequestorToken) error { session := s.sessions.get(requestorToken) + if session == nil { + return server.LogError(errors.Errorf("can't complete binding of unknown session %s", requestorToken)) + } return session.bindingCompleted() } From 43e0692dca4e5e068471a7a936800f0505e76eec Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 12:43:48 +0200 Subject: [PATCH 39/77] Make comment on ServerStatusBinding more clear --- messages.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages.go b/messages.go index 979b392f4..35905abf5 100644 --- a/messages.go +++ b/messages.go @@ -186,7 +186,7 @@ const ( // Server statuses const ( ServerStatusInitialized ServerStatus = "INITIALIZED" // The session has been started and is waiting for the client - ServerStatusBinding ServerStatus = "BINDING" // The client is binding, waiting for the frontend to accept + ServerStatusBinding ServerStatus = "BINDING" // The client is waiting for the frontend to give permission to connect ServerStatusConnected ServerStatus = "CONNECTED" // The client has retrieved the session request, we wait for its response ServerStatusCancelled ServerStatus = "CANCELLED" // The session is cancelled, possibly due to an error ServerStatusDone ServerStatus = "DONE" // The session has completed successfully From ffbddf83d692b5c5533e80336ec2eec10e854922 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 12:46:59 +0200 Subject: [PATCH 40/77] Use early return in bindingHandler --- internal/sessiontest/session_test.go | 37 +++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index eef0dda9f..ac61302e4 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -145,24 +145,27 @@ func TestIssuanceBinding(t *testing.T) { bindingCode = setBindingMethod(irma.BindingMethodPin, handler) } bindingHandler := func(handler *TestHandler) { - if extractClientMaxVersion(handler.client).Above(2, 6) { - require.Equal(t, bindingCode, <-handler.bindingCodeChan) - - // Check whether access to request endpoint is denied as long as binding is not finished - clientTransport := extractClientTransport(handler.dismisser) - err := clientTransport.Get("request", struct{}{}) - require.Error(t, err) - require.Equal(t, 403, err.(*irma.SessionError).RemoteStatus) - - // Check whether binding cannot be disabled again after client is connected. - request := irma.NewOptionsRequest() - result := &irma.SessionOptions{} - err = handler.frontendTransport.Post("frontend/options", result, request) - require.Error(t, err) - - err = handler.frontendTransport.Post("frontend/bindingcompleted", nil, nil) - require.NoError(handler.t, err) + // Below protocol version 2.7 binding is not supported, so then the binding stage is expected to be skipped. + if extractClientMaxVersion(handler.client).Below(2, 7) { + return } + + require.Equal(t, bindingCode, <-handler.bindingCodeChan) + + // Check whether access to request endpoint is denied as long as binding is not finished + clientTransport := extractClientTransport(handler.dismisser) + err := clientTransport.Get("request", struct{}{}) + require.Error(t, err) + require.Equal(t, 403, err.(*irma.SessionError).RemoteStatus) + + // Check whether binding cannot be disabled again after client is connected. + request := irma.NewOptionsRequest() + result := &irma.SessionOptions{} + err = handler.frontendTransport.Post("frontend/options", result, request) + require.Error(t, err) + + err = handler.frontendTransport.Post("frontend/bindingcompleted", nil, nil) + require.NoError(handler.t, err) } sessionHelperWithFrontendOptions(t, request, "issue", nil, frontendOptionsHandler, bindingHandler) } From cd3d9fb47207215a970ad8861913a7987b96b861 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 13:53:58 +0200 Subject: [PATCH 41/77] Check error type in TestIssuanceBinding --- internal/sessiontest/session_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index ac61302e4..9300e809c 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -156,7 +156,10 @@ func TestIssuanceBinding(t *testing.T) { clientTransport := extractClientTransport(handler.dismisser) err := clientTransport.Get("request", struct{}{}) require.Error(t, err) - require.Equal(t, 403, err.(*irma.SessionError).RemoteStatus) + sessionErr := err.(*irma.SessionError) + require.Equal(t, irma.ErrorApi, sessionErr.ErrorType) + require.Equal(t, server.ErrorBindingRequired.Status, sessionErr.RemoteError.Status) + require.Equal(t, string(server.ErrorBindingRequired.Type), sessionErr.RemoteError.ErrorName) // Check whether binding cannot be disabled again after client is connected. request := irma.NewOptionsRequest() From 8e9ea56a5f9cafa98f0b64e2718d878685fcdfe3 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 15:08:20 +0200 Subject: [PATCH 42/77] Implement Validate method for ClientRequest --- irmaclient/session.go | 8 ++++---- requests.go | 27 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/irmaclient/session.go b/irmaclient/session.go index 02194bb30..f75c3b56b 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -258,19 +258,19 @@ func (session *session) getSessionInfo() { session.Handler.StatusUpdate(session.Action, irma.ClientStatusCommunicating) // Get the first IRMA protocol message and parse it - info := &irma.ClientRequest{ + cr := &irma.ClientRequest{ Request: session.request, // As request is an interface, it need to be initialized with a specific instance. } // UnmarshalJSON of ClientRequest takes into account legacy protocols, so we do not have to check that here. - err := session.transport.Get("", info) + err := session.transport.Get("", cr) if err != nil { session.fail(err.(*irma.SessionError)) return } // Check whether binding is needed, and if so, wait for it to be completed. - if info.Options.BindingMethod != irma.BindingMethodNone { - if err = session.handleBinding(info.Options.BindingCode); err != nil { + if cr.Options.BindingMethod != irma.BindingMethodNone { + if err = session.handleBinding(cr.Options.BindingCode); err != nil { session.fail(err.(*irma.SessionError)) return } diff --git a/requests.go b/requests.go index aae95c8c8..10edd8102 100644 --- a/requests.go +++ b/requests.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "fmt" "io/ioutil" + "reflect" "strconv" "time" @@ -1131,27 +1132,37 @@ func NewOptionsRequest() OptionsRequest { } } -func (info *ClientRequest) UnmarshalJSON(data []byte) error { +func (cr *ClientRequest) UnmarshalJSON(data []byte) error { // Unmarshal in alias first to prevent infinite recursion type alias ClientRequest - err := json.Unmarshal(data, (*alias)(info)) + err := json.Unmarshal(data, (*alias)(cr)) if err != nil { return err } - if info.LDContext == LDContextClientRequest { + if cr.LDContext == LDContextClientRequest { return nil } - // For legacy sessions initialize session info by hand using the fetched request - err = json.Unmarshal(data, info.Request) + // For legacy sessions initialize client request by hand using the fetched request + err = json.Unmarshal(data, cr.Request) if err != nil { return err } - info.LDContext = LDContextClientRequest - info.ProtocolVersion = info.Request.Base().ProtocolVersion - info.Options = &SessionOptions{ + cr.LDContext = LDContextClientRequest + cr.ProtocolVersion = cr.Request.Base().ProtocolVersion + cr.Options = &SessionOptions{ LDContext: LDContextSessionOptions, BindingMethod: BindingMethodNone, } return nil } + +func (cr *ClientRequest) Validate() error { + if cr.LDContext != LDContextClientRequest { + return errors.New("Not a client request") + } + if !reflect.ValueOf(cr.Request).Elem().IsZero() { + return cr.Request.Validate() + } + return nil +} From 96f7b875974ed396edbfb69e866c1cf7e6208b0d Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 15:23:34 +0200 Subject: [PATCH 43/77] In cmd's postRequest return nil for transport when returning an error --- irma/cmd/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 1d82f845e..e4f59c20f 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -282,7 +282,7 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth } if err != nil { - return nil, "", transport, err + return nil, "", nil, err } requestorToken := pkg.Token From ded859f8dc0dbf7abccae85b3e6b54f98559081e Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 16:26:54 +0200 Subject: [PATCH 44/77] Add library call to irma server to subscribe on statuses using channels --- irma/cmd/session.go | 22 +++++----------------- server/irmaserver/api.go | 24 ++++++++++++++++++++++++ server/irmaserver/helpers.go | 11 +++++++++++ server/irmaserver/sessions.go | 11 ++++++----- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index e4f59c20f..e09114da3 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -8,7 +8,6 @@ import ( "regexp" "strconv" "sync" - "time" "github.com/go-errors/errors" irma "github.com/privacybydesign/irmago" @@ -134,21 +133,10 @@ func libraryRequest( if binding { // Listen for session status - statuschan := make(chan irma.ServerStatus) - go func() { - var status irma.ServerStatus - for { - newStatus := irmaServer.GetSessionResult(requestorToken).Status - if newStatus != status { - status = newStatus - statuschan <- status - if status.Finished() { - return - } - } - time.Sleep(500 * time.Millisecond) - } - }() + statuschan, err := irmaServer.GetSessionStatus(string(requestorToken), true) + if err != nil { + return nil, errors.WrapPrefix(err, "Failed to start listening for session statuses", 0) + } _, err = handleBinding(sessionOptions, statuschan, func() error { return irmaServer.BindingCompleted(requestorToken) @@ -310,7 +298,7 @@ func handleBinding(options *irma.SessionOptions, statusChan chan irma.ServerStat } return status, nil case err := <-errorChan: - return status, err + return "", err } } } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 3e027bcba..4ab1da8e3 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -329,3 +329,27 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques s.serverSentEvents.ServeHTTP(w, r) return nil } + +// GetSessionStatus retrieves a channel on which the current session status of the specified +// IRMA session can be retrieved. +func GetSessionStatus(token string, requestor bool) (chan irma.ServerStatus, error) { + return s.GetSessionStatus(token, requestor) +} +func (s *Server) GetSessionStatus(token string, requestor bool) (chan irma.ServerStatus, error) { + var session *session + if requestor { + session = s.sessions.get(irma.RequestorToken(token)) + } else { + session = s.sessions.clientGet(irma.ClientToken(token)) + } + if session == nil { + return nil, server.LogError(errors.Errorf("can't get session status of unknown session %s", token)) + } + + statusChan := make(chan irma.ServerStatus) + go func() { + statusChan <- session.status + }() + session.statusChannels = append(session.statusChannels, statusChan) + return statusChan, nil +} diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index be2a4dc25..4e2241f4e 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -43,6 +43,17 @@ func (session *session) setStatus(status irma.ServerStatus) { } func (session *session) onUpdate() { + // Perform sending to channels async to make sure session handling + // will not hang if channel is not (immediately) read. + for _, statusChan := range session.statusChannels { + go func(statusChan chan irma.ServerStatus, status irma.ServerStatus) { + statusChan <- status + if status.Finished() { + close(statusChan) + } + }(statusChan, session.status) + } + if session.sse == nil { return } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index a1f80aab1..c9d2cd15e 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -29,11 +29,12 @@ type session struct { legacyCompatible bool // if the request is convertible to pre-condiscon format implicitDisclosure irma.AttributeConDisCon - options irma.SessionOptions - status irma.ServerStatus - prevStatus irma.ServerStatus - sse *sse.Server - responseCache responseCache + options irma.SessionOptions + status irma.ServerStatus + prevStatus irma.ServerStatus + sse *sse.Server + statusChannels []chan irma.ServerStatus + responseCache responseCache clientAuth irma.ClientAuthorization lastActive time.Time From d33e94777848ac326b4fd1e12d6e08235aae91fe Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 4 Sep 2020 16:40:50 +0200 Subject: [PATCH 45/77] Fixed ServerStatus refactor in TestIssueExpiredKey From 7bd962d4e58b31068e734c0d17521dc52c7cc577 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Sun, 6 Sep 2020 22:40:00 +0200 Subject: [PATCH 46/77] Fixed spelling mistake in comment getSessionInfo --- irmaclient/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irmaclient/session.go b/irmaclient/session.go index f75c3b56b..f432a260c 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -259,7 +259,7 @@ func (session *session) getSessionInfo() { // Get the first IRMA protocol message and parse it cr := &irma.ClientRequest{ - Request: session.request, // As request is an interface, it need to be initialized with a specific instance. + Request: session.request, // As request is an interface, it needs to be initialized with a specific instance. } // UnmarshalJSON of ClientRequest takes into account legacy protocols, so we do not have to check that here. err := session.transport.Get("", cr) From 491b5577507cae89d49b784b109e8ae1cc31fced Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Sun, 6 Sep 2020 23:01:51 +0200 Subject: [PATCH 47/77] Fail if client diverts from protocol in handleGetClientRequest --- server/irmaserver/handle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index a29e1a956..d20ae795d 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -51,7 +51,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c // Protocol versions below 2.7 don't include an authorization header. Therefore skip the authorization // header presence check if a lower version is used. if clientAuth == "" && session.version.Above(2, 6) { - return nil, server.RemoteError(server.ErrorClientUnauthorized, "No authorization header provided") + return nil, session.fail(server.ErrorClientUnauthorized, "No authorization header provided") } session.clientAuth = clientAuth From 85d66d8236a1ab113a8c8c4b53ba0b8603e4096e Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Sun, 6 Sep 2020 23:05:30 +0200 Subject: [PATCH 48/77] Revert earlier change of returning pointers to session request in handleGetClientRequest --- server/irmaserver/handle.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index d20ae795d..411b1679b 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -81,7 +81,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c if session.version.Below(2, 5) { logger.Info("Returning legacy session format") legacy.Base().ProtocolVersion = session.version - return &legacy, nil + return legacy, nil } if session.version.Below(2, 7) { @@ -90,7 +90,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c if err != nil { return nil, session.fail(server.ErrorRevocation, err.Error()) } - return &request, nil + return request, nil } info, err := session.getClientRequest() if err != nil { From e6ced4888bca8db7b309b3b7a950cc5211d643fe Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 29 Sep 2020 15:52:09 +0200 Subject: [PATCH 49/77] Make GetSessionStatus to only accept requestor tokens --- irma/cmd/session.go | 2 +- server/irmaserver/api.go | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index e09114da3..580268cbe 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -133,7 +133,7 @@ func libraryRequest( if binding { // Listen for session status - statuschan, err := irmaServer.GetSessionStatus(string(requestorToken), true) + statuschan, err := irmaServer.GetSessionStatus(requestorToken) if err != nil { return nil, errors.WrapPrefix(err, "Failed to start listening for session statuses", 0) } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 4ab1da8e3..217c411a3 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -332,18 +332,13 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques // GetSessionStatus retrieves a channel on which the current session status of the specified // IRMA session can be retrieved. -func GetSessionStatus(token string, requestor bool) (chan irma.ServerStatus, error) { - return s.GetSessionStatus(token, requestor) +func GetSessionStatus(requestorToken irma.RequestorToken) (chan irma.ServerStatus, error) { + return s.GetSessionStatus(requestorToken) } -func (s *Server) GetSessionStatus(token string, requestor bool) (chan irma.ServerStatus, error) { - var session *session - if requestor { - session = s.sessions.get(irma.RequestorToken(token)) - } else { - session = s.sessions.clientGet(irma.ClientToken(token)) - } +func (s *Server) GetSessionStatus(requestorToken irma.RequestorToken) (chan irma.ServerStatus, error) { + session := s.sessions.get(requestorToken) if session == nil { - return nil, server.LogError(errors.Errorf("can't get session status of unknown session %s", token)) + return nil, server.LogError(errors.Errorf("can't get session status of unknown session %s", requestorToken)) } statusChan := make(chan irma.ServerStatus) From c9aa64bb7a6e36f6d32ed32771f410304bd5148c Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 29 Sep 2020 17:30:33 +0200 Subject: [PATCH 50/77] Add comment to explain use of reflection in ClientRequest's Validate --- requests.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/requests.go b/requests.go index 10edd8102..1b48b4a56 100644 --- a/requests.go +++ b/requests.go @@ -1161,6 +1161,10 @@ func (cr *ClientRequest) Validate() error { if cr.LDContext != LDContextClientRequest { return errors.New("Not a client request") } + // The 'Request' field is not required. When this field is empty, we have to skip the validation. + // In Go empty structs are automatically populated with default values and we cannot solve this + // by using a pointer because SessionRequest is an interface. Therefore we use reflection to + // check whether the struct that implements the interface is empty. if !reflect.ValueOf(cr.Request).Elem().IsZero() { return cr.Request.Validate() } From 88e6ed29b60d5fc53d92c8e46a269bb8a68d79ca Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 6 Oct 2020 09:52:09 +0200 Subject: [PATCH 51/77] Use buffered channel for status updates --- server/irmaserver/api.go | 6 ++---- server/irmaserver/helpers.go | 13 +++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 217c411a3..0ed42b528 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -341,10 +341,8 @@ func (s *Server) GetSessionStatus(requestorToken irma.RequestorToken) (chan irma return nil, server.LogError(errors.Errorf("can't get session status of unknown session %s", requestorToken)) } - statusChan := make(chan irma.ServerStatus) - go func() { - statusChan <- session.status - }() + statusChan := make(chan irma.ServerStatus, 4) + statusChan <- session.status session.statusChannels = append(session.statusChannels, statusChan) return statusChan, nil } diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 4e2241f4e..58f83041d 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -43,15 +43,12 @@ func (session *session) setStatus(status irma.ServerStatus) { } func (session *session) onUpdate() { - // Perform sending to channels async to make sure session handling - // will not hang if channel is not (immediately) read. + // Send status update to all listener channels for _, statusChan := range session.statusChannels { - go func(statusChan chan irma.ServerStatus, status irma.ServerStatus) { - statusChan <- status - if status.Finished() { - close(statusChan) - } - }(statusChan, session.status) + statusChan <- session.status + if session.status.Finished() { + close(statusChan) + } } if session.sse == nil { From 2010853835573b85ee6495f8a2df2dd12f74f0e1 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 6 Oct 2020 09:58:21 +0200 Subject: [PATCH 52/77] Test for specific error when disabling binding after client connected --- internal/sessiontest/session_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index 9300e809c..fc1b85ed9 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -166,6 +166,10 @@ func TestIssuanceBinding(t *testing.T) { result := &irma.SessionOptions{} err = handler.frontendTransport.Post("frontend/options", result, request) require.Error(t, err) + sessionErr = err.(*irma.SessionError) + require.Equal(t, irma.ErrorApi, sessionErr.ErrorType) + require.Equal(t, server.ErrorUnexpectedRequest.Status, sessionErr.RemoteError.Status) + require.Equal(t, string(server.ErrorUnexpectedRequest.Type), sessionErr.RemoteError.ErrorName) err = handler.frontendTransport.Post("frontend/bindingcompleted", nil, nil) require.NoError(handler.t, err) From 57c03d92181afebfe9bcc553c4468441cf92ff03 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 7 Oct 2020 01:47:04 +0200 Subject: [PATCH 53/77] Some more code style fixes --- internal/sessiontest/requestor_test.go | 1 - internal/sessiontest/session_test.go | 2 +- irma/cmd/session.go | 9 +++----- irmaclient/session.go | 3 +-- requests.go | 6 ++--- server/irmaserver/api.go | 2 +- wait_status.go | 32 ++++++++++++-------------- 7 files changed, 24 insertions(+), 31 deletions(-) diff --git a/internal/sessiontest/requestor_test.go b/internal/sessiontest/requestor_test.go index 46b9e72bf..4653ae781 100644 --- a/internal/sessiontest/requestor_test.go +++ b/internal/sessiontest/requestor_test.go @@ -77,7 +77,6 @@ func requestorSessionHelper(t *testing.T, request interface{}, client *irmaclien j, err := json.Marshal(qr) require.NoError(t, err) dismisser := client.NewSession(string(j), h) - clientResult := <-clientChan if opts&sessionOptionIgnoreError == 0 && clientResult != nil { require.NoError(t, clientResult.Err) diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index fc1b85ed9..6197111f6 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -172,7 +172,7 @@ func TestIssuanceBinding(t *testing.T) { require.Equal(t, string(server.ErrorUnexpectedRequest.Type), sessionErr.RemoteError.ErrorName) err = handler.frontendTransport.Post("frontend/bindingcompleted", nil, nil) - require.NoError(handler.t, err) + require.NoError(t, err) } sessionHelperWithFrontendOptions(t, request, "issue", nil, frontendOptionsHandler, bindingHandler) } diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 580268cbe..985b10eeb 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -122,7 +122,7 @@ func libraryRequest( optionsRequest := irma.NewOptionsRequest() optionsRequest.BindingMethod = irma.BindingMethodPin if sessionOptions, err = irmaServer.SetFrontendOptions(requestorToken, &optionsRequest); err != nil { - return nil, errors.WrapPrefix(err, "IRMA enable binding failed", 0) + return nil, errors.WrapPrefix(err, "Failed to enable binding", 0) } } @@ -273,20 +273,17 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth return nil, "", nil, err } - requestorToken := pkg.Token - transport.Server += fmt.Sprintf("session/%s/", requestorToken) - + transport.Server += fmt.Sprintf("session/%s/", pkg.Token) return pkg.SessionPtr, pkg.FrontendAuth, transport, err } func handleBinding(options *irma.SessionOptions, statusChan chan irma.ServerStatus, completeBinding func() error) ( irma.ServerStatus, error) { errorChan := make(chan error) - status := irma.ServerStatusInitialized bindingStarted := false for { select { - case status = <-statusChan: + case status := <-statusChan: if status == irma.ServerStatusInitialized { continue } else if status == irma.ServerStatusBinding { diff --git a/irmaclient/session.go b/irmaclient/session.go index f432a260c..f9b520645 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -234,9 +234,8 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { session.transport.SetHeader(irma.MaxVersionHeader, client.maxVersion.String()) // From protocol version 2.7 also an authorization header must be included. - clientAuth := "" if client.maxVersion.Above(2, 6) { - clientAuth = common.NewSessionToken() + clientAuth := common.NewSessionToken() session.transport.SetHeader(irma.AuthorizationHeader, clientAuth) } diff --git a/requests.go b/requests.go index 1b48b4a56..402752609 100644 --- a/requests.go +++ b/requests.go @@ -1162,9 +1162,9 @@ func (cr *ClientRequest) Validate() error { return errors.New("Not a client request") } // The 'Request' field is not required. When this field is empty, we have to skip the validation. - // In Go empty structs are automatically populated with default values and we cannot solve this - // by using a pointer because SessionRequest is an interface. Therefore we use reflection to - // check whether the struct that implements the interface is empty. + // We cannot detect this easily, because in Go empty structs are automatically populated with + // default values. We can also not use a pointer reference because SessionRequest is an interface. + // Therefore we use reflection to check whether the struct that implements the interface is empty. if !reflect.ValueOf(cr.Request).Elem().IsZero() { return cr.Request.Validate() } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 0ed42b528..50a77954d 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -160,7 +160,7 @@ func (s *Server) Stop() { // StartSession starts an IRMA session, running the handler on completion, if specified. // The session requestorToken (the second return parameter) can be used in GetSessionResult() // and CancelSession(). The session's frontendAuth (the third return parameter) is needed -// by frontend clients (i.e. browser libraries) to POST to the '/options' endpoint of the IRMA protocol. +// by frontend clients (i.e. browser libraries) to POST to the '/frontend' endpoints of the IRMA protocol. // The request parameter can be an irma.RequestorRequest, or an irma.SessionRequest, or a // ([]byte or string) JSON representation of one of those (for more details, see server.ParseSessionRequest().) func StartSession(request interface{}, handler server.SessionHandler, diff --git a/wait_status.go b/wait_status.go index 87810e757..3bf5471f4 100644 --- a/wait_status.go +++ b/wait_status.go @@ -54,26 +54,24 @@ func subscribeSSE(transport *HTTPTransport, statuschan chan ServerStatus, errorc // poll recursively polls the session status until a final status is received. func poll(transport *HTTPTransport, initialStatus ServerStatus, statuschan chan ServerStatus, errorchan chan error) { - go func() { - status := initialStatus - for { - statuschanPolling := make(chan ServerStatus) - errorchanPolling := make(chan error) - go pollUntilChange(transport, status, statuschanPolling, errorchanPolling) - select { - case status = <-statuschanPolling: - statuschan <- status - if status.Finished() { - errorchan <- nil - return - } - break - case err := <-errorchanPolling: - errorchan <- err + status := initialStatus + statuschanPolling := make(chan ServerStatus) + errorchanPolling := make(chan error) + for { + go pollUntilChange(transport, status, statuschanPolling, errorchanPolling) + select { + case status = <-statuschanPolling: + statuschan <- status + if status.Finished() { + errorchan <- nil return } + break + case err := <-errorchanPolling: + errorchan <- err + return } - }() + } } func pollUntilChange(transport *HTTPTransport, initialStatus ServerStatus, statuschan chan ServerStatus, errorchan chan error) { From d8845700885036c59616d27f29b29e3187033d5e Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 7 Oct 2020 09:17:25 +0200 Subject: [PATCH 54/77] Fixed WaitStatus spawning multiple nil's on error channel --- wait_status.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/wait_status.go b/wait_status.go index 3bf5471f4..ae7eab9aa 100644 --- a/wait_status.go +++ b/wait_status.go @@ -57,8 +57,8 @@ func poll(transport *HTTPTransport, initialStatus ServerStatus, statuschan chan status := initialStatus statuschanPolling := make(chan ServerStatus) errorchanPolling := make(chan error) + go pollUntilChange(transport, status, statuschanPolling, errorchanPolling) for { - go pollUntilChange(transport, status, statuschanPolling, errorchanPolling) select { case status = <-statuschanPolling: statuschan <- status @@ -68,8 +68,11 @@ func poll(transport *HTTPTransport, initialStatus ServerStatus, statuschan chan } break case err := <-errorchanPolling: - errorchan <- err - return + if err != nil { + errorchan <- err + return + } + go pollUntilChange(transport, status, statuschanPolling, errorchanPolling) } } } From f8f66270ac4a70c468f3b470892d89b0a13c4fea Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 8 Oct 2020 16:34:56 +0200 Subject: [PATCH 55/77] Remove code duplication in extracting private fields using reflection --- internal/sessiontest/main_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 78f87ae4c..d45a3ee41 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -262,13 +262,16 @@ func sessionHelper(t *testing.T, request irma.SessionRequest, sessiontype string } func extractClientTransport(dismisser irmaclient.SessionDismisser) *irma.HTTPTransport { - rct := reflect.ValueOf(dismisser).Elem().FieldByName("transport") - return reflect.NewAt(rct.Type(), unsafe.Pointer(rct.UnsafeAddr())).Elem().Interface().(*irma.HTTPTransport) + return extractPrivateField(dismisser, "transport").(*irma.HTTPTransport) } func extractClientMaxVersion(client *irmaclient.Client) *irma.ProtocolVersion { - rmv := reflect.ValueOf(client).Elem().FieldByName("maxVersion") - return reflect.NewAt(rmv.Type(), unsafe.Pointer(rmv.UnsafeAddr())).Elem().Interface().(*irma.ProtocolVersion) + return extractPrivateField(client, "maxVersion").(*irma.ProtocolVersion) +} + +func extractPrivateField(i interface{}, field string) interface{} { + rct := reflect.ValueOf(i).Elem().FieldByName(field) + return reflect.NewAt(rct.Type(), unsafe.Pointer(rct.UnsafeAddr())).Elem().Interface() } func setBindingMethod(method irma.BindingMethod, handler *TestHandler) string { From 0452d4152d15ba7bf0c9592b51a3f86ee4853cb6 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Fri, 9 Oct 2020 14:19:01 +0200 Subject: [PATCH 56/77] refactor: rename GetSessionStatus server function to SessionStatus --- irma/cmd/session.go | 2 +- server/irmaserver/api.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 985b10eeb..df90d8d58 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -133,7 +133,7 @@ func libraryRequest( if binding { // Listen for session status - statuschan, err := irmaServer.GetSessionStatus(requestorToken) + statuschan, err := irmaServer.SessionStatus(requestorToken) if err != nil { return nil, errors.WrapPrefix(err, "Failed to start listening for session statuses", 0) } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 50a77954d..cf5a41c6f 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -330,12 +330,12 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques return nil } -// GetSessionStatus retrieves a channel on which the current session status of the specified +// SessionStatus retrieves a channel on which the current session status of the specified // IRMA session can be retrieved. -func GetSessionStatus(requestorToken irma.RequestorToken) (chan irma.ServerStatus, error) { - return s.GetSessionStatus(requestorToken) +func SessionStatus(requestorToken irma.RequestorToken) (chan irma.ServerStatus, error) { + return s.SessionStatus(requestorToken) } -func (s *Server) GetSessionStatus(requestorToken irma.RequestorToken) (chan irma.ServerStatus, error) { +func (s *Server) SessionStatus(requestorToken irma.RequestorToken) (chan irma.ServerStatus, error) { session := s.sessions.get(requestorToken) if session == nil { return nil, server.LogError(errors.Errorf("can't get session status of unknown session %s", requestorToken)) From b784f64ef3d21269b9d7d65f4cfa75a7bedb7df5 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Wed, 14 Oct 2020 10:00:35 +0200 Subject: [PATCH 57/77] Updated CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 359e8a607..c73ce6d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.0] - 2021-03-17 +### Added +* Support for session binding to prevent shoulder surfing (i.e. make it impossible for someone in close physical proximity to a user to scan the QR code that was meant for the user) + * Introduced new frontend endpoints to manage session binding + * The API of the `requestorserver` package has two new functions `SetFrontendOptions` and `BindingCompleted` + * A new server status `"BINDING"` is introduced +* A new function `SessionStatus` is available in the API of the `requestorserver` to get a channel with status updates of an IRMA session + +### Changes +* The `irma.SessionPackage` struct now contains an extra field `FrontendAuth` +* The `StartSession` function from the API of the `requestorserver` package now returns three values: the session pointer (type *irma.QR), the requestor token (type irma.RequestorToken) and the frontend authorization token (type irma.FrontendAuthorization) +* The `token` parameter, as used by most functions in the API of the `requestorserver` package, now has the type `irma.RequestorToken` +* The `server.Status` type has been moved to `irma.ServerStatus`; the related constants are also moved, e.g. from `server.StatusInitialized` to `irma.ServerStatusInitialized` + ## [0.7.0] - 2021-03-17 ### Fixed * Bug causing scheme updating to fail if OS temp dir is on other file system than the schemes @@ -155,6 +169,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Combined issuance-disclosure requests with two schemes one of which has a keyshare server now work as expected - Various other bugfixes +[0.8.0]: https://github.com/privacybydesign/irmago/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/privacybydesign/irmago/compare/v0.6.1...v0.7.0 [0.6.1]: https://github.com/privacybydesign/irmago/compare/v0.6.0...v0.6.1 [0.6.0]: https://github.com/privacybydesign/irmago/compare/v0.5.1...v0.6.0 From c976adad2f68a3f7dafcccf38d35f438c754a018 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 23 Oct 2020 16:24:58 +0200 Subject: [PATCH 58/77] Fixed handleBinding wrapping a SessionError in a SessionError --- irmaclient/session.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/irmaclient/session.go b/irmaclient/session.go index f9b520645..88b58544c 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -293,6 +293,9 @@ func (session *session) handleBinding(bindingCode string) error { return &irma.SessionError{ErrorType: irma.ErrorBindingRejected} } case err := <-errorchan: + if serr, ok := err.(*irma.SessionError); ok { + return serr + } return &irma.SessionError{ ErrorType: irma.ErrorServerResponse, Info: "Binding aborted by server", From f44f55726a80b0d2e1aab5e1ce72c998ba41de02 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 5 Nov 2020 18:38:12 +0100 Subject: [PATCH 59/77] Refactor: rename binding to pairing --- CHANGELOG.md | 8 +-- internal/common/common.go | 8 +-- internal/sessiontest/handlers_test.go | 12 ++--- internal/sessiontest/legacy_test.go | 6 +-- internal/sessiontest/main_test.go | 16 +++--- internal/sessiontest/session_test.go | 30 +++++------ irma/cmd/session.go | 76 +++++++++++++-------------- irmaclient/handlers.go | 4 +- irmaclient/session.go | 20 +++---- messages.go | 6 +-- requests.go | 16 +++--- server/errors.go | 4 +- server/irmaserver/api.go | 16 +++--- server/irmaserver/handle.go | 10 ++-- server/irmaserver/helpers.go | 34 ++++++------ server/irmaserver/sessions.go | 2 +- 16 files changed, 134 insertions(+), 134 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c73ce6d9d..23c785f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.8.0] - 2021-03-17 ### Added -* Support for session binding to prevent shoulder surfing (i.e. make it impossible for someone in close physical proximity to a user to scan the QR code that was meant for the user) - * Introduced new frontend endpoints to manage session binding - * The API of the `requestorserver` package has two new functions `SetFrontendOptions` and `BindingCompleted` - * A new server status `"BINDING"` is introduced +* Support for device pairing to prevent shoulder surfing (i.e. make it impossible for someone in close physical proximity to a user to scan the QR code that was meant for the user) + * Introduced new frontend endpoints to manage device pairing + * The API of the `requestorserver` package has two new functions `SetFrontendOptions` and `PairingCompleted` + * A new server status `"PAIRING"` is introduced * A new function `SessionStatus` is available in the API of the `requestorserver` to get a channel with status updates of an IRMA session ### Changes diff --git a/internal/common/common.go b/internal/common/common.go index 3bd8454d0..b1a707363 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -27,8 +27,8 @@ var ForceHTTPS = true const ( sessionChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" sessionTokenLength = 20 - bindingCodeChars = "0123456789" - bindingCodeLength = 4 + pairingCodeChars = "0123456789" + pairingCodeLength = 4 ) // AssertPathExists returns nil only if it has been successfully @@ -278,8 +278,8 @@ func NewSessionToken() string { return newRandomString(sessionTokenLength, sessionChars) } -func NewBindingCode() string { - return newRandomString(bindingCodeLength, bindingCodeChars) +func NewPairingCode() string { + return newRandomString(pairingCodeLength, pairingCodeChars) } func newRandomString(count int, characterSet string) string { diff --git a/internal/sessiontest/handlers_test.go b/internal/sessiontest/handlers_test.go index 3d9f8bf00..7acd3b7fa 100644 --- a/internal/sessiontest/handlers_test.go +++ b/internal/sessiontest/handlers_test.go @@ -82,7 +82,7 @@ type TestHandler struct { expectedServerName *irma.RequestorInfo wait time.Duration result string - bindingCodeChan chan string + pairingCodeChan chan string dismisser irmaclient.SessionDismisser frontendTransport *irma.HTTPTransport } @@ -153,14 +153,14 @@ func (th TestHandler) RequestSchemeManagerPermission(manager *irma.SchemeManager func (th TestHandler) RequestPin(remainingAttempts int, callback irmaclient.PinHandler) { callback(true, "12345") } -func (th TestHandler) BindingRequired(bindingCode string) { - // Send binding code via channel to calling test. This is done such that +func (th TestHandler) PairingRequired(pairingCode string) { + // Send pairing code via channel to calling test. This is done such that // calling tests can detect it when this handler is skipped unexpectedly. - if th.bindingCodeChan != nil { - th.bindingCodeChan <- bindingCode + if th.pairingCodeChan != nil { + th.pairingCodeChan <- pairingCode return } - th.Failure(&irma.SessionError{ErrorType: irma.ErrorType("Binding required")}) + th.Failure(&irma.SessionError{ErrorType: irma.ErrorType("Pairing required")}) } type SessionResult struct { diff --git a/internal/sessiontest/legacy_test.go b/internal/sessiontest/legacy_test.go index 89c3d3cab..171c09762 100644 --- a/internal/sessiontest/legacy_test.go +++ b/internal/sessiontest/legacy_test.go @@ -35,7 +35,7 @@ func TestSessionUsingLegacyStorage(t *testing.T) { sessionHelper(t, getDisclosureRequest(idRoot), "verification", client) } -func TestWithoutBindingSupport(t *testing.T) { +func TestWithoutPairingSupport(t *testing.T) { defaultMaxVersion := maxClientVersion defer func() { maxClientVersion = defaultMaxVersion @@ -53,7 +53,7 @@ func TestWithoutBindingSupport(t *testing.T) { t.Run("TestIssuanceOptionalZeroLengthAttributes", TestIssuanceOptionalZeroLengthAttributes) t.Run("TestIssuanceOptionalSetAttributes", TestIssuanceOptionalSetAttributes) t.Run("TestIssuanceSameAttributesNotSingleton", TestIssuanceSameAttributesNotSingleton) - t.Run("TestIssuanceBinding", TestIssuanceBinding) + t.Run("TestIssuancePairing", TestIssuancePairing) t.Run("TestLargeAttribute", TestLargeAttribute) t.Run("TestIssuanceSingletonCredential", TestIssuanceSingletonCredential) t.Run("TestUnsatisfiableDisclosureSession", TestUnsatisfiableDisclosureSession) @@ -68,5 +68,5 @@ func TestWithoutBindingSupport(t *testing.T) { t.Run("TestStaticQRSession", TestStaticQRSession) t.Run("TestIssuedCredentialIsStored", TestIssuedCredentialIsStored) t.Run("TestPOSTSizeLimit", TestPOSTSizeLimit) - t.Run("TestDisableBinding", TestDisableBinding) + t.Run("TestDisablePairing", TestDisablePairing) } diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index d45a3ee41..e1ebf40ed 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -206,7 +206,7 @@ func sessionHelperWithFrontendOptions( sessiontype string, client *irmaclient.Client, frontendOptionsHandler func(handler *TestHandler), - bindingHandler func(handler *TestHandler), + pairingHandler func(handler *TestHandler), ) string { if client == nil { var handler *TestClientHandler @@ -229,8 +229,8 @@ func sessionHelperWithFrontendOptions( expectedServerName: expectedRequestorInfo(t, client.Configuration), } - if frontendOptionsHandler != nil || bindingHandler != nil { - h.bindingCodeChan = make(chan string) + if frontendOptionsHandler != nil || pairingHandler != nil { + h.pairingCodeChan = make(chan string) h.frontendTransport = irma.NewHTTPTransport(sesPkg.SessionPtr.URL, false) h.frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendAuth)) } @@ -242,8 +242,8 @@ func sessionHelperWithFrontendOptions( require.NoError(t, err) h.dismisser = client.NewSession(string(qrjson), h) - if bindingHandler != nil { - bindingHandler(h) + if pairingHandler != nil { + pairingHandler(h) } if result := <-c; result != nil { @@ -274,13 +274,13 @@ func extractPrivateField(i interface{}, field string) interface{} { return reflect.NewAt(rct.Type(), unsafe.Pointer(rct.UnsafeAddr())).Elem().Interface() } -func setBindingMethod(method irma.BindingMethod, handler *TestHandler) string { +func setPairingMethod(method irma.PairingMethod, handler *TestHandler) string { optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingMethod = method + optionsRequest.PairingMethod = method options := &irma.SessionOptions{} err := handler.frontendTransport.Post("frontend/options", options, optionsRequest) require.NoError(handler.t, err) - return options.BindingCode + return options.PairingCode } func expectedRequestorInfo(t *testing.T, conf *irma.Configuration) *irma.RequestorInfo { diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index 6197111f6..0db0797a8 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -136,32 +136,32 @@ func TestIssuanceSameAttributesNotSingleton(t *testing.T) { require.Equal(t, prevLen+1, len(client.CredentialInfoList())) } -func TestIssuanceBinding(t *testing.T) { +func TestIssuancePairing(t *testing.T) { id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") request := getCombinedIssuanceRequest(id) - var bindingCode string + var pairingCode string frontendOptionsHandler := func(handler *TestHandler) { - bindingCode = setBindingMethod(irma.BindingMethodPin, handler) + pairingCode = setPairingMethod(irma.PairingMethodPin, handler) } - bindingHandler := func(handler *TestHandler) { - // Below protocol version 2.7 binding is not supported, so then the binding stage is expected to be skipped. + pairingHandler := func(handler *TestHandler) { + // Below protocol version 2.7 pairing is not supported, so then the pairing stage is expected to be skipped. if extractClientMaxVersion(handler.client).Below(2, 7) { return } - require.Equal(t, bindingCode, <-handler.bindingCodeChan) + require.Equal(t, pairingCode, <-handler.pairingCodeChan) - // Check whether access to request endpoint is denied as long as binding is not finished + // Check whether access to request endpoint is denied as long as pairing is not finished clientTransport := extractClientTransport(handler.dismisser) err := clientTransport.Get("request", struct{}{}) require.Error(t, err) sessionErr := err.(*irma.SessionError) require.Equal(t, irma.ErrorApi, sessionErr.ErrorType) - require.Equal(t, server.ErrorBindingRequired.Status, sessionErr.RemoteError.Status) - require.Equal(t, string(server.ErrorBindingRequired.Type), sessionErr.RemoteError.ErrorName) + require.Equal(t, server.ErrorPairingRequired.Status, sessionErr.RemoteError.Status) + require.Equal(t, string(server.ErrorPairingRequired.Type), sessionErr.RemoteError.ErrorName) - // Check whether binding cannot be disabled again after client is connected. + // Check whether pairing cannot be disabled again after client is connected. request := irma.NewOptionsRequest() result := &irma.SessionOptions{} err = handler.frontendTransport.Post("frontend/options", result, request) @@ -171,10 +171,10 @@ func TestIssuanceBinding(t *testing.T) { require.Equal(t, server.ErrorUnexpectedRequest.Status, sessionErr.RemoteError.Status) require.Equal(t, string(server.ErrorUnexpectedRequest.Type), sessionErr.RemoteError.ErrorName) - err = handler.frontendTransport.Post("frontend/bindingcompleted", nil, nil) + err = handler.frontendTransport.Post("frontend/pairingcompleted", nil, nil) require.NoError(t, err) } - sessionHelperWithFrontendOptions(t, request, "issue", nil, frontendOptionsHandler, bindingHandler) + sessionHelperWithFrontendOptions(t, request, "issue", nil, frontendOptionsHandler, pairingHandler) } func TestLargeAttribute(t *testing.T) { @@ -671,13 +671,13 @@ func TestChainedSessions(t *testing.T) { require.NoError(t, errors.New("newly issued credential not found in client")) } -func TestDisableBinding(t *testing.T) { +func TestDisablePairing(t *testing.T) { id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") request := getCombinedIssuanceRequest(id) frontendOptionsHandler := func(handler *TestHandler) { - _ = setBindingMethod(irma.BindingMethodPin, handler) - _ = setBindingMethod(irma.BindingMethodNone, handler) + _ = setPairingMethod(irma.PairingMethodPin, handler) + _ = setPairingMethod(irma.PairingMethodNone, handler) } sessionHelperWithFrontendOptions(t, request, "issue", nil, frontendOptionsHandler, nil) } diff --git a/irma/cmd/session.go b/irma/cmd/session.go index df90d8d58..9302d7385 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -62,7 +62,7 @@ irma session --server http://localhost:8088 --authmethod token --key mytoken --d url, _ := cmd.Flags().GetString("url") serverurl, _ := cmd.Flags().GetString("server") noqr, _ := cmd.Flags().GetBool("noqr") - binding, _ := cmd.Flags().GetBool("binding") + pairing, _ := cmd.Flags().GetBool("pairing") if url != defaulturl && serverurl != "" { die("Failed to read configuration", errors.New("--url can't be combined with --server")) @@ -72,12 +72,12 @@ irma session --server http://localhost:8088 --authmethod token --key mytoken --d port, _ := flags.GetInt("port") privatekeysPath, _ := flags.GetString("privkeys") verbosity, _ := cmd.Flags().GetCount("verbose") - result, err = libraryRequest(request, irmaconfig, url, port, privatekeysPath, noqr, verbosity, binding) + result, err = libraryRequest(request, irmaconfig, url, port, privatekeysPath, noqr, verbosity, pairing) } else { authmethod, _ := flags.GetString("authmethod") key, _ := flags.GetString("key") name, _ := flags.GetString("name") - result, err = serverRequest(request, serverurl, authmethod, key, name, noqr, binding) + result, err = serverRequest(request, serverurl, authmethod, key, name, noqr, pairing) } if err != nil { die("Session failed", err) @@ -100,7 +100,7 @@ func libraryRequest( privatekeysPath string, noqr bool, verbosity int, - binding bool, + pairing bool, ) (*server.SessionResult, error) { if err := configureSessionServer(url, port, privatekeysPath, irmaconfig, verbosity); err != nil { return nil, err @@ -116,13 +116,13 @@ func libraryRequest( return nil, errors.WrapPrefix(err, "IRMA session failed", 0) } - // Enable binding if necessary + // Enable pairing if necessary var sessionOptions *irma.SessionOptions - if binding { + if pairing { optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingMethod = irma.BindingMethodPin + optionsRequest.PairingMethod = irma.PairingMethodPin if sessionOptions, err = irmaServer.SetFrontendOptions(requestorToken, &optionsRequest); err != nil { - return nil, errors.WrapPrefix(err, "Failed to enable binding", 0) + return nil, errors.WrapPrefix(err, "Failed to enable pairing", 0) } } @@ -131,18 +131,18 @@ func libraryRequest( return nil, errors.WrapPrefix(err, "Failed to print QR", 0) } - if binding { + if pairing { // Listen for session status statuschan, err := irmaServer.SessionStatus(requestorToken) if err != nil { return nil, errors.WrapPrefix(err, "Failed to start listening for session statuses", 0) } - _, err = handleBinding(sessionOptions, statuschan, func() error { - return irmaServer.BindingCompleted(requestorToken) + _, err = handlePairing(sessionOptions, statuschan, func() error { + return irmaServer.PairingCompleted(requestorToken) }) if err != nil { - return nil, errors.WrapPrefix(err, "Failed to handle binding", 0) + return nil, errors.WrapPrefix(err, "Failed to handle pairing", 0) } } @@ -154,7 +154,7 @@ func serverRequest( request irma.RequestorRequest, serverurl, authmethod, key, name string, noqr bool, - binding bool, + pairing bool, ) (*server.SessionResult, error) { logger.Debug("Server URL: ", serverurl) @@ -164,17 +164,17 @@ func serverRequest( return nil, err } - // Enable binding if necessary + // Enable pairing if necessary var frontendTransport *irma.HTTPTransport sessionOptions := &irma.SessionOptions{} - if binding { + if pairing { frontendTransport = irma.NewHTTPTransport(qr.URL, false) frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendAuth)) optionsRequest := irma.NewOptionsRequest() - optionsRequest.BindingMethod = irma.BindingMethodPin + optionsRequest.PairingMethod = irma.PairingMethodPin err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) if err != nil { - return nil, errors.WrapPrefix(err, "Failed to enable binding", 0) + return nil, errors.WrapPrefix(err, "Failed to enable pairing", 0) } } @@ -201,20 +201,20 @@ func serverRequest( defer wg.Done() var status irma.ServerStatus - if binding { - status, err = handleBinding(sessionOptions, statuschan, func() error { - err = frontendTransport.Post("frontend/bindingcompleted", nil, nil) + if pairing { + status, err = handlePairing(sessionOptions, statuschan, func() error { + err = frontendTransport.Post("frontend/pairingcompleted", nil, nil) if err != nil { - return errors.WrapPrefix(err, "Failed to complete binding", 0) + return errors.WrapPrefix(err, "Failed to complete pairing", 0) } return nil }) if err != nil { - err = errors.WrapPrefix(err, "Failed to handle binding", 0) + err = errors.WrapPrefix(err, "Failed to handle pairing", 0) return } } else { - // Wait until client connects if binding is disabled + // Wait until client connects if pairing is disabled status := <-statuschan if status != irma.ServerStatusConnected { err = errors.Errorf("Unexpected status: %s", status) @@ -277,21 +277,21 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth return pkg.SessionPtr, pkg.FrontendAuth, transport, err } -func handleBinding(options *irma.SessionOptions, statusChan chan irma.ServerStatus, completeBinding func() error) ( +func handlePairing(options *irma.SessionOptions, statusChan chan irma.ServerStatus, completePairing func() error) ( irma.ServerStatus, error) { errorChan := make(chan error) - bindingStarted := false + pairingStarted := false for { select { case status := <-statusChan: if status == irma.ServerStatusInitialized { continue - } else if status == irma.ServerStatusBinding { - bindingStarted = true - go requestBindingPermission(options, completeBinding, errorChan) + } else if status == irma.ServerStatusPairing { + pairingStarted = true + go requestPairingPermission(options, completePairing, errorChan) continue - } else if status == irma.ServerStatusConnected && !bindingStarted { - fmt.Println("Binding is not supported by the connected device.") + } else if status == irma.ServerStatusConnected && !pairingStarted { + fmt.Println("Pairing is not supported by the connected device.") } return status, nil case err := <-errorChan: @@ -300,24 +300,24 @@ func handleBinding(options *irma.SessionOptions, statusChan chan irma.ServerStat } } -func requestBindingPermission(options *irma.SessionOptions, completeBinding func() error, errorChan chan error) { - if options.BindingMethod == irma.BindingMethodPin { - fmt.Println("\nBinding code:", options.BindingCode) - fmt.Println("Press Enter to confirm your device shows the same binding code; otherwise press Ctrl-C.") +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.") _, err := bufio.NewReader(os.Stdin).ReadString('\n') if err != nil { errorChan <- err return } - if err = completeBinding(); err != nil { + if err = completePairing(); err != nil { errorChan <- err return } - fmt.Println("Binding completed.") + fmt.Println("Pairing completed.") errorChan <- nil return } - errorChan <- errors.Errorf("Binding method %s is not supported", options.BindingMethod) + errorChan <- errors.Errorf("Pairing method %s is not supported", options.PairingMethod) } // Configuration functions @@ -370,7 +370,7 @@ func init() { flags.StringP("url", "u", defaulturl, "external URL to which IRMA app connects (when not using --server), \":port\" being replaced by --port value") flags.IntP("port", "p", 48680, "port to listen at (when not using --server)") flags.Bool("noqr", false, "Print JSON instead of draw QR") - flags.Bool("binding", false, "Enable explicit binding between server and IRMA app") + flags.Bool("pairing", false, "Let IRMA app first pair, by entering the pairing code, before it can access the session") flags.StringP("request", "r", "", "JSON session request") flags.StringP("privkeys", "k", "", "path to private keys") flags.Bool("disable-schemes-update", false, "disable scheme updates") diff --git a/irmaclient/handlers.go b/irmaclient/handlers.go index 930927769..d51efa6a8 100644 --- a/irmaclient/handlers.go +++ b/irmaclient/handlers.go @@ -83,6 +83,6 @@ func (h *keyshareEnrollmentHandler) KeyshareEnrollmentMissing(manager irma.Schem func (h *keyshareEnrollmentHandler) ClientReturnURLSet(clientReturnURL string) { h.fail(errors.New("Keyshare enrollment session unexpectedly found an external return url")) } -func (h *keyshareEnrollmentHandler) BindingRequired(bindingCode string) { - h.fail(errors.New("Keyshare enrollment session failed: session binding required")) +func (h *keyshareEnrollmentHandler) PairingRequired(pairingCode string) { + h.fail(errors.New("Keyshare enrollment session failed: device pairing required")) } diff --git a/irmaclient/session.go b/irmaclient/session.go index 88b58544c..8b82f2af2 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -31,7 +31,7 @@ type PinHandler func(proceed bool, pin string) type Handler interface { StatusUpdate(action irma.Action, status irma.ClientStatus) ClientReturnURLSet(clientReturnURL string) - BindingRequired(bindingCode string) + PairingRequired(pairingCode string) Success(result string) Cancelled() Failure(err *irma.SessionError) @@ -250,7 +250,7 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { // Core session methods // getSessionInfo retrieves the first message in the IRMA protocol (only in interactive sessions) -// If needed, it also handles binding. +// If needed, it also handles pairing. func (session *session) getSessionInfo() { defer session.recoverFromPanic() @@ -267,9 +267,9 @@ func (session *session) getSessionInfo() { return } - // Check whether binding is needed, and if so, wait for it to be completed. - if cr.Options.BindingMethod != irma.BindingMethodNone { - if err = session.handleBinding(cr.Options.BindingCode); err != nil { + // Check whether pairing is needed, and if so, wait for it to be completed. + if cr.Options.PairingMethod != irma.PairingMethodNone { + if err = session.handlePairing(cr.Options.PairingCode); err != nil { session.fail(err.(*irma.SessionError)) return } @@ -278,19 +278,19 @@ func (session *session) getSessionInfo() { session.processSessionInfo() } -func (session *session) handleBinding(bindingCode string) error { - session.Handler.BindingRequired(bindingCode) +func (session *session) handlePairing(pairingCode string) error { + session.Handler.PairingRequired(pairingCode) statuschan := make(chan irma.ServerStatus) errorchan := make(chan error) - go irma.WaitStatusChanged(session.transport, irma.ServerStatusBinding, statuschan, errorchan) + go irma.WaitStatusChanged(session.transport, irma.ServerStatusPairing, statuschan, errorchan) select { case status := <-statuschan: if status == irma.ServerStatusConnected { return session.transport.Get("request", session.request) } else { - return &irma.SessionError{ErrorType: irma.ErrorBindingRejected} + return &irma.SessionError{ErrorType: irma.ErrorPairingRejected} } case err := <-errorchan: if serr, ok := err.(*irma.SessionError); ok { @@ -298,7 +298,7 @@ func (session *session) handleBinding(bindingCode string) error { } return &irma.SessionError{ ErrorType: irma.ErrorServerResponse, - Info: "Binding aborted by server", + Info: "Pairing aborted by server", Err: err, } } diff --git a/messages.go b/messages.go index 35905abf5..f1c252895 100644 --- a/messages.go +++ b/messages.go @@ -186,7 +186,7 @@ const ( // Server statuses const ( ServerStatusInitialized ServerStatus = "INITIALIZED" // The session has been started and is waiting for the client - ServerStatusBinding ServerStatus = "BINDING" // The client is waiting for the frontend to give permission to connect + ServerStatusPairing ServerStatus = "PAIRING" // The client is waiting for the frontend to give permission to connect ServerStatusConnected ServerStatus = "CONNECTED" // The client has retrieved the session request, we wait for its response ServerStatusCancelled ServerStatus = "CANCELLED" // The session is cancelled, possibly due to an error ServerStatusDone ServerStatus = "DONE" // The session has completed successfully @@ -219,8 +219,8 @@ const ( ErrorCrypto = ErrorType("crypto") // Error involving revocation or nonrevocation proofs ErrorRevocation = ErrorType("revocation") - // Our binding attempt was rejected by the server - ErrorBindingRejected = ErrorType("bindingRejected") + // Our pairing attempt was rejected by the server + ErrorPairingRejected = ErrorType("pairingRejected") // Server rejected our response (second IRMA message) ErrorRejected = ErrorType("rejected") // (De)serializing of a message failed diff --git a/requests.go b/requests.go index 402752609..ded48a96e 100644 --- a/requests.go +++ b/requests.go @@ -214,17 +214,17 @@ type AttributeRequest struct { NotNull bool `json:"notNull,omitempty"` } -type BindingMethod string +type PairingMethod string const ( - BindingMethodNone = "none" - BindingMethodPin = "pin" + PairingMethodNone = "none" + PairingMethodPin = "pin" ) // An OptionsRequest asks for a options change of a particular session. type OptionsRequest struct { LDContext string `json:"@context,omitempty"` - BindingMethod BindingMethod `json:"bindingMethod"` + PairingMethod PairingMethod `json:"pairingMethod"` } type RevocationRequest struct { @@ -243,8 +243,8 @@ type NonRevocationParameters map[CredentialTypeIdentifier]*NonRevocationRequest type SessionOptions struct { LDContext string `json:"@context,omitempty"` - BindingMethod BindingMethod `json:"bindingMethod"` - BindingCode string `json:"bindingCode,omitempty"` + PairingMethod PairingMethod `json:"pairingMethod"` + PairingCode string `json:"pairingCode,omitempty"` } // ClientRequest contains all information irmaclient needs to know to initiate a session. @@ -1128,7 +1128,7 @@ func NewAttributeRequest(attr string) AttributeRequest { func NewOptionsRequest() OptionsRequest { return OptionsRequest{ LDContext: LDContextOptionsRequest, - BindingMethod: BindingMethodNone, + PairingMethod: PairingMethodNone, } } @@ -1152,7 +1152,7 @@ func (cr *ClientRequest) UnmarshalJSON(data []byte) error { cr.ProtocolVersion = cr.Request.Base().ProtocolVersion cr.Options = &SessionOptions{ LDContext: LDContextSessionOptions, - BindingMethod: BindingMethodNone, + PairingMethod: PairingMethodNone, } return nil } diff --git a/server/errors.go b/server/errors.go index 9265481bf..54caf2176 100644 --- a/server/errors.go +++ b/server/errors.go @@ -19,8 +19,8 @@ var ( ErrorAttributesWrong Error = Error{Type: "ATTRIBUTES_WRONG", Status: 400, Description: "Specified attribute(s) do not belong to this credential type or missing attributes"} ErrorCannotIssue Error = Error{Type: "CANNOT_ISSUE", Status: 500, Description: "Cannot issue this credential"} - ErrorClientUnauthorized Error = Error{Type: "UNAUTHORIZED", Status: 403, Description: "You are not authorized to access the session"} - ErrorBindingRequired Error = Error{Type: "BINDING_REQUIRED", Status: 403, Description: "Binding is required first"} + ErrorIrmaUnauthorized Error = Error{Type: "UNAUTHORIZED", Status: 403, Description: "You are not authorized to access the session"} + ErrorPairingRequired Error = Error{Type: "PAIRING_REQUIRED", Status: 403, Description: "Pairing is required first"} ErrorIssuanceFailed Error = Error{Type: "ISSUANCE_FAILED", Status: 500, Description: "Failed to create credential(s)"} ErrorInvalidProofs Error = Error{Type: "INVALID_PROOFS", Status: 400, Description: "Invalid secret key commitments and/or disclosure proofs"} ErrorAttributesMissing Error = Error{Type: "ATTRIBUTES_MISSING", Status: 400, Description: "Not all requested-for attributes were present"} diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index cf5a41c6f..6423a70d3 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -117,13 +117,13 @@ func (s *Server) HandlerFunc() http.HandlerFunc { r.Route("/frontend", func(r chi.Router) { r.Use(s.frontendMiddleware) r.Post("/options", s.handleFrontendOptionsPost) - r.Post("/bindingcompleted", s.handleFrontendBindingCompleted) + r.Post("/pairingcompleted", s.handleFrontendPairingCompleted) }) r.Group(func(r chi.Router) { r.Use(s.cacheMiddleware) r.Get("/", s.handleSessionGet) r.Group(func(r chi.Router) { - r.Use(s.bindingMiddleware) + r.Use(s.pairingMiddleware) r.Get("/request", s.handleSessionGetRequest) r.Post("/commitments", s.handleSessionCommitments) r.Post("/proofs", s.handleSessionProofs) @@ -264,17 +264,17 @@ func (s *Server) SetFrontendOptions(requestorToken irma.RequestorToken, request return session.updateFrontendOptions(request) } -// Complete binding between the irma client and the frontend. Returns +// Complete pairing between the irma client and the frontend. Returns // an error when no client is actually connected. -func BindingCompleted(requestorToken irma.RequestorToken) error { - return s.BindingCompleted(requestorToken) +func PairingCompleted(requestorToken irma.RequestorToken) error { + return s.PairingCompleted(requestorToken) } -func (s *Server) BindingCompleted(requestorToken irma.RequestorToken) error { +func (s *Server) PairingCompleted(requestorToken irma.RequestorToken) error { session := s.sessions.get(requestorToken) if session == nil { - return server.LogError(errors.Errorf("can't complete binding of unknown session %s", requestorToken)) + return server.LogError(errors.Errorf("can't complete pairing of unknown session %s", requestorToken)) } - return session.bindingCompleted() + return session.pairingCompleted() } // Revoke revokes the earlier issued credential specified by key. (Can only be used if this server diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 411b1679b..15d3576b4 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -51,7 +51,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c // Protocol versions below 2.7 don't include an authorization header. Therefore skip the authorization // header presence check if a lower version is used. if clientAuth == "" && session.version.Above(2, 6) { - return nil, session.fail(server.ErrorClientUnauthorized, "No authorization header provided") + return nil, session.fail(server.ErrorIrmaUnauthorized, "No authorization header provided") } session.clientAuth = clientAuth @@ -72,8 +72,8 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c logger.WithFields(logrus.Fields{"version": session.version.String()}).Debugf("Protocol version negotiated") session.request.Base().ProtocolVersion = session.version - if session.options.BindingMethod != irma.BindingMethodNone && session.version.Above(2, 6) { - session.setStatus(irma.ServerStatusBinding) + if session.options.PairingMethod != irma.PairingMethodNone && session.version.Above(2, 6) { + session.setStatus(irma.ServerStatusPairing) } else { session.setStatus(irma.ServerStatusConnected) } @@ -462,9 +462,9 @@ func (s *Server) handleFrontendOptionsPost(w http.ResponseWriter, r *http.Reques server.WriteResponse(w, res, nil) } -func (s *Server) handleFrontendBindingCompleted(w http.ResponseWriter, r *http.Request) { +func (s *Server) handleFrontendPairingCompleted(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - if err := session.bindingCompleted(); err != nil { + if err := session.pairingCompleted(); err != nil { server.WriteError(w, server.ErrorUnexpectedRequest, err.Error()) return } diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 58f83041d..4f09ebc68 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -67,25 +67,25 @@ func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*ir if session.status != irma.ServerStatusInitialized { return nil, errors.New("Frontend options cannot be updated when client is already connected") } - if request.BindingMethod == irma.BindingMethodNone { - session.options.BindingCode = "" - } else if request.BindingMethod == irma.BindingMethodPin { - session.options.BindingCode = common.NewBindingCode() + if request.PairingMethod == irma.PairingMethodNone { + session.options.PairingCode = "" + } else if request.PairingMethod == irma.PairingMethodPin { + session.options.PairingCode = common.NewPairingCode() } else { - return nil, errors.New("Binding method unknown") + return nil, errors.New("Pairing method unknown") } - session.options.BindingMethod = request.BindingMethod + session.options.PairingMethod = request.PairingMethod return &session.options, nil } -// Complete the binding process of frontend and irma client -func (session *session) bindingCompleted() error { - if session.status == irma.ServerStatusBinding { +// Complete the pairing process of frontend and irma client +func (session *session) pairingCompleted() error { + if session.status == irma.ServerStatusPairing { session.setStatus(irma.ServerStatusConnected) return nil } - return errors.New("Binding was not enabled") + return errors.New("Pairing was not enabled") } func (session *session) fail(err server.Error, message string) *irma.RemoteError { @@ -310,7 +310,7 @@ func (session *session) getClientRequest() (*irma.ClientRequest, error) { Options: &session.options, } - if session.options.BindingMethod == irma.BindingMethodNone { + if session.options.PairingMethod == irma.PairingMethodNone { request, err := session.getRequest() if err != nil { return nil, err @@ -451,7 +451,7 @@ func (s *Server) frontendMiddleware(next http.Handler) http.Handler { frontendAuth := irma.FrontendAuthorization(r.Header.Get(irma.AuthorizationHeader)) if frontendAuth != session.frontendAuth { - server.WriteError(w, server.ErrorUnauthorized, "") + server.WriteError(w, server.ErrorIrmaUnauthorized, "") return } next.ServeHTTP(w, r) @@ -532,16 +532,16 @@ func (s *Server) sessionMiddleware(next http.Handler) http.Handler { }) } -func (s *Server) bindingMiddleware(next http.Handler) http.Handler { +func (s *Server) pairingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - if session.status == irma.ServerStatusBinding { - server.WriteError(w, server.ErrorBindingRequired, "") + if session.status == irma.ServerStatusPairing { + server.WriteError(w, server.ErrorPairingRequired, "") return } - // Endpoints behind the bindingMiddleware can only be accessed when the client is already connected + // Endpoints behind the pairingMiddleware can only be accessed when the client is already connected // and the request includes the right authorization header to prove we still talk to the same client as before. if session.status != irma.ServerStatusConnected { server.WriteError(w, server.ErrorUnexpectedRequest, "Session not yet started or already finished") @@ -549,7 +549,7 @@ func (s *Server) bindingMiddleware(next http.Handler) http.Handler { } clientAuth := irma.ClientAuthorization(r.Header.Get(irma.AuthorizationHeader)) if session.clientAuth != clientAuth { - server.WriteError(w, server.ErrorClientUnauthorized, "") + server.WriteError(w, server.ErrorIrmaUnauthorized, "") return } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index c9d2cd15e..3c2bbbe00 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -181,7 +181,7 @@ func (s *Server) newSession(action irma.Action, request irma.RequestorRequest) * request: request.SessionRequest(), options: irma.SessionOptions{ LDContext: irma.LDContextSessionOptions, - BindingMethod: irma.BindingMethodNone, + PairingMethod: irma.PairingMethodNone, }, lastActive: time.Now(), requestorToken: requestorToken, From 74bcf913769649eda69a940a9cbf3d96fdc80cd5 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 13 Nov 2020 14:05:37 +0100 Subject: [PATCH 60/77] Added missing Validate function for OptionsRequest --- requests.go | 7 +++++++ server/irmaserver/helpers.go | 17 +++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/requests.go b/requests.go index ded48a96e..d64dd1531 100644 --- a/requests.go +++ b/requests.go @@ -1132,6 +1132,13 @@ func NewOptionsRequest() OptionsRequest { } } +func (or *OptionsRequest) Validate() error { + if or.LDContext != LDContextOptionsRequest { + return errors.New("Not an options request") + } + return nil +} + func (cr *ClientRequest) UnmarshalJSON(data []byte) error { // Unmarshal in alias first to prevent infinite recursion type alias ClientRequest diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 4f09ebc68..30afaca14 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -67,15 +67,16 @@ func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*ir if session.status != irma.ServerStatusInitialized { return nil, errors.New("Frontend options cannot be updated when client is already connected") } - if request.PairingMethod == irma.PairingMethodNone { - session.options.PairingCode = "" - } else if request.PairingMethod == irma.PairingMethodPin { - session.options.PairingCode = common.NewPairingCode() - } else { - return nil, errors.New("Pairing method unknown") + if request.PairingMethod != "" { + if request.PairingMethod == irma.PairingMethodNone { + session.options.PairingCode = "" + } else if request.PairingMethod == irma.PairingMethodPin { + session.options.PairingCode = common.NewPairingCode() + } else { + return nil, errors.New("Pairing method unknown") + } + session.options.PairingMethod = request.PairingMethod } - - session.options.PairingMethod = request.PairingMethod return &session.options, nil } From 1d6db3defd3f3f23fba57a924e1f2e640b54da35 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 3 Dec 2020 12:04:30 +0100 Subject: [PATCH 61/77] Introduce PairingRecommended field in Qr --- messages.go | 2 ++ server/irmaserver/api.go | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/messages.go b/messages.go index f1c252895..e1771bccb 100644 --- a/messages.go +++ b/messages.go @@ -166,6 +166,8 @@ type Qr struct { URL string `json:"u"` // Session type (disclosing, signing, issuing) Type Action `json:"irmaqr"` + // Indicator that shows whether pairing is recommended when starting the session + PairingRecommended bool `json:"pairingRecommended"` } // Tokens to identify a session from the perspective of the different agents diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 6423a70d3..3c8822a63 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -193,6 +193,22 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, } } + pairingRecommended := false + if action == irma.ActionDisclosing { + err := request.Disclosure().Disclose.Iterate(func(attr *irma.AttributeRequest) error { + if attr.Value != nil { + pairingRecommended = true + } + return nil + }) + if err != nil { + return nil, "", "", err + } + } else { + // For issuing and signing actions, we always recommend pairing. + pairingRecommended = true + } + request.Base().DevelopmentMode = !s.conf.Production session := s.newSession(action, rrequest) s.conf.Logger.WithFields(logrus.Fields{"action": action, "session": session.requestorToken}).Infof("Session started") @@ -205,8 +221,9 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, s.handlers[session.requestorToken] = handler } return &irma.Qr{ - Type: action, - URL: s.conf.URL + "session/" + string(session.clientToken), + Type: action, + URL: s.conf.URL + "session/" + string(session.clientToken), + PairingRecommended: pairingRecommended, }, session.requestorToken, session.frontendAuth, nil } From 4adabfc758eb7fe5886b9a226dbe2435f4296655 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 10 Dec 2020 12:18:54 +0100 Subject: [PATCH 62/77] Shorten pairing recommended json tag and omit when false --- messages.go | 4 ++-- server/irmaserver/api.go | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/messages.go b/messages.go index e1771bccb..deb84a9f7 100644 --- a/messages.go +++ b/messages.go @@ -166,8 +166,8 @@ type Qr struct { URL string `json:"u"` // Session type (disclosing, signing, issuing) Type Action `json:"irmaqr"` - // Indicator that shows whether pairing is recommended when starting the session - PairingRecommended bool `json:"pairingRecommended"` + // Indicator to the frontend that shows whether pairing is recommended when starting the session + PairingRecommended *bool `json:"pairingHint,omitempty"` } // Tokens to identify a session from the perspective of the different agents diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 3c8822a63..f7886a411 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -220,11 +220,16 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, if handler != nil { s.handlers[session.requestorToken] = handler } - return &irma.Qr{ - Type: action, - URL: s.conf.URL + "session/" + string(session.clientToken), - PairingRecommended: pairingRecommended, - }, session.requestorToken, session.frontendAuth, nil + + qr := &irma.Qr{ + Type: action, + URL: s.conf.URL + "session/" + string(session.clientToken), + } + + if pairingRecommended { + qr.PairingRecommended = &pairingRecommended + } + return qr, session.requestorToken, session.frontendAuth, nil } // GetSessionResult retrieves the result of the specified IRMA session. From 4e5639ef3438a95eaac447d0ad9c723b48259268 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Thu, 10 Dec 2020 14:00:29 +0100 Subject: [PATCH 63/77] Updated CHANGELOG --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23c785f94..d47856ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Support for device pairing to prevent shoulder surfing (i.e. make it impossible for someone in close physical proximity to a user to scan the QR code that was meant for the user) * Introduced new frontend endpoints to manage device pairing - * The API of the `requestorserver` package has two new functions `SetFrontendOptions` and `PairingCompleted` + * The API of the `irmaserver` package has two new functions `SetFrontendOptions` and `PairingCompleted` * A new server status `"PAIRING"` is introduced + * The `Qr` struct now contains an optional field `PairingRecommended` (named `pairingHint` when being marshalled to JSON) that is set to true when pairing is recommended for that session, as indication to the frontend * A new function `SessionStatus` is available in the API of the `requestorserver` to get a channel with status updates of an IRMA session ### Changes * The `irma.SessionPackage` struct now contains an extra field `FrontendAuth` -* The `StartSession` function from the API of the `requestorserver` package now returns three values: the session pointer (type *irma.QR), the requestor token (type irma.RequestorToken) and the frontend authorization token (type irma.FrontendAuthorization) -* The `token` parameter, as used by most functions in the API of the `requestorserver` package, now has the type `irma.RequestorToken` +* The `StartSession` function from the API of the `irmaserver` package now returns three values: the session pointer (type *irma.QR), the requestor token (type irma.RequestorToken) and the frontend authorization token (type irma.FrontendAuthorization) +* The `token` parameter, as used by most functions in the API of the `irmaserver` package, now has the type `irma.RequestorToken` * The `server.Status` type has been moved to `irma.ServerStatus`; the related constants are also moved, e.g. from `server.StatusInitialized` to `irma.ServerStatusInitialized` ## [0.7.0] - 2021-03-17 From b8f5a113b76befed7ef594e8ce7747afd931e074 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 11 Dec 2020 10:14:42 +0100 Subject: [PATCH 64/77] Remove pointer to bool in Qr struct --- messages.go | 2 +- server/irmaserver/api.go | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/messages.go b/messages.go index deb84a9f7..598b69db9 100644 --- a/messages.go +++ b/messages.go @@ -167,7 +167,7 @@ type Qr struct { // Session type (disclosing, signing, issuing) Type Action `json:"irmaqr"` // Indicator to the frontend that shows whether pairing is recommended when starting the session - PairingRecommended *bool `json:"pairingHint,omitempty"` + PairingRecommended bool `json:"pairingHint,omitempty"` } // Tokens to identify a session from the perspective of the different agents diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index f7886a411..3c8822a63 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -220,16 +220,11 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, if handler != nil { s.handlers[session.requestorToken] = handler } - - qr := &irma.Qr{ - Type: action, - URL: s.conf.URL + "session/" + string(session.clientToken), - } - - if pairingRecommended { - qr.PairingRecommended = &pairingRecommended - } - return qr, session.requestorToken, session.frontendAuth, nil + return &irma.Qr{ + Type: action, + URL: s.conf.URL + "session/" + string(session.clientToken), + PairingRecommended: pairingRecommended, + }, session.requestorToken, session.frontendAuth, nil } // GetSessionResult retrieves the result of the specified IRMA session. From 451c61267549f6a16e218b671f6b4785adc4a6c9 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 15 Dec 2020 10:54:06 +0100 Subject: [PATCH 65/77] Improved CHANGELOG --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d47856ff9..e303efb75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Introduced new frontend endpoints to manage device pairing * The API of the `irmaserver` package has two new functions `SetFrontendOptions` and `PairingCompleted` * A new server status `"PAIRING"` is introduced - * The `Qr` struct now contains an optional field `PairingRecommended` (named `pairingHint` when being marshalled to JSON) that is set to true when pairing is recommended for that session, as indication to the frontend -* A new function `SessionStatus` is available in the API of the `requestorserver` to get a channel with status updates of an IRMA session +* A new function `SessionStatus` is available in the API of the `irmaserver` to get a channel with status updates of an IRMA session ### Changes -* The `irma.SessionPackage` struct now contains an extra field `FrontendAuth` +* The `server.SessionPackage` struct now contains an extra field `FrontendAuth` +* The `irma.Qr` struct now contains an optional field `PairingRecommended` (named `pairingHint` when being marshalled to JSON) that is set to true when pairing is recommended for that session, as indication to the frontend * The `StartSession` function from the API of the `irmaserver` package now returns three values: the session pointer (type *irma.QR), the requestor token (type irma.RequestorToken) and the frontend authorization token (type irma.FrontendAuthorization) * The `token` parameter, as used by most functions in the API of the `irmaserver` package, now has the type `irma.RequestorToken` * The `server.Status` type has been moved to `irma.ServerStatus`; the related constants are also moved, e.g. from `server.StatusInitialized` to `irma.ServerStatusInitialized` From 761a5479e388e8a24682a13ecb4e02924bd87e5b Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Fri, 19 Mar 2021 10:14:42 +0100 Subject: [PATCH 66/77] refactor: decrease indentation level in wait_status.go --- wait_status.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/wait_status.go b/wait_status.go index ae7eab9aa..11c37e686 100644 --- a/wait_status.go +++ b/wait_status.go @@ -2,9 +2,10 @@ package irma import ( "context" - sseclient "github.com/sietseringers/go-sse" "strings" "time" + + sseclient "github.com/sietseringers/go-sse" ) const pollInterval = 1000 * time.Millisecond @@ -30,15 +31,16 @@ func subscribeSSE(transport *HTTPTransport, statuschan chan ServerStatus, errorc go func() { for { e := <-events - if e != nil && e.Type != "open" { - status := ServerStatus(strings.Trim(string(e.Data), `"`)) - statuschan <- status - if untilNextOnly || status.Finished() { - errorchan <- nil - cancelled = true - cancel() - return - } + if e == nil || e.Type == "open" { + return + } + status := ServerStatus(strings.Trim(string(e.Data), `"`)) + statuschan <- status + if untilNextOnly || status.Finished() { + errorchan <- nil + cancelled = true + cancel() + return } } }() From c211db0e30bb709fe9f11ecfea583f2411a0cb98 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Fri, 19 Mar 2021 16:02:45 +0100 Subject: [PATCH 67/77] refactor: decrease indentation level in irmaserver.session.onUpdate() --- server/irmaserver/helpers.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 30afaca14..68be1cddb 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -65,18 +65,18 @@ func (session *session) onUpdate() { // Checks whether requested options are valid in the current session context. func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*irma.SessionOptions, error) { if session.status != irma.ServerStatusInitialized { - return nil, errors.New("Frontend options cannot be updated when client is already connected") - } - if request.PairingMethod != "" { - if request.PairingMethod == irma.PairingMethodNone { - session.options.PairingCode = "" - } else if request.PairingMethod == irma.PairingMethodPin { - session.options.PairingCode = common.NewPairingCode() - } else { - return nil, errors.New("Pairing method unknown") - } - session.options.PairingMethod = request.PairingMethod + return nil, errors.New("Frontend options can only be updated when session is in initialized state") + } + if request.PairingMethod == "" { + return &session.options, nil + } else if request.PairingMethod == irma.PairingMethodNone { + session.options.PairingCode = "" + } else if request.PairingMethod == irma.PairingMethodPin { + session.options.PairingCode = common.NewPairingCode() + } else { + return nil, errors.New("Pairing method unknown") } + session.options.PairingMethod = request.PairingMethod return &session.options, nil } From e861fc7d8a9fb004ae7f37a2c5d6e55febd3a7d9 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Mon, 22 Mar 2021 15:19:30 +0100 Subject: [PATCH 68/77] feat: add new status endpoint for frontend --- messages.go | 8 ++++++++ server/api.go | 2 +- server/irmaserver/api.go | 3 +++ server/irmaserver/handle.go | 7 +++++++ server/irmaserver/sessions.go | 4 ++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/messages.go b/messages.go index 598b69db9..fdf95f022 100644 --- a/messages.go +++ b/messages.go @@ -168,6 +168,9 @@ type Qr struct { Type Action `json:"irmaqr"` // Indicator to the frontend that shows whether pairing is recommended when starting the session PairingRecommended bool `json:"pairingHint,omitempty"` + + MinProtocolVersion *ProtocolVersion `json:"minProtocolVersion"` + MaxProtocolVersion *ProtocolVersion `json:"maxProtocolVersion"` } // Tokens to identify a session from the perspective of the different agents @@ -388,3 +391,8 @@ type ServerSessionResponse struct { ProtocolVersion *ProtocolVersion `json:"-"` SessionType Action `json:"-"` } + +type FrontendSessionStatus struct { + Status ServerStatus `json:"status"` + NextSession *Qr `json:"nextSession,omitempty"` +} diff --git a/server/api.go b/server/api.go index 83225415c..4e99ebfa1 100644 --- a/server/api.go +++ b/server/api.go @@ -41,7 +41,7 @@ type SessionResult struct { Disclosed [][]*irma.DisclosedAttribute `json:"disclosed,omitempty"` Signature *irma.SignedMessage `json:"signature,omitempty"` Err *irma.RemoteError `json:"error,omitempty"` - NextSession string `json:"nextSession,omitempty"` + NextSession irma.RequestorToken `json:"nextSession,omitempty"` LegacySession bool `json:"-"` // true if request was started with legacy (i.e. pre-condiscon) session request } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 3c8822a63..4771711b1 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -116,6 +116,7 @@ func (s *Server) HandlerFunc() http.HandlerFunc { r.Get("/statusevents", s.handleSessionStatusEvents) r.Route("/frontend", func(r chi.Router) { r.Use(s.frontendMiddleware) + r.Get("/status", s.handleFrontendStatus) r.Post("/options", s.handleFrontendOptionsPost) r.Post("/pairingcompleted", s.handleFrontendPairingCompleted) }) @@ -224,6 +225,8 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, Type: action, URL: s.conf.URL + "session/" + string(session.clientToken), PairingRecommended: pairingRecommended, + MinProtocolVersion: minFrontendProtocolVersion, + MaxProtocolVersion: maxFrontendProtocolVersion, }, session.requestorToken, session.frontendAuth, nil } diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 15d3576b4..4096ebe14 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -314,6 +314,7 @@ func (s *Server) startNext(session *session, res *irma.ServerSessionResponse) er return err } session.result.NextSession = token + session.next = qr // All attributes that were disclosed in the previous session, as well as any attributes // from sessions before that, need to be disclosed in the new session as well @@ -440,6 +441,12 @@ func (s *Server) handleSessionGetRequest(w http.ResponseWriter, r *http.Request) server.WriteResponse(w, request, rerr) } +func (s *Server) handleFrontendStatus(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session) + status := irma.FrontendSessionStatus{Status: session.status, NextSession: session.next} + server.WriteResponse(w, status, nil) +} + func (s *Server) handleFrontendOptionsPost(w http.ResponseWriter, r *http.Request) { optionsRequest := &irma.OptionsRequest{} bts, err := ioutil.ReadAll(r.Body) diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index 3c2bbbe00..1e977a721 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -28,6 +28,7 @@ type session struct { request irma.SessionRequest legacyCompatible bool // if the request is convertible to pre-condiscon format implicitDisclosure irma.AttributeConDisCon + next *irma.Qr options irma.SessionOptions status irma.ServerStatus @@ -78,6 +79,9 @@ const ( var ( minProtocolVersion = irma.NewVersion(2, 4) maxProtocolVersion = irma.NewVersion(2, 7) + + minFrontendProtocolVersion = irma.NewVersion(1, 0) + maxFrontendProtocolVersion = irma.NewVersion(1, 1) ) func (s *memorySessionStore) get(t irma.RequestorToken) *session { From e12a2971dd23300458bcf2643a1075e6033f2254 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Mon, 22 Mar 2021 16:07:47 +0100 Subject: [PATCH 69/77] feat: also add new statusevents endpoint for frontend --- server/api.go | 7 ++++--- server/irmaserver/api.go | 2 ++ server/irmaserver/handle.go | 14 ++++++++++++++ server/irmaserver/helpers.go | 7 +++++++ server/irmaserver/sessions.go | 2 ++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/server/api.go b/server/api.go index 4e99ebfa1..3b99921bc 100644 --- a/server/api.go +++ b/server/api.go @@ -66,9 +66,10 @@ type LegacySessionResult struct { } const ( - ComponentRevocation = "revocation" - ComponentSession = "session" - ComponentStatic = "static" + ComponentRevocation = "revocation" + ComponentSession = "session" + ComponentFrontendSession = "frontendsession" + ComponentStatic = "static" ) const ( diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 4771711b1..315f1d512 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -117,6 +117,7 @@ func (s *Server) HandlerFunc() http.HandlerFunc { r.Route("/frontend", func(r chi.Router) { r.Use(s.frontendMiddleware) r.Get("/status", s.handleFrontendStatus) + r.Get("/statusevents", s.handleFrontendStatusEvents) r.Post("/options", s.handleFrontendOptionsPost) r.Post("/pairingcompleted", s.handleFrontendPairingCompleted) }) @@ -345,6 +346,7 @@ func (s *Server) SubscribeServerSentEvents(w http.ResponseWriter, r *http.Reques go func() { time.Sleep(200 * time.Millisecond) s.serverSentEvents.SendMessage("session/"+token, sse.NewMessage("", "", "open")) + s.serverSentEvents.SendMessage("frontendsession/"+token, sse.NewMessage("", "", "open")) }() s.serverSentEvents.ServeHTTP(w, r) return nil diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 4096ebe14..0212baafe 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -447,6 +447,20 @@ func (s *Server) handleFrontendStatus(w http.ResponseWriter, r *http.Request) { server.WriteResponse(w, status, nil) } +func (s *Server) handleFrontendStatusEvents(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session) + session.locked = false + session.Unlock() + r = r.WithContext(context.WithValue(r.Context(), "sse", common.SSECtx{ + Component: server.ComponentFrontendSession, + Arg: string(session.clientToken), + })) + if err := s.SubscribeServerSentEvents(w, r, string(session.clientToken), false); err != nil { + server.WriteError(w, server.ErrorUnknown, err.Error()) + return + } +} + func (s *Server) handleFrontendOptionsPost(w http.ResponseWriter, r *http.Request) { optionsRequest := &irma.OptionsRequest{} bts, err := ioutil.ReadAll(r.Body) diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 68be1cddb..159aecb31 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -51,6 +51,8 @@ func (session *session) onUpdate() { } } + frontendstatus, _ := json.Marshal(irma.FrontendSessionStatus{Status: session.status, NextSession: session.next}) + if session.sse == nil { return } @@ -60,6 +62,9 @@ func (session *session) onUpdate() { session.sse.SendMessage("session/"+string(session.requestorToken), sse.SimpleMessage(fmt.Sprintf(`"%s"`, session.status)), ) + session.sse.SendMessage("frontendsession/"+string(session.clientToken), + sse.SimpleMessage(string(frontendstatus)), + ) } // Checks whether requested options are valid in the current session context. @@ -425,6 +430,8 @@ func eventServer(conf *server.Configuration) *sse.Server { switch ssectx.(common.SSECtx).Component { case server.ComponentSession: return "session/" + ssectx.(common.SSECtx).Arg + case server.ComponentFrontendSession: + return "frontendsession/" + ssectx.(common.SSECtx).Arg case server.ComponentRevocation: return "revocation/" + ssectx.(common.SSECtx).Arg default: diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index 1e977a721..a997bcf65 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -114,6 +114,7 @@ func (s *memorySessionStore) stop() { if session.sse != nil { session.sse.CloseChannel("session/" + string(session.requestorToken)) session.sse.CloseChannel("session/" + string(session.clientToken)) + session.sse.CloseChannel("frontendsession/" + string(session.clientToken)) } } } @@ -156,6 +157,7 @@ func (s *memorySessionStore) deleteExpired() { if session.sse != nil { session.sse.CloseChannel("session/" + string(session.requestorToken)) session.sse.CloseChannel("session/" + string(session.clientToken)) + session.sse.CloseChannel("frontendsession/" + string(session.clientToken)) } delete(s.client, session.clientToken) delete(s.requestor, token) From a8bd786111dbc417c51f42b02c5754fe46419bdb Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Mon, 22 Mar 2021 16:57:14 +0100 Subject: [PATCH 70/77] refactor: rename OptionsRequest to FrontendOptionsRequest --- internal/sessiontest/main_test.go | 2 +- internal/sessiontest/session_test.go | 2 +- irma/cmd/session.go | 4 ++-- requests.go | 32 ++++++++++++++-------------- server/irmaserver/api.go | 4 ++-- server/irmaserver/handle.go | 2 +- server/irmaserver/helpers.go | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index e1ebf40ed..2ce6540aa 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -275,7 +275,7 @@ func extractPrivateField(i interface{}, field string) interface{} { } func setPairingMethod(method irma.PairingMethod, handler *TestHandler) string { - optionsRequest := irma.NewOptionsRequest() + optionsRequest := irma.NewFrontendOptionsRequest() optionsRequest.PairingMethod = method options := &irma.SessionOptions{} err := handler.frontendTransport.Post("frontend/options", options, optionsRequest) diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index 0db0797a8..b4ab49b0d 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -162,7 +162,7 @@ func TestIssuancePairing(t *testing.T) { require.Equal(t, string(server.ErrorPairingRequired.Type), sessionErr.RemoteError.ErrorName) // Check whether pairing cannot be disabled again after client is connected. - request := irma.NewOptionsRequest() + request := irma.NewFrontendOptionsRequest() result := &irma.SessionOptions{} err = handler.frontendTransport.Post("frontend/options", result, request) require.Error(t, err) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index 9302d7385..fbb161d45 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -119,7 +119,7 @@ func libraryRequest( // Enable pairing if necessary var sessionOptions *irma.SessionOptions if pairing { - optionsRequest := irma.NewOptionsRequest() + optionsRequest := irma.NewFrontendOptionsRequest() optionsRequest.PairingMethod = irma.PairingMethodPin if sessionOptions, err = irmaServer.SetFrontendOptions(requestorToken, &optionsRequest); err != nil { return nil, errors.WrapPrefix(err, "Failed to enable pairing", 0) @@ -170,7 +170,7 @@ func serverRequest( if pairing { frontendTransport = irma.NewHTTPTransport(qr.URL, false) frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendAuth)) - optionsRequest := irma.NewOptionsRequest() + optionsRequest := irma.NewFrontendOptionsRequest() optionsRequest.PairingMethod = irma.PairingMethodPin err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) if err != nil { diff --git a/requests.go b/requests.go index d64dd1531..c1dfb2391 100644 --- a/requests.go +++ b/requests.go @@ -19,14 +19,14 @@ import ( ) const ( - LDContextDisclosureRequest = "https://irma.app/ld/request/disclosure/v2" - LDContextSignatureRequest = "https://irma.app/ld/request/signature/v2" - LDContextIssuanceRequest = "https://irma.app/ld/request/issuance/v2" - LDContextRevocationRequest = "https://irma.app/ld/request/revocation/v1" - LDContextOptionsRequest = "https://irma.app/ld/request/options/v1" - LDContextClientRequest = "https://irma.app/ld/request/client/v1" - LDContextSessionOptions = "https://irma.app/ld/options/v1" - DefaultJwtValidity = 120 + LDContextDisclosureRequest = "https://irma.app/ld/request/disclosure/v2" + LDContextSignatureRequest = "https://irma.app/ld/request/signature/v2" + LDContextIssuanceRequest = "https://irma.app/ld/request/issuance/v2" + LDContextRevocationRequest = "https://irma.app/ld/request/revocation/v1" + LDContextFrontendOptionsRequest = "https://irma.app/ld/request/frontendoptions/v1" + LDContextClientRequest = "https://irma.app/ld/request/client/v1" + LDContextSessionOptions = "https://irma.app/ld/options/v1" + DefaultJwtValidity = 120 ) // BaseRequest contains information used by all IRMA session types, such the context and nonce, @@ -221,8 +221,8 @@ const ( PairingMethodPin = "pin" ) -// An OptionsRequest asks for a options change of a particular session. -type OptionsRequest struct { +// An FrontendOptionsRequest asks for a options change of a particular session. +type FrontendOptionsRequest struct { LDContext string `json:"@context,omitempty"` PairingMethod PairingMethod `json:"pairingMethod"` } @@ -1124,16 +1124,16 @@ func NewAttributeRequest(attr string) AttributeRequest { return AttributeRequest{Type: NewAttributeTypeIdentifier(attr)} } -// NewOptionsRequest returns a new options request initialized with default values for each option -func NewOptionsRequest() OptionsRequest { - return OptionsRequest{ - LDContext: LDContextOptionsRequest, +// NewFrontendOptionsRequest returns a new options request initialized with default values for each option +func NewFrontendOptionsRequest() FrontendOptionsRequest { + return FrontendOptionsRequest{ + LDContext: LDContextFrontendOptionsRequest, PairingMethod: PairingMethodNone, } } -func (or *OptionsRequest) Validate() error { - if or.LDContext != LDContextOptionsRequest { +func (or *FrontendOptionsRequest) Validate() error { + if or.LDContext != LDContextFrontendOptionsRequest { return errors.New("Not an options request") } return nil diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 315f1d512..77680a67a 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -274,10 +274,10 @@ func (s *Server) CancelSession(requestorToken irma.RequestorToken) error { // Returns the updated session options struct. Frontend options can only be // changed when the client is not connected yet. Otherwise an error is returned. // Options that are not specified in the request, keep their old value. -func SetFrontendOptions(requestorToken irma.RequestorToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { +func SetFrontendOptions(requestorToken irma.RequestorToken, request *irma.FrontendOptionsRequest) (*irma.SessionOptions, error) { return s.SetFrontendOptions(requestorToken, request) } -func (s *Server) SetFrontendOptions(requestorToken irma.RequestorToken, request *irma.OptionsRequest) (*irma.SessionOptions, error) { +func (s *Server) SetFrontendOptions(requestorToken irma.RequestorToken, request *irma.FrontendOptionsRequest) (*irma.SessionOptions, error) { session := s.sessions.get(requestorToken) if session == nil { return nil, server.LogError(errors.Errorf("can't set frontend options of unknown session %s", requestorToken)) diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 0212baafe..cba465adf 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -462,7 +462,7 @@ func (s *Server) handleFrontendStatusEvents(w http.ResponseWriter, r *http.Reque } func (s *Server) handleFrontendOptionsPost(w http.ResponseWriter, r *http.Request) { - optionsRequest := &irma.OptionsRequest{} + optionsRequest := &irma.FrontendOptionsRequest{} bts, err := ioutil.ReadAll(r.Body) if err != nil { server.WriteError(w, server.ErrorMalformedInput, err.Error()) diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index 159aecb31..dc0034eaf 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -68,7 +68,7 @@ func (session *session) onUpdate() { } // Checks whether requested options are valid in the current session context. -func (session *session) updateFrontendOptions(request *irma.OptionsRequest) (*irma.SessionOptions, error) { +func (session *session) updateFrontendOptions(request *irma.FrontendOptionsRequest) (*irma.SessionOptions, error) { if session.status != irma.ServerStatusInitialized { return nil, errors.New("Frontend options can only be updated when session is in initialized state") } From ebf3ec53e97bb48d976f1785b8b97283d3f01488 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Tue, 23 Mar 2021 09:37:01 +0100 Subject: [PATCH 71/77] feat: StartSession function and endpoint now return frontend-specific data in new FrontendSessionRequest struct --- internal/sessiontest/main_test.go | 8 ++++---- irma/cmd/session.go | 14 +++++++------- messages.go | 5 ----- requests.go | 12 ++++++++++++ server/api.go | 6 +++--- server/irmac/irmac.go | 15 ++++++++------- server/irmaserver/api.go | 29 +++++++++++++++++------------ server/requestorserver/server.go | 8 ++++---- 8 files changed, 55 insertions(+), 42 deletions(-) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 2ce6540aa..666469153 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -131,7 +131,7 @@ func getMultipleIssuanceRequest() *irma.IssuanceRequest { var TestType = "irmaserver-jwt" -func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) (*server.SessionPackage, irma.FrontendAuthorization) { +func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) (*server.SessionPackage, *irma.FrontendSessionRequest) { var ( sesPkg server.SessionPackage err error @@ -152,7 +152,7 @@ func startSession(t *testing.T, request irma.SessionRequest, sessiontype string) } require.NoError(t, err) - return &sesPkg, sesPkg.FrontendAuth + return &sesPkg, sesPkg.FrontendRequest } func getJwt(t *testing.T, request irma.SessionRequest, sessiontype string, alg jwt.SigningMethod) string { @@ -219,7 +219,7 @@ func sessionHelperWithFrontendOptions( defer StopRequestorServer() } - sesPkg, frontendAuth := startSession(t, request, sessiontype) + sesPkg, frontendRequest := startSession(t, request, sessiontype) c := make(chan *SessionResult) h := &TestHandler{ @@ -232,7 +232,7 @@ func sessionHelperWithFrontendOptions( if frontendOptionsHandler != nil || pairingHandler != nil { h.pairingCodeChan = make(chan string) h.frontendTransport = irma.NewHTTPTransport(sesPkg.SessionPtr.URL, false) - h.frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendAuth)) + h.frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendRequest.Authorization)) } if frontendOptionsHandler != nil { frontendOptionsHandler(h) diff --git a/irma/cmd/session.go b/irma/cmd/session.go index fbb161d45..f1630a420 100644 --- a/irma/cmd/session.go +++ b/irma/cmd/session.go @@ -159,7 +159,7 @@ func serverRequest( logger.Debug("Server URL: ", serverurl) // Start session at server - qr, frontendAuth, transport, err := postRequest(serverurl, request, name, authmethod, key) + qr, frontendRequest, transport, err := postRequest(serverurl, request, name, authmethod, key) if err != nil { return nil, err } @@ -169,7 +169,7 @@ func serverRequest( sessionOptions := &irma.SessionOptions{} if pairing { frontendTransport = irma.NewHTTPTransport(qr.URL, false) - frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendAuth)) + frontendTransport.SetHeader(irma.AuthorizationHeader, string(frontendRequest.Authorization)) optionsRequest := irma.NewFrontendOptionsRequest() optionsRequest.PairingMethod = irma.PairingMethodPin err = frontendTransport.Post("frontend/options", sessionOptions, optionsRequest) @@ -244,7 +244,7 @@ func serverRequest( } func postRequest(serverurl string, request irma.RequestorRequest, name, authmethod, key string) ( - *irma.Qr, irma.FrontendAuthorization, *irma.HTTPTransport, error) { + *irma.Qr, *irma.FrontendSessionRequest, *irma.HTTPTransport, error) { var ( err error pkg = &server.SessionPackage{} @@ -261,20 +261,20 @@ func postRequest(serverurl string, request irma.RequestorRequest, name, authmeth var jwtstr string jwtstr, err = signRequest(request, name, authmethod, key) if err != nil { - return nil, "", nil, err + return nil, nil, nil, err } logger.Debug("Session request JWT: ", jwtstr) err = transport.Post("session", pkg, jwtstr) default: - return nil, "", nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)") + return nil, nil, nil, errors.New("Invalid authentication method (must be none, token, hmac or rsa)") } if err != nil { - return nil, "", nil, err + return nil, nil, nil, err } transport.Server += fmt.Sprintf("session/%s/", pkg.Token) - return pkg.SessionPtr, pkg.FrontendAuth, transport, err + return pkg.SessionPtr, pkg.FrontendRequest, transport, err } func handlePairing(options *irma.SessionOptions, statusChan chan irma.ServerStatus, completePairing func() error) ( diff --git a/messages.go b/messages.go index fdf95f022..21e1723b2 100644 --- a/messages.go +++ b/messages.go @@ -166,11 +166,6 @@ type Qr struct { URL string `json:"u"` // Session type (disclosing, signing, issuing) Type Action `json:"irmaqr"` - // Indicator to the frontend that shows whether pairing is recommended when starting the session - PairingRecommended bool `json:"pairingHint,omitempty"` - - MinProtocolVersion *ProtocolVersion `json:"minProtocolVersion"` - MaxProtocolVersion *ProtocolVersion `json:"maxProtocolVersion"` } // Tokens to identify a session from the perspective of the different agents diff --git a/requests.go b/requests.go index c1dfb2391..2d1e146ed 100644 --- a/requests.go +++ b/requests.go @@ -227,6 +227,18 @@ type FrontendOptionsRequest struct { PairingMethod PairingMethod `json:"pairingMethod"` } +// FrontendSessionRequest contains session parameters for the frontend. +type FrontendSessionRequest struct { + // Authorization token to access frontend endpoints. + Authorization FrontendAuthorization `json:"authorization"` + // PairingRecommended indictes to the frontend that pairing is recommended when starting the session. + PairingRecommended bool `json:"pairingHint,omitempty"` + // MinProtocolVersion that the server supports for the frontend protocol. + MinProtocolVersion *ProtocolVersion `json:"minProtocolVersion"` + // MaxProtocolVersion that the server supports for the frontend protocol. + MaxProtocolVersion *ProtocolVersion `json:"maxProtocolVersion"` +} + type RevocationRequest struct { LDContext string `json:"@context,omitempty"` CredentialType CredentialTypeIdentifier `json:"type"` diff --git a/server/api.go b/server/api.go index 3b99921bc..6a0ae5293 100644 --- a/server/api.go +++ b/server/api.go @@ -26,9 +26,9 @@ import ( var Logger *logrus.Logger = logrus.StandardLogger() type SessionPackage struct { - SessionPtr *irma.Qr `json:"sessionPtr"` - Token irma.RequestorToken `json:"token"` - FrontendAuth irma.FrontendAuthorization `json:"frontendAuth"` + SessionPtr *irma.Qr `json:"sessionPtr"` + Token irma.RequestorToken `json:"token"` + FrontendRequest *irma.FrontendSessionRequest `json:"frontendRequest"` } // SessionResult contains session information such as the session status, type, possible errors, diff --git a/server/irmac/irmac.go b/server/irmac/irmac.go index 46bad0fbe..4ffb1cf42 100644 --- a/server/irmac/irmac.go +++ b/server/irmac/irmac.go @@ -8,11 +8,12 @@ import ( "context" "encoding/base64" "encoding/json" - irma "github.com/privacybydesign/irmago" "io/ioutil" "net/http" "net/http/httptest" + irma "github.com/privacybydesign/irmago" + "github.com/privacybydesign/irmago/server" "github.com/privacybydesign/irmago/server/irmaserver" ) @@ -50,10 +51,10 @@ func Initialize(IrmaConfiguration *C.char) *C.char { func StartSession(requestString *C.char) (r *C.char) { // Create struct for return information result := struct { - IrmaQr string - RequestorToken string - FrontendAuth string - Error string + IrmaQr string + RequestorToken string + FrontendRequest *irma.FrontendSessionRequest + Error string }{} defer func() { j, _ := json.Marshal(result) @@ -67,7 +68,7 @@ func StartSession(requestString *C.char) (r *C.char) { } // Run the actual core function - qr, requestorToken, frontendAuth, err := s.StartSession(C.GoString(requestString), nil) + qr, requestorToken, frontendRequest, err := s.StartSession(C.GoString(requestString), nil) // And properly return the result if err != nil { @@ -82,7 +83,7 @@ func StartSession(requestString *C.char) (r *C.char) { // return actual results result.IrmaQr = string(qrJson) result.RequestorToken = string(requestorToken) - result.FrontendAuth = string(frontendAuth) + result.FrontendRequest = frontendRequest return } diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 77680a67a..0c00f393c 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -166,21 +166,21 @@ func (s *Server) Stop() { // The request parameter can be an irma.RequestorRequest, or an irma.SessionRequest, or a // ([]byte or string) JSON representation of one of those (for more details, see server.ParseSessionRequest().) func StartSession(request interface{}, handler server.SessionHandler, -) (*irma.Qr, irma.RequestorToken, irma.FrontendAuthorization, error) { +) (*irma.Qr, irma.RequestorToken, *irma.FrontendSessionRequest, error) { return s.StartSession(request, handler) } func (s *Server) StartSession(req interface{}, handler server.SessionHandler, -) (*irma.Qr, irma.RequestorToken, irma.FrontendAuthorization, error) { +) (*irma.Qr, irma.RequestorToken, *irma.FrontendSessionRequest, error) { rrequest, err := server.ParseSessionRequest(req) if err != nil { - return nil, "", "", err + return nil, "", nil, err } request := rrequest.SessionRequest() action := request.Action() if err := s.validateRequest(request); err != nil { - return nil, "", "", err + return nil, "", nil, err } if action == irma.ActionIssuing { // Include the AttributeTypeIdentifiers of random blind attributes to each CredentialRequest. @@ -191,7 +191,7 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, } if err := s.validateIssuanceRequest(request.(*irma.IssuanceRequest)); err != nil { - return nil, "", "", err + return nil, "", nil, err } } @@ -204,7 +204,7 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, return nil }) if err != nil { - return nil, "", "", err + return nil, "", nil, err } } else { // For issuing and signing actions, we always recommend pairing. @@ -223,12 +223,17 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, s.handlers[session.requestorToken] = handler } return &irma.Qr{ - Type: action, - URL: s.conf.URL + "session/" + string(session.clientToken), - PairingRecommended: pairingRecommended, - MinProtocolVersion: minFrontendProtocolVersion, - MaxProtocolVersion: maxFrontendProtocolVersion, - }, session.requestorToken, session.frontendAuth, nil + Type: action, + URL: s.conf.URL + "session/" + string(session.clientToken), + }, + session.requestorToken, + &irma.FrontendSessionRequest{ + Authorization: session.frontendAuth, + PairingRecommended: pairingRecommended, + MinProtocolVersion: minFrontendProtocolVersion, + MaxProtocolVersion: maxFrontendProtocolVersion, + }, + nil } // GetSessionResult retrieves the result of the specified IRMA session. diff --git a/server/requestorserver/server.go b/server/requestorserver/server.go index 3dba59551..8b1e385af 100644 --- a/server/requestorserver/server.go +++ b/server/requestorserver/server.go @@ -518,16 +518,16 @@ func (s *Server) createSession(w http.ResponseWriter, requestor string, rrequest } // Everything is authenticated and parsed, we're good to go! - qr, requestorToken, frontendAuth, err := s.irmaserv.StartSession(rrequest, s.doResultCallback) + qr, requestorToken, frontendRequest, err := s.irmaserv.StartSession(rrequest, s.doResultCallback) if err != nil { server.WriteError(w, server.ErrorInvalidRequest, err.Error()) return } server.WriteJson(w, server.SessionPackage{ - SessionPtr: qr, - Token: requestorToken, - FrontendAuth: frontendAuth, + SessionPtr: qr, + Token: requestorToken, + FrontendRequest: frontendRequest, }) } From 031810012637e08ebe3bfdbd1d520fdb3456027f Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Tue, 23 Mar 2021 12:03:03 +0100 Subject: [PATCH 72/77] feat: make chained sessions use the same frontend authorization token --- server/irmaserver/handle.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index cba465adf..8095ae5c6 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -320,6 +320,7 @@ func (s *Server) startNext(session *session, res *irma.ServerSessionResponse) er // from sessions before that, need to be disclosed in the new session as well newsession := s.sessions.get(token) newsession.implicitDisclosure = disclosed + newsession.frontendAuth = session.frontendAuth res.NextSession = qr return nil From 08ae605afccbb2d5ed9031e326e574087fe7dd06 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Thu, 25 Mar 2021 15:56:46 +0100 Subject: [PATCH 73/77] refactor: rename ClientRequest to ClientSessionRequest --- irmaclient/session.go | 4 ++-- requests.go | 18 +++++++++--------- server/irmaserver/handle.go | 2 +- server/irmaserver/helpers.go | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/irmaclient/session.go b/irmaclient/session.go index 8b82f2af2..4af006e02 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -257,10 +257,10 @@ func (session *session) getSessionInfo() { session.Handler.StatusUpdate(session.Action, irma.ClientStatusCommunicating) // Get the first IRMA protocol message and parse it - cr := &irma.ClientRequest{ + cr := &irma.ClientSessionRequest{ Request: session.request, // As request is an interface, it needs to be initialized with a specific instance. } - // UnmarshalJSON of ClientRequest takes into account legacy protocols, so we do not have to check that here. + // UnmarshalJSON of ClientSessionRequest takes into account legacy protocols, so we do not have to check that here. err := session.transport.Get("", cr) if err != nil { session.fail(err.(*irma.SessionError)) diff --git a/requests.go b/requests.go index 2d1e146ed..961ab75c3 100644 --- a/requests.go +++ b/requests.go @@ -24,7 +24,7 @@ const ( LDContextIssuanceRequest = "https://irma.app/ld/request/issuance/v2" LDContextRevocationRequest = "https://irma.app/ld/request/revocation/v1" LDContextFrontendOptionsRequest = "https://irma.app/ld/request/frontendoptions/v1" - LDContextClientRequest = "https://irma.app/ld/request/client/v1" + LDContextClientSessionRequest = "https://irma.app/ld/request/client/v1" LDContextSessionOptions = "https://irma.app/ld/options/v1" DefaultJwtValidity = 120 ) @@ -259,8 +259,8 @@ type SessionOptions struct { PairingCode string `json:"pairingCode,omitempty"` } -// ClientRequest contains all information irmaclient needs to know to initiate a session. -type ClientRequest struct { +// ClientSessionRequest contains all information irmaclient needs to know to initiate a session. +type ClientSessionRequest struct { LDContext string `json:"@context,omitempty"` ProtocolVersion *ProtocolVersion `json:"protocolVersion,omitempty"` Options *SessionOptions `json:"options,omitempty"` @@ -1151,14 +1151,14 @@ func (or *FrontendOptionsRequest) Validate() error { return nil } -func (cr *ClientRequest) UnmarshalJSON(data []byte) error { +func (cr *ClientSessionRequest) UnmarshalJSON(data []byte) error { // Unmarshal in alias first to prevent infinite recursion - type alias ClientRequest + type alias ClientSessionRequest err := json.Unmarshal(data, (*alias)(cr)) if err != nil { return err } - if cr.LDContext == LDContextClientRequest { + if cr.LDContext == LDContextClientSessionRequest { return nil } @@ -1167,7 +1167,7 @@ func (cr *ClientRequest) UnmarshalJSON(data []byte) error { if err != nil { return err } - cr.LDContext = LDContextClientRequest + cr.LDContext = LDContextClientSessionRequest cr.ProtocolVersion = cr.Request.Base().ProtocolVersion cr.Options = &SessionOptions{ LDContext: LDContextSessionOptions, @@ -1176,8 +1176,8 @@ func (cr *ClientRequest) UnmarshalJSON(data []byte) error { return nil } -func (cr *ClientRequest) Validate() error { - if cr.LDContext != LDContextClientRequest { +func (cr *ClientSessionRequest) Validate() error { + if cr.LDContext != LDContextClientSessionRequest { return errors.New("Not a client request") } // The 'Request' field is not required. When this field is empty, we have to skip the validation. diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 8095ae5c6..7764ec234 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -85,7 +85,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c } if session.version.Below(2, 7) { - // These versions do not support the ClientRequest format, so send the SessionRequest. + // These versions do not support the ClientSessionRequest format, so send the SessionRequest. request, err := session.getRequest() if err != nil { return nil, session.fail(server.ErrorRevocation, err.Error()) diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index dc0034eaf..8c0ddca30 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -309,9 +309,9 @@ func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, sche return session.kssProofs[scheme], nil } -func (session *session) getClientRequest() (*irma.ClientRequest, error) { - info := irma.ClientRequest{ - LDContext: irma.LDContextClientRequest, +func (session *session) getClientRequest() (*irma.ClientSessionRequest, error) { + info := irma.ClientSessionRequest{ + LDContext: irma.LDContextClientSessionRequest, ProtocolVersion: session.version, Options: &session.options, } From 70713d08a0a01054618833ba8ffda911babeb2e0 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Thu, 25 Mar 2021 15:57:47 +0100 Subject: [PATCH 74/77] fix: incorrect early return in SSE event handler --- wait_status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wait_status.go b/wait_status.go index 11c37e686..4b3d75d66 100644 --- a/wait_status.go +++ b/wait_status.go @@ -32,7 +32,7 @@ func subscribeSSE(transport *HTTPTransport, statuschan chan ServerStatus, errorc for { e := <-events if e == nil || e.Type == "open" { - return + continue } status := ServerStatus(strings.Trim(string(e.Data), `"`)) statuschan <- status From c85ea3692045b6a371d6a2fa1ab716eeaeedb518 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Thu, 25 Mar 2021 15:58:47 +0100 Subject: [PATCH 75/77] feat: clarify FrontendOptionsRequest Validate error message --- requests.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests.go b/requests.go index 961ab75c3..dec7f0bf4 100644 --- a/requests.go +++ b/requests.go @@ -1146,7 +1146,7 @@ func NewFrontendOptionsRequest() FrontendOptionsRequest { func (or *FrontendOptionsRequest) Validate() error { if or.LDContext != LDContextFrontendOptionsRequest { - return errors.New("Not an options request") + return errors.New("Not a frontend options request") } return nil } From 4f8737cc8a8e60f7db021e82304cadf3314aedeb Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Thu, 25 Mar 2021 16:17:51 +0100 Subject: [PATCH 76/77] feat: assign protocol version 2.8 to pairing --- internal/sessiontest/legacy_test.go | 2 +- internal/sessiontest/main_test.go | 2 +- internal/sessiontest/session_test.go | 4 ++-- irmaclient/irmaclient_test.go | 2 +- irmaclient/session.go | 7 ++++--- server/irmaserver/handle.go | 10 +++++----- server/irmaserver/sessions.go | 2 +- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/internal/sessiontest/legacy_test.go b/internal/sessiontest/legacy_test.go index 171c09762..2ca050422 100644 --- a/internal/sessiontest/legacy_test.go +++ b/internal/sessiontest/legacy_test.go @@ -40,7 +40,7 @@ func TestWithoutPairingSupport(t *testing.T) { defer func() { maxClientVersion = defaultMaxVersion }() - maxClientVersion = &irma.ProtocolVersion{Major: 2, Minor: 6} + maxClientVersion = &irma.ProtocolVersion{Major: 2, Minor: 7} t.Run("TestSigningSession", TestSigningSession) t.Run("TestDisclosureSession", TestDisclosureSession) diff --git a/internal/sessiontest/main_test.go b/internal/sessiontest/main_test.go index 666469153..ed69bc1d4 100644 --- a/internal/sessiontest/main_test.go +++ b/internal/sessiontest/main_test.go @@ -21,7 +21,7 @@ import ( ) // Defines the maximum protocol version of an irmaclient in tests -var maxClientVersion = &irma.ProtocolVersion{Major: 2, Minor: 7} +var maxClientVersion = &irma.ProtocolVersion{Major: 2, Minor: 8} func TestMain(m *testing.M) { // Create HTTP server for scheme managers diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index b4ab49b0d..e6beda8de 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -145,8 +145,8 @@ func TestIssuancePairing(t *testing.T) { pairingCode = setPairingMethod(irma.PairingMethodPin, handler) } pairingHandler := func(handler *TestHandler) { - // Below protocol version 2.7 pairing is not supported, so then the pairing stage is expected to be skipped. - if extractClientMaxVersion(handler.client).Below(2, 7) { + // Below protocol version 2.8 pairing is not supported, so then the pairing stage is expected to be skipped. + if extractClientMaxVersion(handler.client).Below(2, 8) { return } diff --git a/irmaclient/irmaclient_test.go b/irmaclient/irmaclient_test.go index 21c800dbe..742f889e1 100644 --- a/irmaclient/irmaclient_test.go +++ b/irmaclient/irmaclient_test.go @@ -124,7 +124,7 @@ func TestCandidates(t *testing.T) { // but we should also get the option to get another value request := irma.NewDisclosureRequest(attrtype) disjunction := request.Disclose[0] - request.ProtocolVersion = &irma.ProtocolVersion{Major: 2, Minor: 7} + request.ProtocolVersion = &irma.ProtocolVersion{Major: 2, Minor: 8} attrs, satisfiable, err := client.candidatesDisCon(request, disjunction) require.NoError(t, err) require.True(t, satisfiable) diff --git a/irmaclient/session.go b/irmaclient/session.go index 4af006e02..ca2b60c47 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -111,7 +111,8 @@ var supportedVersions = map[int][]int{ 4, // old protocol with legacy session requests 5, // introduces condiscon feature 6, // introduces nonrevocation proofs - 7, // introduces chained sessions and session binding + 7, // introduces chained sessions + 8, // introduces session binding }, } @@ -233,8 +234,8 @@ func (client *Client) newQrSession(qr *irma.Qr, handler Handler) *session { session.transport.SetHeader(irma.MinVersionHeader, min.String()) session.transport.SetHeader(irma.MaxVersionHeader, client.maxVersion.String()) - // From protocol version 2.7 also an authorization header must be included. - if client.maxVersion.Above(2, 6) { + // From protocol version 2.8 also an authorization header must be included. + if client.maxVersion.Above(2, 7) { clientAuth := common.NewSessionToken() session.transport.SetHeader(irma.AuthorizationHeader, clientAuth) } diff --git a/server/irmaserver/handle.go b/server/irmaserver/handle.go index 7764ec234..b96c5b462 100644 --- a/server/irmaserver/handle.go +++ b/server/irmaserver/handle.go @@ -48,9 +48,9 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c return nil, session.fail(server.ErrorProtocolVersion, "") } - // Protocol versions below 2.7 don't include an authorization header. Therefore skip the authorization + // Protocol versions below 2.8 don't include an authorization header. Therefore skip the authorization // header presence check if a lower version is used. - if clientAuth == "" && session.version.Above(2, 6) { + if clientAuth == "" && session.version.Above(2, 7) { return nil, session.fail(server.ErrorIrmaUnauthorized, "No authorization header provided") } session.clientAuth = clientAuth @@ -72,7 +72,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c logger.WithFields(logrus.Fields{"version": session.version.String()}).Debugf("Protocol version negotiated") session.request.Base().ProtocolVersion = session.version - if session.options.PairingMethod != irma.PairingMethodNone && session.version.Above(2, 6) { + if session.options.PairingMethod != irma.PairingMethodNone && session.version.Above(2, 7) { session.setStatus(irma.ServerStatusPairing) } else { session.setStatus(irma.ServerStatusConnected) @@ -84,7 +84,7 @@ func (session *session) handleGetClientRequest(min, max *irma.ProtocolVersion, c return legacy, nil } - if session.version.Below(2, 7) { + if session.version.Below(2, 8) { // These versions do not support the ClientSessionRequest format, so send the SessionRequest. request, err := session.getRequest() if err != nil { @@ -430,7 +430,7 @@ func (s *Server) handleSessionGet(w http.ResponseWriter, r *http.Request) { func (s *Server) handleSessionGetRequest(w http.ResponseWriter, r *http.Request) { session := r.Context().Value("session").(*session) - if session.version.Below(2, 7) { + if session.version.Below(2, 8) { server.WriteError(w, server.ErrorUnexpectedRequest, "Endpoint is not support in used protocol version") return } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index a997bcf65..9b0986270 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -78,7 +78,7 @@ const ( var ( minProtocolVersion = irma.NewVersion(2, 4) - maxProtocolVersion = irma.NewVersion(2, 7) + maxProtocolVersion = irma.NewVersion(2, 8) minFrontendProtocolVersion = irma.NewVersion(1, 0) maxFrontendProtocolVersion = irma.NewVersion(1, 1) From 2b248d737bbb3b7dcb68377db37ce00fcccaf6a3 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Thu, 25 Mar 2021 16:39:50 +0100 Subject: [PATCH 77/77] feat: always allow pairing when using chained sessions --- server/irmaserver/api.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/irmaserver/api.go b/server/irmaserver/api.go index 0c00f393c..c87d4dcc2 100644 --- a/server/irmaserver/api.go +++ b/server/irmaserver/api.go @@ -196,7 +196,9 @@ func (s *Server) StartSession(req interface{}, handler server.SessionHandler, } pairingRecommended := false - if action == irma.ActionDisclosing { + if rrequest.Base().NextSession != nil && rrequest.Base().NextSession.URL != "" { + pairingRecommended = true + } else if action == irma.ActionDisclosing { err := request.Disclosure().Disclose.Iterate(func(attr *irma.AttributeRequest) error { if attr.Value != nil { pairingRecommended = true