From 5380772715e027df5f79af8a0361e18e86e38f55 Mon Sep 17 00:00:00 2001 From: Geoff Brown Date: Tue, 14 Sep 2021 13:16:44 -0600 Subject: [PATCH] Add content signature to GMP response (#2179) * Add content signature to GMP response * Add content signature to GMP response * Allow distinct autograph url for gmp requests * Allow for per-product autograph servers --- src/auslib/web/public/client.py | 4 +++- src/auslib/web/public/helpers.py | 30 +++++++++++++++++++++++++++++- src/auslib/web/public/json.py | 15 ++------------- tests/web/test_client.py | 25 +++++++++++++++++++++++++ tests/web/test_json.py | 4 ++-- uwsgi/public.wsgi | 17 ++++++++++++++--- 6 files changed, 75 insertions(+), 20 deletions(-) diff --git a/src/auslib/web/public/client.py b/src/auslib/web/public/client.py index d15a983ae0..0117df5462 100644 --- a/src/auslib/web/public/client.py +++ b/src/auslib/web/public/client.py @@ -10,7 +10,7 @@ from auslib.blobs.base import createBlob from auslib.global_state import dbo from auslib.services import releases -from auslib.web.public.helpers import AUS, get_aus_metadata_headers, with_transaction +from auslib.web.public.helpers import AUS, get_aus_metadata_headers, get_content_signature_headers, with_transaction try: from urllib import unquote @@ -230,6 +230,8 @@ def get_update_blob(transaction, **url): response = make_response(xml) response.headers["Cache-Control"] = app.cacheControl response.headers.extend(get_aus_metadata_headers(eval_metadata)) + if query["product"] in app.config.get("CONTENT_SIGNATURE_PRODUCTS", []): + response.headers.extend(get_content_signature_headers(xml, query["product"])) response.mimetype = "text/xml" return response diff --git a/src/auslib/web/public/helpers.py b/src/auslib/web/public/helpers.py index eb6469033f..b4a420855f 100644 --- a/src/auslib/web/public/helpers.py +++ b/src/auslib/web/public/helpers.py @@ -1,7 +1,13 @@ +import logging from functools import wraps +from flask import current_app as app + from auslib.AUS import AUS -from auslib.global_state import dbo +from auslib.global_state import cache, dbo +from auslib.util.autograph import make_hash, sign_hash + +log = logging.getLogger(__name__) def with_transaction(f): @@ -21,4 +27,26 @@ def get_aus_metadata_headers(eval_metadata): return headers +def get_content_signature_headers(content, product): + headers = {} + if product: + product += "_" + if app.config.get("AUTOGRAPH_%sURL" % product): + hash_ = make_hash(content) + + def sign(): + return sign_hash( + app.config["AUTOGRAPH_%sURL" % product], + app.config["AUTOGRAPH_%sKEYID" % product], + app.config["AUTOGRAPH_%sUSERNAME" % product], + app.config["AUTOGRAPH_%sPASSWORD" % product], + hash_, + ) + + signature, x5u = cache.get("content_signatures", hash_, sign) + headers = {"Content-Signature": f"x5u={x5u}; p384ecdsa={signature}"} + log.debug("Added header: %s" % headers) + return headers + + AUS = AUS() diff --git a/src/auslib/web/public/json.py b/src/auslib/web/public/json.py index e6837a5488..d24ed63fd2 100644 --- a/src/auslib/web/public/json.py +++ b/src/auslib/web/public/json.py @@ -4,9 +4,7 @@ from flask import current_app as app from auslib.AUS import FORCE_FALLBACK_MAPPING, FORCE_MAIN_MAPPING -from auslib.global_state import cache -from auslib.util.autograph import make_hash, sign_hash -from auslib.web.public.helpers import AUS, get_aus_metadata_headers, with_transaction +from auslib.web.public.helpers import AUS, get_aus_metadata_headers, get_content_signature_headers, with_transaction @with_transaction @@ -21,15 +19,6 @@ def get_update(transaction, **parameters): response = json.dumps(release.getResponse(parameters, app.config["ALLOWLISTED_DOMAINS"])) - if app.config.get("AUTOGRAPH_URL"): - hash_ = make_hash(response) - - def sign(): - return sign_hash( - app.config["AUTOGRAPH_URL"], app.config["AUTOGRAPH_KEYID"], app.config["AUTOGRAPH_USERNAME"], app.config["AUTOGRAPH_PASSWORD"], hash_ - ) - - signature, x5u = cache.get("content_signatures", hash_, sign) - headers["Content-Signature"] = f"x5u={x5u}; p384ecdsa={signature}" + headers.update(get_content_signature_headers(response, "")) return Response(response=response, status=200, headers=headers, mimetype="application/json") diff --git a/tests/web/test_client.py b/tests/web/test_client.py index b656397abd..1627f6467c 100644 --- a/tests/web/test_client.py +++ b/tests/web/test_client.py @@ -128,6 +128,7 @@ def setup(self, insert_release, firefox_54_0_1_build1, firefox_56_0_build1, supe "ftp.mozilla.org": ("SystemAddons",), } app.config["VERSION_FILE"] = self.version_file + app.config["CONTENT_SIGNATURE_PRODUCTS"] = ["gmp"] with open(self.version_file, "w+") as f: f.write( """ @@ -895,6 +896,21 @@ def setup(self, insert_release, firefox_54_0_1_build1, firefox_56_0_build1, supe os.remove(self.version_file) +@pytest.fixture(scope="function") +def mock_autograph(monkeypatch): + monkeypatch.setitem(app.config, "AUTOGRAPH_gmp_URL", "fake") + monkeypatch.setitem(app.config, "AUTOGRAPH_gmp_KEYID", "fake") + monkeypatch.setitem(app.config, "AUTOGRAPH_gmp_USERNAME", "fake") + monkeypatch.setitem(app.config, "AUTOGRAPH_gmp_PASSWORD", "fake") + + def mockreturn(*args): + return ("abcdef", "https://this.is/a.x5u") + + import auslib.web.public.helpers + + monkeypatch.setattr(auslib.web.public.helpers, "sign_hash", mockreturn) + + class ClientTest(ClientTestBase): def testGetHeaderArchitectureWindows(self): self.assertEqual(client_api.getHeaderArchitecture("WINNT_x86-msvc", "Firefox Intel Windows"), "Intel") @@ -1270,6 +1286,15 @@ def testDeprecatedEsrVersionStyleGetsUpdates(self): """, ) + def testGMPResponseWithoutSigning(self): + ret = self.client.get("/update/4/gmp/1.0/1/p/l/a/a/a/a/1/update.xml") + assert "Content-Signature" not in ret.headers + + @pytest.mark.usefixtures("mock_autograph") + def testGMPResponseWithSigning(self): + ret = self.client.get("/update/4/gmp/1.0/1/p/l/a/a/a/a/1/update.xml") + assert ret.headers["Content-Signature"] == "x5u=https://this.is/a.x5u; p384ecdsa=abcdef" + def testGetWithResponseProducts(self): ret = self.client.get("/update/4/gmp/1.0/1/p/l/a/a/a/a/1/update.xml") self.assertUpdateEqual( diff --git a/tests/web/test_json.py b/tests/web/test_json.py index e09989a2d9..40c9cd620a 100644 --- a/tests/web/test_json.py +++ b/tests/web/test_json.py @@ -21,9 +21,9 @@ def mock_autograph(monkeypatch): def mockreturn(*args): return ("abcdef", "https://this.is/a.x5u") - import auslib.web.public.json + import auslib.web.public.helpers - monkeypatch.setattr(auslib.web.public.json, "sign_hash", mockreturn) + monkeypatch.setattr(auslib.web.public.helpers, "sign_hash", mockreturn) @pytest.fixture(scope="module") diff --git a/uwsgi/public.wsgi b/uwsgi/public.wsgi index 12c58a8e2d..8eac0fd117 100644 --- a/uwsgi/public.wsgi +++ b/uwsgi/public.wsgi @@ -50,10 +50,20 @@ if os.environ.get("AUTOGRAPH_URL"): application.config["AUTOGRAPH_PASSWORD"] = os.environ["AUTOGRAPH_PASSWORD"] # Autograph responses - # When we start signing things other than Guardian responses we'll need to increase the size of this cache. + # If additional types of responses require signing, consider increasing the size of this cache. # We cache for one day to make sure we resign once per day, because the signatures eventually expire. - cache.make_cache("content_signatures", 50, 86400) - + cache.make_cache("content_signatures", 200, 86400) + +if os.environ.get("AUTOGRAPH_GMP_URL"): + application.config["AUTOGRAPH_GMP_URL"] = os.environ["AUTOGRAPH_GMP_URL"] + application.config["AUTOGRAPH_GMP_KEYID"] = os.environ["AUTOGRAPH_GMP_KEYID"] + application.config["AUTOGRAPH_GMP_USERNAME"] = os.environ["AUTOGRAPH_GMP_USERNAME"] + application.config["AUTOGRAPH_GMP_PASSWORD"] = os.environ["AUTOGRAPH_GMP_PASSWORD"] +elif "AUTOGRAPH_URL" in application.config: + application.config["AUTOGRAPH_GMP_URL"] = application.config["AUTOGRAPH_URL"] + application.config["AUTOGRAPH_GMP_KEYID"] = application.config["AUTOGRAPH_KEYID"] + application.config["AUTOGRAPH_GMP_USERNAME"] = application.config["AUTOGRAPH_USERNAME"] + application.config["AUTOGRAPH_GMP_PASSWORD"] = application.config["AUTOGRAPH_PASSWORD"] cache.make_cache("blob", 500, 3600) cache.make_cache("releases", 500, 3600) @@ -83,6 +93,7 @@ application.config["SPECIAL_FORCE_HOSTS"] = SPECIAL_FORCE_HOSTS # about the current code (version number, commit hash), but doesn't exist in # the repo itself application.config["VERSION_FILE"] = "/app/version.json" +application.config["CONTENT_SIGNATURE_PRODUCTS"] = ["GMP"] if os.environ.get("SENTRY_DSN"): sentry_sdk.init(os.environ["SENTRY_DSN"], integrations=[FlaskIntegration(), LoggingIntegration()])