{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Convex.ThreatModel.RedeemerAssetSubstitution (
redeemerAssetSubstitution,
) where
import Cardano.Api qualified as C
import Convex.ThreatModel
import Data.ByteString qualified as BS
import Data.Maybe (listToMaybe)
import GHC.Exts (fromList, toList)
redeemerAssetSubstitution :: ThreatModel ()
redeemerAssetSubstitution :: ThreatModel ()
redeemerAssetSubstitution = [Char] -> ThreatModel () -> ThreatModel ()
forall a. [Char] -> ThreatModel a -> ThreatModel a
Named [Char]
"Redeemer Asset Substitution" (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ do
Input
scriptInput <- (Input -> Bool) -> ThreatModel Input
anyInputSuchThat (Bool -> Bool
not (Bool -> Bool) -> (Input -> Bool) -> Input -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. AddressAny -> Bool
isKeyAddressAny (AddressAny -> Bool) -> (Input -> AddressAny) -> Input -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Input -> AddressAny
forall t. IsInputOrOutput t => t -> AddressAny
addressOf)
Redeemer
redeemer <-
Input -> ThreatModel (Maybe Redeemer)
getRedeemer Input
scriptInput ThreatModel (Maybe Redeemer)
-> (Maybe Redeemer -> ThreatModel Redeemer) -> ThreatModel Redeemer
forall a b. ThreatModel a -> (a -> ThreatModel b) -> ThreatModel b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Maybe Redeemer
Nothing -> [Char] -> ThreatModel Redeemer
forall a. [Char] -> ThreatModel a
failPrecondition [Char]
"No redeemer found for script input"
Just Redeemer
redeemer' -> Redeemer -> ThreatModel Redeemer
forall a. a -> ThreatModel a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Redeemer
redeemer'
let redeemerByteStrings :: [ByteString]
redeemerByteStrings = Redeemer -> [ByteString]
extractByteStrings Redeemer
redeemer
let validByteStrings :: [ByteString]
validByteStrings = (ByteString -> Bool) -> [ByteString] -> [ByteString]
forall a. (a -> Bool) -> [a] -> [a]
filter (\ByteString
bs -> Bool -> Bool
not (ByteString -> Bool
BS.null ByteString
bs) Bool -> Bool -> Bool
&& ByteString -> Int
BS.length ByteString
bs Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
32) [ByteString]
redeemerByteStrings
[ByteString]
_ <- case [ByteString]
validByteStrings of
[] -> [Char] -> ThreatModel [ByteString]
forall a. [Char] -> ThreatModel a
failPrecondition [Char]
"No valid ByteStrings found in redeemer (empty or too long)"
[ByteString]
xs -> [ByteString] -> ThreatModel [ByteString]
forall a. a -> ThreatModel a
forall (f :: * -> *) a. Applicative f => a -> f a
pure [ByteString]
xs
[Output]
outputs <- ThreatModel [Output]
getTxOutputs
let swappablePairs :: [(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)]
swappablePairs = [ByteString]
-> [Output]
-> [(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)]
findSwappablePairs [ByteString]
validByteStrings [Output]
outputs
(Output
output1, Output
output2, PolicyId
policyId, AssetName
origAssetName, AssetName
otherAssetName, Quantity
origQty, Quantity
otherQty, ByteString
origBs) <-
case [(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)]
-> Maybe
(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
forall a. [a] -> Maybe a
listToMaybe [(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)]
swappablePairs of
Maybe
(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
Nothing ->
[Char]
-> ThreatModel
(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
forall a. [Char] -> ThreatModel a
failPrecondition ([Char]
-> ThreatModel
(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString))
-> [Char]
-> ThreatModel
(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
forall a b. (a -> b) -> a -> b
$
[Char]
"No swappable token pair found. The transaction needs at least two different "
[Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
"tokens from the same policy in different outputs. This naturally happens when "
[Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
"the wallet holds multiple tokens from the same policy and coin selection "
[Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [Char]
"includes them in the transaction."
Just (Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
pair -> (Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
-> ThreatModel
(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
forall a. a -> ThreatModel a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
pair
let newValue1 :: Value
newValue1 = PolicyId
-> AssetName -> AssetName -> Quantity -> Quantity -> Value -> Value
swapToken PolicyId
policyId AssetName
origAssetName AssetName
otherAssetName Quantity
origQty Quantity
otherQty (Output -> Value
forall t. IsInputOrOutput t => t -> Value
valueOf Output
output1)
newValue2 :: Value
newValue2 = PolicyId
-> AssetName -> AssetName -> Quantity -> Quantity -> Value -> Value
swapToken PolicyId
policyId AssetName
otherAssetName AssetName
origAssetName Quantity
otherQty Quantity
origQty (Output -> Value
forall t. IsInputOrOutput t => t -> Value
valueOf Output
output2)
let C.UnsafeAssetName ByteString
otherBs = AssetName
otherAssetName
modifiedRedeemer :: Redeemer
modifiedRedeemer = ByteString -> ByteString -> Redeemer -> Redeemer
substituteByteString ByteString
origBs ByteString
otherBs Redeemer
redeemer
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"Testing Redeemer Asset Substitution vulnerability (swappable-pair approach):"
, [Char]
"Found two outputs with different tokens from the same policy."
, [Char]
"Swapping tokens between outputs and substituting redeemer."
]
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"Policy: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ PolicyId -> [Char]
forall a. Show a => a -> [Char]
show PolicyId
policyId
, [Char]
"Original token (from redeemer): " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ AssetName -> [Char]
forall a. Show a => a -> [Char]
show AssetName
origAssetName
, [Char]
"Other token (for swap): " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ AssetName -> [Char]
forall a. Show a => a -> [Char]
show AssetName
otherAssetName
]
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"Output 1 original value: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Value -> [Char]
forall a. Show a => a -> [Char]
show (Output -> Value
forall t. IsInputOrOutput t => t -> Value
valueOf Output
output1)
, [Char]
"Output 1 new value: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Value -> [Char]
forall a. Show a => a -> [Char]
show Value
newValue1
, [Char]
"Output 2 original value: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Value -> [Char]
forall a. Show a => a -> [Char]
show (Output -> Value
forall t. IsInputOrOutput t => t -> Value
valueOf Output
output2)
, [Char]
"Output 2 new value: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Value -> [Char]
forall a. Show a => a -> [Char]
show Value
newValue2
]
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"Original redeemer: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Redeemer -> [Char]
forall a. Show a => a -> [Char]
show Redeemer
redeemer
, [Char]
"Modified redeemer: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ Redeemer -> [Char]
forall a. Show a => a -> [Char]
show Redeemer
modifiedRedeemer
]
[Char] -> ThreatModel ()
counterexampleTM ([Char] -> ThreatModel ()) -> [Char] -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
[[Char]] -> [Char]
paragraph
[ [Char]
"If this validates, the validator trusts redeemer-provided"
, [Char]
"asset identifiers without cross-checking the datum."
, [Char]
"An attacker could substitute worthless tokens for valuable ones."
]
TxModifier -> ThreatModel ()
shouldNotValidate (TxModifier -> ThreatModel ()) -> TxModifier -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
Input -> Redeemer -> TxModifier
changeRedeemerOf Input
scriptInput Redeemer
modifiedRedeemer
TxModifier -> TxModifier -> TxModifier
forall a. Semigroup a => a -> a -> a
<> Output -> Value -> TxModifier
forall t. IsInputOrOutput t => t -> Value -> TxModifier
changeValueOf Output
output1 Value
newValue1
TxModifier -> TxModifier -> TxModifier
forall a. Semigroup a => a -> a -> a
<> Output -> Value -> TxModifier
forall t. IsInputOrOutput t => t -> Value -> TxModifier
changeValueOf Output
output2 Value
newValue2
findSwappablePairs
:: [BS.ByteString]
-> [Output]
-> [(Output, Output, C.PolicyId, C.AssetName, C.AssetName, C.Quantity, C.Quantity, BS.ByteString)]
findSwappablePairs :: [ByteString]
-> [Output]
-> [(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)]
findSwappablePairs [ByteString]
validBs [Output]
outputs = do
ByteString
bs <- [ByteString]
validBs
let targetAssetName :: AssetName
targetAssetName = ByteString -> AssetName
C.UnsafeAssetName ByteString
bs
Output
output1 <- [Output]
outputs
let out1Value :: Value
out1Value = Output -> Value
forall t. IsInputOrOutput t => t -> Value
valueOf Output
output1
out1Assets :: [Item Value]
out1Assets = Value -> [Item Value]
forall l. IsList l => l -> [Item l]
toList Value
out1Value
(C.AssetId PolicyId
policyId AssetName
assetName1, Quantity
qty1) <- [(AssetId, Quantity)]
out1Assets
Bool -> [()]
guard (AssetName
assetName1 AssetName -> AssetName -> Bool
forall a. Eq a => a -> a -> Bool
== AssetName
targetAssetName Bool -> Bool -> Bool
&& Quantity
qty1 Quantity -> Quantity -> Bool
forall a. Ord a => a -> a -> Bool
> Quantity
0)
Output
output2 <- [Output]
outputs
Bool -> [()]
guard (Output -> TxIx
outputIx Output
output1 TxIx -> TxIx -> Bool
forall a. Eq a => a -> a -> Bool
/= Output -> TxIx
outputIx Output
output2)
let out2Value :: Value
out2Value = Output -> Value
forall t. IsInputOrOutput t => t -> Value
valueOf Output
output2
case PolicyId -> AssetName -> Value -> Maybe (AssetName, Quantity)
findDifferentTokenFromPolicy PolicyId
policyId AssetName
assetName1 Value
out2Value of
Maybe (AssetName, Quantity)
Nothing -> []
Just (AssetName
assetName2, Quantity
qty2) ->
(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)
-> [(Output, Output, PolicyId, AssetName, AssetName, Quantity,
Quantity, ByteString)]
forall a. a -> [a]
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Output
output1, Output
output2, PolicyId
policyId, AssetName
assetName1, AssetName
assetName2, Quantity
qty1, Quantity
qty2, ByteString
bs)
findDifferentTokenFromPolicy
:: C.PolicyId
-> C.AssetName
-> C.Value
-> Maybe (C.AssetName, C.Quantity)
findDifferentTokenFromPolicy :: PolicyId -> AssetName -> Value -> Maybe (AssetName, Quantity)
findDifferentTokenFromPolicy PolicyId
targetPolicy AssetName
excludedName Value
value =
let assets :: [Item Value]
assets = Value -> [Item Value]
forall l. IsList l => l -> [Item l]
toList Value
value
candidates :: [(AssetName, Quantity)]
candidates =
[ (AssetName
name, Quantity
qty)
| (C.AssetId PolicyId
policy AssetName
name, Quantity
qty) <- [(AssetId, Quantity)]
assets
, PolicyId
policy PolicyId -> PolicyId -> Bool
forall a. Eq a => a -> a -> Bool
== PolicyId
targetPolicy
, AssetName
name AssetName -> AssetName -> Bool
forall a. Eq a => a -> a -> Bool
/= AssetName
excludedName
, Quantity
qty Quantity -> Quantity -> Bool
forall a. Ord a => a -> a -> Bool
> Quantity
0
]
in [(AssetName, Quantity)] -> Maybe (AssetName, Quantity)
forall a. [a] -> Maybe a
listToMaybe [(AssetName, Quantity)]
candidates
swapToken
:: C.PolicyId
-> C.AssetName
-> C.AssetName
-> C.Quantity
-> C.Quantity
-> C.Value
-> C.Value
swapToken :: PolicyId
-> AssetName -> AssetName -> Quantity -> Quantity -> Value -> Value
swapToken PolicyId
policyId AssetName
removeAsset AssetName
addAsset Quantity
removeQty Quantity
addQty Value
value =
Value
value
Value -> Value -> Value
forall a. Semigroup a => a -> a -> a
<> Value -> Value
C.negateValue ([Item Value] -> Value
forall l. IsList l => [Item l] -> l
fromList [(PolicyId -> AssetName -> AssetId
C.AssetId PolicyId
policyId AssetName
removeAsset, Quantity
removeQty)])
Value -> Value -> Value
forall a. Semigroup a => a -> a -> a
<> [Item Value] -> Value
forall l. IsList l => [Item l] -> l
fromList [(PolicyId -> AssetName -> AssetId
C.AssetId PolicyId
policyId AssetName
addAsset, Quantity
addQty)]
extractByteStrings :: C.ScriptData -> [BS.ByteString]
= Redeemer -> [ByteString]
go
where
go :: C.ScriptData -> [BS.ByteString]
go :: Redeemer -> [ByteString]
go Redeemer
sd = case Redeemer
sd of
C.ScriptDataBytes ByteString
bs -> [ByteString
bs]
C.ScriptDataConstructor Integer
_ [Redeemer]
fields -> (Redeemer -> [ByteString]) -> [Redeemer] -> [ByteString]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Redeemer -> [ByteString]
go [Redeemer]
fields
C.ScriptDataList [Redeemer]
items -> (Redeemer -> [ByteString]) -> [Redeemer] -> [ByteString]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Redeemer -> [ByteString]
go [Redeemer]
items
C.ScriptDataMap [(Redeemer, Redeemer)]
pairs -> ((Redeemer, Redeemer) -> [ByteString])
-> [(Redeemer, Redeemer)] -> [ByteString]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (\(Redeemer
k, Redeemer
v) -> Redeemer -> [ByteString]
go Redeemer
k [ByteString] -> [ByteString] -> [ByteString]
forall a. [a] -> [a] -> [a]
++ Redeemer -> [ByteString]
go Redeemer
v) [(Redeemer, Redeemer)]
pairs
C.ScriptDataNumber Integer
_ -> []
substituteByteString :: BS.ByteString -> BS.ByteString -> C.ScriptData -> C.ScriptData
substituteByteString :: ByteString -> ByteString -> Redeemer -> Redeemer
substituteByteString ByteString
target ByteString
replacement = Redeemer -> Redeemer
go
where
go :: C.ScriptData -> C.ScriptData
go :: Redeemer -> Redeemer
go Redeemer
sd = case Redeemer
sd of
C.ScriptDataBytes ByteString
bs
| ByteString
bs ByteString -> ByteString -> Bool
forall a. Eq a => a -> a -> Bool
== ByteString
target -> ByteString -> Redeemer
C.ScriptDataBytes ByteString
replacement
| Bool
otherwise -> Redeemer
sd
C.ScriptDataConstructor Integer
n [Redeemer]
fields ->
Integer -> [Redeemer] -> Redeemer
C.ScriptDataConstructor Integer
n ((Redeemer -> Redeemer) -> [Redeemer] -> [Redeemer]
forall a b. (a -> b) -> [a] -> [b]
map Redeemer -> Redeemer
go [Redeemer]
fields)
C.ScriptDataList [Redeemer]
items ->
[Redeemer] -> Redeemer
C.ScriptDataList ((Redeemer -> Redeemer) -> [Redeemer] -> [Redeemer]
forall a b. (a -> b) -> [a] -> [b]
map Redeemer -> Redeemer
go [Redeemer]
items)
C.ScriptDataMap [(Redeemer, Redeemer)]
pairs ->
[(Redeemer, Redeemer)] -> Redeemer
C.ScriptDataMap [(Redeemer -> Redeemer
go Redeemer
k, Redeemer -> Redeemer
go Redeemer
v) | (Redeemer
k, Redeemer
v) <- [(Redeemer, Redeemer)]
pairs]
C.ScriptDataNumber Integer
_ -> Redeemer
sd
guard :: Bool -> [()]
guard :: Bool -> [()]
guard Bool
True = [()]
guard Bool
False = []