github.com/hyperledger/aries-framework-go@v0.3.2/pkg/doc/sdjwt/integration_test.go (about)

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package sdjwt
     8  
     9  import (
    10  	"bytes"
    11  	"crypto"
    12  	"crypto/ed25519"
    13  	"crypto/rand"
    14  	"encoding/json"
    15  	"fmt"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/go-jose/go-jose/v3/jwt"
    20  	"github.com/stretchr/testify/require"
    21  
    22  	"github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport"
    23  	afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
    24  	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder"
    25  	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer"
    26  	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/verifier"
    27  )
    28  
    29  const (
    30  	testIssuer = "https://example.com/issuer"
    31  
    32  	year = 365 * 24 * 60 * time.Minute
    33  )
    34  
    35  func TestSDJWTFlow(t *testing.T) {
    36  	r := require.New(t)
    37  
    38  	issuerPublicKey, issuerPrivateKey, e := ed25519.GenerateKey(rand.Reader)
    39  	r.NoError(e)
    40  
    41  	signer := afjwt.NewEd25519Signer(issuerPrivateKey)
    42  
    43  	signatureVerifier, e := afjwt.NewEd25519Verifier(issuerPublicKey)
    44  	r.NoError(e)
    45  
    46  	claims := map[string]interface{}{
    47  		"given_name": "Albert",
    48  		"last_name":  "Smith",
    49  	}
    50  
    51  	now := time.Now()
    52  
    53  	var timeOpts []issuer.NewOpt
    54  	timeOpts = append(timeOpts,
    55  		issuer.WithNotBefore(jwt.NewNumericDate(now)),
    56  		issuer.WithIssuedAt(jwt.NewNumericDate(now)),
    57  		issuer.WithExpiry(jwt.NewNumericDate(now.Add(year))))
    58  
    59  	t.Run("success - simple claims (flat option)", func(t *testing.T) {
    60  		// Issuer will issue SD-JWT for specified claims.
    61  		token, err := issuer.New(testIssuer, claims, nil, signer, timeOpts...)
    62  		r.NoError(err)
    63  
    64  		var simpleClaimsFlatOption map[string]interface{}
    65  		err = token.DecodeClaims(&simpleClaimsFlatOption)
    66  		r.NoError(err)
    67  
    68  		printObject(t, "Simple Claims:", simpleClaimsFlatOption)
    69  
    70  		combinedFormatForIssuance, err := token.Serialize(false)
    71  		r.NoError(err)
    72  
    73  		fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance))
    74  
    75  		// Holder will parse combined format for issuance and hold on to that
    76  		// combined format for issuance and the claims that can be selected.
    77  		claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
    78  		r.NoError(err)
    79  
    80  		// expected disclosures given_name and last_name
    81  		r.Equal(2, len(claims))
    82  
    83  		selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name"}, claims)
    84  
    85  		// Holder will disclose only sub-set of claims to verifier.
    86  		combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures)
    87  		r.NoError(err)
    88  
    89  		fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation))
    90  
    91  		// Verifier will validate combined format for presentation and create verified claims.
    92  		verifiedClaims, err := verifier.Parse(combinedFormatForPresentation,
    93  			verifier.WithSignatureVerifier(signatureVerifier))
    94  		r.NoError(err)
    95  		r.NotNil(verifiedClaims)
    96  
    97  		printObject(t, "Verified Claims", verifiedClaims)
    98  
    99  		// expected claims iss, exp, iat, nbf, given_name; last_name was not disclosed
   100  		r.Equal(5, len(verifiedClaims))
   101  	})
   102  
   103  	t.Run("success - with holder binding", func(t *testing.T) {
   104  		holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader)
   105  		r.NoError(err)
   106  
   107  		holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey)
   108  		require.NoError(t, err)
   109  
   110  		// Issuer will issue SD-JWT for specified claims and holder public key.
   111  		token, err := issuer.New(testIssuer, claims, nil, signer,
   112  			issuer.WithHashAlgorithm(crypto.SHA512),
   113  			issuer.WithNotBefore(jwt.NewNumericDate(now)),
   114  			issuer.WithIssuedAt(jwt.NewNumericDate(now)),
   115  			issuer.WithExpiry(jwt.NewNumericDate(now.Add(year))),
   116  			issuer.WithHolderPublicKey(holderPublicJWK))
   117  		r.NoError(err)
   118  
   119  		combinedFormatForIssuance, err := token.Serialize(false)
   120  		r.NoError(err)
   121  
   122  		fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance))
   123  
   124  		// Holder will parse combined format for issuance and hold on to that
   125  		// combined format for issuance and the claims that can be selected.
   126  		claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
   127  		r.NoError(err)
   128  
   129  		// expected disclosures given_name and last_name
   130  		r.Equal(2, len(claims))
   131  
   132  		holderSigner := afjwt.NewEd25519Signer(holderPrivateKey)
   133  
   134  		const testAudience = "https://test.com/verifier"
   135  		const testNonce = "nonce"
   136  
   137  		selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name"}, claims)
   138  
   139  		// Holder will disclose only sub-set of claims to verifier and add holder binding.
   140  		combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures,
   141  			holder.WithHolderBinding(&holder.BindingInfo{
   142  				Payload: holder.BindingPayload{
   143  					Nonce:    testNonce,
   144  					Audience: testAudience,
   145  					IssuedAt: jwt.NewNumericDate(time.Now()),
   146  				},
   147  				Signer: holderSigner,
   148  			}))
   149  		r.NoError(err)
   150  
   151  		fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation))
   152  
   153  		// Verifier will validate combined format for presentation and create verified claims.
   154  		verifiedClaims, err := verifier.Parse(combinedFormatForPresentation,
   155  			verifier.WithSignatureVerifier(signatureVerifier),
   156  			verifier.WithHolderBindingRequired(true),
   157  			verifier.WithExpectedAudienceForHolderBinding(testAudience),
   158  			verifier.WithExpectedNonceForHolderBinding(testNonce))
   159  		r.NoError(err)
   160  
   161  		printObject(t, "Verified Claims", verifiedClaims)
   162  
   163  		// expected claims cnf, iss, given_name, iat, nbf, exp; last_name was not disclosed
   164  		r.Equal(6, len(verifiedClaims))
   165  	})
   166  
   167  	t.Run("success - complex claims object with structured claims option", func(t *testing.T) {
   168  		complexClaims := createComplexClaims()
   169  
   170  		// Issuer will issue SD-JWT for specified claims. We will use structured(nested) claims in this test.
   171  		token, err := issuer.New(testIssuer, complexClaims, nil, signer,
   172  			issuer.WithStructuredClaims(true))
   173  		r.NoError(err)
   174  
   175  		var structuredClaims map[string]interface{}
   176  		err = token.DecodeClaims(&structuredClaims)
   177  		r.NoError(err)
   178  
   179  		printObject(t, "Complex Claims(Structured Option) :", structuredClaims)
   180  
   181  		combinedFormatForIssuance, err := token.Serialize(false)
   182  		r.NoError(err)
   183  
   184  		fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance))
   185  
   186  		// Holder will parse combined format for issuance and hold on to that
   187  		// combined format for issuance and the claims that can be selected.
   188  		claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
   189  		r.NoError(err)
   190  
   191  		printObject(t, "Holder Claims", claims)
   192  
   193  		r.Equal(10, len(claims))
   194  
   195  		selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name", "email", "street_address"}, claims)
   196  
   197  		// Holder will disclose only sub-set of claims to verifier.
   198  		combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures)
   199  		r.NoError(err)
   200  
   201  		fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation))
   202  
   203  		// Verifier will validate combined format for presentation and create verified claims.
   204  		verifiedClaims, err := verifier.Parse(combinedFormatForPresentation,
   205  			verifier.WithSignatureVerifier(signatureVerifier))
   206  		r.NoError(err)
   207  
   208  		// expected claims iss, given_name, email, street_address; time options not provided
   209  		r.Equal(4, len(verifiedClaims))
   210  
   211  		printObject(t, "Verified Claims", verifiedClaims)
   212  	})
   213  
   214  	t.Run("success - complex claims object with flat claims option", func(t *testing.T) {
   215  		complexClaims := createComplexClaims()
   216  
   217  		// Issuer will issue SD-JWT for specified claims. We will use structured(nested) claims in this test.
   218  		token, err := issuer.New(testIssuer, complexClaims, nil, signer,
   219  			issuer.WithHashAlgorithm(crypto.SHA384))
   220  		r.NoError(err)
   221  
   222  		var flatClaims map[string]interface{}
   223  		err = token.DecodeClaims(&flatClaims)
   224  		r.NoError(err)
   225  
   226  		printObject(t, "Complex Claims (Flat Option)", flatClaims)
   227  
   228  		combinedFormatForIssuance, err := token.Serialize(false)
   229  		r.NoError(err)
   230  
   231  		fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance))
   232  
   233  		// Holder will parse combined format for issuance and hold on to that
   234  		// combined format for issuance and the claims that can be selected.
   235  		claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
   236  		r.NoError(err)
   237  
   238  		printObject(t, "Holder Claims", claims)
   239  
   240  		r.Equal(7, len(claims))
   241  
   242  		selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name", "email", "address"}, claims)
   243  
   244  		// Holder will disclose only sub-set of claims to verifier.
   245  		combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures)
   246  		r.NoError(err)
   247  
   248  		fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation))
   249  
   250  		// Verifier will validate combined format for presentation and create verified claims.
   251  		verifiedClaims, err := verifier.Parse(combinedFormatForPresentation,
   252  			verifier.WithSignatureVerifier(signatureVerifier))
   253  		r.NoError(err)
   254  
   255  		// expected claims iss, given_name, email, street_address; time options not provided
   256  		r.Equal(4, len(verifiedClaims))
   257  
   258  		printObject(t, "Verified Claims", verifiedClaims)
   259  	})
   260  
   261  	t.Run("success - NewFromVC API", func(t *testing.T) {
   262  		holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader)
   263  		r.NoError(err)
   264  
   265  		holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey)
   266  		require.NoError(t, err)
   267  
   268  		// create VC - we will use template here
   269  		var vc map[string]interface{}
   270  		err = json.Unmarshal([]byte(sampleVCFull), &vc)
   271  		r.NoError(err)
   272  
   273  		token, err := issuer.NewFromVC(vc, nil, signer,
   274  			issuer.WithHolderPublicKey(holderPublicJWK),
   275  			issuer.WithStructuredClaims(true),
   276  			issuer.WithNonSelectivelyDisclosableClaims([]string{"id", "degree.type"}),
   277  		)
   278  		r.NoError(err)
   279  
   280  		var decoded map[string]interface{}
   281  
   282  		err = token.DecodeClaims(&decoded)
   283  		require.NoError(t, err)
   284  
   285  		printObject(t, "SD-JWT Payload", decoded)
   286  
   287  		vcCombinedFormatForIssuance, err := token.Serialize(false)
   288  		r.NoError(err)
   289  
   290  		fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", vcCombinedFormatForIssuance))
   291  
   292  		claims, err := holder.Parse(vcCombinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier))
   293  		r.NoError(err)
   294  
   295  		printObject(t, "Holder Claims", claims)
   296  
   297  		r.Equal(4, len(claims))
   298  
   299  		const testAudience = "https://test.com/verifier"
   300  		const testNonce = "nonce"
   301  
   302  		holderSigner := afjwt.NewEd25519Signer(holderPrivateKey)
   303  
   304  		selectedDisclosures := getDisclosuresFromClaimNames([]string{"degree", "id", "name"}, claims)
   305  
   306  		// Holder will disclose only sub-set of claims to verifier.
   307  		combinedFormatForPresentation, err := holder.CreatePresentation(vcCombinedFormatForIssuance, selectedDisclosures,
   308  			holder.WithHolderBinding(&holder.BindingInfo{
   309  				Payload: holder.BindingPayload{
   310  					Nonce:    testNonce,
   311  					Audience: testAudience,
   312  					IssuedAt: jwt.NewNumericDate(time.Now()),
   313  				},
   314  				Signer: holderSigner,
   315  			}))
   316  		r.NoError(err)
   317  
   318  		fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation))
   319  
   320  		// Verifier will validate combined format for presentation and create verified claims.
   321  		// In this case it will be VC since VC was passed in.
   322  		verifiedClaims, err := verifier.Parse(combinedFormatForPresentation,
   323  			verifier.WithSignatureVerifier(signatureVerifier))
   324  		r.NoError(err)
   325  
   326  		printObject(t, "Verified Claims", verifiedClaims)
   327  
   328  		r.Equal(len(vc), len(verifiedClaims))
   329  	})
   330  }
   331  
   332  func createComplexClaims() map[string]interface{} {
   333  	claims := map[string]interface{}{
   334  		"sub":          "john_doe_42",
   335  		"given_name":   "John",
   336  		"family_name":  "Doe",
   337  		"email":        "johndoe@example.com",
   338  		"phone_number": "+1-202-555-0101",
   339  		"birthdate":    "1940-01-01",
   340  		"address": map[string]interface{}{
   341  			"street_address": "123 Main St",
   342  			"locality":       "Anytown",
   343  			"region":         "Anystate",
   344  			"country":        "US",
   345  		},
   346  	}
   347  
   348  	return claims
   349  }
   350  
   351  func getDisclosuresFromClaimNames(selectedClaimNames []string, claims []*holder.Claim) []string {
   352  	var disclosures []string
   353  
   354  	for _, c := range claims {
   355  		if contains(selectedClaimNames, c.Name) {
   356  			disclosures = append(disclosures, c.Disclosure)
   357  		}
   358  	}
   359  
   360  	return disclosures
   361  }
   362  
   363  func contains(values []string, val string) bool {
   364  	for _, v := range values {
   365  		if v == val {
   366  			return true
   367  		}
   368  	}
   369  
   370  	return false
   371  }
   372  
   373  func printObject(t *testing.T, name string, obj interface{}) {
   374  	t.Helper()
   375  
   376  	objBytes, err := json.Marshal(obj)
   377  	require.NoError(t, err)
   378  
   379  	prettyJSON, err := prettyPrint(objBytes)
   380  	require.NoError(t, err)
   381  
   382  	fmt.Println(name + ":")
   383  	fmt.Println(prettyJSON)
   384  }
   385  
   386  func prettyPrint(msg []byte) (string, error) {
   387  	var prettyJSON bytes.Buffer
   388  
   389  	err := json.Indent(&prettyJSON, msg, "", "\t")
   390  	if err != nil {
   391  		return "", err
   392  	}
   393  
   394  	return prettyJSON.String(), nil
   395  }
   396  
   397  const sampleVCFull = `
   398  {
   399  	"iat": 1673987547,
   400  	"iss": "did:example:76e12ec712ebc6f1c221ebfeb1f",
   401  	"jti": "http://example.edu/credentials/1872",
   402  	"nbf": 1673987547,
   403  	"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
   404  	"vc": {
   405  		"@context": [
   406  			"https://www.w3.org/2018/credentials/v1"
   407  		],
   408  		"credentialSubject": {
   409  			"degree": {
   410  				"degree": "MIT",
   411  				"type": "BachelorDegree",
   412  				"id": "some-id"
   413  			},
   414  			"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
   415  			"name": "Jayden Doe",
   416  			"spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1"
   417  		},
   418  		"first_name": "First name",
   419  		"id": "http://example.edu/credentials/1872",
   420  		"info": "Info",
   421  		"issuanceDate": "2023-01-17T22:32:27.468109817+02:00",
   422  		"issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f",
   423  		"last_name": "Last name",
   424  		"type": "VerifiableCredential"
   425  	}
   426  }`