Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Statuslist: move all code to statuslist package #2804

Merged
merged 4 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions storage/gorm_logger.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
*/

package storage

import (
Expand Down
18 changes: 18 additions & 0 deletions storage/gorm_logger_test.go
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*
*/

package storage

import (
Expand Down
3 changes: 0 additions & 3 deletions vcr/credential/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
}
}
}
Expand Down
4 changes: 0 additions & 4 deletions vcr/credential/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
22 changes: 0 additions & 22 deletions vcr/credential/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
44 changes: 0 additions & 44 deletions vcr/credential/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package credential

import (
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/nuts-node/jsonld"
)

const (
Expand Down Expand Up @@ -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"`
}
52 changes: 5 additions & 47 deletions vcr/credential/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/statuslist"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"github.com/piprate/json-gold/ld"
)
Expand Down Expand Up @@ -146,16 +147,17 @@ func validateCredentialStatus(credential vc.VerifiableCredential) error {
}

// only accept StatusList2021EntryType for now
if credentialStatus.Type != StatusList2021EntryType {
if credentialStatus.Type != statuslist.StatusList2021EntryType {
continue
}
// TODO: AllFieldsDefined validator should be sufficient?

if !credential.ContainsContext(statusList2021ContextURI) {
if !credential.ContainsContext(statuslist.StatusList2021ContextURI) {
return errors.New("StatusList2021 context is required")
}

// unmarshal as StatusList2021Entry
var cs StatusList2021Entry
var cs statuslist.StatusList2021Entry
if err = json.Unmarshal(credentialStatus.Raw(), &cs); err != nil {
return err
}
Expand Down Expand Up @@ -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
}
81 changes: 10 additions & 71 deletions vcr/credential/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/statuslist"
"github.com/nuts-foundation/nuts-node/vdr"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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{}))
Expand All @@ -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(statuslist.StatusList2021EntryType, 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{&statuslist.StatusList2021Entry{
ID: "https://example-com/credentials/status/3#94567",
Type: StatusList2021EntryType,
Type: statuslist.StatusList2021EntryType,
StatusPurpose: "revocation",
StatusListIndex: "94567",
StatusListCredential: "https://example-com/credentials/status/3",
Expand All @@ -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].(*statuslist.StatusList2021Entry).ID = cred.CredentialStatus[0].(*statuslist.StatusList2021Entry).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].(*statuslist.StatusList2021Entry).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].(*statuslist.StatusList2021Entry).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].(*statuslist.StatusList2021Entry).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].(*statuslist.StatusList2021Entry).StatusListCredential = "not a URL"
err := validateCredentialStatus(cred)
assert.EqualError(t, err, "parse StatusList2021Entry.statusListCredential URL: parse \"not a URL\": invalid URI for request")
})
Expand Down
6 changes: 3 additions & 3 deletions vcr/issuer/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ 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 == statuslist.StatusList2021EntryType {
var slEntry statuslist.StatusList2021Entry
err = json.Unmarshal(status.Raw(), &slEntry)
if err != nil {
return err
Expand Down Expand Up @@ -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(statuslist.StatusList2021CredentialType)

func (i issuer) StatusList(ctx context.Context, issuerDID did.DID, page int) (*vc.VerifiableCredential, error) {
// todo: get cached credential if available
Expand Down
Loading
Loading