diff --git a/pkg/doc/sdjwt/common/common.go b/pkg/doc/sdjwt/common/common.go index 8a5f81fa9..e35077ed3 100644 --- a/pkg/doc/sdjwt/common/common.go +++ b/pkg/doc/sdjwt/common/common.go @@ -45,10 +45,43 @@ type Payload struct { SDAlg string `json:"_sd_alg,omitempty"` } -// SDJWT holds SD-JWT info. -type SDJWT struct { - JWTSerialized string +// CombinedFormatForIssuance holds SD-JWT and disclosures. +type CombinedFormatForIssuance struct { + SDJWT string + Disclosures []string +} + +// Serialize will assemble combined format for issuance. +func (cf *CombinedFormatForIssuance) Serialize() string { + presentation := cf.SDJWT + for _, disclosure := range cf.Disclosures { + presentation += DisclosureSeparator + disclosure + } + + return presentation +} + +// CombinedFormatForPresentation holds SD-JWT, disclosures and optional holder binding info. +type CombinedFormatForPresentation struct { + SDJWT string Disclosures []string + HolderBinding string +} + +// Serialize will assemble combined format for presentation. +func (cf *CombinedFormatForPresentation) Serialize() string { + presentation := cf.SDJWT + for _, disclosure := range cf.Disclosures { + presentation += DisclosureSeparator + disclosure + } + + if len(cf.Disclosures) > 0 || cf.HolderBinding != "" { + presentation += DisclosureSeparator + } + + presentation += cf.HolderBinding + + return presentation } // DisclosureClaim defines claim. @@ -107,18 +140,37 @@ func getDisclosureClaim(disclosure string) (*DisclosureClaim, error) { return claim, nil } -// ParseSDJWT parses SD-JWT serialized token into SDJWT parts. -func ParseSDJWT(sdJWTSerialized string) *SDJWT { - parts := strings.Split(sdJWTSerialized, DisclosureSeparator) +// ParseCombinedFormatForIssuance parses combined format for issuance into CombinedFormatForIssuance parts. +func ParseCombinedFormatForIssuance(combinedFormatForIssuance string) *CombinedFormatForIssuance { + parts := strings.Split(combinedFormatForIssuance, DisclosureSeparator) var disclosures []string if len(parts) > 1 { disclosures = parts[1:] } - jwtSerialized := parts[0] + sdJWT := parts[0] + + return &CombinedFormatForIssuance{SDJWT: sdJWT, Disclosures: disclosures} +} + +// ParseCombinedFormatForPresentation parses combined format for presentation into CombinedFormatForPresentation parts. +func ParseCombinedFormatForPresentation(combinedFormatForPresentation string) *CombinedFormatForPresentation { + parts := strings.Split(combinedFormatForPresentation, DisclosureSeparator) + + var disclosures []string + if len(parts) > 2 { + disclosures = parts[1 : len(parts)-1] + } + + var holderBinding string + if len(parts) > 1 { + holderBinding = parts[len(parts)-1] + } + + sdJWT := parts[0] - return &SDJWT{JWTSerialized: jwtSerialized, Disclosures: disclosures} + return &CombinedFormatForPresentation{SDJWT: sdJWT, Disclosures: disclosures, HolderBinding: holderBinding} } // GetHash calculates hash of data using hash function identified by hash. @@ -207,7 +259,7 @@ func getSDAlg(claims map[string]interface{}) (string, error) { return str, nil } -// GetDisclosureDigests returns digests from from claims map. +// GetDisclosureDigests returns digests from claims map. func GetDisclosureDigests(claims map[string]interface{}) (map[string]bool, error) { disclosuresObj, ok := claims[SDKey] if !ok { diff --git a/pkg/doc/sdjwt/common/common_test.go b/pkg/doc/sdjwt/common/common_test.go index f7d22c1de..9249f6b87 100644 --- a/pkg/doc/sdjwt/common/common_test.go +++ b/pkg/doc/sdjwt/common/common_test.go @@ -42,14 +42,79 @@ func TestGetHash(t *testing.T) { }) } -func TestParseSDJWT(t *testing.T) { - t.Run("success", func(t *testing.T) { - sdJWT := ParseSDJWT(sdJWT) - require.Equal(t, 1, len(sdJWT.Disclosures)) +func TestParseCombinedFormatForIssuance(t *testing.T) { + t.Run("success - SD-JWT only", func(t *testing.T) { + cfi := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) + require.Equal(t, testSDJWT, cfi.SDJWT) + require.Equal(t, 1, len(cfi.Disclosures)) + + require.Equal(t, testCombinedFormatForIssuance, cfi.Serialize()) }) - t.Run("success", func(t *testing.T) { - sdJWT := ParseSDJWT(specSDJWT) - require.Equal(t, 7, len(sdJWT.Disclosures)) + t.Run("success - spec example", func(t *testing.T) { + cfi := ParseCombinedFormatForIssuance(specCombinedFormatForIssuance) + require.Equal(t, 7, len(cfi.Disclosures)) + + require.Equal(t, specCombinedFormatForIssuance, cfi.Serialize()) + }) + t.Run("success - AFG generated", func(t *testing.T) { + cfi := ParseCombinedFormatForIssuance(testSDJWT) + require.Equal(t, testSDJWT, cfi.SDJWT) + require.Equal(t, 0, len(cfi.Disclosures)) + + require.Equal(t, testSDJWT, cfi.Serialize()) + }) +} + +func TestParseCombinedFormatForPresentation(t *testing.T) { + const testHolderBinding = "holder.binding.jwt" + + testCombinedFormatForPresentation := testCombinedFormatForIssuance + DisclosureSeparator + + t.Run("success - AFG example", func(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.Equal(t, testCombinedFormatForPresentation, cfp.Serialize()) + }) + + t.Run("success - spec example", func(t *testing.T) { + cfp := ParseCombinedFormatForPresentation(specCombinedFormatForIssuance + DisclosureSeparator) + require.Equal(t, 7, len(cfp.Disclosures)) + require.Empty(t, cfp.HolderBinding) + + require.Equal(t, specCombinedFormatForIssuance+DisclosureSeparator, cfp.Serialize()) + }) + + t.Run("success - AFG test with holder binding", func(t *testing.T) { + testCFI := testCombinedFormatForPresentation + testHolderBinding + 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, testCFI, cfp.Serialize()) + }) + + t.Run("success - SD-JWT only", func(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.Equal(t, testSDJWT, cfp.Serialize()) + }) + + t.Run("success - SD-JWT + holder binding", func(t *testing.T) { + testCFI := testSDJWT + DisclosureSeparator + testHolderBinding + + 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, testCFI, cfp.Serialize()) }) } @@ -62,10 +127,10 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) { signer := afjwt.NewEd25519Signer(privKey) t.Run("success", func(t *testing.T) { - sdJWT := ParseSDJWT(sdJWT) + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) require.Equal(t, 1, len(sdJWT.Disclosures)) - signedJWT, err := afjwt.Parse(sdJWT.JWTSerialized, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + signedJWT, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) require.NoError(t, err) err = VerifyDisclosuresInSDJWT(sdJWT.Disclosures, signedJWT) @@ -98,10 +163,10 @@ func TestVerifyDisclosuresInSDJWT(t *testing.T) { }) t.Run("error - disclosure not present in SD-JWT", func(t *testing.T) { - sdJWT := ParseSDJWT(sdJWT) + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) require.Equal(t, 1, len(sdJWT.Disclosures)) - signedJWT, err := afjwt.Parse(sdJWT.JWTSerialized, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) + signedJWT, err := afjwt.Parse(sdJWT.SDJWT, afjwt.WithSignatureVerifier(&NoopSignatureVerifier{})) require.NoError(t, err) err = VerifyDisclosuresInSDJWT(append(sdJWT.Disclosures, additionalDisclosure), signedJWT) @@ -195,7 +260,7 @@ func TestGetDisclosureClaims(t *testing.T) { r := require.New(t) t.Run("success", func(t *testing.T) { - sdJWT := ParseSDJWT(sdJWT) + sdJWT := ParseCombinedFormatForIssuance(testCombinedFormatForIssuance) require.Equal(t, 1, len(sdJWT.Disclosures)) disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) @@ -207,7 +272,7 @@ func TestGetDisclosureClaims(t *testing.T) { }) t.Run("error - invalid disclosure format (not encoded)", func(t *testing.T) { - sdJWT := ParseSDJWT("jws~xyz") + sdJWT := ParseCombinedFormatForIssuance("jws~xyz") require.Equal(t, 1, len(sdJWT.Disclosures)) disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) @@ -221,7 +286,7 @@ func TestGetDisclosureClaims(t *testing.T) { disclosureJSON, err := json.Marshal(disclosureArr) require.NoError(t, err) - sdJWT := ParseSDJWT(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON))) + sdJWT := ParseCombinedFormatForIssuance(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON))) require.Equal(t, 1, len(sdJWT.Disclosures)) disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) @@ -235,7 +300,7 @@ func TestGetDisclosureClaims(t *testing.T) { disclosureJSON, err := json.Marshal(disclosureArr) require.NoError(t, err) - sdJWT := ParseSDJWT(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON))) + sdJWT := ParseCombinedFormatForIssuance(fmt.Sprintf("jws~%s", base64.RawURLEncoding.EncodeToString(disclosureJSON))) require.Equal(t, 1, len(sdJWT.Disclosures)) disclosureClaims, err := GetDisclosureClaims(sdJWT.Disclosures) @@ -255,7 +320,10 @@ func (sv *NoopSignatureVerifier) Verify(joseHeaders jose.Headers, payload, signi const additionalDisclosure = `WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0` // nolint: lll -const sdJWT = `eyJhbGciOiJFZERTQSJ9.eyJfc2QiOlsicXF2Y3FuY3pBTWdZeDdFeWtJNnd3dHNweXZ5dks3OTBnZTdNQmJRLU51cyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTcwMzAyMzg1NSwiaWF0IjoxNjcxNDg3ODU1LCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsIm5iZiI6MTY3MTQ4Nzg1NX0.vscuzfwcHGi04pWtJCadc4iDELug6NH6YK-qxhY1qacsciIHuoLELAfon1tGamHtuu8TSs6OjtLk3lHE16jqAQ~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd` +const testCombinedFormatForIssuance = `eyJhbGciOiJFZERTQSJ9.eyJfc2QiOlsicXF2Y3FuY3pBTWdZeDdFeWtJNnd3dHNweXZ5dks3OTBnZTdNQmJRLU51cyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTcwMzAyMzg1NSwiaWF0IjoxNjcxNDg3ODU1LCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsIm5iZiI6MTY3MTQ4Nzg1NX0.vscuzfwcHGi04pWtJCadc4iDELug6NH6YK-qxhY1qacsciIHuoLELAfon1tGamHtuu8TSs6OjtLk3lHE16jqAQ~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd` + +// nolint: lll +const testSDJWT = `eyJhbGciOiJFZERTQSJ9.eyJfc2QiOlsicXF2Y3FuY3pBTWdZeDdFeWtJNnd3dHNweXZ5dks3OTBnZTdNQmJRLU51cyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsImV4cCI6MTcwMzAyMzg1NSwiaWF0IjoxNjcxNDg3ODU1LCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsIm5iZiI6MTY3MTQ4Nzg1NX0.vscuzfwcHGi04pWtJCadc4iDELug6NH6YK-qxhY1qacsciIHuoLELAfon1tGamHtuu8TSs6OjtLk3lHE16jqAQ` // 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 specCombinedFormatForIssuance = `eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImNBRUlVcUowY21MekQxa3pHemhlaUJhZzBZUkF6VmRsZnhOMjgwTmdIYUEifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIlJTQSIsICJuIjogInBtNGJPSEJnLW9ZaEF5UFd6UjU2QVdYM3JVSVhwMTFfSUNEa0dnUzZXM1pXTHRzLWh6d0kzeDY1NjU5a2c0aFZvOWRiR29DSkUzWkdGX2VhZXRFMzBVaEJVRWdwR3dyRHJRaUo5enFwcm1jRmZyM3F2dmtHanR0aDhaZ2wxZU0yYkpjT3dFN1BDQkhXVEtXWXMxNTJSN2c2SmcyT1ZwaC1hOHJxLXE3OU1oS0c1UW9XX21UejEwUVRfNkg0YzdQaldHMWZqaDhocFdObmJQX3B2NmQxelN3WmZjNWZsNnlWUkwwRFYwVjNsR0hLZTJXcWZfZU5HakJyQkxWa2xEVGs4LXN0WF9NV0xjUi1FR21YQU92MFVCV2l0U19kWEpLSnUtdlhKeXcxNG5IU0d1eFRJSzJoeDFwdHRNZnQ5Q3N2cWltWEtlRFRVMTRxUUwxZUU3aWhjdyIsICJlIjogIkFRQUIifX19.xqgKrDO6dK_oBL3fiqdcq_elaIGxM6Z-RyuysglGyddR1O1IiE3mIk8kCpoqcRLR88opkVWN2392K_XYfAuAmeT9kJVisD8ZcgNcv-MQlWW9s8WaViXxBRe7EZWkWRQcQVR6jf95XZ5H2-_KA54POq3L42xjk0y5vDr8yc08Reak6vvJVvjXpp-Wk6uxsdEEAKFspt_EYIvISFJhfTuQqyhCjnaW13X312MSQBPwjbHn74ylUqVLljDvqcemxeqjh42KWJq4C3RqNJ7anA2i3FU1kB4-KNZWsijY7-op49iL7BrnIBxdlAMrbHEkoGTbFWdl7Ki17GHtDxxa1jaxQg~WyJkcVR2WE14UzBHYTNEb2FHbmU5eDBRIiwgInN1YiIsICJqb2huX2RvZV80MiJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJLVXhTNWhFX1hiVmFjckdBYzdFRnd3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyIzcXZWSjFCQURwSERTUzkzOVEtUml3IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJFUktNMENOZUZKa2FENW1UWFZfWDh3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0` diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go index f64415ac8..35a7af88a 100644 --- a/pkg/doc/sdjwt/holder/holder.go +++ b/pkg/doc/sdjwt/holder/holder.go @@ -46,7 +46,7 @@ func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt { } // Parse parses issuer SD-JWT and returns claims that can be selected. -func Parse(sdJWTSerialized string, opts ...ParseOpt) ([]*Claim, error) { +func Parse(combinedFormatForIssuance string, opts ...ParseOpt) ([]*Claim, error) { pOpts := &parseOpts{ sigVerifier: &NoopSignatureVerifier{}, } @@ -60,19 +60,19 @@ func Parse(sdJWTSerialized string, opts ...ParseOpt) ([]*Claim, error) { afgjwt.WithSignatureVerifier(pOpts.sigVerifier), afgjwt.WithJWTDetachedPayload(pOpts.detachedPayload)) - sdJWT := common.ParseSDJWT(sdJWTSerialized) + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) - signedJWT, err := afgjwt.Parse(sdJWT.JWTSerialized, jwtOpts...) + signedJWT, err := afgjwt.Parse(cfi.SDJWT, jwtOpts...) if err != nil { return nil, err } - err = common.VerifyDisclosuresInSDJWT(sdJWT.Disclosures, signedJWT) + err = common.VerifyDisclosuresInSDJWT(cfi.Disclosures, signedJWT) if err != nil { return nil, err } - return getClaims(sdJWT.Disclosures) + return getClaims(cfi.Disclosures) } func getClaims(disclosures []string) ([]*Claim, error) { @@ -94,14 +94,14 @@ func getClaims(disclosures []string) ([]*Claim, error) { } // DiscloseClaims discloses claims with specified claim names. -func DiscloseClaims(sdJWTSerialized string, claimNames []string) (string, error) { - sdJWT := common.ParseSDJWT(sdJWTSerialized) +func DiscloseClaims(combinedFormatForIssuance string, claimNames []string) (string, error) { + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) - if len(sdJWT.Disclosures) == 0 { + if len(cfi.Disclosures) == 0 { return "", fmt.Errorf("no disclosures found in SD-JWT") } - disclosures, err := common.GetDisclosureClaims(sdJWT.Disclosures) + disclosures, err := common.GetDisclosureClaims(cfi.Disclosures) if err != nil { return "", err } @@ -110,16 +110,16 @@ func DiscloseClaims(sdJWTSerialized string, claimNames []string) (string, error) for _, claimName := range claimNames { if index := getDisclosureByClaimName(claimName, disclosures); index != notFound { - selectedDisclosures = append(selectedDisclosures, sdJWT.Disclosures[index]) + selectedDisclosures = append(selectedDisclosures, cfi.Disclosures[index]) } } - combinedFormatForPresentation := sdJWT.JWTSerialized - for _, disclosure := range selectedDisclosures { - combinedFormatForPresentation += common.DisclosureSeparator + disclosure + cf := common.CombinedFormatForPresentation{ + SDJWT: cfi.SDJWT, + Disclosures: selectedDisclosures, } - return combinedFormatForPresentation, nil + return cf.Serialize(), nil } func getDisclosureByClaimName(name string, disclosures []*common.DisclosureClaim) int { diff --git a/pkg/doc/sdjwt/holder/holder_test.go b/pkg/doc/sdjwt/holder/holder_test.go index fe19fa408..a9de16bb9 100644 --- a/pkg/doc/sdjwt/holder/holder_test.go +++ b/pkg/doc/sdjwt/holder/holder_test.go @@ -38,14 +38,14 @@ func TestParse(t *testing.T) { token, e := issuer.New(testIssuer, claims, nil, signer) r.NoError(e) - sdJWTSerialized, e := token.Serialize(false) + combinedFormatForIssuance, e := token.Serialize(false) r.NoError(e) verifier, e := afjwt.NewEd25519Verifier(pubKey) r.NoError(e) t.Run("success", func(t *testing.T) { - claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + claims, err := Parse(combinedFormatForIssuance, WithSignatureVerifier(verifier)) r.NoError(err) r.NotNil(claims) r.Equal(1, len(claims)) @@ -54,7 +54,7 @@ func TestParse(t *testing.T) { }) t.Run("success - default is no signature verifier", func(t *testing.T) { - claims, err := Parse(sdJWTSerialized) + claims, err := Parse(combinedFormatForIssuance) r.NoError(err) r.Equal(1, len(claims)) r.Equal("given_name", claims[0].Name) @@ -69,7 +69,8 @@ func TestParse(t *testing.T) { }) t.Run("error - additional disclosure", func(t *testing.T) { - claims, err := Parse(fmt.Sprintf("%s~%s", sdJWTSerialized, additionalDisclosure), WithSignatureVerifier(verifier)) + claims, err := Parse(fmt.Sprintf("%s~%s", combinedFormatForIssuance, additionalDisclosure), + WithSignatureVerifier(verifier)) r.Error(err) r.Nil(claims) r.Contains(err.Error(), @@ -77,7 +78,7 @@ func TestParse(t *testing.T) { }) t.Run("success - with detached payload", func(t *testing.T) { - jwsParts := strings.Split(sdJWTSerialized, ".") + jwsParts := strings.Split(combinedFormatForIssuance, ".") jwsDetached := fmt.Sprintf("%s..%s", jwsParts[0], jwsParts[2]) jwsPayload, err := base64.RawURLEncoding.DecodeString(jwsParts[1]) @@ -112,29 +113,31 @@ func TestDiscloseClaims(t *testing.T) { token, e := issuer.New(testIssuer, claims, nil, signer) r.NoError(e) - sdJWTSerialized, e := token.Serialize(false) + combinedFormatForIssuance, e := token.Serialize(false) r.NoError(e) t.Run("success", func(t *testing.T) { - sdJWTDisclosed, err := DiscloseClaims(sdJWTSerialized, []string{"given_name"}) + combinedFormatForPresentation, err := DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}) r.NoError(err) - require.NotNil(t, sdJWTDisclosed) - require.Equal(t, sdJWTSerialized, sdJWTDisclosed) + require.NotNil(t, combinedFormatForPresentation) + require.Equal(t, combinedFormatForIssuance+common.DisclosureSeparator, combinedFormatForPresentation) }) t.Run("error - no disclosure(s)", func(t *testing.T) { - sdJWT := common.ParseSDJWT(sdJWTSerialized) + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) - sdJWTDisclosed, err := DiscloseClaims(sdJWT.JWTSerialized, []string{"given_name"}) + combinedFormatForPresentation, err := DiscloseClaims(cfi.SDJWT, []string{"given_name"}) r.Error(err) - r.Empty(sdJWTDisclosed) + r.Empty(combinedFormatForPresentation) r.Contains(err.Error(), "no disclosures found in SD-JWT") }) - t.Run("error - invalid disclosure", func(t *testing.T) { - sdJWTDisclosed, err := DiscloseClaims(fmt.Sprintf("%s~%s", sdJWTSerialized, "abc"), []string{"given_name"}) + t.Run("error - add invalid disclosure", func(t *testing.T) { + cfiWithInvalidDisclosure := fmt.Sprintf("%s~%s", combinedFormatForIssuance, "abc") + + combinedFormatForPresentation, err := DiscloseClaims(cfiWithInvalidDisclosure, []string{"given_name"}) r.Error(err) - r.Empty(sdJWTDisclosed) + r.Empty(combinedFormatForPresentation) r.Contains(err.Error(), "failed to unmarshal disclosure array") }) } diff --git a/pkg/doc/sdjwt/integration_test.go b/pkg/doc/sdjwt/integration_test.go index b4efd740f..8d7a59e10 100644 --- a/pkg/doc/sdjwt/integration_test.go +++ b/pkg/doc/sdjwt/integration_test.go @@ -45,26 +45,28 @@ func TestSDJWTFlow(t *testing.T) { r.NoError(e) // TODO: Should we have one call instead of two (designed based on JWT) - sdJWTSerialized, e := token.Serialize(false) + combinedFormatForIssuance, e := token.Serialize(false) r.NoError(e) - fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", sdJWTSerialized)) + fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance)) - // Holder will parse issuer SD-JWT and hold on to that SD-JWT and the claims that can be selected. - claims, err := holder.Parse(sdJWTSerialized, holder.WithSignatureVerifier(signatureVerifier)) + // Holder will parse combined format for issuance and hold on to that + // combined format for issuance and the claims that can be selected. + claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) r.NoError(err) // expected disclosures given_name and last_name r.Equal(2, len(claims)) // Holder will disclose only sub-set of claims to verifier. - sdJWTDisclosed, err := holder.DiscloseClaims(sdJWTSerialized, []string{"given_name"}) + combinedFormatForPresentation, err := holder.DiscloseClaims(combinedFormatForIssuance, []string{"given_name"}) r.NoError(err) - fmt.Println(fmt.Sprintf("holder SD-JWT: %s", sdJWTDisclosed)) + fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation)) - // Verifier will validate holder SD-JWT and create verified claims. - verifiedClaims, err := verifier.Parse(sdJWTDisclosed, verifier.WithSignatureVerifier(signatureVerifier)) + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, + verifier.WithSignatureVerifier(signatureVerifier)) r.NoError(err) // expected claims iss, exp, iat, nbf, given_name; last_name was not disclosed diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go index 973e94c41..209ad046c 100644 --- a/pkg/doc/sdjwt/issuer/issuer.go +++ b/pkg/doc/sdjwt/issuer/issuer.go @@ -248,12 +248,12 @@ func (j *SelectiveDisclosureJWT) Serialize(detached bool) (string, error) { return "", err } - combinedFormatForPresentation := signedJWT - for _, disclosure := range j.Disclosures { - combinedFormatForPresentation += common.DisclosureSeparator + disclosure + cf := common.CombinedFormatForIssuance{ + SDJWT: signedJWT, + Disclosures: j.Disclosures, } - return combinedFormatForPresentation, nil + return cf.Serialize(), nil } func createDisclosures(claims map[string]interface{}, opts *newOpts) ([]string, error) { diff --git a/pkg/doc/sdjwt/issuer/issuer_test.go b/pkg/doc/sdjwt/issuer/issuer_test.go index 772ff5a85..329c7f737 100644 --- a/pkg/doc/sdjwt/issuer/issuer_test.go +++ b/pkg/doc/sdjwt/issuer/issuer_test.go @@ -48,16 +48,16 @@ func TestNew(t *testing.T) { return sampleSalt, nil })) r.NoError(err) - sdJWTSerialized, err := token.Serialize(false) + combinedFormatForIssuance, err := token.Serialize(false) require.NoError(t, err) - fmt.Printf(sdJWTSerialized) + fmt.Printf(combinedFormatForIssuance) - sdJWT := common.ParseSDJWT(sdJWTSerialized) - require.Equal(t, 1, len(sdJWT.Disclosures)) + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + require.Equal(t, 1, len(cfi.Disclosures)) var parsedClaims map[string]interface{} - err = verifyEd25519ViaGoJose(sdJWT.JWTSerialized, pubKey, &parsedClaims) + err = verifyEd25519ViaGoJose(cfi.SDJWT, pubKey, &parsedClaims) r.NoError(err) parsedClaimsBytes, err := json.Marshal(parsedClaims) @@ -70,7 +70,7 @@ func TestNew(t *testing.T) { require.True(t, existsInDisclosures(parsedClaims, expectedHashWithSpaces)) - err = verifyEd25519(sdJWT.JWTSerialized, pubKey) + err = verifyEd25519(cfi.SDJWT, pubKey) r.NoError(err) }) @@ -88,14 +88,14 @@ func TestNew(t *testing.T) { return sampleSalt, nil })) r.NoError(err) - sdJWTSerialized, err := token.Serialize(false) + combinedFormatForIssuance, err := token.Serialize(false) require.NoError(t, err) - sdJWT := common.ParseSDJWT(sdJWTSerialized) - require.Equal(t, 1, len(sdJWT.Disclosures)) + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + require.Equal(t, 1, len(cfi.Disclosures)) var parsedClaims map[string]interface{} - err = verifyRS256ViaGoJose(sdJWT.JWTSerialized, pubKey, &parsedClaims) + err = verifyRS256ViaGoJose(cfi.SDJWT, pubKey, &parsedClaims) r.NoError(err) parsedClaimsBytes, err := json.Marshal(parsedClaims) @@ -109,7 +109,7 @@ func TestNew(t *testing.T) { expectedHashWithSpaces := expectedHashWithSpaces require.True(t, existsInDisclosures(parsedClaims, expectedHashWithSpaces)) - err = verifyRS256(sdJWT.JWTSerialized, pubKey) + err = verifyRS256(cfi.SDJWT, pubKey) r.NoError(err) }) @@ -141,16 +141,16 @@ func TestNew(t *testing.T) { token, err := New(issuer, complexClaims, nil, afjwt.NewEd25519Signer(privKey), newOpts...) r.NoError(err) - sdJWTSerialized, err := token.Serialize(false) + combinedFormatForIssuance, err := token.Serialize(false) require.NoError(t, err) - fmt.Printf(sdJWTSerialized) + fmt.Printf(combinedFormatForIssuance) - sdJWT := common.ParseSDJWT(sdJWTSerialized) - require.Equal(t, 7, len(sdJWT.Disclosures)) + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + require.Equal(t, 7, len(cfi.Disclosures)) var parsedClaims map[string]interface{} - err = verifyEd25519ViaGoJose(sdJWT.JWTSerialized, pubKey, &parsedClaims) + err = verifyEd25519ViaGoJose(cfi.SDJWT, pubKey, &parsedClaims) r.NoError(err) parsedClaimsBytes, err := json.Marshal(parsedClaims) @@ -161,7 +161,7 @@ func TestNew(t *testing.T) { fmt.Println(prettyJSON) - err = verifyEd25519(sdJWT.JWTSerialized, pubKey) + err = verifyEd25519(cfi.SDJWT, pubKey) r.NoError(err) }) @@ -177,13 +177,13 @@ func TestNew(t *testing.T) { token, err := New(issuer, claims, nil, afjwt.NewEd25519Signer(privKey), WithDecoyDigests(true)) r.NoError(err) - sdJWTSerialized, err := token.Serialize(false) + combinedFormatForIssuance, err := token.Serialize(false) r.NoError(err) - sdJWT := common.ParseSDJWT(sdJWTSerialized) - r.Equal(1, len(sdJWT.Disclosures)) + cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) + r.Equal(1, len(cfi.Disclosures)) - afjwtToken, err := afjwt.Parse(sdJWT.JWTSerialized, afjwt.WithSignatureVerifier(verifier)) + afjwtToken, err := afjwt.Parse(cfi.SDJWT, afjwt.WithSignatureVerifier(verifier)) r.NoError(err) var parsedClaims map[string]interface{} diff --git a/pkg/doc/sdjwt/verifier/verifier.go b/pkg/doc/sdjwt/verifier/verifier.go index 150874aca..6e39d8392 100644 --- a/pkg/doc/sdjwt/verifier/verifier.go +++ b/pkg/doc/sdjwt/verifier/verifier.go @@ -47,8 +47,8 @@ func WithSigningAlgorithms(algorithms []string) ParseOpt { } } -// Parse parses input JWT in serialized form into JSON Web Token. -func Parse(sdJWTSerialized string, opts ...ParseOpt) (map[string]interface{}, error) { +// Parse parses combined format for presentation and returns verified claims. +func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]interface{}, error) { pOpts := &parseOpts{ signingAlgorithms: []string{"EdDSA", "RS256"}, } @@ -63,10 +63,10 @@ func Parse(sdJWTSerialized string, opts ...ParseOpt) (map[string]interface{}, er afgjwt.WithJWTDetachedPayload(pOpts.detachedPayload)) // Separate the Presentation into the SD-JWT, the Disclosures (if any), and the Holder Binding JWT (if provided) - sdJWT := common.ParseSDJWT(sdJWTSerialized) + cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) // Validate the signature over the SD-JWT - signedJWT, err := afgjwt.Parse(sdJWT.JWTSerialized, jwtOpts...) + signedJWT, err := afgjwt.Parse(cfp.SDJWT, jwtOpts...) if err != nil { return nil, err } @@ -88,18 +88,18 @@ func Parse(sdJWTSerialized string, opts ...ParseOpt) (map[string]interface{}, er } // Check that there are no duplicate disclosures - err = checkForDuplicates(sdJWT.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(sdJWT.Disclosures, signedJWT) + err = common.VerifyDisclosuresInSDJWT(cfp.Disclosures, signedJWT) if err != nil { return nil, err } - return getVerifiedPayload(sdJWT.Disclosures, signedJWT) + return getVerifiedPayload(cfp.Disclosures, signedJWT) } func getVerifiedPayload(disclosures []string, signedJWT *afgjwt.JSONWebToken) (map[string]interface{}, error) { diff --git a/pkg/doc/sdjwt/verifier/verifier_test.go b/pkg/doc/sdjwt/verifier/verifier_test.go index 11882c093..6b30a3a84 100644 --- a/pkg/doc/sdjwt/verifier/verifier_test.go +++ b/pkg/doc/sdjwt/verifier/verifier_test.go @@ -13,6 +13,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" "strings" "testing" "time" @@ -40,14 +41,16 @@ func TestParse(t *testing.T) { token, e := issuer.New(testIssuer, selectiveClaims, nil, signer) r.NoError(e) - sdJWTSerialized, e := token.Serialize(false) + combinedFormatForIssuance, e := token.Serialize(false) r.NoError(e) + combinedFormatForPresentation := combinedFormatForIssuance + common.DisclosureSeparator + verifier, e := afjwt.NewEd25519Verifier(pubKey) r.NoError(e) t.Run("success - EdDSA signing algorithm", func(t *testing.T) { - claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + claims, err := Parse(combinedFormatForPresentation, WithSignatureVerifier(verifier)) r.NoError(err) require.NotNil(t, claims) // expected claims iss, exp, iat, nbf, given_name @@ -63,12 +66,14 @@ func TestParse(t *testing.T) { v := afjwt.NewRS256Verifier(pubKey) - token, err := issuer.New(testIssuer, selectiveClaims, nil, afjwt.NewRS256Signer(privKey, nil)) + rsaToken, err := issuer.New(testIssuer, selectiveClaims, nil, afjwt.NewRS256Signer(privKey, nil)) r.NoError(err) - tokenSerialized, err := token.Serialize(false) + rsaCombinedFormatForIssuance, err := rsaToken.Serialize(false) require.NoError(t, err) - claims, err := Parse(tokenSerialized, WithSignatureVerifier(v)) + cfp := fmt.Sprintf("%s%s", rsaCombinedFormatForIssuance, common.DisclosureSeparator) + + claims, err := Parse(cfp, WithSignatureVerifier(v)) r.NoError(err) require.Equal(t, 5, len(claims)) }) @@ -83,16 +88,18 @@ func TestParse(t *testing.T) { issuer.WithNotBefore(jwt.NewNumericDate(oneHourInThePast)), issuer.WithExpiry(jwt.NewNumericDate(oneHourInTheFuture))) r.NoError(e) - serialized, e := tokenWithTimes.Serialize(false) + cfIssuance, e := tokenWithTimes.Serialize(false) r.NoError(e) - claims, err := Parse(serialized, WithSignatureVerifier(verifier)) + cfPresentation := fmt.Sprintf("%s%s", cfIssuance, common.DisclosureSeparator) + + claims, err := Parse(cfPresentation, WithSignatureVerifier(verifier)) r.NoError(err) r.NotNil(claims) }) t.Run("error - signing algorithm not supported", func(t *testing.T) { - claims, err := Parse(sdJWTSerialized, + claims, err := Parse(combinedFormatForPresentation, WithSignatureVerifier(verifier), WithSigningAlgorithms([]string{})) r.Error(err) @@ -101,7 +108,7 @@ func TestParse(t *testing.T) { }) t.Run("error - additional disclosure", func(t *testing.T) { - claims, err := Parse(fmt.Sprintf("%s~%s", sdJWTSerialized, additionalDisclosure), + claims, err := Parse(fmt.Sprintf("%s~%s~", combinedFormatForIssuance, additionalDisclosure), WithSignatureVerifier(verifier)) r.Error(err) r.Nil(claims) @@ -110,7 +117,7 @@ func TestParse(t *testing.T) { }) t.Run("error - duplicate disclosure", func(t *testing.T) { - claims, err := Parse(fmt.Sprintf("%s~%s~%s", sdJWTSerialized, additionalDisclosure, additionalDisclosure), + claims, err := Parse(fmt.Sprintf("%s~%s~%s~", combinedFormatForIssuance, additionalDisclosure, additionalDisclosure), WithSignatureVerifier(verifier)) r.Error(err) r.Nil(claims) @@ -119,7 +126,7 @@ func TestParse(t *testing.T) { }) t.Run("success - with detached payload", func(t *testing.T) { - jwsParts := strings.Split(sdJWTSerialized, ".") + jwsParts := strings.Split(combinedFormatForIssuance, ".") jwsDetached := fmt.Sprintf("%s..%s", jwsParts[0], jwsParts[2]) jwsPayload, err := base64.RawURLEncoding.DecodeString(jwsParts[1]) @@ -149,10 +156,10 @@ func TestParse(t *testing.T) { tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer, issuer.WithIssuedAt(jwt.NewNumericDate(oneHourInTheFuture))) r.NoError(e) - sdJWTSerialized, e := tokenWithTimes.Serialize(false) + cfi, e := tokenWithTimes.Serialize(false) r.NoError(e) - claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + claims, err := Parse(cfi, WithSignatureVerifier(verifier)) r.Error(err) r.Contains(err.Error(), "failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation field, token issued in the future (iat)") @@ -166,10 +173,12 @@ func TestParse(t *testing.T) { tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer, issuer.WithNotBefore(jwt.NewNumericDate(oneHourInTheFuture))) r.NoError(e) - sdJWTSerialized, e := tokenWithTimes.Serialize(false) + cfIssuance, e := tokenWithTimes.Serialize(false) r.NoError(e) - claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + cfPresentation := fmt.Sprintf("%s%s", cfIssuance, common.DisclosureSeparator) + + claims, err := Parse(cfPresentation, WithSignatureVerifier(verifier)) r.Error(err) r.Contains(err.Error(), "failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)") @@ -183,10 +192,12 @@ func TestParse(t *testing.T) { tokenWithTimes, e := issuer.New(testIssuer, selectiveClaims, nil, signer, issuer.WithExpiry(jwt.NewNumericDate(oneHourInThePast))) r.NoError(e) - sdJWTSerialized, e := tokenWithTimes.Serialize(false) + cfIssuance, e := tokenWithTimes.Serialize(false) r.NoError(e) - claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) + cfPresentation := fmt.Sprintf("%s%s", cfIssuance, common.DisclosureSeparator) + + claims, err := Parse(cfPresentation, WithSignatureVerifier(verifier)) r.Error(err) r.Contains(err.Error(), "failed to validate SD-JWT time values: go-jose/go-jose/jwt: validation failed, token is expired (exp)")