async def test_verify_name_constraints_raises(aiohttp_session, mock_with_x5u, cache, now_fixed): certs = [ cryptography.x509.load_pem_x509_certificate(pem, backend=default_backend()) for pem in STAGE_CERT_LIST ] # Intermediate cert has the name constraint. intermediate = certs[1] # Change name of leaf cert. mock_leaf = mock_cert(certs[0]) fake_name = mock.Mock() fake_name.value = "bazinga.allizom.org" mock_leaf.subject = mock.Mock() mock_leaf.subject.get_attributes_for_oid.return_value = [fake_name] certs[0] = mock_leaf with mock.patch( "cryptography.x509.load_pem_x509_certificate") as load_cert_mock: load_cert_mock.side_effect = lambda *args, **kwargs: certs.pop(0) s = SignatureVerifier(aiohttp_session, cache, STAGE_ROOT_HASH) with pytest.raises( autograph_utils.CertificateChainNameNotPermitted) as excinfo: await s.verify_x5u(FAKE_CERT_URL) assert " does not match the permitted names " in excinfo.value.detail assert excinfo.value.current == intermediate assert excinfo.value.next == mock_leaf
async def test_verify_name_constraints_excludes(aiohttp_session, mock_with_x5u, cache, now_fixed): certs = [ cryptography.x509.load_pem_x509_certificate(pem, backend=default_backend()) for pem in STAGE_CERT_LIST ] # Intermediate cert has the name constraint. real_intermediate = certs[1] real_constraints = real_intermediate.extensions.get_extension_for_class( cryptography.x509.NameConstraints).value # Reverse meaning of constraints. reversed = mock.Mock() reversed.permitted_subtrees = real_constraints.excluded_subtrees reversed.excluded_subtrees = real_constraints.permitted_subtrees intermediate = mock_cert(real_intermediate) mock_cert_extension(intermediate, cryptography.x509.NameConstraints, reversed) certs[1] = intermediate leaf = certs[0] with mock.patch( "cryptography.x509.load_pem_x509_certificate") as load_cert_mock: load_cert_mock.side_effect = lambda *args, **kwargs: certs.pop(0) s = SignatureVerifier(aiohttp_session, cache, STAGE_ROOT_HASH) with pytest.raises( autograph_utils.CertificateChainNameExcluded) as excinfo: await s.verify_x5u(FAKE_CERT_URL) assert " matches the excluded names " in excinfo.value.detail assert excinfo.value.current == intermediate assert excinfo.value.next == leaf
async def test_verify_basic_constraints_must_have_cert_signing( aiohttp_session, mock_with_x5u, cache, now_fixed): certs = [ cryptography.x509.load_pem_x509_certificate(pem, backend=default_backend()) for pem in STAGE_CERT_LIST ] real_intermediate = certs[1] intermediate = mock_cert(real_intermediate) uses_mock = mock.Mock() uses_mock.key_cert_sign = False mock_cert_extension(intermediate, cryptography.x509.KeyUsage, uses_mock) certs[1] = intermediate with mock.patch( "cryptography.x509.load_pem_x509_certificate") as load_cert_mock: load_cert_mock.side_effect = lambda *args, **kwargs: certs.pop(0) s = SignatureVerifier(aiohttp_session, cache, STAGE_ROOT_HASH) with pytest.raises(autograph_utils.CertificateCannotSign) as excinfo: await s.verify_x5u(FAKE_CERT_URL) assert excinfo.value.detail.startswith( "Certificate cannot be used for signing because ") assert excinfo.value.cert == intermediate assert excinfo.value.extra == "key usage is incomplete"
async def test_verify_leaf_code_signing(aiohttp_session, mock_with_x5u, cache, now_fixed): certs = [ cryptography.x509.load_pem_x509_certificate(pem, backend=default_backend()) for pem in CERT_LIST ] # Change extended_key_usage for leaf cert real_leaf = certs[0] mock_leaf = mock_cert(real_leaf) fake_uses = [ cryptography.x509.oid.ExtendedKeyUsageOID.CODE_SIGNING, cryptography.x509.oid.ExtendedKeyUsageOID.TIME_STAMPING, ] mock_cert_extension(mock_leaf, cryptography.x509.ExtendedKeyUsage, fake_uses) certs[0] = mock_leaf with mock.patch( "cryptography.x509.load_pem_x509_certificate") as load_cert_mock: load_cert_mock.side_effect = lambda *args, **kwargs: certs.pop(0) s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) with pytest.raises( autograph_utils.CertificateLeafHasWrongKeyUsage) as excinfo: await s.verify_x5u(FAKE_CERT_URL) assert excinfo.value.detail.startswith( f"Leaf certificate {mock_leaf!r} should have extended key usage of just " "Code Signing. ") assert excinfo.value.cert == mock_leaf assert excinfo.value.key_usage == fake_uses
async def run(server: str, collection: str, root_hash: str) -> CheckResult: """Fetch recipes from Remote Settings and verify that each attached signature is verified with the related recipe attributes. :param server: URL of Remote Settings server. :param collection: Collection id to obtain recipes from (eg. ``"normandy-recipes"``. :param root_hash: The expected hash for the first certificate in a chain. """ resp = await fetch_json( RECIPES_URL.format(server=server, collection=collection)) recipes = resp["data"] cache = MemoryCache() errors = {} async with ClientSession() as session: verifier = SignatureVerifier(session, cache, decode_mozilla_hash(root_hash)) for recipe in recipes: try: await validate_signature(verifier, recipe) except Exception as e: errors[recipe["id"]] = repr(e) return len(errors) == 0, errors
async def test_verify_x5u_expired(aiohttp_session, mock_with_x5u, cache, now_fixed): now_fixed.return_value = datetime.datetime(2022, 10, 23, 16, 16, 16) s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) with pytest.raises(autograph_utils.CertificateExpired) as excinfo: await s.verify(SIGNED_DATA, SAMPLE_SIGNATURE, FAKE_CERT_URL) assert excinfo.value.detail == "Certificate expired on 2021-07-05 21:57:15"
async def verify_signature(records, timestamp, signature): x5u = signature["x5u"].replace("file:///tmp/autograph/", "http://certchains/") serialized = canonical_json(records, timestamp).encode("utf-8") async with aiohttp.ClientSession() as session: verifier = SignatureVerifier(session, MemoryCache(), FakeRootHash()) await verifier.verify(serialized, signature["signature"], x5u)
async def test_verify_x5u_too_soon(aiohttp_session, mock_with_x5u, cache, now_fixed): now_fixed.return_value = datetime.datetime(2010, 10, 23, 16, 16, 16) s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) with pytest.raises(autograph_utils.CertificateNotYetValid) as excinfo: await s.verify(SIGNED_DATA, SAMPLE_SIGNATURE, FAKE_CERT_URL) assert excinfo.value.detail == "Certificate is not valid until 2016-07-06 21:57:15"
async def test_verify_x5u_name_exact_match(aiohttp_session, mock_with_x5u, cache, now_fixed): s = SignatureVerifier( aiohttp_session, cache, DEV_ROOT_HASH, subject_name_check=ExactMatch( "normandy.content-signature.mozilla.org"), ) await s.verify(SIGNED_DATA, SAMPLE_SIGNATURE, FAKE_CERT_URL)
async def test_verify_x5u_caches_success(aiohttp_session, mock_with_x5u, cache, now_fixed): with mock.patch.object(cache, "set") as set_mock: s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) await s.verify_x5u(FAKE_CERT_URL) assert len(set_mock.call_args_list) == 1 args, kwargs = set_mock.call_args_list[0] assert args[0] == FAKE_CERT_URL assert isinstance(args[1], cryptography.x509.Certificate) assert kwargs == {}
async def run(server: str, buckets: List[str], root_hash: str) -> CheckResult: client = KintoClient(server_url=server) entries = [ entry for entry in await client.get_monitor_changes() if entry["bucket"] in buckets ] # Fetch collections records in parallel. futures = [ client.get_changeset(entry["bucket"], entry["collection"], _expected=entry["last_modified"]) for entry in entries ] start_time = time.time() results = await run_parallel(*futures) elapsed_time = time.time() - start_time logger.info(f"Downloaded all data in {elapsed_time:.2f}s") cache = MemoryCache() async with ClientSession() as session: verifier = SignatureVerifier(session, cache, decode_mozilla_hash(root_hash)) # Validate signatures sequentially. errors = {} for i, (entry, changeset) in enumerate(zip(entries, results)): cid = "{bucket}/{collection}".format(**entry) message = "{:02d}/{:02d} {}: ".format(i + 1, len(entries), cid) try: start_time = time.time() await validate_signature( verifier, changeset["metadata"], changeset["changes"], changeset["timestamp"], ) elapsed_time = time.time() - start_time message += f"OK ({elapsed_time:.2f}s)" logger.info(message) except (BadSignature, BadCertificate) as e: message += "⚠ Signature Error ⚠ " + repr(e) logger.error(message) errors[cid] = repr(e) return len(errors) == 0, errors
async def test_verify_broken_chain(aiohttp_session, mock_aioresponses, cache, now_fixed): # Drop next-to-last cert in cert list broken_chain = CERT_LIST[:1] + CERT_LIST[2:] mock_aioresponses.get(FAKE_CERT_URL, status=200, body=b"\n".join(broken_chain)) s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) with pytest.raises(autograph_utils.CertificateChainBroken) as excinfo: await s.verify_x5u(FAKE_CERT_URL) assert excinfo.value.detail.startswith( "Certificate chain is not continuous. ") assert excinfo.value.previous_cert == cryptography.x509.load_pem_x509_certificate( CERT_LIST[2], backend=default_backend()) assert excinfo.value.next_cert == cryptography.x509.load_pem_x509_certificate( CERT_LIST[0], backend=default_backend())
async def test_verify_x5u_screwy_dates(aiohttp_session, mock_with_x5u, cache, now_fixed): now_fixed.return_value = datetime.datetime(2010, 10, 23, 16, 16, 16) s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) leaf_cert = cryptography.x509.load_pem_x509_certificate( CERT_LIST[0], backend=default_backend()) bad_cert = mock.Mock(spec=leaf_cert) bad_cert.not_valid_before = leaf_cert.not_valid_after bad_cert.not_valid_after = leaf_cert.not_valid_before with mock.patch("autograph_utils.x509.load_pem_x509_certificate") as x509: x509.return_value = bad_cert with pytest.raises(autograph_utils.BadCertificate) as excinfo: await s.verify(SIGNED_DATA, SAMPLE_SIGNATURE, FAKE_CERT_URL) assert excinfo.value.detail == ( "Bad certificate: not_before (2021-07-05 21:57:15) " "after not_after (2016-07-06 21:57:15)")
async def test_verify_x5u_name_exact_doesnt_match(aiohttp_session, mock_with_x5u, cache, now_fixed): s = SignatureVerifier( aiohttp_session, cache, DEV_ROOT_HASH, subject_name_check=ExactMatch( "remote-settings.content-signature.mozilla.org"), ) with pytest.raises(autograph_utils.CertificateHasWrongSubject) as excinfo: await s.verify(SIGNED_DATA, SAMPLE_SIGNATURE, FAKE_CERT_URL) assert excinfo.value.detail == ( "Certificate does not have the expected subject. " "Got 'normandy.content-signature.mozilla.org', " "checking for matches exactly 'remote-settings.content-signature.mozilla.org'" )
async def test_verify_wrong_root_hash(aiohttp_session, mock_with_x5u, cache, now_fixed): wrong_root_hash = DEV_ROOT_HASH[:-1] + b"\x03" s = SignatureVerifier( aiohttp_session, cache, wrong_root_hash, subject_name_check=ExactMatch( "remote-settings.content-signature.mozilla.org"), ) with pytest.raises(autograph_utils.CertificateHasWrongRoot) as excinfo: await s.verify_x5u(FAKE_CERT_URL) actual = "4c35b1c3e312d955e778edd0a7e78a388304ef01bffa0329b2469f3cc5ec3604" expected = actual[:-1] + "3" assert excinfo.value.detail == ( "Certificate is not based on expected root hash. " f"Got '{actual}' expected '{expected}'")
async def test_unknown_key(aiohttp_session, mock_with_x5u, cache, now_fixed): certs = [ cryptography.x509.load_pem_x509_certificate(pem, backend=default_backend()) for pem in CERT_LIST ] # Change public_key for an intermediate cert real_intermediate = certs[1] mock_intermediate = mock_cert(real_intermediate) mock_intermediate.public_key = mock.Mock() certs[1] = mock_intermediate with mock.patch( "cryptography.x509.load_pem_x509_certificate") as load_cert_mock: load_cert_mock.side_effect = lambda *args, **kwargs: certs.pop(0) s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) with pytest.raises( autograph_utils.CertificateUnsupportedKeyType) as excinfo: await s.verify_x5u(FAKE_CERT_URL) assert excinfo.value.cert == mock_intermediate assert excinfo.value.key == mock_intermediate.public_key()
async def test_verify_x5u(aiohttp_session, mock_with_x5u, cache, now_fixed): s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) await s.verify_x5u(FAKE_CERT_URL)
async def test_verify_x5u_returns_cache(aiohttp_session, mock_with_x5u, cache, now_fixed): with mock.patch.object(cache, "get") as get_mock: s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) res = await s.verify_x5u(FAKE_CERT_URL) assert res == get_mock.return_value
async def test_verify_signature(aiohttp_session, mock_with_x5u, cache, now_fixed): s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) await s.verify(SIGNED_DATA, SAMPLE_SIGNATURE, FAKE_CERT_URL)
async def test_verify_stage_cert_chain(aiohttp_session, mock_aioresponses, cache, now_fixed): mock_aioresponses.get(FAKE_CERT_URL, status=200, body=STAGE_CERT_CHAIN) s = SignatureVerifier(aiohttp_session, cache, STAGE_ROOT_HASH) await s.verify_x5u(FAKE_CERT_URL)
async def test_verify_signature_bad_numbers(aiohttp_session, mock_with_x5u, cache, now_fixed): s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH) with pytest.raises(autograph_utils.WrongSignatureSize): await s.verify(SIGNED_DATA, SAMPLE_SIGNATURE[:-4], FAKE_CERT_URL)