diff --git a/CHANGELOG b/CHANGELOG index 8553cce0c..49fc7f204 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ Unreleased: mitmproxy next * Add ASGI support for embedded apps (@mhils) * Updated raw exports to not remove headers (@wchasekelley) * Fix file unlinking before external viewer finishes loading (@wchasekelley) + * Add --cert-passphrase command line argument (@mirosyn) * Add interactive tutorials to the documentation (@mplattner) * --- TODO: add new PRs above this line --- diff --git a/mitmproxy/certs.py b/mitmproxy/certs.py index bac6c59a3..686c05745 100644 --- a/mitmproxy/certs.py +++ b/mitmproxy/certs.py @@ -196,7 +196,7 @@ class CertStore: return dh @classmethod - def from_store(cls, path, basename, key_size): + def from_store(cls, path, basename, key_size, passphrase: typing.Optional[bytes] = None): ca_path = os.path.join(path, basename + "-ca.pem") if not os.path.exists(ca_path): key, ca = cls.create_store(path, basename, key_size) @@ -208,7 +208,8 @@ class CertStore: raw) key = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, - raw) + raw, + passphrase) dh_path = os.path.join(path, basename + "-dhparam.pem") dh = cls.load_dhparam(dh_path) return cls(key, ca, ca_path, dh) @@ -280,7 +281,7 @@ class CertStore: return key, ca - def add_cert_file(self, spec: str, path: str) -> None: + def add_cert_file(self, spec: str, path: str, passphrase: typing.Optional[bytes] = None) -> None: with open(path, "rb") as f: raw = f.read() cert = Cert( @@ -290,7 +291,8 @@ class CertStore: try: privatekey = OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, - raw) + raw, + passphrase) except Exception: privatekey = self.default_privatekey self.add_cert( diff --git a/mitmproxy/options.py b/mitmproxy/options.py index 4fb8e944f..03cb6945a 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -47,6 +47,10 @@ class Options(optmanager.OptManager): certificate as the first entry. """ ) + self.add_option( + "cert_passphrase", Optional[str], None, + "Passphrase for decrypting the private key provided in the --cert option." + ) self.add_option( "ciphers_client", Optional[str], None, "Set supported ciphers for client connections using OpenSSL syntax." diff --git a/mitmproxy/proxy/config.py b/mitmproxy/proxy/config.py index 982afb755..0ba46ee58 100644 --- a/mitmproxy/proxy/config.py +++ b/mitmproxy/proxy/config.py @@ -62,10 +62,12 @@ class ProxyConfig: os.path.dirname(certstore_path) ) key_size = options.key_size + passphrase = options.cert_passphrase.encode("utf-8") if options.cert_passphrase else None self.certstore = certs.CertStore.from_store( certstore_path, moptions.CONF_BASENAME, - key_size + key_size, + passphrase ) for c in options.certs: @@ -79,7 +81,7 @@ class ProxyConfig: "Certificate file does not exist: %s" % cert ) try: - self.certstore.add_cert_file(parts[0], cert) + self.certstore.add_cert_file(parts[0], cert, passphrase) except crypto.Error: raise exceptions.OptionsError( "Invalid certificate format: %s" % cert diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py index eac1561c0..a94f28206 100644 --- a/mitmproxy/tools/cmdline.py +++ b/mitmproxy/tools/cmdline.py @@ -67,6 +67,7 @@ def common_options(parser, opts): # Proxy SSL options group = parser.add_argument_group("SSL") opts.make_parser(group, "certs", metavar="SPEC") + opts.make_parser(group, "cert_passphrase", metavar="PASS") opts.make_parser(group, "ssl_insecure", short="k") opts.make_parser(group, "key_size", metavar="KEY_SIZE") diff --git a/test/mitmproxy/data/mitmproxy.pem b/test/mitmproxy/data/mitmproxy.pem new file mode 100644 index 000000000..f29d40274 --- /dev/null +++ b/test/mitmproxy/data/mitmproxy.pem @@ -0,0 +1,51 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI9fSurwMcOA4CAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECKALBF48zvQnBIIEyKDIy72IMJoZ +4q7LsG0dCSa8oGI/CtAnC9YqRlDj+paWoGKDUkzxnMloUbJkpQlTEYRHXp0xKtdP +IcCjWFqWeQjsaJUlwILNLiliVpbyW/0PLmNQmRSfvLhlZ77rRk08DyLU0mcW2zRX +DuKHuxGhdlmte7EKsNf8czch9hDXqrCLqxlzr86K0pwT40W1r32TgQdX68edluoj +acggWuEzeTTKy1BKkVtlCq63dflgRfSo0as+dYX38wxzC6O6hxKpax277ijoJZHc +QsXxi/zREa+gtVOq9D6Vz5E+MmmIIAzVrXsFe1Uj4wYb3XUSO6WnJ9GlrixqWeu3 +9lkOZOEKyyDgIY+twn06kyZBspKnXvQMMPjeiSSeaqI9LA0qpvRsxuWCxyTJ2YZI +s+xab8j5g5RKOmrt1bGtLl66tcrGNP9jYC5pjMNl6fz3c8+oxC0Bun4q+yOA9QzG +4GaiA834x+9wtsEBSjlMB5AMwYH+1ODo6Q+VUAWH1qBvCm/gQT2mvSgcrz6bcGJI +gimfzl/IbqVuVkWl7yFqNN/renE47pvy34Dbymb0FBK/5Gb1FImno3CcAkCuaEJ1 +sWdx2Ej0Ezit8v1iJN2q29xlD7MrxB0uPvklUPRlD9RVcDJ15GwBPA8ugN/Fjj50 +2BiMJ2/uqBoEnAjMyStINArS5PWL6gthIXenVJ4w0wegBciCsGo4G7UFQ0z/w2Je +7NJ8TjwKdTYJdAfgO5Rr8u6j0ybn72T/+QJfjugNLufRx4sakvPZR90/AFb2YX+L +kgCVS3ySOfom9p5JcxdnI8omelBIi1Qa9xwPKMPaV6oYkqBVjmcDDZocC6qN15PD +jCrgGryV3Fsn5OLYTB+EQDLNqmo+qd1O0pNY2THwD/DGGlx6VhmeQnWdt534g5lo +clQOmLXEeUWIb2u5PanakqNpY5mBQcOJ88/RS+oGAjTGU0e3I1zLb6EN/Ftndjv1 +sfEh+HMwHxIWxdnJb6z6m73XJr4z30VGN8e+f1lC8c9SJ9aTQ/9vH3bsaXLW6GFY +DBisBg2/+vMwRSG9PkYrp1p6rGAhwbaofnZE5zApT7PFEX2RVNPU7lgXn84ycRHw +gZ89Mpa9zShL4T1PS8BrKwS7AH/se7ofKW/s8Z9SgngTWj0Efd4hZmn/EenVHBWf +kjAkvKIgGE8FJF1QlmU5dHDFhRiUGXIaB1rYAcwwuwB06fxRqEL3pU6jkHSru3ry +sYaY/cfpd5D5PT+FlxkzAPH1iiC3knXpcotWpJ2iQshsw9ifwg/vVJB0n20+Rxeu +XTgwiT+X5mJNAQUCj6aExWUg+D5gPnJPwFmzAWBGKWrvwI+vI6zIv4MJywzU+Ei8 +1lU5rezPovAbGSTwUBPDydhORua0P8tVT8KPMmPJhza6IORTPpzdEOCXCOH17CWg +VWKjYvEul8CdNh4O3CJDU4lN8yn6RXCBPK4NKDea17GCIEBgnOnpFny+jdfNT+Ce +9aNh8ah61vbPag9EM2okmBlbnpkhUO+x8K8prZHZE7qRgUbmn1cJwIP6pNN/263q +S2uKZMnoaT65BaQh9wpgSvWmDup3/lGG/C2+m0k087QBVHMSfpTK9WcZ94BbzoeR +S9rWCU2k/woEUOv3hssY5w== +-----END ENCRYPTED PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgIURm3Wk48Uc3RtqZ0FFxCaE33TvjcwDQYJKoZIhvcNAQEL +BQAwSTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBENhbGkxDTALBgNVBAcMBHRlc3Qx +DTALBgNVBAoMBHRlc3QxDTALBgNVBAMMBHRlc3QwIBcNMjAwODI4MTIyNDU4WhgP +MjI5NDA2MTIxMjI0NThaMEkxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARDYWxpMQ0w +CwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDAR0ZXN0MQ0wCwYDVQQDDAR0ZXN0MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyrgqfiwRh2EcPYk/GFMgF2k8yJq +4mkQcjpWlowp/fL99W/c33ySEtV/LyMJ6LtPAIKN4SambUFcx97pEgJi9YEDLRGW +aN9jznss5AAe03uXN2gizpq9LmdPxr/vmH1DnqdI5MKwa8g9phpe9tT6ik3f2qkm +1V9Ka38GlkHbB+w743ytz20jM6ifTtrX0SuDqDbAppandv5Ix3CHdlllRS/MKNEw +LAs7LVkct0UNTp+soTIhGcASmbf24dvJnO+Msfuqw60mHJpoUP/xDcRGcXnjsgAZ +zAi0UXlV9QiItQeOKxLBHIlMSAEd9oEejPCi6uN+zjKb3De7LUD2Vxu7iwIDAQAB +o1MwUTAdBgNVHQ4EFgQU6tvbiSgA6pujKJBSFw/j+QRZOiwwHwYDVR0jBBgwFoAU +6tvbiSgA6pujKJBSFw/j+QRZOiwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B +AQsFAAOCAQEAgVa/jUdGqElv3EzxzSqtYkGxmeY5m2aLPKgsUl2zOPWwgyE1Synu +TuFS+B5pyTT1KZ6IDwRQ+hA9jOnEopNK33lVelz7XBuw7485qVidG4o6QH1uo/J9 +GlxjM5SCY6yQ4frCI8lCY6+LA0NbI05qtVNTS1zgdOBnC/IOlMFpp0oDaf5FZHxc +Ci1I/g32ES3rvKiAGBY2m6hy138GzYpZTXnKS03MaTfUCFfsOvqq/z2KBCeCd4mH +VDO7adjhw4I7EYYXjmly2um6NaqyXtT6/AARY3JuQgFoW7W3XBV6TCsYmsGSeUTH +JrhnGnHiNi06IuBwOXYZDID+orBMr9NDKw== +-----END CERTIFICATE----- diff --git a/test/mitmproxy/test_certs.py b/test/mitmproxy/test_certs.py index 37604f54a..3a977f3c9 100644 --- a/test/mitmproxy/test_certs.py +++ b/test/mitmproxy/test_certs.py @@ -204,3 +204,9 @@ class TestCert: x = certs.Cert('') x.set_state(a) assert x == c + + def test_from_store_with_passphrase(self, tdata, tmpdir): + ca = certs.CertStore.from_store(str(tmpdir), "mitmproxy", 2048, "password") + ca.add_cert_file("*", tdata.path("mitmproxy/data/mitmproxy.pem"), "password") + + assert ca.get_cert(b"foo", []) diff --git a/test/mitmproxy/test_proxy.py b/test/mitmproxy/test_proxy.py index f455b0ff2..1cca58250 100644 --- a/test/mitmproxy/test_proxy.py +++ b/test/mitmproxy/test_proxy.py @@ -42,11 +42,10 @@ class TestProcessProxyOptions: assert self.p() def test_certs(self, tdata): - self.assert_noerr( - "--cert", - tdata.path("mitmproxy/data/testkey.pem")) - with pytest.raises(Exception, match="does not exist"): - self.p("--cert", "nonexistent") + with pytest.raises(Exception, match="ambiguous option"): + self.assert_noerr( + "--cert", + tdata.path("mitmproxy/data/testkey.pem")) class TestProxyServer: