mitmproxy/test/mitmproxy/test_certs.py
Matthew Hughes d9cac6fbcd
Make Cert.not{before,after} timezone aware (#4805)
I noticed when running tests the output of
`web/src/js/__tests__/ducks/_tflow.ts` would change depending on how I
set my timezone, e.g.

    $ TZ=America/Los_Angeles pytest --quiet \
        test/mitmproxy/tools/web/test_app.py >/dev/null \
        && grep --extended-regexp 'not(after|before)' web/src/js/__tests__/ducks/_tflow.ts
                "notafter": 2235132207,
                "notbefore": 1604415807,
    $ TZ=Asia/Tokyo pytest --quiet \
        test/mitmproxy/tools/web/test_app.py >/dev/null \
        && grep --extended-regexp 'not(after|before)'  web/src/js/__tests__/ducks/_tflow.ts
                "notafter": 2235074607,
                "notbefore": 1604354607

It looks like this is because the `cert_to_json` function simply calls
`timestamp` the `datetime` object from
`x509.Certificate.not_valid_before`, however, this `datetime` object is
not timestamp aware, from the docs [1]:

> A naïve datetime representing the beginning of the validity period for
the certificate in UTC

So when serializing to JSON, first convert the `datetime` to UTC then
call `timestamp`.

A test was added by inspecting one of the test certs with:

    $ openssl x509 -in test/mitmproxy/net/data/text_cert_2 -text

Extracting the date and asserting on that.

The corresponding test has also been re-run so that `_tflow.ts` was
regenerated with it's correct value. Snapshots were also updated via:

    $(npm bin)/jest --updateSnapshot

[1] https://cryptography.io/en/latest/x509/reference/#cryptography.x509.Certificate.not_valid_after
2021-09-16 10:53:36 +02:00

278 lines
9.3 KiB
Python

import os
from datetime import datetime, timezone
from pathlib import Path
from cryptography import x509
from cryptography.x509 import NameOID
import pytest
from mitmproxy import certs
from ..conftest import skip_windows
# class TestDNTree:
# def test_simple(self):
# d = certs.DNTree()
# d.add("foo.com", "foo")
# d.add("bar.com", "bar")
# assert d.get("foo.com") == "foo"
# assert d.get("bar.com") == "bar"
# assert not d.get("oink.com")
# assert not d.get("oink")
# assert not d.get("")
# assert not d.get("oink.oink")
#
# d.add("*.match.org", "match")
# assert not d.get("match.org")
# assert d.get("foo.match.org") == "match"
# assert d.get("foo.foo.match.org") == "match"
#
# def test_wildcard(self):
# d = certs.DNTree()
# d.add("foo.com", "foo")
# assert not d.get("*.foo.com")
# d.add("*.foo.com", "wild")
#
# d = certs.DNTree()
# d.add("*", "foo")
# assert d.get("foo.com") == "foo"
# assert d.get("*.foo.com") == "foo"
# assert d.get("com") == "foo"
@pytest.fixture()
def tstore(tdata):
return certs.CertStore.from_store(tdata.path("mitmproxy/data/confdir"), "mitmproxy", 2048)
class TestCertStore:
def test_create_explicit(self, tmpdir):
ca = certs.CertStore.from_store(str(tmpdir), "test", 2048)
assert ca.get_cert("foo", [])
ca2 = certs.CertStore.from_store(str(tmpdir), "test", 2048)
assert ca2.get_cert("foo", [])
assert ca.default_ca.serial == ca2.default_ca.serial
def test_create_no_common_name(self, tstore):
assert tstore.get_cert(None, []).cert.cn is None
def test_chain_file(self, tdata, tmp_path):
cert = Path(tdata.path("mitmproxy/data/confdir/mitmproxy-ca.pem")).read_bytes()
(tmp_path / "mitmproxy-ca.pem").write_bytes(cert)
ca = certs.CertStore.from_store(tmp_path, "mitmproxy", 2048)
assert ca.default_chain_file is None
(tmp_path / "mitmproxy-ca.pem").write_bytes(2 * cert)
ca = certs.CertStore.from_store(tmp_path, "mitmproxy", 2048)
assert ca.default_chain_file == (tmp_path / "mitmproxy-ca.pem")
def test_sans(self, tstore):
c1 = tstore.get_cert("foo.com", ["*.bar.com"])
tstore.get_cert("foo.bar.com", [])
# assert c1 == c2
c3 = tstore.get_cert("bar.com", [])
assert not c1 == c3
def test_sans_change(self, tstore):
tstore.get_cert("foo.com", ["*.bar.com"])
entry = tstore.get_cert("foo.bar.com", ["*.baz.com"])
assert "*.baz.com" in entry.cert.altnames
def test_expire(self, tstore):
tstore.STORE_CAP = 3
tstore.get_cert("one.com", [])
tstore.get_cert("two.com", [])
tstore.get_cert("three.com", [])
assert ("one.com", ()) in tstore.certs
assert ("two.com", ()) in tstore.certs
assert ("three.com", ()) in tstore.certs
tstore.get_cert("one.com", [])
assert ("one.com", ()) in tstore.certs
assert ("two.com", ()) in tstore.certs
assert ("three.com", ()) in tstore.certs
tstore.get_cert("four.com", [])
assert ("one.com", ()) not in tstore.certs
assert ("two.com", ()) in tstore.certs
assert ("three.com", ()) in tstore.certs
assert ("four.com", ()) 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", ["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)
assert filename.exists()
@skip_windows
def test_umask_secret(self, tmpdir):
filename = str(tmpdir.join("secret"))
with certs.CertStore.umask_secret(), open(filename, "wb"):
pass
# TODO: How do we actually attempt to read that file as another user?
assert os.stat(filename).st_mode & 0o77 == 0
class TestDummyCert:
def test_with_ca(self, tstore):
r = certs.dummy_cert(
tstore.default_privatekey,
tstore.default_ca._cert,
"foo.com",
["one.com", "two.com", "*.three.com", "127.0.0.1"],
"Foo Ltd."
)
assert r.cn == "foo.com"
assert r.altnames == ["one.com", "two.com", "*.three.com", "127.0.0.1"]
assert r.organization == "Foo Ltd."
r = certs.dummy_cert(
tstore.default_privatekey,
tstore.default_ca._cert,
None,
[],
None
)
assert r.cn is None
assert r.organization is None
assert r.altnames == []
class TestCert:
def test_simple(self, tdata):
with open(tdata.path("mitmproxy/net/data/text_cert"), "rb") as f:
d = f.read()
c1 = certs.Cert.from_pem(d)
assert c1.cn == "google.com"
assert len(c1.altnames) == 436
assert c1.organization == "Google Inc"
assert hash(c1)
with open(tdata.path("mitmproxy/net/data/text_cert_2"), "rb") as f:
d = f.read()
c2 = certs.Cert.from_pem(d)
assert c2.cn == "www.inode.co.nz"
assert len(c2.altnames) == 2
assert c2.fingerprint()
assert c2.notbefore == datetime(
year=2010,
month=1,
day=11,
hour=19,
minute=27,
second=36,
tzinfo=timezone.utc,
)
assert c2.notafter == datetime(
year=2011,
month=1,
day=12,
hour=9,
minute=14,
second=55,
tzinfo=timezone.utc,
)
assert c2.subject
assert c2.keyinfo == ("RSA", 2048)
assert c2.serial
assert c2.issuer
assert c2.to_pem()
assert c2.has_expired() is not None
assert repr(c2) == "<Cert(cn='www.inode.co.nz', altnames=['www.inode.co.nz', 'inode.co.nz'])>"
assert c1 != c2
def test_convert(self, tdata):
with open(tdata.path("mitmproxy/net/data/text_cert"), "rb") as f:
d = f.read()
c = certs.Cert.from_pem(d)
assert c == certs.Cert.from_pem(c.to_pem())
assert c == certs.Cert.from_state(c.get_state())
assert c == certs.Cert.from_pyopenssl(c.to_pyopenssl())
@pytest.mark.parametrize("filename,name,bits", [
("text_cert", "RSA", 1024),
("dsa_cert.pem", "DSA", 1024),
("ec_cert.pem", "EC (secp256r1)", 256),
])
def test_keyinfo(self, tdata, filename, name, bits):
with open(tdata.path(f"mitmproxy/net/data/{filename}"), "rb") as f:
d = f.read()
c = certs.Cert.from_pem(d)
assert c.keyinfo == (name, bits)
def test_err_broken_sans(self, tdata):
with open(tdata.path("mitmproxy/net/data/text_cert_weird1"), "rb") as f:
d = f.read()
c = certs.Cert.from_pem(d)
# This breaks unless we ignore a decoding error.
assert c.altnames is not None
def test_state(self, tdata):
with open(tdata.path("mitmproxy/net/data/text_cert"), "rb") as f:
d = f.read()
c = certs.Cert.from_pem(d)
c.get_state()
c2 = c.copy()
a = c.get_state()
b = c2.get_state()
assert a == b
assert c == c2
assert c is not c2
c2.set_state(a)
assert c == c2
def test_from_store_with_passphrase(self, tdata, tstore):
tstore.add_cert_file("unencrypted-no-pass", Path(tdata.path("mitmproxy/data/testkey.pem")), None)
tstore.add_cert_file("unencrypted-pass", Path(tdata.path("mitmproxy/data/testkey.pem")), b"password")
tstore.add_cert_file("encrypted-pass", Path(tdata.path("mitmproxy/data/mitmproxy.pem")), b"password")
with pytest.raises(TypeError):
tstore.add_cert_file("encrypted-no-pass", Path(tdata.path("mitmproxy/data/mitmproxy.pem")), None)
def test_special_character(self, tdata):
with open(tdata.path("mitmproxy/net/data/text_cert_with_comma"), "rb") as f:
d = f.read()
c = certs.Cert.from_pem(d)
assert dict(c.issuer).get('O') == 'DigiCert, Inc.'
assert dict(c.subject).get('O') == 'GitHub, Inc.'
def test_multi_valued_rdns(self, tdata):
subject = x509.Name([
x509.RelativeDistinguishedName([
x509.NameAttribute(NameOID.TITLE, u'Test'),
x509.NameAttribute(NameOID.COMMON_NAME, u'Multivalue'),
x509.NameAttribute(NameOID.SURNAME, u'RDNs'),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'TSLA'),
]),
x509.RelativeDistinguishedName([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'PyCA')
]),
])
expected = [('2.5.4.12', 'Test'), ('CN', 'Multivalue'), ('2.5.4.4', 'RDNs'), ('O', 'TSLA'), ('O', 'PyCA')]
assert(certs._name_to_keyval(subject)) == expected