diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,39 @@
+# ChangeLog for crypton-x509-validation
+
+## 1.10.1
+
+* Fix name constraint bypasses: constraints are accumulated per RFC
+  5280, apply to the leaf even if self-issued and to the CN used as a
+  host name, dNSName matching is case-insensitive, and iPAddress,
+  rfc822Name, URI and directoryName constraints are enforced.
+* Undecodable NameConstraints, BasicConstraints, KeyUsage,
+  ExtendedKeyUsage and SubjectAltName extensions fail validation
+  instead of being ignored.
+* Reject degenerate DSA parameters and oversized RSA keys before
+  verifying signatures.
+* Limit the chain depth to 20 CA certificates.
+
+## 1.10.0
+
+* Verify ML-DSA signatures
+  [#35](https://github.com/kazu-yamamoto/crypton-certificate/pull/35)
+* Follow the breaking change in `crypton-x509` 1.10.0
+  [#35](https://github.com/kazu-yamamoto/crypton-certificate/pull/35)
+
+## 1.9.1
+
+* Implementing the X509 name constrains extension.
+  [#30](https://github.com/kazu-yamamoto/crypton-certificate/pull/30)
+
+## 1.9.0
+
+* Using "ram" instead of "memory"
+  [#29](https://github.com/kazu-yamamoto/crypton-certificate/pull/29)
+
+## 1.8.0
+
+* Depend on package `time-hourglass`, rather than `hourglass`. Date and
+  time-related types and classes are now those from the former package.
+  [#18](https://github.com/kazu-yamamoto/crypton-certificate/pull/18)
+* Depend on package `crypton-asn1-types >= 0.4.1` rather than `asn1-types`.
+  ASN.1-related types and classes are now those from the former package.
diff --git a/Data/X509/Validation.hs b/Data/X509/Validation.hs
--- a/Data/X509/Validation.hs
+++ b/Data/X509/Validation.hs
@@ -41,11 +41,12 @@
 import Control.Monad (when)
 import Data.ASN1.Types
 import Data.Bits
-import Data.ByteString (unpack)
+import Data.ByteString (ByteString, unpack)
+import qualified Data.ByteString as B
 import Data.Char (toLower)
 import Data.Default
 import Data.Hourglass
-import Data.IP (IPv4, IPv6, toIPv4, toIPv6)
+import Data.IP (IPv4, IPv6, fromIPv4, fromIPv6b, toIPv4, toIPv6)
 import Data.List
 import Data.Maybe
 import Data.Word (Word8)
@@ -53,10 +54,11 @@
 import Data.X509.CertificateStore
 import Data.X509.Validation.Cache
 import Data.X509.Validation.Fingerprint
+import Data.X509.Validation.Internal (safeCharacterToString)
 import Data.X509.Validation.Signature
 import Data.X509.Validation.Types
-import System.Hourglass
 import Text.Read (readMaybe)
+import Time.System
 
 -- | Possible reason of certificate and chain failure.
 --
@@ -65,7 +67,7 @@
 -- instead.
 data FailedReason
     = -- | certificate contains an unknown critical extension
-      UnknownCriticalExtension
+      UnknownCriticalExtension OID
     | -- | validity ends before checking time
       Expired
     | -- | validity starts after checking time
@@ -272,18 +274,33 @@
     -- ^ the return failed reasons (empty list is no failure)
 validatePure _ _ _ _ _ (CertificateChain []) = [EmptyChain]
 validatePure validationTime hooks checks store (fqhn, _) (CertificateChain (top : rchain)) =
-    hookFilterReason hooks (doLeafChecks |> doCheckChain 0 top rchain)
+    hookFilterReason
+        hooks
+        (doLeafChecks |> doCheckChain 0 top rchain |> doCheckNameConst top rchain)
   where
+    isExhaustive :: Bool
     isExhaustive = checkExhaustive checks
+
+    (|>) :: [FailedReason] -> [FailedReason] -> [FailedReason]
     a |> b = exhaustive isExhaustive a b
 
-    doLeafChecks = doNameCheck top ++ doV3Check topCert ++ doKeyUsageCheck topCert
+    doLeafChecks :: [FailedReason]
+    doLeafChecks =
+        doNameCheck top
+            ++ doV3Check topCert
+            ++ doKeyUsageCheck topCert
       where
         topCert = getCertificate top
 
     doCheckChain
         :: Int -> SignedCertificate -> [SignedCertificate] -> [FailedReason]
-    doCheckChain level current chain =
+    doCheckChain level current chain
+        | level > maxChainDepth = [AuthorityTooDeep]
+        | otherwise = doCheckChain' level current chain
+
+    doCheckChain'
+        :: Int -> SignedCertificate -> [SignedCertificate] -> [FailedReason]
+    doCheckChain' level current chain =
         doCheckCertificate (getCertificate current)
             -- check if we have a trusted certificate in the store belonging to this issuer.
             |> ( case findCertificate (certIssuerDN cert) store of
@@ -300,9 +317,14 @@
                                         |> doCheckChain (level + 1) issuer remaining
                )
       where
+        cert :: Certificate
         cert = getCertificate current
     -- in a strict ordering check the next certificate has to be the issuer.
     -- otherwise we dynamically reorder the chain to have the necessary certificate
+    findIssuer
+        :: DistinguishedName
+        -> [SignedCertificate]
+        -> Maybe (SignedCertificate, [SignedCertificate])
     findIssuer issuerDN chain
         | checkStrictOrdering checks =
             case chain of
@@ -311,8 +333,10 @@
                     | matchSubjectIdentifier issuerDN (getCertificate c) -> Just (c, cs)
                     | otherwise -> Nothing
         | otherwise =
-            (\x -> (x, filter (/= x) chain))
-                `fmap` find (matchSubjectIdentifier issuerDN . getCertificate) chain
+            case break (matchSubjectIdentifier issuerDN . getCertificate) chain of
+                (before, x : after) -> Just (x, before ++ after)
+                _ -> Nothing
+    matchSubjectIdentifier :: DistinguishedName -> Certificate -> Bool
     matchSubjectIdentifier = hookMatchSubjectIssuer hooks
 
     -- we check here that the certificate is allowed to be a certificate
@@ -330,11 +354,13 @@
                 ++ (if allowedDepth then [] else [AuthorityTooDeep])
       where
         extensions = certExtensions cert
-        allowedSign = case extensionGet extensions of
-            Just (ExtKeyUsage flags) -> KeyUsage_keyCertSign `elem` flags
+        -- an extension that cannot be decoded is not treated as absent
+        allowedSign = case extensionGetE extensions of
+            Just (Right (ExtKeyUsage flags)) -> KeyUsage_keyCertSign `elem` flags
+            Just (Left _) -> False
             Nothing -> True
-        (allowedCA, pathLen) = case extensionGet extensions of
-            Just (ExtBasicConstraints True pl) -> (True, pl)
+        (allowedCA, pathLen) = case extensionGetE extensions of
+            Just (Right (ExtBasicConstraints True pl)) -> (True, pl)
             _ -> (False, Nothing)
         allowedDepth = case pathLen of
             Nothing -> True
@@ -342,16 +368,19 @@
                 | fromIntegral pl >= level -> True
                 | otherwise -> False
 
+    doNameCheck :: SignedCertificate -> [FailedReason]
     doNameCheck cert
         | not (checkFQHN checks) = []
         | otherwise = (hookValidateName hooks) fqhn (getCertificate cert)
 
+    doV3Check :: Certificate -> [FailedReason]
     doV3Check cert
         | checkLeafV3 checks = case certVersion cert of
             2 {- confusingly it means X509.V3 -} -> []
             _ -> [LeafNotV3]
         | otherwise = []
 
+    doKeyUsageCheck :: Certificate -> [FailedReason]
     doKeyUsageCheck cert =
         compareListIfExistAndNotNull
             mflags
@@ -362,35 +391,107 @@
                 (checkLeafKeyPurpose checks)
                 LeafKeyPurposeNotAllowed
       where
-        mflags = case extensionGet $ certExtensions cert of
-            Just (ExtKeyUsage keyflags) -> Just keyflags
+        mflags = case extensionGetE $ certExtensions cert of
+            Just (Right (ExtKeyUsage keyflags)) -> Just (Right keyflags)
+            Just (Left e) -> Just (Left e)
             Nothing -> Nothing
-        mpurposes = case extensionGet $ certExtensions cert of
-            Just (ExtExtendedKeyUsage keyPurposes) -> Just keyPurposes
+        mpurposes = case extensionGetE $ certExtensions cert of
+            Just (Right (ExtExtendedKeyUsage keyPurposes)) -> Just (Right keyPurposes)
+            Just (Left e) -> Just (Left e)
             Nothing -> Nothing
         -- compare a list of things to an expected list. the expected list
         -- need to be a subset of the list (if not Nothing), and is not will
-        -- return [err]
+        -- return [err]. An undecodable extension always fails.
         compareListIfExistAndNotNull Nothing _ _ = []
-        compareListIfExistAndNotNull (Just list) expected err
+        compareListIfExistAndNotNull (Just (Left _)) _ err = [err]
+        compareListIfExistAndNotNull (Just (Right list)) expected err
             | null expected = []
             | intersect expected list == expected = []
             | otherwise = [err]
 
+    doCheckCertificate :: Certificate -> [FailedReason]
     doCheckCertificate cert =
         exhaustiveList
             (checkExhaustive checks)
             [ (checkTimeValidity checks, hookValidateTime hooks validationTime cert)
+            , (True, doCriticalExtensionSweep cert)
             ]
-    isSelfSigned :: Certificate -> Bool
-    isSelfSigned cert = certSubjectDN cert == certIssuerDN cert
-
     -- check signature of 'signedCert' against the 'signingCert'
+    checkSignature
+        :: SignedCertificate -> SignedCertificate -> [FailedReason]
     checkSignature signedCert signingCert =
         case verifySignedSignature signedCert (certPubKey $ getCertificate signingCert) of
             SignaturePass -> []
             SignatureFailed r -> [InvalidSignature r]
 
+    -- Failures to build the chain are reported by 'doCheckChain'.
+    doCheckNameConst :: SignedCertificate -> [SignedCertificate] -> [FailedReason]
+    doCheckNameConst current0 chain0 = case loop (0 :: Int) current0 chain0 [] of
+        Left errs -> errs
+        Right ts -> checkNameConstraints ts
+      where
+        loop level current chain acc
+            | level > maxChainDepth = Left []
+            | otherwise = case findCertificate issuer store of
+                Just anchor -> Right $ getNameConstSpec (getCertificate anchor) False : spec : acc
+                Nothing
+                    | null chain -> Left []
+                    | otherwise -> case findIssuer issuer chain of
+                        Nothing -> Left []
+                        Just (issuer', remaining) -> loop (level + 1) issuer' remaining (spec : acc)
+          where
+            cert = getCertificate current
+            issuer = certIssuerDN cert
+            isLeaf = level == 0
+            spec = getNameConstSpec cert (isLeaf && checkFQHN checks)
+
+isSelfSigned :: Certificate -> Bool
+isSelfSigned cert = certSubjectDN cert == certIssuerDN cert
+
+-- | The maximum number of CA certificates followed from the leaf.
+maxChainDepth :: Int
+maxChainDepth = 20
+
+data NameConstSpec = NameConstSpec
+    { ncNames :: Either String [AltName]
+    -- ^ names subject to name constraints
+    , ncExt :: Either String (Maybe ExtNameConstraints)
+    , ncSelfIssued :: Bool
+    }
+
+-- | The second argument tells whether the CN is used as a host name.
+getNameConstSpec :: Certificate -> Bool -> NameConstSpec
+getNameConstSpec cert useCN =
+    NameConstSpec
+        { ncNames = names
+        , ncExt = maybe (Right Nothing) (fmap Just) $ extensionGetE exts
+        , ncSelfIssued = isSelfSigned cert
+        }
+  where
+    exts = certExtensions cert
+    subjDN = certSubjectDN cert
+    subj = [AltNameDN subjDN | not (null (getDistinguishedElements subjDN))]
+    -- RFC 5280: without SAN, emailAddress in the subject is an rfc822Name
+    emails =
+        [ AltNameRFC822 e
+        | (oid, v) <- getDistinguishedElements subjDN
+        , oid == [1, 2, 840, 113549, 1, 9, 1]
+        , Just e <- [safeCharacterToString v]
+        ]
+    cn
+        | useCN = case getNames cert of
+            Right (Just name, []) -> [maybe (AltNameDNS name) ipToAltName (parseIPAddress name)]
+            _ -> []
+        | otherwise = []
+    names = case extensionGetE exts of
+        Nothing -> Right (subj ++ emails ++ cn)
+        Just (Right (ExtSubjectAltName alts)) -> Right (subj ++ alts ++ cn)
+        Just (Left e) -> Left e
+
+ipToAltName :: IPAddress -> AltName
+ipToAltName (IPv4Address ip) = AltNameIP $ B.pack $ map fromIntegral $ fromIPv4 ip
+ipToAltName (IPv6Address ip) = AltNameIP $ B.pack $ map fromIntegral $ fromIPv6b ip
+
 -- | Validate that the current time is between validity bounds
 validateTime :: DateTime -> Certificate -> [FailedReason]
 validateTime currentTime cert
@@ -400,11 +501,14 @@
   where
     (before, after) = certValidity cert
 
-getNames :: Certificate -> (Maybe String, [String])
-getNames cert = (commonName >>= asn1CharacterToString, altNames)
+-- | 'Left' if the SAN extension is present but cannot be decoded.
+getNames :: Certificate -> Either String (Maybe String, [String])
+getNames cert = case extensionGetE $ certExtensions cert of
+    Just (Left e) -> Left e
+    Just (Right san) -> Right (commonName, toAltName san)
+    Nothing -> Right (commonName, [])
   where
-    commonName = getDnElement DnCommonName $ certSubjectDN cert
-    altNames = maybe [] toAltName $ extensionGet $ certExtensions cert
+    commonName = getDnElement DnCommonName (certSubjectDN cert) >>= safeCharacterToString
     toAltName (ExtSubjectAltName names) = catMaybes $ map unAltName names
       where
         unAltName (AltNameDNS s) = Just s
@@ -452,11 +556,19 @@
 -- subjectAltName are considered.  Otherwise, the commonName from the subjectDN
 -- is used.
 --
+-- If the subjectAltName extension cannot be decoded, the name never matches.
+--
 -- Note that DNS names in the subjectAltName are in IDNA A-label form. If the
 -- destination hostname is a UTF-8 name, it must be provided to the TLS context
 -- in (non-transitional) IDNA2008 A-label form.
 validateCertificateName :: HostName -> Certificate -> [FailedReason]
-validateCertificateName fqhn cert
+validateCertificateName fqhn cert = case getNames cert of
+    Left _ -> [NameMismatch fqhn]
+    Right names -> validateNames fqhn cert names
+
+validateNames
+    :: HostName -> Certificate -> (Maybe String, [String]) -> [FailedReason]
+validateNames fqhn cert (commonName, altNames)
     | not $ null altNames =
         case parseIPAddress fqhn of
             Nothing -> findMatch [] $ map matchDomain altNames
@@ -469,8 +581,6 @@
             Nothing -> [NoCommonName]
             Just cn -> findMatch [] $ [matchDomain cn]
   where
-    (commonName, altNames) = getNames cert
-
     findMatch :: [FailedReason] -> [[FailedReason]] -> [FailedReason]
     findMatch _ [] = [NameMismatch fqhn]
     findMatch _ ([] : _) = []
@@ -523,3 +633,174 @@
 exhaustiveList isExhaustive ((performCheck, c) : cs)
     | performCheck = exhaustive isExhaustive c (exhaustiveList isExhaustive cs)
     | otherwise = exhaustiveList isExhaustive cs
+
+-- | Name constraints accumulated along the chain (RFC 5280 6.1):
+-- a name must be in a permitted subtree of every level that has one
+-- and must not be in any excluded subtree.
+data NameConstState = NameConstState [[GeneralSubtree]] [GeneralSubtree]
+
+-- | The first element is the trust anchor and the last is the leaf.
+checkNameConstraints :: [NameConstSpec] -> [FailedReason]
+checkNameConstraints [] = []
+checkNameConstraints (anchor : specs0) =
+    either id (\st -> loop st specs0) $ addNameConst (NameConstState [] []) anchor
+  where
+    loop _ [] = []
+    loop st (spec : specs) =
+        check ++ case specs of
+            [] -> []
+            _ -> either id (\st' -> loop st' specs) $ addNameConst st spec
+      where
+        -- self-issued intermediates are exempt but the leaf is not
+        check
+            | ncSelfIssued spec && not (null specs) = []
+            | otherwise = validateNames' st (ncNames spec)
+
+addNameConst
+    :: NameConstState -> NameConstSpec -> Either [FailedReason] NameConstState
+addNameConst st@(NameConstState permitted excluded) spec = case ncExt spec of
+    Left e -> Left [InvalidName $ "NameConstraints: " ++ e]
+    Right Nothing -> Right st
+    Right (Just (ExtNameConstraints p e)) ->
+        Right $ NameConstState ([p | not (null p)] ++ permitted) (e ++ excluded)
+
+validateNames' :: NameConstState -> Either String [AltName] -> [FailedReason]
+validateNames' (NameConstState [] []) _ = []
+validateNames' _ (Left e) = [InvalidName $ "SubjectAltName: " ++ e]
+validateNames' (NameConstState permitted excluded) (Right altNames) =
+    concatMap inc altNames ++ concatMap exc altNames
+  where
+    inc a
+        | all (nsMatch a) permitted = []
+        | otherwise = [InvalidName $ show a]
+    exc a
+        | nsNotMatch a excluded = []
+        | otherwise = [InvalidName $ show a]
+
+data NameMatch = NotApplicable | Matched | NotMatched | Unsupported
+    deriving (Eq)
+
+-- | A permitted subtree list is satisfied if the name is in one of them
+-- or if none of them is of the same type as the name.
+nsMatch :: AltName -> [GeneralSubtree] -> Bool
+nsMatch a gs = any (== Matched) rs || all (== NotApplicable) rs
+  where
+    rs = map (matchSubtree False a) gs
+
+nsNotMatch :: AltName -> [GeneralSubtree] -> Bool
+nsNotMatch a = all ((`elem` [NotApplicable, NotMatched]) . matchSubtree True a)
+
+-- | The first argument tells whether the subtree is an excluded one.
+-- Unsupported forms count as matched when excluded and as not matched
+-- when permitted.
+matchSubtree :: Bool -> AltName -> GeneralSubtree -> NameMatch
+matchSubtree isExcluded name (GeneralSubtree base _ _) = case (name, base) of
+    (AltNameDNS n, AltNameDNS c) ->
+        toMatch $ dnsInSubtree n c || (isExcluded && wildcardCovers n c)
+    (AltNameDN n, AltNameDN c) -> toMatch $ dnInSubtree n c
+    (AltNameIP n, AltNameIP c) -> ipInSubtree n c
+    (AltNameRFC822 n, AltNameRFC822 c) -> maybe Unsupported toMatch $ emailInSubtree n c
+    (AltNameURI n, AltNameURI c) -> maybe Unsupported (toMatch . (`hostInSubtree` c)) $ uriHost n
+    (AltNameXMPP _, AltNameXMPP _) -> Unsupported
+    (AltNameDNSSRV _, AltNameDNSSRV _) -> Unsupported
+    _ -> NotApplicable
+  where
+    toMatch b = if b then Matched else NotMatched
+
+-- | "example.com" covers itself and its subdomains, ".example.com" only
+-- its subdomains.  Compared case-insensitively.
+dnsInSubtree :: String -> String -> Bool
+dnsInSubtree n c
+    | null c' = True
+    | "." `isPrefixOf` c' = c' `isSuffixOf` n'
+    | otherwise = n' == c' || ('.' : c') `isSuffixOf` n'
+  where
+    n' = map toLower n
+    c' = map toLower c
+
+-- | Whether a wildcard name such as "*.example.com" may stand for a name
+-- in the subtree, e.g. "www.example.com".
+wildcardCovers :: String -> String -> Bool
+wildcardCovers n c = case stripPrefix "*." (map toLower n) of
+    Just rest ->
+        let c' = map toLower c
+            label = take (length c' - length rest - 1) c'
+         in not (null label)
+                && '.' `notElem` label
+                && ('.' : rest) `isSuffixOf` c'
+    Nothing -> False
+
+hostInSubtree :: String -> String -> Bool
+hostInSubtree h c
+    | "." `isPrefixOf` c' = c' `isSuffixOf` h'
+    | otherwise = h' == c'
+  where
+    h' = map toLower h
+    c' = map toLower c
+
+-- | "user@host" matches exactly, "host" matches all mailboxes on the host
+-- and ".example.com" all mailboxes on its subdomains.
+emailInSubtree :: String -> String -> Maybe Bool
+emailInSubtree n c = case breakAt n of
+    Just (local, host) -> Just $ case breakAt c of
+        Just (cLocal, cHost) -> local == cLocal && map toLower host == map toLower cHost
+        Nothing -> hostInSubtree host c
+    Nothing -> Nothing
+  where
+    breakAt x = case break (== '@') (reverse x) of
+        (rhost, '@' : rlocal) -> Just (reverse rlocal, reverse rhost)
+        _ -> Nothing
+
+-- | The host of a URI with an authority; 'Nothing' for IP literals.
+uriHost :: String -> Maybe String
+uriHost uri = case break (== ':') uri of
+    (_, ':' : '/' : '/' : rest) ->
+        let authority = takeWhile (`notElem` ("/?#" :: String)) rest
+            hostport = reverse $ takeWhile (/= '@') $ reverse authority
+            host = takeWhile (/= ':') hostport
+         in if null host || "[" `isPrefixOf` hostport || isJust (parseIPAddress host)
+                then Nothing
+                else Just host
+    _ -> Nothing
+
+-- | The subtree is a prefix of the name.  Attribute values are compared
+-- case-insensitively with whitespace folded.
+dnInSubtree :: DistinguishedName -> DistinguishedName -> Bool
+dnInSubtree (DistinguishedName n) (DistinguishedName c) =
+    length c <= length n && and (zipWith eq n c)
+  where
+    eq (o1, v1) (o2, v2) = o1 == o2 && norm v1 == norm v2
+    norm v = case safeCharacterToString v of
+        Just str -> Right $ unwords $ words $ map toLower str
+        Nothing -> Left v
+
+-- | An IPv4 or IPv6 address against an address with a mask.
+ipInSubtree :: ByteString -> ByteString -> NameMatch
+ipInSubtree n c
+    | B.length c /= 8 && B.length c /= 32 = Unsupported
+    | B.length n /= 4 && B.length n /= 16 = Unsupported
+    | 2 * B.length n /= B.length c = NotMatched
+    | otherwise =
+        if and
+            ( zipWith3
+                (\x a m -> x .&. m == a .&. m)
+                (B.unpack n)
+                (B.unpack addr)
+                (B.unpack mask)
+            )
+            then Matched
+            else NotMatched
+  where
+    (addr, mask) = B.splitAt (B.length n) c
+
+doCriticalExtensionSweep :: Certificate -> [FailedReason]
+doCriticalExtensionSweep cert = case mexts of
+    Nothing -> []
+    Just exts ->
+        [ UnknownCriticalExtension oid
+        | ExtensionRaw oid critical _ <- exts
+        , critical
+        , oid `notElem` recognizedOIDs
+        ]
+  where
+    Extensions mexts = certExtensions cert
diff --git a/Data/X509/Validation/Internal.hs b/Data/X509/Validation/Internal.hs
new file mode 100644
--- /dev/null
+++ b/Data/X509/Validation/Internal.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module      : Data.X509.Validation.Internal
+-- License     : BSD-style
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Guards against exceptions thrown from pure code on malformed input.
+module Data.X509.Validation.Internal (
+    tryPure,
+    safeCharacterToString,
+) where
+
+import Control.Exception (
+    SomeAsyncException,
+    SomeException,
+    evaluate,
+    fromException,
+    throwIO,
+    try,
+ )
+import Data.ASN1.Types (ASN1CharacterString, asn1CharacterToString)
+import qualified Data.List as L
+import System.IO.Unsafe (unsafeDupablePerformIO)
+
+-- | Run a forcing function and turn synchronous exceptions into 'Left'.
+tryPure :: (a -> ()) -> a -> Either String a
+tryPure force x = unsafeDupablePerformIO $ do
+    res <- try $ evaluate $ force x
+    case res of
+        Right () -> return $ Right x
+        Left (e :: SomeException)
+            | Just (_ :: SomeAsyncException) <- fromException e -> throwIO e
+            | otherwise -> return $ Left (show e)
+{-# NOINLINE tryPure #-}
+
+-- | Like 'asn1CharacterToString' but 'Nothing' on invalid encoding.
+safeCharacterToString :: ASN1CharacterString -> Maybe String
+safeCharacterToString cs =
+    asn1CharacterToString cs >>= either (const Nothing) Just . tryPure force
+  where
+    force = L.foldl' (flip seq) ()
diff --git a/Data/X509/Validation/Signature.hs b/Data/X509/Validation/Signature.hs
--- a/Data/X509/Validation/Signature.hs
+++ b/Data/X509/Validation/Signature.hs
@@ -15,11 +15,14 @@
 
 import Crypto.Error (CryptoFailable (..))
 import Crypto.Hash
+import Crypto.Number.Basic (numBits)
 import qualified Crypto.PubKey.DSA as DSA
 import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
 import qualified Crypto.PubKey.ECC.Types as ECC
 import qualified Crypto.PubKey.Ed25519 as Ed25519
 import qualified Crypto.PubKey.Ed448 as Ed448
+import qualified Crypto.PubKey.MLDSA as MLDSA
+import qualified Crypto.PubKey.RSA as RSA
 import qualified Crypto.PubKey.RSA.PKCS15 as RSA
 import qualified Crypto.PubKey.RSA.PSS as PSS
 
@@ -29,6 +32,7 @@
 import Data.ByteString (ByteString)
 import Data.X509
 import Data.X509.EC
+import Data.X509.Validation.Internal (tryPure)
 
 -- | A set of possible return from signature verification.
 --
@@ -80,8 +84,33 @@
     -> ByteString
     -- ^ Signature to verify
     -> SignatureVerification
-verifySignature (SignatureALG_Unknown _) _ _ _ = SignatureFailed SignatureUnimplemented
-verifySignature (SignatureALG hashALG PubKeyALG_RSAPSS) pubkey cdata signature = case verifyF pubkey of
+verifySignature alg pubkey cdata signature
+    | not (acceptablePubKey pubkey) = SignatureFailed SignatureInvalid
+    | otherwise =
+        -- degenerate keys or encodings must not throw from pure code
+        either (const $ SignatureFailed SignatureInvalid) id $
+            tryPure (`seq` ()) (verifySignature' alg pubkey cdata signature)
+
+-- | Reject key parameters that are invalid or too costly to verify with.
+acceptablePubKey :: PubKey -> Bool
+acceptablePubKey (PubKeyRSA (RSA.PublicKey _ n e)) =
+    n > 1 && numBits n <= 16384 && e > 1 && numBits e <= 64
+acceptablePubKey (PubKeyDSA (DSA.PublicKey (DSA.Params p g q) y)) =
+    p > 2
+        && numBits p <= 8192
+        && q > 1
+        && q < p
+        && numBits q <= 512
+        && g > 1
+        && g < p
+        && y > 1
+        && y < p
+acceptablePubKey _ = True
+
+verifySignature'
+    :: SignatureALG -> PubKey -> ByteString -> ByteString -> SignatureVerification
+verifySignature' (SignatureALG_Unknown _) _ _ _ = SignatureFailed SignatureUnimplemented
+verifySignature' (SignatureALG hashALG PubKeyALG_RSAPSS) pubkey cdata signature = case verifyF pubkey of
     Nothing -> SignatureFailed SignatureUnimplemented
     Just f ->
         if f cdata signature
@@ -95,7 +124,7 @@
         | hashALG == HashSHA224 = Just $ PSS.verify (PSS.defaultPSSParams SHA224) key
         | otherwise = Nothing
     verifyF _ = Nothing
-verifySignature (SignatureALG hashALG pubkeyALG) pubkey cdata signature
+verifySignature' (SignatureALG hashALG pubkeyALG) pubkey cdata signature
     | pubkeyToAlg pubkey == pubkeyALG = case verifyF pubkey of
         Nothing -> SignatureFailed SignatureUnimplemented
         Just f ->
@@ -136,18 +165,30 @@
     rsaVerify HashSHA256 = RSA.verify (Just SHA256)
     rsaVerify HashSHA384 = RSA.verify (Just SHA384)
     rsaVerify HashSHA512 = RSA.verify (Just SHA512)
-verifySignature (SignatureALG_IntrinsicHash pubkeyALG) pubkey cdata signature
+verifySignature' (SignatureALG_IntrinsicHash pubkeyALG) pubkey cdata signature
     | pubkeyToAlg pubkey == pubkeyALG = doVerify pubkey
     | otherwise = SignatureFailed SignaturePubkeyMismatch
   where
     doVerify (PubKeyEd25519 key) = eddsa Ed25519.verify Ed25519.signature key
     doVerify (PubKeyEd448 key) = eddsa Ed448.verify Ed448.signature key
+    doVerify (PubKeyMLDSA44 key) = mldsa key
+    doVerify (PubKeyMLDSA65 key) = mldsa key
+    doVerify (PubKeyMLDSA87 key) = mldsa key
     doVerify _ = SignatureFailed SignatureUnimplemented
 
     eddsa verify toSig key =
         case toSig signature of
             CryptoPassed sig
                 | verify key cdata sig -> SignaturePass
+                | otherwise -> SignatureFailed SignatureInvalid
+            CryptoFailed _ -> SignatureFailed SignatureInvalid
+
+    -- RFC 9881 Section 3: the empty context string.
+    mldsa :: MLDSA.MLDSA p => MLDSA.VerificationKey p -> SignatureVerification
+    mldsa key =
+        case MLDSA.signature signature of
+            CryptoPassed sig
+                | MLDSA.verify key MLDSA.emptyContext cdata sig -> SignaturePass
                 | otherwise -> SignatureFailed SignatureInvalid
             CryptoFailed _ -> SignatureFailed SignatureInvalid
 
diff --git a/Tests/Certificate.hs b/Tests/Certificate.hs
--- a/Tests/Certificate.hs
+++ b/Tests/Certificate.hs
@@ -29,8 +29,6 @@
     mkLeaf,
 ) where
 
-import Control.Applicative
-
 import Crypto.Hash.Algorithms
 import Crypto.Number.Serialize
 
@@ -40,6 +38,7 @@
 import qualified Crypto.PubKey.ECC.Types as ECC
 import qualified Crypto.PubKey.Ed25519 as Ed25519
 import qualified Crypto.PubKey.Ed448 as Ed448
+import qualified Crypto.PubKey.MLDSA as MLDSA
 import qualified Crypto.PubKey.RSA as RSA
 import qualified Crypto.PubKey.RSA.PKCS15 as RSA
 import qualified Crypto.PubKey.RSA.PSS as PSS
@@ -51,6 +50,7 @@
 import Data.ASN1.Types
 import Data.ByteArray (convert)
 import Data.Maybe (catMaybes)
+import Data.Proxy (Proxy (..))
 import Data.String (fromString)
 import Data.X509
 
@@ -103,6 +103,12 @@
         -> Alg ECDSA.PublicKey ECDSA.PrivateKey
     AlgEd25519 :: Alg Ed25519.PublicKey Ed25519.SecretKey
     AlgEd448 :: Alg Ed448.PublicKey Ed448.SecretKey
+    AlgMLDSA44
+        :: Alg (MLDSA.VerificationKey MLDSA.MLDSA44) (MLDSA.SigningKey MLDSA.MLDSA44)
+    AlgMLDSA65
+        :: Alg (MLDSA.VerificationKey MLDSA.MLDSA65) (MLDSA.SigningKey MLDSA.MLDSA65)
+    AlgMLDSA87
+        :: Alg (MLDSA.VerificationKey MLDSA.MLDSA87) (MLDSA.SigningKey MLDSA.MLDSA87)
 
 -- | Types of public and private keys used by a signature algorithm.
 type Keys pub priv = (Alg pub priv, pub, priv)
@@ -125,7 +131,16 @@
 generateKeys alg@AlgEd448 = do
     secret <- Ed448.generateSecretKey
     return (alg, Ed448.toPublic secret, secret)
+generateKeys alg@AlgMLDSA44 =
+    withAlg alg <$> MLDSA.generateKeyPair (Proxy :: Proxy MLDSA.MLDSA44)
+generateKeys alg@AlgMLDSA65 =
+    withAlg alg <$> MLDSA.generateKeyPair (Proxy :: Proxy MLDSA.MLDSA65)
+generateKeys alg@AlgMLDSA87 =
+    withAlg alg <$> MLDSA.generateKeyPair (Proxy :: Proxy MLDSA.MLDSA87)
 
+withAlg :: Alg pub priv -> (pub, priv) -> Keys pub priv
+withAlg alg (pub, priv) = (alg, pub, priv)
+
 generateRSAKeys
     :: Alg RSA.PublicKey RSA.PrivateKey
     -> Int
@@ -149,6 +164,9 @@
     bytes = (bits + 7) `div` 8
 getPubKey AlgEd25519 key = PubKeyEd25519 key
 getPubKey AlgEd448 key = PubKeyEd448 key
+getPubKey AlgMLDSA44 key = PubKeyMLDSA44 key
+getPubKey AlgMLDSA65 key = PubKeyMLDSA65 key
+getPubKey AlgMLDSA87 key = PubKeyMLDSA87 key
 
 getSignatureALG :: Alg pub priv -> SignatureALG
 getSignatureALG (AlgRSA _ hash) = SignatureALG (getHashALG hash) PubKeyALG_RSA
@@ -157,6 +175,9 @@
 getSignatureALG (AlgEC _ hash) = SignatureALG (getHashALG hash) PubKeyALG_EC
 getSignatureALG AlgEd25519 = SignatureALG_IntrinsicHash PubKeyALG_Ed25519
 getSignatureALG AlgEd448 = SignatureALG_IntrinsicHash PubKeyALG_Ed448
+getSignatureALG AlgMLDSA44 = SignatureALG_IntrinsicHash PubKeyALG_MLDSA44
+getSignatureALG AlgMLDSA65 = SignatureALG_IntrinsicHash PubKeyALG_MLDSA65
+getSignatureALG AlgMLDSA87 = SignatureALG_IntrinsicHash PubKeyALG_MLDSA87
 
 doSign :: Alg pub priv -> priv -> B.ByteString -> IO B.ByteString
 doSign (AlgRSA _ hash) key msg = do
@@ -193,6 +214,9 @@
     return $ convert $ Ed25519.sign key (Ed25519.toPublic key) msg
 doSign AlgEd448 key msg =
     return $ convert $ Ed448.sign key (Ed448.toPublic key) msg
+doSign AlgMLDSA44 key msg = convert <$> MLDSA.sign key MLDSA.emptyContext msg
+doSign AlgMLDSA65 key msg = convert <$> MLDSA.sign key MLDSA.emptyContext msg
+doSign AlgMLDSA87 key msg = convert <$> MLDSA.sign key MLDSA.emptyContext msg
 
 -- Certificate utilities --
 
diff --git a/Tests/Tests.hs b/Tests/Tests.hs
--- a/Tests/Tests.hs
+++ b/Tests/Tests.hs
@@ -1,15 +1,20 @@
 -- | Validation test suite.
 module Main (main) where
 
-import Control.Applicative
 import Control.Monad (unless)
 
 import Crypto.Hash.Algorithms
 
 import qualified Crypto.PubKey.DSA as DSA
 import qualified Crypto.PubKey.ECC.Types as ECC
+import qualified Crypto.PubKey.RSA as RSA
 import qualified Crypto.PubKey.RSA.PSS as PSS
 
+import Data.ASN1.BinaryEncoding (DER (..))
+import Data.ASN1.BitArray (toBitArray)
+import Data.ASN1.Encoding (encodeASN1')
+import Data.ASN1.Types
+
 import Data.Default
 import Data.Monoid
 import Data.String (fromString)
@@ -20,7 +25,7 @@
 import qualified Data.ByteString as BS
 
 import Data.Hourglass
-import System.Hourglass
+import Time.System
 
 import Test.Tasty
 import Test.Tasty.HUnit
@@ -621,6 +626,487 @@
     caseName = if null hostname then "empty" else hostname
     checks = defaultChecks{checkExhaustive = True}
 
+-- | Validates a chain against a store and checks only success or failure.
+assertPassOrFail
+    :: CertificateStore
+    -> ValidationChecks
+    -> HostName
+    -> [Pair pub priv]
+    -> Bool
+    -> Assertion
+assertPassOrFail store checks hostname ps pass = do
+    actual <- validate HashSHA256 defaultHooks checks store def ident chain
+    unless (null actual == pass) $
+        assertFailure $
+            (if pass then "expected success but got: " else "expected failure but got: ")
+                ++ show actual
+  where
+    ident = (hostname, fromString ":443")
+    chain = CertificateChain (map pairSignedCert ps)
+
+nameConstraints :: [AltName] -> [AltName] -> ExtensionRaw
+nameConstraints p e = mkExtension True $ ExtNameConstraints (map sub p) (map sub e)
+  where
+    sub a = GeneralSubtree a 0 Nothing
+
+caBC :: ExtensionRaw
+caBC = mkExtension True $ ExtBasicConstraints True Nothing
+
+sanExt :: [AltName] -> ExtensionRaw
+sanExt = mkExtension False . ExtSubjectAltName
+
+-- | Tests name constraints of an intermediate CA and an optional sub-CA
+-- against a leaf certificate.
+testNameConstraints
+    :: IO (RData pub priv)
+    -> TestName
+    -> [ExtensionRaw]
+    -- ^ Extensions of the CA in addition to basic constraints
+    -> Maybe [ExtensionRaw]
+    -- ^ Extensions of the sub-CA, if any
+    -> DistinguishedName
+    -- ^ Leaf subject
+    -> [ExtensionRaw]
+    -- ^ Leaf extensions
+    -> HostName
+    -> Bool
+    -- ^ Expected success
+    -> TestTree
+testNameConstraints res caseName caExts msubExts leafDN leafExts hostname pass =
+    testWithRes res caseName $ \rd -> do
+        ca <-
+            mkCertificate
+                2
+                30
+                (mkDn "NC CA")
+                (present rd)
+                (caBC : caExts)
+                (CA $ root rd)
+                (keys1 rd)
+        (issuer, chain) <- case msubExts of
+            Nothing -> return (ca, [ca])
+            Just subExts -> do
+                sub <-
+                    mkCertificate
+                        2
+                        31
+                        (mkDn "NC Sub CA")
+                        (present rd)
+                        (caBC : subExts)
+                        (CA ca)
+                        (keys2 rd)
+                return (sub, [sub, ca])
+        leaf <- mkCertificate 2 100 leafDN (present rd) leafExts (CA issuer) (keys3 rd)
+        assertPassOrFail (rootStore rd) defaultChecks hostname (leaf : chain) pass
+
+-- | A leaf whose subject equals the issuer is not exempt from the name
+-- constraints of a trust anchor.
+testSelfIssuedLeaf :: IO (RData pub priv) -> TestName -> TestTree
+testSelfIssuedLeaf res caseName = testWithRes res caseName $ \rd -> do
+    let exts = [caBC, nameConstraints [AltNameDNS "example.com"] []]
+    anchor <- mkCertificate 2 40 (mkDn "NC Root") (present rd) exts Self (keys1 rd)
+    leaf <-
+        mkCertificate
+            2
+            41
+            (mkDn "NC Root")
+            (present rd)
+            [sanExt [AltNameDNS "www.google.com"]]
+            (CA anchor)
+            (keys2 rd)
+    assertPassOrFail (mkStore [anchor]) defaultChecks "www.google.com" [leaf] False
+
+-- | Tests a chain longer than the supported depth.
+testLongChain :: IO (RData pub priv) -> TestName -> Int -> Bool -> TestTree
+testLongChain res caseName len pass = testWithRes res caseName $ \rd -> do
+    let mk issuer i =
+            mkCertificate
+                2
+                (fromIntegral i)
+                (mkDn ("CA" ++ show i))
+                (present rd)
+                [caBC]
+                (CA issuer)
+                (keys1 rd)
+        go issuer i acc
+            | i > len = return (issuer, acc)
+            | otherwise = mk issuer i >>= \c -> go c (i + 1) (c : acc)
+    (lastCA, cas) <- go (root rd) (1 :: Int) []
+    leaf <- mkLeaf "long-chain" (present rd) (CA lastCA) (keys2 rd)
+    assertPassOrFail (rootStore rd) defaultChecks "long-chain" (leaf : cas) pass
+
+-- | Tests a CA or leaf with an extension that cannot be decoded.
+testMalformedExt
+    :: IO (RData pub priv)
+    -> TestName
+    -> Bool
+    -> ExtensionRaw
+    -> ValidationChecks
+    -> [FailedReason]
+    -> TestTree
+testMalformedExt res caseName onCA ext checks expected = testWithRes res caseName $ \rd -> do
+    ca <-
+        mkCertificate
+            2
+            50
+            (mkDn "Malformed CA")
+            (present rd)
+            ([ext | onCA] ++ [caBC | not (isBC ext)])
+            (CA $ root rd)
+            (keys1 rd)
+    leaf <-
+        mkCertificate
+            2
+            100
+            (mkDn "malformed")
+            (present rd)
+            ([ext | not onCA])
+            (CA ca)
+            (keys2 rd)
+    assertValidationResult rd checks "malformed" [leaf, ca] expected
+  where
+    isBC (ExtensionRaw oid _ _) = oid == [2, 5, 29, 19]
+
+-- | Signature verification with degenerate or oversized keys fails.
+testBadKeys :: TestTree
+testBadKeys =
+    testGroup
+        "bad-keys"
+        [ testCase "DSA p=0" $
+            verifySignature
+                (SignatureALG HashSHA256 PubKeyALG_DSA)
+                (PubKeyDSA (DSA.PublicKey (DSA.Params 0 2 7) 3))
+                (BS.pack [1, 2, 3])
+                (encodeASN1' DER [Start Sequence, IntVal 1, IntVal 1, End Sequence])
+                @?= SignatureFailed SignatureInvalid
+        , testCase "huge RSA modulus" $
+            verifySignature
+                (SignatureALG HashSHA256 PubKeyALG_RSA)
+                (PubKeyRSA (RSA.PublicKey 2049 (2 ^ (16392 :: Int) - 1) 3))
+                (BS.pack [1, 2, 3])
+                (BS.replicate 2049 1)
+                @?= SignatureFailed SignatureInvalid
+        , testCase "huge RSA exponent" $
+            verifySignature
+                (SignatureALG HashSHA256 PubKeyALG_RSA)
+                (PubKeyRSA (RSA.PublicKey 256 (2 ^ (2048 :: Int) - 1) (2 ^ (2047 :: Int) + 1)))
+                (BS.pack [1, 2, 3])
+                (BS.replicate 256 1)
+                @?= SignatureFailed SignatureInvalid
+        , testCase "tiny RSA-PSS modulus" $
+            verifySignature
+                (SignatureALG HashSHA256 PubKeyALG_RSAPSS)
+                (PubKeyRSA (RSA.PublicKey 1 0x7f 3))
+                (BS.pack [1, 2, 3])
+                (BS.pack [1])
+                @?= SignatureFailed SignatureInvalid
+        ]
+
+-- | Regression tests that do not depend on the signature algorithm.
+treeRegression :: TestTree
+treeRegression = withResource (initData AlgEd25519) freeData $ \res ->
+    testGroup
+        "regression"
+        [ testGroup
+            "name-constraints"
+            [ testNameConstraints
+                res
+                "permitted"
+                [ncEx]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.com"]]
+                "www.example.com"
+                True
+            , testNameConstraints
+                res
+                "not permitted"
+                [ncEx]
+                Nothing
+                leafDN
+                [sanExt [dns "www.google.com"]]
+                "www.google.com"
+                False
+            , testNameConstraints
+                res
+                "sub-CA with excluded only"
+                [ncEx]
+                (Just [nameConstraints [] [dns "foo.invalid"]])
+                leafDN
+                [sanExt [dns "www.google.com"]]
+                "www.google.com"
+                False
+            , testNameConstraints
+                res
+                "sub-CA without constraints"
+                [ncEx]
+                (Just [])
+                leafDN
+                [sanExt [dns "www.example.com"]]
+                "www.example.com"
+                True
+            , testNameConstraints
+                res
+                "sub-CA without constraints (outside)"
+                [ncEx]
+                (Just [])
+                leafDN
+                [sanExt [dns "www.google.com"]]
+                "www.google.com"
+                False
+            , testNameConstraints
+                res
+                "sub-CA permitted outside"
+                [ncEx]
+                (Just [nameConstraints [dns "google.com"] []])
+                leafDN
+                [sanExt [dns "www.google.com"]]
+                "www.google.com"
+                False
+            , testNameConstraints
+                res
+                "CN fallback"
+                [ncEx]
+                Nothing
+                (mkDn "www.google.com")
+                []
+                "www.google.com"
+                False
+            , testNameConstraints
+                res
+                "CN fallback (permitted)"
+                [ncEx]
+                Nothing
+                (mkDn "www.example.com")
+                []
+                "www.example.com"
+                True
+            , testNameConstraints
+                res
+                "excluded upper case"
+                [nameConstraints [] [dns "google.com"]]
+                Nothing
+                leafDN
+                [sanExt [dns "WWW.GOOGLE.COM"]]
+                "www.google.com"
+                False
+            , testNameConstraints
+                res
+                "excluded by wildcard"
+                [nameConstraints [] [dns "www.example.com"]]
+                Nothing
+                leafDN
+                [sanExt [dns "*.example.com"]]
+                "www.example.com"
+                False
+            , testNameConstraints
+                res
+                "permitted upper case"
+                [nameConstraints [dns "EXAMPLE.com"] []]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.COM"]]
+                "www.example.com"
+                True
+            , testNameConstraints
+                res
+                "leading dot"
+                [nameConstraints [dns ".example.com"] []]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.com"]]
+                "www.example.com"
+                True
+            , testNameConstraints
+                res
+                "leading dot (apex)"
+                [nameConstraints [dns ".example.com"] []]
+                Nothing
+                leafDN
+                [sanExt [dns "example.com"]]
+                "example.com"
+                False
+            , testNameConstraints
+                res
+                "excluded IP"
+                [nameConstraints [] [ip [8, 8, 8, 8, 255, 255, 255, 255]]]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.com", ip [8, 8, 8, 8]]]
+                "www.example.com"
+                False
+            , testNameConstraints
+                res
+                "permitted IP"
+                [nameConstraints [ip [10, 0, 0, 0, 255, 0, 0, 0]] []]
+                Nothing
+                leafDN
+                [sanExt [dns "x.example", ip [10, 1, 2, 3]]]
+                "10.1.2.3"
+                True
+            , testNameConstraints
+                res
+                "not permitted IP"
+                [nameConstraints [ip [10, 0, 0, 0, 255, 0, 0, 0]] []]
+                Nothing
+                leafDN
+                [sanExt [dns "x.example", ip [11, 1, 2, 3]]]
+                "11.1.2.3"
+                False
+            , testNameConstraints
+                res
+                "IP not constrained by DNS"
+                [ncEx]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.com", ip [8, 8, 8, 8]]]
+                "8.8.8.8"
+                True
+            , testNameConstraints
+                res
+                "excluded email"
+                [nameConstraints [] [AltNameRFC822 "example.org"]]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.com", AltNameRFC822 "a@EXAMPLE.org"]]
+                "www.example.com"
+                False
+            , testNameConstraints
+                res
+                "excluded URI"
+                [nameConstraints [] [AltNameURI ".example.org"]]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.com", AltNameURI "https://www.example.org/"]]
+                "www.example.com"
+                False
+            , testNameConstraints
+                res
+                "permitted DN"
+                [nameConstraints [AltNameDN (mkDn "leaf")] []]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.com"]]
+                "www.example.com"
+                True
+            , testNameConstraints
+                res
+                "excluded DN"
+                [nameConstraints [] [AltNameDN (mkDn "LEAF")]]
+                Nothing
+                leafDN
+                [sanExt [dns "www.example.com"]]
+                "www.example.com"
+                False
+            , testNameConstraints
+                res
+                "unparsable"
+                [ncRegisteredID]
+                Nothing
+                leafDN
+                [sanExt [dns "www.google.com"]]
+                "www.google.com"
+                False
+            , testSelfIssuedLeaf res "self-issued leaf"
+            ]
+        , testGroup
+            "subject-alt-name"
+            [ testNameConstraints
+                res
+                "unknown names skipped"
+                []
+                Nothing
+                leafDN
+                [sanUnknown]
+                "www.example.com"
+                True
+            , testNameConstraints
+                res
+                "undecodable SAN"
+                []
+                Nothing
+                (mkDn "www.example.com")
+                [sanBroken]
+                "www.example.com"
+                False
+            ]
+        , testGroup
+            "malformed-extensions"
+            [ testMalformedExt
+                res
+                "CA key usage"
+                True
+                (raw [2, 5, 29, 15] [IntVal 1])
+                defaultChecks
+                [NotAllowedToSign]
+            , testMalformedExt
+                res
+                "CA basic constraints"
+                True
+                (raw [2, 5, 29, 19] [IntVal 1])
+                defaultChecks
+                [NotAnAuthority]
+            , testMalformedExt
+                res
+                "leaf key usage bit 9"
+                False
+                (raw [2, 5, 29, 15] [BitString (toBitArray (BS.pack [0x80, 0x40]) 6)])
+                defaultChecks
+                    { checkLeafKeyUsage = [KeyUsage_digitalSignature]
+                    , checkFQHN = False
+                    }
+                []
+            , testMalformedExt
+                res
+                "leaf extended key usage"
+                False
+                (raw [2, 5, 29, 37] [Start Sequence, IntVal 1, End Sequence])
+                defaultChecks
+                    { checkLeafKeyPurpose = [KeyUsagePurpose_ServerAuth]
+                    , checkFQHN = False
+                    }
+                [LeafKeyPurposeNotAllowed]
+            ]
+        , testGroup
+            "chain-length"
+            [ testLongChain res "short" 5 True
+            , testLongChain res "too long" 25 False
+            ]
+        , testBadKeys
+        ]
+  where
+    dns = AltNameDNS
+    ip = AltNameIP . BS.pack
+    leafDN = mkDn "leaf"
+    ncEx = nameConstraints [dns "example.com"] []
+    raw oid asn1 = ExtensionRaw oid True (encodeASN1' DER asn1)
+    ncRegisteredID =
+        raw
+            [2, 5, 29, 30]
+            [ Start Sequence
+            , Start (Container Context 0)
+            , Start Sequence
+            , Other Context 2 (fromString "example.com")
+            , End Sequence
+            , Start Sequence
+            , Other Context 8 (BS.pack [0x2a, 0x03])
+            , End Sequence
+            , End (Container Context 0)
+            , End Sequence
+            ]
+    sanUnknown =
+        ExtensionRaw [2, 5, 29, 17] False $
+            encodeASN1'
+                DER
+                [ Start Sequence
+                , Other Context 8 (BS.pack [0x2a, 0x03])
+                , Other Context 2 (fromString "www.example.com")
+                , End Sequence
+                ]
+    sanBroken =
+        ExtensionRaw [2, 5, 29, 17] False $
+            encodeASN1' DER [Start Sequence, IntVal 1, End Sequence]
+
 -- | All validation test cases.
 treeWithAlg :: TestName -> Alg pub priv -> TestTree
 treeWithAlg groupName alg = withResource (initData alg) freeData $ \res ->
@@ -863,19 +1349,73 @@
                     [NameMismatch "266.0.0.1"] -- 266 read in Word8 is 10
                 , testSubjectAltNameIP
                     res
-                    (BS.pack [0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0, 0, 0, 0, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34])
+                    ( BS.pack
+                        [ 0x20
+                        , 0x01
+                        , 0x0d
+                        , 0xb8
+                        , 0x85
+                        , 0xa3
+                        , 0
+                        , 0
+                        , 0
+                        , 0
+                        , 0x8a
+                        , 0x2e
+                        , 0x03
+                        , 0x70
+                        , 0x73
+                        , 0x34
+                        ]
+                    )
                     "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
                     True
                     []
                 , testSubjectAltNameIP
                     res
-                    (BS.pack [0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0, 0, 0, 0, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34])
+                    ( BS.pack
+                        [ 0x20
+                        , 0x01
+                        , 0x0d
+                        , 0xb8
+                        , 0x85
+                        , 0xa3
+                        , 0
+                        , 0
+                        , 0
+                        , 0
+                        , 0x8a
+                        , 0x2e
+                        , 0x03
+                        , 0x70
+                        , 0x73
+                        , 0x34
+                        ]
+                    )
                     "2001:0db8:85a3::8a2e:0370:7334"
                     True
                     []
                 , testSubjectAltNameIP
                     res
-                    (BS.pack [0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0, 0, 0, 0, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34])
+                    ( BS.pack
+                        [ 0x20
+                        , 0x01
+                        , 0x0d
+                        , 0xb8
+                        , 0x85
+                        , 0xa3
+                        , 0
+                        , 0
+                        , 0
+                        , 0
+                        , 0x8a
+                        , 0x2e
+                        , 0x03
+                        , 0x70
+                        , 0x73
+                        , 0x34
+                        ]
+                    )
                     "2001:0db8:85a3:0:0:8a2e:0370:7334"
                     True
                     []
@@ -944,6 +1484,10 @@
             , treeWithAlg "ECDSA" (AlgEC curveName hashSHA512)
             , treeWithAlg "Ed25519" AlgEd25519
             , treeWithAlg "Ed448" AlgEd448
+            , treeWithAlg "ML-DSA-44" AlgMLDSA44
+            , treeWithAlg "ML-DSA-65" AlgMLDSA65
+            , treeWithAlg "ML-DSA-87" AlgMLDSA87
+            , treeRegression
             ]
   where
     pssParams = PSS.defaultPSSParams SHA224
diff --git a/crypton-x509-validation.cabal b/crypton-x509-validation.cabal
--- a/crypton-x509-validation.cabal
+++ b/crypton-x509-validation.cabal
@@ -1,63 +1,69 @@
-Name:                crypton-x509-validation
-version:             1.6.14
-Description:         X.509 Certificate and CRL validation. please see README
-License:             BSD3
-License-file:        LICENSE
-Copyright:           Vincent Hanquez <vincent@snarc.org>
-Author:              Vincent Hanquez <vincent@snarc.org>
-Maintainer:          Kazu Yamamoto <kazu@iij.ad.jp>
-Synopsis:            X.509 Certificate and CRL validation
-Build-Type:          Simple
-Category:            Data
-stability:           experimental
-Homepage:            https://github.com/kazu-yamamoto/crypton-certificate
-Cabal-Version:       >= 1.10
+cabal-version:      >=1.10
+name:               crypton-x509-validation
+version:            1.10.1
+license:            BSD3
+license-file:       LICENSE
+copyright:          Vincent Hanquez <vincent@snarc.org>
+maintainer:         Kazu Yamamoto <kazu@iij.ad.jp>
+author:             Vincent Hanquez <vincent@snarc.org>
+stability:          experimental
+homepage:           https://github.com/kazu-yamamoto/crypton-certificate
+synopsis:           X.509 Certificate and CRL validation
+description:        X.509 Certificate and CRL validation. please see README
+category:           Data
+build-type:         Simple
+extra-source-files: ChangeLog.md
 
-Library
-  Default-Language:  Haskell2010
-  Build-Depends:     base >= 3 && < 5
-                   , bytestring
-                   , memory
-                   , mtl
-                   , containers
-                   , hourglass
-                   , data-default
-                   , pem >= 0.1
-                   , asn1-types >= 0.3 && < 0.4
-                   , asn1-encoding >= 0.9 && < 0.10
-                   , crypton-x509 >= 1.7.5
-                   , crypton-x509-store >= 1.6
-                   , crypton >= 0.24
-                   , iproute >= 1.2.2
-  Exposed-modules:   Data.X509.Validation
-  Other-modules:     Data.X509.Validation.Signature
-                     Data.X509.Validation.Fingerprint
-                     Data.X509.Validation.Cache
-                     Data.X509.Validation.Types
-  ghc-options:       -Wall
+source-repository head
+    type:     git
+    location: https://github.com/kazu-yamamoto/crypton-certificate
+    subdir:   x509-validation
 
-Test-Suite test-x509-validation
-  Default-Language:  Haskell2010
-  type:              exitcode-stdio-1.0
-  hs-source-dirs:    Tests
-  Main-is:           Tests.hs
-  Other-modules:     Certificate
-  Build-Depends:     base >= 3 && < 5
-                   , bytestring
-                   , memory
-                   , data-default
-                   , tasty
-                   , tasty-hunit
-                   , hourglass
-                   , asn1-types
-                   , asn1-encoding
-                   , crypton-x509 >= 1.7.1
-                   , crypton-x509-store
-                   , crypton-x509-validation
-                   , crypton
-  ghc-options:       -Wall
+library
+    exposed-modules:  Data.X509.Validation
+    other-modules:
+        Data.X509.Validation.Signature
+        Data.X509.Validation.Fingerprint
+        Data.X509.Validation.Cache
+        Data.X509.Validation.Types
+        Data.X509.Validation.Internal
 
-source-repository head
-  type:     git
-  location: https://github.com/kazu-yamamoto/crypton-certificate
-  subdir:   x509-validation
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    build-depends:
+        base >=3 && <5,
+        bytestring,
+        containers,
+        crypton >=2.1.8 && <2.3,
+        crypton-asn1-types >=0.4.1 && <0.5,
+        crypton-asn1-encoding >=0.10.0 && <0.11,
+        crypton-pem >=0.2.4 && <0.4,
+        crypton-x509 >=1.10.1 && <1.11,
+        crypton-x509-store >=1.10 && <1.11,
+        data-default,
+        iproute >=1.2.2,
+        mtl,
+        ram,
+        time-hourglass
+
+test-suite test-x509-validation
+    type:             exitcode-stdio-1.0
+    main-is:          Tests.hs
+    hs-source-dirs:   Tests
+    other-modules:    Certificate
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    build-depends:
+        base >=3 && <5,
+        bytestring,
+        crypton,
+        crypton-asn1-encoding,
+        crypton-asn1-types,
+        crypton-x509,
+        crypton-x509-store,
+        crypton-x509-validation,
+        data-default,
+        ram,
+        tasty,
+        tasty-hunit,
+        time-hourglass
