Skip to content

Commit

Permalink
VCR: JWT support for credentials and presentations
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Sep 29, 2023
1 parent 722e7a1 commit c77e82b
Show file tree
Hide file tree
Showing 21 changed files with 599 additions and 206 deletions.
9 changes: 7 additions & 2 deletions auth/api/auth/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
Expand Down Expand Up @@ -165,8 +166,12 @@ func TestWrapper_GetSignSessionStatus(t *testing.T) {

response, err := ctx.wrapper.GetSignSessionStatus(ctx.audit, sessionObj)

assert.Equal(t, expectedResponse, response)
assert.NoError(t, err)
require.NoError(t, err)
actualResponseJSON := httptest.NewRecorder()
require.NoError(t, response.VisitGetSignSessionStatusResponse(actualResponseJSON))
expectedResponseJSON := httptest.NewRecorder()
require.NoError(t, expectedResponse.VisitGetSignSessionStatusResponse(expectedResponseJSON))
assert.JSONEq(t, string(expectedResponseJSON.Body.Bytes()), string(actualResponseJSON.Body.Bytes()))
})

t.Run("nok - SigningSessionStatus returns error", func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion auth/services/oauth/authz_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ func TestService_validateAuthorizationCredentials(t *testing.T) {

err := ctx.oauthService.validateAuthorizationCredentials(tokenCtx)

assert.EqualError(t, err, "invalid jwt.vcs: cannot unmarshal authorization credential: json: cannot unmarshal string into Go value of type map[string]interface {}")
assert.EqualError(t, err, "invalid jwt.vcs: cannot unmarshal authorization credential: failed to parse token: invalid character '}' looking for beginning of value")
})

t.Run("error - jwt.iss <> credentialSubject.ID mismatch", func(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions auth/services/selfsigned/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/nuts-foundation/nuts-node/vcr"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/holder"
"github.com/nuts-foundation/nuts-node/vcr/issuer"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"net/url"
"time"
Expand Down Expand Up @@ -119,15 +120,15 @@ func (v *signer) createVP(ctx context.Context, s types.Session, issuanceDate tim
}

expirationData := issuanceDate.Add(24 * time.Hour)
credentialOptions := vc.VerifiableCredential{
template := vc.VerifiableCredential{
Context: []ssi.URI{credential.NutsV1ContextURI},
Type: []ssi.URI{ssi.MustParseURI(credentialType)},
Issuer: issuerID.URI(),
IssuanceDate: issuanceDate,
ExpirationDate: &expirationData,
CredentialSubject: s.CredentialSubject(),
}
verifiableCredential, err := v.vcr.Issuer().Issue(ctx, credentialOptions, false, false)
verifiableCredential, err := v.vcr.Issuer().Issue(ctx, template, issuer.CredentialOptions{})
if err != nil {
return nil, fmt.Errorf("issue VC failed: %w", err)
}
Expand Down
29 changes: 16 additions & 13 deletions auth/services/selfsigned/signer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ func TestSessionStore_SigningSessionStatus(t *testing.T) {
t.Run("status completed returns VP on SigningSessionResult", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewSigner(mockContext.vcr, "").(*signer)
mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), false, false).Return(&testVC, nil)
mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), issuer.CredentialOptions{
Publish: false,
Public: false,
}).Return(&testVC, nil)
mockContext.wallet.EXPECT().BuildPresentation(context.TODO(), gomock.Len(1), gomock.Any(), &employer, true).Return(&testVP, nil)

sp, err := ss.StartSigningSession(contract.Contract{RawContractText: testContract}, params)
Expand Down Expand Up @@ -191,16 +194,10 @@ func TestSessionStore_SigningSessionStatus(t *testing.T) {
t.Run("correct VC options are passed to issuer", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewSigner(mockContext.vcr, "").(*signer)
mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), false, false).DoAndReturn(
func(arg0 interface{}, unsignedCredential interface{}, public interface{}, publish interface{}) (*vc.VerifiableCredential, error) {
isPublic, ok := public.(bool)
isPublished, ok2 := publish.(bool)
credential, ok3 := unsignedCredential.(vc.VerifiableCredential)
require.True(t, ok)
require.True(t, ok2)
require.True(t, ok3)
assert.False(t, isPublic)
assert.False(t, isPublished)
mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), issuer.CredentialOptions{}).DoAndReturn(
func(arg0 interface{}, credential vc.VerifiableCredential, options issuer.CredentialOptions) (*vc.VerifiableCredential, error) {
assert.False(t, options.Public)
assert.False(t, options.Publish)
assert.Equal(t, employer.URI(), credential.Issuer)
assert.Equal(t, []ssi.URI{ssi.MustParseURI("NutsEmployeeCredential")}, credential.Type)

Expand Down Expand Up @@ -241,7 +238,10 @@ func TestSessionStore_SigningSessionStatus(t *testing.T) {
t.Run("error on VC issuance", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewSigner(mockContext.vcr, "").(*signer)
mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), false, false).Return(nil, errors.New("error"))
mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), issuer.CredentialOptions{
Publish: false,
Public: false,
}).Return(nil, errors.New("error"))

sp, err := ss.StartSigningSession(contract.Contract{RawContractText: testContract}, params)
require.NoError(t, err)
Expand All @@ -256,7 +256,10 @@ func TestSessionStore_SigningSessionStatus(t *testing.T) {
t.Run("error on building VP", func(t *testing.T) {
mockContext := newMockContext(t)
ss := NewSigner(mockContext.vcr, "").(*signer)
mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), false, false).Return(&testVC, nil)
mockContext.issuer.EXPECT().Issue(context.TODO(), gomock.Any(), issuer.CredentialOptions{
Publish: false,
Public: false,
}).Return(&testVC, nil)
mockContext.wallet.EXPECT().BuildPresentation(context.TODO(), gomock.Len(1), gomock.Any(), &employer, true).Return(nil, errors.New("error"))

sp, err := ss.StartSigningSession(contract.Contract{RawContractText: testContract}, params)
Expand Down
10 changes: 8 additions & 2 deletions auth/services/selfsigned/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ func TestSigner_Validator_Roundtrip(t *testing.T) {

// #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)
nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(issuerDID), issuer.CredentialOptions{
Publish: false,
Public: 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)
Expand Down Expand Up @@ -203,7 +206,10 @@ func TestValidator_VerifyVP(t *testing.T) {
// 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)
nutsOrgCred, err := vcrContext.VCR.Issuer().Issue(audit.TestContext(), createOrganizationCredential(didDocument.ID.String()), issuer.CredentialOptions{
Publish: false,
Public: 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)
Expand Down
14 changes: 14 additions & 0 deletions docs/_static/vcr/vcr_v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,13 @@ components:
description: RFC3339 time string until when the credential is valid.
type: string
example: "2012-01-02T12:00:00Z"
format:
description: Proof format for the credential (JSON-LD or JWT). If not set, it defaults to JSON-LD.
default: ldp_vc
type: string
enum:
- ldp_vc
- jwt_vc
publishToNetwork:
description: |
If set, the node publishes this credential to the network. This is the default behaviour.
Expand Down Expand Up @@ -587,6 +594,13 @@ components:
type: string
description: Date and time at which proof will expire. If omitted, the proof does not have an end date.
example: '2021-12-20T09:00:00Z'
format:
description: Proof format for the presentation (JSON-LD or JWT). If not set, it defaults to JSON-LD.
default: ldp_vp
type: string
enum:
- ldp_vp
- jwt_vp

VPVerificationRequest:
required:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/nats-io/nats-server/v2 v2.10.1
github.com/nats-io/nats.go v1.30.1
github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b
github.com/nuts-foundation/go-did v0.6.5
github.com/nuts-foundation/go-did v0.6.6-0.20230929063840-997f267c2776
github.com/nuts-foundation/go-leia/v4 v4.0.0
github.com/nuts-foundation/go-stoabs v1.9.0
// check the oapi-codegen tool version in the makefile when upgrading the runtime
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@ github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b h1:80
github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b/go.mod h1:6YUioYirD6/8IahZkoS4Ypc8xbeJW76Xdk1QKcziNTM=
github.com/nuts-foundation/go-did v0.6.5 h1:y2gPygRN1gBeMI9y8OIWwARp8NpHHheqnbpLwCxajFw=
github.com/nuts-foundation/go-did v0.6.5/go.mod h1:Jb3IgnO2Zeed970JMIlfjr4g1kvikmgWUJA0EfeDEFE=
github.com/nuts-foundation/go-did v0.6.6-0.20230929062946-723b4514eb2e h1:2KmobyTrKmyhXMR2es3qvVRqaZMGBHg2R3WI7F1rBpE=
github.com/nuts-foundation/go-did v0.6.6-0.20230929062946-723b4514eb2e/go.mod h1:Jb3IgnO2Zeed970JMIlfjr4g1kvikmgWUJA0EfeDEFE=
github.com/nuts-foundation/go-did v0.6.6-0.20230929063840-997f267c2776 h1:BJ2MyIj8U3mxplXzTLUVrUmStqlLVYA1LVrTFUqHZhE=
github.com/nuts-foundation/go-did v0.6.6-0.20230929063840-997f267c2776/go.mod h1:Jb3IgnO2Zeed970JMIlfjr4g1kvikmgWUJA0EfeDEFE=
github.com/nuts-foundation/go-leia/v4 v4.0.0 h1:/unYCk18qGG2HWcJK4ld4CaM6k7Tdr0bR1vQd1Jwfcg=
github.com/nuts-foundation/go-leia/v4 v4.0.0/go.mod h1:A246dA4nhY99OPCQpG/XbQ/iPyyfSaJchanivuPWpao=
github.com/nuts-foundation/go-stoabs v1.9.0 h1:zK+ugfolaJYyBvGwsRuavLVdycXk4Yw/1gI+tz17lWQ=
Expand Down
38 changes: 25 additions & 13 deletions vcr/api/vcr/v2/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"errors"
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/vcr/holder"
"github.com/nuts-foundation/nuts-node/vcr/issuer"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"net/http"

Expand Down Expand Up @@ -86,35 +87,33 @@ func (w *Wrapper) ResolveStatusCode(err error) int {

// IssueVC handles the API request for credential issuing.
func (w Wrapper) IssueVC(ctx context.Context, request IssueVCRequestObject) (IssueVCResponseObject, error) {
var (
publish bool
public bool
)

// publish is true by default
options := issuer.CredentialOptions{
Publish: true,
}
if request.Body.PublishToNetwork != nil {
publish = *request.Body.PublishToNetwork
} else {
publish = true
options.Publish = *request.Body.PublishToNetwork
}
if request.Body.Format != nil {
options.Format = string(*request.Body.Format)
}

// Check param constraints:
if request.Body.Visibility == nil || *request.Body.Visibility == "" {
if publish {
if options.Publish {
return nil, core.InvalidInputError("visibility must be set when publishing credential")
}
} else {
// visibility is set
// Visibility can only be used when publishing
if !publish {
if !options.Publish {
return nil, core.InvalidInputError("visibility setting is only allowed when publishing to the network")
}
// Check if the values are in range
if *request.Body.Visibility != Public && *request.Body.Visibility != Private {
return nil, core.InvalidInputError("invalid value for visibility")
}
// Set the actual value
public = *request.Body.Visibility == Public
options.Public = *request.Body.Visibility == Public
}

// Set default context, if not set
Expand All @@ -136,8 +135,17 @@ func (w Wrapper) IssueVC(ctx context.Context, request IssueVCRequestObject) (Iss
if err := json.Unmarshal(rawRequest, &requestedVC); err != nil {
return nil, err
}
// Copy parsed credential to keep control over what we pass to the issuer,
// (and also makes unit testing easier since vc.VerifiableCredential has unexported fields that can't be set).
template := vc.VerifiableCredential{
Context: requestedVC.Context,
Type: requestedVC.Type,
Issuer: requestedVC.Issuer,
ExpirationDate: requestedVC.ExpirationDate,
CredentialSubject: requestedVC.CredentialSubject,
}

vcCreated, err := w.VCR.Issuer().Issue(ctx, requestedVC, publish, public)
vcCreated, err := w.VCR.Issuer().Issue(ctx, template, options)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -253,6 +261,10 @@ func (w *Wrapper) CreateVP(ctx context.Context, request CreateVPRequestObject) (
presentationOptions.ProofOptions.ProofPurpose = string(purpose)
}

if request.Body.Format != nil {
presentationOptions.Format = string(*request.Body.Format)
}

// pass context and type as ssi.URI
if request.Body.Context != nil {
for _, sc := range *request.Body.Context {
Expand Down
27 changes: 19 additions & 8 deletions vcr/api/vcr/v2/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ func TestWrapper_IssueVC(t *testing.T) {
Visibility: &public,
}
// assert that credential.NutsV1ContextURI is added if the request does not contain @context
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Eq(expectedRequestedVC), true, true).Return(&expectedRequestedVC, nil)
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, expectedRequestedVC, issuer.CredentialOptions{
Publish: true,
Public: true,
}).Return(&expectedRequestedVC, nil)

response, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &request})

Expand Down Expand Up @@ -100,9 +103,8 @@ func TestWrapper_IssueVC(t *testing.T) {

public := Public
request := IssueVCRequest{
Type: expectedRequestedVC.Type[0].String(),
Issuer: expectedRequestedVC.Issuer.String(),
//CredentialSubject: expectedRequestedVC.CredentialSubject,
Type: expectedRequestedVC.Type[0].String(),
Issuer: expectedRequestedVC.Issuer.String(),
Visibility: &public,
}

Expand All @@ -129,7 +131,10 @@ func TestWrapper_IssueVC(t *testing.T) {
}
expectedVC := vc.VerifiableCredential{}
expectedResponse := IssueVC200JSONResponse(expectedVC)
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), true, false).Return(&expectedVC, nil)
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), issuer.CredentialOptions{
Publish: true,
Public: false,
}).Return(&expectedVC, nil)

response, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &request})

Expand All @@ -150,7 +155,10 @@ func TestWrapper_IssueVC(t *testing.T) {
}
expectedVC := vc.VerifiableCredential{}
expectedResponse := IssueVC200JSONResponse(expectedVC)
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), true, true).Return(&expectedVC, nil)
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), issuer.CredentialOptions{
Publish: true,
Public: true,
}).Return(&expectedVC, nil)

response, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &request})

Expand Down Expand Up @@ -219,7 +227,10 @@ func TestWrapper_IssueVC(t *testing.T) {
}
expectedVC := vc.VerifiableCredential{}
expectedResponse := IssueVC200JSONResponse(expectedVC)
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), false, false).Return(&expectedVC, nil)
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), issuer.CredentialOptions{
Publish: false,
Public: false,
}).Return(&expectedVC, nil)

response, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &request})

Expand Down Expand Up @@ -267,7 +278,7 @@ func TestWrapper_IssueVC(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
testContext := newMockContext(t)

testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, test.err)
testContext.mockIssuer.EXPECT().Issue(testContext.requestCtx, gomock.Any(), gomock.Any()).Return(nil, test.err)

_, err := testContext.client.IssueVC(testContext.requestCtx, IssueVCRequestObject{Body: &validIssueRequest})

Expand Down
Loading

0 comments on commit c77e82b

Please sign in to comment.