Was That Key Copied

December 9, 2021

 

By Jonathan Landis

Practice Lead, Technical Security Services

Certificate management takes work. It takes less work if you are using the ACME protocol with something like Certbot from the Let’s Encrypt project. But not everybody does automated certificate issuance/renewal, and when something takes manual work, some people will use shortcuts. In the case of certificate management, the shortest possible shortcut would be to create a single wildcard certificate for the whole domain and then copy the certificate and private key to every server that needs one. This could be done through homegrown automation for example.

This shortcut has disadvantages. Copying a private key exposes it to risk of disclosure. The risk increases the more times it is copied and the more places it is stored. It is considered best practice to avoid copying private keys whenever possible. And now 2 paragraphs in we get to the point: during a security assessment we would like a way to detect whether a key has been copied between systems, because operations teams may take shortcuts that expose the organization to risk.

An obvious solution is to fetch certificates and check fingerprints. They are also sometimes called thumbprints. These fingerprints are hashes of the entire certificate and are meant as a short string to uniquely identify the certificate. The fingerprint isn’t part of the certificate itself. It isn’t a signature and the value depends on what hashing algorithm was used. The TLS auditing tool sslabs.com uses sha256 to compute fingerprints for example, even though sha1 and md5 are more common.

If all we wanted to do was detect copying of certificates, we could fetch certificates and compute hashes. We don’t even have to hash them in the same way as ssllabs.com or anyone else assuming all we want to do is compare them with other certificates we find. But that isn’t quite the heart of the matter. It would be possible to use the same private key with 2 different certificates and the fingerprint method would not detect reuse of the private key. We really want to know whether the actual key has been copied.

To do that we have to dig into the certificate and get the public key, because the public key is uniquely associated with the private key. If we find a public key used in different places, we know the private key has been copied. Unfortunately, existing tools for scanning TLS-enabled services such as the nmap ssl-certs script do not directly provide information about the public key. We couldn’t find a solution, so we created one.

The Python script we created connects to a TLS service and computes a hash of just the DER-encoded public key in the server’s certificate. The hash computation algorithm is the same one used to compute key pins in the now-deprecated HTTP Public Key Pinning protocol, so we can use it to compute pins as well if needed. We hope you find it useful!

 

#you may have to do: pip install cryptography

import socket
import ssl
import base64
import sys

from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes

def fetch_key(hostname, port):
    with socket.create_connection((hostname, port)) as sock:
        context = ssl.create_default_context()
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            der = ssock.getpeercert(binary_form=True)
            cert = x509.load_der_x509_certificate(der)
            return cert.public_key()

def compute_pin(pubkey):
    pubkey_bytes = pubkey.public_bytes(
        encoding = serialization.Encoding.DER,
        format = serialization.PublicFormat.SubjectPublicKeyInfo
    )
    sha2 = hashes.Hash(hashes.SHA256())
    sha2.update(pubkey_bytes)
    hash_bytes = sha2.finalize()
    return base64.b64encode(hash_bytes)

if __name__ == “__main__”:
    if len(sys.argv) < 2:
        print(“Specify hostname and optional port, defaults to 443”)
        exit(1)
    host = sys.argv[1]
    if len(sys.argv) >= 3:
        port = int(sys.argv[2])
    else:
        port = 443
    pubkey = fetch_key(host, port)
    pin = compute_pin(pubkey)
    print(pin)

Previous
Previous

Why Application Security Matters: A Developer's Guide

Next
Next

Security for Enterprise Transitions