packages feed

htalkat-0.1.1: Identity.hs

-- This file is part of htalkat
-- Copyright (C) 2021 Martin Bays <mbays@sdf.org>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of version 3 of the GNU General Public License as
-- published by the Free Software Foundation, or any later version.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see http://www.gnu.org/licenses/.

{-# LANGUAGE CPP               #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module Identity where

import           Network.TLS              (Credential, credentialLoadX509)
import           System.FilePath          ((<.>), (</>))

import           Data.ASN1.BinaryEncoding (DER (..))
import           Data.ASN1.Encoding       (encodeASN1')
import           Data.ASN1.OID
import           Data.ASN1.Types          (ASN1Object (..))
import           Data.ASN1.Types.String   (ASN1StringEncoding (UTF8))
import           Data.Hourglass
import           Data.PEM
import           Data.X509                as X

#ifndef WINDOWS
import           System.Posix.Files       (ownerReadMode, ownerWriteMode,
                                           setFileMode, unionFileModes)
#endif

import qualified Crypto.PubKey.Ed25519    as Ed25519
import qualified Data.ByteArray           as BA
import qualified Data.ByteString          as BS
import qualified Data.Text                as TS
import qualified Data.Text.Encoding       as TS

import           Certificate
import           Fingerprint
import           Host
import           Mundanities
import           Petname
import           Prompt
import           User
import           Util

data IdentityType = IdConnect | IdListen deriving Eq

loadIdentity :: FilePath -> IdentityType -> IO (Maybe Credential)
loadIdentity ddir tp = do
    let base = ddir </> "id"
        certpath = base <> (if tp == IdConnect then "-connect" else "-listen") <.> "crt"
        keypath = base <.> "key"
    ignoreIOErrAlt $ eitherToMaybe <$> credentialLoadX509 certpath keypath

saveIdentity :: FilePath -> PrivKey -> CertificateChain -> CertificateChain -> IO ()
saveIdentity ddir secKey connectChain listenChain = do
    let base = ddir </> "id"
        connectCertpath = base <> "-connect" <.> "crt"
        listenCertpath = base <> "-listen" <.> "crt"
        keypath = base <.> "key"
    ignoreIOErr $ do
        writeChain connectCertpath connectChain
        writeChain listenCertpath listenChain
        BS.writeFile keypath . pemWriteBS . PEM "PRIVATE KEY" [] . encodeDER $ secKey
#ifndef WINDOWS
        setFileMode keypath $ unionFileModes ownerReadMode ownerWriteMode -- chmod 600
#endif
    where
    writeChain certpath chain =
        let CertificateChainRaw rawCerts = encodeCertificateChain chain
            chainPEMs = map (pemWriteBS . PEM "CERTIFICATE" []) rawCerts
        in BS.writeFile certpath $ BS.intercalate "\n" chainPEMs
    encodeDER :: ASN1Object o => o -> BS.ByteString
    encodeDER = encodeASN1' DER . (`toASN1` [])

createOrShowIdentity :: FilePath -> Maybe String -> IO ()
createOrShowIdentity ddir mCN =
    mapM (loadIdentity ddir) [IdConnect,IdListen] >>= \case
        [Just (connectChain,_), Just (_,_)]
                | Just cert <- takeTailCert connectChain
                , Nothing <- mCN -> do
            mapM_ putStrLn
                [ "Your fingerprint: talkat:" <> showFingerprint (spkiFingerprint cert)
                , "Your public name: " <> certCN cert
                ]
        [Just (_,key), Just (listenChain,_)]
                | Just cn <- mCN ->
            case key of
                PrivKeyEd25519 secKey -> do
                    connectChain' <- generateSelfSigned secKey cn
                    saveIdentity ddir key connectChain' listenChain
                _ -> putStrLn "Error: Can't regenerate non-ED25519 key!"
        _ -> do
            putStrLn "Generating new identity."
            secKey <- Ed25519.generateSecretKey
            let promptCN = do
                    putStrLn "Enter a public name for this identity (can be blank)."
                    putStrLn "This will be shown to anyone you connect to, and only to them."
                    putStrLn "(You can change this later by rerunning this command)"
                    promptLine "Public name: "
            cn <- maybe promptCN pure mCN
            connectChain <- generateSelfSigned secKey cn
            listenChain <- generateSelfSigned secKey ""
            saveIdentity ddir (PrivKeyEd25519 secKey) connectChain listenChain
            let Just cert = takeTailCert connectChain
                fp = spkiFingerprint cert
            putStrLn $ "Your fingerprint: talkat:" <> showFingerprint fp
            writeName ddir (User fp (parseHost "localhost")) $ Named "self"

generateSelfSigned :: Ed25519.SecretKey -> String -> IO CertificateChain
generateSelfSigned secKey cn =
    let dn = DistinguishedName [(getObjectID DnCommonName,
            ASN1CharacterString UTF8 . TS.encodeUtf8 $ TS.pack cn)]
        sigAlg = SignatureALG_IntrinsicHash PubKeyALG_Ed25519
        -- RFC5280: To indicate that a certificate has no well-defined expiration
        -- date, the notAfter SHOULD be assigned the GeneralizedTime value of
        -- 99991231235959Z.
        notAfterMax :: DateTime
        notAfterMax = DateTime (Date 9999 December 31) (TimeOfDay 23 59 59 0)

        -- RFC5280 has no corresponding prescription for notBefore.
        -- 19500101000000Z seems the canonical choice, but it seems to get
        -- loaded as 2050 for some reason. So we use 1970.
        notBeforeMin :: DateTime
        notBeforeMin = DateTime (Date 1970 January 1) (TimeOfDay 0 0 0 0)

        cert pubKey = X.Certificate
            { certVersion = 2
            , certSerial = 0
            , certSignatureAlg = sigAlg
            , certIssuerDN = dn
            , certSubjectDN = dn
            , certValidity = (timeConvert notBeforeMin, timeConvert notAfterMax)
            , certPubKey = pubKey
            , certExtensions = Extensions Nothing
            }
    in do
        let pubKey = Ed25519.toPublic secKey
        let signed = fst $ objectToSignedExact
                (\b -> (BS.pack . BA.unpack $ Ed25519.sign secKey pubKey b, sigAlg, ()))
                (cert $ PubKeyEd25519 pubKey)
        pure $ CertificateChain [signed]