Skip to content

Commit 64b1795

Browse files
committed
tests
1 parent 824f9d6 commit 64b1795

11 files changed

+115
-70
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# showcert - simple OpenSSL for humans
22

3+
[![Run tests and upload coverage](https://github.com/yaroslaff/showcert/actions/workflows/main.yml/badge.svg)]
4+
[![codecov](https://codecov.io/github/yaroslaff/showcert/graph/badge.svg?token=VOACSID3PP)](https://codecov.io/github/yaroslaff/showcert)
5+
36
showcert consist of two CLI utilities: `showcert` itself - all 'read' operations with X.509 certificates and `gencert` - to create certificates for development purposes.
47

58
showcert tries to follow these principles:

pyproject.toml

+7
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,11 @@ cov-report = [
6363
cov = [
6464
"test-cov",
6565
"cov-report",
66+
]
67+
68+
[tool.coverage.report]
69+
exclude_lines = [
70+
"no cov",
71+
"if __name__ == .__main__.:",
72+
"if TYPE_CHECKING:",
6673
]

showcert/cli/gencert_main.py

+20-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import ipaddress
55
import datetime
66
import os
7+
import sys
8+
import ipaddress
79

810
from typing import List
911

@@ -53,7 +55,7 @@ def generate_cert(hostnames: list[str], ip_addresses: list[str] = None,
5355
if ip_addresses:
5456
for addr in ip_addresses:
5557
# openssl wants DNSnames for ips...
56-
alt_names.append(x509.DNSName(addr))
58+
# we add above: alt_names.append(x509.DNSName(addr))
5759
# ... whereas golang's crypto/tls is stricter, and needs IPAddresses
5860
# note: older versions of cryptography do not understand ip_address objects
5961
alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
@@ -196,10 +198,11 @@ def save_key(fh, key: rsa.RSAPrivateKey):
196198
format=serialization.PrivateFormat.TraditionalOpenSSL,
197199
encryption_algorithm=serialization.NoEncryption()))
198200

199-
def main():
201+
def main() -> int:
200202
args = get_args()
201203
ca_privkey = None
202204
ca_cert = None
205+
ipaddresses = list()
203206

204207
certfile = args.cert
205208
keyfile = args.key
@@ -228,10 +231,19 @@ def main():
228231
# ca_privkey = serialization.load_pem_private_key(fh.read(), password=None)
229232

230233

234+
for h in args.hostnames:
235+
try:
236+
ipaddress.ip_address(h)
237+
ipaddresses.append(h)
238+
except ValueError:
239+
pass
240+
241+
231242
cert, key = generate_cert(hostnames = args.hostnames,
232-
days=args.days, bits=args.bits,
233-
cakey=ca_privkey, cacert=ca_cert,
234-
ca=args.ca)
243+
ip_addresses = ipaddresses,
244+
days=args.days, bits=args.bits,
245+
cakey=ca_privkey, cacert=ca_cert,
246+
ca=args.ca)
235247

236248

237249
with open(certfile, "wb") as fh:
@@ -244,5 +256,7 @@ def main():
244256
with open(keyfile, "wb") as fh:
245257
save_key(fh, key)
246258

259+
return 0
260+
247261
if __name__ == '__main__':
248-
main()
262+
sys.exit(main())

showcert/cli/showcert_main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def main():
9393
chain=args.chain,
9494
limit=args.limit)
9595
maxrc = max(maxrc, rc)
96-
except CertException as e:
96+
except (CertException, ValueError) as e:
9797
print("{}: {}".format(cert, e))
9898
maxrc=1
9999
return(maxrc)

showcert/getremote.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,25 @@
88

99
phrase = namedtuple('Phrase', 'say wait expect')
1010

11+
# not covering conversation because it's hard to find server which would produce protocol errors
12+
1113
def conversation(s, script):
1214
verbose = False
1315
for ph in script:
1416
if ph.say is not None:
1517
if verbose:
16-
print(">", repr(ph.say))
18+
print(">", repr(ph.say)) # pragma: no cover
1719
s.sendall(ph.say.encode())
1820
reply = s.recv(2048).decode('utf8')
1921
if verbose:
20-
print("<", repr(reply))
21-
print("wait:", repr(ph.wait))
22-
if ph.wait is not None and ph.wait not in reply:
23-
raise ServerError('Not found {!r} in server reply {!r} to {!r}'.format(ph.wait, reply, ph.say))
24-
if ph.expect is not None and ph.expect not in reply:
25-
raise ServerError('Not found {!r} in server reply {!r} to {!r}'.format(ph.expect, reply, ph.say))
22+
print("<", repr(reply)) # pragma: no cover
23+
print("wait:", repr(ph.wait)) # pragma: no cover
24+
if ph.wait is not None and ph.wait not in reply: # pragma: no cover
25+
raise ServerError('Not found {!r} in server reply {!r} to {!r}'.format(ph.wait, reply, ph.say)) # pragma: no cover
26+
if ph.expect is not None and ph.expect not in reply: # pragma: no cover
27+
raise ServerError('Not found {!r} in server reply {!r} to {!r}'.format(ph.expect, reply, ph.say)) # pragma: no cover
2628
if verbose:
27-
print("got it")
29+
print("got it") # pragma: no cover
2830

2931
def starttls_imap(s):
3032
script = (
@@ -73,6 +75,9 @@ def start_tls(s, method, port):
7375
except KeyError:
7476
# no special handling needed
7577
return
78+
else:
79+
if method not in method_map:
80+
raise ValueError('Unknown starttls method {!r}'.format(method))
7681

7782
return method_map[method](s)
7883

showcert/printcert.py

+6-51
Original file line numberDiff line numberDiff line change
@@ -41,54 +41,6 @@ def get_names(crt: x509.Certificate) -> List[str]:
4141
names.extend([name.value for name in SAN if isinstance(name, x509.DNSName)])
4242
return names
4343

44-
def OLD_get_names(crt):
45-
46-
def tlist2value(tlist, key):
47-
for t in tlist:
48-
if t[0].decode() == key:
49-
return t[1].decode()
50-
51-
def get_SAN(cert):
52-
53-
def safestr(x):
54-
try:
55-
return str(x)
56-
except:
57-
return ''
58-
59-
def get_san_dns(extdata):
60-
for rec in extension_data['subjectAltName'].split(','):
61-
try:
62-
t, v = rec.strip().split(':')
63-
if t == 'DNS':
64-
yield v
65-
except ValueError:
66-
""" /etc/ssl/certs/Izenpe.com.pem has incorrect(?) SAN field """
67-
pass
68-
69-
extensions = (cert.get_extension(i) for i in range(cert.get_extension_count()))
70-
71-
extension_data = {e.get_short_name().decode(): safestr(e) for e in extensions}
72-
73-
try:
74-
return list(get_san_dns(extension_data))
75-
except KeyError:
76-
return [] # No subjectAltName
77-
except IndexError:
78-
raise InvalidCertificate('Unusual certificate, cannot parse SubjectAltName')
79-
80-
subject = tlist2value(crt.get_subject().get_components(), 'CN')
81-
names = get_SAN(crt)
82-
83-
if subject in names:
84-
names.remove(subject)
85-
86-
if subject:
87-
# add only if Subject exists (yes, not always)
88-
names.insert(0, subject)
89-
return names
90-
91-
9244
def is_self_signed(crt: x509.Certificate):
9345
return crt.issuer == crt.subject
9446

@@ -106,13 +58,16 @@ def is_CA(crt: x509.Certificate):
10658
def print_full_cert(crt):
10759
print(dump_certificate(FILETYPE_TEXT, crt).decode())
10860

109-
def print_names(crt):
110-
names = get_names(crt)
61+
def print_names(crt: X509):
62+
# expects openssl crt!
63+
cc = convert_openssl_to_cryptography(crt)
64+
names = get_names(cc)
11165

11266
print(' '.join(names))
11367

11468
def print_dnames(crt):
115-
names = get_names(crt)
69+
cc = convert_openssl_to_cryptography(crt)
70+
names = get_names(cc)
11671
print('-d', ' -d '.join(names))
11772

11873
def hexify(b: bytes) -> str:

showcert/processcert.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def process_cert(CERT, name=None, insecure=False, warn=None, starttls='auto', ou
7272
file=sys.stderr)
7373
return 1
7474
except socket.timeout as e:
75-
print("Timeout connecting to {}".format(CERT), file=sys.stderr)
75+
print("Timeout with {}".format(CERT), file=sys.stderr)
7676
return 1
7777
except socket.gaierror as e:
7878
print("Error with {}: {}".format(CERT, e), file=sys.stderr)

tests/test_gencert_main.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from unittest import mock
2+
from showcert.cli.gencert_main import main
3+
4+
def test_cacert():
5+
with mock.patch('sys.argv', ['gencert_main.py',
6+
'--ca', '--cert', '/tmp/ca.pem', '--key', '/tmp/ca-priv.pem', "My CA"]):
7+
code = main()
8+
assert code == 0
9+
10+
11+
def test_cacert_combined():
12+
with mock.patch('sys.argv', ['gencert_main.py',
13+
'--ca', '--cert', '/tmp/ca2.pem', "My CA"]):
14+
code = main()
15+
assert code == 0
16+
17+
18+
def test_cert():
19+
with mock.patch('sys.argv', ['gencert_main.py',
20+
'--cacert', '/tmp/ca.pem', '--cakey', '/tmp/ca-priv.pem', 'example.com', 'www.example.com', '0.0.0.1']):
21+
code = main()
22+
assert code == 0
23+
24+
def test_cert_combined():
25+
with mock.patch('sys.argv', ['gencert_main.py',
26+
'--cacert', '/tmp/ca2.pem', 'example.com', 'www.example.com']):
27+
code = main()
28+
assert code == 0

tests/test_local.py

+10
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,13 @@ def test_ca(self):
1818
for ca in self.ca_certs:
1919
rc = process_cert(CERT=ca)
2020
assert(rc == 0)
21+
22+
def test_print(self):
23+
rc = process_cert(CERT=self.ca_certs[0], output='full')
24+
assert(rc == 0)
25+
rc = process_cert(CERT=self.ca_certs[0], output='ext')
26+
assert(rc == 0)
27+
rc = process_cert(CERT=self.snakeoil, output='names', insecure=True)
28+
assert(rc == 0)
29+
rc = process_cert(CERT=self.snakeoil, output='dnames', insecure=True)
30+
assert(rc == 0)

tests/test_remote.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class TestShowcertRemote():
1212
pop3s_sites = ['pop.yandex.ru:995', 'pop.gmail.com:995']
1313
imap_sites = ['imap.yandex.ru:143']
1414
imaps_sites = ['imap.yandex.ru:993', 'imap.gmail.com:993']
15+
smtp_sites = ['smtp.yandex.ru:25']
1516

1617
def test_https(self):
1718
for site in self.sites:
@@ -40,6 +41,11 @@ def test_badssl_ignore(self):
4041
code = process_cert(CERT=site, insecure=True)
4142
assert code == 0
4243

44+
def test_smtp(self):
45+
for site in self.smtp_sites:
46+
code = process_cert(CERT=site)
47+
assert code == 0
48+
4349
def test_pop3(self):
4450
for site in self.pop3_sites:
4551
code = process_cert(CERT=site)
@@ -67,4 +73,3 @@ def test_timeout(self):
6773
print("code:", code)
6874
assert code == 1
6975
assert test_end - test_start >= 1
70-

tests/test_showcert_main.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
from unittest import mock
22
from showcert.cli.showcert_main import main
33

4+
5+
snakeoil = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
6+
47
def test_main():
58
with mock.patch('sys.argv', ['showcert_main.py', 'github.com']):
6-
main()
7-
9+
code = main()
10+
assert code == 0
11+
12+
def test_main_le():
13+
with mock.patch('sys.argv', ['showcert_main.py', ':le']):
14+
code = main()
15+
assert code == 0
16+
17+
def test_main_snakeoil():
18+
with mock.patch('sys.argv', ['showcert_main.py', snakeoil]):
19+
code = main()
20+
assert code == 1
21+
22+
def test_main_notacert():
23+
with mock.patch('sys.argv', ['showcert_main.py', "/etc/fstab"]):
24+
code = main()
25+
assert code == 1

0 commit comments

Comments
 (0)