From 5dac1266619387f3ac4a840f48b022ca4792c202 Mon Sep 17 00:00:00 2001 From: Sandra Vrtikapa Date: Wed, 4 Jan 2023 21:47:59 -0500 Subject: [PATCH] feat: SD-JWT Holder/Verifier - Process Disclosures Added utility function to decode disclosures (used by both holder and verifier) Created disclose claims function for holder that re-creates SD-JWT with disclosures for selected claims only. Created get verified payload function for verifier (removes _sd and _sd_alg from claims and inserts into payload disclosed claims). Closes #3459 Signed-off-by: Sandra Vrtikapa --- pkg/doc/sdjwt/common/common.go | 79 ++++++++++++++++++-- pkg/doc/sdjwt/common/common_test.go | 73 ++++++++++++++++--- pkg/doc/sdjwt/holder/holder.go | 48 ++++++++++++- pkg/doc/sdjwt/holder/holder_test.go | 46 ++++++++++++ pkg/doc/sdjwt/issuer/issuer.go | 5 +- pkg/doc/sdjwt/issuer/issuer_test.go | 2 +- pkg/doc/sdjwt/verifier/verifier.go | 29 +++++++- pkg/doc/sdjwt/verifier/verifier_test.go | 96 ++++++++++++++++--------- 8 files changed, 324 insertions(+), 54 deletions(-) diff --git a/pkg/doc/sdjwt/common/common.go b/pkg/doc/sdjwt/common/common.go index 80bf265c2..0fd2f2e17 100644 --- a/pkg/doc/sdjwt/common/common.go +++ b/pkg/doc/sdjwt/common/common.go @@ -9,6 +9,7 @@ package common import ( "crypto" "encoding/base64" + "encoding/json" "fmt" "strings" @@ -18,7 +19,17 @@ import ( ) // DisclosureSeparator is disclosure separator. -const DisclosureSeparator = "~" +const ( + DisclosureSeparator = "~" + + SDAlgorithmKey = "_sd_alg" + SDKey = "_sd" + + disclosureParts = 3 + saltIndex = 0 + nameIndex = 1 + valueIndex = 2 +) // Payload represents SD-JWT payload. type Payload struct { @@ -40,6 +51,62 @@ type SDJWT struct { Disclosures []string } +// DisclosureClaim defines claim. +type DisclosureClaim struct { + Disclosure string + Salt string + Name string + Value interface{} +} + +// GetDisclosureClaims de-codes disclosures. +func GetDisclosureClaims(disclosures []string) ([]*DisclosureClaim, error) { + var claims []*DisclosureClaim + + for _, disclosure := range disclosures { + claim, err := getDisclosureClaim(disclosure) + if err != nil { + return nil, err + } + + claims = append(claims, claim) + } + + return claims, nil +} + +func getDisclosureClaim(disclosure string) (*DisclosureClaim, error) { + decoded, err := base64.RawURLEncoding.DecodeString(disclosure) + if err != nil { + return nil, fmt.Errorf("failed to decode disclosure: %w", err) + } + + var disclosureArr []interface{} + + err = json.Unmarshal(decoded, &disclosureArr) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal disclosure array: %w", err) + } + + if len(disclosureArr) != disclosureParts { + return nil, fmt.Errorf("disclosure array size[%d] must be %d", len(disclosureArr), disclosureParts) + } + + salt, ok := disclosureArr[saltIndex].(string) + if !ok { + return nil, fmt.Errorf("disclosure salt type[%T] must be string", disclosureArr[saltIndex]) + } + + name, ok := disclosureArr[nameIndex].(string) + if !ok { + return nil, fmt.Errorf("disclosure name type[%T] must be string", disclosureArr[nameIndex]) + } + + claim := &DisclosureClaim{Disclosure: disclosure, Salt: salt, Name: name, Value: disclosureArr[valueIndex]} + + return claim, nil +} + // ParseSDJWT parses SD-JWT serialized token into SDJWT parts. func ParseSDJWT(sdJWTSerialized string) *SDJWT { parts := strings.Split(sdJWTSerialized, DisclosureSeparator) @@ -120,28 +187,28 @@ func getCryptoHash(sdAlg string) (crypto.Hash, error) { case crypto.SHA256.String(): cryptoHash = crypto.SHA256 default: - err = fmt.Errorf("_sd_alg '%s 'not supported", sdAlg) + err = fmt.Errorf("%s '%s 'not supported", SDAlgorithmKey, sdAlg) } return cryptoHash, err } func getSDAlg(claims map[string]interface{}) (string, error) { - obj, ok := claims["_sd_alg"] + obj, ok := claims[SDAlgorithmKey] if !ok { - return "", fmt.Errorf("_sd_alg must be present in SD-JWT") + return "", fmt.Errorf("%s must be present in SD-JWT", SDAlgorithmKey) } str, ok := obj.(string) if !ok { - return "", fmt.Errorf("_sd_alg must be a string") + return "", fmt.Errorf("%s must be a string", SDAlgorithmKey) } return str, nil } func getDisclosureDigests(claims map[string]interface{}) (map[string]bool, error) { - disclosuresObj, ok := claims["_sd"] + disclosuresObj, ok := claims[SDKey] if !ok { return nil, nil } diff --git a/pkg/doc/sdjwt/common/common_test.go b/pkg/doc/sdjwt/common/common_test.go index 70399defe..f7d22c1de 100644 --- a/pkg/doc/sdjwt/common/common_test.go +++ b/pkg/doc/sdjwt/common/common_test.go @@ -10,6 +10,9 @@ import ( "crypto" "crypto/ed25519" "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -84,8 +87,8 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) { t.Run("success - selective disclosures nil", func(t *testing.T) { payload := make(map[string]interface{}) - payload["_sd_alg"] = testAlg - payload["_sd"] = nil + payload[SDAlgorithmKey] = testAlg + payload[SDKey] = nil signedJWT, err := afjwt.NewSigned(payload, nil, signer) r.NoError(err) @@ -132,7 +135,7 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) { err = VerifyDisclosuresInSDJWT(nil, signedJWT) r.Error(err) - r.Contains(err.Error(), "_sd_alg must be present in SD-JWT") + r.Contains(err.Error(), "_sd_alg must be present in SD-JWT", SDAlgorithmKey) }) t.Run("error - invalid algorithm", func(t *testing.T) { @@ -151,7 +154,7 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) { t.Run("error - algorithm is not a string", func(t *testing.T) { payload := make(map[string]interface{}) - payload["_sd_alg"] = 18 + payload[SDAlgorithmKey] = 18 signedJWT, err := afjwt.NewSigned(payload, nil, signer) r.NoError(err) @@ -163,8 +166,8 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) { 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" + payload[SDAlgorithmKey] = testAlg + payload[SDKey] = "test" signedJWT, err := afjwt.NewSigned(payload, nil, signer) r.NoError(err) @@ -176,8 +179,8 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) { 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} + payload[SDAlgorithmKey] = testAlg + payload[SDKey] = []int{123} signedJWT, err := afjwt.NewSigned(payload, nil, signer) r.NoError(err) @@ -188,6 +191,60 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) { }) } +func TestGetDisclosureClaims(t *testing.T) { + r := require.New(t) + + t.Run("success", func(t *testing.T) { + sdJWT := ParseSDJWT(sdJWT) + require.Equal(t, 1, len(sdJWT.Disclosures)) + + disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) + r.NoError(err) + r.Len(disclosureClaims, 1) + + r.Equal("given_name", disclosureClaims[0].Name) + r.Equal("John", disclosureClaims[0].Value) + }) + + t.Run("error - invalid disclosure format (not encoded)", func(t *testing.T) { + sdJWT := ParseSDJWT("jws~xyz") + require.Equal(t, 1, len(sdJWT.Disclosures)) + + disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) + r.Error(err) + r.Nil(disclosureClaims) + r.Contains(err.Error(), "failed to unmarshal disclosure array") + }) + + t.Run("error - invalid disclosure array (not three parts)", func(t *testing.T) { + disclosureArr := []interface{}{"name", "value"} + disclosureJSON, err := json.Marshal(disclosureArr) + require.NoError(t, err) + + sdJWT := ParseSDJWT(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON))) + require.Equal(t, 1, len(sdJWT.Disclosures)) + + disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) + r.Error(err) + r.Nil(disclosureClaims) + r.Contains(err.Error(), "disclosure array size[2] must be 3") + }) + + t.Run("error - invalid disclosure array (name is not a string)", func(t *testing.T) { + disclosureArr := []interface{}{"salt", 123, "value"} + disclosureJSON, err := json.Marshal(disclosureArr) + require.NoError(t, err) + + sdJWT := ParseSDJWT(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON))) + require.Equal(t, 1, len(sdJWT.Disclosures)) + + disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) + r.Error(err) + r.Nil(disclosureClaims) + r.Contains(err.Error(), "disclosure name type[float64] must be string") + }) +} + type NoopSignatureVerifier struct { } diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go index 5330b4e31..f812b4245 100644 --- a/pkg/doc/sdjwt/holder/holder.go +++ b/pkg/doc/sdjwt/holder/holder.go @@ -7,11 +7,15 @@ SPDX-License-Identifier: Apache-2.0 package holder import ( + "fmt" + "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" ) +const notFound = -1 + // jwtParseOpts holds options for the SD-JWT parsing. type parseOpts struct { detachedPayload []byte @@ -37,13 +41,14 @@ func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt { // Parse parses input JWT in serialized form into JSON Web Token. func Parse(sdJWTSerialized string, opts ...ParseOpt) (*common.SDJWT, error) { - pOpts := &parseOpts{} + pOpts := &parseOpts{ + sigVerifier: &NoopSignatureVerifier{}, + } for _, opt := range opts { opt(pOpts) } - // TODO: Holder is not required to check issuer signature so we should probably have default no-op verifier var jwtOpts []afgjwt.ParseOpt jwtOpts = append(jwtOpts, afgjwt.WithSignatureVerifier(pOpts.sigVerifier), @@ -64,6 +69,45 @@ func Parse(sdJWTSerialized string, opts ...ParseOpt) (*common.SDJWT, error) { return sdJWT, nil } +// DiscloseClaims discloses selected claims with specified claim names. +func DiscloseClaims(sdJWTSerialized string, claimNames []string) (string, error) { + sdJWT := common.ParseSDJWT(sdJWTSerialized) + + if len(sdJWT.Disclosures) == 0 { + return "", fmt.Errorf("no disclosures found in SD-JWT") + } + + disclosures, err := common.GetDisclosureClaims(sdJWT.Disclosures) + if err != nil { + return "", err + } + + var selectedDisclosures []string + + for _, claimName := range claimNames { + if index := getDisclosureByClaimName(claimName, disclosures); index != notFound { + selectedDisclosures = append(selectedDisclosures, sdJWT.Disclosures[index]) + } + } + + combinedFormatForPresentation := sdJWT.JWTSerialized + for _, disclosure := range selectedDisclosures { + combinedFormatForPresentation += common.DisclosureSeparator + disclosure + } + + return combinedFormatForPresentation, nil +} + +func getDisclosureByClaimName(name string, disclosures []*common.DisclosureClaim) int { + for index, disclosure := range disclosures { + if disclosure.Name == name { + return index + } + } + + return notFound +} + // NoopSignatureVerifier is no-op signature verifier (signature will not get checked). type NoopSignatureVerifier struct { } diff --git a/pkg/doc/sdjwt/holder/holder_test.go b/pkg/doc/sdjwt/holder/holder_test.go index c35506717..3f1f46f68 100644 --- a/pkg/doc/sdjwt/holder/holder_test.go +++ b/pkg/doc/sdjwt/holder/holder_test.go @@ -19,6 +19,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/jose" afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer" ) @@ -50,6 +51,13 @@ func TestParse(t *testing.T) { require.Equal(t, 1, len(sdJWT.Disclosures)) }) + t.Run("success - default is no signature verifier", func(t *testing.T) { + sdJWT, err := Parse(sdJWTSerialized) + r.NoError(err) + require.NotNil(t, sdJWT) + require.Equal(t, 1, len(sdJWT.Disclosures)) + }) + t.Run("success - spec SD-JWT", func(t *testing.T) { sdJWT, err := Parse(specSDJWT, WithSignatureVerifier(&NoopSignatureVerifier{})) r.NoError(err) @@ -90,6 +98,44 @@ func TestParse(t *testing.T) { }) } +func TestDiscloseClaims(t *testing.T) { + r := require.New(t) + + _, privKey, e := ed25519.GenerateKey(rand.Reader) + r.NoError(e) + + signer := afjwt.NewEd25519Signer(privKey) + claims := map[string]interface{}{"given_name": "Albert"} + + token, e := issuer.New(testIssuer, claims, nil, signer) + r.NoError(e) + sdJWTSerialized, e := token.Serialize(false) + r.NoError(e) + + t.Run("success", func(t *testing.T) { + sdJWTDisclosed, err := DiscloseClaims(sdJWTSerialized, []string{"given_name"}) + r.NoError(err) + require.NotNil(t, sdJWTDisclosed) + require.Equal(t, sdJWTSerialized, sdJWTDisclosed) + }) + + t.Run("error - no disclosure(s)", func(t *testing.T) { + sdJWT := common.ParseSDJWT(sdJWTSerialized) + + sdJWTDisclosed, err := DiscloseClaims(sdJWT.JWTSerialized, []string{"given_name"}) + r.Error(err) + r.Empty(sdJWTDisclosed) + r.Contains(err.Error(), "no disclosures found in SD-JWT") + }) + + t.Run("error - invalid disclosure", func(t *testing.T) { + sdJWTDisclosed, err := DiscloseClaims(fmt.Sprintf("%s~%s", sdJWTSerialized, "abc"), []string{"given_name"}) + r.Error(err) + r.Empty(sdJWTDisclosed) + r.Contains(err.Error(), "failed to unmarshal disclosure array") + }) +} + func TestWithJWTDetachedPayload(t *testing.T) { detachedPayloadOpt := WithJWTDetachedPayload([]byte("payload")) require.NotNil(t, detachedPayloadOpt) diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go index 5ba29486f..fcc8fd827 100644 --- a/pkg/doc/sdjwt/issuer/issuer.go +++ b/pkg/doc/sdjwt/issuer/issuer.go @@ -25,7 +25,7 @@ import ( const ( defaultHash = crypto.SHA256 - defaultSaltSize = 28 + defaultSaltSize = 128 / 8 year = 365 * 24 * 60 * time.Minute ) @@ -241,5 +241,6 @@ func generateSalt() (string, error) { return "", err } - return string(salt), nil + // it is RECOMMENDED to base64url-encode the salt value, producing a string. + return base64.RawURLEncoding.EncodeToString(salt), nil } diff --git a/pkg/doc/sdjwt/issuer/issuer_test.go b/pkg/doc/sdjwt/issuer/issuer_test.go index fc1d7a03c..770436f9e 100644 --- a/pkg/doc/sdjwt/issuer/issuer_test.go +++ b/pkg/doc/sdjwt/issuer/issuer_test.go @@ -465,7 +465,7 @@ func verifyRS256(jws string, pubKey *rsa.PublicKey) error { } func existsInDisclosures(claims map[string]interface{}, val string) bool { - disclosuresObj, ok := claims["_sd"] + disclosuresObj, ok := claims[common.SDKey] if !ok { return false } diff --git a/pkg/doc/sdjwt/verifier/verifier.go b/pkg/doc/sdjwt/verifier/verifier.go index ff8b2553c..150874aca 100644 --- a/pkg/doc/sdjwt/verifier/verifier.go +++ b/pkg/doc/sdjwt/verifier/verifier.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package holder +package verifier import ( "fmt" @@ -48,7 +48,7 @@ func WithSigningAlgorithms(algorithms []string) ParseOpt { } // Parse parses input JWT in serialized form into JSON Web Token. -func Parse(sdJWTSerialized string, opts ...ParseOpt) (*common.SDJWT, error) { +func Parse(sdJWTSerialized string, opts ...ParseOpt) (map[string]interface{}, error) { pOpts := &parseOpts{ signingAlgorithms: []string{"EdDSA", "RS256"}, } @@ -99,7 +99,30 @@ func Parse(sdJWTSerialized string, opts ...ParseOpt) (*common.SDJWT, error) { return nil, err } - return sdJWT, nil + return getVerifiedPayload(sdJWT.Disclosures, signedJWT) +} + +func getVerifiedPayload(disclosures []string, signedJWT *afgjwt.JSONWebToken) (map[string]interface{}, error) { + disclosureClaims, err := common.GetDisclosureClaims(disclosures) + if err != nil { + return nil, fmt.Errorf("failed to get verified claims: %w", err) + } + + var claims map[string]interface{} + + err = signedJWT.DecodeClaims(&claims) + if err != nil { + return nil, fmt.Errorf("failed to get verified claims: %w", err) + } + + for _, dc := range disclosureClaims { + claims[dc.Name] = dc.Value + } + + delete(claims, common.SDKey) + delete(claims, common.SDAlgorithmKey) + + return claims, nil } func verifySigningAlg(joseHeaders jose.Headers, secureAlgs []string) error { diff --git a/pkg/doc/sdjwt/verifier/verifier_test.go b/pkg/doc/sdjwt/verifier/verifier_test.go index ebbd66785..11882c093 100644 --- a/pkg/doc/sdjwt/verifier/verifier_test.go +++ b/pkg/doc/sdjwt/verifier/verifier_test.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package holder +package verifier import ( "crypto/ed25519" @@ -36,9 +36,9 @@ func TestParse(t *testing.T) { r.NoError(e) signer := afjwt.NewEd25519Signer(privKey) - claims := map[string]interface{}{"given_name": "Albert"} + selectiveClaims := map[string]interface{}{"given_name": "Albert"} - token, e := issuer.New(testIssuer, claims, nil, signer) + token, e := issuer.New(testIssuer, selectiveClaims, nil, signer) r.NoError(e) sdJWTSerialized, e := token.Serialize(false) r.NoError(e) @@ -47,10 +47,12 @@ func TestParse(t *testing.T) { r.NoError(e) t.Run("success - EdDSA signing algorithm", func(t *testing.T) { - sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) r.NoError(err) - require.NotNil(t, sdJWT) - require.Equal(t, 1, len(sdJWT.Disclosures)) + require.NotNil(t, claims) + // expected claims iss, exp, iat, nbf, given_name + // TODO: should we default exp, iat, nbf + require.Equal(t, 5, len(claims)) }) t.Run("success - RS256 signing algorithm", func(t *testing.T) { @@ -61,15 +63,14 @@ func TestParse(t *testing.T) { v := afjwt.NewRS256Verifier(pubKey) - token, err := issuer.New(testIssuer, claims, nil, afjwt.NewRS256Signer(privKey, nil)) + token, err := issuer.New(testIssuer, selectiveClaims, nil, afjwt.NewRS256Signer(privKey, nil)) r.NoError(err) tokenSerialized, err := token.Serialize(false) require.NoError(t, err) - sdJWT, err := Parse(tokenSerialized, WithSignatureVerifier(v)) + claims, err := Parse(tokenSerialized, WithSignatureVerifier(v)) r.NoError(err) - require.NotNil(t, sdJWT) - require.Equal(t, 1, len(sdJWT.Disclosures)) + require.Equal(t, 5, len(claims)) }) t.Run("success - valid SD-JWT times", func(t *testing.T) { @@ -77,7 +78,7 @@ func TestParse(t *testing.T) { oneHourInThePast := now.Add(-time.Hour) oneHourInTheFuture := now.Add(time.Hour) - tokenWithTimes, e := issuer.New(testIssuer, claims, nil, signer, + tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer, issuer.WithIssuedAt(jwt.NewNumericDate(oneHourInThePast)), issuer.WithNotBefore(jwt.NewNumericDate(oneHourInThePast)), issuer.WithExpiry(jwt.NewNumericDate(oneHourInTheFuture))) @@ -85,31 +86,34 @@ func TestParse(t *testing.T) { serialized, e := tokenWithTimes.Serialize(false) r.NoError(e) - sdJWT, err := Parse(serialized, WithSignatureVerifier(verifier)) + claims, err := Parse(serialized, WithSignatureVerifier(verifier)) r.NoError(err) - r.NotNil(sdJWT) + r.NotNil(claims) }) t.Run("error - signing algorithm not supported", func(t *testing.T) { - sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier), WithSigningAlgorithms([]string{})) + claims, err := Parse(sdJWTSerialized, + WithSignatureVerifier(verifier), + WithSigningAlgorithms([]string{})) r.Error(err) - require.Nil(t, sdJWT) + require.Nil(t, claims) require.Equal(t, err.Error(), "alg 'EdDSA' is not in the allowed list") }) t.Run("error - additional disclosure", func(t *testing.T) { - sdJWT, err := Parse(fmt.Sprintf("%s~%s", sdJWTSerialized, additionalDisclosure), WithSignatureVerifier(verifier)) + claims, err := Parse(fmt.Sprintf("%s~%s", sdJWTSerialized, additionalDisclosure), + WithSignatureVerifier(verifier)) r.Error(err) - r.Nil(sdJWT) + r.Nil(claims) r.Contains(err.Error(), "disclosure digest 'qqvcqnczAMgYx7EykI6wwtspyvyvK790ge7MBbQ-Nus' not found in SD-JWT disclosure digests") }) t.Run("error - duplicate disclosure", func(t *testing.T) { - sdJWT, err := Parse(fmt.Sprintf("%s~%s~%s", sdJWTSerialized, additionalDisclosure, additionalDisclosure), + claims, err := Parse(fmt.Sprintf("%s~%s~%s", sdJWTSerialized, additionalDisclosure, additionalDisclosure), WithSignatureVerifier(verifier)) r.Error(err) - r.Nil(sdJWT) + r.Nil(claims) r.Contains(err.Error(), "check disclosures: duplicate values found [WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd]") }) @@ -121,10 +125,10 @@ func TestParse(t *testing.T) { jwsPayload, err := base64.RawURLEncoding.DecodeString(jwsParts[1]) require.NoError(t, err) - sdJWT, err := Parse(jwsDetached, + claims, err := Parse(jwsDetached, WithSignatureVerifier(verifier), WithJWTDetachedPayload(jwsPayload)) r.NoError(err) - r.NotNil(r, sdJWT) + r.NotNil(r, claims) }) t.Run("error - invalid claims format", func(t *testing.T) { @@ -132,61 +136,61 @@ func TestParse(t *testing.T) { sdJWTSerialized, err := buildJWS(signer, "not JSON") r.NoError(err) - sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) r.Error(err) r.Contains(err.Error(), "read JWT claims from JWS payload") - r.Nil(sdJWT) + r.Nil(claims) }) t.Run("error - invalid claims(iat)", func(t *testing.T) { now := time.Now() oneHourInTheFuture := now.Add(time.Hour) - tokenWithTimes, e := issuer.New(testIssuer, claims, nil, signer, + tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer, issuer.WithIssuedAt(jwt.NewNumericDate(oneHourInTheFuture))) r.NoError(e) sdJWTSerialized, e := tokenWithTimes.Serialize(false) r.NoError(e) - sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) r.Error(err) r.Contains(err.Error(), "failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation field, token issued in the future (iat)") - r.Nil(sdJWT) + r.Nil(claims) }) t.Run("error - invalid claims(nbf)", func(t *testing.T) { now := time.Now() oneHourInTheFuture := now.Add(time.Hour) - tokenWithTimes, e := issuer.New(testIssuer, claims, nil, signer, + tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer, issuer.WithNotBefore(jwt.NewNumericDate(oneHourInTheFuture))) r.NoError(e) sdJWTSerialized, e := tokenWithTimes.Serialize(false) r.NoError(e) - sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) r.Error(err) r.Contains(err.Error(), "failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)") - r.Nil(sdJWT) + r.Nil(claims) }) t.Run("error - invalid claims(expiry)", func(t *testing.T) { now := time.Now() oneHourInThePast := now.Add(-time.Hour) - tokenWithTimes, e := issuer.New(testIssuer, claims, nil, signer, + tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer, issuer.WithExpiry(jwt.NewNumericDate(oneHourInThePast))) r.NoError(e) sdJWTSerialized, e := tokenWithTimes.Serialize(false) r.NoError(e) - sdJWT, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) r.Error(err) r.Contains(err.Error(), "failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation failed, token is expired (exp)") - r.Nil(sdJWT) + r.Nil(claims) }) } @@ -224,6 +228,34 @@ func TestVerifySigningAlgorithm(t *testing.T) { }) } +func TestGetVerifiedPayload(t *testing.T) { + r := require.New(t) + + _, privKey, e := ed25519.GenerateKey(rand.Reader) + r.NoError(e) + + signer := afjwt.NewEd25519Signer(privKey) + selectiveClaims := map[string]interface{}{"given_name": "Albert"} + + token, e := issuer.New(testIssuer, selectiveClaims, nil, signer) + r.NoError(e) + + t.Run("success", func(t *testing.T) { + claims, err := getVerifiedPayload(token.Disclosures, token.SignedJWT) + r.NoError(err) + r.NotNil(claims) + r.Equal(5, len(claims)) + }) + + t.Run("error - invalid disclosure(not encoded)", func(t *testing.T) { + claims, err := getVerifiedPayload([]string{"xyz"}, token.SignedJWT) + r.Error(err) + r.Nil(claims) + r.Contains(err.Error(), + "failed to get verified claims: failed to unmarshal disclosure array: invalid character") + }) +} + func TestWithJWTDetachedPayload(t *testing.T) { detachedPayloadOpt := WithJWTDetachedPayload([]byte("payload")) require.NotNil(t, detachedPayloadOpt)