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: JWT support for credentials and presentations #2520

Merged
merged 10 commits into from
Oct 20, 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
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
2 changes: 1 addition & 1 deletion crypto/jwx.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import (
// ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported
var ErrUnsupportedSigningKey = errors.New("signing key algorithm not supported")

var supportedAlgorithms = []jwa.SignatureAlgorithm{jwa.PS256, jwa.PS384, jwa.PS512, jwa.ES256, jwa.ES384, jwa.ES512}
var supportedAlgorithms = []jwa.SignatureAlgorithm{jwa.PS256, jwa.PS384, jwa.PS512, jwa.ES256, jwa.EdDSA, jwa.ES384, jwa.ES512}
reinkrul marked this conversation as resolved.
Show resolved Hide resolved

const defaultRsaEncryptionAlgorithm = jwa.RSA_OAEP_256
const defaultEcEncryptionAlgorithm = jwa.ECDH_ES_A256KW
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 (ldp_vc for JSON-LD or jwt_vc for 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
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Nuts documentation
pages/integrating/api.rst
pages/integrating/api-authentication.rst
pages/integrating/vc.rst
pages/integrating/supported-protocols-formats.rst
pages/integrating/faq-errors.rst
pages/release_notes.rst
pages/roadmap.rst
Expand Down
40 changes: 40 additions & 0 deletions docs/pages/integrating/supported-protocols-formats.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.. _supported_protocols_and_formats:

Supported Protocols and Formats
===============================

This page documents which cryptographic algorithms, key types and SSI formats and protocols are supported.

Cryptographic Algorithms
************************
The following cryptographic signing algorithms are supported:

- ECDSA with the NIST P-256, P-384 and P-512 curves.
- EdDSA with Ed25519 curves.
- RSASSA-PSS RSA with keys of at least 2048 bits.

The following encryption algorithms are supported:

- RSA-OAEP-SHA256 (min. 2048 bits)
- ECDH-ES+A256KW
- AES-GCM-256

DID methods
***********

The following DID methods are supported:

- ``did:nuts`` (creating and resolving)
- ``did:web`` (creating and resolving)
- ``did:key`` (resolving)
- ``did:jwk`` (resolving)

Credentials
***********

`W3C Verifiable Credentials v1 <https://www.w3.org/TR/vc-data-model/>`_ and Presentations are supported (issuing and verifying) in JSON-LD and JWT format.

The following protocols are being implemented (work in progress):
- OpenID4VP verifier and SIOPv2 relying party for requesting a presentation from a wallet.
- OpenID4VCI issuer for issuing a credential to a wallet.
- OpenID4VCI wallet for receiving a credential from an issuer.
3 changes: 3 additions & 0 deletions docs/pages/integrating/vc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ Will be expanded by the node to:

The `visibility` property indicates the contents of the VC are published on the network, so it can be read by everyone.

By default, the node will create credentials in JSON-LD format.
You can specify the format by passing the `format` parameter (``jwt_vc`` or ``ldp_vc``).

.. _searching-vcs:

Searching VCs
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/nats-io/nats-server/v2 v2.10.3
github.com/nats-io/nats.go v1.31.0
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.7.1
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 Expand Up @@ -125,6 +125,9 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mr-tron/base58 v1.1.3 // indirect
github.com/multiformats/go-base32 v0.0.3 // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multihash v0.0.11 // indirect
github.com/nats-io/jwt/v2 v2.5.2 // indirect
github.com/nats-io/nkeys v0.4.5 // indirect
Expand Down
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,12 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI=
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4=
github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
github.com/multiformats/go-multihash v0.0.11 h1:yEyBxwoR/7vBM5NfLVXRnpQNVLrMhpS6MRb7Z/1pnzc=
Expand All @@ -438,8 +444,8 @@ github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatR
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b h1:80icUxWHwE1MrIOOEK5rxrtyKOgZeq5Iu1IjAEkggTY=
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.7.1 h1:JKn9QMuOq4eXHPGdYgsSk3XOxRDBWHkPPFYLFbYut0Y=
github.com/nuts-foundation/go-did v0.7.1/go.mod h1:fq65EPzzpdxD+WG5VFqMfbVaADMwbEUB4CpgPajp1LM=
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
Loading
Loading