Cover unsafe TLS material reuse
This commit is contained in:
Executable
+177
@@ -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'
|
||||
@@ -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 | | | |
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user