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 }