packages feed

glirc-2.40: src/Client/Commands/Certificate.hs

{-# 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 (noClientTab)
import Client.Commands.Types
import Client.State (recordError, recordSuccess)
import Control.Applicative (liftA2)
import Control.Exception (displayException, try)
import Data.ByteString qualified as B
import Data.ByteString.Lazy qualified as L
import Data.Foldable (foldl')
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Time (UTCTime(UTCTime), Day(ModifiedJulianDay), getZonedTime)
import Hookup.OpenSSL (getPubKeyDer)
import OpenSSL.EVP.Cipher qualified as Cipher
import OpenSSL.EVP.Digest qualified as Digest
import OpenSSL.PEM qualified as PEM
import OpenSSL.RSA qualified as RSA
import OpenSSL.X509 qualified as X509
import Text.Printf (printf)
import Text.Read (readMaybe)

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") ""