Skip to content

Commit

Permalink
NutsEmployeeCredential does not need to be trusted (v5.3 backport) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul authored Aug 20, 2023
1 parent af17590 commit cf5462f
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 56 deletions.
22 changes: 21 additions & 1 deletion auth/services/selfsigned/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
package selfsigned

import (
"context"
"encoding/json"
"errors"
"fmt"
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/auth/contract"
"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/auth/services/selfsigned/types"
"github.com/nuts-foundation/nuts-node/jsonld"
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/verifier"
"time"
Expand Down Expand Up @@ -92,7 +95,8 @@ func (v validator) VerifyVP(vp vc.VerifiablePresentation, validAt *time.Time) (c
}

func (v validator) verifyVP(vp vc.VerifiablePresentation, validAt *time.Time) (credentialSubject types.EmployeeIdentityCredentialSubject, proof vc.JSONWebSignature2020Proof, resultErr error) {
vcs, err := v.vcr.Verifier().VerifyVP(vp, true, validAt)
// #2428: NutsEmployeeCredential should be valid (signature), but does not need to be trusted.
vcs, err := v.vcr.Verifier().VerifyVP(vp, true, true, validAt)
if err != nil {
if errors.As(err, &verifier.VerificationError{}) {
resultErr = newVerificationError(err.Error())
Expand Down Expand Up @@ -135,6 +139,22 @@ func (v validator) verifyVP(vp vc.VerifiablePresentation, validAt *time.Time) (c
return
}

// #2428: NutsEmployeeCredential trust is derived from the fact that the issuer has a trusted NutsOrganizationCredential
searchTerms := []vcr.SearchTerm{
{IRIPath: jsonld.CredentialSubjectPath, Value: credentialSubject.ID},
{IRIPath: jsonld.OrganizationNamePath, Type: vcr.NotNil},
{IRIPath: jsonld.OrganizationCityPath, Type: vcr.NotNil},
}
nutsOrgCreds, err := v.vcr.Search(context.TODO(), searchTerms, false, validAt)
if err != nil {
resultErr = fmt.Errorf("unable to check NutsEmployeeCredential trust status using NutsOrganizationCredential: %w", err)
return
}
if len(nutsOrgCreds) == 0 {
resultErr = newVerificationError("NutsEmployeeCredential rejected, issuer does not have a trusted NutsOrganizationCredential")
return
}

return credentialSubject, proof, nil
}

Expand Down
107 changes: 89 additions & 18 deletions auth/services/selfsigned/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import (
"context"
"encoding/json"
"errors"
"github.com/golang/mock/gomock"
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/auth/services/selfsigned/types"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/crypto/util"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/issuer"
"github.com/nuts-foundation/nuts-node/vcr/verifier"
"os"
Expand Down Expand Up @@ -74,10 +76,11 @@ func TestSigner_Validator_Roundtrip(t *testing.T) {
}
signerService := NewSigner(vcrContext.VCR, "http://localhost").(*signer)
roleName := "Administrator"
issuerDID := "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm"
createdVP, err := signerService.createVP(audit.TestContext(), types.Session{
ExpiresAt: issuanceDate.Add(time.Hour * 24),
Contract: testContract,
Employer: "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm",
Employer: issuerDID,
Employee: types.Employee{
Identifier: "[email protected]",
RoleName: &roleName,
Expand All @@ -86,6 +89,15 @@ func TestSigner_Validator_Roundtrip(t *testing.T) {
}}, issuanceDate)
require.NoError(t, err)

// #2428: NutsEmployeeCredential does not need to be trusted, but the issuer needs to have a trusted NutsOrganizationCredential (chain of trust).
// Issue() automatically trusts the issuer, so untrust it for asserting trust chain behavior
nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(issuerDID), false, false)
require.NoError(t, err)
err = vcrContext.VCR.StoreCredential(*nutsOrgCred, nil) // Need to explicitly store, since we didn't publish it.
require.NoError(t, err)
err = vcrContext.VCR.Untrust(ssi.MustParseURI(credentialType), did.MustParseDID(issuerDID).URI())
require.NoError(t, err)

// Validate VP
validatorService := NewValidator(vcrContext.VCR, contract.StandardContractTemplates)
checkTime := issuanceDate.Add(time.Minute)
Expand All @@ -107,7 +119,8 @@ func TestValidator_VerifyVP(t *testing.T) {
t.Run("ok using mocks", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, &vpValidTime).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, &vpValidTime).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{testCredential}, nil)

result, err := ss.VerifyVP(vp, &vpValidTime)

Expand All @@ -128,7 +141,8 @@ func TestValidator_VerifyVP(t *testing.T) {
credentialWithoutRole := vc.VerifiableCredential{}
data, _ := os.ReadFile("./test/vc-without-role.json")
_ = json.Unmarshal(data, &credentialWithoutRole)
mockContext.verifier.EXPECT().VerifyVP(vp, true, &vpValidTime).Return([]vc.VerifiableCredential{credentialWithoutRole}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, &vpValidTime).Return([]vc.VerifiableCredential{credentialWithoutRole}, nil)
mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{testCredential}, nil)

result, err := ss.VerifyVP(vp, &vpValidTime)

Expand All @@ -139,7 +153,7 @@ func TestValidator_VerifyVP(t *testing.T) {
t.Run("technical error on verify", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return(nil, errors.New("error"))
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return(nil, errors.New("error"))

_, err := ss.VerifyVP(vp, nil)

Expand All @@ -149,7 +163,7 @@ func TestValidator_VerifyVP(t *testing.T) {
t.Run("verification error on verify", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return(nil, verifier.VerificationError{})
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return(nil, verifier.VerificationError{})

result, err := ss.VerifyVP(vp, nil)

Expand All @@ -159,18 +173,40 @@ func TestValidator_VerifyVP(t *testing.T) {
})

t.Run("ok using in-memory DBs", func(t *testing.T) {
vcrContext := vcr.NewTestVCRContext(t, crypto.NewMemoryCryptoInstance())
keyStore := crypto.NewMemoryStorage()
vcrContext := vcr.NewTestVCRContext(t, crypto.NewTestCryptoInstance(keyStore))
var didDocument did.Document
{
ddBytes, _ := os.ReadFile("./test/diddocument.json")
err := json.Unmarshal(ddBytes, &didDocument)
require.NoError(t, err)
}
{
// Load private key so we can sign
privateKeyData, _ := os.ReadFile("./test/private.pem")
privateKey, err := util.PemToPrivateKey(privateKeyData)
require.NoError(t, err)
err = keyStore.SavePrivateKey(context.Background(), didDocument.VerificationMethod[0].ID.String(), privateKey)
require.NoError(t, err)
}

ss := NewValidator(vcrContext.VCR, contract.StandardContractTemplates)
didDocument := did.Document{}
ddBytes, _ := os.ReadFile("./test/diddocument.json")
_ = json.Unmarshal(ddBytes, &didDocument)
// test transaction for DIDStore ordering
tx := didstore.TestTransaction(didDocument)
tx.SigningTime = docTXTime
err := vcrContext.DIDStore.Add(didDocument, tx)
require.NoError(t, err)
// Trust issuer, only needed for test
vcrContext.VCR.Trust(ssi.MustParseURI(credentialType), didDocument.ID.URI())
// #2428: NutsEmployeeCredential issuer needs a trusted NutsOrganizationCredential
issuer.TimeFunc = func() time.Time {
// Issued credentials get the current date/time as issuance date,
// need to set it to a fixed value that corresponds with vpValidTime for testing.
// Otherwise, the NutsOrganizationCredential is not yet valid or might be expired.
return vpValidTime.Add(-1 * time.Hour)
}
nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(didDocument.ID.String()), false, false)
require.NoError(t, err)
err = vcrContext.VCR.StoreCredential(*nutsOrgCred, &vpValidTime) // Need to explicitly store, since we didn't publish it.
require.NoError(t, err)

result, err := ss.VerifyVP(vp, &vpValidTime)

Expand All @@ -185,7 +221,8 @@ func TestValidator_VerifyVP(t *testing.T) {
vpData, _ := os.ReadFile("./test/vp_invalid_contract.json")
_ = json.Unmarshal(vpData, &vp)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{testCredential}, nil)

result, err := ss.VerifyVP(vp, nil)

Expand All @@ -198,7 +235,8 @@ func TestValidator_VerifyVP(t *testing.T) {
mockContext := newMockContext(t)
now := time.Now()
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, &now).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, &now).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{testCredential}, nil)

result, err := ss.VerifyVP(vp, &now)

Expand All @@ -210,7 +248,7 @@ func TestValidator_VerifyVP(t *testing.T) {
t.Run("error - missing credential", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{}, nil)

result, err := ss.VerifyVP(vp, nil)

Expand All @@ -225,7 +263,7 @@ func TestValidator_VerifyVP(t *testing.T) {
vpData, _ := os.ReadFile("./test/vp_missing_proof.json")
_ = json.Unmarshal(vpData, &vp)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil)

result, err := ss.VerifyVP(vp, nil)

Expand All @@ -240,7 +278,7 @@ func TestValidator_VerifyVP(t *testing.T) {
vpData, _ := os.ReadFile("./test/vp_incorrect_proof_type.json")
_ = json.Unmarshal(vpData, &vp)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil)

result, err := ss.VerifyVP(vp, nil)

Expand All @@ -255,7 +293,7 @@ func TestValidator_VerifyVP(t *testing.T) {
vpData, _ := os.ReadFile("./test/vp_incorrect_signer.json")
_ = json.Unmarshal(vpData, &vp)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{testCredential}, nil)

result, err := ss.VerifyVP(vp, nil)

Expand All @@ -272,14 +310,29 @@ func TestValidator_VerifyVP(t *testing.T) {
_ = json.Unmarshal(vpData, &vp)
credential.Issuer = did.MustParseDID("did:nuts:a").URI()
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
mockContext.verifier.EXPECT().VerifyVP(vp, true, nil).Return([]vc.VerifiableCredential{credential}, nil)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, nil).Return([]vc.VerifiableCredential{credential}, nil)

result, err := ss.VerifyVP(vp, nil)

require.NoError(t, err)
assert.Equal(t, contract.Invalid, result.Validity())
assert.Equal(t, "signer must be credentialSubject", result.Reason())
})

t.Run("error - issuer does not have trusted NutsOrganizationCredential", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewValidator(mockContext.vcr, contract.StandardContractTemplates)
credentialWithoutRole := vc.VerifiableCredential{}
data, _ := os.ReadFile("./test/vc-without-role.json")
_ = json.Unmarshal(data, &credentialWithoutRole)
mockContext.verifier.EXPECT().VerifyVP(vp, true, true, &vpValidTime).Return([]vc.VerifiableCredential{credentialWithoutRole}, nil)
mockContext.vcr.EXPECT().Search(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]vc.VerifiableCredential{}, nil)

result, err := ss.VerifyVP(vp, &vpValidTime)

require.NoError(t, err)
assert.Empty(t, result.DisclosedAttribute(services.UserRoleClaim))
})
}

func Test_validateRequiredAttributes(t *testing.T) {
Expand Down Expand Up @@ -378,3 +431,21 @@ func Test_selfsignedVerificationResult(t *testing.T) {
assert.Equal(t, "test2", vr.DisclosedAttribute("dAttr1"))
})
}

func createOrganizationCredential(issuerDID string) vc.VerifiableCredential {
orgCred := vc.VerifiableCredential{
Context: []ssi.URI{credential.NutsV1ContextURI},
Type: []ssi.URI{ssi.MustParseURI("NutsOrganizationCredential")},
Issuer: did.MustParseDID(issuerDID).URI(),
CredentialSubject: []interface{}{
credential.NutsOrganizationCredentialSubject{
ID: issuerDID,
Organization: map[string]string{
"name": "CareBears",
"city": "CareTown",
},
},
},
}
return orgCred
}
10 changes: 10 additions & 0 deletions docs/pages/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
Release notes
#############

************************
Hazelnut update (v5.3.2)
************************

Release date: 2023-08-20

- Fixed issue where NutsEmployeeCredentials needed to be explicitly trusted when issued by another node

**Full Changelog**: https://github.com/nuts-foundation/nuts-node/compare/v5.3.1...v5.3.2

************************
Hazelnut update (v5.3.1)
************************
Expand Down
23 changes: 20 additions & 3 deletions e2e-tests/auth/selfsigned/apps/selfsigned.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
authAPI "github.com/nuts-foundation/nuts-node/auth/api/auth/v1/client"
"github.com/nuts-foundation/nuts-node/core"
"github.com/rs/zerolog/log"
vcrAPI "github.com/nuts-foundation/nuts-node/vcr/api/vcr/v2"
)

var NodeClientConfig = core.ClientConfig{Address: "http://localhost:1323"}
Expand Down Expand Up @@ -91,12 +92,28 @@ func (s SelfSigned) GetSessionStatus(sessionID string) (string, *authAPI.Verifia
return response.JSON200.Status, response.JSON200.VerifiablePresentation, nil
}

func (s SelfSigned) RequestAccessToken(organizationDID string, purposeOfUse string, presentation *authAPI.VerifiablePresentation) (*authAPI.TokenIntrospectionResponse, error) {
func (s SelfSigned) UntrustEmployeeCredential(issuerDID string) error {
vcrClient, _ := vcrAPI.NewClient(NodeClientConfig.Address)
resp, err := vcrClient.UntrustIssuer(s.Context, vcrAPI.UntrustIssuerJSONRequestBody{
CredentialType: "NutsEmployeeCredential",
Issuer: issuerDID,
})
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
return fmt.Errorf("could not untrust issuer: %s", resp.Status)
}
return nil
}

func (s SelfSigned) RequestAccessToken(requestorDID string, authorizerDID, purposeOfUse string, presentation *authAPI.VerifiablePresentation) (*authAPI.TokenIntrospectionResponse, error) {
authClient, _ := authAPI.NewClient(s.URL)
accessTokenResponse, err := authClient.RequestAccessToken(s.Context, authAPI.RequestAccessTokenJSONRequestBody{
Authorizer: organizationDID,
Authorizer: authorizerDID,
Identity: presentation,
Requester: organizationDID,
Requester: requestorDID,
Service: purposeOfUse,
})
if err != nil {
Expand Down
Loading

0 comments on commit cf5462f

Please sign in to comment.