auditor

An aarch64 constant-time memory access auditing tool.
git clone git://git.ppad.tech/auditor.git
Log | Files | Refs | README | LICENSE

commit 03eec1f2ce8811a1762f98bf3edf1c5b4f4fde4d
parent 4304b5edc8b93688779bb49f3846d0a1993370a4
Author: Jared Tobin <jared@jtobin.io>
Date:   Wed, 11 Feb 2026 14:19:16 +0400

feat: implement sidecar taint config for secret/public arguments (IMPL7)

Add JSON config file support to declare per-function secret/public
argument registers. The taint analysis seeds entry states accordingly,
enabling detection of secret-dependent memory accesses.

- Add TaintConfig and ArgPolicy types with FromJSON instances
- Add --taint-config (-t) CLI option to load config
- Implement seedArgs to seed entry taint from policy
- Add config-aware dataflow: runDataflowWithConfig, runInterProcWithConfig
- Add config-aware checks: checkCFGWithConfig, checkCFGInterProcWithConfig
- Add 8 tests for config parsing and taint seeding

Config format:
  {"func_name": {"secret": ["X0"], "public": ["X1", "X2"]}}

Secret registers are marked Secret at function entry; taint flows
through mov/arithmetic to detect when secrets reach memory addresses.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mapp/Main.hs | 62+++++++++++++++++++++++++++++++++++++++++++++++---------------
Mlib/Audit/AArch64.hs | 48+++++++++++++++++++++++++++++++++++++++++++++++-
Mlib/Audit/AArch64/Check.hs | 43+++++++++++++++++++++++++++++++++++++++++++
Mlib/Audit/AArch64/Taint.hs | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/Audit/AArch64/Types.hs | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mppad-auditor.cabal | 5++++-
Mtest/Main.hs | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 494 insertions(+), 18 deletions(-)

diff --git a/app/Main.hs b/app/Main.hs @@ -2,10 +2,16 @@ module Main where -import Audit.AArch64 (AuditResult(..), Violation(..), ViolationReason(..), - auditFile, auditFileInterProc, parseFile, regName) +import Audit.AArch64 + ( AuditResult(..), Violation(..), ViolationReason(..) + , TaintConfig(..) + , auditFile, auditFileInterProc + , auditFileWithConfig, auditFileInterProcWithConfig + , parseFile, regName, loadTaintConfig + ) import Data.Aeson (encode) import qualified Data.ByteString.Lazy.Char8 as BL +import qualified Data.Map.Strict as Map import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as TIO @@ -13,11 +19,12 @@ import Options.Applicative import System.Exit (exitFailure, exitSuccess) data Options = Options - { optInput :: !FilePath - , optJson :: !Bool - , optQuiet :: !Bool - , optInterProc :: !Bool - , optParseOnly :: !Bool + { optInput :: !FilePath + , optJson :: !Bool + , optQuiet :: !Bool + , optInterProc :: !Bool + , optParseOnly :: !Bool + , optTaintConfig :: !(Maybe FilePath) } deriving (Eq, Show) optParser :: Parser Options @@ -47,6 +54,12 @@ optParser = Options ( long "parse" <> help "Parse only, no analysis" ) + <*> optional (strOption + ( long "taint-config" + <> short 't' + <> metavar "FILE" + <> help "JSON file with per-function taint policies" + )) optInfo :: ParserInfo Options optInfo = info (optParser <**> helper) @@ -69,16 +82,35 @@ main = do TIO.putStrLn $ "Parsed " <> T.pack (show n) <> " lines" exitSuccess else do - let auditor = if optInterProc opts then auditFileInterProc else auditFile - result <- auditor (optInput opts) - case result of + -- Load taint config if provided + mcfg <- case optTaintConfig opts of + Nothing -> pure (Right emptyConfig) + Just path -> loadTaintConfig path + case mcfg of Left err -> do - TIO.putStrLn $ "Error: " <> err + TIO.putStrLn $ "Error loading taint config: " <> err exitFailure - Right ar -> - if optJson opts - then outputJson ar - else outputText opts ar + Right cfg -> do + let auditor = selectAuditor opts cfg + result <- auditor (optInput opts) + case result of + Left err -> do + TIO.putStrLn $ "Error: " <> err + exitFailure + Right ar -> + if optJson opts + then outputJson ar + else outputText opts ar + where + emptyConfig = TaintConfig Map.empty + + selectAuditor opts cfg + | hasConfig && optInterProc opts = auditFileInterProcWithConfig cfg + | hasConfig = auditFileWithConfig cfg + | optInterProc opts = auditFileInterProc + | otherwise = auditFile + where + hasConfig = not (Map.null (tcPolicies cfg)) outputJson :: AuditResult -> IO () outputJson ar = BL.putStrLn (encode (arViolations ar)) diff --git a/lib/Audit/AArch64.hs b/lib/Audit/AArch64.hs @@ -33,6 +33,10 @@ module Audit.AArch64 ( , auditInterProc , auditFile , auditFileInterProc + , auditWithConfig + , auditInterProcWithConfig + , auditFileWithConfig + , auditFileInterProcWithConfig , parseFile -- * Results @@ -42,6 +46,12 @@ module Audit.AArch64 ( , Reg , regName + -- * Taint configuration + , TaintConfig(..) + , ArgPolicy(..) + , emptyArgPolicy + , loadTaintConfig + -- * Re-exports , ParseError , showParseError @@ -50,7 +60,11 @@ module Audit.AArch64 ( import Audit.AArch64.CFG import Audit.AArch64.Check import Audit.AArch64.Parser -import Audit.AArch64.Types (Reg, Violation(..), ViolationReason(..), regName) +import Audit.AArch64.Types + ( Reg, Violation(..), ViolationReason(..), regName + , TaintConfig(..), ArgPolicy(..), emptyArgPolicy + ) +import Data.Aeson (eitherDecodeStrict') import qualified Data.ByteString as BS import Data.Text (Text) import qualified Data.Text as T @@ -100,3 +114,35 @@ parseFile path = do case parseAsm src of Left err -> pure (Left (T.pack (showParseError err))) Right lns -> pure (Right (length lns)) + +-- | Audit assembly source with taint config. +auditWithConfig :: TaintConfig -> Text -> Text -> Either ParseError AuditResult +auditWithConfig tcfg name src = do + lns <- parseAsm src + let cfg = buildCFG lns + pure (checkCFGWithConfig tcfg name cfg) + +-- | Audit with inter-procedural analysis and taint config. +auditInterProcWithConfig :: TaintConfig -> Text -> Text + -> Either ParseError AuditResult +auditInterProcWithConfig tcfg name src = do + lns <- parseAsm src + let cfg = buildCFG lns + pure (checkCFGInterProcWithConfig tcfg name cfg) + +-- | Audit an assembly file with taint config. +auditFileWithConfig :: TaintConfig -> FilePath -> IO (Either Text AuditResult) +auditFileWithConfig tcfg = auditFileWith (auditWithConfig tcfg) + +-- | Audit an assembly file with inter-procedural analysis and taint config. +auditFileInterProcWithConfig :: TaintConfig -> FilePath + -> IO (Either Text AuditResult) +auditFileInterProcWithConfig tcfg = auditFileWith (auditInterProcWithConfig tcfg) + +-- | Load a taint config from a JSON file. +loadTaintConfig :: FilePath -> IO (Either Text TaintConfig) +loadTaintConfig path = do + bs <- BS.readFile path + case eitherDecodeStrict' bs of + Left err -> pure (Left (T.pack err)) + Right cfg -> pure (Right cfg) diff --git a/lib/Audit/AArch64/Check.hs b/lib/Audit/AArch64/Check.hs @@ -17,6 +17,8 @@ module Audit.AArch64.Check ( , checkBlock , checkCFG , checkCFGInterProc + , checkCFGWithConfig + , checkCFGInterProcWithConfig , AuditResult(..) ) where @@ -26,6 +28,7 @@ import Audit.AArch64.Types import qualified Data.IntMap.Strict as IM import qualified Data.Map.Strict as Map import Data.Text (Text) +import Data.Maybe (fromMaybe) -- | Result of auditing assembly. data AuditResult = AuditResult @@ -189,3 +192,43 @@ checkBlockWithSummary sym summaries st0 lns = go mempty st0 lns Nothing -> analyzeLine l st _ -> analyzeLine l st in go (acc <> result) st' ls + +-- | Check entire CFG with taint config. +checkCFGWithConfig :: TaintConfig -> Text -> CFG -> AuditResult +checkCFGWithConfig tcfg sym cfg = + let inStates = runDataflowWithConfig tcfg cfg + blocks = cfgBlocks cfg + in mconcat + [ fst (checkBlock blockSym inState (bbLines bb)) + | (idx, bb) <- zip [0..] blocks + , let blockSym = fromMaybe sym (bbLabel bb) + inState = IM.findWithDefault initTaintState idx inStates + ] + +-- | Check entire CFG with inter-procedural analysis and taint config. +checkCFGInterProcWithConfig :: TaintConfig -> Text -> CFG -> AuditResult +checkCFGInterProcWithConfig tcfg sym cfg = + let summaries = runInterProcWithConfig tcfg cfg + funcs = functionLabels cfg + in mconcat + [ checkFunctionWithConfig cfg tcfg sym func summaries + | func <- funcs + ] + where + checkFunctionWithConfig c tc s func summs = + let blockIdxs = functionBlocks c func + bs = cfgBlocks c + -- Seed entry state with function policy + baseEntry = initTaintState + entryState = case Map.lookup func (tcPolicies tc) of + Nothing -> baseEntry + Just policy -> seedArgs policy baseEntry + inStatesIM = runFunctionBlocksWithEntry c blockIdxs summs entryState + inStatesMap = IM.foldlWithKey' toMap Map.empty inStatesIM + in mconcat + [ fst (checkBlockWithSummary s summs inState (bbLines bb)) + | idx <- blockIdxs + , let bb = bs !! idx + inState = Map.findWithDefault entryState idx inStatesMap + ] + toMap m k v = Map.insert k v m diff --git a/lib/Audit/AArch64/Taint.hs b/lib/Audit/AArch64/Taint.hs @@ -25,6 +25,7 @@ module Audit.AArch64.Taint ( , publicRoots , joinTaintState , runDataflow + , runDataflowWithConfig -- * Function summaries , FuncSummary(..) , initSummary @@ -33,10 +34,18 @@ module Audit.AArch64.Taint ( , runFunctionDataflow , runFunctionBlocks , runInterProc + , runInterProcWithConfig + , runFunctionBlocksWithEntry + -- * Entry taint seeding + , seedArgs ) where import Audit.AArch64.CFG import Audit.AArch64.Types + ( Reg(..), Instr(..), Line(..), Operand(..), AddrMode(..) + , Taint(..), joinTaint, Provenance(..), joinProvenance + , TaintConfig(..), ArgPolicy(..) + ) import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IM import Data.IntSet (IntSet) @@ -102,6 +111,26 @@ initTaintState = TaintState , tsStackProv = IM.empty } +-- | Seed argument registers according to policy. +-- Secret registers are marked Secret with ProvUnknown. +-- Public registers are marked Public with ProvPublic. +-- If a register appears in both lists, secret takes precedence. +seedArgs :: ArgPolicy -> TaintState -> TaintState +seedArgs policy st = + let -- Apply public first, then secret (so secret wins on overlap) + st1 = Set.foldr markPublic st (apPublic policy) + st2 = Set.foldr markSecret st1 (apSecret policy) + in st2 + where + markPublic r s = s + { tsRegs = Map.insert r Public (tsRegs s) + , tsProv = Map.insert r ProvPublic (tsProv s) + } + markSecret r s = s + { tsRegs = Map.insert r Secret (tsRegs s) + , tsProv = Map.insert r ProvUnknown (tsProv s) + } + -- | Get the taint of a register. getTaint :: Reg -> TaintState -> Taint getTaint r st = Map.findWithDefault Unknown r (tsRegs st) @@ -739,3 +768,145 @@ findCallers cfg target = where callGraph = buildCallGraph cfg callees f = Map.findWithDefault [] f callGraph + +-- --------------------------------------------------------------------- +-- Config-aware dataflow analysis + +-- | Run forward dataflow with taint config. +-- Entry states are seeded according to function-specific policies. +runDataflowWithConfig :: TaintConfig -> CFG -> IntMap TaintState +runDataflowWithConfig tcfg cfg + | null (cfgBlocks cfg) = IM.empty + | otherwise = go initWorklist initIn IM.empty + where + blocks = cfgBlocks cfg + nBlocks = length blocks + + -- Build a map from block index to entry taint state + -- Entry blocks of functions get their policy applied + initIn = IM.fromList + [ (i, entryState i bb) + | (i, bb) <- zip [0..] blocks + ] + + entryState idx bb = + let base = initTaintState + in case bbLabel bb of + Nothing -> base + Just lbl -> + -- Check if this block is a function entry + case functionBlocks cfg lbl of + (entry:_) | entry == idx -> + case Map.lookup lbl (tcPolicies tcfg) of + Nothing -> base + Just policy -> seedArgs policy base + _ -> base + + initWorklist = IS.fromList [0..nBlocks-1] + + go worklist inStates outStates + | IS.null worklist = inStates + | otherwise = + let (idx, worklist') = IS.deleteFindMin worklist + bb = blocks !! idx + inState = IM.findWithDefault initTaintState idx inStates + outState = analyzeBlock (bbLines bb) inState + oldOut = IM.lookup idx outStates + changed = oldOut /= Just outState + outStates' = IM.insert idx outState outStates + succs = blockSuccessors cfg idx + (worklist'', inStates') = if changed + then propagate succs outState worklist' inStates + else (worklist', inStates) + in go worklist'' inStates' outStates' + + propagate [] _ wl ins = (wl, ins) + propagate (s:ss) out wl ins = + let oldIn = IM.findWithDefault emptyTaintState s ins + newIn = joinTaintState oldIn out + ins' = IM.insert s newIn ins + wl' = if oldIn /= newIn then IS.insert s wl else wl + in propagate ss out wl' ins' + +-- | Run inter-procedural analysis with taint config. +runInterProcWithConfig :: TaintConfig -> CFG -> Map Text FuncSummary +runInterProcWithConfig tcfg cfg = go initSummaries (Set.fromList funcs) + where + funcs = functionLabels cfg + initSummaries = Map.fromList [(f, initSummary) | f <- funcs] + + go summaries worklist + | Set.null worklist = summaries + | otherwise = + let (func, worklist') = Set.deleteFindMin worklist + blockIdxs = functionBlocks cfg func + outState = runFunctionDataflowWithConfig tcfg cfg func + blockIdxs summaries + newSumm = FuncSummary outState + oldSumm = Map.findWithDefault initSummary func summaries + changed = newSumm /= oldSumm + summaries' = Map.insert func newSumm summaries + callers = findCallers cfg func + worklist'' = if changed + then foldr Set.insert worklist' callers + else worklist' + in go summaries' worklist'' + +-- | Run function dataflow with config-based entry seeding. +runFunctionDataflowWithConfig :: TaintConfig -> CFG -> Text -> [Int] + -> Map Text FuncSummary -> TaintState +runFunctionDataflowWithConfig tcfg cfg funcName blockIndices summaries = + let blocks = cfgBlocks cfg + -- Seed entry state with function policy + baseEntry = initTaintState + entryState = case Map.lookup funcName (tcPolicies tcfg) of + Nothing -> baseEntry + Just policy -> seedArgs policy baseEntry + inStates = runFunctionBlocksWithEntry cfg blockIndices summaries entryState + returnOuts = + [ analyzeBlockWithSummaries bb inState summaries + | i <- blockIndices + , let bb = blocks !! i + inState = IM.findWithDefault entryState i inStates + , endsWithRet bb + ] + in case returnOuts of + [] -> initTaintState + (o:os) -> foldl joinTaintState o os + +-- | Run function blocks with a custom entry state. +runFunctionBlocksWithEntry :: CFG -> [Int] -> Map Text FuncSummary + -> TaintState -> IntMap TaintState +runFunctionBlocksWithEntry _ [] _ _ = IM.empty +runFunctionBlocksWithEntry cfg (entryIdx:rest) summaries entryState = + go initWorklist initIn IM.empty + where + blocks = cfgBlocks cfg + blockSet = IS.fromList (entryIdx:rest) + + initIn = IM.singleton entryIdx entryState + initWorklist = IS.singleton entryIdx + + go wl inStates outStates + | IS.null wl = inStates + | otherwise = + let (idx, wl') = IS.deleteFindMin wl + bb = blocks !! idx + inState = IM.findWithDefault entryState idx inStates + outState = analyzeBlockWithSummaries bb inState summaries + oldOut = IM.lookup idx outStates + changed = oldOut /= Just outState + outStates' = IM.insert idx outState outStates + succs = filter (`IS.member` blockSet) (blockSuccessors cfg idx) + (wl'', inStates') = if changed + then propagate succs outState wl' inStates + else (wl', inStates) + in go wl'' inStates' outStates' + + propagate [] _ wl ins = (wl, ins) + propagate (s:ss) out wl ins = + let oldIn = IM.findWithDefault entryState s ins + newIn = joinTaintState oldIn out + ins' = IM.insert s newIn ins + wl' = if oldIn /= newIn then IS.insert s wl else wl + in propagate ss out wl' ins' diff --git a/lib/Audit/AArch64/Types.hs b/lib/Audit/AArch64/Types.hs @@ -14,6 +14,7 @@ module Audit.AArch64.Types ( -- * Registers Reg(..) , regName + , regFromText -- * Operands and addressing , Shift(..) @@ -36,9 +37,18 @@ module Audit.AArch64.Types ( -- * Analysis results , Violation(..) , ViolationReason(..) + + -- * Taint configuration + , TaintConfig(..) + , ArgPolicy(..) + , emptyArgPolicy ) where -import Data.Aeson (ToJSON(..), (.=), object) +import Data.Aeson (ToJSON(..), FromJSON(..), (.=), (.:?), object, withObject) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Set (Set) +import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as T import GHC.Generics (Generic) @@ -83,6 +93,55 @@ instance ToJSON Reg where regName :: Reg -> Text regName r = T.toLower (T.pack (show r)) +-- | Parse a register from text (case-insensitive). +regFromText :: Text -> Maybe Reg +regFromText t = Map.lookup (T.toUpper t) regMap + where + regMap :: Map Text Reg + regMap = Map.fromList + [ ("X0", X0), ("X1", X1), ("X2", X2), ("X3", X3) + , ("X4", X4), ("X5", X5), ("X6", X6), ("X7", X7) + , ("X8", X8), ("X9", X9), ("X10", X10), ("X11", X11) + , ("X12", X12), ("X13", X13), ("X14", X14), ("X15", X15) + , ("X16", X16), ("X17", X17), ("X18", X18), ("X19", X19) + , ("X20", X20), ("X21", X21), ("X22", X22), ("X23", X23) + , ("X24", X24), ("X25", X25), ("X26", X26), ("X27", X27) + , ("X28", X28), ("X29", X29), ("X30", X30) + , ("W0", W0), ("W1", W1), ("W2", W2), ("W3", W3) + , ("W4", W4), ("W5", W5), ("W6", W6), ("W7", W7) + , ("W8", W8), ("W9", W9), ("W10", W10), ("W11", W11) + , ("W12", W12), ("W13", W13), ("W14", W14), ("W15", W15) + , ("W16", W16), ("W17", W17), ("W18", W18), ("W19", W19) + , ("W20", W20), ("W21", W21), ("W22", W22), ("W23", W23) + , ("W24", W24), ("W25", W25), ("W26", W26), ("W27", W27) + , ("W28", W28), ("W29", W29), ("W30", W30) + , ("SP", SP), ("XZR", XZR), ("WZR", WZR) + , ("D0", D0), ("D1", D1), ("D2", D2), ("D3", D3) + , ("D4", D4), ("D5", D5), ("D6", D6), ("D7", D7) + , ("D8", D8), ("D9", D9), ("D10", D10), ("D11", D11) + , ("D12", D12), ("D13", D13), ("D14", D14), ("D15", D15) + , ("D16", D16), ("D17", D17), ("D18", D18), ("D19", D19) + , ("D20", D20), ("D21", D21), ("D22", D22), ("D23", D23) + , ("D24", D24), ("D25", D25), ("D26", D26), ("D27", D27) + , ("D28", D28), ("D29", D29), ("D30", D30), ("D31", D31) + , ("S0", S0), ("S1", S1), ("S2", S2), ("S3", S3) + , ("S4", S4), ("S5", S5), ("S6", S6), ("S7", S7) + , ("S8", S8), ("S9", S9), ("S10", S10), ("S11", S11) + , ("S12", S12), ("S13", S13), ("S14", S14), ("S15", S15) + , ("S16", S16), ("S17", S17), ("S18", S18), ("S19", S19) + , ("S20", S20), ("S21", S21), ("S22", S22), ("S23", S23) + , ("S24", S24), ("S25", S25), ("S26", S26), ("S27", S27) + , ("S28", S28), ("S29", S29), ("S30", S30), ("S31", S31) + , ("Q0", Q0), ("Q1", Q1), ("Q2", Q2), ("Q3", Q3) + , ("Q4", Q4), ("Q5", Q5), ("Q6", Q6), ("Q7", Q7) + , ("Q8", Q8), ("Q9", Q9), ("Q10", Q10), ("Q11", Q11) + , ("Q12", Q12), ("Q13", Q13), ("Q14", Q14), ("Q15", Q15) + , ("Q16", Q16), ("Q17", Q17), ("Q18", Q18), ("Q19", Q19) + , ("Q20", Q20), ("Q21", Q21), ("Q22", Q22), ("Q23", Q23) + , ("Q24", Q24), ("Q25", Q25), ("Q26", Q26), ("Q27", Q27) + , ("Q28", Q28), ("Q29", Q29), ("Q30", Q30), ("Q31", Q31) + ] + -- | Shift operations for indexed addressing. data Shift = LSL !Int -- ^ Logical shift left @@ -293,3 +352,39 @@ instance ToJSON Violation where , "instr" .= show (vInstr v) , "reason" .= vReason v ] + +-- | Per-function argument taint policy. +-- Specifies which registers should be seeded as secret or public. +data ArgPolicy = ArgPolicy + { apSecret :: !(Set Reg) -- ^ Registers to mark as Secret + , apPublic :: !(Set Reg) -- ^ Registers to mark as Public + } deriving (Eq, Show) + +-- | Empty argument policy (no overrides). +emptyArgPolicy :: ArgPolicy +emptyArgPolicy = ArgPolicy Set.empty Set.empty + +instance FromJSON ArgPolicy where + parseJSON = withObject "ArgPolicy" $ \o -> do + secretTexts <- o .:? "secret" + publicTexts <- o .:? "public" + let parseRegs :: Maybe [Text] -> Either Text (Set Reg) + parseRegs Nothing = Right Set.empty + parseRegs (Just ts) = + let parsed = map (\t -> (t, regFromText t)) ts + bad = [t | (t, Nothing) <- parsed] + in case bad of + [] -> Right $ Set.fromList [r | (_, Just r) <- parsed] + (b:_) -> Left $ "invalid register: " <> b + case (parseRegs secretTexts, parseRegs publicTexts) of + (Left e, _) -> fail (T.unpack e) + (_, Left e) -> fail (T.unpack e) + (Right s, Right p) -> pure $ ArgPolicy s p + +-- | Taint configuration: maps function symbols to argument policies. +newtype TaintConfig = TaintConfig + { tcPolicies :: Map Text ArgPolicy + } deriving (Eq, Show) + +instance FromJSON TaintConfig where + parseJSON v = TaintConfig <$> parseJSON v diff --git a/ppad-auditor.cabal b/ppad-auditor.cabal @@ -48,6 +48,7 @@ executable auditor aeson , base , bytestring + , containers , optparse-applicative >= 0.16 && < 0.19 , ppad-auditor , text @@ -60,8 +61,10 @@ test-suite auditor-tests ghc-options: -rtsopts -Wall -O2 build-depends: - base + aeson + , base , bytestring + , containers , ppad-auditor , tasty , tasty-hunit diff --git a/test/Main.hs b/test/Main.hs @@ -6,6 +6,10 @@ import Audit.AArch64 import Audit.AArch64.Parser import Audit.AArch64.Taint import Audit.AArch64.Types +import Data.Aeson (eitherDecodeStrict') +import qualified Data.ByteString.Char8 as B8 +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set import qualified Data.Text as T import Test.Tasty import Test.Tasty.HUnit @@ -17,6 +21,7 @@ main = defaultMain $ testGroup "ppad-auditor" [ , auditTests , interprocTests , provenanceTests + , taintConfigTests ] -- Parser tests @@ -462,3 +467,84 @@ provenanceTests = testGroup "Provenance" [ Left e -> assertFailure $ "parse failed: " ++ show e Right ar -> assertEqual "one violation" 1 (length (arViolations ar)) ] + +-- Taint config tests + +taintConfigTests :: TestTree +taintConfigTests = testGroup "TaintConfig" [ + testCase "parse valid config" $ do + let json = B8.pack "{\"foo\": {\"secret\": [\"X0\"], \"public\": [\"X1\"]}}" + case eitherDecodeStrict' json :: Either String TaintConfig of + Left e -> assertFailure $ "parse failed: " ++ e + Right cfg -> do + let policy = Map.lookup "foo" (tcPolicies cfg) + case policy of + Nothing -> assertFailure "missing policy for 'foo'" + Just p -> do + assertEqual "secret regs" (Set.singleton X0) (apSecret p) + assertEqual "public regs" (Set.singleton X1) (apPublic p) + + , testCase "parse invalid register" $ do + let json = B8.pack "{\"foo\": {\"secret\": [\"INVALID\"]}}" + case eitherDecodeStrict' json :: Either String TaintConfig of + Left _ -> pure () -- Expected to fail + Right _ -> assertFailure "should have failed on invalid register" + + , testCase "seedArgs marks secret" $ do + let policy = ArgPolicy (Set.singleton X0) Set.empty + st = seedArgs policy initTaintState + assertEqual "X0 is secret" Secret (getTaint X0 st) + + , testCase "seedArgs marks public" $ do + let policy = ArgPolicy Set.empty (Set.singleton X0) + st = seedArgs policy initTaintState + assertEqual "X0 is public" Public (getTaint X0 st) + + , testCase "secret takes precedence over public" $ do + let policy = ArgPolicy (Set.singleton X0) (Set.singleton X0) + st = seedArgs policy initTaintState + assertEqual "X0 is secret" Secret (getTaint X0 st) + + , testCase "secret arg causes violation on indexed access" $ do + let src = T.unlines + [ "_mul_wnaf:" + , " ldr x8, [x20, x0]" -- x0 as index + , " ret" + ] + cfg = TaintConfig $ Map.singleton "_mul_wnaf" + (ArgPolicy (Set.singleton X0) Set.empty) + case auditWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> do + assertEqual "one violation" 1 (length (arViolations ar)) + case arViolations ar of + [v] -> case vReason v of + SecretIndex X0 -> pure () + other -> assertFailure $ "wrong reason: " ++ show other + _ -> assertFailure "expected one violation" + + , testCase "public arg allows indexed access" $ do + let src = T.unlines + [ "_foo:" + , " ldr x8, [x20, x0]" -- x0 as index + , " ret" + ] + cfg = TaintConfig $ Map.singleton "_foo" + (ArgPolicy Set.empty (Set.singleton X0)) + case auditWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> assertEqual "no violations" 0 (length (arViolations ar)) + + , testCase "interproc with config" $ do + let src = T.unlines + [ "_mul_wnaf:" + , " ldr x8, [x20, x0]" -- x0 as secret index -> violation + , " ret" + ] + cfg = TaintConfig $ Map.singleton "_mul_wnaf" + (ArgPolicy (Set.singleton X0) Set.empty) + case auditInterProcWithConfig cfg "test" src of + Left e -> assertFailure $ "parse failed: " ++ show e + Right ar -> do + assertEqual "one violation" 1 (length (arViolations ar)) + ]