Hold Tight...

0 %
Driving cybersecurity excellence
Innovator behind Kodachi OS
  • Residence:
    ::0
  • Uptime Binary:
    110001
  • Mantra:
    Innovate, Secure, Repeat
ONS
EEDS
NSSG
Cybersecurity
Programming & R&D
Cryptocurrency
AI & vibe coding
  • Bash
  • Gambas
  • Delphi
  • PHP
  • Visual Basic
  • Rust

Extracting SSL Fingerprints

13/05/2013
2 min readDeep Tech6 code examplesUpdated October 2026

Key points

  • Three scripts, in Perl, shell and PHP, that read the SSL fingerprint of any site’s certificate from its URL.
  • SHA-1 is no longer a safe fingerprint: use the SHA-256 versions under each script.
  • Certificates now change often, so a fingerprint you pinned goes stale quickly.

Extracting SSL Fingerprints: My Custom Scripts

Updated October 2026: These scripts for extracting SSL fingerprints still mostly run, but three things changed since 2013:

  • SHA-1 is no longer a safe fingerprint: researchers produced a chosen-prefix SHA-1 collision in 2020. Compare SHA-256 fingerprints. OpenSSL 3.5 still prints SHA-1 unless you add -sha256, and my Perl and PHP scripts ask for SHA-1 too; corrected lines are under each script.
  • On OpenSSL 3 the Perl script can crash on servers that close the connection without a TLS close_notify (google.com, its default, is one), because get_https3 then returns no certificate. The corrected Perl lines check for that.
  • Certificates now change often: the CA/Browser Forum cut the maximum lifetime of public TLS certificates to 200 days in March 2026, falling to 47 days by 2029, so a changed fingerprint alone is not proof of an attack. Compare from several locations, as SSLEye did, or look the certificate up in Certificate Transparency logs such as crt.sh.

Why I wrote my own SSL fingerprint scripts

I was looking for a script that can extract the SSL fingerprint from any SSL certificate, given you have the URL. I found a few scripts, but none of them did exactly what I expected, so I decided to write my own based on what I found. I created three versions of the script: one in Perl, one as a Linux shell script, and one in PHP. Note that all of them perform the same task and produce the same results.

The next plan was to develop an automated standalone application that could perform this task from multiple locations automatically (SSLEye, now one of my archived projects). It’s important to understand that secure browser connections can be intercepted and decrypted by anyone (Man-in-the-Middle, MITM) who could spoof the authentic site’s certificate and act on your behalf. This also allows them to read your traffic in clear text. Fortunately, the authentic site’s SHA-256 fingerprint cannot be duplicated, which is the main advantage of having such an application.

Perl script

Perl
#!/usr/bin/perl
# Perl SSL Fingerprint Checker written by Warith AL Maawali  
# (c) 2013 Warith AL Maawali
# https://www.digi77.com
# http://www.om77.net
# script starts here:
# Usage: $perl sslf.pl -d yourdomain
# Example: $perl sslf.pl -d google.com

#libs used
use Net::SSLeay qw(get_https3);
use Getopt::Std;

# get args
getopts("o:i:d:s:e:hvb", \%args);

# set our input vars to easy names
$domain = $args{d};

# gotta have at least the domain and log file
if (!$args{d}) {
 print "\t Domain is blank google.com will be used\n\n";
 $domain ="google.com";

}

$host= $domain;
$port = 443;

($p, $resp, $hdrs, $server_cert) = get_https3($host, $port, '/');
#get finger print
print Net::SSLeay::X509_get_fingerprint($server_cert, "sha1");
print "\n";
Perl

For OpenSSL 3, replace the last lines of the Perl script with these: they stop with a message instead of crashing when no certificate comes back, and print the SHA-256 fingerprint:

# Perl: check that a certificate came back (on OpenSSL 3 get_https3 returns none when the server
# closes without a TLS close_notify, and X509_get_fingerprint then crashes), and ask for SHA-256
($p, $resp, $hdrs, $server_cert) = get_https3($host, $port, '/');
die "No certificate received from $host\n" unless $server_cert;
print Net::SSLeay::X509_get_fingerprint($server_cert, "sha256");
print "\n";
Download

Shell script

ShellScript
#!/bin/bash

# =========================================
# SSL Fingerprint Checker
# =========================================
#
# Version: 1.5
# Script written by Warith AL Maawali
#
# Discord channel: https://discord.gg/KEFErEx
# Twitter: http://twitter.com/warith2020
# Linkedin: http://www.linkedin.com/in/warith1977
# Website: https://www.digi77.com
# (c) 2024
#
# Description:
# This script retrieves the SSL fingerprint for one or multiple domains.
# It supports reading domains from command-line arguments or from a file (.txt or .csv).
# Includes fallback mechanisms and improved error handling for stability.
# Supports output in text or JSON format via the -o option without altering terminal output.
#
# Usage: ./sslfp.sh [options]
#
# Usage Examples:
# Check a Single Domain:
#   ./sslfp.sh -d google.com
# Check Multiple Domains via Command-Line:
#   ./sslfp.sh -d google.com -d example.com -d github.com
# Check Multiple Domains from a File:
#   Create a domains.txt file:
#     google.com
#     example.com
#     github.com
#
#   Run the script:
#     ./sslfp.sh -f domains.txt
# Output in JSON format to a file:
#   ./sslfp.sh -d google.com -o json
# Display Help:
#   ./sslfp.sh -h
# =========================================

# Enable debug mode (uncomment the next line for debugging)
# set -x

# Constants
SSL_PORT=443
RETRY_COUNT=3
TIMEOUT=15
DEFAULT_OUTPUT_FORMAT="text"

# Colors for terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

# Declare domains array globally
declare -a domains=()

# Functions

# Display usage instructions
usage() {
    cat <<EOF
Usage: $0 [options]

Options:
  -d DOMAIN       Specify a single domain to check. Can be used multiple times.
  -f FILE         Specify a file containing a list of domains (one per line).
  -o FORMAT       Specify output format for the file: 'text' or 'json'. Defaults to 'text'.
  -h, --help      Display this help message.

Examples:
  $0 -d google.com
  $0 -f domains.txt
  $0 -d google.com -o json
EOF
    exit 1
}

# Check if openssl is installed, if not, install it
install_openssl() {
    echo -e "${YELLOW}openssl is not installed. Installing now...${NC}"
    if command -v apt-get &>/dev/null; then
        sudo apt-get update
        sudo apt-get install -y openssl
    elif command -v yum &>/dev/null; then
        sudo yum install -y openssl
    else
        echo -e "${RED}Package manager not supported. Please install openssl manually.${NC}"
        exit 1
    fi

    if ! command -v openssl &>/dev/null; then
        echo -e "${RED}Failed to install openssl. Please install it manually and run this script again.${NC}"
        exit 1
    fi
}

# Function to fetch SSL fingerprint using openssl
get_fingerprint_openssl() {
    local domain="$1"
    local port="$2"
    local fingerprint

    fingerprint=$(timeout "${TIMEOUT}" bash -c "echo | openssl s_client -connect '${domain}:${port}' -servername '${domain}' 2>/dev/null" |
        openssl x509 -noout -fingerprint 2>/dev/null | cut -d'=' -f2)

    echo "$fingerprint"
}

# Function to fetch SSL fingerprint with retry mechanism
get_fingerprint() {
    local domain="$1"
    local port="$2"
    local attempt=1
    local fingerprint

    while [ "$attempt" -le "$RETRY_COUNT" ]; do
        fingerprint=$(get_fingerprint_openssl "$domain" "$port")
        if [ -n "$fingerprint" ]; then
            echo "$fingerprint"
            return 0
        else
            echo -e "${YELLOW}Attempt $attempt: Failed to retrieve fingerprint for $domain. Retrying...${NC}" >&2
            attempt=$((attempt + 1))
            sleep 1
        fi
    done

    return 1
}

# Parse command-line arguments
parse_arguments() {
    if [ "$#" -eq 0 ]; then
        usage
    fi

    OUTPUT_FORMAT="$DEFAULT_OUTPUT_FORMAT"

    while [[ "$#" -gt 0 ]]; do
        case "$1" in
            -d)
                if [ -n "$2" ] && [[ ! "$2" =~ ^- ]]; then
                    domains+=("$2")
                    shift 2
                else
                    echo -e "${RED}ERROR: -d requires a non-empty argument.${NC}"
                    usage
                fi
                ;;
            -f)
                if [ -n "$2" ] && [[ ! "$2" =~ ^- ]]; then
                    if [ -f "$2" ]; then
                        while IFS= read -r line || [ -n "$line" ]; do
                            # Trim whitespace
                            line=$(echo "$line" | xargs)
                            # Skip empty lines and comments
                            [[ -z "$line" || "$line" =~ ^# ]] && continue
                            domains+=("$line")
                        done < "$2"
                    else
                        echo -e "${RED}ERROR: File '$2' not found.${NC}"
                        exit 1
                    fi
                    shift 2
                else
                    echo -e "${RED}ERROR: -f requires a valid file path.${NC}"
                    usage
                fi
                ;;
            -o)
                if [ -n "$2" ] && [[ ! "$2" =~ ^- ]]; then
                    if [[ "$2" == "text" || "$2" == "json" ]]; then
                        OUTPUT_FORMAT="$2"
                    else
                        echo -e "${RED}ERROR: Invalid output format. Use 'text' or 'json'.${NC}"
                        usage
                    fi
                    shift 2
                else
                    echo -e "${RED}ERROR: -o requires a valid format (text or json).${NC}"
                    usage
                fi
                ;;
            -h|--help)
                usage
                ;;
            *)
                echo -e "${RED}ERROR: Unknown option: $1${NC}"
                usage
                ;;
        esac
    done

    # Remove duplicate domains
    unique_domains=($(printf "%s\n" "${domains[@]}" | sort -u))
    domains=("${unique_domains[@]}")

    # Check if domains are provided
    if [ "${#domains[@]}" -eq 0 ]; then
        echo -e "${RED}ERROR: No domains provided.${NC}"
        usage
    fi

    # Debugging statement (optional)
    # echo "DEBUG: OUTPUT_FORMAT is '$OUTPUT_FORMAT'"
}

# Output results to terminal with colors
output_to_terminal() {
    local domains_ref=("${!1}")
    declare -n results_ref=$2

    # Output header
    echo -e "${GREEN}Domain                         SSL Fingerprint${NC}"
    echo -e "${GREEN}------                         --------------${NC}"

    for domain in "${domains_ref[@]}"; do
        fingerprint="${results_ref[$domain]}"
        if [ "$fingerprint" != "FAILED" ]; then
            printf "%-30s ${GREEN}%-20s${NC}\n" "$domain" "$fingerprint"
        else
            printf "%-30s ${RED}%-20s${NC}\n" "$domain" "$fingerprint"
        fi
    done
}

# Save results to a file in specified format
save_to_file() {
    local domains_ref=("${!1}")
    declare -n results_ref=$2
    local format="$3"

    timestamp=$(date +%Y%m%d_%H%M%S)

    if [ "$format" == "json" ]; then
        output_file="ssl_fingerprints_${timestamp}.json"
        {
            echo "{"
            echo '  "domains": ['
            total=${#domains_ref[@]}
            for ((i=0; i < total; i++)); do
                domain="${domains_ref[$i]}"
                fingerprint="${results_ref[$domain]}"
                echo "    {"
                echo "      \"domain\": \"${domain}\","
                if [ "$fingerprint" != "FAILED" ]; then
                    echo "      \"fingerprint\": \"${fingerprint}\""
                else
                    echo "      \"fingerprint\": \"FAILED\""
                fi
                if [ "$i" -lt "$((total - 1))" ]; then
                    echo "    },"
                else
                    echo "    }"
                fi
            done
            echo '  ]'
            echo "}"
        } > "$output_file"
    elif [ "$format" == "text" ]; then
        output_file="ssl_fingerprints_${timestamp}.txt"
        {
            echo "Domain                         SSL Fingerprint"
            echo "------                         --------------"
            for domain in "${domains_ref[@]}"; do
                fingerprint="${results_ref[$domain]}"
                printf "%-30s %-20s\n" "$domain" "$fingerprint"
            done
        } > "$output_file"
    else
        echo -e "${RED}ERROR: Unsupported format '$format'. Use 'text' or 'json'.${NC}"
        exit 1
    fi

    echo -e "\n${GREEN}Results saved to $output_file${NC}"
}

# Main Execution

# Check if openssl is installed
if ! command -v openssl &>/dev/null; then
    install_openssl
fi

# Parse arguments and set global variables
parse_arguments "$@"

# Debugging statement (optional)
# echo "DEBUG: Parsed domains: ${domains[@]}"
# echo "DEBUG: Output format: $OUTPUT_FORMAT"

# Declare an associative array to store results
declare -A results

# Fetch fingerprints
for domain in "${domains[@]}"; do
    fingerprint=$(get_fingerprint "$domain" "$SSL_PORT")
    if [ "$?" -eq 0 ]; then
        results["$domain"]="$fingerprint"
    else
        results["$domain"]="FAILED"
    fi
done

# Output to terminal
output_to_terminal domains[@] results

# Save results to a file in the specified format
save_to_file domains[@] results "$OUTPUT_FORMAT"
ShellScript

OpenSSL 3.5 still prints a SHA-1 fingerprint by default; add -sha256 to the openssl x509 command in the script, as in this one-liner:

# OpenSSL still prints a SHA-1 fingerprint unless you ask for SHA-256
echo | openssl s_client -connect google.com:443 -servername google.com 2>/dev/null | openssl x509 -noout -fingerprint -sha256
Download

PHP script

PHP
<?php 

# PHP SSL Fingerprint Checker written by Warith AL Maawali  
# (c) 2013 Warith AL Maawali
# https://www.digi77.com
# http://www.om77.net
# script starts here:
# Usage: http://www.yourdomain.com/sslf.php
# Example: https://www.digi77.com/software/fingerprint/fp-public.php?hosts=www.facebook.com

//avoid timeouts
set_time_limit(0);

//For String variable use prevent sql injections
function StringInputCleaner($data)
{
    $data = trim($data); 
    $data = stripslashes($data); 
    $data=(filter_var($data, FILTER_SANITIZE_STRING));
    return $data;
}    

function getSllCertificate($hostname, $port = 443)
{
    $context = stream_context_create(array("ssl" => array("capture_peer_cert" => true)));
    $socket = @stream_socket_client("ssl://$hostname:$port", $errno, $errstr, ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT, $context);

    if(!$socket)
        return array("md5" => "error", "sha1" => "error");

    $contextdata = stream_context_get_params($socket);
    $contextparams = $contextdata['options']['ssl']['peer_certificate'];

    fclose($socket);

    openssl_x509_export($contextparams, $cert, true);
    openssl_x509_free($contextparams);

    $repl = array("\r", "\n", "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
    $repw = array("", "", "", "");

    $cert = str_replace($repl, $repw, $cert);

    $decoded = base64_decode($cert);
    $fingerprints = array(
        "md5" => md5($decoded),
        "sha1" => sha1($decoded),
    );


    return $fingerprints ;
}

$host=$_REQUEST['hosts'];
//clean string safer coding
$host=StringInputCleaner($host);
$port=443;
$hashes = getSllCertificate($host, $port);

print_r($hashes['sha1']);


?>
PHP

A PHP 8 version of the same function, with the host name validated and a SHA-256 result:

<?php
// PHP 8 version: FILTER_SANITIZE_STRING (deprecated in 8.1) and openssl_x509_free() (deprecated in 8.0) are gone,
// the host name is validated, and openssl_x509_fingerprint() hashes the certificate (SHA-256 by default here)
function getSslFingerprint(string $hostname, int $port = 443, string $algo = 'sha256'): string
{
    $context = stream_context_create(['ssl' => ['capture_peer_cert' => true]]);
    $socket = @stream_socket_client("ssl://$hostname:$port", $errno, $errstr, 15, STREAM_CLIENT_CONNECT, $context);
    if (!$socket) {
        return 'error';
    }
    $cert = stream_context_get_params($socket)['options']['ssl']['peer_certificate'];
    fclose($socket);
    return openssl_x509_fingerprint($cert, $algo) ?: 'error';
}

$host = (string) filter_input(INPUT_GET, 'hosts', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
echo $host === '' ? 'Invalid host name' : htmlspecialchars(getSslFingerprint($host));
Download

Posted in Deep TechTags: