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

feat: support revocation sessions in IRMA server, and move revocation code...

feat: support revocation sessions in IRMA server, and move revocation code from irmaconfig.go to its own file
parent 0c5fb131
......@@ -28,6 +28,10 @@ type Server struct {
}
func New(conf *server.Configuration) (*Server, error) {
if err := conf.Check(); err != nil {
return nil, err
}
s := &Server{
conf: conf,
scheduler: gocron.NewScheduler(),
......@@ -40,9 +44,18 @@ func New(conf *server.Configuration) (*Server, error) {
s.scheduler.Every(10).Seconds().Do(func() {
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.stopScheduler = s.scheduler.Start()
return s, s.conf.Check()
return s, nil
}
func (s *Server) Stop() {
......@@ -120,19 +133,19 @@ func (s *Server) CancelSession(token string) error {
}
func ParsePath(path string) (token, noun string, arg []string, err error) {
client := regexp.MustCompile("session/(\\w+)/?(|commitments|proofs|status|statusevents)$")
matches := client.FindStringSubmatch(path)
if len(matches) == 3 {
return matches[1], matches[2], nil, nil
}
rev := regexp.MustCompile("-/revocation/(records)/?(.*)$")
matches = rev.FindStringSubmatch(path)
matches := rev.FindStringSubmatch(path)
if len(matches) == 3 {
args := strings.Split(matches[2], "/")
return "", matches[1], args, nil
}
client := regexp.MustCompile("session/(\\w+)/?(|commitments|proofs|status|statusevents)$")
matches = client.FindStringSubmatch(path)
if len(matches) == 3 {
return matches[1], matches[2], nil, nil
}
return "", "", nil, server.LogWarning(errors.Errorf("Invalid URL: %s", path))
}
......@@ -368,6 +381,13 @@ 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))
//}
return server.JsonResponse(nil, server.RemoteError(server.ErrorInvalidRequest, ""))
}
......@@ -4,8 +4,8 @@ import (
"time"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/gabi/signed"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server"
"github.com/sirupsen/logrus"
......@@ -36,9 +36,8 @@ 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
// TODO revocation database update mechanism
var err error
if err = session.request.Base().SetRevocationRecords(session.conf.IrmaConfiguration); err != nil {
if err = session.conf.IrmaConfiguration.RevocationSetRecords(session.request.Base()); err != nil {
return nil, session.fail(server.ErrorUnknown, err.Error()) // TODO error type
}
......@@ -200,30 +199,10 @@ func (session *session) handlePostCommitments(commitments *irma.IssueCommitmentM
if err != nil {
return nil, session.fail(server.ErrorIssuanceFailed, err.Error())
}
var witness *revocation.Witness
var nonrevAttr *big.Int
if session.conf.IrmaConfiguration.CredentialTypes[cred.CredentialTypeID].SupportsRevocation() {
db, err := session.conf.IrmaConfiguration.RevocationDB(cred.CredentialTypeID)
if err != nil {
return nil, session.fail(server.ErrorIssuanceFailed, err.Error())
}
if db.Enabled() {
if witness, err = sk.RevocationGenerateWitness(&db.Current); err != nil {
return nil, session.fail(server.ErrorIssuanceFailed, err.Error())
}
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 {
return nil, session.fail(server.ErrorUnknown, "failed to save nonrevocation witness")
}
}
witness, nonrevAttr, err := session.handleRevocation(cred, attributes, sk)
if err != nil {
return nil, session.fail(server.ErrorIssuanceFailed, err.Error()) // TODO error type
}
sig, err := issuer.IssueSignature(proof.U, attributes.Ints, nonrevAttr, commitments.Nonce2)
if err != nil {
return nil, session.fail(server.ErrorIssuanceFailed, err.Error())
......@@ -252,7 +231,7 @@ func (s *Server) handlePostRevocationRecords(
func (s *Server) handleGetRevocationRecords(
cred irma.CredentialTypeIdentifier, index int,
) ([]*revocation.Record, *irma.RemoteError) {
if _, ok := s.conf.RevocationServers[cred]; ok {
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)
......@@ -265,3 +244,33 @@ func (s *Server) handleGetRevocationRecords(
}
return records, nil
}
func (s *Server) handleRevoke(
cred irma.CredentialTypeIdentifier, msg signed.Message,
) (string, *irma.RemoteError) {
sk, err := s.conf.IrmaConfiguration.PrivateKey(cred.IssuerIdentifier())
if err != nil {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
}
if sk == nil {
return "", server.RemoteError(server.ErrorUnknownPublicKey, "")
}
rsk, err := sk.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())
}
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 {
return "", server.RemoteError(server.ErrorUnknown, err.Error())
}
return "OK", nil
}
......@@ -11,6 +11,8 @@ import (
"github.com/dgrijalva/jwt-go"
"github.com/go-errors/errors"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
"github.com/privacybydesign/gabi/revocation"
"github.com/privacybydesign/irmago"
"github.com/privacybydesign/irmago/server"
"github.com/sirupsen/logrus"
......@@ -74,6 +76,40 @@ func (session *session) checkCache(message []byte, expectedStatus server.Status)
// Issuance helpers
func (session *session) handleRevocation(
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
}
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 {
return nil, nil, err
}
}
return
}
func (s *Server) validateIssuanceRequest(request *irma.IssuanceRequest) error {
for _, cred := range request.Credentials {
// Check that we have the appropriate private key
......
......@@ -342,11 +342,13 @@ 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 {
......@@ -364,7 +366,7 @@ 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", "revocation", cred.String())
dbPath := filepath.Join(testdata, "tmp", "issuer", cred.String())
keystore := client.Configuration.RevocationKeystore(iss)
sk, err := client.Configuration.PrivateKey(iss)
require.NoError(t, err)
......@@ -372,6 +374,7 @@ func TestRevocation(t *testing.T) {
require.NoError(t, err)
// enable revocation for our credential type by creating and saving an initial accumulator
StartRevocationServer(t)
editDB(t, dbPath, keystore, false, func(db *revocation.DB) {
require.NoError(t, db.EnableRevocation(revsk))
})
......
......@@ -17,16 +17,18 @@ import (
)
var (
httpServer *http.Server
irmaServer *irmaserver.Server
requestorServer *requestorserver.Server
httpServer *http.Server
irmaServer *irmaserver.Server
revHttpServer *http.Server
revocationServer *irmaserver.Server
requestorServer *requestorserver.Server
logger = logrus.New()
testdata = test.FindTestdataFolder(nil)
)
func init() {
logger.Level = logrus.ErrorLevel
logger.Level = logrus.TraceLevel
logger.Formatter = &prefixed.TextFormatter{ForceFormatting: true, ForceColors: true}
}
......@@ -47,6 +49,32 @@ func StopRequestorServer() {
requestorServer.Stop()
}
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,
RevocationServers: map[irma.CredentialTypeIdentifier]server.RevocationServer{
irma.NewCredentialTypeIdentifier("irma-demo.MijnOverheid.root"): {},
},
})
require.NoError(t, err)
mux := http.NewServeMux()
mux.HandleFunc("/", revocationServer.HandlerFunc())
revHttpServer = &http.Server{Addr: ":48683", Handler: mux}
go func() {
_ = revHttpServer.ListenAndServe()
}()
}
func StopRevocationServer() {
revocationServer.Stop()
_ = revHttpServer.Close()
}
func StartIrmaServer(t *testing.T, updatedIrmaConf bool) {
testdata := test.FindTestdataFolder(t)
irmaconf := "irma_configuration"
......
......@@ -98,6 +98,7 @@ func CreateTestStorage(t *testing.T) {
tmp := filepath.Join(FindTestdataFolder(t), "tmp")
checkError(t, fs.EnsureDirectoryExists(filepath.Join(tmp, "client")))
checkError(t, fs.EnsureDirectoryExists(filepath.Join(tmp, "revocation")))
checkError(t, fs.EnsureDirectoryExists(filepath.Join(tmp, "issuer")))
}
func SetupTestStorage(t *testing.T) {
......
......@@ -10,19 +10,43 @@ var revokeCmd = &cobra.Command{
Short: "Revoke a previously issued credential identified by a given key",
Args: cobra.RangeArgs(2, 3),
Run: func(cmd *cobra.Command, args []string) {
path := irma.DefaultDataPath()
if len(args) > 2 {
path = args[2]
irmaconf := irma.DefaultSchemesPath()
if len(args) == 3 {
irmaconf = args[2]
} else if irmaconf == "" {
die("Failed to find default irma_configuration path", nil)
}
db, sk := configureRevocation(cmd, path, args[0])
if err := db.Revoke(sk, []byte(args[1])); err != nil {
die("failed to revoke", err)
conf, err := irma.NewConfigurationReadOnly(irmaconf)
if err != nil {
die("", err)
}
if err = conf.ParseFolder(); err != nil {
die("", err)
}
cred := irma.NewCredentialTypeIdentifier(args[0])
if _, known := conf.CredentialTypes[cred]; !known {
die("unknown credential type", nil)
}
flags := cmd.Flags()
authmethod, _ := flags.GetString("authmethod")
key, _ := flags.GetString("key")
name, _ := flags.GetString("name")
_ = &irma.RevocationRequest{
LDContext: irma.LDContextRevocationRequest,
CredentialType: cred,
Key: args[1],
}
},
}
func init() {
revokeCmd.Flags().StringP("privatekey", "s", "", `Issuer private key for specified credential type`)
flags := revocationCmd.Flags()
flags.StringP("authmethod", "a", "none", "Authentication method to server (none, token, rsa, hmac)")
flags.String("key", "", "Key to sign request with")
flags.String("name", "", "Requestor name")
revocationCmd.AddCommand(revokeCmd)
}
package irmaclient
import (
"github.com/go-errors/errors"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/irmago"
)
......@@ -58,9 +57,19 @@ func (cred *credential) PrepareNonrevocation(conf *irma.Configuration, request i
} else if updated {
cred.DiscardRevocationCache()
}
if nonrev && cred.NonRevocationWitness.Index < revupdates[len(revupdates)-1].EndIndex {
return false, errors.New("failed to update nonrevocation witness")
// TODO download missing update messages from issuer and retry
// TODO (in both branches): attach our newer updates to response
if nonrev && cred.NonRevocationWitness.Index >= revupdates[len(revupdates)-1].EndIndex {
return nonrev, nil
}
return nonrev, nil
// 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)
if err != nil {
return nonrev, err
}
_, err = cred.NonRevocationWitness.Update(records, keystore)
return nonrev, err
}
......@@ -30,7 +30,6 @@ import (
"github.com/dgrijalva/jwt-go"
"github.com/go-errors/errors"
"github.com/hashicorp/go-multierror"
"github.com/jasonlvhit/gocron"
"github.com/privacybydesign/gabi"
"github.com/privacybydesign/gabi/big"
......@@ -541,91 +540,6 @@ func (conf *Configuration) PublicKeyIndices(issuerid IssuerIdentifier) (i []int,
return conf.matchKeyPattern(issuerid, pubkeyPattern)
}
func (conf *Configuration) RevocationKeystore(issuerid IssuerIdentifier) revocation.Keystore {
return &issuerKeystore{issid: issuerid, conf: conf}
}
// issuerKeystore implements revocation.Keystore.
type issuerKeystore struct {
issid IssuerIdentifier
conf *Configuration
}
var _ revocation.Keystore = (*issuerKeystore)(nil)
func (ks *issuerKeystore) PublicKey(counter uint) (*revocation.PublicKey, error) {
pk, err := ks.conf.PublicKey(ks.issid, int(counter))
if err != nil {
return nil, err
}
if pk == nil {
return nil, errors.Errorf("public key %d of issuer %s not found", counter, ks.issid)
}
if !pk.RevocationSupported() {
return nil, errors.Errorf("public key %d of issuer %s does not support revocation", counter, ks.issid)
}
rpk, err := pk.RevocationKey()
if err != nil {
return nil, err
}
return rpk, nil
}
func (conf *Configuration) RevocationUpdates(credid CredentialTypeIdentifier, index uint64) ([]*revocation.Record, error) {
var records []*revocation.Record
err := NewHTTPTransport(conf.CredentialTypes[credid].RevocationServer).
Get(fmt.Sprintf("/-/revocation/records/%s/%d", credid, index), &records)
if err != nil {
return nil, err
}
return records, nil
}
func (conf *Configuration) RevocationUpdateDB(credid CredentialTypeIdentifier) error {
db, err := conf.RevocationDB(credid)
if err != nil {
return err
}
records, err := conf.RevocationUpdates(credid, db.Current.Index+1)
if err != nil {
return err
}
return db.AddRecords(records)
}
func (conf *Configuration) RevocationDB(credid CredentialTypeIdentifier) (*revocation.DB, error) {
if _, known := conf.CredentialTypes[credid]; !known {
return nil, errors.New("unknown credential type")
}
if conf.revDBs == nil {
conf.revDBs = make(map[CredentialTypeIdentifier]*revocation.DB)
}
if conf.revDBs[credid] == nil {
var err error
db, err := revocation.LoadDB(
filepath.Join(conf.RevocationPath, credid.String()),
conf.RevocationKeystore(credid.IssuerIdentifier()),
)
if err != nil {
return nil, err
}
conf.revDBs[credid] = db
}
return conf.revDBs[credid], nil
}
func (conf *Configuration) Close() error {
merr := &multierror.Error{}
var err error
for _, db := range conf.revDBs {
if err = db.Close(); err != nil {
merr = multierror.Append(merr, err)
}
}
conf.revDBs = nil
return merr.ErrorOrNil()
}
func (conf *Configuration) matchKeyPattern(issuerid IssuerIdentifier, pattern string) (i []int, err error) {
pkpath := fmt.Sprintf(pattern, conf.Path, issuerid.SchemeManagerIdentifier().Name(), issuerid.Name())
files, err := filepath.Glob(pkpath)
......
......@@ -164,6 +164,7 @@ const (
ActionSigning = Action("signing")
ActionIssuing = Action("issuing")
ActionRedirect = Action("redirect")
ActionRevoking = Action("revoking")
ActionUnknown = Action("unknown")
)
......
......@@ -21,6 +21,7 @@ const (
LDContextDisclosureRequest = "https://irma.app/ld/request/disclosure/v2"
LDContextSignatureRequest = "https://irma.app/ld/request/signature/v2"
LDContextIssuanceRequest = "https://irma.app/ld/request/issuance/v2"
LDContextRevocationRequest = "https://irma.app/ld/request/revocation/v1"
)
// BaseRequest contains the context and nonce for an IRMA session.
......@@ -165,6 +166,11 @@ type IdentityProviderJwt struct {
Request *IdentityProviderRequest `json:"iprequest"`
}
type RevocationJwt struct {
ServerJwt
Request *RevocationRequest `json:"revrequest"`
}
// A RequestorJwt contains an IRMA session object.
type RequestorJwt interface {
Action() Action
......@@ -188,6 +194,19 @@ type AttributeRequest struct {
NotNull bool `json:"notNull,omitempty"`
}
type RevocationRequest struct {
LDContext string `json:"@context,omitempty"`
CredentialType CredentialTypeIdentifier `json:"type"`
Key string `json:"key"`
}
func (r *RevocationRequest) Validate() error {
if r.LDContext == LDContextRevocationRequest {
return errors.New("not a revocation request")
}
return nil
}
var (
bigZero = big.NewInt(0)
bigOne = big.NewInt(1)
......@@ -213,24 +232,6 @@ func (b *BaseRequest) GetNonce(*atum.Timestamp) *big.Int {
const revocationUpdateCount = 5
func (b *BaseRequest) SetRevocationRecords(conf *Configuration) error {
if len(b.Revocation) == 0 {
return nil
}
b.RevocationUpdates = make(map[CredentialTypeIdentifier][]*revocation.Record, len(b.Revocation))
for _, credid := range b.Revocation {
db, err := conf.RevocationDB(credid)
if err != nil {
return err
}
b.RevocationUpdates[credid], err = db.LatestRecords(revocationUpdateCount)
if err != nil {
return err
}
}
return nil
}
// CredentialTypes returns an array of all credential types occuring in this conjunction.
func (c AttributeCon) CredentialTypes() []CredentialTypeIdentifier {
var result []CredentialTypeIdentifier
......@@ -909,6 +910,13 @@ func (claims *IdentityProviderJwt) Valid() error {
return nil
}
func (claims *RevocationJwt) Valid() error {
if time.Time(claims.IssuedAt).After(time.Now()) {
return errors.New("Signature jwt not yet valid")
}
return nil
}
func (claims *ServiceProviderJwt) Action() Action { return ActionDisclosing }
func (claims *SignatureRequestorJwt) Action() Action { return ActionSigning }
......
package irma
import (
"fmt"
"path/filepath"
"time"
"github.com/go-errors/errors"
"github.com/hashicorp/go-multierror"
"github.com/privacybydesign/gabi/revocation"
)
func (conf *Configuration) RevocationKeystore(issuerid IssuerIdentifier) revocation.Keystore {
return &issuerKeystore{issid: issuerid, conf: conf}
}
// issuerKeystore implements revocation.Keystore.
type issuerKeystore struct {
issid IssuerIdentifier
conf *Configuration
}
var _ revocation.Keystore = (*issuerKeystore)(nil)
func (ks *issuerKeystore) PublicKey(counter uint) (*revocation.PublicKey, error) {