diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4f49cb7d..0de5436ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,11 +43,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
}
```
- Renewal endpoint for keyshare attribute in the keyshare server (`/users/renewKeyshareAttribute`)
+- Keyshare server /api/v2/prove/... endpoints for the new keyshare protocol
### Changed
- `KeyshareVerifyPin` function in irmaclient ensures the keyshare attribute is valid
-
-### Changed
- Sending the account expiry email is done when user has only valid e-mail addresses
- Strip unnecessary details from database errors
diff --git a/docs/keyshareFlow.puml b/docs/keyshareFlow.puml
new file mode 100644
index 000000000..54977b018
--- /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, response}
+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/getPs \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
diff --git a/go.mod b/go.mod
index 4ad65d337..11a44113e 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.1
github.com/mdp/qrterminal v1.0.1
github.com/mitchellh/mapstructure v1.5.0
- github.com/privacybydesign/gabi v0.0.0-20221012093643-8e978bfbb252
+ 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 413e4b6a7..149cb85b4 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.28.3 h1:swTsge6u/1Ei51b9VLMz/YTzEzWpbsk5SiR7m5fklTI=
github.com/go-co-op/gocron v1.28.3/go.mod h1:39f6KNSGVOU1LO/ZOoZfcSxwlsJDQOKSu8erN0SH48Y=
-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=
@@ -238,7 +236,6 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -302,8 +299,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-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-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=
@@ -314,7 +311,6 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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=
@@ -333,14 +329,12 @@ 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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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=
@@ -362,7 +356,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=
@@ -387,7 +380,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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-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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -408,6 +400,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=
@@ -501,7 +494,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=
diff --git a/internal/keysharecore/operations.go b/internal/keysharecore/operations.go
index dd313bbae..40291eb6f 100644
--- a/internal/keysharecore/operations.go
+++ b/internal/keysharecore/operations.go
@@ -197,6 +197,35 @@ 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)
+ }
+
+ // 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
+ }
+
+ 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
@@ -209,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
@@ -247,7 +277,52 @@ 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
+ }
+
+ // Fetch commit
+ c.commitmentMutex.Lock()
+ commit, ok := c.commitmentData[commitID]
+ delete(c.commitmentData, commitID)
+ c.commitmentMutex.Unlock()
+ if !ok {
+ return "", ErrUnknownCommit
+ }
+
+ // Generate response
+ token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
+ "ProofP": gabi.KeyshareResponseLegacy(s.KeyshareSecret, commit, challenge, key),
+ "iat": time.Now().Unix(),
+ "sub": "ProofP",
+ "iss": c.jwtIssuer,
+ })
+ token.Header["kid"] = c.jwtPrivateKeyID
+ 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.
+// 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,
+ commitID uint64,
+ hashedComms 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
@@ -262,9 +337,28 @@ func (c *Core) GenerateResponse(secrets UserSecrets, accessToken string, commitI
return "", ErrUnknownCommit
}
+ proofP, err := gabi.KeyshareResponse(s.KeyshareSecret, commit, hashedComms, 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
+ }
+
+ // 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
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
- "ProofP": gabi.KeyshareResponse(s.KeyshareSecret, commit, challenge, key),
+ "ProofP": proofP,
"iat": time.Now().Unix(),
"sub": "ProofP",
"iss": c.jwtIssuer,
diff --git a/internal/keysharecore/operations_test.go b/internal/keysharecore/operations_test.go
index d1c033853..04e50e02c 100644
--- a/internal/keysharecore/operations_test.go
+++ b/internal/keysharecore/operations_test.go
@@ -182,6 +182,10 @@ func TestProofFunctionality(t *testing.T) {
jwtt, err := validateAuth(t, c, signer, secrets, pin)
require.NoError(t, err)
+ // For issuance, initially get P_t
+ _, 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 +250,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 +321,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")
diff --git a/irmaclient/client.go b/irmaclient/client.go
index b4a227a3b..d8554622f 100644
--- a/irmaclient/client.go
+++ b/irmaclient/client.go
@@ -952,22 +952,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-%d", keyID.Issuer, keyID.Counter)
+ }
+ }
+
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, credtype.RandomBlindAttributeIndices())
+ client.secretkey.Key, issuerProofNonce, keyshareP, credtype.RandomBlindAttributeIndices())
if err != nil {
return nil, nil, nil, err
}
@@ -986,7 +1008,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 1e9b597f6..bfa19169d 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 {
@@ -89,21 +90,19 @@ func (kss *keyshareServer) HashedPin(pin string) string {
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,36 +155,43 @@ 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
}
-// 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 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 {
ks.sessionHandler.KeyshareCancelled()
@@ -204,12 +208,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
@@ -366,7 +370,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)
@@ -385,7 +392,7 @@ func (ks *keyshareSession) GetCommitments() {
if !distributed {
continue
}
- builder.MergeProofPCommitment(comm)
+ builder.SetProofPCommitment(comm)
}
ks.GetProofPs()
@@ -439,6 +446,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 {
@@ -477,3 +489,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 31b1da38d..7f8274ca4 100644
--- a/irmaclient/legacy.go
+++ b/irmaclient/legacy.go
@@ -558,3 +558,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 eeeac9b63..50fff41ed 100644
--- a/irmaclient/session.go
+++ b/irmaclient/session.go
@@ -115,6 +115,7 @@ var supportedVersions = map[int][]int{
6, // introduces nonrevocation proofs
7, // introduces chained sessions
8, // introduces session binding
+ 9, // new keyshare protocol version
},
}
@@ -492,20 +493,27 @@ 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})
+ return
+ }
+ keyshareSession.builders = session.builders
+ keyshareSession.issuerProofNonce = session.issuerProofNonce
+ keyshareSession.timestamp = session.timestamp
+ keyshareSession.GetCommitments()
}
}
@@ -593,7 +601,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
@@ -603,7 +611,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/irmaconfig.go b/irmaconfig.go
index 4fef97bc7..4f5265c81 100644
--- a/irmaconfig.go
+++ b/irmaconfig.go
@@ -225,7 +225,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()
diff --git a/messages.go b/messages.go
index 767144340..dee57e4ff 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"
@@ -398,6 +399,53 @@ func (ppcm *ProofPCommitmentMap) MarshalJSON() ([]byte, error) {
return json.Marshal(encPPCM)
}
+type PMap struct {
+ Ps map[PublicKeyIdentifier]*big.Int `json:"ps"`
+}
+
+func (pm *PMap) MarshalJSON() ([]byte, error) {
+ var encPM struct {
+ Ps map[string]*big.Int `json:"ps"`
+ }
+ 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)
+}
+
+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:"c"`
+ }
+ 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/requests.go b/requests.go
index cde5c948d..4a2443744 100644
--- a/requests.go
+++ b/requests.go
@@ -619,6 +619,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/keyshare/keyshareserver/server.go b/server/keyshare/keyshareserver/server.go
index 3ece4252d..bc191732d 100644
--- a/server/keyshare/keyshareserver/server.go
+++ b/server/keyshare/keyshareserver/server.go
@@ -121,6 +121,16 @@ 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)
+ r.Post("/prove/getResponse", s.handleResponseV2)
+ r.Post("/prove/getResponseLinkable", s.handleResponseV2Linkable)
+ })
})
// IRMA server for issuing myirma credential during registration
@@ -182,6 +192,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(keysharecore.UserSecrets(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
@@ -196,7 +256,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
}
@@ -240,6 +300,69 @@ 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)
+ 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(keysharecore.UserSecrets(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
+ 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
@@ -302,6 +425,83 @@ func (s *Server) generateResponse(ctx context.Context, user *User, authorization
return proofResponse, nil
}
+// /api/v2/prove/getResponse
+func (s *Server) handleResponseV2(w http.ResponseWriter, r *http.Request) {
+ s.keyshareResponse(r.Context(), w, r, false)
+}
+
+func (s *Server) keyshareResponse(ctx context.Context, 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(ctx, 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(ctx context.Context, 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(ctx, 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(ctx, 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(keysharecore.UserSecrets(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(r.Context(), 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
new file mode 100644
index 000000000..216d24900
--- /dev/null
+++ b/server/keyshare/keyshareserver/server_v2_test.go
@@ -0,0 +1,292 @@
+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) {
+ 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,
+ )
+ 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) {
+ 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,
+ )
+
+ 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) {
+ 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}} {
+ // 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{
+ "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'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":`+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
+}
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
}