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_https3then 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
#!/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";PerlFor 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";
Shell script
#!/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"
ShellScriptOpenSSL 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
PHP script
<?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']);
?>PHPA 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));