module Data.X509.Validation
(
module Data.X509.Validation.Types
, Fingerprint(..)
, FailedReason(..)
, SignatureFailure(..)
, ValidationChecks(..)
, ValidationHooks(..)
, defaultChecks
, defaultHooks
, validate
, validateDefault
, getFingerprint
, module Data.X509.Validation.Cache
) where
import Control.Applicative
import Control.Monad (when)
import Data.Default.Class
import Data.ASN1.Types
import Data.X509
import Data.X509.CertificateStore
import Data.X509.Validation.Signature
import Data.X509.Validation.Fingerprint
import Data.X509.Validation.Cache
import Data.X509.Validation.Types
import Data.Hourglass
import System.Hourglass
import Data.Maybe
import Data.List
data FailedReason =
UnknownCriticalExtension
| Expired
| InFuture
| SelfSigned
| UnknownCA
| NotAllowedToSign
| NotAnAuthority
| AuthorityTooDeep
| NoCommonName
| InvalidName String
| NameMismatch String
| InvalidWildcard
| LeafKeyUsageNotAllowed
| LeafKeyPurposeNotAllowed
| LeafNotV3
| EmptyChain
| CacheSaysNo String
| InvalidSignature SignatureFailure
deriving (Show,Eq)
data ValidationChecks = ValidationChecks
{
checkTimeValidity :: Bool
, checkAtTime :: Maybe DateTime
, checkStrictOrdering :: Bool
, checkCAConstraints :: Bool
, checkExhaustive :: Bool
, checkLeafV3 :: Bool
, checkLeafKeyUsage :: [ExtKeyUsageFlag]
, checkLeafKeyPurpose :: [ExtKeyUsagePurpose]
, checkFQHN :: Bool
} deriving (Show,Eq)
data ValidationHooks = ValidationHooks
{
hookMatchSubjectIssuer :: DistinguishedName -> Certificate -> Bool
, hookValidateTime :: DateTime -> Certificate -> [FailedReason]
, hookValidateName :: HostName -> Certificate -> [FailedReason]
, hookFilterReason :: [FailedReason] -> [FailedReason]
}
defaultChecks :: ValidationChecks
defaultChecks = ValidationChecks
{ checkTimeValidity = True
, checkAtTime = Nothing
, checkStrictOrdering = False
, checkCAConstraints = True
, checkExhaustive = False
, checkLeafV3 = True
, checkLeafKeyUsage = []
, checkLeafKeyPurpose = []
, checkFQHN = True
}
instance Default ValidationChecks where
def = defaultChecks
defaultHooks :: ValidationHooks
defaultHooks = ValidationHooks
{ hookMatchSubjectIssuer = matchSI
, hookValidateTime = validateTime
, hookValidateName = validateCertificateName
, hookFilterReason = id
}
instance Default ValidationHooks where
def = defaultHooks
validateDefault :: CertificateStore
-> ValidationCache
-> ServiceID
-> CertificateChain
-> IO [FailedReason]
validateDefault = validate HashSHA256 defaultHooks defaultChecks
validate :: HashALG
-> ValidationHooks
-> ValidationChecks
-> CertificateStore
-> ValidationCache
-> ServiceID
-> CertificateChain
-> IO [FailedReason]
validate _ _ _ _ _ _ (CertificateChain []) = return [EmptyChain]
validate hashAlg hooks checks store cache ident cc@(CertificateChain (top:_)) = do
cacheResult <- (cacheQuery cache) ident fingerPrint (getCertificate top)
case cacheResult of
ValidationCachePass -> return []
ValidationCacheDenied s -> return [CacheSaysNo s]
ValidationCacheUnknown -> do
validationTime <- maybe (timeConvert <$> timeCurrent) return $ checkAtTime checks
failedReasons <- doValidate validationTime hooks checks store ident cc
when (null failedReasons) $ (cacheAdd cache) ident fingerPrint (getCertificate top)
return failedReasons
where fingerPrint = getFingerprint top hashAlg
doValidate :: DateTime
-> ValidationHooks
-> ValidationChecks
-> CertificateStore
-> ServiceID
-> CertificateChain
-> IO [FailedReason]
doValidate _ _ _ _ _ (CertificateChain []) = return [EmptyChain]
doValidate validationTime hooks checks store (fqhn,_) (CertificateChain (top:rchain)) =
(hookFilterReason hooks) <$> (return doLeafChecks |> doCheckChain 0 top rchain)
where isExhaustive = checkExhaustive checks
a |> b = exhaustive isExhaustive a b
doLeafChecks = doNameCheck top ++ doV3Check topCert ++ doKeyUsageCheck topCert
where topCert = getCertificate top
doCheckChain :: Int -> SignedCertificate -> [SignedCertificate] -> IO [FailedReason]
doCheckChain level current chain = do
r <- doCheckCertificate (getCertificate current)
return r |> (case findCertificate (certIssuerDN cert) store of
Just trustedSignedCert -> return $ checkSignature current trustedSignedCert
Nothing | isSelfSigned cert -> return [SelfSigned] |> return (checkSignature current current)
| null chain -> return [UnknownCA]
| otherwise ->
case findIssuer (certIssuerDN cert) chain of
Nothing -> return [UnknownCA]
Just (issuer, remaining) ->
return (checkCA level $ getCertificate issuer)
|> return (checkSignature current issuer)
|> doCheckChain (level+1) issuer remaining)
where cert = getCertificate current
findIssuer issuerDN chain
| checkStrictOrdering checks =
case chain of
[] -> error "not possible"
(c:cs) | matchSubjectIdentifier issuerDN (getCertificate c) -> Just (c, cs)
| otherwise -> Nothing
| otherwise =
(\x -> (x, filter (/= x) chain)) `fmap` find (matchSubjectIdentifier issuerDN . getCertificate) chain
matchSubjectIdentifier = hookMatchSubjectIssuer hooks
checkCA :: Int -> Certificate -> [FailedReason]
checkCA level cert
| not (checkCAConstraints checks) = []
| and [allowedSign,allowedCA,allowedDepth] = []
| otherwise = (if allowedSign then [] else [NotAllowedToSign])
++ (if allowedCA then [] else [NotAnAuthority])
++ (if allowedDepth then [] else [AuthorityTooDeep])
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
doNameCheck cert
| not (checkFQHN checks) = []
| otherwise = (hookValidateName hooks) fqhn (getCertificate cert)
doV3Check cert
| checkLeafV3 checks = case certVersion cert of
2 -> []
_ -> [LeafNotV3]
| otherwise = []
doKeyUsageCheck cert =
compareListIfExistAndNotNull mflags (checkLeafKeyUsage checks) LeafKeyUsageNotAllowed
++ compareListIfExistAndNotNull mpurposes (checkLeafKeyPurpose checks) LeafKeyPurposeNotAllowed
where mflags = case extensionGet $ certExtensions cert of
Just (ExtKeyUsage keyflags) -> Just keyflags
Nothing -> Nothing
mpurposes = case extensionGet $ certExtensions cert of
Just (ExtExtendedKeyUsage keyPurposes) -> Just keyPurposes
Nothing -> Nothing
compareListIfExistAndNotNull Nothing _ _ = []
compareListIfExistAndNotNull (Just list) expected err
| null expected = []
| intersect expected list == expected = []
| otherwise = [err]
doCheckCertificate cert =
exhaustiveList (checkExhaustive checks)
[ (checkTimeValidity checks, return ((hookValidateTime hooks) validationTime cert))
]
isSelfSigned :: Certificate -> Bool
isSelfSigned cert = certSubjectDN cert == certIssuerDN cert
checkSignature signedCert signingCert =
case verifySignedSignature signedCert (certPubKey $ getCertificate signingCert) of
SignaturePass -> []
SignatureFailed r -> [InvalidSignature r]
validateTime :: DateTime -> Certificate -> [FailedReason]
validateTime currentTime cert
| currentTime < before = [InFuture]
| currentTime > after = [Expired]
| otherwise = []
where (before, after) = certValidity cert
getNames :: Certificate -> (Maybe String, [String])
getNames cert = (commonName >>= asn1CharacterToString, altNames)
where commonName = getDnElement DnCommonName $ certSubjectDN cert
altNames = maybe [] toAltName $ extensionGet $ certExtensions cert
toAltName (ExtSubjectAltName names) = catMaybes $ map unAltName names
where unAltName (AltNameDNS s) = Just s
unAltName _ = Nothing
validateCertificateName :: HostName -> Certificate -> [FailedReason]
validateCertificateName fqhn cert =
case commonName of
Nothing -> [NoCommonName]
Just cn -> findMatch [] $ map (matchDomain . splitDot) (cn : altNames)
where (commonName, altNames) = getNames cert
findMatch :: [FailedReason] -> [[FailedReason]] -> [FailedReason]
findMatch _ [] = [NameMismatch fqhn]
findMatch _ ([]:_) = []
findMatch acc (_ :xs) = findMatch acc xs
matchDomain :: [String] -> [FailedReason]
matchDomain l
| length (filter (== "") l) > 0 = [InvalidName (intercalate "." l)]
| head l == "*" = wildcardMatch (reverse $ drop 1 l)
| l == splitDot fqhn = []
| otherwise = [NameMismatch fqhn]
wildcardMatch l
| length l < 2 = [InvalidWildcard]
| length (head l) <= 2 && length (head $ drop 1 l) <= 3 && length l < 3 = [InvalidWildcard]
| l == take (length l) (reverse $ splitDot fqhn) = []
| otherwise = [NameMismatch fqhn]
splitDot :: String -> [String]
splitDot [] = [""]
splitDot x =
let (y, z) = break (== '.') x in
y : (if z == "" then [] else splitDot $ drop 1 z)
matchSI :: DistinguishedName -> Certificate -> Bool
matchSI issuerDN issuer = certSubjectDN issuer == issuerDN
exhaustive :: Monad m => Bool -> m [FailedReason] -> m [FailedReason] -> m [FailedReason]
exhaustive isExhaustive f1 f2 = f1 >>= cont
where cont l1
| null l1 = f2
| isExhaustive = f2 >>= \l2 -> return (l1 ++ l2)
| otherwise = return l1
exhaustiveList :: Monad m => Bool -> [(Bool, m [FailedReason])] -> m [FailedReason]
exhaustiveList _ [] = return []
exhaustiveList isExhaustive ((performCheck,c):cs)
| performCheck = exhaustive isExhaustive c (exhaustiveList isExhaustive cs)
| otherwise = exhaustiveList isExhaustive cs