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

feat: parse JWT VCs when resolving credential manifests #3375

Merged
merged 1 commit into from
Sep 20, 2022
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
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