Commit 0bdde251 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Adding keyshare protocol

parent 73acf247
......@@ -127,6 +127,10 @@ func (sm *SchemeManager) Identifier() SchemeManagerIdentifier {
return NewSchemeManagerIdentifier(sm.ID)
}
func (sm *SchemeManager) Distributed() bool {
return len(sm.KeyshareServer) > 0
}
// CurrentPublicKey returns the latest known public key of the issuer identified by this instance.
func (id *Issuer) CurrentPublicKey() *gabi.PublicKey {
keys := MetaStore.PublicKeys[id.Identifier()]
......
......@@ -4,13 +4,36 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"strconv"
"math/big"
"github.com/mhe/gabi"
)
type KeyshareSessionHandler interface {
AskPin(remainingAttempts int, callback func(pin string))
KeyshareDone(message interface{})
KeyshareBlocked(duration int)
KeyshareError(err error)
}
type keyshareSession struct {
session Session
builders []gabi.ProofBuilder
transports map[SchemeManagerIdentifier]*HTTPTransport
sessionHandler KeyshareSessionHandler
keyshareServer *keyshareServer
}
type keyshareServer struct {
URL string `json:"url"`
Username string `json:"username"`
Nonce []byte `json:"nonce"`
PrivateKey *paillierPrivateKey `json:"keyPair"`
token string
}
type keyshareRegistration struct {
......@@ -19,10 +42,56 @@ type keyshareRegistration struct {
PublicKey *paillierPublicKey `json:"publicKey"`
}
type keyshareAuthorization struct {
Status string `json:"status"`
Candidates []string `json:"candidates"`
}
type keysharePinMessage struct {
Username string `json:"id"`
Pin string `json:"pin"`
}
type keysharePinStatus struct {
Status string `json:"status"`
Message string `json:"message"`
}
type publicKeyIdentifier struct {
Issuer string `json:"issuer"`
Counter uint `json:"counter"`
}
// TODO update protocol so this can go away
func (pki *publicKeyIdentifier) MarshalJSON() ([]byte, error) {
temp := struct {
Issuer map[string]string `json:"issuer"`
Counter uint `json:"counter"`
}{
Issuer: map[string]string{"identifier": pki.Issuer},
Counter: pki.Counter,
}
return json.Marshal(temp)
}
type proofPCommitmentMap struct {
Commitments map[publicKeyIdentifier]*gabi.ProofPCommitment `json:"c"`
}
type KeyshareHandler interface {
StartKeyshareRegistration(manager *SchemeManager, registrationCallback func(email, pin string))
}
const (
kssUsernameHeader = "IRMA_Username"
kssAuthHeader = "IRMA_Authorization"
kssAuthorized = "authorized"
kssTokenExpired = "expired"
kssPinSuccess = "success"
kssPinFailure = "failure"
kssPinError = "error"
)
func newKeyshareServer(privatekey *paillierPrivateKey, url, email string) (ks *keyshareServer, err error) {
ks = &keyshareServer{
Nonce: make([]byte, 32),
......@@ -38,3 +107,318 @@ func (ks *keyshareServer) HashedPin(pin string) string {
hash := sha256.Sum256(append(ks.Nonce, []byte(pin)...))
return base64.RawStdEncoding.EncodeToString(hash[:])
}
func StartKeyshareSession(
session Session,
builders []gabi.ProofBuilder,
sessionHandler KeyshareSessionHandler,
) {
ksscount := 0
for _, managerId := range session.SchemeManagers() {
if MetaStore.SchemeManagers[managerId].Distributed() {
ksscount++
if _, registered := Manager.keyshareServers[managerId]; !registered {
err := errors.New("Not registered to keyshare server of scheme manager " + managerId.String())
sessionHandler.KeyshareError(err)
return
}
}
}
if _, issuing := session.(*IssuanceRequest); issuing && ksscount > 1 {
err := errors.New("Issuance session involving more than one keyshare servers are not supported")
sessionHandler.KeyshareError(err)
return
}
ks := &keyshareSession{
session: session,
builders: builders,
sessionHandler: sessionHandler,
transports: map[SchemeManagerIdentifier]*HTTPTransport{},
}
askPin := false
for _, managerId := range session.SchemeManagers() {
if !MetaStore.SchemeManagers[managerId].Distributed() {
continue
}
ks.keyshareServer = Manager.keyshareServers[managerId]
transport := NewHTTPTransport(ks.keyshareServer.URL)
transport.SetHeader(kssUsernameHeader, ks.keyshareServer.Username)
transport.SetHeader(kssAuthHeader, ks.keyshareServer.token)
ks.transports[managerId] = transport
authstatus := &keyshareAuthorization{}
err := transport.Post("users/isAuthorized", authstatus, "")
if err != nil {
ks.sessionHandler.KeyshareError(err)
return
}
switch authstatus.Status {
case kssAuthorized: // nop
case kssTokenExpired:
askPin = true
default:
ks.sessionHandler.KeyshareError(errors.New("Keyshare server returned unrecognized authirization status"))
return
}
}
if askPin {
ks.VerifyPin(-1)
}
}
// 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) {
ks.sessionHandler.AskPin(attempts, func(pin string) {
success, attemptsRemaining, blocked, err := ks.verifyPinAttempt(pin)
if err != nil {
ks.sessionHandler.KeyshareError(err)
return
}
if blocked != 0 {
ks.sessionHandler.KeyshareBlocked(blocked)
return
}
if success {
ks.GetCommitments()
return
}
// Not successful but no error and not yet blocked: try again
ks.VerifyPin(attemptsRemaining)
})
}
// Verify the specified pin at each of the keyshare servers involved in the specified session.
//
// - If the pin did not verify at one of the keyshare servers but there are attempts remaining,
// the amount of remaining attempts is returned as the second return value.
//
// - If the pin did not verify at one of the keyshare servers and there are no attempts remaining,
// the amount of time for which we are blocked at the keyshare server is returned as the third
// parameter.
//
// - If this or anything else (specified in err) goes wrong, success will be false.
// If all is ok, success will be true.
func (ks *keyshareSession) verifyPinAttempt(pin string) (success bool, tries int, blocked int, err error) {
for _, managerId := range ks.session.SchemeManagers() {
if !MetaStore.SchemeManagers[managerId].Distributed() {
continue
}
kss := Manager.keyshareServers[managerId]
transport := ks.transports[managerId]
pinmsg := keysharePinMessage{Username: kss.Username, Pin: kss.HashedPin(pin)}
pinresult := &keysharePinStatus{}
err = transport.Post("users/verify/pin", pinresult, pinmsg)
if err != nil {
return
}
switch pinresult.Status {
case kssPinSuccess:
kss.token = pinresult.Message
transport.SetHeader(kssAuthHeader, kss.token)
case kssPinFailure:
tries, err = strconv.Atoi(pinresult.Message)
if err != nil {
return
}
return
case kssPinError:
blocked, err = strconv.Atoi(pinresult.Message)
if err != nil {
return
}
return
default:
err = errors.New("Keyshare server returned unrecognized PIN status")
return
}
}
success = true
return
}
// GetCommitments gets the commitments (first message in Schnorr zero-knowledge protocol)
// 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[SchemeManagerIdentifier][]*publicKeyIdentifier{}
commitments := map[publicKeyIdentifier]*gabi.ProofPCommitment{}
// 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 {
pk := builder.PublicKey()
managerId := NewIssuerIdentifier(pk.Issuer).SchemeManagerIdentifier()
if !MetaStore.SchemeManagers[managerId].Distributed() {
continue
}
if _, contains := pkids[managerId]; !contains {
pkids[managerId] = []*publicKeyIdentifier{}
}
pkids[managerId] = append(pkids[managerId], &publicKeyIdentifier{Issuer: pk.Issuer, Counter: pk.Counter})
}
// Now inform each keyshare server of with respect to which public keys
// we want them to send us commitments
for _, managerId := range ks.session.SchemeManagers() {
if !MetaStore.SchemeManagers[managerId].Distributed() {
continue
}
transport := ks.transports[managerId]
comms := &proofPCommitmentMap{}
err := transport.Post("prove/getCommitments", comms, pkids[managerId])
if err != nil {
ks.sessionHandler.KeyshareError(err)
return
}
for pki, c := range comms.Commitments {
commitments[pki] = c
}
}
// Merge in the commitments
for _, builder := range ks.builders {
pk := builder.PublicKey()
pki := publicKeyIdentifier{Issuer: pk.Issuer, Counter: pk.Counter}
comm, distributed := commitments[pki]
if !distributed {
continue
}
builder.MergeProofPCommitment(comm)
}
ks.GetProofPs()
}
// 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.(*SignatureRequest)
_, issuing := ks.session.(*IssuanceRequest)
challenge := gabi.DistributedChallenge(ks.session.GetContext(), ks.session.GetNonce(), ks.builders, issig)
kssChallenge := challenge
// In disclosure or signature sessions the challenge is Paillier encrypted.
if !issuing {
bytes, err := ks.keyshareServer.PrivateKey.Encrypt(challenge.Bytes())
if err != nil {
ks.sessionHandler.KeyshareError(err)
}
kssChallenge = new(big.Int).SetBytes(bytes)
}
// Post the challenge, obtaining JWT's containing the ProofP's
responses := map[SchemeManagerIdentifier]string{}
for _, managerId := range ks.session.SchemeManagers() {
transport, distributed := ks.transports[managerId]
if !distributed {
continue
}
var jwt string
err := transport.Post("prove/getResponse", &jwt, kssChallenge)
if err != nil {
ks.sessionHandler.KeyshareError(err)
return
}
responses[managerId] = jwt
}
ks.Finish(challenge, responses)
}
// Finish the keyshare protocol: in case of issuance, put the keyshare jwt in the
// IssueCommitmentMessage; in case of disclosure and signing, parse each keyshare jwt,
// merge in the received ProofP's, and finish.
func (ks *keyshareSession) Finish(challenge *big.Int, responses map[SchemeManagerIdentifier]string) {
switch ks.session.(type) {
case *DisclosureRequest:
case *SignatureRequest:
proofPs := make([]*gabi.ProofP, len(ks.builders))
for i, builder := range ks.builders {
// Parse each received JWT
managerId := NewIssuerIdentifier(builder.PublicKey().Issuer).SchemeManagerIdentifier()
if !MetaStore.SchemeManagers[managerId].Distributed() {
continue
}
msg := struct {
ProofP *gabi.ProofP
}{}
_, err := JwtDecode(responses[managerId], msg)
if err != nil {
ks.sessionHandler.KeyshareError(err)
return
}
// Decrypt the responses and populate a slice of ProofP's
proofPs[i] = msg.ProofP
bytes, err := ks.keyshareServer.PrivateKey.Decrypt(proofPs[i].C.Bytes())
if err != nil {
ks.sessionHandler.KeyshareError(err)
return
}
proofPs[i].C = new(big.Int).SetBytes(bytes)
}
// Create merged proofs and finish protocol
list, err := gabi.BuildDistributedProofList(challenge, ks.builders, proofPs)
if err != nil {
ks.sessionHandler.KeyshareError(err)
return
}
ks.sessionHandler.KeyshareDone(list)
case *IssuanceRequest:
// Calculate IssueCommitmentMessage, without merging in any of the received ProofP's:
// instead, include the keyshare server's JWT in the IssueCommitmentMessage for the
// issuance server to verify
message, err := Manager.IssueCommitments(ks.session.(*IssuanceRequest))
if err != nil {
ks.sessionHandler.KeyshareError(err)
return
}
for _, response := range responses {
message.ProofPjwt = response
break
}
ks.sessionHandler.KeyshareDone(message)
}
}
// TODO this message is ugly, should update protocol
func (comms *proofPCommitmentMap) UnmarshalJSON(bytes []byte) error {
comms.Commitments = map[publicKeyIdentifier]*gabi.ProofPCommitment{}
temp := struct {
C [][]*json.RawMessage `json:"c"`
}{}
if err := json.Unmarshal(bytes, &temp); err != nil {
return err
}
for _, raw := range temp.C {
tempPkId := struct {
Issuer struct {
Identifier string `json:"identifier"`
} `json:"issuer"`
Counter uint `json:"counter"`
}{}
comm := gabi.ProofPCommitment{}
if err := json.Unmarshal([]byte(*raw[0]), &tempPkId); err != nil {
return err
}
if err := json.Unmarshal([]byte(*raw[1]), &comm); err != nil {
return err
}
pkid := publicKeyIdentifier{Issuer: tempPkId.Issuer.Identifier, Counter: tempPkId.Counter}
comms.Commitments[pkid] = &comm
}
return nil
}
......@@ -231,9 +231,13 @@ type Session interface {
GetContext() *big.Int
SetContext(*big.Int)
DisjunctionList() AttributeDisjunctionList
DisclosureChoice() *DisclosureChoice
SetDisclosureChoice(choice *DisclosureChoice)
Distributed() bool
SchemeManagers() []SchemeManagerIdentifier
}
func (cm *CredentialManager) proofsBuilders(choice *DisclosureChoice) ([]gabi.ProofBuilder, error) {
func (cm *CredentialManager) ProofBuilders(choice *DisclosureChoice) ([]gabi.ProofBuilder, error) {
todisclose, err := cm.groupCredentials(choice)
if err != nil {
return nil, err
......@@ -252,16 +256,14 @@ func (cm *CredentialManager) proofsBuilders(choice *DisclosureChoice) ([]gabi.Pr
// Proofs computes disclosure proofs containing the attributes specified by choice.
func (cm *CredentialManager) Proofs(choice *DisclosureChoice, request Session, issig bool) (gabi.ProofList, error) {
builders, err := cm.proofsBuilders(choice)
builders, err := cm.ProofBuilders(choice)
if err != nil {
return nil, err
}
return gabi.BuildProofList(request.GetContext(), request.GetNonce(), builders, issig), nil
}
// IssueCommitments computes issuance commitments, along with disclosure proofs
// specified by choice.
func (cm *CredentialManager) IssueCommitments(choice *DisclosureChoice, request *IssuanceRequest) (*gabi.IssueCommitmentMessage, error) {
func (cm *CredentialManager) IssuanceProofBuilders(request *IssuanceRequest) ([]gabi.ProofBuilder, error) {
state, err := newIssuanceState()
if err != nil {
return nil, err
......@@ -276,14 +278,23 @@ func (cm *CredentialManager) IssueCommitments(choice *DisclosureChoice, request
proofBuilders = append(proofBuilders, credBuilder)
}
disclosures, err := cm.proofsBuilders(choice)
disclosures, err := cm.ProofBuilders(request.choice)
if err != nil {
return nil, err
}
proofBuilders = append(disclosures, proofBuilders...)
return proofBuilders, nil
}
// IssueCommitments computes issuance commitments, along with disclosure proofs
// specified by choice.
func (cm *CredentialManager) IssueCommitments(request *IssuanceRequest) (*gabi.IssueCommitmentMessage, error) {
proofBuilders, err := cm.IssuanceProofBuilders(request)
if err != nil {
return nil, err
}
list := gabi.BuildProofList(request.GetContext(), request.GetNonce(), proofBuilders, false)
return &gabi.IssueCommitmentMessage{Proofs: list, Nonce2: state.nonce2}, nil
return &gabi.IssueCommitmentMessage{Proofs: list, Nonce2: request.state.nonce2}, nil
}
// ConstructCredentials constructs and saves new credentials
......@@ -316,13 +327,21 @@ func (cm *CredentialManager) ConstructCredentials(msg []*gabi.IssueSignatureMess
}
// PaillierKey returns a new Paillier key (and generates a new one in a goroutine).
func (cm *CredentialManager) paillierKey() *paillierPrivateKey {
func (cm *CredentialManager) paillierKey(wait bool) *paillierPrivateKey {
retval := cm.paillierKeyCache
ch := make(chan bool)
go func() {
newkey, _ := paillier.GenerateKey(rand.Reader, 2048)
converted := paillierPrivateKey(*newkey)
cm.paillierKeyCache = &converted
if wait && retval == nil {
ch <- true
}
}()
if wait && retval == nil {
<-ch
return cm.paillierKeyCache
}
return retval
}
......@@ -349,7 +368,7 @@ func (cm *CredentialManager) KeyshareEnroll(managerId SchemeManagerIdentifier, e
}
transport := NewHTTPTransport(manager.KeyshareServer)
kss, err := newKeyshareServer(Manager.paillierKey(), manager.URL, email)
kss, err := newKeyshareServer(Manager.paillierKey(true), manager.KeyshareServer, email)
if err != nil {
return err
}
......
package irmago
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
)
// Action encodes the session type of an IRMA session (e.g., disclosing).
type Action string
// ErrorCode are session errors.
type ErrorCode string
// Error is a protocol error.
type Error struct {
Err error
ErrorCode
*ApiError
Info string
Status int
}
// ApiError is an error message returned by the API server on errors.
type ApiError struct {
Status int `json:"status"`
ErrorName string `json:"error"`
Description string `json:"description"`
Message string `json:"message"`
Stacktrace string `json:"stacktrace"`
}
// Actions
const (
ActionDisclosing = Action("disclosing")
ActionSigning = Action("signing")
ActionIssuing = Action("issuing")
ActionUnknown = Action("unknown")
)
// Protocol errors
const (
// Protocol version not supported
ErrorProtocolVersionNotSupported = ErrorCode("versionNotSupported")
// Error in HTTP communication
ErrorTransport = ErrorCode("httpError")
// Invalid client JWT in first IRMA message
ErrorInvalidJWT = ErrorCode("invalidJwt")
// Unkown session type (not disclosing, signing, or issuing)
ErrorUnknownAction = ErrorCode("unknownAction")
// Crypto error during calculation of our response (second IRMA message)
ErrorCrypto = ErrorCode("cryptoResponseError")
// Server rejected our response (second IRMA message)
ErrorRejected = ErrorCode("rejectedByServer")
// (De)serializing of a message failed
ErrorSerialization = ErrorCode("serializationError")
ErrorKeyshare = ErrorCode("keyshare")
ErrorKeyshareBlocked = ErrorCode("keyshareBlocked")
)
func (e *Error) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s", string(e.ErrorCode), e.Err.Error())
}
return string(e.ErrorCode)
}
func JwtDecode(jwt string, body interface{}) (string, error) {
jwtparts := strings.Split(jwt, ".")
if jwtparts == nil || len(jwtparts) < 2 {
return "", errors.New("Not a JWT")
}
headerbytes, err := base64.RawStdEncoding.DecodeString(jwtparts[0])
if err != nil {
return "", err
}
var header struct {
Issuer string `json:"iss"`
}
err = json.Unmarshal([]byte(headerbytes), &header)
if err != nil {
return "", err
}
bodybytes, err := base64.RawStdEncoding.DecodeString(jwtparts[1])
if err != nil {
return "", err
}
return header.Issuer, json.Unmarshal(bodybytes, body)
}
......@@ -6,9 +6,6 @@ import (
"strconv"
"strings"
"encoding/base64"
"encoding/json"
"github.com/credentials/irmago"
"github.com/mhe/gabi"
)
......@@ -28,6 +25,8 @@ type Handler interface {
AskIssuancePermission(request irmago.IssuanceRequest, ServerName string, callback PermissionHandler)
AskVerificationPermission(request irmago.DisclosureRequest, ServerName string, callback PermissionHandler)
AskSignaturePermission(request irmago.SignatureRequest, ServerName string, callback PermissionHandler)
AskPin(remainingAttempts int, callback func(pin string))
}