Commit fb498c65 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

Initial work on attribute verification and issuance server/library


Co-authored-by: Tomas's avatarConfiks <confiks@scriptbase.org>
parent 4f379880
package irmaserver
import (
"github.com/Sirupsen/logrus"
"github.com/mhe/gabi"
"github.com/privacybydesign/irmago"
)
type Configuration struct {
IrmaConfigurationPath string
PrivateKeys map[irma.IssuerIdentifier]*gabi.PrivateKey
IrmaConfiguration *irma.Configuration
Logger *logrus.Logger
}
type SessionResult struct {
Token string
Status irma.ProofStatus
Disclosed []*irma.DisclosedAttribute
Signature *irma.SignedMessage
Err *irma.RemoteError
}
type Status string
const (
StatusInitialized Status = "INITIALIZED"
StatusConnected Status = "CONNECTED"
StatusCancelled Status = "CANCELLED"
StatusDone Status = "DONE"
)
package backend
import (
"encoding/json"
"net/http"
"regexp"
"github.com/Sirupsen/logrus"
"github.com/go-errors/errors"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/irmaserver"
)
func Initialize(configuration *irmaserver.Configuration) error {
conf = configuration
if conf.Logger == nil {
conf.Logger = logrus.New()
conf.Logger.Level = logrus.DebugLevel
conf.Logger.Formatter = &logrus.TextFormatter{}
}
if conf.IrmaConfiguration == nil {
var err error
conf.IrmaConfiguration, err = irma.NewConfiguration(conf.IrmaConfigurationPath, "")
if err != nil {
return err
}
if err = conf.IrmaConfiguration.ParseFolder(); err != nil {
return err
}
}
return nil
}
func StartSession(request irma.SessionRequest) (*irma.Qr, string, error) {
if err := request.Validate(); err != nil {
return nil, "", err
}
action := irma.ActionUnknown
switch request.(type) {
case *irma.DisclosureRequest:
action = irma.ActionDisclosing
case *irma.SignatureRequest:
action = irma.ActionSigning
case *irma.IssuanceRequest:
action = irma.ActionIssuing
if err := validateIssuanceRequest(request.(*irma.IssuanceRequest)); err != nil {
return nil, "", err
}
default:
conf.Logger.Warnf("Attempt to start session of invalid type")
return nil, "", errors.New("Invalid session type")
}
session := newSession(action, request)
conf.Logger.Infof("%s session started, token %s", action, session.token)
return &irma.Qr{
Type: action,
URL: session.token,
}, session.token, nil
}
func HandleProtocolMessage(
path string,
method string,
headers map[string][]string,
message []byte,
) (status int, output []byte, result *irmaserver.SessionResult) {
// Parse path into session and action
if len(path) > 0 { // Remove any starting and trailing slash
if path[0] == '/' {
path = path[1:]
}
if path[len(path)-1] == '/' {
path = path[:len(path)-1]
}
}
conf.Logger.Debugf("Routing protocol message: %s %s", method, path)
pattern := regexp.MustCompile("(\\w+)/?(\\w*)")
matches := pattern.FindStringSubmatch(path)
if len(matches) != 3 {
conf.Logger.Warnf("Invalid URL: %s", path)
return failSession(nil, irmaserver.ErrorInvalidRequest, "")
}
token := matches[1]
verb := matches[2]
session := sessions.get(token)
if session == nil {
conf.Logger.Warnf("Session not found: %s", token)
return failSession(nil, irmaserver.ErrorSessionUnknown, "")
}
// Route to handler
switch len(verb) {
case 0:
if method == "DELETE" {
return handleDelete(session)
}
if method == "GET" {
h := http.Header(headers)
min := &irma.ProtocolVersion{}
max := &irma.ProtocolVersion{}
if err := json.Unmarshal([]byte(h.Get(irma.MinVersionHeader)), min); err != nil {
return failSession(session, irmaserver.ErrorMalformedInput, err.Error())
}
if err := json.Unmarshal([]byte(h.Get(irma.MaxVersionHeader)), max); err != nil {
return failSession(session, irmaserver.ErrorMalformedInput, err.Error())
}
return handleGetSession(session, min, max)
}
return failSession(session, irmaserver.ErrorInvalidRequest, "")
default:
if method == "POST" && (verb == "proofs" || (verb == "commitments" && session.action == irma.ActionIssuing)) {
return handlePostResponse(session, message)
}
if method == "GET" && verb == "status" {
return handleGetStatus(session)
}
return failSession(session, irmaserver.ErrorInvalidRequest, "")
}
}
package backend
import (
"encoding/json"
"net/http"
"runtime/debug"
"github.com/mhe/gabi"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/irmaserver"
)
var conf *irmaserver.Configuration
func handleDelete(session *session) (int, []byte, *irmaserver.SessionResult) {
var res *irmaserver.SessionResult
if session.alive() {
res = &irmaserver.SessionResult{Token: session.token} // TODO what to return here?
}
session.status = irmaserver.StatusCancelled
return http.StatusOK, nil, res
}
func handleGetSession(session *session, min, max *irma.ProtocolVersion) (int, []byte, *irmaserver.SessionResult) {
var err error
session.status = irmaserver.StatusConnected
if session.version, err = chooseProtocolVersion(min, max); err != nil {
return failSession(session, irmaserver.ErrorProtocolVersion, "")
}
session.request.SetVersion(session.version)
s, b := responseJson(session.request)
return s, b, nil
}
func handleGetStatus(session *session) (int, []byte, *irmaserver.SessionResult) {
b, _ := json.Marshal(session.status)
return http.StatusOK, b, nil
}
func handlePostResponse(session *session, message []byte) (int, []byte, *irmaserver.SessionResult) {
if !session.alive() {
return failSession(session, irmaserver.ErrorUnexpectedRequest, "")
}
switch session.action {
case irma.ActionSigning:
sig := &irma.SignedMessage{}
if err := irma.UnmarshalValidate(message, sig); err != nil {
return failSession(session, irmaserver.ErrorMalformedInput, "")
}
session.signature = sig
session.disclosed, session.proofStatus = sig.Verify(conf.IrmaConfiguration, session.request.(*irma.SignatureRequest))
s, b := responseJson(session.proofStatus)
return s, b, finishSession(session)
case irma.ActionDisclosing:
pl := gabi.ProofList{}
if err := irma.UnmarshalValidate(message, &pl); err != nil {
return failSession(session, irmaserver.ErrorMalformedInput, "")
}
session.disclosed, session.proofStatus = irma.ProofList(pl).Verify(conf.IrmaConfiguration, session.request.(*irma.DisclosureRequest))
s, b := responseJson(session.proofStatus)
return s, b, finishSession(session)
case irma.ActionIssuing:
commitments := &gabi.IssueCommitmentMessage{}
if err := irma.UnmarshalValidate(message, commitments); err != nil {
return failSession(session, irmaserver.ErrorMalformedInput, "")
}
return session.issue(commitments)
}
return failSession(session, irmaserver.ErrorUnknown, "")
}
func responseJson(v interface{}) (int, []byte) {
b, err := json.Marshal(v)
if err != nil {
return http.StatusInternalServerError, nil // TODO
}
return http.StatusOK, b
}
func (session *session) alive() bool {
return session.status != irmaserver.StatusDone && session.status != irmaserver.StatusCancelled
}
func finishSession(session *session) *irmaserver.SessionResult {
session.status = irmaserver.StatusDone
return &irmaserver.SessionResult{
Token: session.token,
Status: session.proofStatus,
Disclosed: session.disclosed,
Signature: session.signature,
}
}
func failSession(session *session, err irmaserver.Error, message string) (int, []byte, *irmaserver.SessionResult) {
rerr := &irma.RemoteError{
Status: err.Status,
Description: err.Description,
ErrorName: string(err.Type),
Message: message,
Stacktrace: string(debug.Stack()),
}
conf.Logger.Errorf("Error: %d %s %s\n%s", rerr.Status, rerr.ErrorName, rerr.Message, rerr.Stacktrace)
var res *irmaserver.SessionResult
if session != nil {
if session.alive() {
res = &irmaserver.SessionResult{Err: rerr, Token: session.token}
}
session.status = irmaserver.StatusCancelled
}
b, _ := json.Marshal(rerr)
return err.Status, b, res
}
package backend
import (
"fmt"
"strconv"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-errors/errors"
"github.com/mhe/gabi"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/irmaserver"
)
func validateIssuanceRequest(request *irma.IssuanceRequest) error {
for _, cred := range request.Credentials {
// Check that we have the appropriate private key
iss := cred.CredentialTypeID.IssuerIdentifier()
privatekey, havekey := conf.PrivateKeys[iss]
if !havekey {
return fmt.Errorf("missing private key of issuer %s", iss.String())
}
pubkey, err := conf.IrmaConfiguration.PublicKey(iss, int(privatekey.Counter))
if err != nil {
return err
}
if pubkey == nil {
return fmt.Errorf("missing public key of issuer %s", iss.String())
}
cred.KeyCounter = int(privatekey.Counter)
// Check that the credential is consistent with irma_configuration
if err := cred.Validate(conf.IrmaConfiguration); err != nil {
return err
}
// Ensure the credential has an expiry date
defaultValidity := irma.Timestamp(time.Now().Add(6 * time.Hour))
if cred.Validity == nil {
cred.Validity = &defaultValidity
}
if cred.Validity.Before(irma.Timestamp(time.Now())) {
return errors.New("cannot issue expired credentials")
}
}
return nil
}
func (session *session) getProofP(commitments *gabi.IssueCommitmentMessage, scheme irma.SchemeManagerIdentifier) (*gabi.ProofP, error) {
if session.kssProofs == nil {
session.kssProofs = make(map[irma.SchemeManagerIdentifier]*gabi.ProofP)
}
if _, contains := session.kssProofs[scheme]; !contains {
str, contains := commitments.ProofPjwts[scheme.Name()]
if !contains {
return nil, errors.Errorf("no keyshare proof included for scheme %s", scheme.Name())
}
claims := &struct {
jwt.StandardClaims
ProofP *gabi.ProofP
}{}
token, err := jwt.ParseWithClaims(str, claims, func(t *jwt.Token) (interface{}, error) {
var kid int
if kidstr, ok := t.Header["kid"].(string); ok {
var err error
if kid, err = strconv.Atoi(kidstr); err != nil {
return nil, err
}
}
return conf.IrmaConfiguration.KeyshareServerPublicKey(scheme, kid)
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.Errorf("invalid keyshare proof included for scheme %s", scheme.Name())
}
session.kssProofs[scheme] = claims.ProofP
}
return session.kssProofs[scheme], nil
}
func (session *session) issue(commitments *gabi.IssueCommitmentMessage) (int, []byte, *irmaserver.SessionResult) {
request := session.request.(*irma.IssuanceRequest)
discloseCount := len(request.Disclose)
if len(commitments.Proofs) != len(request.Credentials)+discloseCount {
return failSession(session, irmaserver.ErrorAttributesMissing, "")
}
// Compute list of public keys against which to verify the received proofs
disclosureproofs := irma.ProofList(commitments.Proofs[:discloseCount])
pubkeys, err := disclosureproofs.ExtractPublicKeys(conf.IrmaConfiguration)
if err != nil {
return failSession(session, irmaserver.ErrorInvalidProofs, err.Error())
}
for _, cred := range request.Credentials {
iss := cred.CredentialTypeID.IssuerIdentifier()
pubkey, _ := conf.IrmaConfiguration.PublicKey(iss, cred.KeyCounter) // No error, already checked earlier
pubkeys = append(pubkeys, pubkey)
}
// Verify and merge keyshare server proofs, if any
for i, proof := range commitments.Proofs {
pubkey := pubkeys[i]
schemeid := irma.NewIssuerIdentifier(pubkey.Issuer).SchemeManagerIdentifier()
if conf.IrmaConfiguration.SchemeManagers[schemeid].Distributed() {
proofP, err := session.getProofP(commitments, schemeid)
if err != nil {
failSession(session, irmaserver.ErrorKeyshareProofMissing, err.Error())
}
proof.MergeProofP(proofP, pubkey)
}
}
// Verify all proofs and check disclosed attributes, if any, against request
session.disclosed, session.proofStatus = irma.ProofList(commitments.Proofs).VerifyAgainstDisjunctions(
conf.IrmaConfiguration, request.Disclose, request.Context, request.Nonce, pubkeys, false)
if session.proofStatus != irma.ProofStatusValid {
return failSession(session, irmaserver.ErrorInvalidProofs, "")
}
// Compute CL signatures
var sigs []*gabi.IssueSignatureMessage
for i, cred := range request.Credentials {
id := cred.CredentialTypeID.IssuerIdentifier()
pk, _ := conf.IrmaConfiguration.PublicKey(id, cred.KeyCounter)
issuer := gabi.NewIssuer(conf.PrivateKeys[id], pk, one)
proof := commitments.Proofs[i+discloseCount].(*gabi.ProofU)
attributes, err := cred.AttributeList(conf.IrmaConfiguration, 0x03)
if err != nil {
return failSession(session, irmaserver.ErrorUnknown, err.Error())
}
sig, err := issuer.IssueSignature(proof.U, attributes.Ints, commitments.Nonce2)
if err != nil {
return failSession(session, irmaserver.ErrorUnknown, err.Error())
}
sigs = append(sigs, sig)
}
s, b := responseJson(sigs)
return s, b, finishSession(session)
}
package backend
import (
"math/big"
"math/rand"
"sync"
"github.com/go-errors/errors"
"github.com/mhe/gabi"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/irmaserver"
)
type session struct {
action irma.Action
token string
version *irma.ProtocolVersion
request irma.SessionRequest
status irmaserver.Status
proofStatus irma.ProofStatus
disclosed []*irma.DisclosedAttribute
signature *irma.SignedMessage
kssProofs map[irma.SchemeManagerIdentifier]*gabi.ProofP
}
type sessionStore interface {
get(token string) *session
add(token string, session *session)
}
type memorySessionStore struct {
sync.RWMutex
m map[string]*session
}
const sessionChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var (
minProtocolVersion = irma.NewVersion(2, 4)
maxProtocolVersion = irma.NewVersion(2, 4)
sessions sessionStore = &memorySessionStore{
m: make(map[string]*session),
}
)
func (s *memorySessionStore) get(token string) *session {
s.RLock()
defer s.RUnlock()
return s.m[token]
}
func (s *memorySessionStore) add(token string, session *session) {
s.Lock()
defer s.Unlock()
s.m[token] = session
}
var one *big.Int = big.NewInt(1)
func newSession(action irma.Action, request irma.SessionRequest) *session {
s := &session{
action: action,
request: request,
status: irmaserver.StatusInitialized,
token: newSessionToken(),
}
nonce, _ := gabi.RandomBigInt(gabi.DefaultSystemParameters[2048].Lstatzk)
request.SetNonce(nonce)
request.SetContext(one)
sessions.add(s.token, s)
return s
}
func newSessionToken() string {
b := make([]byte, 20)
for i := range b {
b[i] = sessionChars[rand.Int63()%int64(len(sessionChars))]
}
return string(b)
}
func chooseProtocolVersion(min, max *irma.ProtocolVersion) (*irma.ProtocolVersion, error) {
if min.AboveVersion(minProtocolVersion) || max.BelowVersion(min) {
return nil, errors.Errorf("Protocol version negotiation failed, min=%s max=%s", min.String(), max.String())
}
if max.AboveVersion(maxProtocolVersion) {
return maxProtocolVersion, nil
} else {
return max, nil
}
}
package irmaserver
type Error struct {
Type ErrorType `json:"error"`
Status int `json:"status"`
Description string `json:"description"`
}
type ErrorType string
var (
ErrorMalformedIssuerRequest Error = Error{Type: "MALFORMED_ISSUER_REQUEST", Status: 400, Description: "Malformed issuer request"}
ErrorInvalidTimestamp Error = Error{Type: "INVALID_TIMESTAMP", Status: 400, Description: "Timestamp was not an epoch boundary"}
ErrorIssuingDisabled Error = Error{Type: "ISSUING_DISABLED", Status: 403, Description: "This server does not support issuing"}
ErrorMalformedVerifierRequest Error = Error{Type: "MALFORMED_VERIFIER_REQUEST", Status: 400, Description: "Malformed verification request"}
ErrorMalformedSignatureRequest Error = Error{Type: "MALFORMED_SIGNATURE_REQUEST", Status: 400, Description: "Malformed signature request"}
ErrorInvalidProofs Error = Error{Type: "INVALID_PROOFS", Status: 400, Description: "Invalid secret key commitments and/or disclosure proofs"}
ErrorAttributesMissing Error = Error{Type: "ATTRIBUTES_MISSING", Status: 400, Description: "Not all requested-for attributes were present"}
ErrorAttributesExpired Error = Error{Type: "ATTRIBUTES_EXPIRED", Status: 400, Description: "Disclosed attributes were expired"}
ErrorUnexpectedRequest Error = Error{Type: "UNEXPECTED_REQUEST", Status: 403, Description: "Unexpected request in this state"}
ErrorUnknownPublicKey Error = Error{Type: "UNKNOWN_PUBLIC_KEY", Status: 403, Description: "Attributes were not valid against a known public key"}
ErrorKeyshareProofMissing Error = Error{Type: "KEYSHARE_PROOF_MISSING", Status: 403, Description: "ProofP object from a keyshare server missing"}
ErrorUnauthorized Error = Error{Type: "UNAUTHORIZED", Status: 403, Description: "You are not authorized to issue or verify this attribute"}
ErrorAttributesWrong Error = Error{Type: "ATTRIBUTES_WRONG", Status: 400, Description: "Specified attribute(s) do not belong to this credential type or missing attributes"}
ErrorSessionUnknown Error = Error{Type: "SESSION_UNKNOWN", Status: 400, Description: "Unknown or expired session"}
ErrorSessionTokenMalformed Error = Error{Type: "SESSION_TOKEN_MALFORMED", Status: 400, Description: "Malformed session token"}
ErrorMalformedInput Error = Error{Type: "MALFORMED_INPUT", Status: 400, Description: "Input could not be parsed"}
ErrorCannotIssue Error = Error{Type: "CANNOT_ISSUE", Status: 500, Description: "Cannot issue this credential"}
ErrorIssuanceFailed Error = Error{Type: "ISSUANCE_FAILED", Status: 500, Description: "Failed to create credential(s)"}
ErrorUnknown Error = Error{Type: "EXCEPTION", Status: 500, Description: "Encountered unexpected problem"}
ErrorInvalidRequest Error = Error{Type: "INVALID_REQUEST", Status: 400, Description: "Invalid HTTP request"}
ErrorProtocolVersion Error = Error{Type: "PROTOCOL_VERSION", Status: 400, Description: "Protocol version negotiation failed"}
)
package irmarequestor
import (
"io/ioutil"
"net/http"
"sync"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/irmaserver"
"github.com/privacybydesign/irmago/irmaserver/backend"
)
type SessionHandler func(*irmaserver.SessionResult)
type SessionStore interface {
Get(token string) *irmaserver.SessionResult
Add(token string, result *irmaserver.SessionResult)
GetHandler(token string) SessionHandler
SetHandler(token string, handler SessionHandler)
SupportHandlers() bool
}
var Sessions SessionStore = &MemorySessionStore{
m: make(map[string]*irmaserver.SessionResult),
h: make(map[string]SessionHandler),
}
type MemorySessionStore struct {
sync.RWMutex
m map[string]*irmaserver.SessionResult
h map[string]SessionHandler
}
func Initialize(configuration *irmaserver.Configuration) error {
return backend.Initialize(configuration)
}
func StartSession(request irma.SessionRequest, handler SessionHandler) (*irma.Qr, string, error) {
if handler != nil && !Sessions.SupportHandlers() {
panic("Handlers not supported")
}
qr, token, err := backend.StartSession(request)
if err != nil {
return nil, "", err
}
if handler != nil {
Sessi