diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f30fa649..e783b9d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## Unreleased: mitmproxy next +- Improve the error message when users specify the `certs` option without a matching private key. + ([#7073](https://github.com/mitmproxy/mitmproxy/pull/7073), @mhils) +- Fix a bug where intermediate certificates would not be transmitted when using QUIC. + ([#7073](https://github.com/mitmproxy/mitmproxy/pull/7073), @mhils) ## 02 August 2024: mitmproxy 10.4.2 diff --git a/mitmproxy/certs.py b/mitmproxy/certs.py index 59402b797..a5a81ea1a 100644 --- a/mitmproxy/certs.py +++ b/mitmproxy/certs.py @@ -1,6 +1,7 @@ import contextlib import datetime import ipaddress +import logging import os import sys import warnings @@ -19,12 +20,15 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import dsa from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.types import CertificatePublicKeyTypes from cryptography.hazmat.primitives.serialization import pkcs12 from cryptography.x509 import ExtendedKeyUsageOID from cryptography.x509 import NameOID from mitmproxy.coretypes import serializable +logger = logging.getLogger(__name__) + # Default expiry must not be too long: https://github.com/mitmproxy/mitmproxy/issues/815 CA_EXPIRY = datetime.timedelta(days=10 * 365) CERT_EXPIRY = datetime.timedelta(days=365) @@ -91,6 +95,9 @@ class Cert(serializable.Serializable): def to_pyopenssl(self) -> OpenSSL.crypto.X509: return OpenSSL.crypto.X509.from_cryptography(self._cert) + def public_key(self) -> CertificatePublicKeyTypes: + return self._cert.public_key() + def fingerprint(self) -> bytes: return self._cert.fingerprint(hashes.SHA256()) @@ -127,6 +134,17 @@ class Cert(serializable.Serializable): def serial(self) -> int: return self._cert.serial_number + @property + def is_ca(self) -> bool: + constraints: x509.BasicConstraints + try: + constraints = self._cert.extensions.get_extension_for_class( + x509.BasicConstraints + ).value + return constraints.ca + except x509.ExtensionNotFound: + return False + @property def keyinfo(self) -> tuple[str, int]: public_key = self._cert.public_key() @@ -502,11 +520,34 @@ class CertStore: raw = path.read_bytes() cert = Cert.from_pem(raw) try: - key = load_pem_private_key(raw, password=passphrase) - except ValueError: - key = self.default_privatekey + private_key = load_pem_private_key(raw, password=passphrase) + except ValueError as e: + private_key = self.default_privatekey + if cert.public_key() != private_key.public_key(): + raise ValueError( + f'Unable to find private key in "{path.absolute()}": {e}' + ) from e + else: + if cert.public_key() != private_key.public_key(): + raise ValueError( + f'Private and public keys in "{path.absolute()}" do not match:\n' + f"{cert.public_key()=}\n" + f"{private_key.public_key()=}" + ) - self.add_cert(CertStoreEntry(cert, key, path, [cert]), spec) + try: + chain = [Cert(x) for x in x509.load_pem_x509_certificates(raw)] + except ValueError as e: + logger.warning(f"Failed to read certificate chain: {e}") + chain = [cert] + + if cert.is_ca: + logger.warning( + f'"{path.absolute()}" is a certificate authority and not a leaf certificate. ' + f"This indicates a misconfiguration, see https://docs.mitmproxy.org/stable/concepts-certificates/." + ) + + self.add_cert(CertStoreEntry(cert, private_key, path, chain), spec) def add_cert(self, entry: CertStoreEntry, *names: str) -> None: """ diff --git a/test/mitmproxy/net/data/verificationcerts/generate.py b/test/mitmproxy/net/data/verificationcerts/generate.py index d9cad6339..570db7110 100644 --- a/test/mitmproxy/net/data/verificationcerts/generate.py +++ b/test/mitmproxy/net/data/verificationcerts/generate.py @@ -92,3 +92,22 @@ for x in ["self-signed", "trusted-leaf", "trusted-leaf-ip", "trusted-root"]: pem.write(key.read()) shutil.copyfile("trusted-leaf.pem", "example.mitmproxy.org.pem") +with ( + open(f"trusted-leaf.crt") as crt, + open(f"self-signed.key") as key, + open(f"private-public-mismatch.pem", "w") as pem, +): + pem.write(crt.read()) + pem.write(key.read()) + +with ( + open(f"trusted-leaf.pem") as crt1, + open(f"trusted-root.crt") as crt2, + open(f"trusted-chain.pem", "w") as pem, +): + pem.write(crt1.read()) + pem.write(crt2.read()) + +with open(f"trusted-leaf.pem") as crt1, open(f"trusted-chain-invalid.pem", "w") as pem: + pem.write(crt1.read()) + pem.write("-----BEGIN CERTIFICATE-----\nnotacert\n-----END CERTIFICATE-----\n") diff --git a/test/mitmproxy/net/data/verificationcerts/private-public-mismatch.pem b/test/mitmproxy/net/data/verificationcerts/private-public-mismatch.pem new file mode 100644 index 000000000..220559bf6 --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/private-public-mismatch.pem @@ -0,0 +1,48 @@ +-----BEGIN CERTIFICATE----- +MIIDZDCCAkygAwIBAgIUSWoLwl7Rc//TKPwKwv25Vhb6uFAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDMwNzAzMjZaFw00MDEw +MjkwNzAzMjZaMDQxHjAcBgNVBAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzESMBAG +A1UECgwJbWl0bXByb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +qcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGWj8wFJ/XpZsFmAWlH +2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaKwvDbigy037OY97sT +6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV1/6O/5cQ7TOW2E83 +xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/nsJUwLliZpz3j/dUD0 +yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2TVMVLDT17uzNeH1x5 +UXxdB4cexTOm4amvWSwvTQIDAQABo10wWzAfBgNVHSMEGDAWgBTkDj66Wmaf9YP0 +YMqGKG51PejxWzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAgBgNVHREEGTAXghVl +eGFtcGxlLm1pdG1wcm94eS5vcmcwDQYJKoZIhvcNAQELBQADggEBABwKrf+Gy1I5 +BnfhAqbDUYj8OVcjbB5esbvVYAADNNNQ10RREyz5OTTo2dSKy2hIVQeGxaBy5W6d +cNk0CoLzOgpgkwQPQ351+Y0C5yQztXIg/JM5aSeFuAbQg+NtzOQISrcDB390Xr2f +7YhyYL0XLX4NCP7ek7+lAClrf+lL9MysHTTP0pmFWLPP6AIR8JIsLpNpTDV10c8p +QcnJ8+5q6oLHNKN+M/HRcdKzj/XyJ538UzSOlmU+izhNT1j7mbszS0tc9yh0dmpP +9uuBY0wcD/NAHBpSX9v9ZJcOuWntsAxTbMXj1eWNRyt0QJLURlkqeVn6ZrE1/BDS +BqDdOKYJbkw= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAw1IZNjnWZOU8jJqlHVK45PLOpr+y8U0ks52OT6pDhTBWGFkn +hEffHAey/kSwSoszdb7e19VF0SLj4wXr36X7CKkJfFhzHrFOjg16ciNo0GRic82K +4VvlC0qRJVXakTVhN/Tm/zNL90GJLTBMti47jmDup52I8j39/PSb8n/6IWMfNMqt +6fqSDYfeATgzeK0jxG+SEpCm+iqeAdzLE3ZotZJMtZ3CTiQfZ5eJamHSJ8pURlZD +fha8JyS+DzeeIZEvA2QGWgDRigS4n20p7ykrwUxaUbLh2RV2n+kVy+CwgdsgFfE1 +40HI5MJ3WU7/TfoxcKUJF5gB3zmkhwD+s22i5QIDAQABAoIBADK7Gi1JbHQcTlO+ +vvAU0k00+5O36sRd4xB79cCfWpY3bcU5Mthayoo/PbBpKtjRuvX0M3EfxdiCFWqb +2R3nwIIJVZtkZdIs/1hKC+mlZM3rpN6rHk1WTvFV1sk5uWFJ2gxsoarbKfn4naaN +Cv+ulm1uo84JTs6MZ3HSHscnklIlNnDG3piC8JnzXVgS7kzbJPXTccZJI1Mwgyho +D1HoWatIep5XAp3OZePlApfRpKn/+2Hg6USkbcHYzX/XpA3b3/YEBqsXVUFxAs5m +nChdLpKUCIjcARInAXoULh0AQHGcH9dfJk1QF1Lw6nCoY12P1sZfdGU5trcslA1O +W28syoECgYEA6GjaqFYffceJHlODukHgpLNfa1EUy49ZWjmdmXTYXJpufzjD4n4H +sUkEnJ00ssaCVdd0XClbN/l/QvIAz3W8ZXXQWtZJHV2wAIyjWpqiOwaiXQFZIZ67 +kxGsLVjkuiST33CX6yl8IKAD+8B67uZhmWTnYBmxGnIZODqUatpqW3ECgYEA1yV+ +CGoNp6VQqej0t0y8C8pXiMGe2DGj5p1WFn00cn/fC1E2Yc0HG+E62OB7tgUFI180 +/nArOQHnTZCzAQAc44M63gesRMeu+0cxuTUqOUCn1lGD+4K/3IA59Rgzfvvnq6xu +tn2jidfqaDPfKavqtCqRbs8wJYaGEhgcb8pUvLUCgYEA4pjpKFvgFGip/lF7C+0T +NEJXdHD3j4lSmy+1w1szYQaJWa1k/73VjjsdLf3w1aXKihuprfn8oFS4ifMeayfl +6h62aPqpCuK/qal10+8U4ewT/g5Ecw0q4bfHYedcC0mCi8ZhuL0X809Q0vLWaXti +CYdiOEaUcK5yfGpRLuWJ8WECgYAWWXa2OQ4iFDJE9EY3pGkEcIiXVEXD/6QfGMkQ +nQENw+rPqigUENBkPQl37hnr1qmp+wHuTIiw61mz3Qw7Vl+p4sACwJlMq9GpmMO5 +kaRJPkYxJVaokfSMW2Wp6FGxJ0nxs3/sxTBv6VYYbQsJsSo4fROOh0dhHpBe4NJT +aplS4QKBgQCaMUN88HNaVr0+DPQKeFoflqwCCYsdK43QTWEirrFDWDliJ0k81DZJ +k7N3oR5UujsTMgfYDJ6Cp3enZVYyuoFnOlVT8v4XzvzAGTehyo4rMiE2TbMir7Xn +agPTZYSuQc4Iy7BPwh/PMlX6bDOTeq/ftyWrDuF9+pC5w3v4SkuMew== +-----END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/net/data/verificationcerts/trusted-chain-invalid.pem b/test/mitmproxy/net/data/verificationcerts/trusted-chain-invalid.pem new file mode 100644 index 000000000..ce51e2ce6 --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/trusted-chain-invalid.pem @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIDZDCCAkygAwIBAgIUSWoLwl7Rc//TKPwKwv25Vhb6uFAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDMwNzAzMjZaFw00MDEw +MjkwNzAzMjZaMDQxHjAcBgNVBAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzESMBAG +A1UECgwJbWl0bXByb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +qcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGWj8wFJ/XpZsFmAWlH +2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaKwvDbigy037OY97sT +6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV1/6O/5cQ7TOW2E83 +xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/nsJUwLliZpz3j/dUD0 +yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2TVMVLDT17uzNeH1x5 +UXxdB4cexTOm4amvWSwvTQIDAQABo10wWzAfBgNVHSMEGDAWgBTkDj66Wmaf9YP0 +YMqGKG51PejxWzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAgBgNVHREEGTAXghVl +eGFtcGxlLm1pdG1wcm94eS5vcmcwDQYJKoZIhvcNAQELBQADggEBABwKrf+Gy1I5 +BnfhAqbDUYj8OVcjbB5esbvVYAADNNNQ10RREyz5OTTo2dSKy2hIVQeGxaBy5W6d +cNk0CoLzOgpgkwQPQ351+Y0C5yQztXIg/JM5aSeFuAbQg+NtzOQISrcDB390Xr2f +7YhyYL0XLX4NCP7ek7+lAClrf+lL9MysHTTP0pmFWLPP6AIR8JIsLpNpTDV10c8p +QcnJ8+5q6oLHNKN+M/HRcdKzj/XyJ538UzSOlmU+izhNT1j7mbszS0tc9yh0dmpP +9uuBY0wcD/NAHBpSX9v9ZJcOuWntsAxTbMXj1eWNRyt0QJLURlkqeVn6ZrE1/BDS +BqDdOKYJbkw= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGW +j8wFJ/XpZsFmAWlH2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaK +wvDbigy037OY97sT6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV +1/6O/5cQ7TOW2E83xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/ns +JUwLliZpz3j/dUD0yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2T +VMVLDT17uzNeH1x5UXxdB4cexTOm4amvWSwvTQIDAQABAoIBAEb3arF8E3qR2F7S +6zHCASqjXIA3+QR5lGoOOPdgMU4uEgDQgEchXc506dyBYamZX+XlSxifgDqgmkUn +G1mS6Fy+9XysFVVqKmP4p6D79TQWTv5PKFeS5dTytvMGXB7E4O/xDeb08Ve/L3JA +Ic7vPLX0/YqVf2ZuNO0fNSlQhyawUVlSS6yq/B1GUo44PX/PXrWYZd66mc29g+KF +7D0nnbloLY+5QmOEVYbf99RHJ64Cwxzwxi4ic9AxLaNMfkrydjbGQ0VhJq14WEux +/TJLvinzIDyslnx4aYstno3ryoxLtR/9ALysrdQ8vVMaVCdbf0aMu832R2dI4s9K +ixQlgmkCgYEA0Ho0a4qzwk9oLWYk1x16kOCqmfuenlEz3Tv8uBB/jdz7UM63sS3h +OUQYKhhtzOGf6CDA3CE11ie6SX7zHrowPLJBSpacJZJcrHPGZxdBZ/As/QpGnZll +1eTMlVRXk06k/fEYplU1dS5IDbsvUIaaF9CF9Qd3MkDRglhbM19Iy+sCgYEA0HM1 +d7OBsjSi09zFDE+Joq6Gih8LqqFLtMsIHEKAJ0gJgmt6MTfjtdPo55ZKVLCR2pbP +yRHagK4LRUFY9Pl5eGlEUF9cKEKKtZOURUFVz3yBMYPBje/B1o/UCCj+nr2reWgx +qj/jwZrph6QKCkEsnQXi98tE7XSgihUh6UEQO6cCgYASVe0uWDCfMmSzOXyb/te8 +zkWy7VJyEipBlvkPJ0RQsdLYtJWrW6Gna7nEWgmuL1nlDJxpv/IAN9ZGiIfReAau +D+92I/DvzQOhlz0n6/+wqIsMZk73pXozacAkkhpxtkUEoKPOXUgqWju0GXZ72prK +5WgiuNle7hx/Hk5HImZAqQKBgQCK7W4iRGpZeklXiNlvtgcWfNlAbyaYZ34MlhDm +vM+q3pEv8i/zY7uJcR3WU81gmnnrRP5hlVuazeTHGKGQTEFQJmCYbKYAUzEdiamV +atElQ2bbuGOlFLmNJjj7406oP+NsPCx1urUyUOv6MjNa2EtCsCywWDKtTEC/Jwx9 +6JZIGwKBgDa4Ephl+lcyGC8D9Tdc4Nme6n/10pIN53ZCplqVSuma+kOOQX7ZXpJb +0csHR9KODcpQuo2yOnUfZ1z4+tsjms2Uv97GgcZ9sO84I/3RGJCvxmzAEiRmJMfH +y38GtEYCnat5VOKdo//eIYtiu9YSXuuS1ENSWUPZ9YInrkUk0j8S +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +notacert +-----END CERTIFICATE----- diff --git a/test/mitmproxy/net/data/verificationcerts/trusted-chain.pem b/test/mitmproxy/net/data/verificationcerts/trusted-chain.pem new file mode 100644 index 000000000..0f4146510 --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/trusted-chain.pem @@ -0,0 +1,69 @@ +-----BEGIN CERTIFICATE----- +MIIDZDCCAkygAwIBAgIUSWoLwl7Rc//TKPwKwv25Vhb6uFAwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDMwNzAzMjZaFw00MDEw +MjkwNzAzMjZaMDQxHjAcBgNVBAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzESMBAG +A1UECgwJbWl0bXByb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +qcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGWj8wFJ/XpZsFmAWlH +2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaKwvDbigy037OY97sT +6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV1/6O/5cQ7TOW2E83 +xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/nsJUwLliZpz3j/dUD0 +yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2TVMVLDT17uzNeH1x5 +UXxdB4cexTOm4amvWSwvTQIDAQABo10wWzAfBgNVHSMEGDAWgBTkDj66Wmaf9YP0 +YMqGKG51PejxWzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDAgBgNVHREEGTAXghVl +eGFtcGxlLm1pdG1wcm94eS5vcmcwDQYJKoZIhvcNAQELBQADggEBABwKrf+Gy1I5 +BnfhAqbDUYj8OVcjbB5esbvVYAADNNNQ10RREyz5OTTo2dSKy2hIVQeGxaBy5W6d +cNk0CoLzOgpgkwQPQ351+Y0C5yQztXIg/JM5aSeFuAbQg+NtzOQISrcDB390Xr2f +7YhyYL0XLX4NCP7ek7+lAClrf+lL9MysHTTP0pmFWLPP6AIR8JIsLpNpTDV10c8p +QcnJ8+5q6oLHNKN+M/HRcdKzj/XyJ538UzSOlmU+izhNT1j7mbszS0tc9yh0dmpP +9uuBY0wcD/NAHBpSX9v9ZJcOuWntsAxTbMXj1eWNRyt0QJLURlkqeVn6ZrE1/BDS +BqDdOKYJbkw= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqcEdB7TZD7DxJlJH/ZTyrovP0din2vLCRZRZ07ix48arITGW +j8wFJ/XpZsFmAWlH2LriXGC8BB54EJt8Epix5tcdKvCZ/7vTgaBQ+DQVdCNiscaK +wvDbigy037OY97sT6Ou9xYjjgg0cb+WTkQGMOTMbCW2e5iuSvyMPdZ2+2RzbTcNV +1/6O/5cQ7TOW2E83xeaN4stBMfRmsQRoHXBri0tqtQ3bHd3V4H9iai7HFBOcB/ns +JUwLliZpz3j/dUD0yZAgdgB9XBlGvs/e746bGctyvl2QZu2JeACEVPmVrAUhqe2T +VMVLDT17uzNeH1x5UXxdB4cexTOm4amvWSwvTQIDAQABAoIBAEb3arF8E3qR2F7S +6zHCASqjXIA3+QR5lGoOOPdgMU4uEgDQgEchXc506dyBYamZX+XlSxifgDqgmkUn +G1mS6Fy+9XysFVVqKmP4p6D79TQWTv5PKFeS5dTytvMGXB7E4O/xDeb08Ve/L3JA +Ic7vPLX0/YqVf2ZuNO0fNSlQhyawUVlSS6yq/B1GUo44PX/PXrWYZd66mc29g+KF +7D0nnbloLY+5QmOEVYbf99RHJ64Cwxzwxi4ic9AxLaNMfkrydjbGQ0VhJq14WEux +/TJLvinzIDyslnx4aYstno3ryoxLtR/9ALysrdQ8vVMaVCdbf0aMu832R2dI4s9K +ixQlgmkCgYEA0Ho0a4qzwk9oLWYk1x16kOCqmfuenlEz3Tv8uBB/jdz7UM63sS3h +OUQYKhhtzOGf6CDA3CE11ie6SX7zHrowPLJBSpacJZJcrHPGZxdBZ/As/QpGnZll +1eTMlVRXk06k/fEYplU1dS5IDbsvUIaaF9CF9Qd3MkDRglhbM19Iy+sCgYEA0HM1 +d7OBsjSi09zFDE+Joq6Gih8LqqFLtMsIHEKAJ0gJgmt6MTfjtdPo55ZKVLCR2pbP +yRHagK4LRUFY9Pl5eGlEUF9cKEKKtZOURUFVz3yBMYPBje/B1o/UCCj+nr2reWgx +qj/jwZrph6QKCkEsnQXi98tE7XSgihUh6UEQO6cCgYASVe0uWDCfMmSzOXyb/te8 +zkWy7VJyEipBlvkPJ0RQsdLYtJWrW6Gna7nEWgmuL1nlDJxpv/IAN9ZGiIfReAau +D+92I/DvzQOhlz0n6/+wqIsMZk73pXozacAkkhpxtkUEoKPOXUgqWju0GXZ72prK +5WgiuNle7hx/Hk5HImZAqQKBgQCK7W4iRGpZeklXiNlvtgcWfNlAbyaYZ34MlhDm +vM+q3pEv8i/zY7uJcR3WU81gmnnrRP5hlVuazeTHGKGQTEFQJmCYbKYAUzEdiamV +atElQ2bbuGOlFLmNJjj7406oP+NsPCx1urUyUOv6MjNa2EtCsCywWDKtTEC/Jwx9 +6JZIGwKBgDa4Ephl+lcyGC8D9Tdc4Nme6n/10pIN53ZCplqVSuma+kOOQX7ZXpJb +0csHR9KODcpQuo2yOnUfZ1z4+tsjms2Uv97GgcZ9sO84I/3RGJCvxmzAEiRmJMfH +y38GtEYCnat5VOKdo//eIYtiu9YSXuuS1ENSWUPZ9YInrkUk0j8S +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUB5WLNdtHRvciUwDfySAEHYVSNe0wDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDExMDMwNzAzMjZaFw00MDEw +MjkwNzAzMjZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDVWUg2YdsztWIZpsWdbIB6Aewa1Igt7mNkKMQ1NJ6v +priVlzWhZr0FfSVTid2m3Od2f5A751Q+IXLkNJ8d6Y4qVirAstEM57sCH/Hmt1E3 +cZOAzLQY2xCIrKPmbu6d8eSGO1q+Y8KstCK/V94/QRahjjoHxkLcSAmdg6PAkbhB +7MwOxiJslIbsBY4UCXP0l4kUUIuMi8W2Y4M1VgvLRpUjaKVo1NQ0hLi4XBnU7Bsq +A8/PZDuVeP0NcJtAxRicqqLX/MjgkTrA2tPnSj9m60lO79S40GSzB238o2zJQ5ON +jHJdAhLlrsrkxZmDmKM59IdmV2OhN7O844ktbXDOqvq5AgMBAAGjUzBRMB0GA1Ud +DgQWBBTkDj66Wmaf9YP0YMqGKG51PejxWzAfBgNVHSMEGDAWgBTkDj66Wmaf9YP0 +YMqGKG51PejxWzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAo +9FwbGob1WRAo7ORShaDhopIDAYMCL1oc9YZ2ZQK8aDuZbqIZ/7+1LbWCeMAV4PTH +Tcx+n9Zg/g/RkoSNu8KqFoQGFmdBZnKOMU4vlITu/ORpDu1sjSA+Eo9YbipemX+n +jv+YHI4STFAWnyext3IUZVkT6wpU3pwUjX0fbk4LJfVLR41y4iD11XGergxAcpjj +T03txkJcrTiX65SnB041Y4exUMLOUn5lTs/q2rBNkiLNljXQ6l+8L1rdQEN/j0Mx +OYdc6FIUIESC1mMOf80+YOwxPJ862SyTv/cJB3npwTj/DdQu7blf/z2hMP7a7w+X +l5d31XzcDOrCf/bTthvb +-----END CERTIFICATE----- diff --git a/test/mitmproxy/test_certs.py b/test/mitmproxy/test_certs.py index d3850fdaf..9e80bfc99 100644 --- a/test/mitmproxy/test_certs.py +++ b/test/mitmproxy/test_certs.py @@ -108,19 +108,6 @@ class TestCertStore: assert ("three.com", x509.GeneralNames([])) in tstore.certs assert ("four.com", x509.GeneralNames([])) in tstore.certs - def test_overrides(self, tmp_path): - ca1 = certs.CertStore.from_store(tmp_path / "ca1", "test", 2048) - ca2 = certs.CertStore.from_store(tmp_path / "ca2", "test", 2048) - assert not ca1.default_ca.serial == ca2.default_ca.serial - - dc = ca2.get_cert("foo.com", [x509.DNSName("sans.example.com")]) - dcp = tmp_path / "dc" - dcp.write_bytes(dc.cert.to_pem()) - ca1.add_cert_file("foo.com", dcp) - - ret = ca1.get_cert("foo.com", []) - assert ret.cert.serial == dc.cert.serial - def test_create_dhparams(self, tmp_path): filename = tmp_path / "dhparam.pem" certs.CertStore.load_dhparam(filename) @@ -203,6 +190,7 @@ class TestCert: assert c2.cn == "www.inode.co.nz" assert len(c2.altnames) == 2 assert c2.fingerprint() + assert c2.public_key() assert c2.notbefore == datetime( year=2010, month=1, @@ -257,6 +245,19 @@ class TestCert: c = certs.Cert.from_pem(d) assert c.keyinfo == (name, bits) + @pytest.mark.parametrize( + "filename,is_ca", + [ + ("mitmproxy/net/data/verificationcerts/trusted-leaf.crt", False), + ("mitmproxy/net/data/verificationcerts/trusted-root.crt", True), + ("mitmproxy/data/invalid-subject.pem", False), # no basic constraints + ], + ) + def test_is_ca(self, tdata, filename, is_ca): + pem = Path(tdata.path(filename)).read_bytes() + cert = certs.Cert.from_pem(pem) + assert cert.is_ca == is_ca + def test_err_broken_sans(self, tdata): with open(tdata.path("mitmproxy/net/data/text_cert_weird1"), "rb") as f: d = f.read() @@ -280,6 +281,14 @@ class TestCert: c2.set_state(a) assert c == c2 + def test_add_cert_overrides(self, tdata, tstore): + certfile = Path( + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem") + ) + cert = certs.Cert.from_pem(certfile.read_bytes()) + tstore.add_cert_file("example.com", certfile) + assert cert == tstore.get_cert("example.com", []).cert + def test_from_store_with_passphrase(self, tdata, tstore): tstore.add_cert_file( "unencrypted-no-pass", Path(tdata.path("mitmproxy/data/testkey.pem")), None @@ -302,6 +311,54 @@ class TestCert: None, ) + def test_add_cert_with_no_private_key(self, tdata, tstore): + with pytest.raises(ValueError, match="Unable to find private key"): + tstore.add_cert_file( + "example.com", + Path( + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.crt") + ), + ) + + def test_add_cert_private_public_mismatch(self, tdata, tstore): + with pytest.raises( + ValueError, match='Private and public keys in ".+" do not match' + ): + tstore.add_cert_file( + "example.com", + Path( + tdata.path( + "mitmproxy/net/data/verificationcerts/private-public-mismatch.pem" + ) + ), + ) + + def test_add_cert_chain(self, tdata, tstore): + tstore.add_cert_file( + "example.com", + Path(tdata.path("mitmproxy/net/data/verificationcerts/trusted-chain.pem")), + ) + assert len(tstore.get_cert("example.com", []).chain_certs) == 2 + + def test_add_cert_chain_invalid(self, tdata, tstore, caplog): + tstore.add_cert_file( + "example.com", + Path( + tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-chain-invalid.pem" + ) + ), + ) + assert "Failed to read certificate chain" in caplog.text + assert len(tstore.get_cert("example.com", []).chain_certs) == 1 + + def test_add_cert_is_ca(self, tdata, tstore, caplog): + tstore.add_cert_file( + "example.com", + Path(tdata.path("mitmproxy/net/data/verificationcerts/trusted-root.pem")), + ) + assert "is a certificate authority and not a leaf certificate" in caplog.text + def test_special_character(self, tdata): with open(tdata.path("mitmproxy/net/data/text_cert_with_comma"), "rb") as f: d = f.read()