Skip to content

Commit

Permalink
feat: SD-JWT Holder: Add Holder Binding
Browse files Browse the repository at this point in the history
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 hyperledger-archives#3470

Signed-off-by: Sandra Vrtikapa <[email protected]>
  • Loading branch information
sandrask committed Jan 11, 2023
1 parent 3e3fda9 commit b0b88bd
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 3 deletions.
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 - with holder binding", 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

0 comments on commit b0b88bd

Please sign in to comment.