From fb6f4f1d24b0e93defc415bb6f7482204c037a13 Mon Sep 17 00:00:00 2001 From: Dirk Wetter Date: Fri, 16 Oct 2026 14:12:29 +0200 Subject: [PATCH 1/4] Fix comment --- utils/create_ca_hashes.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/create_ca_hashes.sh b/utils/create_ca_hashes.sh index 22737f51..14f944f2 100755 --- a/utils/create_ca_hashes.sh +++ b/utils/create_ca_hashes.sh @@ -14,7 +14,7 @@ if [[ ! -e etc ]]; then exit 99 fi -echo "Extracting private key hashes from CA bundles" +echo "Extracting SPKI (public key) hashes from CA bundles" echo -n > "$TEMPDIR/cahashes" for bundle_fname in etc/*.pem; do if [[ ! -r $bundle_fname ]]; then From 0ed8d49692ac3de583d826da6e15f48a000934dd Mon Sep 17 00:00:00 2001 From: Dirk Wetter Date: Fri, 16 Oct 2026 19:32:50 +0200 Subject: [PATCH 2/4] Make t/05_ca_hashes_up_to_date.t work --- t/05_ca_hashes_up_to_date.t | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/t/05_ca_hashes_up_to_date.t b/t/05_ca_hashes_up_to_date.t index 2bba6c9f..bab6b452 100755 --- a/t/05_ca_hashes_up_to_date.t +++ b/t/05_ca_hashes_up_to_date.t @@ -4,14 +4,29 @@ use strict; use warnings; use Test::More; -printf "\n%s\n", "Make sure CA certificate stores are older than their SPKI hashes \"~/etc/ca_hashes.txt\" ..."; +# Verify that etc/ca_hashes.txt is in sync with the CA bundles (etc/*.pem). +# Regenerates the hashes and compares content, rather than relying on +# file modification times (which git does not preserve). -my $newer_bundles=`find etc/*.pem -newer etc/ca_hashes.txt`; +# Regenerate ca_hashes.txt from the .pem bundles +my $rc = system('./utils/create_ca_hashes.sh'); +is($rc, 0, "create_ca_hashes.sh exited successfully"); -#1 -is($newer_bundles,"","Checking if there's an output with a *.pem file. If so: run \"~/utils/create_ca_hashes.sh\""); +# Check whether the regenerated file differs from what is committed. +# Read git's exit status via system() and Perl's $?. Do NOT use a +# backtick `...; echo $?`: Perl interpolates $? as the *previous* +# command's status (the script's, always 0), so the shell would just +# echo 0 and the diff would never be detected. +my $diff_rc = system('git diff --quiet -- etc/ca_hashes.txt 2>/dev/null'); +my $has_diff = (($diff_rc >> 8) == 0) ? "0" : "1"; + +# Restore the original file and remove the .bak the script creates +system('git checkout -- etc/ca_hashes.txt 2>/dev/null'); +unlink 'etc/ca_hashes.txt.bak'; + +is($has_diff, "0", + "ca_hashes.txt is up to date with the .pem bundles (run ./utils/create_ca_hashes.sh if not)"); -printf "\n"; done_testing; From 13111ef2b00db500c89f53fd012ba6abb3aa8228 Mon Sep 17 00:00:00 2001 From: Dirk Wetter Date: Fri, 16 Oct 2026 19:34:47 +0200 Subject: [PATCH 3/4] Add cleanup --- utils/create_ca_hashes.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/create_ca_hashes.sh b/utils/create_ca_hashes.sh index 14f944f2..6386256a 100755 --- a/utils/create_ca_hashes.sh +++ b/utils/create_ca_hashes.sh @@ -6,7 +6,8 @@ # This file generates the file etc/ca_hashes.txt from the (root)certificate # Bundles in etc (etc/*.pem) -TEMPDIR="/tmp" +TEMPDIR=$(mktemp -d) +trap 'rm -rf "$TEMPDIR"' EXIT # Check if we are in the right directory if [[ ! -e etc ]]; then From 06f364f25b19f2f692649c274c2bebc7e545225f Mon Sep 17 00:00:00 2001 From: Dirk Wetter Date: Sat, 17 Oct 2026 17:06:55 +0200 Subject: [PATCH 4/4] Make ca_hashes up-to-date test platform-independent The test compared the regenerated etc/ca_hashes.txt byte-for-byte against the committed file, but two aspects are not stable across platforms: - The name column comes from 'openssl x509 -subject', which formats differently between LibreSSL (macOS) and OpenSSL (Linux). - The line order comes from 'sort', whose collation differs between BSD libc and glibc even under an identically-named locale. Only the SPKI hash (field 1) is deterministic, and it is the sole column testssl.sh consumes. So: - The test now compares the set of SPKI hashes (field 1) between the committed and regenerated files, order-independently, using 'LC_ALL=C sort -u' for a stable, platform-independent collation. - create_ca_hashes.sh pins the name format ('-nameopt compat') and sort collation ('LC_ALL=C') so the generated file is stable. Verified on macOS (LibreSSL) and Ubuntu 24.04 (OpenSSL). --- t/05_ca_hashes_up_to_date.t | 21 ++++++++++++++------- utils/create_ca_hashes.sh | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/t/05_ca_hashes_up_to_date.t b/t/05_ca_hashes_up_to_date.t index bab6b452..4a70d72e 100755 --- a/t/05_ca_hashes_up_to_date.t +++ b/t/05_ca_hashes_up_to_date.t @@ -12,13 +12,20 @@ use Test::More; my $rc = system('./utils/create_ca_hashes.sh'); is($rc, 0, "create_ca_hashes.sh exited successfully"); -# Check whether the regenerated file differs from what is committed. -# Read git's exit status via system() and Perl's $?. Do NOT use a -# backtick `...; echo $?`: Perl interpolates $? as the *previous* -# command's status (the script's, always 0), so the shell would just -# echo 0 and the diff would never be detected. -my $diff_rc = system('git diff --quiet -- etc/ca_hashes.txt 2>/dev/null'); -my $has_diff = (($diff_rc >> 8) == 0) ? "0" : "1"; +# Compare the set of SPKI hashes (field 1) between the committed file and +# the freshly regenerated one, order-independently. We do NOT compare the +# whole file byte-for-byte: the name column (field 2) and the line order +# are cosmetic and are not stable across platforms. The name comes from +# `openssl x509 -subject`, which formats differently between LibreSSL +# (macOS) and OpenSSL (Linux); the order comes from `sort`, whose collation +# differs between BSD libc and glibc even under an identically-named locale. +# Only the SPKI hash is deterministic, and it is the sole column testssl.sh +# consumes (grep -h "$hpkp_spki"), so that is what "up to date" means here. +# LC_ALL=C pins the collation so the set comparison is byte-order and +# platform-independent. +my $committed = `git show HEAD:etc/ca_hashes.txt 2>/dev/null | cut -d' ' -f1 | LC_ALL=C sort -u`; +my $regenerated = `cut -d' ' -f1 etc/ca_hashes.txt 2>/dev/null | LC_ALL=C sort -u`; +my $has_diff = ($committed eq $regenerated) ? "0" : "1"; # Restore the original file and remove the .bak the script creates system('git checkout -- etc/ca_hashes.txt 2>/dev/null'); diff --git a/utils/create_ca_hashes.sh b/utils/create_ca_hashes.sh index 6386256a..6b185a46 100755 --- a/utils/create_ca_hashes.sh +++ b/utils/create_ca_hashes.sh @@ -33,7 +33,7 @@ for bundle_fname in etc/*.pem; do echo -n "." hpkp_key_ca="$( ( openssl x509 -in "$cert_fname" -pubkey -noout | grep -v PUBLIC | openssl base64 -d | openssl dgst -sha256 -binary | openssl enc -base64 ) 2>/dev/null )" - hpkp_name=$( openssl x509 -in "$cert_fname" -subject -noout 2>/dev/null | sed "s/^subject= //") + hpkp_name=$( openssl x509 -in "$cert_fname" -subject -noout -nameopt compat 2>/dev/null | sed "s/^subject= //") if [[ $(echo $hpkp_name|grep 'CN='|wc -l) -eq 1 ]]; then hpkp_name=$(echo -n $hpkp_name|sed 's/^.*CN=//'|sed 's/\/.*$//') fi @@ -45,4 +45,4 @@ done # Make a backup first cp etc/ca_hashes.txt etc/ca_hashes.txt.bak -sort -u "$TEMPDIR/cahashes" > etc/ca_hashes.txt +LC_ALL=C sort -u "$TEMPDIR/cahashes" > etc/ca_hashes.txt