diff --git a/crypto/account.go b/crypto/account.go index 9fb368d5..4aead19e 100644 --- a/crypto/account.go +++ b/crypto/account.go @@ -189,6 +189,8 @@ type LogicSigAccount struct { // MakeLogicSigAccountEscrow creates a new escrow LogicSigAccount. The address // of this account will be a hash of its program. +// Deprecated: This method is deprecated for not applying basic sanity check over program bytes, +// use `MakeLogicSigAccountEscrowChecked` instead. func MakeLogicSigAccountEscrow(program []byte, args [][]byte) LogicSigAccount { return LogicSigAccount{ Lsig: types.LogicSig{ @@ -198,6 +200,16 @@ func MakeLogicSigAccountEscrow(program []byte, args [][]byte) LogicSigAccount { } } +// MakeLogicSigAccountEscrowChecked creates a new escrow LogicSigAccount. +// The address of this account will be a hash of its program. +func MakeLogicSigAccountEscrowChecked(program []byte, args [][]byte) (LogicSigAccount, error) { + lsig, err := MakeLogicSig(program, args, nil, MultisigAccount{}) + if err != nil { + return LogicSigAccount{}, err + } + return LogicSigAccount{Lsig: lsig}, nil +} + // MakeLogicSigAccountDelegated creates a new delegated LogicSigAccount. This // type of LogicSig has the authority to sign transactions on behalf of another // account, called the delegating account. If the delegating account is a diff --git a/crypto/crypto.go b/crypto/crypto.go index a47c7f55..8aec7a19 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -5,13 +5,13 @@ import ( "crypto/rand" "crypto/sha512" "encoding/base32" + "encoding/base64" "encoding/binary" "fmt" "golang.org/x/crypto/ed25519" "github.com/algorand/go-algorand-sdk/encoding/msgpack" - "github.com/algorand/go-algorand-sdk/logic" "github.com/algorand/go-algorand-sdk/types" ) @@ -160,7 +160,7 @@ func SignBytes(sk ed25519.PrivateKey, bytesToSign []byte) (signature []byte, err return } -//VerifyBytes verifies that the signature is valid +// VerifyBytes verifies that the signature is valid func VerifyBytes(pk ed25519.PublicKey, message, signature []byte) bool { msgParts := [][]byte{bytesPrefix, message} toBeVerified := bytes.Join(msgParts, nil) @@ -454,6 +454,39 @@ func ComputeGroupID(txgroup []types.Transaction) (gid types.Digest, err error) { /* LogicSig support */ +func isAsciiPrintableByte(symbol byte) bool { + isBreakLine := symbol == '\n' + isStdPrintable := symbol >= ' ' && symbol <= '~' + return isBreakLine || isStdPrintable +} + +func isAsciiPrintable(program []byte) bool { + for _, b := range program { + if !isAsciiPrintableByte(b) { + return false + } + } + return true +} + +// sanityCheckProgram performs heuristic program validation: +// check if passed in bytes are Algorand address or is B64 encoded, rather than Teal bytes +func sanityCheckProgram(program []byte) error { + if len(program) == 0 { + return fmt.Errorf("empty program") + } + if isAsciiPrintable(program) { + if _, err := types.DecodeAddress(string(program)); err == nil { + return fmt.Errorf("requesting program bytes, get Algorand address") + } + if _, err := base64.StdEncoding.DecodeString(string(program)); err == nil { + return fmt.Errorf("program should not be b64 encoded") + } + return fmt.Errorf("program bytes are all ASCII printable characters, not looking like Teal byte code") + } + return nil +} + // VerifyLogicSig verifies that a LogicSig contains a valid program and, if a // delegated signature is present, that the signature is valid. // @@ -462,7 +495,7 @@ func ComputeGroupID(txgroup []types.Transaction) (gid types.Digest, err error) { // multsig account). In that case, it should be the address of the delegating // account. func VerifyLogicSig(lsig types.LogicSig, singleSigner types.Address) (result bool) { - if err := logic.CheckProgram(lsig.Logic, lsig.Args); err != nil { + if err := sanityCheckProgram(lsig.Logic); err != nil { return false } @@ -602,8 +635,9 @@ func AddressFromProgram(program []byte) types.Address { // MakeLogicSig produces a new LogicSig signature. // -// THIS FUNCTION IS DEPRECATED. It will be removed in v2 of this library. Use -// one of MakeLogicSigAccountEscrow, MakeLogicSigAccountDelegated, or +// Deprecated: THIS FUNCTION IS DEPRECATED. +// It will be removed in v2 of this library. +// Use one of MakeLogicSigAccountEscrow, MakeLogicSigAccountDelegated, or // MakeLogicSigAccountDelegatedMsig instead. // // The function can work in three modes: @@ -611,11 +645,7 @@ func AddressFromProgram(program []byte) types.Address { // 2. If no ma provides, it returns Sig delegated LogicSig // 3. If both sk and ma specified the function returns Multisig delegated LogicSig func MakeLogicSig(program []byte, args [][]byte, sk ed25519.PrivateKey, ma MultisigAccount) (lsig types.LogicSig, err error) { - if len(program) == 0 { - err = errLsigInvalidProgram - return - } - if err = logic.CheckProgram(program, args); err != nil { + if err = sanityCheckProgram(program); err != nil { return } diff --git a/crypto/crypto_test.go b/crypto/crypto_test.go index f7101fac..3da8677a 100644 --- a/crypto/crypto_test.go +++ b/crypto/crypto_test.go @@ -327,13 +327,6 @@ func TestMakeLogicSigBasic(t *testing.T) { err = msgpack.Decode(encoded, &lsig1) require.NoError(t, err) require.Equal(t, lsig, lsig1) - - // check invalid program fails - programMod := make([]byte, len(program)) - copy(programMod[:], program) - programMod[0] = 128 - lsig, err = MakeLogicSig(programMod, args, sk, pk) - require.Error(t, err) } func TestMakeLogicSigSingle(t *testing.T) { diff --git a/logic/logic.go b/logic/logic.go index d2dfcca6..1e98768c 100644 --- a/logic/logic.go +++ b/logic/logic.go @@ -10,12 +10,14 @@ import ( "github.com/algorand/go-algorand-sdk/types" ) +// Deprecated type langSpec struct { EvalMaxVersion int LogicSigVersion int Ops []operation } +// Deprecated type operation struct { Opcode int Name string @@ -29,16 +31,23 @@ type operation struct { Group []string } +// Deprecated var spec *langSpec + +// Deprecated var opcodes []operation // CheckProgram performs basic program validation: instruction count and program cost +// Deprecated: Validation relies on metadata (`langspec.json`) that does not accurately represent opcode behavior across program versions. +// The behavior of `CheckProgram` relies on `langspec.json`. Thus, this method is being deprecated. func CheckProgram(program []byte, args [][]byte) error { _, _, err := ReadProgram(program, args) return err } // ReadProgram is used to validate a program as well as extract found variables +// Deprecated: Validation relies on metadata (`langspec.json`) that does not accurately represent opcode behavior across program versions. +// The behavior of `ReadProgram` relies on `langspec.json`. Thus, this method is being deprecated. func ReadProgram(program []byte, args [][]byte) (ints []uint64, byteArrays [][]byte, err error) { const intcblockOpcode = 32 const bytecblockOpcode = 38 @@ -138,6 +147,7 @@ func ReadProgram(program []byte, args [][]byte) (ints []uint64, byteArrays [][]b return } +// Deprecated func readIntConstBlock(program []byte, pc int) (size int, ints []uint64, err error) { size = 1 numInts, bytesUsed := binary.Uvarint(program[pc+size:]) @@ -163,6 +173,7 @@ func readIntConstBlock(program []byte, pc int) (size int, ints []uint64, err err return } +// Deprecated func readByteConstBlock(program []byte, pc int) (size int, byteArrays [][]byte, err error) { size = 1 numInts, bytesUsed := binary.Uvarint(program[pc+size:]) @@ -195,6 +206,7 @@ func readByteConstBlock(program []byte, pc int) (size int, byteArrays [][]byte, return } +// Deprecated func readPushIntOp(program []byte, pc int) (size int, foundInt uint64, err error) { size = 1 foundInt, bytesUsed := binary.Uvarint(program[pc+size:]) @@ -207,6 +219,7 @@ func readPushIntOp(program []byte, pc int) (size int, foundInt uint64, err error return } +// Deprecated func readPushByteOp(program []byte, pc int) (size int, byteArray []byte, err error) { size = 1 itemLen, bytesUsed := binary.Uvarint(program[pc+size:]) diff --git a/test/steps_test.go b/test/steps_test.go index f5528ef2..b35dc7bc 100644 --- a/test/steps_test.go +++ b/test/steps_test.go @@ -107,6 +107,8 @@ var txTrace future.DryrunTxnResult var trace string var sourceMap logic.SourceMap var srcMapping map[string]interface{} +var seeminglyProgram []byte +var sanityCheckError error var assetTestFixture struct { Creator string @@ -387,7 +389,10 @@ func FeatureContext(s *godog.Suite) { s.Step(`^the resulting source map is the same as the json "([^"]*)"$`, theResultingSourceMapIsTheSameAsTheJson) s.Step(`^getting the line associated with a pc "([^"]*)" equals "([^"]*)"$`, gettingTheLineAssociatedWithAPcEquals) s.Step(`^getting the last pc associated with a line "([^"]*)" equals "([^"]*)"$`, gettingTheLastPcAssociatedWithALineEquals) - + s.Step(`^a base64 encoded program bytes for heuristic sanity check "([^"]*)"$`, takeB64encodedBytes) + s.Step(`^I start heuristic sanity check over the bytes$`, heuristicCheckOverBytes) + s.Step(`^if the heuristic sanity check throws an error, the error contains "([^"]*)"$`, checkErrorIfMatching) + s.BeforeScenario(func(interface{}) { stxObj = types.SignedTxn{} abiMethods = nil @@ -2634,3 +2639,30 @@ func theResultingSourceMapIsTheSameAsTheJson(expectedJsonPath string) error { return nil } + +func takeB64encodedBytes(b64encodedBytes string) error { + var err error + seeminglyProgram, err = base64.StdEncoding.DecodeString(b64encodedBytes) + if err != nil { + return err + } + return nil +} + +func heuristicCheckOverBytes() error { + _, sanityCheckError = crypto.MakeLogicSigAccountEscrowChecked(seeminglyProgram, nil) + return nil +} + +func checkErrorIfMatching(errMsg string) error { + if len(errMsg) == 0 { + if sanityCheckError != nil { + return fmt.Errorf("expected err message to be empty, but sanity check says %w", sanityCheckError) + } + } else { + if sanityCheckError == nil || !strings.Contains(sanityCheckError.Error(), errMsg) { + return fmt.Errorf("expected err to contain %s, but sanity check error not matching: %w", errMsg, sanityCheckError) + } + } + return nil +} diff --git a/test/unit.tags b/test/unit.tags index 9dc6ffc2..f8384ed5 100644 --- a/test/unit.tags +++ b/test/unit.tags @@ -12,6 +12,7 @@ @unit.indexer.logs @unit.indexer.rekey @unit.offline +@unit.program_sanity_check @unit.rekey @unit.responses @unit.responses.231