diff --git a/postgres_get_server_cert.py b/postgres_get_server_cert.py new file mode 100755 index 0000000..4f4b114 --- /dev/null +++ b/postgres_get_server_cert.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +import argparse +import socket +import ssl +import struct +import subprocess +import sys +import urlparse + + +def main(): + args = get_args() + target = get_target_address_from_args(args) + sock = socket.create_connection(target) + try: + certificate_as_pem = get_certificate_from_socket(sock) + print certificate_as_pem + except Exception as exc: + sys.stderr.write('Something failed while fetching certificate: %s' % + exc.message) + sys.exit(1) + finally: + sock.close() + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument('database', help='Either an IP address, hostname or' + ' URL with host and port') + return parser.parse_args() + + +def get_target_address_from_args(args): + specified_target = args.database + if '//' not in specified_target: + specified_target = '//' + specified_target + parsed = urlparse.urlparse(specified_target) + return (parsed.hostname, parsed.port or 5432) + + +def get_certificate_from_socket(sock): + request_ssl(sock) + ssl_context = get_ssl_context() + sock = ssl_context.wrap_socket(sock) + sock.do_handshake() + certificate_as_der = sock.getpeercert(binary_form=True) + certificate_as_pem = encode_der_as_pem(certificate_as_der) + return certificate_as_pem + + +def request_ssl(sock): + # 1234.5679 is the magic protocol version used to request TLS, defined + # in pgcomm.h) + version_ssl = postgres_protocol_version_to_binary(1234, 5679) + + packet = '%(length)s%(version)s' % { + 'length': struct.pack('!I', 8), + 'version': version_ssl, + } + sock.sendall(packet) + data = read_n_bytes_from_socket(sock, 1) + if data != 'S': + raise Exception('Backend does not support TLS') + + +def get_ssl_context(): + # Return the strongest SSL context available locally + for proto in ('PROTOCOL_TLSv1_2', 'PROTOCOL_TLSv1', 'PROTOCOL_SSLv23'): + protocol = getattr(ssl, proto, None) + if protocol: + break + return ssl.SSLContext(protocol) + + +def encode_der_as_pem(cert): + # Forking out to openssl to not have to add any dependencies to script, + # preferably you'd do this with pycrypto or other ssl libraries. + cmd = ['openssl', 'x509', '-inform', 'DER'] + pipe = subprocess.PIPE + process = subprocess.Popen(cmd, stdin=pipe, stdout=pipe, stderr=pipe) + stdout, stderr = process.communicate(cert) + if stderr: + raise Exception('openssl errored when converting cert to PEM: %s' % + stderr) + return stdout.strip() + + +def read_n_bytes_from_socket(sock, n): + buf = bytearray(n) + view = memoryview(buf) + while n: + nbytes = sock.recv_into(view, n) + view = view[nbytes:] # slicing views is cheap + n -= nbytes + return str(buf) + + +def postgres_protocol_version_to_binary(major, minor): + return struct.pack('!I', major << 16 | minor) + + +if __name__ == '__main__': + main()