diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go index 4ac2b74f9..50c5ddf99 100644 --- a/pkg/doc/sdjwt/holder/holder.go +++ b/pkg/doc/sdjwt/holder/holder.go @@ -15,12 +15,11 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" ) -const notFound = -1 - // Claim defines claim. type Claim struct { - Name string - Value interface{} + Disclosure string + Name string + Value interface{} } // jwtParseOpts holds options for the SD-JWT parsing. @@ -86,8 +85,9 @@ func getClaims(disclosures []string) ([]*Claim, error) { for _, disclosure := range disclosureClaims { claims = append(claims, &Claim{ - Name: disclosure.Name, - Value: disclosure.Value, + Disclosure: disclosure.Disclosure, + Name: disclosure.Name, + Value: disclosure.Value, }) } @@ -122,8 +122,10 @@ func WithHolderBinding(info *BindingInfo) Option { } } -// DiscloseClaims discloses claims with specified claim names. -func DiscloseClaims(combinedFormatForIssuance string, claimNames []string, opts ...Option) (string, error) { +// CreatePresentation is a convenience method to assemble combined format for presentation +// using selected disclosures (claimsToDisclose) and optional holder binding. +// This call assumes that combinedFormatForIssuance has already been parsed and verified using Parse() function. +func CreatePresentation(combinedFormatForIssuance string, claimsToDisclose []string, opts ...Option) (string, error) { hOpts := &options{} for _, opt := range opts { @@ -136,24 +138,20 @@ func DiscloseClaims(combinedFormatForIssuance string, claimNames []string, opts return "", fmt.Errorf("no disclosures found in SD-JWT") } - disclosures, err := common.GetDisclosureClaims(cfi.Disclosures) - if err != nil { - return "", err - } - - var selectedDisclosures []string + disclosuresMap := sliceToMap(cfi.Disclosures) - for _, claimName := range claimNames { - if index := getDisclosureByClaimName(claimName, disclosures); index != notFound { - selectedDisclosures = append(selectedDisclosures, cfi.Disclosures[index]) - } else { - return "", fmt.Errorf("claim name '%s' not found", claimName) + for _, ctd := range claimsToDisclose { + if _, ok := disclosuresMap[ctd]; !ok { + return "", fmt.Errorf("disclosure '%s' not found in SD-JWT", ctd) } } + var err error + var hbJWT string + if hOpts.holderBindingInfo != nil { - hbJWT, err = createHolderBinding(hOpts.holderBindingInfo) + hbJWT, err = CreateHolderBinding(hOpts.holderBindingInfo) if err != nil { return "", fmt.Errorf("failed to create holder binding: %w", err) } @@ -161,14 +159,15 @@ func DiscloseClaims(combinedFormatForIssuance string, claimNames []string, opts cf := common.CombinedFormatForPresentation{ SDJWT: cfi.SDJWT, - Disclosures: selectedDisclosures, + Disclosures: claimsToDisclose, HolderBinding: hbJWT, } return cf.Serialize(), nil } -func createHolderBinding(info *BindingInfo) (string, error) { +// CreateHolderBinding will create holder binding from binding info. +func CreateHolderBinding(info *BindingInfo) (string, error) { hbJWT, err := afgjwt.NewSigned(info.Payload, nil, info.Signer) if err != nil { return "", err @@ -177,14 +176,14 @@ func createHolderBinding(info *BindingInfo) (string, error) { return hbJWT.Serialize(false) } -func getDisclosureByClaimName(name string, disclosures []*common.DisclosureClaim) int { - for index, disclosure := range disclosures { - if disclosure.Name == name { - return index - } +func sliceToMap(ids []string) map[string]bool { + // convert slice to map + values := make(map[string]bool) + for _, id := range ids { + values[id] = true } - return notFound + return values } // NoopSignatureVerifier is no-op signature verifier (signature will not get checked). diff --git a/pkg/doc/sdjwt/holder/holder_test.go b/pkg/doc/sdjwt/holder/holder_test.go index f9c9a4b0a..ac01a1110 100644 --- a/pkg/doc/sdjwt/holder/holder_test.go +++ b/pkg/doc/sdjwt/holder/holder_test.go @@ -140,8 +140,12 @@ func TestDiscloseClaims(t *testing.T) { combinedFormatForIssuance, e := token.Serialize(false) r.NoError(e) + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + + claimsToDisclose := []string{cfi.Disclosures[0]} + t.Run("success", func(t *testing.T) { - combinedFormatForPresentation, err := DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}) + combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, claimsToDisclose) r.NoError(err) require.NotNil(t, combinedFormatForPresentation) require.Equal(t, combinedFormatForIssuance+common.CombinedFormatSeparator, combinedFormatForPresentation) @@ -153,7 +157,7 @@ func TestDiscloseClaims(t *testing.T) { holderSigner := afjwt.NewEd25519Signer(holderPrivKey) - combinedFormatForPresentation, err := DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, claimsToDisclose, WithHolderBinding(&BindingInfo{ Payload: BindingPayload{ Audience: "https://example.com/verifier", @@ -168,7 +172,7 @@ func TestDiscloseClaims(t *testing.T) { }) t.Run("error - failed to create holder binding due to signing error", func(t *testing.T) { - combinedFormatForPresentation, err := DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, claimsToDisclose, WithHolderBinding(&BindingInfo{ Payload: BindingPayload{}, Signer: &mockSigner{Err: fmt.Errorf("signing error")}, @@ -184,27 +188,18 @@ func TestDiscloseClaims(t *testing.T) { t.Run("error - no disclosure(s)", func(t *testing.T) { cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) - combinedFormatForPresentation, err := DiscloseClaims(cfi.SDJWT, []string{"given_name"}) + combinedFormatForPresentation, err := CreatePresentation(cfi.SDJWT, claimsToDisclose) r.Error(err) r.Empty(combinedFormatForPresentation) r.Contains(err.Error(), "no disclosures found in SD-JWT") }) - t.Run("error - add invalid disclosure", func(t *testing.T) { - cfiWithInvalidDisclosure := fmt.Sprintf("%s~%s", combinedFormatForIssuance, "abc") - - combinedFormatForPresentation, err := DiscloseClaims(cfiWithInvalidDisclosure, []string{"given_name"}) - r.Error(err) - r.Empty(combinedFormatForPresentation) - r.Contains(err.Error(), "failed to unmarshal disclosure array") - }) - - t.Run("error - claim name not found", func(t *testing.T) { - combinedFormatForPresentation, err := DiscloseClaims(combinedFormatForIssuance, - []string{"given_name", "non_existent"}) + t.Run("error - disclosure not found", func(t *testing.T) { + combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, + []string{"non_existent"}) r.Error(err) r.Empty(combinedFormatForPresentation) - r.Contains(err.Error(), "claim name 'non_existent' not found") + r.Contains(err.Error(), "disclosure 'non_existent' not found") }) } diff --git a/pkg/doc/sdjwt/integration_test.go b/pkg/doc/sdjwt/integration_test.go index 99733ca10..b51e1adb2 100644 --- a/pkg/doc/sdjwt/integration_test.go +++ b/pkg/doc/sdjwt/integration_test.go @@ -63,7 +63,8 @@ func TestSDJWTFlow(t *testing.T) { r.Equal(2, len(claims)) // Holder will disclose only sub-set of claims to verifier. - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}) + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, + []string{claims[0].Disclosure}) r.NoError(err) fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation)) @@ -110,7 +111,8 @@ func TestSDJWTFlow(t *testing.T) { 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"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, + []string{claims[0].Disclosure}, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, diff --git a/pkg/doc/sdjwt/verifier/verifier_test.go b/pkg/doc/sdjwt/verifier/verifier_test.go index f7a5de15b..92a12b53d 100644 --- a/pkg/doc/sdjwt/verifier/verifier_test.go +++ b/pkg/doc/sdjwt/verifier/verifier_test.go @@ -267,8 +267,12 @@ func TestHolderBinding(t *testing.T) { holderSigner := afjwt.NewEd25519Signer(holderPrivKey) + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + + claimsToDisclose := []string{cfi.Disclosures[0]} + t.Run("success - with holder binding", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -290,7 +294,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("success - with holder binding; expected nonce and audience not specified", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -311,7 +315,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("success - with holder binding (required)", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -336,7 +340,7 @@ func TestHolderBinding(t *testing.T) { 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"}) + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose) r.NoError(err) // Verifier will validate combined format for presentation and create verified claims. @@ -352,7 +356,7 @@ func TestHolderBinding(t *testing.T) { }) 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"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -376,7 +380,7 @@ func TestHolderBinding(t *testing.T) { 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"}) + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose) r.NoError(err) // add fake holder binding @@ -395,7 +399,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("error - holder signature algorithm not supported", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -421,7 +425,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("error - invalid iat for holder binding", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: "different", @@ -444,7 +448,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("error - unexpected nonce for holder binding", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: "different", @@ -467,7 +471,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("error - unexpected audience for holder binding", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -496,10 +500,12 @@ func TestHolderBinding(t *testing.T) { cfiWithoutHolderPublicKey, err := tokenWithoutHolderPublicKey.Serialize(false) r.NoError(err) + ctd := []string{common.ParseCombinedFormatForIssuance(cfiWithoutHolderPublicKey).Disclosures[0]} + _, err = holder.Parse(cfiWithoutHolderPublicKey, holder.WithSignatureVerifier(signatureVerifier)) r.NoError(err) - combinedFormatForPresentation, err := holder.DiscloseClaims(cfiWithoutHolderPublicKey, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(cfiWithoutHolderPublicKey, ctd, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -522,7 +528,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("error - holder binding provided, however cnf is not an object", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -553,7 +559,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("error - holder binding provided, cnf is missing jwk", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -587,7 +593,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("error - holder binding provided, invalid jwk in cnf", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -621,7 +627,7 @@ func TestHolderBinding(t *testing.T) { }) t.Run("error - holder binding provided, invalid jwk in cnf", func(t *testing.T) { - combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, @@ -655,7 +661,7 @@ func TestHolderBinding(t *testing.T) { }) 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"}, + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, holder.WithHolderBinding(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce,