diff --git a/storage/gorm_logger.go b/storage/gorm_logger.go
index 377a7c76ad..bcd1610638 100644
--- a/storage/gorm_logger.go
+++ b/storage/gorm_logger.go
@@ -1,3 +1,21 @@
+/*
+ * Copyright (C) 2024 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
package storage
import (
diff --git a/storage/gorm_logger_test.go b/storage/gorm_logger_test.go
index a1ffc1e40b..5c27e90378 100644
--- a/storage/gorm_logger_test.go
+++ b/storage/gorm_logger_test.go
@@ -1,3 +1,21 @@
+/*
+ * Copyright (C) 2024 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
package storage
import (
diff --git a/vcr/credential/resolver.go b/vcr/credential/resolver.go
index a95caa9b9d..cf34fe8b0c 100644
--- a/vcr/credential/resolver.go
+++ b/vcr/credential/resolver.go
@@ -38,9 +38,6 @@ func FindValidator(credential vc.VerifiableCredential) Validator {
return nutsOrganizationCredentialValidator{}
case NutsAuthorizationCredentialType:
return nutsAuthorizationCredentialValidator{}
- case StatusList2021CredentialType:
- // TODO: is this needed? The only place where should be receiving StatusList2021Credentials is in the StatusList2021 caching layer, where we know what credential we should be receiving.
- return statusList2021CredentialValidator{}
}
}
}
diff --git a/vcr/credential/resolver_test.go b/vcr/credential/resolver_test.go
index 6b979a149b..7ee277741b 100644
--- a/vcr/credential/resolver_test.go
+++ b/vcr/credential/resolver_test.go
@@ -49,10 +49,6 @@ func TestFindValidator(t *testing.T) {
t.Run("validator and builder found for NutsAuthorizationCredential", func(t *testing.T) {
assert.IsType(t, nutsAuthorizationCredentialValidator{}, FindValidator(*ValidNutsAuthorizationCredential()))
})
-
- t.Run("validator and builder found for StatusList2021Credential", func(t *testing.T) {
- assert.IsType(t, statusList2021CredentialValidator{}, FindValidator(ValidStatusList2021Credential(t)))
- })
}
func TestExtractTypes(t *testing.T) {
diff --git a/vcr/credential/test.go b/vcr/credential/test.go
index bdae044062..33de4bcd0e 100644
--- a/vcr/credential/test.go
+++ b/vcr/credential/test.go
@@ -97,28 +97,6 @@ func JWTNutsOrganizationCredential(t *testing.T, subjectID did.DID) vc.Verifiabl
return *jwtVC
}
-func ValidStatusList2021Credential(_ testing.TB) vc.VerifiableCredential {
- id := ssi.MustParseURI("https://example.com/credentials/status/3")
- validFrom := time.Now()
- validUntilTomorrow := validFrom.Add(24 * time.Hour)
- return vc.VerifiableCredential{
- Context: []ssi.URI{vc.VCContextV1URI(), statusList2021ContextURI},
- ID: &id,
- Type: []ssi.URI{vc.VerifiableCredentialTypeV1URI(), stringToURI(StatusList2021CredentialType)},
- Issuer: ssi.MustParseURI("did:example:12345"),
- ValidFrom: &validFrom,
- ValidUntil: &validUntilTomorrow,
- CredentialStatus: nil,
- CredentialSubject: []any{&StatusList2021CredentialSubject{
- Id: "https://example-com/status/3#list",
- Type: StatusList2021CredentialSubjectType,
- StatusPurpose: "revocation",
- EncodedList: "H4sIAAAAAAAA_-zAsQAAAAACsNDypwqjZ2sAAAAAAAAAAAAAAAAAAACAtwUAAP__NxdfzQBAAAA=", // has bit 1 set to true
- }},
- Proof: []any{vc.Proof{}},
- }
-}
-
func stringToURI(input string) ssi.URI {
return ssi.MustParseURI(input)
}
diff --git a/vcr/credential/types.go b/vcr/credential/types.go
index 659e1c3669..d46410e045 100644
--- a/vcr/credential/types.go
+++ b/vcr/credential/types.go
@@ -21,7 +21,6 @@ package credential
import (
ssi "github.com/nuts-foundation/go-did"
- "github.com/nuts-foundation/nuts-node/jsonld"
)
const (
@@ -85,46 +84,3 @@ type Resource struct {
type BaseCredentialSubject struct {
ID string `json:"id"`
}
-
-const (
- // StatusList2021CredentialType is the type of StatusList2021Credential
- StatusList2021CredentialType = "StatusList2021Credential"
- // StatusList2021CredentialSubjectType is the credentialSubject.type in a StatusList2021Credential
- StatusList2021CredentialSubjectType = "StatusList2021"
- // StatusList2021EntryType is the credentialStatus.type that lists the entry of that credential on a list
- StatusList2021EntryType = "StatusList2021Entry"
-)
-
-var statusList2021ContextURI = ssi.MustParseURI(jsonld.W3cStatusList2021Context)
-var statusList2021CredentialTypeURI = ssi.MustParseURI(StatusList2021CredentialType)
-
-// StatusList2021Entry is the "credentialStatus" property used by issuers to enable VerifiableCredential status information.
-type StatusList2021Entry struct {
- // ID is expected to be a URL that identifies the status information associated with the verifiable credential.
- // It MUST NOT be the URL for the status list, which is in StatusListCredential.
- ID string `json:"id,omitempty"`
- // Type MUST be "StatusList2021Entry"
- Type string `json:"type,omitempty"`
- // StatusPurpose indicates what it means if the VerifiableCredential is on the list.
- // The value is arbitrary, with predefined values `revocation` and `suspension`.
- // This value must match credentialSubject.statusPurpose value in the VerifiableCredential.
- StatusPurpose string `json:"statusPurpose,omitempty"`
- // StatusListIndex is an arbitrary size integer greater than or equal to 0, expressed as a string.
- // The value identifies the bit position of the status of the verifiable credential.
- StatusListIndex string `json:"statusListIndex,omitempty"`
- // The statusListCredential property MUST be a URL to a verifiable credential.
- // When the URL is dereferenced, the resulting verifiable credential MUST have type property that includes the "StatusList2021Credential" value.
- StatusListCredential string `json:"statusListCredential,omitempty"`
-}
-
-type StatusList2021CredentialSubject struct {
- // ID for the credential subject
- Id string `json:"id"`
- // Type MUST be "StatusList2021Credential"
- Type string `json:"type"`
- // StatusPurpose defines the reason credentials are listed. ('revocation', 'suspension')
- StatusPurpose string `json:"statusPurpose"`
- // EncodedList is the GZIP-compressed [RFC1952], base-64 encoded [RFC4648] bitstring values for the associated range
- // of verifiable credential status values. The uncompressed bitstring MUST be at least 16KB in size.
- EncodedList string `json:"encodedList"`
-}
diff --git a/vcr/credential/validator.go b/vcr/credential/validator.go
index eb6049813b..41ff24f070 100644
--- a/vcr/credential/validator.go
+++ b/vcr/credential/validator.go
@@ -30,6 +30,7 @@ import (
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
+ "github.com/nuts-foundation/nuts-node/vcr/statuslist2021"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"github.com/piprate/json-gold/ld"
)
@@ -70,7 +71,7 @@ type AllFieldsDefinedValidator struct {
// Validate implements Validator.Validate.
func (d AllFieldsDefinedValidator) Validate(input vc.VerifiableCredential) error {
- // Expand with safe mode enabled, which asserts that all properties are defined in the JSON-LD context.
+ // expand with safe mode enabled, which asserts that all properties are defined in the JSON-LD context.
inputAsJSON, _ := input.MarshalJSON()
document, err := ld.DocumentFromReader(bytes.NewReader(inputAsJSON))
if err != nil {
@@ -145,17 +146,18 @@ func validateCredentialStatus(credential vc.VerifiableCredential) error {
return errors.New("credentialStatus.type is required")
}
- // only accept StatusList2021EntryType for now
- if credentialStatus.Type != StatusList2021EntryType {
+ // only accept StatusList2021Entry for now
+ if credentialStatus.Type != statuslist2021.EntryType {
continue
}
+ // TODO: AllFieldsDefined validator should be sufficient?
- if !credential.ContainsContext(statusList2021ContextURI) {
+ if !credential.ContainsContext(statuslist2021.ContextURI) {
return errors.New("StatusList2021 context is required")
}
// unmarshal as StatusList2021Entry
- var cs StatusList2021Entry
+ var cs statuslist2021.Entry
if err = json.Unmarshal(credentialStatus.Raw(), &cs); err != nil {
return err
}
@@ -320,47 +322,3 @@ func validateNutsCredentialID(credential vc.VerifiableCredential) error {
}
return nil
}
-
-// statusList2021CredentialValidator validates that all required fields of a StatusList2021CredentialType are present
-type statusList2021CredentialValidator struct{}
-
-func (d statusList2021CredentialValidator) Validate(credential vc.VerifiableCredential) error {
- if err := (defaultCredentialValidator{}).Validate(credential); err != nil {
- return err
- }
-
- { // Credential checks
- if !credential.ContainsContext(statusList2021ContextURI) {
- return failure("context '%s' is required", statusList2021ContextURI)
- }
- if !credential.IsType(statusList2021CredentialTypeURI) {
- return failure("type '%s' is required", statusList2021CredentialTypeURI)
- }
- }
-
- { // CredentialSubject checks
- var target []StatusList2021CredentialSubject
- err := credential.UnmarshalCredentialSubject(&target)
- if err != nil {
- return failure(err.Error())
- }
- // The spec is not clear if there could be multiple CredentialSubjects. This could allow 'revocation' and 'suspension' to be defined in a single credential.
- // However, it is not defined how to select the correct list (StatusPurpose) when validating credentials that are using this StatusList2021Credential.
- if len(target) != 1 {
- return failure("single CredentialSubject expected")
- }
- cs := target[0]
-
- if cs.Type != StatusList2021CredentialSubjectType {
- return failure("credentialSubject.type '%s' is required", StatusList2021CredentialSubjectType)
- }
- if cs.StatusPurpose == "" {
- return failure("credentialSubject.statusPurpose is required")
- }
- if cs.EncodedList == "" {
- return failure("credentialSubject.encodedList is required")
- }
- }
-
- return nil
-}
diff --git a/vcr/credential/validator_test.go b/vcr/credential/validator_test.go
index 18e1236cfe..6211542cc5 100644
--- a/vcr/credential/validator_test.go
+++ b/vcr/credential/validator_test.go
@@ -23,6 +23,7 @@ import (
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/jsonld"
+ "github.com/nuts-foundation/nuts-node/vcr/statuslist2021"
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
@@ -509,68 +510,6 @@ func TestDefaultCredentialValidator(t *testing.T) {
})
}
-func TestStatusList2021CredentialValidator_Validate(t *testing.T) {
- t.Run("ok", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.NoError(t, err)
- })
- t.Run("error - wraps defaultCredentialValidator", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.Context = []ssi.URI{statusList2021CredentialTypeURI}
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: default context is required")
- })
- t.Run("error - missing status list context", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.Context = []ssi.URI{vc.VCContextV1URI()}
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: context 'https://w3id.org/vc/status-list/2021/v1' is required")
- })
- t.Run("error - missing StatusList credential type", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.Type = []ssi.URI{vc.VerifiableCredentialTypeV1URI()}
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: type 'StatusList2021Credential' is required")
- })
- t.Run("error - invalid credential subject", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.CredentialSubject = []any{"{"}
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: json: cannot unmarshal string into Go value of type credential.StatusList2021CredentialSubject")
- })
- t.Run("error - wrong credential subject", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.CredentialSubject = []any{NutsAuthorizationCredentialSubject{}}
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: credentialSubject.type 'StatusList2021' is required")
- })
- t.Run("error - multiple credentialSubject", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.CredentialSubject = []any{StatusList2021CredentialSubject{}, StatusList2021CredentialSubject{}}
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: single CredentialSubject expected")
- })
- t.Run("error - missing credentialSubject.type", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.CredentialSubject[0].(*StatusList2021CredentialSubject).Type = ""
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: credentialSubject.type 'StatusList2021' is required")
- })
- t.Run("error - missing statusPurpose", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.CredentialSubject[0].(*StatusList2021CredentialSubject).StatusPurpose = ""
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: credentialSubject.statusPurpose is required")
- })
- t.Run("error - missing encodedList", func(t *testing.T) {
- cred := ValidStatusList2021Credential(t)
- cred.CredentialSubject[0].(*StatusList2021CredentialSubject).EncodedList = ""
- err := statusList2021CredentialValidator{}.Validate(cred)
- assert.EqualError(t, err, "validation failed: credentialSubject.encodedList is required")
- })
-}
-
func Test_validateCredentialStatus(t *testing.T) {
t.Run("ok - no credentialStatus", func(t *testing.T) {
assert.NoError(t, validateCredentialStatus(vc.VerifiableCredential{}))
@@ -591,13 +530,13 @@ func Test_validateCredentialStatus(t *testing.T) {
assert.EqualError(t, err, "credentialStatus.type is required")
})
- t.Run(StatusList2021EntryType, func(t *testing.T) {
+ t.Run(statuslist2021.EntryType, func(t *testing.T) {
makeValidCSEntry := func() vc.VerifiableCredential {
return vc.VerifiableCredential{
- Context: []ssi.URI{statusList2021ContextURI},
- CredentialStatus: []any{&StatusList2021Entry{
+ Context: []ssi.URI{ssi.MustParseURI(jsonld.W3cStatusList2021Context)},
+ CredentialStatus: []any{&statuslist2021.Entry{
ID: "https://example-com/credentials/status/3#94567",
- Type: StatusList2021EntryType,
+ Type: statuslist2021.EntryType,
StatusPurpose: "revocation",
StatusListIndex: "94567",
StatusListCredential: "https://example-com/credentials/status/3",
@@ -621,31 +560,31 @@ func Test_validateCredentialStatus(t *testing.T) {
})
t.Run("error - id == statusListCredential", func(t *testing.T) {
cred := makeValidCSEntry()
- cred.CredentialStatus[0].(*StatusList2021Entry).ID = cred.CredentialStatus[0].(*StatusList2021Entry).StatusListCredential
+ cred.CredentialStatus[0].(*statuslist2021.Entry).ID = cred.CredentialStatus[0].(*statuslist2021.Entry).StatusListCredential
err := validateCredentialStatus(cred)
assert.EqualError(t, err, "StatusList2021Entry.id is the same as the StatusList2021Entry.statusListCredential")
})
t.Run("error - missing statusPurpose", func(t *testing.T) {
cred := makeValidCSEntry()
- cred.CredentialStatus[0].(*StatusList2021Entry).StatusPurpose = ""
+ cred.CredentialStatus[0].(*statuslist2021.Entry).StatusPurpose = ""
err := validateCredentialStatus(cred)
assert.EqualError(t, err, "StatusList2021Entry.statusPurpose is required")
})
t.Run("error - statusListIndex is negative", func(t *testing.T) {
cred := makeValidCSEntry()
- cred.CredentialStatus[0].(*StatusList2021Entry).StatusListIndex = "-1"
+ cred.CredentialStatus[0].(*statuslist2021.Entry).StatusListIndex = "-1"
err := validateCredentialStatus(cred)
assert.EqualError(t, err, "invalid StatusList2021Entry.statusListIndex")
})
t.Run("error - statusListIndex is not a number", func(t *testing.T) {
cred := makeValidCSEntry()
- cred.CredentialStatus[0].(*StatusList2021Entry).StatusListIndex = "one"
+ cred.CredentialStatus[0].(*statuslist2021.Entry).StatusListIndex = "one"
err := validateCredentialStatus(cred)
assert.EqualError(t, err, "invalid StatusList2021Entry.statusListIndex")
})
t.Run("error - statusListCredential is not a valid URL", func(t *testing.T) {
cred := makeValidCSEntry()
- cred.CredentialStatus[0].(*StatusList2021Entry).StatusListCredential = "not a URL"
+ cred.CredentialStatus[0].(*statuslist2021.Entry).StatusListCredential = "not a URL"
err := validateCredentialStatus(cred)
assert.EqualError(t, err, "parse StatusList2021Entry.statusListCredential URL: parse \"not a URL\": invalid URI for request")
})
diff --git a/vcr/issuer/issuer.go b/vcr/issuer/issuer.go
index 2bbc1d8997..02be8e356e 100644
--- a/vcr/issuer/issuer.go
+++ b/vcr/issuer/issuer.go
@@ -24,7 +24,7 @@ import (
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
- "github.com/nuts-foundation/nuts-node/vcr/statuslist"
+ "github.com/nuts-foundation/nuts-node/vcr/statuslist2021"
"github.com/nuts-foundation/nuts-node/vdr/didnuts"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"time"
@@ -56,7 +56,7 @@ var TimeFunc = time.Now
func NewIssuer(store Store, vcrStore types.Writer, networkPublisher Publisher,
openidHandlerFn func(ctx context.Context, id did.DID) (OpenIDHandler, error),
didResolver resolver.DIDResolver, keyStore crypto.KeyStore, jsonldManager jsonld.JSONLD, trustConfig *trust.Config,
- statusList statuslist.StatusList2021Issuer) Issuer {
+ statusList statuslist2021.StatusList2021Issuer) Issuer {
keyResolver := vdrKeyResolver{
publicKeyResolver: resolver.DIDKeyResolver{Resolver: didResolver},
privateKeyResolver: keyStore,
@@ -88,7 +88,7 @@ type issuer struct {
jsonldManager jsonld.JSONLD
vcrStore types.Writer
walletResolver openid4vci.IdentifierResolver
- statusListStore statuslist.StatusList2021Issuer
+ statusListStore statuslist2021.StatusList2021Issuer
}
// Issue creates a new credential, signs, stores it.
@@ -222,7 +222,7 @@ func (i issuer) buildAndSignVC(ctx context.Context, template vc.VerifiableCreden
}
if options.WithStatusListRevocation {
// add credential status
- credentialStatusEntry, err := i.statusListStore.Create(ctx, *issuerDID, statuslist.StatusPurposeRevocation)
+ credentialStatusEntry, err := i.statusListStore.Create(ctx, *issuerDID, statuslist2021.StatusPurposeRevocation)
if err != nil {
return nil, err
}
@@ -337,14 +337,14 @@ func (i issuer) revokeStatusList(ctx context.Context, credentialID ssi.URI) erro
// find the correct credentialStatus and revoke it on the relevant statuslist
for _, status := range statuses {
- if status.Type == credential.StatusList2021EntryType {
- var slEntry credential.StatusList2021Entry
+ if status.Type == statuslist2021.EntryType {
+ var slEntry statuslist2021.Entry
err = json.Unmarshal(status.Raw(), &slEntry)
if err != nil {
return err
}
// TODO: make sure it is the correct entry when we allow other purposes, or VC issuance that include other credential statuses
- if slEntry.StatusPurpose != statuslist.StatusPurposeRevocation {
+ if slEntry.StatusPurpose != statuslist2021.StatusPurposeRevocation {
continue
}
return i.statusListStore.Revoke(ctx, credentialID, slEntry)
@@ -426,7 +426,7 @@ func (i issuer) SearchCredential(credentialType ssi.URI, issuer did.DID, subject
const statusListValidity = 24 * time.Hour // TODO: make configurable
var statusList2021ContextURI = ssi.MustParseURI(jsonld.W3cStatusList2021Context)
-var statusList2021CredentialTypeURI = ssi.MustParseURI(credential.StatusList2021CredentialType)
+var statusList2021CredentialTypeURI = ssi.MustParseURI(statuslist2021.CredentialType)
func (i issuer) StatusList(ctx context.Context, issuerDID did.DID, page int) (*vc.VerifiableCredential, error) {
// todo: get cached credential if available
diff --git a/vcr/issuer/issuer_test.go b/vcr/issuer/issuer_test.go
index 9d039e81c3..d2b1d88767 100644
--- a/vcr/issuer/issuer_test.go
+++ b/vcr/issuer/issuer_test.go
@@ -28,7 +28,7 @@ import (
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/storage"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
- "github.com/nuts-foundation/nuts-node/vcr/statuslist"
+ "github.com/nuts-foundation/nuts-node/vcr/statuslist2021"
"github.com/nuts-foundation/nuts-node/vcr/verifier"
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
@@ -168,7 +168,7 @@ func Test_issuer_buildAndSignVC(t *testing.T) {
statuses, err := result.CredentialStatuses()
require.NoError(t, err)
require.Len(t, statuses, 1)
- assert.Equal(t, credential.StatusList2021EntryType, statuses[0].Type)
+ assert.Equal(t, statuslist2021.EntryType, statuses[0].Type)
})
t.Run("error - did:nuts", func(t *testing.T) {
ctrl := gomock.NewController(t)
@@ -787,7 +787,7 @@ func Test_issuer_revokeNetwork(t *testing.T) {
func TestIssuer_revokeStatusList(t *testing.T) {
issuerDID := did.MustParseDID("did:web:example.com:iam:123")
- storeWithCred := func(c *gomock.Controller, entry credential.StatusList2021Entry) (*MockStore, ssi.URI) {
+ storeWithCred := func(c *gomock.Controller, entry statuslist2021.Entry) (*MockStore, ssi.URI) {
credentialID := ssi.MustParseURI(issuerDID.String() + "#identifier")
cred := &vc.VerifiableCredential{
ID: &credentialID,
@@ -801,7 +801,7 @@ func TestIssuer_revokeStatusList(t *testing.T) {
t.Run("ok", func(t *testing.T) {
status := NewTestStatusListStore(t, issuerDID)
- entry, err := status.Create(context.Background(), issuerDID, statuslist.StatusPurposeRevocation)
+ entry, err := status.Create(context.Background(), issuerDID, statuslist2021.StatusPurposeRevocation)
require.NoError(t, err)
issuerStore, credentialID := storeWithCred(gomock.NewController(t), *entry)
sut := issuer{
@@ -816,7 +816,7 @@ func TestIssuer_revokeStatusList(t *testing.T) {
})
t.Run("error - double revocation", func(t *testing.T) {
status := NewTestStatusListStore(t, issuerDID)
- entry, err := status.Create(context.Background(), issuerDID, statuslist.StatusPurposeRevocation)
+ entry, err := status.Create(context.Background(), issuerDID, statuslist2021.StatusPurposeRevocation)
require.NoError(t, err)
issuerStore, credentialID := storeWithCred(gomock.NewController(t), *entry)
sut := issuer{
@@ -842,7 +842,7 @@ func TestIssuer_revokeStatusList(t *testing.T) {
})
t.Run("error - statuslist credential not found", func(t *testing.T) {
status := NewTestStatusListStore(t, issuerDID)
- entry, err := status.Create(context.Background(), issuerDID, statuslist.StatusPurposeRevocation)
+ entry, err := status.Create(context.Background(), issuerDID, statuslist2021.StatusPurposeRevocation)
require.NoError(t, err)
entry.StatusListCredential = "unknown status list"
issuerStore, credentialID := storeWithCred(gomock.NewController(t), *entry)
@@ -858,8 +858,8 @@ func TestIssuer_revokeStatusList(t *testing.T) {
t.Run("error - invalid credentialStatus", func(t *testing.T) {
})
t.Run("error - no revokable credential status", func(t *testing.T) {
- issuerStore, credentialID := storeWithCred(gomock.NewController(t), credential.StatusList2021Entry{
- Type: credential.StatusList2021EntryType,
+ issuerStore, credentialID := storeWithCred(gomock.NewController(t), statuslist2021.Entry{
+ Type: statuslist2021.EntryType,
StatusPurpose: "not revocation",
})
sut := issuer{store: issuerStore}
@@ -943,7 +943,7 @@ func TestIssuer_StatusList(t *testing.T) {
trustConfig: trustConfig,
statusListStore: NewTestStatusListStore(t, issuerDID),
}
- _, err = sut.statusListStore.Create(ctx, issuerDID, statuslist.StatusPurposeRevocation)
+ _, err = sut.statusListStore.Create(ctx, issuerDID, statuslist2021.StatusPurposeRevocation)
require.NoError(t, err)
result, err := sut.StatusList(ctx, issuerDID, 1)
@@ -953,16 +953,16 @@ func TestIssuer_StatusList(t *testing.T) {
require.NotNil(t, result)
assert.Contains(t, result.Context, statusList2021ContextURI)
assert.Equal(t, result.Issuer.String(), issuerDID.String())
- assert.True(t, result.IsType(ssi.MustParseURI(credential.StatusList2021CredentialType)))
+ assert.True(t, result.IsType(ssi.MustParseURI(statuslist2021.CredentialType)))
// credential subject
- var subjects []credential.StatusList2021CredentialSubject
+ var subjects []statuslist2021.CredentialSubject
err = result.UnmarshalCredentialSubject(&subjects)
require.NoError(t, err)
require.Len(t, subjects, 1)
assert.Equal(t, subjects[0].Id, issuerURL.JoinPath("statuslist", "1").String())
- assert.Equal(t, subjects[0].Type, credential.StatusList2021CredentialSubjectType)
- assert.Equal(t, subjects[0].StatusPurpose, statuslist.StatusPurposeRevocation)
+ assert.Equal(t, subjects[0].Type, statuslist2021.CredentialSubjectType)
+ assert.Equal(t, subjects[0].StatusPurpose, statuslist2021.StatusPurposeRevocation)
assert.NotEmpty(t, subjects[0].EncodedList, "")
// verify credential -> trust is not added automatically
@@ -994,7 +994,7 @@ func TestIssuer_StatusList(t *testing.T) {
trustConfig: trustConfig,
statusListStore: NewTestStatusListStore(t, issuerDID),
}
- _, err = sut.statusListStore.Create(ctx, issuerDID, statuslist.StatusPurposeRevocation)
+ _, err = sut.statusListStore.Create(ctx, issuerDID, statuslist2021.StatusPurposeRevocation)
require.NoError(t, err)
result, err := sut.StatusList(ctx, issuerDID, 1)
@@ -1013,12 +1013,12 @@ func TestIssuer_StatusList(t *testing.T) {
})
}
-func NewTestStatusListStore(t testing.TB, dids ...did.DID) statuslist.StatusList2021Issuer {
+func NewTestStatusListStore(t testing.TB, dids ...did.DID) statuslist2021.StatusList2021Issuer {
storageEngine := storage.NewTestStorageEngine(t)
require.NoError(t, storageEngine.Start())
db := storageEngine.GetSQLDatabase()
storage.AddDIDtoSQLDB(t, db, dids...)
- store, err := statuslist.NewStatusListStore(db)
+ store, err := statuslist2021.NewStatusListStore(db)
require.NoError(t, err)
return store
}
diff --git a/vcr/credential/statuslist2021/bitstring.go b/vcr/statuslist2021/bitstring.go
similarity index 75%
rename from vcr/credential/statuslist2021/bitstring.go
rename to vcr/statuslist2021/bitstring.go
index 71acc85c0c..0fac780239 100644
--- a/vcr/credential/statuslist2021/bitstring.go
+++ b/vcr/statuslist2021/bitstring.go
@@ -28,19 +28,19 @@ import (
var ErrIndexNotInBitstring = errors.New("index not in status list")
const defaultBitstringLengthInBytes = 16 * 1024 // *8 = herd privacy of 16kB or 131072 bit
-const MaxBitstringIndex = defaultBitstringLengthInBytes*8 - 1
+const maxBitstringIndex = defaultBitstringLengthInBytes*8 - 1
-// Bitstring is not thread-safe
-type Bitstring []byte
+// bitstring is not thread-safe
+type bitstring []byte
-// NewBitstring creates a new Bitstring with 16kB entries initialized to 0.
-func NewBitstring() *Bitstring {
- bs := Bitstring(make([]byte, defaultBitstringLengthInBytes))
+// newBitstring creates a new bitstring with 16kB entries initialized to 0.
+func newBitstring() *bitstring {
+ bs := bitstring(make([]byte, defaultBitstringLengthInBytes))
return &bs
}
-// Bit returns the value of the Bitstring at statusListIndex, or (false, error) if the requested index is out of bounds.
-func (bs *Bitstring) Bit(statusListIndex int) (bool, error) {
+// bit returns the value of the bitstring at statusListIndex, or (false, error) if the requested index is out of bounds.
+func (bs *bitstring) bit(statusListIndex int) (bool, error) {
q, r := statusListIndex/8, byte(statusListIndex%8)
if statusListIndex < 0 || q >= len(*bs) {
return false, ErrIndexNotInBitstring
@@ -48,8 +48,8 @@ func (bs *Bitstring) Bit(statusListIndex int) (bool, error) {
return isSet((*bs)[q], r), nil
}
-// SetBit set the value of the bit at statusListIndex to 1, or returns an error when the index is out of bounds.
-func (bs *Bitstring) SetBit(statusListIndex int, value bool) error {
+// setBit set the value of the bit at statusListIndex to 1, or returns an error when the index is out of bounds.
+func (bs *bitstring) setBit(statusListIndex int, value bool) error {
q, r := statusListIndex/8, byte(statusListIndex%8)
if statusListIndex < 0 || q >= len(*bs) {
return ErrIndexNotInBitstring
@@ -66,8 +66,8 @@ func isSet(b, r byte) bool {
return b>>(7-r)&1 == 1
}
-// Compress a StatusList2021 bitstring. The input is gzip compressed followed by base64 encoding.
-func Compress(bitstring []byte) (string, error) {
+// compress a StatusList2021 bitstring. The input is gzip compressed followed by base64 encoding.
+func compress(bitstring []byte) (string, error) {
// gzip compression
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
@@ -81,14 +81,14 @@ func Compress(bitstring []byte) (string, error) {
}
// encode to base64 string.
- // Bitstring Status List spec clarified this to be multibase base64URL encoding without padding. StatusList2021 spec is not multibase.
+ // bitstring Status List spec clarified this to be multibase base64URL encoding without padding. StatusList2021 spec is not multibase.
return base64.RawURLEncoding.EncodeToString(buf.Bytes()), nil
}
-// Expand a compressed StatusList2021 bitstring. It first applies base64 decoding followed by gzip decompression.
-func Expand(encodedList string) (Bitstring, error) {
+// expand a compressed StatusList2021 bitstring. It first applies base64 decoding followed by gzip decompression.
+func expand(encodedList string) (bitstring, error) {
// base64 decode
- // Bitstring Status List spec clarified this to be multibase base64URL encoding without padding. StatusList2021 spec is not multibase.
+ // bitstring Status List spec clarified this to be multibase base64URL encoding without padding. StatusList2021 spec is not multibase.
enc := base64.RawURLEncoding
if len(encodedList)%4 == 0 {
// if encoding is a multiple of 4 it may or may not be padded. URLEncoding can handle both.
diff --git a/vcr/credential/statuslist2021/bitstring_test.go b/vcr/statuslist2021/bitstring_test.go
similarity index 71%
rename from vcr/credential/statuslist2021/bitstring_test.go
rename to vcr/statuslist2021/bitstring_test.go
index 333273020c..37b9e74923 100644
--- a/vcr/credential/statuslist2021/bitstring_test.go
+++ b/vcr/statuslist2021/bitstring_test.go
@@ -26,7 +26,7 @@ import (
)
func TestBitstring_Bit(t *testing.T) {
- bs := Bitstring{33} // 33 => 00100001
+ bs := bitstring{33} // 33 => 00100001
type test struct {
index int
isSet bool
@@ -46,7 +46,7 @@ func TestBitstring_Bit(t *testing.T) {
{8, false, ErrIndexNotInBitstring},
}
for _, tc := range tt {
- v, err := bs.Bit(tc.index)
+ v, err := bs.bit(tc.index)
assert.ErrorIs(t, err, tc.err)
assert.Equal(t, tc.isSet, v)
}
@@ -54,23 +54,23 @@ func TestBitstring_Bit(t *testing.T) {
func TestBitstring_SetBit(t *testing.T) {
t.Run("ok - set value", func(t *testing.T) {
- bs := Bitstring{0}
- assert.NoError(t, bs.SetBit(2, true))
- assert.NoError(t, bs.SetBit(2, true)) // applies value, not a simple bit flip
- assert.NoError(t, bs.SetBit(7, true))
- assert.Equal(t, Bitstring{33}, bs) // 33 => 00100001
+ bs := bitstring{0}
+ assert.NoError(t, bs.setBit(2, true))
+ assert.NoError(t, bs.setBit(2, true)) // applies value, not a simple bit flip
+ assert.NoError(t, bs.setBit(7, true))
+ assert.Equal(t, bitstring{33}, bs) // 33 => 00100001
})
t.Run("ok - unset value", func(t *testing.T) {
- bs := Bitstring{33} // 33 => 00100001
- assert.NoError(t, bs.SetBit(2, false))
- assert.NoError(t, bs.SetBit(2, false)) // applies value, not a simple bit flip
- assert.Equal(t, Bitstring{1}, bs)
+ bs := bitstring{33} // 33 => 00100001
+ assert.NoError(t, bs.setBit(2, false))
+ assert.NoError(t, bs.setBit(2, false)) // applies value, not a simple bit flip
+ assert.Equal(t, bitstring{1}, bs)
})
t.Run("error - index OOB", func(t *testing.T) {
- bs := Bitstring{0} // single byte
- assert.ErrorIs(t, bs.SetBit(-1, true), ErrIndexNotInBitstring)
- assert.NoError(t, bs.SetBit(0, true))
- assert.ErrorIs(t, bs.SetBit(8, true), ErrIndexNotInBitstring)
+ bs := bitstring{0} // single byte
+ assert.ErrorIs(t, bs.setBit(-1, true), ErrIndexNotInBitstring)
+ assert.NoError(t, bs.setBit(0, true))
+ assert.ErrorIs(t, bs.setBit(8, true), ErrIndexNotInBitstring)
})
}
@@ -78,45 +78,45 @@ func Test_CompressExpand(t *testing.T) {
t.Run("ok - empty bitstring from example", func(t *testing.T) {
// this EncodedList comes from the StatusList2021example https://www.w3.org/TR/2023/WD-vc-status-list-20230427/
exampleEncodedList := "H4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA"
- bs := NewBitstring()
+ bs := newBitstring()
- expanded, err := Expand(exampleEncodedList)
+ expanded, err := expand(exampleEncodedList)
assert.NoError(t, err)
assert.Len(t, *bs, len(expanded), "comparison invalid if these are not the same (16kB)")
assert.Equal(t, *bs, expanded)
})
- t.Run("ok - input >> Compress >> Expand == input", func(t *testing.T) {
+ t.Run("ok - input >> compress >> expand == input", func(t *testing.T) {
// make bitstring with random flags
- bs := *NewBitstring()
- assert.Equal(t, bs, *NewBitstring())
+ bs := *newBitstring()
+ assert.Equal(t, bs, *newBitstring())
for i := 0; i < 10; i++ {
n := rand.Intn(defaultBitstringLengthInBytes * 8)
- assert.NoError(t, bs.SetBit(n, true))
+ assert.NoError(t, bs.setBit(n, true))
}
- assert.NotEqual(t, bs, *NewBitstring())
+ assert.NotEqual(t, bs, *newBitstring())
// compress the input
- compressed, err := Compress(bs)
+ compressed, err := compress(bs)
require.NoError(t, err)
// GZIP can contain some platform specific values meaning that we cannot compare it to a hardcoded result
- compressedEmpty, err := Compress(*NewBitstring())
+ compressedEmpty, err := compress(*newBitstring())
require.NoError(t, err)
assert.NotEqual(t, compressed, compressedEmpty)
// expand compressed and validate against original
- expanded, err := Expand(compressed)
+ expanded, err := expand(compressed)
assert.NoError(t, err)
assert.Equal(t, bs, expanded)
})
- t.Run("ok - Expand is padding agnostic", func(t *testing.T) {
+ t.Run("ok - expand is padding agnostic", func(t *testing.T) {
// accepts padding
padded := "H4sIAAAAAAAA_-zaMQ6FIBAE0P-NhaVH9ugm9lBJhpX3WpoJFcPuj1dc6QAwypkOAAAAAMBKts6Zr6qHa4By_ukAANUd6QAszDIIAMBUms8zrQG-z3SkQXGlpD0dgFJ6KwQAjKBjwzTuAAAA___vXAvwAEAAAA=="
- padExpanded, err := Expand(padded)
+ padExpanded, err := expand(padded)
require.NoError(t, err)
// accepts no padding
notPadded := "H4sIAAAAAAAA_-zaMQ6FIBAE0P-NhaVH9ugm9lBJhpX3WpoJFcPuj1dc6QAwypkOAAAAAMBKts6Zr6qHa4By_ukAANUd6QAszDIIAMBUms8zrQG-z3SkQXGlpD0dgFJ6KwQAjKBjwzTuAAAA___vXAvwAEAAAA"
- nopadExpanded, err := Expand(notPadded)
+ nopadExpanded, err := expand(notPadded)
require.NoError(t, err)
// results are the same
diff --git a/vcr/verifier/credential_status.go b/vcr/statuslist2021/credential_status.go
similarity index 75%
rename from vcr/verifier/credential_status.go
rename to vcr/statuslist2021/credential_status.go
index 7720bb0f0a..62627e0792 100644
--- a/vcr/verifier/credential_status.go
+++ b/vcr/statuslist2021/credential_status.go
@@ -16,7 +16,7 @@
*
*/
-package verifier
+package statuslist2021
import (
"encoding/json"
@@ -27,21 +27,27 @@ import (
"strconv"
"time"
- ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/core"
- "github.com/nuts-foundation/nuts-node/vcr/credential"
- "github.com/nuts-foundation/nuts-node/vcr/credential/statuslist2021"
"github.com/nuts-foundation/nuts-node/vcr/log"
"github.com/nuts-foundation/nuts-node/vcr/types"
)
-type credentialStatus struct {
+type VerifySignFn func(credentialToVerify vc.VerifiableCredential, validateAt *time.Time) error // TODO: replace with new SignatureVerifier interface?
+
+func NewCredentialStatus(client core.HTTPRequestDoer, signVerifier VerifySignFn) *CredentialStatus {
+ return &CredentialStatus{
+ client: client,
+ verifySignature: signVerifier,
+ }
+}
+
+type CredentialStatus struct {
client core.HTTPRequestDoer
- verifySignature func(credentialToVerify vc.VerifiableCredential, validateAt *time.Time) error // TODO: replace with new SignatureVerifier interface?
+ verifySignature VerifySignFn
}
-// statusList is an immutable struct containing all information needed to verify a credentialStatus
+// statusList is an immutable struct containing all information needed to Verify a credentialStatus
type statusList struct {
// credential is the complete StatusList2021Credential this statusList is about
credential *vc.VerifiableCredential
@@ -50,21 +56,21 @@ type statusList struct {
statusListCredential string
// statusPurpose is the purpose listed in the StatusList2021Credential.credentialSubject
statusPurpose string
- // expanded StatusList2021 Bitstring
- expanded statuslist2021.Bitstring
+ // expanded StatusList2021 bitstring
+ expanded bitstring
// lastUpdated is the timestamp this statusList was generated
lastUpdated time.Time
}
-// VerifyCredentialStatus returns a types.ErrRevoked when the credentialStatus contains a 'StatusList2021Entry' that can be resolved and lists the credential as 'revoked'
+// Verify CredentialStatus 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 *credentialStatus) verify(credentialToVerify vc.VerifiableCredential) error {
+func (cs *CredentialStatus) Verify(credentialToVerify vc.VerifiableCredential) error {
if credentialToVerify.CredentialStatus == nil {
return nil
}
statuses, err := credentialToVerify.CredentialStatuses()
if err != nil {
- // cannot happen. already validated in credential.defaultCredentialValidator{}
+ // cannot happen. already validated in defaultCredentialValidator{}
return err
}
@@ -72,7 +78,7 @@ func (cs *credentialStatus) verify(credentialToVerify vc.VerifiableCredential) e
// returns errors if processing fails -> TODO: hard/soft fail option?
// returns types.ErrRevoked if correct type, purpose, and listed.
for _, status := range statuses {
- if status.Type != credential.StatusList2021EntryType {
+ if status.Type != EntryType {
// ignore other credentialStatus.type
// TODO: what log level?
log.Logger().
@@ -80,7 +86,7 @@ func (cs *credentialStatus) verify(credentialToVerify vc.VerifiableCredential) e
Info("ignoring credentialStatus with unknown type")
continue
}
- var slEntry credential.StatusList2021Entry // CredentialStatus of the credentialToVerify
+ var slEntry Entry // CredentialStatus of the credentialToVerify
if err = json.Unmarshal(status.Raw(), &slEntry); err != nil {
// cannot happen. already validated in credential.defaultCredentialValidator{}
return err
@@ -109,7 +115,7 @@ func (cs *credentialStatus) verify(credentialToVerify vc.VerifiableCredential) e
// cannot happen. already validated in credential.defaultCredentialValidator{}
return err
}
- revoked, err := sList.expanded.Bit(index)
+ revoked, err := sList.expanded.bit(index)
if err != nil {
return err
}
@@ -120,14 +126,14 @@ func (cs *credentialStatus) verify(credentialToVerify vc.VerifiableCredential) e
return nil
}
-func (cs *credentialStatus) statusList(statusListCredential string) (*statusList, error) {
+func (cs *CredentialStatus) statusList(statusListCredential string) (*statusList, error) {
// TODO: check if there is a cached version to return
return cs.update(statusListCredential)
}
// update
-func (cs *credentialStatus) update(statusListCredential string) (*statusList, error) {
- // download and verify
+func (cs *CredentialStatus) update(statusListCredential string) (*statusList, error) {
+ // download and Verify
cred, err := cs.download(statusListCredential)
if err != nil {
return nil, err
@@ -138,7 +144,7 @@ func (cs *credentialStatus) update(statusListCredential string) (*statusList, er
}
// make statusList
- expanded, err := statuslist2021.Expand(credSubject.EncodedList)
+ expanded, err := expand(credSubject.EncodedList)
if err != nil {
// cant happen, already checked in verifyStatusList2021Credential
return nil, err
@@ -156,7 +162,7 @@ func (cs *credentialStatus) update(statusListCredential string) (*statusList, er
}
// download the StatusList2021Credential found at statusList2021Entry.statusListCredential
-func (cs *credentialStatus) download(statusListCredential string) (*vc.VerifiableCredential, error) {
+func (cs *CredentialStatus) download(statusListCredential string) (*vc.VerifiableCredential, error) {
var cred vc.VerifiableCredential // VC containing CredentialStatus of the credentialToVerify
req, err := http.NewRequest(http.MethodGet, statusListCredential, nil)
if err != nil {
@@ -171,7 +177,7 @@ func (cs *credentialStatus) download(statusListCredential string) (*vc.Verifiabl
// log, don't fail
log.Logger().
WithError(err).
- WithField("method", "credentialStatus.download").
+ WithField("method", "CredentialStatus.download").
Debug("failed to close response body")
}
}()
@@ -186,14 +192,14 @@ func (cs *credentialStatus) download(statusListCredential string) (*vc.Verifiabl
}
// verifyStatusList2021Credential checks that the StatusList2021Credential is currently valid
-func (cs *credentialStatus) verifyStatusList2021Credential(cred vc.VerifiableCredential) (*credential.StatusList2021CredentialSubject, error) {
- // make sure we have the correct credential.
- if len(cred.Type) > 2 || !cred.IsType(ssi.MustParseURI(credential.StatusList2021CredentialType)) {
+func (cs *CredentialStatus) verifyStatusList2021Credential(cred vc.VerifiableCredential) (*CredentialSubject, error) {
+ // make sure we have the correct credential type.
+ if len(cred.Type) != 2 || !cred.IsType(credentialTypeURI) {
return nil, errors.New("incorrect credential types")
}
- // returns statusList2021CredentialValidator, or Validate() fails because base type is missing
- if err := credential.FindValidator(cred).Validate(cred); err != nil {
+ // validate credential.
+ if err := (credentialValidator{}).Validate(cred); err != nil {
return nil, err
}
@@ -203,18 +209,18 @@ func (cs *credentialStatus) verifyStatusList2021Credential(cred vc.VerifiableCre
}
// check credentialSubject
- var credSubjects []credential.StatusList2021CredentialSubject
+ var credSubjects []CredentialSubject
if err := cred.UnmarshalCredentialSubject(&credSubjects); err != nil {
- // cannot happen. already validated in credential.statusList2021CredentialValidator{}
+ // cannot happen. already validated in credentialValidator{}
return nil, err
}
credSubject := credSubjects[0] // validators already ensured there is exactly 1 credentialSubject
- _, err := statuslist2021.Expand(credSubject.EncodedList)
+ _, err := expand(credSubject.EncodedList)
if err != nil {
return nil, fmt.Errorf("credentialSubject.encodedList is invalid: %w", err)
}
- // verify signature
+ // Verify signature
if err = cs.verifySignature(cred, nil); err != nil {
return nil, err
}
diff --git a/vcr/verifier/credential_status_test.go b/vcr/statuslist2021/credential_status_test.go
similarity index 74%
rename from vcr/verifier/credential_status_test.go
rename to vcr/statuslist2021/credential_status_test.go
index a36c813e59..8a472ae8f6 100644
--- a/vcr/verifier/credential_status_test.go
+++ b/vcr/statuslist2021/credential_status_test.go
@@ -16,7 +16,7 @@
*
*/
-package verifier
+package statuslist2021
import (
"encoding/json"
@@ -28,64 +28,77 @@ import (
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/vc"
- "github.com/nuts-foundation/nuts-node/vcr/credential"
+ "github.com/nuts-foundation/nuts-node/vcr/assets"
"github.com/nuts-foundation/nuts-node/vcr/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "go.uber.org/mock/gomock"
)
+// copy of vcr/validNutsOrganizationCredential to prevent circular deps.
+func validNutsOrganizationCredential(t *testing.T) vc.VerifiableCredential {
+ inputVC := vc.VerifiableCredential{}
+ vcJSON, err := assets.TestAssets.ReadFile("test_assets/vc.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = json.Unmarshal(vcJSON, &inputVC)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return inputVC
+}
+
func TestCredentialStatus_verify(t *testing.T) {
t.Run("ok", func(t *testing.T) {
cs, entry, _ := testSetup(t, false)
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = []any{entry}
- assert.NoError(t, cs.verify(cred))
+ assert.NoError(t, cs.Verify(cred))
})
t.Run("ok - multiple credentialStatus", func(t *testing.T) {
cs, entry, _ := testSetup(t, false)
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = []any{entry, entry}
- assert.NoError(t, cs.verify(cred))
+ assert.NoError(t, cs.Verify(cred))
})
t.Run("ok - no credentialStatus", func(t *testing.T) {
cs, _, _ := testSetup(t, false)
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = nil
- assert.NoError(t, cs.verify(cred))
+ assert.NoError(t, cs.Verify(cred))
})
t.Run("ok - unknown credentialStatus.type is ignored", func(t *testing.T) {
cs, entry, _ := testSetup(t, false)
entry.Type = "SomethingElse"
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = []any{entry}
- assert.NoError(t, cs.verify(cred))
+ assert.NoError(t, cs.Verify(cred))
})
t.Run("ok - revoked", func(t *testing.T) {
cs, entry, _ := testSetup(t, true) // true
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = []any{entry}
- err := cs.verify(cred)
+ err := cs.Verify(cred)
assert.ErrorIs(t, err, types.ErrRevoked)
})
t.Run("ok - credentialStatus.statusPurpose != 'revocation' is ignored", func(t *testing.T) {
cs, entry, _ := testSetup(t, true) // true
entry.StatusPurpose = "suspension"
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = []any{entry}
- assert.NoError(t, cs.verify(cred))
+ assert.NoError(t, cs.Verify(cred))
})
t.Run("error - cannot get statusList", func(t *testing.T) {
cs, entry, _ := testSetup(t, false)
cs.client = http.DefaultClient
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = []any{entry}
- assert.ErrorContains(t, cs.verify(cred), "tls: failed to verify certificate: x509: certificate signed by unknown authority")
+ assert.ErrorContains(t, cs.Verify(cred), "tls: failed to verify certificate: x509: certificate signed by unknown authority")
})
t.Run("error - statusPurpose mismatch", func(t *testing.T) {
// server that return StatusList2021Credential with statusPurpose == suspension
- statusList2021Credential := credential.ValidStatusList2021Credential(t)
- statusList2021Credential.CredentialSubject[0].(*credential.StatusList2021CredentialSubject).StatusPurpose = "suspension"
+ statusList2021Credential := ValidStatusList2021Credential(t)
+ statusList2021Credential.CredentialSubject[0].(*CredentialSubject).StatusPurpose = "suspension"
credBytes, err := json.Marshal(statusList2021Credential)
require.NoError(t, err)
ts := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
@@ -101,19 +114,19 @@ func TestCredentialStatus_verify(t *testing.T) {
// test credential
entry.StatusListCredential = ts.URL
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = []any{entry}
- err = cs.verify(cred)
+ err = cs.Verify(cred)
assert.EqualError(t, err, "StatusList2021Credential.credentialSubject.statusPuspose='suspension' does not match vc.credentialStatus.statusPurpose='revocation'")
})
t.Run("error - credentialStatus.statusListIndex out of bounds", func(t *testing.T) {
cs, entry, _ := testSetup(t, false)
entry.StatusListIndex = "500000" // max is ±130k
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
cred.CredentialStatus = []any{entry}
- assert.EqualError(t, cs.verify(cred), "index not in status list")
+ assert.EqualError(t, cs.Verify(cred), "index not in status list")
})
}
@@ -143,9 +156,7 @@ func TestCredentialStatus_update(t *testing.T) {
})
t.Run("error - verifyStatusList2021Credential", func(t *testing.T) {
cs, _, ts := testSetup(t, false)
- mockVerifier := NewMockVerifier(gomock.NewController(t))
- mockVerifier.EXPECT().VerifySignature(gomock.Any(), nil).Return(errors.New("custom error"))
- cs.verifySignature = mockVerifier.VerifySignature
+ cs.verifySignature = func(_ vc.VerifiableCredential, _ *time.Time) error { return errors.New("custom error") }
sl, err := cs.update(ts.URL)
@@ -156,7 +167,7 @@ func TestCredentialStatus_update(t *testing.T) {
func TestCredentialStatus_download(t *testing.T) {
t.Run("ok", func(t *testing.T) {
- cred := credential.ValidStatusList2021Credential(t) // has bit 1 set
+ cred := ValidStatusList2021Credential(t) // has bit 1 set
expected, err := json.Marshal(cred)
require.NoError(t, err)
ts := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
@@ -166,7 +177,7 @@ func TestCredentialStatus_download(t *testing.T) {
}))
defer ts.Close()
- cs := credentialStatus{client: ts.Client()}
+ cs := CredentialStatus{client: ts.Client()}
received, err := cs.download(ts.URL)
assert.NoError(t, err)
@@ -175,7 +186,7 @@ func TestCredentialStatus_download(t *testing.T) {
assert.JSONEq(t, string(expected), string(actual))
})
t.Run("error - statusListCredential not a URL", func(t *testing.T) {
- cs := credentialStatus{client: http.DefaultClient}
+ cs := CredentialStatus{client: http.DefaultClient}
received, err := cs.download("%%")
assert.EqualError(t, err, "parse \"%%\": invalid URL escape \"%%\"")
assert.Nil(t, received)
@@ -186,7 +197,7 @@ func TestCredentialStatus_download(t *testing.T) {
}))
defer ts.Close()
- cs := credentialStatus{client: ts.Client()}
+ cs := CredentialStatus{client: ts.Client()}
received, err := cs.download(ts.URL)
assert.ErrorContains(t, err, "fetching StatusList2021Credential from")
@@ -200,7 +211,7 @@ func TestCredentialStatus_download(t *testing.T) {
}))
defer ts.Close()
- cs := &credentialStatus{client: ts.Client()}
+ cs := &CredentialStatus{client: ts.Client()}
received, err := cs.download(ts.URL)
assert.EqualError(t, err, "unexpected end of JSON input")
@@ -209,60 +220,60 @@ func TestCredentialStatus_download(t *testing.T) {
}
func TestCredentialStatus_verifyStatusList2021Credential(t *testing.T) {
- credentialStatusNoSignCheck := &credentialStatus{
+ credentialStatusNoSignCheck := &CredentialStatus{
client: nil,
verifySignature: func(credentialToVerify vc.VerifiableCredential, validateAt *time.Time) error {
return nil
},
}
t.Run("ok", func(t *testing.T) {
- cred := credential.ValidStatusList2021Credential(t)
- expected := cred.CredentialSubject[0].(*credential.StatusList2021CredentialSubject)
+ cred := ValidStatusList2021Credential(t)
+ expected := cred.CredentialSubject[0].(*CredentialSubject)
credSubj, err := credentialStatusNoSignCheck.verifyStatusList2021Credential(cred)
assert.NoError(t, err)
require.NotNil(t, credSubj)
assert.Equal(t, *expected, *credSubj)
})
t.Run("error - incorrect credential type", func(t *testing.T) {
- cred := credential.ValidNutsOrganizationCredential(t)
+ cred := validNutsOrganizationCredential(t)
credSubj, err := credentialStatusNoSignCheck.verifyStatusList2021Credential(cred)
assert.EqualError(t, err, "incorrect credential types")
assert.Nil(t, credSubj)
})
t.Run("error - too many credential types", func(t *testing.T) {
- cred := credential.ValidStatusList2021Credential(t)
+ cred := ValidStatusList2021Credential(t)
cred.Type = append(cred.Type, ssi.MustParseURI("OneTooMany"))
credSubj, err := credentialStatusNoSignCheck.verifyStatusList2021Credential(cred)
assert.EqualError(t, err, "incorrect credential types")
assert.Nil(t, credSubj)
})
t.Run("error - credential validation failed", func(t *testing.T) {
- cred := credential.ValidStatusList2021Credential(t)
- cred.CredentialSubject[0].(*credential.StatusList2021CredentialSubject).Type = "wrong type"
+ cred := ValidStatusList2021Credential(t)
+ cred.CredentialSubject[0].(*CredentialSubject).Type = "wrong type"
credSubj, err := credentialStatusNoSignCheck.verifyStatusList2021Credential(cred)
- assert.EqualError(t, err, "validation failed: credentialSubject.type 'StatusList2021' is required")
+ assert.EqualError(t, err, "credentialSubject.type 'StatusList2021' is required")
assert.Nil(t, credSubj)
})
t.Run("error - contains CredentialStatus", func(t *testing.T) {
- cred := credential.ValidStatusList2021Credential(t)
+ cred := ValidStatusList2021Credential(t)
cred.CredentialStatus = []any{}
credSubj, err := credentialStatusNoSignCheck.verifyStatusList2021Credential(cred)
assert.EqualError(t, err, "StatusList2021Credential with a CredentialStatus is not supported")
assert.Nil(t, credSubj)
})
t.Run("error - invalid credentialSubject.encodedList", func(t *testing.T) {
- cred := credential.ValidStatusList2021Credential(t)
- cred.CredentialSubject[0].(*credential.StatusList2021CredentialSubject).EncodedList = "@"
+ cred := ValidStatusList2021Credential(t)
+ cred.CredentialSubject[0].(*CredentialSubject).EncodedList = "@"
credSubj, err := credentialStatusNoSignCheck.verifyStatusList2021Credential(cred)
assert.EqualError(t, err, "credentialSubject.encodedList is invalid: illegal base64 data at input byte 0")
assert.Nil(t, credSubj)
})
t.Run("error -invalid signature", func(t *testing.T) {
- cred := credential.ValidStatusList2021Credential(t)
- mockVerifier := NewMockVerifier(gomock.NewController(t))
- mockVerifier.EXPECT().VerifySignature(cred, nil).Return(errors.New("invalid signature"))
- cs := credentialStatus{verifySignature: mockVerifier.VerifySignature}
+ cred := ValidStatusList2021Credential(t)
+ cs := CredentialStatus{verifySignature: func(credentialToVerify vc.VerifiableCredential, validateAt *time.Time) error {
+ return errors.New("invalid signature")
+ }}
credSubj, err := cs.verifyStatusList2021Credential(cred)
assert.EqualError(t, err, "invalid signature")
assert.Nil(t, credSubj)
@@ -270,12 +281,12 @@ func TestCredentialStatus_verifyStatusList2021Credential(t *testing.T) {
}
// testSetup returns
-// - credentialStatus that does NOT verify signatures, and a client configured for the test server
+// - credentialStatus that does NOT Verify signatures, and a client configured for the test server
// - a StatusList2021Entry pointing to the test server, optionally provide a statusListIndex matching statusList2021Credential.encodedList to simulate revocation
// - the test server
-func testSetup(t testing.TB, entryIsRevoked bool) (*credentialStatus, credential.StatusList2021Entry, *httptest.Server) {
+func testSetup(t testing.TB, entryIsRevoked bool) (*CredentialStatus, Entry, *httptest.Server) {
// make test server
- statusList2021Credential := credential.ValidStatusList2021Credential(t) // has bit 1 set
+ statusList2021Credential := ValidStatusList2021Credential(t) // has bit 1 set
credBytes, err := json.Marshal(statusList2021Credential)
if err != nil {
t.Fatal(err)
@@ -288,7 +299,7 @@ func testSetup(t testing.TB, entryIsRevoked bool) (*credentialStatus, credential
t.Cleanup(func() { ts.Close() })
// make credentialStatus
- credentialStatusNoSignCheck := &credentialStatus{
+ credentialStatusNoSignCheck := &CredentialStatus{
client: ts.Client(),
verifySignature: func(credentialToVerify vc.VerifiableCredential, validateAt *time.Time) error {
return nil
@@ -296,8 +307,8 @@ func testSetup(t testing.TB, entryIsRevoked bool) (*credentialStatus, credential
}
// make StatusList2021Entry
- slEntry := credential.StatusList2021Entry{
- Type: credential.StatusList2021EntryType,
+ slEntry := Entry{
+ Type: EntryType,
StatusPurpose: "revocation",
StatusListIndex: "76248",
StatusListCredential: ts.URL,
diff --git a/vcr/statuslist/store.go b/vcr/statuslist2021/store.go
similarity index 85%
rename from vcr/statuslist/store.go
rename to vcr/statuslist2021/store.go
index 80ac85c899..e765d3c153 100644
--- a/vcr/statuslist/store.go
+++ b/vcr/statuslist2021/store.go
@@ -1,4 +1,22 @@
-package statuslist
+/*
+ * Copyright (C) 2024 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package statuslist2021
import (
"context"
@@ -9,8 +27,6 @@ import (
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
- "github.com/nuts-foundation/nuts-node/vcr/credential"
- "github.com/nuts-foundation/nuts-node/vcr/credential/statuslist2021"
"github.com/nuts-foundation/nuts-node/vcr/types"
"github.com/nuts-foundation/nuts-node/vdr/didweb"
"gorm.io/driver/sqlite"
@@ -38,15 +54,15 @@ const (
// them by setting the relevant bit on the StatusList.
// Individual statuses should be derived from the StatusList2021Credential(s), not inspected here.
type StatusList2021Issuer interface {
- // CredentialSubject creates a StatusList2021CredentialSubject to incorporate in a StatusList2021Credential issued by issuer.
- CredentialSubject(ctx context.Context, issuer did.DID, page int) (*credential.StatusList2021CredentialSubject, error)
- // Create a credential.StatusList2021Entry that can be added to the credentialStatus of a VC.
+ // CredentialSubject creates a CredentialSubject to incorporate in a StatusList2021Credential issued by issuer.
+ CredentialSubject(ctx context.Context, issuer did.DID, page int) (*CredentialSubject, error)
+ // Create a StatusList2021Entry that can be added to the credentialStatus of a VC.
// The corresponding credential will have a gap in the bitstring if the returned entry does not make it into a credential.
- Create(ctx context.Context, issuer did.DID, purpose StatusPurpose) (*credential.StatusList2021Entry, error)
+ Create(ctx context.Context, issuer did.DID, purpose StatusPurpose) (*Entry, error)
// Revoke by adding StatusList2021Entry to the list of revocations.
// The credentialID is only used to allow reverse search of revocations, its issuer is NOT compared to the entry issuer.
// Returns types.ErrRevoked if already revoked, or types.ErrNotFound when the entry.StatusListCredential is unknown.
- Revoke(ctx context.Context, credentialID ssi.URI, entry credential.StatusList2021Entry) error
+ Revoke(ctx context.Context, credentialID ssi.URI, entry Entry) error
}
func (s statusListCredentialRecord) TableName() string {
@@ -62,7 +78,7 @@ type statusListCredentialRecord struct {
Issuer string
// Page number corresponding to this SubjectID.
Page int
- // LastIssuedIndex on this page. Range: 0 <= StatusListIndex < statuslist2021.MaxBitstringIndex
+ // LastIssuedIndex on this page. Range: 0 <= StatusListIndex < statuslist2021.maxBitstringIndex
LastIssuedIndex int
// Revocations list all revocations for this SubjectID
Revocations []revocationRecord `gorm:"foreignKey:StatusListCredential;references:SubjectID"`
@@ -76,7 +92,7 @@ func (s revocationRecord) TableName() string {
type revocationRecord struct {
// StatusListCredential is the credentialSubject.ID this revocation belongs to. Example https://example.com/iam/id/statuslist/1
StatusListCredential string `gorm:"primaryKey"`
- // StatusListIndex of the revoked status list entry. Range: 0 <= StatusListIndex <= statuslist2021.MaxBitstringIndex
+ // StatusListIndex of the revoked status list entry. Range: 0 <= StatusListIndex <= statuslist2021.maxBitstringIndex
StatusListIndex int `gorm:"primaryKey;autoIncrement:false"`
// CredentialID is the VC.ID of the credential revoked by this status list entry.
// The value is stored as convenience during revocation, but is not validated.
@@ -102,7 +118,7 @@ func NewStatusListStore(db *gorm.DB) (*sqlStore, error) {
return &sqlStore{db: db}, nil
}
-func (s *sqlStore) CredentialSubject(ctx context.Context, issuer did.DID, page int) (*credential.StatusList2021CredentialSubject, error) {
+func (s *sqlStore) CredentialSubject(ctx context.Context, issuer did.DID, page int) (*CredentialSubject, error) {
statusListCredential, err := toStatusListCredential(issuer, page)
if err != nil {
return nil, err
@@ -120,29 +136,29 @@ func (s *sqlStore) CredentialSubject(ctx context.Context, issuer did.DID, page i
}
// make encodedList
- bitstring := statuslist2021.NewBitstring()
+ bitstring := newBitstring()
for _, rev := range statuslist.Revocations {
- if err = bitstring.SetBit(rev.StatusListIndex, true); err != nil {
+ if err = bitstring.setBit(rev.StatusListIndex, true); err != nil {
// can't happen
return nil, err
}
}
- encodedList, err := statuslist2021.Compress(*bitstring)
+ encodedList, err := compress(*bitstring)
if err != nil {
// can't happen
return nil, err
}
// return credential subject
- return &credential.StatusList2021CredentialSubject{
+ return &CredentialSubject{
Id: statusListCredential,
- Type: credential.StatusList2021CredentialSubjectType,
+ Type: CredentialSubjectType,
StatusPurpose: StatusPurposeRevocation,
EncodedList: encodedList,
}, nil
}
-func (s *sqlStore) Create(ctx context.Context, issuer did.DID, purpose StatusPurpose) (*credential.StatusList2021Entry, error) {
+func (s *sqlStore) Create(ctx context.Context, issuer did.DID, purpose StatusPurpose) (*Entry, error) {
if purpose != StatusPurposeRevocation {
return nil, errUnsupportedPurpose
}
@@ -181,7 +197,7 @@ func (s *sqlStore) Create(ctx context.Context, issuer did.DID, purpose StatusPur
// first time issuer; prepare to create a new Page / StatusListCredential
credentialRecord = statusListCredentialRecord{
Issuer: issuer.String(),
- LastIssuedIndex: statuslist2021.MaxBitstringIndex, // this will be incremented to move to page 1
+ LastIssuedIndex: maxBitstringIndex, // this will be incremented to move to page 1
Page: 0,
}
}
@@ -190,7 +206,7 @@ func (s *sqlStore) Create(ctx context.Context, issuer did.DID, purpose StatusPur
credentialRecord.LastIssuedIndex++
// create new page (statusListCredential) if current is full
- if credentialRecord.LastIssuedIndex > statuslist2021.MaxBitstringIndex {
+ if credentialRecord.LastIssuedIndex > maxBitstringIndex {
credentialRecord.LastIssuedIndex = 0
credentialRecord.Page++
@@ -229,16 +245,16 @@ func (s *sqlStore) Create(ctx context.Context, issuer did.DID, purpose StatusPur
break
}
- return &credential.StatusList2021Entry{
+ return &Entry{
ID: fmt.Sprintf("%s#%d", credentialRecord.SubjectID, credentialRecord.LastIssuedIndex),
- Type: credential.StatusList2021EntryType,
+ Type: EntryType,
StatusPurpose: StatusPurposeRevocation,
StatusListIndex: strconv.Itoa(credentialRecord.LastIssuedIndex),
StatusListCredential: credentialRecord.SubjectID,
}, nil
}
-func (s *sqlStore) Revoke(ctx context.Context, credentialID ssi.URI, entry credential.StatusList2021Entry) error {
+func (s *sqlStore) Revoke(ctx context.Context, credentialID ssi.URI, entry Entry) error {
// parse StatusListIndex
statusListIndex, err := strconv.Atoi(entry.StatusListIndex)
if err != nil {
@@ -263,7 +279,7 @@ func (s *sqlStore) Revoke(ctx context.Context, credentialID ssi.URI, entry crede
// validate StatusListIndex
if statusListIndex < 0 || statusListIndex > statuslist.LastIssuedIndex {
- return statuslist2021.ErrIndexNotInBitstring
+ return ErrIndexNotInBitstring
}
// revoke
diff --git a/vcr/statuslist/store_test.go b/vcr/statuslist2021/store_test.go
similarity index 89%
rename from vcr/statuslist/store_test.go
rename to vcr/statuslist2021/store_test.go
index ae2bc4a54e..d33da9aecb 100644
--- a/vcr/statuslist/store_test.go
+++ b/vcr/statuslist2021/store_test.go
@@ -1,4 +1,22 @@
-package statuslist
+/*
+ * Copyright (C) 2024 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package statuslist2021
import (
"context"
@@ -10,8 +28,6 @@ import (
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/storage"
- "github.com/nuts-foundation/nuts-node/vcr/credential"
- "github.com/nuts-foundation/nuts-node/vcr/credential/statuslist2021"
"github.com/nuts-foundation/nuts-node/vcr/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -42,7 +58,7 @@ func TestSqlStore_Create(t *testing.T) {
require.NoError(t, err)
storage.AddDIDtoSQLDB(t, s.db, aliceDID, bobDID) // NOTE: most tests re-use same store
- var entry *credential.StatusList2021Entry
+ var entry *Entry
t.Run("ok", func(t *testing.T) {
@@ -57,7 +73,7 @@ func TestSqlStore_Create(t *testing.T) {
assert.Equal(t, statusListCredential, entry.StatusListCredential)
assert.Equal(t, "0", entry.StatusListIndex)
assert.Equal(t, fmt.Sprintf("%s#0", statusListCredential), entry.ID)
- assert.Equal(t, credential.StatusList2021EntryType, entry.Type)
+ assert.Equal(t, EntryType, entry.Type)
assert.Equal(t, StatusPurposeRevocation, entry.StatusPurpose)
})
t.Run("second entry", func(t *testing.T) {
@@ -78,7 +94,7 @@ func TestSqlStore_Create(t *testing.T) {
// set last_issued_index to max value for a single credential so the next entry will be in page 2
s.db.Model(&statusListCredentialRecord{}).
Where("subject_id = ?", statusListCredential).
- Update("last_issued_index", statuslist2021.MaxBitstringIndex)
+ Update("last_issued_index", maxBitstringIndex)
statusListCredential, _ = toStatusListCredential(aliceDID, 2) // now expect page 2 to be used
entry, err = s.Create(nil, aliceDID, StatusPurposeRevocation)
@@ -178,7 +194,7 @@ func TestSqlStore_Create(t *testing.T) {
defer storePG.writeLock.Unlock()
raceFn(storePG)
// To confirm there was a race condition on the page creation (can't happen with SQLite), check the logs for:
- // 2024/02/12 19:53:20 .../nuts-node/vcr/statuslist/store.go:196 duplicated key not allowed
+ // 2024/02/12 19:53:20 .../nuts-node/vcr/statuslist2021/store.go:196 duplicated key not allowed
// If this error was logged and the test did not fail it is handled correctly.
})
})
@@ -227,7 +243,7 @@ func TestSqlStore_Revoke(t *testing.T) {
t.Run("error - statusListIndex OOB", func(t *testing.T) {
cEntry := entry
cEntry.StatusListIndex = "10"
- assert.ErrorIs(t, s.Revoke(nil, ssi.URI{}, cEntry), statuslist2021.ErrIndexNotInBitstring)
+ assert.ErrorIs(t, s.Revoke(nil, ssi.URI{}, cEntry), ErrIndexNotInBitstring)
})
}
@@ -242,11 +258,11 @@ func TestSqlStore_CredentialSubject(t *testing.T) {
entry := *entryP
t.Run("ok - empty bitstring", func(t *testing.T) {
- encodedList, err := statuslist2021.Compress(*statuslist2021.NewBitstring())
+ encodedList, err := compress(*newBitstring())
assert.NoError(t, err)
- expectedCS := credential.StatusList2021CredentialSubject{
+ expectedCS := CredentialSubject{
Id: entry.StatusListCredential,
- Type: credential.StatusList2021CredentialSubjectType,
+ Type: CredentialSubjectType,
StatusPurpose: StatusPurposeRevocation,
EncodedList: encodedList,
}
@@ -264,7 +280,7 @@ func TestSqlStore_CredentialSubject(t *testing.T) {
assert.NoError(t, err)
require.NotNil(t, cs)
- bs, err := statuslist2021.Expand(cs.EncodedList)
+ bs, err := expand(cs.EncodedList)
assert.NoError(t, err)
assert.NotEmpty(t, bs)
})
diff --git a/vcr/statuslist2021/test.go b/vcr/statuslist2021/test.go
new file mode 100644
index 0000000000..399825c8d4
--- /dev/null
+++ b/vcr/statuslist2021/test.go
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package statuslist2021
+
+import (
+ "github.com/nuts-foundation/go-did"
+ "github.com/nuts-foundation/go-did/vc"
+ "testing"
+ "time"
+)
+
+func ValidStatusList2021Credential(_ testing.TB) vc.VerifiableCredential {
+ id := ssi.MustParseURI("https://example.com/credentials/status/3")
+ validFrom := time.Now()
+ validUntilTomorrow := validFrom.Add(24 * time.Hour)
+ return vc.VerifiableCredential{
+ Context: []ssi.URI{vc.VCContextV1URI(), ContextURI},
+ ID: &id,
+ Type: []ssi.URI{vc.VerifiableCredentialTypeV1URI(), ssi.MustParseURI(CredentialType)},
+ Issuer: ssi.MustParseURI("did:example:12345"),
+ ValidFrom: &validFrom,
+ ValidUntil: &validUntilTomorrow,
+ CredentialStatus: nil,
+ CredentialSubject: []any{&CredentialSubject{
+ Id: "https://example-com/status/3#list",
+ Type: CredentialSubjectType,
+ StatusPurpose: "revocation",
+ EncodedList: "H4sIAAAAAAAA_-zAsQAAAAACsNDypwqjZ2sAAAAAAAAAAAAAAAAAAACAtwUAAP__NxdfzQBAAAA=", // has bit 1 set to true
+ }},
+ Proof: []any{vc.Proof{}},
+ }
+}
diff --git a/vcr/statuslist2021/types.go b/vcr/statuslist2021/types.go
new file mode 100644
index 0000000000..801c1366d7
--- /dev/null
+++ b/vcr/statuslist2021/types.go
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package statuslist2021
+
+import (
+ "github.com/nuts-foundation/go-did"
+ "github.com/nuts-foundation/nuts-node/jsonld"
+)
+
+const (
+ // CredentialType is the type of StatusList2021Credential
+ CredentialType = "StatusList2021Credential"
+ // CredentialSubjectType is the credentialSubject.type in a StatusList2021Credential
+ CredentialSubjectType = "StatusList2021"
+ // EntryType is the credentialStatus.type that lists the entry of that credential on a list
+ EntryType = "StatusList2021Entry"
+)
+
+var ContextURI = ssi.MustParseURI(jsonld.W3cStatusList2021Context)
+var credentialTypeURI = ssi.MustParseURI(CredentialType)
+
+// Entry is the "credentialStatus" property used by issuers to enable VerifiableCredential status information.
+type Entry struct {
+ // ID is expected to be a URL that identifies the status information associated with the verifiable credential.
+ // It MUST NOT be the URL for the status list, which is in StatusListCredential.
+ ID string `json:"id,omitempty"`
+ // Type MUST be "StatusList2021Entry"
+ Type string `json:"type,omitempty"`
+ // StatusPurpose indicates what it means if the VerifiableCredential is on the list.
+ // The value is arbitrary, with predefined values `revocation` and `suspension`.
+ // This value must match credentialSubject.statusPurpose value in the VerifiableCredential.
+ StatusPurpose string `json:"statusPurpose,omitempty"`
+ // StatusListIndex is an arbitrary size integer greater than or equal to 0, expressed as a string.
+ // The value identifies the bit position of the status of the verifiable credential.
+ StatusListIndex string `json:"statusListIndex,omitempty"`
+ // The statusListCredential property MUST be a URL to a verifiable credential.
+ // When the URL is dereferenced, the resulting verifiable credential MUST have type property that includes the "StatusList2021Credential" value.
+ StatusListCredential string `json:"statusListCredential,omitempty"`
+}
+
+type CredentialSubject struct {
+ // ID for the credential subject
+ Id string `json:"id"`
+ // Type MUST be "StatusList2021Credential"
+ Type string `json:"type"`
+ // StatusPurpose defines the reason credentials are listed. ('revocation', 'suspension')
+ StatusPurpose string `json:"statusPurpose"`
+ // EncodedList is the GZIP-compressed [RFC1952], base-64 encoded [RFC4648] bitstring values for the associated range
+ // of verifiable credential status values. The uncompressed bitstring MUST be at least 16KB in size.
+ EncodedList string `json:"encodedList"`
+}
diff --git a/vcr/statuslist2021/validator.go b/vcr/statuslist2021/validator.go
new file mode 100644
index 0000000000..e417cfae26
--- /dev/null
+++ b/vcr/statuslist2021/validator.go
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package statuslist2021
+
+import (
+ "errors"
+ "fmt"
+ "github.com/nuts-foundation/go-did/vc"
+)
+
+// TODO: copy from credential package, merge with other
+type defaultCredentialValidator struct {
+}
+
+func (d defaultCredentialValidator) Validate(credential vc.VerifiableCredential) error {
+ if !credential.IsType(vc.VerifiableCredentialTypeV1URI()) {
+ return errors.New("type 'VerifiableCredential' is required")
+ }
+
+ if !credential.ContainsContext(vc.VCContextV1URI()) {
+ return errors.New("default context is required")
+ }
+
+ if credential.ID == nil {
+ return errors.New("'ID' is required")
+ }
+
+ // 'issuanceDate' must be present, but can be zero if replaced by alias 'validFrom'
+ if (credential.IssuanceDate == nil || credential.IssuanceDate.IsZero()) &&
+ (credential.ValidFrom == nil || credential.ValidFrom.IsZero()) {
+ return errors.New("'issuanceDate' is required")
+ }
+
+ if credential.Format() == vc.JSONLDCredentialProofFormat && credential.Proof == nil {
+ return errors.New("'proof' is required for JSON-LD credentials")
+ }
+
+ //// CredentialStatus is not specific to the credential type and the syntax (not status) should be checked here.
+ //if err := credential.validateCredentialStatus(credential); err != nil {
+ // return fmt.Errorf("invalid credentialStatus: %s", err)
+ //}
+
+ return nil
+}
+
+// credentialValidator validates that all required fields of a StatusList2021Credential are present
+type credentialValidator struct{}
+
+func (d credentialValidator) Validate(credential vc.VerifiableCredential) error {
+ if err := (defaultCredentialValidator{}).Validate(credential); err != nil {
+ return err
+ }
+
+ { // Credential checks
+ if !credential.ContainsContext(ContextURI) {
+ return fmt.Errorf("context '%s' is required", ContextURI)
+ }
+ if !credential.IsType(credentialTypeURI) {
+ return fmt.Errorf("type '%s' is required", credentialTypeURI)
+ }
+ }
+
+ { // CredentialSubject checks
+ var target []CredentialSubject
+ err := credential.UnmarshalCredentialSubject(&target)
+ if err != nil {
+ return err
+ }
+ // The spec is not clear if there could be multiple CredentialSubjects. This could allow 'revocation' and 'suspension' to be defined in a single credential.
+ // However, it is not defined how to select the correct list (StatusPurpose) when validating credentials that are using this StatusList2021Credential.
+ if len(target) != 1 {
+ return errors.New("single CredentialSubject expected")
+ }
+ cs := target[0]
+
+ if cs.Type != CredentialSubjectType {
+ return fmt.Errorf("credentialSubject.type '%s' is required", CredentialSubjectType)
+ }
+ if cs.StatusPurpose == "" {
+ return errors.New("credentialSubject.statusPurpose is required")
+ }
+ if cs.EncodedList == "" {
+ return errors.New("credentialSubject.encodedList is required")
+ }
+ }
+
+ return nil
+}
diff --git a/vcr/statuslist2021/validator_test.go b/vcr/statuslist2021/validator_test.go
new file mode 100644
index 0000000000..dd008fcf2a
--- /dev/null
+++ b/vcr/statuslist2021/validator_test.go
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 Nuts community
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package statuslist2021
+
+import (
+ "github.com/nuts-foundation/go-did"
+ "github.com/nuts-foundation/go-did/vc"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestStatusList2021CredentialValidator_Validate(t *testing.T) {
+ t.Run("ok", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ err := credentialValidator{}.Validate(cred)
+ assert.NoError(t, err)
+ })
+ t.Run("error - wraps defaultCredentialValidator", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.Context = []ssi.URI{credentialTypeURI}
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "default context is required")
+ })
+ t.Run("error - missing status list context", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.Context = []ssi.URI{vc.VCContextV1URI()}
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "context 'https://w3id.org/vc/status-list/2021/v1' is required")
+ })
+ t.Run("error - missing StatusList credential type", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.Type = []ssi.URI{vc.VerifiableCredentialTypeV1URI()}
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "type 'StatusList2021Credential' is required")
+ })
+ t.Run("error - invalid credential subject", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.CredentialSubject = []any{"{"}
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "json: cannot unmarshal string into Go value of type statuslist2021.CredentialSubject")
+ })
+ t.Run("error - wrong credential subject", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.CredentialSubject = []any{struct{}{}}
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "credentialSubject.type 'StatusList2021' is required")
+ })
+ t.Run("error - multiple credentialSubject", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.CredentialSubject = []any{CredentialSubject{}, CredentialSubject{}}
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "single CredentialSubject expected")
+ })
+ t.Run("error - missing credentialSubject.type", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.CredentialSubject[0].(*CredentialSubject).Type = ""
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "credentialSubject.type 'StatusList2021' is required")
+ })
+ t.Run("error - missing statusPurpose", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.CredentialSubject[0].(*CredentialSubject).StatusPurpose = ""
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "credentialSubject.statusPurpose is required")
+ })
+ t.Run("error - missing encodedList", func(t *testing.T) {
+ cred := ValidStatusList2021Credential(t)
+ cred.CredentialSubject[0].(*CredentialSubject).EncodedList = ""
+ err := credentialValidator{}.Validate(cred)
+ assert.EqualError(t, err, "credentialSubject.encodedList is required")
+ })
+}
diff --git a/vcr/vcr.go b/vcr/vcr.go
index 4fa7defb2f..9a3084fb70 100644
--- a/vcr/vcr.go
+++ b/vcr/vcr.go
@@ -29,7 +29,7 @@ import (
"github.com/nuts-foundation/nuts-node/pki"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/openid4vci"
- "github.com/nuts-foundation/nuts-node/vcr/statuslist"
+ "github.com/nuts-foundation/nuts-node/vcr/statuslist2021"
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"io/fs"
@@ -263,7 +263,7 @@ func (c *vcr) Configure(config core.ServerConfig) error {
c.walletHttpClient = core.NewStrictHTTPClient(config.Strictmode, c.config.OpenID4VCI.Timeout, tlsConfig)
c.openidSessionStore = c.storageClient.GetSessionDatabase()
}
- status, err := statuslist.NewStatusListStore(c.storageClient.GetSQLDatabase())
+ status, err := statuslist2021.NewStatusListStore(c.storageClient.GetSQLDatabase())
if err != nil {
return err
}
diff --git a/vcr/verifier/verifier.go b/vcr/verifier/verifier.go
index 71a6ce5f74..35c3011c0a 100644
--- a/vcr/verifier/verifier.go
+++ b/vcr/verifier/verifier.go
@@ -22,6 +22,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "github.com/nuts-foundation/nuts-node/vcr/statuslist2021"
"strings"
"time"
@@ -57,7 +58,7 @@ type verifier struct {
store Store
trustConfig *trust.Config
signatureVerifier
- credentialStatus *credentialStatus
+ credentialStatus *statuslist2021.CredentialStatus
}
// VerificationError is used to describe a VC/VP verification failure.
@@ -91,10 +92,8 @@ func NewVerifier(store Store, didResolver resolver.DIDResolver, keyResolver reso
keyResolver: keyResolver,
jsonldManager: jsonldManager,
}
- v.credentialStatus = &credentialStatus{
- client: client,
- verifySignature: v.signatureVerifier.VerifySignature,
- }
+ v.credentialStatus = statuslist2021.NewCredentialStatus(client, v.signatureVerifier.VerifySignature)
+
return v
}
@@ -126,7 +125,7 @@ func (v verifier) Verify(credentialToVerify vc.VerifiableCredential, allowUntrus
}
// Check the credentialStatus if the credential is revoked
- err := v.credentialStatus.verify(credentialToVerify)
+ err := v.credentialStatus.Verify(credentialToVerify)
if err != nil {
// soft fail, only return an error when revocation is confirmed and log everything else
if errors.Is(err, types.ErrRevoked) {
diff --git a/vcr/verifier/verifier_test.go b/vcr/verifier/verifier_test.go
index 09ad3e0025..387acf1214 100644
--- a/vcr/verifier/verifier_test.go
+++ b/vcr/verifier/verifier_test.go
@@ -22,6 +22,7 @@ import (
crypt "crypto"
"encoding/json"
"errors"
+ "github.com/nuts-foundation/nuts-node/vcr/statuslist2021"
"github.com/nuts-foundation/nuts-node/vcr/test"
"net/http"
"net/http/httptest"
@@ -133,7 +134,7 @@ func TestVerifier_Verify(t *testing.T) {
t.Run("validate credentialStatus", func(t *testing.T) {
// make StatusList2021Credential with a revocation bit set
- statusListCred := credential.ValidStatusList2021Credential(t)
+ statusListCred := statuslist2021.ValidStatusList2021Credential(t)
statusListCredBytes, err := json.Marshal(statusListCred)
require.NoError(t, err)
statusListIndex := 1 // bit 1 is set in slCred
@@ -144,9 +145,9 @@ func TestVerifier_Verify(t *testing.T) {
}))
// statusListEntry for credentialToValidate without statusListIndex
- slEntry := credential.StatusList2021Entry{
+ slEntry := statuslist2021.Entry{
ID: "https://example-com/credentials/status/3#statusListIndex",
- Type: credential.StatusList2021EntryType,
+ Type: statuslist2021.EntryType,
StatusPurpose: "revocation",
StatusListCredential: ts.URL,
}
@@ -155,7 +156,7 @@ func TestVerifier_Verify(t *testing.T) {
http.DefaultClient = ts.Client() // newMockContext sets credentialStatus.client to http.DefaultClient
ctx := newMockContext(t)
ctx.store.EXPECT().GetRevocations(gomock.Any()).Return([]*credential.Revocation{{}}, ErrNotFound).AnyTimes()
- ctx.verifier.credentialStatus.verifySignature = func(credentialToVerify vc.VerifiableCredential, validateAt *time.Time) error { return nil } // don't check signatures on 'downloaded' StatusList2021Credentials
+ ctx.verifier.credentialStatus = statuslist2021.NewCredentialStatus(http.DefaultClient, func(_ vc.VerifiableCredential, _ *time.Time) error { return nil }) // don't check signatures on 'downloaded' StatusList2021Credentials
cred := credential.ValidNutsOrganizationCredential(t)
cred.Context = append(cred.Context, ssi.MustParseURI(jsonld.W3cStatusList2021Context))
@@ -189,9 +190,9 @@ func TestVerifier_Verify(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(400)
}))
- slEntry := credential.StatusList2021Entry{
+ slEntry := statuslist2021.Entry{
ID: "not relevant",
- Type: credential.StatusList2021EntryType,
+ Type: statuslist2021.EntryType,
StatusPurpose: "revocation",
StatusListIndex: "1",
StatusListCredential: ts.URL, //