From 045f93c49e54a7c86309dc983837a7645fbbcbaa Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Wed, 22 Jul 2026 16:23:05 +0200 Subject: [PATCH] Cover unsafe TLS material reuse --- contrib/testing/test-config-tls-material.sh | 177 ++++++++++++++++++++ docs/release-testing-0.7.md | 2 +- docs/test-evidence/0.7-r1.md | 30 ++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100755 contrib/testing/test-config-tls-material.sh diff --git a/contrib/testing/test-config-tls-material.sh b/contrib/testing/test-config-tls-material.sh new file mode 100755 index 0000000..c10c0ec --- /dev/null +++ b/contrib/testing/test-config-tls-material.sh @@ -0,0 +1,177 @@ +#!/bin/sh +# This program is free software, licensed under the terms of the GNU GPL. +# See the Bongo COPYING file for full details. +# Copyright (c) 2026 Bongo Project contributors + +set -eu +export LC_ALL=C + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 BONGO_CONFIG TEMPORARY_SSL_DIRECTORY" >&2 + exit 64 +fi + +binary=$1 +directory=$2 +case "$directory" in + /tmp/*) ;; + *) + echo "Refusing a TLS test directory outside /tmp: $directory" >&2 + exit 64 + ;; +esac +if [ ! -x "$binary" ]; then + echo "Not an executable bongo-config test binary: $binary" >&2 + exit 64 +fi + +certificate="$directory/server.crt" +key="$directory/server.key" +good_certificate="$directory/server.crt.test-good" +good_key="$directory/server.key.test-good" +wrong_key="$directory/server.key.test-wrong" +for path in "$certificate" "$key" "$good_certificate" "$good_key" "$wrong_key"; do + if [ -e "$path" ] || [ -L "$path" ]; then + echo "Refusing to replace pre-existing test input: $path" >&2 + exit 64 + fi +done +mkdir -p "$directory" + +cleanup() +{ + rm -f "$certificate" "$key" "$good_certificate" "$good_key" "$wrong_key" +} +trap cleanup EXIT HUP INT TERM + +run_crypto() +{ + unshare --user --map-root-user "$binary" \ + --hostname mail.bongo.test \ + --tls-name mail.bongo.test \ + --tls-name smtp.bongo.test crypto +} + +restore_pair() +{ + rm -f "$certificate" "$key" + cp "$good_certificate" "$certificate" + cp "$good_key" "$key" + chmod 0644 "$certificate" + chmod 0640 "$key" +} + +expect_failure() +{ + label=$1 + expected=$2 + before=$(sha256sum "$certificate" 2>/dev/null || true) + before="$before|$(sha256sum "$key" 2>/dev/null || true)" + set +e + output=$(run_crypto 2>&1) + status=$? + set -e + after=$(sha256sum "$certificate" 2>/dev/null || true) + after="$after|$(sha256sum "$key" 2>/dev/null || true)" + if [ "$status" -ne 6 ] || + ! printf '%s' "$output" | grep -F "$expected" >/dev/null || + [ "$before" != "$after" ]; then + printf 'FAIL %s: status=%s output=%s\n' "$label" "$status" "$output" >&2 + exit 1 + fi + printf 'PASS %s: rejected without changing input files\n' "$label" +} + +run_crypto | grep -F 'Crypto data generated.' >/dev/null + +permissions=$(unshare --user --map-root-user stat -c '%a %u:%g' \ + "$directory" "$certificate" "$key") +expected_permissions='750 0:0 +644 0:0 +640 0:0' +if [ "$permissions" != "$expected_permissions" ]; then + printf 'FAIL TLS permissions:\n%s\n' "$permissions" >&2 + exit 1 +fi + +information=$(certtool --certificate-info --infile "$certificate" 2>&1) +for expected in \ + 'Version: 3' \ + 'Algorithm Security Level: High (3072 bits)' \ + 'DNSname: mail.bongo.test' \ + 'DNSname: smtp.bongo.test' \ + 'Certificate Authority (CA): FALSE' \ + 'Digital signature.' \ + 'Key encipherment.' \ + 'TLS WWW Server.' \ + 'Signature Algorithm: RSA-SHA256'; do + if ! printf '%s' "$information" | grep -F "$expected" >/dev/null; then + printf 'FAIL generated certificate lacks: %s\n' "$expected" >&2 + exit 1 + fi +done +certificate_id=$(certtool --pubkey-info --load-certificate "$certificate" 2>&1 | + sed -n 's/^[[:space:]]*sha256://p' | head -n 1) +key_id=$(certtool --pubkey-info --load-privkey "$key" 2>&1 | + sed -n 's/^[[:space:]]*sha256://p' | head -n 1) +if [ -z "$certificate_id" ] || [ "$certificate_id" != "$key_id" ]; then + echo 'FAIL generated certificate and private key do not match' >&2 + exit 1 +fi + +first_hashes=$(sha256sum "$certificate" "$key") +run_crypto | grep -F 'Reusing existing TLS certificate and key' >/dev/null +second_hashes=$(sha256sum "$certificate" "$key") +if [ "$first_hashes" != "$second_hashes" ]; then + echo 'FAIL a valid existing TLS pair was replaced' >&2 + exit 1 +fi +printf 'PASS generated TLS material and byte-identical reuse\n' + +cp "$certificate" "$good_certificate" +cp "$key" "$good_key" +certtool --generate-privkey --key-type rsa --bits 3072 \ + --outfile "$wrong_key" >/dev/null 2>&1 +chmod 0640 "$wrong_key" + +cp "$wrong_key" "$key" +expect_failure 'mismatched private key' 'do not form a valid pair' +restore_pair + +rm -f "$key" +expect_failure 'incomplete pair' 'Unsafe or incomplete TLS key pair' +restore_pair + +chmod 0666 "$key" +expect_failure 'unsafe private-key mode' 'Unsafe or incomplete TLS key pair' +restore_pair + +rm -f "$key" +ln -s "$good_key" "$key" +expect_failure 'private-key symlink' 'Unsafe or incomplete TLS key pair' +restore_pair + +rm -f "$certificate" +ln -s "$good_certificate" "$certificate" +expect_failure 'certificate symlink' 'Unsafe or incomplete TLS key pair' +restore_pair + +printf '%s\n' 'not a certificate' >"$certificate" +expect_failure 'malformed certificate' 'do not form a valid pair' +restore_pair + +before=$(sha256sum "$certificate" "$key") +set +e +output=$(unshare --user --map-root-user "$binary" \ + --hostname mail.bongo.test --tls-name pop.bongo.test crypto 2>&1) +status=$? +set -e +after=$(sha256sum "$certificate" "$key") +if [ "$status" -ne 6 ] || + ! printf '%s' "$output" | + grep -F 'does not cover DNS name pop.bongo.test' >/dev/null || + [ "$before" != "$after" ]; then + printf 'FAIL missing SAN: status=%s output=%s\n' "$status" "$output" >&2 + exit 1 +fi +printf 'PASS missing SAN: rejected without changing input files\n' diff --git a/docs/release-testing-0.7.md b/docs/release-testing-0.7.md index 47c9369..7b92946 100644 --- a/docs/release-testing-0.7.md +++ b/docs/release-testing-0.7.md @@ -94,7 +94,7 @@ inputs and outputs is absent from the ZIP. | CFG-07 | Scanner editor preserves unrelated settings and restores on failure | [PASS](test-evidence/0.7-r1.md#cfg-07) | | | | CFG-08 | Reverse-proxy, HAProxy, direct HTTP, and direct HTTPS validation paths | [PASS](test-evidence/0.7-r1.md#cfg-08) | | | | CFG-09 | Self-signed GnuTLS certificate/key generation and permission checks | [PASS](test-evidence/0.7-r1.md#cfg-09) | | | -| CFG-10 | Existing certificate/key validation and safe rejection of broken pairs | | | | +| CFG-10 | Existing certificate/key validation and safe rejection of broken pairs | [PASS](test-evidence/0.7-r1.md#cfg-10) | | | | CFG-11 | DKIM/SPF/DMARC discovery, key import/generation, DNS hints, and task | | | | | CFG-12 | Manager start, reload, restart, clean stop, stale PID, and crash recovery | | | | | CFG-13 | Every agent starts as intended and drops privileges | | | | diff --git a/docs/test-evidence/0.7-r1.md b/docs/test-evidence/0.7-r1.md index c471abb..b4cdf03 100644 --- a/docs/test-evidence/0.7-r1.md +++ b/docs/test-evidence/0.7-r1.md @@ -1064,3 +1064,33 @@ CTest run passed all 82 tests in 7.01 seconds. Runtime configuration: isolated build and disposable TLS/state roots below `/tmp`; no live certificate, Store document, listener, or service state was changed. The private key was not committed. + +## CFG-10 + +Result: **PASS** + +The repeatable `contrib/testing/test-config-tls-material.sh` integration test +ran the real `bongo-config crypto` executable with compile-time TLS paths below +`/tmp`. It first confirmed that a matching certificate/key pair covering all +requested names is reused. It then independently injected a mismatched private +key, an incomplete pair, a world-writable private key, private-key and +certificate symlinks, malformed certificate data, and a requested DNS name +which the otherwise valid certificate did not cover. + +All seven unsafe or invalid states returned the documented crypto failure +status and a specific diagnostic. SHA-256 snapshots before and after every +validation failure were identical, including the missing-file state of the +incomplete-pair case: validation never replaced, truncated, or removed an +operator-supplied input. The script also rejects test directories outside +`/tmp` and refuses any pre-existing TLS input before it starts. + +- Source under test: commit + `6e03d3a65d5d5d91601503dea7ade35c0bb19dd3`. +- Result: valid-pair reuse plus seven rejection cases passed. +- Regression coverage: multiple SANs, key/certificate identity, X.509 + properties, file modes, byte-identical reuse, pair integrity, symlink and + permission safety, malformed PEM, and hostname coverage. + +Runtime configuration: private user namespace and disposable paths below +`/tmp`. The harness removed all generated private-key material on exit and did +not read or change the live Bongo TLS pair.