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

Issue:
Add interop test for verifier
Add interop tests for verifier - this is also to prove that we will not have issues with json libraries for interop (string hardening vs. canonicalization note in spec). 

Since disclosures are created by the issuer the value can be influenced by marshalling (e.g. spaces, map order etc.) however it will not influence functionality since we are not supposed to re-create disclosures at any point.

Pull request:
chore: Add interop test for verifier
Add interop tests for verifier - this is also to prove that we will not have issues with json libraries for interop (string hardening vs. canonicalization note in spec).

Since disclosures are created by the issuer the value can be influenced by marshalling (e.g. spaces, map order etc.) however it will not influence functionality since we are not supposed to re-create disclosures at any point.

Closes #3518

Signed-off-by: Sandra Vrtikapa <sandra.vrtikapa@securekey.com>



Patch:
diff --git a/pkg/doc/sdjwt/verifier/verifier.go b/pkg/doc/sdjwt/verifier/verifier.go
index e7a186ab..a8531e11 100644
--- a/pkg/doc/sdjwt/verifier/verifier.go
+++ b/pkg/doc/sdjwt/verifier/verifier.go
@@ -9,6 +9,7 @@ package verifier
 import (
 	"encoding/json"
 	"fmt"
+	"time"
 
 	"github.com/go-jose/go-jose/v3/jwt"
 
@@ -30,6 +31,8 @@ type parseOpts struct {
 	holderBindingRequired            bool
 	expectedAudienceForHolderBinding string
 	expectedNonceForHolderBinding    string
+
+	leewayForClaimsValidation time.Duration
 }
 
 // ParseOpt is the SD-JWT Parser option.
@@ -84,12 +87,20 @@ func WithExpectedNonceForHolderBinding(nonce string) ParseOpt {
 	}
 }
 
+// WithLeewayForClaimsValidation is an option for claims time(s) validation.
+func WithLeewayForClaimsValidation(duration time.Duration) ParseOpt {
+	return func(opts *parseOpts) {
+		opts.leewayForClaimsValidation = duration
+	}
+}
+
 // 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{
-		issuerSigningAlgorithms: defaultSigningAlgorithms,
-		holderSigningAlgorithms: defaultSigningAlgorithms,
+		issuerSigningAlgorithms:   defaultSigningAlgorithms,
+		holderSigningAlgorithms:   defaultSigningAlgorithms,
+		leewayForClaimsValidation: jwt.DefaultLeeway,
 	}
 
 	for _, opt := range opts {
@@ -121,7 +132,7 @@ func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]i
 
 	// Check that the SD-JWT is valid using nbf, iat, and exp claims,
 	// if provided in the SD-JWT, and not selectively disclosed.
-	err = verifyJWT(signedJWT)
+	err = verifyJWT(signedJWT, pOpts.leewayForClaimsValidation)
 	if err != nil {
 		return nil, err
 	}
@@ -190,7 +201,7 @@ func verifyHolderJWT(holderJWT *afgjwt.JSONWebToken, pOpts *parseOpts) error {
 		return fmt.Errorf("failed to verify holder signing algorithm: %w", err)
 	}
 
-	err = verifyJWT(holderJWT)
+	err = verifyJWT(holderJWT, pOpts.leewayForClaimsValidation)
 	if err != nil {
 		return err
 	}
@@ -327,7 +338,7 @@ func checkForDuplicates(values []string) 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 {
+func verifyJWT(signedJWT *afgjwt.JSONWebToken, leeway time.Duration) error {
 	var claims jwt.Claims
 
 	err := signedJWT.DecodeClaims(&claims)
@@ -336,11 +347,10 @@ func verifyJWT(signedJWT *afgjwt.JSONWebToken) error {
 	}
 
 	// Validate checks claims in a token against expected values.
-	// A default leeway value of one minute is used to compare time values.
 	// It is validated using the expected.Time, or time.Now if not provided
 	expected := jwt.Expected{}
 
-	err = claims.Validate(expected)
+	err = claims.ValidateWithLeeway(expected, leeway)
 	if err != nil {
 		return fmt.Errorf("invalid JWT time values: %w", err)
 	}
diff --git a/pkg/doc/verifiable/credential_sdjwt.go b/pkg/doc/verifiable/credential_sdjwt.go
index 2365802f..127cefe0 100644
--- a/pkg/doc/verifiable/credential_sdjwt.go
+++ b/pkg/doc/verifiable/credential_sdjwt.go
@@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0
 package verifiable
 
 import (
+	"crypto"
 	"encoding/json"
 	"fmt"
 
@@ -233,10 +234,24 @@ func filterDisclosures(
 	return out, nil
 }
 
+type makeSDJWTOpts struct {
+	hashAlg crypto.Hash
+}
+
+// MakeSDJWTOption provides an option for creating an SD-JWT from a VC.
+type MakeSDJWTOption func(opts *makeSDJWTOpts)
+
+// MakeSDJWTWithHash sets the hash to use for an SD-JWT VC.
+func MakeSDJWTWithHash(hash crypto.Hash) MakeSDJWTOption {
+	return func(opts *makeSDJWTOpts) {
+		opts.hashAlg = hash
+	}
+}
+
 // MakeSDJWT creates an SD-JWT in combined format for issuance, with all fields in credentialSubject converted
 // recursively into selectively-disclosable SD-JWT claims.
-func (vc *Credential) MakeSDJWT(signer jose.Signer, signingKeyID string) (string, error) {
-	sdjwt, err := makeSDJWT(vc, signer, signingKeyID)
+func (vc *Credential) MakeSDJWT(signer jose.Signer, signingKeyID string, options ...MakeSDJWTOption) (string, error) {
+	sdjwt, err := makeSDJWT(vc, signer, signingKeyID, options...)
 	if err != nil {
 		return "", err
 	}
@@ -249,7 +264,14 @@ func (vc *Credential) MakeSDJWT(signer jose.Signer, signingKeyID string) (string
 	return sdjwtSerialized, nil
 }
 
-func makeSDJWT(vc *Credential, signer jose.Signer, signingKeyID string) (*issuer.SelectiveDisclosureJWT, error) {
+func makeSDJWT(vc *Credential, signer jose.Signer, signingKeyID string, options ...MakeSDJWTOption,
+) (*issuer.SelectiveDisclosureJWT, error) {
+	opts := &makeSDJWTOpts{}
+
+	for _, option := range options {
+		option(opts)
+	}
+
 	claims, err := vc.JWTClaims(false)
 	if err != nil {
 		return nil, fmt.Errorf("constructing VC JWT claims: %w", err)
@@ -271,9 +293,16 @@ func makeSDJWT(vc *Credential, signer jose.Signer, signingKeyID string) (*issuer
 		jose.HeaderKeyID: signingKeyID,
 	}
 
-	sdjwt, err := issuer.NewFromVC(claimMap, headers, signer,
+	issuerOptions := []issuer.NewOpt{
 		issuer.WithStructuredClaims(true),
-		issuer.WithNonSelectivelyDisclosableClaims([]string{"id"}))
+		issuer.WithNonSelectivelyDisclosableClaims([]string{"id"}),
+	}
+
+	if opts.hashAlg != 0 {
+		issuerOptions = append(issuerOptions, issuer.WithHashAlgorithm(opts.hashAlg))
+	}
+
+	sdjwt, err := issuer.NewFromVC(claimMap, headers, signer, issuerOptions...)
 	if err != nil {
 		return nil, fmt.Errorf("creating SD-JWT from VC: %w", err)
 	}


Test patch:
diff --git a/pkg/doc/sdjwt/verifier/verifier_interop_test.go b/pkg/doc/sdjwt/verifier/verifier_interop_test.go
new file mode 100644
index 00000000..4d81cd1c
--- /dev/null
+++ b/pkg/doc/sdjwt/verifier/verifier_interop_test.go
@@ -0,0 +1,140 @@
+/*
+Copyright SecureKey Technologies Inc. All Rights Reserved.
+
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package verifier
+
+import (
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common"
+	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder"
+)
+
+// nolint:lll
+const specIssuanceExample1 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.gieinY5mTgTV69KZJyaFPeIJ9tfXlzCHKfs-HMBO9UIREz6Dh_lpTMrwUUXQXcO0pB3K_8uXjiMBGwXpMz_ayg~WyJkcVR2WE14UzBHYTNEb2FHbmU5eDBRIiwgInN1YiIsICJqb2huX2RvZV80MiJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJLVXhTNWhFX1hiVmFjckdBYzdFRnd3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyIzcXZWSjFCQURwSERTUzkzOVEtUml3IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJFUktNMENOZUZKa2FENW1UWFZfWDh3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0`
+
+//nolint:lll
+const specPresentationExample1 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.gieinY5mTgTV69KZJyaFPeIJ9tfXlzCHKfs-HMBO9UIREz6Dh_lpTMrwUUXQXcO0pB3K_8uXjiMBGwXpMz_ayg~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~eyJhbGciOiAiRVMyNTYifQ.eyJub25jZSI6ICJYWk9VY28xdV9nRVBrbnhTNzhzV1dnIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE2NzA1NzQ0MTh9._TZe98TAQDrV_21TjEKBRKKCt5EO5Q0-MHNZ79qVvBR9gL4nCXBu6c--QDysTgnXk_oe-qVin6EOzHF3Oh9tbQ`
+
+func TestInterop(t *testing.T) {
+	r := require.New(t)
+
+	t.Run("success - Example 1", func(t *testing.T) {
+		cfi := specIssuanceExample1 + common.CombinedFormatSeparator
+
+		claims, err := Parse(cfi,
+			WithIssuerSigningAlgorithms([]string{"ES256"}),
+			WithSignatureVerifier(&holder.NoopSignatureVerifier{}),
+			// expiry time for example 1 is 2018-01-17 22:43:42 -0500 EST
+			// so we have to have great leeway in order to pass test
+			WithLeewayForClaimsValidation(10*12*30*24*time.Hour))
+		r.NoError(err)
+
+		printObject(t, "Disclosed Claims for Example 1 - All Claims Disclosed", claims)
+
+		var example1ClaimsObj map[string]interface{}
+		err = json.Unmarshal([]byte(claimsExample1), &example1ClaimsObj)
+		r.NoError(err)
+
+		var disclosedClaimsForExample1Obj map[string]interface{}
+		err = json.Unmarshal([]byte(disclosedAllClaimsForExample1), &disclosedClaimsForExample1Obj)
+		r.NoError(err)
+
+		// expected claims are example 1 claims plus exp, iat, iss, cnf
+		r.Equal(len(disclosedClaimsForExample1Obj), len(example1ClaimsObj)+4)
+
+		r.Equal(len(disclosedClaimsForExample1Obj), len(claims))
+	})
+
+	t.Run("success - Example 1 with Holder Binding", func(t *testing.T) {
+		claims, err := Parse(specPresentationExample1,
+			WithIssuerSigningAlgorithms([]string{"ES256"}),
+			WithHolderSigningAlgorithms([]string{"ES256"}),
+			WithSignatureVerifier(&holder.NoopSignatureVerifier{}),
+			// expiry time for example 1 is 2018-01-17 22:43:42 -0500 EST
+			// so we have to have great leeway in order to pass test
+			WithLeewayForClaimsValidation(10*12*30*24*time.Hour))
+		r.NoError(err)
+
+		printObject(t, "Disclosed Claims For Example 1 - Partial Disclosure", claims)
+
+		var disclosedPartialClaimsForExample1Obj map[string]interface{}
+		err = json.Unmarshal([]byte(disclosedPartialClaimsForExample1), &disclosedPartialClaimsForExample1Obj)
+		r.NoError(err)
+
+		r.Equal(len(disclosedPartialClaimsForExample1Obj), len(claims))
+	})
+}
+
+const claimsExample1 = `
+{
+  "sub": "john_doe_42",
+  "given_name": "John",
+  "family_name": "Doe",
+  "email": "johndoe@example.com",
+  "phone_number": "+1-202-555-0101",
+  "address": {
+    "street_address": "123 Main St",
+    "locality": "Anytown",
+    "region": "Anystate",
+    "country": "US"
+  },
+  "birthdate": "1940-01-01"
+}`
+
+const disclosedAllClaimsForExample1 = `
+{
+	"sub": "john_doe_42",
+	"given_name": "John",
+	"family_name": "Doe",
+	"email": "johndoe@example.com",
+	"phone_number": "+1-202-555-0101",
+	"address": {
+		"country": "US",
+		"locality": "Anytown",
+		"region": "Anystate",
+		"street_address": "123 Main St"
+	},
+	"birthdate": "1940-01-01",
+	"exp": 1516247022,
+	"iat": 1516239022,
+	"iss": "https://example.com/issuer",
+	"cnf": {
+		"jwk": {
+			"crv": "P-256",
+			"kty": "EC",
+			"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
+			"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
+		}
+	}
+}`
+
+const disclosedPartialClaimsForExample1 = `
+{
+	"family_name": "Doe",
+	"given_name": "John",
+	"address": {
+		"country": "US",
+		"locality": "Anytown",
+		"region": "Anystate",
+		"street_address": "123 Main St"
+	},
+	"cnf": {
+		"jwk": {
+			"crv": "P-256",
+			"kty": "EC",
+			"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
+			"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
+		}
+	},
+	"exp": 1516247022,
+	"iat": 1516239022,
+	"iss": "https://example.com/issuer"
+}`
diff --git a/pkg/doc/sdjwt/verifier/verifier_test.go b/pkg/doc/sdjwt/verifier/verifier_test.go
index c227b755..dbc7873c 100644
--- a/pkg/doc/sdjwt/verifier/verifier_test.go
+++ b/pkg/doc/sdjwt/verifier/verifier_test.go
@@ -298,7 +298,8 @@ func TestHolderBinding(t *testing.T) {
 		verifiedClaims, err := Parse(combinedFormatForPresentation,
 			WithSignatureVerifier(signatureVerifier),
 			WithExpectedAudienceForHolderBinding(testAudience),
-			WithExpectedNonceForHolderBinding(testNonce))
+			WithExpectedNonceForHolderBinding(testNonce),
+			WithLeewayForClaimsValidation(time.Hour))
 		r.NoError(err)
 
 		// expected claims cnf, iss, given_name; last_name was not disclosed
diff --git a/pkg/doc/verifiable/credential_sdjwt_test.go b/pkg/doc/verifiable/credential_sdjwt_test.go
index 610f9848..59fb273b 100644
--- a/pkg/doc/verifiable/credential_sdjwt_test.go
+++ b/pkg/doc/verifiable/credential_sdjwt_test.go
@@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
 package verifiable
 
 import (
+	"crypto"
 	"crypto/ed25519"
 	"crypto/rand"
 	"encoding/base64"
@@ -299,11 +300,22 @@ func TestMakeSDJWT(t *testing.T) {
 	require.NoError(t, e)
 
 	t.Run("success", func(t *testing.T) {
-		sdjwt, err := vc.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1")
-		require.NoError(t, err)
+		t.Run("with default hash", func(t *testing.T) {
+			sdjwt, err := vc.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1")
+			require.NoError(t, err)
+
+			_, err = ParseCredential([]byte(sdjwt), WithPublicKeyFetcher(holderPublicKeyFetcher(pubKey)))
+			require.NoError(t, err)
+		})
 
-		_, err = ParseCredential([]byte(sdjwt), WithPublicKeyFetcher(holderPublicKeyFetcher(pubKey)))
-		require.NoError(t, err)
+		t.Run("with hash option", func(t *testing.T) {
+			sdjwt, err := vc.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1",
+				MakeSDJWTWithHash(crypto.SHA512))
+			require.NoError(t, err)
+
+			_, err = ParseCredential([]byte(sdjwt), WithPublicKeyFetcher(holderPublicKeyFetcher(pubKey)))
+			require.NoError(t, err)
+		})
 	})
 
 	t.Run("failure", func(t *testing.T) {