danecheck-1.0.0.0: Dane/Scanner/DNS/Dane.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
module Dane.Scanner.DNS.Dane (daneCheck) where
import qualified Data.Text as T
import Control.Monad (when)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (runExceptT)
import Control.Monad.Trans.State.Strict (gets)
import Data.List (nub, sortOn)
import Data.IP (IP(IPv4, IPv6))
import Net.DNSBase ( Domain(RootDomain)
, T_a(T_A), T_aaaa(T_AAAA)
, T_dnskey, T_ds, T_mx(..), T_soa, T_tlsa
, RRTYPE(SOA)
, appendDomain, dnLit8, shortBytes
)
import Net.DNSBase.Present (putBuilder)
import Net.DNSBase.Resolver (withResolver)
import qualified Text.IDNA2008 as I
import qualified Text.IDNA2008.Wire as IW
import qualified Dane.Scanner.Opts as Opts
import Dane.Scanner.State
import Dane.Scanner.Util
import Dane.Scanner.DNS.Lookup
import Dane.Scanner.DNS.Reply
import Dane.Scanner.DNS.Toascii
import Dane.Scanner.SMTP.Chain
-- | Prefix prepended to a TLSA base domain to form the TLSA
-- query name (e.g. @_25._tcp.mx1.example.com@). Built once at
-- compile-time via 'DNS.dnLit8'; combined with the per-host base
-- via 'DNS.appendDomain', which returns 'Nothing' when the
-- result would overflow the 255-octet wire-form name limit.
tlsaPrefix :: Domain
tlsaPrefix = $$(dnLit8 "_25._tcp.")
-- | Skip hosts that securely don't exist.
skipHost :: RC -> Bool
skipHost NXDomainRC = True
skipHost _ = False
-- | Label-form set acceptable for an MX hostname received from
-- DNS: LDH + ALABEL + RLDH + FAKEA. Excludes ATTRLEAF
-- (leading-underscore service labels are not MX exchanger
-- names), ULABEL (wire form is never UTF-8 here), WILDLABEL,
-- OCTET, BLANK, LAXULABEL. xn-- labels are accepted whether or
-- not they decode as valid Punycode — FAKEA admission means we
-- don't reject names whose Punycode happens not to decode.
mxHostForms :: I.LabelFormSet
mxHostForms = mempty I.<+> I.LDH I.<+> I.ALABEL I.<+> I.RLDH I.<+> I.FAKEA
-- | Validate an MX hostname's label shape. Classifies the
-- wire-form bytes directly via 'IW.unparseOpts' (no 'Text'
-- round-trip) against 'mxHostForms'. Flags are 'mempty' —
-- pure shape classification, with ALABELCHECK / NFCCHECK /
-- BIDICHECK / character mappings all off, since none of them
-- are meaningful constraints on data received off the wire.
validMXHost :: Domain -> Bool
validMXHost dom =
case IW.unparseOpts mxHostForms mempty (shortBytes dom) of
Right _ -> True
Left _ -> False
-- | Given a non-empty A or AAAA response, try to find TLSA
-- records at the CNAME-expanded name if the entire chain is
-- secure. If no TLSA records are found there, and the CNAME
-- chain is non-empty, try at the initial name if secure.
--
getTLSA :: Reply a -- ^ MX-host address reply
-> Scanner (Maybe (Domain, Reply T_tlsa))
getTLSA a = case repValidity a of
Secure -> do
cnamebt <- baseTLSA (repOwner a)
case cnamebt of
Just (_, t) | done t -> return cnamebt
_ -> baseTLSA (repQname a)
Insecure
| repCnAD a
, nonempty (repRD a)
, nonempty (repCnames a) -> baseTLSA (repQname a)
_ -> return Nothing
where
-- Is the initial qname a candidate base domain?
done :: Reply T_tlsa -> Bool
done t = case repValidity t of
Indeterminate -> True
Secure -> True
_ | not (repCnAD a) -> True
| null (repCnames a) -> True
| otherwise -> False
baseTLSA :: Domain -> Scanner (Maybe (Domain, Reply T_tlsa))
baseTLSA b = case appendDomain tlsaPrefix b of
Nothing -> return Nothing -- host name too long for TLSA prefix
Just q -> do
rv <- gets scannerResolver
t <- liftIO $ rawReply T_tlsa rv q
return (Just (b, t))
-- | For each MX host display the corresponding per-host
-- information. This includes the A\/AAAA records and, when
-- DNSSEC-secure, the TLSA records and per-IP SMTP chain probes.
doMXHost :: Domain -- ^ Qname of MX RRset
-> Domain -- ^ MX RRset owner
-> Domain -- ^ MX host to process
-> Scanner ()
doMXHost qname owner mx
| Just _ <- rootOrTLD mx
= displayFailA (noReply TldMX False mx)
| not (validMXHost mx)
= displayFailA (noReply BadName False mx)
| otherwise = do
rv <- gets scannerResolver
a <- liftIO $ rawReply T_a rv mx
noteInsecure a
if skipHost (repRC a)
then liftIO $ putBuilder $ putReplyA a mempty
else do
aaaa <- liftIO $ rawReply T_aaaa rv mx
noteInsecure aaaa
opts <- gets scannerOpts
let doV4 = Opts.enableV4 opts
doV6 = Opts.enableV6 opts
v4addrs = if_ doV4 [ IPv4 ip | T_A ip <- repRD a ] []
v6addrs = if_ doV6 [ IPv6 ip | T_AAAA ip <- repRD aaaa ] []
connaddrs = v4addrs ++ v6addrs
bt <- getTLSA a
case bt of
Just (b, t) -> renderHost a aaaa (Just (b, t)) connaddrs
Nothing -> do
bt' <- getTLSA aaaa
case bt' of
Just (b, t) -> renderHost a aaaa (Just (b, t)) connaddrs
Nothing
| Opts.useAll opts -> do
let synth = case appendDomain tlsaPrefix mx of
Just n -> noReply NoErrorRC False n
Nothing -> noReply NoErrorRC False mx
renderHost a aaaa (Just (mx, synth)) connaddrs
| otherwise -> do
displayFailA a
displayFailAaaa aaaa
where
noteInsecure r = case repRC r of
NoErrorRC | repCnAD r -> return ()
NXDomainRC | repCnAD r -> return ()
_ -> scannerFail ()
displayFailA r = do
liftIO $ putBuilder $ putReplyA r mempty
scannerFail ()
displayFailAaaa r = do
liftIO $ putBuilder $ putReplyAaaa Nothing [] r mempty
scannerFail ()
renderHost :: Reply T_a -> Reply T_aaaa
-> Maybe (Domain, Reply T_tlsa)
-> [IP]
-> Scanner ()
renderHost a aaaa (Just (b, t)) connaddrs = do
chains <- getchains t b connaddrs
let tlsa = TlsaInfo { tlsaBase = b, tlsaRRset = t }
liftIO $ putBuilder $
putReplyA a . putReplyAaaa (Just tlsa) chains aaaa $ mempty
renderHost a aaaa Nothing _ =
liftIO $ putBuilder $
putReplyA a . putReplyAaaa Nothing [] aaaa $ mempty
getchains :: Reply T_tlsa -> Domain -> [IP] -> Scanner [AddrChain]
getchains tlsa base addrs = case repValidity tlsa of
Secure ->
let refnames = nub [base, qname, owner]
in getAddrChains mx base refnames (repRD tlsa) addrs
_ -> gets scannerOpts >>= \opts ->
if Opts.useAll opts
then scannerFail () >> getAddrChains mx base [] [] addrs
else scannerFail []
-- | If the MX RRset is secure (AD==True), for each MX host
-- perform A\/AAAA lookups and then TLSA lookups if those are in
-- turn secure.
chaseMX :: Reply T_mx -> Scanner ()
chaseMX m = case repAD m of
False -> return ()
True
| NoErrorRC <- repRC m, null (repRD m)
-> doMXHost (repQname m) (repOwner m) (repOwner m)
| otherwise
-> mapM_ (doMXHost (repQname m) (repOwner m)) (gethosts (repRD m))
where
gethosts mxs = nub [ exch | T_MX _ exch <- sortOn mxPref mxs
, exch /= RootDomain ]
-- | For sub-domains, check DS, DNSKEY, MX and then recurse over
-- the MX hosts for A\/AAAA and TLSA.
checkDomain :: Domain -> Scanner Bool
checkDomain dom = do
rv <- gets scannerResolver
ds <- liftIO $ rawReply T_ds rv dom
liftIO $ putBuilder $ putReplyDs ds mempty
if not (repValidated ds)
then return False
else do
dk <- liftIO $ rawReply T_dnskey rv dom
liftIO $ putBuilder $ putReplyDnskey dk mempty
if not (repValidated dk)
then return False
else do
m <- liftIO $ rawReply T_mx rv dom
liftIO $ putBuilder $ putReplyMx m mempty
when (not (repValidated m)) (scannerFail ())
chaseMX m
gets scannerOK
-- | For the root and TLDs, check DS (if applicable), DNSKEY, and
-- SOA in turn. Each step requires a secure validated answer; the
-- first failure stops the chain. DS is skipped at the root, which
-- has no parent zone and therefore no DS RRset to look up.
checkTld :: Domain -> Scanner Bool
checkTld tld = do
rv <- gets scannerResolver
dsOK <- case tld of
RootDomain -> return True
_ -> do
ds <- liftIO $ rawReply T_ds rv tld
liftIO $ putBuilder $ putReplyDs ds mempty
return (repValidity ds == Secure)
if not dsOK
then return False
else do
dk <- liftIO $ rawReply T_dnskey rv tld
liftIO $ putBuilder $ putReplyDnskey dk mempty
if repValidity dk /= Secure
then return False
else do
soa <- liftIO $ rawReply T_soa rv tld
liftIO $ putBuilder $ putReplyAny SOA soa mempty
return (repValidity soa == Secure)
-- | For the root domain and TLDs simply check for working
-- DNSSEC. For sub-domains attempt to verify MX host DANE TLSA.
check :: String -> Scanner Bool
check domain = case todomain forms flags (T.pack domain) of
Just dom
| Just _ <- rootOrTLD dom -> checkTld dom
| otherwise -> checkDomain dom
Nothing -> do
liftIO $ putStrLn $ domain ++ " IN MX ? ; Invalid AD=0"
return False
where
-- Hostname-shaped label set: LDH + ALABEL + ULABEL + RLDH +
-- FAKEA. Excludes ATTRLEAF (leading-underscore service
-- labels like @_dmarc@, @_25@), WILDLABEL, OCTET, BLANK,
-- LAXULABEL — none of which a user-typed mail domain has
-- any legitimate use for. Tighter than the pre-port
-- Toascii.hs, which short-circuited any all-ASCII input
-- through without validation and accepted ATTRLEAF and
-- worse by accident.
forms = I.hostnameLabelForms
flags = I.defaultIdnaFlags <> I.allIdnaMappings
-- | Scan the requested domain after seeding the DNS resolver.
daneCheck :: Opts.Opts -> IO Bool
daneCheck opts = do
seedRes <- runExceptT (dnsConf opts)
case seedRes of
Left e -> do
putStrLn $ "Resolver configuration error: " ++ show e
return False
Right seed -> withResolver seed $ \rv ->
evalScanner (check (Opts.dnsDomain opts))
ScannerSt { scannerOpts = opts
, scannerResolver = rv
, scannerOK = True }