diff --git a/README.rst b/README.rst
index b468b09c99..b112588ca3 100644
--- a/README.rst
+++ b/README.rst
@@ -15,12 +15,12 @@ See the `documentation `_ for how t
:target: https://nuts-node.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
-.. image:: https://api.codeclimate.com/v1/badges/69f77bd34f3ac253cae0/test_coverage
- :target: https://codeclimate.com/github/nuts-foundation/nuts-node/test_coverage
+.. image:: https://qlty.sh/gh/nuts-foundation/projects/nuts-node/coverage.svg
+ :target: https://qlty.sh/gh/nuts-foundation/projects/nuts-node
:alt: Code coverage
-.. image:: https://api.codeclimate.com/v1/badges/69f77bd34f3ac253cae0/maintainability
- :target: https://codeclimate.com/github/nuts-foundation/nuts-node/maintainability
+.. image:: https://qlty.sh/gh/nuts-foundation/projects/nuts-node/maintainability.svg
+ :target: https://qlty.sh/gh/nuts-foundation/projects/nuts-node
:alt: Maintainability
.. image:: https://github.com/nuts-foundation/nuts-node/actions/workflows/build-images.yaml/badge.svg
@@ -217,6 +217,12 @@ The following options can be configured on the server:
storage.session.redis.sentinel.username Username for authenticating to Redis Sentinels.
storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address).
storage.sql.connection Connection string for the SQL database. If not set it, defaults to a SQLite database stored inside the configured data directory. Note: using SQLite is not recommended in production environments. If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL').
+ **Tracing**
+ tracing.endpoint OTLP collector endpoint for OpenTelemetry tracing (e.g., 'localhost:4318'). When empty, tracing is disabled.
+ tracing.insecure false Disable TLS for the OTLP connection.
+ tracing.servicename Service name reported to the tracing backend. Defaults to 'nuts-node'.
+ **VCR**
+ vcr.verifier.revocation.maxage 15m0s Max age of revocation information. If the revocation information is older than this, it will be refreshed from the issuer. If set to 0 or negative, revocation information will always be refreshed.
**policy**
policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping.
======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ============================================================================================================================================================================================================================================================================================================================================
diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst
index 27fcb7e04f..c2760c592f 100755
--- a/docs/pages/deployment/server_options.rst
+++ b/docs/pages/deployment/server_options.rst
@@ -57,11 +57,13 @@
storage.session.redis.sentinel.password Password for authenticating to Redis Sentinels.
storage.session.redis.sentinel.username Username for authenticating to Redis Sentinels.
storage.session.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address).
- storage.sql.connection Connection string for the SQL database. If not set it, defaults to a SQLite database stored inside the configured data directory. Note: using SQLite is not recommended in production environments. If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL').
- **Tracing**
- tracing.endpoint OTLP collector endpoint for OpenTelemetry tracing (e.g., 'localhost:4318'). When empty, tracing is disabled.
- tracing.insecure false Disable TLS for the OTLP connection.
- tracing.servicename nuts-node Service name reported to the tracing backend.
+ storage.sql.connection Connection string for the SQL database. If not set it, defaults to a SQLite database stored inside the configured data directory. Note: using SQLite is not recommended in production environments. If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL').
+ **Tracing**
+ tracing.endpoint OTLP collector endpoint for OpenTelemetry tracing (e.g., 'localhost:4318'). When empty, tracing is disabled.
+ tracing.insecure false Disable TLS for the OTLP connection.
+ tracing.servicename Service name reported to the tracing backend. Defaults to 'nuts-node'.
+ **VCR**
+ vcr.verifier.revocation.maxage 15m0s Max age of revocation information. If the revocation information is older than this, it will be refreshed from the issuer. If set to 0 or negative, revocation information will always be refreshed.
**policy**
policy.directory ./config/policy Directory to read policy files from. Policy files are JSON files that contain a scope to PresentationDefinition mapping.
======================================== =================================================================================================================================================================================================================================================================================================================================================================================================================================================================== ============================================================================================================================================================================================================================================================================================================================================
diff --git a/vcr/cmd/cmd.go b/vcr/cmd/cmd.go
index 01db27af37..8d5dd86446 100644
--- a/vcr/cmd/cmd.go
+++ b/vcr/cmd/cmd.go
@@ -21,11 +21,12 @@ package cmd
import (
"encoding/json"
"fmt"
+ "strings"
+ "time"
+
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/spf13/pflag"
- "strings"
- "time"
"github.com/nuts-foundation/nuts-node/core"
api "github.com/nuts-foundation/nuts-node/vcr/api/vcr/v2"
@@ -40,6 +41,7 @@ func FlagSet() *pflag.FlagSet {
flagSet.String("vcr.openid4vci.definitionsdir", defs.OpenID4VCI.DefinitionsDIR, "Directory with the additional credential definitions the node could issue (experimental, may change without notice).")
flagSet.Bool("vcr.openid4vci.enabled", defs.OpenID4VCI.Enabled, "Enable issuing and receiving credentials over OpenID4VCI.")
flagSet.Duration("vcr.openid4vci.timeout", time.Second*30, "Time-out for OpenID4VCI HTTP client operations.")
+ flagSet.Duration("vcr.verifier.revocation.maxage", time.Minute*15, "Max age of revocation information. If the revocation information is older than this, it will be refreshed from the issuer. If set to 0 or negative, revocation information will always be refreshed.")
return flagSet
}
diff --git a/vcr/config.go b/vcr/config.go
index 614ddf7291..a474cebb17 100644
--- a/vcr/config.go
+++ b/vcr/config.go
@@ -20,8 +20,9 @@
package vcr
import (
- "github.com/nuts-foundation/nuts-node/vcr/openid4vci"
"time"
+
+ "github.com/nuts-foundation/nuts-node/vcr/openid4vci"
)
// ModuleName is the name of this module.
@@ -31,6 +32,15 @@ const ModuleName = "VCR"
type Config struct {
// OpenID4VCI holds the config for the OpenID4VCI credential issuer and wallet
OpenID4VCI openid4vci.Config `koanf:"openid4vci"`
+ Verifier VerifierConfig `koanf:"verifier"`
+}
+
+type VerifierConfig struct {
+ Revocation VerifierRevocationConfig `koanf:"revocation"`
+}
+
+type VerifierRevocationConfig struct {
+ MaxAge time.Duration `koanf:"maxage"`
}
// DefaultConfig returns a fresh Config filled with default values
diff --git a/vcr/issuer/issuer_test.go b/vcr/issuer/issuer_test.go
index 2cfc4d2272..c69fbd1f41 100644
--- a/vcr/issuer/issuer_test.go
+++ b/vcr/issuer/issuer_test.go
@@ -1106,7 +1106,7 @@ func TestIssuer_StatusList(t *testing.T) {
func newTestStatusList2021(t testing.TB, db *gorm.DB, signingKey crypto.PublicKey, dids ...did.DID) *revocation.StatusList2021 {
storage.AddDIDtoSQLDB(t, db, dids...)
- cs := revocation.NewStatusList2021(db, nil, "https://example.com")
+ cs := revocation.NewStatusList2021(db, nil, "https://example.com", 15*time.Minute)
cs.Sign = func(_ context.Context, unsignedCredential vc.VerifiableCredential, kid string) (*vc.VerifiableCredential, error) {
unsignedCredential.ID, _ = ssi.ParseURI("test-credential")
bs, err := json.Marshal(unsignedCredential)
diff --git a/vcr/revocation/statuslist2021_verifier.go b/vcr/revocation/statuslist2021_verifier.go
index 4071ac54cd..cc28f7f5dd 100644
--- a/vcr/revocation/statuslist2021_verifier.go
+++ b/vcr/revocation/statuslist2021_verifier.go
@@ -33,9 +33,6 @@ import (
"gorm.io/gorm/clause"
)
-// maxAgeExternal is the maximum age of external StatusList2021Credentials. If older than this we try to refresh.
-const maxAgeExternal = 15 * time.Minute
-
// Verify StatusList2021 returns a types.ErrRevoked when the credentialStatus contains a 'StatusList2021Entry' that can be resolved and lists the credential as 'revoked'
// Other credentialStatus type/statusPurpose are ignored. Verification may fail with other non-standardized errors.
func (cs *StatusList2021) Verify(credentialToVerify vc.VerifiableCredential) error {
@@ -117,7 +114,7 @@ func (cs *StatusList2021) statusList(statusListCredential string) (*credentialRe
// TODO: renewal criteria need to be reconsidered if we add other purposes. A 'suspension' may have been canceled
// renew expired certificates
if (cr.Expires != nil && time.Unix(*cr.Expires, 0).Before(time.Now())) || // expired
- time.Unix(cr.CreatedAt, 0).Add(maxAgeExternal).Before(time.Now()) { // older than 15 min
+ time.Unix(cr.CreatedAt, 0).Add(cs.maxAge).Before(time.Now()) { // older than 15 min
crUpdated, err := cs.update(statusListCredential)
if err == nil {
return crUpdated, nil
diff --git a/vcr/revocation/statuslist2021_verifier_test.go b/vcr/revocation/statuslist2021_verifier_test.go
index dfe9eb6f3f..6ded1dd671 100644
--- a/vcr/revocation/statuslist2021_verifier_test.go
+++ b/vcr/revocation/statuslist2021_verifier_test.go
@@ -180,7 +180,7 @@ func TestStatusList2021_statusList(t *testing.T) {
t.Run("ok - exceeded max age", func(t *testing.T) {
cs, _, ts := testSetup(t, false)
cr, cir := makeRecords(ts.URL)
- cr.CreatedAt = time.Now().Add(-2 * maxAgeExternal).Unix()
+ cr.CreatedAt = time.Now().Add(-2 * cs.maxAge).Unix()
require.NoError(t, cs.db.Create(&cr).Error)
actualCR, err := cs.statusList(cir.SubjectID)
diff --git a/vcr/revocation/types.go b/vcr/revocation/types.go
index 66f20d02ce..f96f976dc7 100644
--- a/vcr/revocation/types.go
+++ b/vcr/revocation/types.go
@@ -122,12 +122,14 @@ type StatusList2021 struct {
VerifySignature VerifySignFn // injected by verifier
Sign SignFn // injected by issuer, context must contain an audit log
ResolveKey ResolveKeyFn // injected by issuer
+ // maxAge is the maximum age of external StatusList2021Credentials. If older than this we try to refresh.
+ maxAge time.Duration
}
// NewStatusList2021 returns a StatusList2021 without a Sign or VerifySignature method.
// The URL in the credential will be constructed as follows using the given base URL: /statuslist//
-func NewStatusList2021(db *gorm.DB, client core.HTTPRequestDoer, baseURL string) *StatusList2021 {
- return &StatusList2021{client: client, db: db, baseURL: baseURL}
+func NewStatusList2021(db *gorm.DB, client core.HTTPRequestDoer, baseURL string, maxAgeForRefresh time.Duration) *StatusList2021 {
+ return &StatusList2021{client: client, db: db, baseURL: baseURL, maxAge: maxAgeForRefresh}
}
// StatusList2021Entry is the "credentialStatus" property used by issuers to enable VerifiableCredential status information.
diff --git a/vcr/revocation/types_test.go b/vcr/revocation/types_test.go
index 91b08c6716..a12ec51672 100644
--- a/vcr/revocation/types_test.go
+++ b/vcr/revocation/types_test.go
@@ -22,10 +22,11 @@ import (
"context"
"crypto"
"encoding/json"
- "github.com/nuts-foundation/nuts-node/vdr/resolver"
"testing"
"time"
+ "github.com/nuts-foundation/nuts-node/vdr/resolver"
+
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/storage"
@@ -34,7 +35,7 @@ import (
// newTestStatusList2021 returns a StatusList2021 that does not Sign or VerifySignature, with a SQLite db containing the dids, and no http-client.
func newTestStatusList2021(t testing.TB, dids ...did.DID) *StatusList2021 {
- cs := NewStatusList2021(storage.NewTestStorageEngine(t).GetSQLDatabase(), nil, "https://example.com")
+ cs := NewStatusList2021(storage.NewTestStorageEngine(t).GetSQLDatabase(), nil, "https://example.com", 15*time.Minute)
cs.Sign = noopSign
cs.ResolveKey = noopResolveKey
cs.VerifySignature = noopSignVerify
diff --git a/vcr/vcr.go b/vcr/vcr.go
index 6cf9876b89..75a041079b 100644
--- a/vcr/vcr.go
+++ b/vcr/vcr.go
@@ -24,6 +24,12 @@ import (
"encoding/json"
"errors"
"fmt"
+ "io/fs"
+ "net/http"
+ "path"
+ "strings"
+ "time"
+
"github.com/nuts-foundation/go-leia/v4"
"github.com/nuts-foundation/nuts-node/http/client"
"github.com/nuts-foundation/nuts-node/pki"
@@ -32,11 +38,6 @@ import (
"github.com/nuts-foundation/nuts-node/vcr/revocation"
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
- "io/fs"
- "net/http"
- "path"
- "strings"
- "time"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
@@ -229,7 +230,7 @@ func (c *vcr) Configure(config core.ServerConfig) error {
networkPublisher = issuer.NewNetworkPublisher(c.network, didResolver, c.keyStore)
}
- status := revocation.NewStatusList2021(c.storageClient.GetSQLDatabase(), client.NewWithCache(config.HTTPClient.Timeout), config.URL)
+ status := revocation.NewStatusList2021(c.storageClient.GetSQLDatabase(), client.NewWithCache(config.HTTPClient.Timeout), config.URL, c.config.Verifier.Revocation.MaxAge)
c.issuer = issuer.NewIssuer(c.issuerStore, c, networkPublisher, openidHandlerFn, didResolver, c.keyStore, c.jsonldManager, c.trustConfig, status)
c.verifier = verifier.NewVerifier(c.verifierStore, didResolver, c.keyResolver, c.jsonldManager, c.trustConfig, status, c.pkiProvider)
diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go
index 8c60c4b75c..75de9d1c1c 100644
--- a/vcr/verifier/verifier_test.go
+++ b/vcr/verifier/verifier_test.go
@@ -23,8 +23,6 @@ import (
"crypto"
"encoding/json"
"errors"
- "github.com/nuts-foundation/nuts-node/storage/orm"
- "github.com/nuts-foundation/nuts-node/test/pki"
"net/http"
"net/http/httptest"
"os"
@@ -33,6 +31,9 @@ import (
"testing"
"time"
+ "github.com/nuts-foundation/nuts-node/storage/orm"
+ "github.com/nuts-foundation/nuts-node/test/pki"
+
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
ssi "github.com/nuts-foundation/go-did"
@@ -154,7 +155,7 @@ func TestVerifier_Verify(t *testing.T) {
ctx := newMockContext(t)
ctx.store.EXPECT().GetRevocations(gomock.Any()).Return([]*credential.Revocation{{}}, ErrNotFound).AnyTimes()
db := storage.NewTestStorageEngine(t).GetSQLDatabase()
- ctx.verifier.credentialStatus = revocation.NewStatusList2021(db, ts.Client(), "https://example.com")
+ ctx.verifier.credentialStatus = revocation.NewStatusList2021(db, ts.Client(), "https://example.com", 15*time.Minute)
ctx.verifier.credentialStatus.(*revocation.StatusList2021).VerifySignature = func(_ vc.VerifiableCredential, _ *time.Time) error { return nil } // don't check signatures on 'downloaded' StatusList2021Credentials
ctx.verifier.credentialStatus.(*revocation.StatusList2021).Sign = func(_ context.Context, unsignedCredential vc.VerifiableCredential, _ string) (*vc.VerifiableCredential, error) {
bs, err := json.Marshal(unsignedCredential)
@@ -848,7 +849,7 @@ func newMockContext(t *testing.T) mockContext {
verifierStore := NewMockStore(ctrl)
trustConfig := trust.NewConfig(path.Join(io.TestDirectory(t), "trust.yaml"))
db := orm.NewTestDatabase(t)
- verifier := NewVerifier(verifierStore, didResolver, keyResolver, jsonldManager, trustConfig, revocation.NewStatusList2021(db, nil, ""), nil).(*verifier)
+ verifier := NewVerifier(verifierStore, didResolver, keyResolver, jsonldManager, trustConfig, revocation.NewStatusList2021(db, nil, "", time.Minute*15), nil).(*verifier)
return mockContext{
ctrl: ctrl,
verifier: verifier,