def test_invalid_dates(self, mocker, settings): settings.CERTIFICATES_CHECK_VALIDITY = True settings.CERTIFICATES_EXPECTED_ROOT_HASH = None settings.CERTIFICATES_EXPECTED_SUBJECT_CN = None mock_requests = mocker.patch("normandy.recipes.signing.requests") mock_extract_certs_from_pem = mocker.patch( "normandy.recipes.signing.extract_certs_from_pem") mock_parse_cert_from_der = mocker.patch( "normandy.recipes.signing.parse_cert_from_der") url = "https://example.com/cert.pem" now = datetime.now().replace(tzinfo=pytz.UTC) not_before = now - timedelta(days=2) not_after = now - timedelta(days=1) mock_extract_certs_from_pem.return_value = ["a"] mock_parse_cert_from_der.return_value = self._fake_cert( not_before=not_before, not_after=not_after) with pytest.raises(signing.CertificateExpired): signing.verify_x5u(url) assert mock_requests.get.called_once_with(url) body = mock_requests.get.return_value.content.decode.return_value assert mock_extract_certs_from_pem.called_once_with(body) assert mock_parse_cert_from_der.called_once_with( mock_extract_certs_from_pem.return_value[0])
def test_invalid_dates(self, mocker): mock_requests = mocker.patch("normandy.recipes.signing.requests") mock_parse_pem_to_certs = mocker.patch("normandy.recipes.signing.parse_pem_to_certs") date_format = "%y%m%d%H%M%SZ" url = "https://example.com/cert.pem" now = datetime.now() not_before = (now - timedelta(days=2)).strftime(date_format).encode() not_after = (now - timedelta(days=1)).strftime(date_format).encode() mock_parse_pem_to_certs.return_value = [ { "tbsCertificate": { "validity": { "notBefore": {"utcTime": not_before}, "notAfter": {"utcTime": not_after}, } } } ] with pytest.raises(signing.CertificateExpired): signing.verify_x5u(url) assert mock_requests.get.called_once_with(url) body = mock_requests.get.return_value.content.decode.return_value assert mock_parse_pem_to_certs.called_once_with(body)
def test_invalid_dates(self, mocker): mock_requests = mocker.patch('normandy.recipes.signing.requests') mock_parse_pem_to_certs = mocker.patch( 'normandy.recipes.signing.parse_pem_to_certs') date_format = '%y%m%d%H%M%SZ' url = 'https://example.com/cert.pem' now = datetime.now() not_before = (now - timedelta(days=2)).strftime(date_format).encode() not_after = (now - timedelta(days=1)).strftime(date_format).encode() mock_parse_pem_to_certs.return_value = [{ 'tbsCertificate': { 'validity': { 'notBefore': { 'utcTime': not_before }, 'notAfter': { 'utcTime': not_after }, } } }] with pytest.raises(signing.CertificateExpired): signing.verify_x5u(url) assert mock_requests.get.called_once_with(url) body = mock_requests.get.return_value.content.decode.return_value assert mock_parse_pem_to_certs.called_once_with(body)
def test_bad_unexpected_format(self, mocker): mocker.patch("normandy.recipes.signing.requests") mock_parse_pem_to_certs = mocker.patch("normandy.recipes.signing.parse_pem_to_certs") date_format = "%Y%m%d%H%M%S" url = "https://example.com/cert.pem" now = datetime.now() not_before = (now - timedelta(days=2)).strftime(date_format).encode() not_after = (now - timedelta(days=1)).strftime(date_format).encode() mock_parse_pem_to_certs.return_value = [ { "tbsCertificate": { "validity": { "notBefore": {"unsupportedTimestamp": not_before}, "notAfter": {"unsupportedTimestamp": not_after}, } } } ] with pytest.raises(signing.BadCertificate) as exc: signing.verify_x5u(url) assert "Timestamp not in expected format" in str(exc) assert "unsupportedTimestamp" in str(exc)
def signatures_use_good_certificates(app_configs, **kwargs): errors = [] expire_early = None if settings.CERTIFICATES_EXPIRE_EARLY_DAYS: expire_early = timedelta(days=settings.CERTIFICATES_EXPIRE_EARLY_DAYS) try: Signature = apps.get_model('recipes', 'Signature') Recipe = apps.get_model('recipes', 'Recipe') Action = apps.get_model('recipes', 'Action') signatures = list(Signature.objects.all()) except (ProgrammingError, OperationalError, ImproperlyConfigured) as e: msg = f'Could not retrieve signatures: {e}' errors.append(Info(msg, id=INFO_COULD_NOT_RETRIEVE_SIGNATURES)) else: urls = set(s.x5u for s in signatures) for url in urls: try: signing.verify_x5u(url, expire_early) except signing.BadCertificate as exc: matching_recipes = Recipe.objects.filter(signature__x5u=url) matching_actions = Action.objects.filter(signature__x5u=url) bad_objects = list(matching_recipes) + list(matching_actions) object_names = ', '.join(bad_objects) msg = ( f'{len(bad_objects)} objects are signed with a bad cert: {object_names}. ' f'{exc.detail}. Certificate url is {url}. ') errors.append(Warning(msg, id=WARNING_BAD_SIGNING_CERTIFICATE)) return errors
def test_bad_unexpected_format(self, mocker): mocker.patch("normandy.recipes.signing.requests") mock_parse_pem_to_certs = mocker.patch( "normandy.recipes.signing.parse_pem_to_certs") date_format = "%Y%m%d%H%M%S" url = "https://example.com/cert.pem" now = datetime.now() not_before = (now - timedelta(days=2)).strftime(date_format).encode() not_after = (now - timedelta(days=1)).strftime(date_format).encode() mock_parse_pem_to_certs.return_value = [{ "tbsCertificate": { "validity": { "notBefore": { "unsupportedTimestamp": not_before }, "notAfter": { "unsupportedTimestamp": not_after }, } } }] with pytest.raises(signing.BadCertificate) as exc: signing.verify_x5u(url) assert "Timestamp not in expected format" in str(exc) assert "unsupportedTimestamp" in str(exc)
def test_bad_unexpected_format(self, mocker): mocker.patch('normandy.recipes.signing.requests') mock_parse_pem_to_certs = mocker.patch( 'normandy.recipes.signing.parse_pem_to_certs') date_format = '%Y%m%d%H%M%S' url = 'https://example.com/cert.pem' now = datetime.now() not_before = (now - timedelta(days=2)).strftime(date_format).encode() not_after = (now - timedelta(days=1)).strftime(date_format).encode() mock_parse_pem_to_certs.return_value = [{ 'tbsCertificate': { 'validity': { 'notBefore': { 'unsupportedTimestamp': not_before }, 'notAfter': { 'unsupportedTimestamp': not_after }, } } }] with pytest.raises(signing.BadCertificate) as exc: signing.verify_x5u(url) assert 'Timestamp not in expected format' in str(exc) assert 'unsupportedTimestamp' in str(exc)
def recipe_signatures_use_good_certificates(app_configs, **kwargs): errors = [] expire_early = None if settings.CERTIFICATES_EXPIRE_EARLY_DAYS: expire_early = timedelta(days=settings.CERTIFICATES_EXPIRE_EARLY_DAYS) try: Recipe = apps.get_model('recipes', 'Recipe') signed_recipes = list(Recipe.objects.exclude(signature=None)) except (ProgrammingError, OperationalError, ImproperlyConfigured): errors.append(Info('Could not retrieve recipes', id=INFO_COULD_NOT_RETRIEVE_RECIPES)) else: urls = set(r.signature.x5u for r in signed_recipes) for url in urls: try: signing.verify_x5u(url, expire_early) except signing.BadCertificate as exc: matching_recipes = Recipe.objects.filter(signature__x5u=url) count = matching_recipes.count() ids = ', '.join(str(r.id) for r in matching_recipes) msg = (f'{count} recipes (ids {ids}) are signed with a bad cert. {exc.detail}. ' f'Certificate url is {url}') errors.append(Warning(msg, id=WARNING_BAD_SIGNING_CERTIFICATE)) return errors
def test_it_checks_cert_subject(self, mocker, settings): path = os.path.join(os.path.dirname(__file__), "data", "test_certs.pem") with open(path) as f: cert_pem = f.read() settings.CERTIFICATES_CHECK_VALIDITY = False settings.CERTIFICATES_EXPECTED_ROOT_HASH = None settings.CERTIFICATES_EXPECTED_SUBJECT_CN = "wrong.subject.example.com" mock_requests = mocker.patch("normandy.recipes.signing.requests") mock_requests.get.return_value.content.decode.return_value = cert_pem with pytest.raises(signing.CertificateHasWrongSubject): signing.verify_x5u("https://example.com/cert.pem")
def test_it_works(self, mocker, settings): settings.CERTIFICATES_CHECK_VALIDITY = True settings.CERTIFICATES_EXPECTED_ROOT_HASH = None settings.CERTIFICATES_EXPECTED_SUBJECT_CN = None mock_requests = mocker.patch("normandy.recipes.signing.requests") mock_extract_certs_from_pem = mocker.patch( "normandy.recipes.signing.extract_certs_from_pem") mock_parse_cert_from_der = mocker.patch( "normandy.recipes.signing.parse_cert_from_der") url = "https://example.com/cert.pem" now = datetime.now() not_before = now - timedelta(days=1) not_after = now + timedelta(days=1) mock_extract_certs_from_pem.return_value = ["a", "b"] mock_parse_cert_from_der.return_value = self._fake_cert( not_before=not_before, not_after=not_after) assert signing.verify_x5u(url) assert mock_requests.get.called_once_with(url) body = mock_requests.get.return_value.content.decode.return_value assert mock_extract_certs_from_pem.called_once_with(body) assert mock_parse_cert_from_der.called_twice()
def test_mixed_timestamp_format(self, mocker): # The certificate used for testing expired on 2018-04-24. This test is # only concerned with the parsing of the dates, so mock the call to the # validate function and assert about the values of the dates. mock_requests = mocker.patch("normandy.recipes.signing.requests") mock_check_validity = mocker.patch( "normandy.recipes.signing.check_validity") path = os.path.join(os.path.dirname(__file__), "data", "mixed_timestamps_certs.pem") with open(path, "rb") as f: mock_requests.get.return_value.content = f.read() assert signing.verify_x5u("https://example.com/cert.pem") assert mock_check_validity.mock_calls == [ call( datetime(2017, 12, 25, tzinfo=pytz.UTC), datetime(2018, 4, 24, tzinfo=pytz.UTC), None, ), call( datetime(2017, 5, 4, 0, 12, 39, tzinfo=pytz.UTC), datetime(2019, 5, 4, 0, 12, 39, tzinfo=pytz.UTC), None, ), call( datetime(2015, 3, 17, 22, 53, 57, tzinfo=pytz.UTC), datetime(2025, 3, 14, 22, 53, 57, tzinfo=pytz.UTC), None, ), ]
def test_mixed_timestamp_format(self, mocker): mock_requests = mocker.patch('normandy.recipes.signing.requests') path = os.path.join(os.path.dirname(__file__), 'data', 'mixed_timestamps_certs.pem') with open(path, 'rb') as f: mock_requests.get.return_value.content = f.read() assert signing.verify_x5u('https://example.com/cert.pem')
def signatures_use_good_certificates(app_configs, **kwargs): errors = [] expire_early = None if settings.CERTIFICATES_EXPIRE_EARLY_DAYS: expire_early = timedelta(days=settings.CERTIFICATES_EXPIRE_EARLY_DAYS) try: Recipe = apps.get_model("recipes", "Recipe") Action = apps.get_model("recipes", "Action") urls = set() for recipe in Recipe.objects.exclude(signature=None): urls.add(recipe.signature.x5u) for action in Action.objects.exclude(signature=None): urls.add(action.signature.x5u) except (ProgrammingError, OperationalError, ImproperlyConfigured) as e: msg = f"Could not retrieve signatures: {e}" errors.append(Info(msg, id=INFO_COULD_NOT_RETRIEVE_SIGNATURES)) else: def get_matching_object_names(url): matching_recipes = Recipe.objects.filter(signature__x5u=url) matching_actions = Action.objects.filter(signature__x5u=url) matching_objects = list(matching_recipes) + list(matching_actions) object_names = [str(o) for o in matching_objects] return object_names for url in urls: try: signing.verify_x5u(url, expire_early) except signing.BadCertificate as exc: bad_object_names = get_matching_object_names(url) msg = ( f"{len(bad_object_names)} objects are signed with a bad cert: " f"{bad_object_names}. {exc.detail}. Certificate url is {url}. " ) errors.append(Error(msg, id=ERROR_BAD_SIGNING_CERTIFICATE)) except requests.RequestException as exc: bad_object_names = get_matching_object_names(url) msg = ( f"The certificate at {url} could not be fetched due to a network error to " f"verify. {len(bad_object_names)} objects are signed with this certificate: " f"{bad_object_names}. {exc}") errors.append(Error(msg, id=ERROR_COULD_NOT_VERIFY_CERTIFICATE)) return errors
def test_recipe_signatures(conf, requests_session): r = requests_session.get(conf.getoption("server") + "/api/v1/recipe/signed/") r.raise_for_status() data = r.json() if len(data) == 0: pytest.skip("No signed recipes") cert_urls = set() for item in data: canonical_recipe = canonical_json(item["recipe"]) signature = item["signature"]["signature"] pubkey = item["signature"]["public_key"] cert_urls.add(item["signature"]["x5u"]) assert signing.verify_signature(canonical_recipe, signature, pubkey) for url in cert_urls: signing.verify_x5u(url)
def test_action_signatures(conf, requests_session): r = requests_session.get(conf.getoption("server") + "/api/v1/action/signed/") r.raise_for_status() data = r.json() if len(data) == 0: pytest.skip("No signed actions") cert_urls = set() for item in data: canonical_action = canonical_json(item["action"]) signature = item["signature"]["signature"] pubkey = item["signature"]["public_key"] cert_urls.add(item["signature"]["x5u"]) assert signing.verify_signature(canonical_action, signature, pubkey) for url in cert_urls: signing.verify_x5u(url)
def signatures_use_good_certificates(app_configs, **kwargs): errors = [] expire_early = None if settings.CERTIFICATES_EXPIRE_EARLY_DAYS: expire_early = timedelta(days=settings.CERTIFICATES_EXPIRE_EARLY_DAYS) try: Recipe = apps.get_model("recipes", "Recipe") Action = apps.get_model("recipes", "Action") x5u_urls = defaultdict(list) for recipe in Recipe.objects.exclude( signature__x5u=None).select_related("signature"): x5u_urls[recipe.signature.x5u].append(str(recipe)) for action in Action.objects.exclude( signature__x5u=None).select_related("signature"): x5u_urls[action.signature.x5u].append(str(action)) except (ProgrammingError, OperationalError, ImproperlyConfigured) as e: msg = f"Could not retrieve signatures: {e}" errors.append(Info(msg, id=INFO_COULD_NOT_RETRIEVE_SIGNATURES)) else: for url in x5u_urls: try: signing.verify_x5u(url, expire_early) except signing.BadCertificate as exc: bad_object_names = x5u_urls[url] msg = ( f"{len(bad_object_names)} objects are signed with a bad cert: " f"{bad_object_names}. {exc.detail}. Certificate url is {url}. " ) errors.append(Error(msg, id=ERROR_BAD_SIGNING_CERTIFICATE)) except requests.RequestException as exc: bad_object_names = x5u_urls[url] msg = ( f"The certificate at {url} could not be fetched due to a network error to " f"verify. {len(bad_object_names)} objects are signed with this certificate: " f"{bad_object_names}. {exc}") errors.append(Error(msg, id=ERROR_COULD_NOT_VERIFY_CERTIFICATE)) return errors
def test_recipe_signatures(conf, requests_session): r = requests_session.get( conf.getoption('server') + '/api/v1/recipe/signed/') r.raise_for_status() data = r.json() if len(data) == 0: pytest.skip('No signed recipes') cert_urls = set() for item in data: canonical_recipe = canonical_json(item['recipe']) signature = item['signature']['signature'] pubkey = item['signature']['public_key'] cert_urls.add(item['signature']['x5u']) assert signing.verify_signature(canonical_recipe, signature, pubkey) for url in cert_urls: signing.verify_x5u(url)
def signatures_use_good_certificates(app_configs, **kwargs): errors = [] expire_early = None if settings.CERTIFICATES_EXPIRE_EARLY_DAYS: expire_early = timedelta(days=settings.CERTIFICATES_EXPIRE_EARLY_DAYS) try: Recipe = apps.get_model("recipes", "Recipe") Action = apps.get_model("recipes", "Action") x5u_urls = defaultdict(list) for recipe in Recipe.objects.exclude(signature__x5u=None).select_related("signature"): x5u_urls[recipe.signature.x5u].append(str(recipe)) for action in Action.objects.exclude(signature__x5u=None).select_related("signature"): x5u_urls[action.signature.x5u].append(str(action)) except (ProgrammingError, OperationalError, ImproperlyConfigured) as e: msg = f"Could not retrieve signatures: {e}" errors.append(Info(msg, id=INFO_COULD_NOT_RETRIEVE_SIGNATURES)) else: for url in x5u_urls: try: signing.verify_x5u(url, expire_early) except signing.BadCertificate as exc: bad_object_names = x5u_urls[url] msg = ( f"{len(bad_object_names)} objects are signed with a bad cert: " f"{bad_object_names}. {exc.detail}. Certificate url is {url}. " ) errors.append(Error(msg, id=ERROR_BAD_SIGNING_CERTIFICATE)) except requests.RequestException as exc: bad_object_names = x5u_urls[url] msg = ( f"The certificate at {url} could not be fetched due to a network error to " f"verify. {len(bad_object_names)} objects are signed with this certificate: " f"{bad_object_names}. {exc}" ) errors.append(Error(msg, id=ERROR_COULD_NOT_VERIFY_CERTIFICATE)) return errors
def test_mixed_timestamp_format(self, mocker): # The certificate used for testing expired on 2018-04-24. This test is # only concerned with the parsing of the dates, so mock the call to the # validate function and assert about the value also s of the dates. mock_requests = mocker.patch("normandy.recipes.signing.requests") mock_check_validity = mocker.patch("normandy.recipes.signing.check_validity") path = os.path.join(os.path.dirname(__file__), "data", "mixed_timestamps_certs.pem") with open(path, "rb") as f: mock_requests.get.return_value.content = f.read() assert signing.verify_x5u("https://example.com/cert.pem") assert mock_check_validity.mock_calls == [ call(datetime(2017, 12, 25), datetime(2018, 4, 24), None), call(datetime(2017, 5, 4, 0, 12, 39), datetime(2019, 5, 4, 0, 12, 39), None), call(datetime(2015, 3, 17, 22, 53, 57), datetime(2025, 3, 14, 22, 53, 57), None), ]
def test_it_works(self, mocker): mock_requests = mocker.patch("normandy.recipes.signing.requests") mock_parse_pem_to_certs = mocker.patch( "normandy.recipes.signing.parse_pem_to_certs") date_format = "%y%m%d%H%M%SZ" url = "https://example.com/cert.pem" now = datetime.now() not_before = (now - timedelta(days=1)).strftime(date_format).encode() not_after = (now + timedelta(days=1)).strftime(date_format).encode() mock_parse_pem_to_certs.return_value = [ { "tbsCertificate": { "validity": { "notBefore": { "utcTime": not_before }, "notAfter": { "utcTime": not_after }, } } }, { "tbsCertificate": { "validity": { "notBefore": { "utcTime": not_before }, "notAfter": { "utcTime": not_after }, } } }, ] assert signing.verify_x5u(url) assert mock_requests.get.called_once_with(url) body = mock_requests.get.return_value.content.decode.return_value assert mock_parse_pem_to_certs.called_once_with(body)