diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go index 35a7af88a..5281c9bc3 100644 --- a/pkg/doc/sdjwt/holder/holder.go +++ b/pkg/doc/sdjwt/holder/holder.go @@ -9,6 +9,8 @@ package holder import ( "fmt" + "github.com/go-jose/go-jose/v3/jwt" + "github.com/hyperledger/aries-framework-go/pkg/doc/jose" afgjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" @@ -93,8 +95,42 @@ func getClaims(disclosures []string) ([]*Claim, error) { return claims, nil } +// BindingPayload represents holder binding payload. +type BindingPayload struct { + Nonce string `json:"nonce,omitempty"` + Audience string `json:"aud,omitempty"` + IssuedAt *jwt.NumericDate `json:"iat,omitempty"` +} + +// BindingInfo defines holder binding payload and signer. +type BindingInfo struct { + Payload BindingPayload + Signer jose.Signer +} + +// options holds options for holder. +type options struct { + holderBindingInfo *BindingInfo +} + +// Option is a holder option. +type Option func(opts *options) + +// WithHolderBinding option to set optional holder binding. +func WithHolderBinding(info *BindingInfo) Option { + return func(opts *options) { + opts.holderBindingInfo = info + } +} + // DiscloseClaims discloses claims with specified claim names. -func DiscloseClaims(combinedFormatForIssuance string, claimNames []string) (string, error) { +func DiscloseClaims(combinedFormatForIssuance string, claimNames []string, opts ...Option) (string, error) { + hOpts := &options{} + + for _, opt := range opts { + opt(hOpts) + } + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) if len(cfi.Disclosures) == 0 { @@ -111,17 +147,37 @@ func DiscloseClaims(combinedFormatForIssuance string, claimNames []string) (stri 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) + } + } + + var hbJWT string + if hOpts.holderBindingInfo != nil { + hbJWT, err = createHolderBinding(hOpts.holderBindingInfo) + if err != nil { + return "", fmt.Errorf("failed to create holder binding: %w", err) } } cf := common.CombinedFormatForPresentation{ - SDJWT: cfi.SDJWT, - Disclosures: selectedDisclosures, + SDJWT: cfi.SDJWT, + Disclosures: selectedDisclosures, + HolderBinding: hbJWT, } return cf.Serialize(), nil } +func createHolderBinding(info *BindingInfo) (string, error) { + hbJWT, err := afgjwt.NewSigned(info.Payload, nil, info.Signer) + if err != nil { + return "", err + } + + return hbJWT.Serialize(false) +} + func getDisclosureByClaimName(name string, disclosures []*common.DisclosureClaim) int { for index, disclosure := range disclosures { if disclosure.Name == name { diff --git a/pkg/doc/sdjwt/holder/holder_test.go b/pkg/doc/sdjwt/holder/holder_test.go index a9de16bb9..5eb0110a4 100644 --- a/pkg/doc/sdjwt/holder/holder_test.go +++ b/pkg/doc/sdjwt/holder/holder_test.go @@ -14,7 +14,9 @@ import ( "fmt" "strings" "testing" + "time" + "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/require" "github.com/hyperledger/aries-framework-go/pkg/doc/jose" @@ -123,6 +125,40 @@ func TestDiscloseClaims(t *testing.T) { require.Equal(t, combinedFormatForIssuance+common.DisclosureSeparator, combinedFormatForPresentation) }) + t.Run("success - with holder binding", func(t *testing.T) { + _, holderPrivKey, e := ed25519.GenerateKey(rand.Reader) + r.NoError(e) + + holderSigner := afjwt.NewEd25519Signer(holderPrivKey) + + combinedFormatForPresentation, err := DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + WithHolderBinding(&BindingInfo{ + Payload: BindingPayload{ + Audience: "https://example.com/verifier", + Nonce: "nonce", + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Signer: holderSigner, + })) + r.NoError(err) + r.NotEmpty(combinedFormatForPresentation) + r.Contains(combinedFormatForPresentation, combinedFormatForIssuance+common.DisclosureSeparator) + }) + + t.Run("error - failed to create holder binding due to signing error", func(t *testing.T) { + combinedFormatForPresentation, err := DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}, + WithHolderBinding(&BindingInfo{ + Payload: BindingPayload{}, + Signer: &mockSigner{Err: fmt.Errorf("signing error")}, + })) + + r.Error(err) + r.Empty(combinedFormatForPresentation) + + r.Contains(err.Error(), + "failed to create holder binding: create JWS: sign JWS: sign JWS verification data: signing error") + }) + t.Run("error - no disclosure(s)", func(t *testing.T) { cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) @@ -140,6 +176,14 @@ func TestDiscloseClaims(t *testing.T) { 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"}) + r.Error(err) + r.Empty(combinedFormatForPresentation) + r.Contains(err.Error(), "claim name 'non_existent' not found") + }) } func TestGetClaims(t *testing.T) { @@ -182,6 +226,28 @@ func buildJWS(signer jose.Signer, claims interface{}) (string, error) { return jws.SerializeCompact(false) } +// Signer defines JWS Signer interface. It makes signing of data and provides custom JWS headers relevant to the signer. +type mockSigner struct { + Err error +} + +// Sign signs. +func (m *mockSigner) Sign(_ []byte) ([]byte, error) { + if m.Err != nil { + return nil, m.Err + } + + return nil, nil +} + +// Headers provides JWS headers. +func (m *mockSigner) Headers() jose.Headers { + headers := make(jose.Headers) + headers["alg"] = "EdDSA" + + return headers +} + // nolint: lll const additionalDisclosure = `WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd`