Skip to content
This repository has been archived by the owner on Mar 27, 2024. It is now read-only.

Commit

Permalink
feat: parse JWT VCs when resolving credential manifests (#3375)
Browse files Browse the repository at this point in the history
Signed-off-by: Filip Burlacu <[email protected]>

Signed-off-by: Filip Burlacu <[email protected]>
  • Loading branch information
Moopli authored Sep 20, 2022
1 parent c16ea40 commit e1f4b94
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 108 deletions.
1 change: 1 addition & 0 deletions internal/testdata/samples/wallet/sample_udc_jwtvc.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa25DMXd3UzZERVl3dEdiWlpvMlF2alFqa2gycVNCamI0R1ltYnllOGR2NFM1I3o2TWtuQzF3d1M2REVZd3RHYlpabzJRdmpRamtoMnFTQmpiNEdZbWJ5ZThkdjRTNSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzc5MDY2MDQsImlhdCI6MTI2MjM3MzgwNCwiaXNzIjoiZGlkOmtleTp6Nk1rbkMxd3dTNkRFWXd0R2JaWm8yUXZqUWpraDJxU0JqYjRHWW1ieWU4ZHY0UzUiLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMTg3MiIsIm5iZiI6MTI2MjM3MzgwNCwic3ViIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSIsImh0dHBzOi8vdzNpZC5vcmcvc2VjdXJpdHkvYmJzL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImRlZ3JlZSI6eyJ0eXBlIjoiQmFjaGVsb3JEZWdyZWUiLCJ1bml2ZXJzaXR5IjoiTUlUIn0sImlkIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwibmFtZSI6IkpheWRlbiBEb2UiLCJzcG91c2UiOiJkaWQ6ZXhhbXBsZTpjMjc2ZTEyZWMyMWViZmViMWY3MTJlYmM2ZjEifSwiZXhwaXJhdGlvbkRhdGUiOiIyMDIwLTAxLTAxVDE5OjIzOjI0WiIsImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzE4NzIiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDE5OjIzOjI0WiIsImlzc3VlciI6eyJpZCI6ImRpZDprZXk6ejZNa25DMXd3UzZERVl3dEdiWlpvMlF2alFqa2gycVNCamI0R1ltYnllOGR2NFM1IiwibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSJ9LCJyZWZlcmVuY2VOdW1iZXIiOjguMzI5NDg0N2UrMDcsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdfX0.1awJIhmRcxaU8LY2YBG__A4uN2LoWVed9Jg5A6E1GQSioOwyZQYy_n0duKcqNCFbt7QkfoDUBEYn-zuBjjKCBg
2 changes: 2 additions & 0 deletions internal/testdata/testdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ var (
CredentialResponseWithMultipleVCs []byte
//go:embed samples/wallet/sample_udc_vc.json
SampleUDCVC []byte
//go:embed samples/wallet/sample_udc_jwtvc.txt
SampleUDCJWTVC []byte
//go:embed samples/wallet/sample_udc_vc_signed.json
SampleUDCVCWithProof []byte
//go:embed samples/wallet/sample_udc_vc_with_credschema.json
Expand Down
114 changes: 58 additions & 56 deletions pkg/doc/cm/credentialmanifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ var (
var (
//go:embed testdata/credential_university_degree.jsonld
validVC []byte // nolint:gochecknoglobals
//go:embed testdata/credential_university_degree_jwt.txt
validJWTVC []byte // nolint:gochecknoglobals
)

// miscellaneous samples.
Expand Down Expand Up @@ -546,73 +548,73 @@ func TestResolveResponse(t *testing.T) {
}

func TestResolveCredential(t *testing.T) {
t.Run("Successes - resolve credential instance", func(t *testing.T) {
manifest := &cm.CredentialManifest{}
require.NoError(t, manifest.UnmarshalJSON(credentialManifestUniversityDegree))
t.Run("Successes", func(t *testing.T) {
testCases := []struct {
name string
credResolver func(t *testing.T) cm.CredentialToResolveOption
}{
{
name: "resolve credential struct",
credResolver: func(t *testing.T) cm.CredentialToResolveOption {
t.Helper()

vc := parseTestCredential(t, validVC)
vc := parseTestCredential(t, validVC)

result, err := manifest.ResolveCredential("bachelors_degree", cm.CredentialToResolve(vc))
require.NoError(t, err)
require.NotEmpty(t, result)
require.Equal(t, result.Title, "Bachelor of Applied Science")
require.Equal(t, result.Subtitle, "Electrical Systems Specialty")

expected := map[string]*cm.ResolvedProperty{
"With distinction": {
Label: "With distinction",
Value: true,
Schema: cm.Schema{
Type: "boolean",
},
},
"Years studied": {
Label: "Years studied",
Value: float64(4),
Schema: cm.Schema{
Type: "number",
return cm.CredentialToResolve(vc)
},
},
}

for _, property := range result.Properties {
expectedVal, ok := expected[property.Label]
require.True(t, ok, "unexpected label '%s' in resolved properties", property.Label)
require.EqualValues(t, expectedVal, property)
}
})

t.Run("Successes - resolve raw Credential", func(t *testing.T) {
manifest := &cm.CredentialManifest{}
require.NoError(t, manifest.UnmarshalJSON(credentialManifestUniversityDegree))
{
name: "resolve raw JSON-LD credential",
credResolver: func(t *testing.T) cm.CredentialToResolveOption {
t.Helper()

result, err := manifest.ResolveCredential("bachelors_degree", cm.RawCredentialToResolve(validVC))
require.NoError(t, err)
require.NotEmpty(t, result)
require.Equal(t, result.Title, "Bachelor of Applied Science")
require.Equal(t, result.Subtitle, "Electrical Systems Specialty")

expected := map[string]*cm.ResolvedProperty{
"With distinction": {
Label: "With distinction",
Value: true,
Schema: cm.Schema{
Type: "boolean",
return cm.RawCredentialToResolve(validVC)
},
},
"Years studied": {
Label: "Years studied",
Value: float64(4),
Schema: cm.Schema{
Type: "number",
{
name: "resolve raw JWT credential",
credResolver: func(t *testing.T) cm.CredentialToResolveOption {
t.Helper()

return cm.RawCredentialToResolve(validJWTVC)
},
},
}

for _, property := range result.Properties {
expectedVal, ok := expected[property.Label]
require.True(t, ok, "unexpected label '%s' in resolved properties", property.Label)
require.EqualValues(t, expectedVal, property)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
manifest := &cm.CredentialManifest{}
require.NoError(t, manifest.UnmarshalJSON(credentialManifestUniversityDegree))

result, err := manifest.ResolveCredential("bachelors_degree", tc.credResolver(t))
require.NoError(t, err)
require.NotEmpty(t, result)
require.Equal(t, result.Title, "Bachelor of Applied Science")
require.Equal(t, result.Subtitle, "Electrical Systems Specialty")

expected := map[string]*cm.ResolvedProperty{
"With distinction": {
Label: "With distinction",
Value: true,
Schema: cm.Schema{
Type: "boolean",
},
},
"Years studied": {
Label: "Years studied",
Value: float64(4),
Schema: cm.Schema{
Type: "number",
},
},
}

for _, property := range result.Properties {
expectedVal, ok := expected[property.Label]
require.True(t, ok, "unexpected label '%s' in resolved properties", property.Label)
require.EqualValues(t, expectedVal, property)
}
})
}
})

Expand Down
10 changes: 10 additions & 0 deletions pkg/doc/cm/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ func (cm *CredentialManifest) ResolveCredential(descriptorID string, credential

fallthrough
case len(opts.rawCredential) > 0:
if opts.rawCredential[0] != '{' {
// try to parse as jwt vc
var jwtCred []byte

jwtCred, err = verifiable.JWTVCToJSON(opts.rawCredential)
if err == nil {
opts.rawCredential = jwtCred
}
}

err = json.Unmarshal(opts.rawCredential, &vcmap)
if err != nil {
return nil, err
Expand Down
1 change: 1 addition & 0 deletions pkg/doc/cm/testdata/credential_university_degree_jwt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa25DMXd3UzZERVl3dEdiWlpvMlF2alFqa2gycVNCamI0R1ltYnllOGR2NFM1I3o2TWtuQzF3d1M2REVZd3RHYlpabzJRdmpRamtoMnFTQmpiNEdZbWJ5ZThkdjRTNSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1Nzc5MDY2MDQsImlhdCI6MTI2MjM3MzgwNCwiaXNzIjoiZGlkOmtleTp6Nk1rbkMxd3dTNkRFWXd0R2JaWm8yUXZqUWpraDJxU0JqYjRHWW1ieWU4ZHY0UzUiLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMTg3MiIsIm5iZiI6MTI2MjM3MzgwNCwic3ViIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSIsImh0dHBzOi8vdzNpZC5vcmcvc2VjdXJpdHkvandzL3YxIiwiaHR0cHM6Ly90cnVzdGJsb2MuZ2l0aHViLmlvL2NvbnRleHQvdmMvZXhhbXBsZXMtdjEuanNvbmxkIl0sImNyZWRlbnRpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL2V4YW1wbGUuZWR1L3N0YXR1cy8yNCIsInR5cGUiOiJDcmVkZW50aWFsU3RhdHVzTGlzdDIwMTcifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOiJQaHlzaWNhbCIsImV2aWRlbmNlRG9jdW1lbnQiOiJEcml2ZXJzTGljZW5zZSIsImlkIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9ldmlkZW5jZS9mMmFlZWM5Ny1mYzBkLTQyYmYtOGNhNy0wNTQ4MTkyZDQyMzEiLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvMTQifSx7ImRvY3VtZW50UHJlc2VuY2UiOiJEaWdpdGFsIiwiZXZpZGVuY2VEb2N1bWVudCI6IkZsdWlkIER5bmFtaWNzIEZvY3VzIiwiaWQiOiJodHRwczovL2V4YW1wbGUuZWR1L2V2aWRlbmNlL2YyYWVlYzk3LWZjMGQtNDJiZi04Y2E3LTA1NDgxOTJkeHl6YWIiLCJzdWJqZWN0UHJlc2VuY2UiOiJEaWdpdGFsIiwidHlwZSI6WyJTdXBwb3J0aW5nQWN0aXZpdHkiXSwidmVyaWZpZXIiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvMTQifV0sImV4cGlyYXRpb25EYXRlIjoiMjAyMC0wMS0wMVQxOToyMzoyNFoiLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6a2V5Ono2TWtuQzF3d1M2REVZd3RHYlpabzJRdmpRamtoMnFTQmpiNEdZbWJ5ZThkdjRTNSIsImltYWdlIjoiZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SIiwibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSJ9LCJtaW5vciI6IkVsZWN0cmljYWwgU3lzdGVtcyBTcGVjaWFsdHkiLCJyZWZyZXNoU2VydmljZSI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5lZHUvcmVmcmVzaC8zNzMyIiwidHlwZSI6Ik1hbnVhbFJlZnJlc2hTZXJ2aWNlMjAxOCJ9LCJ0ZXJtc09mVXNlIjp7ImlkIjoiaHR0cDovL2V4YW1wbGUuY29tL3BvbGljaWVzL2NyZWRlbnRpYWwvNCIsInByb2ZpbGUiOiJodHRwOi8vZXhhbXBsZS5jb20vcHJvZmlsZXMvY3JlZGVudGlhbCIsInByb2hpYml0aW9uIjpbeyJhY3Rpb24iOlsiQXJjaGl2YWwiXSwiYXNzaWduZWUiOiJBbGxWZXJpZmllcnMiLCJhc3NpZ25lciI6Imh0dHBzOi8vZXhhbXBsZS5lZHUvaXNzdWVycy8xNCIsInRhcmdldCI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8zNzMyIn1dLCJ0eXBlIjoiSXNzdWVyUG9saWN5In0sInRpdGxlIjoiQmFjaGVsb3Igb2YgQXBwbGllZCBTY2llbmNlIiwidHlwZSI6IlZlcmlmaWFibGVDcmVkZW50aWFsIiwid2l0aERpc3RpbmN0aW9uIjp0cnVlLCJ5ZWFyc1N0dWRpZWQiOjR9fQ.zU0v3xSCMWaIDTz9irV58CWtQUrifbk4JD5gnp_AQv9S7pPSN0k9tx6DMv8_zR--5zUSUT70i14q-Yp8vbR1AA
40 changes: 21 additions & 19 deletions pkg/doc/verifiable/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
package verifiable

import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
Expand Down Expand Up @@ -419,10 +420,10 @@ func (i *Issuer) MarshalJSON() ([]byte, error) {
}

// UnmarshalJSON unmarshals issuer from JSON.
func (i *Issuer) UnmarshalJSON(bytes []byte) error {
func (i *Issuer) UnmarshalJSON(data []byte) error {
var issuerID string

if err := json.Unmarshal(bytes, &issuerID); err == nil {
if err := json.Unmarshal(data, &issuerID); err == nil {
// as string
i.ID = issuerID
return nil
Expand All @@ -435,7 +436,7 @@ func (i *Issuer) UnmarshalJSON(bytes []byte) error {

i.CustomFields = make(CustomFields)

err := jsonutil.UnmarshalWithCustomFields(bytes, alias, i.CustomFields)
err := jsonutil.UnmarshalWithCustomFields(data, alias, i.CustomFields)
if err != nil {
return fmt.Errorf("unmarshal Issuer: %w", err)
}
Expand Down Expand Up @@ -469,10 +470,10 @@ func (s *Subject) MarshalJSON() ([]byte, error) {
}

// UnmarshalJSON unmarshals Subject from JSON.
func (s *Subject) UnmarshalJSON(bytes []byte) error {
func (s *Subject) UnmarshalJSON(data []byte) error {
var subjectID string

if err := json.Unmarshal(bytes, &subjectID); err == nil {
if err := json.Unmarshal(data, &subjectID); err == nil {
// as string
s.ID = subjectID
return nil
Expand All @@ -484,7 +485,7 @@ func (s *Subject) UnmarshalJSON(bytes []byte) error {

s.CustomFields = make(CustomFields)

err := jsonutil.UnmarshalWithCustomFields(bytes, alias, s.CustomFields)
err := jsonutil.UnmarshalWithCustomFields(data, alias, s.CustomFields)
if err != nil {
return fmt.Errorf("unmarshal Subject: %w", err)
}
Expand Down Expand Up @@ -997,21 +998,21 @@ func newCredential(raw *rawCredential) (*Credential, error) {
}, nil
}

func parseTypedID(bytes json.RawMessage) ([]TypedID, error) {
if len(bytes) == 0 {
func parseTypedID(data json.RawMessage) ([]TypedID, error) {
if len(data) == 0 {
return nil, nil
}

var singleTypedID TypedID

err := json.Unmarshal(bytes, &singleTypedID)
err := json.Unmarshal(data, &singleTypedID)
if err == nil {
return []TypedID{singleTypedID}, nil
}

var composedTypedID []TypedID

err = json.Unmarshal(bytes, &composedTypedID)
err = json.Unmarshal(data, &composedTypedID)
if err == nil {
return composedTypedID, nil
}
Expand Down Expand Up @@ -1061,20 +1062,21 @@ func decodeRaw(vcData []byte, vcOpts *credentialOpts) ([]byte, string, error) {
}

if jwt.IsJWTUnsecured(vcStr) { // Embedded proof.
vcDecodedBytes, err := decodeCredJWTUnsecured(vcStr)
if err != nil {
return nil, "", fmt.Errorf("unsecured JWT decoding: %w", err)
vcData, e = decodeCredJWTUnsecured(vcStr)
if e != nil {
return nil, "", fmt.Errorf("unsecured JWT decoding: %w", e)
}

vc, err := checkEmbeddedProof(vcDecodedBytes, getEmbeddedProofCheckOpts(vcOpts))

return vc, "", err
}

// Embedded proof.
vc, e := checkEmbeddedProof(vcData, getEmbeddedProofCheckOpts(vcOpts))
return vcData, "", checkEmbeddedProof(vcData, getEmbeddedProofCheckOpts(vcOpts))
}

// JWTVCToJSON parses a JWT VC without verifying, and returns the JSON VC contents.
func JWTVCToJSON(vc []byte) ([]byte, error) {
vc = bytes.Trim(vc, "\"' ")

return vc, "", e
return decodeCredJWS(string(vc), false, nil)
}

func getEmbeddedProofCheckOpts(vcOpts *credentialOpts) *embeddedProofCheckOpts {
Expand Down
23 changes: 23 additions & 0 deletions pkg/doc/verifiable/credential_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,29 @@ func TestContextToSerialize(t *testing.T) {
}))
}

func Test_JWTVCToJSON(t *testing.T) {
signer, err := newCryptoSigner(kms.ED25519Type)
require.NoError(t, err)

vcSource, err := parseTestCredential(t, []byte(validCredential))
require.NoError(t, err)

jwtClaims, err := vcSource.JWTClaims(true)
require.NoError(t, err)

jws, err := jwtClaims.MarshalJWS(EdDSA, signer, "any")
require.NoError(t, err)

t.Run("success", func(t *testing.T) {
jsonCred, err := JWTVCToJSON([]byte(jws))
require.NoError(t, err)

vcActual, err := parseTestCredential(t, jsonCred, WithDisabledProofCheck())
require.NoError(t, err)
require.Equal(t, vcSource, vcActual)
})
}

func TestParseCredentialFromRaw(t *testing.T) {
issuer, err := json.Marshal("did:example:76e12ec712ebc6f1c221ebfeb1f")
require.NoError(t, err)
Expand Down
18 changes: 9 additions & 9 deletions pkg/doc/verifiable/embedded_proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,37 +56,37 @@ type embeddedProofCheckOpts struct {
jsonldCredentialOpts
}

func checkEmbeddedProof(docBytes []byte, opts *embeddedProofCheckOpts) ([]byte, error) {
func checkEmbeddedProof(docBytes []byte, opts *embeddedProofCheckOpts) error {
if opts.disabledProofCheck {
return docBytes, nil
return nil
}

var jsonldDoc map[string]interface{}

if err := json.Unmarshal(docBytes, &jsonldDoc); err != nil {
return nil, fmt.Errorf("embedded proof is not JSON: %w", err)
return fmt.Errorf("embedded proof is not JSON: %w", err)
}

delete(jsonldDoc, "jwt")

proofElement, ok := jsonldDoc["proof"]
if !ok || proofElement == nil {
// do not make a check if there is no proof defined as proof presence is not mandatory
return docBytes, nil
return nil
}

proofs, err := getProofs(proofElement)
if err != nil {
return nil, fmt.Errorf("check embedded proof: %w", err)
return fmt.Errorf("check embedded proof: %w", err)
}

ldpSuites, err := getSuites(proofs, opts)
if err != nil {
return nil, err
return err
}

if opts.publicKeyFetcher == nil {
return nil, errors.New("public key fetcher is not defined")
return errors.New("public key fetcher is not defined")
}

checkedDoc := docBytes
Expand All @@ -99,10 +99,10 @@ func checkEmbeddedProof(docBytes []byte, opts *embeddedProofCheckOpts) ([]byte,

err = checkLinkedDataProof(checkedDoc, ldpSuites, opts.publicKeyFetcher, &opts.jsonldCredentialOpts)
if err != nil {
return nil, fmt.Errorf("check embedded proof: %w", err)
return fmt.Errorf("check embedded proof: %w", err)
}

return docBytes, nil
return nil
}

// nolint:gocyclo
Expand Down
Loading

0 comments on commit e1f4b94

Please sign in to comment.