Skip to content

Commit

Permalink
Merge PR #85 from usit-gd/feature-public-client-ca
Browse files Browse the repository at this point in the history
This PR takes care of 2 issues:
- It automates the upgrading process of the client CA certificate,
  making it easy to upgrade, and "documenting" (in the form of code)
  how to do it. #27
- It publishes a bundle with the currently active CA certificates on
  the web server, facilitating 3rd party use. #32 

See also: https://github.com/usit-gd/nivlheim/wiki/Client-certificates
  • Loading branch information
oyvindhagberg authored Mar 13, 2019
2 parents 0bdcdd3 + c027423 commit b613d50
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 30 deletions.
23 changes: 10 additions & 13 deletions client/nivlheim_client
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ sub getpassword();
# Options with default values
my %defaultopt = (
'config' => '/etc/nivlheim/client.conf:/usr/local/etc/nivlheim/client.conf', # configuration file
'ca_file' => '/var/nivlheim/nivlheimca.crt:/var/www/nivlheim/CA/nivlheimca.crt'
'ca_file' => '/var/nivlheim/nivlheimca.crt'
. ':/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-bundle.crt'
. ':/etc/pki/tls/certs/ca-bundle.crt:/usr/local/et/ssl/cert.pem',
'cert_file' => '/var/nivlheim/my.crt',
Expand All @@ -62,7 +62,7 @@ my $NAME = 'nivlheim_client';
my $AUTHOR = 'Øyvind Hagberg';
my $CONTACT = '[email protected]';
my $RIGHTS = 'USIT/IT-DRIFT/GD/GID, University of Oslo, Norway';
my $VERSION = '0.10.1';
my $VERSION = '0.12.1';

# Usage text
my $USAGE = <<"END_USAGE";
Expand Down Expand Up @@ -494,15 +494,13 @@ sub ssl_connect($$$) {
my ($hostname, $port, $use_client_cert) = @_;

if (!-r $opt{cert_file} || !-r $opt{key_file}) { $use_client_cert = 0; }
my $commonName;
if ($real_ca_file =~ m!/nivlheimca\.crt$!) {
$commonName = "localhost";
} else {
$commonName = $hostname;

my $verify_mode = IO::Socket::SSL::SSL_VERIFY_PEER();
if ($hostname eq 'localhost') {
$verify_mode = IO::Socket::SSL::SSL_VERIFY_NONE();
}
if ($opt{debug}) {
print "hostname=$hostname port=$port\n";
print "commonName=$commonName use_client_cert=$use_client_cert\n";
print "hostname=$hostname port=$port use_client_cert=$use_client_cert\n";
#$IO::Socket::SSL::DEBUG = 3;
}

Expand Down Expand Up @@ -542,9 +540,9 @@ sub ssl_connect($$$) {
# We have a connection - try to start SSL
if (my $ssl = IO::Socket::SSL->new_from_fd(
$socket->fileno(),
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
SSL_verify_mode => $verify_mode,
SSL_verifycn_scheme => 'http',
SSL_verifycn_name => $commonName,
SSL_verifycn_name => $hostname,
SSL_ca_file => $real_ca_file,
SSL_ca_path => '',
SSL_use_cert => $use_client_cert ? 1 : 0,
Expand All @@ -560,8 +558,7 @@ sub ssl_connect($$$) {
}
else {
# Negotiation failed
warn("Could not establish SSL connection: $! $@\n")
if $opt{debug};
warn("Could not establish SSL connection: $! $@\n");
}
$socket->close();
}
Expand Down
7 changes: 6 additions & 1 deletion rpm/nivlheim.spec
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Requires: perl(Sys::Syslog)
%package server
Summary: Server components of Nivlheim
Group: Applications/System
Requires: perl, openssl, httpd, mod_ssl, systemd
Requires: perl, openssl, httpd, mod_ssl, systemd, cronie
Requires: postgresql, postgresql-server, postgresql-contrib
Requires: unzip, file
Requires: perl(Archive::Tar)
Expand Down Expand Up @@ -190,6 +190,7 @@ install -p -m 0755 server/setup.sh %{buildroot}%{_localstatedir}/nivlheim/
install -p -m 0755 server/cgi/processarchive %{buildroot}/var/www/cgi-bin/
install -p -m 0644 server/nivlheim.service %{buildroot}%{_unitdir}/%{name}.service
install -p -m 0644 -D client/cronjob %{buildroot}%{_sysconfdir}/cron.d/nivlheim_client
install -p -m 0755 -D server/client_CA_cert.sh %{buildroot}%{_sysconfdir}/cron.daily/client_CA_cert.sh
rm -rf server/website/mockapi server/website/templates server/website/libs
cp -a server/website/* %{buildroot}%{_localstatedir}/www/html/
install -p -m 0644 ../jquery-3.3.1/dist/jquery.min.js %{buildroot}%{_localstatedir}/www/html/libs/jquery-3.3.1.min.js
Expand Down Expand Up @@ -244,6 +245,7 @@ rm -rf %{buildroot}
%config(noreplace) %{_sysconfdir}/httpd/conf.d/nivlheim.conf
%config %{_sysconfdir}/nivlheim/openssl_ca.conf
%config(noreplace) %{_sysconfdir}/nivlheim/server.conf
%{_sysconfdir}/cron.daily/client_CA_cert.sh
%{_unitdir}/%{name}.service
%{_sbindir}/nivlheim_service
%dir /var/log/nivlheim
Expand All @@ -263,6 +265,9 @@ rm -rf %{buildroot}
%systemd_postun_with_restart %{name}.service

%changelog
* Mon Mar 11 2019 Øyvind Hagberg <[email protected]> - 0.12.2-20190311
- New cron job that maintains the client CA certificates

* Tue Dec 11 2018 Øyvind Hagberg <[email protected]> - 0.11.0-20181211
- Include 3rd party javascript and css libraries in the rpm file

Expand Down
14 changes: 12 additions & 2 deletions server/cgi/ping2
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@ $timestamp =~ s/\s+GMT$//;
my $time = Time::Piece->strptime($timestamp, "%b %d %H:%M:%S %Y");
my $left = $time - gmtime;
if ($left->days < 30) {
print "Status: 403\nContent-Type: text/plain\n\nYour cert is about to expire, please renew it.\n";
print "Status: 400\nContent-Type: text/plain\n\nYour certificate is about to expire, please renew it.\n";
exit;
}

# Compute the client cert fingerprint
# If the client cert was signed by a different CA than the one that's currently active,
# politely ask it to renew
my $clientcert = $ENV{'SSL_CLIENT_CERT'};
my $x509 = Crypt::OpenSSL::X509->new_from_string($clientcert);
my $value1 = $x509->issuer();
my $ca = Crypt::OpenSSL::X509->new_from_file('/var/www/nivlheim/CA/nivlheimca.crt');
my $value2 = $ca->subject();
if ($value1 ne $value2) {
print "Status: 400\nContent-Type: text/plain\n\nThe server has a new CA certificate, please renew your certificate.\n";
exit;
}

# Compute the client cert fingerprint
my $fingerprint = $x509->fingerprint_sha1();
$fingerprint =~ s/://g;

Expand Down
92 changes: 92 additions & 0 deletions server/client_CA_cert.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/bin/bash

# This script is a part of Nivlheim.
# It is used to create CA certificates that are used for signing
# client certificates.
#
# It is intended to be run without parameters, as a cron job.
# It will check the expiry date of the existing CA certificate,
# and replace it with a new one when necessary.
#
# To make it easier for 3rd party software to verify client certificates,
# a new CA certificate will appear in the bundle https://<server>/clientca.pem
# 3 weeks before it is actually put to use.


if [ `whoami` != "root" ]; then
echo "This script must be run as root."
exit 1
fi

# What operations to perform
CREATE=0
ACTIVATE=0
VERBOSE=0

# Parameters may override normal operations
while (( "$#" )); do
if [[ "$1" == "--force-create" ]]; then
CREATE=1
elif [[ "$1" == "--force-activate" ]]; then
ACTIVATE=1
elif [[ "$1" == "--verbose" ]] || [[ "$1" == "-v" ]]; then
VERBOSE=1
else
echo "Unknown argument: $1"
exit 1
fi
shift
done

cd /var/www/nivlheim/CA

# If the CA certificate will expire in less than 30 days, create a new one
if [ ! -f nivlheimca.crt ] || ! openssl x509 -checkend 2592000 -noout -in nivlheimca.crt -enddate >/dev/null; then
CREATE=1
fi

# If the CA certificate will expire in less than 9 days, change to the new one
if [ ! -f nivlheimca.crt ] || ! openssl x509 -checkend 777600 -noout -in nivlheimca.crt >/dev/null; then
ACTIVATE=1
fi

if [[ $CREATE -eq 1 ]]; then
if [ ! -f new_nivlheimca.crt ] || [ ! -f new_nivlheimca.key ]; then
[ $VERBOSE -eq 1 ] && echo "Creating a new CA certificate"

# Generate a new certificate
rm -f old_*
openssl genrsa -out new_nivlheimca.key 4096 >/dev/null 2>&1
openssl req -new -key new_nivlheimca.key -out new_nivlheimca.csr -subj "/C=NO/ST=Oslo/L=Oslo/O=UiO/OU=USIT/CN=Nivlheim$RANDOM"
openssl x509 -req -days 365 -in new_nivlheimca.csr -out new_nivlheimca.crt -signkey new_nivlheimca.key >/dev/null 2>&1

# Fix permissions
chgrp apache new_nivlheimca.*
chmod 640 new_nivlheimca.key

# Show results
[ $VERBOSE -eq 1 ] && openssl x509 -in new_nivlheimca.crt -noout -enddate

# create a bundle with the old and the new CA
cat nivlheimca.crt new_nivlheimca.crt > /var/www/html/clientca.pem
else
echo "Won't create a new CA certificate; One has already been created and is waiting"
fi
fi

if [[ $ACTIVATE -eq 1 ]]; then
if [ -f new_nivlheimca.crt ] && [ -f new_nivlheimca.key ]; then
[ $VERBOSE -eq 1 ] && echo "Activating the new CA certificate"
# Activate/change to the new CA certificate
mv nivlheimca.key old_nivlheimca.key
mv nivlheimca.csr old_nivlheimca.csr
mv nivlheimca.crt old_nivlheimca.crt
mv new_nivlheimca.key nivlheimca.key
mv new_nivlheimca.csr nivlheimca.csr
mv new_nivlheimca.crt nivlheimca.crt
systemctl restart httpd
else
echo "There's no new CA certificate to activate"
exit 1
fi
fi
2 changes: 1 addition & 1 deletion server/httpd_ssl.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SSLCertificateFile /var/www/nivlheim/default_cert.pem
SSLCertificateKeyFile /var/www/nivlheim/default_key.pem

# Client CA
SSLCACertificateFile /var/www/nivlheim/CA/nivlheimca.crt
SSLCACertificateFile /var/www/html/clientca.pem
SSLVerifyClient optional
SSLVerifyDepth 10

Expand Down
1 change: 1 addition & 0 deletions server/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ if [ ! -f nivlheimca.key ]; then
openssl genrsa -out nivlheimca.key 4096
openssl req -new -key nivlheimca.key -out nivlheimca.csr -config /etc/nivlheim/openssl_ca.conf
openssl x509 -req -days 365 -in nivlheimca.csr -out nivlheimca.crt -signkey nivlheimca.key
cp nivlheimca.crt /var/www/html/clientca.pem
fi

# generate a SSL certificate as a default for the web server
Expand Down
16 changes: 7 additions & 9 deletions tests/test_cert_handling.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ trap finish EXIT
# Clean/init everything
sudo systemctl stop nivlheim
sudo rm -f /var/log/nivlheim/system.log /var/nivlheim/my.{crt,key} \
/var/run/nivlheim_client_last_run /var/www/nivlheim/certs/*
/var/run/nivlheim_client_last_run /var/www/nivlheim/certs/* \
/var/www/nivlheim/queue/*
echo -n | sudo tee /var/log/httpd/error_log
sudo -u apache /var/nivlheim/installdb.sh --wipe
sudo systemctl start nivlheim
Expand Down Expand Up @@ -115,24 +116,21 @@ popd
# Blacklist and check response
sudo psql apache -q -c "UPDATE certificates SET revoked=true"
# Test ping
if sudo curl -sf --cacert /var/www/nivlheim/CA/nivlheimca.crt \
--cert /var/nivlheim/my.crt --key /var/nivlheim/my.key \
if sudo curl -skf --cert /var/nivlheim/my.crt --key /var/nivlheim/my.key \
https://localhost/cgi-bin/secure/ping; then
echo "Secure/ping worked even though cert was blacklisted."
exit 1
fi
# Test post (it will get a 403 anyway, because the nonce is missing)
sudo curl -sS --cacert /var/www/nivlheim/CA/nivlheimca.crt \
--cert /var/nivlheim/my.crt --key /var/nivlheim/my.key \
https://localhost/cgi-bin/secure/post > $tempdir/postresult
sudo curl -sk --cert /var/nivlheim/my.crt --key /var/nivlheim/my.key \
https://localhost/cgi-bin/secure/post > $tempdir/postresult || true
if ! grep -qi "revoked" $tempdir/postresult; then
echo "Post worked even though cert was blacklisted."
exit 1
fi
# Test renew
sudo curl -sf --cacert /var/www/nivlheim/CA/nivlheimca.crt \
--cert /var/nivlheim/my.crt --key /var/nivlheim/my.key \
https://localhost/cgi-bin/secure/renewcert > $tempdir/renewresult
sudo curl -sk --cert /var/nivlheim/my.crt --key /var/nivlheim/my.key \
https://localhost/cgi-bin/secure/renewcert > $tempdir/renewresult || true
if ! grep -qi "revoked" $tempdir/renewresult; then
echo "Renewcert worked even though cert was blacklisted."
exit 1
Expand Down
92 changes: 92 additions & 0 deletions tests/test_change_ca.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/bin/bash

echo "-------------- Testing creating/activating a new client CA certificate -----------"
set -e

# Clean/init everything
sudo systemctl stop nivlheim
sudo rm -f /var/log/nivlheim/system.log /var/nivlheim/my.{crt,key} \
/var/run/nivlheim_client_last_run /var/www/nivlheim/certs/* \
/var/www/nivlheim/queue/*
echo -n | sudo tee /var/log/httpd/error_log
sudo -u apache /var/nivlheim/installdb.sh --wipe
sudo systemctl start nivlheim
sleep 4

# Run the client. This will call reqcert and post
if ! grep -s -e "^server" /etc/nivlheim/client.conf > /dev/null; then
echo "server=localhost" | sudo tee -a /etc/nivlheim/client.conf
fi
curl -sS -X POST 'http://localhost:4040/api/v0/settings/ipranges' -d 'ipRange=127.0.0.0/24'
sudo /usr/sbin/nivlheim_client
if [[ ! -f /var/run/nivlheim_client_last_run ]]; then
echo "The client failed to post data successfully."
exit 1
fi

# Create a new CA certificate
sudo /etc/cron.daily/client_CA_cert.sh --force-create --verbose

# Verify that the old client certificate still works
if ! sudo curl -sSkf --cert /var/nivlheim/my.crt --key /var/nivlheim/my.key \
https://localhost/cgi-bin/secure/ping; then
echo "The client cert didn't work after a new CA was created."
exit 1
fi

# Verify that the client doesn't ask for a new certificate yet
OLDMD5=$(md5sum /var/nivlheim/my.crt)
sudo /usr/sbin/nivlheim_client
NEWMD5=$(md5sum /var/nivlheim/my.crt)
if [[ "$OLDMD5" != "$NEWMD5" ]]; then
echo "The client got get a new certificate before the new CA was activated."
exit 1
fi

# Ask for a new certificate, verify that they are still being signed with the old CA cert
A=`openssl x509 -in /var/nivlheim/my.crt -noout -issuer_hash`
sudo rm -f /var/nivlheim/my.* /var/run/nivlheim_client_last_run
sudo /usr/sbin/nivlheim_client
if [[ ! -f /var/run/nivlheim_client_last_run ]]; then
echo "The client failed to run the second time."
exit 1
fi
B=`openssl x509 -in /var/nivlheim/my.crt -noout -issuer_hash`
if [[ "$A" != "$B" ]]; then
echo "After creating a new CA cert, it was used for issuing even before it was activated."
exit 1
fi

# Activate the new CA certificate
sudo /etc/cron.daily/client_CA_cert.sh --force-activate --verbose

# Verify that the old client certificate still works
sudo cp /var/www/cgi-bin/ping /var/www/cgi-bin/secure/foo
if ! sudo curl -sSkf --cert /var/nivlheim/my.crt --key /var/nivlheim/my.key \
https://localhost/cgi-bin/secure/foo; then
echo "The client cert didn't work after a new CA was activated."
exit 1
fi

# Run the client again, verify that it asked for (and got) a new certificate
# (because secure/ping should return 400)
# and verify that it was signed with the new CA cert
OLDMD5=$(md5sum /var/nivlheim/my.crt)
sudo rm -f /var/run/nivlheim_client_last_run
sudo /usr/sbin/nivlheim_client
if [[ ! -f /var/run/nivlheim_client_last_run ]]; then
echo "The client failed to run the third time."
exit 1
fi
NEWMD5=$(md5sum /var/nivlheim/my.crt)
if [[ "$OLDMD5" == "$NEWMD5" ]]; then
echo "The client didn't get a new certificate after the server got a new CA."
exit 1
fi
C=`openssl x509 -in /var/nivlheim/my.crt -noout -issuer_hash`
if [[ "$B" == "$C" ]]; then
echo "Still signing with the old CA cert, even after the new one was activated."
exit 1
fi

echo "Test result: OK"
3 changes: 2 additions & 1 deletion tests/test_client_timing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ if [[ "$1" != "--skipsetup" ]]; then
# Clean/init everything
sudo systemctl stop nivlheim
sudo rm -f /var/log/nivlheim/system.log /var/nivlheim/my.{crt,key} \
/var/run/nivlheim_client_last_run /var/www/nivlheim/certs/*
/var/run/nivlheim_client_last_run /var/www/nivlheim/certs/* \
/var/www/nivlheim/queue/*
echo -n | sudo tee /var/log/httpd/error_log
sudo -u apache /var/nivlheim/installdb.sh --wipe
sudo systemctl start nivlheim
Expand Down
7 changes: 4 additions & 3 deletions tests/test_clones.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ trap finish EXIT

# Clean/init everything
sudo systemctl stop nivlheim
sudo rm -f /var/log/nivlheim/system.log /var/nivlheim/my.{crt,key} /var/run/nivlheim_client_last_run
sudo rm -f /var/log/nivlheim/system.log /var/nivlheim/my.{crt,key} \
/var/run/nivlheim_client_last_run /var/www/nivlheim/certs/* \
/var/www/nivlheim/queue/*
echo -n | sudo tee /var/log/httpd/error_log
sudo -u apache /var/nivlheim/installdb.sh --wipe
sudo systemctl start nivlheim
Expand Down Expand Up @@ -58,8 +60,7 @@ if [[ -f /var/run/nivlheim_client_last_run ]]; then
fi

# The certificate should be revoked now
if sudo curl -sf --cacert /var/www/nivlheim/CA/nivlheimca.crt \
--cert /var/nivlheim/my.crt --key /var/nivlheim/my.key 'https://localhost/cgi-bin/secure/ping'
if sudo curl -skf --cert /var/nivlheim/my.crt --key /var/nivlheim/my.key 'https://localhost/cgi-bin/secure/ping'
then
echo "The certificate wasn't revoked!"
exit 1
Expand Down

0 comments on commit b613d50

Please sign in to comment.