Skip to content

Commit

Permalink
tests
Browse files Browse the repository at this point in the history
  • Loading branch information
yaroslaff committed Feb 19, 2025
1 parent 824f9d6 commit 64b1795
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 70 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# showcert - simple OpenSSL for humans

[![Run tests and upload coverage](https://github.com/yaroslaff/showcert/actions/workflows/main.yml/badge.svg)]
[![codecov](https://codecov.io/github/yaroslaff/showcert/graph/badge.svg?token=VOACSID3PP)](https://codecov.io/github/yaroslaff/showcert)

showcert consist of two CLI utilities: `showcert` itself - all 'read' operations with X.509 certificates and `gencert` - to create certificates for development purposes.

showcert tries to follow these principles:
Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,11 @@ cov-report = [
cov = [
"test-cov",
"cov-report",
]

[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
26 changes: 20 additions & 6 deletions showcert/cli/gencert_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import ipaddress
import datetime
import os
import sys
import ipaddress

from typing import List

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

def main():
def main() -> int:
args = get_args()
ca_privkey = None
ca_cert = None
ipaddresses = list()

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


for h in args.hostnames:
try:
ipaddress.ip_address(h)
ipaddresses.append(h)
except ValueError:
pass


cert, key = generate_cert(hostnames = args.hostnames,
days=args.days, bits=args.bits,
cakey=ca_privkey, cacert=ca_cert,
ca=args.ca)
ip_addresses = ipaddresses,
days=args.days, bits=args.bits,
cakey=ca_privkey, cacert=ca_cert,
ca=args.ca)


with open(certfile, "wb") as fh:
Expand All @@ -244,5 +256,7 @@ def main():
with open(keyfile, "wb") as fh:
save_key(fh, key)

return 0

if __name__ == '__main__':
main()
sys.exit(main())
2 changes: 1 addition & 1 deletion showcert/cli/showcert_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def main():
chain=args.chain,
limit=args.limit)
maxrc = max(maxrc, rc)
except CertException as e:
except (CertException, ValueError) as e:
print("{}: {}".format(cert, e))
maxrc=1
return(maxrc)
Expand Down
21 changes: 13 additions & 8 deletions showcert/getremote.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@

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

# not covering conversation because it's hard to find server which would produce protocol errors

def conversation(s, script):
verbose = False
for ph in script:
if ph.say is not None:
if verbose:
print(">", repr(ph.say))
print(">", repr(ph.say)) # pragma: no cover
s.sendall(ph.say.encode())
reply = s.recv(2048).decode('utf8')
if verbose:
print("<", repr(reply))
print("wait:", repr(ph.wait))
if ph.wait is not None and ph.wait not in reply:
raise ServerError('Not found {!r} in server reply {!r} to {!r}'.format(ph.wait, reply, ph.say))
if ph.expect is not None and ph.expect not in reply:
raise ServerError('Not found {!r} in server reply {!r} to {!r}'.format(ph.expect, reply, ph.say))
print("<", repr(reply)) # pragma: no cover
print("wait:", repr(ph.wait)) # pragma: no cover
if ph.wait is not None and ph.wait not in reply: # pragma: no cover
raise ServerError('Not found {!r} in server reply {!r} to {!r}'.format(ph.wait, reply, ph.say)) # pragma: no cover
if ph.expect is not None and ph.expect not in reply: # pragma: no cover
raise ServerError('Not found {!r} in server reply {!r} to {!r}'.format(ph.expect, reply, ph.say)) # pragma: no cover
if verbose:
print("got it")
print("got it") # pragma: no cover

def starttls_imap(s):
script = (
Expand Down Expand Up @@ -73,6 +75,9 @@ def start_tls(s, method, port):
except KeyError:
# no special handling needed
return
else:
if method not in method_map:
raise ValueError('Unknown starttls method {!r}'.format(method))

return method_map[method](s)

Expand Down
57 changes: 6 additions & 51 deletions showcert/printcert.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,54 +41,6 @@ def get_names(crt: x509.Certificate) -> List[str]:
names.extend([name.value for name in SAN if isinstance(name, x509.DNSName)])
return names

def OLD_get_names(crt):

def tlist2value(tlist, key):
for t in tlist:
if t[0].decode() == key:
return t[1].decode()

def get_SAN(cert):

def safestr(x):
try:
return str(x)
except:
return ''

def get_san_dns(extdata):
for rec in extension_data['subjectAltName'].split(','):
try:
t, v = rec.strip().split(':')
if t == 'DNS':
yield v
except ValueError:
""" /etc/ssl/certs/Izenpe.com.pem has incorrect(?) SAN field """
pass

extensions = (cert.get_extension(i) for i in range(cert.get_extension_count()))

extension_data = {e.get_short_name().decode(): safestr(e) for e in extensions}

try:
return list(get_san_dns(extension_data))
except KeyError:
return [] # No subjectAltName
except IndexError:
raise InvalidCertificate('Unusual certificate, cannot parse SubjectAltName')

subject = tlist2value(crt.get_subject().get_components(), 'CN')
names = get_SAN(crt)

if subject in names:
names.remove(subject)

if subject:
# add only if Subject exists (yes, not always)
names.insert(0, subject)
return names


def is_self_signed(crt: x509.Certificate):
return crt.issuer == crt.subject

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

def print_names(crt):
names = get_names(crt)
def print_names(crt: X509):
# expects openssl crt!
cc = convert_openssl_to_cryptography(crt)
names = get_names(cc)

print(' '.join(names))

def print_dnames(crt):
names = get_names(crt)
cc = convert_openssl_to_cryptography(crt)
names = get_names(cc)
print('-d', ' -d '.join(names))

def hexify(b: bytes) -> str:
Expand Down
2 changes: 1 addition & 1 deletion showcert/processcert.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def process_cert(CERT, name=None, insecure=False, warn=None, starttls='auto', ou
file=sys.stderr)
return 1
except socket.timeout as e:
print("Timeout connecting to {}".format(CERT), file=sys.stderr)
print("Timeout with {}".format(CERT), file=sys.stderr)
return 1
except socket.gaierror as e:
print("Error with {}: {}".format(CERT, e), file=sys.stderr)
Expand Down
28 changes: 28 additions & 0 deletions tests/test_gencert_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from unittest import mock
from showcert.cli.gencert_main import main

def test_cacert():
with mock.patch('sys.argv', ['gencert_main.py',
'--ca', '--cert', '/tmp/ca.pem', '--key', '/tmp/ca-priv.pem', "My CA"]):
code = main()
assert code == 0


def test_cacert_combined():
with mock.patch('sys.argv', ['gencert_main.py',
'--ca', '--cert', '/tmp/ca2.pem', "My CA"]):
code = main()
assert code == 0


def test_cert():
with mock.patch('sys.argv', ['gencert_main.py',
'--cacert', '/tmp/ca.pem', '--cakey', '/tmp/ca-priv.pem', 'example.com', 'www.example.com', '0.0.0.1']):
code = main()
assert code == 0

def test_cert_combined():
with mock.patch('sys.argv', ['gencert_main.py',
'--cacert', '/tmp/ca2.pem', 'example.com', 'www.example.com']):
code = main()
assert code == 0
10 changes: 10 additions & 0 deletions tests/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,13 @@ def test_ca(self):
for ca in self.ca_certs:
rc = process_cert(CERT=ca)
assert(rc == 0)

def test_print(self):
rc = process_cert(CERT=self.ca_certs[0], output='full')
assert(rc == 0)
rc = process_cert(CERT=self.ca_certs[0], output='ext')
assert(rc == 0)
rc = process_cert(CERT=self.snakeoil, output='names', insecure=True)
assert(rc == 0)
rc = process_cert(CERT=self.snakeoil, output='dnames', insecure=True)
assert(rc == 0)
7 changes: 6 additions & 1 deletion tests/test_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class TestShowcertRemote():
pop3s_sites = ['pop.yandex.ru:995', 'pop.gmail.com:995']
imap_sites = ['imap.yandex.ru:143']
imaps_sites = ['imap.yandex.ru:993', 'imap.gmail.com:993']
smtp_sites = ['smtp.yandex.ru:25']

def test_https(self):
for site in self.sites:
Expand Down Expand Up @@ -40,6 +41,11 @@ def test_badssl_ignore(self):
code = process_cert(CERT=site, insecure=True)
assert code == 0

def test_smtp(self):
for site in self.smtp_sites:
code = process_cert(CERT=site)
assert code == 0

def test_pop3(self):
for site in self.pop3_sites:
code = process_cert(CERT=site)
Expand Down Expand Up @@ -67,4 +73,3 @@ def test_timeout(self):
print("code:", code)
assert code == 1
assert test_end - test_start >= 1

22 changes: 20 additions & 2 deletions tests/test_showcert_main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
from unittest import mock
from showcert.cli.showcert_main import main


snakeoil = '/etc/ssl/certs/ssl-cert-snakeoil.pem'

def test_main():
with mock.patch('sys.argv', ['showcert_main.py', 'github.com']):
main()

code = main()
assert code == 0

def test_main_le():
with mock.patch('sys.argv', ['showcert_main.py', ':le']):
code = main()
assert code == 0

def test_main_snakeoil():
with mock.patch('sys.argv', ['showcert_main.py', snakeoil]):
code = main()
assert code == 1

def test_main_notacert():
with mock.patch('sys.argv', ['showcert_main.py', "/etc/fstab"]):
code = main()
assert code == 1

0 comments on commit 64b1795

Please sign in to comment.