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

Issue:
SD-JWT: Add structured claims option
Add: 
- structured claims option to issuer
- structured claims processing to verifier

Pull request description:
feat: Add structured claims option
Added:
- structured claims option to issuer
- verify digests in structured claims for holder
- structured claims processing to verifier

Closes #3478

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 2387d2e6..3ca71f06 100644
--- a/pkg/doc/sdjwt/common/common.go
+++ b/pkg/doc/sdjwt/common/common.go
@@ -184,18 +184,8 @@ func VerifyDisclosuresInSDJWT(disclosures []string, signedJWT *afgjwt.JSONWebTok
 	}
 
 	// check that the _sd_alg claim is present
-	sdAlg, err := getSDAlg(claims)
-	if err != nil {
-		return err
-	}
-
 	// check that _sd_alg value is understood and the hash algorithm is deemed secure.
-	cryptoHash, err := getCryptoHash(sdAlg)
-	if err != nil {
-		return err
-	}
-
-	claimsDisclosureDigests, err := GetDisclosureDigests(claims)
+	cryptoHash, err := GetCryptoHashFromClaims(claims)
 	if err != nil {
 		return err
 	}
@@ -206,7 +196,12 @@ func VerifyDisclosuresInSDJWT(disclosures []string, signedJWT *afgjwt.JSONWebTok
 			return err
 		}
 
-		if _, ok := claimsDisclosureDigests[digest]; !ok {
+		found, err := isDigestInClaims(digest, claims)
+		if err != nil {
+			return err
+		}
+
+		if !found {
 			return fmt.Errorf("disclosure digest '%s' not found in SD-JWT disclosure digests", digest)
 		}
 	}
@@ -214,7 +209,48 @@ func VerifyDisclosuresInSDJWT(disclosures []string, signedJWT *afgjwt.JSONWebTok
 	return nil
 }
 
-func getCryptoHash(sdAlg string) (crypto.Hash, error) {
+func isDigestInClaims(digest string, claims map[string]interface{}) (bool, error) {
+	var found bool
+
+	digests, err := GetDisclosureDigests(claims)
+	if err != nil {
+		return false, err
+	}
+
+	for _, value := range claims {
+		if obj, ok := value.(map[string]interface{}); ok {
+			found, err = isDigestInClaims(digest, obj)
+			if err != nil {
+				return false, err
+			}
+
+			if found {
+				return found, nil
+			}
+		}
+	}
+
+	_, ok := digests[digest]
+
+	return ok, nil
+}
+
+// GetCryptoHashFromClaims returns crypto hash from claims.
+func GetCryptoHashFromClaims(claims map[string]interface{}) (crypto.Hash, error) {
+	var cryptoHash crypto.Hash
+
+	// check that the _sd_alg claim is present
+	sdAlg, err := GetSDAlg(claims)
+	if err != nil {
+		return cryptoHash, err
+	}
+
+	// check that _sd_alg value is understood and the hash algorithm is deemed secure.
+	return GetCryptoHash(sdAlg)
+}
+
+// GetCryptoHash returns crypto hash from SD algorithm.
+func GetCryptoHash(sdAlg string) (crypto.Hash, error) {
 	var err error
 
 	var cryptoHash crypto.Hash
@@ -229,7 +265,8 @@ func getCryptoHash(sdAlg string) (crypto.Hash, error) {
 	return cryptoHash, err
 }
 
-func getSDAlg(claims map[string]interface{}) (string, error) {
+// GetSDAlg returns SD algorithm from claims.
+func GetSDAlg(claims map[string]interface{}) (string, error) {
 	obj, ok := claims[SDAlgorithmKey]
 	if !ok {
 		return "", fmt.Errorf("%s must be present in SD-JWT", SDAlgorithmKey)
@@ -258,6 +295,57 @@ func GetDisclosureDigests(claims map[string]interface{}) (map[string]bool, error
 	return sliceToMap(disclosures), nil
 }
 
+// GetDisclosedClaims returns disclosed claims only.
+func GetDisclosedClaims(disclosureClaims []*DisclosureClaim, claims map[string]interface{}) (map[string]interface{}, error) { // nolint:lll
+	hash, err := GetCryptoHashFromClaims(claims)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get crypto hash from claims: %w", err)
+	}
+
+	output := copyMap(claims)
+
+	err = processDisclosedClaims(disclosureClaims, output, hash)
+	if err != nil {
+		return nil, fmt.Errorf("failed to process disclosed claims: %w", err)
+	}
+
+	return output, nil
+}
+
+func processDisclosedClaims(disclosureClaims []*DisclosureClaim, claims map[string]interface{}, hash crypto.Hash) error { // nolint:lll
+	digests, err := GetDisclosureDigests(claims)
+	if err != nil {
+		return err
+	}
+
+	for key, value := range claims {
+		if obj, ok := value.(map[string]interface{}); ok {
+			err := processDisclosedClaims(disclosureClaims, obj, hash)
+			if err != nil {
+				return err
+			}
+
+			claims[key] = obj
+		}
+	}
+
+	for _, dc := range disclosureClaims {
+		digest, err := GetHash(hash, dc.Disclosure)
+		if err != nil {
+			return err
+		}
+
+		if _, ok := digests[digest]; ok {
+			claims[dc.Name] = dc.Value
+		}
+	}
+
+	delete(claims, SDKey)
+	delete(claims, SDAlgorithmKey)
+
+	return nil
+}
+
 func stringArray(entry interface{}) ([]string, error) {
 	if entry == nil {
 		return nil, nil
@@ -290,3 +378,18 @@ func sliceToMap(ids []string) map[string]bool {
 
 	return values
 }
+
+func copyMap(m map[string]interface{}) map[string]interface{} {
+	cm := make(map[string]interface{})
+
+	for k, v := range m {
+		vm, ok := v.(map[string]interface{})
+		if ok {
+			cm[k] = copyMap(vm)
+		} else {
+			cm[k] = v
+		}
+	}
+
+	return cm
+}
diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go
index 7ef413bb..71d95651 100644
--- a/pkg/doc/sdjwt/issuer/issuer.go
+++ b/pkg/doc/sdjwt/issuer/issuer.go
@@ -23,6 +23,7 @@ import (
 	"github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk"
 	afgjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
 	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common"
+	jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json"
 )
 
 const (
@@ -56,7 +57,8 @@ type newOpts struct {
 	jsonMarshal func(v interface{}) ([]byte, error)
 	getSalt     func() (string, error)
 
-	addDecoyDigests bool
+	addDecoyDigests  bool
+	structuredClaims bool
 }
 
 // NewOpt is the SD-JWT New option.
@@ -132,6 +134,13 @@ func WithDecoyDigests(flag bool) NewOpt {
 	}
 }
 
+// WithStructuredClaims is an option for handling structured claims(default is false).
+func WithStructuredClaims(flag bool) NewOpt {
+	return func(opts *newOpts) {
+		opts.structuredClaims = flag
+	}
+}
+
 // New creates new signed Selective Disclosure JWT based on input claims.
 func New(issuer string, claims interface{}, headers jose.Headers,
 	signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) {
@@ -143,6 +152,7 @@ func New(issuer string, claims interface{}, headers jose.Headers,
 		HashAlg:     defaultHash,
 
 		// TODO: Discuss with Troy about defaults
+		// TODO: We may not need defaulted values here at all
 		IssuedAt:  jwt.NewNumericDate(now),
 		NotBefore: jwt.NewNumericDate(now),
 		Expiry:    jwt.NewNumericDate(now.Add(year)),
@@ -152,28 +162,21 @@ func New(issuer string, claims interface{}, headers jose.Headers,
 		opt(nOpts)
 	}
 
-	decoyDisclosures, err := createDecoyDisclosures(nOpts)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create decoy disclosures: %w", err)
-	}
-
 	claimsMap, err := afgjwt.PayloadToMap(claims)
 	if err != nil {
 		return nil, fmt.Errorf("convert payload to map: %w", err)
 	}
 
-	disclosures, err := createDisclosures(claimsMap, nOpts)
+	disclosures, digests, err := createDisclosuresAndDigests(claimsMap, nOpts)
 	if err != nil {
 		return nil, err
 	}
 
-	digests, err := createDigests(append(disclosures, decoyDisclosures...), nOpts)
+	payload, err := jsonutil.MergeCustomFields(createPayload(issuer, nOpts), digests)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("failed to merge payload and digests: %w", err)
 	}
 
-	payload := createPayload(issuer, digests, nOpts)
-
 	signedJWT, err := afgjwt.NewSigned(payload, headers, signer)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create SD-JWT from payload[%+v]: %w", payload, err)
@@ -182,7 +185,7 @@ func New(issuer string, claims interface{}, headers jose.Headers,
 	return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil
 }
 
-func createPayload(issuer string, digests []string, nOpts *newOpts) *payload {
+func createPayload(issuer string, nOpts *newOpts) *payload {
 	var cnf map[string]interface{}
 	if nOpts.HolderPublicKey != nil {
 		cnf = make(map[string]interface{})
@@ -197,7 +200,6 @@ func createPayload(issuer string, digests []string, nOpts *newOpts) *payload {
 		Expiry:    nOpts.Expiry,
 		NotBefore: nOpts.NotBefore,
 		CNF:       cnf,
-		SD:        digests,
 		SDAlg:     strings.ToLower(nOpts.HashAlg.String()),
 	}
 
@@ -279,19 +281,48 @@ func (j *SelectiveDisclosureJWT) Serialize(detached bool) (string, error) {
 	return cf.Serialize(), nil
 }
 
-func createDisclosures(claims map[string]interface{}, opts *newOpts) ([]string, error) {
+func createDisclosuresAndDigests(claims map[string]interface{}, opts *newOpts) ([]string, map[string]interface{}, error) { // nolint:lll
 	var disclosures []string
 
+	var levelDisclosures []string
+
+	digestsMap := make(map[string]interface{})
+
+	decoyDisclosures, err := createDecoyDisclosures(opts)
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to create decoy disclosures: %w", err)
+	}
+
 	for key, value := range claims {
-		disclosure, err := createDisclosure(key, value, opts)
-		if err != nil {
-			return nil, fmt.Errorf("create disclosure: %w", err)
+		if obj, ok := value.(map[string]interface{}); ok && opts.structuredClaims {
+			nestedDisclosures, nestedDigestsMap, e := createDisclosuresAndDigests(obj, opts)
+			if e != nil {
+				return nil, nil, e
+			}
+
+			digestsMap[key] = nestedDigestsMap
+
+			disclosures = append(disclosures, nestedDisclosures...)
+		} else {
+			disclosure, e := createDisclosure(key, value, opts)
+			if e != nil {
+				return nil, nil, fmt.Errorf("create disclosure: %w", e)
+			}
+
+			levelDisclosures = append(levelDisclosures, disclosure)
 		}
+	}
+
+	disclosures = append(disclosures, levelDisclosures...)
 
-		disclosures = append(disclosures, disclosure)
+	digests, err := createDigests(append(levelDisclosures, decoyDisclosures...), opts)
+	if err != nil {
+		return nil, nil, err
 	}
 
-	return disclosures, nil
+	digestsMap[common.SDKey] = digests
+
+	return disclosures, digestsMap, nil
 }
 
 func createDisclosure(key string, value interface{}, opts *newOpts) (string, error) {
@@ -334,6 +365,5 @@ type payload struct {
 
 	CNF map[string]interface{} `json:"cnf,omitempty"`
 
-	SD    []string `json:"_sd,omitempty"`
-	SDAlg string   `json:"_sd_alg,omitempty"`
+	SDAlg string `json:"_sd_alg,omitempty"`
 }
diff --git a/pkg/doc/sdjwt/verifier/verifier.go b/pkg/doc/sdjwt/verifier/verifier.go
index ed00afeb..f9286e75 100644
--- a/pkg/doc/sdjwt/verifier/verifier.go
+++ b/pkg/doc/sdjwt/verifier/verifier.go
@@ -145,7 +145,7 @@ func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]i
 		return nil, fmt.Errorf("failed to verify holder binding: %w", err)
 	}
 
-	return getVerifiedPayload(cfp.Disclosures, signedJWT)
+	return getDisclosedClaims(cfp.Disclosures, signedJWT)
 }
 
 func verifyHolderBinding(sdJWT *afgjwt.JSONWebToken, holderBinding string, pOpts *parseOpts) error {
@@ -274,27 +274,25 @@ func getSignatureVerifierFromCNF(cnf map[string]interface{}) (jose.SignatureVeri
 	return signatureVerifier, nil
 }
 
-func getVerifiedPayload(disclosures []string, signedJWT *afgjwt.JSONWebToken) (map[string]interface{}, error) {
+func getDisclosedClaims(disclosures []string, signedJWT *afgjwt.JSONWebToken) (map[string]interface{}, error) {
 	disclosureClaims, err := common.GetDisclosureClaims(disclosures)
 	if err != nil {
-		return nil, fmt.Errorf("failed to get verified claims: %w", err)
+		return nil, fmt.Errorf("failed to get verified payload: %w", err)
 	}
 
 	var claims map[string]interface{}
 
 	err = signedJWT.DecodeClaims(&claims)
 	if err != nil {
-		return nil, fmt.Errorf("failed to get verified claims: %w", err)
+		return nil, fmt.Errorf("failed to decode verified payload: %w", err)
 	}
 
-	for _, dc := range disclosureClaims {
-		claims[dc.Name] = dc.Value
+	disclosedClaims, err := common.GetDisclosedClaims(disclosureClaims, claims)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get disclosed claims: %w", err)
 	}
 
-	delete(claims, common.SDKey)
-	delete(claims, common.SDAlgorithmKey)
-
-	return claims, nil
+	return disclosedClaims, nil
 }
 
 func verifySigningAlg(joseHeaders jose.Headers, secureAlgs []string) error {


Test patch:
diff --git a/pkg/doc/sdjwt/common/common_test.go b/pkg/doc/sdjwt/common/common_test.go
index b7c40cff..90739a3f 100644
--- a/pkg/doc/sdjwt/common/common_test.go
+++ b/pkg/doc/sdjwt/common/common_test.go
@@ -116,6 +116,17 @@ func TestParseCombinedFormatForPresentation(t *testing.T) {
 
 		require.Equal(t, testCFI, cfp.Serialize())
 	})
+
+	t.Run("success - SD-JWT + multiple disclosures only", func(t *testing.T) {
+		specExample2bPresentation := fmt.Sprintf("%s%s", specExample2bJWT, specExample2bDisclosures)
+
+		cfp := ParseCombinedFormatForPresentation(specExample2bPresentation)
+		require.Equal(t, specExample2bJWT, cfp.SDJWT)
+		require.Equal(t, 6, len(cfp.Disclosures))
+		require.Empty(t, cfp.HolderBinding)
+
+		require.Equal(t, specExample2bPresentation, cfp.Serialize())
+	})
 }
 
 func TestVerifyDisclosuresInSDJWT(t *testing.T) {
@@ -137,6 +148,18 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 		r.NoError(err)
 	})
 
+	t.Run("success - complex struct(spec example 2b)", func(t *testing.T) {
+		specExample2bPresentation := fmt.Sprintf("%s%s", specExample2bJWT, specExample2bDisclosures)
+
+		sdJWT := ParseCombinedFormatForPresentation(specExample2bPresentation)
+
+		signedJWT, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{}))
+		require.NoError(t, err)
+
+		err = VerifyDisclosuresInSDJWT(sdJWT.Disclosures, signedJWT)
+		r.NoError(err)
+	})
+
 	t.Run("success - no selective disclosures(valid case)", func(t *testing.T) {
 		jwtPayload := &payload{
 			Issuer: "issuer",
@@ -237,7 +260,7 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 		signedJWT, err := afjwt.NewSigned(payload, nil, signer)
 		r.NoError(err)
 
-		err = VerifyDisclosuresInSDJWT(nil, signedJWT)
+		err = VerifyDisclosuresInSDJWT([]string{additionalDisclosure}, signedJWT)
 		r.Error(err)
 		r.Contains(err.Error(), "get disclosure digests: entry type[string] is not an array")
 	})
@@ -250,7 +273,7 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 		signedJWT, err := afjwt.NewSigned(payload, nil, signer)
 		r.NoError(err)
 
-		err = VerifyDisclosuresInSDJWT(nil, signedJWT)
+		err = VerifyDisclosuresInSDJWT([]string{additionalDisclosure}, signedJWT)
 		r.Error(err)
 		r.Contains(err.Error(), "get disclosure digests: entry item type[float64] is not a string")
 	})
@@ -310,6 +333,119 @@ func TestGetDisclosureClaims(t *testing.T) {
 	})
 }
 
+func TestGetDisclosedClaims(t *testing.T) {
+	r := require.New(t)
+
+	cfi := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance)
+	r.Equal(testSDJWT, cfi.SDJWT)
+	r.Equal(1, len(cfi.Disclosures))
+
+	disclosureClaims, err := GetDisclosureClaims(cfi.Disclosures)
+	r.NoError(err)
+
+	token, err := afjwt.Parse(cfi.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{}))
+	r.NoError(err)
+
+	var claims map[string]interface{}
+	err = token.DecodeClaims(&claims)
+	r.NoError(err)
+
+	t.Run("success", func(t *testing.T) {
+		disclosedClaims, err := GetDisclosedClaims(disclosureClaims, claims)
+		r.NoError(err)
+		r.NotNil(disclosedClaims)
+
+		r.Equal(5, len(disclosedClaims))
+		r.NotEmpty(disclosedClaims["iat"])
+		r.NotEmpty(disclosedClaims["nbf"])
+		r.NotEmpty(disclosedClaims["iss"])
+		r.Equal("https://example.com/issuer", disclosedClaims["iss"])
+		r.Equal("John", disclosedClaims["given_name"])
+	})
+
+	t.Run("success - with complex object", func(t *testing.T) {
+		testClaims := copyMap(claims)
+
+		parentObj := make(map[string]interface{})
+		parentObj["given_name"] = "Albert"
+		parentObj[SDKey] = claims[SDKey]
+
+		testClaims["father"] = parentObj
+
+		disclosedClaims, err := GetDisclosedClaims(disclosureClaims, testClaims)
+		r.NoError(err)
+		r.NotNil(disclosedClaims)
+
+		r.Equal(6, len(disclosedClaims))
+		r.Equal("John", disclosedClaims["given_name"])
+		r.Equal("John", disclosedClaims["father"].(map[string]interface{})["given_name"])
+	})
+
+	t.Run("error - with complex object", func(t *testing.T) {
+		testClaims := copyMap(claims)
+
+		parentObj := make(map[string]interface{})
+		parentObj["given_name"] = "Albert"
+		parentObj[SDKey] = []interface{}{0}
+
+		testClaims["father"] = parentObj
+
+		disclosedClaims, err := GetDisclosedClaims(disclosureClaims, testClaims)
+		r.Error(err)
+		r.Nil(disclosedClaims)
+
+		r.Contains(err.Error(),
+			"failed to process disclosed claims: get disclosure digests: entry item type[int] is not a string")
+	})
+
+	t.Run("error - no _sd_alg", func(t *testing.T) {
+		disclosedClaims, err := GetDisclosedClaims(disclosureClaims, make(map[string]interface{}))
+		r.Error(err)
+		r.Nil(disclosedClaims)
+
+		r.Contains(err.Error(),
+			"failed to get crypto hash from claims: _sd_alg must be present in SD-JWT")
+	})
+
+	t.Run("error - invalid _sd item", func(t *testing.T) {
+		testClaims := make(map[string]interface{})
+		testClaims[SDAlgorithmKey] = testAlg
+		testClaims[SDKey] = []interface{}{0}
+
+		disclosedClaims, err := GetDisclosedClaims(disclosureClaims, testClaims)
+		r.Error(err)
+		r.Nil(disclosedClaims)
+
+		r.Contains(err.Error(),
+			"failed to process disclosed claims: get disclosure digests: entry item type[int] is not a string")
+	})
+
+	t.Run("error - invalid _sd type", func(t *testing.T) {
+		testClaims := make(map[string]interface{})
+		testClaims[SDAlgorithmKey] = "sha-256"
+		testClaims[SDKey] = "not-array"
+
+		disclosedClaims, err := GetDisclosedClaims(disclosureClaims, testClaims)
+		r.Error(err)
+		r.Nil(disclosedClaims)
+
+		r.Contains(err.Error(),
+			"failed to process disclosed claims: get disclosure digests: entry type[string] is not an array")
+	})
+
+	t.Run("error - get hash fails", func(t *testing.T) {
+		testClaims := make(map[string]interface{})
+		testClaims[SDAlgorithmKey] = "sha-256"
+		testClaims[SDKey] = []interface{}{"abc"}
+
+		err := processDisclosedClaims(disclosureClaims, testClaims, 0)
+		r.Error(err)
+
+		r.Contains(err.Error(),
+			"hash function not available for: 0")
+	})
+}
+
 type NoopSignatureVerifier struct {
 }
 
@@ -335,3 +471,6 @@ type payload struct {
 
 	SDAlg string `json:"_sd_alg,omitempty"`
 }
+
+const specExample2bJWT = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIjBsa1NjS2ppSk1IZWRKZnE3c0pCN0hRM3FvbUdmckVMYm81Z1podktSV28iLCAiMWgyOWdnUWkxeG91LV9OalZ5eW9DaEsyYXN3VXRvMlVqQ2ZGLTFMYWhBOCIsICIzQ29MVUxtRHh4VXdfTGR5WUVUanVkdVh1RXBHdUJ5NHJYSG90dUQ0MFg0IiwgIkFJRHlveHgxaXB5NDUtR0ZwS2d2Yy1ISWJjVnJsTGxyWUxYbXgzZXYyZTQiLCAiT2x0aGZSb0ZkUy1KNlM4Mk9XbHJPNHBXaG9lUk1ySF9LR1BfaDZFYXBZUSIsICJyNGRicEdlZWlhMDJTeUdMNWdCZEhZMXc4SWhqVDN4eDA1UnNmeXlIVWs0Il0sICJhZGRyZXNzIjogeyJfc2QiOiBbIjZPS053bkdHS1dYQ0k5dWlqTkFzdjY0dTIyZUxTNHJNZExObGcxZnFKcDQiLCAiSEVWTWdELU5LSzVOdlhQYkFSb3JWZE9ESVRta1V5dU1wQ3NfbTdIWG5ZYyIsICJVcTAyblY3M0swYmRSSzIzcnphYm1uRGE0TzhZTlFadnQ5eDhMeWtva19ZIiwgIm94RlJpbG5vMjZVWWU3a3FNTTRiZHE4SXZOTXRJaTZGOHB0dC11aVBMYk0iXX0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTUxNjIzOTAyMiwgImV4cCI6IDE1MTYyNDcwMjIsICJfc2RfYWxnIjogInNoYS0yNTYifQ.M45AUExpi9THOTVIHfBmb2GL0WXJf4TeWB5QPmsxdBkj9pUcLOPR8YVafLIt8m_imYHTBYYcAyf7qSnquxMxGQ` // nolint:lll
+const specExample2bDisclosures = `~WyJSdHczZUFFUE5wWjIwTkhZSzNNRWNnIiwgImZhbWlseV9uYW1lIiwgIlx1NWM3MVx1NzUzMCJd~WyJicjgxenVSc0NUcXJuWEp4MHVqMkRRIiwgImdpdmVuX25hbWUiLCAiXHU1OTJhXHU5MGNlIl0~WyI1Z2NXRmxWSEM1VVEwbktrallybDlnIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJJTms2bkx4WGFybDF4NmVabHdBOTV3IiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAbmlob24uY29tIl0~WyJNOVY2N3V0UC1hTF9lR1B0UU5hM0RRIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJzNFhNSmxXQ2Eza3hDWk4wSVVrbnlBIiwgImNvdW50cnkiLCAiSlAiXQ~`                                                                                                                                                                                                                                                                                                                                                                                                                                                             // nolint:lll
diff --git a/pkg/doc/sdjwt/holder/holder_test.go b/pkg/doc/sdjwt/holder/holder_test.go
index 3b3ae0de..987aceba 100644
--- a/pkg/doc/sdjwt/holder/holder_test.go
+++ b/pkg/doc/sdjwt/holder/holder_test.go
@@ -70,6 +70,21 @@ func TestParse(t *testing.T) {
 		require.Equal(t, 7, len(claims))
 	})
 
+	t.Run("success - complex claims", func(t *testing.T) {
+		complexClaims := createComplexClaims()
+
+		token, e := issuer.New(testIssuer, complexClaims, nil, signer,
+			issuer.WithStructuredClaims(true))
+		r.NoError(e)
+		cfi, e := token.Serialize(false)
+		r.NoError(e)
+
+		claims, err := Parse(cfi, WithSignatureVerifier(verifier))
+		r.NoError(err)
+		r.NotNil(claims)
+		r.Equal(10, len(claims))
+	})
+
 	t.Run("error - additional disclosure", func(t *testing.T) {
 		claims, err := Parse(fmt.Sprintf("%s~%s", combinedFormatForIssuance, additionalDisclosure),
 			WithSignatureVerifier(verifier))
@@ -248,6 +263,25 @@ func (m *mockSigner) Headers() jose.Headers {
 	return headers
 }
 
+func createComplexClaims() map[string]interface{} {
+	claims := map[string]interface{}{
+		"sub":          "john_doe_42",
+		"given_name":   "John",
+		"family_name":  "Doe",
+		"email":        "johndoe@example.com",
+		"phone_number": "+1-202-555-0101",
+		"birthdate":    "1940-01-01",
+		"address": map[string]interface{}{
+			"street_address": "123 Main St",
+			"locality":       "Anytown",
+			"region":         "Anystate",
+			"country":        "US",
+		},
+	}
+
+	return claims
+}
+
 // nolint: lll
 const additionalDisclosure = `WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd`
 
diff --git a/pkg/doc/sdjwt/issuer/issuer_test.go b/pkg/doc/sdjwt/issuer/issuer_test.go
index 54fcb200..0a9fb00c 100644
--- a/pkg/doc/sdjwt/issuer/issuer_test.go
+++ b/pkg/doc/sdjwt/issuer/issuer_test.go
@@ -187,6 +187,47 @@ func TestNew(t *testing.T) {
 		r.NoError(err)
 	})
 
+	t.Run("Create Complex Claims JWS with structured claims flag", func(t *testing.T) {
+		r := require.New(t)
+
+		pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
+		r.NoError(err)
+
+		complexClaims := createComplexClaims()
+
+		var newOpts []NewOpt
+
+		newOpts = append(newOpts,
+			WithStructuredClaims(true),
+		)
+
+		token, err := New(issuer, complexClaims, nil, afjwt.NewEd25519Signer(privKey), newOpts...)
+		r.NoError(err)
+		combinedFormatForIssuance, err := token.Serialize(false)
+		require.NoError(t, err)
+
+		fmt.Printf(combinedFormatForIssuance)
+
+		cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance)
+		// expected 6 simple + 4 address object disclosures
+		require.Equal(t, 10, len(cfi.Disclosures))
+
+		var parsedClaims map[string]interface{}
+		err = verifyEd25519ViaGoJose(cfi.SDJWT, pubKey, &parsedClaims)
+		r.NoError(err)
+
+		parsedClaimsBytes, err := json.Marshal(parsedClaims)
+		require.NoError(t, err)
+
+		prettyJSON, err := prettyPrint(parsedClaimsBytes)
+		require.NoError(t, err)
+
+		fmt.Println(prettyJSON)
+
+		err = verifyEd25519(cfi.SDJWT, pubKey)
+		r.NoError(err)
+	})
+
 	t.Run("Create SD-JWS with decoy disclosures", func(t *testing.T) {
 		r := require.New(t)
 
@@ -267,7 +308,7 @@ func TestNew(t *testing.T) {
 		r.Nil(token)
 
 		r.Contains(err.Error(),
-			"unmarshallable claims: marshal interface[*issuer.payload]: json: error calling MarshalJSON for type *jwk.JWK: go-jose/go-jose: unknown key type 'string'") //nolint:lll
+			"failed to merge payload and digests: json: error calling MarshalJSON for type *jwk.JWK: go-jose/go-jose: unknown key type 'string'") //nolint:lll
 	})
 
 	t.Run("error - create decoy disclosures failed", func(t *testing.T) {
diff --git a/pkg/doc/sdjwt/verifier/verifier_test.go b/pkg/doc/sdjwt/verifier/verifier_test.go
index 490a2cd8..9989443b 100644
--- a/pkg/doc/sdjwt/verifier/verifier_test.go
+++ b/pkg/doc/sdjwt/verifier/verifier_test.go
@@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0
 package verifier
 
 import (
+	"bytes"
 	"crypto/ed25519"
 	"crypto/rand"
 	"crypto/rsa"
@@ -718,18 +719,20 @@ func TestGetVerifiedPayload(t *testing.T) {
 	r.NoError(e)
 
 	t.Run("success", func(t *testing.T) {
-		claims, err := getVerifiedPayload(token.Disclosures, token.SignedJWT)
+		claims, err := getDisclosedClaims(token.Disclosures, token.SignedJWT)
 		r.NoError(err)
 		r.NotNil(claims)
 		r.Equal(5, len(claims))
+
+		printObject(t, "Disclosed Claims", claims)
 	})
 
 	t.Run("error - invalid disclosure(not encoded)", func(t *testing.T) {
-		claims, err := getVerifiedPayload([]string{"xyz"}, token.SignedJWT)
+		claims, err := getDisclosedClaims([]string{"xyz"}, token.SignedJWT)
 		r.Error(err)
 		r.Nil(claims)
 		r.Contains(err.Error(),
-			"failed to get verified claims: failed to unmarshal disclosure array: invalid character")
+			"failed to get verified payload: failed to unmarshal disclosure array: invalid character")
 	})
 }
 
@@ -756,4 +759,28 @@ func buildJWS(signer afjose.Signer, claims interface{}) (string, error) {
 	return jws.SerializeCompact(false)
 }
 
+func printObject(t *testing.T, name string, obj interface{}) {
+	t.Helper()
+
+	parsedClaimsBytes, err := json.Marshal(obj)
+	require.NoError(t, err)
+
+	prettyJSON, err := prettyPrint(parsedClaimsBytes)
+	require.NoError(t, err)
+
+	fmt.Println(name + ":")
+	fmt.Println(prettyJSON)
+}
+
+func prettyPrint(msg []byte) (string, error) {
+	var prettyJSON bytes.Buffer
+
+	err := json.Indent(&prettyJSON, msg, "", "\t")
+	if err != nil {
+		return "", err
+	}
+
+	return prettyJSON.String(), nil
+}
+
 const additionalDisclosure = `WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd`