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,8 +98,18 @@ 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
104115 std::string_view signature_payload;
@@ -125,8 +136,8 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
125136
126137 // Parse wasmsign2 0.2.6 format:
127138 // 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),
139+ // signed_hashes_count (varint), then for each SignedHash:
140+ // signed_hash_len (varint), hashes_count (varint), hashes (32 bytes each for SHA-256),
130141 // signatures_count (varint), then for each signature:
131142 // key_id_len (varint), key_id (bytes), signature_id (byte),
132143 // signature_len (varint), signature (bytes)
@@ -158,8 +169,28 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
158169 return false ;
159170 }
160171
161- // In wasmsign2 0.2.6, there is no signed_hashes_count varint
162- // The format goes directly to the SignedHash structure
172+ // Parse signed_hashes_count
173+ uint32_t signed_hashes_count = 0 ;
174+ if (!parseVarint (pos, end, signed_hashes_count) || signed_hashes_count == 0 ) {
175+ message = " Invalid or zero signed_hashes_count" ;
176+ return false ;
177+ }
178+
179+ // For simplicity, we only support single SignedHash verification
180+ if (signed_hashes_count != 1 ) {
181+ message = " Only single SignedHash is supported (found " + std::to_string (signed_hashes_count) +
182+ " SignedHash entries)" ;
183+ return false ;
184+ }
185+
186+ // Parse signed_hash_len (the length of the SignedHash structure)
187+ uint32_t signed_hash_len = 0 ;
188+ if (!parseVarint (pos, end, signed_hash_len)) {
189+ message = " Invalid signed_hash_len" ;
190+ return false ;
191+ }
192+
193+ // Now parse the SignedHash structure
163194 uint32_t hashes_count = 0 ;
164195 if (!parseVarint (pos, end, hashes_count) || hashes_count == 0 ) {
165196 message = " Invalid or zero hashes_count" ;
@@ -189,6 +220,13 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
189220 }
190221
191222 // We only verify the first signature
223+ // wasmsign2 0.2.6 includes a signature_bytes_len field before each signature
224+ uint32_t signature_bytes_len = 0 ;
225+ if (!parseVarint (pos, end, signature_bytes_len)) {
226+ message = " Invalid signature_bytes_len" ;
227+ return false ;
228+ }
229+
192230 uint32_t key_id_len = 0 ;
193231 if (!parseVarint (pos, end, key_id_len)) {
194232 message = " Invalid key_id_len" ;
@@ -289,6 +327,21 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
289327 }
290328
291329 // Verify the signature
330+ // wasmsign2 signs a message that includes domain separation and metadata:
331+ // "wasmsig" + spec_version + content_type + hash_fn + hash
332+ // See:
333+ // https://github.com/wasm-signatures/wasmsign2/blob/0.2.6/src/lib/src/signature/multi.rs#L268-L278
334+ const char *domain = " wasmsig" ;
335+ size_t domain_len = 7 ;
336+ size_t msg_len = domain_len + 3 + 32 ; // domain + 3 bytes (spec/content/hash) + 32 bytes (hash)
337+ auto signature_msg = std::make_unique<uint8_t []>(msg_len);
338+
339+ std::memcpy (signature_msg.get (), domain, domain_len);
340+ signature_msg[domain_len] = spec_version;
341+ signature_msg[domain_len + 1 ] = content_type;
342+ signature_msg[domain_len + 2 ] = hash_fn;
343+ std::memcpy (signature_msg.get () + domain_len + 3 , expected_hash, 32 );
344+
292345 static const auto ed25519_pubkey = hex2pubkey<32 >(PROXY_WASM_VERIFY_WITH_ED25519_PUBKEY);
293346
294347 EVP_PKEY *pubkey = EVP_PKEY_new_raw_public_key (EVP_PKEY_ED25519, nullptr , ed25519_pubkey.data (),
@@ -305,9 +358,9 @@ bool SignatureUtil::verifySignature(std::string_view bytecode, std::string &mess
305358 return false ;
306359 }
307360
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 );
361+ bool ok = ( EVP_DigestVerifyInit (mdctx, nullptr , nullptr , nullptr , pubkey) != 0 ) &&
362+ ( EVP_DigestVerify (mdctx, signature, 64 /* ED25519_SIGNATURE_LEN */ , signature_msg. get (),
363+ msg_len ) != 0 );
311364
312365 EVP_MD_CTX_free (mdctx);
313366 EVP_PKEY_free (pubkey);
0 commit comments