Skip to content

Commit

Permalink
feat: Add decoy digests to SD-JWT
Browse files Browse the repository at this point in the history
Add decoy digests to SD-JWT (if enabled)

Closes hyperledger-archives#3463

Signed-off-by: Sandra Vrtikapa <[email protected]>
  • Loading branch information
sandrask committed Jan 9, 2023
1 parent f7617dd commit a1bb1e1
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 12 deletions.
5 changes: 3 additions & 2 deletions pkg/doc/sdjwt/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func VerifyDisclosuresInSDJWT(disclosures []string, signedJWT *afgjwt.JSONWebTok
return err
}

claimsDisclosureDigests, err := getDisclosureDigests(claims)
claimsDisclosureDigests, err := GetDisclosureDigests(claims)
if err != nil {
return err
}
Expand Down Expand Up @@ -207,7 +207,8 @@ func getSDAlg(claims map[string]interface{}) (string, error) {
return str, nil
}

func getDisclosureDigests(claims map[string]interface{}) (map[string]bool, error) {
// GetDisclosureDigests returns digests from from claims map.
func GetDisclosureDigests(claims map[string]interface{}) (map[string]bool, error) {
disclosuresObj, ok := claims[SDKey]
if !ok {
return nil, nil
Expand Down
74 changes: 64 additions & 10 deletions pkg/doc/sdjwt/issuer/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"encoding/json"
"errors"
"fmt"
mathrand "math/rand"
"strings"
"time"

Expand All @@ -27,9 +28,14 @@ const (
defaultHash = crypto.SHA256
defaultSaltSize = 128 / 8

decoyMinElements = 1
decoyMaxElements = 4

year = 365 * 24 * 60 * time.Minute
)

var mr = mathrand.New(mathrand.NewSource(time.Now().Unix())) // nolint:gochecknoglobals

// Claims defines JSON Web Token Claims (https://tools.ietf.org/html/rfc7519#section-4)
type Claims jwt.Claims

Expand All @@ -46,6 +52,8 @@ type newOpts struct {

jsonMarshal func(v interface{}) ([]byte, error)
getSalt func() (string, error)

addDecoyDigests bool
}

// NewOpt is the SD-JWT New option.
Expand Down Expand Up @@ -107,6 +115,13 @@ func WithHashAlgorithm(alg crypto.Hash) NewOpt {
}
}

// WithDecoyDigests is an option for adding decoy digests(default is false).
func WithDecoyDigests(flag bool) NewOpt {
return func(opts *newOpts) {
opts.addDecoyDigests = flag
}
}

// New creates new signed Selective Disclosure JWT based on input claims.
func New(issuer string, claims interface{}, headers jose.Headers,
signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) {
Expand All @@ -127,6 +142,11 @@ func New(issuer string, claims interface{}, headers jose.Headers,
opt(nOpts)
}

decoyDisclosures, err := createDecoyDisclosures(nOpts)
if err != nil {
return nil, fmt.Errorf("failed to create decoy disclosures: %w", err)
}

claimsMap, err := afgjwt.PayloadToMap(claims)
if err != nil {
return nil, err
Expand All @@ -137,15 +157,9 @@ func New(issuer string, claims interface{}, headers jose.Headers,
return nil, err
}

var hashedDisclosures []string

for _, disclosure := range disclosures {
hashedDisclosure, inErr := common.GetHash(nOpts.HashAlg, disclosure)
if inErr != nil {
return nil, fmt.Errorf("hash disclosure: %w", inErr)
}

hashedDisclosures = append(hashedDisclosures, hashedDisclosure)
digests, err := getDigests(append(disclosures, decoyDisclosures...), nOpts)
if err != nil {
return nil, err
}

payload := &common.Payload{
Expand All @@ -155,7 +169,7 @@ func New(issuer string, claims interface{}, headers jose.Headers,
IssuedAt: nOpts.IssuedAt,
Expiry: nOpts.Expiry,
NotBefore: nOpts.NotBefore,
SD: hashedDisclosures,
SD: digests,
SDAlg: strings.ToLower(nOpts.HashAlg.String()),
}

Expand All @@ -167,6 +181,46 @@ func New(issuer string, claims interface{}, headers jose.Headers,
return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil
}

func getDigests(disclosures []string, nOpts *newOpts) ([]string, error) {
var digests []string

for _, disclosure := range disclosures {
digest, inErr := common.GetHash(nOpts.HashAlg, disclosure)
if inErr != nil {
return nil, fmt.Errorf("hash disclosure: %w", inErr)
}

digests = append(digests, digest)
}

mr.Shuffle(len(digests), func(i, j int) {
digests[i], digests[j] = digests[j], digests[i]
})

return digests, nil
}

func createDecoyDisclosures(opts *newOpts) ([]string, error) {
if !opts.addDecoyDigests {
return nil, nil
}

n := mr.Intn(decoyMaxElements-decoyMinElements+1) + decoyMinElements

var decoyDisclosures []string

for i := 1; i < n; i++ {
salt, err := opts.getSalt()
if err != nil {
return nil, err
}

decoyDisclosures = append(decoyDisclosures, salt)
}

return decoyDisclosures, nil
}

// SelectiveDisclosureJWT defines Selective Disclosure JSON Web Token (https://tools.ietf.org/html/rfc7519)
type SelectiveDisclosureJWT struct {
SignedJWT *afgjwt.JSONWebToken
Expand Down
51 changes: 51 additions & 0 deletions pkg/doc/sdjwt/issuer/issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,57 @@ func TestNew(t *testing.T) {
r.NoError(err)
})

t.Run("Create SD-JWS with decoy disclosures", func(t *testing.T) {
r := require.New(t)

pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
r.NoError(err)

verifier, e := afjwt.NewEd25519Verifier(pubKey)
r.NoError(e)

token, err := New(issuer, claims, nil, afjwt.NewEd25519Signer(privKey),
WithDecoyDigests(true))
r.NoError(err)
sdJWTSerialized, err := token.Serialize(false)
r.NoError(err)

sdJWT := common.ParseSDJWT(sdJWTSerialized)
r.Equal(1, len(sdJWT.Disclosures))

afjwtToken, err := afjwt.Parse(sdJWT.JWTSerialized, afjwt.WithSignatureVerifier(verifier))
r.NoError(err)

var parsedClaims map[string]interface{}
err = afjwtToken.DecodeClaims(&parsedClaims)
r.NoError(err)

fmt.Println(fmt.Sprintf("%+v", parsedClaims))

digests, err := common.GetDisclosureDigests(parsedClaims)
require.NoError(t, err)

if len(digests) < 1+decoyMinElements || len(digests) > 1+decoyMaxElements {
r.Fail(fmt.Sprintf("invalid number of digests: %d", len(digests)))
}
})

t.Run("error - create decoy disclosures failed", func(t *testing.T) {
r := require.New(t)

_, privKey, err := ed25519.GenerateKey(rand.Reader)
r.NoError(err)

token, err := New(issuer, claims, nil, afjwt.NewEd25519Signer(privKey),
WithDecoyDigests(true),
WithSaltFnc(func() (string, error) {
return "", fmt.Errorf("salt error")
}))
r.Error(err)
r.Nil(token)
r.Contains(err.Error(), "failed to create decoy disclosures: salt error")
})

t.Run("error - wrong hash function", func(t *testing.T) {
r := require.New(t)

Expand Down

0 comments on commit a1bb1e1

Please sign in to comment.