From f3e17c641ee9cf20fb27ccbf400c71764a1a37eb Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 18 Oct 2023 12:02:31 +0200 Subject: [PATCH 1/5] Make sure JWT VCs and VPs marshal into the correct format --- vc/vc.go | 16 +++++++++++++--- vc/vc_test.go | 14 ++++++++++++-- vc/vp.go | 26 ++++++++++++++------------ vc/vp_test.go | 2 +- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/vc/vc.go b/vc/vc.go index f1a8935..1d71292 100644 --- a/vc/vc.go +++ b/vc/vc.go @@ -37,9 +37,9 @@ func VCContextV1URI() ssi.URI { const ( // JSONLDCredentialProofFormat is the format for JSON-LD based credentials. JSONLDCredentialProofFormat string = "ldp_vc" - // JWTCredentialsProofFormat is the format for JWT based credentials. + // JWTCredentialProofFormat is the format for JWT based credentials. // Note: various specs have not yet decided on the exact const (jwt_vc or jwt_vc_json, etc), so this is subject to change. - JWTCredentialsProofFormat = "jwt_vc" + JWTCredentialProofFormat = "jwt_vc" ) var errCredentialSubjectWithoutID = errors.New("credential subjects have no ID") @@ -99,7 +99,7 @@ func parseJWTCredential(raw string) (*VerifiableCredential, error) { } else if jti != nil { result.ID = jti } - result.format = JWTCredentialsProofFormat + result.format = JWTCredentialProofFormat result.raw = raw result.token = token return &result, nil @@ -190,6 +190,16 @@ func (vc VerifiableCredential) Proofs() ([]Proof, error) { } func (vc VerifiableCredential) MarshalJSON() ([]byte, error) { + if vc.raw != "" { + // Credential instance created through ParseVerifiableCredential() + if vc.format == JWTCredentialProofFormat { + // Marshal as JSON string + return json.Marshal(vc.raw) + } + // JSON-LD, already in JSON format so return as-is + return []byte(vc.raw), nil + } + // Must be a (new) JSON-LD credential (library does not support creating JWT VCs) type alias VerifiableCredential tmp := alias(vc) if data, err := json.Marshal(tmp); err != nil { diff --git a/vc/vc_test.go b/vc/vc_test.go index c1adf5d..ab10ed8 100644 --- a/vc/vc_test.go +++ b/vc/vc_test.go @@ -27,7 +27,9 @@ BND3LDTn9H7FQokEsUEi8jKwXhGvoN3JtRa51xrNDgXDb0cq1UTYB-rK4Ft9YVmR1NI_ZOF8oGc_7wAp txJy6M1-lD7a5HTzanYTWBPAUHDZGyGKXdJw-W_x0IWChBzI8t3kpG253fg6V3tPgHeKXE94fz_QpYfg --7kLsyBAfQGbg` -func TestVerifiableCredential_UnmarshalJSON(t *testing.T) { +// TestVerifiableCredential_JSONMarshalling tests JSON marshalling of VerifiableCredential. +// Credentials in JSON-LD format are marshalled JSON object, while JWT credentials are marshalled as JSON string. +func TestVerifiableCredential_JSONMarshalling(t *testing.T) { t.Run("JSON-LD", func(t *testing.T) { input := VerifiableCredential{} raw := `{ @@ -43,6 +45,10 @@ func TestVerifiableCredential_UnmarshalJSON(t *testing.T) { assert.Equal(t, JSONLDCredentialProofFormat, input.Format()) assert.Equal(t, raw, input.Raw()) assert.Nil(t, input.JWT()) + // Should marshal into JSON object + marshalled, err := json.Marshal(input) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(string(marshalled), "{")) }) t.Run("JWT", func(t *testing.T) { input := VerifiableCredential{} @@ -52,9 +58,13 @@ func TestVerifiableCredential_UnmarshalJSON(t *testing.T) { assert.Equal(t, []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("UniversityDegreeCredential")}, input.Type) assert.Len(t, input.CredentialSubject, 1) assert.NotNil(t, input.CredentialSubject[0].(map[string]interface{})["degree"]) - assert.Equal(t, JWTCredentialsProofFormat, input.Format()) + assert.Equal(t, JWTCredentialProofFormat, input.Format()) assert.Equal(t, raw, input.Raw()) assert.NotNil(t, input.JWT()) + // Should marshal into JSON string + marshalled, err := json.Marshal(input) + require.NoError(t, err) + assert.JSONEq(t, `"`+raw+`"`, string(marshalled)) }) } diff --git a/vc/vp.go b/vc/vp.go index 54a25c9..2161e3a 100644 --- a/vc/vp.go +++ b/vc/vp.go @@ -155,19 +155,21 @@ func (vp VerifiablePresentation) Proofs() ([]Proof, error) { } func (vp VerifiablePresentation) MarshalJSON() ([]byte, error) { - switch vp.format { - case JWTPresentationProofFormat: - return json.Marshal(vp.raw) - case JSONLDPresentationProofFormat: - fallthrough - default: - type alias VerifiablePresentation - tmp := alias(vp) - if data, err := json.Marshal(tmp); err != nil { - return nil, err - } else { - return marshal.NormalizeDocument(data, pluralContext, marshal.Unplural(typeKey), marshal.Unplural(verifiableCredentialKey), marshal.Unplural(proofKey)) + if vp.raw != "" { + // Presentation instance created through ParseVerifiablePresentation() + if vp.format == JWTPresentationProofFormat { + // Marshal as JSON string + return json.Marshal(vp.raw) } + // JSON-LD, already in JSON format so return as-is + return []byte(vp.raw), nil + } + type alias VerifiablePresentation + tmp := alias(vp) + if data, err := json.Marshal(tmp); err != nil { + return nil, err + } else { + return marshal.NormalizeDocument(data, pluralContext, marshal.Unplural(typeKey), marshal.Unplural(verifiableCredentialKey), marshal.Unplural(proofKey)) } } diff --git a/vc/vp_test.go b/vc/vp_test.go index b449d34..fb708a5 100644 --- a/vc/vp_test.go +++ b/vc/vp_test.go @@ -219,7 +219,7 @@ func TestParseVerifiablePresentation(t *testing.T) { // Assert contained JWT VerifiableCredential was unmarshalled assert.Len(t, vp.VerifiableCredential, 1) vc := vp.VerifiableCredential[0] - assert.Equal(t, JWTCredentialsProofFormat, vc.Format()) + assert.Equal(t, JWTCredentialProofFormat, vc.Format()) assert.Equal(t, "http://example.edu/credentials/3732", vc.ID.String()) }) t.Run("json.UnmarshalJSON for JWT-VP wrapped inside other document", func(t *testing.T) { From b53ca54ae147f736c584206ecc9bfd8569ad9e4b Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 18 Oct 2023 14:27:44 +0200 Subject: [PATCH 2/5] more fixes --- internal/marshal/marshal.go | 9 +++++++++ vc/vc.go | 21 ++++++++++++++------- vc/vc_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/internal/marshal/marshal.go b/internal/marshal/marshal.go index c9a6262..1a6b26a 100644 --- a/internal/marshal/marshal.go +++ b/internal/marshal/marshal.go @@ -70,3 +70,12 @@ func PluralValueOrMap(key string) Normalizer { } } } + +// PruneString returns a Normalizer that removes keys that have a string value match a given string. +func PruneString(key string, match string) Normalizer { + return func(m map[string]interface{}) { + if value, ok := m[key]; ok && value == match { + delete(m, key) + } + } +} diff --git a/vc/vc.go b/vc/vc.go index 1d71292..906d73d 100644 --- a/vc/vc.go +++ b/vc/vc.go @@ -74,8 +74,10 @@ func parseJWTCredential(raw string) (*VerifiableCredential, error) { } } // parse exp - exp := token.Expiration() - result.ExpirationDate = &exp + if _, ok := token.Get("exp"); ok { + exp := token.Expiration() + result.ExpirationDate = &exp + } // parse iss if iss, err := parseURIClaim(token, jwt.IssuerKey); err != nil { return nil, err @@ -125,11 +127,11 @@ func parseJSONLDCredential(raw string) (*VerifiableCredential, error) { // VerifiableCredential represents a credential as defined by the Verifiable Credentials Data Model 1.0 specification (https://www.w3.org/TR/vc-data-model/). type VerifiableCredential struct { // Context defines the json-ld context to dereference the URIs - Context []ssi.URI `json:"@context"` + Context []ssi.URI `json:"@context,omitempty"` // ID is an unique identifier for the credential. It is optional ID *ssi.URI `json:"id,omitempty"` // Type holds multiple types for a credential. A credential must always have the 'VerifiableCredential' type. - Type []ssi.URI `json:"type"` + Type []ssi.URI `json:"type,omitempty"` // Issuer refers to the party that issued the credential Issuer ssi.URI `json:"issuer"` // IssuanceDate is a rfc3339 formatted datetime. @@ -139,9 +141,9 @@ type VerifiableCredential struct { // CredentialStatus holds information on how the credential can be revoked. It is optional CredentialStatus *CredentialStatus `json:"credentialStatus,omitempty"` // CredentialSubject holds the actual data for the credential. It must be extracted using the UnmarshalCredentialSubject method and a custom type. - CredentialSubject []interface{} `json:"credentialSubject"` + CredentialSubject []interface{} `json:"credentialSubject,omitempty"` // Proof contains the cryptographic proof(s). It must be extracted using the Proofs method or UnmarshalProofValue method for non-generic proof fields. - Proof []interface{} `json:"proof"` + Proof []interface{} `json:"proof,omitempty"` format string raw string @@ -205,7 +207,12 @@ func (vc VerifiableCredential) MarshalJSON() ([]byte, error) { if data, err := json.Marshal(tmp); err != nil { return nil, err } else { - return marshal.NormalizeDocument(data, pluralContext, marshal.Unplural(typeKey), marshal.Unplural(credentialSubjectKey), marshal.Unplural(proofKey)) + return marshal.NormalizeDocument(data, pluralContext, marshal.Unplural(typeKey), marshal.Unplural(credentialSubjectKey), marshal.Unplural(proofKey), + // Do not marshal empty issuer fields + marshal.PruneString("issuer", ""), + // Do not marshal "zero-ed" issuanceDate fields + marshal.PruneString("issuanceDate", "0001-01-01T00:00:00Z"), + ) } } diff --git a/vc/vc_test.go b/vc/vc_test.go index ab10ed8..39f7559 100644 --- a/vc/vc_test.go +++ b/vc/vc_test.go @@ -1,7 +1,12 @@ package vc import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "encoding/json" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwt" ssi "github.com/nuts-foundation/go-did" "github.com/stretchr/testify/require" "strings" @@ -49,6 +54,13 @@ func TestVerifiableCredential_JSONMarshalling(t *testing.T) { marshalled, err := json.Marshal(input) require.NoError(t, err) assert.True(t, strings.HasPrefix(string(marshalled), "{")) + + t.Run("marshal empty VC", func(t *testing.T) { + input := VerifiableCredential{} + marshalled, err := json.Marshal(input) + require.NoError(t, err) + assert.Equal(t, "{}", string(marshalled)) + }) }) t.Run("JWT", func(t *testing.T) { input := VerifiableCredential{} @@ -89,6 +101,18 @@ func TestParseVerifiableCredential(t *testing.T) { assert.Len(t, input.CredentialSubject, 1) assert.NotNil(t, input.CredentialSubject[0].(map[string]interface{})["degree"]) }) + t.Run("JWT without `exp` and `nbf` claim", func(t *testing.T) { + token := jwt.New() + require.NoError(t, token.Set("vc", map[string]interface{}{})) + keyPair, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + tokenBytes, err := jwt.Sign(token, jwa.ES256, keyPair) + require.NoError(t, err) + credential, err := ParseVerifiableCredential(string(tokenBytes)) + require.NoError(t, err) + assert.Equal(t, JWTCredentialProofFormat, credential.Format()) + assert.Nil(t, credential.ExpirationDate) + assert.Empty(t, credential.IssuanceDate) + }) } func TestVerifiableCredential_UnmarshalCredentialSubject(t *testing.T) { From 202a3b1ea0f7b046937daa4616a72f95f0fc0230 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Thu, 19 Oct 2023 12:16:02 +0200 Subject: [PATCH 3/5] Introduce CreateJWTVerifiableCredential func --- internal/marshal/marshal.go | 9 ----- vc/vc.go | 65 +++++++++++++++++++++++++----------- vc/vc_test.go | 66 ++++++++++++++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 29 deletions(-) diff --git a/internal/marshal/marshal.go b/internal/marshal/marshal.go index 1a6b26a..c9a6262 100644 --- a/internal/marshal/marshal.go +++ b/internal/marshal/marshal.go @@ -70,12 +70,3 @@ func PluralValueOrMap(key string) Normalizer { } } } - -// PruneString returns a Normalizer that removes keys that have a string value match a given string. -func PruneString(key string, match string) Normalizer { - return func(m map[string]interface{}) { - if value, ok := m[key]; ok && value == match { - delete(m, key) - } - } -} diff --git a/vc/vc.go b/vc/vc.go index 906d73d..627b65b 100644 --- a/vc/vc.go +++ b/vc/vc.go @@ -1,9 +1,11 @@ package vc import ( + "context" "encoding/json" "errors" "fmt" + "github.com/lestrrat-go/jwx/jws" "github.com/lestrrat-go/jwx/jwt" "github.com/nuts-foundation/go-did/did" "strings" @@ -127,11 +129,11 @@ func parseJSONLDCredential(raw string) (*VerifiableCredential, error) { // VerifiableCredential represents a credential as defined by the Verifiable Credentials Data Model 1.0 specification (https://www.w3.org/TR/vc-data-model/). type VerifiableCredential struct { // Context defines the json-ld context to dereference the URIs - Context []ssi.URI `json:"@context,omitempty"` + Context []ssi.URI `json:"@context"` // ID is an unique identifier for the credential. It is optional ID *ssi.URI `json:"id,omitempty"` // Type holds multiple types for a credential. A credential must always have the 'VerifiableCredential' type. - Type []ssi.URI `json:"type,omitempty"` + Type []ssi.URI `json:"type"` // Issuer refers to the party that issued the credential Issuer ssi.URI `json:"issuer"` // IssuanceDate is a rfc3339 formatted datetime. @@ -141,9 +143,9 @@ type VerifiableCredential struct { // CredentialStatus holds information on how the credential can be revoked. It is optional CredentialStatus *CredentialStatus `json:"credentialStatus,omitempty"` // CredentialSubject holds the actual data for the credential. It must be extracted using the UnmarshalCredentialSubject method and a custom type. - CredentialSubject []interface{} `json:"credentialSubject,omitempty"` + CredentialSubject []interface{} `json:"credentialSubject"` // Proof contains the cryptographic proof(s). It must be extracted using the Proofs method or UnmarshalProofValue method for non-generic proof fields. - Proof []interface{} `json:"proof,omitempty"` + Proof []interface{} `json:"proof"` format string raw string @@ -192,27 +194,17 @@ func (vc VerifiableCredential) Proofs() ([]Proof, error) { } func (vc VerifiableCredential) MarshalJSON() ([]byte, error) { - if vc.raw != "" { - // Credential instance created through ParseVerifiableCredential() - if vc.format == JWTCredentialProofFormat { - // Marshal as JSON string - return json.Marshal(vc.raw) - } - // JSON-LD, already in JSON format so return as-is - return []byte(vc.raw), nil + if vc.format == JWTCredentialProofFormat { + // Marshal as JSON string + return json.Marshal(vc.raw) // raw is only set by the parse function } - // Must be a (new) JSON-LD credential (library does not support creating JWT VCs) + // Must be a JSON-LD credential type alias VerifiableCredential tmp := alias(vc) if data, err := json.Marshal(tmp); err != nil { return nil, err } else { - return marshal.NormalizeDocument(data, pluralContext, marshal.Unplural(typeKey), marshal.Unplural(credentialSubjectKey), marshal.Unplural(proofKey), - // Do not marshal empty issuer fields - marshal.PruneString("issuer", ""), - // Do not marshal "zero-ed" issuanceDate fields - marshal.PruneString("issuanceDate", "0001-01-01T00:00:00Z"), - ) + return marshal.NormalizeDocument(data, pluralContext, marshal.Unplural(typeKey), marshal.Unplural(credentialSubjectKey), marshal.Unplural(proofKey)) } } @@ -304,3 +296,38 @@ func (vc VerifiableCredential) ContainsContext(context ssi.URI) bool { return false } + +type JWTSigner func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) + +// CreateJWTVerifiableCredential creates a JWT Verifiable Credential from the given credential template. +// For signing the actual JWT it calls the given signer. +func CreateJWTVerifiableCredential(ctx context.Context, template VerifiableCredential, signer JWTSigner) (*VerifiableCredential, error) { + subjectDID, err := template.SubjectDID() + if err != nil { + return nil, err + } + headers := map[string]interface{}{ + jws.TypeKey: "JWT", + } + claims := map[string]interface{}{ + jwt.NotBeforeKey: template.IssuanceDate, + jwt.IssuerKey: template.Issuer.String(), + jwt.SubjectKey: subjectDID.String(), + "vc": map[string]interface{}{ + "@context": template.Context, + "type": template.Type, + "credentialSubject": template.CredentialSubject, + }, + } + if template.ID != nil { + claims[jwt.JwtIDKey] = template.ID.String() + } + if template.ExpirationDate != nil { + claims[jwt.ExpirationKey] = *template.ExpirationDate + } + token, err := signer(ctx, claims, headers) + if err != nil { + return nil, fmt.Errorf("unable to sign JWT credential: %w", err) + } + return ParseVerifiableCredential(token) +} diff --git a/vc/vc_test.go b/vc/vc_test.go index 39f7559..e55eaf9 100644 --- a/vc/vc_test.go +++ b/vc/vc_test.go @@ -1,6 +1,7 @@ package vc import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -8,9 +9,11 @@ import ( "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwt" ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/did" "github.com/stretchr/testify/require" "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -59,7 +62,7 @@ func TestVerifiableCredential_JSONMarshalling(t *testing.T) { input := VerifiableCredential{} marshalled, err := json.Marshal(input) require.NoError(t, err) - assert.Equal(t, "{}", string(marshalled)) + assert.Equal(t, "{\"@context\":null,\"credentialSubject\":null,\"issuanceDate\":\"0001-01-01T00:00:00Z\",\"issuer\":\"\",\"proof\":null,\"type\":null}", string(marshalled)) }) }) t.Run("JWT", func(t *testing.T) { @@ -315,3 +318,64 @@ func TestVerifiableCredential_SubjectDID(t *testing.T) { assert.EqualError(t, err, "unable to get subject DID from VC: invalid DID") }) } + +func TestCreateJWTVerifiableCredential(t *testing.T) { + issuerDID := did.MustParseDID("did:example:issuer") + subjectDID := did.MustParseDID("did:example:subject") + credentialID := ssi.MustParseURI(issuerDID.String() + "#1") + issuanceDate := time.Date(2050, 1, 1, 0, 0, 0, 0, time.UTC) + expirationDate := issuanceDate.AddDate(0, 0, 10) + template := VerifiableCredential{ + ID: &credentialID, + Context: []ssi.URI{ + VCContextV1URI(), + }, + Type: []ssi.URI{ + VerifiableCredentialTypeV1URI(), + ssi.MustParseURI("https://example.com/custom"), + }, + IssuanceDate: issuanceDate, + ExpirationDate: &expirationDate, + CredentialSubject: []interface{}{ + map[string]interface{}{ + "id": subjectDID.String(), + }, + }, + Issuer: issuerDID.URI(), + } + ctx := context.Background() + t.Run("all properties", func(t *testing.T) { + var claims map[string]interface{} + var headers map[string]interface{} + _, err := CreateJWTVerifiableCredential(ctx, template, func(_ context.Context, c map[string]interface{}, h map[string]interface{}) (string, error) { + claims = c + headers = h + return jwtCredential, nil + }) + assert.NoError(t, err) + assert.Equal(t, issuerDID.String(), claims[jwt.IssuerKey]) + assert.Equal(t, subjectDID.String(), claims[jwt.SubjectKey]) + assert.Equal(t, template.ID.String(), claims[jwt.JwtIDKey]) + assert.Equal(t, issuanceDate, claims[jwt.NotBeforeKey]) + assert.Equal(t, expirationDate, claims[jwt.ExpirationKey]) + assert.Equal(t, map[string]interface{}{ + "credentialSubject": template.CredentialSubject, + "@context": template.Context, + "type": template.Type, + }, claims["vc"]) + assert.Equal(t, map[string]interface{}{"typ": "JWT"}, headers) + }) + t.Run("only mandatory properties", func(t *testing.T) { + minimumTemplate := template + minimumTemplate.ExpirationDate = nil + minimumTemplate.ID = nil + var claims map[string]interface{} + _, err := CreateJWTVerifiableCredential(ctx, minimumTemplate, func(_ context.Context, c map[string]interface{}, _ map[string]interface{}) (string, error) { + claims = c + return jwtCredential, nil + }) + assert.NoError(t, err) + assert.Nil(t, claims[jwt.ExpirationKey]) + assert.Nil(t, claims[jwt.JwtIDKey]) + }) +} From 7fa78908e765a7d024f320701ca9efe3186fdf0b Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 20 Oct 2023 10:00:40 +0200 Subject: [PATCH 4/5] do not overwrite issuanceDate if nbf is not set --- vc/vc.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vc/vc.go b/vc/vc.go index 627b65b..4c190e2 100644 --- a/vc/vc.go +++ b/vc/vc.go @@ -76,7 +76,7 @@ func parseJWTCredential(raw string) (*VerifiableCredential, error) { } } // parse exp - if _, ok := token.Get("exp"); ok { + if _, ok := token.Get(jwt.ExpirationKey); ok { exp := token.Expiration() result.ExpirationDate = &exp } @@ -87,7 +87,9 @@ func parseJWTCredential(raw string) (*VerifiableCredential, error) { result.Issuer = *iss } // parse nbf - result.IssuanceDate = token.NotBefore() + if _, ok := token.Get(jwt.NotBeforeKey); ok { + result.IssuanceDate = token.NotBefore() + } // parse sub if token.Subject() != "" { for _, credentialSubjectInterf := range result.CredentialSubject { From 5230b04540814a7d1214f1d37966bb217e71ee7c Mon Sep 17 00:00:00 2001 From: reinkrul Date: Fri, 20 Oct 2023 10:09:20 +0200 Subject: [PATCH 5/5] Apply suggestions from code review --- vc/vc.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vc/vc.go b/vc/vc.go index 4c190e2..0076e6b 100644 --- a/vc/vc.go +++ b/vc/vc.go @@ -302,7 +302,8 @@ func (vc VerifiableCredential) ContainsContext(context ssi.URI) bool { type JWTSigner func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) // CreateJWTVerifiableCredential creates a JWT Verifiable Credential from the given credential template. -// For signing the actual JWT it calls the given signer. +// For signing the actual JWT it calls the given signer, which must return the created JWT in string format. +// Note: the signer is responsible for adding the right key claims (e.g. `kid`). func CreateJWTVerifiableCredential(ctx context.Context, template VerifiableCredential, signer JWTSigner) (*VerifiableCredential, error) { subjectDID, err := template.SubjectDID() if err != nil { @@ -331,5 +332,5 @@ func CreateJWTVerifiableCredential(ctx context.Context, template VerifiableCrede if err != nil { return nil, fmt.Errorf("unable to sign JWT credential: %w", err) } - return ParseVerifiableCredential(token) + return parseJWTCredential(token) }