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

Issue:
SD-JWT Verifier: Verify Holder Binding
If Holder Binding is required, verify that the Holder Binding JWT is signed by the Holder and valid.

Validation steps:

1. If Holder Binding JWT is not provided, the Verifier MUST reject the Presentation.
2. Determine the public key for the Holder from the SD-JWT.
3. Ensure that a signing algorithm was used that was deemed secure for the application. Refer to [[RFC8725](https://www.rfc-editor.org/info/rfc8725)], Sections 3.1 and 3.2 for details. The none algorithm MUST NOT be accepted.
4. Validate the signature over the Holder Binding JWT.
5. Check that the Holder Binding JWT is valid using nbf, iat, and exp claims, if provided in the Holder Binding JWT.
6. Determine that the Holder Binding JWT is bound to the current transaction and was created for this Verifier (replay protection). This is usually achieved by a nonce and aud field within the Holder Binding JWT.


Pull request description:
feat: SD-JWT Verifier: Verify Holder Binding
If Holder Binding is required, verify that the Holder Binding JWT is signed by the Holder and valid.

Closes #3471

Signed-off-by: Sandra Vrtikapa <sandra.vrtikapa@securekey.com>



Patch:
diff --git a/pkg/doc/jwt/jwt.go b/pkg/doc/jwt/jwt.go
index 5145781b..065204ba 100644
--- a/pkg/doc/jwt/jwt.go
+++ b/pkg/doc/jwt/jwt.go
@@ -287,7 +287,7 @@ func PayloadToMap(i interface{}) (map[string]interface{}, error) {
 	default:
 		b, err = json.Marshal(i)
 		if err != nil {
-			return nil, fmt.Errorf("convert to bytes: ")
+			return nil, fmt.Errorf("marshal interface[%T]: %w", i, err)
 		}
 	}
 
diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go
index 5281c9bc..4ac2b74f 100644
--- a/pkg/doc/sdjwt/holder/holder.go
+++ b/pkg/doc/sdjwt/holder/holder.go
@@ -8,7 +8,6 @@ package holder
 
 import (
 	"fmt"
-
 	"github.com/go-jose/go-jose/v3/jwt"
 
 	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go
index bcf57888..800dad8f 100644
--- a/pkg/doc/sdjwt/issuer/issuer.go
+++ b/pkg/doc/sdjwt/issuer/issuer.go
@@ -159,7 +159,7 @@ func New(issuer string, claims interface{}, headers jose.Headers,
 
 	claimsMap, err := afgjwt.PayloadToMap(claims)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("convert payload to map: %w", err)
 	}
 
 	disclosures, err := createDisclosures(claimsMap, nOpts)
@@ -176,7 +176,7 @@ func New(issuer string, claims interface{}, headers jose.Headers,
 
 	signedJWT, err := afgjwt.NewSigned(payload, headers, signer)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("failed to create SD-JWT from payload[%+v]: %w", payload, err)
 	}
 
 	return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil
diff --git a/pkg/doc/sdjwt/verifier/verifier.go b/pkg/doc/sdjwt/verifier/verifier.go
index 6e39d839..ed00afeb 100644
--- a/pkg/doc/sdjwt/verifier/verifier.go
+++ b/pkg/doc/sdjwt/verifier/verifier.go
@@ -7,20 +7,31 @@ SPDX-License-Identifier: Apache-2.0
 package verifier
 
 import (
+	"encoding/json"
 	"fmt"
 
 	"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"
+	"github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier"
 )
 
+const confirmationClaim = "cnf"
+
 // jwtParseOpts holds options for the SD-JWT parsing.
 type parseOpts struct {
-	detachedPayload   []byte
-	sigVerifier       jose.SignatureVerifier
-	signingAlgorithms []string
+	detachedPayload []byte
+	sigVerifier     jose.SignatureVerifier
+
+	issuerSigningAlgorithms []string
+	holderSigningAlgorithms []string
+
+	holderBindingRequired            bool
+	expectedAudienceForHolderBinding string
+	expectedNonceForHolderBinding    string
 }
 
 // ParseOpt is the SD-JWT Parser option.
@@ -40,17 +51,47 @@ func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt {
 	}
 }
 
-// WithSigningAlgorithms option is for defining secure signing algorithms.
-func WithSigningAlgorithms(algorithms []string) ParseOpt {
+// WithIssuerSigningAlgorithms option is for defining secure signing algorithms (for issuer).
+func WithIssuerSigningAlgorithms(algorithms []string) ParseOpt {
+	return func(opts *parseOpts) {
+		opts.issuerSigningAlgorithms = algorithms
+	}
+}
+
+// WithHolderSigningAlgorithms option is for defining secure signing algorithms (for holder).
+func WithHolderSigningAlgorithms(algorithms []string) ParseOpt {
 	return func(opts *parseOpts) {
-		opts.signingAlgorithms = algorithms
+		opts.holderSigningAlgorithms = algorithms
+	}
+}
+
+// WithHolderBindingRequired option is for enforcing holder binding.
+func WithHolderBindingRequired(flag bool) ParseOpt {
+	return func(opts *parseOpts) {
+		opts.holderBindingRequired = flag
+	}
+}
+
+// WithExpectedAudienceForHolderBinding option is to pass expected audience for holder binding.
+func WithExpectedAudienceForHolderBinding(audience string) ParseOpt {
+	return func(opts *parseOpts) {
+		opts.expectedAudienceForHolderBinding = audience
+	}
+}
+
+// WithExpectedNonceForHolderBinding option is to pass nonce value for holder binding.
+func WithExpectedNonceForHolderBinding(nonce string) ParseOpt {
+	return func(opts *parseOpts) {
+		opts.expectedNonceForHolderBinding = nonce
 	}
 }
 
 // Parse parses combined format for presentation and returns verified claims.
 func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]interface{}, error) {
+	defaultSigningAlgorithms := []string{"EdDSA", "RS256"}
 	pOpts := &parseOpts{
-		signingAlgorithms: []string{"EdDSA", "RS256"},
+		issuerSigningAlgorithms: defaultSigningAlgorithms,
+		holderSigningAlgorithms: defaultSigningAlgorithms,
 	}
 
 	for _, opt := range opts {
@@ -73,16 +114,16 @@ func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]i
 
 	// Ensure that a signing algorithm was used that was deemed secure for the application.
 	// The none algorithm MUST NOT be accepted.
-	err = verifySigningAlg(signedJWT.Headers, pOpts.signingAlgorithms)
+	err = verifySigningAlg(signedJWT.Headers, pOpts.issuerSigningAlgorithms)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("failed to verify issuer signing algorithm: %w", err)
 	}
 
 	// TODO: Validate the Issuer of the SD-JWT and that the signing key belongs to this Issuer.
 
 	// Check that the SD-JWT is valid using nbf, iat, and exp claims,
 	// if provided in the SD-JWT, and not selectively disclosed.
-	err = verifySDJWT(signedJWT)
+	err = verifyJWT(signedJWT)
 	if err != nil {
 		return nil, err
 	}
@@ -99,9 +140,140 @@ func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]i
 		return nil, err
 	}
 
+	err = verifyHolderBinding(signedJWT, cfp.HolderBinding, pOpts)
+	if err != nil {
+		return nil, fmt.Errorf("failed to verify holder binding: %w", err)
+	}
+
 	return getVerifiedPayload(cfp.Disclosures, signedJWT)
 }
 
+func verifyHolderBinding(sdJWT *afgjwt.JSONWebToken, holderBinding string, pOpts *parseOpts) error {
+	if pOpts.holderBindingRequired && holderBinding == "" {
+		return fmt.Errorf("holder binding is required")
+	}
+
+	if holderBinding == "" {
+		// not required and not present - nothing to do
+		return nil
+	}
+
+	var claims map[string]interface{}
+
+	err := sdJWT.DecodeClaims(&claims)
+	if err != nil {
+		return fmt.Errorf("failed to decode presentation claims: %w", err)
+	}
+
+	signatureVerifier, err := getSignatureVerifier(claims)
+	if err != nil {
+		return fmt.Errorf("failed to get signature verifier from presentation claims: %w", err)
+	}
+
+	holderJWT, err := afgjwt.Parse(holderBinding,
+		afgjwt.WithSignatureVerifier(signatureVerifier))
+	if err != nil {
+		return fmt.Errorf("failed to parse holder binding: %w", err)
+	}
+
+	err = verifyHolderJWT(holderJWT, pOpts)
+	if err != nil {
+		return fmt.Errorf("failed to verify holder JWT: %w", err)
+	}
+
+	return nil
+}
+
+func verifyHolderJWT(holderJWT *afgjwt.JSONWebToken, pOpts *parseOpts) error {
+	// Ensure that a signing algorithm was used that was deemed secure for the application.
+	// The none algorithm MUST NOT be accepted.
+	err := verifySigningAlg(holderJWT.Headers, pOpts.holderSigningAlgorithms)
+	if err != nil {
+		return fmt.Errorf("failed to verify holder signing algorithm: %w", err)
+	}
+
+	err = verifyJWT(holderJWT)
+	if err != nil {
+		return err
+	}
+
+	var bindingPayload holderBindingPayload
+
+	err = holderJWT.DecodeClaims(&bindingPayload)
+	if err != nil {
+		return fmt.Errorf("failed to decode holder claims: %w", err)
+	}
+
+	if pOpts.expectedNonceForHolderBinding != "" && pOpts.expectedNonceForHolderBinding != bindingPayload.Nonce {
+		return fmt.Errorf("nonce value '%s' does not match expected nonce value '%s'",
+			bindingPayload.Nonce, pOpts.expectedNonceForHolderBinding)
+	}
+
+	if pOpts.expectedAudienceForHolderBinding != "" && pOpts.expectedAudienceForHolderBinding != bindingPayload.Audience {
+		return fmt.Errorf("audience value '%s' does not match expected audience value '%s'",
+			bindingPayload.Audience, pOpts.expectedAudienceForHolderBinding)
+	}
+
+	return nil
+}
+
+func getSignatureVerifier(claims map[string]interface{}) (jose.SignatureVerifier, error) {
+	cnf, err := getCNF(claims)
+	if err != nil {
+		return nil, err
+	}
+
+	signatureVerifier, err := getSignatureVerifierFromCNF(cnf)
+	if err != nil {
+		return nil, err
+	}
+
+	return signatureVerifier, nil
+}
+
+func getCNF(claims map[string]interface{}) (map[string]interface{}, error) {
+	obj, ok := claims[confirmationClaim]
+	if !ok {
+		return nil, fmt.Errorf("%s must be present in SD-JWT", confirmationClaim)
+	}
+
+	cnf, ok := obj.(map[string]interface{})
+	if !ok {
+		return nil, fmt.Errorf("%s must be an object", confirmationClaim)
+	}
+
+	return cnf, nil
+}
+
+// getSignatureVerifierFromCNF will evolve over time as we support more cnf modes and algorithms.
+func getSignatureVerifierFromCNF(cnf map[string]interface{}) (jose.SignatureVerifier, error) {
+	jwkObj, ok := cnf["jwk"]
+	if !ok {
+		return nil, fmt.Errorf("jwk must be present in cnf")
+	}
+
+	// TODO: Add handling other methods: "jwe", "jku" and "kid"
+
+	jwkObjBytes, err := json.Marshal(jwkObj)
+	if err != nil {
+		return nil, fmt.Errorf("marshal jwk: %w", err)
+	}
+
+	j := jwk.JWK{}
+
+	err = j.UnmarshalJSON(jwkObjBytes)
+	if err != nil {
+		return nil, fmt.Errorf("unmarshal jwk: %w", err)
+	}
+
+	signatureVerifier, err := afgjwt.GetVerifier(&verifier.PublicKey{JWK: &j})
+	if err != nil {
+		return nil, fmt.Errorf("get verifier from jwk: %w", err)
+	}
+
+	return signatureVerifier, nil
+}
+
 func getVerifiedPayload(disclosures []string, signedJWT *afgjwt.JSONWebToken) (map[string]interface{}, error) {
 	disclosureClaims, err := common.GetDisclosureClaims(disclosures)
 	if err != nil {
@@ -172,8 +344,8 @@ func checkForDuplicates(values []string) error {
 	return nil
 }
 
-// verifySDJWT checks that the SD-JWT is valid using nbf, iat, and exp claims (if provided in the SD-JWT).
-func verifySDJWT(signedJWT *afgjwt.JSONWebToken) error {
+// verifyJWT checks that the JWT is valid using nbf, iat, and exp claims (if provided in the JWT).
+func verifyJWT(signedJWT *afgjwt.JSONWebToken) error {
 	var claims jwt.Claims
 
 	err := signedJWT.DecodeClaims(&claims)
@@ -188,8 +360,15 @@ func verifySDJWT(signedJWT *afgjwt.JSONWebToken) error {
 
 	err = claims.Validate(expected)
 	if err != nil {
-		return fmt.Errorf("failed to validate SD-JWT time values: %w", err)
+		return fmt.Errorf("invalid JWT time values: %w", err)
 	}
 
 	return nil
 }
+
+// holderBindingPayload represents expected holder binding payload.
+type holderBindingPayload struct {
+	Nonce    string           `json:"nonce,omitempty"`
+	Audience string           `json:"aud,omitempty"`
+	IssuedAt *jwt.NumericDate `json:"iat,omitempty"`
+}


Test patch:
diff --git a/pkg/doc/jwt/jwt_test.go b/pkg/doc/jwt/jwt_test.go
index 8fcb0ff8..aa30bfe8 100644
--- a/pkg/doc/jwt/jwt_test.go
+++ b/pkg/doc/jwt/jwt_test.go
@@ -453,7 +453,7 @@ func Test_toMap(t *testing.T) {
 	// pass invalid structure
 	resultMap, err = PayloadToMap(make(chan int))
 	r.Error(err)
-	r.Contains(err.Error(), "convert to bytes")
+	r.Contains(err.Error(), "marshal interface[chan int]: json: unsupported type: chan int")
 	r.Nil(resultMap)
 }
 
diff --git a/pkg/doc/sdjwt/integration_test.go b/pkg/doc/sdjwt/integration_test.go
index 8d7a59e1..99733ca1 100644
--- a/pkg/doc/sdjwt/integration_test.go
+++ b/pkg/doc/sdjwt/integration_test.go
@@ -11,9 +11,12 @@ import (
 	"crypto/rand"
 	"fmt"
 	"testing"
+	"time"
 
+	"github.com/go-jose/go-jose/v3/jwt"
 	"github.com/stretchr/testify/require"
 
+	"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/holder"
 	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer"
@@ -27,26 +30,27 @@ const (
 func TestSDJWTFlow(t *testing.T) {
 	r := require.New(t)
 
-	pubKey, privKey, e := ed25519.GenerateKey(rand.Reader)
+	issuerPublicKey, issuerPrivateKey, e := ed25519.GenerateKey(rand.Reader)
+	r.NoError(e)
+
+	signer := afjwt.NewEd25519Signer(issuerPrivateKey)
+
+	signatureVerifier, e := afjwt.NewEd25519Verifier(issuerPublicKey)
 	r.NoError(e)
 
-	signer := afjwt.NewEd25519Signer(privKey)
 	claims := map[string]interface{}{
 		"given_name": "Albert",
 		"last_name":  "Smith",
 	}
 
-	signatureVerifier, e := afjwt.NewEd25519Verifier(pubKey)
-	r.NoError(e)
-
 	t.Run("success", func(t *testing.T) {
 		// Issuer will issue SD-JWT for specified claims.
-		token, e := issuer.New(testIssuer, claims, nil, signer)
-		r.NoError(e)
+		token, err := issuer.New(testIssuer, claims, nil, signer)
+		r.NoError(err)
 
 		// TODO: Should we have one call instead of two (designed based on JWT)
-		combinedFormatForIssuance, e := token.Serialize(false)
-		r.NoError(e)
+		combinedFormatForIssuance, err := token.Serialize(false)
+		r.NoError(err)
 
 		fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance))
 
@@ -74,4 +78,62 @@ func TestSDJWTFlow(t *testing.T) {
 
 		fmt.Println(fmt.Sprintf("verified claims: %+v", verifiedClaims))
 	})
+
+	t.Run("success - with holder binding", func(t *testing.T) {
+		holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader)
+		r.NoError(err)
+
+		holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey)
+		require.NoError(t, err)
+
+		// Issuer will issue SD-JWT for specified claims and holder public key.
+		token, err := issuer.New(testIssuer, claims, nil, signer,
+			issuer.WithHolderPublicKey(holderPublicJWK))
+		r.NoError(err)
+
+		combinedFormatForIssuance, err := token.Serialize(false)
+		r.NoError(err)
+
+		fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance))
+
+		// Holder will parse combined format for issuance and hold on to that
+		// combined format for issuance and the claims that can be selected.
+		claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
+		r.NoError(err)
+
+		// expected disclosures given_name and last_name
+		r.Equal(2, len(claims))
+
+		holderSigner := afjwt.NewEd25519Signer(holderPrivateKey)
+
+		const testAudience = "https://test.com/verifier"
+		const testNonce = "nonce"
+
+		// Holder will disclose only sub-set of claims to verifier and add holder binding.
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation))
+
+		// Verifier will validate combined format for presentation and create verified claims.
+		verifiedClaims, err := verifier.Parse(combinedFormatForPresentation,
+			verifier.WithSignatureVerifier(signatureVerifier),
+			verifier.WithHolderBindingRequired(true),
+			verifier.WithExpectedAudienceForHolderBinding(testAudience),
+			verifier.WithExpectedNonceForHolderBinding(testNonce))
+		r.NoError(err)
+
+		fmt.Println(fmt.Sprintf("verified claims: %+v", verifiedClaims))
+
+		// expected claims cnf, iss, exp, iat, nbf, given_name; last_name was not disclosed
+		r.Equal(6, len(verifiedClaims))
+	})
 }
diff --git a/pkg/doc/sdjwt/issuer/issuer_test.go b/pkg/doc/sdjwt/issuer/issuer_test.go
index 90ef72d2..885c88ed 100644
--- a/pkg/doc/sdjwt/issuer/issuer_test.go
+++ b/pkg/doc/sdjwt/issuer/issuer_test.go
@@ -18,11 +18,13 @@ import (
 	"testing"
 	"time"
 
+	"github.com/go-jose/go-jose/v3"
 	"github.com/go-jose/go-jose/v3/json"
 	"github.com/go-jose/go-jose/v3/jwt"
 	"github.com/stretchr/testify/require"
 
-	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
+	afjose "github.com/hyperledger/aries-framework-go/pkg/doc/jose"
+	"github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk"
 	"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"
@@ -233,6 +235,21 @@ func TestNew(t *testing.T) {
 		fmt.Println(prettyJSON)
 	})
 
+	t.Run("error - invalid holder public key", func(t *testing.T) {
+		r := require.New(t)
+
+		_, privKey, err := ed25519.GenerateKey(rand.Reader)
+		r.NoError(err)
+
+		token, err := New(issuer, claims, nil, afjwt.NewEd25519Signer(privKey),
+			WithHolderPublicKey(&jwk.JWK{JSONWebKey: jose.JSONWebKey{Key: "abc"}}))
+		r.Error(err)
+		r.Nil(token)
+
+		r.Contains(err.Error(),
+			"unmarshallable claims: marshal interface[*common.Payload]: 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) {
 		r := require.New(t)
 
@@ -511,7 +528,7 @@ func verifyEd25519(jws string, pubKey ed25519.PublicKey) error {
 		return err
 	}
 
-	sVerifier := jose.NewCompositeAlgSigVerifier(jose.AlgSignatureVerifier{
+	sVerifier := afjose.NewCompositeAlgSigVerifier(afjose.AlgSignatureVerifier{
 		Alg:      "EdDSA",
 		Verifier: v,
 	})
@@ -531,7 +548,7 @@ func verifyEd25519(jws string, pubKey ed25519.PublicKey) error {
 func verifyRS256(jws string, pubKey *rsa.PublicKey) error {
 	v := afjwt.NewRS256Verifier(pubKey)
 
-	sVerifier := jose.NewCompositeAlgSigVerifier(jose.AlgSignatureVerifier{
+	sVerifier := afjose.NewCompositeAlgSigVerifier(afjose.AlgSignatureVerifier{
 		Alg:      "RS256",
 		Verifier: v,
 	})
diff --git a/pkg/doc/sdjwt/verifier/verifier_test.go b/pkg/doc/sdjwt/verifier/verifier_test.go
index 6b30a3a8..2c2b0f6e 100644
--- a/pkg/doc/sdjwt/verifier/verifier_test.go
+++ b/pkg/doc/sdjwt/verifier/verifier_test.go
@@ -13,7 +13,6 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
-	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common"
 	"strings"
 	"testing"
 	"time"
@@ -21,13 +20,20 @@ import (
 	"github.com/go-jose/go-jose/v3/jwt"
 	"github.com/stretchr/testify/require"
 
-	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
+	afjose "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"
+	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder"
 	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer"
 )
 
 const (
 	testIssuer = "https://example.com/issuer"
+
+	testAudience = "https://test.com/verifier"
+	testNonce    = "nonce"
+	testSDAlg    = "sha-256"
 )
 
 func TestParse(t *testing.T) {
@@ -101,10 +107,10 @@ func TestParse(t *testing.T) {
 	t.Run("error - signing algorithm not supported", func(t *testing.T) {
 		claims, err := Parse(combinedFormatForPresentation,
 			WithSignatureVerifier(verifier),
-			WithSigningAlgorithms([]string{}))
+			WithIssuerSigningAlgorithms([]string{}))
 		r.Error(err)
 		require.Nil(t, claims)
-		require.Equal(t, err.Error(), "alg 'EdDSA' is not in the allowed list")
+		require.Equal(t, err.Error(), "failed to verify issuer signing algorithm: alg 'EdDSA' is not in the allowed list")
 	})
 
 	t.Run("error - additional disclosure", func(t *testing.T) {
@@ -126,7 +132,7 @@ func TestParse(t *testing.T) {
 	})
 
 	t.Run("success - with detached payload", func(t *testing.T) {
-		jwsParts := strings.Split(combinedFormatForIssuance, ".")
+		jwsParts := strings.Split(combinedFormatForPresentation, ".")
 		jwsDetached := fmt.Sprintf("%s..%s", jwsParts[0], jwsParts[2])
 
 		jwsPayload, err := base64.RawURLEncoding.DecodeString(jwsParts[1])
@@ -162,7 +168,7 @@ func TestParse(t *testing.T) {
 		claims, err := Parse(cfi, 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)")
+			"invalid JWT time values: go-jose/go-jose/jwt: validation field, token issued in the future (iat)")
 		r.Nil(claims)
 	})
 
@@ -181,7 +187,7 @@ func TestParse(t *testing.T) {
 		claims, err := Parse(cfPresentation, 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)")
+			"invalid JWT time values: go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)")
 		r.Nil(claims)
 	})
 
@@ -200,30 +206,490 @@ func TestParse(t *testing.T) {
 		claims, err := Parse(cfPresentation, 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)")
+			"invalid JWT time values: go-jose/go-jose/jwt: validation failed, token is expired (exp)")
 		r.Nil(claims)
 	})
 }
 
+func TestHolderBinding(t *testing.T) {
+	r := require.New(t)
+
+	issuerPubKey, issuerPrivateKey, e := ed25519.GenerateKey(rand.Reader)
+	r.NoError(e)
+
+	signer := afjwt.NewEd25519Signer(issuerPrivateKey)
+
+	signatureVerifier, e := afjwt.NewEd25519Verifier(issuerPubKey)
+	r.NoError(e)
+
+	claims := map[string]interface{}{
+		"given_name": "Albert",
+		"last_name":  "Smith",
+	}
+
+	holderPubKey, holderPrivKey, e := ed25519.GenerateKey(rand.Reader)
+	r.NoError(e)
+
+	holderPublicJWK, e := jwksupport.JWKFromKey(holderPubKey)
+	require.NoError(t, e)
+
+	token, e := issuer.New(testIssuer, claims, nil, signer,
+		issuer.WithHolderPublicKey(holderPublicJWK))
+	r.NoError(e)
+
+	combinedFormatForIssuance, e := token.Serialize(false)
+	r.NoError(e)
+
+	_, e = holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
+	r.NoError(e)
+
+	holderSigner := afjwt.NewEd25519Signer(holderPrivKey)
+
+	t.Run("success - with holder binding", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.NoError(err)
+
+		// expected claims cnf, iss, exp, iat, nbf, given_name; last_name was not disclosed
+		r.Equal(6, len(verifiedClaims))
+	})
+
+	t.Run("success - with holder binding; expected nonce and audience not specified", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithHolderBindingRequired(true))
+		r.NoError(err)
+
+		// expected claims cnf, iss, exp, iat, nbf, given_name; last_name was not disclosed
+		r.Equal(6, len(verifiedClaims))
+	})
+
+	t.Run("success - with holder binding (required)", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		// Verifier will validate combined format for presentation and create verified claims.
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithHolderBindingRequired(true),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.NoError(err)
+
+		// expected claims cnf, iss, exp, iat, nbf, given_name; last_name was not disclosed
+		r.Equal(6, len(verifiedClaims))
+	})
+
+	t.Run("error - holder binding required, however not provided by the holder", func(t *testing.T) {
+		// holder will not issue holder binding
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"})
+		r.NoError(err)
+
+		// Verifier will validate combined format for presentation and create verified claims.
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithHolderBindingRequired(true),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(), "failed to verify holder binding: holder binding is required")
+	})
+
+	t.Run("error - holder signature is not matching holder public key in SD-JWT", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: signer, // should have been holder signer; on purpose sign holder binding with wrong signer
+			}))
+		r.NoError(err)
+
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to parse holder binding: parse JWT from compact JWS: ed25519: invalid signature") // nolint:lll
+	})
+
+	t.Run("error - invalid holder binding JWT provided by the holder", func(t *testing.T) {
+		// holder will not issue holder binding
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"})
+		r.NoError(err)
+
+		// add fake holder binding
+		combinedFormatForPresentation += "invalid-holder-jwt"
+
+		// Verifier will validate combined format for presentation and create verified claims.
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to parse holder binding: JWT of compacted JWS form is supported only")
+	})
+
+	t.Run("error - holder signature algorithm not supported", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		// Verifier will validate combined format for presentation and create verified claims.
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithHolderBindingRequired(true),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce),
+			WithHolderSigningAlgorithms([]string{}))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to verify holder JWT: failed to verify holder signing algorithm: alg 'EdDSA'") //nolint:lll
+	})
+
+	t.Run("error - invalid iat for holder binding", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    "different",
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now().AddDate(1, 0, 0)), // in future
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to verify holder JWT: invalid JWT time values: go-jose/go-jose/jwt: validation field, token issued in the future (iat)") //nolint:lll
+	})
+
+	t.Run("error - unexpected nonce for holder binding", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    "different",
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to verify holder JWT: nonce value 'different' does not match expected nonce value 'nonce'") //nolint:lll
+	})
+
+	t.Run("error - unexpected audience for holder binding", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: "different",
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to verify holder JWT: audience value 'different' does not match expected audience value 'https://test.com/verifier'") //nolint:lll
+	})
+
+	t.Run("error - holder binding provided, however cnf claim not in SD-JWT", func(t *testing.T) {
+		tokenWithoutHolderPublicKey, err := issuer.New(testIssuer, claims, nil, signer)
+		r.NoError(err)
+
+		cfiWithoutHolderPublicKey, err := tokenWithoutHolderPublicKey.Serialize(false)
+		r.NoError(err)
+
+		_, err = holder.Parse(cfiWithoutHolderPublicKey, holder.WithSignatureVerifier(signatureVerifier))
+		r.NoError(err)
+
+		combinedFormatForPresentation, err := holder.DiscloseClaims(cfiWithoutHolderPublicKey, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		verifiedClaims, err := Parse(combinedFormatForPresentation,
+			WithSignatureVerifier(signatureVerifier),
+			WithExpectedAudienceForHolderBinding(testAudience),
+			WithExpectedNonceForHolderBinding(testNonce))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to get signature verifier from presentation claims: cnf must be present in SD-JWT") //nolint:lll
+	})
+
+	t.Run("error - holder binding provided, however cnf is not an object", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation)
+
+		claims := make(map[string]interface{})
+		claims["cnf"] = "abc"
+		claims["_sd_alg"] = testSDAlg // TODO: Should alg be mandatory if there are no selective disclosures
+
+		sdJWT, err := buildJWS(signer, claims)
+		r.NoError(err)
+
+		cfpWithInvalidCNF := sdJWT + common.DisclosureSeparator + cfp.HolderBinding
+
+		verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to get signature verifier from presentation claims: cnf must be an object") // nolint:lll
+	})
+
+	t.Run("error - holder binding provided, cnf is missing jwk", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation)
+
+		cnf := make(map[string]interface{})
+		cnf["test"] = "test"
+
+		claims := make(map[string]interface{})
+		claims["cnf"] = cnf
+		claims["_sd_alg"] = testSDAlg
+
+		sdJWT, err := buildJWS(signer, claims)
+		r.NoError(err)
+
+		cfpWithInvalidCNF := sdJWT + common.DisclosureSeparator + cfp.HolderBinding
+
+		verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to get signature verifier from presentation claims: jwk must be present in cnf") // nolint:lll
+	})
+
+	t.Run("error - holder binding provided, invalid jwk in cnf", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation)
+
+		cnf := make(map[string]interface{})
+		cnf["jwk"] = make(map[string]interface{})
+
+		claims := make(map[string]interface{})
+		claims["cnf"] = cnf
+		claims["_sd_alg"] = testSDAlg
+
+		sdJWT, err := buildJWS(signer, claims)
+		r.NoError(err)
+
+		cfpWithInvalidCNF := sdJWT + common.DisclosureSeparator + cfp.HolderBinding
+
+		verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to get signature verifier from presentation claims: unmarshal jwk: unable to read jose JWK, go-jose/go-jose: unknown json web key type ''") // nolint:lll
+	})
+
+	t.Run("error - holder binding provided, invalid jwk in cnf", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation)
+
+		cnf := make(map[string]interface{})
+		cnf["jwk"] = make(map[string]interface{})
+
+		claims := make(map[string]interface{})
+		claims["cnf"] = cnf
+		claims["_sd_alg"] = testSDAlg
+
+		sdJWT, err := buildJWS(signer, claims)
+		r.NoError(err)
+
+		cfpWithInvalidCNF := sdJWT + common.DisclosureSeparator + cfp.HolderBinding
+
+		verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to get signature verifier from presentation claims: unmarshal jwk: unable to read jose JWK, go-jose/go-jose: unknown json web key type ''") // nolint:lll
+	})
+
+	t.Run("error - holder binding provided with EdDSA, jwk in cnf is RSA", func(t *testing.T) {
+		combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"},
+			holder.WithHolderBinding(&holder.BindingInfo{
+				Payload: holder.BindingPayload{
+					Nonce:    testNonce,
+					Audience: testAudience,
+					IssuedAt: jwt.NewNumericDate(time.Now()),
+				},
+				Signer: holderSigner,
+			}))
+		r.NoError(err)
+
+		cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation)
+
+		claims := make(map[string]interface{})
+		claims["cnf"] = map[string]interface{}{
+			"jwk": map[string]interface{}{
+				"kty": "RSA",
+				"e":   "AQAB",
+				"n":   "pm4bOHBg-oYhAyPWzR56AWX3rUIXp11",
+			},
+		}
+
+		claims["_sd_alg"] = testSDAlg
+
+		sdJWT, err := buildJWS(signer, claims)
+		r.NoError(err)
+
+		cfpWithInvalidCNF := sdJWT + common.DisclosureSeparator + cfp.HolderBinding
+
+		verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier))
+		r.Error(err)
+		r.Nil(verifiedClaims)
+
+		r.Contains(err.Error(),
+			"failed to verify holder binding: failed to parse holder binding: parse JWT from compact JWS: no verifier found for EdDSA algorithm") // nolint:lll
+	})
+}
+
 func TestVerifySigningAlgorithm(t *testing.T) {
 	r := require.New(t)
 
 	t.Run("success - EdDSA signing algorithm", func(t *testing.T) {
-		headers := make(jose.Headers)
+		headers := make(afjose.Headers)
 		headers["alg"] = "EdDSA"
 		err := verifySigningAlg(headers, []string{"EdDSA"})
 		r.NoError(err)
 	})
 
 	t.Run("error - signing algorithm can not be empty", func(t *testing.T) {
-		headers := make(jose.Headers)
+		headers := make(afjose.Headers)
 		err := verifySigningAlg(headers, []string{"RS256"})
 		r.Error(err)
 		r.Contains(err.Error(), "missing alg")
 	})
 
 	t.Run("success - EdDSA signing algorithm not in allowed list", func(t *testing.T) {
-		headers := make(jose.Headers)
+		headers := make(afjose.Headers)
 		headers["alg"] = "EdDSA"
 		err := verifySigningAlg(headers, []string{"RS256"})
 		r.Error(err)
@@ -231,7 +697,7 @@ func TestVerifySigningAlgorithm(t *testing.T) {
 	})
 
 	t.Run("error - signing algorithm can not be none", func(t *testing.T) {
-		headers := make(jose.Headers)
+		headers := make(afjose.Headers)
 		headers["alg"] = "none"
 		err := verifySigningAlg(headers, []string{"RS256"})
 		r.Error(err)
@@ -276,13 +742,13 @@ func TestWithJWTDetachedPayload(t *testing.T) {
 	require.Equal(t, []byte("payload"), opts.detachedPayload)
 }
 
-func buildJWS(signer jose.Signer, claims interface{}) (string, error) {
+func buildJWS(signer afjose.Signer, claims interface{}) (string, error) {
 	claimsBytes, err := json.Marshal(claims)
 	if err != nil {
 		return "", err
 	}
 
-	jws, err := jose.NewJWS(nil, nil, claimsBytes, signer)
+	jws, err := afjose.NewJWS(nil, nil, claimsBytes, signer)
 	if err != nil {
 		return "", err
 	}