From d97630ef45b74c35e87eac19b9f762029ba9b285 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Thu, 14 Apr 2022 11:15:09 +0530 Subject: [PATCH] Encryption enforcement Applied https://github.com/mattermost/gosaml2-internal/commit/5ea31b199f3a2956e09d4ecb50d4f7d7e811b6fd --- decode_response.go | 24 +++++++++--- decode_response_test.go | 1 - providertests/onelogin_test.go | 15 +++++++- providertests/pingfed_test.go | 15 +++++++- providertests/providers_test.go | 5 +-- providertests/utils.go | 9 +++-- saml_test.go | 19 ++++++++++ testdata/mixed_assertions.xml | 65 +++++++++++++++++++++++++++++++++ 8 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 testdata/mixed_assertions.xml diff --git a/decode_response.go b/decode_response.go index 10d5ae9..bf0aca6 100644 --- a/decode_response.go +++ b/decode_response.go @@ -189,11 +189,7 @@ func (sp *SAMLServiceProvider) decryptAssertions(el *etree.Element) error { return nil } - if err := etreeutils.NSFindIterate(el, SAMLAssertionNamespace, EncryptedAssertionTag, decryptAssertion); err != nil { - return err - } else { - return nil - } + return etreeutils.NSFindIterate(el, SAMLAssertionNamespace, EncryptedAssertionTag, decryptAssertion) } func (sp *SAMLServiceProvider) validateElementSignature(el *etree.Element) (*etree.Element, error) { @@ -265,6 +261,24 @@ func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) ( return nil, err } + elAssertion, err := etreeutils.NSFindOne(el, SAMLAssertionNamespace, AssertionTag) + if err != nil { + return nil, err + } + elEncAssertion, err := etreeutils.NSFindOne(el, SAMLAssertionNamespace, EncryptedAssertionTag) + if err != nil { + return nil, err + } + // We verify that either one of assertion or encrypted assertion elements are present, + // but not both. + if (elAssertion == nil) == (elEncAssertion == nil) { + return nil, fmt.Errorf("found both or no assertion and encrypted assertion elements") + } + // And if a decryptCert is present, then it's only encrypted assertion elements. + if sp.SPKeyStore != nil && elAssertion != nil { + return nil, fmt.Errorf("all assertions are not encrypted") + } + var responseSignatureValidated bool if !sp.SkipSignatureValidation { el, err = sp.validateElementSignature(el) diff --git a/decode_response_test.go b/decode_response_test.go index 80ff554..3886a87 100644 --- a/decode_response_test.go +++ b/decode_response_test.go @@ -151,7 +151,6 @@ func TestCompressedResponse(t *testing.T) { sp := SAMLServiceProvider{ AssertionConsumerServiceURL: "https://f1f51ddc.ngrok.io/api/sso/saml2/acs/58cafd0573d4f375b8e70e8e", - SPKeyStore: dsig.TLSCertKeyStore(cert), IDPCertificateStore: &dsig.MemoryX509CertificateStore{ Roots: []*x509.Certificate{idpCert}, }, diff --git a/providertests/onelogin_test.go b/providertests/onelogin_test.go index 871ee01..38fef67 100644 --- a/providertests/onelogin_test.go +++ b/providertests/onelogin_test.go @@ -18,7 +18,7 @@ import ( "fmt" "testing" - "github.com/mattermost/gosaml2" + saml2 "github.com/mattermost/gosaml2" ) var oneLoginScenarioErrors = map[int]string{ @@ -182,6 +182,10 @@ var oneLoginScenarioWarnings = map[int]scenarioWarnings{ }, } +// oneLoginNilKeyStoreIndices is a slice of indices where keyStore is not required, but is passed nevertheless. +// This is to make the tests pass. +var oneLoginNilKeyStoreIndices = []int{1, 3, 4, 11, 12, 13, 14, 15, 21, 22, 25, 26, 31, 33, 34, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 92, 93, 94, 99, 155, 156, 157, 158, 159} + var oneLoginAtTimes = map[int]string{ 25: "2017-08-30T23:00:00Z", 26: "2017-08-30T23:55:00Z", @@ -204,10 +208,17 @@ func TestOneLoginCasesLocally(t *testing.T) { scenarios := []ProviderTestScenario{} for _, idx := range scenarioIndexes(oneLoginScenarioErrors, oneLoginScenarioWarnings) { response := LoadRawResponse(fmt.Sprintf("./testdata/onelogin/olgn09_response_%02d.b64", idx)) + var nilKeyStore bool + for _, ind := range oneLoginNilKeyStoreIndices { + if idx == ind { + nilKeyStore = true + break + } + } scenarios = append(scenarios, ProviderTestScenario{ ScenarioName: fmt.Sprintf("Scenario_%02d", idx), Response: response, - ServiceProvider: spAtTime(sp, getAtTime(idx, oneLoginAtTimes), response), + ServiceProvider: spAtTime(sp, getAtTime(idx, oneLoginAtTimes), response, nilKeyStore), CheckError: scenarioErrorChecker(idx, oneLoginScenarioErrors), CheckWarningInfo: scenarioWarningChecker(idx, oneLoginScenarioWarnings), }) diff --git a/providertests/pingfed_test.go b/providertests/pingfed_test.go index 4a5337a..fba4076 100644 --- a/providertests/pingfed_test.go +++ b/providertests/pingfed_test.go @@ -18,7 +18,7 @@ import ( "fmt" "testing" - "github.com/mattermost/gosaml2" + saml2 "github.com/mattermost/gosaml2" ) var pingFedScenarioErrors = map[int]string{ @@ -51,6 +51,10 @@ var pingFedScenarioErrors = map[int]string{ var pingFedScenarioWarnings = map[int]scenarioWarnings{} +// pingFedNilKeyStoreIndices is a slice of indices where keyStore is not required, but is passed nevertheless. +// This is to make the tests pass. +var pingFedNilKeyStoreIndices = []int{1, 2, 3} + var pingFedAtTimes = map[int]string{} func TestPingFedCasesLocally(t *testing.T) { @@ -68,10 +72,17 @@ func TestPingFedCasesLocally(t *testing.T) { scenarios := []ProviderTestScenario{} for _, idx := range scenarioIndexes(pingFedScenarioErrors, pingFedScenarioWarnings) { response := LoadRawResponse(fmt.Sprintf("./testdata/pingfed/pfed11_response_%02d.b64", idx)) + var nilKeyStore bool + for _, ind := range pingFedNilKeyStoreIndices { + if idx == ind { + nilKeyStore = true + break + } + } scenarios = append(scenarios, ProviderTestScenario{ ScenarioName: fmt.Sprintf("Scenario_%02d", idx), Response: response, - ServiceProvider: spAtTime(sp, getAtTime(idx, pingFedAtTimes), response), + ServiceProvider: spAtTime(sp, getAtTime(idx, pingFedAtTimes), response, nilKeyStore), CheckError: scenarioErrorChecker(idx, pingFedScenarioErrors), CheckWarningInfo: scenarioWarningChecker(idx, pingFedScenarioWarnings), }) diff --git a/providertests/providers_test.go b/providertests/providers_test.go index dbbcc36..193357a 100644 --- a/providertests/providers_test.go +++ b/providertests/providers_test.go @@ -19,8 +19,8 @@ import ( "time" "github.com/jonboulle/clockwork" - "github.com/mattermost/gosaml2" - "github.com/russellhaering/goxmldsig" + saml2 "github.com/mattermost/gosaml2" + dsig "github.com/russellhaering/goxmldsig" ) func TestValidateResponses(t *testing.T) { @@ -47,7 +47,6 @@ func TestValidateResponses(t *testing.T) { SignAuthnRequests: false, AudienceURI: "https://saml.test.nope/session/sso/saml/spentityid/dknhyszjl7", IDPCertificateStore: LoadCertificateStore("./testdata/adfs_idp_signing_cert.pem"), - SPKeyStore: LoadKeyStore("./testdata/adfs_sp_encryption_cert.pem", "./testdata/adfs_sp_encryption_key.pem"), SPSigningKeyStore: LoadKeyStore("./testdata/adfs_sp_signing_cert.pem", "./testdata/adfs_sp_signing_key.pem"), Clock: dsig.NewFakeClock(clockwork.NewFakeClockAt(time.Date(2017, 9, 21, 23, 28, 0, 0, time.UTC))), }, diff --git a/providertests/utils.go b/providertests/utils.go index 42da0cd..2407acd 100644 --- a/providertests/utils.go +++ b/providertests/utils.go @@ -27,9 +27,9 @@ import ( "time" "github.com/jonboulle/clockwork" - "github.com/mattermost/gosaml2" + saml2 "github.com/mattermost/gosaml2" "github.com/mattermost/gosaml2/types" - "github.com/russellhaering/goxmldsig" + dsig "github.com/russellhaering/goxmldsig" "github.com/stretchr/testify/require" ) @@ -136,7 +136,7 @@ func getAtTime(idx int, scenarioAtTimes map[int]string) (atTime time.Time) { return // zero time } -func spAtTime(template *saml2.SAMLServiceProvider, atTime time.Time, rawResp string) *saml2.SAMLServiceProvider { +func spAtTime(template *saml2.SAMLServiceProvider, atTime time.Time, rawResp string, nilKeyStore bool) *saml2.SAMLServiceProvider { resp := &types.Response{} if rawResp == "" { panic(fmt.Errorf("empty rawResp")) @@ -152,6 +152,9 @@ func spAtTime(template *saml2.SAMLServiceProvider, atTime time.Time, rawResp str var sp saml2.SAMLServiceProvider sp = *template // copy most fields template, we only set the clock below + if nilKeyStore { + sp.SPKeyStore = nil + } if atTime.IsZero() { // Prefer more official Assertion IssueInstant over Response IssueIntant // (Assertion will be signed, either individually or as part of Response) diff --git a/saml_test.go b/saml_test.go index d399c12..6f63009 100644 --- a/saml_test.go +++ b/saml_test.go @@ -153,6 +153,10 @@ func TestSAML(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, authRequestURL) + // XXX: Need to set the keystore to nil to bypass the assertion + // to check if all elements are encrypted. + sp.SPKeyStore = nil + authRequestString, err := sp.BuildAuthRequest() require.NoError(t, err) require.NotEmpty(t, authRequestString) @@ -287,6 +291,21 @@ func TestInvalidResponseBadBase64(t *testing.T) { require.Nil(t, response) } +func TestMixedAssertions(t *testing.T) { + f, err := ioutil.ReadFile("./testdata/mixed_assertions.xml") + if err != nil { + t.Fatalf("could not open test file: %v\n", err) + } + + b64Response := base64.StdEncoding.EncodeToString(f) + sp := &SAMLServiceProvider{ + SkipSignatureValidation: true, + } + response, err := sp.ValidateEncodedResponse(b64Response) + require.EqualError(t, err, "found both or no assertion and encrypted assertion elements") + require.Nil(t, response) +} + func TestInvalidResponseBadCompression(t *testing.T) { sp := &SAMLServiceProvider{} diff --git a/testdata/mixed_assertions.xml b/testdata/mixed_assertions.xml new file mode 100644 index 0000000..561dda0 --- /dev/null +++ b/testdata/mixed_assertions.xml @@ -0,0 +1,65 @@ +http://www.okta.com/exkbb59wb20X96NY20h7 + + http://www.okta.com/exk659aytfMeNI49v0h7 + + russellhaering + + + + + + "123" + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + russell.haering@scaleft.com + + + + + + + + + + + + 93Cl2F0L56ilGhdPuwewQF5wgnTFKocc4ulHKv8fLKBMf2pOxb3VAYeq5GuuwqB14Ug2QA8jBJ3QrwL9g6FE9ZSEUk7GqX18veEUstLgTwhakl87HcIHDxY4v6BhM5dAPY4JRPZGdSSuYbxCxLbz2HbaELJVpFiK+klLWM3N5OyZXbP+sMdcS3zHL7vgmGwPFZWux/YWFahHbgZjHM+3YoamCi1B4VqqoJIoMw9gSdnQNemsLjlCb4CRytBUs/i6rPzkPaC7HNoln4r1/fidXteCvHYscyf06c8KqbWRA70YCBH/1rjU1/az/BTg52zGGu01x4OiH7xuVWYU5QBGUD+C1nsxYUbyN+B7u+pE+pgr9oaEcAjXUUIXby+ozqX1I8Gr47UZ7QQhzgYtS7+J9yu6aGmikTUHN+bbwwHcerW4VgXJcZpW34nXqDtaHPApilpsQwAeyVwLZuouPJlEteMEnD0mCuplI0tmmrcpo3vM4VySy/DjFMnoAHM3+XDAPW2+j6ncL9ZUOlGefD8CO/N4o+U2XmXfhcoR6RDp2nj5OW8ENekPHdF4qr21ukyrjsnjfx3RXPILzErE7w40dm6tEkeLc9KdK6NSBsVfKfgkFiAgS+P3Hru1MkEmNpxShoj1bm45Ufvjz36NwsNvipPSEhm+3uoLNqhhSS+V5UxaWTDPnNfpLYTfRqZULuvxj6JcAKJ4hNqNTVdCuk0OL+0PoiSWm4ymSXZCr6eQ3Izq/bR5EujyAn6HuxW7vsLi+hnk4mAQhSOj/QAUBaLEbwp1cYFWXqciZb9TcDP8+sJA0jz8tZPAwyL0DBKV4TrtdNY5JcLEnpIinqjCtbsSsO3cQKxBq7hPZnpcYKgOUx4q2ghn7kbhf9J9y0kMq2/1sumg9+q5nsV3qk4VxiKqkR53lzNmrbFIU1S94cdV3NWbQYf+Ve6GOSVEniVrfCpfmxR9bOzi4FMzU4Iema342ufT5E9951kNrVJxKCW6Nk+MqM72l9muscNqBT2JL+hJmbAZodHpnRvlRZv2Ffr4BUcarN5nt/YT7wFaVVV47ANxxIo6aylMoaF5MsoVIjEZncBRiN735hLMuX4PAvLWXOweodVpW7y5GAgmUrQSvH9uAwAIpPibZKTPXovuzK4A+P9IWOfk7Vb+aggGBOQWwcmp9vnv9tImx864K8KFNqLWIyESfCwH4WfzRRmcBoZSCblrEKl8JPT6lc5RB/FBun0TK9a5NLP4cI7c16KPySxeFMcGjjVryddzMFLEbmzGB0tA7LacXwkWE+IHoOGtqIhd/LJTVoZ6QBBp9axpUkWF45HtDUYlsDLjbzXpYqRp5ZsVGhurbuWVrv09iLDJTXFQmsrvGHIyolScd2tT2fJ0h4jK2u8bgIYRlyJabktmR3GOD7uFT7RidduI2wWSgnebGDn1dgh934GfI7BRKiVvA1maxOHnymK4RE5bwbXxbZulzQfCjFn0OT4JYR+xt9tiMMD/QUBBaGSSc+Hpk2tiV1CsVOT43gqBfam8b7SrSu7C8QwuVBBdEqDfxalfzg5EM8SAJ8Gt8fPsBLJrnG8hXm5JVlj/oc9vxXhX6dS4bjDiOaJx4eU8gQXAWnanB+nep+gg8QzcTDmgy4nfqJDxs6/IUlAn70mfrraMdIgWpGYv2oU/tVloG26gRVXzHpPzDO4wtQUdRa4iG7d6ORqOMTkZyUkuaDfycSUH6YeH2+Xr/5U9/CSbyT3Kxsyv2prLOtPKLEHVZ1IOeQISs/+UpZZfDA1E5cexWvrxmQDm9vEki4lrXoCqIr6c6oOIsG292RtXyQ8eJ2VA3zaYdlXEBo2iSdALS+PRmxNyiF2igAO4od47ee1XbdyNs8U4/ywKz6plOOFrF8ZxGK8gtftqmmpulVqZPfsnbk18u3V+ZZ0TihdnbVD+9bDq/XmeHuyGrcz61hqFnyfL5IKVmn3YHj/btqs5n1VLvJ4yyA8xeFHITPR4dJlXSxqAnPQnnHoBw94rqJQD0tVPKzdYfbAYNxC32jQEpyOQJJ3krZa5TG0RrHZurGmm3Ge9azl0GD0XdyalAuP8JO3dFmr8GKhtGk0y7ewMoaYXPNv5p35kr5ZmIjfoeaQiLRPb2KKryjJXDFwOf65zG1AM1i8V25LdxHS2Wd41PvFAGX8ms7fFMbmAo35YY96513SXNyrfS3gn23EK4wKgML9dRLEZ76fUBoPeY85gfVeeZZkANTsMqXYP6j0tBYXeNSHgTX26InyoOL/MKxTjSOV/HDuzF2zwWrCuj0nsOjB+g5XMonNV9rn8vDXgiSrP/W70T4gmiTOWEWhve6eKmld4KNtILurXfUItvHx3lGpG5M0HK5maZLINa85FPH80K5CcZX1twLOUB5nliOZk64yePcZcjnLXW5ho1VNAJ6NzJRLp4pd873O3npQ01EjzKXXcjS46HZiNurJNKnOYhyb8hGtSaaCQIUhZTkHaQOG8vqkwvSlhipvBjjuHaT3+nugh9xXW8mkrpeMmmO9BUt/NOSopHzRtt6AmFRrSx4MXbhVZeaDvo5xSaOXP71ISIBJr3nuV7hHzDK9gn9Ga37cnAx303ou2ArjevYqhq0tcGWNyfJd+IcEA9d13cOkBeo0o/Fzz4tdYZS7iv0wGZDl9UdGOGAJf0s+h3jlVUyE0NT/H/BJd5ak2zawqiBRe6K3RndvnmHepfi07M6bnuFHdP3Hu7/3FacWemBYRSHmhxa3LT7YAN591j/T1+Ka7Zf5TMVnbhDUmMeQM99R/xKUA8jZxUDRtarGxbLkW0LW+HPpjonY7pU18DCYa4+Wma2fVV03kRisiD+stUQF81xWMUculnvMqxjgY/51Hv5/mGGrTZAe7GNKjk17o7gEbNPXBpvEk6Elp9IYRVxIFnX9VmIrOBb3CxkmONcjMPxbscObnVUwkSGczfGY5vHh4My4BvqWDpZFXoaBhoU5T8ojKceKn7ILqf1IjihF2HPxoLCn8AlB0NUiOin0q1KsROeGO+XaGRIzTwEdlCMJOzSDNjDVxJzVk8zq+g5Tm2DhibzPVkqNSyFKNFQPVYS1qLo72KslekUUoiQ4UUT/9AzgECMuV1BUaV8hwnR2K6sYCtd45bz+uOKUVXTMdNQnyMt8NOicd6akPGCv/0bwD9195I0rehtz+KHfTXCXWry0kY5dndmrf875iUyMO3WWi1SBzQOCsSE19uOGn54A0zXy5FlnwzgdrxCrZWiAbduxDzyMqNDUvZeOhDeBipwGi62cx2BmXtktKFw/cZ8AVtgKfceBdH+d7WsaOC7dvRZkpSUWFd9YghhY6PkosujGeB6NGoJmUn1V4HkvS+FMLhyl5ztasxK1IL6JvqaYmUD0r6y5pScyLePHh8sNOf+GTiJfJ048u/kCeatDLjlP66EMtrP+KKpcrj33wXFybukpvqSlDQbdUghQl7J0e+jF5kGPeFP2IkyxuN0BMB6B/u8qXBCIk39+by0wKh0n8BMiAnDMDCuk+HeIBGmbV2taAey3gtIIfu+BOSK7iPEg6J3kuiRWFXeMPkdlfnmShkXCDgerjooZhR/hOMAY1JB1P4+AydItxgrshD0ANvSq+6ekVfb4uZvzoGi8P4n7rehqti18vZ6E2WUP2TUDBO8OARcEChw/jUv47SMK6YpXGjEnm9UmFKzS5bwS1h9kSWyshNvma2+wfTGuRVny/9gDHJfgOz0UCmkRmRIkbhG9v7pOCf4y7L65flRTEn+Kn2YM+AON6M0v59VeXxXccrSWFgpIrRjKPp5eGm/72Y/o0NeiBLz/Um17yNZoDndzBomTq2zRtNDoRNwSiKbFtTNhkgHm9jbO7iY+95yL8nj9dIIpZNHdvkFdQdmzT5X3fUa/J3DGcaDzIdyiMc6pHB7t9GYxOWiZqz0OZAF2efFpc4OgtbHEDZyfWAIs1yZnKziA4c69zHBzLAgb+PsZUXk8ICWYB2eMNWdDMr+YNx6itWV+P/OjYW284eFo0ArcRYAPgezbiu2nimJbRnxqAocq8onKd9kE20nL2XN7NjTAfCL48gpQZtfNdpIjaTCcg+ZERMzlhYEHgksT2IpW4u8N1beQwotXp737X73iyTXGX2eaaLHBf/CSUuWPQOgFP3tqj91oUX/nq+06KSlUn/fsrqPOxUv/g9cltLwDNpQFe+dxMsxZtmYurwRgCic/eqX8QOg94FganT0zBMn9taO6+jp/Mih5T7k79MzM6GzGGTJNH8CNjuHMdIZRHLydR48D04QDPAsRvaP/TsxbaE6J2ZfD2sZU3yRp/Nn14tG/yuSx8hN4Kod2JKZYSJA3+Ci3rlxx3flvtYAB5HjARZnXlfWT+6kdK8LC7PwMyW+fewexQk8045SM5W+ReN9zRhEPDsN5NfAiERxX30sfwCr8K9m0Dmq9oL2Jcje7GB2PsdzQ5tq6aM3xzqcSMnCL+Je/wxJazDH1tLxsdCa3x9AypIqfSk1vLmLpwFJAFNFtI+UTnjighp4XmjCegJPk7Ewloy4CR1VWJGOa0t7S7Y9pbH49Mosq/uUEXPu1u/rdNeAugn0VdNL4C0famvbvmJTq4+sUxhoaPsVjKarUc19LDfYbHzmuBg0e6ktjmweM1fp3BT2CNlPWKG7qwkKR8MzonPrMG8PTcVzo26G5KjOzLDa6WvtRJpFLU37tNeqcAQt4NSQpUmninB0wRhm8OPcW7pbz8Xmj4W3zBivDUMgyvcDOHBYluyIotZM/FhWGzIteiMTGS096E6pKLIh897BIF1WKhMT2auJcfg4K8mh8N1znqGtPQDE4JkrVc3EwqjDrfWpg3AzMYGi8CHZh0Wq72HhXHw5/TQRv+z952S1HMDMwNjI1LT/qowPBLxdy29BR99xG/7FftsD5EtbueehrqDJLjSR31YFhJnfjxTmi7k4us/yhJr8PttoKFJ/NgdGgNxvwqIfL7f3OFpW5akuww7UdTH420a436vJLs6khAJZETHQ9YDr0q5FnC0vLo8YhZ1AeudRx/R6xF+ZJsfLNkzsdpgy6EqLNrI62E4OvDe+3/pvD6dHxgwNCw4h/aZUz8lb6GL8WvvtQLJIIjtMUmT77pKqzWaZAf+nQACVdo9mF0QlBvpboXiTP63vfdwL/zIZ9Pf4ZKu/DaWKG1fh4B/GfgO2G4wunomZTzywePsUgelsbQQB7dfsmwCZKQnFxTrbVuZxTrHoiV4K18TLWsj7E3VrrfmhqZadvs56L7oG/lsFKmc7anPl1UPMa2snjaCCKRmh1vWfEaVeAx/2wa6zUcQ86Ooj7FStY02StHINXVg154Fvcql98faVTL/pG4b3Dh4/ipScIeBeYG4qQmxTBdu3HYr/yaLANnwtKZUPqbzRrZkolBYWyDh8FHg8ZWXf4kM8XI+GrDFWBpE670UZbCNhBFCeQJTqNO32zPa+c1XE3q9Z3FEqzjneenJFGJLL9EgvlSdRmwqelTZhA5HxHqkihTxXr+LtjxtFcz7zB62EGtE+eQRo49JurQGnOraJ8+rYdAKYQINECyfJV9LqHLNUEe42L4kEywXD1ygvv2sEGyqnsuFDjVdbJQT0p6OGsJdHdSTiFDnqxWQt2YpkUQRVAnltvtqzmbsh9QuRzQzYv9q4xwHzyunxYa/btqHxEaZd8+5U0Xcm/ERceuV7p20SXv6J7RSOeV9+dX1GVv+PgH5wSi3SXVVx0JCTb091TxQ0o24Qr6dcPZ5GbmrmbYsQL7846abmESCpz5SM0FQerEHlyHcV6yAEUGyh+ZQfy9sb7sy1KBPi1Ge49tvRQvlEJ+4/CVhAMSi+SNTwjoN2x9xt0oGQ3gxk2951jux/HsrI2mrIOoCem6v0Zeb7gbslVYG3Xoy0helgtX9h1QIdrctGYZaoPeBVStxz7CSaEOQjY0CJ/e3khFu1ywvD0WNLlkOWyKa7Bg3dvzOF6dJB5XD5GtZxlby/kLopT2AEw949y4JO6MedETjEaCUR/ed9SM44KOErL/kbAyoWoYjSTBm6IupwqY7IU1D5kgZHlOq0smwn2fCcRHvFJIorSv2iHsJy+jKkTZfs3shRmLXDdvAxbOIbKCl888v/dpwgOYgZLPYvB5t29S1TYtWE4KGUjAxPb8zBPN6XXRQ1CVR7qdyjUjtn1NdtiTFDWJyu75ETAaL1ZiLDonh5tkpbwxvmzKd5wwRyXC+XITkW3lS8bU05oJ7T0mXJkTPZoyfM0mxdppAAEVA3BO5JPwFBiMHcjPGDPFNK2hBMAA0XV5urKPrCUIjBsv2LHiCeJuxa+icwhef7dqEZH1z9wXk2YEG6Fc+uOCoUU53UOM+lJFdZ59XD57715Qp+qIGGjNm3OFovMtPTWsnEQltwcfHh0VEiwvQMD/VqrnSU5bBl4uXCTBKOA6J44qkfSX+ym8wTmWSNcjVn6j6SDGxo6cBpJ/eNlrKcKL+VuTy+5vm2Lfi5Ybt7LbsfGyj9xVNs8oj+m/PW19DT5JsKqynplXcxxtGSk+5brFVUTvvupZzEC4VHPoZi2HFyW/VgbMyYMYCw6M59enOgCVfKN7hSO2J+vTvvysuOvCzu9uIaumVgXyxIoOoviz5BFqEyjPogn8P5yqAraDmW5XmFFHXZKGxarWM/w6A8N6w1/1Ef9zKj2Wfkcm19gQaIkqjSGQsKxpRzGGLTPIk+X3NBiHHfqDk+Vte3JPF8CEg4ruKi8Ur3nssg6OQATDRtjU0UmJJh5wDOpcoLz6QjkNOGDrh4PtjoS3w0NC6ZOPKUWf721W1Xyw/BYGYsm9qKARONXV/IKZpGd65/++k2pXVJISbNwOVRX5PSQhChOJLO2yrIIcCO7us81/GXX6JZctb5EnM8KpXte/L1tht4Wg2knk40L34GN0FSm3mmrBckLMTHPdmuD2X16akB19hfco+VtXj5PCDAbvRp1LcivbUGTJ3Bie/xut32O89gROXq63xwXKzCCIHgMVKvyN2xeddQbAXiF0WNUSzjT1R36SlCh1cqrv29aPyrXDY63yDFu7zK58Hs2fzh734K6ZVJC//AsLhLw18VAijSSChThve+TZJF+aEMWzbOu9KSH74Fw8gill42I45e/KMvBMYKO64CuI2ZUdFlQQeit/5W6+ut707Lyj4w2IF67AIfVH88ZsotIbbnyqJFklgerdyxiVjPq7ZqSWB5u0vsgzY2DUFxsLyhsr+t1TvsAhRSUUkJzENXpt5hViYIUU3osDWckARBxK5CDLj1ZgFAFtzronk5BSOfJpX65fWaHjGA7PyLEauh/bc4+xhWMNcanxwwbXAUC3H1u1JZwOQEG+PQMnghie8+1YwoetjNcYB5IxXvePNE1PpRmTfNRzrxWVwAcswypqZFw4UypzX/fuQjEO1/sxYx494dblki1VaNMo3y2iW4zs1BowPzDUMkmc9qvptIOtxuBapyxC0bxbtUZnGlBKxPYQ/PVwSMvH0mbzrRJT+TVDrWjP73VEOihM6ANSSmwk/JMc0dLkP58gKWJY4UZikHpX+Og/dvAir/qNLSSgfeAQbHNdMWY7Iw5TQmQw+EweotQ7eAgeVjwZYjjP114O4KQWOlPelA2A + + + + + + + + + MIIDPjCCAiagAwIBAgIJAOvpZtJNtQ/rMA0GCSqGSIb3DQEBCwUAMFUxCzAJBgNVBAYTAlVTMRMw +EQYDVQQIEwpDYWxpZm9ybmlhMRgwFgYDVQQKEw9TQU1MIFRlc3QgTm9wZS4xFzAVBgNVBAMTDnNh +bWwudGVzdC5ub3BlMCAXDTE3MDgxNDAxNDg1N1oYDzIxMTYwMzA4MDE0ODU3WjBVMQswCQYDVQQG +EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEYMBYGA1UEChMPU0FNTCBUZXN0IE5vcGUuMRcwFQYD +VQQDEw5zYW1sLnRlc3Qubm9wZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKfUlciw +DPCJ9JXy1NsxKfqyOfLQXFKKGgNKKRLzLAWO43GgQYrV67QaAny5LvgOa81KQOX4IvjJy0nd/VrO +6jG9pTdMR9vFHT0Tu3W1QUsyPPSQqpKXwb2ZCSXNghYD5gBrNtmOxAOs1Azx5qmIzBJ4F0RkRivc +dbXSfBUBLpCn1M285c4tk5SEvoRgBeisLJJDB7hmylA1V6/+BN+N6b3M+ZZG//CTvOjJKgnwkMAL +zXbPbQWa0kpbMLDMz8Kr2pODLKb3rGmdf4P6A9aA41k6L4clTsNdcK7ZwJ2fGF79WGzE6RLyBX2U +S3gPHUXt/WMQZXyULO3exiTDsQXWFf8CAwEAAaMPMA0wCwYDVR0PBAQDAgQQMA0GCSqGSIb3DQEB +CwUAA4IBAQB3fQZz2qNAo/wohPNaR2uAakLt25cKrsbgn3XwO+X+DRH4nqMSZkHdLGr82xD3i33B +LSQwXzFecEN+CA8SlpNoIXHwk6D6V3uEBSjKemZtJFCJqe8d+5qt3GV50AKmtEt2Nc7B94TN8xB5 +EmZladnTJWcBgSuuz1hLXQQ02dJ3dDq8FMlyR70S2DTtuPu0i2/f6KtrlmmNsU24Aq+fCzEG43Nq +ShOMeM5rDxLsSJCtjfMRwG8YeyNf6cCMSMxYAJfrTyj+AS8RtZQNGF7tX+exEWHOf1C9CBzHtKRC +lBRL6zDjlpPCIDwcMFi8NXvaiVclxIq3XdfqxiGBBEldkhgZ + + + + hnCCrXAB+bHtA/4pJyPEYg1CDpAdF1yyrV4t7ZTkSD+BJiQcPzIb2U6E8/TyClCZ89ylb17Hmq69sMQjds3YIeTtBv5qsKAs1GQ4oBpCSQxQ6Dkre/qYuX0QNoiGM04yuB7933TWFB21lqTkCk42UwnFVTcLxKXxk3T7rhTMfInLayQVD+JSVVCqxLS5/TtahKXPeoQlCERR/6z4LWQ+LHiSi9urMLJKfNLt8A6liFhBnc9+3miZwth3GYDUdF+g6TI4wiNC7e4wonOwFcgSmujd0S5mnLbupyuPt4RCTf+L/udMkQpcUg5vfUNwg+GfuHC/cf436qbf0Jrc+KD3FQ== + + + + + + \ No newline at end of file