From c0349ccec1071aafbb6a4c5bba457da12ea828e6 Mon Sep 17 00:00:00 2001 From: Sandra Vrtikapa Date: Wed, 11 Jan 2023 17:31:19 -0500 Subject: [PATCH] feat: SD-JWT Holder: Add Holder Binding The Holder MAY add an optional JWT to prove Holder Binding to the Verifier. Nonce and aud claims are included to show that the proof is intended for the Verifier. Closes #3470 Signed-off-by: Sandra Vrtikapa --- pkg/doc/sdjwt/holder/holder.go | 62 +++++++++++++++++++++++++-- pkg/doc/sdjwt/holder/holder_test.go | 66 +++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go index 35a7af88a3..5281c9bc36 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 a9de16bb94..5eb0110a48 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`