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

Commit

Permalink
feat: Add SD-JWT Verifier (Flat)
Browse files Browse the repository at this point in the history
Parse and verify combined presentation from issuer.

Closes #3455

Signed-off-by: Sandra Vrtikapa <[email protected]>
  • Loading branch information
sandrask committed Jan 3, 2023
1 parent 6d96423 commit ace435d
Show file tree
Hide file tree
Showing 7 changed files with 737 additions and 306 deletions.
118 changes: 118 additions & 0 deletions pkg/doc/sdjwt/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"strings"

"github.com/go-jose/go-jose/v3/jwt"

afgjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
)

// DisclosureSeparator is disclosure separator.
Expand Down Expand Up @@ -68,3 +70,119 @@ func GetHash(hash crypto.Hash, value string) (string, error) {

return base64.RawURLEncoding.EncodeToString(result), nil
}

// VerifyDisclosuresInSDJWT checks for disclosure inclusion in SD-JWT.
func VerifyDisclosuresInSDJWT(disclosures []string, signedJWT *afgjwt.JSONWebToken) error {
var claims map[string]interface{}

err := signedJWT.DecodeClaims(&claims)
if err != nil {
return err
}

// check that the _sd_alg claim is present
sdAlg, err := getSDAlg(claims)
if err != nil {
return err
}

// check that _sd_alg value is understood and the hash algorithm is deemed secure.
cryptoHash, err := getCryptoHash(sdAlg)
if err != nil {
return err
}

claimsDisclosureDigests, err := getDisclosureDigests(claims)
if err != nil {
return err
}

for _, disclosure := range disclosures {
digest, err := GetHash(cryptoHash, disclosure)
if err != nil {
return err
}

if _, ok := claimsDisclosureDigests[digest]; !ok {
return fmt.Errorf("disclosure digest '%s' not found in SD-JWT disclosure digests", digest)
}
}

return nil
}

func getCryptoHash(sdAlg string) (crypto.Hash, error) {
var err error

var cryptoHash crypto.Hash

switch strings.ToUpper(sdAlg) {
case crypto.SHA256.String():
cryptoHash = crypto.SHA256
default:
err = fmt.Errorf("_sd_alg '%s 'not supported", sdAlg)
}

return cryptoHash, err
}

func getSDAlg(claims map[string]interface{}) (string, error) {
obj, ok := claims["_sd_alg"]
if !ok {
return "", fmt.Errorf("_sd_alg must be present in SD-JWT")
}

str, ok := obj.(string)
if !ok {
return "", fmt.Errorf("_sd_alg must be a string")
}

return str, nil
}

func getDisclosureDigests(claims map[string]interface{}) (map[string]bool, error) {
disclosuresObj, ok := claims["_sd"]
if !ok {
return nil, nil
}

disclosures, err := stringArray(disclosuresObj)
if err != nil {
return nil, fmt.Errorf("get disclosure digests: %w", err)
}

return sliceToMap(disclosures), nil
}

func stringArray(entry interface{}) ([]string, error) {
if entry == nil {
return nil, nil
}

entries, ok := entry.([]interface{})
if !ok {
return nil, fmt.Errorf("entry type[%T] is not an array", entry)
}

var result []string

for _, e := range entries {
if eStr, ok := e.(string); ok {
result = append(result, eStr)
} else {
return nil, fmt.Errorf("entry item type[%T] is not a string", e)
}
}

return result, nil
}

func sliceToMap(ids []string) map[string]bool {
// convert slice to map
values := make(map[string]bool)
for _, id := range ids {
values[id] = true
}

return values
}
165 changes: 164 additions & 1 deletion pkg/doc/sdjwt/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@ package common

import (
"crypto"
"crypto/ed25519"
"crypto/rand"
"testing"

"github.com/stretchr/testify/require"

"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
)

const defaultHash = crypto.SHA256
const (
defaultHash = crypto.SHA256

testAlg = "sha-256"
)

func TestGetHash(t *testing.T) {
t.Run("success", func(t *testing.T) {
Expand All @@ -35,7 +44,161 @@ func TestParseSDJWT(t *testing.T) {
sdJWT := ParseSDJWT(sdJWT)
require.Equal(t, 1, len(sdJWT.Disclosures))
})
t.Run("success", func(t *testing.T) {
sdJWT := ParseSDJWT(specSDJWT)
require.Equal(t, 7, len(sdJWT.Disclosures))
})
}

func TestVerifyDisclosuresInSDJWT(t *testing.T) {
r := require.New(t)

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

signer := afjwt.NewEd25519Signer(privKey)

t.Run("success", func(t *testing.T) {
sdJWT := ParseSDJWT(sdJWT)
require.Equal(t, 1, len(sdJWT.Disclosures))

signedJWT, err := afjwt.Parse(sdJWT.JWTSerialized, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{}))
require.NoError(t, err)

err = VerifyDisclosuresInSDJWT(sdJWT.Disclosures, signedJWT)
r.NoError(err)
})

t.Run("success - no selective disclosures(valid case)", func(t *testing.T) {
payload := &Payload{
Issuer: "issuer",
SDAlg: "sha-256",
}

signedJWT, err := afjwt.NewSigned(payload, nil, signer)
r.NoError(err)

err = VerifyDisclosuresInSDJWT(nil, signedJWT)
r.NoError(err)
})

t.Run("success - selective disclosures nil", func(t *testing.T) {
payload := make(map[string]interface{})
payload["_sd_alg"] = testAlg
payload["_sd"] = nil

signedJWT, err := afjwt.NewSigned(payload, nil, signer)
r.NoError(err)

err = VerifyDisclosuresInSDJWT(nil, signedJWT)
r.NoError(err)
})

t.Run("error - disclosure not present in SD-JWT", func(t *testing.T) {
sdJWT := ParseSDJWT(sdJWT)
require.Equal(t, 1, len(sdJWT.Disclosures))

signedJWT, err := afjwt.Parse(sdJWT.JWTSerialized, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{}))
require.NoError(t, err)

err = VerifyDisclosuresInSDJWT(append(sdJWT.Disclosures, additionalDisclosure), signedJWT)
r.Error(err)
r.Contains(err.Error(),
"disclosure digest 'X9yH0Ajrdm1Oij4tWso9UzzKJvPoDxwmuEcO3XAdRC0' not found in SD-JWT disclosure digests")
})

t.Run("error - disclosure not present in SD-JWT without selective disclosures", func(t *testing.T) {
payload := &Payload{
Issuer: "issuer",
SDAlg: testAlg,
}

signedJWT, err := afjwt.NewSigned(payload, nil, signer)
r.NoError(err)

err = VerifyDisclosuresInSDJWT([]string{additionalDisclosure}, signedJWT)
r.Error(err)
r.Contains(err.Error(),
"disclosure digest 'X9yH0Ajrdm1Oij4tWso9UzzKJvPoDxwmuEcO3XAdRC0' not found in SD-JWT disclosure digests")
})

t.Run("error - missing algorithm", func(t *testing.T) {
payload := &Payload{
Issuer: "issuer",
}

signedJWT, err := afjwt.NewSigned(payload, nil, signer)
r.NoError(err)

err = VerifyDisclosuresInSDJWT(nil, signedJWT)
r.Error(err)
r.Contains(err.Error(), "_sd_alg must be present in SD-JWT")
})

t.Run("error - invalid algorithm", func(t *testing.T) {
payload := &Payload{
Issuer: "issuer",
SDAlg: "SHA-XXX",
}

signedJWT, err := afjwt.NewSigned(payload, nil, signer)
r.NoError(err)

err = VerifyDisclosuresInSDJWT(nil, signedJWT)
r.Error(err)
r.Contains(err.Error(), "_sd_alg 'SHA-XXX 'not supported")
})

t.Run("error - algorithm is not a string", func(t *testing.T) {
payload := make(map[string]interface{})
payload["_sd_alg"] = 18

signedJWT, err := afjwt.NewSigned(payload, nil, signer)
r.NoError(err)

err = VerifyDisclosuresInSDJWT(nil, signedJWT)
r.Error(err)
r.Contains(err.Error(), "_sd_alg must be a string")
})

t.Run("error - selective disclosures must be an array", func(t *testing.T) {
payload := make(map[string]interface{})
payload["_sd_alg"] = testAlg
payload["_sd"] = "test"

signedJWT, err := afjwt.NewSigned(payload, nil, signer)
r.NoError(err)

err = VerifyDisclosuresInSDJWT(nil, signedJWT)
r.Error(err)
r.Contains(err.Error(), "get disclosure digests: entry type[string] is not an array")
})

t.Run("error - selective disclosures must be a string", func(t *testing.T) {
payload := make(map[string]interface{})
payload["_sd_alg"] = testAlg
payload["_sd"] = []int{123}

signedJWT, err := afjwt.NewSigned(payload, nil, signer)
r.NoError(err)

err = VerifyDisclosuresInSDJWT(nil, signedJWT)
r.Error(err)
r.Contains(err.Error(), "get disclosure digests: entry item type[float64] is not a string")
})
}

type NoopSignatureVerifier struct {
}

func (sv *NoopSignatureVerifier) Verify(joseHeaders jose.Headers, payload, signingInput, signature []byte) error {
return nil
}

const additionalDisclosure = `WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0`

// nolint: lll
const sdJWT = `eyJhbGciOiJFZERTQSJ9.eyJfc2QiOlsicXF2Y3FuY3pBTWdZeDdFeWtJNnd3dHNweXZ5dks3OTBnZTdNQmJRLU51cyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTcwMzAyMzg1NSwiaWF0IjoxNjcxNDg3ODU1LCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsIm5iZiI6MTY3MTQ4Nzg1NX0.vscuzfwcHGi04pWtJCadc4iDELug6NH6YK-qxhY1qacsciIHuoLELAfon1tGamHtuu8TSs6OjtLk3lHE16jqAQ~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd`

// nolint: lll
const specSDJWT = `eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImNBRUlVcUowY21MekQxa3pHemhlaUJhZzBZUkF6VmRsZnhOMjgwTmdIYUEifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIlJTQSIsICJuIjogInBtNGJPSEJnLW9ZaEF5UFd6UjU2QVdYM3JVSVhwMTFfSUNEa0dnUzZXM1pXTHRzLWh6d0kzeDY1NjU5a2c0aFZvOWRiR29DSkUzWkdGX2VhZXRFMzBVaEJVRWdwR3dyRHJRaUo5enFwcm1jRmZyM3F2dmtHanR0aDhaZ2wxZU0yYkpjT3dFN1BDQkhXVEtXWXMxNTJSN2c2SmcyT1ZwaC1hOHJxLXE3OU1oS0c1UW9XX21UejEwUVRfNkg0YzdQaldHMWZqaDhocFdObmJQX3B2NmQxelN3WmZjNWZsNnlWUkwwRFYwVjNsR0hLZTJXcWZfZU5HakJyQkxWa2xEVGs4LXN0WF9NV0xjUi1FR21YQU92MFVCV2l0U19kWEpLSnUtdlhKeXcxNG5IU0d1eFRJSzJoeDFwdHRNZnQ5Q3N2cWltWEtlRFRVMTRxUUwxZUU3aWhjdyIsICJlIjogIkFRQUIifX19.xqgKrDO6dK_oBL3fiqdcq_elaIGxM6Z-RyuysglGyddR1O1IiE3mIk8kCpoqcRLR88opkVWN2392K_XYfAuAmeT9kJVisD8ZcgNcv-MQlWW9s8WaViXxBRe7EZWkWRQcQVR6jf95XZ5H2-_KA54POq3L42xjk0y5vDr8yc08Reak6vvJVvjXpp-Wk6uxsdEEAKFspt_EYIvISFJhfTuQqyhCjnaW13X312MSQBPwjbHn74ylUqVLljDvqcemxeqjh42KWJq4C3RqNJ7anA2i3FU1kB4-KNZWsijY7-op49iL7BrnIBxdlAMrbHEkoGTbFWdl7Ki17GHtDxxa1jaxQg~WyJkcVR2WE14UzBHYTNEb2FHbmU5eDBRIiwgInN1YiIsICJqb2huX2RvZV80MiJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJLVXhTNWhFX1hiVmFjckdBYzdFRnd3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyIzcXZWSjFCQURwSERTUzkzOVEtUml3IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJFUktNMENOZUZKa2FENW1UWFZfWDh3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0`
Loading

0 comments on commit ace435d

Please sign in to comment.