Commit 2919f416 authored by Sietse Ringers's avatar Sietse Ringers

feat: send issuance records and add revocation api in irmaserver

parent 4953edc7
......@@ -45,13 +45,21 @@ func New(conf *server.Configuration) (*Server, error) {
s.sessions.deleteExpired()
})
// TODO: how do we not update revocation state for credential types of which we are the authoritative server?
//s.scheduler.Every(5).Minutes().Do(func() {
// if err := s.conf.IrmaConfiguration.RevocationUpdateAll(); err != nil {
// s.conf.Logger.Error("failed to update revocation database:")
// _ = server.LogError(err)
// }
//})
s.scheduler.Every(5).Minutes().Do(func() {
for credid, credtype := range s.conf.IrmaConfiguration.CredentialTypes {
if !credtype.SupportsRevocation() {
continue
}
if _, ours := conf.RevocationServers[credid]; ours {
// TODO rethink this condition
continue
}
if err := s.conf.IrmaConfiguration.RevocationUpdateDB(credid); err != nil {
s.conf.Logger.Error("failed to update revocation database for %s:", credid.String())
_ = server.LogError(err)
}
}
})
s.stopScheduler = s.scheduler.Start()
......@@ -132,8 +140,12 @@ func (s *Server) CancelSession(token string) error {
return nil
}
func (s *Server) Revoke(credid irma.CredentialTypeIdentifier, key string) error {
return s.conf.IrmaConfiguration.Revoke(credid, key)
}
func ParsePath(path string) (token, noun string, arg []string, err error) {
rev := regexp.MustCompile("-/revocation/(records)/?(.*)$")
rev := regexp.MustCompile("-/revocation/(records|issuancerecord)/?(.*)$")
matches := rev.FindStringSubmatch(path)
if len(matches) == 3 {
args := strings.Split(matches[2], "/")
......@@ -381,13 +393,17 @@ func (s *Server) handleRevocationMessage(
}
return server.JsonResponse(s.handlePostRevocationRecords(cred, records))
}
//if noun == "revoke" && method == http.MethodPost {
// if len(args) != 1 {
// return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, "POST records expects 1 url arguments"))
// }
// cred := irma.NewCredentialTypeIdentifier(args[0])
// return server.JsonResponse(s.handleRevoke(cred, message))
//}
if noun == "issuancerecord" && method == http.MethodPost {
if len(args) != 2 {
return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, "POST issuancercord expects 2 url arguments"))
}
cred := irma.NewCredentialTypeIdentifier(args[0])
counter, err := strconv.Atoi(args[1])
if err != nil {
return server.JsonResponse(nil, server.RemoteError(server.ErrorMalformedInput, err.Error()))
}
return server.JsonResponse(s.handlePostIssuanceRecord(cred, counter, message))
}
return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, ""))
}
......@@ -199,7 +199,7 @@ func (session *session) handlePostCommitments(commitments *irma.IssueCommitmentM
if err != nil {
return nil, session.fail(server.ErrorIssuanceFailed, err.Error())
}
witness, nonrevAttr, err := session.handleRevocation(cred, attributes, sk)
witness, nonrevAttr, err := session.issuanceHandleRevocation(cred, attributes, sk)
if err != nil {
return nil, session.fail(server.ErrorIssuanceFailed, err.Error()) // TODO error type
}
......@@ -245,32 +245,39 @@ func (s *Server) handleGetRevocationRecords(
return records, nil
}
func (s *Server) handleRevoke(
cred irma.CredentialTypeIdentifier, msg signed.Message,
func (s *Server) handlePostIssuanceRecord(
cred irma.CredentialTypeIdentifier, counter int, message []byte,
) (string, *irma.RemoteError) {
sk, err := s.conf.IrmaConfiguration.PrivateKey(cred.IssuerIdentifier())
if _, ours := s.conf.RevocationServers[cred]; !ours {
return "", server.RemoteError(server.ErrorInvalidRequest, "not supported by this server")
}
// Grab the counter-th issuer public key, with which the message should be signed,
// and verify and unmarshal the issuance record
pk, err := s.conf.IrmaConfiguration.PublicKey(cred.IssuerIdentifier(), counter)
if err != nil {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
}
if sk == nil {
if pk == nil {
return "", server.RemoteError(server.ErrorUnknownPublicKey, "")
}
rsk, err := sk.RevocationKey()
revpk, err := pk.RevocationKey()
if err != nil {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
}
var cmd revocation.Command
if err = signed.UnmarshalVerify(&rsk.ECDSA.PublicKey, msg, &cmd); err != nil {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
var rec revocation.IssuanceRecord
if err := signed.UnmarshalVerify(revpk.ECDSA, message, &rec); err != nil {
return "", server.RemoteError(server.ErrorUnauthorized, err.Error())
}
// Insert the record into the database
db, err := s.conf.IrmaConfiguration.RevocationDB(cred)
if err != nil {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
}
if err = db.RevokeAttr(rsk, cmd.E); err != nil {
if err = db.AddIssuanceRecord(&rec); err != nil {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
}
return "OK", nil
}
......@@ -76,34 +76,43 @@ func (session *session) checkCache(message []byte, expectedStatus server.Status)
// Issuance helpers
func (session *session) handleRevocation(
func (session *session) issuanceHandleRevocation(
cred *irma.CredentialRequest, attributes *irma.AttributeList, sk *gabi.PrivateKey,
) (witness *revocation.Witness, nonrevAttr *big.Int, err error) {
if !session.conf.IrmaConfiguration.CredentialTypes[cred.CredentialTypeID].SupportsRevocation() {
return
}
// ensure the client always gets an up to date nonrevocation witness
if _, ours := session.conf.RevocationServers[cred.CredentialTypeID]; !ours {
// ensure the client always gets an up to date nonrevocation witness
// TODO: post IssuanceRecord to remote?
if err = session.conf.IrmaConfiguration.RevocationUpdateDB(cred.CredentialTypeID); err != nil {
return
}
}
db, err := session.conf.IrmaConfiguration.RevocationDB(cred.CredentialTypeID)
if err != nil {
return
}
if db.Enabled() {
if witness, err = sk.RevocationGenerateWitness(&db.Current); err != nil {
return
}
nonrevAttr = witness.E
if err = db.AddIssuanceRecord(&revocation.IssuanceRecord{
Key: cred.RevocationKey,
Attr: nonrevAttr,
Issued: time.Now().UnixNano(), // or (floored) cred issuance time?
ValidUntil: attributes.Expiry().UnixNano(),
}); err != nil {
if !db.Enabled() {
return
}
if witness, err = sk.RevocationGenerateWitness(&db.Current); err != nil {
return
}
nonrevAttr = witness.E
issrecord := &revocation.IssuanceRecord{
Key: cred.RevocationKey,
Attr: nonrevAttr,
Issued: time.Now().UnixNano(), // or (floored) cred issuance time?
ValidUntil: attributes.Expiry().UnixNano(),
}
err = session.conf.IrmaConfiguration.SendRevocationIssuanceRecord(cred.CredentialTypeID, issrecord)
if err != nil {
_ = server.LogWarning(errors.WrapPrefix(err, "Failed to send issuance record to revocation server", 0))
session.conf.Logger.Warn("Storing issuance record locally")
if err = db.AddIssuanceRecord(issrecord); err != nil {
return nil, nil, err
}
}
......
......@@ -6,14 +6,12 @@ import (
"io/ioutil"
"net/http"
"crypto/rand"
"path/filepath"
"reflect"
"testing"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test"
......@@ -341,16 +339,6 @@ func TestOptionalDisclosure(t *testing.T) {
}
}
func editDB(t *testing.T, path string, keystore revocation.Keystore, enabled bool, f func(*revocation.DB)) {
StopRevocationServer()
db, err := revocation.LoadDB(path, keystore)
require.NoError(t, err)
require.True(t, !enabled || db.Enabled())
f(db)
require.NoError(t, db.Close())
StartRevocationServer(t)
}
func revocationSession(t *testing.T, client *irmaclient.Client, options ...sessionOption) *requestorSessionResult {
attr := irma.NewAttributeTypeIdentifier("irma-demo.MijnOverheid.root.BSN")
req := irma.NewDisclosureRequest(attr)
......@@ -366,7 +354,6 @@ func TestRevocation(t *testing.T) {
client, _ := parseStorage(t)
iss := irma.NewIssuerIdentifier("irma-demo.MijnOverheid")
cred := irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root")
dbPath := filepath.Join(testdata, "tmp", "issuer", cred.String())
keystore := client.Configuration.RevocationKeystore(iss)
sk, err := client.Configuration.PrivateKey(iss)
require.NoError(t, err)
......@@ -374,48 +361,45 @@ func TestRevocation(t *testing.T) {
require.NoError(t, err)
// enable revocation for our credential type by creating and saving an initial accumulator
db, err := revocation.LoadDB(filepath.Join(testdata, "tmp", "issuer", cred.String()), keystore)
require.NoError(t, err)
require.NoError(t, db.EnableRevocation(revsk))
require.NoError(t, db.Close()) // so StartRevocationServer() can open it again
StartRevocationServer(t)
editDB(t, dbPath, keystore, false, func(db *revocation.DB) {
require.NoError(t, db.EnableRevocation(revsk))
})
// issue MijnOverheid.root instance with revocation enabled
// issue two MijnOverheid.root instances with revocation enabled
request := irma.NewIssuanceRequest([]*irma.CredentialRequest{{
RevocationKey: "12345", // once revocation is required for a credential type, this key is required
CredentialTypeID: irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root"),
RevocationKey: "cred0", // once revocation is required for a credential type, this key is required
CredentialTypeID: cred,
Attributes: map[string]string{
"BSN": "299792458",
},
}})
result := requestorSessionHelper(t, request, client)
require.Nil(t, result.Err)
// issue second one which overwrites the first one, as our credtype is a singleton
// this is ok, as we use cred0 only to revoke it, to see if cred1 keeps working
request.Credentials[0].RevocationKey = "cred1"
result = requestorSessionHelper(t, request, client)
require.Nil(t, result.Err)
// perform disclosure session with nonrevocation proof
// perform disclosure session (of cred1) with nonrevocation proof
result = revocationSession(t, client)
require.Equal(t, irma.ProofStatusValid, result.ProofStatus)
require.NotEmpty(t, result.Disclosed)
// revoke fake other credential
e, err := rand.Prime(rand.Reader, 207)
require.NoError(t, err)
editDB(t, dbPath, keystore, true, func(db *revocation.DB) {
require.NoError(t, db.AddIssuanceRecord(&revocation.IssuanceRecord{
Key: "fake",
Attr: big.Convert(e),
}))
require.NoError(t, db.Revoke(revsk, []byte("fake")))
})
// revoke cred0
require.NoError(t, revocationServer.Revoke(cred, "cred0"))
// perform another disclosure session with nonrevocation proof
// perform another disclosure session with nonrevocation proof to see that cred1 still works
// client updates its witness to the new accumulator first
result = revocationSession(t, client)
require.Equal(t, irma.ProofStatusValid, result.ProofStatus)
require.NotEmpty(t, result.Disclosed)
// revoke our credential
editDB(t, dbPath, keystore, true, func(db *revocation.DB) {
require.NoError(t, db.Revoke(revsk, []byte("12345")))
})
// revoke cred1
require.NoError(t, revocationServer.Revoke(cred, "cred1"))
// try to perform session with revoked credential
// client notices that is credential is revoked and aborts
......
......@@ -53,9 +53,9 @@ func StartRevocationServer(t *testing.T) {
var err error
revocationServer, err = irmaserver.New(&server.Configuration{
Logger: logger,
SchemesPath: filepath.Join(testdata, "irma_configuration"),
RevocationPath: filepath.Join(testdata, "tmp", "issuer"),
DisableSchemesUpdate: true,
SchemesPath: filepath.Join(testdata, "irma_configuration"),
RevocationPath: filepath.Join(testdata, "tmp", "issuer"), // todo rename this path to revocation?
RevocationServers: map[irma.CredentialTypeIdentifier]server.RevocationServer{
irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root"): {},
},
......@@ -84,10 +84,11 @@ func StartIrmaServer(t *testing.T, updatedIrmaConf bool) {
var err error
irmaServer, err = irmaserver.New(&server.Configuration{
URL: "http://localhost:48680",
Logger: logger,
SchemesPath: filepath.Join(testdata, irmaconf),
RevocationPath: filepath.Join(testdata, "tmp", "revocation"),
URL: "http://localhost:48680",
Logger: logger,
DisableSchemesUpdate: true,
SchemesPath: filepath.Join(testdata, irmaconf),
RevocationPath: filepath.Join(testdata, "tmp", "revocation"),
})
require.NoError(t, err)
......
......@@ -216,14 +216,23 @@ func (client *Client) addCredential(cred *credential) (err error) {
id = cred.CredentialType().Identifier()
}
// Don't add duplicate creds
// If we receive a duplicate credential it should overwrite the previous one; remove it first
// (it makes no sense to possess duplicate credentials, but the new signature might contain new
// functionality such as a nonrevocation witness, so it does not suffice to just return here)
index := -1
for _, attrlistlist := range client.attributes {
for _, attrs := range attrlistlist {
for i, attrs := range attrlistlist {
if attrs.Hash() == cred.AttributeList().Hash() {
return nil
index = i
break
}
}
}
if index != -1 {
if err = client.remove(id, index, false); err != nil {
return err
}
}
// If this is a singleton credential type, ensure we have at most one by removing any previous instance
// If a credential already exists with exactly the same attribute values (except metadata), delete the previous credential
......
......@@ -8,6 +8,7 @@ import (
"github.com/go-errors/errors"
"github.com/hashicorp/go-multierror"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/gabi/signed"
)
func (conf *Configuration) RevocationKeystore(issuerid IssuerIdentifier) revocation.Keystore {
......@@ -127,6 +128,37 @@ func (conf *Configuration) revocationUpdateDelayed(credid CredentialTypeIdentifi
return nil
}
func (conf *Configuration) SendRevocationIssuanceRecord(
cred CredentialTypeIdentifier, rec *revocation.IssuanceRecord,
) error {
credtype := conf.CredentialTypes[cred]
if credtype == nil {
return errors.New("unknown credential type")
}
if credtype.RevocationServer == "" {
return errors.New("credential type has no revocation server")
}
sk, err := conf.PrivateKey(cred.IssuerIdentifier())
if err != nil {
return err
}
if sk == nil {
return errors.New("private key not found")
}
revsk, err := sk.RevocationKey()
if err != nil {
return err
}
message, err := signed.MarshalSign(revsk.ECDSA, rec)
if err != nil {
return err
}
return NewHTTPTransport(credtype.RevocationServer).Post(
fmt.Sprintf("-/revocation/issuancerecord/%s/%d", cred, sk.Counter), nil, []byte(message),
)
}
func (conf *Configuration) Revoke(credid CredentialTypeIdentifier, key string) error {
sk, err := conf.PrivateKey(credid.IssuerIdentifier())
if err != nil {
......
......@@ -96,6 +96,13 @@ func (s *Server) CancelSession(token string) error {
return s.Server.CancelSession(token)
}
func Revoke(credid irma.CredentialTypeIdentifier, key string) error {
return s.Revoke(credid, key)
}
func (s *Server) Revoke(credid irma.CredentialTypeIdentifier, key string) error {
return s.Server.Revoke(credid, key)
}
// SubscribeServerSentEvents subscribes the HTTP client to server sent events on status updates
// of the specified IRMA session.
func SubscribeServerSentEvents(w http.ResponseWriter, r *http.Request, token string, requestor bool) error {
......
......@@ -330,7 +330,7 @@ func (s *Server) handleCreate(w http.ResponseWriter, r *http.Request) {
if rrequest != nil {
s.handleCreateSession(w, requestor, rrequest)
} else {
s.handleCreateRevocation(w, requestor, revreq)
s.handleRevoke(w, requestor, revreq)
}
}
......@@ -391,7 +391,7 @@ func (s *Server) handleCreateStatic(w http.ResponseWriter, r *http.Request) {
server.WriteJson(w, qr)
}
func (s *Server) handleCreateRevocation(w http.ResponseWriter, requestor string, request *irma.RevocationRequest) {
func (s *Server) handleRevoke(w http.ResponseWriter, requestor string, request *irma.RevocationRequest) {
allowed, reason := s.conf.CanRevoke(requestor, request.CredentialType)
if !allowed {
s.conf.Logger.WithFields(logrus.Fields{"requestor": requestor, "message": reason}).
......@@ -399,7 +399,7 @@ func (s *Server) handleCreateRevocation(w http.ResponseWriter, requestor string,
server.WriteError(w, server.ErrorUnauthorized, reason)
return
}
if err := s.conf.IrmaConfiguration.Revoke(request.CredentialType, request.Key); err != nil {
if err := s.irmaserv.Revoke(request.CredentialType, request.Key); err != nil {
server.WriteError(w, server.ErrorUnknown, err.Error())
}
server.WriteString(w, "OK")
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment