From a4b9ccee8befb661be43bd9d74be8b189df9e3a0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 5 Aug 2026 16:00:09 +0000 Subject: [PATCH] :bug: Fix SNS signature verification field sets per AWS spec Address critical code review findings: - Remove 'Signature' field from string-to-sign (it's the output, not input) - Differentiate V1 vs V2 field sets per AWS SNS documentation: - V1 Notification: Message, MessageId, Subject, Timestamp, TopicArn, Type - V1 SubscriptionConfirmation: adds Token, SubscribeURL (excludes SigningCertURL, SignatureVersion) - V2 Notification/Subscription: all fields except Signature - Add 'Token' field to SubscriptionConfirmation (required by AWS spec) - Add AWS documentation URL comments for future reference - Improve error handling in fetch-certificate and verify-signature - Add end-to-end signature verification tests with real key pairs - Add test resources (certificate and private key) for signature tests See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html Closes #11092 AI-assisted-by: qwen3.7-plus --- backend/deps.edn | 2 +- backend/src/app/http/awsns.clj | 97 ++++++--- .../backend_tests/bounce_handling_test.clj | 195 +++++++++++++----- backend/test/resources/sns-test-cert.pem | 19 ++ backend/test/resources/sns-test-key.pem | 28 +++ 5 files changed, 263 insertions(+), 78 deletions(-) create mode 100644 backend/test/resources/sns-test-cert.pem create mode 100644 backend/test/resources/sns-test-key.pem diff --git a/backend/deps.edn b/backend/deps.edn index 2599066e0b..d9e8141c6d 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -77,7 +77,7 @@ org.clojure/data.csv {:mvn/version "1.1.1"} com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"} mockery/mockery {:mvn/version "0.1.4"}} - :extra-paths ["test" "dev"]} + :extra-paths ["test" "test/resources" "dev"]} :build {:extra-deps diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index 91e003aaf9..18bf8a9429 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -35,7 +35,8 @@ (defn- valid-sns-url? "Validates that a URL originates from an SNS endpoint. - Only accepts sns..amazonaws.com hosts." + Only accepts sns..amazonaws.com hosts. + See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html" [url] (when (string? url) (try @@ -49,21 +50,52 @@ (catch Exception _ false)))) -(def ^:private notification-fields - ["Message" "MessageId" "Subject" "Timestamp" - "TopicArn" "Type" "SigningCertURL" "SignatureVersion" "Signature"]) +;; AWS SNS Signature Verification Field Sets +;; See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html +;; +;; Signature Version 1 signs only specific fields (excludes SigningCertURL, SignatureVersion, Signature) +;; Signature Version 2 signs all fields except Signature +;; +;; IMPORTANT: The "Signature" field is NEVER part of the string-to-sign (it's the output, not input) -(def ^:private subscription-fields - ["Message" "MessageId" "SubscribeURL" "Timestamp" - "TopicArn" "Type" "SigningCertURL" "SignatureVersion" "Signature"]) +;; V1 Notification: Message, MessageId, Subject (if present), Timestamp, TopicArn, Type +(def ^:private v1-notification-fields + ["Message" "MessageId" "Subject" "Timestamp" "TopicArn" "Type"]) + +;; V1 SubscriptionConfirmation: Message, MessageId, SubscribeURL, Timestamp, Token, TopicArn, Type +(def ^:private v1-subscription-fields + ["Message" "MessageId" "SubscribeURL" "Timestamp" "Token" "TopicArn" "Type"]) + +;; V2 Notification: All fields except Signature +(def ^:private v2-notification-fields + ["Message" "MessageId" "Subject" "Timestamp" "TopicArn" "Type" + "SigningCertURL" "SignatureVersion"]) + +;; V2 SubscriptionConfirmation: All fields except Signature (includes Token) +(def ^:private v2-subscription-fields + ["Message" "MessageId" "SubscribeURL" "Timestamp" "Token" "TopicArn" "Type" + "SigningCertURL" "SignatureVersion"]) (defn- build-string-to-sign "Builds the string-to-sign for AWS SNS signature verification. See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html" [body] - (let [fields (if (= "SubscriptionConfirmation" (get body "Type")) - subscription-fields - notification-fields)] + (let [sig-version (get body "SignatureVersion") + msg-type (get body "Type") + fields (cond + (= "1" sig-version) + (if (= "SubscriptionConfirmation" msg-type) + v1-subscription-fields + v1-notification-fields) + + (= "2" sig-version) + (if (= "SubscriptionConfirmation" msg-type) + v2-subscription-fields + v2-notification-fields) + + :else + (throw (ex-info "Unsupported SNS signature version" + {:version sig-version})))] (->> fields (filter #(contains? body %)) (map #(str % "\n" (get body %) "\n")) @@ -71,36 +103,52 @@ (defn- fetch-certificate "Fetches the X.509 certificate from the given URL. - Returns an InputStream that must be closed by the caller." + Returns an InputStream that must be closed by the caller. + See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html" [cfg cert-url] (let [response (http/req cfg {:uri cert-url :method :get :timeout 10000} {:sync? true :response-type :input-stream})] - (when (= 200 (:status response)) - (:body response)))) + (when-not (= 200 (:status response)) + (l/wrn :hint "failed to fetch SNS signing certificate" + :action "sns-cert-fetch-failed" + :status (:status response) + :cert-url cert-url) + (ex/raise :type :internal :code :cert-fetch-failed)) + (:body response))) (defn- verify-signature - "Verifies the RSA signature of the message." + "Verifies the RSA signature of the message. + See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html" [cfg body] (let [cert-url (get body "SigningCertURL") signature (get body "Signature") sig-version (get body "SignatureVersion") - string-sign (build-string-to-sign body) algorithm (case sig-version "1" "SHA1withRSA" "2" "SHA256withRSA" nil)] (when-not algorithm (throw (ex-info "Unsupported SNS signature version" - {:version sig-version}))) + {:type :validation :version sig-version}))) (when (and cert-url signature) (try - (with-open [cert-stream (fetch-certificate cfg cert-url)] - (let [cf (CertificateFactory/getInstance "X.509") - cert (.generateCertificate cf cert-stream) - sig (Signature/getInstance algorithm)] - (.initVerify sig (.getPublicKey cert)) - (.update sig (.getBytes string-sign java.nio.charset.StandardCharsets/UTF_8)) - (.verify sig (.decode (Base64/getDecoder) signature)))) + (let [string-sign (build-string-to-sign body)] + (with-open [cert-stream (fetch-certificate cfg cert-url)] + (let [cf (CertificateFactory/getInstance "X.509") + cert (.generateCertificate cf cert-stream) + sig (Signature/getInstance algorithm)] + (.initVerify sig (.getPublicKey cert)) + (.update sig (.getBytes string-sign java.nio.charset.StandardCharsets/UTF_8)) + (.verify sig (.decode (Base64/getDecoder) signature))))) + (catch clojure.lang.ExceptionInfo e + (let [data (ex-data e)] + (if (= :validation (:type data)) + (throw e) + (do + (l/wrn :hint "SNS signature verification exception" + :action "sns-signature-verification-exception" + :cause e) + false)))) (catch Exception e (l/wrn :hint "SNS signature verification exception" :action "sns-signature-verification-exception" @@ -109,7 +157,8 @@ (defn- verify-sns-message! "Verifies the AWS SNS message signature and URL validity. - Throws if verification fails." + Throws if verification fails. + See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html" [cfg body] (let [cert-url (get body "SigningCertURL") subscribe-url (get body "SubscribeURL") diff --git a/backend/test/backend_tests/bounce_handling_test.clj b/backend/test/backend_tests/bounce_handling_test.clj index 4555f199d2..5a70c0a7c6 100644 --- a/backend/test/backend_tests/bounce_handling_test.clj +++ b/backend/test/backend_tests/bounce_handling_test.clj @@ -308,66 +308,130 @@ (t/is (true? (#'awsns/valid-sns-url? "https://sns.us-east-1.amazonaws.com/cert.pem"))) (t/is (true? (#'awsns/valid-sns-url? "https://sns.ap-southeast-1.amazonaws.com/cert.pem")))) -(t/deftest test-verify-signature-version-1 - (let [keypair (java.security.KeyPairGenerator/getInstance "RSA") - _ (.initialize keypair 2048) - kp (.generateKeyPair keypair) - private-key (.getPrivate kp) - public-key (.getPublic kp) +;; Helper to load test certificate and private key from resources +;; See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html +(defn- load-test-cert-and-key + "Loads the test certificate and private key from test resources." + [] + (let [cert-pem (slurp (clojure.java.io/resource "sns-test-cert.pem")) + key-pem (slurp (clojure.java.io/resource "sns-test-key.pem")) + ;; Parse certificate + cert-bytes (.getBytes (-> cert-pem + (clojure.string/replace "-----BEGIN CERTIFICATE-----" "") + (clojure.string/replace "-----END CERTIFICATE-----" "") + (clojure.string/replace #"\s+" "")) + java.nio.charset.StandardCharsets/UTF_8) + cert-input (java.io.ByteArrayInputStream. (.decode (java.util.Base64/getDecoder) cert-bytes)) + cf (java.security.cert.CertificateFactory/getInstance "X.509") + cert (.generateCertificate cf cert-input) + ;; Parse private key + key-bytes (.getBytes (-> key-pem + (clojure.string/replace "-----BEGIN PRIVATE KEY-----" "") + (clojure.string/replace "-----END PRIVATE KEY-----" "") + (clojure.string/replace #"\s+" "")) + java.nio.charset.StandardCharsets/UTF_8) + key-spec (java.security.spec.PKCS8EncodedKeySpec. (.decode (java.util.Base64/getDecoder) key-bytes)) + kf (java.security.KeyFactory/getInstance "RSA") + private-key (.generatePrivate kf key-spec)] + {:cert cert + :cert-bytes (.getEncoded cert) + :private-key private-key + :public-key (.getPublicKey cert)})) - ;; Create a simple message - msg {"Type" "Notification" - "MessageId" "test-msg-1" - "TopicArn" "arn:aws:sns:us-east-1:123:topic" - "Message" "test message" - "Timestamp" "2021-02-04T14:41:37.020Z" - "SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem" - "SignatureVersion" "1"} +(t/deftest test-verify-signature-end-to-end-v1 + (let [{:keys [cert-bytes private-key]} (load-test-cert-and-key) + + msg {"Type" "Notification" + "MessageId" "test-msg-1" + "TopicArn" "arn:aws:sns:us-east-1:123:topic" + "Message" "test message" + "Timestamp" "2021-02-04T14:41:37.020Z" + "SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem" + "SignatureVersion" "1"} - ;; Build string to sign string-to-sign (#'awsns/build-string-to-sign msg) - - ;; Sign with SHA1 - sig (java.security.Signature/getInstance "SHA1withRSA") - _ (.initSign sig private-key) - _ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8)) - signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig)) - + sig (java.security.Signature/getInstance "SHA1withRSA") + _ (.initSign sig private-key) + _ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8)) + signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig)) msg-with-sig (assoc msg "Signature" signature)] - ;; Test that signature verification logic would work - (t/is (string? string-to-sign)) - (t/is (string? signature)))) + (with-redefs [awsns/fetch-certificate (fn [_ _] + (java.io.ByteArrayInputStream. cert-bytes))] + (t/is (true? (#'awsns/verify-signature {} msg-with-sig)))))) -(t/deftest test-verify-signature-version-2 - (let [keypair (java.security.KeyPairGenerator/getInstance "RSA") - _ (.initialize keypair 2048) - kp (.generateKeyPair keypair) - private-key (.getPrivate kp) +(t/deftest test-verify-signature-end-to-end-v2 + (let [{:keys [cert-bytes private-key]} (load-test-cert-and-key) - ;; Create a simple message - msg {"Type" "Notification" - "MessageId" "test-msg-2" - "TopicArn" "arn:aws:sns:us-east-1:123:topic" - "Message" "test message" - "Timestamp" "2021-02-04T14:41:37.020Z" - "SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem" - "SignatureVersion" "2"} + msg {"Type" "Notification" + "MessageId" "test-msg-2" + "TopicArn" "arn:aws:sns:us-east-1:123:topic" + "Message" "test message" + "Timestamp" "2021-02-04T14:41:37.020Z" + "SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem" + "SignatureVersion" "2"} - ;; Build string to sign string-to-sign (#'awsns/build-string-to-sign msg) - - ;; Sign with SHA256 - sig (java.security.Signature/getInstance "SHA256withRSA") - _ (.initSign sig private-key) - _ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8)) - signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig)) - + sig (java.security.Signature/getInstance "SHA256withRSA") + _ (.initSign sig private-key) + _ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8)) + signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig)) msg-with-sig (assoc msg "Signature" signature)] - ;; Test that signature verification logic would work - (t/is (string? string-to-sign)) - (t/is (string? signature)))) + (with-redefs [awsns/fetch-certificate (fn [_ _] + (java.io.ByteArrayInputStream. cert-bytes))] + (t/is (true? (#'awsns/verify-signature {} msg-with-sig)))))) + +(t/deftest test-verify-signature-end-to-end-subscription-confirmation + (let [{:keys [cert-bytes private-key]} (load-test-cert-and-key) + + msg {"Type" "SubscriptionConfirmation" + "MessageId" "test-msg-3" + "TopicArn" "arn:aws:sns:us-east-1:123:topic" + "Message" "You have chosen to subscribe" + "Timestamp" "2021-02-04T14:41:37.020Z" + "Token" "test-token-123" + "SubscribeURL" "https://sns.us-east-1.amazonaws.com/confirm" + "SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem" + "SignatureVersion" "1"} + + string-to-sign (#'awsns/build-string-to-sign msg) + sig (java.security.Signature/getInstance "SHA1withRSA") + _ (.initSign sig private-key) + _ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8)) + signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig)) + msg-with-sig (assoc msg "Signature" signature)] + + (with-redefs [awsns/fetch-certificate (fn [_ _] + (java.io.ByteArrayInputStream. cert-bytes))] + (t/is (true? (#'awsns/verify-signature {} msg-with-sig)))))) + +(t/deftest test-verify-signature-rejects-wrong-key + (let [{:keys [cert-bytes]} (load-test-cert-and-key) + ;; Generate a different keypair for signing + keypair-gen (java.security.KeyPairGenerator/getInstance "RSA") + _ (.initialize keypair-gen 2048) + kp (.generateKeyPair keypair-gen) + wrong-private-key (.getPrivate kp) + + msg {"Type" "Notification" + "MessageId" "test-msg-4" + "TopicArn" "arn:aws:sns:us-east-1:123:topic" + "Message" "test message" + "Timestamp" "2021-02-04T14:41:37.020Z" + "SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem" + "SignatureVersion" "1"} + + string-to-sign (#'awsns/build-string-to-sign msg) + sig (java.security.Signature/getInstance "SHA1withRSA") + _ (.initSign sig wrong-private-key) + _ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8)) + signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig)) + msg-with-sig (assoc msg "Signature" signature)] + + (with-redefs [awsns/fetch-certificate (fn [_ _] + (java.io.ByteArrayInputStream. cert-bytes))] + (t/is (false? (#'awsns/verify-signature {} msg-with-sig)))))) (t/deftest test-verify-signature-rejects-unsupported-version (let [msg {"Type" "Notification" @@ -379,11 +443,10 @@ "SignatureVersion" "3" "Signature" "fake=="}] - ;; Should throw exception for unsupported version (t/is (thrown? clojure.lang.ExceptionInfo (#'awsns/verify-signature {} msg))))) -(t/deftest test-build-string-to-sign-notification +(t/deftest test-build-string-to-sign-v1-notification (let [msg {"Type" "Notification" "MessageId" "msg-123" "TopicArn" "arn:aws:sns:eu-central-1:123:topic" @@ -399,8 +462,30 @@ (t/is (.contains result "TopicArn")) (t/is (.contains result "Message")) (t/is (.contains result "Timestamp")) + ;; V1 does NOT include SigningCertURL, SignatureVersion, or Signature + (t/is (not (.contains result "SigningCertURL"))) + (t/is (not (.contains result "SignatureVersion"))) + (t/is (not (.contains result "Signature"))))) + +(t/deftest test-build-string-to-sign-v2-notification + (let [msg {"Type" "Notification" + "MessageId" "msg-123" + "TopicArn" "arn:aws:sns:eu-central-1:123:topic" + "Message" "{\"notificationType\":\"Bounce\"}" + "Timestamp" "2021-02-04T14:41:37.020Z" + "SigningCertURL" "https://sns.eu-central-1.amazonaws.com/cert.pem" + "SignatureVersion" "2" + "Signature" "abc123=="} + result (#'awsns/build-string-to-sign msg)] + (t/is (string? result)) + (t/is (.contains result "MessageId")) + (t/is (.contains result "TopicArn")) + ;; V2 includes SigningCertURL and SignatureVersion but NOT Signature (t/is (.contains result "SigningCertURL")) - (t/is (.contains result "SignatureVersion")))) + (t/is (.contains result "SignatureVersion")) + ;; Check that "Signature\n" (the field name) is NOT in the result + ;; Note: "SignatureVersion" contains "Signature" as substring, so we check for the exact field pattern + (t/is (not (.contains result "Signature\n"))))) (t/deftest test-build-string-to-sign-subscription-confirmation (let [msg {"Type" "SubscriptionConfirmation" @@ -408,6 +493,7 @@ "TopicArn" "arn:aws:sns:eu-central-1:123:topic" "Message" "You have chosen to subscribe" "Timestamp" "2021-02-04T14:41:37.020Z" + "Token" "test-token-123" "SigningCertURL" "https://sns.eu-central-1.amazonaws.com/cert.pem" "SignatureVersion" "1" "Signature" "xyz789==" @@ -415,7 +501,10 @@ result (#'awsns/build-string-to-sign msg)] (t/is (string? result)) (t/is (.contains result "SubscribeURL")) - (t/is (.contains result "https://sns.eu-central-1.amazonaws.com/confirm")))) + (t/is (.contains result "https://sns.eu-central-1.amazonaws.com/confirm")) + ;; Token must be included for SubscriptionConfirmation + (t/is (.contains result "Token")) + (t/is (.contains result "test-token-123")))) (t/deftest test-handle-request-returns-4xx-for-invalid-signature (let [body (j/write-str diff --git a/backend/test/resources/sns-test-cert.pem b/backend/test/resources/sns-test-cert.pem new file mode 100644 index 0000000000..b21199ba33 --- /dev/null +++ b/backend/test/resources/sns-test-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGDCCAgCgAwIBAgIJAMp8qfcfOi+AMA0GCSqGSIb3DQEBDAUAMDoxCzAJBgNV +BAYTAlVTMQ0wCwYDVQQHEwRUZXN0MQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQDEwRU +ZXN0MB4XDTI2MDgwNTE1NTQxN1oXDTI3MDgwNTE1NTQxN1owOjELMAkGA1UEBhMC +VVMxDTALBgNVBAcTBFRlc3QxDTALBgNVBAoTBFRlc3QxDTALBgNVBAMTBFRlc3Qw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDj18ep0Pm5Y8VdXOSH9kXv +7RWzmxfNvN/zD1Fp2scADGBXnuVUhHYoR09Pe8U26X2gmDRg+JS7h6XXcGaWGUyh +28HISH3J0XrnKQmDKO77kxWFOAeu5apP2NVv1Nndbv+VqQczZqNZ6Kq9juflHjfK +hnnYiQqKZZ118IMCnu0uX26ShTwZJe7rAtO4MB4UNnfmAeRdrBOdI+1fp7QItYRc +2ppK3pXo2MAh8WhFeUy2UrlW6w1FpRjG/8nMmr14mD/dP9aWW85vcht5sA19bU3I +sf8ZOxOO9SGAk8Tg5Ey8aCdL5BU05OF8PNClOuqhHdGKShul3n2Pgojb2yGBHTYB +AgMBAAGjITAfMB0GA1UdDgQWBBTQbRwLjgbx/S7j1S/QFNB5HakDEjANBgkqhkiG +9w0BAQwFAAOCAQEAklxu68LkEdTFthA/ASyzm7KNxiWw+k+9IaEcViuwVa3XR2Ly +cO3L00NznsctwtcbevIT6ZlaK454snnpc5qZWHZSTYjQP34svT+XUZkgT29JdiYe +r5so+cUdorJyngMAen/tzpAHXV2l9SbRsi5+fRkR8DjPv9Ctz+kmh/Oy5fF3x/nu +AVZiwO8Oq4M3r+ElEfFed+9MkIQ6OZ3+ezgRhJYlXzVzii1OYQYpndocGNB+Jgwy +1FqzP1/02dzT6Oz95Md3B9fxj0OSCpCCBMo3ihkOKuNixaqyQ2+n/06Y+qpvnfuM +YrXnhbdpH7hsJM0/xNjWJMQlP04+zr1CQ9V9tg== +-----END CERTIFICATE----- diff --git a/backend/test/resources/sns-test-key.pem b/backend/test/resources/sns-test-key.pem new file mode 100644 index 0000000000..a6b0847ebd --- /dev/null +++ b/backend/test/resources/sns-test-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDj18ep0Pm5Y8Vd +XOSH9kXv7RWzmxfNvN/zD1Fp2scADGBXnuVUhHYoR09Pe8U26X2gmDRg+JS7h6XX +cGaWGUyh28HISH3J0XrnKQmDKO77kxWFOAeu5apP2NVv1Nndbv+VqQczZqNZ6Kq9 +juflHjfKhnnYiQqKZZ118IMCnu0uX26ShTwZJe7rAtO4MB4UNnfmAeRdrBOdI+1f +p7QItYRc2ppK3pXo2MAh8WhFeUy2UrlW6w1FpRjG/8nMmr14mD/dP9aWW85vcht5 +sA19bU3Isf8ZOxOO9SGAk8Tg5Ey8aCdL5BU05OF8PNClOuqhHdGKShul3n2Pgojb +2yGBHTYBAgMBAAECggEAIPku65wTL+nI+9iAOE8DUxQoIlyNJtixPl9WpG+lghPI +c5XK0Z7z7KNZToL2iRpkdHPijLAc8kDQ1uts5UcXCIuhsUcQcT8wPrj5J/KqF11z +bVqs/fo92h1i0jLnLr0sHvAd2yn89PuPjixa0hU79MLeamB21o2bKqDajOwMHjwq +bAFUED3rmQe0nLv1pJbCIUUqd4ZElx+/XV+Y0c329zfA0WzjJI2EkHCnsS4tQn50 +KX77Mzcbm9ujVQf2xsr5pnRscsCk2LrzXmAW4nOw5jpeWxxudvVTzARhpv46mj0j +MN0EN00YSw9FAHXBwDrjzS6cvvkchmBTJCUpKcoEWQKBgQDmO23YlsHrtnpWZRKf +BTu/uIObLHXgNt1yXby0mFk2BSe6L6yJDxalJQ1jxdyay73+LLBUdJwfg2K/jRdw +PFDdOvSRpb42LxTJ0TQxNaXsr91FDmbkjj0rFk1ygZKd/79HZq7xKWsfHsyTfUI7 +wgYz9QaapYYM1t1+diXlWxupJQKBgQD9V+T0EsuOTof8goKcBNw6C8zPnif8mKwy +qtFWVySWpcreq74Q9+qyhMzNKDQ2flb1Akc6J3CSSvZBR21IKuqk9Cgu3qMH5CrX +pe8smw8LpPaO5adnHvRgPTih7tJ0u7jUErWhmavkqroEq6DmvngmQ/6w9nhwTaC4 +tWfJzPvIrQKBgQDA19qEZpJ7y1bhcruMMyf+yKCDo1QAwDPwjY94fXuMAflqvG/6 +RYckQMrcXWkQx8OWWPxBYYM76iMWaynMutjI1Y7xSDDw1bLF8NOUvGkEvbHLG+sX +WgTmSEIKvXl/mi4vslSqb5TodjXI/Ew0HapwbrZfZnHH41mXiYLof83FeQKBgQDK +91T1ee1c6GuoEINFLdumIXgHyeStST+EJDgsXQpyKwd6F8vhWk3MkfpmTtRt6BAQ +oK+h1qEogygBKpFR5Rgx6W4cBsBEfTcZp9YTPXLzWEk0OKdCRZlxVPr/OQ+g+Bhe +x1J+0lfVjjYTsdDprCUkOwtciUn6ZybhdGxfT3tUzQKBgQC2BxPNlzPJvRkLvfMJ +fJXfM9g96Uy7to7a7+LcLB2+OEAt/5kQAMg6ishwwoNvxVMtdy5uLEYNpq74lnQ2 +bCVc7qc834edVMykhjlK5CLu0GiK2NM4aQavBNRGBoLF7I1Ih+tcToZFUC6W8YEU +djPrILOfRncjBj+epHXo92Z0iA== +-----END PRIVATE KEY-----