Skip to content

Commit 0102da1

Browse files
authored
Fix wasmsign2 v1 to v2 format migration in signature verification
1 parent f2791fe commit 0102da1

File tree

1 file changed

+209
-24
lines changed

1 file changed

+209
-24
lines changed

src/signature_util.cc

Lines changed: 209 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
#ifdef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY
2121
#include <openssl/evp.h>
22-
#include <openssl/sha.h>
2322
#endif
2423

2524
#include "include/proxy-wasm/bytecode_util.h"
@@ -53,6 +52,42 @@ template <size_t N> constexpr std::array<uint8_t, N> hex2pubkey(const char (&hex
5352

5453
} // namespace
5554

55+
namespace {
56+
57+
#ifdef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY
58+
59+
// Helper function to parse a varint from the payload
60+
bool parseVarint(const char *&pos, const char *end, uint32_t &ret) {
61+
uint32_t shift = 0;
62+
uint32_t total = 0;
63+
uint32_t v;
64+
char b;
65+
while (pos < end) {
66+
if (pos + 1 > end) {
67+
return false;
68+
}
69+
b = *pos++;
70+
v = (b & 0x7f);
71+
if (shift == 28 && v > 3) {
72+
return false;
73+
}
74+
total += v << shift;
75+
if ((b & 0x80) == 0) {
76+
ret = total;
77+
return true;
78+
}
79+
shift += 7;
80+
if (shift > 28) {
81+
return false;
82+
}
83+
}
84+
return false;
85+
}
86+
87+
#endif
88+
89+
} // namespace
90+
5691
namespace proxy_wasm {
5792

5893
bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &message) {
@@ -61,50 +96,200 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
6196

6297
/*
6398
* Ed25519 signature generated using https://github.com/wasm-signatures/wasmsign2
99+
* Format specification: https://github.com/WebAssembly/tool-conventions/blob/main/Signatures.md
64100
*/
65101

66-
std::string_view payload;
67-
if (!BytecodeUtil::getCustomSection(bytecode, "signature", payload)) {
102+
std::string_view signature_payload;
103+
if (!BytecodeUtil::getCustomSection(bytecode, "signature", signature_payload)) {
68104
message = "Failed to parse corrupted Wasm module";
69105
return false;
70106
}
71107

72-
if (payload.empty()) {
108+
if (signature_payload.empty()) {
73109
message = "Custom Section \"signature\" not found";
74110
return false;
75111
}
76112

77-
if (bytecode.data() + bytecode.size() != payload.data() + payload.size()) {
78-
message = "Custom Section \"signature\" not at the end of Wasm module";
113+
// In wasmsign2 v2, the signature section must be FIRST, not last
114+
// Check if the signature section is at the beginning (after the WASM header)
115+
// The signature section should start at bytecode offset 8 (after magic + version)
116+
const char *sig_section_start = bytecode.data() + 8;
117+
118+
// Verify the signature section is at the beginning by checking if its custom section (type 0)
119+
if (sig_section_start >= bytecode.data() + bytecode.size() || *sig_section_start != 0) {
120+
message = "Custom Section \"signature\" not at the beginning of Wasm module";
79121
return false;
80122
}
81123

82-
if (payload.size() != 68) {
83-
message = "Signature has a wrong size (want: 68, is: " + std::to_string(payload.size()) + ")";
124+
// Parse wasmsign2 v2 format:
125+
// spec_version (byte), content_type (byte), hash_fn (byte),
126+
// signed_hashes_count (varint), then for each signed_hash:
127+
// hashes_count (varint), hashes (32 bytes each for SHA-256),
128+
// signature_count (varint), then for each signature:
129+
// key_id_len (varint), key_id (bytes), signature_id (byte),
130+
// signature_len (varint), signature (bytes)
131+
132+
const char *pos = signature_payload.data();
133+
const char *end = signature_payload.data() + signature_payload.size();
134+
135+
if (pos + 3 > end) {
136+
message = "Signature payload too short";
84137
return false;
85138
}
86139

87-
uint32_t alg_id;
88-
std::memcpy(&alg_id, payload.data(), sizeof(uint32_t));
89-
alg_id = wasmtoh(alg_id, true);
140+
uint8_t spec_version = static_cast<uint8_t>(*pos++);
141+
uint8_t content_type = static_cast<uint8_t>(*pos++);
142+
uint8_t hash_fn = static_cast<uint8_t>(*pos++);
90143

91-
if (alg_id != 2) {
92-
message = "Signature has a wrong alg_id (want: 2, is: " + std::to_string(alg_id) + ")";
144+
if (spec_version != 0x01) {
145+
message = "Unsupported signature spec version: " + std::to_string(spec_version);
93146
return false;
94147
}
95148

96-
const auto *signature = reinterpret_cast<const uint8_t *>(payload.data()) + sizeof(uint32_t);
149+
if (content_type != 0x01) {
150+
message = "Unsupported content type: " + std::to_string(content_type);
151+
return false;
152+
}
153+
154+
if (hash_fn != 0x01) {
155+
message = "Unsupported hash function: " + std::to_string(hash_fn) + " (only SHA-256 supported)";
156+
return false;
157+
}
97158

98-
SHA512_CTX ctx;
99-
SHA512_Init(&ctx);
100-
SHA512_Update(&ctx, "WasmSignature", sizeof("WasmSignature") - 1);
101-
const uint32_t ad_len = 0;
102-
SHA512_Update(&ctx, &ad_len, sizeof(uint32_t));
103-
const size_t section_len = 3 + sizeof("signature") - 1 + 68;
104-
SHA512_Update(&ctx, bytecode.data(), bytecode.size() - section_len);
105-
uint8_t hash[SHA512_DIGEST_LENGTH];
106-
SHA512_Final(hash, &ctx);
159+
uint32_t signed_hashes_count = 0;
160+
if (!parseVarint(pos, end, signed_hashes_count) || signed_hashes_count == 0) {
161+
message = "Invalid or zero signed_hashes_count";
162+
return false;
163+
}
164+
165+
// We only verify the first signed_hash set
166+
uint32_t hashes_count = 0;
167+
if (!parseVarint(pos, end, hashes_count) || hashes_count == 0) {
168+
message = "Invalid or zero hashes_count";
169+
return false;
170+
}
171+
172+
// For simplicity, we only support single-hash verification (no partial verification)
173+
if (hashes_count != 1) {
174+
message = "Only single-hash signatures are supported (found " + std::to_string(hashes_count) + " hashes)";
175+
return false;
176+
}
177+
178+
if (pos + 32 > end) {
179+
message = "Signature payload too short for hash";
180+
return false;
181+
}
182+
183+
// Extract the expected hash
184+
const uint8_t *expected_hash = reinterpret_cast<const uint8_t *>(pos);
185+
pos += 32;
186+
187+
uint32_t signatures_count = 0;
188+
if (!parseVarint(pos, end, signatures_count) || signatures_count == 0) {
189+
message = "Invalid or zero signatures_count";
190+
return false;
191+
}
192+
193+
// We only verify the first signature
194+
uint32_t key_id_len = 0;
195+
if (!parseVarint(pos, end, key_id_len)) {
196+
message = "Invalid key_id_len";
197+
return false;
198+
}
199+
200+
// Skip the key_id
201+
if (key_id_len > 0) {
202+
if (pos + key_id_len > end) {
203+
message = "Signature payload too short for key_id";
204+
return false;
205+
}
206+
pos += key_id_len;
207+
}
208+
209+
if (pos + 1 > end) {
210+
message = "Signature payload too short for signature_id";
211+
return false;
212+
}
213+
214+
uint8_t signature_id = static_cast<uint8_t>(*pos++);
215+
if (signature_id != 0x01) {
216+
message = "Unsupported signature algorithm: " + std::to_string(signature_id) + " (only Ed25519 supported)";
217+
return false;
218+
}
219+
220+
uint32_t signature_len = 0;
221+
if (!parseVarint(pos, end, signature_len)) {
222+
message = "Invalid signature_len";
223+
return false;
224+
}
225+
226+
if (signature_len != 64) {
227+
message = "Invalid Ed25519 signature length: " + std::to_string(signature_len) + " (expected 64)";
228+
return false;
229+
}
230+
231+
if (pos + signature_len > end) {
232+
message = "Signature payload too short for signature data";
233+
return false;
234+
}
235+
236+
const uint8_t *signature = reinterpret_cast<const uint8_t *>(pos);
237+
238+
// Compute the hash of the module content (everything after the signature section)
239+
// We need to find where the signature section ends in the original bytecode
240+
// The signature section structure in WASM is:
241+
// section_type (1 byte) + section_len (varint) + name_len (varint) + name + payload
242+
243+
// Find the end of the signature section
244+
const char *sig_section_pos = sig_section_start;
245+
sig_section_pos++; // skip section type (0)
246+
247+
uint32_t section_len = 0;
248+
if (!parseVarint(sig_section_pos, bytecode.data() + bytecode.size(), section_len)) {
249+
message = "Failed to parse signature section length";
250+
return false;
251+
}
252+
253+
uint32_t name_len = 0;
254+
const char *section_data_start = sig_section_pos;
255+
if (!parseVarint(sig_section_pos, bytecode.data() + bytecode.size(), name_len)) {
256+
message = "Failed to parse signature section name length";
257+
return false;
258+
}
259+
260+
// The content to hash starts after the signature section
261+
const char *content_start = section_data_start + section_len;
262+
size_t content_len = bytecode.size() - (content_start - bytecode.data());
263+
264+
// Compute SHA-256 hash of the content
265+
EVP_MD_CTX *hash_ctx = EVP_MD_CTX_new();
266+
if (hash_ctx == nullptr) {
267+
message = "Failed to allocate memory for hash context";
268+
return false;
269+
}
270+
271+
uint8_t computed_hash[32]; // SHA-256 produces 32 bytes
272+
unsigned int hash_len = 0;
273+
274+
bool hash_ok =
275+
(EVP_DigestInit_ex(hash_ctx, EVP_sha256(), nullptr) != 0) &&
276+
(EVP_DigestUpdate(hash_ctx, content_start, content_len) != 0) &&
277+
(EVP_DigestFinal_ex(hash_ctx, computed_hash, &hash_len) != 0);
278+
279+
EVP_MD_CTX_free(hash_ctx);
280+
281+
if (!hash_ok || hash_len != 32) {
282+
message = "Failed to compute SHA-256 hash";
283+
return false;
284+
}
285+
286+
// Verify the computed hash matches the expected hash
287+
if (std::memcmp(computed_hash, expected_hash, 32) != 0) {
288+
message = "Hash mismatch";
289+
return false;
290+
}
107291

292+
// Verify the signature
108293
static const auto ed25519_pubkey = hex2pubkey<32>(PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY);
109294

110295
EVP_PKEY *pubkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, ed25519_pubkey.data(),
@@ -123,7 +308,7 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
123308

124309
bool ok =
125310
(EVP_DigestVerifyInit(mdctx, nullptr, nullptr, nullptr, pubkey) != 0) &&
126-
(EVP_DigestVerify(mdctx, signature, 64 /* ED25519_SIGNATURE_LEN */, hash, sizeof(hash)) != 0);
311+
(EVP_DigestVerify(mdctx, signature, 64 /* ED25519_SIGNATURE_LEN */, expected_hash, 32) != 0);
127312

128313
EVP_MD_CTX_free(mdctx);
129314
EVP_PKEY_free(pubkey);

0 commit comments

Comments
 (0)