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

Issue:
SD-JWT Holder/Verifier - Process Disclosures
Add utility function to decode disclosures (will be used by both holder and verifier)

Create disclose claims function for holder that will re-create SD-JWT with disclosures for selected claims only.

Verifier should insert decoded claims (claim name/value) at the level of the _sd claim and remove _sd_claims and _sd_alg from SD-JWT payload.

Pull request description:
feat: SD-JWT Holder/Verifier - Process Disclosures
Added utility function to decode disclosures (used by both holder and verifier) Created disclose claims function for holder that re-creates SD-JWT with disclosures for selected claims only.

Created get verified payload function for verifier (removes _sd and _sd_alg from claims and inserts into payload disclosed claims).

Closes #3459

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 80bf265c..0fd2f2e1 100644
--- a/pkg/doc/sdjwt/common/common.go
+++ b/pkg/doc/sdjwt/common/common.go
@@ -9,6 +9,7 @@ package common
 import (
 	"crypto"
 	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -18,7 +19,17 @@ import (
 )
 
 // DisclosureSeparator is disclosure separator.
-const DisclosureSeparator = "~"
+const (
+	DisclosureSeparator = "~"
+
+	SDAlgorithmKey = "_sd_alg"
+	SDKey          = "_sd"
+
+	disclosureParts = 3
+	saltIndex       = 0
+	nameIndex       = 1
+	valueIndex      = 2
+)
 
 // Payload represents SD-JWT payload.
 type Payload struct {
@@ -40,6 +51,62 @@ type SDJWT struct {
 	Disclosures   []string
 }
 
+// DisclosureClaim defines claim.
+type DisclosureClaim struct {
+	Disclosure string
+	Salt       string
+	Name       string
+	Value      interface{}
+}
+
+// GetDisclosureClaims de-codes disclosures.
+func GetDisclosureClaims(disclosures []string) ([]*DisclosureClaim, error) {
+	var claims []*DisclosureClaim
+
+	for _, disclosure := range disclosures {
+		claim, err := getDisclosureClaim(disclosure)
+		if err != nil {
+			return nil, err
+		}
+
+		claims = append(claims, claim)
+	}
+
+	return claims, nil
+}
+
+func getDisclosureClaim(disclosure string) (*DisclosureClaim, error) {
+	decoded, err := base64.RawURLEncoding.DecodeString(disclosure)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode disclosure: %w", err)
+	}
+
+	var disclosureArr []interface{}
+
+	err = json.Unmarshal(decoded, &disclosureArr)
+	if err != nil {
+		return nil, fmt.Errorf("failed to unmarshal disclosure array: %w", err)
+	}
+
+	if len(disclosureArr) != disclosureParts {
+		return nil, fmt.Errorf("disclosure array size[%d] must be %d", len(disclosureArr), disclosureParts)
+	}
+
+	salt, ok := disclosureArr[saltIndex].(string)
+	if !ok {
+		return nil, fmt.Errorf("disclosure salt type[%T] must be string", disclosureArr[saltIndex])
+	}
+
+	name, ok := disclosureArr[nameIndex].(string)
+	if !ok {
+		return nil, fmt.Errorf("disclosure name type[%T] must be string", disclosureArr[nameIndex])
+	}
+
+	claim := &DisclosureClaim{Disclosure: disclosure, Salt: salt, Name: name, Value: disclosureArr[valueIndex]}
+
+	return claim, nil
+}
+
 // ParseSDJWT parses SD-JWT serialized token into SDJWT parts.
 func ParseSDJWT(sdJWTSerialized string) *SDJWT {
 	parts := strings.Split(sdJWTSerialized, DisclosureSeparator)
@@ -120,28 +187,28 @@ func getCryptoHash(sdAlg string) (crypto.Hash, error) {
 	case crypto.SHA256.String():
 		cryptoHash = crypto.SHA256
 	default:
-		err = fmt.Errorf("_sd_alg '%s 'not supported", sdAlg)
+		err = fmt.Errorf("%s '%s 'not supported", SDAlgorithmKey, sdAlg)
 	}
 
 	return cryptoHash, err
 }
 
 func getSDAlg(claims map[string]interface{}) (string, error) {
-	obj, ok := claims["_sd_alg"]
+	obj, ok := claims[SDAlgorithmKey]
 	if !ok {
-		return "", fmt.Errorf("_sd_alg must be present in SD-JWT")
+		return "", fmt.Errorf("%s must be present in SD-JWT", SDAlgorithmKey)
 	}
 
 	str, ok := obj.(string)
 	if !ok {
-		return "", fmt.Errorf("_sd_alg must be a string")
+		return "", fmt.Errorf("%s must be a string", SDAlgorithmKey)
 	}
 
 	return str, nil
 }
 
 func getDisclosureDigests(claims map[string]interface{}) (map[string]bool, error) {
-	disclosuresObj, ok := claims["_sd"]
+	disclosuresObj, ok := claims[SDKey]
 	if !ok {
 		return nil, nil
 	}
diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go
index 5330b4e3..f812b424 100644
--- a/pkg/doc/sdjwt/holder/holder.go
+++ b/pkg/doc/sdjwt/holder/holder.go
@@ -7,11 +7,15 @@ SPDX-License-Identifier: Apache-2.0
 package holder
 
 import (
+	"fmt"
+
 	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
 	afgjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
 	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common"
 )
 
+const notFound = -1
+
 // jwtParseOpts holds options for the SD-JWT parsing.
 type parseOpts struct {
 	detachedPayload []byte
@@ -37,13 +41,14 @@ func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt {
 
 // Parse parses input JWT in serialized form into JSON Web Token.
 func Parse(sdJWTSerialized string, opts ...ParseOpt) (*common.SDJWT, error) {
-	pOpts := &parseOpts{}
+	pOpts := &parseOpts{
+		sigVerifier: &NoopSignatureVerifier{},
+	}
 
 	for _, opt := range opts {
 		opt(pOpts)
 	}
 
-	// TODO: Holder is not required to check issuer signature so we should probably have default no-op verifier
 	var jwtOpts []afgjwt.ParseOpt
 	jwtOpts = append(jwtOpts,
 		afgjwt.WithSignatureVerifier(pOpts.sigVerifier),
@@ -64,6 +69,45 @@ func Parse(sdJWTSerialized string, opts ...ParseOpt) (*common.SDJWT, error) {
 	return sdJWT, nil
 }
 
+// DiscloseClaims discloses selected claims with specified claim names.
+func DiscloseClaims(sdJWTSerialized string, claimNames []string) (string, error) {
+	sdJWT := common.ParseSDJWT(sdJWTSerialized)
+
+	if len(sdJWT.Disclosures) == 0 {
+		return "", fmt.Errorf("no disclosures found in SD-JWT")
+	}
+
+	disclosures, err := common.GetDisclosureClaims(sdJWT.Disclosures)
+	if err != nil {
+		return "", err
+	}
+
+	var selectedDisclosures []string
+
+	for _, claimName := range claimNames {
+		if index := getDisclosureByClaimName(claimName, disclosures); index != notFound {
+			selectedDisclosures = append(selectedDisclosures, sdJWT.Disclosures[index])
+		}
+	}
+
+	combinedFormatForPresentation := sdJWT.JWTSerialized
+	for _, disclosure := range selectedDisclosures {
+		combinedFormatForPresentation += common.DisclosureSeparator + disclosure
+	}
+
+	return combinedFormatForPresentation, nil
+}
+
+func getDisclosureByClaimName(name string, disclosures []*common.DisclosureClaim) int {
+	for index, disclosure := range disclosures {
+		if disclosure.Name == name {
+			return index
+		}
+	}
+
+	return notFound
+}
+
 // NoopSignatureVerifier is no-op signature verifier (signature will not get checked).
 type NoopSignatureVerifier struct {
 }
diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go
index 5ba29486..fcc8fd82 100644
--- a/pkg/doc/sdjwt/issuer/issuer.go
+++ b/pkg/doc/sdjwt/issuer/issuer.go
@@ -25,7 +25,7 @@ import (
 
 const (
 	defaultHash     = crypto.SHA256
-	defaultSaltSize = 28
+	defaultSaltSize = 128 / 8
 
 	year = 365 * 24 * 60 * time.Minute
 )
@@ -241,5 +241,6 @@ func generateSalt() (string, error) {
 		return "", err
 	}
 
-	return string(salt), nil
+	// it is RECOMMENDED to base64url-encode the salt value, producing a string.
+	return base64.RawURLEncoding.EncodeToString(salt), nil
 }
diff --git a/pkg/doc/sdjwt/verifier/verifier.go b/pkg/doc/sdjwt/verifier/verifier.go
index ff8b2553..150874ac 100644
--- a/pkg/doc/sdjwt/verifier/verifier.go
+++ b/pkg/doc/sdjwt/verifier/verifier.go
@@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved.
 SPDX-License-Identifier: Apache-2.0
 */
 
-package holder
+package verifier
 
 import (
 	"fmt"
@@ -48,7 +48,7 @@ func WithSigningAlgorithms(algorithms []string) ParseOpt {
 }
 
 // Parse parses input JWT in serialized form into JSON Web Token.
-func Parse(sdJWTSerialized string, opts ...ParseOpt) (*common.SDJWT, error) {
+func Parse(sdJWTSerialized string, opts ...ParseOpt) (map[string]interface{}, error) {
 	pOpts := &parseOpts{
 		signingAlgorithms: []string{"EdDSA", "RS256"},
 	}
@@ -99,7 +99,30 @@ func Parse(sdJWTSerialized string, opts ...ParseOpt) (*common.SDJWT, error) {
 		return nil, err
 	}
 
-	return sdJWT, nil
+	return getVerifiedPayload(sdJWT.Disclosures, signedJWT)
+}
+
+func getVerifiedPayload(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)
+	}
+
+	var claims map[string]interface{}
+
+	err = signedJWT.DecodeClaims(&claims)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get verified claims: %w", err)
+	}
+
+	for _, dc := range disclosureClaims {
+		claims[dc.Name] = dc.Value
+	}
+
+	delete(claims, common.SDKey)
+	delete(claims, common.SDAlgorithmKey)
+
+	return claims, 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 70399def..f7d22c1d 100644
--- a/pkg/doc/sdjwt/common/common_test.go
+++ b/pkg/doc/sdjwt/common/common_test.go
@@ -10,6 +10,9 @@ import (
 	"crypto"
 	"crypto/ed25519"
 	"crypto/rand"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -84,8 +87,8 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 
 	t.Run("success - selective disclosures nil", func(t *testing.T) {
 		payload := make(map[string]interface{})
-		payload["_sd_alg"] = testAlg
-		payload["_sd"] = nil
+		payload[SDAlgorithmKey] = testAlg
+		payload[SDKey] = nil
 
 		signedJWT, err := afjwt.NewSigned(payload, nil, signer)
 		r.NoError(err)
@@ -132,7 +135,7 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 
 		err = VerifyDisclosuresInSDJWT(nil, signedJWT)
 		r.Error(err)
-		r.Contains(err.Error(), "_sd_alg must be present in SD-JWT")
+		r.Contains(err.Error(), "_sd_alg must be present in SD-JWT", SDAlgorithmKey)
 	})
 
 	t.Run("error - invalid algorithm", func(t *testing.T) {
@@ -151,7 +154,7 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 
 	t.Run("error - algorithm is not a string", func(t *testing.T) {
 		payload := make(map[string]interface{})
-		payload["_sd_alg"] = 18
+		payload[SDAlgorithmKey] = 18
 
 		signedJWT, err := afjwt.NewSigned(payload, nil, signer)
 		r.NoError(err)
@@ -163,8 +166,8 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 
 	t.Run("error - selective disclosures must be an array", func(t *testing.T) {
 		payload := make(map[string]interface{})
-		payload["_sd_alg"] = testAlg
-		payload["_sd"] = "test"
+		payload[SDAlgorithmKey] = testAlg
+		payload[SDKey] = "test"
 
 		signedJWT, err := afjwt.NewSigned(payload, nil, signer)
 		r.NoError(err)
@@ -176,8 +179,8 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 
 	t.Run("error - selective disclosures must be a string", func(t *testing.T) {
 		payload := make(map[string]interface{})
-		payload["_sd_alg"] = testAlg
-		payload["_sd"] = []int{123}
+		payload[SDAlgorithmKey] = testAlg
+		payload[SDKey] = []int{123}
 
 		signedJWT, err := afjwt.NewSigned(payload, nil, signer)
 		r.NoError(err)
@@ -188,6 +191,60 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) {
 	})
 }
 
+func TestGetDisclosureClaims(t *testing.T) {
+	r := require.New(t)
+
+	t.Run("success", func(t *testing.T) {
+		sdJWT := ParseSDJWT(sdJWT)
+		require.Equal(t, 1, len(sdJWT.Disclosures))
+
+		disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures)
+		r.NoError(err)
+		r.Len(disclosureClaims, 1)
+
+		r.Equal("given_name", disclosureClaims[0].Name)
+		r.Equal("John", disclosureClaims[0].Value)
+	})
+
+	t.Run("error - invalid disclosure format (not encoded)", func(t *testing.T) {
+		sdJWT := ParseSDJWT("jws~xyz")
+		require.Equal(t, 1, len(sdJWT.Disclosures))
+
+		disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures)
+		r.Error(err)
+		r.Nil(disclosureClaims)
+		r.Contains(err.Error(), "failed to unmarshal disclosure array")
+	})
+
+	t.Run("error - invalid disclosure array (not three parts)", func(t *testing.T) {
+		disclosureArr := []interface{}{"name", "value"}
+		disclosureJSON, err := json.Marshal(disclosureArr)
+		require.NoError(t, err)
+
+		sdJWT := ParseSDJWT(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON)))
+		require.Equal(t, 1, len(sdJWT.Disclosures))
+
+		disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures)
+		r.Error(err)
+		r.Nil(disclosureClaims)
+		r.Contains(err.Error(), "disclosure array size[2] must be 3")
+	})
+
+	t.Run("error - invalid disclosure array (name is not a string)", func(t *testing.T) {
+		disclosureArr := []interface{}{"salt", 123, "value"}
+		disclosureJSON, err := json.Marshal(disclosureArr)
+		require.NoError(t, err)
+
+		sdJWT := ParseSDJWT(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON)))
+		require.Equal(t, 1, len(sdJWT.Disclosures))
+
+		disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures)
+		r.Error(err)
+		r.Nil(disclosureClaims)
+		r.Contains(err.Error(), "disclosure name type[float64] must be string")
+	})
+}
+
 type NoopSignatureVerifier struct {
 }
 
diff --git a/pkg/doc/sdjwt/holder/holder_test.go b/pkg/doc/sdjwt/holder/holder_test.go
index c3550671..3f1f46f6 100644
--- a/pkg/doc/sdjwt/holder/holder_test.go
+++ b/pkg/doc/sdjwt/holder/holder_test.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
 	afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
+	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common"
 	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer"
 )
 
@@ -50,6 +51,13 @@ func TestParse(t *testing.T) {
 		require.Equal(t, 1, len(sdJWT.Disclosures))
 	})
 
+	t.Run("success - default is no signature verifier", func(t *testing.T) {
+		sdJWT, err := Parse(sdJWTSerialized)
+		r.NoError(err)
+		require.NotNil(t, sdJWT)
+		require.Equal(t, 1, len(sdJWT.Disclosures))
+	})
+
 	t.Run("success - spec SD-JWT", func(t *testing.T) {
 		sdJWT, err := Parse(specSDJWT, WithSignatureVerifier(&NoopSignatureVerifier{}))
 		r.NoError(err)
@@ -90,6 +98,44 @@ func TestParse(t *testing.T) {
 	})
 }
 
+func TestDiscloseClaims(t *testing.T) {
+	r := require.New(t)
+
+	_, privKey, e := ed25519.GenerateKey(rand.Reader)
+	r.NoError(e)
+
+	signer := afjwt.NewEd25519Signer(privKey)
+	claims := map[string]interface{}{"given_name": "Albert"}
+
+	token, e := issuer.New(testIssuer, claims, nil, signer)
+	r.NoError(e)
+	sdJWTSerialized, e := token.Serialize(false)
+	r.NoError(e)
+
+	t.Run("success", func(t *testing.T) {
+		sdJWTDisclosed, err := DiscloseClaims(sdJWTSerialized, []string{"given_name"})
+		r.NoError(err)
+		require.NotNil(t, sdJWTDisclosed)
+		require.Equal(t, sdJWTSerialized, sdJWTDisclosed)
+	})
+
+	t.Run("error - no disclosure(s)", func(t *testing.T) {
+		sdJWT := common.ParseSDJWT(sdJWTSerialized)
+
+		sdJWTDisclosed, err := DiscloseClaims(sdJWT.JWTSerialized, []string{"given_name"})
+		r.Error(err)
+		r.Empty(sdJWTDisclosed)
+		r.Contains(err.Error(), "no disclosures found in SD-JWT")
+	})
+
+	t.Run("error - invalid disclosure", func(t *testing.T) {
+		sdJWTDisclosed, err := DiscloseClaims(fmt.Sprintf("%s~%s", sdJWTSerialized, "abc"), []string{"given_name"})
+		r.Error(err)
+		r.Empty(sdJWTDisclosed)
+		r.Contains(err.Error(), "failed to unmarshal disclosure array")
+	})
+}
+
 func TestWithJWTDetachedPayload(t *testing.T) {
 	detachedPayloadOpt := WithJWTDetachedPayload([]byte("payload"))
 	require.NotNil(t, detachedPayloadOpt)
diff --git a/pkg/doc/sdjwt/issuer/issuer_test.go b/pkg/doc/sdjwt/issuer/issuer_test.go
index fc1d7a03..770436f9 100644
--- a/pkg/doc/sdjwt/issuer/issuer_test.go
+++ b/pkg/doc/sdjwt/issuer/issuer_test.go
@@ -465,7 +465,7 @@ func verifyRS256(jws string, pubKey *rsa.PublicKey) error {
 }
 
 func existsInDisclosures(claims map[string]interface{}, val string) bool {
-	disclosuresObj, ok := claims["_sd"]
+	disclosuresObj, ok := claims[common.SDKey]
 	if !ok {
 		return false
 	}
diff --git a/pkg/doc/sdjwt/verifier/verifier_test.go b/pkg/doc/sdjwt/verifier/verifier_test.go
index ebbd6678..11882c09 100644
--- a/pkg/doc/sdjwt/verifier/verifier_test.go
+++ b/pkg/doc/sdjwt/verifier/verifier_test.go
@@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved.
 SPDX-License-Identifier: Apache-2.0
 */
 
-package holder
+package verifier
 
 import (
 	"crypto/ed25519"
@@ -36,9 +36,9 @@ func TestParse(t *testing.T) {
 	r.NoError(e)
 
 	signer := afjwt.NewEd25519Signer(privKey)
-	claims := map[string]interface{}{"given_name": "Albert"}
+	selectiveClaims := map[string]interface{}{"given_name": "Albert"}
 
-	token, e := issuer.New(testIssuer, claims, nil, signer)
+	token, e := issuer.New(testIssuer, selectiveClaims, nil, signer)
 	r.NoError(e)
 	sdJWTSerialized, e := token.Serialize(false)
 	r.NoError(e)
@@ -47,10 +47,12 @@ func TestParse(t *testing.T) {
 	r.NoError(e)
 
 	t.Run("success - EdDSA signing algorithm", func(t *testing.T) {
-		sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
+		claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
 		r.NoError(err)
-		require.NotNil(t, sdJWT)
-		require.Equal(t, 1, len(sdJWT.Disclosures))
+		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 - RS256 signing algorithm", func(t *testing.T) {
@@ -61,15 +63,14 @@ func TestParse(t *testing.T) {
 
 		v := afjwt.NewRS256Verifier(pubKey)
 
-		token, err := issuer.New(testIssuer, claims, nil, afjwt.NewRS256Signer(privKey, nil))
+		token, err := issuer.New(testIssuer, selectiveClaims, nil, afjwt.NewRS256Signer(privKey, nil))
 		r.NoError(err)
 		tokenSerialized, err := token.Serialize(false)
 		require.NoError(t, err)
 
-		sdJWT, err := Parse(tokenSerialized, WithSignatureVerifier(v))
+		claims, err := Parse(tokenSerialized, WithSignatureVerifier(v))
 		r.NoError(err)
-		require.NotNil(t, sdJWT)
-		require.Equal(t, 1, len(sdJWT.Disclosures))
+		require.Equal(t, 5, len(claims))
 	})
 
 	t.Run("success - valid SD-JWT times", func(t *testing.T) {
@@ -77,7 +78,7 @@ func TestParse(t *testing.T) {
 		oneHourInThePast := now.Add(-time.Hour)
 		oneHourInTheFuture := now.Add(time.Hour)
 
-		tokenWithTimes, e := issuer.New(testIssuer, claims, nil, signer,
+		tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer,
 			issuer.WithIssuedAt(jwt.NewNumericDate(oneHourInThePast)),
 			issuer.WithNotBefore(jwt.NewNumericDate(oneHourInThePast)),
 			issuer.WithExpiry(jwt.NewNumericDate(oneHourInTheFuture)))
@@ -85,31 +86,34 @@ func TestParse(t *testing.T) {
 		serialized, e := tokenWithTimes.Serialize(false)
 		r.NoError(e)
 
-		sdJWT, err := Parse(serialized, WithSignatureVerifier(verifier))
+		claims, err := Parse(serialized, WithSignatureVerifier(verifier))
 		r.NoError(err)
-		r.NotNil(sdJWT)
+		r.NotNil(claims)
 	})
 
 	t.Run("error - signing algorithm not supported", func(t *testing.T) {
-		sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier), WithSigningAlgorithms([]string{}))
+		claims, err := Parse(sdJWTSerialized,
+			WithSignatureVerifier(verifier),
+			WithSigningAlgorithms([]string{}))
 		r.Error(err)
-		require.Nil(t, sdJWT)
+		require.Nil(t, claims)
 		require.Equal(t, err.Error(), "alg 'EdDSA' is not in the allowed list")
 	})
 
 	t.Run("error - additional disclosure", func(t *testing.T) {
-		sdJWT, err := Parse(fmt.Sprintf("%s~%s", sdJWTSerialized, additionalDisclosure), WithSignatureVerifier(verifier))
+		claims, err := Parse(fmt.Sprintf("%s~%s", sdJWTSerialized, additionalDisclosure),
+			WithSignatureVerifier(verifier))
 		r.Error(err)
-		r.Nil(sdJWT)
+		r.Nil(claims)
 		r.Contains(err.Error(),
 			"disclosure digest 'qqvcqnczAMgYx7EykI6wwtspyvyvK790ge7MBbQ-Nus' not found in SD-JWT disclosure digests")
 	})
 
 	t.Run("error - duplicate disclosure", func(t *testing.T) {
-		sdJWT, err := Parse(fmt.Sprintf("%s~%s~%s", sdJWTSerialized, additionalDisclosure, additionalDisclosure),
+		claims, err := Parse(fmt.Sprintf("%s~%s~%s", sdJWTSerialized, additionalDisclosure, additionalDisclosure),
 			WithSignatureVerifier(verifier))
 		r.Error(err)
-		r.Nil(sdJWT)
+		r.Nil(claims)
 		r.Contains(err.Error(),
 			"check disclosures: duplicate values found [WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd]")
 	})
@@ -121,10 +125,10 @@ func TestParse(t *testing.T) {
 		jwsPayload, err := base64.RawURLEncoding.DecodeString(jwsParts[1])
 		require.NoError(t, err)
 
-		sdJWT, err := Parse(jwsDetached,
+		claims, err := Parse(jwsDetached,
 			WithSignatureVerifier(verifier), WithJWTDetachedPayload(jwsPayload))
 		r.NoError(err)
-		r.NotNil(r, sdJWT)
+		r.NotNil(r, claims)
 	})
 
 	t.Run("error - invalid claims format", func(t *testing.T) {
@@ -132,61 +136,61 @@ func TestParse(t *testing.T) {
 		sdJWTSerialized, err := buildJWS(signer, "not JSON")
 		r.NoError(err)
 
-		sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
+		claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
 		r.Error(err)
 		r.Contains(err.Error(), "read JWT claims from JWS payload")
-		r.Nil(sdJWT)
+		r.Nil(claims)
 	})
 
 	t.Run("error - invalid claims(iat)", func(t *testing.T) {
 		now := time.Now()
 		oneHourInTheFuture := now.Add(time.Hour)
 
-		tokenWithTimes, e := issuer.New(testIssuer, claims, nil, signer,
+		tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer,
 			issuer.WithIssuedAt(jwt.NewNumericDate(oneHourInTheFuture)))
 		r.NoError(e)
 		sdJWTSerialized, e := tokenWithTimes.Serialize(false)
 		r.NoError(e)
 
-		sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
+		claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
 		r.Error(err)
 		r.Contains(err.Error(),
 			"failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation field, token issued in the future (iat)")
-		r.Nil(sdJWT)
+		r.Nil(claims)
 	})
 
 	t.Run("error - invalid claims(nbf)", func(t *testing.T) {
 		now := time.Now()
 		oneHourInTheFuture := now.Add(time.Hour)
 
-		tokenWithTimes, e := issuer.New(testIssuer, claims, nil, signer,
+		tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer,
 			issuer.WithNotBefore(jwt.NewNumericDate(oneHourInTheFuture)))
 		r.NoError(e)
 		sdJWTSerialized, e := tokenWithTimes.Serialize(false)
 		r.NoError(e)
 
-		sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
+		claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
 		r.Error(err)
 		r.Contains(err.Error(),
 			"failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)")
-		r.Nil(sdJWT)
+		r.Nil(claims)
 	})
 
 	t.Run("error - invalid claims(expiry)", func(t *testing.T) {
 		now := time.Now()
 		oneHourInThePast := now.Add(-time.Hour)
 
-		tokenWithTimes, e := issuer.New(testIssuer, claims, nil, signer,
+		tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer,
 			issuer.WithExpiry(jwt.NewNumericDate(oneHourInThePast)))
 		r.NoError(e)
 		sdJWTSerialized, e := tokenWithTimes.Serialize(false)
 		r.NoError(e)
 
-		sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
+		claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier))
 		r.Error(err)
 		r.Contains(err.Error(),
 			"failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation failed, token is expired (exp)")
-		r.Nil(sdJWT)
+		r.Nil(claims)
 	})
 }
 
@@ -224,6 +228,34 @@ func TestVerifySigningAlgorithm(t *testing.T) {
 	})
 }
 
+func TestGetVerifiedPayload(t *testing.T) {
+	r := require.New(t)
+
+	_, privKey, e := ed25519.GenerateKey(rand.Reader)
+	r.NoError(e)
+
+	signer := afjwt.NewEd25519Signer(privKey)
+	selectiveClaims := map[string]interface{}{"given_name": "Albert"}
+
+	token, e := issuer.New(testIssuer, selectiveClaims, nil, signer)
+	r.NoError(e)
+
+	t.Run("success", func(t *testing.T) {
+		claims, err := getVerifiedPayload(token.Disclosures, token.SignedJWT)
+		r.NoError(err)
+		r.NotNil(claims)
+		r.Equal(5, len(claims))
+	})
+
+	t.Run("error - invalid disclosure(not encoded)", func(t *testing.T) {
+		claims, err := getVerifiedPayload([]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")
+	})
+}
+
 func TestWithJWTDetachedPayload(t *testing.T) {
 	detachedPayloadOpt := WithJWTDetachedPayload([]byte("payload"))
 	require.NotNil(t, detachedPayloadOpt)