diff --git a/pkg/doc/sdjwt/common/common.go b/pkg/doc/sdjwt/common/common.go index 0fd2f2e17..8a5f81fa9 100644 --- a/pkg/doc/sdjwt/common/common.go +++ b/pkg/doc/sdjwt/common/common.go @@ -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 } @@ -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 diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go index fcc8fd827..973e94c41 100644 --- a/pkg/doc/sdjwt/issuer/issuer.go +++ b/pkg/doc/sdjwt/issuer/issuer.go @@ -13,6 +13,7 @@ import ( "encoding/json" "errors" "fmt" + mathrand "math/rand" "strings" "time" @@ -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 @@ -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. @@ -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) { @@ -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 @@ -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 := createDigests(append(disclosures, decoyDisclosures...), nOpts) + if err != nil { + return nil, err } payload := &common.Payload{ @@ -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()), } @@ -167,6 +181,46 @@ func New(issuer string, claims interface{}, headers jose.Headers, return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil } +func createDigests(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 := 0; 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 diff --git a/pkg/doc/sdjwt/issuer/issuer_test.go b/pkg/doc/sdjwt/issuer/issuer_test.go index 770436f9e..772ff5a85 100644 --- a/pkg/doc/sdjwt/issuer/issuer_test.go +++ b/pkg/doc/sdjwt/issuer/issuer_test.go @@ -165,6 +165,55 @@ 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) + + 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)