diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index 9e50077e..c3d3f21d 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -88,3 +88,4 @@ jobs: - name: Test (signed Wasm module) run: | bazel test --define runtime=${{ matrix.runtime }} --per_file_copt=//...@-DPROXY_WASM_VERIFY_WITH_ED25519_PUBKEY=\"$(xxd -p -c 256 test/test_data/signature_key1.pub | cut -b9-)\" //test:signature_util_test + diff --git a/include/proxy-wasm/signature_util.h b/include/proxy-wasm/signature_util.h index 68579dcd..e6eb0fd9 100644 --- a/include/proxy-wasm/signature_util.h +++ b/include/proxy-wasm/signature_util.h @@ -27,7 +27,7 @@ class SignatureUtil { * @param message is the reference to store the message (success or error). * @return indicates whether the bytecode has a valid Wasm signature. */ - static bool verifySignature(std::string_view bytecode, std::string &message); + static bool verifySignature(std::string_view bytecode, const std::string pubkey, std::string &message); }; } // namespace proxy_wasm diff --git a/include/proxy-wasm/wasm.h b/include/proxy-wasm/wasm.h index 78ffbe37..05d35b1b 100644 --- a/include/proxy-wasm/wasm.h +++ b/include/proxy-wasm/wasm.h @@ -58,7 +58,7 @@ class WasmBase : public std::enable_shared_from_this { WasmBase(const std::shared_ptr &other, WasmVmFactory factory); virtual ~WasmBase(); - bool load(const std::string &code, bool allow_precompiled = false); + bool load(const std::string &code, bool allow_precompiled = false, const std::string pubkey = ""); bool initialize(); void startVm(ContextBase *root_context); bool configure(ContextBase *root_context, std::shared_ptr plugin); @@ -345,8 +345,10 @@ using WasmHandleCloneFactory = std::function(std::shared_ptr wasm)>; // Returns nullptr on failure (i.e. initialization of the VM fails). +// TODO: Consider a VerificationOptions struct rather than a single pubkey. std::shared_ptr -createWasm(std::string vm_key, std::string code, std::shared_ptr plugin, + createWasm(std::string vm_key, std::string code, std::string pubkey, + std::shared_ptr plugin, WasmHandleFactory factory, WasmHandleCloneFactory clone_factory, bool allow_precompiled); // Get an existing ThreadLocal VM matching 'vm_id' or nullptr if there isn't one. std::shared_ptr getThreadLocalWasm(std::string_view vm_id); diff --git a/src/signature_util.cc b/src/signature_util.cc index 71b1341c..61ef6c89 100644 --- a/src/signature_util.cc +++ b/src/signature_util.cc @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -24,8 +25,6 @@ namespace { -#ifdef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY - static uint8_t hex2dec(const unsigned char c) { if (c >= '0' && c <= '9') { return c - '0'; @@ -46,73 +45,81 @@ template constexpr std::array hex2pubkey(const char (&hex return pubkey; } +std::optional> getEd25519PubKey(std::string pubkey) { +#ifdef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY + static const auto ed25519_pubkey = hex2pubkey<32>(PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY); + return ed25519_pubkey; +#else + if (!pubkey.empty()) { + char pubkey_char[65]; + strcpy(pubkey_char, pubkey.c_str()); + return hex2pubkey<32>(pubkey_char); + } #endif + return std::nullopt; +} } // namespace namespace proxy_wasm { -bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &message) { - -#ifdef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY - - /* - * Ed25519 signature generated using https://github.com/jedisct1/wasmsign - */ - - std::string_view payload; - if (!BytecodeUtil::getCustomSection(bytecode, "signature_wasmsign", payload)) { - message = "Failed to parse corrupted Wasm module"; - return false; - } - - if (payload.empty()) { - message = "Custom Section \"signature_wasmsign\" not found"; - return false; - } - - if (bytecode.data() + bytecode.size() != payload.data() + payload.size()) { - message = "Custom Section \"signature_wasmsign\" not at the end of Wasm module"; - return false; - } - - if (payload.size() != 68) { - message = "Signature has a wrong size (want: 68, is: " + std::to_string(payload.size()) + ")"; - return false; - } - - uint32_t alg_id; - std::memcpy(&alg_id, payload.data(), sizeof(uint32_t)); - - if (alg_id != 2) { - message = "Signature has a wrong alg_id (want: 2, is: " + std::to_string(alg_id) + ")"; - return false; +bool SignatureUtil::verifySignature(std::string_view bytecode, std::string pubkey, + std::string &message) { + const auto ed25519_pubkey = getEd25519PubKey(pubkey); + if (ed25519_pubkey) { + /* + * Ed25519 signature generated using https://github.com/jedisct1/wasmsign + */ + + std::string_view payload; + if (!BytecodeUtil::getCustomSection(bytecode, "signature_wasmsign", payload)) { + message = "Failed to parse corrupted Wasm module"; + return false; + } + + if (payload.empty()) { + message = "Custom Section \"signature_wasmsign\" not found"; + return false; + } + + if (bytecode.data() + bytecode.size() != payload.data() + payload.size()) { + message = "Custom Section \"signature_wasmsign\" not at the end of Wasm module"; + return false; + } + + if (payload.size() != 68) { + message = "Signature has a wrong size (want: 68, is: " + std::to_string(payload.size()) + ")"; + return false; + } + + uint32_t alg_id; + std::memcpy(&alg_id, payload.data(), sizeof(uint32_t)); + + if (alg_id != 2) { + message = "Signature has a wrong alg_id (want: 2, is: " + std::to_string(alg_id) + ")"; + return false; + } + + const auto *signature = reinterpret_cast(payload.data()) + sizeof(uint32_t); + + SHA512_CTX ctx; + SHA512_Init(&ctx); + SHA512_Update(&ctx, "WasmSignature", sizeof("WasmSignature") - 1); + const uint32_t ad_len = 0; + SHA512_Update(&ctx, &ad_len, sizeof(uint32_t)); + const size_t section_len = 3 + sizeof("signature_wasmsign") - 1 + 68; + SHA512_Update(&ctx, bytecode.data(), bytecode.size() - section_len); + uint8_t hash[SHA512_DIGEST_LENGTH]; + SHA512_Final(hash, &ctx); + + if (!ED25519_verify(hash, sizeof(hash), signature, ed25519_pubkey.value().data())) { + message = "Signature mismatch"; + return false; + } + + message = "Wasm signature OK (Ed25519)"; + return true; } - - const auto *signature = reinterpret_cast(payload.data()) + sizeof(uint32_t); - - SHA512_CTX ctx; - SHA512_Init(&ctx); - SHA512_Update(&ctx, "WasmSignature", sizeof("WasmSignature") - 1); - const uint32_t ad_len = 0; - SHA512_Update(&ctx, &ad_len, sizeof(uint32_t)); - const size_t section_len = 3 + sizeof("signature_wasmsign") - 1 + 68; - SHA512_Update(&ctx, bytecode.data(), bytecode.size() - section_len); - uint8_t hash[SHA512_DIGEST_LENGTH]; - SHA512_Final(hash, &ctx); - - static const auto ed25519_pubkey = hex2pubkey<32>(PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY); - - if (!ED25519_verify(hash, sizeof(hash), signature, ed25519_pubkey.data())) { - message = "Signature mismatch"; - return false; - } - - message = "Wasm signature OK (Ed25519)"; - return true; - -#endif - return true; } diff --git a/src/wasm.cc b/src/wasm.cc index 4707c214..938f2b59 100644 --- a/src/wasm.cc +++ b/src/wasm.cc @@ -232,7 +232,7 @@ WasmBase::~WasmBase() { pending_delete_.clear(); } -bool WasmBase::load(const std::string &code, bool allow_precompiled) { +bool WasmBase::load(const std::string &code, bool allow_precompiled, const std::string pubkey) { assert(!started_from_.has_value()); if (!wasm_vm_) { @@ -249,9 +249,9 @@ bool WasmBase::load(const std::string &code, bool allow_precompiled) { return true; } - // Verify signature. + // Verify signature if a pubkey is present. std::string message; - if (!SignatureUtil::verifySignature(code, message)) { + if (!SignatureUtil::verifySignature(code, pubkey, message)) { fail(FailState::UnableToInitializeCode, message); return false; } else { @@ -463,6 +463,7 @@ WasmForeignFunction WasmBase::getForeignFunction(std::string_view function_name) } std::shared_ptr createWasm(std::string vm_key, std::string code, + std::string pubkey, std::shared_ptr plugin, WasmHandleFactory factory, WasmHandleCloneFactory clone_factory, @@ -490,7 +491,7 @@ std::shared_ptr createWasm(std::string vm_key, std::string code, (*base_wasms)[vm_key] = wasm_handle; } - if (!wasm_handle->wasm()->load(code, allow_precompiled)) { + if (!wasm_handle->wasm()->load(code, allow_precompiled, pubkey)) { wasm_handle->wasm()->fail(FailState::UnableToInitializeCode, "Failed to load Wasm code"); return nullptr; } diff --git a/test/BUILD b/test/BUILD index 33df7b86..4a3284c5 100644 --- a/test/BUILD +++ b/test/BUILD @@ -35,6 +35,8 @@ cc_test( "//test/test_data:abi_export.signed.with.key1.wasm", "//test/test_data:abi_export.signed.with.key2.wasm", "//test/test_data:abi_export.wasm", + "//test/test_data:signature_key1.pub", + "//test/test_data:signature_key2.pub", ], # Test only when compiled to verify plugins. tags = ["manual"], diff --git a/test/signature_util_test.cc b/test/signature_util_test.cc index f4b2ae24..b74868b3 100644 --- a/test/signature_util_test.cc +++ b/test/signature_util_test.cc @@ -24,36 +24,72 @@ namespace proxy_wasm { -TEST(TestSignatureUtil, GoodSignature) { +std::string BytesToHex(std::vector bytes) { + static const char *const hex = "0123456789ABCDEF"; + std::string result; + result.reserve(bytes.size() * 2); + for (auto byte : bytes) { + result.push_back(hex[byte >> 4]); + result.push_back(hex[byte & 0xf]); + } + return result; +} + +static std::string publickey(std::string filename) { + auto path = "test/test_data/" + filename; + std::ifstream file(path, std::ios::binary); + EXPECT_FALSE(file.fail()) << "failed to open: " << path; + std::stringstream file_string_stream; + file_string_stream << file.rdbuf(); + std::string input = file_string_stream.str(); + std::vector bytes(input.begin() + 4, input.end()); + return BytesToHex(bytes); +} + +TEST(TestSignatureUtil, NoPublicKey) { #ifndef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY - FAIL() << "Built without a key for verifying signed Wasm modules."; + const auto bytecode = readTestWasmFile("abi_export.signed.with.key1.wasm"); + std::string message; + EXPECT_TRUE(SignatureUtil::verifySignature(bytecode, "", message)); + EXPECT_EQ(message, ""); #endif +} +TEST(TestSignatureUtil, GoodSignature) { + std::string pubkey = publickey("signature_key1.pub"); const auto bytecode = readTestWasmFile("abi_export.signed.with.key1.wasm"); std::string message; - EXPECT_TRUE(SignatureUtil::verifySignature(bytecode, message)); + EXPECT_TRUE(SignatureUtil::verifySignature(bytecode, pubkey, message)); EXPECT_EQ(message, "Wasm signature OK (Ed25519)"); } TEST(TestSignatureUtil, BadSignature) { -#ifndef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY - FAIL() << "Built without a key for verifying signed Wasm modules."; -#endif - + std::string pubkey = publickey("signature_key1.pub"); const auto bytecode = readTestWasmFile("abi_export.signed.with.key2.wasm"); std::string message; - EXPECT_FALSE(SignatureUtil::verifySignature(bytecode, message)); + EXPECT_FALSE(SignatureUtil::verifySignature(bytecode, pubkey, message)); EXPECT_EQ(message, "Signature mismatch"); } -TEST(TestSignatureUtil, NoSignature) { -#ifndef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY - FAIL() << "Built without a key for verifying signed Wasm modules."; +TEST(TestSignatureUtil, BuildTimeKeyPrecedence) { + // Uses hard-coded key if defined. + std::string pubkey = publickey("signature_key2.pub"); + const auto bytecode = readTestWasmFile("abi_export.signed.with.key1.wasm"); + std::string message; +#ifdef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY + EXPECT_TRUE(SignatureUtil::verifySignature(bytecode, pubkey, message)); + EXPECT_EQ(message, "Wasm signature OK (Ed25519)"); +#else + EXPECT_FALSE(SignatureUtil::verifySignature(bytecode, pubkey, message)); + EXPECT_EQ(message, "Signature mismatch"); #endif +} +TEST(TestSignatureUtil, NoSignature) { + std::string pubkey = publickey("signature_key1.pub"); const auto bytecode = readTestWasmFile("abi_export.wasm"); std::string message; - EXPECT_FALSE(SignatureUtil::verifySignature(bytecode, message)); + EXPECT_FALSE(SignatureUtil::verifySignature(bytecode, pubkey, message)); EXPECT_EQ(message, "Custom Section \"signature_wasmsign\" not found"); }