mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
388 lines
12 KiB
Haskell
388 lines
12 KiB
Haskell
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
module Lib.Ssl
|
|
( DeriveCertificate(..)
|
|
, root_CA_CERT_NAME
|
|
, writeRootCaCert
|
|
, writeIntermediateCert
|
|
, domain_CSR_CONF
|
|
, writeLeafCert
|
|
, root_CA_OPENSSL_CONF
|
|
, intermediate_CA_OPENSSL_CONF
|
|
, segment
|
|
)
|
|
where
|
|
|
|
import Startlude
|
|
|
|
import Control.Lens
|
|
import Data.String.Interpolate.IsString
|
|
import System.Process
|
|
|
|
root_CA_CERT_NAME :: Text
|
|
root_CA_CERT_NAME = "Embassy Local Root CA"
|
|
|
|
root_CA_OPENSSL_CONF :: FilePath -> ByteString
|
|
root_CA_OPENSSL_CONF path = [i|
|
|
# OpenSSL root CA configuration file.
|
|
# Copy to `/root/ca/openssl.cnf`.
|
|
|
|
[ ca ]
|
|
# `man ca`
|
|
default_ca = CA_default
|
|
|
|
[ CA_default ]
|
|
# Directory and file locations.
|
|
dir = #{path}
|
|
certs = $dir/certs
|
|
crl_dir = $dir/crl
|
|
new_certs_dir = $dir/newcerts
|
|
database = $dir/index.txt
|
|
serial = $dir/serial
|
|
RANDFILE = $dir/private/.rand
|
|
|
|
# The root key and root certificate.
|
|
private_key = $dir/private/ca.key.pem
|
|
certificate = $dir/certs/ca.cert.pem
|
|
|
|
# For certificate revocation lists.
|
|
crlnumber = $dir/crlnumber
|
|
crl = $dir/crl/ca.crl.pem
|
|
crl_extensions = crl_ext
|
|
default_crl_days = 30
|
|
|
|
# SHA-1 is deprecated, so use SHA-2 instead.
|
|
default_md = sha256
|
|
|
|
name_opt = ca_default
|
|
cert_opt = ca_default
|
|
default_days = 375
|
|
preserve = no
|
|
policy = policy_loose
|
|
|
|
[ policy_loose ]
|
|
# Allow the intermediate CA to sign a more diverse range of certificates.
|
|
# See the POLICY FORMAT section of the `ca` man page.
|
|
countryName = optional
|
|
stateOrProvinceName = optional
|
|
localityName = optional
|
|
organizationName = optional
|
|
organizationalUnitName = optional
|
|
commonName = supplied
|
|
emailAddress = optional
|
|
|
|
[ req ]
|
|
# Options for the `req` tool (`man req`).
|
|
default_bits = 4096
|
|
distinguished_name = req_distinguished_name
|
|
string_mask = utf8only
|
|
prompt = no
|
|
|
|
# SHA-1 is deprecated, so use SHA-2 instead.
|
|
default_md = sha256
|
|
|
|
# Extension to add when the -x509 option is used.
|
|
x509_extensions = v3_ca
|
|
|
|
[ req_distinguished_name ]
|
|
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
|
|
CN = #{root_CA_CERT_NAME}
|
|
O = Start9 Labs
|
|
OU = Embassy
|
|
|
|
[ v3_ca ]
|
|
# Extensions for a typical CA (`man x509v3_config`).
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer
|
|
basicConstraints = critical, CA:true
|
|
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
|
|
|
[ v3_intermediate_ca ]
|
|
# Extensions for a typical intermediate CA (`man x509v3_config`).
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer
|
|
basicConstraints = critical, CA:true, pathlen:0
|
|
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
|
|
|
[ usr_cert ]
|
|
# Extensions for client certificates (`man x509v3_config`).
|
|
basicConstraints = CA:FALSE
|
|
nsCertType = client, email
|
|
nsComment = "OpenSSL Generated Client Certificate"
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid,issuer
|
|
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
|
|
extendedKeyUsage = clientAuth, emailProtection
|
|
|
|
[ server_cert ]
|
|
# Extensions for server certificates (`man x509v3_config`).
|
|
basicConstraints = CA:FALSE
|
|
nsCertType = server
|
|
nsComment = "OpenSSL Generated Server Certificate"
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid,issuer:always
|
|
keyUsage = critical, digitalSignature, keyEncipherment
|
|
extendedKeyUsage = serverAuth
|
|
|
|
[ crl_ext ]
|
|
# Extension for CRLs (`man x509v3_config`).
|
|
authorityKeyIdentifier=keyid:always
|
|
|
|
[ ocsp ]
|
|
# Extension for OCSP signing certificates (`man ocsp`).
|
|
basicConstraints = CA:FALSE
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid,issuer
|
|
keyUsage = critical, digitalSignature
|
|
extendedKeyUsage = critical, OCSPSigning
|
|
|]
|
|
|
|
intermediate_CA_OPENSSL_CONF :: Text -> ByteString
|
|
intermediate_CA_OPENSSL_CONF path = [i|
|
|
# OpenSSL intermediate CA configuration file.
|
|
# Copy to `/root/ca/intermediate/openssl.cnf`.
|
|
|
|
[ ca ]
|
|
# `man ca`
|
|
default_ca = CA_default
|
|
|
|
[ CA_default ]
|
|
# Directory and file locations.
|
|
dir = #{path}
|
|
certs = $dir/certs
|
|
crl_dir = $dir/crl
|
|
new_certs_dir = $dir/newcerts
|
|
database = $dir/index.txt
|
|
serial = $dir/serial
|
|
RANDFILE = $dir/private/.rand
|
|
|
|
# The root key and root certificate.
|
|
private_key = $dir/private/intermediate.key.pem
|
|
certificate = $dir/certs/intermediate.cert.pem
|
|
|
|
# For certificate revocation lists.
|
|
crlnumber = $dir/crlnumber
|
|
crl = $dir/crl/intermediate.crl.pem
|
|
crl_extensions = crl_ext
|
|
default_crl_days = 30
|
|
|
|
# SHA-1 is deprecated, so use SHA-2 instead.
|
|
default_md = sha256
|
|
|
|
name_opt = ca_default
|
|
cert_opt = ca_default
|
|
default_days = 375
|
|
preserve = no
|
|
copy_extensions = copy
|
|
policy = policy_loose
|
|
|
|
|
|
[ policy_loose ]
|
|
# Allow the intermediate CA to sign a more diverse range of certificates.
|
|
# See the POLICY FORMAT section of the `ca` man page.
|
|
countryName = optional
|
|
stateOrProvinceName = optional
|
|
localityName = optional
|
|
organizationName = optional
|
|
organizationalUnitName = optional
|
|
commonName = supplied
|
|
emailAddress = optional
|
|
|
|
[ req ]
|
|
# Options for the `req` tool (`man req`).
|
|
default_bits = 4096
|
|
distinguished_name = req_distinguished_name
|
|
string_mask = utf8only
|
|
prompt = no
|
|
|
|
# SHA-1 is deprecated, so use SHA-2 instead.
|
|
default_md = sha256
|
|
|
|
# Extension to add when the -x509 option is used.
|
|
x509_extensions = v3_ca
|
|
|
|
[ req_distinguished_name ]
|
|
CN = Embassy Local Intermediate CA
|
|
O = Start9 Labs
|
|
OU = Embassy
|
|
|
|
[ v3_ca ]
|
|
# Extensions for a typical CA (`man x509v3_config`).
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer
|
|
basicConstraints = critical, CA:true
|
|
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
|
|
|
[ v3_intermediate_ca ]
|
|
# Extensions for a typical intermediate CA (`man x509v3_config`).
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer
|
|
basicConstraints = critical, CA:true, pathlen:0
|
|
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
|
|
|
[ usr_cert ]
|
|
# Extensions for client certificates (`man x509v3_config`).
|
|
basicConstraints = CA:FALSE
|
|
nsCertType = client, email
|
|
nsComment = "OpenSSL Generated Client Certificate"
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid,issuer
|
|
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
|
|
extendedKeyUsage = clientAuth, emailProtection
|
|
|
|
[ server_cert ]
|
|
# Extensions for server certificates (`man x509v3_config`).
|
|
basicConstraints = CA:FALSE
|
|
nsCertType = server
|
|
nsComment = "OpenSSL Generated Server Certificate"
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid,issuer:always
|
|
keyUsage = critical, digitalSignature, keyEncipherment
|
|
extendedKeyUsage = serverAuth
|
|
|
|
[ crl_ext ]
|
|
# Extension for CRLs (`man x509v3_config`).
|
|
authorityKeyIdentifier=keyid:always
|
|
|
|
[ ocsp ]
|
|
# Extension for OCSP signing certificates (`man ocsp`).
|
|
basicConstraints = CA:FALSE
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid,issuer
|
|
keyUsage = critical, digitalSignature
|
|
extendedKeyUsage = critical, OCSPSigning
|
|
|]
|
|
|
|
domain_CSR_CONF :: Text -> ByteString
|
|
domain_CSR_CONF name = [i|
|
|
[req]
|
|
default_bits = 4096
|
|
default_md = sha256
|
|
distinguished_name = req_distinguished_name
|
|
prompt = no
|
|
|
|
[req_distinguished_name]
|
|
CN = #{name}
|
|
O = Start9 Labs
|
|
OU = Embassy
|
|
|]
|
|
|
|
writeRootCaCert :: MonadIO m => FilePath -> FilePath -> FilePath -> m (ExitCode, String, String)
|
|
writeRootCaCert confPath keyFilePath certFileDestinationPath = liftIO $ readProcessWithExitCode
|
|
"openssl"
|
|
[ "req"
|
|
, -- use x509
|
|
"-new"
|
|
, -- new request
|
|
"-x509"
|
|
, -- self signed x509
|
|
"-nodes"
|
|
, -- no passphrase
|
|
"-days"
|
|
, -- expires in...
|
|
"3650"
|
|
, -- valid for 10 years. Max is 20 years
|
|
"-key"
|
|
, -- source private key
|
|
toS keyFilePath
|
|
, "-out"
|
|
-- target cert path
|
|
, toS certFileDestinationPath
|
|
, "-config"
|
|
-- configured by...
|
|
, toS confPath
|
|
]
|
|
""
|
|
|
|
data DeriveCertificate = DeriveCertificate
|
|
{ applicantConfPath :: FilePath
|
|
, applicantKeyPath :: FilePath
|
|
, applicantCertPath :: FilePath
|
|
, signingConfPath :: FilePath
|
|
, signingKeyPath :: FilePath
|
|
, signingCertPath :: FilePath
|
|
, duration :: Integer
|
|
}
|
|
writeIntermediateCert :: MonadIO m => DeriveCertificate -> m (ExitCode, String, String)
|
|
writeIntermediateCert DeriveCertificate {..} = liftIO $ fromSys $ interpret $ do
|
|
lift . lift $ time "Intermediate Cert Write Start"
|
|
-- openssl genrsa -out dump/int.key 4096
|
|
segment $ openssl [i|ecparam -genkey -name prime256v1 -noout -out #{applicantKeyPath}|]
|
|
lift . lift $ time "Generate intermediate RSA Key"
|
|
-- openssl req -new -config dump/int-csr.conf -key dump/int.key -nodes -out dump/int.csr
|
|
segment $ openssl [i|req -new
|
|
-config #{applicantConfPath}
|
|
-key #{applicantKeyPath}
|
|
-nodes
|
|
-out #{applicantCertPath <> ".csr"}|]
|
|
lift . lift $ time "Generate intermediate CSR"
|
|
-- openssl x509 -CA dump/ca.crt -CAkey dump/ca.key -CAcreateserial -days 3650 -req -in dump/int.csr -out dump/int.crt
|
|
segment $ openssl [i|ca -batch
|
|
-config #{signingConfPath}
|
|
-rand_serial
|
|
-keyfile #{signingKeyPath}
|
|
-cert #{signingCertPath}
|
|
-extensions v3_intermediate_ca
|
|
-days #{duration}
|
|
-notext
|
|
-in #{applicantCertPath <> ".csr"}
|
|
-out #{applicantCertPath}|]
|
|
lift . lift $ time "Sign intermediate certificate"
|
|
liftIO $ readFile signingCertPath >>= appendFile applicantCertPath
|
|
lift . lift $ time "Update certificate chain"
|
|
|
|
writeLeafCert :: MonadIO m => DeriveCertificate -> Text -> Text -> m (ExitCode, String, String)
|
|
writeLeafCert DeriveCertificate {..} hostname torAddress = liftIO $ fromSys $ interpret $ do
|
|
lift . lift $ time "Leaf Cert Write Start"
|
|
segment $ openssl [i|ecparam -genkey -name prime256v1 -noout -out #{applicantKeyPath}|]
|
|
lift . lift $ time "Generate leaf RSA Key"
|
|
segment $ openssl [i|req -config #{applicantConfPath}
|
|
-key #{applicantKeyPath}
|
|
-new
|
|
-addext subjectAltName=DNS:#{hostname},DNS:*.#{hostname},DNS:#{torAddress},DNS:*.#{torAddress}
|
|
-out #{applicantCertPath <> ".csr"}|]
|
|
lift . lift $ time "Generate leaf CSR"
|
|
segment $ openssl [i|ca -batch
|
|
-config #{signingConfPath}
|
|
-rand_serial
|
|
-keyfile #{signingKeyPath}
|
|
-cert #{signingCertPath}
|
|
-extensions server_cert
|
|
-days #{duration}
|
|
-notext
|
|
-in #{applicantCertPath <> ".csr"}
|
|
-out #{applicantCertPath}
|
|
|]
|
|
lift . lift $ time "Sign leaf CSR"
|
|
liftIO $ readFile signingCertPath >>= appendFile applicantCertPath
|
|
lift . lift $ time "Update certificate chain"
|
|
|
|
openssl :: MonadIO m => Text -> m (ExitCode, String, String)
|
|
openssl = liftIO . ($ "") . readProcessWithExitCode "openssl" . fmap toS . words
|
|
{-# INLINE openssl #-}
|
|
|
|
interpret :: MonadIO m => ExceptT ExitCode (StateT (String, String) m) () -> m (ExitCode, String, String)
|
|
interpret = fmap (over _1 (either id (const ExitSuccess)) . regroup) . flip runStateT ("", "") . runExceptT
|
|
{-# INLINE interpret #-}
|
|
|
|
regroup :: (a, (b, c)) -> (a, b, c)
|
|
regroup (a, (b, c)) = (a, b, c)
|
|
{-# INLINE regroup #-}
|
|
|
|
segment :: MonadIO m => m (ExitCode, String, String) -> ExceptT ExitCode (StateT (String, String) m) ()
|
|
segment action = (lift . lift) action >>= \case
|
|
(ExitSuccess, o, e) -> modify (bimap (<> o) (<> e))
|
|
(ec , o, e) -> modify (bimap (<> o) (<> e)) *> throwE ec
|
|
{-# INLINE segment #-}
|
|
|
|
time :: MonadIO m => Text -> StateT UTCTime m ()
|
|
time t = do
|
|
last <- Startlude.get
|
|
now <- liftIO getCurrentTime
|
|
putStrLn @Text [i|#{t}: #{diffUTCTime now last}|]
|
|
put now
|
|
|
|
fromSys :: MonadIO m => StateT UTCTime m a -> m a
|
|
fromSys m = liftIO getCurrentTime >>= evalStateT m
|
|
|