Skip to content

Commit 41bccb2

Browse files
committed
Fix wasmsign2 0.2.6 signature format parser and verification
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
1 parent 3e809e9 commit 41bccb2

File tree

1 file changed

+63
-9
lines changed

1 file changed

+63
-9
lines changed

src/signature_util.cc

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <array>
1818
#include <cstring>
19+
#include <memory>
1920

2021
#ifdef PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY
2122
#include <openssl/evp.h>
@@ -97,10 +98,21 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
9798
/*
9899
* Ed25519 signature generated using https://github.com/wasm-signatures/wasmsign2 0.2.6
99100
* Format specification: https://github.com/WebAssembly/tool-conventions/blob/main/Signatures.md
100-
* Note: wasmsign2 0.2.6 omits the signed_hashes_count wrapper and directly embeds a single
101-
* SignedHash
101+
*
102+
* Format notes:
103+
* - wasmsign2 0.2.6 DOES include signed_hashes_count (previously thought to omit it)
104+
* - wasmsign2 0.2.6 includes length fields not in the spec:
105+
* - signed_hash_len: length of each SignedHash structure (using varint::put_slice)
106+
* - signature_bytes_len: length of each signature's data (using varint::put_slice)
107+
*
108+
* Signature verification:
109+
* - The signature is over a message with domain separation, NOT just the hash
110+
* - Message format: "wasmsig" + spec_version + content_type + hash_fn + hash
111+
* - See:
112+
* https://github.com/wasm-signatures/wasmsign2/blob/0.2.6/src/lib/src/signature/multi.rs#L268-L278
102113
*/
103114

115+
104116
std::string_view signature_payload;
105117
if (!BytecodeUtil::getCustomSection(bytecode, "signature", signature_payload)) {
106118
message = "Failed to parse corrupted Wasm module";
@@ -125,8 +137,8 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
125137

126138
// Parse wasmsign2 0.2.6 format:
127139
// spec_version (byte), content_type (byte), hash_fn (byte),
128-
// then directly the SignedHash structure:
129-
// hashes_count (varint), hashes (32 bytes each for SHA-256),
140+
// signed_hashes_count (varint), then for each SignedHash:
141+
// signed_hash_len (varint), hashes_count (varint), hashes (32 bytes each for SHA-256),
130142
// signatures_count (varint), then for each signature:
131143
// key_id_len (varint), key_id (bytes), signature_id (byte),
132144
// signature_len (varint), signature (bytes)
@@ -158,8 +170,28 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
158170
return false;
159171
}
160172

161-
// In wasmsign2 0.2.6, there is no signed_hashes_count varint
162-
// The format goes directly to the SignedHash structure
173+
// Parse signed_hashes_count
174+
uint32_t signed_hashes_count = 0;
175+
if (!parseVarint(pos, end, signed_hashes_count) || signed_hashes_count == 0) {
176+
message = "Invalid or zero signed_hashes_count";
177+
return false;
178+
}
179+
180+
// For simplicity, we only support single SignedHash verification
181+
if (signed_hashes_count != 1) {
182+
message = "Only single SignedHash is supported (found " + std::to_string(signed_hashes_count) +
183+
" SignedHash entries)";
184+
return false;
185+
}
186+
187+
// Parse signed_hash_len (the length of the SignedHash structure)
188+
uint32_t signed_hash_len = 0;
189+
if (!parseVarint(pos, end, signed_hash_len)) {
190+
message = "Invalid signed_hash_len";
191+
return false;
192+
}
193+
194+
// Now parse the SignedHash structure
163195
uint32_t hashes_count = 0;
164196
if (!parseVarint(pos, end, hashes_count) || hashes_count == 0) {
165197
message = "Invalid or zero hashes_count";
@@ -189,6 +221,13 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
189221
}
190222

191223
// We only verify the first signature
224+
// wasmsign2 0.2.6 includes a signature_bytes_len field before each signature
225+
uint32_t signature_bytes_len = 0;
226+
if (!parseVarint(pos, end, signature_bytes_len)) {
227+
message = "Invalid signature_bytes_len";
228+
return false;
229+
}
230+
192231
uint32_t key_id_len = 0;
193232
if (!parseVarint(pos, end, key_id_len)) {
194233
message = "Invalid key_id_len";
@@ -289,6 +328,21 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
289328
}
290329

291330
// Verify the signature
331+
// wasmsign2 signs a message that includes domain separation and metadata:
332+
// "wasmsig" + spec_version + content_type + hash_fn + hash
333+
// See:
334+
// https://github.com/wasm-signatures/wasmsign2/blob/0.2.6/src/lib/src/signature/multi.rs#L268-L278
335+
const char *domain = "wasmsig";
336+
size_t domain_len = 7;
337+
size_t msg_len = domain_len + 3 + 32; // domain + 3 bytes (spec/content/hash) + 32 bytes (hash)
338+
auto signature_msg = std::make_unique<uint8_t[]>(msg_len);
339+
340+
std::memcpy(signature_msg.get(), domain, domain_len);
341+
signature_msg[domain_len] = spec_version;
342+
signature_msg[domain_len + 1] = content_type;
343+
signature_msg[domain_len + 2] = hash_fn;
344+
std::memcpy(signature_msg.get() + domain_len + 3, expected_hash, 32);
345+
292346
static const auto ed25519_pubkey = hex2pubkey<32>(PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY);
293347

294348
EVP_PKEY *pubkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, ed25519_pubkey.data(),
@@ -305,9 +359,9 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
305359
return false;
306360
}
307361

308-
bool ok =
309-
(EVP_DigestVerifyInit(mdctx, nullptr, nullptr, nullptr, pubkey) != 0) &&
310-
(EVP_DigestVerify(mdctx, signature, 64 /* ED25519_SIGNATURE_LEN */, expected_hash, 32) != 0);
362+
bool ok = (EVP_DigestVerifyInit(mdctx, nullptr, nullptr, nullptr, pubkey) != 0) &&
363+
(EVP_DigestVerify(mdctx, signature, 64 /* ED25519_SIGNATURE_LEN */, signature_msg.get(),
364+
msg_len) != 0);
311365

312366
EVP_MD_CTX_free(mdctx);
313367
EVP_PKEY_free(pubkey);

0 commit comments

Comments
 (0)