diff --git a/go.mod b/go.mod index 11a44113..126ff0c7 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.1 github.com/mdp/qrterminal v1.0.1 github.com/mitchellh/mapstructure v1.5.0 - github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750 + github.com/privacybydesign/gabi v0.0.0-20230831075606-4e3065ad82c8 github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cast v1.5.0 @@ -62,15 +62,17 @@ require ( github.com/jackc/pgx/v5 v5.3.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/microsoft/go-mssqldb v1.1.0 // indirect github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect - github.com/minio/sha256-simd v0.1.1 // indirect - github.com/mr-tron/base58 v1.1.3 // indirect - github.com/multiformats/go-multihash v0.0.11 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-multihash v0.0.14 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect github.com/nightlyone/lockfile v1.0.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.2 // indirect diff --git a/go.sum b/go.sum index 149cb85b..87e31e92 100644 --- a/go.sum +++ b/go.sum @@ -236,6 +236,9 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -263,16 +266,20 @@ github.com/microsoft/go-mssqldb v1.1.0/go.mod h1:LzkFdl4z2Ck+Hi+ycGOTbL56VEfgoyA github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= -github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/multiformats/go-multihash v0.0.11 h1:yEyBxwoR/7vBM5NfLVXRnpQNVLrMhpS6MRb7Z/1pnzc= -github.com/multiformats/go-multihash v0.0.11/go.mod h1:LXRDJcYYY+9BjlsFe6i5LV7uekf0OoEJdnRmitUshxk= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-multihash v0.0.14 h1:QoBceQYQQtNUuf6s7wHxnE2c8bhbMqhfGzNI032se/I= +github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -299,8 +306,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750 h1:3RuYOQTlArQ6Uw2TgySusmZGluP+18WdQL56YSfkM3Q= -github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750/go.mod h1:QZI8hX8Ff2GfZ7UJuxyWw3nAGgt2s5+U4hxY6rmwQvs= +github.com/privacybydesign/gabi v0.0.0-20230831075606-4e3065ad82c8 h1:LYtJIL5/UNgGM9hyOHruNFK3DSIHrgRGSb4Ro8bPH48= +github.com/privacybydesign/gabi v0.0.0-20230831075606-4e3065ad82c8/go.mod h1:fg/AOtaB2FM/LzAulWo075CdeKeFAqLtpwVcGNArqN8= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= @@ -400,7 +407,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/identifiers.go b/identifiers.go index e3cd66c2..617fb02b 100644 --- a/identifiers.go +++ b/identifiers.go @@ -281,7 +281,11 @@ func (pki *PublicKeyIdentifier) UnmarshalText(text []byte) error { } func (pki *PublicKeyIdentifier) MarshalText() (text []byte, err error) { - return []byte(fmt.Sprintf("%s-%d", pki.Issuer, pki.Counter)), nil + return []byte(pki.String()), nil +} + +func (pki *PublicKeyIdentifier) String() string { + return fmt.Sprintf("%s-%d", pki.Issuer, pki.Counter) } // MarshalText implements encoding.TextMarshaler. diff --git a/internal/sessiontest/helper_dosession_test.go b/internal/sessiontest/helper_dosession_test.go index 11567e51..cbd6ce7d 100644 --- a/internal/sessiontest/helper_dosession_test.go +++ b/internal/sessiontest/helper_dosession_test.go @@ -50,6 +50,7 @@ const ( optionClientWait optionWait optionPrePairingClient + optionLinkableKeyshareResponse optionPolling optionNoSchemeAssets ) diff --git a/internal/sessiontest/helper_main_test.go b/internal/sessiontest/helper_main_test.go index 25bfbea4..1f70599d 100644 --- a/internal/sessiontest/helper_main_test.go +++ b/internal/sessiontest/helper_main_test.go @@ -71,10 +71,14 @@ func parseExistingStorage(t *testing.T, storage string, options ...option) (*irm err = client.Configuration.ParseFolder() require.NoError(t, err) } + + version := extractClientMaxVersion(client) if opts.enabled(optionPrePairingClient) { - version := extractClientMaxVersion(client) - // set to largest protocol version that dos not support pairing + // Set to largest protocol version that does not support pairing *version = irma.ProtocolVersion{Major: 2, Minor: 7} + } else if opts.enabled(optionLinkableKeyshareResponse) { + // Set to largest protocol version that uses linkable keyshare responses + *version = irma.ProtocolVersion{Major: 2, Minor: 8} } client.SetPreferences(irmaclient.Preferences{DeveloperMode: true}) diff --git a/internal/sessiontest/keyshare_test.go b/internal/sessiontest/keyshare_test.go index d9790979..27100b7f 100644 --- a/internal/sessiontest/keyshare_test.go +++ b/internal/sessiontest/keyshare_test.go @@ -166,12 +166,19 @@ func TestMultipleKeyshareServers(t *testing.T) { client.KeyshareEnroll(test2SchemeID, nil, "12345", "en") require.NoError(t, <-handler.c) + // A session request that contains attributes from both test and test2 (both distributed schemes) should fail. request := irma.NewDisclosureRequest( irma.NewAttributeTypeIdentifier("test.test.mijnirma.email"), irma.NewAttributeTypeIdentifier("test2.test.mijnirma.email"), ) - doSession(t, request, client, irmaServer, nil, nil, nil) + _, _, _, err = irmaServer.irma.StartSession(request, nil) + require.ErrorIs(t, err, irma.ErrMultipleDistributedSchemes) + // Do a session with a request that contains attributes from test2 only. + request = irma.NewDisclosureRequest( + irma.NewAttributeTypeIdentifier("test2.test.mijnirma.email"), + ) + doSession(t, request, client, irmaServer, nil, nil, nil) logs, err = client.LoadNewestLogs(20) require.NoError(t, err) require.Len(t, logs, logsAmount+2) diff --git a/internal/sessiontest/legacy_test.go b/internal/sessiontest/legacy_test.go index fb1a045a..29f71eee 100644 --- a/internal/sessiontest/legacy_test.go +++ b/internal/sessiontest/legacy_test.go @@ -5,6 +5,7 @@ import ( irma "github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago/internal/test" + "github.com/privacybydesign/irmago/internal/testkeyshare" "github.com/privacybydesign/irmago/server/irmaserver" "github.com/stretchr/testify/require" ) @@ -72,3 +73,13 @@ func TestWithoutPairingSupport(t *testing.T) { t.Run("StaticQRSession", apply(testStaticQRSession, nil, optionPrePairingClient)) } + +func TestLinkableKeyshareResponse(t *testing.T) { + keyshareServer := testkeyshare.StartKeyshareServer(t, logger, irma.NewSchemeManagerIdentifier("test")) + defer keyshareServer.Stop() + client, handler := parseStorage(t, optionLinkableKeyshareResponse) + defer test.ClearTestStorage(t, client, handler.storage) + irmaServer := StartIrmaServer(t, nil) + defer irmaServer.Stop() + keyshareSessions(t, client, irmaServer) +} diff --git a/irmaclient/client.go b/irmaclient/client.go index d8554622..997e7e42 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -12,7 +12,6 @@ import ( "github.com/go-errors/errors" "github.com/privacybydesign/gabi" "github.com/privacybydesign/gabi/big" - "github.com/privacybydesign/gabi/gabikeys" "github.com/privacybydesign/gabi/revocation" irma "github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago/internal/common" @@ -961,7 +960,7 @@ func (client *Client) IssuanceProofBuilders( } builders := gabi.ProofBuilderList([]gabi.ProofBuilder{}) - var keysharePs = map[irma.SchemeManagerIdentifier]*irma.PMap{} + var keysharePs = map[irma.PublicKeyIdentifier]*big.Int{} if keyshareSession != nil { keysharePs, err = keyshareSession.getKeysharePs(request) if err != nil { @@ -970,26 +969,14 @@ func (client *Client) IssuanceProofBuilders( } for _, futurecred := range request.Credentials { - var pk *gabikeys.PublicKey keyID := futurecred.PublicKeyIdentifier() - schemeID := keyID.Issuer.SchemeManagerIdentifier() - distributed := client.Configuration.SchemeManagers[schemeID].Distributed() - var keyshareP *big.Int - var present bool - if distributed { - keyshareP, present = keysharePs[schemeID].Ps[keyID] - if distributed && !present { - return nil, nil, nil, errors.Errorf("missing keyshareP for %s-%d", keyID.Issuer, keyID.Counter) - } - } - - pk, err = client.Configuration.PublicKey(futurecred.CredentialTypeID.IssuerIdentifier(), futurecred.KeyCounter) + pk, err := client.Configuration.PublicKey(futurecred.CredentialTypeID.IssuerIdentifier(), futurecred.KeyCounter) if err != nil { return nil, nil, nil, err } credtype := client.Configuration.CredentialTypes[futurecred.CredentialTypeID] credBuilder, err := gabi.NewCredentialBuilder(pk, request.GetContext(), - client.secretkey.Key, issuerProofNonce, keyshareP, credtype.RandomBlindAttributeIndices()) + client.secretkey.Key, issuerProofNonce, keysharePs[keyID], credtype.RandomBlindAttributeIndices()) if err != nil { return nil, nil, nil, err } @@ -1173,7 +1160,7 @@ func (client *Client) keyshareEnrollWorker(managerID irma.SchemeManagerIdentifie transport := irma.NewHTTPTransport(manager.KeyshareServer, !client.Preferences.DeveloperMode) qr := &irma.Qr{} - err = transport.Post("client/register", qr, irma.KeyshareEnrollment{EnrollmentJWT: jwtt}) + err = transport.Post("api/v1/client/register", qr, irma.KeyshareEnrollment{EnrollmentJWT: jwtt}) if err != nil { return err } @@ -1407,6 +1394,10 @@ func (client *Client) keyshareRemoveMultiple(schemeIDs []irma.SchemeManagerIdent if err != nil { return err } + err = client.storage.TxDeleteKeyshareCachedPs(tx) + if err != nil { + return err + } } } diff --git a/irmaclient/keyshare.go b/irmaclient/keyshare.go index bfa19169..4baff0da 100644 --- a/irmaclient/keyshare.go +++ b/irmaclient/keyshare.go @@ -13,6 +13,7 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/privacybydesign/gabi" "github.com/privacybydesign/gabi/big" + "github.com/privacybydesign/gabi/gabikeys" irma "github.com/privacybydesign/irmago" ) @@ -189,6 +190,30 @@ func (kss *keyshareServer) tokenValid(conf *irma.Configuration) bool { return true } +func (ks *keyshareSession) fail(manager irma.SchemeManagerIdentifier, err error) { + serr, ok := err.(*irma.SessionError) + if ok { + if serr.RemoteError != nil && len(serr.RemoteError.ErrorName) > 0 { + switch serr.RemoteError.ErrorName { + case "USER_NOT_FOUND": + ks.sessionHandler.KeyshareEnrollmentDeleted(manager) + case "USER_NOT_REGISTERED": + ks.sessionHandler.KeyshareEnrollmentIncomplete(manager) + case "USER_BLOCKED": + duration, err := strconv.Atoi(serr.RemoteError.Message) + if err != nil { // Not really clear what to do with duration, but should never happen anyway + duration = -1 + } + ks.sessionHandler.KeyshareBlocked(manager, duration) + default: + ks.sessionHandler.KeyshareError(&manager, err) + } + } + } else { + ks.sessionHandler.KeyshareError(&manager, err) + } +} + // VerifyPin asks for a pin, repeatedly if necessary, informing the handler of success or failure. // It returns whether the authentication was successful or not. func (ks *keyshareSession) VerifyPin(attempts int) bool { @@ -232,7 +257,7 @@ func (kss *keyshareServer) doChallengeResponse(signer Signer, transport *irma.HT } auth := &irma.KeyshareAuthChallenge{} - err = transport.Post("users/verify_start", auth, irma.KeyshareAuthRequest{AuthRequestJWT: jwtt}) + err = transport.Post("api/v1/users/verify_start", auth, irma.KeyshareAuthRequest{AuthRequestJWT: jwtt}) if err != nil { return nil, err } @@ -259,7 +284,7 @@ func (kss *keyshareServer) doChallengeResponse(signer Signer, transport *irma.HT } pinresult := &irma.KeysharePinStatus{} - err = transport.Post("users/verify/pin_challengeresponse", pinresult, irma.KeyshareAuthResponse{AuthResponseJWT: jwtt}) + err = transport.Post("api/v1/users/verify/pin_challengeresponse", pinresult, irma.KeyshareAuthResponse{AuthResponseJWT: jwtt}) if err != nil { return nil, err } @@ -336,34 +361,49 @@ func (ks *keyshareSession) verifyPinAttempt(pin string) ( // of all keyshare servers of their part of the private key, and merges these commitments // in our own proof builders. func (ks *keyshareSession) GetCommitments() { - pkids := map[irma.SchemeManagerIdentifier][]*irma.PublicKeyIdentifier{} - commitments := map[irma.PublicKeyIdentifier]*gabi.ProofPCommitment{} + pkidsBuilders := make([]irma.PublicKeyIdentifier, len(ks.builders)) + pkidsKeyshare := map[irma.SchemeManagerIdentifier][]irma.PublicKeyIdentifier{} + pksKeyshare := map[irma.PublicKeyIdentifier]*gabikeys.PublicKey{} // For each scheme manager, build a list of public keys under this manager // that we will use in the keyshare protocol with the keyshare server of this manager - for _, builder := range ks.builders { + for i, builder := range ks.builders { pk := builder.PublicKey() + pkid := irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier(pk.Issuer), Counter: pk.Counter} + pkidsBuilders[i] = pkid + managerID := irma.NewIssuerIdentifier(pk.Issuer).SchemeManagerIdentifier() - if !ks.client.Configuration.SchemeManagers[managerID].Distributed() { - continue - } - if _, contains := pkids[managerID]; !contains { - pkids[managerID] = []*irma.PublicKeyIdentifier{} + if ks.client.Configuration.SchemeManagers[managerID].Distributed() { + pksKeyshare[pkid] = pk + pkidsKeyshare[managerID] = append(pkidsKeyshare[managerID], pkid) } - pkids[managerID] = append(pkids[managerID], &irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier(pk.Issuer), Counter: pk.Counter}) + } + + // Construct randomizer for proving equality of the secret key of which knowledge is proven in the multiple proofs + randomizers, err := gabi.NewProofRandomizers() + if err != nil { + ks.fail(irma.NewSchemeManagerIdentifier(""), irma.WrapErrorPrefix(err, "randomizers could not be constructed")) + return + } + + // Calculate the user commitments + hash, challengeInput, err := gabi.KeyshareUserCommitmentRequest(ks.builders, randomizers, pksKeyshare) + if err != nil { + ks.fail(irma.NewSchemeManagerIdentifier(""), irma.WrapErrorPrefix(err, "keyshare user commitment could not be calculated")) + return } // Now inform each keyshare server of with respect to which public keys // we want them to send us commitments - for managerID := range ks.schemeIDs { - if !ks.client.Configuration.SchemeManagers[managerID].Distributed() { - continue + commitments := map[irma.PublicKeyIdentifier]*gabi.ProofPCommitment{} + for managerID, pkids := range pkidsKeyshare { + req := irma.GetCommitmentsRequest{ + Keys: pkids, + Hash: hash, } - transport := ks.transports[managerID] - comms := &irma.ProofPCommitmentMap{} - err := transport.Post("prove/getCommitments", comms, pkids[managerID]) - if err != nil { + comms := &irma.ProofPCommitmentMapV2{} + if err := ks.transports[managerID].Post("api/v2/prove/getCommitments", comms, req); err != nil { if err.(*irma.SessionError).RemoteError != nil && err.(*irma.SessionError).RemoteError.Status == http.StatusForbidden && !ks.pinCheck { // JWT may be out of date due to clock drift; request pin and try again @@ -379,31 +419,29 @@ func (ks *keyshareSession) GetCommitments() { ks.sessionHandler.KeyshareError(&managerID, err) return } - for pki, c := range comms.Commitments { - commitments[pki] = c + for pkid, c := range comms.Commitments { + commitments[pkid] = &gabi.ProofPCommitment{Pcommit: c} } } // Merge in the commitments - for _, builder := range ks.builders { - pk := builder.PublicKey() - pki := irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier(pk.Issuer), Counter: pk.Counter} - comm, distributed := commitments[pki] - if !distributed { - continue + for i, pkid := range pkidsBuilders { + if comm, ok := commitments[pkid]; ok { + ks.builders[i].SetProofPCommitment(comm) } - builder.SetProofPCommitment(comm) } - ks.GetProofPs() + ks.GetProofPs(randomizers, challengeInput) } // GetProofPs uses the combined commitments of all keyshare servers and ourself // to calculate the challenge, which is sent to the keyshare servers in order to // receive their responses (2nd and 3rd message in Schnorr zero-knowledge protocol). -func (ks *keyshareSession) GetProofPs() { - _, issig := ks.session.(*irma.SignatureRequest) - challenge, err := ks.builders.Challenge(ks.session.Base().GetContext(), ks.session.GetNonce(ks.timestamp), issig) +func (ks *keyshareSession) GetProofPs(randomizers map[string]*big.Int, hashInput []gabi.KeyshareUserChallengeInput[irma.PublicKeyIdentifier]) { + _, isSig := ks.session.(*irma.SignatureRequest) + _, isIssuance := ks.session.(*irma.IssuanceRequest) + + req, challenge, err := gabi.KeyshareUserResponseRequest(ks.builders, randomizers, hashInput, ks.session.Base().GetContext(), ks.session.GetNonce(ks.timestamp), isSig) if err != nil { ks.sessionHandler.KeyshareError(&ks.keyshareServer.SchemeManagerIdentifier, err) return @@ -416,13 +454,25 @@ func (ks *keyshareSession) GetProofPs() { if !distributed { continue } - var j string - err = transport.Post("prove/getResponse", &j, challenge) + + // If the protocol version is below 2.9, the P value should be included in the JWT. Legacy issuers need this P value to validate the commitments. + // We obtain the JWT containing the P value using the api/v2/prove/getResponseLinkable endpoint. + // For disclosure and signing sessions, the P value is being merged on our side (the client side). + // This means that in these cases we need to use the api/v2/prove/getResponse endpoint. Otherwise, we would trigger legacy behavior in gabi. + var endpoint string + if ks.protocolVersion.Below(2, 9) && isIssuance { + endpoint = "api/v2/prove/getResponseLinkable" + } else { + endpoint = "api/v2/prove/getResponse" + } + + var respJwt string + err = transport.Post(endpoint, &respJwt, req) if err != nil { ks.sessionHandler.KeyshareError(&managerID, err) return } - responses[managerID] = j + responses[managerID] = respJwt } ks.Finish(challenge, responses) @@ -472,8 +522,7 @@ func (ks *keyshareSession) finishDisclosureOrSigning(challenge *big.Int, respons jwt.StandardClaims ProofP *gabi.ProofP }{} - parser := new(jwt.Parser) - parser.SkipClaimsValidation = true // no need to abort due to clock drift issues + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) // no need to validate claims due to clock drift issues if _, err := parser.ParseWithClaims(responses[managerID], &claims, ks.client.Configuration.KeyshareServerKeyFunc(managerID)); err != nil { ks.sessionHandler.KeyshareError(&managerID, err) return @@ -492,7 +541,7 @@ func (ks *keyshareSession) finishDisclosureOrSigning(challenge *big.Int, respons // getKeysharePs retrieves all P values (i.e. R_0^{keyshare server secret}) from all keyshare servers, // for use during issuance. -func (ks *keyshareSession) getKeysharePs(request *irma.IssuanceRequest) (map[irma.SchemeManagerIdentifier]*irma.PMap, error) { +func (ks *keyshareSession) getKeysharePs(request *irma.IssuanceRequest) (map[irma.PublicKeyIdentifier]*big.Int, error) { // Assemble keys of which to retrieve P's, grouped per keyshare server distributedKeys := map[irma.SchemeManagerIdentifier][]irma.PublicKeyIdentifier{} for _, futurecred := range request.Credentials { @@ -502,13 +551,32 @@ func (ks *keyshareSession) getKeysharePs(request *irma.IssuanceRequest) (map[irm } } - keysharePs := map[irma.SchemeManagerIdentifier]*irma.PMap{} - for schemeID, keys := range distributedKeys { - Ps := irma.PMap{Ps: map[irma.PublicKeyIdentifier]*big.Int{}} - if err := ks.transports[schemeID].Post("api/v2/prove/getPs", &Ps, keys); err != nil { + // Collect the P values for the public keys we want to get commitments for. + keysharePs := map[irma.PublicKeyIdentifier]*big.Int{} + missingKeysharePs := map[irma.SchemeManagerIdentifier][]irma.PublicKeyIdentifier{} + for _, pkids := range distributedKeys { + for _, pkid := range pkids { + if p, err := ks.client.storage.LoadKeyshareCachedP(pkid); err == nil { + keysharePs[pkid] = p + } else { + managerID := pkid.Issuer.SchemeManagerIdentifier() + missingKeysharePs[managerID] = append(missingKeysharePs[managerID], pkid) + } + } + } + + // If we don't have all P values, we ask the keyshare server for the missing ones. + for managerID, pkids := range missingKeysharePs { + var pMap *irma.PMap + if err := ks.transports[managerID].Post("api/v2/prove/getPs", &pMap, pkids); err != nil { return nil, err } - keysharePs[schemeID] = &Ps + if err := ks.client.storage.StoreKeyshareCachedPs(pMap.Ps); err != nil { + return nil, err + } + for pkid, p := range pMap.Ps { + keysharePs[pkid] = p + } } return keysharePs, nil diff --git a/irmaclient/legacy.go b/irmaclient/legacy.go index 7f8274ca..66e38d66 100644 --- a/irmaclient/legacy.go +++ b/irmaclient/legacy.go @@ -540,7 +540,7 @@ func (kss *keyshareServer) registerPublicKey(client *Client, transport *irma.HTT } result := &irma.KeysharePinStatus{} - err = transport.Post("users/register_publickey", result, irma.KeyshareKeyRegistration{PublicKeyRegistrationJWT: jwtt}) + err = transport.Post("api/v1/users/register_publickey", result, irma.KeyshareKeyRegistration{PublicKeyRegistrationJWT: jwtt}) if err != nil { err = irma.WrapErrorPrefix(err, "failed to register public key") return nil, err diff --git a/irmaclient/storage.go b/irmaclient/storage.go index 89e31e2a..ad847cd0 100644 --- a/irmaclient/storage.go +++ b/irmaclient/storage.go @@ -10,6 +10,7 @@ import ( "time" "github.com/privacybydesign/gabi" + "github.com/privacybydesign/gabi/big" "github.com/privacybydesign/gabi/revocation" irma "github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago/internal/common" @@ -45,9 +46,10 @@ const ( updatesKey = "updates" // Value: []update kssKey = "kss" // Value: map[irma.SchemeManagerIdentifier]*keyshareServer - attributesBucket = "attrs" // Key: []byte, value: []*irma.AttributeList - logsBucket = "logs" // Key: (auto-increment index), value: *LogEntry - signaturesBucket = "sigs" // Key: credential.attrs.Hash, value: *gabi.CLSignature + attributesBucket = "attrs" // Key: []byte, value: []*irma.AttributeList + logsBucket = "logs" // Key: (auto-increment index), value: *LogEntry + signaturesBucket = "sigs" // Key: credential.attrs.Hash, value: *gabi.CLSignature + keyshareCachedPsBucket = "keyshare_cached_ps" // Key: irma.PublicKeyIdentifier, value: *big.Int ) func (s *storage) path(p string) string { @@ -333,6 +335,17 @@ func (s *storage) TxStoreUpdates(tx *transaction, updates []update) error { return s.txStore(tx, userdataBucket, updatesKey, updates) } +func (s *storage) StoreKeyshareCachedPs(ps map[irma.PublicKeyIdentifier]*big.Int) error { + return s.Transaction(func(tx *transaction) error { + for pkid, p := range ps { + if err := s.txStore(tx, keyshareCachedPsBucket, pkid.String(), p); err != nil { + return err + } + } + return nil + }) +} + func (s *storage) LoadSignature(attrs *irma.AttributeList) (*gabi.CLSignature, *revocation.Witness, error) { credType := attrs.CredentialType() if credType == nil { @@ -560,6 +573,15 @@ func (s *storage) LoadPreferences() (Preferences, error) { return config, err } +func (s *storage) LoadKeyshareCachedP(pkid irma.PublicKeyIdentifier) (*big.Int, error) { + var cachedP *big.Int + found, err := s.load(keyshareCachedPsBucket, pkid.String(), &cachedP) + if err == nil && !found { + return nil, errors.New("no cached p found") + } + return cachedP, err +} + func (s *storage) TxDeleteUserdata(tx *transaction) error { return tx.DeleteBucket([]byte(userdataBucket)) } @@ -582,6 +604,15 @@ func (s *storage) TxDeleteLogs(tx *transaction) error { return tx.DeleteBucket([]byte(logsBucket)) } +func (s *storage) TxDeleteKeyshareCachedPs(tx *transaction) error { + err := tx.DeleteBucket([]byte(keyshareCachedPsBucket)) + // This bucket might not exist yet, so ignore the error if it does not exist. + if err != nil && err != bbolt.ErrBucketNotFound { + return err + } + return nil +} + func (s *storage) TxDeleteAll(tx *transaction) error { if err := s.TxDeleteAllAttributes(tx); err != nil && err != bbolt.ErrBucketNotFound { return err @@ -595,6 +626,9 @@ func (s *storage) TxDeleteAll(tx *transaction) error { if err := s.TxDeleteLogs(tx); err != nil && err != bbolt.ErrBucketNotFound { return err } + if err := s.TxDeleteKeyshareCachedPs(tx); err != nil && err != bbolt.ErrBucketNotFound { + return err + } return nil } diff --git a/requests.go b/requests.go index 4a244374..b230213a 100644 --- a/requests.go +++ b/requests.go @@ -29,6 +29,8 @@ const ( DefaultJwtValidity = 120 ) +var ErrMultipleDistributedSchemes = errors.New("multiple distributed schemes (having a keyshare server) within one disclosure request are not supported") + // BaseRequest contains information used by all IRMA session types, such the context and nonce, // and revocation information. type BaseRequest struct { @@ -475,6 +477,8 @@ func (dc AttributeDisCon) Satisfy(proofs gabi.ProofList, indices []*DisclosedAtt } func (cdc AttributeConDisCon) Validate(conf *Configuration) error { + distributedSchemes := map[SchemeManagerIdentifier]struct{}{} + for _, discon := range cdc { for _, con := range discon { var nonsingleton *CredentialTypeIdentifier @@ -487,9 +491,19 @@ func (cdc AttributeConDisCon) Validate(conf *Configuration) error { nonsingleton = &typ } } + + managerID := typ.SchemeManagerIdentifier() + if conf.SchemeManagers[managerID].Distributed() { + distributedSchemes[managerID] = struct{}{} + } } } } + + if len(distributedSchemes) > 1 { + return ErrMultipleDistributedSchemes + } + return nil } diff --git a/server/irmaserver/helpers.go b/server/irmaserver/helpers.go index edf11ce8..367dedef 100644 --- a/server/irmaserver/helpers.go +++ b/server/irmaserver/helpers.go @@ -333,6 +333,18 @@ func (session *session) getProofP(commitments *irma.IssueCommitmentMessage, sche if !token.Valid { return nil, errors.Errorf("invalid keyshare proof included for scheme %s", scheme.Name()) } + + // Validate whether proofP has the expected values given the chosen protocol version. + if session.Version.Below(2, 9) { + if claims.ProofP.P == nil { + return nil, errors.Errorf("p value missing in keyshare server proofP for scheme %s", scheme.Name()) + } + } else { + if claims.ProofP.P != nil { + return nil, errors.Errorf("p value unexpectedly set in keyshare server proofP for scheme %s", scheme.Name()) + } + } + session.KssProofs[scheme] = claims.ProofP } diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index 4462e435..da30b87c 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -125,7 +125,7 @@ var ( minProtocolVersion = irma.NewVersion(2, 4) minSecureProtocolVersion = irma.NewVersion(2, 8) - maxProtocolVersion = irma.NewVersion(2, 8) + maxProtocolVersion = irma.NewVersion(2, 9) minFrontendProtocolVersion = irma.NewVersion(1, 0) maxFrontendProtocolVersion = irma.NewVersion(1, 1) diff --git a/server/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go index bc191732..a9e2fe61 100644 --- a/server/keyshare/keyshareserver/server.go +++ b/server/keyshare/keyshareserver/server.go @@ -497,7 +497,7 @@ func (s *Server) generateResponseV2(ctx context.Context, user *User, authorizati return proofResponse, nil } -// /prove/getLinkableResponse +// /api/v2/prove/getLinkableResponse func (s *Server) handleResponseV2Linkable(w http.ResponseWriter, r *http.Request) { s.keyshareResponse(r.Context(), w, r, true) }