blob: f2eff81552131fed5ea8b816b27f6332395c3815 [file]
#ifdef WITH_RIVE_SCRIPTING
#include "catch.hpp"
#include "rive_file_reader.hpp"
#include "rive/assets/file_asset_contents.hpp"
#include "rive/assets/script_asset.hpp"
#include "rive/assets/script_module_asset.hpp"
#include "rive/assets/shader_asset.hpp"
#ifdef WITH_RIVE_SCRIPTING_WASM
#include "rive/wasm/wasm_scripting_vm.hpp"
#endif
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#ifndef _WIN32
#include <unistd.h>
#endif
using namespace rive;
namespace
{
// The AnimaScript fixtures were exported with SampleSigningContext, whose key
// only builds carrying its public half accept; the Luau one was signed in
// production.
#ifdef WITH_RIVE_TEST_SIGNATURE
constexpr bool kSampleKeyVerifies = true;
#else
constexpr bool kSampleKeyVerifies = false;
#endif
constexpr bool kProductionKeyVerifies = !kSampleKeyVerifies;
#ifdef WITH_RIVE_TOOLS
constexpr bool kUnsignedAccepted = true;
#else
constexpr bool kUnsignedAccepted = false;
#endif
// AnimaScript only, so nothing in it is signed.
constexpr const char* kUnsigned = "assets/animascript.riv";
// AnimaScript beside a shader, whose signature covers the shader alone.
constexpr const char* kShaderSigned = "assets/animascript_shader_signed.riv";
// A Luau script and two shaders signed together.
constexpr const char* kLuauShader = "assets/luau_shader_signed.riv";
rcp<File> importBytes(
const std::vector<uint8_t>& bytes,
bool requireSignedScripts = false)
{
ImportResult result;
auto file = File::import(
bytes,
&gNoOpFactory,
&result,
nullptr,
nullptr,
requireSignedScripts);
REQUIRE(result == ImportResult::success);
REQUIRE(file != nullptr);
return file;
}
template <typename T> std::vector<T*> assetsOf(File* file)
{
std::vector<T*> found;
for (auto asset : file->assets())
{
if (asset->is<T>())
{
found.push_back(asset->as<T>());
}
}
return found;
}
std::vector<uint8_t>::iterator find(
std::vector<uint8_t>& bytes,
const char* text)
{
auto at = std::search(
bytes.begin(),
bytes.end(),
text,
text + std::strlen(text));
REQUIRE(at != bytes.end());
return at;
}
// The file's signature property; it is the last one its contents object
// writes.
std::vector<uint8_t>::iterator findSignature(std::vector<uint8_t>& bytes)
{
const uint8_t key[] = {0x8F, 0x07, 64};
static_assert(
FileAssetContentsBase::signaturePropertyKey == 0x38F,
"the key bytes above encode the signature property");
auto at = std::search(bytes.begin(), bytes.end(), key, key + 3);
REQUIRE(at != bytes.end());
REQUIRE(at[3 + 64] == 0);
return at;
}
void stripSignature(std::vector<uint8_t>& bytes)
{
auto at = findSignature(bytes);
bytes.erase(at, at + 3 + 64);
}
#ifndef __has_feature
#define __has_feature(x) 0
#endif
void checkModuleRuns(File* file)
{
REQUIRE(assetsOf<ScriptModuleAsset>(file).size() == 1);
// ASan's larger frames overflow WAMR's native stack check at instantiate.
#if defined(WITH_RIVE_SCRIPTING_WASM) && !defined(__SANITIZE_ADDRESS__) && \
!__has_feature(address_sanitizer)
REQUIRE(file->wasmScriptingVM() != nullptr);
for (auto script : assetsOf<ScriptAsset>(file))
{
CHECK(script->isModule() == (script->generatorFunctionRef() == 0));
}
#endif
}
bool shaderUsed(const ShaderAsset* shader)
{
bool exposed = !shader->rstb().empty();
if (!exposed)
{
CHECK(shader->textureSamplerPairs().empty());
for (uint8_t target = 0; target <= 16; target++)
{
CHECK(shader->findShader(target).empty());
}
}
return exposed;
}
void checkShadersUsed(File* file, bool used)
{
auto shaders = assetsOf<ShaderAsset>(file);
REQUIRE_FALSE(shaders.empty());
for (auto shader : shaders)
{
CHECK(shaderUsed(shader) == used);
}
}
#ifdef WITH_RIVE_SCRIPTING_LUAU
bool luauRuns(File* file) { return file->scriptingVM() != nullptr; }
#endif
#ifndef _WIN32
// What [run] prints to stderr.
template <typename Run> std::string stderrOf(Run run)
{
fflush(stderr);
FILE* capture = tmpfile();
REQUIRE(capture != nullptr);
int saved = dup(fileno(stderr));
dup2(fileno(capture), fileno(stderr));
run();
fflush(stderr);
dup2(saved, fileno(stderr));
close(saved);
std::string text;
rewind(capture);
for (int c = fgetc(capture); c != EOF; c = fgetc(capture))
{
text.push_back((char)c);
}
fclose(capture);
return text;
}
size_t countOf(const std::string& text, const std::string& needle)
{
size_t count = 0;
for (size_t at = text.find(needle); at != std::string::npos;
at = text.find(needle, at + 1))
{
count++;
}
return count;
}
#endif
} // namespace
TEST_CASE("an unsigned AnimaScript module runs", "[signing]")
{
for (const char* path : {kUnsigned, kShaderSigned})
{
for (bool requireSigned : {false, true})
{
auto file = importBytes(ReadFile(path), requireSigned);
checkModuleRuns(file.get());
}
}
}
#if defined(WITH_RIVE_SCRIPTING_WASM) && !defined(__EMSCRIPTEN__)
TEST_CASE("a module slot holding AOT code is refused", "[signing]")
{
// Unsigned, a file must never hand WAMR precompiled native code to run.
const uint8_t aot[] = {0, 'a', 'o', 't', 1, 0, 0, 0};
std::string error;
CHECK(
WasmScriptingVM::make(
Span<const uint8_t>(aot, sizeof(aot)),
&gNoOpFactory,
error) == nullptr);
CHECK(error == "script module is not wasm bytecode");
auto bytes = ReadFile(kUnsigned);
const uint8_t wasm[] = {0, 'a', 's', 'm'};
auto at = std::search(bytes.begin(), bytes.end(), wasm, wasm + 4);
REQUIRE(at != bytes.end());
std::copy(aot, aot + 4, at);
for (bool requireSigned : {false, true})
{
auto file = importBytes(bytes, requireSigned);
CHECK(file->wasmScriptingVM() == nullptr);
}
}
#endif
TEST_CASE(
"the signature beside an AnimaScript module covers the shader alone",
"[signing]")
{
auto file = importBytes(ReadFile(kShaderSigned));
auto shaders = assetsOf<ShaderAsset>(file.get());
REQUIRE(shaders.size() == 1);
CHECK(shaders.front()->verified() == kSampleKeyVerifies);
// Records carry no contents, so they are no part of the signed group.
for (auto script : assetsOf<ScriptAsset>(file.get()))
{
CHECK_FALSE(script->verified());
}
checkShadersUsed(file.get(), kUnsignedAccepted || kSampleKeyVerifies);
// The test key proves nothing about bytes the embedder did not supply.
auto required = importBytes(ReadFile(kShaderSigned), true);
checkShadersUsed(required.get(), false);
}
TEST_CASE("a signed file's shaders are used", "[signing]")
{
auto file = importBytes(ReadFile(kLuauShader));
checkShadersUsed(file.get(), kUnsignedAccepted || kProductionKeyVerifies);
auto required = importBytes(ReadFile(kLuauShader), true);
checkShadersUsed(required.get(), kProductionKeyVerifies);
}
TEST_CASE(
"a tampered shader is refused with the Luau it was signed with",
"[signing]")
{
for (const char* path : {kShaderSigned, kLuauShader})
{
auto bytes = ReadFile(path);
find(bytes, "VertexOut")[0] ^= 0x01;
for (bool requireSigned : {false, true})
{
auto file = importBytes(bytes, requireSigned);
for (auto shader : assetsOf<ShaderAsset>(file.get()))
{
CHECK_FALSE(shader->verified());
}
checkShadersUsed(file.get(), kUnsignedAccepted && !requireSigned);
for (auto script : assetsOf<ScriptAsset>(file.get()))
{
CHECK_FALSE(script->verified());
}
#ifdef WITH_RIVE_SCRIPTING_LUAU
if (path == kLuauShader && requireSigned)
{
CHECK_FALSE(luauRuns(file.get()));
}
#endif
}
}
}
TEST_CASE(
"a shader without its signature is refused unless unsigned content "
"is accepted",
"[signing]")
{
for (const char* path : {kShaderSigned, kLuauShader})
{
auto bytes = ReadFile(path);
stripSignature(bytes);
for (bool requireSigned : {false, true})
{
auto file = importBytes(bytes, requireSigned);
for (auto shader : assetsOf<ShaderAsset>(file.get()))
{
CHECK_FALSE(shader->verified());
}
checkShadersUsed(file.get(), kUnsignedAccepted && !requireSigned);
}
}
}
TEST_CASE("a referenced shader verifies its own signature", "[signing]")
{
// The fixture's group holds the shader alone, so its signature is the one
// a referenced export embeds.
auto bytes = ReadFile(kShaderSigned);
auto file = importBytes(bytes);
auto shaders = assetsOf<ShaderAsset>(file.get());
REQUIRE(shaders.size() == 1);
auto rstb = shaders.front()->rstb();
REQUIRE_FALSE(rstb.empty());
auto signature = findSignature(bytes) + 3;
std::vector<uint8_t> envelope = {0x80};
envelope.insert(envelope.end(), signature, signature + 64);
envelope.insert(envelope.end(), rstb.begin(), rstb.end());
auto tampered = envelope;
tampered.back() ^= 0x01;
for (bool requireSigned : {false, true})
{
ShaderAsset referenced;
referenced.importedWith(requireSigned);
REQUIRE(referenced.decode(envelope, nullptr));
CHECK(referenced.verified() == kSampleKeyVerifies);
CHECK(
shaderUsed(&referenced) ==
(!requireSigned && (kUnsignedAccepted || kSampleKeyVerifies)));
ShaderAsset forged;
forged.importedWith(requireSigned);
REQUIRE(forged.decode(tampered, nullptr));
CHECK_FALSE(forged.verified());
CHECK(shaderUsed(&forged) == (!requireSigned && kUnsignedAccepted));
}
}
TEST_CASE(
"a Luau and shader file signed together verifies its Luau",
"[signing]")
{
auto file = importBytes(ReadFile(kLuauShader));
auto scripts = assetsOf<ScriptAsset>(file.get());
REQUIRE_FALSE(scripts.empty());
for (auto script : scripts)
{
CHECK(script->verified() == kProductionKeyVerifies);
}
for (auto shader : assetsOf<ShaderAsset>(file.get()))
{
CHECK(shader->verified() == kProductionKeyVerifies);
}
#ifdef WITH_RIVE_SCRIPTING_LUAU
auto required = importBytes(ReadFile(kLuauShader), true);
CHECK(luauRuns(required.get()) == kProductionKeyVerifies);
#endif
}
TEST_CASE("a tampered Luau script is refused", "[signing]")
{
auto bytes = ReadFile(kLuauShader);
find(bytes, "add WGSL assets")[0] ^= 0x01;
auto file = importBytes(bytes, /*requireSignedScripts*/ true);
for (auto script : assetsOf<ScriptAsset>(file.get()))
{
CHECK_FALSE(script->verified());
}
checkShadersUsed(file.get(), false);
#ifdef WITH_RIVE_SCRIPTING_LUAU
CHECK_FALSE(luauRuns(file.get()));
#endif
}
#ifndef _WIN32
TEST_CASE(
"a refused shader is reported once its file is read, never on reads",
"[signing]")
{
auto bytes = ReadFile(kLuauShader);
find(bytes, "VertexOut")[0] ^= 0x01;
const std::string refusal = "is unavailable: its signature did not verify";
rcp<File> file;
std::string imported = stderrOf(
[&] { file = importBytes(bytes, /*requireSignedScripts*/ true); });
auto shaders = assetsOf<ShaderAsset>(file.get());
CHECK(countOf(imported, refusal) == shaders.size());
std::string read = stderrOf([&] { checkShadersUsed(file.get(), false); });
CHECK(countOf(read, refusal) == 0);
}
#endif
#endif