From 8c3ba8a7b2ebecb8ca31eec76fda4656a6deb7dc Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Thu, 1 Sep 2022 16:57:41 +0200 Subject: [PATCH 01/30] Add first draft of keyshare protocol sequence diagram. Incomplete and still work in progress. --- docs/keyshareFlow.puml | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/keyshareFlow.puml diff --git a/docs/keyshareFlow.puml b/docs/keyshareFlow.puml new file mode 100644 index 000000000..25f531a71 --- /dev/null +++ b/docs/keyshareFlow.puml @@ -0,0 +1,79 @@ +@startuml +skinparam backgroundColor #transparent +participant "IRMA app" as app +participant "requestor" as requestor +participant "keyshare server" as keyshare +participant "database" as db +participant "mail server" as mail + +title Keyshare server endpoints + +app -> keyshare ++: POST /client/register signed jwt:{pin, email, language, publickey} +keyshare -> keyshare: validate jwt +keyshare -> db: Generate keyshare server account, \nincl secret and store publickey +keyshare -> mail: Send registration mail \nif address is specified +keyshare -> keyshare: Issue keyshare credential +return Invisible issuing of keyshare credential \n(directly via session endpoints in keyshare server) +||| + +'already enrolled app: similar to the one sent by the irmaclient when changing your IRMA PIN code +'note: hier komt een nieuwe jwt terug +app -> keyshare ++: POST /users/register_publickey signed jwt:{id, pin, publickey} +keyshare -> keyshare: validate jwt +keyshare -> keyshare: validate PIN +keyshare -> db: store publickey +return jwt +||| + +'get challenge +app -> keyshare ++: POST /users/verify_start {id} +keyshare -> keyshare: generate challenge +return "pin_challengeresponse", challenge +||| + +app -> keyshare ++: POST /users/verify/pin_challengeresponse {id, pin, reponse} +keyshare -> db: fetch user +keyshare -> keyshare: verify pin \n(incl reservePinCheck and blocking) +keyshare -> keyshare: verify response which correponds to challenge +return access token +||| + +'reply attacks not possible, so no challenge-response needed +app -> keyshare ++: POST /users/change/pin signed jwt:{id, oldpin, newpin} +keyshare -> db: fetch user +keyshare -> keyshare: reserve pin check +keyshare -> keyshare: validate jwt signature +keyshare -> db: check old pin and update with new pin +return OK +||| + +app -> requestor: start session / get nonce +return nonce +||| + +' initial P_t from kss, new endpoint, do once before issuance +app -> keyshare ++: POST /prove/getP \n["irma-demo.IRMATube-1"] + access token +return P_t +||| + +app -> app: hw=hash(P,W_uu) +||| + +app -> keyshare ++: POST /prove/getCommitments \n["irma-demo.IRMATube-1"] + h_w + access token +keyshare -> keyshare: verify token +keyshare -> keyshare: generate commitments +keyshare -> keyshare: store commitID for later requests +return commitments W_t +||| + +app -> app: generate c = hash(nonce,P,W), s_u +app -> keyshare ++: POST /prove/getResponse + \nnonce + s_u + W_u + P + access token +keyshare -> keyshare: verify h_w +keyshare -> keyshare: re-calculate c +keyshare -> keyshare: log for MyIRMA +keyshare -> keyshare: get commitment data from \nmemory store and build \nresponse with challenge +return +signed jwt over challenge which is in ProofP + +app -> requestor: challenge/response, signed jwt, P=A*P_t || P=U*P_t + +@enduml From a52ed18f279b6a72cbd6676b8d668c2733a1b9c8 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Mon, 14 Nov 2022 16:52:11 +0100 Subject: [PATCH 02/30] Mark new changes in keyshare protocol green in sequence diagram. --- docs/keyshareFlow.puml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/keyshareFlow.puml b/docs/keyshareFlow.puml index 25f531a71..7c3720b0f 100644 --- a/docs/keyshareFlow.puml +++ b/docs/keyshareFlow.puml @@ -18,7 +18,7 @@ return Invisible issuing of keyshare credential \n(directly via session endpoint 'already enrolled app: similar to the one sent by the irmaclient when changing your IRMA PIN code 'note: hier komt een nieuwe jwt terug -app -> keyshare ++: POST /users/register_publickey signed jwt:{id, pin, publickey} +app -> keyshare ++: POST /users/register_publickey signed jwt:{id, pin, publickey} keyshare -> keyshare: validate jwt keyshare -> keyshare: validate PIN keyshare -> db: store publickey @@ -52,28 +52,28 @@ return nonce ||| ' initial P_t from kss, new endpoint, do once before issuance -app -> keyshare ++: POST /prove/getP \n["irma-demo.IRMATube-1"] + access token -return P_t +app -> keyshare ++: POST /prove/getP \n["irma-demo.IRMATube-1"] + access token +return P_t ||| -app -> app: hw=hash(P,W_uu) +app -> app: hw=hash(P,W_uu) ||| -app -> keyshare ++: POST /prove/getCommitments \n["irma-demo.IRMATube-1"] + h_w + access token +app -> keyshare ++: POST /prove/getCommitments \n["irma-demo.IRMATube-1"] + h_w + access token keyshare -> keyshare: verify token keyshare -> keyshare: generate commitments -keyshare -> keyshare: store commitID for later requests +keyshare -> keyshare: store commitID for later requests return commitments W_t ||| app -> app: generate c = hash(nonce,P,W), s_u -app -> keyshare ++: POST /prove/getResponse + \nnonce + s_u + W_u + P + access token -keyshare -> keyshare: verify h_w -keyshare -> keyshare: re-calculate c +app -> keyshare ++: POST /prove/getResponse + \nnonce + s_u + W_u + P + access token +keyshare -> keyshare: verify h_w +keyshare -> keyshare: re-calculate c keyshare -> keyshare: log for MyIRMA keyshare -> keyshare: get commitment data from \nmemory store and build \nresponse with challenge return +signed jwt over challenge which is in ProofP -app -> requestor: challenge/response, signed jwt, P=A*P_t || P=U*P_t +app -> requestor: challenge/response, signed jwt, P=A*P_t || P=U*P_t @enduml From 52a52f3ddf66b77ccedd905579b3e2c578fa93ae Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Tue, 15 Nov 2022 12:05:55 +0100 Subject: [PATCH 03/30] Adjust /getPs endpoint name in sequence diagram to match implementation. --- docs/keyshareFlow.puml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/keyshareFlow.puml b/docs/keyshareFlow.puml index 7c3720b0f..ff7ee81d7 100644 --- a/docs/keyshareFlow.puml +++ b/docs/keyshareFlow.puml @@ -52,7 +52,7 @@ return nonce ||| ' initial P_t from kss, new endpoint, do once before issuance -app -> keyshare ++: POST /prove/getP \n["irma-demo.IRMATube-1"] + access token +app -> keyshare ++: POST /prove/getPs \n["irma-demo.IRMATube-1"] + access token return P_t ||| From 9260d48e1aa602605584a00a780ac244b66c6cc6 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Tue, 15 Nov 2022 12:09:52 +0100 Subject: [PATCH 04/30] Introduce /getPs endpoint for initially fetching keyshare server P (R_0^secret) prior to issuance. This is needed for the client to calculate the hash h_w. --- internal/keysharecore/operations.go | 28 ++++++++++ messages.go | 22 ++++++++ server/keyshare/keyshareserver/server.go | 51 +++++++++++++++++++ server/keyshare/keyshareserver/server_test.go | 34 +++++++++++++ 4 files changed, 135 insertions(+) diff --git a/internal/keysharecore/operations.go b/internal/keysharecore/operations.go index dd313bbae..3c284e140 100644 --- a/internal/keysharecore/operations.go +++ b/internal/keysharecore/operations.go @@ -197,6 +197,34 @@ func (c *Core) verifyAccess(secrets UserSecrets, jwtToken string) (unencryptedUs return s, nil } +// GeneratePs generates a list of keyshare server P's, i.e. a list of R_0^keyshareSecret. +func (c *Core) GeneratePs(secrets UserSecrets, accessToken string, keyIDs []irma.PublicKeyIdentifier) ([]*big.Int, error) { + // Validate input request and build key list + var keyList []*gabikeys.PublicKey + for _, keyID := range keyIDs { + key, ok := c.trustedKeys[keyID] + if !ok { + return nil, ErrKeyNotFound + } + keyList = append(keyList, key) + } + + // verify access and decrypt + s, err := c.verifyAccess(secrets, accessToken) + if err != nil { + return nil, err + } + + var ps []*big.Int + + for _, key := range keyList { + ps = append(ps, + new(big.Int).Exp(key.R[0], s.KeyshareSecret, key.N)) + } + + return ps, nil +} + // GenerateCommitments generates keyshare commitments using the specified Idemix public key(s). func (c *Core) GenerateCommitments(secrets UserSecrets, accessToken string, keyIDs []irma.PublicKeyIdentifier) ([]*gabi.ProofPCommitment, uint64, error) { // Validate input request and build key list diff --git a/messages.go b/messages.go index f2251c6fe..56acf8c4a 100644 --- a/messages.go +++ b/messages.go @@ -3,6 +3,7 @@ package irma import ( "bytes" "encoding/json" + "github.com/privacybydesign/gabi/big" "net/url" "regexp" "strconv" @@ -391,6 +392,27 @@ func (ppcm *ProofPCommitmentMap) MarshalJSON() ([]byte, error) { return json.Marshal(encPPCM) } +type PMap struct { + Ps map[PublicKeyIdentifier]*big.Int `json:"p"` +} + +func (pm *PMap) MarshalJSON() ([]byte, error) { + var encPM struct { + Ps map[string]*big.Int `json:"p"` + } + encPM.Ps = make(map[string]*big.Int) + + for pki, v := range pm.Ps { + pkiBytes, err := pki.MarshalText() + if err != nil { + return nil, err + } + encPM.Ps[string(pkiBytes)] = v + } + + return json.Marshal(encPM) +} + // // Errors // diff --git a/server/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go index f327a5f24..7c331284c 100644 --- a/server/keyshare/keyshareserver/server.go +++ b/server/keyshare/keyshareserver/server.go @@ -132,6 +132,7 @@ func (s *Server) Handler() http.Handler { router.Group(func(router chi.Router) { router.Use(s.userMiddleware) router.Use(s.authorizationMiddleware) + router.Post("/prove/getPs", s.handlePs) router.Post("/prove/getCommitments", s.handleCommitments) router.Post("/prove/getResponse", s.handleResponse) }) @@ -163,6 +164,56 @@ func (s *Server) loadIdemixKeys(conf *irma.Configuration) error { return errs.ErrorOrNil() } +// /prove/getPs +func (s *Server) handlePs(w http.ResponseWriter, r *http.Request) { + // Fetch from context + user := r.Context().Value("user").(*User) + authorization := r.Context().Value("authorization").(string) + + // Read keys + var keys []irma.PublicKeyIdentifier + if err := server.ParseBody(r, &keys); err != nil { + server.WriteError(w, server.ErrorInvalidRequest, err.Error()) + return + } + if len(keys) == 0 { + s.conf.Logger.Info("Malformed request: no keys for P specified") + server.WriteError(w, server.ErrorInvalidRequest, "No key specified") + return + } + + ps, err := s.generatePs(user, authorization, keys) + if err != nil && err == keysharecore.ErrInvalidJWT { + server.WriteError(w, server.ErrorInvalidRequest, err.Error()) + return + } + if err != nil { + // already logged + server.WriteError(w, server.ErrorInternal, err.Error()) + return + } + + server.WriteJson(w, ps) +} + +func (s *Server) generatePs(user *User, authorization string, keys []irma.PublicKeyIdentifier) (*irma.PMap, error) { + // Generate Ps + ps, err := s.core.GeneratePs(user.Secrets, authorization, keys) + if err != nil { + s.conf.Logger.WithField("error", err).Warn("Could not generate Ps for request") + return nil, err + } + + // Prepare output message format + mappedPs := map[irma.PublicKeyIdentifier]*big.Int{} + for i, keyID := range keys { + mappedPs[keyID] = ps[i] + } + + // And send response + return &irma.PMap{Ps: mappedPs}, nil +} + // /prove/getCommitments func (s *Server) handleCommitments(w http.ResponseWriter, r *http.Request) { // Fetch from context diff --git a/server/keyshare/keyshareserver/server_test.go b/server/keyshare/keyshareserver/server_test.go index 894702037..02053a776 100644 --- a/server/keyshare/keyshareserver/server_test.go +++ b/server/keyshare/keyshareserver/server_test.go @@ -64,6 +64,14 @@ func TestServerInvalidMessage(t *testing.T) { "[]", nil, 403, nil, ) + test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, + 403, nil, + ) + test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + "[]", nil, + 403, nil, + ) test.HTTPPost(t, nil, "http://localhost:8080/prove/getResponse", "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, 403, nil, @@ -376,6 +384,14 @@ func TestMissingUser(t *testing.T) { 403, nil, ) + test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + `["test.test-3"]`, http.Header{ + "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, + "Authorization": []string{"ey.ey.ey"}, + }, + 403, nil, + ) + test.HTTPPost(t, nil, "http://localhost:8080/prove/getResponse", "123456789", http.Header{ "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, @@ -438,6 +454,24 @@ func TestKeyshareSessions(t *testing.T) { 200, nil, ) + // can't retrieve Ps with fake authorization + test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + `["test.test-3"]`, http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{"fakeauthorization"}, + }, + 400, nil, + ) + + // retrieve Ps normally + test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + `["test.test-3"]`, http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 200, nil, + ) + // can't retrieve resukt with fake authorization test.HTTPPost(t, nil, "http://localhost:8080/prove/getResponse", "12345678", http.Header{ From 9fd5e0f2067236aa777844e383cfd87eaa208c46 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Tue, 15 Nov 2022 12:12:42 +0100 Subject: [PATCH 05/30] Fix linter issue (typo) in sequence diagram. --- docs/keyshareFlow.puml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/keyshareFlow.puml b/docs/keyshareFlow.puml index ff7ee81d7..54977b018 100644 --- a/docs/keyshareFlow.puml +++ b/docs/keyshareFlow.puml @@ -31,7 +31,7 @@ keyshare -> keyshare: generate challenge return "pin_challengeresponse", challenge ||| -app -> keyshare ++: POST /users/verify/pin_challengeresponse {id, pin, reponse} +app -> keyshare ++: POST /users/verify/pin_challengeresponse {id, pin, response} keyshare -> db: fetch user keyshare -> keyshare: verify pin \n(incl reservePinCheck and blocking) keyshare -> keyshare: verify response which correponds to challenge From d00d3fec4d9447458d5ac36d1d4c67ba4a8aeef9 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Wed, 16 Nov 2022 11:54:59 +0100 Subject: [PATCH 06/30] Add GeneratePs functionality test. --- internal/keysharecore/operations_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/keysharecore/operations_test.go b/internal/keysharecore/operations_test.go index d1c033853..825dceff5 100644 --- a/internal/keysharecore/operations_test.go +++ b/internal/keysharecore/operations_test.go @@ -182,6 +182,11 @@ func TestProofFunctionality(t *testing.T) { jwtt, err := validateAuth(t, c, signer, secrets, pin) require.NoError(t, err) + // For issuance, initially get P_t + // TODO: The result ps will be used in the generate commitment step and checked when the response is made. + _, err = c.GeneratePs(secrets, jwtt, []irma.PublicKeyIdentifier{irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier("test"), Counter: 1}}) + require.NoError(t, err) + // Get keyshare commitment W, commitID, err := c.GenerateCommitments(secrets, jwtt, []irma.PublicKeyIdentifier{irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier("test"), Counter: 1}}) require.NoError(t, err) @@ -246,6 +251,10 @@ func TestCorruptedUserSecrets(t *testing.T) { _, err = changePin(t, c, signer, secrets, pin, pin) assert.Error(t, err, "ChangePin accepts corrupted keyshare user secrets") + // GeneratePs + _, err = c.GeneratePs(secrets, jwtt, []irma.PublicKeyIdentifier{irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier("test"), Counter: 1}}) + assert.Error(t, err, "GeneratePs accepts corrupted keyshare user secrets") + // GenerateCommitments _, _, err = c.GenerateCommitments(secrets, jwtt, []irma.PublicKeyIdentifier{irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier("test"), Counter: 1}}) assert.Error(t, err, "GenerateCommitments accepts corrupted keyshare user secrets") @@ -313,6 +322,10 @@ func TestMissingKey(t *testing.T) { jwtt, err := validateAuth(t, c, signer, secrets, pin) require.NoError(t, err) + // GeneratePs + _, err = c.GeneratePs(secrets, jwtt, []irma.PublicKeyIdentifier{irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier("DNE"), Counter: 1}}) + assert.Error(t, err, "Missing key not detected by generatePs") + // GenerateCommitments _, _, err = c.GenerateCommitments(secrets, jwtt, []irma.PublicKeyIdentifier{irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier("DNE"), Counter: 1}}) assert.Error(t, err, "Missing key not detected by generateCommitments") From 72ebce67797ca77813b99c2e2808590301d7ab2c Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Mon, 28 Nov 2022 11:50:34 +0100 Subject: [PATCH 07/30] Add comment to verifyAccess calls. --- internal/keysharecore/operations.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/keysharecore/operations.go b/internal/keysharecore/operations.go index 3c284e140..e68019643 100644 --- a/internal/keysharecore/operations.go +++ b/internal/keysharecore/operations.go @@ -209,7 +209,8 @@ func (c *Core) GeneratePs(secrets UserSecrets, accessToken string, keyIDs []irma keyList = append(keyList, key) } - // verify access and decrypt + // Use verifyAccess to get the decrypted secrets. The access has already been verified in the + // middleware. We use the call merely to fetch the unencryptedUserSecrets here. s, err := c.verifyAccess(secrets, accessToken) if err != nil { return nil, err @@ -237,7 +238,8 @@ func (c *Core) GenerateCommitments(secrets UserSecrets, accessToken string, keyI keyList = append(keyList, key) } - // verify access and decrypt + // Use verifyAccess to get the decrypted secrets. The access has already been verified in the + // middleware. We use the call merely to fetch the unencryptedUserSecrets here. s, err := c.verifyAccess(secrets, accessToken) if err != nil { return nil, 0, err @@ -275,7 +277,8 @@ func (c *Core) GenerateResponse(secrets UserSecrets, accessToken string, commitI return "", ErrKeyNotFound } - // verify access and decrypt + // Use verifyAccess to get the decrypted secrets. The access has already been verified in the + // middleware. We use the call merely to fetch the unencryptedUserSecrets here. s, err := c.verifyAccess(secrets, accessToken) if err != nil { return "", err From cf3f06c40016fdcaae1ae0096ac04d97497df9d4 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Fri, 2 Dec 2022 09:35:28 +0100 Subject: [PATCH 08/30] Add /api/v2 route and move getP there. --- server/keyshare/keyshareserver/server.go | 8 +++++++- server/keyshare/keyshareserver/server_test.go | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/server/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go index cf8544585..9b652bc7e 100644 --- a/server/keyshare/keyshareserver/server.go +++ b/server/keyshare/keyshareserver/server.go @@ -124,6 +124,13 @@ func (s *Server) Handler() http.Handler { }) }) + router.Route("/api/v2", func(r chi.Router) { + // Keyshare sessions with provably secure keyshare protocol + r.Use(s.userMiddleware) + r.Use(s.authorizationMiddleware) + r.Post("/prove/getPs", s.handlePs) + }) + // IRMA server for issuing myirma credential during registration router.Mount("/irma/", s.irmaserv.HandlerFunc()) return router @@ -150,7 +157,6 @@ func (s *Server) routeHandler(r chi.Router) http.Handler { r.Group(func(router chi.Router) { router.Use(s.userMiddleware) router.Use(s.authorizationMiddleware) - router.Post("/prove/getPs", s.handlePs) router.Post("/prove/getCommitments", s.handleCommitments) router.Post("/prove/getResponse", s.handleResponse) }) diff --git a/server/keyshare/keyshareserver/server_test.go b/server/keyshare/keyshareserver/server_test.go index e8916a1a0..088e5255e 100644 --- a/server/keyshare/keyshareserver/server_test.go +++ b/server/keyshare/keyshareserver/server_test.go @@ -65,11 +65,11 @@ func TestServerInvalidMessage(t *testing.T) { 403, nil, ) // TODO: move. - test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, 403, nil, ) - test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", "[]", nil, 403, nil, ) @@ -386,7 +386,7 @@ func TestMissingUser(t *testing.T) { ) // TODO: move - test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", `["test.test-3"]`, http.Header{ "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, "Authorization": []string{"ey.ey.ey"}, @@ -458,7 +458,7 @@ func TestKeyshareSessions(t *testing.T) { // TODO: move // can't retrieve Ps with fake authorization - test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + test.HTTPPost(t, nil, "http://localhost:8080/api/v2prove/getPs", `["test.test-3"]`, http.Header{ "X-IRMA-Keyshare-Username": []string{user.username}, "Authorization": []string{"fakeauthorization"}, @@ -467,7 +467,7 @@ func TestKeyshareSessions(t *testing.T) { ) // retrieve Ps normally - test.HTTPPost(t, nil, "http://localhost:8080/prove/getPs", + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", `["test.test-3"]`, http.Header{ "X-IRMA-Keyshare-Username": []string{user.username}, "Authorization": []string{user.auth}, From 8f365cd7cb16ea00768f8c152079823b88fac0db Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Fri, 2 Dec 2022 09:38:11 +0100 Subject: [PATCH 09/30] Upgrade to latest gabi version which contains the crypto for the provably secure keyshare protocol. The upgrade includes breaking changes in both the keyshare server and the app client. Since this pull request will only update the keyshare server, the client code is not upgraded properly here but merely so that it compiles again. --- go.mod | 2 +- go.sum | 2 ++ internal/keysharecore/operations.go | 2 +- irmaclient/client.go | 2 +- irmaclient/keyshare.go | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index e4c2f506f..5ee1991d7 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/jinzhu/gorm v1.9.16 github.com/mdp/qrterminal v1.0.1 github.com/mitchellh/mapstructure v1.5.0 - github.com/privacybydesign/gabi v0.0.0-20221012093643-8e978bfbb252 + github.com/privacybydesign/gabi v0.0.0-20221109103539-8e9ce1f4eb2c github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cast v1.5.0 diff --git a/go.sum b/go.sum index 3d8743cef..0b4107709 100644 --- a/go.sum +++ b/go.sum @@ -286,6 +286,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/privacybydesign/gabi v0.0.0-20221012093643-8e978bfbb252 h1:q5BP7Eh/x1GCyIToZTBYQXELmO876ezbQvek7gJaBg4= github.com/privacybydesign/gabi v0.0.0-20221012093643-8e978bfbb252/go.mod h1:HQ6L5rKBY7qaqcheK6zpaVf7fhGWD0PvUAXJTDws+0M= +github.com/privacybydesign/gabi v0.0.0-20221109103539-8e9ce1f4eb2c h1:Ffb+ORxS/DiG7crVSV0Zf0WXCrk6VHDbBFU92lRe2pI= +github.com/privacybydesign/gabi v0.0.0-20221109103539-8e9ce1f4eb2c/go.mod h1:QZI8hX8Ff2GfZ7UJuxyWw3nAGgt2s5+U4hxY6rmwQvs= 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= diff --git a/internal/keysharecore/operations.go b/internal/keysharecore/operations.go index e68019643..06391ad49 100644 --- a/internal/keysharecore/operations.go +++ b/internal/keysharecore/operations.go @@ -295,7 +295,7 @@ func (c *Core) GenerateResponse(secrets UserSecrets, accessToken string, commitI // Generate response token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ - "ProofP": gabi.KeyshareResponse(s.KeyshareSecret, commit, challenge, key), + "ProofP": gabi.KeyshareResponseLegacy(s.KeyshareSecret, commit, challenge, key), "iat": time.Now().Unix(), "sub": "ProofP", "iss": c.jwtIssuer, diff --git a/irmaclient/client.go b/irmaclient/client.go index d60fe2429..695470782 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -969,7 +969,7 @@ func (client *Client) IssuanceProofBuilders(request *irma.IssuanceRequest, choic } credtype := client.Configuration.CredentialTypes[futurecred.CredentialTypeID] credBuilder, err := gabi.NewCredentialBuilder(pk, request.GetContext(), - client.secretkey.Key, issuerProofNonce, credtype.RandomBlindAttributeIndices()) + client.secretkey.Key, issuerProofNonce, big.NewInt(2346234), credtype.RandomBlindAttributeIndices()) if err != nil { return nil, nil, nil, err } diff --git a/irmaclient/keyshare.go b/irmaclient/keyshare.go index 8ea830512..0404e8c07 100644 --- a/irmaclient/keyshare.go +++ b/irmaclient/keyshare.go @@ -408,7 +408,7 @@ func (ks *keyshareSession) GetCommitments() { if !distributed { continue } - builder.MergeProofPCommitment(comm) + builder.SetProofPCommitment(comm) } ks.GetProofPs() From b22ef68235e4fe3f064d20ab8cd58c88c72e7762 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Fri, 2 Dec 2022 10:13:31 +0100 Subject: [PATCH 10/30] Cleanup go.sum. --- go.sum | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/go.sum b/go.sum index 0b4107709..fc2a0b542 100644 --- a/go.sum +++ b/go.sum @@ -103,7 +103,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fxamacker/cbor v1.5.0/go.mod h1:UjdWSysJckWsChYy9I5zMbkGvK4xXDR+LmDb8kPGYgA= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= @@ -112,7 +111,6 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.14.0 h1:Fphr0ZWKtr6V33jjUdhF7ggwI0idt/RxBxwybIwr6Zo= github.com/go-co-op/gocron v1.14.0/go.mod h1:HfX00TY7w5syy3awgLeo0Po9MAkYDbz7hQGvwWcQSqg= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -225,7 +223,6 @@ github.com/jinzhu/now v1.0.1/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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 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.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -284,8 +281,6 @@ 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-20221012093643-8e978bfbb252 h1:q5BP7Eh/x1GCyIToZTBYQXELmO876ezbQvek7gJaBg4= -github.com/privacybydesign/gabi v0.0.0-20221012093643-8e978bfbb252/go.mod h1:HQ6L5rKBY7qaqcheK6zpaVf7fhGWD0PvUAXJTDws+0M= github.com/privacybydesign/gabi v0.0.0-20221109103539-8e9ce1f4eb2c h1:Ffb+ORxS/DiG7crVSV0Zf0WXCrk6VHDbBFU92lRe2pI= github.com/privacybydesign/gabi v0.0.0-20221109103539-8e9ce1f4eb2c/go.mod h1:QZI8hX8Ff2GfZ7UJuxyWw3nAGgt2s5+U4hxY6rmwQvs= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -298,7 +293,6 @@ github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXY github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50 h1:vgWWQM2SnMoO9BiUZ2WFAYuYF6U0jNss9Vn/PZoi+tU= github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50/go.mod h1:W/QHK9G0i5yrmHvej5+hhoFMXTSZIWHGQRcpbGgqV9s= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -317,12 +311,10 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= @@ -342,7 +334,6 @@ github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a/go.mod h1:iSvu 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= -github.com/x448/float16 v0.8.3/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -366,7 +357,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -384,6 +374,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/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= @@ -471,7 +462,6 @@ golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 5f4c4831640640db099ad32dc601aa7afcf694e3 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Fri, 2 Dec 2022 10:42:24 +0100 Subject: [PATCH 11/30] Introduce /api/v2/getCommitments endpoint. --- messages.go | 26 ++++ server/keyshare/keyshareserver/server.go | 66 +++++++++ .../keyshare/keyshareserver/server_v2_test.go | 126 ++++++++++++++++++ server/keyshare/keyshareserver/session.go | 2 + 4 files changed, 220 insertions(+) create mode 100644 server/keyshare/keyshareserver/server_v2_test.go diff --git a/messages.go b/messages.go index 56acf8c4a..810b00100 100644 --- a/messages.go +++ b/messages.go @@ -413,6 +413,32 @@ func (pm *PMap) MarshalJSON() ([]byte, error) { return json.Marshal(encPM) } +type GetCommitmentsRequest struct { + Keys []PublicKeyIdentifier `json:"keys"` + Hash gabi.KeyshareCommitmentRequest `json:"hw"` +} + +type ProofPCommitmentMapV2 struct { + Commitments map[PublicKeyIdentifier]*big.Int `json:"c"` +} + +func (cm *ProofPCommitmentMapV2) MarshalJSON() ([]byte, error) { + var encCM struct { + Commitments map[string]*big.Int `json:"p"` + } + encCM.Commitments = make(map[string]*big.Int) + + for pki, c := range cm.Commitments { + pkiBytes, err := pki.MarshalText() + if err != nil { + return nil, err + } + encCM.Commitments[string(pkiBytes)] = c + } + + return json.Marshal(encCM) +} + // // Errors // diff --git a/server/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go index 9b652bc7e..a88849906 100644 --- a/server/keyshare/keyshareserver/server.go +++ b/server/keyshare/keyshareserver/server.go @@ -129,6 +129,7 @@ func (s *Server) Handler() http.Handler { r.Use(s.userMiddleware) r.Use(s.authorizationMiddleware) r.Post("/prove/getPs", s.handlePs) + r.Post("/prove/getCommitments", s.handleCommitmentsV2) }) // IRMA server for issuing myirma credential during registration @@ -297,6 +298,71 @@ func (s *Server) generateCommitments(user *User, authorization string, keys []ir return &irma.ProofPCommitmentMap{Commitments: mappedCommitments}, nil } +// /api/v2/prove/getCommitments +func (s *Server) handleCommitmentsV2(w http.ResponseWriter, r *http.Request) { + // Fetch from context + user := r.Context().Value("user").(*User) + authorization := r.Context().Value("authorization").(string) + + // Read keys + var req irma.GetCommitmentsRequest + if err := server.ParseBody(r, &req); err != nil { + server.WriteError(w, server.ErrorInvalidRequest, err.Error()) + return + } + if len(req.Keys) == 0 { + s.conf.Logger.Info("Malformed request: no keys for commitment specified") + server.WriteError(w, server.ErrorInvalidRequest, "No key specified") + return + } + + commitments, err := s.generateCommitmentsV2(user, authorization, req) + // TODO: can ErrInvalidChallenge be removed? + if err != nil && (err == keysharecore.ErrInvalidChallenge || err == keysharecore.ErrInvalidJWT) { + server.WriteError(w, server.ErrorInvalidRequest, err.Error()) + return + } + if err != nil { + // already logged + server.WriteError(w, server.ErrorInternal, err.Error()) + return + } + + server.WriteJson(w, commitments) +} + +func (s *Server) generateCommitmentsV2(user *User, authorization string, req irma.GetCommitmentsRequest) (*irma.ProofPCommitmentMapV2, error) { + // Generate commitments + commitments, commitID, err := s.core.GenerateCommitments(user.Secrets, authorization, req.Keys) + if err != nil { + s.conf.Logger.WithField("error", err).Warn("Could not generate commitments for request") + return nil, err + } + + // Prepare output message format + // TODO: move logic to gabi? + mappedCommitments := map[irma.PublicKeyIdentifier]*big.Int{} + for i, keyID := range req.Keys { + mappedCommitments[keyID] = commitments[i].Pcommit + } + + // Store needed data for later requests. + // Of all keys involved in the current session, store the ID of the last one to be used when + // the user comes back later to retrieve her response. gabi.ProofP.P will depend on this public + // key, which is used only during issuance. Thus, this assumes that during issuance, the user + // puts the key ID of the credential(s) being issued at the last index (indeed, the irmaclient + // always puts all ProofU's after the ProofD's in the list of proofs it sends to the IRMA + // server). + s.store.add(user.Username, &session{ + KeyID: req.Keys[len(req.Keys)-1], + Hw: req.Hash, + CommitID: commitID, + }) + + // And send response + return &irma.ProofPCommitmentMapV2{Commitments: mappedCommitments}, nil +} + // /prove/getResponse func (s *Server) handleResponse(w http.ResponseWriter, r *http.Request) { // Fetch from context diff --git a/server/keyshare/keyshareserver/server_v2_test.go b/server/keyshare/keyshareserver/server_v2_test.go new file mode 100644 index 000000000..5bb7fcc96 --- /dev/null +++ b/server/keyshare/keyshareserver/server_v2_test.go @@ -0,0 +1,126 @@ +package keyshareserver + +import ( + irma "github.com/privacybydesign/irmago" + "github.com/privacybydesign/irmago/internal/test" + "github.com/stretchr/testify/require" + "net/http" + "testing" +) + +func TestServerInvalidMessageV2(t *testing.T) { + keyshareServer, httpServer := StartKeyshareServer(t, NewMemoryDB(), "") + defer StopKeyshareServer(t, keyshareServer, httpServer) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", + "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, + 403, nil, + ) + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", + "[]", nil, + 403, nil, + ) + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", + "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, + 403, nil, + ) + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", + "[]", nil, + 403, nil, + ) +} + +func TestMissingUserV2(t *testing.T) { + keyshareServer, httpServer := StartKeyshareServer(t, NewMemoryDB(), "") + defer StopKeyshareServer(t, keyshareServer, httpServer) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", + `["test.test-3"]`, http.Header{ + "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, + "Authorization": []string{"ey.ey.ey"}, + }, + 403, nil, + ) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", + `["test.test-3"]`, http.Header{ + "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, + "Authorization": []string{"ey.ey.ey"}, + }, + 403, nil, + ) +} + +func TestKeyshareSessionsV2(t *testing.T) { + db := createDB(t) + keyshareServer, httpServer := StartKeyshareServer(t, db, "") + defer StopKeyshareServer(t, keyshareServer, httpServer) + + jwtt := doChallengeResponse(t, loadClientPrivateKey(t), "testusername", "puZGbaLDmFywGhFDi4vW2G87ZhXpaUsvymZwNJfB/SU=\n") + var jwtMsg irma.KeysharePinStatus + test.HTTPPost(t, nil, "http://localhost:8080/api/v1/users/verify/pin_challengeresponse", + marshalJSON(t, irma.KeyshareAuthResponse{AuthResponseJWT: jwtt}), nil, + 200, &jwtMsg, + ) + require.Equal(t, "success", jwtMsg.Status) + auth1 := jwtMsg.Message + + test.HTTPPost(t, nil, "http://localhost:8080/api/v1/users/verify/pin", + marshalJSON(t, irma.KeyshareAuthResponse{KeyshareAuthResponseData: irma.KeyshareAuthResponseData{ + Username: "legacyuser", + Pin: "puZGbaLDmFywGhFDi4vW2G87ZhXpaUsvymZwNJfB/SU=\n", + }}), nil, + 200, &jwtMsg, + ) + auth2 := jwtMsg.Message + + for _, user := range []struct { + username, auth string + }{{"testusername", auth1}, {"legacyuser", auth2}} { + // can't retrieve commitments with fake authorization + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", + `["test.test-3"]`, http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{"fakeauthorization"}, + }, + 400, nil, + ) + + // retrieve commitments normally + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", + `{"keys":["test.test-3"],"hw":{"hashedComms":"WW91ciBTdHJpbmc="}}`, http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 200, nil, + ) + + // can't retrieve Ps with fake authorization + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", + `["test.test-3"]`, http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{"fakeauthorization"}, + }, + 400, nil, + ) + + // retrieve Ps normally + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", + `["test.test-3"]`, http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 200, nil, + ) + + // can start session while another is already active + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", + `{"keys":["test.test-3"],"hw":{"hashedComms":"WW91ciBTdHJpbmc="}}`, http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 200, nil, + ) + + } +} diff --git a/server/keyshare/keyshareserver/session.go b/server/keyshare/keyshareserver/session.go index a1a4e6e4d..027514249 100644 --- a/server/keyshare/keyshareserver/session.go +++ b/server/keyshare/keyshareserver/session.go @@ -1,6 +1,7 @@ package keyshareserver import ( + "github.com/privacybydesign/gabi" "sync" "time" @@ -10,6 +11,7 @@ import ( type session struct { KeyID irma.PublicKeyIdentifier // last used key, used in signing the issuance message CommitID uint64 + Hw gabi.KeyshareCommitmentRequest expiry time.Time } From f069df40a0e8249c88b5f69fa07ee489fdea9220 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Fri, 2 Dec 2022 10:44:05 +0100 Subject: [PATCH 12/30] Remove /api/v2/getPs tests from v1 server tests. --- server/keyshare/keyshareserver/server_test.go | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/server/keyshare/keyshareserver/server_test.go b/server/keyshare/keyshareserver/server_test.go index 088e5255e..191d5273f 100644 --- a/server/keyshare/keyshareserver/server_test.go +++ b/server/keyshare/keyshareserver/server_test.go @@ -64,15 +64,6 @@ func TestServerInvalidMessage(t *testing.T) { "[]", nil, 403, nil, ) - // TODO: move. - test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", - "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, - 403, nil, - ) - test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", - "[]", nil, - 403, nil, - ) test.HTTPPost(t, nil, "http://localhost:8080/api/v1/prove/getResponse", "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, 403, nil, @@ -385,15 +376,6 @@ func TestMissingUser(t *testing.T) { 403, nil, ) - // TODO: move - test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", - `["test.test-3"]`, http.Header{ - "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, - "Authorization": []string{"ey.ey.ey"}, - }, - 403, nil, - ) - test.HTTPPost(t, nil, "http://localhost:8080/api/v1/prove/getResponse", "123456789", http.Header{ "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, @@ -456,25 +438,6 @@ func TestKeyshareSessions(t *testing.T) { 200, nil, ) - // TODO: move - // can't retrieve Ps with fake authorization - test.HTTPPost(t, nil, "http://localhost:8080/api/v2prove/getPs", - `["test.test-3"]`, http.Header{ - "X-IRMA-Keyshare-Username": []string{user.username}, - "Authorization": []string{"fakeauthorization"}, - }, - 400, nil, - ) - - // retrieve Ps normally - test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getPs", - `["test.test-3"]`, http.Header{ - "X-IRMA-Keyshare-Username": []string{user.username}, - "Authorization": []string{user.auth}, - }, - 200, nil, - ) - // can't retrieve resukt with fake authorization test.HTTPPost(t, nil, "http://localhost:8080/api/v1/prove/getResponse", "12345678", http.Header{ From d3317f91a34068b7b391ff22c106cd07383b6e95 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Fri, 9 Dec 2022 10:34:06 +0100 Subject: [PATCH 13/30] Pass nil instead of random placeholder for keyshareP to new credential builder. Accidently committed the placeholder earlier. --- irmaclient/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irmaclient/client.go b/irmaclient/client.go index 695470782..abad29202 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -969,7 +969,7 @@ func (client *Client) IssuanceProofBuilders(request *irma.IssuanceRequest, choic } credtype := client.Configuration.CredentialTypes[futurecred.CredentialTypeID] credBuilder, err := gabi.NewCredentialBuilder(pk, request.GetContext(), - client.secretkey.Key, issuerProofNonce, big.NewInt(2346234), credtype.RandomBlindAttributeIndices()) + client.secretkey.Key, issuerProofNonce, nil, credtype.RandomBlindAttributeIndices()) if err != nil { return nil, nil, nil, err } From 85aace0d9de76d6fbe6280786dc00f09a87b1c9a Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Sun, 11 Dec 2022 18:58:57 +0100 Subject: [PATCH 14/30] feat: support gabi with updated keyshare protocol This version of gabi produces a proofU that contains both keyshares. This commit changes the following: - irmaclient.Client.startKeyshareSession() is now called newKeyshareSession, because it does not actually immediately start the session. Instead, it returns after checking the user's PIN. This is necessary because what happens next now differs per session type, see next point. - In case of disclosure/ABS, the session proceeds as before. In case of issuance, /api/v2/prove/getPs at the keyshare server is first invoked to retrieve the P values (R_0^{keyshare server secret}), after which the session is started. - In case of the old keyshare protocol, the keyshare's P is divided out from the U of the proofU because that is what the issuer expects in the old keyshare protocol. --- irmaclient/client.go | 28 +++++++- irmaclient/keyshare.go | 126 ++++++++++++++++++++++------------ irmaclient/legacy.go | 14 ++++ irmaclient/session.go | 27 +++++--- requests.go | 7 ++ server/irmaserver/sessions.go | 2 +- 6 files changed, 145 insertions(+), 59 deletions(-) diff --git a/irmaclient/client.go b/irmaclient/client.go index abad29202..67d52274f 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -954,22 +954,44 @@ func generateIssuerProofNonce() (*big.Int, error) { // IssuanceProofBuilders constructs a list of proof builders in the issuance protocol // for the future credentials as well as possibly any disclosed attributes, and generates // a nonce against which the issuer's proof of knowledge must verify. -func (client *Client) IssuanceProofBuilders(request *irma.IssuanceRequest, choice *irma.DisclosureChoice, +func (client *Client) IssuanceProofBuilders( + request *irma.IssuanceRequest, choice *irma.DisclosureChoice, keyshareSession *keyshareSession, ) (gabi.ProofBuilderList, irma.DisclosedAttributeIndices, *big.Int, error) { issuerProofNonce, err := generateIssuerProofNonce() if err != nil { return nil, nil, nil, err } builders := gabi.ProofBuilderList([]gabi.ProofBuilder{}) + + var keysharePs = map[irma.SchemeManagerIdentifier]*irma.PMap{} + if keyshareSession != nil { + keysharePs, err = keyshareSession.getKeysharePs(request) + if err != nil { + return nil, nil, nil, err + } + } + 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", keyID) + } + } + 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, nil, credtype.RandomBlindAttributeIndices()) + client.secretkey.Key, issuerProofNonce, keyshareP, credtype.RandomBlindAttributeIndices()) if err != nil { return nil, nil, nil, err } @@ -988,7 +1010,7 @@ func (client *Client) IssuanceProofBuilders(request *irma.IssuanceRequest, choic // and also returns the credential builders which will become the new credentials upon combination with the issuer's signature. func (client *Client) IssueCommitments(request *irma.IssuanceRequest, choice *irma.DisclosureChoice, ) (*irma.IssueCommitmentMessage, gabi.ProofBuilderList, error) { - builders, choices, issuerProofNonce, err := client.IssuanceProofBuilders(request, choice) + builders, choices, issuerProofNonce, err := client.IssuanceProofBuilders(request, choice, nil) if err != nil { return nil, nil, err } diff --git a/irmaclient/keyshare.go b/irmaclient/keyshare.go index 0404e8c07..b32665a4f 100644 --- a/irmaclient/keyshare.go +++ b/irmaclient/keyshare.go @@ -49,6 +49,7 @@ type keyshareSession struct { issuerProofNonce *big.Int timestamp *atum.Timestamp pinCheck bool + protocolVersion *irma.ProtocolVersion } type keyshareServer struct { @@ -81,29 +82,27 @@ func newKeyshareServer(schemeManagerIdentifier irma.SchemeManagerIdentifier) (*k return ks, nil } -func (ks *keyshareServer) HashedPin(pin string) string { - hash := sha256.Sum256(append(ks.Nonce, []byte(pin)...)) +func (kss *keyshareServer) HashedPin(pin string) string { + hash := sha256.Sum256(append(kss.Nonce, []byte(pin)...)) // We must be compatible with the old Android app here, // which uses Base64.encodeToString(hash, Base64.DEFAULT), // which appends a newline. return base64.StdEncoding.EncodeToString(hash[:]) + "\n" } -// startKeyshareSession starts and completes the entire keyshare protocol with all involved keyshare servers +// newKeyshareSession starts and completes the entire keyshare protocol with all involved keyshare servers // for the specified session, merging the keyshare proofs into the specified ProofBuilder's. // The user's pin is retrieved using the KeysharePinRequestor, repeatedly, until either it is correct; or the // user cancels; or one of the keyshare servers blocks us. // Error, blocked or success of the keyshare session is reported back to the keyshareSessionHandler. -func startKeyshareSession( +func newKeyshareSession( sessionHandler keyshareSessionHandler, client *Client, pin KeysharePinRequestor, - builders gabi.ProofBuilderList, session irma.SessionRequest, implicitDisclosure [][]*irma.AttributeIdentifier, - issuerProofNonce *big.Int, - timestamp *atum.Timestamp, -) { + protocolVersion *irma.ProtocolVersion, +) (*keyshareSession, bool) { ksscount := 0 // A number of times below we need to look at all involved schemes, and then we need to take into @@ -122,27 +121,25 @@ func startKeyshareSession( if _, enrolled := client.keyshareServers[managerID]; !enrolled { err := errors.New("Not enrolled to keyshare server of scheme manager " + managerID.String()) sessionHandler.KeyshareError(&managerID, err) - return + return nil, false } } } if _, issuing := session.(*irma.IssuanceRequest); issuing && ksscount > 1 { err := errors.New("Issuance session involving more than one keyshare servers are not supported") sessionHandler.KeyshareError(nil, err) - return + return nil, false } ks := &keyshareSession{ - schemeIDs: schemeIDs, - session: session, - client: client, - builders: builders, - sessionHandler: sessionHandler, - transports: map[irma.SchemeManagerIdentifier]*irma.HTTPTransport{}, - pinRequestor: pin, - issuerProofNonce: issuerProofNonce, - timestamp: timestamp, - pinCheck: false, + schemeIDs: schemeIDs, + session: session, + client: client, + sessionHandler: sessionHandler, + transports: map[irma.SchemeManagerIdentifier]*irma.HTTPTransport{}, + pinRequestor: pin, + pinCheck: false, + protocolVersion: protocolVersion, } for managerID := range schemeIDs { @@ -158,31 +155,38 @@ func startKeyshareSession( ks.transports[managerID] = transport // Try to parse token as a jwt to see if it is still valid; if so we don't need to ask for the PIN - parser := new(jwt.Parser) - parser.SkipClaimsValidation = true // We want to verify expiry on our own below so we can add leeway - claims := jwt.StandardClaims{} - _, err := parser.ParseWithClaims(ks.keyshareServer.token, &claims, ks.client.Configuration.KeyshareServerKeyFunc(managerID)) - if err != nil { - irma.Logger.Info("Keyshare server token invalid, asking for PIN") - irma.Logger.Debug("Token: ", ks.keyshareServer.token) - ks.pinCheck = true - continue - } - // Add a minute of leeway for possible clockdrift with the server, - // and for the rest of the protocol to take place with this token - if !claims.VerifyExpiresAt(time.Now().Add(1*time.Minute).Unix(), true) { - irma.Logger.Info("Keyshare server token expires too soon, asking for PIN") - irma.Logger.Debug("Token: ", ks.keyshareServer.token) + if !ks.keyshareServer.tokenValid(ks.client.Configuration) { ks.pinCheck = true } } - if ks.pinCheck { - ks.sessionHandler.KeysharePin() - ks.VerifyPin(-1) - } else { - ks.GetCommitments() + if !ks.pinCheck { + return ks, true } + + ks.sessionHandler.KeysharePin() + return ks, ks.VerifyPin(-1) +} + +func (kss *keyshareServer) tokenValid(conf *irma.Configuration) bool { + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) // We want to verify expiry on our own below so we can add leeway + claims := jwt.RegisteredClaims{} + _, err := parser.ParseWithClaims(kss.token, &claims, conf.KeyshareServerKeyFunc(kss.SchemeManagerIdentifier)) + if err != nil { + irma.Logger.Info("Keyshare server token invalid") + irma.Logger.Debug("Token: ", kss.token) + return false + } + + // Add a minute of leeway for possible clockdrift with the server, + // and for the rest of the protocol to take place with this token + if !claims.VerifyExpiresAt(time.Now().Add(1*time.Minute), true) { + irma.Logger.Info("Keyshare server token expires too soon") + irma.Logger.Debug("Token: ", kss.token) + return false + } + + return true } func (ks *keyshareSession) fail(manager irma.SchemeManagerIdentifier, err error) { @@ -209,9 +213,9 @@ func (ks *keyshareSession) fail(manager irma.SchemeManagerIdentifier, err error) } } -// Ask for a pin, repeatedly if necessary, and either continue the keyshare protocol -// with authorization, or stop the keyshare protocol and inform of failure. -func (ks *keyshareSession) VerifyPin(attempts int) { +// VerifyPin asks for a pin, repeatedly if necessary, informing the handler of success or failure. +// It returns whether or not the authentication was succesful. +func (ks *keyshareSession) VerifyPin(attempts int) bool { ks.pinRequestor.RequestPin(attempts, PinHandler(func(proceed bool, pin string) { if !proceed { ks.sessionHandler.KeyshareCancelled() @@ -228,12 +232,12 @@ func (ks *keyshareSession) VerifyPin(attempts int) { } if success { ks.sessionHandler.KeysharePinOK() - ks.GetCommitments() return } // Not successful but no error and not yet blocked: try again ks.VerifyPin(attemptsRemaining) })) + return ks.keyshareServer.tokenValid(ks.client.Configuration) } // challengeRequestJWTExpiry is the expiry of the JWT sent to the keyshareserver at @@ -389,7 +393,10 @@ func (ks *keyshareSession) GetCommitments() { // (but only if we did not ask for a PIN earlier) ks.pinCheck = false ks.sessionHandler.KeysharePin() - ks.VerifyPin(-1) + authenticated := ks.VerifyPin(-1) + if authenticated { + ks.GetCommitments() + } return } ks.sessionHandler.KeyshareError(&managerID, err) @@ -462,6 +469,11 @@ func (ks *keyshareSession) Finish(challenge *big.Int, responses map[irma.SchemeM ks.sessionHandler.KeyshareError(&ks.keyshareServer.SchemeManagerIdentifier, err) return } + + if ks.protocolVersion.Below(2, 9) { + ks.removeKeysharePsFromProofUs(list) + } + message := &gabi.IssueCommitmentMessage{Proofs: list, Nonce2: ks.issuerProofNonce} message.ProofPjwts = map[string]string{} for manager, response := range responses { @@ -500,3 +512,27 @@ func (ks *keyshareSession) finishDisclosureOrSigning(challenge *big.Int, respons } ks.sessionHandler.KeyshareDone(list) } + +// 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) { + // Assemble keys of which to retrieve P's, grouped per keyshare server + distributedKeys := map[irma.SchemeManagerIdentifier][]irma.PublicKeyIdentifier{} + for _, futurecred := range request.Credentials { + schemeID := futurecred.CredentialTypeID.IssuerIdentifier().SchemeManagerIdentifier() + if ks.client.Configuration.SchemeManagers[schemeID].Distributed() { + distributedKeys[schemeID] = append(distributedKeys[schemeID], futurecred.PublicKeyIdentifier()) + } + } + + 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 { + return nil, err + } + keysharePs[schemeID] = &Ps + } + + return keysharePs, nil +} diff --git a/irmaclient/legacy.go b/irmaclient/legacy.go index a190f71f5..04208fece 100644 --- a/irmaclient/legacy.go +++ b/irmaclient/legacy.go @@ -503,3 +503,17 @@ func (kss *keyshareServer) registerPublicKey(client *Client, transport *irma.HTT return result, nil } + +// removeKeysharePsFromProofUs fixes a difference in gabi between the old keyshare protocol and +// the new one. In the old one, during issuance the client sends a proof of knowledge only of its +// own keyshare to the issuer. In the new one, it sends a proof of knowledge of the full secret. +// Therefore, the proofU contains a PoK over the full secret, while in case of the old keyshare +// protocol, the issuer expects a PoK only of the user's keyshare. This method removes the +// keyshare server's contribution for use in the old keyshare protocol. +func (ks *keyshareSession) removeKeysharePsFromProofUs(proofs gabi.ProofList) { + for i, proof := range proofs { + if proofU, ok := proof.(*gabi.ProofU); ok { + proofU.RemoveKeyshareP(ks.builders[i].(*gabi.CredentialBuilder)) + } + } +} diff --git a/irmaclient/session.go b/irmaclient/session.go index 1ecaf6fa6..cb91f15a6 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -113,6 +113,7 @@ var supportedVersions = map[int][]int{ 6, // introduces nonrevocation proofs 7, // introduces chained sessions 8, // introduces session binding + 9, // new keyshare protocol version }, } @@ -488,20 +489,26 @@ func (session *session) doSession(proceed bool, choice *irma.DisclosureChoice) { session.finish(false) } else { var err error - session.builders, session.attrIndices, session.issuerProofNonce, err = session.getBuilders() - if err != nil { - session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err}) - } - startKeyshareSession( + keyshareSession, auth := newKeyshareSession( session, session.client, session.Handler, - session.builders, session.request, session.implicitDisclosure, - session.issuerProofNonce, - session.timestamp, + session.Version, ) + if !auth { + // newKeyshareSession() calls session.fail() in case of failure, no need to do that here + return + } + session.builders, session.attrIndices, session.issuerProofNonce, err = session.getBuilders(keyshareSession) + if err != nil { + session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err}) + } + keyshareSession.builders = session.builders + keyshareSession.issuerProofNonce = session.issuerProofNonce + keyshareSession.timestamp = session.timestamp + keyshareSession.GetCommitments() } } @@ -584,7 +591,7 @@ func (session *session) sendResponse(message interface{}) { // getBuilders computes the builders for disclosure proofs or secretkey-knowledge proof (in case of disclosure/signing // and issuing respectively). -func (session *session) getBuilders() (gabi.ProofBuilderList, irma.DisclosedAttributeIndices, *big.Int, error) { +func (session *session) getBuilders(keyshareSession *keyshareSession) (gabi.ProofBuilderList, irma.DisclosedAttributeIndices, *big.Int, error) { var builders gabi.ProofBuilderList var err error var issuerProofNonce *big.Int @@ -594,7 +601,7 @@ func (session *session) getBuilders() (gabi.ProofBuilderList, irma.DisclosedAttr case irma.ActionSigning, irma.ActionDisclosing: builders, choices, session.timestamp, err = session.client.ProofBuilders(session.choice, session.request) case irma.ActionIssuing: - builders, choices, issuerProofNonce, err = session.client.IssuanceProofBuilders(session.request.(*irma.IssuanceRequest), session.choice) + builders, choices, issuerProofNonce, err = session.client.IssuanceProofBuilders(session.request.(*irma.IssuanceRequest), session.choice, keyshareSession) } return builders, choices, issuerProofNonce, err diff --git a/requests.go b/requests.go index 879f7732c..d74db2232 100644 --- a/requests.go +++ b/requests.go @@ -615,6 +615,13 @@ func (dr *DisclosureRequest) Validate() error { return nil } +func (cr *CredentialRequest) PublicKeyIdentifier() PublicKeyIdentifier { + return PublicKeyIdentifier{ + Issuer: cr.CredentialTypeID.IssuerIdentifier(), + Counter: cr.KeyCounter, + } +} + func (cr *CredentialRequest) Info(conf *Configuration, metadataVersion byte, issuedAt time.Time) (*CredentialInfo, error) { list, err := cr.AttributeList(conf, metadataVersion, nil, issuedAt) if err != nil { diff --git a/server/irmaserver/sessions.go b/server/irmaserver/sessions.go index acde5e1ce..bb2bde544 100644 --- a/server/irmaserver/sessions.go +++ b/server/irmaserver/sessions.go @@ -120,7 +120,7 @@ const ( var ( minProtocolVersion = irma.NewVersion(2, 4) - maxProtocolVersion = irma.NewVersion(2, 8) + maxProtocolVersion = irma.NewVersion(2, 8) // TODO support 2.9 minFrontendProtocolVersion = irma.NewVersion(1, 0) maxFrontendProtocolVersion = irma.NewVersion(1, 1) From 23213cd3060657f3e1ec77a0adf51acf20f54f86 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Mon, 12 Dec 2022 11:20:23 +0100 Subject: [PATCH 15/30] Update gabi dependency to include complete legacy keyshare protocol. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5ee1991d7..8573729d4 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/jinzhu/gorm v1.9.16 github.com/mdp/qrterminal v1.0.1 github.com/mitchellh/mapstructure v1.5.0 - github.com/privacybydesign/gabi v0.0.0-20221109103539-8e9ce1f4eb2c + github.com/privacybydesign/gabi v0.0.0-20221212095008-68a086907750 github.com/sietseringers/go-sse v0.0.0-20200801161811-e2cf2c63ca50 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cast v1.5.0 diff --git a/go.sum b/go.sum index fc2a0b542..bcb1fd472 100644 --- a/go.sum +++ b/go.sum @@ -281,8 +281,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-20221109103539-8e9ce1f4eb2c h1:Ffb+ORxS/DiG7crVSV0Zf0WXCrk6VHDbBFU92lRe2pI= -github.com/privacybydesign/gabi v0.0.0-20221109103539-8e9ce1f4eb2c/go.mod h1:QZI8hX8Ff2GfZ7UJuxyWw3nAGgt2s5+U4hxY6rmwQvs= +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/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= From b5e4ad82d6f3d3535fa1a3f91496928baf507357 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Mon, 12 Dec 2022 11:37:20 +0100 Subject: [PATCH 16/30] Print PublicKeyIdentifier correctly in error text. --- irmaclient/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irmaclient/client.go b/irmaclient/client.go index 67d52274f..e30870708 100644 --- a/irmaclient/client.go +++ b/irmaclient/client.go @@ -981,7 +981,7 @@ func (client *Client) IssuanceProofBuilders( if distributed { keyshareP, present = keysharePs[schemeID].Ps[keyID] if distributed && !present { - return nil, nil, nil, errors.Errorf("missing keyshareP for %s", keyID) + return nil, nil, nil, errors.Errorf("missing keyshareP for %s-%d", keyID.Issuer, keyID.Counter) } } From f06a57a5abdf17e03c8110e9b761aacc4c51504c Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Mon, 12 Dec 2022 11:41:48 +0100 Subject: [PATCH 17/30] Fix linter warnings. --- irmaclient/keyshare.go | 2 +- irmaconfig.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/irmaclient/keyshare.go b/irmaclient/keyshare.go index b32665a4f..b9b859b82 100644 --- a/irmaclient/keyshare.go +++ b/irmaclient/keyshare.go @@ -214,7 +214,7 @@ func (ks *keyshareSession) fail(manager irma.SchemeManagerIdentifier, err error) } // VerifyPin asks for a pin, repeatedly if necessary, informing the handler of success or failure. -// It returns whether or not the authentication was succesful. +// It returns whether the authentication was successful or not. func (ks *keyshareSession) VerifyPin(attempts int) bool { ks.pinRequestor.RequestPin(attempts, PinHandler(func(proceed bool, pin string) { if !proceed { diff --git a/irmaconfig.go b/irmaconfig.go index 92367638b..3e8658241 100644 --- a/irmaconfig.go +++ b/irmaconfig.go @@ -211,7 +211,7 @@ func (conf *Configuration) ParseFolder() (err error) { // Any error encountered during parsing is considered recoverable only if it is of type *SchemeManagerError; // In this case the scheme in which it occurred is downloaded from its remote and re-parsed. // If any other error is encountered at any time, it is returned immediately. -// If no error is returned, parsing and possibly restoring has been succesfull, and there should be no +// If no error is returned, parsing and possibly restoring has been successful, and there should be no // disabled schemes. func (conf *Configuration) ParseOrRestoreFolder() (rerr error) { err := conf.ParseFolder() From fb779f242699723b5754a3d572dc95dc565ed326 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Mon, 12 Dec 2022 14:45:12 +0100 Subject: [PATCH 18/30] Correct nesting of /api/v2 router. --- server/keyshare/keyshareserver/server.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go index a88849906..596a42a5a 100644 --- a/server/keyshare/keyshareserver/server.go +++ b/server/keyshare/keyshareserver/server.go @@ -122,14 +122,14 @@ func (s *Server) Handler() http.Handler { router.Route("/api/v1", func(r chi.Router) { s.routeHandler(r) }) - }) - router.Route("/api/v2", func(r chi.Router) { - // Keyshare sessions with provably secure keyshare protocol - r.Use(s.userMiddleware) - r.Use(s.authorizationMiddleware) - r.Post("/prove/getPs", s.handlePs) - r.Post("/prove/getCommitments", s.handleCommitmentsV2) + router.Route("/api/v2", func(r chi.Router) { + // Keyshare sessions with provably secure keyshare protocol + r.Use(s.userMiddleware) + r.Use(s.authorizationMiddleware) + r.Post("/prove/getPs", s.handlePs) + r.Post("/prove/getCommitments", s.handleCommitmentsV2) + }) }) // IRMA server for issuing myirma credential during registration From eecf58a1543068cd4c6ae16894605e083c7003b1 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Tue, 20 Dec 2022 14:21:07 +0100 Subject: [PATCH 19/30] Fix typo in JSON marshalling of ProofPCommitmentMapV2. --- messages.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages.go b/messages.go index 810b00100..fe3a445a1 100644 --- a/messages.go +++ b/messages.go @@ -424,7 +424,7 @@ type ProofPCommitmentMapV2 struct { func (cm *ProofPCommitmentMapV2) MarshalJSON() ([]byte, error) { var encCM struct { - Commitments map[string]*big.Int `json:"p"` + Commitments map[string]*big.Int `json:"c"` } encCM.Commitments = make(map[string]*big.Int) From e722c148cfd21830f3cf6493b3bc9c0a66fe0482 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Tue, 20 Dec 2022 14:26:21 +0100 Subject: [PATCH 20/30] Adjust error messages to lower-case for consistency with Go best practice. --- server/keyshare/keyshareserver/server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go index 596a42a5a..de608bd52 100644 --- a/server/keyshare/keyshareserver/server.go +++ b/server/keyshare/keyshareserver/server.go @@ -200,7 +200,7 @@ func (s *Server) handlePs(w http.ResponseWriter, r *http.Request) { } if len(keys) == 0 { s.conf.Logger.Info("Malformed request: no keys for P specified") - server.WriteError(w, server.ErrorInvalidRequest, "No key specified") + server.WriteError(w, server.ErrorInvalidRequest, "no key specified") return } @@ -250,7 +250,7 @@ func (s *Server) handleCommitments(w http.ResponseWriter, r *http.Request) { } if len(keys) == 0 { s.conf.Logger.Info("Malformed request: no keys for commitment specified") - server.WriteError(w, server.ErrorInvalidRequest, "No key specified") + server.WriteError(w, server.ErrorInvalidRequest, "no key specified") return } @@ -312,7 +312,7 @@ func (s *Server) handleCommitmentsV2(w http.ResponseWriter, r *http.Request) { } if len(req.Keys) == 0 { s.conf.Logger.Info("Malformed request: no keys for commitment specified") - server.WriteError(w, server.ErrorInvalidRequest, "No key specified") + server.WriteError(w, server.ErrorInvalidRequest, "no key specified") return } From f9ae2f83618b222a57f50efee53dc2b42a999efd Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Wed, 21 Dec 2022 20:13:10 +0100 Subject: [PATCH 21/30] fix: add missing return statement in error handling --- irmaclient/session.go | 1 + 1 file changed, 1 insertion(+) diff --git a/irmaclient/session.go b/irmaclient/session.go index cb91f15a6..26f1b22fd 100644 --- a/irmaclient/session.go +++ b/irmaclient/session.go @@ -504,6 +504,7 @@ func (session *session) doSession(proceed bool, choice *irma.DisclosureChoice) { session.builders, session.attrIndices, session.issuerProofNonce, err = session.getBuilders(keyshareSession) if err != nil { session.fail(&irma.SessionError{ErrorType: irma.ErrorCrypto, Err: err}) + return } keyshareSession.builders = session.builders keyshareSession.issuerProofNonce = session.issuerProofNonce From 88466bd15053bb865e6a959f6f10160029efbd47 Mon Sep 17 00:00:00 2001 From: Maja Reissner Date: Thu, 19 Jan 2023 12:51:17 +0100 Subject: [PATCH 22/30] Add keyshare server getResponse v2 endpoints (both linkable and unlinkable). --- internal/keysharecore/operations.go | 56 ++++++ server/keyshare/keyshareserver/server.go | 79 ++++++++ .../keyshare/keyshareserver/server_v2_test.go | 168 +++++++++++++++++- 3 files changed, 302 insertions(+), 1 deletion(-) diff --git a/internal/keysharecore/operations.go b/internal/keysharecore/operations.go index 06391ad49..8959f56be 100644 --- a/internal/keysharecore/operations.go +++ b/internal/keysharecore/operations.go @@ -304,6 +304,62 @@ func (c *Core) GenerateResponse(secrets UserSecrets, accessToken string, commitI return token.SignedString(c.jwtPrivateKey) } +// GenerateResponseV2 generates the response of a zero-knowledge proof of the keyshare secret, for a given previous commit and response request. +func (c *Core) GenerateResponseV2( + secrets UserSecrets, + accessToken string, + commitID uint64, + hash gabi.KeyshareCommitmentRequest, + req gabi.KeyshareResponseRequest[irma.PublicKeyIdentifier], + keyID irma.PublicKeyIdentifier, + linkable bool) (string, error) { + // Validate request + key, ok := c.trustedKeys[keyID] + if !ok { + return "", ErrKeyNotFound + } + + // Use verifyAccess to get the decrypted secrets. The access has already been verified in the + // middleware. We use the call merely to fetch the unencryptedUserSecrets here. + s, err := c.verifyAccess(secrets, accessToken) + if err != nil { + return "", err + } + + // Fetch commit + c.commitmentMutex.Lock() + commit, ok := c.commitmentData[commitID] + delete(c.commitmentData, commitID) + c.commitmentMutex.Unlock() + if !ok { + return "", ErrUnknownCommit + } + + proofP, err := gabi.KeyshareResponse(s.KeyshareSecret, commit, hash, req, c.trustedKeys) + if err != nil { + return "", err + } + + if uint(proofP.C.BitLen()) > gabikeys.DefaultSystemParameters[1024].Lh || proofP.C.Cmp(big.NewInt(0)) < 0 { + return "", ErrInvalidChallenge + } + + // Linkable response for legacy purposes + if linkable { + proofP.P = new(big.Int).Exp(key.R[0], s.KeyshareSecret, key.N) + } + + // Generate response + token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ + "ProofP": proofP, + "iat": time.Now().Unix(), + "sub": "ProofP", + "iss": c.jwtIssuer, + }) + token.Header["kid"] = c.jwtPrivateKeyID + return token.SignedString(c.jwtPrivateKey) +} + func (c *Core) GenerateChallenge(secrets UserSecrets, jwtt string) ([]byte, error) { s, err := c.decryptUserSecrets(secrets) if err != nil { diff --git a/server/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go index de608bd52..a21074075 100644 --- a/server/keyshare/keyshareserver/server.go +++ b/server/keyshare/keyshareserver/server.go @@ -129,6 +129,8 @@ func (s *Server) Handler() http.Handler { r.Use(s.authorizationMiddleware) r.Post("/prove/getPs", s.handlePs) r.Post("/prove/getCommitments", s.handleCommitmentsV2) + r.Post("/prove/getResponse", s.handleResponseV2) + r.Post("/prove/getResponseLinkable", s.handleResponseV2Linkable) }) }) @@ -432,6 +434,83 @@ func (s *Server) generateResponse(user *User, authorization string, challenge *b return proofResponse, nil } +// /api/v2/prove/getResponse +func (s *Server) handleResponseV2(w http.ResponseWriter, r *http.Request) { + s.keyshareResponse(w, r, false) +} + +func (s *Server) keyshareResponse(w http.ResponseWriter, r *http.Request, linkable bool) { + // Fetch from context + user := r.Context().Value("user").(*User) + authorization := r.Context().Value("authorization").(string) + + var req gabi.KeyshareResponseRequest[irma.PublicKeyIdentifier] + if err := server.ParseBody(r, &req); err != nil { + server.WriteError(w, server.ErrorInvalidRequest, err.Error()) + return + } + + // verify access (avoids leaking whether there is a session ongoing to unauthorized callers) + if !r.Context().Value("hasValidAuthorization").(bool) { + s.conf.Logger.Warn("Could not generate keyshare response due to invalid authorization") + server.WriteError(w, server.ErrorInvalidRequest, "Invalid authorization") + return + } + + // And do the actual responding + proofResponse, err := s.generateResponseV2(user, authorization, req, linkable) + if err != nil && + (err == keysharecore.ErrInvalidChallenge || + err == keysharecore.ErrInvalidJWT || + err == errMissingCommitment) { + server.WriteError(w, server.ErrorInvalidRequest, err.Error()) + return + } + if err != nil { + // already logged + server.WriteError(w, server.ErrorInternal, err.Error()) + return + } + + server.WriteString(w, proofResponse) +} + +func (s *Server) generateResponseV2(user *User, authorization string, req gabi.KeyshareResponseRequest[irma.PublicKeyIdentifier], linkable bool) (string, error) { + // Get data from session + sessionData := s.store.get(user.Username) + if sessionData == nil { + s.conf.Logger.Warn("Request for response without previous call to get commitments") + return "", errMissingCommitment + } + + // Indicate activity on user account + err := s.db.setSeen(user) + if err != nil { + s.conf.Logger.WithField("error", err).Error("Could not mark user as seen recently") + // Do not send to user + } + + // Make log entry + err = s.db.addLog(user, eventTypeIRMASession, nil) + if err != nil { + s.conf.Logger.WithField("error", err).Error("Could not add log entry for user") + return "", err + } + + proofResponse, err := s.core.GenerateResponseV2(user.Secrets, authorization, sessionData.CommitID, sessionData.Hw, req, sessionData.KeyID, linkable) + if err != nil { + s.conf.Logger.WithField("error", err).Error("Could not generate response for request") + return "", err + } + + return proofResponse, nil +} + +// /prove/getLinkableResponse +func (s *Server) handleResponseV2Linkable(w http.ResponseWriter, r *http.Request) { + s.keyshareResponse(w, r, true) +} + // /users/verify_start func (s *Server) handleVerifyStart(w http.ResponseWriter, r *http.Request) { // Extract request diff --git a/server/keyshare/keyshareserver/server_v2_test.go b/server/keyshare/keyshareserver/server_v2_test.go index 5bb7fcc96..216d24900 100644 --- a/server/keyshare/keyshareserver/server_v2_test.go +++ b/server/keyshare/keyshareserver/server_v2_test.go @@ -1,11 +1,18 @@ package keyshareserver import ( + "crypto/sha256" + "github.com/fxamacker/cbor" + "github.com/privacybydesign/gabi" + "github.com/privacybydesign/gabi/big" + "github.com/privacybydesign/gabi/gabikeys" irma "github.com/privacybydesign/irmago" "github.com/privacybydesign/irmago/internal/test" + "github.com/privacybydesign/irmago/server" "github.com/stretchr/testify/require" "net/http" "testing" + "time" ) func TestServerInvalidMessageV2(t *testing.T) { @@ -28,6 +35,14 @@ func TestServerInvalidMessageV2(t *testing.T) { "[]", nil, 403, nil, ) + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponse", + "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, + 403, nil, + ) + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponseLinkable", + "asdlkzdsf;lskajl;kasdjfvl;jzxclvyewr", nil, + 403, nil, + ) } func TestMissingUserV2(t *testing.T) { @@ -49,6 +64,22 @@ func TestMissingUserV2(t *testing.T) { }, 403, nil, ) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponse", + "123456789", http.Header{ + "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, + "Authorization": []string{"ey.ey.ey"}, + }, + 403, nil, + ) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponseLinkable", + "123456789", http.Header{ + "X-IRMA-Keyshare-Username": []string{"doesnotexist"}, + "Authorization": []string{"ey.ey.ey"}, + }, + 403, nil, + ) } func TestKeyshareSessionsV2(t *testing.T) { @@ -77,6 +108,23 @@ func TestKeyshareSessionsV2(t *testing.T) { for _, user := range []struct { username, auth string }{{"testusername", auth1}, {"legacyuser", auth2}} { + // no active session, can't retrieve result + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponse", + "12345678", http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 400, nil, + ) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponseLinkable", + "12345678", http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 400, nil, + ) + // can't retrieve commitments with fake authorization test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", `["test.test-3"]`, http.Header{ @@ -113,14 +161,132 @@ func TestKeyshareSessionsV2(t *testing.T) { 200, nil, ) + // can't retrieve result with fake authorization + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponse", + "12345678", http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{"fakeauthorization"}, + }, + 400, nil, + ) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponseLinkable", + "12345678", http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{"fakeauthorization"}, + }, + 400, nil, + ) + + commitmentReq, responseReq := prepareRequests(t) + // can start session while another is already active test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", - `{"keys":["test.test-3"],"hw":{"hashedComms":"WW91ciBTdHJpbmc="}}`, http.Header{ + `{"keys":["test.test-3"],"hw":`+server.ToJson(commitmentReq)+`}`, http.Header{ "X-IRMA-Keyshare-Username": []string{user.username}, "Authorization": []string{user.auth}, }, 200, nil, ) + // finish session + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponse", + server.ToJson(responseReq), http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 200, nil, + ) + + // complete session with standard getCommitments call and linkable response call + commitmentReq2, responseReq2 := prepareRequests(t) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getCommitments", + `{"keys":["test.test-3"],"hw":`+server.ToJson(commitmentReq2)+`}`, http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 200, nil, + ) + + test.HTTPPost(t, nil, "http://localhost:8080/api/v2/prove/getResponseLinkable", + server.ToJson(responseReq2), http.Header{ + "X-IRMA-Keyshare-Username": []string{user.username}, + "Authorization": []string{user.auth}, + }, + 200, nil, + ) } } + +func prepareRequests(t *testing.T) (gabi.KeyshareCommitmentRequest, gabi.KeyshareResponseRequest[irma.PublicKeyIdentifier]) { + challenge := big.NewInt(73645263) + keyID := irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier("test.test"), Counter: 3} + + kssSecret, err := gabi.GenerateSecretAttribute() + require.NoError(t, err) + userSecret, err := gabi.GenerateSecretAttribute() + require.NoError(t, err) + + nonce, err := gabi.GenerateNonce() + require.NoError(t, err) + + n := s2big("96063359353814070257464989369098573470645843347358957127875426328487326540633303185702306359400766259130239226832166456957259123554826741975265634464478609571816663003684533868318795865194004795637221226902067194633407757767792795252414073029114153019362701793292862118990912516058858923030408920700061749321") + S := s2big("68460510129747727135744503403370273952956360997532594630007762045745171031173231339034881007977792852962667675924510408558639859602742661846943843432940752427075903037429735029814040501385798095836297700111333573975220392538916785564158079116348699773855815825029476864341585033111676283214405517983188761136") + Z := s2big("44579327840225837958738167571392618381868336415293109834301264408385784355849790902532728798897199236650711385876328647206143271336410651651791998475869027595051047904885044274040212624547595999947339956165755500019260290516022753290814461070607850420459840370288988976468437318992206695361417725670417150636") + rValues := []string{"75350858539899247205099195870657569095662997908054835686827949842616918065279527697469302927032348256512990413925385972530386004430200361722733856287145745926519366823425418198189091190950415327471076288381822950611094023093577973125683837586451857056904547886289627214081538422503416179373023552964235386251", + "16493273636283143082718769278943934592373185321248797185217530224336539646051357956879850630049668377952487166494198481474513387080523771033539152347804895674103957881435528189990601782516572803731501616717599698546778915053348741763191226960285553875185038507959763576845070849066881303186850782357485430766", + "13291821743359694134120958420057403279203178581231329375341327975072292378295782785938004910295078955941500173834360776477803543971319031484244018438746973179992753654070994560440903251579649890648424366061116003693414594252721504213975050604848134539324290387019471337306533127861703270017452296444985692840", + "86332479314886130384736453625287798589955409703988059270766965934046079318379171635950761546707334446554224830120982622431968575935564538920183267389540869023066259053290969633312602549379541830869908306681500988364676409365226731817777230916908909465129739617379202974851959354453994729819170838277127986187", + "68324072803453545276056785581824677993048307928855083683600441649711633245772441948750253858697288489650767258385115035336890900077233825843691912005645623751469455288422721175655533702255940160761555155932357171848703103682096382578327888079229101354304202688749783292577993444026613580092677609916964914513", + "65082646756773276491139955747051924146096222587013375084161255582716233287172212541454173762000144048198663356249316446342046266181487801411025319914616581971563024493732489885161913779988624732795125008562587549337253757085766106881836850538709151996387829026336509064994632876911986826959512297657067426387"} + + // Too bad there is no better way to have big int constants + R := make([]*big.Int, len(rValues)) + for i, rv := range rValues { + R[i], _ = new(big.Int).SetString(rv, 10) + } + + testPubK, _ := gabikeys.NewPublicKey(n, Z, S, nil, nil, R, "", 0, time.Now().AddDate(1, 0, 0)) + + testPubK.Issuer = "testPubK" + + keysSlice := []*gabikeys.PublicKey{testPubK} + + _, kssComm, err := gabi.NewKeyshareCommitments(kssSecret, keysSlice) + require.NoError(t, err) + userRandomizer, userComm, err := gabi.NewKeyshareCommitments(userSecret, keysSlice) + require.NoError(t, err) + + totalP := new(big.Int) + totalP.Mul(userComm[0].P, kssComm[0].P).Mod(totalP, testPubK.N) + totalW := new(big.Int) + totalW.Mul(userComm[0].Pcommit, kssComm[0].Pcommit).Mod(totalW, testPubK.N) + + i := []gabi.KeyshareUserChallengeInput[irma.PublicKeyIdentifier]{{ + KeyID: &keyID, + Value: totalP, + Commitment: userComm[0].Pcommit, + }} + + resp := gabi.KeyshareResponseRequest[irma.PublicKeyIdentifier]{ + Nonce: nonce, + UserResponse: new(big.Int).Add(userRandomizer, new(big.Int).Mul(challenge, userSecret)), + IsSignatureSession: false, + UserChallengeInput: i, + } + + bts, _ := cbor.Marshal(i, cbor.EncOptions{}) + h := sha256.Sum256(bts) + + req := gabi.KeyshareCommitmentRequest{HashedUserCommitments: h[:]} + + return req, resp +} + +// A convenience function for initializing big integers from known correct (10 +// base) strings. Use with care, errors are ignored. +func s2big(s string) (r *big.Int) { + r, _ = new(big.Int).SetString(s, 10) + return +} From 24e164fbf9153f364b9e27d254dc99e705022d23 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 28 Jul 2023 15:11:05 +0200 Subject: [PATCH 23/30] Refactor: rename parameter in keysharecore's GenerateResponseV2 --- internal/keysharecore/operations.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/keysharecore/operations.go b/internal/keysharecore/operations.go index 8959f56be..26c6cfdca 100644 --- a/internal/keysharecore/operations.go +++ b/internal/keysharecore/operations.go @@ -309,7 +309,7 @@ func (c *Core) GenerateResponseV2( secrets UserSecrets, accessToken string, commitID uint64, - hash gabi.KeyshareCommitmentRequest, + hashedComms gabi.KeyshareCommitmentRequest, req gabi.KeyshareResponseRequest[irma.PublicKeyIdentifier], keyID irma.PublicKeyIdentifier, linkable bool) (string, error) { @@ -335,7 +335,7 @@ func (c *Core) GenerateResponseV2( return "", ErrUnknownCommit } - proofP, err := gabi.KeyshareResponse(s.KeyshareSecret, commit, hash, req, c.trustedKeys) + proofP, err := gabi.KeyshareResponse(s.KeyshareSecret, commit, hashedComms, req, c.trustedKeys) if err != nil { return "", err } From e573ad068699a81e5d3f8225c2492eb56cd35e67 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 28 Jul 2023 15:26:12 +0200 Subject: [PATCH 24/30] Docs: add explanation about keysharecore's GenerateResponseV2 linkable parameter --- internal/keysharecore/operations.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/keysharecore/operations.go b/internal/keysharecore/operations.go index 26c6cfdca..fc07d9ca8 100644 --- a/internal/keysharecore/operations.go +++ b/internal/keysharecore/operations.go @@ -305,6 +305,8 @@ func (c *Core) GenerateResponse(secrets UserSecrets, accessToken string, commitI } // GenerateResponseV2 generates the response of a zero-knowledge proof of the keyshare secret, for a given previous commit and response request. +// In older versions of the IRMA protocol (2.8 or below), issuers need a response that is linkable to earlier issuance sessions. In this case, +// the ProofP.P will be set as well. The linkable parameter indicates whether the ProofP.P should be included. func (c *Core) GenerateResponseV2( secrets UserSecrets, accessToken string, @@ -344,7 +346,7 @@ func (c *Core) GenerateResponseV2( return "", ErrInvalidChallenge } - // Linkable response for legacy purposes + // Set Proof.P to R_0^userSecret if the response should be linkable. if linkable { proofP.P = new(big.Int).Exp(key.R[0], s.KeyshareSecret, key.N) } From f7853752302ea1a3df6beec8a0be134df773bcc9 Mon Sep 17 00:00:00 2001 From: Sietse Ringers Date: Mon, 28 Aug 2023 13:30:55 +0200 Subject: [PATCH 25/30] Fix keyshare server sessions involving a legacy issuer --- internal/keysharecore/operations.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/keysharecore/operations.go b/internal/keysharecore/operations.go index fc07d9ca8..40291eb6f 100644 --- a/internal/keysharecore/operations.go +++ b/internal/keysharecore/operations.go @@ -346,9 +346,14 @@ func (c *Core) GenerateResponseV2( return "", ErrInvalidChallenge } - // Set Proof.P to R_0^userSecret if the response should be linkable. + // If the session involves a legacy issuer that doesn't understand the new keyshare protocol, + // return a legacy ProofP of the old keyshare protocol, differing as follows to a normal ProofP: + // - Includes P = R_0^userSecret in the Proof.P field (making the ProofP linkable) + // - The response in ProofP.SResponse should be just our response, not with the user's response added + // as done by added earlier in `gabi.KeyshareResponse()` above, so we subtract the user's response from it. if linkable { proofP.P = new(big.Int).Exp(key.R[0], s.KeyshareSecret, key.N) + proofP.SResponse.Sub(proofP.SResponse, req.UserResponse) } // Generate response From 3aadc2d913b8dec3b77a7e94636fcf06a37f42c2 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 1 Sep 2023 15:36:58 +0200 Subject: [PATCH 26/30] Chore: remove leftover TODOs --- internal/keysharecore/operations_test.go | 1 - server/keyshare/keyshareserver/server.go | 2 -- 2 files changed, 3 deletions(-) diff --git a/internal/keysharecore/operations_test.go b/internal/keysharecore/operations_test.go index 825dceff5..04e50e02c 100644 --- a/internal/keysharecore/operations_test.go +++ b/internal/keysharecore/operations_test.go @@ -183,7 +183,6 @@ func TestProofFunctionality(t *testing.T) { require.NoError(t, err) // For issuance, initially get P_t - // TODO: The result ps will be used in the generate commitment step and checked when the response is made. _, err = c.GeneratePs(secrets, jwtt, []irma.PublicKeyIdentifier{irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier("test"), Counter: 1}}) require.NoError(t, err) diff --git a/server/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go index 9fc75a000..9af977ac3 100644 --- a/server/keyshare/keyshareserver/server.go +++ b/server/keyshare/keyshareserver/server.go @@ -315,7 +315,6 @@ func (s *Server) handleCommitmentsV2(w http.ResponseWriter, r *http.Request) { } commitments, err := s.generateCommitmentsV2(user, authorization, req) - // TODO: can ErrInvalidChallenge be removed? if err != nil && (err == keysharecore.ErrInvalidChallenge || err == keysharecore.ErrInvalidJWT) { server.WriteError(w, server.ErrorInvalidRequest, err.Error()) return @@ -338,7 +337,6 @@ func (s *Server) generateCommitmentsV2(user *User, authorization string, req irm } // Prepare output message format - // TODO: move logic to gabi? mappedCommitments := map[irma.PublicKeyIdentifier]*big.Int{} for i, keyID := range req.Keys { mappedCommitments[keyID] = commitments[i].Pcommit From 926dd06c7df4837cf7c764e45bd29549126754ad Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 1 Sep 2023 15:49:00 +0200 Subject: [PATCH 27/30] Chore: updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75faaa0c4..b8d504bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## Added +- Keyshare server /api/v2/prove/... endpoints for the new keyshare protocol + ### Added - E-mail address revalidation, addressing issues where user's e-mail addresses can be (temporary) invalid From a26449accf42e1a8af81eb69accd7d937375dac1 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Fri, 1 Sep 2023 16:42:29 +0200 Subject: [PATCH 28/30] Chore: fix duplicate headers in changelog --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d504bc6..c8ec8773d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,9 @@ 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). ## Unreleased - -## Added -- Keyshare server /api/v2/prove/... endpoints for the new keyshare protocol - ### Added - E-mail address revalidation, addressing issues where user's e-mail addresses can be (temporary) invalid +- Keyshare server /api/v2/prove/... endpoints for the new keyshare protocol ### Changed - Use separate application user in Dockerfile for entrypoint From 1171a7032ce5f300fa9d91b87399eb3863499a2f Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Mon, 4 Sep 2023 11:41:05 +0200 Subject: [PATCH 29/30] Chore: make PMap JSON tags consistent with variable names --- messages.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messages.go b/messages.go index 28788e1cd..135ece15a 100644 --- a/messages.go +++ b/messages.go @@ -393,12 +393,12 @@ func (ppcm *ProofPCommitmentMap) MarshalJSON() ([]byte, error) { } type PMap struct { - Ps map[PublicKeyIdentifier]*big.Int `json:"p"` + Ps map[PublicKeyIdentifier]*big.Int `json:"ps"` } func (pm *PMap) MarshalJSON() ([]byte, error) { var encPM struct { - Ps map[string]*big.Int `json:"p"` + Ps map[string]*big.Int `json:"ps"` } encPM.Ps = make(map[string]*big.Int) From 586291482da81e785f1dc6cf243b158d0d3ea555 Mon Sep 17 00:00:00 2001 From: Ivar Derksen Date: Tue, 5 Sep 2023 11:50:13 +0200 Subject: [PATCH 30/30] Docs: remove duplicate changelog line --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3361111ed..c435ed8ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added -- E-mail address revalidation, addressing issues where user's e-mail addresses can be (temporary) invalid - Keyshare server /api/v2/prove/... endpoints for the new keyshare protocol ## [0.13.2] - 2023-08-22