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.Time.Clock
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 UTCTime
, 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 :: UTCTime -> 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 getCurrentTime 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 :: UTCTime
-> 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 :: UTCTime -> 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