happstack-server-tls only works with self-signed certificates

300 views Asked by At

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
0

There are 0 answers