packages feed

danecheck-1.0.0.0: Dane/Scanner/SMTP/Certs.hs

{-# LANGUAGE RecordWildCards #-}

module Dane.Scanner.SMTP.Certs
    ( cnOf
    , orgOf
    , encodeDER
    , genChainInfo
    , ChainInfo
    , CertInfo(..)
    , CertHashes(..)
    , HostName
    ) where

import           Crypto.Hash                      (hashWith)
import           Crypto.Hash.Algorithms           (SHA256(..), SHA512(..))
import           Data.ASN1.BinaryEncoding
import           Data.ASN1.Encoding
import           Data.ASN1.Types
import qualified Data.ByteArray as BA
import           Data.ByteString                  (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LB
import           Data.Hourglass (timeConvert, DateTime)
import           Data.IORef (IORef, writeIORef)
import           Data.Int (Int64)
import           Data.List (find)
import           Data.Maybe (catMaybes)
import           Data.X509
import           Data.X509.CertificateStore
import           Data.X509.Validation
import           Foreign.C.Types (CTime(..))

import           Dane.Scanner.Util

data CertHashes =
     CertHashes
         { _cert256, _spki256, _cert512, _spki512 :: ByteString }

type Timestamp = Int64

type LifeSpan = (Timestamp, Timestamp)

data CertInfo = CertInfo { _depth :: !Int -- chain depth
                         , _idn :: !DistinguishedName -- issuer
                         , _life :: !LifeSpan         -- dates
                         , _sdn :: !DistinguishedName -- subject
                         , _hashes :: !CertHashes
                         , _cert :: !ByteString -- Cert as DER ByteString
                         , _spki :: !ByteString -- Cert as DER ByteString
                         , _alg :: !PubKeyALG
                         }

cnOf :: DistinguishedName -> Maybe String
cnOf dn = getDnElement DnCommonName dn >>= asn1CharacterToString

orgOf :: DistinguishedName -> Maybe String
orgOf dn = getDnElement DnOrganization dn >>= asn1CharacterToString

type ChainInfo = ( [HostName] -- DNS-IDs or CN-ID from leaf cert
                 , [CertInfo] -- Constructed chain
                 , Int64      -- Time observed
                 )

genChainInfo :: IORef ChainInfo
             -> CertificateStore
             -> ValidationCache
             -> ServiceID
             -> CertificateChain
             -> IO [FailedReason]
genChainInfo cref _ _ _ chain = do
    now <- gettime
    writeIORef cref $ buildChain now chain
    return [] -- Always succeed

buildChain :: Int64 -> CertificateChain -> ChainInfo
buildChain now (CertificateChain xs) =
    case xs of
      [] -> error "Empty certificate chain"
      top:chain ->
          let names = getNames top
              topinfo = certInfo 0 top
              (issuer, rest) = getIssuer 0 chain top
              infos = maybe [topinfo] ((topinfo:). getChainInfo 1 rest) issuer
          in (names, infos, now)
    where
        certInfo _depth scert =
            let cert = getCertificate scert
                _cert = encodeSignedObject scert
                _spki = encodeDER $ certPubKey cert
                _hashes = getHashes _cert _spki
                _idn = certIssuerDN cert
                _life = getDates cert
                _sdn = certSubjectDN cert
                _alg = pubkeyToAlg $ certPubKey cert
             in CertInfo{..}

        getDates :: Certificate -> LifeSpan
        getDates = toTimestampTuple . certValidity
            where
                toTimestampTuple :: (DateTime, DateTime) -> LifeSpan
                toTimestampTuple = (,) <$> toTimestamp . fst <*> toTimestamp . snd
                    where
                        toTimestamp :: DateTime -> Timestamp
                        toTimestamp = (\(CTime x) -> x) . timeConvert

        getChainInfo :: Int
                     -> [SignedCertificate]
                     -> SignedCertificate
                     -> [CertInfo]
        getChainInfo depth cc cert =
            let info = certInfo depth cert
                (issuer, rest) = getIssuer depth cc cert
            in maybe [info] ((info:). getChainInfo (depth + 1) rest) issuer

getIssuer :: Int
          -> [SignedCertificate]
          ->  SignedCertificate
          -> ( Maybe (SignedCertificate)
             ,       [SignedCertificate]
             )
getIssuer dep cc cert =
    let iss = (certIssuerDN . getCertificate $ cert)
        isMatch = (==iss) . certSubjectDN . getCertificate
        isCA c = case certVersion c of
                     0 -> True
                     2 -> checkV3CA dep c
                     _ -> False
        isSigner = checkSignature cert
     in nulled $ (\x -> (x, filter (/= x) cc)) <$>
                 find (\s -> isMatch s && isCA (getCertificate s)
                                       && isSigner s) cc
    where
        nulled :: Maybe (a,[b]) -> (Maybe a, [b])
        nulled Nothing = (Nothing, [])
        nulled (Just (x,ys)) = (Just x, ys)

getHashes :: ByteString -> ByteString -> CertHashes
getHashes cert spki =
    let _cert256 =  BS.pack $ BA.unpack $ hashWith SHA256 cert
        _spki256 =  BS.pack $ BA.unpack $ hashWith SHA256 spki
        _cert512 =  BS.pack $ BA.unpack $ hashWith SHA512 cert
        _spki512 =  BS.pack $ BA.unpack $ hashWith SHA512 spki
     in CertHashes{..}

encodeDER :: ASN1Object a => a -> ByteString
encodeDER = LB.toStrict . encodeASN1 DER . flip toASN1 []

getNames :: SignedCertificate -> [HostName]
getNames scert =
    let cert = getCertificate scert
        sdn = certSubjectDN cert
        altNames = maybe [] toAltName $ extensionGet $ certExtensions cert
     in case altNames of
          [] -> case cnOf sdn of
                    Just s -> [s]
                    Nothing -> []
          x -> x
    where
        unAltName :: AltName -> Maybe HostName
        unAltName (AltNameDNS s) = Just s
        unAltName _              = Nothing

        toAltName :: ExtSubjectAltName -> [HostName]
        toAltName (ExtSubjectAltName names) = catMaybes $ map unAltName names

checkV3CA :: Int -> Certificate -> Bool
checkV3CA level cert = allowedSign && allowedCA && allowedDepth
    where extensions  = certExtensions cert
          allowedSign = case extensionGet extensions of
                          Just (ExtKeyUsage flags) -> KeyUsage_keyCertSign `elem` flags
                          Nothing                  -> True
          (allowedCA,pathLen) = case extensionGet extensions of
                                  Just (ExtBasicConstraints True pl) -> (True, pl)
                                  _                                  -> (False, Nothing)
          allowedDepth = case pathLen of
                           Nothing                            -> True
                           Just pl | fromIntegral pl >= level -> True
                                   | otherwise                -> False

checkSignature :: SignedCertificate -> SignedCertificate -> Bool
checkSignature signedCert signingCert =
    case verifySignedSignature signedCert (certPubKey $ getCertificate signingCert) of
      SignaturePass     -> True
      SignatureFailed _ -> False