Skip to content
This repository has been archived by the owner on Mar 27, 2024. It is now read-only.

feat: SD-JWT Holder: Add Holder Binding #3473

Merged
merged 1 commit into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions pkg/doc/sdjwt/holder/holder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
66 changes: 66 additions & 0 deletions pkg/doc/sdjwt/holder/holder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand All @@ -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) {
Expand Down Expand Up @@ -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`

Expand Down