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