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

VCR: Don't check VC existence when revoking #1913

Merged
merged 4 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions auth/services/oauth/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ func TestService_validateAuthorizationCredentials(t *testing.T) {
}

assert.NotNil(t, tokenCtx.credentialIDs)
assert.Equal(t, "did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#1", tokenCtx.credentialIDs[0])
assert.Equal(t, "did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#38E90E8C-F7E5-4333-B63A-F9DD155A0272", tokenCtx.credentialIDs[0])
})

t.Run("ok - no authorization credentials", func(t *testing.T) {
Expand Down Expand Up @@ -482,7 +482,7 @@ func TestService_validateAuthorizationCredentials(t *testing.T) {
if !assert.Error(t, err) {
return
}
assert.EqualError(t, err, "credentialSubject.ID did:nuts:B8PUHs2AUHbFF1xLLK4eZjgErEcMXHxs68FteY7NDtCY of authorization credential with ID: did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#1 does not match jwt.iss: unknown")
assert.EqualError(t, err, "credentialSubject.ID did:nuts:B8PUHs2AUHbFF1xLLK4eZjgErEcMXHxs68FteY7NDtCY of authorization credential with ID: did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#38E90E8C-F7E5-4333-B63A-F9DD155A0272 does not match jwt.iss: unknown")
})

t.Run("error - jwt.sub <> issuer mismatch", func(t *testing.T) {
Expand All @@ -496,7 +496,7 @@ func TestService_validateAuthorizationCredentials(t *testing.T) {
if !assert.Error(t, err) {
return
}
assert.EqualError(t, err, "issuer did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW of authorization credential with ID: did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#1 does not match jwt.sub: unknown")
assert.EqualError(t, err, "issuer did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW of authorization credential with ID: did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#38E90E8C-F7E5-4333-B63A-F9DD155A0272 does not match jwt.sub: unknown")
})

t.Run("error - invalid credential", func(t *testing.T) {
Expand Down
13 changes: 13 additions & 0 deletions docs/pages/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ Release notes

What has been changed, and how to update between versions.

***********************
Coconut update (v5.0.9)
***********************

Release date: 2023-02-21

This patch release fixes the following:

- Validations performed when revoking a VC are now more lenient: don't check whether it can actually find the VC in the issuer's database.
Enables issuers to revoke VCs even if they've lost track of them (e.g. incorrect database backup/restore).

**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v5.0.8...v5.0.9

***********************
Coconut update (v5.0.8)
***********************
Expand Down
6 changes: 3 additions & 3 deletions vcr/credential/revocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ var nowFunc = time.Now
var RevocationType = ssi.MustParseURI("CredentialRevocation")

// BuildRevocation generates a revocation based on the credential
func BuildRevocation(credential vc.VerifiableCredential) Revocation {
func BuildRevocation(issuer ssi.URI, subject ssi.URI) Revocation {
nutsCredentialContext := ssi.MustParseURI("https://nuts.nl/credentials/v1")
return Revocation{
Context: []ssi.URI{nutsCredentialContext},
Type: []ssi.URI{RevocationType},
Issuer: credential.Issuer,
Subject: *credential.ID,
Issuer: issuer,
Subject: subject,
Date: nowFunc(),
}
}
Expand Down
2 changes: 1 addition & 1 deletion vcr/credential/revocation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestBuildRevocation(t *testing.T) {
nowFunc = time.Now
}()

r := BuildRevocation(target)
r := BuildRevocation(target.Issuer, *target.ID)

assert.Equal(t, *target.ID, r.Subject)
assert.Equal(t, target.Issuer, r.Issuer)
Expand Down
2 changes: 1 addition & 1 deletion vcr/credential/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
)

func ValidNutsAuthorizationCredential() *vc.VerifiableCredential {
id := stringToURI(vdr.TestDIDA.String() + "#1")
id := stringToURI(vdr.TestDIDA.String() + "#38E90E8C-F7E5-4333-B63A-F9DD155A0272")
return &vc.VerifiableCredential{
Context: []ssi.URI{vc.VCContextV1URI(), NutsV1ContextURI},
ID: &id,
Expand Down
36 changes: 22 additions & 14 deletions vcr/issuer/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,10 @@ func (i issuer) buildVC(credentialOptions vc.VerifiableCredential) (*vc.Verifiab
}

func (i issuer) Revoke(credentialID ssi.URI) (*credential.Revocation, error) {
// first find it using a query on id.
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
credentialToRevoke, err := i.store.GetCredential(credentialID)
if err != nil {
return nil, fmt.Errorf("could not revoke (id=%s): %w", credentialID, err)
}

// Previously we first tried to resolve the credential, but that's not necessary:
// if the credential doesn't actually exist the revocation doesn't apply to anything, no harm done.
// Although it is a bit ugly, it helps issuers to revoke credentials that they don't have anymore,
// for whatever reason (e.g. incorrect database backup/restore).
isRevoked, err := i.isRevoked(credentialID)
if err != nil {
return nil, fmt.Errorf("error while checking revocation status: %w", err)
Expand All @@ -182,7 +180,7 @@ func (i issuer) Revoke(credentialID ssi.URI) (*credential.Revocation, error) {
return nil, vcr.ErrRevoked
}

revocation, err := i.buildRevocation(*credentialToRevoke)
revocation, err := i.buildRevocation(credentialID)
if err != nil {
return nil, err
}
Expand All @@ -198,14 +196,24 @@ func (i issuer) Revoke(credentialID ssi.URI) (*credential.Revocation, error) {
}

log.Logger().
WithField(core.LogFieldCredentialID, credentialToRevoke.ID).
WithField(core.LogFieldCredentialID, credentialID).
Info("Verifiable Credential revoked")
return revocation, nil
}

func (i issuer) buildRevocation(credentialToRevoke vc.VerifiableCredential) (*credential.Revocation, error) {
// find issuer
issuerDID, err := did.ParseDID(credentialToRevoke.Issuer.String())
func (i issuer) buildRevocation(credentialID ssi.URI) (*credential.Revocation, error) {
// Sanity check: since we don't check existence of the VC, at least somewhat guard against mistyped credential IDs
// (although nobody should be typing those in).
_, err := uuid.Parse(credentialID.Fragment)
if err != nil {
return nil, core.InvalidInputError("invalid credential ID")
}

// find issuer from credential ID
issuer := credentialID
issuer.Path = ""
issuer.Fragment = ""
issuerDID, err := did.ParseDID(issuer.String())
if err != nil {
return nil, fmt.Errorf("failed to extract issuer: %w", err)
}
Expand All @@ -215,12 +223,12 @@ func (i issuer) buildRevocation(credentialToRevoke vc.VerifiableCredential) (*cr
const errString = "failed to revoke credential (%s): could not resolve an assertionKey for issuer: %w"
// Differentiate between a DID document not found and some other error:
if errors.Is(err, vdr.ErrNotFound) {
return nil, core.InvalidInputError(errString, credentialToRevoke.ID, err)
return nil, core.InvalidInputError(errString, credentialID, err)
}
return nil, fmt.Errorf(errString, credentialToRevoke.ID, err)
return nil, fmt.Errorf(errString, credentialID, err)
}
// set defaults
revocation := credential.BuildRevocation(credentialToRevoke)
revocation := credential.BuildRevocation(issuerDID.URI(), credentialID)

revocationAsMap := map[string]interface{}{}
b, _ := json.Marshal(revocation)
Expand Down
76 changes: 37 additions & 39 deletions vcr/issuer/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ func Test_issuer_buildRevocation(t *testing.T) {
Issuer: issuerDID.URI(),
ID: &credentialID,
}
revocation, err := sut.buildRevocation(credentialToRevoke)
revocation, err := sut.buildRevocation(*credentialToRevoke.ID)
assert.NoError(t, err)
t.Logf("revocation %+v", revocation)
})
Expand Down Expand Up @@ -352,7 +352,7 @@ _:c14n0 <https://www.w3.org/2018/credentials#issuer> <did:nuts:123> .
keyResolverMock.EXPECT().ResolveAssertionKey(issuerDID).Return(nil, errors.New("b00m!"))
sut := issuer{keyResolver: keyResolverMock}

_, err := sut.buildRevocation(testVC)
_, err := sut.buildRevocation(*testVC.ID)
assert.EqualError(t, err, fmt.Sprintf("failed to revoke credential (%s): could not resolve an assertionKey for issuer: b00m!", testVC.ID))
})

Expand All @@ -364,15 +364,15 @@ _:c14n0 <https://www.w3.org/2018/credentials#issuer> <did:nuts:123> .
keyResolverMock.EXPECT().ResolveAssertionKey(issuerDID).Return(nil, vdr.ErrNotFound)
sut := issuer{keyResolver: keyResolverMock}

_, err := sut.buildRevocation(testVC)
_, err := sut.buildRevocation(*testVC.ID)
assert.ErrorIs(t, err, core.InvalidInputError("failed to revoke credential: could not resolve an assertionKey for issuer: unable to find the DID document"))
})

})

}
func Test_issuer_Revoke(t *testing.T) {
credentialID := "did:nuts:123#abc"
credentialID := "did:nuts:123#38E90E8C-F7E5-4333-B63A-F9DD155A0272"
credentialURI := ssi.MustParseURI(credentialID)
issuerID := "did:nuts:123"
issuerURI := ssi.MustParseURI(issuerID)
Expand All @@ -382,16 +382,8 @@ func Test_issuer_Revoke(t *testing.T) {
key := crypto.NewTestKey(kid.String())

t.Run("for a known credential", func(t *testing.T) {
credentialToRevoke := func() *vc.VerifiableCredential {
return &vc.VerifiableCredential{
ID: &credentialURI,
Issuer: issuerURI,
}
}

storeWithActualCredential := func(c *gomock.Controller) *MockStore {
store := NewMockStore(c)
store.EXPECT().GetCredential(credentialURI).Return(credentialToRevoke(), nil)
store.EXPECT().GetRevocation(credentialURI).Return(nil, ErrNotFound)
return store
}
Expand Down Expand Up @@ -434,20 +426,48 @@ func Test_issuer_Revoke(t *testing.T) {
assert.Equal(t, kid, revocation.Proof.VerificationMethod)
})

t.Run("it handles a buildRevocation error", func(t *testing.T) {
t.Run("error - unable to check revocation status", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// the credential does not contain a valid issuer:
invalidCredential := vc.VerifiableCredential{}
// GetRevocation fails
store := NewMockStore(ctrl)
store.EXPECT().GetCredential(credentialURI).Return(&invalidCredential, nil)
store.EXPECT().GetRevocation(credentialURI).Return(nil, ErrNotFound)
store.EXPECT().GetRevocation(credentialURI).Return(nil, errors.New("oops"))

sut := issuer{
store: store,
}
revocation, err := sut.Revoke(credentialURI)
assert.EqualError(t, err, "error while checking revocation status: oops")
assert.Nil(t, revocation)
})

t.Run("error - invalid credential ID (fragment is not a UUID)", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

store := NewMockStore(ctrl)
store.EXPECT().GetRevocation(gomock.Any()).Return(nil, ErrNotFound)

sut := issuer{
store: store,
}
revocation, err := sut.Revoke(ssi.MustParseURI("did:nuts:123#invalid"))
assert.EqualError(t, err, "invalid credential ID")
assert.Nil(t, revocation)
})

t.Run("error - invalid DID", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

store := NewMockStore(ctrl)
store.EXPECT().GetRevocation(gomock.Any()).Return(nil, ErrNotFound)

sut := issuer{
store: store,
}
revocation, err := sut.Revoke(ssi.MustParseURI("a#38E90E8C-F7E5-4333-B63A-F9DD155A0272"))
assert.EqualError(t, err, "failed to extract issuer: invalid DID: input length is less than 7")
assert.Nil(t, revocation)
})
Expand Down Expand Up @@ -481,7 +501,6 @@ func Test_issuer_Revoke(t *testing.T) {
publisher.EXPECT().PublishRevocation(gomock.Any()).Return(nil)
store.EXPECT().StoreRevocation(gomock.Any()).Return(nil)
// 2nd revocation
store.EXPECT().GetCredential(credentialURI).Return(credentialToRevoke(), nil)
store.EXPECT().GetRevocation(credentialURI).Return(&credential.Revocation{}, nil)

sut := issuer{
Expand All @@ -501,27 +520,6 @@ func Test_issuer_Revoke(t *testing.T) {
assert.Nil(t, revocation)
})
})

t.Run("for an unknown credential", func(t *testing.T) {
storeWithoutCredential := func(c *gomock.Controller) Store {
store := NewMockStore(c)
store.EXPECT().GetCredential(credentialURI).Return(nil, ErrNotFound)
return store
}

t.Run("it returns an error", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

sut := issuer{
store: storeWithoutCredential(ctrl),
}

revocation, err := sut.Revoke(credentialURI)
assert.EqualError(t, err, "could not revoke (id=did:nuts:123#abc): not found")
assert.Nil(t, revocation)
})
})
}

func TestIssuer_isRevoked(t *testing.T) {
Expand Down