{-# Language OverloadedStrings #-} {-| Module : Client.Commands.Certificate Description : Certificate management commands Copyright : (c) Eric Mertens, 2016-2020 License : ISC Maintainer : emertens@gmail.com -} module Client.Commands.Certificate (newCertificateCommand) where import Client.Commands.Arguments.Spec import Client.Commands.TabCompletion import Client.Commands.Types import Client.State import Control.Applicative import Control.Exception import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as L import Data.Foldable (foldl') import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as Text import Data.Time import Hookup.OpenSSL (getPubKeyDer) import qualified OpenSSL.RSA as RSA import qualified OpenSSL.X509 as X509 import qualified OpenSSL.PEM as PEM import qualified OpenSSL.EVP.Cipher as Cipher import qualified OpenSSL.EVP.Digest as Digest import Text.Read (readMaybe) import Text.Printf (printf) keysizeArg :: Args a (Maybe (Int, String)) keysizeArg = optionalArg (liftA2 (,) (tokenArg "[keysize]" (const parseSize)) (remainingArg "[passphrase]")) parseSize :: String -> Maybe Int parseSize str = case readMaybe str of Just n | 1024 <= n, n <= 8192 -> Just n _ -> Nothing newCertificateCommand :: Command newCertificateCommand = Command (pure "new-self-signed-cert") (liftA2 (,) (simpleToken "filename") keysizeArg) "\^BParameters:\^B\n\ \\n\ \ filename: Certificate and private key PEM output\n\ \ keysize: Public-key size (default 2048, range 1024-8192)\n\ \ passphrase: Optional AES-128 private key passphrase\n\ \\n\ \\^BDescription:\^B\n\ \\n\ \ Generate a new self-signed certificate for network service\n\ \ identification.\n\ \\n\ \\^BExample command:\^B\n\ \\n\ \ /new-self-signed-cert /home/me/.glirc/config/my.pem 2048 SeCrEt\n\ \\n\ \\^BExample configuration:\^B\n\ \ servers:\n\ \ * name: \"fn\"\n\ \ hostname: \"irc.libera.chat\"\n\ \ sasl: mechanism: external\n\ \ tls: yes\n\ \ tls-client-cert: \"my.pem\"\n\ \ tls-client-key-password: \"SeCrEt\"\n" (ClientCommand cmdNewCert noClientTab) cmdNewCert :: ClientCommand (String, Maybe (Int, String)) cmdNewCert st (path, mbExtra) = do now <- getZonedTime let size = case mbExtra of Nothing -> 2048 Just (n,_) -> n pass <- case mbExtra of Just (_,p) | not (null p) -> do cipher <- fromMaybe (error "No aes128!") <$> Cipher.getCipherByName "aes128" pure (Just (cipher, PEM.PwStr p)) _ -> pure Nothing rsa <- RSA.generateRSAKey' size 65537 x509 <- X509.newX509 X509.setVersion x509 2 X509.setSerialNumber x509 1 X509.setIssuerName x509 [("CN","glirc")] X509.setSubjectName x509 [("CN","glirc")] X509.setNotBefore x509 (UTCTime (ModifiedJulianDay 40587) 0) -- 1970-01-01 X509.setNotAfter x509 (UTCTime (ModifiedJulianDay 77112) 0) -- 2070-01-01 X509.setPublicKey x509 rsa X509.signX509 x509 rsa Nothing ctder <- X509.writeDerX509 x509 pkder <- getPubKeyDer x509 msgss <- traverse (getFingerprint ctder pkder) ["sha1", "sha256", "sha512"] pem1 <- PEM.writePKCS8PrivateKey rsa pass pem2 <- PEM.writeX509 x509 res <- try (writeFile path (pem1 ++ pem2)) case res of Left e -> commandFailure (recordError now "" (Text.pack (displayException (e :: IOError))) st) Right () -> do let msg = "Certificate saved: \x02" <> path <> "\x02" commandSuccess (foldl' (recordSuccess now) st (concat msgss ++ [Text.pack msg])) getFingerprint :: L.ByteString -> B.ByteString -> String -> IO [Text] getFingerprint crt pub name = do mb <- Digest.getDigestByName name pure $ case mb of Nothing -> [] Just d -> map Text.pack [printf "CERT %-6s fingerprint: \^C07%s" name (hexString (Digest.digestLBS d crt)) ,printf "SPKI %-6s fingerprint: \^C07%s" name (hexString (Digest.digestBS d pub)) ] hexString :: B.ByteString -> String hexString = B.foldr (printf "%02x%s") ""