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

Issue:
SD-JWT Issuer: Add Holder Public Key Claim (JWK)
If the Issuer wants to enable Holder Binding, it MAY include a public key associated with the Holder, or a reference thereto.

We will use confirmation "cnf" claim as defined in [[RFC7800](https://www.rfc-editor.org/info/rfc7800)] to include raw public key by value in SD-JWT.

In this first iteration of "cnf" claim we will use jwk confirmation method and include public key information in JWK format. Other confirmation methods such as "jwe", "kid" and "jku" may be implemented at later time (if required).

Pull request description:
feat: SD-JWT Issuer: Add Holder Public Key Claim (JWK)
If the Issuer wants to enable Holder Binding, it MAY include a public key associated with the Holder, or a reference thereto.

We use confirmation "cnf" claim to include raw public key by value in SD-JWT.

In this first iteration of "cnf" claim we will use "jwk" confirmation method and include public key information in JWK format. Other confirmation methods such as "jwe", "kid" and "jku" may be implemented at later time (if required).

Closes #3469 

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 e35077ed..fc218e95 100644
--- a/pkg/doc/sdjwt/common/common.go
+++ b/pkg/doc/sdjwt/common/common.go
@@ -41,6 +41,8 @@ type Payload struct {
 	NotBefore *jwt.NumericDate `json:"nbf,omitempty"`
 	IssuedAt  *jwt.NumericDate `json:"iat,omitempty"`
 
+	CNF map[string]interface{} `json:"cnf,omitempty"`
+
 	SD    []string `json:"_sd,omitempty"`
 	SDAlg string   `json:"_sd_alg,omitempty"`
 }
diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go
index 209ad046..bcf57888 100644
--- a/pkg/doc/sdjwt/issuer/issuer.go
+++ b/pkg/doc/sdjwt/issuer/issuer.go
@@ -20,6 +20,7 @@ import (
 	"github.com/go-jose/go-jose/v3/jwt"
 
 	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
+	"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"
 )
@@ -48,6 +49,8 @@ type newOpts struct {
 	NotBefore *jwt.NumericDate
 	IssuedAt  *jwt.NumericDate
 
+	HolderPublicKey *jwk.JWK
+
 	HashAlg crypto.Hash
 
 	jsonMarshal func(v interface{}) ([]byte, error)
@@ -108,6 +111,13 @@ func WithID(id string) NewOpt {
 	}
 }
 
+// WithHolderPublicKey is an option for SD-JWT payload.
+func WithHolderPublicKey(jwk *jwk.JWK) NewOpt {
+	return func(opts *newOpts) {
+		opts.HolderPublicKey = jwk
+	}
+}
+
 // WithHashAlgorithm is an option for hashing disclosures.
 func WithHashAlgorithm(alg crypto.Hash) NewOpt {
 	return func(opts *newOpts) {
@@ -162,6 +172,23 @@ func New(issuer string, claims interface{}, headers jose.Headers,
 		return nil, err
 	}
 
+	payload := createPayload(issuer, digests, nOpts)
+
+	signedJWT, err := afgjwt.NewSigned(payload, headers, signer)
+	if err != nil {
+		return nil, err
+	}
+
+	return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil
+}
+
+func createPayload(issuer string, digests []string, nOpts *newOpts) *common.Payload {
+	var cnf map[string]interface{}
+	if nOpts.HolderPublicKey != nil {
+		cnf = make(map[string]interface{})
+		cnf["jwk"] = nOpts.HolderPublicKey
+	}
+
 	payload := &common.Payload{
 		Issuer:    issuer,
 		ID:        nOpts.ID,
@@ -169,16 +196,12 @@ func New(issuer string, claims interface{}, headers jose.Headers,
 		IssuedAt:  nOpts.IssuedAt,
 		Expiry:    nOpts.Expiry,
 		NotBefore: nOpts.NotBefore,
+		CNF:       cnf,
 		SD:        digests,
 		SDAlg:     strings.ToLower(nOpts.HashAlg.String()),
 	}
 
-	signedJWT, err := afgjwt.NewSigned(payload, headers, signer)
-	if err != nil {
-		return nil, err
-	}
-
-	return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil
+	return payload
 }
 
 func createDigests(disclosures []string, nOpts *newOpts) ([]string, error) {


Test patch:
diff --git a/pkg/doc/sdjwt/issuer/issuer_test.go b/pkg/doc/sdjwt/issuer/issuer_test.go
index 329c7f73..90ef72d2 100644
--- a/pkg/doc/sdjwt/issuer/issuer_test.go
+++ b/pkg/doc/sdjwt/issuer/issuer_test.go
@@ -23,6 +23,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
+	"github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport"
 	afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
 	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common"
 )
@@ -133,7 +134,6 @@ func TestNew(t *testing.T) {
 			WithNotBefore(jwt.NewNumericDate(notBefore)),
 			WithID("id"),
 			WithSubject("subject"),
-			// TODO: Audience ???
 			WithSaltFnc(generateSalt),
 			WithJSONMarshaller(json.Marshal),
 			WithHashAlgorithm(crypto.SHA256),
@@ -198,6 +198,41 @@ func TestNew(t *testing.T) {
 		}
 	})
 
+	t.Run("Create JWS with holder public key", func(t *testing.T) {
+		r := require.New(t)
+
+		pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
+		r.NoError(err)
+
+		_, holderPublicKey, err := ed25519.GenerateKey(rand.Reader)
+		require.NoError(t, err)
+
+		holderJWK, err := jwksupport.JWKFromKey(holderPublicKey)
+		require.NoError(t, err)
+
+		token, err := New(issuer, claims, nil, afjwt.NewEd25519Signer(privKey),
+			WithHolderPublicKey(holderJWK))
+		r.NoError(err)
+		combinedFormatForIssuance, err := token.Serialize(false)
+		require.NoError(t, err)
+
+		cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance)
+		require.Equal(t, 1, len(cfi.Disclosures))
+
+		var parsedClaims map[string]interface{}
+		err = verifyEd25519ViaGoJose(cfi.SDJWT, pubKey, &parsedClaims)
+		r.NoError(err)
+		require.NotEmpty(t, parsedClaims["cnf"])
+
+		parsedClaimsBytes, err := json.Marshal(parsedClaims)
+		require.NoError(t, err)
+
+		prettyJSON, err := prettyPrint(parsedClaimsBytes)
+		require.NoError(t, err)
+
+		fmt.Println(prettyJSON)
+	})
+
 	t.Run("error - create decoy disclosures failed", func(t *testing.T) {
 		r := require.New(t)