Repository: hyperledger/aries-framework-go (go)

Issue:
SD-JWT: Support claims with 'vc' verification in issuer
Change logic for resolving _sd_alg to look for _sd_alg in the following path: 
1. claims top level (as per spec)
2. claims -> vc -> credential subject (in case that it is not present at claims top level)


Pull request description:
chore: SD-JWT: Support claims with 'vc' verification in issuer
Change logic for resolving _sd_alg to look for _sd_alg in the following path:
1. claims top level (as per spec)
2. claims -> vc -> credential subject (in case that it is not present at claims top level)

Closes #3482

Signed-off-by: Sandra Vrtikapa <sandra.vrtikapa@securekey.com>


Patch:
diff --git a/pkg/doc/sdjwt/common/common.go b/pkg/doc/sdjwt/common/common.go
index 3ca71f06..bb51a2f8 100644
--- a/pkg/doc/sdjwt/common/common.go
+++ b/pkg/doc/sdjwt/common/common.go
@@ -267,17 +267,53 @@ func GetCryptoHash(sdAlg string) (crypto.Hash, error) {
 
 // GetSDAlg returns SD algorithm from claims.
 func GetSDAlg(claims map[string]interface{}) (string, error) {
+	var alg string
+
 	obj, ok := claims[SDAlgorithmKey]
 	if !ok {
-		return "", fmt.Errorf("%s must be present in SD-JWT", SDAlgorithmKey)
+		// if claims contain 'vc' claim it may be present in credential subject
+		obj, ok = getSDAlgFromCredentialSubject(claims)
+		if !ok {
+			return "", fmt.Errorf("%s must be present in SD-JWT", SDAlgorithmKey)
+		}
 	}
 
-	str, ok := obj.(string)
+	alg, ok = obj.(string)
 	if !ok {
 		return "", fmt.Errorf("%s must be a string", SDAlgorithmKey)
 	}
 
-	return str, nil
+	return alg, nil
+}
+
+// getSDAlgFromCredentialSubject returns SD algorithm from VC credential subject.
+func getSDAlgFromCredentialSubject(claims map[string]interface{}) (interface{}, bool) {
+	vcObj, ok := claims["vc"]
+	if !ok {
+		return nil, false
+	}
+
+	vc, ok := vcObj.(map[string]interface{})
+	if !ok {
+		return nil, false
+	}
+
+	csObj, ok := vc["credentialSubject"]
+	if !ok {
+		return nil, false
+	}
+
+	cs, ok := csObj.(map[string]interface{})
+	if !ok {
+		return nil, false
+	}
+
+	obj, ok := cs[SDAlgorithmKey]
+	if !ok {
+		return nil, false
+	}
+
+	return obj, true
 }
 
 // GetDisclosureDigests returns digests from claims map.


Test patch:
diff --git a/pkg/doc/sdjwt/common/common_test.go b/pkg/doc/sdjwt/common/common_test.go
index 90739a3f..2166ea4b 100644
--- a/pkg/doc/sdjwt/common/common_test.go
+++ b/pkg/doc/sdjwt/common/common_test.go
@@ -446,6 +446,127 @@ func TestGetDisclosedClaims(t *testing.T) {
 	})
 }
 
+func TestGetSDAlgFromVC(t *testing.T) {
+	r := require.New(t)
+
+	t.Run("success", func(t *testing.T) {
+		claims := map[string]interface{}{
+			"_sd_alg": "sha-256",
+		}
+
+		alg, err := GetSDAlg(claims)
+		r.NoError(err)
+		r.Equal("sha-256", alg)
+	})
+
+	t.Run("success - algorithm is in VC credential subject", func(t *testing.T) {
+		claims := map[string]interface{}{
+			"given_name": "John",
+			"vc": map[string]interface{}{
+				"credentialSubject": map[string]interface{}{
+					"_sd_alg": "sha-256",
+				},
+			},
+		}
+
+		alg, err := GetSDAlg(claims)
+		r.NoError(err)
+		r.Equal("sha-256", alg)
+	})
+
+	t.Run("error - algorithm not found (empty claims)", func(t *testing.T) {
+		alg, err := GetSDAlg(make(map[string]interface{}))
+		r.Error(err)
+		r.Empty(alg)
+
+		r.Contains(err.Error(), "_sd_alg must be present in SD-JWT")
+	})
+
+	t.Run("error - algorithm not found", func(t *testing.T) {
+		claims := map[string]interface{}{
+			"vc": map[string]interface{}{
+				"credentialSubject": map[string]interface{}{
+					"given_name": "John",
+				},
+			},
+		}
+
+		alg, err := GetSDAlg(claims)
+		r.Error(err)
+		r.Empty(alg)
+
+		r.Contains(err.Error(), "_sd_alg must be present in SD-JWT")
+	})
+
+	t.Run("error - algorithm not found (vc claim is not a map)", func(t *testing.T) {
+		claims := map[string]interface{}{
+			"vc": "invalid",
+		}
+
+		alg, err := GetSDAlg(claims)
+		r.Error(err)
+		r.Empty(alg)
+
+		r.Contains(err.Error(), "_sd_alg must be present in SD-JWT")
+	})
+
+	t.Run("error - algorithm not found (no credential subject in vc)", func(t *testing.T) {
+		claims := map[string]interface{}{
+			"vc": map[string]interface{}{
+				"id": "test-id",
+			},
+		}
+
+		alg, err := GetSDAlg(claims)
+		r.Error(err)
+		r.Empty(alg)
+
+		r.Contains(err.Error(), "_sd_alg must be present in SD-JWT")
+	})
+
+	t.Run("error - algorithm not found (credential subject is not a map)", func(t *testing.T) {
+		claims := map[string]interface{}{
+			"vc": map[string]interface{}{
+				"credentialSubject": "invalid",
+			},
+		}
+
+		alg, err := GetSDAlg(claims)
+		r.Error(err)
+		r.Empty(alg)
+
+		r.Contains(err.Error(), "_sd_alg must be present in SD-JWT")
+	})
+
+	t.Run("error - algorithm must be a string", func(t *testing.T) {
+		claims := map[string]interface{}{
+			"vc": map[string]interface{}{
+				"credentialSubject": map[string]interface{}{
+					"_sd_alg": 123,
+				},
+			},
+		}
+
+		alg, err := GetSDAlg(claims)
+		r.Error(err)
+		r.Empty(alg)
+
+		r.Contains(err.Error(), "_sd_alg must be a string")
+	})
+
+	t.Run("error - algorithm must be a string", func(t *testing.T) {
+		claims := map[string]interface{}{
+			"_sd_alg": 123,
+		}
+
+		alg, err := GetSDAlg(claims)
+		r.Error(err)
+		r.Empty(alg)
+
+		r.Contains(err.Error(), "_sd_alg must be a string")
+	})
+}
+
 type NoopSignatureVerifier struct {
 }
 
diff --git a/pkg/doc/sdjwt/holder/holder_test.go b/pkg/doc/sdjwt/holder/holder_test.go
index 987aceba..f9c9a4b0 100644
--- a/pkg/doc/sdjwt/holder/holder_test.go
+++ b/pkg/doc/sdjwt/holder/holder_test.go
@@ -70,6 +70,13 @@ func TestParse(t *testing.T) {
 		require.Equal(t, 7, len(claims))
 	})
 
+	t.Run("success - VC example", func(t *testing.T) {
+		claims, err := Parse(vcCombinedFormatForIssuance, WithSignatureVerifier(&NoopSignatureVerifier{}))
+		r.NoError(err)
+		require.NotNil(t, claims)
+		require.Equal(t, 3, len(claims))
+	})
+
 	t.Run("success - complex claims", func(t *testing.T) {
 		complexClaims := createComplexClaims()
 
@@ -287,3 +294,5 @@ const additionalDisclosure = `WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbW
 
 // nolint: lll
 const specSDJWT = `eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImNBRUlVcUowY21MekQxa3pHemhlaUJhZzBZUkF6VmRsZnhOMjgwTmdIYUEifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIlJTQSIsICJuIjogInBtNGJPSEJnLW9ZaEF5UFd6UjU2QVdYM3JVSVhwMTFfSUNEa0dnUzZXM1pXTHRzLWh6d0kzeDY1NjU5a2c0aFZvOWRiR29DSkUzWkdGX2VhZXRFMzBVaEJVRWdwR3dyRHJRaUo5enFwcm1jRmZyM3F2dmtHanR0aDhaZ2wxZU0yYkpjT3dFN1BDQkhXVEtXWXMxNTJSN2c2SmcyT1ZwaC1hOHJxLXE3OU1oS0c1UW9XX21UejEwUVRfNkg0YzdQaldHMWZqaDhocFdObmJQX3B2NmQxelN3WmZjNWZsNnlWUkwwRFYwVjNsR0hLZTJXcWZfZU5HakJyQkxWa2xEVGs4LXN0WF9NV0xjUi1FR21YQU92MFVCV2l0U19kWEpLSnUtdlhKeXcxNG5IU0d1eFRJSzJoeDFwdHRNZnQ5Q3N2cWltWEtlRFRVMTRxUUwxZUU3aWhjdyIsICJlIjogIkFRQUIifX19.xqgKrDO6dK_oBL3fiqdcq_elaIGxM6Z-RyuysglGyddR1O1IiE3mIk8kCpoqcRLR88opkVWN2392K_XYfAuAmeT9kJVisD8ZcgNcv-MQlWW9s8WaViXxBRe7EZWkWRQcQVR6jf95XZ5H2-_KA54POq3L42xjk0y5vDr8yc08Reak6vvJVvjXpp-Wk6uxsdEEAKFspt_EYIvISFJhfTuQqyhCjnaW13X312MSQBPwjbHn74ylUqVLljDvqcemxeqjh42KWJq4C3RqNJ7anA2i3FU1kB4-KNZWsijY7-op49iL7BrnIBxdlAMrbHEkoGTbFWdl7Ki17GHtDxxa1jaxQg~WyJkcVR2WE14UzBHYTNEb2FHbmU5eDBRIiwgInN1YiIsICJqb2huX2RvZV80MiJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJLVXhTNWhFX1hiVmFjckdBYzdFRnd3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyIzcXZWSjFCQURwSERTUzkzOVEtUml3IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJFUktNMENOZUZKa2FENW1UWFZfWDh3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0`
+
+const vcCombinedFormatForIssuance = `eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp0cnVzdGJsb2M6YWJjI2tleTEifQ.eyJpYXQiOjE2NzM5ODc1NDcsImlzcyI6ImRpZDpleGFtcGxlOjc2ZTEyZWM3MTJlYmM2ZjFjMjIxZWJmZWIxZiIsImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwibmJmIjoxNjczOTg3NTQ3LCJzdWIiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJfc2QiOlsiVklMLXlXZHlZX3hNbk5icEJvbHRrcnlBOFZESVFVYllLd2dQaHVEUEx1ZyIsIkxDLUg0R2N4UG1OdGN5VWNiSGFDbTlEUDFnZDROYXJsZ2RiUFc4ZEVvZ2siLCJlbHVRRFVtbHpwM19naU9uRFVRVk1WLWpWM1hMNXVIck1BNzRROXI0dVF3Il0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifSwiZmlyc3RfbmFtZSI6IkZpcnN0IG5hbWUiLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwiaW5mbyI6IkluZm8iLCJpc3N1YW5jZURhdGUiOiIyMDIzLTAxLTE3VDIyOjMyOjI3LjQ2ODEwOTgxNyswMjowMCIsImlzc3VlciI6ImRpZDpleGFtcGxlOjc2ZTEyZWM3MTJlYmM2ZjFjMjIxZWJmZWIxZiIsImxhc3RfbmFtZSI6Ikxhc3QgbmFtZSIsInR5cGUiOiJWZXJpZmlhYmxlQ3JlZGVudGlhbCJ9fQ.PvRYW8-EAG7K4QQL3TV-GNF--vaYIGc3TWJrRSoc2qBCVT5sFkez7FTLv7iae24S2mi2GH5lcxy1dx75LSjOBA~WyJIb01DbEdxLUpRUUZIMUVZZnFCN1FBIiwic3BvdXNlIiwiZGlkOmV4YW1wbGU6YzI3NmUxMmVjMjFlYmZlYjFmNzEyZWJjNmYxIl0~WyI5ZDFzTUFYUEVTZkEzaTE0NDNzVTRRIiwiZGVncmVlIix7ImRlZ3JlZSI6Ik1JVCIsInR5cGUiOiJCYWNoZWxvckRlZ3JlZSJ9XQ~WyJiekpGY1pYMkYyRjE3XzVsSFU2MjF3IiwibmFtZSIsIkpheWRlbiBEb2UiXQ` // nolint: lll
diff --git a/pkg/doc/sdjwt/verifier/verifier_test.go b/pkg/doc/sdjwt/verifier/verifier_test.go
index 9989443b..f7a5de15 100644
--- a/pkg/doc/sdjwt/verifier/verifier_test.go
+++ b/pkg/doc/sdjwt/verifier/verifier_test.go
@@ -60,11 +60,32 @@ func TestParse(t *testing.T) {
 		claims, err := Parse(combinedFormatForPresentation, WithSignatureVerifier(verifier))
 		r.NoError(err)
 		require.NotNil(t, claims)
+
 		// expected claims iss, exp, iat, nbf, given_name
 		// TODO: should we default exp, iat, nbf
 		require.Equal(t, 5, len(claims))
 	})
 
+	t.Run("success - VCS sample", func(t *testing.T) {
+		token, err := afjwt.Parse(vcSDJWT, afjwt.WithSignatureVerifier(&holder.NoopSignatureVerifier{}))
+		r.NoError(err)
+
+		var payload map[string]interface{}
+		err = token.DecodeClaims(&payload)
+		r.NoError(err)
+
+		printObject(t, "SD-JWT Payload with VC", payload)
+
+		vcCombinedFormatForPresentation := vcCombinedFormatForIssuance + common.CombinedFormatSeparator
+		claims, err := Parse(vcCombinedFormatForPresentation, WithSignatureVerifier(&holder.NoopSignatureVerifier{}))
+		r.NoError(err)
+
+		printObject(t, "Disclosed Claims with VC", claims)
+
+		// expected claims iat, iss, jti, nbf, sub, vc
+		require.Equal(t, 6, len(claims))
+	})
+
 	t.Run("success - RS256 signing algorithm", func(t *testing.T) {
 		privKey, err := rsa.GenerateKey(rand.Reader, 2048)
 		r.NoError(err)
@@ -762,10 +783,10 @@ func buildJWS(signer afjose.Signer, claims interface{}) (string, error) {
 func printObject(t *testing.T, name string, obj interface{}) {
 	t.Helper()
 
-	parsedClaimsBytes, err := json.Marshal(obj)
+	objBytes, err := json.Marshal(obj)
 	require.NoError(t, err)
 
-	prettyJSON, err := prettyPrint(parsedClaimsBytes)
+	prettyJSON, err := prettyPrint(objBytes)
 	require.NoError(t, err)
 
 	fmt.Println(name + ":")
@@ -784,3 +805,6 @@ func prettyPrint(msg []byte) (string, error) {
 }
 
 const additionalDisclosure = `WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd`
+
+const vcCombinedFormatForIssuance = `eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp0cnVzdGJsb2M6YWJjI2tleTEifQ.eyJpYXQiOjE2NzM5ODc1NDcsImlzcyI6ImRpZDpleGFtcGxlOjc2ZTEyZWM3MTJlYmM2ZjFjMjIxZWJmZWIxZiIsImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwibmJmIjoxNjczOTg3NTQ3LCJzdWIiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJfc2QiOlsiVklMLXlXZHlZX3hNbk5icEJvbHRrcnlBOFZESVFVYllLd2dQaHVEUEx1ZyIsIkxDLUg0R2N4UG1OdGN5VWNiSGFDbTlEUDFnZDROYXJsZ2RiUFc4ZEVvZ2siLCJlbHVRRFVtbHpwM19naU9uRFVRVk1WLWpWM1hMNXVIck1BNzRROXI0dVF3Il0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifSwiZmlyc3RfbmFtZSI6IkZpcnN0IG5hbWUiLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwiaW5mbyI6IkluZm8iLCJpc3N1YW5jZURhdGUiOiIyMDIzLTAxLTE3VDIyOjMyOjI3LjQ2ODEwOTgxNyswMjowMCIsImlzc3VlciI6ImRpZDpleGFtcGxlOjc2ZTEyZWM3MTJlYmM2ZjFjMjIxZWJmZWIxZiIsImxhc3RfbmFtZSI6Ikxhc3QgbmFtZSIsInR5cGUiOiJWZXJpZmlhYmxlQ3JlZGVudGlhbCJ9fQ.PvRYW8-EAG7K4QQL3TV-GNF--vaYIGc3TWJrRSoc2qBCVT5sFkez7FTLv7iae24S2mi2GH5lcxy1dx75LSjOBA~WyJIb01DbEdxLUpRUUZIMUVZZnFCN1FBIiwic3BvdXNlIiwiZGlkOmV4YW1wbGU6YzI3NmUxMmVjMjFlYmZlYjFmNzEyZWJjNmYxIl0~WyI5ZDFzTUFYUEVTZkEzaTE0NDNzVTRRIiwiZGVncmVlIix7ImRlZ3JlZSI6Ik1JVCIsInR5cGUiOiJCYWNoZWxvckRlZ3JlZSJ9XQ~WyJiekpGY1pYMkYyRjE3XzVsSFU2MjF3IiwibmFtZSIsIkpheWRlbiBEb2UiXQ` // nolint: lll
+const vcSDJWT = `eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp0cnVzdGJsb2M6YWJjI2tleTEifQ.eyJpYXQiOjE2NzM5ODc1NDcsImlzcyI6ImRpZDpleGFtcGxlOjc2ZTEyZWM3MTJlYmM2ZjFjMjIxZWJmZWIxZiIsImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwibmJmIjoxNjczOTg3NTQ3LCJzdWIiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJfc2QiOlsiVklMLXlXZHlZX3hNbk5icEJvbHRrcnlBOFZESVFVYllLd2dQaHVEUEx1ZyIsIkxDLUg0R2N4UG1OdGN5VWNiSGFDbTlEUDFnZDROYXJsZ2RiUFc4ZEVvZ2siLCJlbHVRRFVtbHpwM19naU9uRFVRVk1WLWpWM1hMNXVIck1BNzRROXI0dVF3Il0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEifSwiZmlyc3RfbmFtZSI6IkZpcnN0IG5hbWUiLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwiaW5mbyI6IkluZm8iLCJpc3N1YW5jZURhdGUiOiIyMDIzLTAxLTE3VDIyOjMyOjI3LjQ2ODEwOTgxNyswMjowMCIsImlzc3VlciI6ImRpZDpleGFtcGxlOjc2ZTEyZWM3MTJlYmM2ZjFjMjIxZWJmZWIxZiIsImxhc3RfbmFtZSI6Ikxhc3QgbmFtZSIsInR5cGUiOiJWZXJpZmlhYmxlQ3JlZGVudGlhbCJ9fQ.PvRYW8-EAG7K4QQL3TV-GNF--vaYIGc3TWJrRSoc2qBCVT5sFkez7FTLv7iae24S2mi2GH5lcxy1dx75LSjOBA`                                                                                                                                                                                                                                                                                                   // nolint: lll