I am trying to create a web server using happstack-server-tls that will use a certificate signed by a private CA. Unfortunately, the TLS handshake only seems to succeed if I give the server a self-signed certificate. Wireshark shows that when my server is using a certificate signed by my private CA, instead of sending the client a Server Hello message, it sends a Fatal (2) Alert message reporting Handshake Failure (40).
This is not a case of the web browser rejecting the server's certificate; Wireshark shows that the TLS handshake never even gets to the point where the server presents a certificate to the browser. Nor is it a case of failure to agree on cryptographic algorithms, since the server runs without problems using a self-signed certificate that uses the same algorithm as the one I want to use. The certificate itself seems valid, since it works as expected using openssl s_server
on the command line, and happstack-server-tls appears to use OpenSSL for TLS operations, so I don't think it's a problem with how the certificate is generated.
What do I need to do to get happstack-server-tls to work with a non-self-signed certificate?
The following is a minimal Haskell program that uses happstack-server-tls to run a web server on port 8443 that demonstrates the problem. It takes up to three arguments: the file containing the server's private key, the file containing the server's certificate, and (optionally) the file containing the CA's certificate.
module Main ( main ) where
import Happstack.Server.Response (ok)
import Happstack.Server.SimpleHTTPS (TLSConf (..), nullTLSConf, simpleHTTPS)
import System.Environment (getArgs)
main :: IO ()
main = do args <- getArgs
case args of
[keyFile, certFile] -> runServer keyFile certFile Nothing
[keyFile, certFile, caFile] -> runServer keyFile certFile (Just caFile)
runServer :: FilePath -> FilePath -> Maybe FilePath -> IO ()
runServer keyFile certFile caFile = simpleHTTPS conf $ ok ":-)"
where conf = nullTLSConf { tlsPort = 8443
, tlsCert = certFile
, tlsKey = keyFile
, tlsCA = caFile
}
The above program works using a self-signed certificate generated via the following script:
#! /bin/sh
openssl ecparam -name secp384r1 -genkey -out selfsigned.key
openssl req -new -x509 -days 365 -key selfsigned.key -out selfsigned.crt
And running the following command:
runghc Main.hs selfsigned.key selfsigned.crt
It works in that a web browser can connect to the server successfully (albeit complaining about a self-signed certificate, as expected).
However, it doesn't work using a non-self-signed certificate generated by the following script, which first creates a new private CA and then uses that CA to generate the certificate for the server:
#! /bin/sh
rm -rf ca
mkdir ca
mkdir ca/newcerts
mkdir ca/private
touch ca/index.txt
echo "100001" >ca/serial
openssl ecparam -name secp384r1 -genkey -out ca/private/cakey.pem
openssl req -new -x509 -days 365 -key ca/private/cakey.pem -out ca/cacert.pem
openssl ecparam -name secp384r1 -genkey -out onelevel-server.key
openssl req -new -days 365 -key onelevel-server.key -out onelevel-server.req
openssl ca -config onelevel.openssl.cnf -in onelevel-server.req -out onelevel-server.crt
When I run the following command:
runghc Main.hs onelevel-server.key onelevel-server.crt ca/cacert.pem
Web browsers can't connect because (as shown by Wireshark) the initial TLS handshake fails.
However, a web browser can establish a TLS connecting when using openssl
to act as a server using the same certificate:
openssl s_server -accept 8443 -key onelevel-server.key -cert onelevel-server.crt -CAfile ca/cacert.pem
To complete the example, here is the configuration file onelevel.openssl.cnf
referenced earlier that provides the configuration for the private CA:
[ ca ]
default_ca = CA_onelevel
[ CA_onelevel ]
dir = ./ca
certs = $dir/certs
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
serial = $dir/serial
crlnumber = $dir/crlnumber
crl = $dir/crl.pem
private_key = $dir/private/cakey.pem
x509_extensions = onelevel_cert
name_opt = ca_default
cert_opt = ca_default
default_days = 365
default_crl_days = 30
default_md = default
preserve = no
policy = policy_match
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = match
commonName = supplied
emailAddress = optional
[ onelevel_cert ]
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer