Skip to content

Commit ac64548

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 ac64548

File tree

1 file changed

+62
-9
lines changed

1 file changed

+62
-9
lines changed

src/signature_util.cc

Lines changed: 62 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,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

Comments
 (0)