Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: use new keyshare protocol in irmaclient #327

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
6 changes: 5 additions & 1 deletion identifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,11 @@ func (pki *PublicKeyIdentifier) UnmarshalText(text []byte) error {
}

func (pki *PublicKeyIdentifier) MarshalText() (text []byte, err error) {
return []byte(fmt.Sprintf("%s-%d", pki.Issuer, pki.Counter)), nil
return []byte(pki.String()), nil
}

func (pki *PublicKeyIdentifier) String() string {
return fmt.Sprintf("%s-%d", pki.Issuer, pki.Counter)
}

// MarshalText implements encoding.TextMarshaler.
Expand Down
1 change: 1 addition & 0 deletions internal/sessiontest/helper_dosession_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const (
optionClientWait
optionWait
optionPrePairingClient
optionLinkableKeyshareResponse
optionPolling
optionNoSchemeAssets
)
Expand Down
8 changes: 6 additions & 2 deletions internal/sessiontest/helper_main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,14 @@ func parseExistingStorage(t *testing.T, storage string, options ...option) (*irm
err = client.Configuration.ParseFolder()
require.NoError(t, err)
}

version := extractClientMaxVersion(client)
if opts.enabled(optionPrePairingClient) {
version := extractClientMaxVersion(client)
// set to largest protocol version that dos not support pairing
// Set to largest protocol version that does not support pairing
*version = irma.ProtocolVersion{Major: 2, Minor: 7}
} else if opts.enabled(optionLinkableKeyshareResponse) {
// Set to largest protocol version that uses linkable keyshare responses
*version = irma.ProtocolVersion{Major: 2, Minor: 8}
}

client.SetPreferences(irmaclient.Preferences{DeveloperMode: true})
Expand Down
9 changes: 8 additions & 1 deletion internal/sessiontest/keyshare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,19 @@ func TestMultipleKeyshareServers(t *testing.T) {
client.KeyshareEnroll(test2SchemeID, nil, "12345", "en")
require.NoError(t, <-handler.c)

// A session request that contains attributes from both test and test2 (both distributed schemes) should fail.
request := irma.NewDisclosureRequest(
irma.NewAttributeTypeIdentifier("test.test.mijnirma.email"),
irma.NewAttributeTypeIdentifier("test2.test.mijnirma.email"),
)
doSession(t, request, client, irmaServer, nil, nil, nil)
_, _, _, err = irmaServer.irma.StartSession(request, nil)
require.ErrorIs(t, err, irma.ErrMultipleDistributedSchemes)
ivard marked this conversation as resolved.
Show resolved Hide resolved

// Do a session with a request that contains attributes from test2 only.
request = irma.NewDisclosureRequest(
irma.NewAttributeTypeIdentifier("test2.test.mijnirma.email"),
)
doSession(t, request, client, irmaServer, nil, nil, nil)
logs, err = client.LoadNewestLogs(20)
require.NoError(t, err)
require.Len(t, logs, logsAmount+2)
Expand Down
11 changes: 11 additions & 0 deletions internal/sessiontest/legacy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test"
"github.com/privacybydesign/irmago/internal/testkeyshare"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -68,3 +69,13 @@ func TestWithoutPairingSupport(t *testing.T) {

t.Run("StaticQRSession", apply(testStaticQRSession, nil, optionPrePairingClient))
}

func TestLinkableKeyshareResponse(t *testing.T) {
keyshareServer := testkeyshare.StartKeyshareServer(t, logger, irma.NewSchemeManagerIdentifier("test"))
defer keyshareServer.Stop()
client, handler := parseStorage(t, optionLinkableKeyshareResponse)
defer test.ClearTestStorage(t, client, handler.storage)
irmaServer := StartIrmaServer(t, nil)
defer irmaServer.Stop()
keyshareSessions(t, client, irmaServer)
}
ivard marked this conversation as resolved.
Show resolved Hide resolved
25 changes: 8 additions & 17 deletions irmaclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/go-errors/errors"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/gabikeys"
"github.com/privacybydesign/gabi/revocation"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/common"
Expand Down Expand Up @@ -963,7 +962,7 @@ func (client *Client) IssuanceProofBuilders(
}
builders := gabi.ProofBuilderList([]gabi.ProofBuilder{})

var keysharePs = map[irma.SchemeManagerIdentifier]*irma.PMap{}
var keysharePs = map[irma.PublicKeyIdentifier]*big.Int{}
if keyshareSession != nil {
keysharePs, err = keyshareSession.getKeysharePs(request)
if err != nil {
Expand All @@ -972,26 +971,14 @@ func (client *Client) IssuanceProofBuilders(
}

for _, futurecred := range request.Credentials {
var pk *gabikeys.PublicKey
keyID := futurecred.PublicKeyIdentifier()
schemeID := keyID.Issuer.SchemeManagerIdentifier()
distributed := client.Configuration.SchemeManagers[schemeID].Distributed()
var keyshareP *big.Int
var present bool
if distributed {
keyshareP, present = keysharePs[schemeID].Ps[keyID]
if distributed && !present {
return nil, nil, nil, errors.Errorf("missing keyshareP for %s-%d", keyID.Issuer, keyID.Counter)
}
}

pk, err = client.Configuration.PublicKey(futurecred.CredentialTypeID.IssuerIdentifier(), futurecred.KeyCounter)
pk, err := client.Configuration.PublicKey(futurecred.CredentialTypeID.IssuerIdentifier(), futurecred.KeyCounter)
if err != nil {
return nil, nil, nil, err
}
credtype := client.Configuration.CredentialTypes[futurecred.CredentialTypeID]
credBuilder, err := gabi.NewCredentialBuilder(pk, request.GetContext(),
client.secretkey.Key, issuerProofNonce, keyshareP, credtype.RandomBlindAttributeIndices())
client.secretkey.Key, issuerProofNonce, keysharePs[keyID], credtype.RandomBlindAttributeIndices())
if err != nil {
return nil, nil, nil, err
}
Expand Down Expand Up @@ -1175,7 +1162,7 @@ func (client *Client) keyshareEnrollWorker(managerID irma.SchemeManagerIdentifie

transport := irma.NewHTTPTransport(manager.KeyshareServer, !client.Preferences.DeveloperMode)
qr := &irma.Qr{}
err = transport.Post("client/register", qr, irma.KeyshareEnrollment{EnrollmentJWT: jwtt})
err = transport.Post("api/v1/client/register", qr, irma.KeyshareEnrollment{EnrollmentJWT: jwtt})
if err != nil {
return err
}
Expand Down Expand Up @@ -1385,6 +1372,10 @@ func (client *Client) keyshareRemoveMultiple(schemeIDs []irma.SchemeManagerIdent
if err != nil {
return err
}
err = client.storage.TxDeleteKeyshareCachedPs(tx)
if err != nil {
return err
}
}
}

Expand Down
130 changes: 88 additions & 42 deletions irmaclient/keyshare.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (
"github.com/golang-jwt/jwt/v4"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/gabikeys"
irma "github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/common"
)

// This file contains an implementation of the client side of the keyshare protocol,
Expand Down Expand Up @@ -256,7 +258,7 @@ func (kss *keyshareServer) doChallengeResponse(signer Signer, transport *irma.HT
}

auth := &irma.KeyshareAuthChallenge{}
err = transport.Post("users/verify_start", auth, irma.KeyshareAuthRequest{AuthRequestJWT: jwtt})
err = transport.Post("api/v1/users/verify_start", auth, irma.KeyshareAuthRequest{AuthRequestJWT: jwtt})
if err != nil {
return nil, err
}
Expand All @@ -283,7 +285,7 @@ func (kss *keyshareServer) doChallengeResponse(signer Signer, transport *irma.HT
}

pinresult := &irma.KeysharePinStatus{}
err = transport.Post("users/verify/pin_challengeresponse", pinresult, irma.KeyshareAuthResponse{AuthResponseJWT: jwtt})
err = transport.Post("api/v1/users/verify/pin_challengeresponse", pinresult, irma.KeyshareAuthResponse{AuthResponseJWT: jwtt})
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -359,34 +361,50 @@ func (ks *keyshareSession) verifyPinAttempt(pin string) (
// of all keyshare servers of their part of the private key, and merges these commitments
// in our own proof builders.
func (ks *keyshareSession) GetCommitments() {
pkids := map[irma.SchemeManagerIdentifier][]*irma.PublicKeyIdentifier{}
commitments := map[irma.PublicKeyIdentifier]*gabi.ProofPCommitment{}
pkidsBuilders := make([]irma.PublicKeyIdentifier, len(ks.builders))
pkidsKeyshare := map[irma.SchemeManagerIdentifier][]irma.PublicKeyIdentifier{}
pksKeyshare := map[irma.PublicKeyIdentifier]*gabikeys.PublicKey{}

// For each scheme manager, build a list of public keys under this manager
// that we will use in the keyshare protocol with the keyshare server of this manager
for _, builder := range ks.builders {
for i, builder := range ks.builders {
pk := builder.PublicKey()
pkid := irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier(pk.Issuer), Counter: pk.Counter}
pkidsBuilders[i] = pkid

managerID := irma.NewIssuerIdentifier(pk.Issuer).SchemeManagerIdentifier()
if !ks.client.Configuration.SchemeManagers[managerID].Distributed() {
continue
}
if _, contains := pkids[managerID]; !contains {
pkids[managerID] = []*irma.PublicKeyIdentifier{}
if ks.client.Configuration.SchemeManagers[managerID].Distributed() {
pksKeyshare[pkid] = pk
pkidsKeyshare[managerID] = append(pkidsKeyshare[managerID], pkid)
}
pkids[managerID] = append(pkids[managerID], &irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier(pk.Issuer), Counter: pk.Counter})
}

// TODO: this code is copied from gabi, because there was no way to call the gabi code. Check how we can resolve this.
ivard marked this conversation as resolved.
Show resolved Hide resolved
// The secret key may be used across credentials supporting different attribute sizes.
// So we should take it, and hence also its commitment, to fit within the smallest size -
// otherwise it will be too big so that we cannot perform the range proof showing
// that it is not too big.
skRandomizer := common.RandomBigInt(new(big.Int).Lsh(big.NewInt(1), gabikeys.DefaultSystemParameters[1024].LmCommit))
randomizers := map[string]*big.Int{"secretkey": skRandomizer}

// Calculate the user commitments
hash, challengeInput, err := gabi.KeyshareUserCommitmentRequest(ks.builders, randomizers, pksKeyshare)
if err != nil {
ks.fail(irma.NewSchemeManagerIdentifier(""), irma.WrapErrorPrefix(err, "keyshare user commitment could not be calculated"))
return
}

// Now inform each keyshare server of with respect to which public keys
// we want them to send us commitments
for managerID := range ks.schemeIDs {
if !ks.client.Configuration.SchemeManagers[managerID].Distributed() {
continue
commitments := map[irma.PublicKeyIdentifier]*gabi.ProofPCommitment{}
for managerID, pkids := range pkidsKeyshare {
req := irma.GetCommitmentsRequest{
Keys: pkids,
Hash: hash,
}

transport := ks.transports[managerID]
comms := &irma.ProofPCommitmentMap{}
err := transport.Post("prove/getCommitments", comms, pkids[managerID])
if err != nil {
comms := &irma.ProofPCommitmentMapV2{}
if err := ks.transports[managerID].Post("api/v2/prove/getCommitments", comms, req); err != nil {
if err.(*irma.SessionError).RemoteError != nil &&
err.(*irma.SessionError).RemoteError.Status == http.StatusForbidden && !ks.pinCheck {
// JWT may be out of date due to clock drift; request pin and try again
Expand All @@ -402,31 +420,29 @@ func (ks *keyshareSession) GetCommitments() {
ks.sessionHandler.KeyshareError(&managerID, err)
return
}
for pki, c := range comms.Commitments {
commitments[pki] = c
for pkid, c := range comms.Commitments {
commitments[pkid] = &gabi.ProofPCommitment{Pcommit: c}
}
}

// Merge in the commitments
for _, builder := range ks.builders {
pk := builder.PublicKey()
pki := irma.PublicKeyIdentifier{Issuer: irma.NewIssuerIdentifier(pk.Issuer), Counter: pk.Counter}
comm, distributed := commitments[pki]
if !distributed {
continue
for i, pkid := range pkidsBuilders {
if comm, ok := commitments[pkid]; ok {
ks.builders[i].SetProofPCommitment(comm)
}
builder.SetProofPCommitment(comm)
}

ks.GetProofPs()
ks.GetProofPs(randomizers, challengeInput)
}

// GetProofPs uses the combined commitments of all keyshare servers and ourself
// to calculate the challenge, which is sent to the keyshare servers in order to
// receive their responses (2nd and 3rd message in Schnorr zero-knowledge protocol).
func (ks *keyshareSession) GetProofPs() {
_, issig := ks.session.(*irma.SignatureRequest)
challenge, err := ks.builders.Challenge(ks.session.Base().GetContext(), ks.session.GetNonce(ks.timestamp), issig)
func (ks *keyshareSession) GetProofPs(randomizers map[string]*big.Int, hashInput []gabi.KeyshareUserChallengeInput[irma.PublicKeyIdentifier]) {
_, isSig := ks.session.(*irma.SignatureRequest)
_, isIssuance := ks.session.(*irma.IssuanceRequest)

req, challenge, err := gabi.KeyshareUserResponseRequest(ks.builders, randomizers, hashInput, ks.session.Base().GetContext(), ks.session.GetNonce(ks.timestamp), isSig)
if err != nil {
ks.sessionHandler.KeyshareError(&ks.keyshareServer.SchemeManagerIdentifier, err)
return
Expand All @@ -439,13 +455,25 @@ func (ks *keyshareSession) GetProofPs() {
if !distributed {
continue
}
var j string
err = transport.Post("prove/getResponse", &j, challenge)

// If the protocol version is below 2.9, the P value should be included in the JWT. Legacy issuers need this P value to validate the commitments.
// We obtain the JWT containing the P value using the api/v2/prove/getResponseLinkable endpoint.
// For disclosure and signing sessions, the P value is being merged on our side (the client side).
// This means that in these cases we need to use the api/v2/prove/getResponse endpoint. Otherwise, we would trigger legacy behavior in gabi.
var endpoint string
if ks.protocolVersion.Below(2, 9) && isIssuance {
endpoint = "api/v2/prove/getResponseLinkable"
} else {
endpoint = "api/v2/prove/getResponse"
}

var respJwt string
err = transport.Post(endpoint, &respJwt, req)
if err != nil {
ks.sessionHandler.KeyshareError(&managerID, err)
return
}
responses[managerID] = j
responses[managerID] = respJwt
}

ks.Finish(challenge, responses)
Expand Down Expand Up @@ -495,8 +523,7 @@ func (ks *keyshareSession) finishDisclosureOrSigning(challenge *big.Int, respons
jwt.StandardClaims
ProofP *gabi.ProofP
}{}
parser := new(jwt.Parser)
parser.SkipClaimsValidation = true // no need to abort due to clock drift issues
parser := jwt.NewParser(jwt.WithoutClaimsValidation()) // no need to validate claims due to clock drift issues
if _, err := parser.ParseWithClaims(responses[managerID], &claims, ks.client.Configuration.KeyshareServerKeyFunc(managerID)); err != nil {
ks.sessionHandler.KeyshareError(&managerID, err)
return
Expand All @@ -515,7 +542,7 @@ func (ks *keyshareSession) finishDisclosureOrSigning(challenge *big.Int, respons

// getKeysharePs retrieves all P values (i.e. R_0^{keyshare server secret}) from all keyshare servers,
// for use during issuance.
func (ks *keyshareSession) getKeysharePs(request *irma.IssuanceRequest) (map[irma.SchemeManagerIdentifier]*irma.PMap, error) {
func (ks *keyshareSession) getKeysharePs(request *irma.IssuanceRequest) (map[irma.PublicKeyIdentifier]*big.Int, error) {
// Assemble keys of which to retrieve P's, grouped per keyshare server
distributedKeys := map[irma.SchemeManagerIdentifier][]irma.PublicKeyIdentifier{}
for _, futurecred := range request.Credentials {
Expand All @@ -525,13 +552,32 @@ func (ks *keyshareSession) getKeysharePs(request *irma.IssuanceRequest) (map[irm
}
}

keysharePs := map[irma.SchemeManagerIdentifier]*irma.PMap{}
for schemeID, keys := range distributedKeys {
Ps := irma.PMap{Ps: map[irma.PublicKeyIdentifier]*big.Int{}}
if err := ks.transports[schemeID].Post("api/v2/prove/getPs", &Ps, keys); err != nil {
// Collect the P values for the public keys we want to get commitments for.
keysharePs := map[irma.PublicKeyIdentifier]*big.Int{}
missingKeysharePs := map[irma.SchemeManagerIdentifier][]irma.PublicKeyIdentifier{}
for _, pkids := range distributedKeys {
for _, pkid := range pkids {
if p, err := ks.client.storage.LoadKeyshareCachedP(pkid); err == nil {
keysharePs[pkid] = p
} else {
managerID := pkid.Issuer.SchemeManagerIdentifier()
missingKeysharePs[managerID] = append(missingKeysharePs[managerID], pkid)
}
}
}

// If we don't have all P values, we ask the keyshare server for the missing ones.
for managerID, pkids := range missingKeysharePs {
var pMap *irma.PMap
if err := ks.transports[managerID].Post("api/v2/prove/getPs", &pMap, pkids); err != nil {
return nil, err
}
keysharePs[schemeID] = &Ps
if err := ks.client.storage.StoreKeyshareCachedPs(pMap.Ps); err != nil {
return nil, err
}
for pkid, p := range pMap.Ps {
keysharePs[pkid] = p
}
}

return keysharePs, nil
Expand Down
2 changes: 1 addition & 1 deletion irmaclient/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ func (kss *keyshareServer) registerPublicKey(client *Client, transport *irma.HTT
}

result := &irma.KeysharePinStatus{}
err = transport.Post("users/register_publickey", result, irma.KeyshareKeyRegistration{PublicKeyRegistrationJWT: jwtt})
err = transport.Post("api/v1/users/register_publickey", result, irma.KeyshareKeyRegistration{PublicKeyRegistrationJWT: jwtt})
if err != nil {
err = irma.WrapErrorPrefix(err, "failed to register public key")
return nil, err
Expand Down
Loading