Commit 786f0836 authored by Sietse Ringers's avatar Sietse Ringers
Browse files

refactor: move revocation.DB and related structs from gabi to root package

parent 2919f416
......@@ -14,7 +14,6 @@ import (
"github.com/go-errors/errors"
"github.com/jasonlvhit/gocron"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server"
"github.com/sirupsen/logrus"
......@@ -54,7 +53,7 @@ func New(conf *server.Configuration) (*Server, error) {
// TODO rethink this condition
continue
}
if err := s.conf.IrmaConfiguration.RevocationUpdateDB(credid); err != nil {
if err := s.conf.IrmaConfiguration.RevocationStorage.RevocationUpdateDB(credid); err != nil {
s.conf.Logger.Error("failed to update revocation database for %s:", credid.String())
_ = server.LogError(err)
}
......@@ -67,7 +66,7 @@ func New(conf *server.Configuration) (*Server, error) {
}
func (s *Server) Stop() {
if err := s.conf.IrmaConfiguration.Close(); err != nil {
if err := s.conf.IrmaConfiguration.RevocationStorage.Close(); err != nil {
_ = server.LogWarning(err)
}
s.stopScheduler <- true
......@@ -141,7 +140,7 @@ func (s *Server) CancelSession(token string) error {
}
func (s *Server) Revoke(credid irma.CredentialTypeIdentifier, key string) error {
return s.conf.IrmaConfiguration.Revoke(credid, key)
return s.conf.IrmaConfiguration.RevocationStorage.Revoke(credid, key)
}
func ParsePath(path string) (token, noun string, arg []string, err error) {
......@@ -387,7 +386,7 @@ func (s *Server) handleRevocationMessage(
return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, "POST records expects 1 url arguments"))
}
cred := irma.NewCredentialTypeIdentifier(args[0])
var records []*revocation.Record
var records []*irma.Record
if err := json.Unmarshal(message, &records); err != nil {
return server.JsonResponse(nil, server.RemoteError(server.ErrorMalformedInput, err.Error()))
}
......
......@@ -4,7 +4,6 @@ import (
"time"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/gabi/signed"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server"
......@@ -37,7 +36,7 @@ func (session *session) handleGetRequest(min, max *irma.ProtocolVersion) (irma.S
// we include the latest revocation records for the client here, as opposed to when the session
// was started, so that the client always gets the very latest revocation records
var err error
if err = session.conf.IrmaConfiguration.RevocationSetRecords(session.request.Base()); err != nil {
if err = session.conf.IrmaConfiguration.RevocationStorage.RevocationSetRecords(session.request.Base()); err != nil {
return nil, session.fail(server.ErrorUnknown, err.Error()) // TODO error type
}
......@@ -216,9 +215,9 @@ func (session *session) handlePostCommitments(commitments *irma.IssueCommitmentM
}
func (s *Server) handlePostRevocationRecords(
cred irma.CredentialTypeIdentifier, records []*revocation.Record,
cred irma.CredentialTypeIdentifier, records []*irma.Record,
) (interface{}, *irma.RemoteError) {
db, err := s.conf.IrmaConfiguration.RevocationDB(cred)
db, err := s.conf.IrmaConfiguration.RevocationStorage.RevocationDB(cred)
if err != nil {
return nil, server.RemoteError(server.ErrorUnknown, err.Error()) // TODO error type
}
......@@ -230,11 +229,11 @@ func (s *Server) handlePostRevocationRecords(
func (s *Server) handleGetRevocationRecords(
cred irma.CredentialTypeIdentifier, index int,
) ([]*revocation.Record, *irma.RemoteError) {
) ([]*irma.Record, *irma.RemoteError) {
if _, ok := s.conf.RevocationServers[cred]; !ok {
return nil, server.RemoteError(server.ErrorInvalidRequest, "not supported by this server")
}
db, err := s.conf.IrmaConfiguration.RevocationDB(cred)
db, err := s.conf.IrmaConfiguration.RevocationStorage.RevocationDB(cred)
if err != nil {
return nil, server.RemoteError(server.ErrorUnknown, err.Error()) // TODO error type
}
......@@ -265,13 +264,13 @@ func (s *Server) handlePostIssuanceRecord(
if err != nil {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
}
var rec revocation.IssuanceRecord
var rec irma.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)
db, err := s.conf.IrmaConfiguration.RevocationStorage.RevocationDB(cred)
if err != nil {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
}
......
......@@ -85,12 +85,12 @@ func (session *session) issuanceHandleRevocation(
// ensure the client always gets an up to date nonrevocation witness
if _, ours := session.conf.RevocationServers[cred.CredentialTypeID]; !ours {
if err = session.conf.IrmaConfiguration.RevocationUpdateDB(cred.CredentialTypeID); err != nil {
if err = session.conf.IrmaConfiguration.RevocationStorage.RevocationUpdateDB(cred.CredentialTypeID); err != nil {
return
}
}
db, err := session.conf.IrmaConfiguration.RevocationDB(cred.CredentialTypeID)
db, err := session.conf.IrmaConfiguration.RevocationStorage.RevocationDB(cred.CredentialTypeID)
if err != nil {
return
}
......@@ -102,13 +102,13 @@ func (session *session) issuanceHandleRevocation(
return
}
nonrevAttr = witness.E
issrecord := &revocation.IssuanceRecord{
issrecord := &irma.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)
err = session.conf.IrmaConfiguration.RevocationStorage.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")
......@@ -144,7 +144,7 @@ func (s *Server) validateIssuanceRequest(request *irma.IssuanceRequest) error {
return err
}
if s.conf.IrmaConfiguration.CredentialTypes[cred.CredentialTypeID].SupportsRevocation() {
db, err := s.conf.IrmaConfiguration.RevocationDB(cred.CredentialTypeID)
db, err := s.conf.IrmaConfiguration.RevocationStorage.RevocationDB(cred.CredentialTypeID)
if err != nil {
return err
}
......
......@@ -6,13 +6,10 @@ import (
"io/ioutil"
"net/http"
"path/filepath"
"reflect"
"testing"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/internal/test"
"github.com/privacybydesign/irmago/irmaclient"
......@@ -352,20 +349,7 @@ func TestRevocation(t *testing.T) {
// setup client, constants, and revocation key material
defer test.ClearTestStorage(t)
client, _ := parseStorage(t)
iss := irma.NewIssuerIdentifier("irma-demo.MijnOverheid")
cred := irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root")
keystore := client.Configuration.RevocationKeystore(iss)
sk, err := client.Configuration.PrivateKey(iss)
require.NoError(t, err)
revsk, err := sk.RevocationKey()
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)
// issue two MijnOverheid.root instances with revocation enabled
......
......@@ -51,15 +51,24 @@ func StopRequestorServer() {
func StartRevocationServer(t *testing.T) {
var err error
revocationServer, err = irmaserver.New(&server.Configuration{
cred := irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root")
conf := &server.Configuration{
Logger: logger,
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"): {},
cred: {},
},
})
}
revocationServer, err = irmaserver.New(conf)
require.NoError(t, err)
sk, err := conf.IrmaConfiguration.RevocationStorage.PrivateKey(cred.IssuerIdentifier())
require.NoError(t, err)
db, err := conf.IrmaConfiguration.RevocationStorage.RevocationDB(cred)
require.NoError(t, err)
err = db.EnableRevocation(sk)
require.NoError(t, err)
mux := http.NewServeMux()
......
......@@ -51,8 +51,7 @@ func (cred *credential) PrepareNonrevocation(conf *irma.Configuration, request i
revupdates := m[credtype]
nonrev := len(revupdates) > 0
keystore := conf.RevocationKeystore(credtype.IssuerIdentifier())
if updated, err := cred.NonRevocationWitness.Update(revupdates, keystore); err != nil {
if updated, err := conf.RevocationStorage.UpdateWitness(cred.NonRevocationWitness, revupdates, credtype.IssuerIdentifier()); err != nil {
return false, err
} else if updated {
cred.DiscardRevocationCache()
......@@ -65,11 +64,10 @@ func (cred *credential) PrepareNonrevocation(conf *irma.Configuration, request i
// nonrevocation witness is still out of date after applying the updates from the request,
// i.e. we were too far behind. Update from revocation server.
records, err := conf.RevocationGetUpdates(credtype, cred.NonRevocationWitness.Index+1)
revupdates, err := conf.RevocationStorage.RevocationGetUpdates(credtype, cred.NonRevocationWitness.Index+1)
if err != nil {
return nonrev, err
}
_, err = cred.NonRevocationWitness.Update(records, keystore)
_, err = conf.RevocationStorage.UpdateWitness(cred.NonRevocationWitness, revupdates, credtype.IssuerIdentifier())
return nonrev, err
}
......@@ -33,7 +33,6 @@ import (
"github.com/jasonlvhit/gocron"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/gabi/signed"
"github.com/privacybydesign/irmago/internal/fs"
)
......@@ -46,6 +45,8 @@ type Configuration struct {
CredentialTypes map[CredentialTypeIdentifier]*CredentialType
AttributeTypes map[AttributeTypeIdentifier]*AttributeType
RevocationStorage *RevocationStorage
// Path to the irma_configuration folder that this instance represents
Path string
RevocationPath string
......@@ -60,7 +61,6 @@ type Configuration struct {
publicKeys map[IssuerIdentifier]map[int]*gabi.PublicKey
privateKeys map[IssuerIdentifier]*gabi.PrivateKey
reverseHashes map[string]CredentialTypeIdentifier
revDBs map[CredentialTypeIdentifier]*revocation.DB
initialized bool
assets string
readOnly bool
......@@ -143,6 +143,7 @@ func newConfiguration(path string, assets string) (conf *Configuration, err erro
RevocationPath: filepath.Join(DefaultDataPath(), "revocation"),
assets: assets,
}
conf.RevocationStorage = &RevocationStorage{conf: conf}
if conf.assets != "" { // If an assets folder is specified, then it must exist
if err = fs.AssertPathExists(conf.assets); err != nil {
......
......@@ -13,7 +13,6 @@ import (
"github.com/go-errors/errors"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/irmago/internal/fs"
)
......@@ -33,10 +32,10 @@ type BaseRequest struct {
Revocation []CredentialTypeIdentifier `json:"revocation,omitempty"`
// Set by the IRMA server during the session
Context *big.Int `json:"context,omitempty"`
Nonce *big.Int `json:"nonce,omitempty"`
ProtocolVersion *ProtocolVersion `json:"protocolVersion,omitempty"`
RevocationUpdates map[CredentialTypeIdentifier][]*revocation.Record `json:"revocationUpdates,omitempty"`
Context *big.Int `json:"context,omitempty"`
Nonce *big.Int `json:"nonce,omitempty"`
ProtocolVersion *ProtocolVersion `json:"protocolVersion,omitempty"`
RevocationUpdates map[CredentialTypeIdentifier][]*Record `json:"revocationUpdates,omitempty"`
ids *IrmaIdentifierSet // cache for Identifiers() method
......
......@@ -7,21 +7,75 @@ import (
"github.com/go-errors/errors"
"github.com/hashicorp/go-multierror"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/gabi/signed"
"github.com/timshannon/bolthold"
bolt "go.etcd.io/bbolt"
)
func (conf *Configuration) RevocationKeystore(issuerid IssuerIdentifier) revocation.Keystore {
type (
// Keystore provides support for revocation public key rollover.
Keystore interface {
// PublicKey either returns the specified, non-nil public key or an error
PublicKey(counter uint) (*revocation.PublicKey, error)
}
// DB is a bolthold database storing revocation state for a particular accumulator
// (Record instances, and IssuanceRecord instances if used by an issuer).
DB struct {
Current revocation.Accumulator
Updated time.Time
onChange []func(*Record)
bolt *bolthold.Store
keystore Keystore
}
RevocationStorage struct {
dbs map[CredentialTypeIdentifier]*DB
conf *Configuration
}
// Record contains a signed AccumulatorUpdate and associated information.
Record struct {
StartIndex uint64
EndIndex uint64
PublicKeyIndex uint
Message signed.Message // signed revocation.AccumulatorUpdate
}
TimeRecord struct {
Index uint64
Start, End int64
}
// IssuanceRecord contains information generated during issuance, needed for later revocation.
IssuanceRecord struct {
Key string
Attr *big.Int
Issued int64
ValidUntil int64
RevokedAt int64 // 0 if not currently revoked
}
currentRecord struct {
Index uint64
}
)
const boltCurrentIndexKey = "currentIndex"
func (conf *Configuration) RevocationKeystore(issuerid IssuerIdentifier) Keystore {
return &issuerKeystore{issid: issuerid, conf: conf}
}
// issuerKeystore implements revocation.Keystore.
// issuerKeystore implements Keystore.
type issuerKeystore struct {
issid IssuerIdentifier
conf *Configuration
}
var _ revocation.Keystore = (*issuerKeystore)(nil)
var _ Keystore = (*issuerKeystore)(nil)
func (ks *issuerKeystore) PublicKey(counter uint) (*revocation.PublicKey, error) {
pk, err := ks.conf.PublicKey(ks.issid, int(counter))
......@@ -41,9 +95,304 @@ func (ks *issuerKeystore) PublicKey(counter uint) (*revocation.PublicKey, error)
return rpk, nil
}
func (conf *Configuration) RevocationGetUpdates(credid CredentialTypeIdentifier, index uint64) ([]*revocation.Record, error) {
var records []*revocation.Record
err := NewHTTPTransport(conf.CredentialTypes[credid].RevocationServer).
func (rdb *DB) EnableRevocation(sk *revocation.PrivateKey) error {
msg, acc, err := revocation.NewAccumulator(sk)
if err != nil {
return err
}
if err = rdb.Add(msg, sk.Counter); err != nil {
return err
}
rdb.Current = acc
return nil
}
// Revoke revokes the credential specified specified by key if found within the current database,
// by updating its revocation time to now, adding its revocation attribute to the current accumulator,
// and updating the revocation database on disk.
func (rdb *DB) Revoke(sk *revocation.PrivateKey, key []byte) error {
return rdb.bolt.Bolt().Update(func(tx *bolt.Tx) error {
var err error
cr := IssuanceRecord{}
if err = rdb.bolt.TxGet(tx, key, &cr); err != nil {
return err
}
cr.RevokedAt = time.Now().UnixNano()
if err = rdb.bolt.TxUpdate(tx, key, &cr); err != nil {
return err
}
return rdb.revokeAttr(sk, cr.Attr, tx)
})
}
// Get returns all records that a client requires to update its revocation state if it is currently
// at the specified index, that is, all records whose end index is greater than or equal to
// the specified index.
func (rdb *DB) RevocationRecords(index int) ([]*Record, error) {
var records []*Record
if err := rdb.bolt.Find(&records, bolthold.Where(bolthold.Key).Ge(uint64(index))); err != nil {
return nil, err
}
return records, nil
}
func (rdb *DB) LatestRecords(count int) ([]*Record, error) {
c := int(rdb.Current.Index) - count + 1
if c < 0 {
c = 0
}
return rdb.RevocationRecords(c)
}
func (rdb *DB) IssuanceRecordExists(key []byte) (bool, error) {
_, err := rdb.IssuanceRecord(key)
switch err {
case nil:
return true, nil
case bolthold.ErrNotFound:
return false, nil
default:
return false, err
}
}
func (rdb *DB) AddIssuanceRecord(r *IssuanceRecord) error {
return rdb.bolt.Insert([]byte(r.Key), r)
}
func (rdb *DB) IssuanceRecord(key []byte) (*IssuanceRecord, error) {
r := &IssuanceRecord{}
if err := rdb.bolt.Get(key, r); err != nil {
return nil, err
}
return r, nil
}
func (rdb *DB) AddRecords(records []*Record) error {
var err error
for _, r := range records {
if err = rdb.Add(r.Message, r.PublicKeyIndex); err != nil {
return err
}
}
rdb.Updated = time.Now() // TODO update this in add()?
return nil
}
func (rdb *DB) Add(updateMsg signed.Message, counter uint) error {
var err error
var update revocation.AccumulatorUpdate
pk, err := rdb.keystore.PublicKey(counter)
if err != nil {
return err
}
if err = signed.UnmarshalVerify(pk.ECDSA, updateMsg, &update); err != nil {
return err
}
return rdb.bolt.Bolt().Update(func(tx *bolt.Tx) error {
return rdb.add(update, updateMsg, counter, tx)
})
}
func (rdb *DB) add(update revocation.AccumulatorUpdate, updateMsg signed.Message, pkCounter uint, tx *bolt.Tx) error {
var err error
record := &Record{
StartIndex: update.StartIndex,
EndIndex: update.Accumulator.Index,
PublicKeyIndex: pkCounter,
Message: updateMsg,
}
if err = rdb.bolt.TxInsert(tx, update.Accumulator.Index, record); err != nil {
return err
}
if update.Accumulator.Index != 0 {
var tr TimeRecord
if err = rdb.bolt.TxGet(tx, update.Accumulator.Index-1, &tr); err == nil {
tr.End = time.Now().UnixNano()
if err = rdb.bolt.TxUpdate(tx, update.Accumulator.Index-1, &tr); err != nil {
return err
}
}
}
if err = rdb.bolt.TxInsert(tx, update.Accumulator.Index, &TimeRecord{
Index: update.Accumulator.Index,
Start: time.Now().UnixNano(),
}); err != nil {
return err
}
if err = rdb.bolt.TxUpsert(tx, boltCurrentIndexKey, &currentRecord{update.Accumulator.Index}); err != nil {
return err
}
for _, f := range rdb.onChange {
f(record)
}
rdb.Current = update.Accumulator
return nil
}
func (rdb *DB) Enabled() bool {
var currentIndex currentRecord
err := rdb.bolt.Get(boltCurrentIndexKey, &currentIndex)
return err == nil
}
func (rdb *DB) loadCurrent() error {
var currentIndex currentRecord
if err := rdb.bolt.Get(boltCurrentIndexKey, &currentIndex); err == bolthold.ErrNotFound {
return errors.New("revocation database not initialized")
} else if err != nil {
return err
}
var record Record
if err := rdb.bolt.Get(currentIndex.Index, &record); err != nil {
return err
}
pk, err := rdb.keystore.PublicKey(record.PublicKeyIndex)
if err != nil {
return err
}
var u revocation.AccumulatorUpdate
if err = signed.UnmarshalVerify(pk.ECDSA, record.Message, &u); err != nil {
return err
}
rdb.Current = u.Accumulator
return nil
}
func (rdb *DB) RevokeAttr(sk *revocation.PrivateKey, e *big.Int) error {
return rdb.bolt.Bolt().Update(func(tx *bolt.Tx) error {
return rdb.revokeAttr(sk, e, tx)
})
}
func (rdb *DB) revokeAttr(sk *revocation.PrivateKey, e *big.Int, tx *bolt.Tx) error {
// don't update rdb.Current until after all possible errors are handled
newAcc, err := rdb.Current.Remove(sk, e)
if err != nil {
return err
}
update := revocation.AccumulatorUpdate{
Accumulator: *newAcc,
StartIndex: newAcc.Index,
Revoked: []*big.Int{e},
Time: time.Now().UnixNano(),
}
updateMsg, err := signed.MarshalSign(sk.ECDSA, update)
if err != nil {
return err
}
if err = rdb.add(update, updateMsg, sk.Counter, tx); err != nil {
return err
}
rdb.Current = *newAcc
return nil
}
func (rdb *DB) Close() error {
rdb.onChange = nil
if rdb.bolt != nil {
return rdb.bolt.Close()
}
return nil
}
func (rdb *DB) OnChange(handler func(*Record)) {
rdb.onChange = append(rdb.onChange, handler)
}
func (r *Record) UnmarshalVerify(keystore Keystore) (*revocation.AccumulatorUpdate, error) {
pk, err := keystore.PublicKey(r.PublicKeyIndex)
if err != nil {
return nil, err
}
msg := &revocation.AccumulatorUpdate{}
if err := signed.UnmarshalVerify(pk.ECDSA, r.Message, msg); err != nil {
return nil, err
}
if (r.StartIndex != msg.StartIndex) ||
(r.EndIndex > 0 && r.EndIndex != msg.StartIndex+uint64(len(msg.Revoked))-1) {
return nil, errors.New("record has invalid start or end index")
}
return msg, nil
}
func (rs *RevocationStorage) LoadDB(credid CredentialTypeIdentifier) (*DB, error) {
path := filepath.Join(rs.conf.RevocationPath, credid.String())
keystore := rs.conf.RevocationKeystore(credid.IssuerIdentifier())
b, err := bolthold.Open(path, 0600, &bolthold.Options{Options: &bolt.Options{Timeout: 1 * time.Second}})
if err != nil {
return nil, err
}
db := &DB{
bolt: b,
keystore: keystore,
}
if db.Enabled() {
if err = db.loadCurrent(); err != nil {
_ = db.Close()