diff --git a/.github/actions/integration-test/action.yml b/.github/actions/integration-test/action.yml new file mode 100644 index 00000000..e6cf68b0 --- /dev/null +++ b/.github/actions/integration-test/action.yml @@ -0,0 +1,77 @@ +name: Integration test +description: Runs the tests in ./internal/sessiontest/client_integration_test.go against the given IRMA server and keyshare server artifacts. +inputs: + test-ref: + description: The branch, tag or SHA to check out the tests from + required: true + irma-server-artifact: + description: Artifact url or id of the irma server artifact to use + required: true + keyshare-server-artifact: + description: Artifact url or id of the keyshare server artifact to use + required: true +runs: + using: composite + steps: + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ^1.18 + + - name: Download IRMA server artifact (url) + if: startsWith(inputs.irma-server-artifact, 'https://') + run: curl --create-dirs -L -o ./bin-is/irma-linux-amd64 ${{ inputs.irma-server-artifact }} + shell: bash + + - name: Download IRMA server artifact (artifact id) + if: ${{ !startsWith(inputs.irma-server-artifact, 'https://') }} + uses: actions/download-artifact@v3 + with: + name: ${{ inputs.irma-server-artifact }} + path: bin-is + + - name: Set file permissions for bin-is + run: chmod +x ./bin-is/irma-linux-amd64 + shell: bash + + - name: Download keyshare server artifact (url) + if: startsWith(inputs.keyshare-server-artifact, 'https://') + run: curl --create-dirs -L -o ./bin-ks/irma-linux-amd64 ${{ inputs.keyshare-server-artifact }} + shell: bash + + - name: Download keyshare server artifact (artifact id) + if: ${{ !startsWith(inputs.keyshare-server-artifact, 'https://') }} + uses: actions/download-artifact@v3 + with: + name: ${{ inputs.keyshare-server-artifact }} + path: bin-ks + + - name: Set file permissions for bin-ks + run: chmod +x ./bin-ks/irma-linux-amd64 + shell: bash + + - name: Run keyshare server utilities + run: docker-compose up -d + shell: bash + + # We add & at the end of each command to run them in the background. + - name: Run IRMA server + run: ./bin-is/irma-linux-amd64 server -s testdata/irma_configuration --url http://localhost:port -p 48682 -k testdata/privatekeys & + shell: bash + + - name: Run keyshare server + run: ./bin-ks/irma-linux-amd64 keyshare server -c testdata/configurations/keyshareserver.yml & + shell: bash + + - name: Checkout test code + uses: actions/checkout@v3 + with: + ref: ${{ inputs.test-ref }} + path: irmago_test_checkout + + - name: Run integration tests + working-directory: irmago_test_checkout + env: + IRMAGO_INTEGRATION_TESTS: Y + run: go test -v -run TestClientIntegration -p 1 ./... + shell: bash diff --git a/.github/workflows/status-checks.yml b/.github/workflows/status-checks.yml index 79ccb521..5a50447a 100644 --- a/.github/workflows/status-checks.yml +++ b/.github/workflows/status-checks.yml @@ -29,10 +29,17 @@ jobs: - name: Build artifact uses: ./.github/actions/build + id: build with: os: ${{ matrix.os }} arch: ${{ matrix.arch }} + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: irma-${{ matrix.os }}-${{ matrix.arch }} + path: ${{ steps.build.outputs.artifact-name }} + docker-build: runs-on: ubuntu-latest steps: @@ -89,6 +96,43 @@ jobs: - name: Run all unit tests run: docker-compose run test -v ./... + # The integration tests are split into two jobs, one for the client side and one for the server side. + # They test whether irmago versions with different versions of gabi can interact. + # We assume that the keyshare server is always kept up to date, so we don't test using older versions of the keyshare server. + integration-test-clientside: # Checks whether irmaclient interacts with older IRMA servers + needs: build + strategy: + matrix: + irma-server: + - v0.13.2 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run integration test + uses: ./.github/actions/integration-test + with: + test-ref: ${{ github.ref }} + irma-server-artifact: https://github.com/privacybydesign/irmago/releases/download/${{ matrix.irma-server }}/irma-linux-amd64 + keyshare-server-artifact: irma-linux-amd64 # Current build + + integration-test-serverside: # Checks whether IRMA server interacts with older irmaclients + needs: build + strategy: + matrix: + irmaclient-ref: + - f0023141ec429d912e329665f937d05b178a3fee # v0.13.2 (integration test did not exist when version tag was created) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run integration test + uses: ./.github/actions/integration-test + with: + test-ref: ${{ matrix.irmaclient-ref }} + irma-server-artifact: irma-linux-amd64 # Current build + keyshare-server-artifact: irma-linux-amd64 # Current build + analyze: needs: build runs-on: ubuntu-latest diff --git a/internal/sessiontest/client_integration_test.go b/internal/sessiontest/client_integration_test.go new file mode 100644 index 00000000..cbc8a71d --- /dev/null +++ b/internal/sessiontest/client_integration_test.go @@ -0,0 +1,76 @@ +package sessiontest + +/* +This file contains the integration tests for the irmaclient library. +A subset of tests from session_test.go and keyshare_test.go can be run against specific versions of the IRMA server and keyshare server. +In this way we test the backwards compatibility of the irmaclient library. +The other way around, the backwards compatibility of the IRMA server and keyshare server, can be tested by checking out the +source code of an older irmago version and run an older version of this test against a newer server version. +This integration test is being introduced after irmago v0.13.2, so older irmaclient versions cannot be tested using this setup. + +This test only runs if you pass IRMAGO_INTEGRATION_TESTS=Y to go test, i.e.: IRMAGO_INTEGRATION_TESTS=Y go test -run TestClientIntegration -p 1 ./... +Before running this test, you should start the IRMA server and keyshare server manually. + +First, ensure you installed the desired irma version. +To start the IRMA server, run the following command: +$ irma server -s testdata/irma_configuration --url http://localhost:port -p 48682 -k testdata/privatekeys + +To start the keyshare server, run the following commands: +$ docker-compose up -d +$ irma keyshare server -c testdata/configurations/keyshareserver.yml +*/ + +import ( + "os" + "strings" + "testing" + + irma "github.com/privacybydesign/irmago" + "github.com/privacybydesign/irmago/internal/test" +) + +func TestClientIntegration(t *testing.T) { + if !strings.HasPrefix(strings.ToUpper(os.Getenv("IRMAGO_INTEGRATION_TESTS")), "Y") { + t.Skip("set IRMAGO_INTEGRATION_TESTS=Y to run this test") + } + + // Tests without keyshare server. + t.Run("DisclosureSession", apply(testDisclosureSession, nil, optionReuseServer, optionForceNoAuth)) + t.Run("NoAttributeDisclosureSession", apply(testNoAttributeDisclosureSession, nil, optionReuseServer, optionForceNoAuth)) + t.Run("EmptyDisclosure", apply(testEmptyDisclosure, nil, optionReuseServer, optionForceNoAuth)) + t.Run("SigningSession", apply(testSigningSession, nil, optionReuseServer, optionForceNoAuth)) + t.Run("IssuanceSession", apply(testIssuanceSession, nil, optionReuseServer, optionForceNoAuth)) + t.Run("MultipleIssuanceSession", apply(testMultipleIssuanceSession, nil, optionReuseServer, optionForceNoAuth)) + t.Run("DefaultCredentialValidity", apply(testDefaultCredentialValidity, nil, optionReuseServer, optionForceNoAuth)) + t.Run("IssuanceDisclosureEmptyAttributes", apply(testIssuanceDisclosureEmptyAttributes, nil, optionReuseServer, optionForceNoAuth)) + t.Run("IssuanceOptionalZeroLengthAttributes", apply(testIssuanceOptionalZeroLengthAttributes, nil, optionReuseServer, optionForceNoAuth)) + t.Run("IssuanceOptionalSetAttributes", apply(testIssuanceOptionalSetAttributes, nil, optionReuseServer, optionForceNoAuth)) + t.Run("IssuanceSameAttributesNotSingleton", apply(testIssuanceSameAttributesNotSingleton, nil, optionReuseServer, optionForceNoAuth)) + t.Run("IssuancePairing", apply(testIssuancePairing, nil, optionReuseServer, optionForceNoAuth)) + t.Run("PairingRejected", apply(testPairingRejected, nil, optionReuseServer, optionForceNoAuth)) + t.Run("LargeAttribute", apply(testLargeAttribute, nil, optionReuseServer, optionForceNoAuth)) + t.Run("IssuanceSingletonCredential", apply(testIssuanceSingletonCredential, nil, optionReuseServer, optionForceNoAuth)) + t.Run("UnsatisfiableDisclosureSession", apply(testUnsatisfiableDisclosureSession, nil, optionReuseServer, optionForceNoAuth)) + t.Run("AttributeByteEncoding", apply(testAttributeByteEncoding, nil, optionReuseServer, optionForceNoAuth)) + t.Run("IssuedCredentialIsStored", apply(testIssuedCredentialIsStored, nil, optionReuseServer, optionForceNoAuth)) + t.Run("BlindIssuanceSession", apply(testBlindIssuanceSession, nil, optionReuseServer, optionForceNoAuth)) + t.Run("DisablePairing", apply(testDisablePairing, nil, optionReuseServer, optionForceNoAuth)) + t.Run("DisclosureMultipleAttrs", apply(testDisclosureMultipleAttrs, nil, optionReuseServer, optionForceNoAuth)) + t.Run("CombinedSessionMultipleAttributes", apply(testCombinedSessionMultipleAttributes, nil, optionReuseServer, optionForceNoAuth)) + t.Run("ConDisCon", apply(testConDisCon, nil, optionReuseServer, optionForceNoAuth)) + t.Run("OptionalDisclosure", apply(testOptionalDisclosure, nil, optionReuseServer, optionForceNoAuth)) + + // Test with keyshare server. + t.Run("KeyshareSessions", func(t *testing.T) { + storage := test.CreateTestStorage(t) + client, handler := parseExistingStorage(t, storage) + defer test.ClearTestStorage(t, client, handler.storage) + + // Fresh irmaclient storage was used, so we need to do some initialization. + client.KeyshareEnroll(irma.NewSchemeManagerIdentifier("test"), nil, "12345", "en") + req := getIssuanceRequest(false) + doSession(t, req, client, nil, nil, nil, nil, optionReuseServer, optionForceNoAuth) + + keyshareSessions(t, client, nil, optionReuseServer, optionForceNoAuth) + }) +} diff --git a/internal/sessiontest/helper_dosession_test.go b/internal/sessiontest/helper_dosession_test.go index c1c59b09..11567e51 100644 --- a/internal/sessiontest/helper_dosession_test.go +++ b/internal/sessiontest/helper_dosession_test.go @@ -46,6 +46,7 @@ const ( optionRetryPost optionIgnoreError optionReuseServer // makes doSession assume a requestor server with authentication is used + optionForceNoAuth // makes doSession assume no authentication is required (useful when reused server has no authentication) optionClientWait optionWait optionPrePairingClient @@ -104,7 +105,7 @@ func startServer(t *testing.T, opts option, irmaServer *IrmaServer, conf interfa // startSessionAtServer starts an IRMA session using the specified session request, against an IRMA server // or library, as determined by the type of serv. -func startSessionAtServer(t *testing.T, serv stopper, conf interface{}, request interface{}) *server.SessionPackage { +func startSessionAtServer(t *testing.T, serv stopper, useJWTs bool, request interface{}) *server.SessionPackage { switch s := serv.(type) { case *IrmaServer: qr, requestorToken, frontendRequest, err := s.irma.StartSession(request, nil) @@ -116,15 +117,9 @@ func startSessionAtServer(t *testing.T, serv stopper, conf interface{}, request } default: var ( - sesPkg server.SessionPackage - err error - useJWTs bool + sesPkg server.SessionPackage + err error ) - if conf != nil { - useJWTs = !conf.(*requestorserver.Configuration).DisableRequestorAuthentication - } else { - useJWTs = true - } url := requestorServerURL if useJWTs { skbts, err := os.ReadFile(filepath.Join(testdata, "jwtkeys", "requestor1-sk.pem")) @@ -153,7 +148,7 @@ func startSessionAtClient(t *testing.T, sesPkg *server.SessionPackage, client *i } // getSessionResult retrieves the session result from the IRMA server or library. -func getSessionResult(t *testing.T, sesPkg *server.SessionPackage, serv stopper, opts option) *server.SessionResult { +func getSessionResult(t *testing.T, sesPkg *server.SessionPackage, serv stopper, useJWTs bool, opts option) *server.SessionResult { waitSessionFinished(t, serv, sesPkg.Token, opts.enabled(optionWait)) switch s := serv.(type) { @@ -162,28 +157,35 @@ func getSessionResult(t *testing.T, sesPkg *server.SessionPackage, serv stopper, require.NoError(t, err) return result default: - var res string - err := irma.NewHTTPTransport(requestorServerURL+"/session/"+string(sesPkg.Token), false).Get("result-jwt", &res) - require.NoError(t, err) + if useJWTs { + var res string + err := irma.NewHTTPTransport(requestorServerURL+"/session/"+string(sesPkg.Token), false).Get("result-jwt", &res) + require.NoError(t, err) - bts, err := os.ReadFile(jwtPrivkeyPath) - require.NoError(t, err) - sk, err := jwt.ParseRSAPrivateKeyFromPEM(bts) - require.NoError(t, err) + bts, err := os.ReadFile(jwtPrivkeyPath) + require.NoError(t, err) + sk, err := jwt.ParseRSAPrivateKeyFromPEM(bts) + require.NoError(t, err) - // Validate JWT - claims := struct { - jwt.RegisteredClaims - *server.SessionResult - }{} - _, err = jwt.ParseWithClaims(res, &claims, func(_ *jwt.Token) (interface{}, error) { - return &sk.PublicKey, nil - }) - require.NoError(t, err) + // Validate JWT + claims := struct { + jwt.RegisteredClaims + *server.SessionResult + }{} + _, err = jwt.ParseWithClaims(res, &claims, func(_ *jwt.Token) (interface{}, error) { + return &sk.PublicKey, nil + }) + require.NoError(t, err) - // Check default expiration time - require.True(t, claims.IssuedAt.Add(irma.DefaultJwtValidity*time.Second).Equal(claims.ExpiresAt.Time)) - return claims.SessionResult + // Check default expiration time + require.True(t, claims.IssuedAt.Add(irma.DefaultJwtValidity*time.Second).Equal(claims.ExpiresAt.Time)) + return claims.SessionResult + } else { + var res server.SessionResult + err := irma.NewHTTPTransport(requestorServerURL+"/session/"+string(sesPkg.Token), false).Get("result", &res) + require.NoError(t, err) + return &res + } } } @@ -264,7 +266,14 @@ func doSession( defer serv.Stop() } - sesPkg := startSessionAtServer(t, serv, conf, request) + useJWTs := true + if opts.enabled(optionForceNoAuth) { + useJWTs = false + } else if rconf, ok := conf.(*requestorserver.Configuration); ok { + useJWTs = !rconf.DisableRequestorAuthentication + } + + sesPkg := startSessionAtServer(t, serv, useJWTs, request) sessionHandler, clientChan := createSessionHandler(t, opts, client, sesPkg, frontendOptionsHandler, pairingHandler) if frontendOptionsHandler != nil { @@ -293,7 +302,7 @@ func doSession( return &requestorSessionResult{nil, nil, clientResult.Missing, dismisser} } - serverResult := getSessionResult(t, sesPkg, serv, opts) + serverResult := getSessionResult(t, sesPkg, serv, useJWTs, opts) require.Equal(t, sesPkg.Token, serverResult.Token) if opts.enabled(optionRetryPost) { diff --git a/internal/sessiontest/keyshare_test.go b/internal/sessiontest/keyshare_test.go index 205e8efa..bc4efae5 100644 --- a/internal/sessiontest/keyshare_test.go +++ b/internal/sessiontest/keyshare_test.go @@ -62,7 +62,7 @@ func TestKeyshareSessions(t *testing.T) { keyshareSessions(t, client, irmaServer) } -func keyshareSessions(t *testing.T, client *irmaclient.Client, irmaServer *IrmaServer) { +func keyshareSessions(t *testing.T, client *irmaclient.Client, irmaServer *IrmaServer, options ...option) { id := irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID") expiry := irma.Timestamp(irma.NewMetadataAttribute(0).Expiry()) issuanceRequest := getCombinedIssuanceRequest(id) @@ -73,15 +73,15 @@ func keyshareSessions(t *testing.T, client *irmaclient.Client, irmaServer *IrmaS Attributes: map[string]string{"email": "testusername"}, }, ) - doSession(t, issuanceRequest, client, irmaServer, nil, nil, nil) + doSession(t, issuanceRequest, client, irmaServer, nil, nil, nil, options...) disclosureRequest := getDisclosureRequest(id) disclosureRequest.AddSingle(irma.NewAttributeTypeIdentifier("test.test.mijnirma.email"), nil, nil) - doSession(t, disclosureRequest, client, irmaServer, nil, nil, nil) + doSession(t, disclosureRequest, client, irmaServer, nil, nil, nil, options...) sigRequest := getSigningRequest(id) sigRequest.AddSingle(irma.NewAttributeTypeIdentifier("test.test.mijnirma.email"), nil, nil) - doSession(t, sigRequest, client, irmaServer, nil, nil, nil) + doSession(t, sigRequest, client, irmaServer, nil, nil, nil, options...) } func TestIssuanceCombinedMultiSchemeSession(t *testing.T) { diff --git a/internal/sessiontest/session_test.go b/internal/sessiontest/session_test.go index d4b020a9..800ecec7 100644 --- a/internal/sessiontest/session_test.go +++ b/internal/sessiontest/session_test.go @@ -987,7 +987,8 @@ func TestStatusEventsSSE(t *testing.T) { // Start a session at the server request := irma.NewDisclosureRequest(irma.NewAttributeTypeIdentifier("irma-demo.RU.studentCard.studentID")) - sesPkg := startSessionAtServer(t, rs, conf, request) + useJWTs := !conf.DisableRequestorAuthentication + sesPkg := startSessionAtServer(t, rs, useJWTs, request) // Start SSE connections to the SSE endpoints url := fmt.Sprintf("http://localhost:%d/session/%s/statusevents", conf.Port, sesPkg.Token)