From ef0deab749b2068e2145410f5acb197fdc84f22b Mon Sep 17 00:00:00 2001 From: Stas Dm Date: Wed, 9 Aug 2023 23:47:08 +0200 Subject: [PATCH] feat: SDJWT V5 Signed-off-by: Mykhailo Sizov feat: improve coverage fix: nil feat: more coverage --- .golangci.yml | 2 + Makefile | 7 + cmd/aries-agent-mobile/go.mod | 1 + cmd/aries-agent-mobile/go.sum | 5 +- cmd/aries-agent-rest/go.mod | 1 + cmd/aries-agent-rest/go.sum | 4 +- cmd/aries-js-worker/go.mod | 1 + cmd/aries-js-worker/go.sum | 5 +- component/models/go.mod | 10 +- component/models/go.sum | 15 +- component/models/jwt/jwt.go | 5 +- component/models/presexch/definition_test.go | 14 +- component/models/sdjwt/common/common.go | 323 +++--- component/models/sdjwt/common/common_test.go | 627 ++++++++---- .../array_element_and_one_missing_v5.json | 10 + .../common/testdata/full_disclosures_v5.json | 12 + component/models/sdjwt/common/types.go | 13 + component/models/sdjwt/common/verification.go | 392 +++++++ .../models/sdjwt/common/verification_test.go | 542 ++++++++++ component/models/sdjwt/example_test.go | 2 +- component/models/sdjwt/holder/example_test.go | 4 +- component/models/sdjwt/holder/holder.go | 149 ++- component/models/sdjwt/holder/holder_test.go | 102 +- component/models/sdjwt/integration_test.go | 119 ++- component/models/sdjwt/issuer/issuer.go | 258 +++-- component/models/sdjwt/issuer/issuer_test.go | 148 ++- component/models/sdjwt/issuer/v2.go | 145 +++ component/models/sdjwt/issuer/v5.go | 341 +++++++ component/models/sdjwt/issuer/v5_test.go | 684 +++++++++++++ .../models/sdjwt/verifier/holderbidning.go | 63 ++ component/models/sdjwt/verifier/keybidning.go | 62 ++ component/models/sdjwt/verifier/verifier.go | 300 +++--- .../sdjwt/verifier/verifier_interop_test.go | 215 +++- .../models/sdjwt/verifier/verifier_test.go | 962 +++++++++--------- component/models/verifiable/credential.go | 75 +- component/models/verifiable/credential_jws.go | 20 +- .../models/verifiable/credential_jws_test.go | 16 +- component/models/verifiable/credential_jwt.go | 58 +- .../models/verifiable/credential_jwt_test.go | 49 +- .../verifiable/credential_jwt_unsecured.go | 14 +- .../models/verifiable/credential_sdjwt.go | 156 ++- .../verifiable/credential_sdjwt_test.go | 87 +- .../models/verifiable/credential_test.go | 13 +- component/models/verifiable/jws.go | 10 +- component/models/verifiable/jwt_unsecured.go | 6 +- .../models/verifiable/jwt_unsecured_test.go | 6 +- .../models/verifiable/presentation_jws.go | 2 +- .../verifiable/presentation_jwt_unsecured.go | 2 +- go.mod | 3 + go.sum | 7 +- pkg/doc/sdjwt/common/common.go | 4 +- pkg/doc/sdjwt/holder/holder.go | 36 +- pkg/doc/sdjwt/issuer/issuer.go | 107 ++ pkg/doc/sdjwt/verifier/verifier.go | 385 +------ pkg/doc/verifiable/verifiable.go | 33 +- pkg/doc/verifiable/withvdr_test.go | 20 + test/bdd/go.mod | 1 + test/bdd/go.sum | 5 +- 58 files changed, 5047 insertions(+), 1611 deletions(-) create mode 100644 component/models/sdjwt/common/testdata/array_element_and_one_missing_v5.json create mode 100644 component/models/sdjwt/common/testdata/full_disclosures_v5.json create mode 100644 component/models/sdjwt/common/types.go create mode 100644 component/models/sdjwt/common/verification.go create mode 100644 component/models/sdjwt/common/verification_test.go create mode 100644 component/models/sdjwt/issuer/v2.go create mode 100644 component/models/sdjwt/issuer/v5.go create mode 100644 component/models/sdjwt/issuer/v5_test.go create mode 100644 component/models/sdjwt/verifier/holderbidning.go create mode 100644 component/models/sdjwt/verifier/keybidning.go diff --git a/.golangci.yml b/.golangci.yml index 60e99649e..f04fc1868 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -56,6 +56,8 @@ linters-settings: lll: line-length: 120 tab-width: 1 + exclude: + - "^\\s*\\/\\/.*" unused: check-exported: false unparam: diff --git a/Makefile b/Makefile index 83bd3b492..34df14e8e 100644 --- a/Makefile +++ b/Makefile @@ -255,3 +255,10 @@ clean-fixtures: @cd test/bdd/fixtures/sidetree-mock && docker-compose down 2> /dev/null @cd test/bdd/fixtures/agent-rest && docker-compose down 2> /dev/null +.PHONY: tidy-modules +tidy-modules: + @find . -type d \( -name build -prune \) -o -name go.mod -print | while read -r gomod_path; do \ + dir_path=$$(dirname "$$gomod_path"); \ + echo "Executing 'go mod tidy' in directory: $$dir_path"; \ + (cd "$$dir_path" && go mod tidy) || exit 1; \ + done \ No newline at end of file diff --git a/cmd/aries-agent-mobile/go.mod b/cmd/aries-agent-mobile/go.mod index 77684bd0b..d4ff94c5f 100644 --- a/cmd/aries-agent-mobile/go.mod +++ b/cmd/aries-agent-mobile/go.mod @@ -70,6 +70,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/crypto v0.1.0 // indirect + golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect golang.org/x/sys v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/cmd/aries-agent-mobile/go.sum b/cmd/aries-agent-mobile/go.sum index c10d88abe..bd527b714 100644 --- a/cmd/aries-agent-mobile/go.sum +++ b/cmd/aries-agent-mobile/go.sum @@ -59,7 +59,7 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= @@ -170,6 +170,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -183,7 +185,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= diff --git a/cmd/aries-agent-rest/go.mod b/cmd/aries-agent-rest/go.mod index 4f13c0ad5..6a12a3cba 100644 --- a/cmd/aries-agent-rest/go.mod +++ b/cmd/aries-agent-rest/go.mod @@ -92,6 +92,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect go.mongodb.org/mongo-driver v1.8.0 // indirect + golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect golang.org/x/net v0.1.0 // indirect golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect golang.org/x/sys v0.2.0 // indirect diff --git a/cmd/aries-agent-rest/go.sum b/cmd/aries-agent-rest/go.sum index 7ec622df0..8cde1be38 100644 --- a/cmd/aries-agent-rest/go.sum +++ b/cmd/aries-agent-rest/go.sum @@ -166,7 +166,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -576,6 +576,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/cmd/aries-js-worker/go.mod b/cmd/aries-js-worker/go.mod index 8b7ea84e3..53705a075 100644 --- a/cmd/aries-js-worker/go.mod +++ b/cmd/aries-js-worker/go.mod @@ -69,6 +69,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/crypto v0.1.0 // indirect + golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect golang.org/x/sys v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/cmd/aries-js-worker/go.sum b/cmd/aries-js-worker/go.sum index 429f80007..35eedd82d 100644 --- a/cmd/aries-js-worker/go.sum +++ b/cmd/aries-js-worker/go.sum @@ -58,7 +58,7 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= @@ -165,6 +165,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -180,7 +182,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= diff --git a/component/models/go.mod b/component/models/go.mod index 6ba29ca3f..abbe9c57e 100644 --- a/component/models/go.mod +++ b/component/models/go.mod @@ -17,7 +17,7 @@ require ( github.com/google/tink/go v1.7.0 github.com/google/uuid v1.3.0 github.com/hyperledger/aries-framework-go/component/kmscrypto v0.0.0-20230622082138-3ffab1691857 - github.com/hyperledger/aries-framework-go/component/log v0.0.0-20230417184158-344a7f82c4c2 + github.com/hyperledger/aries-framework-go/component/log v0.0.0-20230427134832-0c9969493bd3 github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20230427134832-0c9969493bd3 github.com/hyperledger/aries-framework-go/spi v0.0.0-20230516135652-20c4d4beb991 github.com/kawamuray/jsonpath v0.0.0-20201211160320-7483bafabd7e @@ -30,6 +30,7 @@ require ( github.com/tidwall/sjson v1.1.4 github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/crypto v0.1.0 + golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 ) require ( @@ -40,19 +41,20 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/hyperledger/aries-framework-go v0.3.2 // indirect github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2 // indirect github.com/hyperledger/ursa-wrapper-go v0.3.1 // indirect github.com/kilic/bls12-381 v0.1.1-0.20210503002446-7b7597926c69 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect - github.com/mr-tron/base58 v1.1.3 // indirect - github.com/multiformats/go-base32 v0.0.3 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/teserakt-io/golang-ed25519 v0.0.0-20210104091850-3888c087a4c8 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect golang.org/x/sys v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/component/models/go.sum b/component/models/go.sum index 33071924f..993235955 100644 --- a/component/models/go.sum +++ b/component/models/go.sum @@ -49,17 +49,21 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hyperledger/aries-framework-go v0.3.2 h1:GsSUaSEW82cr5X8b3Qf90GAi37kmTKHqpPJLhar13X8= +github.com/hyperledger/aries-framework-go v0.3.2/go.mod h1:SorUysWEBw+uyXhY5RAtg2iyNkWTIIPM8+Slkt1Spno= github.com/hyperledger/aries-framework-go/component/kmscrypto v0.0.0-20230622082138-3ffab1691857 h1:x7Lt4FAPmMNyKQCUhnUOYeDpskaHnRNrtZj4rKfSKfU= github.com/hyperledger/aries-framework-go/component/kmscrypto v0.0.0-20230622082138-3ffab1691857/go.mod h1:xgNlHAVQjqwoknzHbXkeHkAJgUxRWKfHXPT3nhVhH3Q= github.com/hyperledger/aries-framework-go/component/log v0.0.0-20230417184158-344a7f82c4c2 h1:fKaNw6yi5PIXRDmEiOPNIErS6Mv92m03JcAE7wxj/Bk= github.com/hyperledger/aries-framework-go/component/log v0.0.0-20230417184158-344a7f82c4c2/go.mod h1:CvYs4l8X2NrrF93weLOu5RTOIJeVdoZITtjEflyuTyM= +github.com/hyperledger/aries-framework-go/component/log v0.0.0-20230427134832-0c9969493bd3 h1:x5qFQraTX86z9GCwF28IxfnPm6QH5YgHaX+4x97Jwvw= +github.com/hyperledger/aries-framework-go/component/log v0.0.0-20230427134832-0c9969493bd3/go.mod h1:CvYs4l8X2NrrF93weLOu5RTOIJeVdoZITtjEflyuTyM= github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20230427134832-0c9969493bd3 h1:JGYA9l5zTlvsvfnXT9hYPpCokAjmVKX0/r7njba7OX4= github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20230427134832-0c9969493bd3/go.mod h1:aSG2dWjYVzu2PVBtOqsYghaChA5+UUXnBbL+MfVceYQ= github.com/hyperledger/aries-framework-go/spi v0.0.0-20230516135652-20c4d4beb991 h1:1V0SW20i/MYefmu+O13/VNrzcO0fQ1FjTbGMFj2Bf5Y= @@ -86,8 +90,12 @@ github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqky github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= github.com/multiformats/go-multibase v0.1.1 h1:3ASCDsuLX8+j4kx58qnJ4YFq/JWTJpCyDW27ztsVTOI= @@ -127,6 +135,8 @@ github.com/tidwall/sjson v1.1.4 h1:bTSsPLdAYF5QNLSwYsKfBKKTnlGbIuhqL3CpRsjzGhg= github.com/tidwall/sjson v1.1.4/go.mod h1:wXpKXu8CtDjKAZ+3DrKY5ROCorDFahq8l0tey/Lx1fg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= @@ -138,6 +148,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -149,7 +161,6 @@ golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= diff --git a/component/models/jwt/jwt.go b/component/models/jwt/jwt.go index 54182c0d1..30e4e82df 100644 --- a/component/models/jwt/jwt.go +++ b/component/models/jwt/jwt.go @@ -23,6 +23,8 @@ import ( const ( // TypeJWT defines JWT type. TypeJWT = "JWT" + // TypeSDJWT defines SD-JWT type v5+. + TypeSDJWT = "SD-JWT" // AlgorithmNone used to indicate unsecured JWT. AlgorithmNone = "none" @@ -292,9 +294,10 @@ func checkTypHeader(typ interface{}) error { chunks := strings.Split(typStr, "+") if len(chunks) > 1 { + ending := strings.ToUpper(chunks[1]) // Explicit typing. // https://www.rfc-editor.org/rfc/rfc8725.html#name-use-explicit-typing - if strings.ToUpper(chunks[1]) != TypeJWT { + if ending != TypeJWT && ending != TypeSDJWT { return errors.New("invalid typ header") } diff --git a/component/models/presexch/definition_test.go b/component/models/presexch/definition_test.go index 212e7ca8a..7a6a51196 100644 --- a/component/models/presexch/definition_test.go +++ b/component/models/presexch/definition_test.go @@ -29,6 +29,9 @@ import ( "github.com/hyperledger/aries-framework-go/component/kmscrypto/kms/localkms" mockkms "github.com/hyperledger/aries-framework-go/component/kmscrypto/mock/kms" "github.com/hyperledger/aries-framework-go/component/kmscrypto/secretlock/noop" + "github.com/hyperledger/aries-framework-go/component/storageutil/mock/storage" + "github.com/hyperledger/aries-framework-go/spi/kms" + lddocloader "github.com/hyperledger/aries-framework-go/component/models/ld/documentloader" ldprocessor "github.com/hyperledger/aries-framework-go/component/models/ld/processor" ldtestutil "github.com/hyperledger/aries-framework-go/component/models/ld/testutil" @@ -39,8 +42,6 @@ import ( "github.com/hyperledger/aries-framework-go/component/models/signature/verifier" utiltime "github.com/hyperledger/aries-framework-go/component/models/util/time" "github.com/hyperledger/aries-framework-go/component/models/verifiable" - "github.com/hyperledger/aries-framework-go/component/storageutil/mock/storage" - "github.com/hyperledger/aries-framework-go/spi/kms" ) const errMsgSchema = "credentials do not satisfy requirements" @@ -2308,7 +2309,11 @@ func getTestVC() *verifiable.Credential { return getTestVCWithContext(nil) } -func newSdJwtVC(t *testing.T, vc *verifiable.Credential, signer sigutil.Signer) *verifiable.Credential { +func newSdJwtVC( + t *testing.T, + vc *verifiable.Credential, + signer sigutil.Signer, +) *verifiable.Credential { t.Helper() pubKey := signer.PublicKeyBytes() @@ -2323,7 +2328,8 @@ func newSdJwtVC(t *testing.T, vc *verifiable.Credential, signer sigutil.Signer) algName, err := jwsAlgo.Name() require.NoError(t, err) - combinedFormatForIssuance, err := vc.MakeSDJWT(verifiable.GetJWTSigner(signer, algName), verMethod) + combinedFormatForIssuance, err := vc.MakeSDJWT( + verifiable.GetJWTSigner(signer, algName), verMethod) require.NoError(t, err) parsed, err := verifiable.ParseCredential([]byte(combinedFormatForIssuance), diff --git a/component/models/sdjwt/common/common.go b/component/models/sdjwt/common/common.go index d080ef5ad..c4b266eb0 100644 --- a/component/models/sdjwt/common/common.go +++ b/component/models/sdjwt/common/common.go @@ -9,29 +9,70 @@ package common import ( "crypto" "encoding/base64" - "encoding/json" "fmt" "reflect" "strings" - - afgjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" - utils "github.com/hyperledger/aries-framework-go/component/models/util/maphelpers" ) // CombinedFormatSeparator is disclosure separator. const ( CombinedFormatSeparator = "~" - SDAlgorithmKey = "_sd_alg" - SDKey = "_sd" - CNFKey = "cnf" + SDAlgorithmKey = "_sd_alg" + SDKey = "_sd" + CNFKey = "cnf" + ArrayElementDigestKey = "..." +) + +// SDJWTVersion represents version SD-JWT according to spec version. +type SDJWTVersion int + +const ( + // SDJWTVersionDefault default SD-JWT version for compatibility purposes. + SDJWTVersionDefault = SDJWTVersionV2 + // SDJWTVersionV2 SD-JWT v2 spec. + SDJWTVersionV2 = SDJWTVersion(2) + // SDJWTVersionV5 SD-JWT v5 spec. + SDJWTVersionV5 = SDJWTVersion(5) +) + +const ( + disclosureElementsAmountForArrayDigest = 2 + disclosureElementsAmountForSDDigest = 3 - disclosureParts = 3 - saltIndex = 0 - nameIndex = 1 - valueIndex = 2 + saltPosition = 0 + arrayDigestValuePosition = 1 + sdDigestNamePosition = 1 + sdDigestValuePosition = 2 ) +// DisclosureClaimType disclosure claim type, used for sd-jwt v5+. +type DisclosureClaimType int + +const ( + // DisclosureClaimTypeUnknown default type for disclosure claim. + DisclosureClaimTypeUnknown = DisclosureClaimType(0) + // DisclosureClaimTypeArrayElement array element. + DisclosureClaimTypeArrayElement = DisclosureClaimType(2) + // DisclosureClaimTypeObject object. + DisclosureClaimTypeObject = DisclosureClaimType(3) + // DisclosureClaimTypePlainText object. + DisclosureClaimTypePlainText = DisclosureClaimType(3) +) + +// DisclosureClaim defines claim. +type DisclosureClaim struct { + Digest string + Disclosure string + Salt string + Elements int + Type DisclosureClaimType + Version SDJWTVersion + Name string + Value interface{} + IsValueParsed bool +} + // CombinedFormatForIssuance holds SD-JWT and disclosures. type CombinedFormatForIssuance struct { SDJWT string @@ -50,9 +91,13 @@ func (cf *CombinedFormatForIssuance) Serialize() string { // CombinedFormatForPresentation holds SD-JWT, disclosures and optional holder binding info. type CombinedFormatForPresentation struct { - SDJWT string - Disclosures []string - HolderBinding string + SDJWT string + Disclosures []string + + // Holder Verification JWT. + // For SD JWT V2 field contains Holder Binding JWT data. + // For SD JWT V5 field contains Key Binding JWT data. + HolderVerification string } // Serialize will assemble combined format for presentation. @@ -62,69 +107,43 @@ func (cf *CombinedFormatForPresentation) Serialize() string { presentation += CombinedFormatSeparator + disclosure } - if len(cf.Disclosures) > 0 || cf.HolderBinding != "" { + if len(cf.Disclosures) > 0 || cf.HolderVerification != "" { presentation += CombinedFormatSeparator } - presentation += cf.HolderBinding + presentation += cf.HolderVerification return presentation } -// 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) +func GetDisclosureClaims( + disclosures []string, + hash crypto.Hash, +) ([]*DisclosureClaim, error) { + disclosureClaims, err := getDisclosureClaims(disclosures, hash) if err != nil { - return nil, fmt.Errorf("failed to decode disclosure: %w", err) + return nil, err } - var disclosureArr []interface{} - - err = json.Unmarshal(decoded, &disclosureArr) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal disclosure array: %w", err) + recData := &recursiveData{ + disclosures: disclosureClaims, + cleanupDigestsClaims: true, } - if len(disclosureArr) != disclosureParts { - return nil, fmt.Errorf("disclosure array size[%d] must be %d", len(disclosureArr), disclosureParts) + for _, wrappedDisclosureClaim := range disclosureClaims { + if err = setDisclosureClaimValue(recData, wrappedDisclosureClaim); err != nil { + return nil, err + } } - salt, ok := disclosureArr[saltIndex].(string) - if !ok { - return nil, fmt.Errorf("disclosure salt type[%T] must be string", disclosureArr[saltIndex]) - } + final := make([]*DisclosureClaim, 0, len(disclosureClaims)) - name, ok := disclosureArr[nameIndex].(string) - if !ok { - return nil, fmt.Errorf("disclosure name type[%T] must be string", disclosureArr[nameIndex]) + for _, disclosureClaim := range recData.disclosures { + final = append(final, disclosureClaim) } - claim := &DisclosureClaim{Disclosure: disclosure, Salt: salt, Name: name, Value: disclosureArr[valueIndex]} - - return claim, nil + return final, nil } // ParseCombinedFormatForIssuance parses combined format for issuance into CombinedFormatForIssuance parts. @@ -157,7 +176,7 @@ func ParseCombinedFormatForPresentation(combinedFormatForPresentation string) *C sdJWT := parts[0] - return &CombinedFormatForPresentation{SDJWT: sdJWT, Disclosures: disclosures, HolderBinding: holderBinding} + return &CombinedFormatForPresentation{SDJWT: sdJWT, Disclosures: disclosures, HolderVerification: holderBinding} } // GetHash calculates hash of data using hash function identified by hash. @@ -177,62 +196,6 @@ 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 { - claims := utils.CopyMap(signedJWT.Payload) - - // check that the _sd_alg claim is present - // check that _sd_alg value is understood and the hash algorithm is deemed secure. - cryptoHash, err := GetCryptoHashFromClaims(claims) - if err != nil { - return err - } - - for _, disclosure := range disclosures { - digest, err := GetHash(cryptoHash, disclosure) - if err != nil { - return err - } - - found, err := isDigestInClaims(digest, claims) - if err != nil { - return err - } - - if !found { - return fmt.Errorf("disclosure digest '%s' not found in SD-JWT disclosure digests", digest) - } - } - - return nil -} - -func isDigestInClaims(digest string, claims map[string]interface{}) (bool, error) { - var found bool - - digests, err := GetDisclosureDigests(claims) - if err != nil { - return false, err - } - - for _, value := range claims { - if obj, ok := value.(map[string]interface{}); ok { - found, err = isDigestInClaims(digest, obj) - if err != nil { - return false, err - } - - if found { - return found, nil - } - } - } - - _, ok := digests[digest] - - return ok, nil -} - // GetCryptoHashFromClaims returns crypto hash from claims. func GetCryptoHashFromClaims(claims map[string]interface{}) (crypto.Hash, error) { var cryptoHash crypto.Hash @@ -293,6 +256,10 @@ func GetSDAlg(claims map[string]interface{}) (string, error) { // GetKeyFromVC returns key value from VC. func GetKeyFromVC(key string, claims map[string]interface{}) (interface{}, bool) { + if obj, ok := claims[key]; ok { + return obj, true + } + vcObj, ok := claims["vc"] if !ok { return nil, false @@ -329,106 +296,74 @@ func GetCNF(claims map[string]interface{}) (map[string]interface{}, error) { return cnf, nil } -// GetDisclosureDigests returns digests from claims map. +// GetDisclosureDigests returns digests from claims map considering +// either SDKey and array elements that are objects with one key, that key being ... and referring to a string. func GetDisclosureDigests(claims map[string]interface{}) (map[string]bool, error) { - disclosuresObj, ok := claims[SDKey] - if !ok { - return nil, nil + var ( + digests []string + err error + ) + + // Find all objects having an _sd key that refers to an array of strings. + digestsObj, exist := claims[SDKey] + if exist { + digests, err = stringArray(digestsObj) + if err != nil { + return nil, fmt.Errorf("get disclosure digests: %w", err) + } } - disclosures, err := stringArray(disclosuresObj) - if err != nil { - return nil, fmt.Errorf("get disclosure digests: %w", err) + // Find all array elements that are objects with one key, that key being ... and referring to a string. + for _, v := range claims { + switch t := v.(type) { + case []interface{}: + for _, vv := range t { + valueMapped, ok := vv.(map[string]interface{}) + if !ok { + continue + } + + if digestIface, ok := valueMapped[ArrayElementDigestKey]; ok && len(valueMapped) == 1 { + if digest, ok := digestIface.(string); ok { + digests = append(digests, digest) + } + } + } + } } - return SliceToMap(disclosures), nil + return SliceToMap(digests), nil } // GetDisclosedClaims returns disclosed claims only. func GetDisclosedClaims(disclosureClaims []*DisclosureClaim, claims map[string]interface{}) (map[string]interface{}, error) { // nolint:lll - hash, err := GetCryptoHashFromClaims(claims) + _, err := GetCryptoHashFromClaims(claims) if err != nil { return nil, fmt.Errorf("failed to get crypto hash from claims: %w", err) } - output := utils.CopyMap(claims) - includedDigests := make(map[string]bool) - - err = processDisclosedClaims(disclosureClaims, output, includedDigests, hash) - if err != nil { - return nil, fmt.Errorf("failed to process disclosed claims: %w", err) - } - - return output, nil -} + disclosureClaimsMap := make(map[string]*DisclosureClaim, len(disclosureClaims)) -func processDisclosedClaims(disclosureClaims []*DisclosureClaim, claims map[string]interface{}, includedDigests map[string]bool, hash crypto.Hash) error { // nolint:lll - digests, err := GetDisclosureDigests(claims) - if err != nil { - return err + for _, d := range disclosureClaims { + disclosureClaimsMap[d.Digest] = d } - for key, value := range claims { - if obj, ok := value.(map[string]interface{}); ok { - err := processDisclosedClaims(disclosureClaims, obj, includedDigests, hash) - if err != nil { - return err - } - - claims[key] = obj - } + recData := &recursiveData{ + disclosures: disclosureClaimsMap, + cleanupDigestsClaims: true, } - for _, dc := range disclosureClaims { - digest, err := GetHash(hash, dc.Disclosure) - if err != nil { - return err - } - - if _, ok := digests[digest]; !ok { - continue - } - - _, digestAlreadyIncluded := includedDigests[digest] - if digestAlreadyIncluded { - // If there is more than one place where the digest is included, - // the Verifier MUST reject the Presentation. - return fmt.Errorf("digest '%s' has been included in more than one place", digest) - } - - err = validateClaim(dc, claims) - if err != nil { - return err - } - - claims[dc.Name] = dc.Value - - includedDigests[digest] = true - } - - delete(claims, SDKey) - delete(claims, SDAlgorithmKey) - - return nil -} - -func validateClaim(dc *DisclosureClaim, claims map[string]interface{}) error { - _, claimNameExists := claims[dc.Name] - if claimNameExists { - // If the claim name already exists at the same level, the Verifier MUST reject the Presentation. - return fmt.Errorf("claim name '%s' already exists at the same level", dc.Name) + output, err := discloseClaimValue(claims, recData) + if err != nil { + return nil, fmt.Errorf("failed to process disclosed claims: %w", err) } - m, ok := getMap(dc.Value) - if ok { - if KeyExistsInMap(SDKey, m) { - // If the claim value contains an object with an _sd key (at the top level or nested deeper), - // the Verifier MUST reject the Presentation. - return fmt.Errorf("claim value contains an object with an '%s' key", SDKey) - } + outputMapped, ok := output.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("unexpected output type") } - return nil + return outputMapped, nil } func getMap(value interface{}) (map[string]interface{}, bool) { diff --git a/component/models/sdjwt/common/common_test.go b/component/models/sdjwt/common/common_test.go index a1eb985cf..74e917486 100644 --- a/component/models/sdjwt/common/common_test.go +++ b/component/models/sdjwt/common/common_test.go @@ -9,16 +9,17 @@ package common import ( "bytes" "crypto" - "crypto/ed25519" - "crypto/rand" + _ "embed" "encoding/base64" "encoding/json" "fmt" + "reflect" "testing" "github.com/stretchr/testify/require" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + afjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" utils "github.com/hyperledger/aries-framework-go/component/models/util/maphelpers" ) @@ -29,6 +30,12 @@ const ( testAlg = "sha-256" ) +//go:embed testdata/full_disclosures_v5.json +var fullDisclosuresV5TestData []byte + +//go:embed testdata/array_element_and_one_missing_v5.json +var arrayElementAndOneMissingV5TestData []byte + func TestGetHash(t *testing.T) { t.Run("success", func(t *testing.T) { digest, err := GetHash(defaultHash, "WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0") @@ -76,7 +83,7 @@ func TestParseCombinedFormatForPresentation(t *testing.T) { cfp := ParseCombinedFormatForPresentation(testCombinedFormatForPresentation) require.Equal(t, testSDJWT, cfp.SDJWT) require.Equal(t, 1, len(cfp.Disclosures)) - require.Empty(t, cfp.HolderBinding) + require.Empty(t, cfp.HolderVerification) require.Equal(t, testCombinedFormatForPresentation, cfp.Serialize()) }) @@ -84,7 +91,7 @@ func TestParseCombinedFormatForPresentation(t *testing.T) { t.Run("success - spec example", func(t *testing.T) { cfp := ParseCombinedFormatForPresentation(specCombinedFormatForIssuance + CombinedFormatSeparator) require.Equal(t, 7, len(cfp.Disclosures)) - require.Empty(t, cfp.HolderBinding) + require.Empty(t, cfp.HolderVerification) require.Equal(t, specCombinedFormatForIssuance+CombinedFormatSeparator, cfp.Serialize()) }) @@ -94,7 +101,7 @@ func TestParseCombinedFormatForPresentation(t *testing.T) { cfp := ParseCombinedFormatForPresentation(testCFI) require.Equal(t, testSDJWT, cfp.SDJWT) require.Equal(t, 1, len(cfp.Disclosures)) - require.Equal(t, testHolderBinding, cfp.HolderBinding) + require.Equal(t, testHolderBinding, cfp.HolderVerification) require.Equal(t, testCFI, cfp.Serialize()) }) @@ -103,7 +110,7 @@ func TestParseCombinedFormatForPresentation(t *testing.T) { cfp := ParseCombinedFormatForPresentation(testSDJWT) require.Equal(t, testSDJWT, cfp.SDJWT) require.Equal(t, 0, len(cfp.Disclosures)) - require.Empty(t, cfp.HolderBinding) + require.Empty(t, cfp.HolderVerification) require.Equal(t, testSDJWT, cfp.Serialize()) }) @@ -114,7 +121,7 @@ func TestParseCombinedFormatForPresentation(t *testing.T) { cfp := ParseCombinedFormatForPresentation(testCFI) require.Equal(t, testSDJWT, cfp.SDJWT) require.Equal(t, 0, len(cfp.Disclosures)) - require.Equal(t, testHolderBinding, cfp.HolderBinding) + require.Equal(t, testHolderBinding, cfp.HolderVerification) require.Equal(t, testCFI, cfp.Serialize()) }) @@ -125,199 +132,138 @@ func TestParseCombinedFormatForPresentation(t *testing.T) { cfp := ParseCombinedFormatForPresentation(specExample2bPresentation) require.Equal(t, specExample2bJWT, cfp.SDJWT) require.Equal(t, 6, len(cfp.Disclosures)) - require.Empty(t, cfp.HolderBinding) + require.Empty(t, cfp.HolderVerification) require.Equal(t, specExample2bPresentation, cfp.Serialize()) }) } -func TestVerifyDisclosuresInSDJWT(t *testing.T) { +func TestGetDisclosureClaims(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 := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) require.Equal(t, 1, len(sdJWT.Disclosures)) - signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) - require.NoError(t, err) - - err = VerifyDisclosuresInSDJWT(sdJWT.Disclosures, signedJWT) + token, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) r.NoError(err) - }) - - t.Run("success - complex struct(spec example 2b)", func(t *testing.T) { - specExample2bPresentation := fmt.Sprintf("%s%s", specExample2bJWT, specExample2bDisclosures) - - sdJWT := ParseCombinedFormatForPresentation(specExample2bPresentation) - signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) - require.NoError(t, err) - - err = VerifyDisclosuresInSDJWT(sdJWT.Disclosures, signedJWT) + hash, err := GetCryptoHashFromClaims(token.Payload) r.NoError(err) - }) - t.Run("success - no selective disclosures(valid case)", func(t *testing.T) { - jwtPayload := &payload{ - Issuer: "issuer", - SDAlg: "sha-256", - } - - signedJWT, err := afjwt.NewSigned(jwtPayload, nil, signer) + disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures, hash) r.NoError(err) + r.Len(disclosureClaims, 1) - err = VerifyDisclosuresInSDJWT(nil, signedJWT) - r.NoError(err) + r.Equal("given_name", disclosureClaims[0].Name) + r.Equal("John", disclosureClaims[0].Value) }) - t.Run("success - selective disclosures nil", func(t *testing.T) { - payload := make(map[string]interface{}) - payload[SDAlgorithmKey] = testAlg - payload[SDKey] = nil + t.Run("full disclosures V5", func(t *testing.T) { + var disData []string + r.NoError(json.Unmarshal(fullDisclosuresV5TestData, &disData)) - signedJWT, err := afjwt.NewSigned(payload, nil, signer) + parsed, err := GetDisclosureClaims(disData, crypto.SHA256) r.NoError(err) + r.Len(parsed, 10) - err = VerifyDisclosuresInSDJWT(nil, signedJWT) - r.NoError(err) - }) - - t.Run("error - disclosure not present in SD-JWT", func(t *testing.T) { - sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) - require.Equal(t, 1, len(sdJWT.Disclosures)) - - signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, 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) { - jwtPayload := &payload{ - Issuer: "issuer", - SDAlg: testAlg, + var address *DisclosureClaim + for _, cl := range parsed { + if cl.Name == "address" { + address = cl + break + } } - signedJWT, err := afjwt.NewSigned(jwtPayload, 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") + r.Equal(map[string]interface{}{ + "extraArrInclude": []interface{}{ + "UA", "PL", + }, + "extra": map[string]interface{}{ + "recursive": map[string]interface{}{ + "key1": "value1", + }, + }, + "region": "Sachsen-Anhalt", + "country": "DE", + "street_address": "Schulstr. 12", + "locality": "Schulpforta", + "extraArr": []interface{}{ + "Extra1", "Extra2", + }, + }, address.Value) }) - t.Run("error - missing algorithm", func(t *testing.T) { - jwtPayload := &payload{ - Issuer: "issuer", - } - - signedJWT, err := afjwt.NewSigned(jwtPayload, nil, signer) + t.Run("array element and one value missing V5", func(t *testing.T) { + // - "WyJ5WElBaTZSb1Y1eDV2X3lsVm1wXzhBIiwibG9jYWxpdHkiLCJTY2h1bHBmb3J0YSJd" locality + // - "WyJURWtwSjJkYWxraGltUUVLd25Cblp3IiwiVUEiXQ", UA + var disData []string + r.NoError(json.Unmarshal(arrayElementAndOneMissingV5TestData, &disData)) + parsed, err := GetDisclosureClaims(disData, crypto.SHA256) r.NoError(err) + r.Len(parsed, 8) - err = VerifyDisclosuresInSDJWT(nil, signedJWT) - r.Error(err) - r.Contains(err.Error(), "_sd_alg must be present in SD-JWT", SDAlgorithmKey) - }) - - t.Run("error - invalid algorithm", func(t *testing.T) { - jwtPayload := payload{ - Issuer: "issuer", - SDAlg: "SHA-XXX", + var address *DisclosureClaim + for _, cl := range parsed { + if cl.Name == "address" { + address = cl + break + } } - signedJWT, err := afjwt.NewSigned(jwtPayload, 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[SDAlgorithmKey] = 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[SDAlgorithmKey] = testAlg - payload[SDKey] = "test" - - signedJWT, err := afjwt.NewSigned(payload, nil, signer) - r.NoError(err) - - err = VerifyDisclosuresInSDJWT([]string{additionalDisclosure}, 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[SDAlgorithmKey] = testAlg - payload[SDKey] = []float64{123} - - signedJWT, err := afjwt.NewSigned(payload, nil, signer) - r.NoError(err) - - err = VerifyDisclosuresInSDJWT([]string{additionalDisclosure}, signedJWT) - r.Error(err) - r.Contains(err.Error(), "get disclosure digests: entry item type[float64] is not a string") + r.Equal(map[string]interface{}{ + "extraArrInclude": []interface{}{ + "PL", + }, + "extra": map[string]interface{}{ + "recursive": map[string]interface{}{ + "key1": "value1", + }, + }, + "region": "Sachsen-Anhalt", + "country": "DE", + "street_address": "Schulstr. 12", + "extraArr": []interface{}{ + "Extra1", "Extra2", + }, + }, address.Value) }) -} -func TestGetDisclosureClaims(t *testing.T) { - r := require.New(t) - - t.Run("success", func(t *testing.T) { - sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) + t.Run("error - invalid disclosure format (not encoded)", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testSDJWT + "~xyz") require.Equal(t, 1, len(sdJWT.Disclosures)) - disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) + token, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) 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 := ParseCombinedFormatForIssuance("jws~xyz") - require.Equal(t, 1, len(sdJWT.Disclosures)) + hash, err := GetCryptoHashFromClaims(token.Payload) + r.NoError(err) - disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) + disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures, hash) 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"} + t.Run("error - invalid disclosure array (less then 2 parts)", func(t *testing.T) { + disclosureArr := []interface{}{"name"} disclosureJSON, err := json.Marshal(disclosureArr) require.NoError(t, err) - sdJWT := ParseCombinedFormatForIssuance(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON))) + sdJWT := ParseCombinedFormatForIssuance(fmt.Sprintf("%s~%s", testSDJWT, + base64.RawURLEncoding.EncodeToString(disclosureJSON))) require.Equal(t, 1, len(sdJWT.Disclosures)) - disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) + token, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + r.NoError(err) + + hash, err := GetCryptoHashFromClaims(token.Payload) + r.NoError(err) + + disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures, hash) r.Error(err) r.Nil(disclosureClaims) - r.Contains(err.Error(), "disclosure array size[2] must be 3") + r.Contains(err.Error(), "disclosure array size[1] must be greater 2") }) t.Run("error - invalid disclosure array (name is not a string)", func(t *testing.T) { @@ -325,10 +271,17 @@ func TestGetDisclosureClaims(t *testing.T) { disclosureJSON, err := json.Marshal(disclosureArr) require.NoError(t, err) - sdJWT := ParseCombinedFormatForIssuance(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON))) + sdJWT := ParseCombinedFormatForIssuance(fmt.Sprintf("%s~%s", testSDJWT, + base64.RawURLEncoding.EncodeToString(disclosureJSON))) require.Equal(t, 1, len(sdJWT.Disclosures)) - disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) + token, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + r.NoError(err) + + hash, err := GetCryptoHashFromClaims(token.Payload) + r.NoError(err) + + disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures, hash) r.Error(err) r.Nil(disclosureClaims) r.Contains(err.Error(), "disclosure name type[float64] must be string") @@ -342,10 +295,13 @@ func TestGetDisclosedClaims(t *testing.T) { r.Equal(testSDJWT, cfi.SDJWT) r.Equal(1, len(cfi.Disclosures)) - disclosureClaims, err := GetDisclosureClaims(cfi.Disclosures) + token, _, err := afjwt.Parse(cfi.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) r.NoError(err) - token, _, err := afjwt.Parse(cfi.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + hash, err := GetCryptoHashFromClaims(token.Payload) + r.NoError(err) + + disclosureClaims, err := GetDisclosureClaims(cfi.Disclosures, hash) r.NoError(err) var claims map[string]interface{} @@ -365,10 +321,88 @@ func TestGetDisclosedClaims(t *testing.T) { r.Equal("John", disclosedClaims["given_name"]) }) + t.Run("success V5", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuanceV5) + require.Equal(t, 6, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + disclosureClaimsV5, err := GetDisclosureClaims(sdJWT.Disclosures, crypto.SHA256) + require.NoError(t, err) + + disclosedClaims, err := GetDisclosedClaims(disclosureClaimsV5, signedJWT.Payload) + r.NoError(err) + r.NotNil(disclosedClaims) + + r.Equal(2, len(disclosedClaims)) + r.Equal("https://example.com/issuer", disclosedClaims["iss"]) + r.Equal(map[string]interface{}{ + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "cities": []interface{}{"Albuquerque", "El Paso"}, + "countryCodes": []interface{}{"UA", "PL"}, + "extra": map[string]interface{}{ + "recursive": map[string]interface{}{ + "key1": "value1", + }, + }, + }, disclosedClaims["address"]) + }) + + t.Run("success V5 not all disclosures provided", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuanceV5) + require.Equal(t, 6, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + disclosureClaimsV5, err := GetDisclosureClaims(sdJWT.Disclosures, crypto.SHA256) + require.NoError(t, err) + + var disclosuresLimitedList []*DisclosureClaim + for _, d := range disclosureClaimsV5 { + // Remove UA array element + if v, ok := d.Value.(string); ok && v == "UA" { + continue + } + // Remove PL array element + if v, ok := d.Value.(string); ok && v == "PL" { + continue + } + // Remove Albuquerque array element + if v, ok := d.Value.(string); ok && v == "Albuquerque" { + continue + } + // Remove one sd element + if v, ok := d.Value.(string); ok && v == "Schulpforta" { + continue + } + + disclosuresLimitedList = append(disclosuresLimitedList, d) + } + + disclosedClaims, err := GetDisclosedClaims(disclosuresLimitedList, signedJWT.Payload) + r.NoError(err) + r.NotNil(disclosedClaims) + + r.Equal(2, len(disclosedClaims)) + r.Equal("https://example.com/issuer", disclosedClaims["iss"]) + r.Equal(map[string]interface{}{ + "region": "Sachsen-Anhalt", + "cities": []interface{}{"El Paso"}, + "extra": map[string]interface{}{ + "recursive": map[string]interface{}{ + "key1": "value1", + }, + }, + }, disclosedClaims["address"]) + }) + t.Run("success - with complex object", func(t *testing.T) { testClaims := utils.CopyMap(claims) - additionalDigest, err := GetHash(crypto.SHA256, additionalDisclosure) + additionalDigest, err := GetHash(crypto.SHA256, additionalSDDisclosure) r.NoError(err) parentObj := make(map[string]interface{}) @@ -381,9 +415,16 @@ func TestGetDisclosedClaims(t *testing.T) { disclosedClaims, err := GetDisclosedClaims(append(disclosureClaims, &DisclosureClaim{ - Disclosure: additionalDisclosure, - Name: "key-x", - Value: "value-y"}), + Digest: additionalDigest, + Disclosure: additionalSDDisclosure, + Salt: "", + Elements: disclosureElementsAmountForSDDigest, + Type: DisclosureClaimTypePlainText, + Version: SDJWTVersionV2, + Name: "key-x", + Value: "value-y", + IsValueParsed: false, + }), testClaims) r.NoError(err) r.NotNil(disclosedClaims) @@ -395,10 +436,10 @@ func TestGetDisclosedClaims(t *testing.T) { r.Equal("value-y", disclosedClaims["father"].(map[string]interface{})["key-x"]) }) - t.Run("error - claim value contains _sd", func(t *testing.T) { + t.Run("success - claim value contains _sd", func(t *testing.T) { testClaims := utils.CopyMap(claims) - additionalDigest, err := GetHash(crypto.SHA256, additionalDisclosure) + additionalDigest, err := GetHash(crypto.SHA256, additionalSDDisclosure) r.NoError(err) parentObj := make(map[string]interface{}) @@ -409,16 +450,21 @@ func TestGetDisclosedClaims(t *testing.T) { disclosedClaims, err := GetDisclosedClaims(append(disclosureClaims, &DisclosureClaim{ - Disclosure: additionalDisclosure, + Digest: additionalDigest, + Disclosure: additionalSDDisclosure, + Salt: "", + Elements: disclosureElementsAmountForSDDigest, + Type: DisclosureClaimTypeObject, + Version: SDJWTVersionV2, Name: "key-x", Value: map[string]interface{}{ "_sd": []interface{}{"test-digest"}, }, + IsValueParsed: false, }), testClaims) - r.Error(err) - r.Nil(disclosedClaims) - r.Contains(err.Error(), "failed to process disclosed claims: claim value contains an object with an '_sd' key") + r.NoError(err) + r.NotNil(disclosedClaims) }) t.Run("error - same claim key at the same level ", func(t *testing.T) { @@ -429,6 +475,7 @@ func TestGetDisclosedClaims(t *testing.T) { parentObj[SDKey] = claims[SDKey] testClaims["father"] = parentObj + delete(testClaims, SDKey) printObject(t, "Complex Claims", testClaims) @@ -508,18 +555,6 @@ func TestGetDisclosedClaims(t *testing.T) { r.Contains(err.Error(), "failed to process disclosed claims: get disclosure digests: entry type[string] is not an array") }) - - t.Run("error - get hash fails", func(t *testing.T) { - testClaims := make(map[string]interface{}) - testClaims[SDAlgorithmKey] = "sha-256" - testClaims[SDKey] = []interface{}{"abc"} - - err := processDisclosedClaims(disclosureClaims, testClaims, make(map[string]bool), 0) - r.Error(err) - - r.Contains(err.Error(), - "hash function not available for: 0") - }) } func TestGetCryptoHash(t *testing.T) { @@ -721,6 +756,237 @@ func TestKeyExistInMap(t *testing.T) { }) } +func TestGetKeyFromVC(t *testing.T) { + type args struct { + key string + claims map[string]interface{} + } + + tests := []struct { + name string + args args + want interface{} + want1 bool + }{ + { + name: "success - vc root claim does not exist", + args: args{ + key: "credentialSubject", + claims: map[string]interface{}{ + "credentialSubject": 123, + }, + }, + want: 123, + want1: true, + }, + { + name: "success - vc root claim exist", + args: args{ + key: "credentialSubject", + claims: map[string]interface{}{ + "vc": map[string]interface{}{ + "credentialSubject": 321, + }, + }, + }, + want: 321, + want1: true, + }, + { + name: "error - vc root claim does not exist", + args: args{ + key: "credentialSubject", + claims: map[string]interface{}{ + "some": map[string]interface{}{ + "credentialSubject": 321, + }, + }, + }, + want: nil, + want1: false, + }, + { + name: "error - vc root claim exist but not a map", + args: args{ + key: "credentialSubject", + claims: map[string]interface{}{ + "vc": 123, + }, + }, + want: nil, + want1: false, + }, + { + name: "error - vc root claim exist but key does not exist in nested map", + args: args{ + key: "credentialSubject", + claims: map[string]interface{}{ + "vc": map[string]interface{}{ + "some": 321, + }, + }, + }, + want: nil, + want1: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := GetKeyFromVC(tt.args.key, tt.args.claims) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetKeyFromVC() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("GetKeyFromVC() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestGetDisclosureDigests(t *testing.T) { + type args struct { + claims map[string]interface{} + } + + tests := []struct { + name string + args args + want map[string]bool + wantErr bool + }{ + { + name: "success - sd and array elements", + args: args{ + claims: map[string]interface{}{ + SDKey: []string{ + "digest1", "digest2", + }, + "claim1": []interface{}{ + map[string]interface{}{ + ArrayElementDigestKey: "digest3", + }, + }, + }, + }, + want: map[string]bool{ + "digest1": true, + "digest2": true, + "digest3": true, + }, + wantErr: false, + }, + { + name: "success - sd root and nested claims", + args: args{ + claims: map[string]interface{}{ + SDKey: []string{ + "digest1", "digest2", + }, + "claim1": map[string]interface{}{ + SDKey: []string{ + "digest3", + }, + }, + }, + }, + want: map[string]bool{ + "digest1": true, + "digest2": true, + }, + wantErr: false, + }, + { + name: "success - array element on nested level", + args: args{ + claims: map[string]interface{}{ + "claim1": map[string]interface{}{ + "claim2": map[string]interface{}{ + ArrayElementDigestKey: "digest3", + }, + }, + }, + }, + want: map[string]bool{}, + wantErr: false, + }, + { + name: "success - array element not a string", + args: args{ + claims: map[string]interface{}{ + "claim1": []interface{}{ + map[string]interface{}{ + ArrayElementDigestKey: 123, + }, + }, + }, + }, + want: map[string]bool{}, + wantErr: false, + }, + { + name: "success - array element map longer then one", + args: args{ + claims: map[string]interface{}{ + "claim1": []interface{}{ + map[string]interface{}{ + ArrayElementDigestKey: "digest3", + "claim2": "digest4", + }, + }, + }, + }, + want: map[string]bool{}, + wantErr: false, + }, + { + name: "success - array element is not a map", + args: args{ + claims: map[string]interface{}{ + "claim1": []interface{}{"digest3"}, + }, + }, + want: map[string]bool{}, + wantErr: false, + }, + { + name: "success - no array and sd elements", + args: args{ + claims: map[string]interface{}{ + "claim1": map[string]interface{}{ + "claim2": []interface{}{"claim3"}, + }, + }, + }, + want: map[string]bool{}, + wantErr: false, + }, + { + name: "error - sd element is not a string", + args: args{ + claims: map[string]interface{}{ + SDKey: []int{123}, + }, + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetDisclosureDigests(tt.args.claims) + if (err != nil) != tt.wantErr { + t.Errorf("GetDisclosureDigests() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetDisclosureDigests() got = %v, want %v", got, tt.want) + } + }) + } +} + func printObject(t *testing.T, name string, obj interface{}) { t.Helper() @@ -752,10 +1018,11 @@ func (sv *NoopSignatureVerifier) Verify(joseHeaders jose.Headers, payload, signi return nil } -const additionalDisclosure = `WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0` +const additionalSDDisclosure = `WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0` +const additionalArrayElementDisclosure = `WyJjc3AteWZLWWNTYWlkUElUMHpyOFNRIiwiTWluYXMgVGlyaXRoIl0` -// nolint: lll -const testCombinedFormatForIssuance = `eyJhbGciOiJFZERTQSJ9.eyJfc2QiOlsicXF2Y3FuY3pBTWdZeDdFeWtJNnd3dHNweXZ5dks3OTBnZTdNQmJRLU51cyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTcwMzAyMzg1NSwiaWF0IjoxNjcxNDg3ODU1LCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsIm5iZiI6MTY3MTQ4Nzg1NX0.vscuzfwcHGi04pWtJCadc4iDELug6NH6YK-qxhY1qacsciIHuoLELAfon1tGamHtuu8TSs6OjtLk3lHE16jqAQ~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd` +const testCombinedFormatForIssuance = `eyJhbGciOiJFZERTQSJ9.eyJfc2QiOlsicXF2Y3FuY3pBTWdZeDdFeWtJNnd3dHNweXZ5dks3OTBnZTdNQmJRLU51cyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTcwMzAyMzg1NSwiaWF0IjoxNjcxNDg3ODU1LCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsIm5iZiI6MTY3MTQ4Nzg1NX0.vscuzfwcHGi04pWtJCadc4iDELug6NH6YK-qxhY1qacsciIHuoLELAfon1tGamHtuu8TSs6OjtLk3lHE16jqAQ~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd` // nolint: lll +const testCombinedFormatForIssuanceV5 = `eyJhbGciOiJFZERTQSJ9.eyJfc2RfYWxnIjoic2hhLTI1NiIsImFkZHJlc3MiOnsiX3NkIjpbIlRaV0JRdlpTam1VemxRZ1AzZ2EydkFlYlV6cDhpU2NRNlBFT0gzSHQ1bm8iXSwiY2l0aWVzIjpbeyIuLi4iOiI0U1lCT3NMcVRURU42QnpTSV9NX0pyQ0NzWFJ0Y1BTbWNqV3ROMEdjU0dJIn0sIkVsIFBhc28iXSwiY291bnRyeUNvZGVzIjpbeyIuLi4iOiJab2hsNGd4OXd0czJBRlVrbmd1c3FleWJDUERWUVFLNHNPR3A4dWZHcWg4In0seyIuLi4iOiIxbVl4V1VZN2M5T1pEWlZnd0N6aUFuWkY1TDgzUzZaN2pGb1U2ck5vaEtzIn1dLCJleHRyYSI6eyJfc2QiOlsibXZtZVoxb3ZmY1RRTi01Q3A5YlhYcElKREd2THVkNVg4SVIyajctVUd0WSJdfSwicmVnaW9uIjoiU2FjaHNlbi1BbmhhbHQifSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIifQ.l-xc_9hGQMHfkPmMeG_EQIZU5guVme9FSKgN58WqfBJcMvfrb9rTc2PHmxveerMTA2cjgJzM2OZgibQCxRePAg~WyJTUEh2T185NEsyWENVdVhSeURjcHJnIiwibG9jYWxpdHkiLCJTY2h1bHBmb3J0YSJd~WyJSaHh1bDBnd2x6cTlSNDg4ZV8tQ3B3IiwiVUEiXQ~WyJKQWlwWm5uSUM3ejAtZzJoNzZmc0FBIiwiUEwiXQ~WyIxdzZVNkRkSG9laFdUdG5UNG5iS3RnIiwiQWxidXF1ZXJxdWUiXQ~WyJVWnUxcjR5YnpfUGNiU3BRcTFpMllRIiwicmVjdXJzaXZlIix7Il9zZCI6WyJydjZNejBheXJZYWU1MHpWRXYtbExKNFZRRzhNMGFJdjJOVW1LVDRRRjVJIl19XQ~WyJoRWNiQmxZQ0ZSVGVtMG1uVXQzTVNnIiwia2V5MSIsInZhbHVlMSJd` // nolint: lll // nolint: lll const testSDJWT = `eyJhbGciOiJFZERTQSJ9.eyJfc2QiOlsicXF2Y3FuY3pBTWdZeDdFeWtJNnd3dHNweXZ5dks3OTBnZTdNQmJRLU51cyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTcwMzAyMzg1NSwiaWF0IjoxNjcxNDg3ODU1LCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsIm5iZiI6MTY3MTQ4Nzg1NX0.vscuzfwcHGi04pWtJCadc4iDELug6NH6YK-qxhY1qacsciIHuoLELAfon1tGamHtuu8TSs6OjtLk3lHE16jqAQ` diff --git a/component/models/sdjwt/common/testdata/array_element_and_one_missing_v5.json b/component/models/sdjwt/common/testdata/array_element_and_one_missing_v5.json new file mode 100644 index 000000000..3fe0f1ff1 --- /dev/null +++ b/component/models/sdjwt/common/testdata/array_element_and_one_missing_v5.json @@ -0,0 +1,10 @@ +[ + "WyI2NDYwRkU1STJvN0l0bktGX2s4YWZ3IiwiYWRkcmVzcyIseyJfc2QiOlsiQjJBQVFzTVk4V1B1bWZoOWtXY3J1RXV4TUVaT2J5bW5MNGU2OVB5U0psNCIsIkV3TGVJbVFVdWIyS1F6eURoNmxnU1c1TnZsMExqQWFlaXFCZHJzeDY1T28iLCI4TG56SUJ5QlNDZ243SG1zcURUbW1GeXZSVTRRdHRuaVNlZE4xanN2LXhvIiwibk54dThkU3laV0x3QVAteTJtV0k5aXRpMmRHejQ5RUM0eDY2RGxDZ0QwZyJdLCJleHRyYSI6eyJfc2QiOlsib2RqUjRraHQxcGVNZFRUMzVFMUotWXF5Q0gyM0dfSGhrQXRLZks0cUpIVSJdfSwiZXh0cmFBcnJJbmNsdWRlIjpbeyIuLi4iOiIxRVN5RGlLTE9KbEF2VnYtQnJaN1JQTU4zRXVOdU1Jc014aXVMeGhZWjg0In0sIlBMIl0sInJlZ2lvbiI6IlNhY2hzZW4tQW5oYWx0In1d", + "WyJZbEFCTmZkaXhKSVF4Z3hNZ0RTcUpRIiwiY291bnRyeSIsIkRFIl0", + "WyJJdGZ2ellZdXlMSTF3TGZLU213M0dRIiwiZXh0cmFBcnIiLFt7Ii4uLiI6IkRsVHpXT0tWbzJNbzNPNUZER0hpWGhuSnd4c2hBTkQyMUVibmQyWFRiT0kifSx7Ii4uLiI6IkMwbUl4SXdEQm4xZ1IxamZBTDFYZVJNdGlYLVdDMVBjc3FUVkphdnJMQTQifV1d", + "WyJkWW10LWNjcWMyeUJYd1ZGTFZkeFdnIiwic3RyZWV0X2FkZHJlc3MiLCJTY2h1bHN0ci4gMTIiXQ", + "WyIxRTlRZnRDS3YtbTFjN0VFOXlXMmh3IiwiRXh0cmExIl0", + "WyI0Mjl4ejFGeTlEdU9SQ0R2cHd3bzFBIiwiRXh0cmEyIl0", + "WyJvVy1oMDZYVUNsTU45YTBVV3VHMGhBIiwicmVjdXJzaXZlIix7Il9zZCI6WyJoX2h1bVhsYjhVekM5T0tGOHc3SEd6ZmYzSGgzMmh3SGR6Vms5WS1oOGR3Il19XQ", + "WyItdHBPTUlPS3dnOUNtNlBRVTlDTktBIiwia2V5MSIsInZhbHVlMSJd" +] \ No newline at end of file diff --git a/component/models/sdjwt/common/testdata/full_disclosures_v5.json b/component/models/sdjwt/common/testdata/full_disclosures_v5.json new file mode 100644 index 000000000..3ad05f6ba --- /dev/null +++ b/component/models/sdjwt/common/testdata/full_disclosures_v5.json @@ -0,0 +1,12 @@ +[ + "WyI2NDYwRkU1STJvN0l0bktGX2s4YWZ3IiwiYWRkcmVzcyIseyJfc2QiOlsiQjJBQVFzTVk4V1B1bWZoOWtXY3J1RXV4TUVaT2J5bW5MNGU2OVB5U0psNCIsIkV3TGVJbVFVdWIyS1F6eURoNmxnU1c1TnZsMExqQWFlaXFCZHJzeDY1T28iLCI4TG56SUJ5QlNDZ243SG1zcURUbW1GeXZSVTRRdHRuaVNlZE4xanN2LXhvIiwibk54dThkU3laV0x3QVAteTJtV0k5aXRpMmRHejQ5RUM0eDY2RGxDZ0QwZyJdLCJleHRyYSI6eyJfc2QiOlsib2RqUjRraHQxcGVNZFRUMzVFMUotWXF5Q0gyM0dfSGhrQXRLZks0cUpIVSJdfSwiZXh0cmFBcnJJbmNsdWRlIjpbeyIuLi4iOiIxRVN5RGlLTE9KbEF2VnYtQnJaN1JQTU4zRXVOdU1Jc014aXVMeGhZWjg0In0sIlBMIl0sInJlZ2lvbiI6IlNhY2hzZW4tQW5oYWx0In1d", + "WyJZbEFCTmZkaXhKSVF4Z3hNZ0RTcUpRIiwiY291bnRyeSIsIkRFIl0", + "WyJJdGZ2ellZdXlMSTF3TGZLU213M0dRIiwiZXh0cmFBcnIiLFt7Ii4uLiI6IkRsVHpXT0tWbzJNbzNPNUZER0hpWGhuSnd4c2hBTkQyMUVibmQyWFRiT0kifSx7Ii4uLiI6IkMwbUl4SXdEQm4xZ1IxamZBTDFYZVJNdGlYLVdDMVBjc3FUVkphdnJMQTQifV1d", + "WyJkWW10LWNjcWMyeUJYd1ZGTFZkeFdnIiwic3RyZWV0X2FkZHJlc3MiLCJTY2h1bHN0ci4gMTIiXQ", + "WyJURWtwSjJkYWxraGltUUVLd25Cblp3IiwiVUEiXQ", + "WyIxRTlRZnRDS3YtbTFjN0VFOXlXMmh3IiwiRXh0cmExIl0", + "WyI0Mjl4ejFGeTlEdU9SQ0R2cHd3bzFBIiwiRXh0cmEyIl0", + "WyJvVy1oMDZYVUNsTU45YTBVV3VHMGhBIiwicmVjdXJzaXZlIix7Il9zZCI6WyJoX2h1bVhsYjhVekM5T0tGOHc3SEd6ZmYzSGgzMmh3SGR6Vms5WS1oOGR3Il19XQ", + "WyItdHBPTUlPS3dnOUNtNlBRVTlDTktBIiwia2V5MSIsInZhbHVlMSJd", + "WyJ5WElBaTZSb1Y1eDV2X3lsVm1wXzhBIiwibG9jYWxpdHkiLCJTY2h1bHBmb3J0YSJd" +] \ No newline at end of file diff --git a/component/models/sdjwt/common/types.go b/component/models/sdjwt/common/types.go new file mode 100644 index 000000000..e85121b25 --- /dev/null +++ b/component/models/sdjwt/common/types.go @@ -0,0 +1,13 @@ +/* +Copyright Avast Software. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package common + +type recursiveData struct { + disclosures map[string]*DisclosureClaim + nestedSD []string + cleanupDigestsClaims bool +} diff --git a/component/models/sdjwt/common/verification.go b/component/models/sdjwt/common/verification.go new file mode 100644 index 000000000..88919f651 --- /dev/null +++ b/component/models/sdjwt/common/verification.go @@ -0,0 +1,392 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package common + +import ( + "crypto" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "time" + + "golang.org/x/exp/slices" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/mitchellh/mapstructure" + + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + + afgjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" + utils "github.com/hyperledger/aries-framework-go/component/models/util/maphelpers" +) + +// VerifySigningAlg ensures that a signing algorithm was used that was deemed secure for the application. +// The none algorithm MUST NOT be accepted. +func VerifySigningAlg(joseHeaders jose.Headers, secureAlgs []string) error { + alg, ok := joseHeaders.Algorithm() + if !ok { + return fmt.Errorf("missing alg") + } + + if alg == afgjwt.AlgorithmNone { + return fmt.Errorf("alg value cannot be 'none'") + } + + if !contains(secureAlgs, alg) { + return fmt.Errorf("alg '%s' is not in the allowed list", alg) + } + + return nil +} + +func contains(values []string, val string) bool { + for _, v := range values { + if v == val { + return true + } + } + + return false +} + +// VerifyJWT checks that the JWT is valid using nbf, iat, and exp claims (if provided in the JWT). +func VerifyJWT(signedJWT *afgjwt.JSONWebToken, leeway time.Duration) error { + var claims jwt.Claims + + d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &claims, + TagName: "json", + Squash: true, + WeaklyTypedInput: true, + DecodeHook: utils.JSONNumberToJwtNumericDate(), + }) + if err != nil { + return fmt.Errorf("mapstruct verifyJWT. error: %w", err) + } + + if err = d.Decode(signedJWT.Payload); err != nil { + return fmt.Errorf("mapstruct verifyJWT decode. error: %w", err) + } + + // Validate checks claims in a token against expected values. + // It is validated using the expected.Time, or time.Now if not provided + expected := jwt.Expected{} + + err = claims.ValidateWithLeeway(expected, leeway) + if err != nil { + return fmt.Errorf("invalid JWT time values: %w", err) + } + + return nil +} + +// VerifyTyp checks JWT header parameters for the SD-JWT component. +func VerifyTyp(joseHeaders jose.Headers, expectedTyp string) error { + typ, ok := joseHeaders.Type() + if !ok { + return fmt.Errorf("missing typ") + } + + if typ != expectedTyp { + return fmt.Errorf("unexpected typ \"%s\"", typ) + } + + return nil +} + +// VerifyDisclosuresInSDJWT checks for disclosure inclusion in SD-JWT. +func VerifyDisclosuresInSDJWT( + disclosures []string, + signedJWT *afgjwt.JSONWebToken, +) error { + claims := utils.CopyMap(signedJWT.Payload) + + cryptoHash, err := GetCryptoHashFromClaims(claims) + if err != nil { + return err + } + + parsedDisclosureClaims, err := getDisclosureClaims(disclosures, cryptoHash) + if err != nil { + return err + } + + recData := &recursiveData{ + disclosures: parsedDisclosureClaims, + cleanupDigestsClaims: false, + } + + _, err = discloseClaimValue(claims, recData) + if err != nil { + return err + } + + // If the digest cannot be found in the SD-JWT payload, the Verifier MUST reject the Presentation. + for _, disclosure := range parsedDisclosureClaims { + if !disclosure.IsValueParsed { + return fmt.Errorf("disclosure digest '%s' not found in SD-JWT disclosure digests", disclosure.Digest) + } + } + + return nil +} + +func setDisclosureClaimValue(recData *recursiveData, disclosureClaim *DisclosureClaim) error { + if disclosureClaim.IsValueParsed { + return nil + } + + newValue, err := discloseClaimValue(disclosureClaim.Value, recData) + if err != nil { + return err + } + + disclosureClaim.Value = newValue + disclosureClaim.IsValueParsed = true + + return nil +} + +// discloseClaimValue returns new value of claim, resolving dependencies on other disclosures. +func discloseClaimValue(claim interface{}, recData *recursiveData) (interface{}, error) { // nolint:funlen,gocyclo + switch disclosureValue := claim.(type) { + case []interface{}: + var newValues []interface{} + + for _, value := range disclosureValue { + parsedMap, ok := getMap(value) + if !ok { + // If it's not a map - use value as it is. + newValues = append(newValues, value) + continue + } + + // Find all array elements that are objects with one key, that key being ... and referring to a string. + arrayElementDigestIface, ok := parsedMap[ArrayElementDigestKey] + if !ok { + // If it's not a array element digest - object - use value as it is. + newValues = append(newValues, value) + continue + } + + arrayElementDigest, ok := arrayElementDigestIface.(string) + if !ok { + return nil, errors.New("invalid array struct") + } + + if slices.Contains(recData.nestedSD, arrayElementDigest) { + // If any digests were found more than once in the previous step, the SD-JWT MUST be rejected. + return nil, fmt.Errorf("digest '%s' has been included in more than one place", arrayElementDigest) + } + + recData.nestedSD = append(recData.nestedSD, arrayElementDigest) + + disclosureClaim, ok := recData.disclosures[arrayElementDigest] + if !ok { + if recData.cleanupDigestsClaims { + continue + } + // If there is no disclosure provided for given array element digest - use map as it is. + newValues = append(newValues, value) + + continue + } + + // If the digest was found in an array element: + // If the respective Disclosure is not a JSON-encoded array of two elements, the SD-JWT MUST be rejected. + if disclosureClaim.Elements != disclosureElementsAmountForArrayDigest { + return nil, fmt.Errorf("invald disclosure associated with array element digest %s", arrayElementDigest) + } + + // If disclosure is provided - parse the value. + if err := setDisclosureClaimValue(recData, disclosureClaim); err != nil { + return nil, err + } + + // Use parsed disclosure value from prev strep. + newValues = append(newValues, disclosureClaim.Value) + } + + if len(newValues) == 0 { + return nil, nil + } + + return newValues, nil + case map[string]interface{}: + newValues := make(map[string]interface{}, len(disclosureValue)) + + // If there is nested digests. + if nestedSDListIface, ok := disclosureValue[SDKey]; ok { // nolint:nestif + nestedSDList, err := stringArray(nestedSDListIface) + if err != nil { + return nil, fmt.Errorf("get disclosure digests: %w", err) + } + + var missingSDs []interface{} + + for _, digest := range nestedSDList { + if slices.Contains(recData.nestedSD, digest) { + // If any digests were found more than once in the previous step, the SD-JWT MUST be rejected. + return nil, fmt.Errorf("digest '%s' has been included in more than one place", digest) + } + + recData.nestedSD = append(recData.nestedSD, digest) + + disclosureClaim, ok := recData.disclosures[digest] + if !ok { + missingSDs = append(missingSDs, digest) + continue + } + + if disclosureClaim.Elements != disclosureElementsAmountForSDDigest { + // If the digest was found in an object's _sd key: + // If the respective Disclosure is not a JSON-encoded array of three elements, the SD-JWT MUST be rejected. + return nil, fmt.Errorf("invald disclosure associated with sd element digest %s", digest) + } + + if err = setDisclosureClaimValue(recData, disclosureClaim); err != nil { + return nil, err + } + + // If the claim name already exists at the same level, the SD-JWT MUST be rejected. + if _, ok = newValues[disclosureClaim.Name]; ok { + return nil, fmt.Errorf("claim name '%s' already exists at the same level", disclosureClaim.Name) + } + + newValues[disclosureClaim.Name] = disclosureClaim.Value + } + + if !recData.cleanupDigestsClaims && len(missingSDs) > 0 { + newValues[SDKey] = missingSDs + } + } + + for k, disclosureNestedClaim := range disclosureValue { + if k == SDKey { + continue + } + + if k == SDAlgorithmKey && recData.cleanupDigestsClaims { + continue + } + + newValue, err := discloseClaimValue(disclosureNestedClaim, recData) + if err != nil { + return nil, err + } + + // If the claim name already exists at the same level, the SD-JWT MUST be rejected. + if _, ok := newValues[k]; ok { + return nil, fmt.Errorf("claim name '%s' already exists at the same level", k) + } + + if newValue != nil { + newValues[k] = newValue + } + } + + return newValues, nil + default: + return claim, nil + } +} + +// getDisclosureClaims parses disclosures and returns map[string]*DisclosureClaim, +// where the key is disclosure digest calculated using provided hash. +func getDisclosureClaims(disclosures []string, hash crypto.Hash) (map[string]*DisclosureClaim, error) { + wrappedClaims := make(map[string]*DisclosureClaim, len(disclosures)) + + for _, disclosure := range disclosures { + claim, err := getDisclosureClaim(disclosure, hash) + if err != nil { + return nil, err + } + + wrappedClaims[claim.Digest] = claim + } + + return wrappedClaims, nil +} + +// getDisclosureClaim parses disclosure and returns *DisclosureClaim. +func getDisclosureClaim(disclosure string, hash crypto.Hash) (*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) < disclosureElementsAmountForArrayDigest { + return nil, fmt.Errorf("disclosure array size[%d] must be greater %d", len(disclosureArr), + 2) + } + + salt, ok := disclosureArr[saltPosition].(string) + if !ok { + return nil, fmt.Errorf("disclosure salt type[%T] must be string", disclosureArr[1]) + } + + digest, err := GetHash(hash, disclosure) + if err != nil { + return nil, fmt.Errorf("get disclosure hash: %w", err) + } + + claim := &DisclosureClaim{ + Digest: digest, + Disclosure: disclosure, + Salt: salt, + Version: SDJWTVersionV2, + IsValueParsed: false, + Elements: len(disclosureArr), + } + + switch len(disclosureArr) { + case disclosureElementsAmountForArrayDigest: //array element + enrichWithArrayElement(claim, disclosureArr) + case disclosureElementsAmountForSDDigest: + if err = enrichWithSDElement(claim, disclosureArr); err != nil { + return nil, err + } + } + + return claim, nil +} + +func enrichWithArrayElement(claim *DisclosureClaim, disclosureElementsArr []interface{}) { + claim.Value = disclosureElementsArr[arrayDigestValuePosition] + claim.Type = DisclosureClaimTypeArrayElement + claim.Version = SDJWTVersionV5 +} + +func enrichWithSDElement(claim *DisclosureClaim, disclosureElementsArr []interface{}) error { + name, ok := disclosureElementsArr[sdDigestNamePosition].(string) + if !ok { + return fmt.Errorf("disclosure name type[%T] must be string", disclosureElementsArr[1]) + } + + claim.Name = name + claim.Value = disclosureElementsArr[sdDigestValuePosition] + + switch t := disclosureElementsArr[sdDigestValuePosition].(type) { + case map[string]interface{}: + claim.Type = DisclosureClaimTypeObject + if KeyExistsInMap(SDKey, t) { + claim.Version = SDJWTVersionV5 + } + default: + claim.Type = DisclosureClaimTypePlainText + } + + return nil +} diff --git a/component/models/sdjwt/common/verification_test.go b/component/models/sdjwt/common/verification_test.go new file mode 100644 index 000000000..f0818c865 --- /dev/null +++ b/component/models/sdjwt/common/verification_test.go @@ -0,0 +1,542 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package common + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + "fmt" + "testing" + "time" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/stretchr/testify/require" + + afjose "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + afjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" +) + +func TestVerifySigningAlgorithm(t *testing.T) { + r := require.New(t) + + t.Run("success - EdDSA signing algorithm", func(t *testing.T) { + headers := make(afjose.Headers) + headers["alg"] = "EdDSA" + err := VerifySigningAlg(headers, []string{"EdDSA"}) + r.NoError(err) + }) + + t.Run("error - signing algorithm can not be empty", func(t *testing.T) { + headers := make(afjose.Headers) + err := VerifySigningAlg(headers, []string{"RS256"}) + r.Error(err) + r.Contains(err.Error(), "missing alg") + }) + + t.Run("success - EdDSA signing algorithm not in allowed list", func(t *testing.T) { + headers := make(afjose.Headers) + headers["alg"] = "EdDSA" + err := VerifySigningAlg(headers, []string{"RS256"}) + r.Error(err) + r.Contains(err.Error(), "alg 'EdDSA' is not in the allowed list") + }) + + t.Run("error - signing algorithm can not be none", func(t *testing.T) { + headers := make(afjose.Headers) + headers["alg"] = "none" + err := VerifySigningAlg(headers, []string{"RS256"}) + r.Error(err) + r.Contains(err.Error(), "alg value cannot be 'none'") + }) +} + +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 := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) + require.Equal(t, 1, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + err = VerifyDisclosuresInSDJWT(sdJWT.Disclosures, signedJWT) + r.NoError(err) + }) + + t.Run("success V5", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuanceV5) + require.Equal(t, 6, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + err = VerifyDisclosuresInSDJWT(sdJWT.Disclosures, signedJWT) + r.NoError(err) + }) + + t.Run("success - complex struct(spec example 2b)", func(t *testing.T) { + specExample2bPresentation := fmt.Sprintf("%s%s", specExample2bJWT, specExample2bDisclosures) + + sdJWT := ParseCombinedFormatForPresentation(specExample2bPresentation) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, 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) { + jwtPayload := &payload{ + Issuer: "issuer", + SDAlg: "sha-256", + } + + signedJWT, err := afjwt.NewSigned(jwtPayload, 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[SDAlgorithmKey] = testAlg + payload[SDKey] = 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 := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) + require.Equal(t, 1, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + err = VerifyDisclosuresInSDJWT(append(sdJWT.Disclosures, additionalSDDisclosure), 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) { + jwtPayload := &payload{ + Issuer: "issuer", + SDAlg: testAlg, + } + + signedJWT, err := afjwt.NewSigned(jwtPayload, nil, signer) + r.NoError(err) + + err = VerifyDisclosuresInSDJWT([]string{additionalSDDisclosure}, 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) { + jwtPayload := &payload{ + Issuer: "issuer", + } + + signedJWT, err := afjwt.NewSigned(jwtPayload, nil, signer) + r.NoError(err) + + err = VerifyDisclosuresInSDJWT(nil, signedJWT) + r.Error(err) + r.Contains(err.Error(), "_sd_alg must be present in SD-JWT", SDAlgorithmKey) + }) + + t.Run("error - invalid algorithm", func(t *testing.T) { + jwtPayload := payload{ + Issuer: "issuer", + SDAlg: "SHA-XXX", + } + + signedJWT, err := afjwt.NewSigned(jwtPayload, 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[SDAlgorithmKey] = 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[SDAlgorithmKey] = testAlg + payload[SDKey] = "test" + + signedJWT, err := afjwt.NewSigned(payload, nil, signer) + r.NoError(err) + + err = VerifyDisclosuresInSDJWT([]string{additionalSDDisclosure}, 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[SDAlgorithmKey] = testAlg + payload[SDKey] = []float64{123} + + signedJWT, err := afjwt.NewSigned(payload, nil, signer) + r.NoError(err) + + err = VerifyDisclosuresInSDJWT([]string{additionalSDDisclosure}, signedJWT) + r.Error(err) + r.Contains(err.Error(), "get disclosure digests: entry item type[float64] is not a string") + }) + + t.Run("error - array element associated disclosure is invalid", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuanceV5) + require.Equal(t, 6, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + additionalDigest, err := GetHash(crypto.SHA256, additionalSDDisclosure) + r.NoError(err) + + oldDigest := findAndReplaceArrayElementDigest(signedJWT.Payload, additionalDigest) + + updatedDisclosures := []string{additionalSDDisclosure} + for _, d := range sdJWT.Disclosures { + h, err := GetHash(crypto.SHA256, d) // nolint + r.NoError(err) + if h == oldDigest { + continue + } + updatedDisclosures = append(updatedDisclosures, d) + } + + err = VerifyDisclosuresInSDJWT(updatedDisclosures, signedJWT) + r.ErrorContains(err, fmt.Sprintf("invald disclosure associated with array element digest %s", additionalDigest)) + }) + + t.Run("error - array element was found more then once", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuanceV5) + require.Equal(t, 6, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + additionalDigest, err := GetHash(crypto.SHA256, additionalArrayElementDisclosure) + r.NoError(err) + + ok := findAndAppendArrayElementDigest(signedJWT.Payload, additionalDigest) + r.True(ok) + + err = VerifyDisclosuresInSDJWT(append(sdJWT.Disclosures, additionalArrayElementDisclosure), signedJWT) + r.ErrorContains(err, fmt.Sprintf("digest '%s' has been included in more than one place", additionalDigest)) + }) + + t.Run("error - sd element associated disclosure is invalid", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuanceV5) + require.Equal(t, 6, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + additionalDigest, err := GetHash(crypto.SHA256, additionalArrayElementDisclosure) + r.NoError(err) + + ok := findAndAppendSDElementDigest(signedJWT.Payload, additionalDigest) + r.True(ok) + + err = VerifyDisclosuresInSDJWT(append(sdJWT.Disclosures, additionalArrayElementDisclosure), signedJWT) + r.ErrorContains(err, fmt.Sprintf("invald disclosure associated with sd element digest %s", additionalDigest)) + }) + + t.Run("error - sd element was found more then once", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuanceV5) + require.Equal(t, 6, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + additionalDigest, err := GetHash(crypto.SHA256, additionalSDDisclosure) + r.NoError(err) + + ok := findAndAppendSDElementDigest(signedJWT.Payload, additionalDigest, additionalDigest) + r.True(ok) + + err = VerifyDisclosuresInSDJWT(append(sdJWT.Disclosures, additionalSDDisclosure), signedJWT) + r.ErrorContains(err, fmt.Sprintf("digest '%s' has been included in more than one place", additionalDigest)) + }) + + t.Run("error - claim name was found more then once", func(t *testing.T) { + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuanceV5) + require.Equal(t, 6, len(sdJWT.Disclosures)) + + signedJWT, _, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + require.NoError(t, err) + + signedJWT.Payload["address"].(map[string]interface{})["locality"] = "some existing claim" + + err = VerifyDisclosuresInSDJWT(append(sdJWT.Disclosures, additionalSDDisclosure), signedJWT) + r.ErrorContains(err, "claim name 'locality' already exists at the same level") + }) +} + +func findAndAppendSDElementDigest(claimsMap map[string]interface{}, additionalDigest ...interface{}) bool { + if digests, ok := claimsMap[SDKey]; ok { + if d, ok := digests.([]interface{}); ok { + claimsMap[SDKey] = append(d, additionalDigest...) + return true + } + } + + for _, v := range claimsMap { + switch t := v.(type) { + case map[string]interface{}: + if ok := findAndAppendSDElementDigest(t, additionalDigest...); ok { + return ok + } + } + } + + return false +} + +func findAndReplaceArrayElementDigest(claimsMap map[string]interface{}, additionalDigest string) string { + if digest, ok := claimsMap[ArrayElementDigestKey]; ok { + claimsMap[ArrayElementDigestKey] = additionalDigest + return digest.(string) + } + + for _, v := range claimsMap { + switch t := v.(type) { + case map[string]interface{}: + res := findAndReplaceArrayElementDigest(t, additionalDigest) + if res == "" { + continue + } + + return res + case []interface{}: + for _, nv := range t { + if mapped, ok := nv.(map[string]interface{}); ok { + return findAndReplaceArrayElementDigest(mapped, additionalDigest) + } + } + } + } + + return "" +} + +func findAndAppendArrayElementDigest(claimsMap map[string]interface{}, additionalDigest string) bool { + for k, v := range claimsMap { + switch t := v.(type) { + case map[string]interface{}: + if ok := findAndAppendArrayElementDigest(t, additionalDigest); ok { + return true + } + case []interface{}: + for _, nv := range t { + if mapped, ok := nv.(map[string]interface{}); ok { + if _, ok := mapped[ArrayElementDigestKey]; ok { + updatedList := append(t, map[string]interface{}{ + ArrayElementDigestKey: additionalDigest, + }, map[string]interface{}{ + ArrayElementDigestKey: additionalDigest, + }) + + claimsMap[k] = updatedList + + return true + } + + return findAndAppendArrayElementDigest(mapped, additionalDigest) + } + } + } + } + + return false +} + +func TestVerifyTyp(t *testing.T) { + type args struct { + joseHeaders afjose.Headers + expectedTyp string + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "success", + args: args{ + joseHeaders: afjose.Headers{ + afjose.HeaderType: "kb+jwt", + }, + expectedTyp: "kb+jwt", + }, + wantErr: false, + }, + { + name: "error - missed typ", + args: args{ + joseHeaders: afjose.Headers{}, + }, + wantErr: true, + }, + { + name: "error - mismatch", + args: args{ + joseHeaders: afjose.Headers{ + afjose.HeaderType: "vc-sd+jwt", + }, + expectedTyp: "kb+jwt", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := VerifyTyp(tt.args.joseHeaders, tt.args.expectedTyp); (err != nil) != tt.wantErr { + t.Errorf("VerifyTyp() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestVerifyJWT(t *testing.T) { + r := require.New(t) + + _, privKey, err := ed25519.GenerateKey(rand.Reader) + r.NoError(err) + + signer := afjwt.NewEd25519Signer(privKey) + + type args struct { + getSignedJWT func() *afjwt.JSONWebToken + leeway time.Duration + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "success", + args: args{ + getSignedJWT: func() *afjwt.JSONWebToken { + jwtPayload := jwt.Claims{ + Issuer: "issuer", + Subject: "subject", + Audience: []string{"aud1"}, + } + + signedJWT, err := afjwt.NewSigned(jwtPayload, nil, signer) + r.NoError(err) + + return signedJWT + }, + leeway: time.Minute, + }, + wantErr: false, + }, + { + name: "error invalid payload", + args: args{ + getSignedJWT: func() *afjwt.JSONWebToken { + jwtPayload := jwt.Claims{} + + signedJWT, err := afjwt.NewSigned(jwtPayload, nil, signer) + r.NoError(err) + + signedJWT.Payload = map[string]interface{}{ + "iss": []string{"iss1"}, + } + + return signedJWT + }, + }, + wantErr: true, + }, + { + name: "error exp invalid", + args: args{ + getSignedJWT: func() *afjwt.JSONWebToken { + exp := jwt.NumericDate(time.Now().Add(-time.Hour).Unix()) + jwtPayload := jwt.Claims{ + Issuer: "issuer", + Subject: "subject", + Audience: []string{"aud1"}, + Expiry: &exp, + } + + signedJWT, err := afjwt.NewSigned(jwtPayload, nil, signer) + r.NoError(err) + + return signedJWT + }, + leeway: time.Minute, + }, + wantErr: true, + }, + { + name: "error iat invalid", + args: args{ + getSignedJWT: func() *afjwt.JSONWebToken { + iat := jwt.NumericDate(time.Now().Add(time.Hour).Unix()) + jwtPayload := jwt.Claims{ + Issuer: "issuer", + Subject: "subject", + Audience: []string{"aud1"}, + IssuedAt: &iat, + } + + signedJWT, err := afjwt.NewSigned(jwtPayload, nil, signer) + r.NoError(err) + + return signedJWT + }, + leeway: time.Minute, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := VerifyJWT(tt.args.getSignedJWT(), tt.args.leeway); (err != nil) != tt.wantErr { + t.Errorf("VerifyJWT() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/component/models/sdjwt/example_test.go b/component/models/sdjwt/example_test.go index 7bd34b597..9bf0b93b0 100644 --- a/component/models/sdjwt/example_test.go +++ b/component/models/sdjwt/example_test.go @@ -134,7 +134,7 @@ func ExampleComplexClaimsWithHolderBinding() { //nolint:govet // Holder will disclose only sub-set of claims to verifier. combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures, - holder.WithHolderBinding(&holder.BindingInfo{ + holder.WithHolderVerification(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: "nonce", Audience: "https://test.com/verifier", diff --git a/component/models/sdjwt/holder/example_test.go b/component/models/sdjwt/holder/example_test.go index 0abb1f32d..f9c737941 100644 --- a/component/models/sdjwt/holder/example_test.go +++ b/component/models/sdjwt/holder/example_test.go @@ -122,7 +122,7 @@ func ExampleCreatePresentation() { // Holder will disclose only sub-set of claims to verifier and create holder binding for the verifier. combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, selectedDisclosures, - WithHolderBinding(&BindingInfo{ + WithHolderVerification(&BindingInfo{ Payload: BindingPayload{ Nonce: "nonce", Audience: "https://test.com/verifier", @@ -136,7 +136,7 @@ func ExampleCreatePresentation() { cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) - fmt.Println(cfp.HolderBinding != "") + fmt.Println(cfp.HolderVerification != "") // Output: true } diff --git a/component/models/sdjwt/holder/holder.go b/component/models/sdjwt/holder/holder.go index 3678f75d2..1cc414aec 100644 --- a/component/models/sdjwt/holder/holder.go +++ b/component/models/sdjwt/holder/holder.go @@ -8,11 +8,14 @@ SPDX-License-Identifier: Apache-2.0 package holder import ( + "crypto" "fmt" + "time" "github.com/go-jose/go-jose/v3/jwt" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + afgjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" ) @@ -28,6 +31,12 @@ type Claim struct { type parseOpts struct { detachedPayload []byte sigVerifier jose.SignatureVerifier + + issuerSigningAlgorithms []string + sdjwtV5Validation bool + expectedTypHeader string + + leewayForClaimsValidation time.Duration } // ParseOpt is the SD-JWT Parser option. @@ -47,6 +56,37 @@ func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt { } } +// WithSDJWTV5Validation option is for defining additional holder verification defined in SDJWT V5 spec. +// Section: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#section-6.1-3 +func WithSDJWTV5Validation(flag bool) ParseOpt { + return func(opts *parseOpts) { + opts.sdjwtV5Validation = flag + } +} + +// WithIssuerSigningAlgorithms option is for defining secure signing algorithms (for holder verification). +func WithIssuerSigningAlgorithms(algorithms []string) ParseOpt { + return func(opts *parseOpts) { + opts.issuerSigningAlgorithms = algorithms + } +} + +// WithLeewayForClaimsValidation is an option for claims time(s) validation. +func WithLeewayForClaimsValidation(duration time.Duration) ParseOpt { + return func(opts *parseOpts) { + opts.leewayForClaimsValidation = duration + } +} + +// WithExpectedTypHeader is an option for JWT typ header validation. +// Might be relevant for SDJWT V5 VC validation. +// Spec: https://vcstuff.github.io/draft-terbu-sd-jwt-vc/draft-terbu-oauth-sd-jwt-vc.html#name-header-parameters +func WithExpectedTypHeader(typ string) ParseOpt { + return func(opts *parseOpts) { + opts.expectedTypHeader = typ + } +} + // Parse parses issuer SD-JWT and returns claims that can be selected. // The Holder MUST perform the following (or equivalent) steps when receiving a Combined Format for Issuance: // @@ -71,28 +111,41 @@ func Parse(combinedFormatForIssuance string, opts ...ParseOpt) ([]*Claim, error) opt(pOpts) } - var jwtOpts []afgjwt.ParseOpt - jwtOpts = append(jwtOpts, + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + + // Validate the signature over the Issuer-signed JWT. + signedJWT, _, err := afgjwt.Parse(cfi.SDJWT, afgjwt.WithSignatureVerifier(pOpts.sigVerifier), afgjwt.WithJWTDetachedPayload(pOpts.detachedPayload)) + if err != nil { + return nil, err + } - cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + if pOpts.sdjwtV5Validation { + // Apply additional validation for V5. + if err = applySDJWTV5Validation(signedJWT, cfi.Disclosures, pOpts); err != nil { + return nil, err + } + } - signedJWT, _, err := afgjwt.Parse(cfi.SDJWT, jwtOpts...) + err = common.VerifyDisclosuresInSDJWT(cfi.Disclosures, signedJWT) if err != nil { return nil, err } - err = common.VerifyDisclosuresInSDJWT(cfi.Disclosures, signedJWT) + cryptoHash, err := common.GetCryptoHashFromClaims(signedJWT.Payload) if err != nil { return nil, err } - return getClaims(cfi.Disclosures) + return getClaims(cfi.Disclosures, cryptoHash) } -func getClaims(disclosures []string) ([]*Claim, error) { - disclosureClaims, err := common.GetDisclosureClaims(disclosures) +func getClaims( + disclosures []string, + hash crypto.Hash, +) ([]*Claim, error) { + disclosureClaims, err := common.GetDisclosureClaims(disclosures, hash) if err != nil { return nil, fmt.Errorf("failed to get claims from disclosures: %w", err) } @@ -110,42 +163,92 @@ func getClaims(disclosures []string) ([]*Claim, error) { return claims, nil } -// BindingPayload represents holder binding payload. +// applySDJWTV5Validation applies additional validation to signedJWT that were introduces in V5 spec. +// Doc: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#section-6.1-3. +func applySDJWTV5Validation(signedJWT *afgjwt.JSONWebToken, disclosures []string, pOpts *parseOpts) error { + // If a Key Binding JWT is received by a Holder, the SD-JWT SHOULD be rejected. + var possibleKeyBinding string + if l := len(disclosures); l > 0 { + possibleKeyBinding = disclosures[l-1] + } + + if afgjwt.IsJWS(possibleKeyBinding) || afgjwt.IsJWTUnsecured(possibleKeyBinding) { + return fmt.Errorf("unexpected key binding JWT supplied") + } + + if pOpts.expectedTypHeader != "" { + // Check that the typ header. + // Spec: https://vcstuff.github.io/draft-terbu-sd-jwt-vc/draft-terbu-oauth-sd-jwt-vc.html#name-header-parameters + err := common.VerifyTyp(signedJWT.Headers, pOpts.expectedTypHeader) + if err != nil { + return fmt.Errorf("verify typ header: %w", err) + } + } + + // Ensure that a signing algorithm was used that was deemed secure for the application. + // The none algorithm MUST NOT be accepted. + err := common.VerifySigningAlg(signedJWT.Headers, pOpts.issuerSigningAlgorithms) + if err != nil { + return fmt.Errorf("failed to verify issuer signing algorithm: %w", err) + } + + // TODO: Validate the Issuer of the SD-JWT and that the signing key belongs to this Issuer. + + // Check that the SD-JWT is valid using nbf, iat, and exp claims, + // if provided in the SD-JWT, and not selectively disclosed. + err = common.VerifyJWT(signedJWT, pOpts.leewayForClaimsValidation) + if err != nil { + return err + } + + return nil +} + +// BindingPayload represents holder verification 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. +// BindingInfo defines holder verification payload and signer. type BindingInfo struct { Payload BindingPayload Signer jose.Signer + Headers jose.Headers } // options holds options for holder. type options struct { - holderBindingInfo *BindingInfo + holderVerificationInfo *BindingInfo } // Option is a holder option. type Option func(opts *options) // WithHolderBinding option to set optional holder binding. +// Deprecated. Use WithHolderVerification instead. func WithHolderBinding(info *BindingInfo) Option { return func(opts *options) { - opts.holderBindingInfo = info + opts.holderVerificationInfo = info + } +} + +// WithHolderVerification option to set optional holder verification. +func WithHolderVerification(info *BindingInfo) Option { + return func(opts *options) { + opts.holderVerificationInfo = info } } // CreatePresentation is a convenience method to assemble combined format for presentation -// using selected disclosures (claimsToDisclose) and optional holder binding. +// using selected disclosures (claimsToDisclose) and optional holder verification. // This call assumes that combinedFormatForIssuance has already been parsed and verified using Parse() function. // // For presentation to a Verifier, the Holder MUST perform the following (or equivalent) steps: // - Decide which Disclosures to release to the Verifier, obtaining proper End-User consent if necessary. // - If Holder Binding is required, create a Holder Binding JWT. -// - Create the Combined Format for Presentation from selected Disclosures and Holder Binding JWT(if applicable). +// - Create the Combined Format for Presentation from selected Disclosures and Holder Verification JWT(if applicable). // - Send the Presentation to the Verifier. func CreatePresentation(combinedFormatForIssuance string, claimsToDisclose []string, opts ...Option) (string, error) { hOpts := &options{} @@ -172,25 +275,25 @@ func CreatePresentation(combinedFormatForIssuance string, claimsToDisclose []str var hbJWT string - if hOpts.holderBindingInfo != nil { - hbJWT, err = CreateHolderBinding(hOpts.holderBindingInfo) + if hOpts.holderVerificationInfo != nil { + hbJWT, err = CreateHolderVerification(hOpts.holderVerificationInfo) if err != nil { - return "", fmt.Errorf("failed to create holder binding: %w", err) + return "", fmt.Errorf("failed to create holder verification: %w", err) } } cf := common.CombinedFormatForPresentation{ - SDJWT: cfi.SDJWT, - Disclosures: claimsToDisclose, - HolderBinding: hbJWT, + SDJWT: cfi.SDJWT, + Disclosures: claimsToDisclose, + HolderVerification: hbJWT, } return cf.Serialize(), nil } -// CreateHolderBinding will create holder binding from binding info. -func CreateHolderBinding(info *BindingInfo) (string, error) { - hbJWT, err := afgjwt.NewSigned(info.Payload, nil, info.Signer) +// CreateHolderVerification will create holder verification from binding info. +func CreateHolderVerification(info *BindingInfo) (string, error) { + hbJWT, err := afgjwt.NewSigned(info.Payload, info.Headers, info.Signer) if err != nil { return "", err } diff --git a/component/models/sdjwt/holder/holder_test.go b/component/models/sdjwt/holder/holder_test.go index 274f446fb..58fde7d5a 100644 --- a/component/models/sdjwt/holder/holder_test.go +++ b/component/models/sdjwt/holder/holder_test.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0 package holder import ( + "crypto" "crypto/ed25519" "crypto/rand" "encoding/base64" @@ -63,13 +64,24 @@ func TestParse(t *testing.T) { r.Equal("Albert", claims[0].Value) }) - t.Run("success - spec SD-JWT", func(t *testing.T) { - claims, err := Parse(specSDJWT, WithSignatureVerifier(&NoopSignatureVerifier{})) + t.Run("success - spec SD-JWT V2", func(t *testing.T) { + claims, err := Parse(specSDJWTV2, WithSignatureVerifier(&NoopSignatureVerifier{})) r.NoError(err) require.NotNil(t, claims) require.Equal(t, 7, len(claims)) }) + t.Run("success - spec SD-JWT V5", func(t *testing.T) { + claims, err := Parse(specSDJWTV5, + WithSDJWTV5Validation(true), + WithIssuerSigningAlgorithms([]string{"ES256"}), + WithLeewayForClaimsValidation(10*12*30*24*time.Hour), + WithSignatureVerifier(&NoopSignatureVerifier{})) + r.NoError(err) + require.NotNil(t, claims) + require.Equal(t, 10, len(claims)) + }) + t.Run("success - VC example", func(t *testing.T) { claims, err := Parse(vcCombinedFormatForIssuance, WithSignatureVerifier(&NoopSignatureVerifier{})) r.NoError(err) @@ -124,9 +136,76 @@ func TestParse(t *testing.T) { r.Nil(claims) r.Contains(err.Error(), "read JWT claims from JWS payload") }) + + t.Run("error - applySDJWTV5Validation key binding supplied", func(t *testing.T) { + holderSigner, _, err := setUpHolderBinding() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + holderVerification, err := CreateHolderVerification(&BindingInfo{ + Payload: BindingPayload{ + Nonce: "nonce", + Audience: "https://test.com/verifier", + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Signer: holderSigner, + }) + r.NoError(err) + + cfp := specSDJWTV5 + common.CombinedFormatSeparator + holderVerification + + claims, err := Parse(cfp, + WithSDJWTV5Validation(true), + WithIssuerSigningAlgorithms([]string{"ES256"}), + WithSignatureVerifier(&NoopSignatureVerifier{})) + r.Nil(claims) + r.Error(err) + r.ErrorContains(err, "unexpected key binding JWT supplied") + }) + + t.Run("error - applySDJWTV5Validation unexpected typ header", func(t *testing.T) { + complexClaims := createComplexClaims() + + token, e := issuer.New(testIssuer, complexClaims, map[string]interface{}{ + jose.HeaderType: "JWT", + }, signer, + issuer.WithStructuredClaims(true)) + r.NoError(e) + cfi, e := token.Serialize(false) + r.NoError(e) + + claims, err := Parse(cfi, + WithSDJWTV5Validation(true), + WithIssuerSigningAlgorithms([]string{"ES256"}), + WithExpectedTypHeader("vc+sd-jwt"), + WithSignatureVerifier(verifier)) + r.Nil(claims) + r.Error(err) + r.ErrorContains(err, "unexpected typ \"JWT\"") + }) + + t.Run("error - applySDJWTV5Validation signing alg", func(t *testing.T) { + claims, err := Parse(specSDJWTV5, + WithSDJWTV5Validation(true), + WithSignatureVerifier(&NoopSignatureVerifier{})) + r.Nil(claims) + r.Error(err) + r.ErrorContains(err, "failed to verify issuer signing algorithm: alg 'ES256' is not in the allowed list") + }) + + t.Run("error - applySDJWTV5Validation leeway claims", func(t *testing.T) { + claims, err := Parse(specSDJWTV5, + WithSDJWTV5Validation(true), + WithIssuerSigningAlgorithms([]string{"ES256"}), + WithLeewayForClaimsValidation(-time.Hour*24*365*10), + WithSignatureVerifier(&NoopSignatureVerifier{})) + r.Nil(claims) + r.ErrorContains(err, " validation failed, token is expired (exp)") + }) } -func TestDiscloseClaims(t *testing.T) { +func TestCreatePresentation(t *testing.T) { r := require.New(t) _, privKey, e := ed25519.GenerateKey(rand.Reader) @@ -151,7 +230,7 @@ func TestDiscloseClaims(t *testing.T) { require.Equal(t, combinedFormatForIssuance+common.CombinedFormatSeparator, combinedFormatForPresentation) }) - t.Run("success - with holder binding", func(t *testing.T) { + t.Run("success - with holder verification", func(t *testing.T) { _, holderPrivKey, e := ed25519.GenerateKey(rand.Reader) r.NoError(e) @@ -171,9 +250,9 @@ func TestDiscloseClaims(t *testing.T) { r.Contains(combinedFormatForPresentation, combinedFormatForIssuance+common.CombinedFormatSeparator) }) - t.Run("error - failed to create holder binding due to signing error", func(t *testing.T) { + t.Run("error - failed to create holder verification due to signing error", func(t *testing.T) { combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - WithHolderBinding(&BindingInfo{ + WithHolderVerification(&BindingInfo{ Payload: BindingPayload{}, Signer: &mockSigner{Err: fmt.Errorf("signing error")}, })) @@ -182,7 +261,7 @@ func TestDiscloseClaims(t *testing.T) { r.Empty(combinedFormatForPresentation) r.Contains(err.Error(), - "failed to create holder binding: create JWS: sign JWS: sign JWS verification data: signing error") + "failed to create holder verification: create JWS: sign JWS: sign JWS verification data: signing error") }) t.Run("error - no disclosure(s)", func(t *testing.T) { @@ -207,13 +286,13 @@ func TestGetClaims(t *testing.T) { r := require.New(t) t.Run("success", func(t *testing.T) { - claims, err := getClaims([]string{additionalDisclosure}) + claims, err := getClaims([]string{additionalDisclosure}, crypto.SHA256) r.NoError(err) r.Len(claims, 1) }) t.Run("error - not base64 encoded ", func(t *testing.T) { - claims, err := getClaims([]string{"!!!"}) + claims, err := getClaims([]string{"!!!"}, crypto.SHA256) r.Error(err) r.Nil(claims) r.Contains(err.Error(), "failed to decode disclosure") @@ -288,7 +367,10 @@ func createComplexClaims() map[string]interface{} { const additionalDisclosure = `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` +const specSDJWTV2 = `eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImNBRUlVcUowY21MekQxa3pHemhlaUJhZzBZUkF6VmRsZnhOMjgwTmdIYUEifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIlJTQSIsICJuIjogInBtNGJPSEJnLW9ZaEF5UFd6UjU2QVdYM3JVSVhwMTFfSUNEa0dnUzZXM1pXTHRzLWh6d0kzeDY1NjU5a2c0aFZvOWRiR29DSkUzWkdGX2VhZXRFMzBVaEJVRWdwR3dyRHJRaUo5enFwcm1jRmZyM3F2dmtHanR0aDhaZ2wxZU0yYkpjT3dFN1BDQkhXVEtXWXMxNTJSN2c2SmcyT1ZwaC1hOHJxLXE3OU1oS0c1UW9XX21UejEwUVRfNkg0YzdQaldHMWZqaDhocFdObmJQX3B2NmQxelN3WmZjNWZsNnlWUkwwRFYwVjNsR0hLZTJXcWZfZU5HakJyQkxWa2xEVGs4LXN0WF9NV0xjUi1FR21YQU92MFVCV2l0U19kWEpLSnUtdlhKeXcxNG5IU0d1eFRJSzJoeDFwdHRNZnQ5Q3N2cWltWEtlRFRVMTRxUUwxZUU3aWhjdyIsICJlIjogIkFRQUIifX19.xqgKrDO6dK_oBL3fiqdcq_elaIGxM6Z-RyuysglGyddR1O1IiE3mIk8kCpoqcRLR88opkVWN2392K_XYfAuAmeT9kJVisD8ZcgNcv-MQlWW9s8WaViXxBRe7EZWkWRQcQVR6jf95XZ5H2-_KA54POq3L42xjk0y5vDr8yc08Reak6vvJVvjXpp-Wk6uxsdEEAKFspt_EYIvISFJhfTuQqyhCjnaW13X312MSQBPwjbHn74ylUqVLljDvqcemxeqjh42KWJq4C3RqNJ7anA2i3FU1kB4-KNZWsijY7-op49iL7BrnIBxdlAMrbHEkoGTbFWdl7Ki17GHtDxxa1jaxQg~WyJkcVR2WE14UzBHYTNEb2FHbmU5eDBRIiwgInN1YiIsICJqb2huX2RvZV80MiJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJLVXhTNWhFX1hiVmFjckdBYzdFRnd3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyIzcXZWSjFCQURwSERTUzkzOVEtUml3IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJFUktNMENOZUZKa2FENW1UWFZfWDh3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0` + +// nolint: lll +const specSDJWTV5 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0` // nolint: lll const vcCombinedFormatForIssuance = `eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjEuNjczOTg3NTQ3ZSswOSwiaXNzIjoiZGlkOmV4YW1wbGU6NzZlMTJlYzcxMmViYzZmMWMyMjFlYmZlYjFmIiwianRpIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzE4NzIiLCJuYmYiOjEuNjczOTg3NTQ3ZSswOSwic3ViIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiZDlYemtRbVJMQncxSXpfeHVGUmVLMUItRmpCdTdjT0N3RTlOR2F1d251SSJ9fSwiY3JlZGVudGlhbFN1YmplY3QiOnsiX3NkIjpbInBBdjJUMU10YmRXNGttUUdxT1VVRUpjQmdTZi1mSFRHV2xQVUV4aWlIbVEiLCI2dDlBRUJCQnEzalZwckJ3bGljOGhFWnNNSmxXSXhRdUw5c3ExMzJZTnYwIl0sImRlZ3JlZSI6eyJfc2QiOlsibzZzV2h4RjcxWHBvZ1cxVUxCbU90bjR1SXFGdjJ3ODF6emRuelJXdlpqYyIsIi1yRklXbU1YR3ZXX0FIYVEtODhpMy11ZzRUVjhLUTg5TjdmZmtneFc2X2MiXX0sImlkIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIn0sImZpcnN0X25hbWUiOiJGaXJzdCBuYW1lIiwiaWQiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMTg3MiIsImluZm8iOiJJbmZvIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0wMS0xN1QyMjozMjoyNy40NjgxMDk4MTcrMDI6MDAiLCJpc3N1ZXIiOiJkaWQ6ZXhhbXBsZTo3NmUxMmVjNzEyZWJjNmYxYzIyMWViZmViMWYiLCJsYXN0X25hbWUiOiJMYXN0IG5hbWUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwifX0.GcfSA6NkONxdsm5Lxj9-988eWx1ZvMz5vJ1uh2x8UK1iKIeQLmhsWpA_34RbtAm2HnuoxW4_ZGeiHBzQ1GLTDQ~WyJFWkVDRVZ1YWVJOXhZWmlWb3VMQldBIiwidHlwZSIsIkJhY2hlbG9yRGVncmVlIl0~WyJyMno1UzZMa25FRTR3TWwteFB0VEx3IiwiZGVncmVlIiwiTUlUIl0~WyJ2VkhfaGhNQy1aSUt5WFdtdDUyOWpnIiwic3BvdXNlIiwiZGlkOmV4YW1wbGU6YzI3NmUxMmVjMjFlYmZlYjFmNzEyZWJjNmYxIl0~WyJrVzh0WVVwbVl1VmRoZktFT050TnFnIiwibmFtZSIsIkpheWRlbiBEb2UiXQ` diff --git a/component/models/sdjwt/integration_test.go b/component/models/sdjwt/integration_test.go index cddaf6fe8..b31460461 100644 --- a/component/models/sdjwt/integration_test.go +++ b/component/models/sdjwt/integration_test.go @@ -20,7 +20,9 @@ import ( "github.com/stretchr/testify/require" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk/jwksupport" + afjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" + "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/holder" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/issuer" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/verifier" @@ -138,7 +140,7 @@ func TestSDJWTFlow(t *testing.T) { // Holder will disclose only sub-set of claims to verifier and add holder binding. combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures, - holder.WithHolderBinding(&holder.BindingInfo{ + holder.WithHolderVerification(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, Audience: testAudience, @@ -153,9 +155,9 @@ func TestSDJWTFlow(t *testing.T) { // Verifier will validate combined format for presentation and create verified claims. verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, verifier.WithSignatureVerifier(signatureVerifier), - verifier.WithHolderBindingRequired(true), - verifier.WithExpectedAudienceForHolderBinding(testAudience), - verifier.WithExpectedNonceForHolderBinding(testNonce)) + verifier.WithHolderVerificationRequired(true), + verifier.WithExpectedAudienceForHolderVerification(testAudience), + verifier.WithExpectedNonceForHolderVerification(testNonce)) r.NoError(err) printObject(t, "Verified Claims", verifiedClaims) @@ -294,7 +296,111 @@ func TestSDJWTFlow(t *testing.T) { printObject(t, "Holder Claims", claims) - r.Equal(4, len(claims)) + r.Equal(5, len(claims)) + + const testAudience = "https://test.com/verifier" + const testNonce = "nonce" + + holderSigner := afjwt.NewEd25519Signer(holderPrivateKey) + + selectedDisclosures := getDisclosuresFromClaimNames([]string{"degree", "id", "name"}, claims) + + // Holder will disclose only sub-set of claims to verifier. + combinedFormatForPresentation, err := holder.CreatePresentation(vcCombinedFormatForIssuance, selectedDisclosures, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Signer: holderSigner, + })) + r.NoError(err) + + fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation)) + + // Verifier will validate combined format for presentation and create verified claims. + // In this case it will be VC since VC was passed in. + verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, + verifier.WithSignatureVerifier(signatureVerifier)) + r.NoError(err) + + printObject(t, "Verified Claims", verifiedClaims) + + r.Equal(len(vc), len(verifiedClaims)) + }) + + t.Run("success - NewFromVC API v5", func(t *testing.T) { + holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader) + r.NoError(err) + + holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey) + require.NoError(t, err) + + localVc := ` +{ + "iat": 1673987547, + "iss": "did:example:76e12ec712ebc6f1c221ebfeb1f", + "jti": "http://example.edu/credentials/1872", + "nbf": 1673987547, + "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "vc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "credentialSubject": { + "degree": { + "degree": "MIT", + "type": "BachelorDegree", + "id": "some-id" + }, + "arr" : ["a", "b"], + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name": "Jayden Doe", + "spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1" + }, + "first_name": "First name", + "id": "http://example.edu/credentials/1872", + "info": "Info", + "issuanceDate": "2023-01-17T22:32:27.468109817+02:00", + "issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f", + "last_name": "Last name", + "type": "VerifiableCredential" + } +}` + // create VC - we will use template here + var vc map[string]interface{} + err = json.Unmarshal([]byte(localVc), &vc) + r.NoError(err) + + token, err := issuer.NewFromVC(vc, nil, signer, + issuer.WithHolderPublicKey(holderPublicJWK), + issuer.WithStructuredClaims(true), + //issuer.WithNonSelectivelyDisclosableClaims([]string{"id", "degree.type"}), + issuer.WithSDJWTVersion(common.SDJWTVersionV5), + ) + r.NoError(err) + + var decoded map[string]interface{} + + err = token.DecodeClaims(&decoded) + require.NoError(t, err) + + printObject(t, "SD-JWT Payload", decoded) + + vcCombinedFormatForIssuance, err := token.Serialize(false) + r.NoError(err) + + fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", vcCombinedFormatForIssuance)) + + claims, err := holder.Parse(vcCombinedFormatForIssuance, + holder.WithSignatureVerifier(signatureVerifier), + ) + r.NoError(err) + + printObject(t, "Holder Claims", claims) + + r.Equal(8, len(claims)) const testAudience = "https://test.com/verifier" const testNonce = "nonce" @@ -305,7 +411,7 @@ func TestSDJWTFlow(t *testing.T) { // Holder will disclose only sub-set of claims to verifier. combinedFormatForPresentation, err := holder.CreatePresentation(vcCombinedFormatForIssuance, selectedDisclosures, - holder.WithHolderBinding(&holder.BindingInfo{ + holder.WithHolderVerification(&holder.BindingInfo{ Payload: holder.BindingPayload{ Nonce: testNonce, Audience: testAudience, @@ -411,6 +517,7 @@ const sampleVCFull = ` "type": "BachelorDegree", "id": "some-id" }, + "arr" : ["a", "b"], "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", "name": "Jayden Doe", "spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1" diff --git a/component/models/sdjwt/issuer/issuer.go b/component/models/sdjwt/issuer/issuer.go index 9217c8554..e36b4e1ce 100644 --- a/component/models/sdjwt/issuer/issuer.go +++ b/component/models/sdjwt/issuer/issuer.go @@ -55,6 +55,7 @@ import ( "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk" + afgjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" jsonutil "github.com/hyperledger/aries-framework-go/component/models/util/json" @@ -62,8 +63,7 @@ import ( ) const ( - defaultHash = crypto.SHA256 - defaultSaltSize = 128 / 8 + defaultHash = crypto.SHA256 decoyMinElements = 1 decoyMaxElements = 4 @@ -98,12 +98,22 @@ type newOpts struct { addDecoyDigests bool structuredClaims bool - nonSDClaimsMap map[string]bool + nonSDClaimsMap map[string]bool + version common.SDJWTVersion + alwaysInclude map[string]bool + recursiveClaimMap map[string]bool } // NewOpt is the SD-JWT New option. type NewOpt func(opts *newOpts) +// WithSDJWTVersion sets version for SD-JWT VC. +func WithSDJWTVersion(version common.SDJWTVersion) NewOpt { + return func(opts *newOpts) { + opts.version = version + } +} + // WithJSONMarshaller is option is for marshalling disclosure. func WithJSONMarshaller(jsonMarshal func(v interface{}) ([]byte, error)) NewOpt { return func(opts *newOpts) { @@ -222,6 +232,111 @@ func WithNonSelectivelyDisclosableClaims(nonSDClaims []string) NewOpt { } } +// WithAlwaysIncludeObjects is an option for provide object keys that should be a part of +// selectively disclosable claims. +// Eexample if you would like to keep original claims structure from example below, but selectively disclose all claims +// +// { +// "degree": { +// "degree": "MIT", +// "type": "BachelorDegree", +// }, +// "name": "Jayden Doe", +// "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", +// } +// +// you should specify the following array: []string{"degree"}. +// As output, you will receive: +// +// { +// "_sd": [ +// "zDSZ9PKx_bB2CrFU8Xd__LkpMip06ApY-V6Y9fnppuo", +// "5Hnqg9PgQ4MdHxTv2KDt9qp8ILd1JEYq0luNO8JZ7G4" +// ], +// "degree": { +// "_sd": [ +// "i03SehlKmaFrwPM-gX8s3XuF_LTTE2T1XQQSJXjo6pw", +// "qZEZR8g_uc8fMyQCvs4DjXdY8uOI9IHpOokzx0cH_Qw" +// ] +// } +// } +func WithAlwaysIncludeObjects(alwaysIncludeObjects []string) NewOpt { + return func(opts *newOpts) { + opts.alwaysInclude = common.SliceToMap(alwaysIncludeObjects) + } +} + +// WithRecursiveClaimsObjects is an option for provide object keys that should be selective disclosed recursively, e.g. +// output digest for given object will refer to the disclosure, that contains digests of nested claims. +// For example if you would like to define degree object as selective disclosed recursively +// +// { +// "degree": { +// "degree": "MIT", +// "type": "BachelorDegree", +// }, +// "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", +// } +// +// you should specify the following array: []string{"degree"}. +// As output, you will receive: +// +// { +// "_sd": [ +// "fgoQstuIzTLQ4zqosjUC_qCk-xx3wjDQU2QkQtbn7FI", +// "mdephPRizMUa-LLs3JVeuTRS0tPaTd0faHg5kgKHNGk" +// ] +// } +// +// and 4 disclosures: +// nolint:lll +// [ +// +// { +// "Result": "WyJ2Y2g2YXVDVEo3bGdWWjFxNjN3cWF3IiwiZGVncmVlIix7Il9zZCI6WyJnZnNlcUhtTml0SXUwLTBoMTR5bnFNenV2cTFFaXJUQXpVaERuRWxTVlgwIiwiNDNoZm5NN1N6WnNhbEFkYlhReXE3dzRVdmQ1M1lPeFRORnBGSnI0WkcwQSJdfV0", +// "Salt": "vch6auCTJ7lgVZ1q63wqaw", +// "Key": "degree", +// "Value": { +// "_sd": [ +// "gfseqHmNitIu0-0h14ynqMzuvq1EirTAzUhDnElSVX0", +// "43hfnM7SzZsalAdbXQyq7w4Uvd53YOxTNFpFJr4ZG0A" +// ] +// }, +// "DebugStr": "[\"vch6auCTJ7lgVZ1q63wqaw\",\"degree\",{\"_sd\":[\"gfseqHmNitIu0-0h14ynqMzuvq1EirTAzUhDnElSVX0\",\"43hfnM7SzZsalAdbXQyq7w4Uvd53YOxTNFpFJr4ZG0A\"]}]", +// "DebugDigest": "mdephPRizMUa-LLs3JVeuTRS0tPaTd0faHg5kgKHNGk" +// }, +// { +// "Result": "WyJaVHFiUzI0ZWlybmpQMFlObmFmakxRIiwiaWQiLCJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiXQ", +// "Salt": "ZTqbS24eirnjP0YNnafjLQ", +// "Key": "id", +// "Value": "did:example:ebfeb1f712ebc6f1c276e12ec21", +// "DebugStr": "[\"ZTqbS24eirnjP0YNnafjLQ\",\"id\",\"did:example:ebfeb1f712ebc6f1c276e12ec21\"]", +// "DebugDigest": "fgoQstuIzTLQ4zqosjUC_qCk-xx3wjDQU2QkQtbn7FI" +// }, +// { +// "Result": "WyIyOEEzMmR0OW9JR0lLZW9iVEdIM2F3IiwiZGVncmVlIiwiTUlUIl0", +// "Salt": "28A32dt9oIGIKeobTGH3aw", +// "Key": "degree", +// "Value": "MIT", +// "DebugStr": "[\"28A32dt9oIGIKeobTGH3aw\",\"degree\",\"MIT\"]", +// "DebugDigest": "43hfnM7SzZsalAdbXQyq7w4Uvd53YOxTNFpFJr4ZG0A" +// }, +// { +// "Result": "WyJUNE8wRlZ2MDBpREhGNFZpYy0wR1VnIiwidHlwZSIsIkJhY2hlbG9yRGVncmVlIl0", +// "Salt": "T4O0FVv00iDHF4Vic-0GUg", +// "Key": "type", +// "Value": "BachelorDegree", +// "DebugStr": "[\"T4O0FVv00iDHF4Vic-0GUg\",\"type\",\"BachelorDegree\"]", +// "DebugDigest": "gfseqHmNitIu0-0h14ynqMzuvq1EirTAzUhDnElSVX0" +// } +// +// ]. +func WithRecursiveClaimsObjects(recursiveClaimsObject []string) NewOpt { + return func(opts *newOpts) { + opts.recursiveClaimMap = common.SliceToMap(recursiveClaimsObject) + } +} + // New creates new signed Selective Disclosure JWT based on input claims. // The Issuer MUST create a Disclosure for each selectively disclosable claim as follows: // Create an array of three elements in this order: @@ -238,9 +353,9 @@ func New(issuer string, claims interface{}, headers jose.Headers, signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) { nOpts := &newOpts{ jsonMarshal: json.Marshal, - getSalt: generateSalt, HashAlg: defaultHash, nonSDClaimsMap: make(map[string]bool), + version: common.SDJWTVersionDefault, } for _, opt := range opts { @@ -258,7 +373,12 @@ func New(issuer string, claims interface{}, headers jose.Headers, return nil, fmt.Errorf("key '%s' cannot be present in the claims", common.SDKey) } - disclosures, digests, err := createDisclosuresAndDigests("", claimsMap, nOpts) + sdJWTBuilder := getBuilderByVersion(nOpts.version) + if nOpts.getSalt == nil { + nOpts.getSalt = sdJWTBuilder.GenerateSalt + } + + disclosures, digests, err := sdJWTBuilder.CreateDisclosuresAndDigests("", claimsMap, nOpts) if err != nil { return nil, err } @@ -273,11 +393,16 @@ func New(issuer string, claims interface{}, headers jose.Headers, return nil, fmt.Errorf("failed to create SD-JWT from payload[%+v]: %w", payload, err) } - return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil + var disArr []string + for _, d := range disclosures { + disArr = append(disArr, d.Result) + } + + return &SelectiveDisclosureJWT{Disclosures: disArr, SignedJWT: signedJWT}, nil } /* -NewFromVC creates new signed Selective Disclosure JWT based on Verifiable Credential. +NewFromVC creates new signed Selective Disclosure JWT based on Verifiable Credential in map representation. Algorithm: - extract credential subject map from verifiable credential @@ -289,6 +414,14 @@ Algorithm: */ func NewFromVC(vc map[string]interface{}, headers jose.Headers, signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) { + nOpts := &newOpts{ + version: common.SDJWTVersionDefault, + } + + for _, opt := range opts { + opt(nOpts) + } + csObj, ok := common.GetKeyFromVC(credentialSubjectKey, vc) if !ok { return nil, fmt.Errorf("credential subject not found") @@ -304,20 +437,26 @@ func NewFromVC(vc map[string]interface{}, headers jose.Headers, return nil, err } + vcClaims, err := getBuilderByVersion(nOpts.version).ExtractCredentialClaims(vc) + if err != nil { + return nil, err + } + selectiveCredentialSubject := utils.CopyMap(token.SignedJWT.Payload) // move _sd_alg key from credential subject to vc as per example 4 in spec - vc[vcKey].(map[string]interface{})[common.SDAlgorithmKey] = selectiveCredentialSubject[common.SDAlgorithmKey] + vcClaims[common.SDAlgorithmKey] = selectiveCredentialSubject[common.SDAlgorithmKey] delete(selectiveCredentialSubject, common.SDAlgorithmKey) // move cnf key from credential subject to vc as per example 4 in spec cnfObj, ok := selectiveCredentialSubject[common.CNFKey] if ok { - vc[vcKey].(map[string]interface{})[common.CNFKey] = cnfObj + vcClaims[common.CNFKey] = cnfObj + delete(selectiveCredentialSubject, common.CNFKey) } // update VC with 'selective' credential subject - vc[vcKey].(map[string]interface{})[credentialSubjectKey] = selectiveCredentialSubject + vcClaims[credentialSubjectKey] = selectiveCredentialSubject // sign VC with 'selective' credential subject signedJWT, err := afgjwt.NewSigned(vc, headers, signer) @@ -353,11 +492,11 @@ func createPayload(issuer string, nOpts *newOpts) *payload { return payload } -func createDigests(disclosures []string, nOpts *newOpts) ([]string, error) { +func createDigests(disclosures []*DisclosureEntity, nOpts *newOpts) ([]string, error) { var digests []string for _, disclosure := range disclosures { - digest, inErr := common.GetHash(nOpts.HashAlg, disclosure) + digest, inErr := createDigest(disclosure, nOpts) if inErr != nil { return nil, fmt.Errorf("hash disclosure: %w", inErr) } @@ -372,14 +511,25 @@ func createDigests(disclosures []string, nOpts *newOpts) ([]string, error) { return digests, nil } -func createDecoyDisclosures(opts *newOpts) ([]string, error) { +func createDigest(disclosure *DisclosureEntity, nOpts *newOpts) (string, error) { + digest, inErr := common.GetHash(nOpts.HashAlg, disclosure.Result) + if inErr != nil { + return "", fmt.Errorf("hash disclosure: %w", inErr) + } + + disclosure.DebugDigest = digest + + return digest, nil +} + +func createDecoyDisclosures(opts *newOpts) ([]*DisclosureEntity, error) { if !opts.addDecoyDigests { return nil, nil } n := mr.Intn(decoyMaxElements-decoyMinElements+1) + decoyMinElements - var decoyDisclosures []string + var decoyDisclosures []*DisclosureEntity for i := 0; i < n; i++ { salt, err := opts.getSalt() @@ -387,7 +537,10 @@ func createDecoyDisclosures(opts *newOpts) ([]string, error) { return nil, err } - decoyDisclosures = append(decoyDisclosures, salt) + decoyDisclosures = append(decoyDisclosures, &DisclosureEntity{ + Salt: salt, + Result: salt, + }) } return decoyDisclosures, nil @@ -428,79 +581,8 @@ func (j *SelectiveDisclosureJWT) Serialize(detached bool) (string, error) { return cf.Serialize(), nil } -func createDisclosuresAndDigests(path string, claims map[string]interface{}, opts *newOpts) ([]string, map[string]interface{}, error) { // nolint:lll - var disclosures []string - - var levelDisclosures []string - - digestsMap := make(map[string]interface{}) - - decoyDisclosures, err := createDecoyDisclosures(opts) - if err != nil { - return nil, nil, fmt.Errorf("failed to create decoy disclosures: %w", err) - } - - for key, value := range claims { - curPath := key - if path != "" { - curPath = path + "." + key - } - - if obj, ok := value.(map[string]interface{}); ok && opts.structuredClaims { - nestedDisclosures, nestedDigestsMap, e := createDisclosuresAndDigests(curPath, obj, opts) - if e != nil { - return nil, nil, e - } - - digestsMap[key] = nestedDigestsMap - - disclosures = append(disclosures, nestedDisclosures...) - } else { - if _, ok := opts.nonSDClaimsMap[curPath]; ok { - digestsMap[key] = value - - continue - } - - disclosure, e := createDisclosure(key, value, opts) - if e != nil { - return nil, nil, fmt.Errorf("create disclosure: %w", e) - } - - levelDisclosures = append(levelDisclosures, disclosure) - } - } - - disclosures = append(disclosures, levelDisclosures...) - - digests, err := createDigests(append(levelDisclosures, decoyDisclosures...), opts) - if err != nil { - return nil, nil, err - } - - digestsMap[common.SDKey] = digests - - return disclosures, digestsMap, nil -} - -func createDisclosure(key string, value interface{}, opts *newOpts) (string, error) { - salt, err := opts.getSalt() - if err != nil { - return "", fmt.Errorf("generate salt: %w", err) - } - - disclosure := []interface{}{salt, key, value} - - disclosureBytes, err := opts.jsonMarshal(disclosure) - if err != nil { - return "", fmt.Errorf("marshal disclosure: %w", err) - } - - return base64.RawURLEncoding.EncodeToString(disclosureBytes), nil -} - -func generateSalt() (string, error) { - salt := make([]byte, defaultSaltSize) +func generateSalt(sizeBytes int) (string, error) { + salt := make([]byte, sizeBytes) _, err := rand.Read(salt) if err != nil { diff --git a/component/models/sdjwt/issuer/issuer_test.go b/component/models/sdjwt/issuer/issuer_test.go index 733c7e38a..99fe7d051 100644 --- a/component/models/sdjwt/issuer/issuer_test.go +++ b/component/models/sdjwt/issuer/issuer_test.go @@ -27,6 +27,7 @@ import ( afjose "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk/jwksupport" + afjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" ) @@ -159,7 +160,9 @@ func TestNew(t *testing.T) { WithID("id"), WithSubject("subject"), WithAudience("audience"), - WithSaltFnc(generateSalt), + WithSaltFnc(func() (string, error) { + return generateSalt(128 / 8) + }), WithJSONMarshaller(json.Marshal), WithHashAlgorithm(crypto.SHA256), ) @@ -382,6 +385,43 @@ func TestNew(t *testing.T) { } }) + t.Run("Create SD-JWS V5 with structured claims, recursive SD and SD array elements", func(t *testing.T) { + r := require.New(t) + + complexClaims := createComplexClaimsWithSlice() + + pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) + r.NoError(err) + + verifier, e := afjwt.NewEd25519Verifier(pubKey) + r.NoError(e) + + token, err := New(issuer, complexClaims, nil, afjwt.NewEd25519Signer(privKey), + WithSDJWTVersion(common.SDJWTVersionV5), + WithStructuredClaims(true), + WithAlwaysIncludeObjects([]string{"address.countryCodes", "address.extra"}), + WithNonSelectivelyDisclosableClaims([]string{"address.cities[1]", "address.region"}), + WithRecursiveClaimsObjects([]string{"address.extra.recursive"}), + ) + r.NoError(err) + combinedFormatForIssuance, err := token.Serialize(false) + r.NoError(err) + + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + r.Equal(6, len(cfi.Disclosures)) + + afjwtToken, _, err := afjwt.Parse(cfi.SDJWT, 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) + require.Empty(t, digests) + }) + t.Run("Create JWS with holder public key", func(t *testing.T) { r := require.New(t) @@ -580,6 +620,50 @@ func TestNewFromVC(t *testing.T) { r.Contains(err.Error(), "unknown key id") }) + t.Run("success - structured claims + holder binding + SD JWT V5 format", func(t *testing.T) { + holderPublicKey, _, err := ed25519.GenerateKey(rand.Reader) + r.NoError(err) + + holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey) + require.NoError(t, err) + + // create VC - we will use template here + var vc map[string]interface{} + err = json.Unmarshal([]byte(sampleSDJWTV5Full), &vc) + r.NoError(err) + + token, err := NewFromVC(vc, nil, signer, + WithHolderPublicKey(holderPublicJWK), + WithStructuredClaims(true), + WithNonSelectivelyDisclosableClaims([]string{"id", "degree.type"}), + WithSDJWTVersion(common.SDJWTVersionV5)) + r.NoError(err) + + vcCombinedFormatForIssuance, err := token.Serialize(false) + r.NoError(err) + + fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", vcCombinedFormatForIssuance)) + + var vcWithSelectedDisclosures map[string]interface{} + err = token.DecodeClaims(&vcWithSelectedDisclosures) + r.NoError(err) + + printObject(t, "VC with selected disclosures", vcWithSelectedDisclosures) + + id, err := jsonpath.Get("$.credentialSubject.id", vcWithSelectedDisclosures) + r.NoError(err) + r.Equal("did:example:ebfeb1f712ebc6f1c276e12ec21", id) + + degreeType, err := jsonpath.Get("$.credentialSubject.degree.type", vcWithSelectedDisclosures) + r.NoError(err) + r.Equal("BachelorDegree", degreeType) + + degreeID, err := jsonpath.Get("$.credentialSubject.degree.id", vcWithSelectedDisclosures) + r.Error(err) + r.Nil(degreeID) + r.Contains(err.Error(), "unknown key id") + }) + t.Run("success - flat claims + holder binding", func(t *testing.T) { holderPublicKey, _, err := ed25519.GenerateKey(rand.Reader) r.NoError(err) @@ -733,11 +817,11 @@ func TestJSONWebToken_createDisclosure(t *testing.T) { expectedDisclosureWithSpaces := "WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd" expectedHashWithSpaces := expectedHashWithSpaces - disclosure, err := createDisclosure("given_name", "John", nOpts) + disclosure, err := NewSDJWTBuilderV2().createDisclosure("given_name", "John", nOpts) require.NoError(t, err) - require.Equal(t, expectedDisclosureWithSpaces, disclosure) + require.Equal(t, expectedDisclosureWithSpaces, disclosure.Result) - dh, err := common.GetHash(defaultHash, disclosure) + dh, err := common.GetHash(defaultHash, disclosure.Result) require.NoError(t, err) require.Equal(t, expectedHashWithSpaces, dh) }) @@ -753,9 +837,9 @@ func TestJSONWebToken_createDisclosure(t *testing.T) { return "_26bc4LT-ac6q2KI6cBW5es", nil })) - disclosure, err := createDisclosure("family_name", "Möbius", nOpts) + disclosure, err := NewSDJWTBuilderV2().createDisclosure("family_name", "Möbius", nOpts) require.NoError(t, err) - require.Equal(t, expectedDisclosureWithoutSpaces, disclosure) + require.Equal(t, expectedDisclosureWithoutSpaces, disclosure.Result) nOpts = getOpts( WithJSONMarshaller(jsonMarshalWithSpace), @@ -763,16 +847,15 @@ func TestJSONWebToken_createDisclosure(t *testing.T) { return "_26bc4LT-ac6q2KI6cBW5es", nil })) - disclosure, err = createDisclosure("family_name", "Möbius", nOpts) + disclosure, err = NewSDJWTBuilderV2().createDisclosure("family_name", "Möbius", nOpts) require.NoError(t, err) - require.Equal(t, expectedDisclosureWithSpaces, disclosure) + require.Equal(t, expectedDisclosureWithSpaces, disclosure.Result) }) } func getOpts(opts ...NewOpt) *newOpts { nOpts := &newOpts{ jsonMarshal: json.Marshal, - getSalt: generateSalt, HashAlg: defaultHash, } @@ -866,6 +949,24 @@ func createComplexClaims() map[string]interface{} { return claims } +func createComplexClaimsWithSlice() map[string]interface{} { + claims := map[string]interface{}{ + "address": map[string]interface{}{ + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "countryCodes": []string{"UA", "PL"}, + "cities": []string{"Albuquerque", "El Paso"}, + "extra": map[string]interface{}{ + "recursive": map[string]interface{}{ + "key1": "value1", + }, + }, + }, + } + + return claims +} + func verifyEd25519(jws string, pubKey ed25519.PublicKey) error { v, err := afjwt.NewEd25519Verifier(pubKey) if err != nil { @@ -1014,3 +1115,32 @@ const sampleVCFull = ` "type": "VerifiableCredential" } }` + +const sampleSDJWTV5Full = ` +{ + "iat": 1673987547, + "iss": "did:example:76e12ec712ebc6f1c221ebfeb1f", + "jti": "http://example.edu/credentials/1872", + "nbf": 1673987547, + "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "credentialSubject": { + "degree": { + "degree": "MIT", + "type": "BachelorDegree", + "id": "some-id" + }, + "name": "Jayden Doe", + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1" + }, + "first_name": "First name", + "id": "http://example.edu/credentials/1872", + "info": "Info", + "issuanceDate": "2023-01-17T22:32:27.468109817+02:00", + "issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f", + "last_name": "Last name", + "type": "VerifiableCredential" +}` diff --git a/component/models/sdjwt/issuer/v2.go b/component/models/sdjwt/issuer/v2.go new file mode 100644 index 000000000..175e3acf3 --- /dev/null +++ b/component/models/sdjwt/issuer/v2.go @@ -0,0 +1,145 @@ +/* +Copyright Avast Software. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package issuer + +import ( + "encoding/base64" + "errors" + "fmt" + + "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" +) + +type builder interface { + CreateDisclosuresAndDigests( + path string, + claims map[string]interface{}, + opts *newOpts, + ) ([]*DisclosureEntity, map[string]interface{}, error) + ExtractCredentialClaims(vcClaims map[string]interface{}) (map[string]interface{}, error) + GenerateSalt() (string, error) +} + +func getBuilderByVersion( + version common.SDJWTVersion, +) builder { + switch version { + case common.SDJWTVersionV5: + return NewSDJWTBuilderV5() + default: + return NewSDJWTBuilderV2() + } +} + +// SDJWTBuilderV2 represents builder struct for SD-JWT v2 spec. +type SDJWTBuilderV2 struct { + defaultSaltSize int +} + +// GenerateSalt generates salt. +func (s *SDJWTBuilderV2) GenerateSalt() (string, error) { + return generateSalt(s.defaultSaltSize) +} + +// NewSDJWTBuilderV2 returns new instance of SDJWTBuilderV2. +func NewSDJWTBuilderV2() *SDJWTBuilderV2 { + return &SDJWTBuilderV2{ + defaultSaltSize: 128 / 8, + } +} + +// CreateDisclosuresAndDigests creates disclosures and digests. +func (s *SDJWTBuilderV2) CreateDisclosuresAndDigests( + path string, + claims map[string]interface{}, + opts *newOpts, +) ([]*DisclosureEntity, map[string]interface{}, error) { // nolint:lll + var disclosures []*DisclosureEntity + + var levelDisclosures []*DisclosureEntity + + digestsMap := make(map[string]interface{}) + + decoyDisclosures, err := createDecoyDisclosures(opts) + if err != nil { + return nil, nil, fmt.Errorf("failed to create decoy disclosures: %w", err) + } + + for key, value := range claims { + curPath := key + if path != "" { + curPath = path + "." + key + } + + if obj, ok := value.(map[string]interface{}); ok && opts.structuredClaims { + nestedDisclosures, nestedDigestsMap, e := s.CreateDisclosuresAndDigests(curPath, obj, opts) + if e != nil { + return nil, nil, e + } + + digestsMap[key] = nestedDigestsMap + + disclosures = append(disclosures, nestedDisclosures...) + } else { + if _, ok := opts.nonSDClaimsMap[curPath]; ok { + digestsMap[key] = value + + continue + } + + disclosure, e := s.createDisclosure(key, value, opts) + if e != nil { + return nil, nil, fmt.Errorf("create disclosure: %w", e) + } + + levelDisclosures = append(levelDisclosures, disclosure) + } + } + + disclosures = append(disclosures, levelDisclosures...) + + digests, err := createDigests(append(levelDisclosures, decoyDisclosures...), opts) + if err != nil { + return nil, nil, err + } + + digestsMap[common.SDKey] = digests + + return disclosures, digestsMap, nil +} + +func (s *SDJWTBuilderV2) createDisclosure( + key string, + value interface{}, + opts *newOpts, +) (*DisclosureEntity, error) { + salt, err := opts.getSalt() + if err != nil { + return nil, fmt.Errorf("generate salt: %w", err) + } + + disclosure := []interface{}{salt, key, value} + + disclosureBytes, err := opts.jsonMarshal(disclosure) + if err != nil { + return nil, fmt.Errorf("marshal disclosure: %w", err) + } + + return &DisclosureEntity{ + Result: base64.RawURLEncoding.EncodeToString(disclosureBytes), + }, nil +} + +// ExtractCredentialClaims extracts credential claims. +func (s *SDJWTBuilderV2) ExtractCredentialClaims(vcClaims map[string]interface{}) (map[string]interface{}, error) { + vc, ok := vcClaims[vcKey].(map[string]interface{}) + if !ok { + return nil, errors.New("invalid vc claim") + } + + return vc, nil +} diff --git a/component/models/sdjwt/issuer/v5.go b/component/models/sdjwt/issuer/v5.go new file mode 100644 index 000000000..eaff2a741 --- /dev/null +++ b/component/models/sdjwt/issuer/v5.go @@ -0,0 +1,341 @@ +/* +Copyright Avast Software. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package issuer + +import ( + "encoding/base64" + "errors" + "fmt" + "reflect" + + "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" +) + +// SDJWTBuilderV5 represents builder struct for SD-JWT v5 spec. +type SDJWTBuilderV5 struct { + debugMode bool + saltSize int +} + +// GenerateSalt generates salt. +func (s *SDJWTBuilderV5) GenerateSalt() (string, error) { + return generateSalt(s.saltSize) +} + +// NewSDJWTBuilderV5 returns new instance of SDJWTBuilderV5. +func NewSDJWTBuilderV5() *SDJWTBuilderV5 { + return &SDJWTBuilderV5{ + saltSize: 128 / 8, + } +} + +func (s *SDJWTBuilderV5) isAlwaysInclude(curPath string, opts *newOpts) bool { + if opts == nil || len(opts.alwaysInclude) == 0 { + return false + } + + _, ok := opts.alwaysInclude[curPath] + + return ok +} + +func (s *SDJWTBuilderV5) isIgnored(curPath string, opts *newOpts) bool { + if opts == nil || len(opts.nonSDClaimsMap) == 0 { + return false + } + + _, ok := opts.nonSDClaimsMap[curPath] + + return ok +} + +func (s *SDJWTBuilderV5) isRecursive(curPath string, opts *newOpts) bool { + if opts == nil || len(opts.recursiveClaimMap) == 0 { + return false + } + + _, ok := opts.recursiveClaimMap[curPath] + + return ok +} + +func (s *SDJWTBuilderV5) extractValueOptions(curPath string, opts *newOpts) valueOption { + return valueOption{ + IsStructured: opts.structuredClaims, + IsAlwaysInclude: s.isAlwaysInclude(curPath, opts), + IsIgnored: s.isIgnored(curPath, opts), + IsRecursive: s.isRecursive(curPath, opts), + } +} + +type valueOption struct { + IsStructured bool + IsAlwaysInclude bool + IsIgnored bool + IsRecursive bool +} + +// CreateDisclosuresAndDigests creates disclosures and digests. +func (s *SDJWTBuilderV5) CreateDisclosuresAndDigests( // nolint:funlen,gocyclo + path string, + claims map[string]interface{}, + opts *newOpts, +) ([]*DisclosureEntity, map[string]interface{}, error) { + return s.createDisclosuresAndDigestsInternal(path, claims, opts, false) +} + +//nolint:funlen,gocyclo +func (s *SDJWTBuilderV5) createDisclosuresAndDigestsInternal( + path string, + claims map[string]interface{}, + opts *newOpts, + ignorePrimitives bool, +) ([]*DisclosureEntity, map[string]interface{}, error) { + digestsMap := map[string]interface{}{} + finalSDDigest, err := createDecoyDisclosures(opts) + + if err != nil { + return nil, nil, fmt.Errorf("failed to create decoy disclosures: %w", err) + } + + var allDisclosures []*DisclosureEntity + + for key, value := range claims { + curPath := key + if path != "" { + curPath = path + "." + key + } + + kind := reflect.TypeOf(value).Kind() + + valOption := s.extractValueOptions(curPath, opts) + + switch kind { + case reflect.Map: + if valOption.IsIgnored { // nolint:nestif + digestsMap[key] = value + } else if valOption.IsRecursive { + nestedDisclosures, nestedDigestsMap, mapErr := s.createDisclosuresAndDigestsInternal( + curPath, + value.(map[string]interface{}), + opts, + false, + ) + if mapErr != nil { + return nil, nil, mapErr + } + + disclosure, disErr := s.createDisclosure(key, nestedDigestsMap, opts) + + if disErr != nil { + return nil, nil, fmt.Errorf( + "create disclosure for recursive disclosure value with path [%v]: %w", + path, disErr) + } + + if valOption.IsAlwaysInclude { + digestsMap[key] = nestedDigestsMap + } else { + finalSDDigest = append(finalSDDigest, disclosure) + } + + allDisclosures = append(allDisclosures, nestedDisclosures...) + } else if valOption.IsAlwaysInclude || valOption.IsStructured { + nestedDisclosures, nestedDigestsMap, mapErr := s.createDisclosuresAndDigestsInternal( + curPath, + value.(map[string]interface{}), + opts, + false, + ) + if mapErr != nil { + return nil, nil, mapErr + } + + digestsMap[key] = nestedDigestsMap + + allDisclosures = append(allDisclosures, nestedDisclosures...) + } else { // plain + nestedDisclosures, nestedDigestsMap, mapErr := s.createDisclosuresAndDigestsInternal( + curPath, + value.(map[string]interface{}), + opts, + true, + ) + if mapErr != nil { + return nil, nil, mapErr + } + + disclosure, disErr := s.createDisclosure(key, nestedDigestsMap, opts) + if disErr != nil { + return nil, nil, fmt.Errorf("create disclosure for map object [%v]: %w", + path, disErr) + } + + finalSDDigest = append(finalSDDigest, disclosure) + allDisclosures = append(allDisclosures, nestedDisclosures...) + } + case reflect.Array: + fallthrough + case reflect.Slice: + if valOption.IsIgnored { // whole array ignored + digestsMap[key] = value + continue + } + + elementsDigest, elementsDisclosures, arrayElemErr := s.processArrayElements(value, curPath, opts) + if arrayElemErr != nil { + return nil, nil, arrayElemErr + } + + if valOption.IsAlwaysInclude || valOption.IsStructured { + digestsMap[key] = elementsDigest + } else { // plain + disclosure, disErr := s.createDisclosure(key, elementsDigest, opts) + if disErr != nil { + return nil, nil, fmt.Errorf("create disclosure for whole array err with path [%v]: %w", + path, disErr) + } + + finalSDDigest = append(finalSDDigest, disclosure) + } + + allDisclosures = append(allDisclosures, elementsDisclosures...) + default: + if valOption.IsIgnored || ignorePrimitives { + digestsMap[key] = value + continue + } + + disclosure, disErr := s.createDisclosure(key, value, opts) + + if disErr != nil { + return nil, nil, fmt.Errorf("create disclosure for simple value with path [%v]: %w", + path, disErr) + } + + finalSDDigest = append(finalSDDigest, disclosure) + } + } + + digests, err := createDigests(finalSDDigest, opts) + + if err != nil { + return nil, nil, err + } + + if len(digests) > 0 { + digestsMap[common.SDKey] = digests + } + + return append(finalSDDigest, allDisclosures...), digestsMap, nil +} + +func (s *SDJWTBuilderV5) processArrayElements( + value interface{}, + path string, + opts *newOpts, +) ([]interface{}, []*DisclosureEntity, error) { + valSl := reflect.ValueOf(value) + + var digestArr []interface{} + + var elementsDisclosures []*DisclosureEntity + + for i := 0; i < valSl.Len(); i++ { + elementPath := fmt.Sprintf("%v[%v]", path, i) + elementOptions := s.extractValueOptions(elementPath, opts) + elementValue := valSl.Index(i).Interface() + + if elementOptions.IsIgnored { + digestArr = append(digestArr, elementValue) + continue + } + + disclosure, err := s.createDisclosure("", elementValue, opts) + if err != nil { + return nil, nil, + fmt.Errorf("create element disclosure for path [%v]: %w", elementPath, err) + } + + digest, err := createDigest(disclosure, opts) + if err != nil { + return nil, nil, + fmt.Errorf("can not create digest for array element [%v]: %w", elementPath, err) + } + + elementsDisclosures = append(elementsDisclosures, disclosure) + digestArr = append(digestArr, map[string]string{common.ArrayElementDigestKey: digest}) + } + + return digestArr, elementsDisclosures, nil +} + +func (s *SDJWTBuilderV5) createDisclosure( + key string, + value interface{}, + opts *newOpts, +) (*DisclosureEntity, error) { + if opts.getSalt == nil { + return nil, errors.New("missing salt function") + } + + salt, err := opts.getSalt() + + if err != nil { + return nil, fmt.Errorf("generate salt: %w", err) + } + + finalDis := &DisclosureEntity{ + Salt: salt, + } + disclosure := []interface{}{salt} + + if key != "" { + disclosure = append(disclosure, key) + } + + disclosure = append(disclosure, value) + + disclosureBytes, err := opts.jsonMarshal(disclosure) + if err != nil { + return nil, fmt.Errorf("marshal disclosure: %w", err) + } + + finalDis.Key = key + finalDis.Value = value + finalDis.Result = base64.RawURLEncoding.EncodeToString(disclosureBytes) + + if s.debugMode { + finalDis.DebugArr = disclosure + finalDis.DebugStr = string(disclosureBytes) + } + + return finalDis, nil +} + +// DisclosureEntity represents disclosure with extra information. +type DisclosureEntity struct { + Result string + Salt string + Key string + Value interface{} + DebugArr []interface{} `json:"-"` + DebugStr string + DebugDigest string +} + +// ExtractCredentialClaims extracts credential claims. +func (s *SDJWTBuilderV5) ExtractCredentialClaims( + vc map[string]interface{}, +) (map[string]interface{}, error) { + vcClaims, ok := vc[vcKey].(map[string]interface{}) + if ok { + return vcClaims, nil + } + + return vc, nil +} diff --git a/component/models/sdjwt/issuer/v5_test.go b/component/models/sdjwt/issuer/v5_test.go new file mode 100644 index 000000000..2dc5d102b --- /dev/null +++ b/component/models/sdjwt/issuer/v5_test.go @@ -0,0 +1,684 @@ +/* +Copyright Avast Software. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package issuer + +import ( + "crypto" + "encoding/json" + "errors" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" +) + +const ( + sampleV5ComplexData = `{ + "address": { + "street_address": "Schulstr. 12", + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "country": "DE", + "extraArrInclude" : ["UA", "PL"], + "extraArr" : ["Extra1", "Extra2"], + "extra" : { + "recursive" : { + "key1" : "value1" + } + } + } + }` + sampleV5AddressMapTestData = `{ + "address": { + "street_address": "Schulstr. 12", + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "country": { + "code" : "DE" + } + } +}` + sampleV5TestData = `{ + "some_map": { + "a" : "b" + }, + "nationalities": [ + "US", + "DE" + ] + }` + simpleV5TestData = `{ + "some_arr" : ["UA"] + }` + arrayTwoElementsV5TestData = `{ + "some_arr" : ["UA", "PL"] + }` + addressV5TestData = `{ + "address": { + "postal_code": "12345", + "locality": "Irgendwo", + "street_address": "Sonnenstrasse 23", + "country_code": "DE" + } + }` +) + +func TestDisclosureV5Map( + t *testing.T, +) { + t.Run("recursive", func(t *testing.T) { + input := `{ + "address": { + "street_address": "Schulstr. 12", + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "country": "DE" + } +}` + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(input), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + recursiveClaimMap: map[string]bool{ + "address": true, + }, + }) + assert.NoError(t, err) + + sort.Slice(disclosures, func(i, j int) bool { + return disclosures[i].Key < disclosures[j].Key + }) + + assert.Len(t, disclosures, 5) + + for _, dis := range disclosures { + assert.NotEmpty(t, dis.Salt) + assert.NotEmpty(t, dis.Result) + } + + assert.Equal(t, "address", disclosures[0].Key) + assert.Equal(t, "country", disclosures[1].Key) + assert.Equal(t, "DE", disclosures[1].Value) + assert.Equal(t, "locality", disclosures[2].Key) + assert.Equal(t, "Schulpforta", disclosures[2].Value) + assert.Equal(t, "region", disclosures[3].Key) + assert.Equal(t, "Sachsen-Anhalt", disclosures[3].Value) + assert.Equal(t, "street_address", disclosures[4].Key) + assert.Equal(t, "Schulstr. 12", disclosures[4].Value) + + recursiveElements := disclosures[0].Value.(map[string]interface{})["_sd"].([]string) // nolint:errcheck + assert.Len(t, recursiveElements, 4) + + for _, expected := range []string{ + disclosures[1].DebugDigest, + disclosures[2].DebugDigest, + disclosures[3].DebugDigest, + disclosures[4].DebugDigest, + } { + assert.True(t, slices.Contains(recursiveElements, expected)) + } + assert.Len(t, cred, 1) + sd := cred["_sd"].([]string) // nolint:errcheck + assert.Len(t, sd, 1) + assert.Equal(t, disclosures[0].DebugDigest, sd[0]) + }) + + t.Run("recursive with array and and include always", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleV5ComplexData), &parsedInput)) + bb := NewSDJWTBuilderV5() + bb.debugMode = true + + disclosures, finalMap, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + alwaysInclude: map[string]bool{ + "address.extraArrInclude": true, + "address.extra": true, + }, + nonSDClaimsMap: map[string]bool{ + "address.extraArrInclude[1]": true, + "address.region": true, + }, + recursiveClaimMap: map[string]bool{ + "address": true, + "address.extra.recursive": true, + }, + }) + assert.NoError(t, err) + + var disString []string + for _, d := range disclosures { + disString = append(disString, d.Result) + } + printObject(t, "raw dis", disString) + + printObject(t, "final credentials", finalMap) + printObject(t, "disclosures", disclosures) + + assert.Len(t, disclosures, 10) + assert.Len(t, finalMap, 1) + + disMap := map[string]*DisclosureEntity{} + for _, d := range disclosures { + disMap[d.DebugDigest] = d + } + + sdID := finalMap["_sd"].([]string)[0] + assert.NotEmpty(t, sdID) + val := disMap[sdID] + recursiveData := val.Value.(map[string]interface{})["extra"].(map[string]interface{})["_sd"].([]string)[0] + recursiveDis := disMap[recursiveData] + + assert.Equal(t, "recursive", recursiveDis.Key) + itemData := recursiveDis.Value.(map[string]interface{})["_sd"].([]string)[0] + itemDis := disMap[itemData] + assert.Equal(t, "key1", itemDis.Key) + assert.Equal(t, "value1", itemDis.Value) + }) +} + +func TestDisclosureV5Array( + t *testing.T, +) { + t.Run("always visible", func(t *testing.T) { + input := `{ + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "phone_number_verified": true, + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "updated_at": 1570000000, + "nationalities": [ + "US", + "DE" + ], + "visible_map" : { + "a" : "b" + } + }` + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(input), &parsedInput)) + bb := NewSDJWTBuilderV5() + bb.debugMode = true + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + alwaysInclude: map[string]bool{ + "nationalities": true, + "visible_map": true, + }, + }) + + assert.NoError(t, err) + assert.Len(t, disclosures, 11) + disMap := map[string]*DisclosureEntity{} + for _, d := range disclosures { + disMap[d.DebugDigest] = d + } + for _, expectedArrayElements := range []string{"DE", "US"} { + found := false + for _, d := range disclosures { + if d.Key != "" { // for array elements key is empty + continue + } + + found = d.Value == expectedArrayElements // exact + if found { + break + } + } + + assert.True(t, found, "element %v not found", expectedArrayElements) + } + + visibleMapData := cred["visible_map"].(map[string]interface{})["_sd"].([]string) // nolint:errcheck + assert.Len(t, visibleMapData, 1) + + visibleDisclosure := disMap[visibleMapData[0]] + assert.Equal(t, "a", visibleDisclosure.Key) + assert.Equal(t, "b", visibleDisclosure.Value) + + nationalities := cred["nationalities"].([]interface{}) // nolint:errcheck + assert.Len(t, nationalities, 2) + + for i, nat := range nationalities { + value := nat.(map[string]string)["..."] + assert.NotEmpty(t, value) + + element := disMap[value] + assert.Empty(t, element.Key) + if i == 0 { + assert.Equal(t, "US", element.Value) + } else { + assert.Equal(t, "DE", element.Value) + } + } + assert.NotNil(t, disclosures, cred) + }) + + t.Run("one array element ignored", func(t *testing.T) { + input := `{ + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "phone_number_verified": true, + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "updated_at": 1570000000, + "nationalities": [ + "US", + "DE" + ] + }` + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(input), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + nonSDClaimsMap: map[string]bool{ + "nationalities[1]": true, + }, + alwaysInclude: map[string]bool{ + "nationalities": true, + }, + }) + assert.NoError(t, err) + + disMap := map[string]*DisclosureEntity{} + for _, d := range disclosures { + disMap[d.DebugDigest] = d + } + + assert.Len(t, disclosures, 9) + assert.Len(t, cred["_sd"].([]string), 8) + + nat := cred["nationalities"].([]interface{}) // nolint:errcheck + assert.Len(t, nat, 2) + + nat1Val := nat[0].(map[string]string)["..."] + nat2Val := nat[1].(string) // nolint:errcheck + + assert.Equal(t, "DE", nat2Val) + assert.Equal(t, "US", disMap[nat1Val].Value) + }) + + t.Run("one array element ignored", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + nonSDClaimsMap: map[string]bool{ + "some_map": true, + "nationalities": true, + }, + }) + assert.NoError(t, err) + + assert.Len(t, disclosures, 0) + assert.Len(t, cred, 2) + assert.Equal(t, map[string]interface{}{ + "a": "b", + }, cred["some_map"]) + assert.Equal(t, []interface{}{"US", "DE"}, cred["nationalities"].([]interface{})) + disMap := map[string]*DisclosureEntity{} + for _, d := range disclosures { + disMap[d.DebugDigest] = d + } + }) +} + +func TestFailCases(t *testing.T) { + t.Run("map object", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + nonSDClaimsMap: map[string]bool{ + "nationalities": true, + }, + }) + assert.ErrorContains(t, err, "create disclosure for map object []: missing salt function") + assert.Nil(t, disclosures, cred) + }) + + t.Run("map object", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + nonSDClaimsMap: map[string]bool{ + "some_map": true, + }, + }) + assert.ErrorContains(t, err, "create element disclosure for path [nationalities[0]]: "+ + "missing salt function") + assert.Nil(t, disclosures, cred) + }) + + t.Run("simple value", func(t *testing.T) { + input := `{ + "a" : "b" + }` + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(input), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + }) + assert.ErrorContains(t, err, "create disclosure for simple value with path []: missing salt function") + assert.Nil(t, disclosures, cred) + }) + + t.Run("map object recursive", func(t *testing.T) { + input := `{ + "some_map": { + "a" : "b" + } + }` + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(input), &parsedInput)) + bb := NewSDJWTBuilderV5() + + i := 0 + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: func() (string, error) { + i++ + if i == 1 { + return generateSalt(128) + } + + return "", errors.New("can not create salt") + }, + recursiveClaimMap: map[string]bool{ + "some_map": true, + }, + }) + assert.ErrorContains(t, err, "create disclosure for recursive disclosure value with path []: "+ + "generate salt: can not create salt") + assert.Nil(t, disclosures, cred) + }) + + t.Run("disclosure for slice", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(simpleV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + i := 0 + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: func() (string, error) { + i++ + if i == 1 { + return generateSalt(128) + } + + return "", errors.New("can not create salt") + }, + }) + assert.ErrorContains(t, err, "create disclosure for whole array err with path []: generate salt: "+ + "can not create salt") + assert.Nil(t, disclosures, cred) + }) + + t.Run("can not create decoy", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(simpleV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + addDecoyDigests: true, + getSalt: func() (string, error) { + return "", errors.New("can not create salt") + }, + }) + assert.ErrorContains(t, err, "failed to create decoy disclosures: can not create salt") + assert.Nil(t, disclosures, cred) + }) + + t.Run("can not create decoy", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(simpleV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: crypto.MD5SHA1, + addDecoyDigests: true, + getSalt: bb.GenerateSalt, + }) + assert.ErrorContains(t, err, "can not create digest for array element [some_arr[0]]: "+ + "hash disclosure: hash function not available") + assert.Nil(t, disclosures, cred) + }) +} + +func TestExamplesV5( + t *testing.T, +) { + t.Run("test array disclosures", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(arrayTwoElementsV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + }) + assert.NoError(t, err) + + disMap := map[string]*DisclosureEntity{} + for _, d := range disclosures { + disMap[d.DebugDigest] = d + } + + assert.Len(t, disclosures, 3) + assert.Equal(t, "some_arr", disclosures[0].Key) + + addressArr := disclosures[0].Value.([]interface{}) // nolint:errcheck + assert.Len(t, addressArr, 2) + + assert.Equal(t, "UA", disMap[addressArr[0].(map[string]string)["..."]].Value) + assert.Equal(t, "PL", disMap[addressArr[1].(map[string]string)["..."]].Value) + + assert.Len(t, cred, 1) + assert.Len(t, cred["_sd"].([]string), 1) + }) + t.Run("test array disclosures with one ignored", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(arrayTwoElementsV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + bb.debugMode = true + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + nonSDClaimsMap: map[string]bool{ + "some_arr[0]": true, + }, + }) + assert.NoError(t, err) + + disMap := map[string]*DisclosureEntity{} + for _, d := range disclosures { + disMap[d.DebugDigest] = d + } + + assert.Len(t, disclosures, 2) + assert.Equal(t, "some_arr", disclosures[0].Key) + + addressArr := disclosures[0].Value.([]interface{}) // nolint:errcheck + assert.Len(t, addressArr, 2) + + assert.Equal(t, "UA", addressArr[0]) + assert.Equal(t, "PL", disMap[addressArr[1].(map[string]string)["..."]].Value) + + assert.Len(t, cred, 1) + assert.Len(t, cred["_sd"].([]string), 1) + }) + t.Run("always include", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(addressV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + alwaysInclude: map[string]bool{ + "address": true, + }, + }) + assert.NoError(t, err) + + assert.Len(t, disclosures, 4) + assert.Len(t, cred["address"].(map[string]interface{})["_sd"].([]string), 4) + assert.Len(t, cred, 1) + }) + t.Run("non sd claim", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(addressV5TestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + nonSDClaimsMap: map[string]bool{ + "address": true, + }, + }) + assert.NoError(t, err) + + assert.Len(t, disclosures, 0) + assert.Len(t, cred["address"].(map[string]interface{}), 4) + assert.Equal(t, cred["address"].(map[string]interface{})["postal_code"], "12345") + assert.Len(t, cred, 1) + }) + t.Run("map address", func(t *testing.T) { + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(sampleV5AddressMapTestData), &parsedInput)) + bb := NewSDJWTBuilderV5() + bb.debugMode = true + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + alwaysInclude: map[string]bool{ + "address.country": true, + }, + recursiveClaimMap: map[string]bool{ + "address.country": true, + }, + }) + assert.NoError(t, err) + + disMap := map[string]*DisclosureEntity{} + for _, d := range disclosures { + disMap[d.DebugDigest] = d + } + + assert.NotNil(t, disclosures, cred) + assert.Len(t, disclosures, 2) + assert.Len(t, cred, 1) + + address := disMap[cred["_sd"].([]string)[0]] + assert.Len(t, address.Value, 4) + assert.Equal(t, "Schulstr. 12", address.Value.(map[string]interface{})["street_address"].(string)) + assert.Equal(t, "Schulpforta", address.Value.(map[string]interface{})["locality"].(string)) + assert.Equal(t, "Sachsen-Anhalt", address.Value.(map[string]interface{})["region"].(string)) + + country := address.Value.(map[string]interface{})["country"].(map[string]interface{}) // nolint:errcheck + disCountry := disMap[country["_sd"].([]string)[0]] + assert.Equal(t, "code", disCountry.Key) + assert.Equal(t, "DE", disCountry.Value) + + printObject(t, "final credentials", cred) + printObject(t, "disclosures", disclosures) + }) + t.Run("4a", func(t *testing.T) { + t.Run("recursive", func(t *testing.T) { + // https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#name-example-4a-sd-jwt-based-ver + // in example they have 3 items in _sd - decoy + input := `{ + "first_name": "Erika", + "family_name": "Mustermann", + "nationalities": [ + "DE" + ], + "birth_family_name": "Schmidt", + "birthdate": "1973-01-01", + "address": { + "postal_code": "12345", + "locality": "Irgendwo", + "street_address": "Sonnenstrasse 23", + "country_code": "DE" + }, + "is_over_18": true, + "is_over_21": true, + "is_over_65": false +}` + var parsedInput map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(input), &parsedInput)) + bb := NewSDJWTBuilderV5() + + disclosures, cred, err := bb.CreateDisclosuresAndDigests("", parsedInput, &newOpts{ + jsonMarshal: json.Marshal, + HashAlg: defaultHash, + getSalt: bb.GenerateSalt, + }) + assert.NoError(t, err) + + assert.Len(t, disclosures, 10) + assert.Len(t, cred["_sd"].([]string), 9) // -1 for array element + assert.Len(t, cred, 1) + }) + }) +} diff --git a/component/models/sdjwt/verifier/holderbidning.go b/component/models/sdjwt/verifier/holderbidning.go new file mode 100644 index 000000000..0b45fe909 --- /dev/null +++ b/component/models/sdjwt/verifier/holderbidning.go @@ -0,0 +1,63 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +/* +Package verifier enables the Verifier: An entity that requests, checks and +extracts the claims from an SD-JWT and respective Disclosures. +*/ + +package verifier + +import ( + "fmt" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/mitchellh/mapstructure" + + afgjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" + utils "github.com/hyperledger/aries-framework-go/component/models/util/maphelpers" +) + +// verifyHolderBindingJWT verifies holder binding JWT. +// Section: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#section-6.3-4.3.1 +func verifyHolderBindingJWT(holderJWT *afgjwt.JSONWebToken, pOpts *parseOpts) error { + var bindingPayload holderBindingPayload + + d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &bindingPayload, + TagName: "json", + Squash: true, + WeaklyTypedInput: true, + DecodeHook: utils.JSONNumberToJwtNumericDate(), + }) + if err != nil { + return fmt.Errorf("mapstruct verifyHodlder. error: %w", err) + } + + if err = d.Decode(holderJWT.Payload); err != nil { + return fmt.Errorf("mapstruct verifyHodlder decode. error: %w", err) + } + + if pOpts.expectedNonceForHolderVerification != "" && + pOpts.expectedNonceForHolderVerification != bindingPayload.Nonce { + return fmt.Errorf("nonce value '%s' does not match expected nonce value '%s'", + bindingPayload.Nonce, pOpts.expectedNonceForHolderVerification) + } + + if pOpts.expectedAudienceForHolderVerification != "" && + pOpts.expectedAudienceForHolderVerification != bindingPayload.Audience { + return fmt.Errorf("audience value '%s' does not match expected audience value '%s'", + bindingPayload.Audience, pOpts.expectedAudienceForHolderVerification) + } + + return nil +} + +// holderBindingPayload represents expected holder binding payload. +type holderBindingPayload struct { + Nonce string `json:"nonce,omitempty"` + Audience string `json:"aud,omitempty"` + IssuedAt *jwt.NumericDate `json:"iat,omitempty"` +} diff --git a/component/models/sdjwt/verifier/keybidning.go b/component/models/sdjwt/verifier/keybidning.go new file mode 100644 index 000000000..ec2e8094f --- /dev/null +++ b/component/models/sdjwt/verifier/keybidning.go @@ -0,0 +1,62 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ +/* +Package verifier enables the Verifier: An entity that requests, checks and +extracts the claims from an SD-JWT and respective Disclosures. +*/ + +package verifier + +import ( + "fmt" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/mitchellh/mapstructure" + + afgjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" + utils "github.com/hyperledger/aries-framework-go/component/models/util/maphelpers" +) + +// verifyKeyBindingJWT verifies key binding JWT. +// Section: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-02.html#section-6.2-4.6.1 +func verifyKeyBindingJWT(holderJWT *afgjwt.JSONWebToken, pOpts *parseOpts) error { + var bindingPayload keyBindingPayload + + d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &bindingPayload, + TagName: "json", + Squash: true, + WeaklyTypedInput: true, + DecodeHook: utils.JSONNumberToJwtNumericDate(), + }) + if err != nil { + return fmt.Errorf("mapstruct verifyHodlder. error: %w", err) + } + + if err = d.Decode(holderJWT.Payload); err != nil { + return fmt.Errorf("mapstruct verifyHodlder decode. error: %w", err) + } + + if pOpts.expectedNonceForHolderVerification != "" && pOpts.expectedNonceForHolderVerification != bindingPayload.Nonce { + return fmt.Errorf("nonce value '%s' does not match expected nonce value '%s'", + bindingPayload.Nonce, pOpts.expectedNonceForHolderVerification) + } + + if pOpts.expectedAudienceForHolderVerification != "" && + pOpts.expectedAudienceForHolderVerification != bindingPayload.Audience { + return fmt.Errorf("audience value '%s' does not match expected audience value '%s'", + bindingPayload.Audience, pOpts.expectedAudienceForHolderVerification) + } + + return nil +} + +// keyBindingPayload represents expected key binding payload. +type keyBindingPayload struct { + Nonce string `json:"nonce,omitempty"` + Audience string `json:"aud,omitempty"` + IssuedAt *jwt.NumericDate `json:"iat,omitempty"` +} diff --git a/component/models/sdjwt/verifier/verifier.go b/component/models/sdjwt/verifier/verifier.go index 184ea0d98..00ba97ecd 100644 --- a/component/models/sdjwt/verifier/verifier.go +++ b/component/models/sdjwt/verifier/verifier.go @@ -11,22 +11,22 @@ extracts the claims from an SD-JWT and respective Disclosures. package verifier import ( + "crypto" "encoding/json" "fmt" "time" - "github.com/go-jose/go-jose/v3/jwt" - "github.com/mitchellh/mapstructure" - "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk" afgjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" "github.com/hyperledger/aries-framework-go/component/models/signature/verifier" utils "github.com/hyperledger/aries-framework-go/component/models/util/maphelpers" + + "github.com/go-jose/go-jose/v3/jwt" ) -// jwtParseOpts holds options for the SD-JWT parsing. +// parseOpts holds options for the SD-JWT parsing. type parseOpts struct { detachedPayload []byte sigVerifier jose.SignatureVerifier @@ -34,11 +34,13 @@ type parseOpts struct { issuerSigningAlgorithms []string holderSigningAlgorithms []string - holderBindingRequired bool - expectedAudienceForHolderBinding string - expectedNonceForHolderBinding string + holderVerificationRequired bool + expectedAudienceForHolderVerification string + expectedNonceForHolderVerification string leewayForClaimsValidation time.Duration + + expectedTypHeader string } // ParseOpt is the SD-JWT Parser option. @@ -73,23 +75,43 @@ func WithHolderSigningAlgorithms(algorithms []string) ParseOpt { } // WithHolderBindingRequired option is for enforcing holder binding. +// Deprecated: use WithHolderVerificationRequired instead. func WithHolderBindingRequired(flag bool) ParseOpt { - return func(opts *parseOpts) { - opts.holderBindingRequired = flag - } + return WithHolderVerificationRequired(flag) } // WithExpectedAudienceForHolderBinding option is to pass expected audience for holder binding. +// Deprecated: use WithExpectedAudienceForHolderVerification instead. func WithExpectedAudienceForHolderBinding(audience string) ParseOpt { - return func(opts *parseOpts) { - opts.expectedAudienceForHolderBinding = audience - } + return WithExpectedAudienceForHolderVerification(audience) } // WithExpectedNonceForHolderBinding option is to pass nonce value for holder binding. +// Deprecated: use WithExpectedNonceForHolderVerification instead. func WithExpectedNonceForHolderBinding(nonce string) ParseOpt { + return WithExpectedNonceForHolderVerification(nonce) +} + +// WithHolderVerificationRequired option is for enforcing holder verification. +// For SDJWT V2 - this option defines Holder Binding verification as required. +// For SDJWT V5 - this option defines Key Binding verification as required. +func WithHolderVerificationRequired(flag bool) ParseOpt { + return func(opts *parseOpts) { + opts.holderVerificationRequired = flag + } +} + +// WithExpectedAudienceForHolderVerification option is to pass expected audience for holder verification. +func WithExpectedAudienceForHolderVerification(audience string) ParseOpt { return func(opts *parseOpts) { - opts.expectedNonceForHolderBinding = nonce + opts.expectedAudienceForHolderVerification = audience + } +} + +// WithExpectedNonceForHolderVerification option is to pass nonce value for holder verification. +func WithExpectedNonceForHolderVerification(nonce string) ParseOpt { + return func(opts *parseOpts) { + opts.expectedNonceForHolderVerification = nonce } } @@ -100,19 +122,30 @@ func WithLeewayForClaimsValidation(duration time.Duration) ParseOpt { } } +// WithExpectedTypHeader is an option for JWT typ header validation. +// Might be relevant for SDJWT V5 VC validation. +// Spec: https://vcstuff.github.io/draft-terbu-sd-jwt-vc/draft-terbu-oauth-sd-jwt-vc.html#name-header-parameters +func WithExpectedTypHeader(typ string) ParseOpt { + return func(opts *parseOpts) { + opts.expectedTypHeader = typ + } +} + // Parse parses combined format for presentation and returns verified claims. // The Verifier has to verify that all disclosed claim values were part of the original, Issuer-signed SD-JWT. // // At a high level, the Verifier: // - receives the Combined Format for Presentation from the Holder and verifies the signature of the SD-JWT using the // Issuer's public key, -// - verifies the Holder Binding JWT, if Holder Binding is required by the Verifier's policy, +// - verifies the Holder (Key) Binding JWT, if Holder Verification is required by the Verifier's policy, // using the public key included in the SD-JWT, // - calculates the digests over the Holder-Selected Disclosures and verifies that each digest // is contained in the SD-JWT. // // Detailed algorithm: -// https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-02.html#name-verification-by-the-verifier +// nolint:lll +// V2 https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-02.html#name-verification-by-the-verifier +// V5 https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#name-verification-by-the-verifier // // The Verifier will not, however, learn any claim values not disclosed in the Disclosures. func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]interface{}, error) { @@ -127,123 +160,92 @@ func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]i opt(pOpts) } - var jwtOpts []afgjwt.ParseOpt - jwtOpts = append(jwtOpts, - afgjwt.WithSignatureVerifier(pOpts.sigVerifier), - afgjwt.WithJWTDetachedPayload(pOpts.detachedPayload)) - - // Separate the Presentation into the SD-JWT, the Disclosures (if any), and the Holder Binding JWT (if provided) + // Separate the Presentation into the SD-JWT, the Disclosures (if any), and the Holder Verification JWT (if provided) cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) - // Validate the signature over the SD-JWT - signedJWT, _, err := afgjwt.Parse(cfp.SDJWT, jwtOpts...) + signedJWT, err := validateIssuerSignedSDJWT(cfp.SDJWT, cfp.Disclosures, pOpts) if err != nil { return nil, err } - // Ensure that a signing algorithm was used that was deemed secure for the application. - // The none algorithm MUST NOT be accepted. - err = verifySigningAlg(signedJWT.Headers, pOpts.issuerSigningAlgorithms) - if err != nil { - return nil, fmt.Errorf("failed to verify issuer signing algorithm: %w", err) - } - - // TODO: Validate the Issuer of the SD-JWT and that the signing key belongs to this Issuer. - - // Check that the SD-JWT is valid using nbf, iat, and exp claims, - // if provided in the SD-JWT, and not selectively disclosed. - err = verifyJWT(signedJWT, pOpts.leewayForClaimsValidation) + // Verify that all disclosures are present in SD-JWT. + err = common.VerifyDisclosuresInSDJWT(cfp.Disclosures, signedJWT) if err != nil { return nil, err } - // Check that there are no duplicate disclosures - err = checkForDuplicates(cfp.Disclosures) - if err != nil { - return nil, fmt.Errorf("check disclosures: %w", err) + if pOpts.expectedTypHeader != "" { + err = common.VerifyTyp(signedJWT.Headers, pOpts.expectedTypHeader) + if err != nil { + return nil, fmt.Errorf("failed to verify typ header: %w", err) + } } - // Verify that all disclosures are present in SD-JWT. - err = common.VerifyDisclosuresInSDJWT(cfp.Disclosures, signedJWT) + err = runHolderVerification(signedJWT, cfp.HolderVerification, pOpts) if err != nil { - return nil, err + return nil, fmt.Errorf("run holder verification: %w", err) } - err = verifyHolderBinding(signedJWT, cfp.HolderBinding, pOpts) + cryptoHash, err := common.GetCryptoHashFromClaims(signedJWT.Payload) if err != nil { - return nil, fmt.Errorf("failed to verify holder binding: %w", err) + return nil, err } - return getDisclosedClaims(cfp.Disclosures, signedJWT) + // Process the Disclosures. + // Section: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-02.html#section-6.2-4.5.1 + // Section: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#section-6.1-3 + return getDisclosedClaims(cfp.Disclosures, signedJWT, cryptoHash) } -func verifyHolderBinding(sdJWT *afgjwt.JSONWebToken, holderBinding string, pOpts *parseOpts) error { - if pOpts.holderBindingRequired && holderBinding == "" { - return fmt.Errorf("holder binding is required") - } - - if holderBinding == "" { - // not required and not present - nothing to do - return nil - } - - signatureVerifier, err := getSignatureVerifier(utils.CopyMap(sdJWT.Payload)) - if err != nil { - return fmt.Errorf("failed to get signature verifier from presentation claims: %w", err) - } - - holderJWT, _, err := afgjwt.Parse(holderBinding, - afgjwt.WithSignatureVerifier(signatureVerifier)) +func validateIssuerSignedSDJWT(sdjwt string, disclosures []string, pOpts *parseOpts) (*afgjwt.JSONWebToken, error) { + // Validate the signature over the SD-JWT. + signedJWT, _, err := afgjwt.Parse(sdjwt, + afgjwt.WithSignatureVerifier(pOpts.sigVerifier), + afgjwt.WithJWTDetachedPayload(pOpts.detachedPayload)) if err != nil { - return fmt.Errorf("failed to parse holder binding: %w", err) + return nil, err } - err = verifyHolderJWT(holderJWT, pOpts) + // Ensure that a signing algorithm was used that was deemed secure for the application. + // The none algorithm MUST NOT be accepted. + err = common.VerifySigningAlg(signedJWT.Headers, pOpts.issuerSigningAlgorithms) if err != nil { - return fmt.Errorf("failed to verify holder JWT: %w", err) + return nil, fmt.Errorf("failed to verify issuer signing algorithm: %w", err) } - return nil -} + // TODO: Validate the Issuer of the SD-JWT and that the signing key belongs to this Issuer. -func verifyHolderJWT(holderJWT *afgjwt.JSONWebToken, pOpts *parseOpts) error { - // Ensure that a signing algorithm was used that was deemed secure for the application. - // The none algorithm MUST NOT be accepted. - err := verifySigningAlg(holderJWT.Headers, pOpts.holderSigningAlgorithms) + // Check that the SD-JWT is valid using nbf, iat, and exp claims, + // if provided in the SD-JWT, and not selectively disclosed. + err = common.VerifyJWT(signedJWT, pOpts.leewayForClaimsValidation) if err != nil { - return fmt.Errorf("failed to verify holder signing algorithm: %w", err) + return nil, err } - err = verifyJWT(holderJWT, pOpts.leewayForClaimsValidation) + // Check that there are no duplicate disclosures + err = checkForDuplicates(disclosures) if err != nil { - return err + return nil, fmt.Errorf("check disclosures: %w", err) } - var bindingPayload holderBindingPayload + return signedJWT, nil +} - d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - Result: &bindingPayload, - TagName: "json", - Squash: true, - WeaklyTypedInput: true, - DecodeHook: utils.JSONNumberToJwtNumericDate(), - }) - if err != nil { - return fmt.Errorf("mapstruct verifyHodlder. error: %w", err) - } +func checkForDuplicates(values []string) error { + var duplicates []string - if err = d.Decode(holderJWT.Payload); err != nil { - return fmt.Errorf("mapstruct verifyHodlder decode. error: %w", err) - } + valuesMap := make(map[string]bool) - if pOpts.expectedNonceForHolderBinding != "" && pOpts.expectedNonceForHolderBinding != bindingPayload.Nonce { - return fmt.Errorf("nonce value '%s' does not match expected nonce value '%s'", - bindingPayload.Nonce, pOpts.expectedNonceForHolderBinding) + for _, val := range values { + if _, ok := valuesMap[val]; !ok { + valuesMap[val] = true + } else { + duplicates = append(duplicates, val) + } } - if pOpts.expectedAudienceForHolderBinding != "" && pOpts.expectedAudienceForHolderBinding != bindingPayload.Audience { - return fmt.Errorf("audience value '%s' does not match expected audience value '%s'", - bindingPayload.Audience, pOpts.expectedAudienceForHolderBinding) + if len(duplicates) > 0 { + return fmt.Errorf("duplicate values found %v", duplicates) } return nil @@ -292,8 +294,12 @@ func getSignatureVerifierFromCNF(cnf map[string]interface{}) (jose.SignatureVeri return signatureVerifier, nil } -func getDisclosedClaims(disclosures []string, signedJWT *afgjwt.JSONWebToken) (map[string]interface{}, error) { - disclosureClaims, err := common.GetDisclosureClaims(disclosures) +func getDisclosedClaims( + disclosures []string, + signedJWT *afgjwt.JSONWebToken, + hash crypto.Hash, +) (map[string]interface{}, error) { + disclosureClaims, err := common.GetDisclosureClaims(disclosures, hash) if err != nil { return nil, fmt.Errorf("failed to get verified payload: %w", err) } @@ -306,87 +312,61 @@ func getDisclosedClaims(disclosures []string, signedJWT *afgjwt.JSONWebToken) (m return disclosedClaims, nil } -func verifySigningAlg(joseHeaders jose.Headers, secureAlgs []string) error { - alg, ok := joseHeaders.Algorithm() - if !ok { - return fmt.Errorf("missing alg") - } - - if alg == afgjwt.AlgorithmNone { - return fmt.Errorf("alg value cannot be 'none'") +func runHolderVerification(sdJWT *afgjwt.JSONWebToken, holderVerificationJWT string, pOpts *parseOpts) error { + if pOpts.holderVerificationRequired && holderVerificationJWT == "" { + return fmt.Errorf("holder verification is required") } - if !contains(secureAlgs, alg) { - return fmt.Errorf("alg '%s' is not in the allowed list", alg) + if holderVerificationJWT == "" { + // not required and not present - nothing to do + return nil } - return nil -} - -func contains(values []string, val string) bool { - for _, v := range values { - if v == val { - return true - } + signatureVerifier, err := getSignatureVerifier(utils.CopyMap(sdJWT.Payload)) + if err != nil { + return fmt.Errorf("failed to get signature verifier from presentation claims: %w", err) } - return false -} - -func checkForDuplicates(values []string) error { - var duplicates []string - - valuesMap := make(map[string]bool) - - for _, val := range values { - if _, ok := valuesMap[val]; !ok { - valuesMap[val] = true - } else { - duplicates = append(duplicates, val) - } + // Validate the signature over the Key Binding JWT. + holderJWT, _, err := afgjwt.Parse(holderVerificationJWT, + afgjwt.WithSignatureVerifier(signatureVerifier)) + if err != nil { + return fmt.Errorf("parse holder verification JWT: %w", err) } - if len(duplicates) > 0 { - return fmt.Errorf("duplicate values found %v", duplicates) + err = verifyHolderVerificationJWT(holderJWT, pOpts) + if err != nil { + return fmt.Errorf("verify holder JWT: %w", err) } return nil } -// verifyJWT checks that the JWT is valid using nbf, iat, and exp claims (if provided in the JWT). -func verifyJWT(signedJWT *afgjwt.JSONWebToken, leeway time.Duration) error { - var claims jwt.Claims - - d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - Result: &claims, - TagName: "json", - Squash: true, - WeaklyTypedInput: true, - DecodeHook: utils.JSONNumberToJwtNumericDate(), - }) +// verifyHolderVerificationJWT verifies Holder/Key Binding JWT. +func verifyHolderVerificationJWT(holderJWT *afgjwt.JSONWebToken, pOpts *parseOpts) error { + // Ensure that a signing algorithm was used that was deemed secure for the application. + // The none algorithm MUST NOT be accepted. + err := common.VerifySigningAlg(holderJWT.Headers, pOpts.holderSigningAlgorithms) if err != nil { - return fmt.Errorf("mapstruct verifyJWT. error: %w", err) - } - - if err = d.Decode(signedJWT.Payload); err != nil { - return fmt.Errorf("mapstruct verifyJWT decode. error: %w", err) + return fmt.Errorf("failed to verify holder signing algorithm: %w", err) } - // Validate checks claims in a token against expected values. - // It is validated using the expected.Time, or time.Now if not provided - expected := jwt.Expected{} - - err = claims.ValidateWithLeeway(expected, leeway) + err = common.VerifyJWT(holderJWT, pOpts.leewayForClaimsValidation) if err != nil { - return fmt.Errorf("invalid JWT time values: %w", err) + return err } - return nil -} + sdJWTVersion := common.SDJWTVersionV2 + holderVerificationTyp, ok := holderJWT.Headers.Type() + // Check that the typ of the Key Binding JWT is kb+jwt. If so - it's SD JWT V5. + if ok && holderVerificationTyp == "kb+jwt" { + sdJWTVersion = common.SDJWTVersionV5 + } -// holderBindingPayload represents expected holder binding payload. -type holderBindingPayload struct { - Nonce string `json:"nonce,omitempty"` - Audience string `json:"aud,omitempty"` - IssuedAt *jwt.NumericDate `json:"iat,omitempty"` + switch sdJWTVersion { + case common.SDJWTVersionV5: + return verifyKeyBindingJWT(holderJWT, pOpts) + default: + return verifyHolderBindingJWT(holderJWT, pOpts) + } } diff --git a/component/models/sdjwt/verifier/verifier_interop_test.go b/component/models/sdjwt/verifier/verifier_interop_test.go index 778efc5a8..aa755e006 100644 --- a/component/models/sdjwt/verifier/verifier_interop_test.go +++ b/component/models/sdjwt/verifier/verifier_interop_test.go @@ -17,17 +17,18 @@ import ( "github.com/hyperledger/aries-framework-go/component/models/sdjwt/holder" ) -// nolint:lll -const specIssuanceExample1 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.gieinY5mTgTV69KZJyaFPeIJ9tfXlzCHKfs-HMBO9UIREz6Dh_lpTMrwUUXQXcO0pB3K_8uXjiMBGwXpMz_ayg~WyJkcVR2WE14UzBHYTNEb2FHbmU5eDBRIiwgInN1YiIsICJqb2huX2RvZV80MiJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJLVXhTNWhFX1hiVmFjckdBYzdFRnd3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyIzcXZWSjFCQURwSERTUzkzOVEtUml3IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJFUktNMENOZUZKa2FENW1UWFZfWDh3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0` +const specIssuanceExample1SDJWTV2 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.gieinY5mTgTV69KZJyaFPeIJ9tfXlzCHKfs-HMBO9UIREz6Dh_lpTMrwUUXQXcO0pB3K_8uXjiMBGwXpMz_ayg~WyJkcVR2WE14UzBHYTNEb2FHbmU5eDBRIiwgInN1YiIsICJqb2huX2RvZV80MiJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJLVXhTNWhFX1hiVmFjckdBYzdFRnd3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyIzcXZWSjFCQURwSERTUzkzOVEtUml3IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJFUktNMENOZUZKa2FENW1UWFZfWDh3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0` // nolint:lll +const specIssuanceExample1SDJWTV5 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0` // nolint:lll +const specIssuanceExample3SDJWTV5 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIi1hU3puSWQ5bVdNOG9jdVFvbENsbHN4VmdncTEtdkhXNE90bmhVdFZtV3ciLCAiSUticllObjN2QTdXRUZyeXN2YmRCSmpERFVfRXZRSXIwVzE4dlRScFVTZyIsICJvdGt4dVQxNG5CaXd6TkozTVBhT2l0T2w5cFZuWE9hRUhhbF94a3lOZktJIl0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2ZXJpZmllZF9jbGFpbXMiOiB7InZlcmlmaWNhdGlvbiI6IHsiX3NkIjogWyI3aDRVRTlxU2N2REtvZFhWQ3VvS2ZLQkpwVkJmWE1GX1RtQUdWYVplM1NjIiwgInZUd2UzcmFISUZZZ0ZBM3hhVUQyYU14Rno1b0RvOGlCdTA1cUtsT2c5THciXSwgInRydXN0X2ZyYW1ld29yayI6ICJkZV9hbWwiLCAiZXZpZGVuY2UiOiBbeyIuLi4iOiAidFlKMFREdWN5WlpDUk1iUk9HNHFSTzV2a1BTRlJ4RmhVRUxjMThDU2wzayJ9XX0sICJjbGFpbXMiOiB7Il9zZCI6IFsiUmlPaUNuNl93NVpIYWFka1FNcmNRSmYwSnRlNVJ3dXJSczU0MjMxRFRsbyIsICJTXzQ5OGJicEt6QjZFYW5mdHNzMHhjN2NPYW9uZVJyM3BLcjdOZFJtc01vIiwgIldOQS1VTks3Rl96aHNBYjlzeVdPNklJUTF1SGxUbU9VOHI4Q3ZKMGNJTWsiLCAiV3hoX3NWM2lSSDliZ3JUQkppLWFZSE5DTHQtdmpoWDFzZC1pZ09mXzlsayIsICJfTy13SmlIM2VuU0I0Uk9IbnRUb1FUOEptTHR6LW1oTzJmMWM4OVhvZXJRIiwgImh2RFhod21HY0pRc0JDQTJPdGp1TEFjd0FNcERzYVUwbmtvdmNLT3FXTkUiXX19LCAiX3NkX2FsZyI6ICJzaGEtMjU2In0.Xtpp8nvAq22k6wNRiYHGRoRnkn3EBaHdjcaa0sf0sYjCiyZnmSRlxv_C72gRwfVQkSA36ID_I46QSTZvBrgm3g~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInRpbWUiLCAiMjAxMi0wNC0yM1QxODoyNVoiXQ~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgeyJfc2QiOiBbIjl3cGpWUFd1RDdQSzBuc1FETDhCMDZsbWRnVjNMVnliaEh5ZFFwVE55TEkiLCAiRzVFbmhPQU9vVTlYXzZRTU52ekZYanBFQV9SYy1BRXRtMWJHX3djYUtJayIsICJJaHdGcldVQjYzUmNacTl5dmdaMFhQYzdHb3doM08ya3FYZUJJc3dnMUI0IiwgIldweFE0SFNvRXRjVG1DQ0tPZURzbEJfZW11Y1lMejJvTzhvSE5yMWJFVlEiXX1d~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgIm1ldGhvZCIsICJwaXBwIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImdpdmVuX25hbWUiLCAiTWF4Il0~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImZhbWlseV9uYW1lIiwgIk1cdTAwZmNsbGVyIl0~WyJ5MXNWVTV3ZGZKYWhWZGd3UGdTN1JRIiwgImFkZHJlc3MiLCB7ImxvY2FsaXR5IjogIk1heHN0YWR0IiwgInBvc3RhbF9jb2RlIjogIjEyMzQ0IiwgImNvdW50cnkiOiAiREUiLCAic3RyZWV0X2FkZHJlc3MiOiAiV2VpZGVuc3RyYVx1MDBkZmUgMjIifV0` // nolint:lll -//nolint:lll -const specPresentationExample1 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.gieinY5mTgTV69KZJyaFPeIJ9tfXlzCHKfs-HMBO9UIREz6Dh_lpTMrwUUXQXcO0pB3K_8uXjiMBGwXpMz_ayg~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~eyJhbGciOiAiRVMyNTYifQ.eyJub25jZSI6ICJYWk9VY28xdV9nRVBrbnhTNzhzV1dnIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE2NzA1NzQ0MTh9._TZe98TAQDrV_21TjEKBRKKCt5EO5Q0-MHNZ79qVvBR9gL4nCXBu6c--QDysTgnXk_oe-qVin6EOzHF3Oh9tbQ` +const specPresentationExample1SDJWTV2 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.gieinY5mTgTV69KZJyaFPeIJ9tfXlzCHKfs-HMBO9UIREz6Dh_lpTMrwUUXQXcO0pB3K_8uXjiMBGwXpMz_ayg~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~eyJhbGciOiAiRVMyNTYifQ.eyJub25jZSI6ICJYWk9VY28xdV9nRVBrbnhTNzhzV1dnIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE2NzA1NzQ0MTh9._TZe98TAQDrV_21TjEKBRKKCt5EO5Q0-MHNZ79qVvBR9gL4nCXBu6c--QDysTgnXk_oe-qVin6EOzHF3Oh9tbQ` //nolint:lll +const specPresentationExample1SDJWTV5 = `eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6ICIxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE2ODgxNjA0ODN9.tKnLymr8fQfupOgvMgBK3GCEIDEzhgta4MgnxYm9fWGMkqrz2R5PSkv0I-AXKXtIF6bdZRbjL-t43vC87jVoZQ` //nolint:lll func TestInterop(t *testing.T) { r := require.New(t) t.Run("success - Example 1", func(t *testing.T) { - cfi := specIssuanceExample1 + common.CombinedFormatSeparator + cfi := specIssuanceExample1SDJWTV2 + common.CombinedFormatSeparator claims, err := Parse(cfi, WithIssuerSigningAlgorithms([]string{"ES256"}), @@ -40,11 +41,11 @@ func TestInterop(t *testing.T) { printObject(t, "Disclosed Claims for Example 1 - All Claims Disclosed", claims) var example1ClaimsObj map[string]interface{} - err = json.Unmarshal([]byte(claimsExample1), &example1ClaimsObj) + err = json.Unmarshal([]byte(claimsExample1SDJWTV2), &example1ClaimsObj) r.NoError(err) var disclosedClaimsForExample1Obj map[string]interface{} - err = json.Unmarshal([]byte(disclosedAllClaimsForExample1), &disclosedClaimsForExample1Obj) + err = json.Unmarshal([]byte(disclosedAllClaimsForExample1SDJWTV2), &disclosedClaimsForExample1Obj) r.NoError(err) // expected claims are example 1 claims plus exp, iat, iss, cnf @@ -53,8 +54,88 @@ func TestInterop(t *testing.T) { r.Equal(len(disclosedClaimsForExample1Obj), len(claims)) }) + t.Run("success - Example 1 SDJWT V5", func(t *testing.T) { + cfp := specIssuanceExample1SDJWTV5 + common.CombinedFormatSeparator + + claims, err := Parse(cfp, + WithIssuerSigningAlgorithms([]string{"ES256"}), + WithSignatureVerifier(&holder.NoopSignatureVerifier{}), + // expiry time for example 1 is 2018-01-17 22:43:42 -0500 EST + // so we have to have great leeway in order to pass test + WithLeewayForClaimsValidation(10*12*30*24*time.Hour)) + r.NoError(err) + + printObject(t, "Disclosed Claims for Example 1 - All Claims Disclosed", claims) + + var example1ClaimsObj map[string]interface{} + err = json.Unmarshal([]byte(claimsExample1SDJWTV5), &example1ClaimsObj) + r.NoError(err) + + var disclosedClaimsForExample1Obj map[string]interface{} + err = json.Unmarshal([]byte(disclosedAllClaimsForExample1SDJWTV5), &disclosedClaimsForExample1Obj) + r.NoError(err) + + // expected claims are example 1 claims plus exp, iat, iss, cnf + r.Equal(len(disclosedClaimsForExample1Obj), len(example1ClaimsObj)+4) + + r.Equal(len(disclosedClaimsForExample1Obj), len(claims)) + + claimBytes, err := json.Marshal(claims) + r.NoError(err) + + r.NotContains(string(claimBytes), common.SDKey) + r.NotContains(string(claimBytes), common.SDAlgorithmKey) + r.NotContains(string(claimBytes), common.ArrayElementDigestKey) + }) + + t.Run("success - Example 3 SDJWT V5", func(t *testing.T) { + cfp := specIssuanceExample3SDJWTV5 + common.CombinedFormatSeparator + + claims, err := Parse(cfp, + WithIssuerSigningAlgorithms([]string{"ES256"}), + WithSignatureVerifier(&holder.NoopSignatureVerifier{}), + // expiry time for example 1 is 2018-01-17 22:43:42 -0500 EST + // so we have to have great leeway in order to pass test + WithLeewayForClaimsValidation(10*12*30*24*time.Hour)) + r.NoError(err) + + printObject(t, "Disclosed Claims for Example 3 - All Claims Disclosed", claims) + + var disclosedClaimsForExample1Obj map[string]interface{} + err = json.Unmarshal([]byte(disclosedAllClaimsForExample3SDJWTV5), &disclosedClaimsForExample1Obj) + r.NoError(err) + + r.Equal(len(disclosedClaimsForExample1Obj), len(claims)) + + claimBytes, err := json.Marshal(claims) + r.NoError(err) + + r.NotContains(string(claimBytes), common.SDKey) + r.NotContains(string(claimBytes), common.SDAlgorithmKey) + r.NotContains(string(claimBytes), common.ArrayElementDigestKey) + }) + t.Run("success - Example 1 with Holder Binding", func(t *testing.T) { - claims, err := Parse(specPresentationExample1, + claims, err := Parse(specPresentationExample1SDJWTV2, + WithIssuerSigningAlgorithms([]string{"ES256"}), + WithHolderSigningAlgorithms([]string{"ES256"}), + WithSignatureVerifier(&holder.NoopSignatureVerifier{}), + // expiry time for example 1 is 2018-01-17 22:43:42 -0500 EST + // so we have to have great leeway in order to pass test + WithLeewayForClaimsValidation(10*12*30*24*time.Hour)) + r.NoError(err) + + printObject(t, "Disclosed Claims For Example 1 - Partial Disclosure", claims) + + var disclosedPartialClaimsForExample1Obj map[string]interface{} + err = json.Unmarshal([]byte(disclosedPartialClaimsForExample1SDJWTV2), &disclosedPartialClaimsForExample1Obj) + r.NoError(err) + + r.Equal(len(disclosedPartialClaimsForExample1Obj), len(claims)) + }) + + t.Run("success - Example 1 with Key Binding SDJWT V5", func(t *testing.T) { + claims, err := Parse(specPresentationExample1SDJWTV5, WithIssuerSigningAlgorithms([]string{"ES256"}), WithHolderSigningAlgorithms([]string{"ES256"}), WithSignatureVerifier(&holder.NoopSignatureVerifier{}), @@ -66,14 +147,14 @@ func TestInterop(t *testing.T) { printObject(t, "Disclosed Claims For Example 1 - Partial Disclosure", claims) var disclosedPartialClaimsForExample1Obj map[string]interface{} - err = json.Unmarshal([]byte(disclosedPartialClaimsForExample1), &disclosedPartialClaimsForExample1Obj) + err = json.Unmarshal([]byte(disclosedPartialClaimsForExample1SDJWTV5), &disclosedPartialClaimsForExample1Obj) r.NoError(err) r.Equal(len(disclosedPartialClaimsForExample1Obj), len(claims)) }) } -const claimsExample1 = ` +const claimsExample1SDJWTV2 = ` { "sub": "john_doe_42", "given_name": "John", @@ -89,7 +170,29 @@ const claimsExample1 = ` "birthdate": "1940-01-01" }` -const disclosedAllClaimsForExample1 = ` +const claimsExample1SDJWTV5 = ` +{ + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "phone_number_verified": true, + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "updated_at": 1570000000, + "nationalities": [ + "US", + "DE" + ] +}` + +const disclosedAllClaimsForExample1SDJWTV2 = ` { "sub": "john_doe_42", "given_name": "John", @@ -116,9 +219,94 @@ const disclosedAllClaimsForExample1 = ` } }` -const disclosedPartialClaimsForExample1 = ` +const disclosedAllClaimsForExample1SDJWTV5 = ` +{ + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "phone_number_verified": true, + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "updated_at": 1570000000, + "nationalities": [ + "US", + "DE" + ], + "exp": 1883000000, + "iat": 1683000000, + "iss": "https://example.com/issuer", + "cnf": { + "jwk": { + "crv": "P-256", + "kty": "EC", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + } + } +}` + +const disclosedAllClaimsForExample3SDJWTV5 = ` +{ + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "verified_claims": { + "verification": { + "trust_framework": "de_aml", + "evidence": [ + { + "method": "pipp" + } + ], + "time": "2012-04-23T18:25Z" + }, + "claims": { + "given_name": "Max", + "family_name": "Müller", + "address": { + "locality": "Maxstadt", + "postal_code": "12344", + "country": "DE", + "street_address": "Weidenstraße 22" + } + } + } +}` + +const disclosedPartialClaimsForExample1SDJWTV2 = ` +{ + "family_name": "Doe", + "given_name": "John", + "address": { + "country": "US", + "locality": "Anytown", + "region": "Anystate", + "street_address": "123 Main St" + }, + "cnf": { + "jwk": { + "crv": "P-256", + "kty": "EC", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + } + }, + "exp": 1516247022, + "iat": 1516239022, + "iss": "https://example.com/issuer" +}` + +const disclosedPartialClaimsForExample1SDJWTV5 = ` { "family_name": "Doe", + "sub": "user_42", "given_name": "John", "address": { "country": "US", @@ -126,6 +314,9 @@ const disclosedPartialClaimsForExample1 = ` "region": "Anystate", "street_address": "123 Main St" }, + "nationalities": [ + "US" + ], "cnf": { "jwk": { "crv": "P-256", diff --git a/component/models/sdjwt/verifier/verifier_test.go b/component/models/sdjwt/verifier/verifier_test.go index 9f047fbec..42ff36d43 100644 --- a/component/models/sdjwt/verifier/verifier_test.go +++ b/component/models/sdjwt/verifier/verifier_test.go @@ -8,6 +8,7 @@ package verifier import ( "bytes" + "crypto" "crypto/ed25519" "crypto/rand" "crypto/rsa" @@ -54,9 +55,14 @@ func TestParse(t *testing.T) { timeOpts = append(timeOpts, issuer.WithNotBefore(jwt.NewNumericDate(now)), issuer.WithIssuedAt(jwt.NewNumericDate(now)), - issuer.WithExpiry(jwt.NewNumericDate(now.Add(year)))) + issuer.WithExpiry(jwt.NewNumericDate(now.Add(year))), + issuer.WithSDJWTVersion(common.SDJWTVersionV2)) - token, e := issuer.New(testIssuer, selectiveClaims, nil, signer, timeOpts...) + headers := afjose.Headers{ + afjose.HeaderType: "JWT", + } + + token, e := issuer.New(testIssuer, selectiveClaims, headers, signer, timeOpts...) r.NoError(e) combinedFormatForIssuance, e := token.Serialize(false) r.NoError(e) @@ -67,7 +73,9 @@ func TestParse(t *testing.T) { r.NoError(e) t.Run("success - EdDSA signing algorithm", func(t *testing.T) { - claims, err := Parse(combinedFormatForPresentation, WithSignatureVerifier(verifier)) + claims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(verifier), + WithExpectedTypHeader("JWT")) r.NoError(err) require.NotNil(t, claims) @@ -103,14 +111,14 @@ func TestParse(t *testing.T) { v := afjwt.NewRS256Verifier(pubKey) - rsaToken, err := issuer.New(testIssuer, selectiveClaims, nil, afjwt.NewRS256Signer(privKey, nil)) + rsaToken, err := issuer.New(testIssuer, selectiveClaims, headers, afjwt.NewRS256Signer(privKey, nil)) r.NoError(err) rsaCombinedFormatForIssuance, err := rsaToken.Serialize(false) require.NoError(t, err) cfp := fmt.Sprintf("%s%s", rsaCombinedFormatForIssuance, common.CombinedFormatSeparator) - claims, err := Parse(cfp, WithSignatureVerifier(v)) + claims, err := Parse(cfp, WithSignatureVerifier(v), WithExpectedTypHeader("JWT")) r.NoError(err) // expected claims iss, given_name @@ -147,6 +155,15 @@ func TestParse(t *testing.T) { require.Equal(t, err.Error(), "failed to verify issuer signing algorithm: alg 'EdDSA' is not in the allowed list") }) + t.Run("error - unexpected typ header", func(t *testing.T) { + claims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(verifier), + WithExpectedTypHeader("vc-sd+jwt")) + r.Error(err) + require.Nil(t, claims) + require.Equal(t, err.Error(), "failed to verify typ header: unexpected typ \"JWT\"") + }) + t.Run("error - additional disclosure", func(t *testing.T) { claims, err := Parse(fmt.Sprintf("%s~%s~", combinedFormatForIssuance, additionalDisclosure), WithSignatureVerifier(verifier)) @@ -245,7 +262,7 @@ func TestParse(t *testing.T) { }) } -func TestHolderBinding(t *testing.T) { +func TestHolderVerification(t *testing.T) { r := require.New(t) issuerPubKey, issuerPrivateKey, e := ed25519.GenerateKey(rand.Reader) @@ -283,467 +300,467 @@ func TestHolderBinding(t *testing.T) { claimsToDisclose := []string{cfi.Disclosures[0]} - t.Run("success - with holder binding", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce), - WithLeewayForClaimsValidation(time.Hour)) - r.NoError(err) - - // expected claims cnf, iss, given_name; last_name was not disclosed - r.Equal(3, len(verifiedClaims)) - }) - - t.Run("success - with holder binding; expected nonce and audience not specified", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithHolderBindingRequired(true)) - r.NoError(err) - - // expected claims cnf, iss, given_name; last_name was not disclosed - r.Equal(3, len(verifiedClaims)) - }) - - t.Run("success - with holder binding (required)", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - // Verifier will validate combined format for presentation and create verified claims. - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithHolderBindingRequired(true), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce)) - r.NoError(err) - - // expected claims cnf, iss, given_name; last_name was not disclosed - r.Equal(3, len(verifiedClaims)) - }) - - t.Run("error - holder binding required, however not provided by the holder", func(t *testing.T) { - // holder will not issue holder binding - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose) - r.NoError(err) - - // Verifier will validate combined format for presentation and create verified claims. - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithHolderBindingRequired(true), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), "failed to verify holder binding: holder binding is required") - }) - - t.Run("error - holder signature is not matching holder public key in SD-JWT", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: signer, // should have been holder signer; on purpose sign holder binding with wrong signer - })) - r.NoError(err) - - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to parse holder binding: parse JWT from compact JWS: ed25519: invalid signature") // nolint:lll - }) - - t.Run("error - invalid holder binding JWT provided by the holder", func(t *testing.T) { - // holder will not issue holder binding - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose) - r.NoError(err) - - // add fake holder binding - combinedFormatForPresentation += "invalid-holder-jwt" - - // Verifier will validate combined format for presentation and create verified claims. - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to parse holder binding: JWT of compacted JWS form is supported only") - }) - - t.Run("error - holder signature algorithm not supported", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - // Verifier will validate combined format for presentation and create verified claims. - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithHolderBindingRequired(true), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce), - WithHolderSigningAlgorithms([]string{})) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to verify holder JWT: failed to verify holder signing algorithm: alg 'EdDSA'") //nolint:lll - }) - - t.Run("error - invalid iat for holder binding", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: "different", - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now().AddDate(1, 0, 0)), // in future - }, - Signer: holderSigner, - })) - r.NoError(err) - - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to verify holder JWT: invalid JWT time values: go-jose/go-jose/jwt: validation field, token issued in the future (iat)") //nolint:lll - }) - - t.Run("error - unexpected nonce for holder binding", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: "different", - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to verify holder JWT: nonce value 'different' does not match expected nonce value 'nonce'") //nolint:lll - }) - - t.Run("error - unexpected audience for holder binding", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: "different", - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to verify holder JWT: audience value 'different' does not match expected audience value 'https://test.com/verifier'") //nolint:lll - }) - - t.Run("error - holder binding provided, however cnf claim not in SD-JWT", func(t *testing.T) { - tokenWithoutHolderPublicKey, err := issuer.New(testIssuer, claims, nil, signer) - r.NoError(err) - - cfiWithoutHolderPublicKey, err := tokenWithoutHolderPublicKey.Serialize(false) - r.NoError(err) - - ctd := []string{common.ParseCombinedFormatForIssuance(cfiWithoutHolderPublicKey).Disclosures[0]} - - _, err = holder.Parse(cfiWithoutHolderPublicKey, holder.WithSignatureVerifier(signatureVerifier)) - r.NoError(err) - - combinedFormatForPresentation, err := holder.CreatePresentation(cfiWithoutHolderPublicKey, ctd, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - verifiedClaims, err := Parse(combinedFormatForPresentation, - WithSignatureVerifier(signatureVerifier), - WithExpectedAudienceForHolderBinding(testAudience), - WithExpectedNonceForHolderBinding(testNonce)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to get signature verifier from presentation claims: cnf must be present in SD-JWT") //nolint:lll - }) - - t.Run("error - holder binding provided, however cnf is not an object", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) - - claims := make(map[string]interface{}) - claims["cnf"] = "abc" - claims["_sd_alg"] = testSDAlg - - sdJWT, err := buildJWS(signer, claims) - r.NoError(err) - - cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderBinding - - verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to get signature verifier from presentation claims: cnf must be an object") // nolint:lll - }) - - t.Run("error - holder binding provided, cnf is missing jwk", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) - - cnf := make(map[string]interface{}) - cnf["test"] = "test" - - claims := make(map[string]interface{}) - claims["cnf"] = cnf - claims["_sd_alg"] = testSDAlg - - sdJWT, err := buildJWS(signer, claims) - r.NoError(err) - - cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderBinding - - verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to get signature verifier from presentation claims: jwk must be present in cnf") // nolint:lll - }) - - t.Run("error - holder binding provided, invalid jwk in cnf", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) - - cnf := make(map[string]interface{}) - cnf["jwk"] = make(map[string]interface{}) - - claims := make(map[string]interface{}) - claims["cnf"] = cnf - claims["_sd_alg"] = testSDAlg - - sdJWT, err := buildJWS(signer, claims) - r.NoError(err) - - cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderBinding - - verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to get signature verifier from presentation claims: unmarshal jwk: unable to read jose JWK, go-jose/go-jose: unknown json web key type ''") // nolint:lll - }) - - t.Run("error - holder binding provided, invalid jwk in cnf", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) - - cnf := make(map[string]interface{}) - cnf["jwk"] = make(map[string]interface{}) - - claims := make(map[string]interface{}) - claims["cnf"] = cnf - claims["_sd_alg"] = testSDAlg - - sdJWT, err := buildJWS(signer, claims) - r.NoError(err) - - cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderBinding - - verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to get signature verifier from presentation claims: unmarshal jwk: unable to read jose JWK, go-jose/go-jose: unknown json web key type ''") // nolint:lll - }) - - t.Run("error - holder binding provided with EdDSA, jwk in cnf is RSA", func(t *testing.T) { - combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, - holder.WithHolderBinding(&holder.BindingInfo{ - Payload: holder.BindingPayload{ - Nonce: testNonce, - Audience: testAudience, - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - Signer: holderSigner, - })) - r.NoError(err) - - cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) - - claims := make(map[string]interface{}) - claims["cnf"] = map[string]interface{}{ - "jwk": map[string]interface{}{ - "kty": "RSA", - "e": "AQAB", - "n": "pm4bOHBg-oYhAyPWzR56AWX3rUIXp11", + tests := []struct { + name string + headers afjose.Headers + }{ + { + name: "holder binding", + headers: nil, + }, + { + name: "key binding", + headers: map[string]interface{}{ + afjose.HeaderType: "kb+jwt", }, - } - - claims["_sd_alg"] = testSDAlg - - sdJWT, err := buildJWS(signer, claims) - r.NoError(err) - - cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderBinding - - verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) - r.Error(err) - r.Nil(verifiedClaims) - - r.Contains(err.Error(), - "failed to verify holder binding: failed to parse holder binding: parse JWT from compact JWS: no verifier found for EdDSA algorithm") // nolint:lll - }) -} - -func TestVerifySigningAlgorithm(t *testing.T) { - r := require.New(t) - - t.Run("success - EdDSA signing algorithm", func(t *testing.T) { - headers := make(afjose.Headers) - headers["alg"] = "EdDSA" - err := verifySigningAlg(headers, []string{"EdDSA"}) - r.NoError(err) - }) - - t.Run("error - signing algorithm can not be empty", func(t *testing.T) { - headers := make(afjose.Headers) - err := verifySigningAlg(headers, []string{"RS256"}) - r.Error(err) - r.Contains(err.Error(), "missing alg") - }) - - t.Run("success - EdDSA signing algorithm not in allowed list", func(t *testing.T) { - headers := make(afjose.Headers) - headers["alg"] = "EdDSA" - err := verifySigningAlg(headers, []string{"RS256"}) - r.Error(err) - r.Contains(err.Error(), "alg 'EdDSA' is not in the allowed list") - }) + }, + } - t.Run("error - signing algorithm can not be none", func(t *testing.T) { - headers := make(afjose.Headers) - headers["alg"] = "none" - err := verifySigningAlg(headers, []string{"RS256"}) - r.Error(err) - r.Contains(err.Error(), "alg value cannot be 'none'") - }) + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + t.Run("success", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithExpectedAudienceForHolderBinding(testAudience), + WithExpectedNonceForHolderBinding(testNonce), + WithLeewayForClaimsValidation(time.Hour)) + r.NoError(err) + + // expected claims cnf, iss, given_name; last_name was not disclosed + r.Equal(3, len(verifiedClaims)) + }) + + t.Run("success - expected nonce and audience not specified", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithHolderBindingRequired(true)) + r.NoError(err) + + // expected claims cnf, iss, given_name; last_name was not disclosed + r.Equal(3, len(verifiedClaims)) + }) + + t.Run("success (required)", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithHolderVerificationRequired(true), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce)) + r.NoError(err) + + // expected claims cnf, iss, given_name; last_name was not disclosed + r.Equal(3, len(verifiedClaims)) + }) + + t.Run("error - holder verification required, however not provided by the holder", func(t *testing.T) { + // holder will not issue holder binding + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose) + r.NoError(err) + + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithHolderVerificationRequired(true), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), "run holder verification: holder verification is required") + }) + + t.Run("error - holder signature is not matching holder public key in SD-JWT", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: signer, // should have been holder signer; on purpose sign holder binding with wrong signer + })) + r.NoError(err) + + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "parse JWT from compact JWS: ed25519: invalid signature") // nolint:lll + }) + + t.Run("error - invalid holder verification JWT provided by the holder", func(t *testing.T) { + // holder will not issue holder verification + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose) + r.NoError(err) + + // add fake holder binding + combinedFormatForPresentation += "invalid-holder-jwt" + + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "parse holder verification JWT: JWT of compacted JWS form is supported only") + }) + + t.Run("error - holder signature algorithm not supported", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithHolderVerificationRequired(true), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce), + WithHolderSigningAlgorithms([]string{})) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "failed to verify holder signing algorithm: alg 'EdDSA' is not in the allowed list") //nolint:lll + }) + + t.Run("error - invalid iat", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: "different", + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now().AddDate(1, 0, 0)), // in future + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "verify holder JWT: invalid JWT time values: go-jose/go-jose/jwt: validation field, token issued in the future (iat)") //nolint:lll + }) + + t.Run("error - unexpected nonce", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: "different", + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "run holder verification: verify holder JWT: nonce value 'different' does not match expected nonce value 'nonce'") //nolint:lll + }) + + t.Run("error - unexpected audience", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: "different", + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "run holder verification: verify holder JWT: audience value 'different' does not match expected audience value 'https://test.com/verifier'") //nolint:lll + }) + + t.Run("error - holder verification provided, however cnf claim not in SD-JWT", func(t *testing.T) { + tokenWithoutHolderPublicKey, err := issuer.New(testIssuer, claims, nil, signer) + r.NoError(err) + + cfiWithoutHolderPublicKey, err := tokenWithoutHolderPublicKey.Serialize(false) + r.NoError(err) + + ctd := []string{common.ParseCombinedFormatForIssuance(cfiWithoutHolderPublicKey).Disclosures[0]} + + _, err = holder.Parse(cfiWithoutHolderPublicKey, holder.WithSignatureVerifier(signatureVerifier)) + r.NoError(err) + + combinedFormatForPresentation, err := holder.CreatePresentation(cfiWithoutHolderPublicKey, ctd, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier), + WithExpectedAudienceForHolderVerification(testAudience), + WithExpectedNonceForHolderVerification(testNonce)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "run holder verification: failed to get signature verifier from presentation claims: cnf must be present in SD-JWT") //nolint:lll + }) + + t.Run("error - holder verification provided, however cnf is not an object", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) + + claims := make(map[string]interface{}) + claims["cnf"] = "abc" + claims["_sd_alg"] = testSDAlg + + sdJWT, err := buildJWS(signer, claims) + r.NoError(err) + + cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderVerification + + verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "run holder verification: failed to get signature verifier from presentation claims: cnf must be an object") // nolint:lll + }) + + t.Run("error - holder verification provided, cnf is missing jwk", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) + + cnf := make(map[string]interface{}) + cnf["test"] = "test" + + claims := make(map[string]interface{}) + claims["cnf"] = cnf + claims["_sd_alg"] = testSDAlg + + sdJWT, err := buildJWS(signer, claims) + r.NoError(err) + + cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderVerification + + verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "run holder verification: failed to get signature verifier from presentation claims: jwk must be present in cnf") // nolint:lll + }) + + t.Run("error - holder verification provided, invalid jwk in cnf", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) + + cnf := make(map[string]interface{}) + cnf["jwk"] = make(map[string]interface{}) + + claims := make(map[string]interface{}) + claims["cnf"] = cnf + claims["_sd_alg"] = testSDAlg + + sdJWT, err := buildJWS(signer, claims) + r.NoError(err) + + cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderVerification + + verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "run holder verification: failed to get signature verifier from presentation claims: unmarshal jwk: unable to read jose JWK, go-jose/go-jose: unknown json web key type ''") // nolint:lll + }) + + t.Run("error - holder verification provided, invalid jwk in cnf", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) + + cnf := make(map[string]interface{}) + cnf["jwk"] = make(map[string]interface{}) + + claims := make(map[string]interface{}) + claims["cnf"] = cnf + claims["_sd_alg"] = testSDAlg + + sdJWT, err := buildJWS(signer, claims) + r.NoError(err) + + cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderVerification + + verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "run holder verification: failed to get signature verifier from presentation claims: unmarshal jwk: unable to read jose JWK, go-jose/go-jose: unknown json web key type ''") // nolint:lll + }) + + t.Run("error - holder verification provided with EdDSA, jwk in cnf is RSA", func(t *testing.T) { + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, claimsToDisclose, + holder.WithHolderVerification(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: testNonce, + Audience: testAudience, + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Headers: testCase.headers, + Signer: holderSigner, + })) + r.NoError(err) + + cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) + + claims := make(map[string]interface{}) + claims["cnf"] = map[string]interface{}{ + "jwk": map[string]interface{}{ + "kty": "RSA", + "e": "AQAB", + "n": "pm4bOHBg-oYhAyPWzR56AWX3rUIXp11", + }, + } + + claims["_sd_alg"] = testSDAlg + + sdJWT, err := buildJWS(signer, claims) + r.NoError(err) + + cfpWithInvalidCNF := sdJWT + common.CombinedFormatSeparator + cfp.HolderVerification + + verifiedClaims, err := Parse(cfpWithInvalidCNF, WithSignatureVerifier(signatureVerifier)) + r.Error(err) + r.Nil(verifiedClaims) + + r.Contains(err.Error(), + "run holder verification: parse holder verification JWT: parse JWT from compact JWS: no verifier found for EdDSA algorithm") // nolint:lll + }) + }) + } } func TestGetVerifiedPayload(t *testing.T) { @@ -766,8 +783,17 @@ func TestGetVerifiedPayload(t *testing.T) { token, e := issuer.New(testIssuer, selectiveClaims, nil, signer, timeOpts...) r.NoError(e) - t.Run("success", func(t *testing.T) { - claims, err := getDisclosedClaims(token.Disclosures, token.SignedJWT) + t.Run("success V2", func(t *testing.T) { + claims, err := getDisclosedClaims(token.Disclosures, token.SignedJWT, crypto.SHA256) + r.NoError(err) + r.NotNil(claims) + r.Equal(5, len(claims)) + + printObject(t, "Disclosed Claims", claims) + }) + + t.Run("success V5", func(t *testing.T) { + claims, err := getDisclosedClaims(token.Disclosures, token.SignedJWT, crypto.SHA256) r.NoError(err) r.NotNil(claims) r.Equal(5, len(claims)) @@ -776,7 +802,7 @@ func TestGetVerifiedPayload(t *testing.T) { }) t.Run("error - invalid disclosure(not encoded)", func(t *testing.T) { - claims, err := getDisclosedClaims([]string{"xyz"}, token.SignedJWT) + claims, err := getDisclosedClaims([]string{"xyz"}, token.SignedJWT, crypto.SHA256) r.Error(err) r.Nil(claims) r.Contains(err.Error(), diff --git a/component/models/verifiable/credential.go b/component/models/verifiable/credential.go index fff665ca7..f988af64a 100644 --- a/component/models/verifiable/credential.go +++ b/component/models/verifiable/credential.go @@ -7,6 +7,7 @@ package verifiable import ( "bytes" + "crypto" "encoding/binary" "encoding/json" "errors" @@ -22,6 +23,9 @@ import ( "github.com/xeipuuv/gojsonschema" "github.com/hyperledger/aries-framework-go/component/log" + + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + "github.com/hyperledger/aries-framework-go/component/models/jwt" docjsonld "github.com/hyperledger/aries-framework-go/component/models/ld/validator" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" @@ -514,6 +518,7 @@ type Credential struct { RefreshService []TypedID JWT string + SDJWTVersion common.SDJWTVersion SDJWTHashAlg string SDJWTDisclosures []*common.DisclosureClaim SDHolderBinding string @@ -523,22 +528,23 @@ type Credential struct { // rawCredential is a basic verifiable credential. type rawCredential struct { - Context interface{} `json:"@context,omitempty"` - ID string `json:"id,omitempty"` - Type interface{} `json:"type,omitempty"` - Subject json.RawMessage `json:"credentialSubject,omitempty"` - Issued *util.TimeWrapper `json:"issuanceDate,omitempty"` - Expired *util.TimeWrapper `json:"expirationDate,omitempty"` - Proof json.RawMessage `json:"proof,omitempty"` - Status *TypedID `json:"credentialStatus,omitempty"` - Issuer json.RawMessage `json:"issuer,omitempty"` - Schema interface{} `json:"credentialSchema,omitempty"` - Evidence Evidence `json:"evidence,omitempty"` - TermsOfUse json.RawMessage `json:"termsOfUse,omitempty"` - RefreshService json.RawMessage `json:"refreshService,omitempty"` - JWT string `json:"jwt,omitempty"` - SDJWTHashAlg string `json:"_sd_alg,omitempty"` - SDJWTDisclosures []string `json:"-"` + Context interface{} `json:"@context,omitempty"` + ID string `json:"id,omitempty"` + Type interface{} `json:"type,omitempty"` + Subject json.RawMessage `json:"credentialSubject,omitempty"` + Issued *util.TimeWrapper `json:"issuanceDate,omitempty"` + Expired *util.TimeWrapper `json:"expirationDate,omitempty"` + Proof json.RawMessage `json:"proof,omitempty"` + Status *TypedID `json:"credentialStatus,omitempty"` + Issuer json.RawMessage `json:"issuer,omitempty"` + Schema interface{} `json:"credentialSchema,omitempty"` + Evidence Evidence `json:"evidence,omitempty"` + TermsOfUse json.RawMessage `json:"termsOfUse,omitempty"` + RefreshService json.RawMessage `json:"refreshService,omitempty"` + JWT string `json:"jwt,omitempty"` + SDJWTHashAlg string `json:"_sd_alg,omitempty"` + SDJWTDisclosures []string `json:"-"` + SDJWTVersion common.SDJWTVersion `json:"-"` // All unmapped fields are put here. CustomFields `json:"-"` @@ -842,11 +848,12 @@ func ParseCredential(vcData []byte, opts ...CredentialOpt) (*Credential, error) isJWT bool disclosures []string holderBinding string + sdJWTVersion common.SDJWTVersion ) isJWT, vcStr, disclosures, holderBinding = isJWTVC(vcStr) if isJWT { - vcDataDecoded, err = decodeJWTVC(vcStr, vcOpts) + _, vcDataDecoded, err = decodeJWTVC(vcStr, vcOpts) if err != nil { return nil, fmt.Errorf("decode new JWT credential: %w", err) } @@ -864,7 +871,7 @@ func ParseCredential(vcData []byte, opts ...CredentialOpt) (*Credential, error) } } - vc, err := populateCredential(vcDataDecoded, disclosures) + vc, err := populateCredential(vcDataDecoded, disclosures, sdJWTVersion) if err != nil { return nil, err } @@ -910,7 +917,7 @@ func validateDisclosures(vcBytes []byte, disclosures []string) error { return nil } -func populateCredential(vcJSON []byte, sdDisclosures []string) (*Credential, error) { +func populateCredential(vcJSON []byte, sdDisclosures []string, sdJWTVersion common.SDJWTVersion) (*Credential, error) { // Unmarshal raw credential from JSON. var raw rawCredential @@ -920,6 +927,7 @@ func populateCredential(vcJSON []byte, sdDisclosures []string) (*Credential, err } raw.SDJWTDisclosures = sdDisclosures + raw.SDJWTVersion = sdJWTVersion // Create credential from raw. vc, err := newCredential(&raw) @@ -1083,7 +1091,15 @@ func newCredential(raw *rawCredential) (*Credential, error) { return nil, fmt.Errorf("fill credential subject from raw: %w", err) } - disclosures, err := parseDisclosures(raw.SDJWTDisclosures) + alg, _ := common.GetCryptoHash(raw.SDJWTHashAlg) // nolint:errcheck + if alg == 0 { + sub, _ := subjects.([]Subject) // nolint:errcheck + if len(sub) > 0 && len(sub[0].CustomFields) > 0 { + alg, _ = common.GetCryptoHashFromClaims(sub[0].CustomFields) // nolint:errcheck + } + } + + disclosures, err := parseDisclosures(raw.SDJWTDisclosures, alg) if err != nil { return nil, fmt.Errorf("fill credential sdjwt disclosures from raw: %w", err) } @@ -1106,6 +1122,7 @@ func newCredential(raw *rawCredential) (*Credential, error) { JWT: raw.JWT, CustomFields: raw.CustomFields, SDJWTHashAlg: raw.SDJWTHashAlg, + SDJWTVersion: raw.SDJWTVersion, SDJWTDisclosures: disclosures, }, nil } @@ -1132,12 +1149,12 @@ func parseTypedID(data json.RawMessage) ([]TypedID, error) { return nil, err } -func parseDisclosures(disclosures []string) ([]*common.DisclosureClaim, error) { +func parseDisclosures(disclosures []string, hash crypto.Hash) ([]*common.DisclosureClaim, error) { if len(disclosures) == 0 { return nil, nil } - disc, err := common.GetDisclosureClaims(disclosures) + disc, err := common.GetDisclosureClaims(disclosures, hash) if err != nil { return nil, fmt.Errorf("parsing disclosures from SD-JWT credential: %w", err) } @@ -1199,7 +1216,7 @@ func isJWTVC(vcStr string) (bool, string, []string, string) { disclosures = cffp.Disclosures tmpVCStr = cffp.SDJWT - holderBinding = cffp.HolderBinding + holderBinding = cffp.HolderVerification } else { cffi := common.ParseCombinedFormatForIssuance(vcStr) disclosures = cffi.Disclosures @@ -1215,17 +1232,17 @@ func isJWTVC(vcStr string) (bool, string, []string, string) { return false, vcStr, nil, "" } -func decodeJWTVC(vcStr string, vcOpts *credentialOpts) ([]byte, error) { +func decodeJWTVC(vcStr string, vcOpts *credentialOpts) (jose.Headers, []byte, error) { if vcOpts.publicKeyFetcher == nil && !vcOpts.disabledProofCheck { - return nil, errors.New("public key fetcher is not defined") + return nil, nil, errors.New("public key fetcher is not defined") } - vcDecodedBytes, err := decodeCredJWS(vcStr, !vcOpts.disabledProofCheck, vcOpts.publicKeyFetcher) + joseHeaders, vcDecodedBytes, err := decodeCredJWS(vcStr, !vcOpts.disabledProofCheck, vcOpts.publicKeyFetcher) if err != nil { - return nil, fmt.Errorf("JWS decoding: %w", err) + return nil, nil, fmt.Errorf("JWS decoding: %w", err) } - return vcDecodedBytes, nil + return joseHeaders, vcDecodedBytes, nil } func decodeLDVC(vcData []byte, vcStr string, vcOpts *credentialOpts) ([]byte, error) { @@ -1246,7 +1263,7 @@ func decodeLDVC(vcData []byte, vcStr string, vcOpts *credentialOpts) ([]byte, er func JWTVCToJSON(vc []byte) ([]byte, error) { vc = bytes.Trim(vc, "\"' ") - jsonVC, err := decodeCredJWS(string(vc), false, nil) + _, jsonVC, err := decodeCredJWS(string(vc), false, nil) return jsonVC, err } diff --git a/component/models/verifiable/credential_jws.go b/component/models/verifiable/credential_jws.go index 5f14996c8..a935e566f 100644 --- a/component/models/verifiable/credential_jws.go +++ b/component/models/verifiable/credential_jws.go @@ -5,24 +5,32 @@ SPDX-License-Identifier: Apache-2.0 package verifiable +import ( + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" +) + // MarshalJWS serializes JWT into signed form (JWS). func (jcc *JWTCredClaims) MarshalJWS(signatureAlg JWSAlgorithm, signer Signer, keyID string) (string, error) { return marshalJWS(jcc, signatureAlg, signer, keyID) } -func unmarshalJWSClaims(rawJwt string, checkProof bool, fetcher PublicKeyFetcher) (*JWTCredClaims, error) { +func unmarshalJWSClaims( + rawJwt string, + checkProof bool, + fetcher PublicKeyFetcher, +) (jose.Headers, *JWTCredClaims, error) { var claims JWTCredClaims - err := unmarshalJWS(rawJwt, checkProof, fetcher, &claims) + joseHeaders, err := unmarshalJWS(rawJwt, checkProof, fetcher, &claims) if err != nil { - return nil, err + return nil, nil, err } - return &claims, err + return joseHeaders, &claims, err } -func decodeCredJWS(rawJwt string, checkProof bool, fetcher PublicKeyFetcher) ([]byte, error) { - return decodeCredJWT(rawJwt, func(vcJWTBytes string) (*JWTCredClaims, error) { +func decodeCredJWS(rawJwt string, checkProof bool, fetcher PublicKeyFetcher) (jose.Headers, []byte, error) { + return decodeCredJWT(rawJwt, func(vcJWTBytes string) (jose.Headers, *JWTCredClaims, error) { return unmarshalJWSClaims(rawJwt, checkProof, fetcher) }) } diff --git a/component/models/verifiable/credential_jws_test.go b/component/models/verifiable/credential_jws_test.go index 5f6fd4455..4c8f28cab 100644 --- a/component/models/verifiable/credential_jws_test.go +++ b/component/models/verifiable/credential_jws_test.go @@ -15,6 +15,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/require" + ariesjose "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" "github.com/hyperledger/aries-framework-go/component/models/signature/verifier" "github.com/hyperledger/aries-framework-go/spi/kms" ) @@ -33,13 +34,14 @@ func TestJWTCredClaimsMarshalJWS(t *testing.T) { jws, err := jwtClaims.MarshalJWS(RS256, signer, "did:123#key1") require.NoError(t, err) - vcBytes, err := decodeCredJWS(jws, true, func(issuerID, keyID string) (*verifier.PublicKey, error) { + headers, vcBytes, err := decodeCredJWS(jws, true, func(issuerID, keyID string) (*verifier.PublicKey, error) { return &verifier.PublicKey{ Type: kms.RSARS256, Value: signer.PublicKeyBytes(), }, nil }) require.NoError(t, err) + require.Equal(t, ariesjose.Headers{"alg": "RS256", "kid": "did:123#key1"}, headers) vcRaw := new(rawCredential) err = json.Unmarshal(vcBytes, &vcRaw) @@ -70,8 +72,9 @@ func TestCredJWSDecoderUnmarshal(t *testing.T) { validJWS := createRS256JWS(t, []byte(jwtTestCredential), signer, false) t.Run("Successful JWS decoding", func(t *testing.T) { - vcBytes, err := decodeCredJWS(string(validJWS), true, pkFetcher) + headers, vcBytes, err := decodeCredJWS(string(validJWS), true, pkFetcher) require.NoError(t, err) + require.NotNil(t, headers) vcRaw := new(rawCredential) err = json.Unmarshal(vcBytes, &vcRaw) @@ -83,10 +86,11 @@ func TestCredJWSDecoderUnmarshal(t *testing.T) { }) t.Run("Invalid serialized JWS", func(t *testing.T) { - jws, err := decodeCredJWS("invalid JWS", true, pkFetcher) + joseHeaders, jws, err := decodeCredJWS("invalid JWS", true, pkFetcher) require.Error(t, err) require.Contains(t, err.Error(), "unmarshal VC JWT claims") require.Nil(t, jws) + require.Nil(t, joseHeaders) }) t.Run("Invalid format of \"vc\" claim", func(t *testing.T) { @@ -106,10 +110,11 @@ func TestCredJWSDecoderUnmarshal(t *testing.T) { jwtCompact, err := jwt.Signed(signer).Claims(claims).CompactSerialize() require.NoError(t, err) - jws, err := decodeCredJWS(jwtCompact, true, pkFetcher) + joseHeaders, jws, err := decodeCredJWS(jwtCompact, true, pkFetcher) require.Error(t, err) require.Contains(t, err.Error(), "unmarshal VC JWT claims") require.Nil(t, jws) + require.Nil(t, joseHeaders) }) t.Run("Invalid signature of JWS", func(t *testing.T) { @@ -124,9 +129,10 @@ func TestCredJWSDecoderUnmarshal(t *testing.T) { }, nil } - jws, err := decodeCredJWS(string(validJWS), true, pkFetcherOther) + joseHeaders, jws, err := decodeCredJWS(string(validJWS), true, pkFetcherOther) require.Error(t, err) require.Contains(t, err.Error(), "unmarshal VC JWT claims") require.Nil(t, jws) + require.Nil(t, joseHeaders) }) } diff --git a/component/models/verifiable/credential_jwt.go b/component/models/verifiable/credential_jwt.go index 2e7e8b3ee..d49f1bd0e 100644 --- a/component/models/verifiable/credential_jwt.go +++ b/component/models/verifiable/credential_jwt.go @@ -13,6 +13,7 @@ import ( josejwt "github.com/go-jose/go-jose/v3/jwt" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" "github.com/hyperledger/aries-framework-go/component/models/jwt" jsonutil "github.com/hyperledger/aries-framework-go/component/models/util/json" ) @@ -32,6 +33,49 @@ type JWTCredClaims struct { VC map[string]interface{} `json:"vc,omitempty"` } +// ToSDJWTV5CredentialPayload defines custom marshalling of JWTCredClaims. +// Key difference with default marshaller is that returned object does not contain custom "vc" root claim. +// Example: +// +// https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#name-example-4b-w3c-verifiable-c. +func (jcc *JWTCredClaims) ToSDJWTV5CredentialPayload() ([]byte, error) { + type Alias JWTCredClaims + + alias := Alias(*jcc) + + vcMap := alias.VC + + alias.VC = nil + + data, err := jsonutil.MarshalWithCustomFields(alias, vcMap) + if err != nil { + return nil, fmt.Errorf("marshal JWTW3CCredClaims: %w", err) + } + + return data, nil +} + +// UnmarshalJSON defines custom unmarshalling of JWTCredClaims from JSON. +// For SD-JWT case, it supports both v2 and v5 formats. +func (jcc *JWTCredClaims) UnmarshalJSON(data []byte) error { + type Alias JWTCredClaims + + alias := (*Alias)(jcc) + + customFields := make(CustomFields) + + err := jsonutil.UnmarshalWithCustomFields(data, alias, customFields) + if err != nil { + return fmt.Errorf("unmarshal JWTCredClaims: %w", err) + } + + if len(customFields) > 0 && len(alias.VC) == 0 { + alias.VC = customFields + } + + return nil +} + // newJWTCredClaims creates JWT Claims of VC with an option to minimize certain fields of VC // which is put into "vc" claim. func newJWTCredClaims(vc *Credential, minimizeVC bool) (*JWTCredClaims, error) { @@ -91,14 +135,14 @@ func newJWTCredClaims(vc *Credential, minimizeVC bool) (*JWTCredClaims, error) { } // JWTCredClaimsUnmarshaller unmarshals verifiable credential bytes into JWT claims with extra "vc" claim. -type JWTCredClaimsUnmarshaller func(vcJWTBytes string) (*JWTCredClaims, error) +type JWTCredClaimsUnmarshaller func(vcJWTBytes string) (jose.Headers, *JWTCredClaims, error) // decodeCredJWT parses JWT from the specified bytes array in compact format using unmarshaller. -// It returns decoded Verifiable Credential refined by JWT Claims in raw byte array form, and the claims object itself. -func decodeCredJWT(rawJWT string, unmarshaller JWTCredClaimsUnmarshaller) ([]byte, error) { - credClaims, err := unmarshaller(rawJWT) +// It returns jwt.JSONWebToken and decoded Verifiable Credential refined by JWT Claims in raw byte array form. +func decodeCredJWT(rawJWT string, unmarshaller JWTCredClaimsUnmarshaller) (jose.Headers, []byte, error) { + joseHeaders, credClaims, err := unmarshaller(rawJWT) if err != nil { - return nil, fmt.Errorf("unmarshal VC JWT claims: %w", err) + return nil, nil, fmt.Errorf("unmarshal VC JWT claims: %w", err) } // Apply VC-related claims from JWT. @@ -106,10 +150,10 @@ func decodeCredJWT(rawJWT string, unmarshaller JWTCredClaimsUnmarshaller) ([]byt vcData, err := json.Marshal(credClaims.VC) if err != nil { - return nil, errors.New("failed to marshal 'vc' claim of JWT") + return nil, nil, errors.New("failed to marshal 'vc' claim of JWT") } - return vcData, nil + return joseHeaders, vcData, nil } func (jcc *JWTCredClaims) refineFromJWTClaims() { diff --git a/component/models/verifiable/credential_jwt_test.go b/component/models/verifiable/credential_jwt_test.go index aeece760e..dafa29322 100644 --- a/component/models/verifiable/credential_jwt_test.go +++ b/component/models/verifiable/credential_jwt_test.go @@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0 package verifiable import ( + "encoding/json" "errors" "testing" "time" @@ -13,16 +14,18 @@ import ( josejwt "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/require" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" "github.com/hyperledger/aries-framework-go/component/models/jwt" ) func TestDecodeJWT(t *testing.T) { - vcBytes, err := decodeCredJWT("", func(string) (*JWTCredClaims, error) { - return nil, errors.New("cannot parse JWT claims") + joseHeaders, vcBytes, err := decodeCredJWT("", func(string) (jose.Headers, *JWTCredClaims, error) { + return nil, nil, errors.New("cannot parse JWT claims") }) require.Error(t, err) require.Contains(t, err.Error(), "cannot parse JWT claims") require.Nil(t, vcBytes) + require.Nil(t, joseHeaders) } func TestRefineVcFromJwtClaims(t *testing.T) { @@ -53,3 +56,45 @@ func TestRefineVcFromJwtClaims(t *testing.T) { require.Equal(t, "2019-08-10T00:00:00Z", vcMap["issuanceDate"]) require.Equal(t, "2029-08-10T00:00:00Z", vcMap["expirationDate"]) } + +func TestJWTCredClaims_ToSDJWTCredentialPayload(t *testing.T) { + jcc := &JWTCredClaims{ + Claims: &jwt.Claims{ + Issuer: "issuer", + Subject: "subject", + Audience: josejwt.Audience{"leela", "fry"}, + NotBefore: josejwt.NewNumericDate(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)), + ID: "http://example.edu/credentials/3732", + }, + VC: map[string]interface{}{ + "@context": []interface{}{ + "https://www.w3.org/2018/credentials/v1", + "https://trustbloc.github.io/context/vc/examples-v1.jsonld", + }, + "id": "http://example.edu/credentials/1989", + "type": "VerifiableCredential", + "credentialSubject": map[string]interface{}{ + "id": "did:example:iuajk1f712ebc6f1c276e12ec21", + }, + "issuer": map[string]interface{}{ + "id": "did:example:09s12ec712ebc6f1c671ebfeb1f", + "name": "Example University", + }, + "issuanceDate": "2020-01-01T10:54:01Z", + "credentialStatus": map[string]interface{}{ + "id": "https://example.gov/status/65", + "type": "CredentialStatusList2017", + }, + }, + } + + got, err := jcc.ToSDJWTV5CredentialPayload() + require.NoError(t, err) + require.NotContains(t, string(got), `"vc"`) + + var jccMapped *JWTCredClaims + err = json.Unmarshal(got, &jccMapped) + require.NoError(t, err) + + require.Equal(t, jcc, jccMapped) +} diff --git a/component/models/verifiable/credential_jwt_unsecured.go b/component/models/verifiable/credential_jwt_unsecured.go index df373904c..0ed2aeb4e 100644 --- a/component/models/verifiable/credential_jwt_unsecured.go +++ b/component/models/verifiable/credential_jwt_unsecured.go @@ -7,6 +7,8 @@ package verifiable import ( "fmt" + + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" ) // MarshalUnsecuredJWT serialized JWT into unsecured JWT. @@ -14,17 +16,19 @@ func (jcc *JWTCredClaims) MarshalUnsecuredJWT() (string, error) { return marshalUnsecuredJWT(nil, jcc) } -func unmarshalUnsecuredJWTClaims(rawJWT string) (*JWTCredClaims, error) { +func unmarshalUnsecuredJWTClaims(rawJWT string) (jose.Headers, *JWTCredClaims, error) { var claims JWTCredClaims - err := unmarshalUnsecuredJWT(rawJWT, &claims) + hoseHeaders, err := unmarshalUnsecuredJWT(rawJWT, &claims) if err != nil { - return nil, fmt.Errorf("parse VC in JWT Unsecured form: %w", err) + return nil, nil, fmt.Errorf("parse VC in JWT Unsecured form: %w", err) } - return &claims, nil + return hoseHeaders, &claims, nil } func decodeCredJWTUnsecured(rawJwt string) ([]byte, error) { - return decodeCredJWT(rawJwt, unmarshalUnsecuredJWTClaims) + _, vcBytes, err := decodeCredJWT(rawJwt, unmarshalUnsecuredJWTClaims) + + return vcBytes, err } diff --git a/component/models/verifiable/credential_sdjwt.go b/component/models/verifiable/credential_sdjwt.go index 65591d33a..49fffa796 100644 --- a/component/models/verifiable/credential_sdjwt.go +++ b/component/models/verifiable/credential_sdjwt.go @@ -12,6 +12,7 @@ import ( "fmt" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/holder" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/issuer" @@ -25,6 +26,8 @@ type marshalDisclosureOpts struct { holderBinding *holder.BindingInfo signer jose.Signer signingKeyID string + + sdjwtVersion common.SDJWTVersion } // MarshalDisclosureOption provides an option for Credential.MarshalWithDisclosure. @@ -81,10 +84,26 @@ func DisclosureSigner(signer jose.Signer, signingKeyID string) MarshalDisclosure } } +// MarshalWithSDJWTVersion sets version for SD-JWT VC. +func MarshalWithSDJWTVersion(version common.SDJWTVersion) MarshalDisclosureOption { + return func(opts *marshalDisclosureOpts) { + opts.sdjwtVersion = version + } +} + // MarshalWithDisclosure marshals a SD-JWT credential in combined format for presentation, including precisely // the disclosures indicated by provided options, and optionally a holder binding if given the requisite option. func (vc *Credential) MarshalWithDisclosure(opts ...MarshalDisclosureOption) (string, error) { - options := &marshalDisclosureOpts{} + // Take default SD JWT version + sdJWTVersion := common.SDJWTVersionDefault + if vc.SDJWTVersion != 0 { + // If SD JWT version present in VC - use it as default. + sdJWTVersion = vc.SDJWTVersion + } + + options := &marshalDisclosureOpts{ + sdjwtVersion: sdJWTVersion, + } for _, opt := range opts { opt(options) @@ -95,6 +114,7 @@ func (vc *Credential) MarshalWithDisclosure(opts ...MarshalDisclosureOption) (st } if vc.JWT != "" && vc.SDJWTHashAlg != "" { + // If VC already in SD JWT format. return filterSDJWTVC(vc, options) } @@ -102,6 +122,7 @@ func (vc *Credential) MarshalWithDisclosure(opts ...MarshalDisclosureOption) (st return "", fmt.Errorf("credential needs signer to create SD-JWT") } + // If VC in not SD JWT. return createSDJWTPresentation(vc, options) } @@ -112,13 +133,13 @@ func filterSDJWTVC(vc *Credential, options *marshalDisclosureOpts) (string, erro } cf := common.CombinedFormatForPresentation{ - SDJWT: vc.JWT, - Disclosures: disclosureCodes, - HolderBinding: vc.SDHolderBinding, + SDJWT: vc.JWT, + Disclosures: disclosureCodes, + HolderVerification: vc.SDHolderBinding, } if options.holderBinding != nil { - cf.HolderBinding, err = holder.CreateHolderBinding(options.holderBinding) + cf.HolderVerification, err = holder.CreateHolderVerification(options.holderBinding) if err != nil { return "", fmt.Errorf("failed to create holder binding: %w", err) } @@ -128,12 +149,14 @@ func filterSDJWTVC(vc *Credential, options *marshalDisclosureOpts) (string, erro } func createSDJWTPresentation(vc *Credential, options *marshalDisclosureOpts) (string, error) { - issued, err := makeSDJWT(vc, options.signer, options.signingKeyID) + issued, err := makeSDJWT(vc, options.signer, options.signingKeyID, MakeSDJWTWithVersion(options.sdjwtVersion)) if err != nil { return "", fmt.Errorf("creating SD-JWT from Credential: %w", err) } - disclosureClaims, err := common.GetDisclosureClaims(issued.Disclosures) + alg, _ := common.GetCryptoHashFromClaims(issued.SignedJWT.Payload) // nolint:errcheck + + disclosureClaims, err := common.GetDisclosureClaims(issued.Disclosures, alg) if err != nil { return "", fmt.Errorf("parsing disclosure claims from vc sdjwt: %w", err) } @@ -146,7 +169,7 @@ func createSDJWTPresentation(vc *Credential, options *marshalDisclosureOpts) (st var presOpts []holder.Option if options.holderBinding != nil { - presOpts = append(presOpts, holder.WithHolderBinding(options.holderBinding)) + presOpts = append(presOpts, holder.WithHolderVerification(options.holderBinding)) } issuedSerialized, err := issued.Serialize(false) @@ -235,23 +258,76 @@ func filterDisclosures( return out, nil } -type makeSDJWTOpts struct { - hashAlg crypto.Hash +// MakeSDJWTOpts provides SD-JWT options for VC. +type MakeSDJWTOpts struct { + hashAlg crypto.Hash + version common.SDJWTVersion + recursiveClaimsObject []string + alwaysIncludeObjects []string + nonSDClaims []string +} + +// GetNonSDClaims returns nonSDClaims mostly for testing purposes. +func (o *MakeSDJWTOpts) GetNonSDClaims() []string { + return o.nonSDClaims +} + +// GetRecursiveClaimsObject returns recursiveClaimsObject mostly for testing purposes. +func (o *MakeSDJWTOpts) GetRecursiveClaimsObject() []string { + return o.recursiveClaimsObject +} + +// GetAlwaysIncludeObject returns alwaysIncludeObjects mostly for testing purposes. +func (o *MakeSDJWTOpts) GetAlwaysIncludeObject() []string { + return o.alwaysIncludeObjects } // MakeSDJWTOption provides an option for creating an SD-JWT from a VC. -type MakeSDJWTOption func(opts *makeSDJWTOpts) +type MakeSDJWTOption func(opts *MakeSDJWTOpts) // MakeSDJWTWithHash sets the hash to use for an SD-JWT VC. func MakeSDJWTWithHash(hash crypto.Hash) MakeSDJWTOption { - return func(opts *makeSDJWTOpts) { + return func(opts *MakeSDJWTOpts) { opts.hashAlg = hash } } +// MakeSDJWTWithVersion sets version for SD-JWT VC. +func MakeSDJWTWithVersion(version common.SDJWTVersion) MakeSDJWTOption { + return func(opts *MakeSDJWTOpts) { + opts.version = version + } +} + +// MakeSDJWTWithRecursiveClaimsObjects sets version for SD-JWT VC. SD-JWT v5+ support. +func MakeSDJWTWithRecursiveClaimsObjects(recursiveClaimsObject []string) MakeSDJWTOption { + return func(opts *MakeSDJWTOpts) { + opts.recursiveClaimsObject = recursiveClaimsObject + } +} + +// MakeSDJWTWithAlwaysIncludeObjects is an option for provide object keys that should be a part of +// selectively disclosable claims. +func MakeSDJWTWithAlwaysIncludeObjects(alwaysIncludeObjects []string) MakeSDJWTOption { + return func(opts *MakeSDJWTOpts) { + opts.alwaysIncludeObjects = alwaysIncludeObjects + } +} + +// MakeSDJWTWithNonSelectivelyDisclosableClaims is an option for provide claim names +// that should be ignored when creating selectively disclosable claims. +func MakeSDJWTWithNonSelectivelyDisclosableClaims(nonSDClaims []string) MakeSDJWTOption { + return func(opts *MakeSDJWTOpts) { + opts.nonSDClaims = nonSDClaims + } +} + // MakeSDJWT creates an SD-JWT in combined format for issuance, with all fields in credentialSubject converted // recursively into selectively-disclosable SD-JWT claims. -func (vc *Credential) MakeSDJWT(signer jose.Signer, signingKeyID string, options ...MakeSDJWTOption) (string, error) { +func (vc *Credential) MakeSDJWT( + signer jose.Signer, + signingKeyID string, + options ...MakeSDJWTOption) (string, error) { sdjwt, err := makeSDJWT(vc, signer, signingKeyID, options...) if err != nil { return "", err @@ -265,9 +341,22 @@ func (vc *Credential) MakeSDJWT(signer jose.Signer, signingKeyID string, options return sdjwtSerialized, nil } -func makeSDJWT(vc *Credential, signer jose.Signer, signingKeyID string, options ...MakeSDJWTOption, +func makeSDJWT( //nolint:funlen,gocyclo + vc *Credential, + signer jose.Signer, + signingKeyID string, + options ...MakeSDJWTOption, ) (*issuer.SelectiveDisclosureJWT, error) { - opts := &makeSDJWTOpts{} + // Take default SD JWT version + sdJWTVersion := common.SDJWTVersionDefault + if vc.SDJWTVersion != 0 { + // If SD JWT version present in VC - use it as default. + sdJWTVersion = vc.SDJWTVersion + } + + opts := &MakeSDJWTOpts{ + version: sdJWTVersion, + } for _, option := range options { option(opts) @@ -278,7 +367,13 @@ func makeSDJWT(vc *Credential, signer jose.Signer, signingKeyID string, options return nil, fmt.Errorf("constructing VC JWT claims: %w", err) } - claimBytes, err := json.Marshal(claims) + var claimBytes []byte + if opts.version == common.SDJWTVersionV5 { + claimBytes, err = claims.ToSDJWTV5CredentialPayload() + } else { + claimBytes, err = json.Marshal(claims) + } + if err != nil { return nil, err } @@ -294,11 +389,32 @@ func makeSDJWT(vc *Credential, signer jose.Signer, signingKeyID string, options jose.HeaderKeyID: signingKeyID, } + if opts.version == common.SDJWTVersionV5 { + headers[jose.HeaderType] = "vc+sd-jwt" + } + issuerOptions := []issuer.NewOpt{ issuer.WithStructuredClaims(true), - issuer.WithNonSelectivelyDisclosableClaims([]string{"id"}), + issuer.WithSDJWTVersion(opts.version), + } + + if len(opts.recursiveClaimsObject) > 0 { + issuerOptions = append(issuerOptions, + issuer.WithRecursiveClaimsObjects(opts.recursiveClaimsObject), + ) + } + + if len(opts.alwaysIncludeObjects) > 0 { + issuerOptions = append(issuerOptions, + issuer.WithAlwaysIncludeObjects(opts.alwaysIncludeObjects), + ) } + opts.nonSDClaims = append(opts.nonSDClaims, "id") + issuerOptions = append(issuerOptions, + issuer.WithNonSelectivelyDisclosableClaims(opts.nonSDClaims), + ) + if opts.hashAlg != 0 { issuerOptions = append(issuerOptions, issuer.WithHashAlgorithm(opts.hashAlg)) } @@ -359,7 +475,7 @@ func (vc *Credential) CreateDisplayCredential( // nolint:funlen,gocyclo return vc, nil } - credClaims, err := unmarshalJWSClaims(vc.JWT, false, nil) + _, credClaims, err := unmarshalJWSClaims(vc.JWT, false, nil) if err != nil { return nil, fmt.Errorf("unmarshal VC JWT claims: %w", err) } @@ -382,7 +498,7 @@ func (vc *Credential) CreateDisplayCredential( // nolint:funlen,gocyclo return nil, fmt.Errorf("marshalling vc object to JSON: %w", err) } - newVC, err := populateCredential(vcBytes, nil) + newVC, err := populateCredential(vcBytes, nil, 0) if err != nil { return nil, fmt.Errorf("parsing new VC from JSON: %w", err) } @@ -419,7 +535,7 @@ func (vc *Credential) CreateDisplayCredentialMap( // nolint:funlen,gocyclo return json2.ToMap(bytes) } - credClaims, err := unmarshalJWSClaims(vc.JWT, false, nil) + _, credClaims, err := unmarshalJWSClaims(vc.JWT, false, nil) if err != nil { return nil, fmt.Errorf("unmarshal VC JWT claims: %w", err) } diff --git a/component/models/verifiable/credential_sdjwt_test.go b/component/models/verifiable/credential_sdjwt_test.go index d02ca29b7..aca6ed345 100644 --- a/component/models/verifiable/credential_sdjwt_test.go +++ b/component/models/verifiable/credential_sdjwt_test.go @@ -15,13 +15,15 @@ import ( "testing" "github.com/go-jose/go-jose/v3/jwt" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + "github.com/hyperledger/aries-framework-go/spi/kms" + afgojwt "github.com/hyperledger/aries-framework-go/component/models/jwt" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/holder" - "github.com/hyperledger/aries-framework-go/spi/kms" ) func TestParseSDJWT(t *testing.T) { @@ -37,6 +39,16 @@ func TestParseSDJWT(t *testing.T) { require.NotNil(t, newVC) }) + t.Run("success with SD JWT Version 5", func(t *testing.T) { + sdJWTCredFormatString, issuerCredFormatID := createTestSDJWTCred(t, privKey, + MakeSDJWTWithVersion(common.SDJWTVersionV5)) + + newVC, e := ParseCredential([]byte(sdJWTCredFormatString), + WithPublicKeyFetcher(createDIDKeyFetcher(t, pubKey, issuerCredFormatID))) + require.NoError(t, e) + require.NotNil(t, newVC) + }) + t.Run("success with sd alg in subject", func(t *testing.T) { vc, e := ParseCredential([]byte(sdJWTString), WithDisabledProofCheck()) require.NoError(t, e) @@ -62,6 +74,32 @@ func TestParseSDJWT(t *testing.T) { require.NotNil(t, newVC) }) + t.Run("success with sd alg in subject and v5", func(t *testing.T) { + vc, e := ParseCredential([]byte(sdJWTString), WithDisabledProofCheck()) + require.NoError(t, e) + + claims, e := vc.JWTClaims(false) + require.NoError(t, e) + + claims.VC["credentialSubject"].(map[string]interface{})["_sd_alg"] = claims.VC["_sd_alg"] + delete(claims.VC, "_sd_alg") + + ed25519Signer, e := newCryptoSigner(kms.ED25519Type) + require.NoError(t, e) + + vc.JWT, e = claims.MarshalJWS(EdDSA, ed25519Signer, issuerID+"#keys-1") + require.NoError(t, e) + + vc.SDJWTVersion = 100500 + modifiedCred, e := vc.MarshalWithDisclosure(DiscloseAll(), MarshalWithSDJWTVersion(common.SDJWTVersionV5)) + require.NoError(t, e) + + newVC, e := ParseCredential([]byte(modifiedCred), + WithPublicKeyFetcher(createDIDKeyFetcher(t, ed25519Signer.PublicKeyBytes(), issuerID))) + require.NoError(t, e) + require.NotNil(t, newVC) + }) + t.Run("success with mock holder binding", func(t *testing.T) { mockHolderBinding := "e30.e30.mockHolderBinding" @@ -122,7 +160,7 @@ func TestMarshalWithDisclosure(t *testing.T) { }) require.Equal(t, src.Disclosures, res.Disclosures) - require.NotEmpty(t, res.HolderBinding) + require.NotEmpty(t, res.HolderVerification) }) t.Run("disclose required and some if-available claims", func(t *testing.T) { @@ -159,7 +197,7 @@ func TestMarshalWithDisclosure(t *testing.T) { res := common.ParseCombinedFormatForPresentation(resultCred) require.Len(t, res.Disclosures, 1) - require.NotEmpty(t, res.HolderBinding) + require.NotEmpty(t, res.HolderVerification) }) }) @@ -306,6 +344,25 @@ func TestMakeSDJWT(t *testing.T) { require.NoError(t, err) }) + t.Run("with SD JWT V5", func(t *testing.T) { + originalVersion := vc.SDJWTVersion + vc.SDJWTVersion = common.SDJWTVersionDefault + defer func() { + vc.SDJWTVersion = originalVersion + }() + + sdjwt, err := vc.MakeSDJWT( + afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1", + MakeSDJWTWithVersion(common.SDJWTVersionV5), + MakeSDJWTWithRecursiveClaimsObjects([]string{"degree"}), + MakeSDJWTWithAlwaysIncludeObjects([]string{"degree"}), + ) + require.NoError(t, err) + + _, err = ParseCredential([]byte(sdjwt), WithPublicKeyFetcher(holderPublicKeyFetcher(pubKey))) + require.NoError(t, err) + }) + t.Run("with hash option", func(t *testing.T) { sdjwt, err := vc.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1", MakeSDJWTWithHash(crypto.SHA512)) @@ -338,6 +395,25 @@ func TestMakeSDJWT(t *testing.T) { }) } +func TestOptions(t *testing.T) { + opts := []MakeSDJWTOption{ + MakeSDJWTWithRecursiveClaimsObjects([]string{"aa", "bb"}), + MakeSDJWTWithAlwaysIncludeObjects([]string{"cc", "dd"}), + MakeSDJWTWithNonSelectivelyDisclosableClaims([]string{"xx", "yy"}), + MakeSDJWTWithVersion(100500), + } + + opt := &MakeSDJWTOpts{} + for _, o := range opts { + o(opt) + } + + assert.Equal(t, []string{"aa", "bb"}, opt.GetRecursiveClaimsObject()) + assert.Equal(t, []string{"cc", "dd"}, opt.GetAlwaysIncludeObject()) + assert.Equal(t, []string{"xx", "yy"}, opt.GetNonSDClaims()) + assert.Equal(t, common.SDJWTVersion(100500), opt.version) +} + func TestCreateDisplayCredential(t *testing.T) { ed25519Signer, e := newCryptoSigner(kms.ED25519Type) require.NoError(t, e) @@ -510,7 +586,8 @@ func (m *mockSigner) Headers() jose.Headers { return jose.Headers{"alg": "foo"} } -func createTestSDJWTCred(t *testing.T, privKey ed25519.PrivateKey) (sdJWTCred string, issuerID string) { +func createTestSDJWTCred( + t *testing.T, privKey ed25519.PrivateKey, opts ...MakeSDJWTOption) (sdJWTCred string, issuerID string) { t.Helper() testCred := []byte(jwtTestCredential) @@ -518,7 +595,7 @@ func createTestSDJWTCred(t *testing.T, privKey ed25519.PrivateKey) (sdJWTCred st srcVC, err := parseTestCredential(t, testCred) require.NoError(t, err) - sdjwt, err := srcVC.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), srcVC.Issuer.ID+"#keys-1") + sdjwt, err := srcVC.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), srcVC.Issuer.ID+"#keys-1", opts...) require.NoError(t, err) return sdjwt, srcVC.Issuer.ID diff --git a/component/models/verifiable/credential_test.go b/component/models/verifiable/credential_test.go index 436dcd69d..48f4437eb 100644 --- a/component/models/verifiable/credential_test.go +++ b/component/models/verifiable/credential_test.go @@ -11,20 +11,23 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" "time" "github.com/piprate/json-gold/ld" "github.com/stretchr/testify/require" "github.com/xeipuuv/gojsonschema" + "golang.org/x/exp/slices" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + "github.com/hyperledger/aries-framework-go/spi/kms" + jsonld "github.com/hyperledger/aries-framework-go/component/models/ld/processor" "github.com/hyperledger/aries-framework-go/component/models/signature/suite" "github.com/hyperledger/aries-framework-go/component/models/signature/suite/ed25519signature2018" "github.com/hyperledger/aries-framework-go/component/models/signature/verifier" jsonutil "github.com/hyperledger/aries-framework-go/component/models/util/json" - "github.com/hyperledger/aries-framework-go/spi/kms" ) const singleCredentialSubject = ` @@ -729,7 +732,13 @@ func TestCredential_MarshalJSON(t *testing.T) { // original sd-jwt is in 'issuance' format, without a trailing tilde, while MarshalJSON will marshal // in 'presentation' format, including a trailing tilde if the sd-jwt has disclosures but no holder binding. - require.Equal(t, string(unQuote([]byte(sdJWTString)))+"~", string(unQuote(byteCred))) + + sdJWTSegments := strings.Split(string(unQuote([]byte(sdJWTString)))+"~", "~") + byteCredSegments := strings.Split(string(unQuote(byteCred)), "~") + + slices.Sort(sdJWTSegments) + slices.Sort(byteCredSegments) + require.Equal(t, sdJWTSegments, byteCredSegments) // convert SD-JWT json string to verifiable credential cred2, err := ParseCredential(byteCred, diff --git a/component/models/verifiable/jws.go b/component/models/verifiable/jws.go index 5c9c58618..52b20ec59 100644 --- a/component/models/verifiable/jws.go +++ b/component/models/verifiable/jws.go @@ -71,7 +71,7 @@ func marshalJWS(jwtClaims interface{}, signatureAlg JWSAlgorithm, signer Signer, return token.Serialize(false) } -func unmarshalJWS(rawJwt string, checkProof bool, fetcher PublicKeyFetcher, claims interface{}) error { +func unmarshalJWS(rawJwt string, checkProof bool, fetcher PublicKeyFetcher, claims interface{}) (jose.Headers, error) { var verifier jose.SignatureVerifier if checkProof { @@ -80,18 +80,18 @@ func unmarshalJWS(rawJwt string, checkProof bool, fetcher PublicKeyFetcher, clai verifier = &noVerifier{} } - _, claimsRaw, err := jwt.Parse(rawJwt, + jsonWebToken, claimsRaw, err := jwt.Parse(rawJwt, jwt.WithSignatureVerifier(verifier), jwt.WithIgnoreClaimsMapDecoding(true), ) if err != nil { - return fmt.Errorf("parse JWT: %w", err) + return nil, fmt.Errorf("parse JWT: %w", err) } err = json.Unmarshal(claimsRaw, claims) if err != nil { - return err + return nil, err } - return nil + return jsonWebToken.Headers, nil } diff --git a/component/models/verifiable/jwt_unsecured.go b/component/models/verifiable/jwt_unsecured.go index ff9ac6b28..c9c3c5624 100644 --- a/component/models/verifiable/jwt_unsecured.go +++ b/component/models/verifiable/jwt_unsecured.go @@ -21,11 +21,11 @@ func marshalUnsecuredJWT(headers jose.Headers, claims interface{}) (string, erro return token.Serialize(false) } -func unmarshalUnsecuredJWT(rawJWT string, claims interface{}) error { +func unmarshalUnsecuredJWT(rawJWT string, claims interface{}) (jose.Headers, error) { token, _, err := jwt.Parse(rawJWT, jwt.WithSignatureVerifier(jwt.UnsecuredJWTVerifier())) if err != nil { - return fmt.Errorf("unmarshal unsecured JWT: %w", err) + return nil, fmt.Errorf("unmarshal unsecured JWT: %w", err) } - return token.DecodeClaims(claims) + return token.Headers, token.DecodeClaims(claims) } diff --git a/component/models/verifiable/jwt_unsecured_test.go b/component/models/verifiable/jwt_unsecured_test.go index 8737ff1d0..96950fb6c 100644 --- a/component/models/verifiable/jwt_unsecured_test.go +++ b/component/models/verifiable/jwt_unsecured_test.go @@ -22,10 +22,11 @@ func TestUnsecuredJWT(t *testing.T) { require.NotEmpty(t, serializedJWT) var claimsParsed map[string]interface{} - err = unmarshalUnsecuredJWT(serializedJWT, &claimsParsed) + joseHeaders, err := unmarshalUnsecuredJWT(serializedJWT, &claimsParsed) require.NoError(t, err) require.Equal(t, claims, claimsParsed) + require.Equal(t, joseHeaders, headers) // marshal with invalid claims invalidClaims := map[string]interface{}{"error": map[chan int]interface{}{make(chan int): 6}} @@ -35,8 +36,9 @@ func TestUnsecuredJWT(t *testing.T) { require.Empty(t, serializedJWT) // unmarshal invalid JWT - err = unmarshalUnsecuredJWT("not a valid compact serialized JWT", &claimsParsed) + joseHeaders, err = unmarshalUnsecuredJWT("not a valid compact serialized JWT", &claimsParsed) require.Error(t, err) require.Contains(t, err.Error(), "marshal unsecured JWT") require.Empty(t, serializedJWT) + require.Empty(t, joseHeaders) } diff --git a/component/models/verifiable/presentation_jws.go b/component/models/verifiable/presentation_jws.go index 10e056494..93d3acce0 100644 --- a/component/models/verifiable/presentation_jws.go +++ b/component/models/verifiable/presentation_jws.go @@ -13,7 +13,7 @@ func (jpc *JWTPresClaims) MarshalJWS(signatureAlg JWSAlgorithm, signer Signer, k func unmarshalPresJWSClaims(vpJWT string, checkProof bool, fetcher PublicKeyFetcher) (*JWTPresClaims, error) { var claims JWTPresClaims - err := unmarshalJWS(vpJWT, checkProof, fetcher, &claims) + _, err := unmarshalJWS(vpJWT, checkProof, fetcher, &claims) if err != nil { return nil, err } diff --git a/component/models/verifiable/presentation_jwt_unsecured.go b/component/models/verifiable/presentation_jwt_unsecured.go index 5cfc6f9f8..7b653a3c8 100644 --- a/component/models/verifiable/presentation_jwt_unsecured.go +++ b/component/models/verifiable/presentation_jwt_unsecured.go @@ -17,7 +17,7 @@ func (jpc *JWTPresClaims) MarshalUnsecuredJWT() (string, error) { func unmarshalUnsecuredJWTPresClaims(vpJWT string) (*JWTPresClaims, error) { var claims JWTPresClaims - err := unmarshalUnsecuredJWT(vpJWT, &claims) + _, err := unmarshalUnsecuredJWT(vpJWT, &claims) if err != nil { return nil, fmt.Errorf("parse VP in JWT Unsecured form: %w", err) } diff --git a/go.mod b/go.mod index 0e6f69951..2fc734a4c 100644 --- a/go.mod +++ b/go.mod @@ -73,9 +73,12 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/time v0.1.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) + +replace github.com/hyperledger/aries-framework-go/component/models => ./component/models diff --git a/go.sum b/go.sum index 6aabe2c30..2d5fc36dd 100644 --- a/go.sum +++ b/go.sum @@ -60,7 +60,7 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= @@ -77,8 +77,6 @@ github.com/hyperledger/aries-framework-go/component/kmscrypto v0.0.0-20230622082 github.com/hyperledger/aries-framework-go/component/kmscrypto v0.0.0-20230622082138-3ffab1691857/go.mod h1:xgNlHAVQjqwoknzHbXkeHkAJgUxRWKfHXPT3nhVhH3Q= github.com/hyperledger/aries-framework-go/component/log v0.0.0-20230427134832-0c9969493bd3 h1:x5qFQraTX86z9GCwF28IxfnPm6QH5YgHaX+4x97Jwvw= github.com/hyperledger/aries-framework-go/component/log v0.0.0-20230427134832-0c9969493bd3/go.mod h1:CvYs4l8X2NrrF93weLOu5RTOIJeVdoZITtjEflyuTyM= -github.com/hyperledger/aries-framework-go/component/models v0.0.0-20230622171716-43af8054a539 h1:3wjwGDB4/D2z4lZexGtD8tf13KRy/jiXqI9mtiEHmUo= -github.com/hyperledger/aries-framework-go/component/models v0.0.0-20230622171716-43af8054a539/go.mod h1:Qklxf9WG44vpLGF+Efs1aCWeHhsVOU0HFvEslf0RDrQ= github.com/hyperledger/aries-framework-go/component/storage/edv v0.0.0-20221025204933-b807371b6f1e h1:/hrQfwJvHJrwV2FSmfnRp5L6yKY9DqDFqwYyb+oVuDU= github.com/hyperledger/aries-framework-go/component/storage/edv v0.0.0-20221025204933-b807371b6f1e/go.mod h1:ACGP1L+WeecDtyA0Mi2E1kqtPLIGrCWPSJ43q2elwX8= github.com/hyperledger/aries-framework-go/component/storageutil v0.0.0-20230427134832-0c9969493bd3 h1:JGYA9l5zTlvsvfnXT9hYPpCokAjmVKX0/r7njba7OX4= @@ -182,6 +180,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -199,7 +199,6 @@ golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= diff --git a/pkg/doc/sdjwt/common/common.go b/pkg/doc/sdjwt/common/common.go index f16e51182..4463f52e8 100644 --- a/pkg/doc/sdjwt/common/common.go +++ b/pkg/doc/sdjwt/common/common.go @@ -32,8 +32,8 @@ type CombinedFormatForPresentation = common.CombinedFormatForPresentation type DisclosureClaim = common.DisclosureClaim // GetDisclosureClaims de-codes disclosures. -func GetDisclosureClaims(disclosures []string) ([]*DisclosureClaim, error) { - return common.GetDisclosureClaims(disclosures) +func GetDisclosureClaims(disclosures []string, hash crypto.Hash) ([]*DisclosureClaim, error) { + return common.GetDisclosureClaims(disclosures, hash) } // ParseCombinedFormatForIssuance parses combined format for issuance into CombinedFormatForIssuance parts. diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go index ca6134519..5e3c752da 100644 --- a/pkg/doc/sdjwt/holder/holder.go +++ b/pkg/doc/sdjwt/holder/holder.go @@ -8,9 +8,10 @@ SPDX-License-Identifier: Apache-2.0 package holder import ( - "github.com/hyperledger/aries-framework-go/component/models/sdjwt/holder" + "time" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + "github.com/hyperledger/aries-framework-go/component/models/sdjwt/holder" ) // Claim defines claim. @@ -29,6 +30,29 @@ func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt { return holder.WithSignatureVerifier(signatureVerifier) } +// WithIssuerSigningAlgorithms option is for defining secure signing algorithms (for holder verification). +func WithIssuerSigningAlgorithms(algorithms []string) ParseOpt { + return holder.WithIssuerSigningAlgorithms(algorithms) +} + +// WithLeewayForClaimsValidation is an option for claims time(s) validation. +func WithLeewayForClaimsValidation(duration time.Duration) ParseOpt { + return holder.WithLeewayForClaimsValidation(duration) +} + +// WithSDJWTV5Validation option is for defining additional holder verification defined in SDJWT V5 spec. +// Section: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#section-6.1-3 +func WithSDJWTV5Validation(flag bool) ParseOpt { + return holder.WithSDJWTV5Validation(flag) +} + +// WithExpectedTypHeader is an option for JWT typ header validation. +// Might be relevant for SDJWT V5 VC validation. +// Spec: https://vcstuff.github.io/draft-terbu-sd-jwt-vc/draft-terbu-oauth-sd-jwt-vc.html#name-header-parameters +func WithExpectedTypHeader(typ string) ParseOpt { + return holder.WithExpectedTypHeader(typ) +} + // Parse parses issuer SD-JWT and returns claims that can be selected. // The Holder MUST perform the following (or equivalent) steps when receiving a Combined Format for Issuance: // @@ -58,8 +82,14 @@ type BindingInfo = holder.BindingInfo type Option = holder.Option // WithHolderBinding option to set optional holder binding. +// Deprecated. Use WithHolderVerification instead. func WithHolderBinding(info *BindingInfo) Option { - return holder.WithHolderBinding(info) + return holder.WithHolderVerification(info) +} + +// WithHolderVerification option to set optional holder binding. +func WithHolderVerification(info *BindingInfo) Option { + return holder.WithHolderVerification(info) } // CreatePresentation is a convenience method to assemble combined format for presentation @@ -77,7 +107,7 @@ func CreatePresentation(combinedFormatForIssuance string, claimsToDisclose []str // CreateHolderBinding will create holder binding from binding info. func CreateHolderBinding(info *BindingInfo) (string, error) { - return holder.CreateHolderBinding(info) + return holder.CreateHolderVerification(info) } // NoopSignatureVerifier is no-op signature verifier (signature will not get checked). diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go index 4e9b60379..9cd62449d 100644 --- a/pkg/doc/sdjwt/issuer/issuer.go +++ b/pkg/doc/sdjwt/issuer/issuer.go @@ -47,6 +47,7 @@ import ( "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk" + "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/issuer" ) @@ -56,6 +57,11 @@ type Claims = issuer.Claims // NewOpt is the SD-JWT New option. type NewOpt = issuer.NewOpt +// WithSDJWTVersion sets version for SD-JWT VC. +func WithSDJWTVersion(version common.SDJWTVersion) NewOpt { + return issuer.WithSDJWTVersion(version) +} + // WithJSONMarshaller is option is for marshalling disclosure. func WithJSONMarshaller(jsonMarshal func(v interface{}) ([]byte, error)) NewOpt { return issuer.WithJSONMarshaller(jsonMarshal) @@ -146,6 +152,107 @@ func WithNonSelectivelyDisclosableClaims(nonSDClaims []string) NewOpt { return issuer.WithNonSelectivelyDisclosableClaims(nonSDClaims) } +// WithAlwaysIncludeObjects is an option for provide object keys that should be a part of +// selectively disclosable claims. +// Eexample if you would like to keep original claims structure from example below, but selectively disclose all claims +// +// { +// "degree": { +// "degree": "MIT", +// "type": "BachelorDegree", +// }, +// "name": "Jayden Doe", +// "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", +// } +// +// you should specify the following array: []string{"degree"}. +// As output, you will receive: +// +// { +// "_sd": [ +// "zDSZ9PKx_bB2CrFU8Xd__LkpMip06ApY-V6Y9fnppuo", +// "5Hnqg9PgQ4MdHxTv2KDt9qp8ILd1JEYq0luNO8JZ7G4" +// ], +// "degree": { +// "_sd": [ +// "i03SehlKmaFrwPM-gX8s3XuF_LTTE2T1XQQSJXjo6pw", +// "qZEZR8g_uc8fMyQCvs4DjXdY8uOI9IHpOokzx0cH_Qw" +// ] +// } +// } +func WithAlwaysIncludeObjects(alwaysIncludeObjects []string) NewOpt { + return issuer.WithAlwaysIncludeObjects(alwaysIncludeObjects) +} + +// WithRecursiveClaimsObjects is an option for provide object keys that should be selective disclosed recursively, e.g. +// output digest for given object will refer to the disclosure, that contains digests of nested claims. +// For example if you would like to define degree object as selective disclosed recursively +// +// { +// "degree": { +// "degree": "MIT", +// "type": "BachelorDegree", +// }, +// "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", +// } +// +// you should specify the following array: []string{"degree"}. +// As output, you will receive: +// +// { +// "_sd": [ +// "fgoQstuIzTLQ4zqosjUC_qCk-xx3wjDQU2QkQtbn7FI", +// "mdephPRizMUa-LLs3JVeuTRS0tPaTd0faHg5kgKHNGk" +// ] +// } +// +// and 4 disclosures: +// nolint:lll +// [ +// +// { +// "Result": "WyJ2Y2g2YXVDVEo3bGdWWjFxNjN3cWF3IiwiZGVncmVlIix7Il9zZCI6WyJnZnNlcUhtTml0SXUwLTBoMTR5bnFNenV2cTFFaXJUQXpVaERuRWxTVlgwIiwiNDNoZm5NN1N6WnNhbEFkYlhReXE3dzRVdmQ1M1lPeFRORnBGSnI0WkcwQSJdfV0", +// "Salt": "vch6auCTJ7lgVZ1q63wqaw", +// "Key": "degree", +// "Value": { +// "_sd": [ +// "gfseqHmNitIu0-0h14ynqMzuvq1EirTAzUhDnElSVX0", +// "43hfnM7SzZsalAdbXQyq7w4Uvd53YOxTNFpFJr4ZG0A" +// ] +// }, +// "DebugStr": "[\"vch6auCTJ7lgVZ1q63wqaw\",\"degree\",{\"_sd\":[\"gfseqHmNitIu0-0h14ynqMzuvq1EirTAzUhDnElSVX0\",\"43hfnM7SzZsalAdbXQyq7w4Uvd53YOxTNFpFJr4ZG0A\"]}]", +// "DebugDigest": "mdephPRizMUa-LLs3JVeuTRS0tPaTd0faHg5kgKHNGk" +// }, +// { +// "Result": "WyJaVHFiUzI0ZWlybmpQMFlObmFmakxRIiwiaWQiLCJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiXQ", +// "Salt": "ZTqbS24eirnjP0YNnafjLQ", +// "Key": "id", +// "Value": "did:example:ebfeb1f712ebc6f1c276e12ec21", +// "DebugStr": "[\"ZTqbS24eirnjP0YNnafjLQ\",\"id\",\"did:example:ebfeb1f712ebc6f1c276e12ec21\"]", +// "DebugDigest": "fgoQstuIzTLQ4zqosjUC_qCk-xx3wjDQU2QkQtbn7FI" +// }, +// { +// "Result": "WyIyOEEzMmR0OW9JR0lLZW9iVEdIM2F3IiwiZGVncmVlIiwiTUlUIl0", +// "Salt": "28A32dt9oIGIKeobTGH3aw", +// "Key": "degree", +// "Value": "MIT", +// "DebugStr": "[\"28A32dt9oIGIKeobTGH3aw\",\"degree\",\"MIT\"]", +// "DebugDigest": "43hfnM7SzZsalAdbXQyq7w4Uvd53YOxTNFpFJr4ZG0A" +// }, +// { +// "Result": "WyJUNE8wRlZ2MDBpREhGNFZpYy0wR1VnIiwidHlwZSIsIkJhY2hlbG9yRGVncmVlIl0", +// "Salt": "T4O0FVv00iDHF4Vic-0GUg", +// "Key": "type", +// "Value": "BachelorDegree", +// "DebugStr": "[\"T4O0FVv00iDHF4Vic-0GUg\",\"type\",\"BachelorDegree\"]", +// "DebugDigest": "gfseqHmNitIu0-0h14ynqMzuvq1EirTAzUhDnElSVX0" +// } +// +// ]. +func WithRecursiveClaimsObjects(recursiveClaimsObject []string) NewOpt { + return issuer.WithRecursiveClaimsObjects(recursiveClaimsObject) +} + // New creates new signed Selective Disclosure JWT based on input claims. // The Issuer MUST create a Disclosure for each selectively disclosable claim as follows: // Create an array of three elements in this order: diff --git a/pkg/doc/sdjwt/verifier/verifier.go b/pkg/doc/sdjwt/verifier/verifier.go index 6d7c5b76d..9a1f0b180 100644 --- a/pkg/doc/sdjwt/verifier/verifier.go +++ b/pkg/doc/sdjwt/verifier/verifier.go @@ -11,94 +11,77 @@ extracts the claims from an SD-JWT and respective Disclosures. package verifier import ( - "encoding/json" - "fmt" "time" - "github.com/go-jose/go-jose/v3/jwt" - "github.com/mitchellh/mapstructure" - "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" - "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk" - afgjwt "github.com/hyperledger/aries-framework-go/component/models/jwt" - "github.com/hyperledger/aries-framework-go/component/models/signature/verifier" - utils "github.com/hyperledger/aries-framework-go/component/models/util/maphelpers" - - "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" + "github.com/hyperledger/aries-framework-go/component/models/sdjwt/verifier" ) -// jwtParseOpts holds options for the SD-JWT parsing. -type parseOpts struct { - detachedPayload []byte - sigVerifier jose.SignatureVerifier - - issuerSigningAlgorithms []string - holderSigningAlgorithms []string - - holderBindingRequired bool - expectedAudienceForHolderBinding string - expectedNonceForHolderBinding string - - leewayForClaimsValidation time.Duration -} - -// ParseOpt is the SD-JWT Parser option. -type ParseOpt func(opts *parseOpts) - // WithJWTDetachedPayload option is for definition of JWT detached payload. -func WithJWTDetachedPayload(payload []byte) ParseOpt { - return func(opts *parseOpts) { - opts.detachedPayload = payload - } +func WithJWTDetachedPayload(payload []byte) verifier.ParseOpt { + return verifier.WithJWTDetachedPayload(payload) } // WithSignatureVerifier option is for definition of signature verifier. -func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt { - return func(opts *parseOpts) { - opts.sigVerifier = signatureVerifier - } +func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) verifier.ParseOpt { + return verifier.WithSignatureVerifier(signatureVerifier) } // WithIssuerSigningAlgorithms option is for defining secure signing algorithms (for issuer). -func WithIssuerSigningAlgorithms(algorithms []string) ParseOpt { - return func(opts *parseOpts) { - opts.issuerSigningAlgorithms = algorithms - } +func WithIssuerSigningAlgorithms(algorithms []string) verifier.ParseOpt { + return verifier.WithIssuerSigningAlgorithms(algorithms) } // WithHolderSigningAlgorithms option is for defining secure signing algorithms (for holder). -func WithHolderSigningAlgorithms(algorithms []string) ParseOpt { - return func(opts *parseOpts) { - opts.holderSigningAlgorithms = algorithms - } +func WithHolderSigningAlgorithms(algorithms []string) verifier.ParseOpt { + return verifier.WithHolderSigningAlgorithms(algorithms) } // WithHolderBindingRequired option is for enforcing holder binding. -func WithHolderBindingRequired(flag bool) ParseOpt { - return func(opts *parseOpts) { - opts.holderBindingRequired = flag - } +// Deprecated: use WithHolderVerificationRequired instead. +func WithHolderBindingRequired(flag bool) verifier.ParseOpt { + return WithHolderVerificationRequired(flag) } // WithExpectedAudienceForHolderBinding option is to pass expected audience for holder binding. -func WithExpectedAudienceForHolderBinding(audience string) ParseOpt { - return func(opts *parseOpts) { - opts.expectedAudienceForHolderBinding = audience - } +// Deprecated: use WithExpectedAudienceForHolderVerification instead. +func WithExpectedAudienceForHolderBinding(audience string) verifier.ParseOpt { + return WithExpectedAudienceForHolderVerification(audience) } // WithExpectedNonceForHolderBinding option is to pass nonce value for holder binding. -func WithExpectedNonceForHolderBinding(nonce string) ParseOpt { - return func(opts *parseOpts) { - opts.expectedNonceForHolderBinding = nonce - } +// Deprecated: use WithExpectedNonceForHolderVerification instead. +func WithExpectedNonceForHolderBinding(nonce string) verifier.ParseOpt { + return WithExpectedNonceForHolderVerification(nonce) +} + +// WithHolderVerificationRequired option is for enforcing holder verification. +// For SDJWT V2 - this option defines Holder Binding verification as required. +// For SDJWT V5 - this option defines Key Binding verification as required. +func WithHolderVerificationRequired(flag bool) verifier.ParseOpt { + return verifier.WithHolderVerificationRequired(flag) +} + +// WithExpectedAudienceForHolderVerification option is to pass expected audience for holder verification. +func WithExpectedAudienceForHolderVerification(audience string) verifier.ParseOpt { + return verifier.WithExpectedAudienceForHolderVerification(audience) +} + +// WithExpectedNonceForHolderVerification option is to pass nonce value for holder verification. +func WithExpectedNonceForHolderVerification(nonce string) verifier.ParseOpt { + return verifier.WithExpectedNonceForHolderVerification(nonce) } // WithLeewayForClaimsValidation is an option for claims time(s) validation. -func WithLeewayForClaimsValidation(duration time.Duration) ParseOpt { - return func(opts *parseOpts) { - opts.leewayForClaimsValidation = duration - } +func WithLeewayForClaimsValidation(duration time.Duration) verifier.ParseOpt { + return verifier.WithLeewayForClaimsValidation(duration) +} + +// WithExpectedTypHeader is an option for JWT typ header validation. +// Might be relevant for SDJWT V5 VC validation. +// Spec: https://vcstuff.github.io/draft-terbu-sd-jwt-vc/draft-terbu-oauth-sd-jwt-vc.html#name-header-parameters +func WithExpectedTypHeader(typ string) verifier.ParseOpt { + return verifier.WithExpectedTypHeader(typ) } // Parse parses combined format for presentation and returns verified claims. @@ -113,281 +96,11 @@ func WithLeewayForClaimsValidation(duration time.Duration) ParseOpt { // is contained in the SD-JWT. // // Detailed algorithm: -// https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-02.html#name-verification-by-the-verifier +// nolint:lll +// V2 https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-02.html#name-verification-by-the-verifier +// V5 https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#name-verification-by-the-verifier // // The Verifier will not, however, learn any claim values not disclosed in the Disclosures. -func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]interface{}, error) { - defaultSigningAlgorithms := []string{"EdDSA", "RS256"} - pOpts := &parseOpts{ - issuerSigningAlgorithms: defaultSigningAlgorithms, - holderSigningAlgorithms: defaultSigningAlgorithms, - leewayForClaimsValidation: jwt.DefaultLeeway, - } - - for _, opt := range opts { - opt(pOpts) - } - - var jwtOpts []afgjwt.ParseOpt - jwtOpts = append(jwtOpts, - afgjwt.WithSignatureVerifier(pOpts.sigVerifier), - afgjwt.WithJWTDetachedPayload(pOpts.detachedPayload)) - - // Separate the Presentation into the SD-JWT, the Disclosures (if any), and the Holder Binding JWT (if provided) - cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) - - // Validate the signature over the SD-JWT - signedJWT, _, err := afgjwt.Parse(cfp.SDJWT, jwtOpts...) - if err != nil { - return nil, err - } - - // Ensure that a signing algorithm was used that was deemed secure for the application. - // The none algorithm MUST NOT be accepted. - err = verifySigningAlg(signedJWT.Headers, pOpts.issuerSigningAlgorithms) - if err != nil { - return nil, fmt.Errorf("failed to verify issuer signing algorithm: %w", err) - } - - // TODO: Validate the Issuer of the SD-JWT and that the signing key belongs to this Issuer. - - // Check that the SD-JWT is valid using nbf, iat, and exp claims, - // if provided in the SD-JWT, and not selectively disclosed. - err = verifyJWT(signedJWT, pOpts.leewayForClaimsValidation) - if err != nil { - return nil, err - } - - // Check that there are no duplicate disclosures - err = checkForDuplicates(cfp.Disclosures) - if err != nil { - return nil, fmt.Errorf("check disclosures: %w", err) - } - - // Verify that all disclosures are present in SD-JWT. - err = common.VerifyDisclosuresInSDJWT(cfp.Disclosures, signedJWT) - if err != nil { - return nil, err - } - - err = verifyHolderBinding(signedJWT, cfp.HolderBinding, pOpts) - if err != nil { - return nil, fmt.Errorf("failed to verify holder binding: %w", err) - } - - return getDisclosedClaims(cfp.Disclosures, signedJWT) -} - -func verifyHolderBinding(sdJWT *afgjwt.JSONWebToken, holderBinding string, pOpts *parseOpts) error { - if pOpts.holderBindingRequired && holderBinding == "" { - return fmt.Errorf("holder binding is required") - } - - if holderBinding == "" { - // not required and not present - nothing to do - return nil - } - - signatureVerifier, err := getSignatureVerifier(utils.CopyMap(sdJWT.Payload)) - if err != nil { - return fmt.Errorf("failed to get signature verifier from presentation claims: %w", err) - } - - holderJWT, _, err := afgjwt.Parse(holderBinding, - afgjwt.WithSignatureVerifier(signatureVerifier)) - if err != nil { - return fmt.Errorf("failed to parse holder binding: %w", err) - } - - err = verifyHolderJWT(holderJWT, pOpts) - if err != nil { - return fmt.Errorf("failed to verify holder JWT: %w", err) - } - - return nil -} - -func verifyHolderJWT(holderJWT *afgjwt.JSONWebToken, pOpts *parseOpts) error { - // Ensure that a signing algorithm was used that was deemed secure for the application. - // The none algorithm MUST NOT be accepted. - err := verifySigningAlg(holderJWT.Headers, pOpts.holderSigningAlgorithms) - if err != nil { - return fmt.Errorf("failed to verify holder signing algorithm: %w", err) - } - - err = verifyJWT(holderJWT, pOpts.leewayForClaimsValidation) - if err != nil { - return err - } - - var bindingPayload holderBindingPayload - - d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - Result: &bindingPayload, - TagName: "json", - Squash: true, - WeaklyTypedInput: true, - DecodeHook: utils.JSONNumberToJwtNumericDate(), - }) - if err != nil { - return fmt.Errorf("mapstruct verifyHodlder. error: %w", err) - } - - if err = d.Decode(holderJWT.Payload); err != nil { - return fmt.Errorf("mapstruct verifyHodlder decode. error: %w", err) - } - - if pOpts.expectedNonceForHolderBinding != "" && pOpts.expectedNonceForHolderBinding != bindingPayload.Nonce { - return fmt.Errorf("nonce value '%s' does not match expected nonce value '%s'", - bindingPayload.Nonce, pOpts.expectedNonceForHolderBinding) - } - - if pOpts.expectedAudienceForHolderBinding != "" && pOpts.expectedAudienceForHolderBinding != bindingPayload.Audience { - return fmt.Errorf("audience value '%s' does not match expected audience value '%s'", - bindingPayload.Audience, pOpts.expectedAudienceForHolderBinding) - } - - return nil -} - -func getSignatureVerifier(claims map[string]interface{}) (jose.SignatureVerifier, error) { - cnf, err := common.GetCNF(claims) - if err != nil { - return nil, err - } - - signatureVerifier, err := getSignatureVerifierFromCNF(cnf) - if err != nil { - return nil, err - } - - return signatureVerifier, nil -} - -// getSignatureVerifierFromCNF will evolve over time as we support more cnf modes and algorithms. -func getSignatureVerifierFromCNF(cnf map[string]interface{}) (jose.SignatureVerifier, error) { - jwkObj, ok := cnf["jwk"] - if !ok { - return nil, fmt.Errorf("jwk must be present in cnf") - } - - // TODO: Add handling other methods: "jwe", "jku" and "kid" - - jwkObjBytes, err := json.Marshal(jwkObj) - if err != nil { - return nil, fmt.Errorf("marshal jwk: %w", err) - } - - j := jwk.JWK{} - - err = j.UnmarshalJSON(jwkObjBytes) - if err != nil { - return nil, fmt.Errorf("unmarshal jwk: %w", err) - } - - signatureVerifier, err := afgjwt.GetVerifier(&verifier.PublicKey{JWK: &j}) - if err != nil { - return nil, fmt.Errorf("get verifier from jwk: %w", err) - } - - return signatureVerifier, nil -} - -func getDisclosedClaims(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 payload: %w", err) - } - - disclosedClaims, err := common.GetDisclosedClaims(disclosureClaims, utils.CopyMap(signedJWT.Payload)) - if err != nil { - return nil, fmt.Errorf("failed to get disclosed claims: %w", err) - } - - return disclosedClaims, nil -} - -func verifySigningAlg(joseHeaders jose.Headers, secureAlgs []string) error { - alg, ok := joseHeaders.Algorithm() - if !ok { - return fmt.Errorf("missing alg") - } - - if alg == afgjwt.AlgorithmNone { - return fmt.Errorf("alg value cannot be 'none'") - } - - if !contains(secureAlgs, alg) { - return fmt.Errorf("alg '%s' is not in the allowed list", alg) - } - - return nil -} - -func contains(values []string, val string) bool { - for _, v := range values { - if v == val { - return true - } - } - - return false -} - -func checkForDuplicates(values []string) error { - var duplicates []string - - valuesMap := make(map[string]bool) - - for _, val := range values { - if _, ok := valuesMap[val]; !ok { - valuesMap[val] = true - } else { - duplicates = append(duplicates, val) - } - } - - if len(duplicates) > 0 { - return fmt.Errorf("duplicate values found %v", duplicates) - } - - return nil -} - -// verifyJWT checks that the JWT is valid using nbf, iat, and exp claims (if provided in the JWT). -func verifyJWT(signedJWT *afgjwt.JSONWebToken, leeway time.Duration) error { - var claims jwt.Claims - - d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - Result: &claims, - TagName: "json", - Squash: true, - WeaklyTypedInput: true, - DecodeHook: utils.JSONNumberToJwtNumericDate(), - }) - if err != nil { - return fmt.Errorf("mapstruct verifyJWT. error: %w", err) - } - - if err = d.Decode(signedJWT.Payload); err != nil { - return fmt.Errorf("mapstruct verifyJWT decode. error: %w", err) - } - - // Validate checks claims in a token against expected values. - // It is validated using the expected.Time, or time.Now if not provided - expected := jwt.Expected{} - - err = claims.ValidateWithLeeway(expected, leeway) - if err != nil { - return fmt.Errorf("invalid JWT time values: %w", err) - } - - return nil -} - -// holderBindingPayload represents expected holder binding payload. -type holderBindingPayload struct { - Nonce string `json:"nonce,omitempty"` - Audience string `json:"aud,omitempty"` - IssuedAt *jwt.NumericDate `json:"iat,omitempty"` +func Parse(combinedFormatForPresentation string, opts ...verifier.ParseOpt) (map[string]interface{}, error) { + return verifier.Parse(combinedFormatForPresentation, opts...) } diff --git a/pkg/doc/verifiable/verifiable.go b/pkg/doc/verifiable/verifiable.go index 9f2f6c394..10b8d486e 100644 --- a/pkg/doc/verifiable/verifiable.go +++ b/pkg/doc/verifiable/verifiable.go @@ -20,12 +20,14 @@ import ( jsonld "github.com/piprate/json-gold/ld" "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose" + "github.com/hyperledger/aries-framework-go/spi/kms" + "github.com/hyperledger/aries-framework-go/spi/vdr" + "github.com/hyperledger/aries-framework-go/component/models/did" + "github.com/hyperledger/aries-framework-go/component/models/sdjwt/common" "github.com/hyperledger/aries-framework-go/component/models/sdjwt/holder" "github.com/hyperledger/aries-framework-go/component/models/signature/verifier" "github.com/hyperledger/aries-framework-go/component/models/verifiable" - "github.com/hyperledger/aries-framework-go/spi/kms" - "github.com/hyperledger/aries-framework-go/spi/vdr" ) // DefaultSchemaTemplate describes default schema. @@ -329,6 +331,11 @@ func DisclosureSigner(signer jose.Signer, signingKeyID string) MarshalDisclosure return verifiable.DisclosureSigner(signer, signingKeyID) } +// MarshalWithSDJWTVersion sets version for SD-JWT VC. +func MarshalWithSDJWTVersion(version common.SDJWTVersion) MarshalDisclosureOption { + return verifiable.MarshalWithSDJWTVersion(version) +} + // MakeSDJWTOption provides an option for creating an SD-JWT from a VC. type MakeSDJWTOption = verifiable.MakeSDJWTOption @@ -337,6 +344,28 @@ func MakeSDJWTWithHash(hash crypto.Hash) MakeSDJWTOption { return verifiable.MakeSDJWTWithHash(hash) } +// MakeSDJWTWithVersion sets version for SD-JWT VC. +func MakeSDJWTWithVersion(version common.SDJWTVersion) MakeSDJWTOption { + return verifiable.MakeSDJWTWithVersion(version) +} + +// MakeSDJWTWithRecursiveClaimsObjects sets version for SD-JWT VC. SD-JWT v5+ support. +func MakeSDJWTWithRecursiveClaimsObjects(recursiveClaimsObject []string) MakeSDJWTOption { + return verifiable.MakeSDJWTWithRecursiveClaimsObjects(recursiveClaimsObject) +} + +// MakeSDJWTWithAlwaysIncludeObjects is an option for provide object keys that should be a part of +// selectively disclosable claims. +func MakeSDJWTWithAlwaysIncludeObjects(alwaysIncludeObjects []string) MakeSDJWTOption { + return verifiable.MakeSDJWTWithAlwaysIncludeObjects(alwaysIncludeObjects) +} + +// MakeSDJWTWithNonSelectivelyDisclosableClaims is an option for provide claim names +// that should be ignored when creating selectively disclosable claims. +func MakeSDJWTWithNonSelectivelyDisclosableClaims(nonSDClaims []string) MakeSDJWTOption { + return verifiable.MakeSDJWTWithNonSelectivelyDisclosableClaims(nonSDClaims) +} + // DisplayCredentialOption provides an option for Credential.CreateDisplayCredential. type DisplayCredentialOption = verifiable.DisplayCredentialOption diff --git a/pkg/doc/verifiable/withvdr_test.go b/pkg/doc/verifiable/withvdr_test.go index 81f8dde23..c5ea47315 100644 --- a/pkg/doc/verifiable/withvdr_test.go +++ b/pkg/doc/verifiable/withvdr_test.go @@ -10,9 +10,11 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/hyperledger/aries-framework-go/component/models/did" + "github.com/hyperledger/aries-framework-go/component/models/verifiable" mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" "github.com/hyperledger/aries-framework-go/pkg/vdr" @@ -134,3 +136,21 @@ func createDIDDoc() *did.Doc { return didDoc } + +func TestOptions(t *testing.T) { + opts := []MakeSDJWTOption{ + MakeSDJWTWithRecursiveClaimsObjects([]string{"aa", "bb"}), + MakeSDJWTWithAlwaysIncludeObjects([]string{"cc", "dd"}), + MakeSDJWTWithNonSelectivelyDisclosableClaims([]string{"xx", "yy"}), + MakeSDJWTWithVersion(100500), + } + + opt := &verifiable.MakeSDJWTOpts{} + for _, o := range opts { + o(opt) + } + + assert.Equal(t, []string{"aa", "bb"}, opt.GetRecursiveClaimsObject()) + assert.Equal(t, []string{"cc", "dd"}, opt.GetAlwaysIncludeObject()) + assert.Equal(t, []string{"xx", "yy"}, opt.GetNonSDClaims()) +} diff --git a/test/bdd/go.mod b/test/bdd/go.mod index a520c2a59..427b3b039 100644 --- a/test/bdd/go.mod +++ b/test/bdd/go.mod @@ -97,6 +97,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opencensus.io v0.23.0 // indirect golang.org/x/crypto v0.1.0 // indirect + golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect golang.org/x/sys v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/test/bdd/go.sum b/test/bdd/go.sum index 6f0bf1960..be1fc0726 100644 --- a/test/bdd/go.sum +++ b/test/bdd/go.sum @@ -172,7 +172,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= @@ -407,6 +407,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= +golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -475,7 +477,6 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=