Merge branch 'feature/ca-pinning' of https://github.com/seccubus/testssl.sh into seccubus-feature/ca-pinning

This commit is contained in:
Dirk Wetter 2016-07-20 18:47:05 +02:00
commit 535c37fbb3
2 changed files with 199 additions and 13 deletions

79
t/02_hpkp_pinning.t Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/env perl
use strict;
use Test::More;
use Data::Dumper;
use JSON;
my $tests = 0;
my (
$out,
$json,
$found,
);
# OK
pass("Running testssl.sh against ssl.sectionzero.org"); $tests++;
$out = `./testssl.sh -H --jsonfile tmp.json --color 0 ssl.sectionzero.org`;
$json = json('tmp.json');
# It is better to have findings in a hash
# Look for a leaf cert match in the process.
my $found = 0;
my %findings;
foreach my $f ( @$json ) {
$findings{$f->{id}} = $f;
if ( $f->{finding} =~ /matches the leaf certificate/ ) {
$found++;
}
}
is($found,1,"We found 1 'matches the leaf certificate' finding"); $tests++;
like($out,'/Leaf cert match/',"There is a 'Leaf cert match' in the text output"); $tests++;
# Sub CA match
ok( exists $findings{"hpkp_YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg"},"We have a finding for key YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg"); $tests++;
like($findings{"hpkp_YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg"}->{finding},'/Intermediate CA key matches a key pinned in the HPKP header/',"We have our Sub CA finding"); $tests++;
is($findings{"hpkp_YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg"}->{severity}, "OK", "The finding is ok"); $tests++;
like($out,'/Sub CA match \: YLh1dUR9y6Kja30RrAn7JKnbQG\/uEtLMkBgFF2Fuihg/',"There is a 'Sub CA match' in the text output"); $tests++;
# Root CA match Lets encrypt
ok( exists $findings{"hpkp_Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys"},"We have a finding for key Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys"); $tests++;
like($findings{"hpkp_Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys"}->{finding},'/Root CA key matches a key pinned in the HPKP header/',"This is a Root CA finding"); $tests++;
like($findings{"hpkp_Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys"}->{finding},'/DST Root CA X3/',"Correct Root CA"); $tests++;
like($findings{"hpkp_Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys"}->{finding},'/The CA is part of the chain/',"CA is indeed part of chain"); $tests++;
is($findings{"hpkp_Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys"}->{severity}, "OK", "The finding is ok"); $tests++;
like($out,'/Root CA match \: Vjs8r4z\+80wjNcr1YKepWQboSIRi63WsWXhIMN\+eWys/',"There is a 'Root CA match' in the text output"); $tests++;
# Root CA StartCom
ok( exists $findings{"hpkp_5C8kvU039KouVrl52D0eZSGf4Onjo4Khs8tmyTlV3nU"},"We have a finding for key 5C8kvU039KouVrl52D0eZSGf4Onjo4Khs8tmyTlV3nU"); $tests++;
like($findings{"hpkp_5C8kvU039KouVrl52D0eZSGf4Onjo4Khs8tmyTlV3nU"}->{finding},'/Root CA key matches a key pinned in the HPKP header/',"This is a Root CA finding"); $tests++;
like($findings{"hpkp_5C8kvU039KouVrl52D0eZSGf4Onjo4Khs8tmyTlV3nU"}->{finding},'/StartCom Certification Authority/',"Correct Root CA"); $tests++;
like($findings{"hpkp_5C8kvU039KouVrl52D0eZSGf4Onjo4Khs8tmyTlV3nU"}->{finding},'/The CA is not part of the chain/',"CA is indeed NOT part of chain"); $tests++;
is($findings{"hpkp_5C8kvU039KouVrl52D0eZSGf4Onjo4Khs8tmyTlV3nU"}->{severity}, "OK", "The finding is ok"); $tests++;
like($out,'/Root CA match \: 5C8kvU039KouVrl52D0eZSGf4Onjo4Khs8tmyTlV3nU/',"There is a 'Root CA match' in the text output"); $tests++;
# Bad PIN
ok( exists $findings{"hpkp_123bad123bad123bad123bad123bad123bd123bad12"},"We have a finding for key 123bad123bad123bad123bad123bad123bd123bad12"); $tests++;
like($findings{"hpkp_123bad123bad123bad123bad123bad123bd123bad12"}->{finding},'/doesn\'t match anything/',"It doesn't match indeed"); $tests++;
is($findings{"hpkp_123bad123bad123bad123bad123bad123bd123bad12"}->{severity}, "WARN", "The finding is ok"); $tests++;
like($out,'/Unmatched key : 123bad123bad123bad123bad123bad123bd123bad12/',"There is an 'unmatched key' in the text output"); $tests++;
like($findings{hpkp_keys}->{finding},'/5 keys pinned/',"5 keys pinned in json"); $tests++;
like($out,'/\# of keys: 5/',"5 keys pinned in text output"); $tests++;
like($findings{hpkp_age}->{finding},'/90 days/',"90 days in json"); $tests++;
like($out,'/90 days/',"90 days in text output"); $tests++;
like($findings{hpkp_subdomains}->{finding},'/this domain only/',"this domain only in json"); $tests++;
like($out,'/just this domain/',"just this domain text output"); $tests++;
like($findings{hpkp_preload}->{finding},'/NOT marked for/',"no preloading in json"); $tests++;
done_testing($tests);
sub json($) {
my $file = shift;
$file = `cat $file`;
unlink $file;
return from_json($file);
}

View File

@ -483,7 +483,7 @@ fileout() { # ID, SEVERITY, FINDING
if "$do_json"; then if "$do_json"; then
"$FIRST_FINDING" || echo -n "," >> $JSONFILE "$FIRST_FINDING" || echo -n "," >> $JSONFILE
echo -e " { echo " {
\"id\" : \"$1\", \"id\" : \"$1\",
\"ip\" : \"$NODE/$NODEIP\", \"ip\" : \"$NODE/$NODEIP\",
\"port\" : \"$PORT\", \"port\" : \"$PORT\",
@ -963,6 +963,7 @@ run_hpkp() {
local i local i
local hpkp_headers local hpkp_headers
local first_hpkp_header local first_hpkp_header
local ca_bundles="$INSTALL_DIR/etc/*.pem"
if [[ ! -s $HEADERFILE ]]; then if [[ ! -s $HEADERFILE ]]; then
run_http_header "$1" || return 3 run_http_header "$1" || return 3
@ -1029,27 +1030,135 @@ run_hpkp() {
fileout "hpkp_preload" "INFO" "HPKP header is NOT marked for browser preloading" fileout "hpkp_preload" "INFO" "HPKP header is NOT marked for browser preloading"
fi fi
# Get the pins first
pins=$(tr ';' '\n' < $TMPFILE | tr -d ' ' | tr -d '\"' | awk -F'=' '/pin.*=/ { print $2 }')
# Look at the host certificate first
# get the key fingerprint from the host certificate
if [[ ! -s "$HOSTCERT" ]]; then if [[ ! -s "$HOSTCERT" ]]; then
get_host_cert || return 1 get_host_cert || return 1
fi fi
# get the key fingerprint from the host certificate
hpkp_key_hostcert="$($OPENSSL x509 -in $HOSTCERT -pubkey -noout | grep -v PUBLIC | \ hpkp_key_hostcert="$($OPENSSL x509 -in $HOSTCERT -pubkey -noout | grep -v PUBLIC | \
$OPENSSL base64 -d | $OPENSSL dgst -sha256 -binary | $OPENSSL base64)" $OPENSSL base64 -d | $OPENSSL dgst -sha256 -binary | $OPENSSL base64)"
# compare it with the ones provided in the header hpkp_ca="$($OPENSSL x509 -in $HOSTCERT -issuer -noout|sed 's/^.*CN=//' | sed 's/\/.*$//')"
while read hpkp_key; do
# Get keys/hashes from intermediate certificates
$OPENSSL s_client -showcerts $STARTTLS $BUGS $PROXY -showcerts -connect $NODEIP:$PORT ${sni[i]} </dev/null >$TMPFILE 2>$ERRFILE
# Place the server's certificate in $HOSTCERT and any intermediate
# certificates that were provided in $TEMPDIR/intermediatecerts.pem
# http://backreference.org/2010/05/09/ocsp-verification-with-openssl/
awk -v n=-1 "/Certificate chain/ {start=1}
/-----BEGIN CERTIFICATE-----/{ if (start) {inc=1; n++} }
inc { print > (\"$TEMPDIR/level\" n \".crt\") }
/---END CERTIFICATE-----/{ inc=0 }" $TMPFILE
nrsaved=$(count_words "$(echo $TEMPDIR/level?.crt 2>/dev/null)")
rm $TEMPDIR/level0.crt
if [[ nrsaved -ge 2 ]]; then
echo -n "" > "$TEMPDIR/intermediate.hashes"
for cert_fname in $TEMPDIR/level?.crt; do
hpkp_key_ca="$($OPENSSL x509 -in "$cert_fname" -pubkey -noout | grep -v PUBLIC | $OPENSSL base64 -d |
$OPENSSL dgst -sha256 -binary | $OPENSSL enc -base64)"
hpkp_name="$(get_cn_from_cert $cert_fname)"
hpkp_ca="$($OPENSSL x509 -in $cert_fname -issuer -noout|sed 's/^.*CN=//' | sed 's/\/.*$//')"
[[ -n $hpkp_name ]] || hpkp_name=$($OPENSSL x509 -in "$cert_fname" -subject -noout | sed 's/^subject= //')
echo "$hpkp_key_ca $hpkp_name" >> "$TEMPDIR/intermediate.hashes"
done
fi
rm $TEMPDIR/level*.crt
# Get keys from Root CAs
# Clear temp file
echo -n "" > "$TEMPDIR/cahashes"
for bundle_fname in $ca_bundles; do
if [[ ! -r $bundle_fname ]]; then
pr_warningln "\"$bundle_fname\" cannot be found / not readable"
return 7
fi
# Split up the certificate bundle
awk -v n=-1 "BEGIN {start=1}
/-----BEGIN CERTIFICATE-----/{ if (start) {inc=1; n++} }
inc { print >> (\"$TEMPDIR/$bundle_name.\" n \".crt\") ; close (\"$TEMPDIR/$bundle_name.\" n \".crt\") }
/---END CERTIFICATE-----/{ inc=0 }" $bundle_fname
for cert_fname in $TEMPDIR/$bundle_name.*.crt; do
hpkp_key_ca="$($OPENSSL x509 -in "$cert_fname" -pubkey -noout | grep -v PUBLIC | $OPENSSL base64 -d |
$OPENSSL dgst -sha256 -binary | $OPENSSL enc -base64)"
issuer=$(get_cn_from_cert $cert_fname)
[[ -n $hpkp_name ]] || hpkp_name=$($OPENSSL x509 -in "$cert_fname" -subject -noout| sed "s/^subject= //")
echo "$hpkp_key_ca $issuer" >> "$TEMPDIR/cahashes"
done
done
pins_match=false
for hpkp_key in $(echo $pins); do
key_found=false
# compare pin against the leaf certificate
if [[ "$hpkp_key_hostcert" == "$hpkp_key" ]] || [[ "$hpkp_key_hostcert" == "$hpkp_key=" ]]; then if [[ "$hpkp_key_hostcert" == "$hpkp_key" ]] || [[ "$hpkp_key_hostcert" == "$hpkp_key=" ]]; then
out "\n$spaces matching host key: " out "\n\n$spaces Leaf cert match : "
pr_done_good "$hpkp_key" pr_done_good "$hpkp_key"
fileout "hpkp_keymatch" "OK" "Key matches a key pinned in the HPKP header" fileout "hpkp_$hpkp_key" "OK" "PIN $hpkp_key matches the leaf certificate"
key_found=true key_found=true
pins_match=true
fi fi
debugme out "\n $hpkp_key | $hpkp_key_hostcert" debugme out "\n $hpkp_key | $hpkp_key_hostcert"
done < <(tr ';' '\n' < $TMPFILE | tr -d ' ' | tr -d '\"' | awk -F'=' '/pin.*=/ { print $2 }')
if ! $key_found ; then # Check for intermediate match
out "\n$spaces" if ! $key_found ; then
hpkp_matches=$(grep "$hpkp_key" $TEMPDIR/intermediate.hashes)
if [[ -n $hpkp_matches ]]; then
# We have a winner!
key_found=true
pins_match=true
out "\n\n$spaces Sub CA match : "
pr_done_good "$hpkp_key"
out "\n$spaces $(echo $hpkp_matches|sed "s/^[a-zA-Z0-9\+\/]*=* *//")"
fileout "hpkp_$hpkp_key" "OK" "Intermediate CA key matches a key pinned in the HPKP header.\\nKey/CA: $hpkp_matches"
fi
fi
if ! $key_found ; then
hpkp_matches=$(grep -h "$hpkp_key" $TEMPDIR/cahashes | sort -u)
if [[ -n $hpkp_matches ]]; then
# We have a winner!
key_found=true
pins_match=true
if [[ $(count_lines "$hpkp_matches") -eq 1 ]]; then
match_ca=$(echo "$hpkp_matches" | sed "s/[a-zA-Z0-9\+\/]*=* *//")
else
match_ca=""
fi
out "\n\n$spaces Root CA match : "
pr_done_good "$hpkp_key"
echo "$hpkp_matches"|sort -u|while read line; do
out "\n$spaces $(echo $line |sed "s/^[a-zA-Z0-9\+\/]*=* *//")"
done
if [[ $match_ca == $hpkp_ca ]]; then
pr_done_good "\n$spaces This CA is part of the chain"
fileout "hpkp_$hpkp_key" "OK" "Root CA key matches a key pinned in the HPKP header\\nKey/OS/CA: $hpkp_matches\\nThe CA is part of the chain"
else
out "\n$spaces This CA is not part of the chain and likely a backup PIN"
fileout "hpkp_$hpkp_key" "OK" "Root CA key matches a key pinned in the HPKP header\\nKey/OS/CA: $hpkp_matches\\nThe CA is not part of the chain, this is a backup PIN"
fi
fi
fi
if ! $key_found ; then
# Houston we may have a problem
out "\n\n$spaces Unmatched key : "
pr_warning "$hpkp_key"
out "\n$spaces ( This is OK for a backup pin of a leaf cert )"
fileout "hpkp_$hpkp_key" "WARN" "PIN $hpkp_key doesn't match anything.\nThis could be ok if it is a backup pin for a leaf certificate"
fi
done
# If all else fails...
if ! $pins_match ; then
pr_svrty_high " No matching key for pins found " pr_svrty_high " No matching key for pins found "
out "(CAs pinned? -- not checked for yet)" fileout "hpkp_keymatch" "NOT ok" "None of the HPKP PINS match your leaf certificate, intermediate CA or known root CAs. You may have bricked this site"
fileout "hpkp_keymatch" "DEBUG" "The TLS key does not match any key pinned in the HPKP header. If you pinned a CA key you can ignore this"
fi fi
else else
out "--" out "--"
@ -6809,7 +6918,6 @@ check4openssl_oldfarts() {
outln outln
} }
# FreeBSD needs to have /dev/fd mounted. This is a friendly hint, see #258 # FreeBSD needs to have /dev/fd mounted. This is a friendly hint, see #258
check_bsd_mount() { check_bsd_mount() {
if [[ "$(uname)" == FreeBSD ]]; then if [[ "$(uname)" == FreeBSD ]]; then
@ -6823,7 +6931,6 @@ check_bsd_mount() {
fi fi
} }
help() { help() {
cat << EOF cat << EOF