Let's explore code signing with WebAssembly

If you're just looking to sign your binaries, I recommend using a tool that has been validated and tested by the community. This blog post aims to cover the process of code signing using WASM as a medium and may not cover every use case. A few I found while writing this were wasm-sign and wasmsign.

Code signing is the practice of embedding a digital signature in software that allows a user to verify its integrity as well as its author. This is a critical security measure in software today as it provides a mechanism for operating systems and tools to validate the authenticity of software their users are attempting to use before installation or execution.

We can see an example of this by using the codesign utility to view the signing information of macOS applications. Here's the signing certificate data of my Visual Studio Code installation.

$ codesign -d -vvv /Applications/Visual\ Studio\ Code.app

Executable=/Applications/Visual Studio Code.app/Contents/MacOS/Electron
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20500 size=3040 flags=0x10000(runtime) hashes=86+5 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha1=0f20c43c30cd255f755f49ee78552f64d8ea303e
CandidateCDHashFull sha1=0f20c43c30cd255f755f49ee78552f64d8ea303e
CandidateCDHash sha256=8ac7d94ab14fa31d50d60384648f1220ab07ba85
CandidateCDHashFull sha256=8ac7d94ab14fa31d50d60384648f1220ab07ba8543cadd357c812e2fc1f4b8dd
Hash choices=sha1,sha256
Signature size=9061
Authority=Developer ID Application: Microsoft Corporation (UBF8T346G9)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Feb 9, 2022 at 5:45:51 PM
Info.plist entries=33
Runtime Version=11.1.0
Sealed Resources version=2 rules=13 files=1015
Internal requirements count=1 size=180

What is interesting is that the signing data is embedded into the binary which allows the software and operating system to validate the authenticity of the certificates. This process is all backed by Public Key Infrastructure (PKI).

graph LR subgraph Certificate Authority auth(Certificate Signing Request)-->signedCert(Signed Certificate) end subgraph Generate Binary Signature hash(Hash Binary)-->encrypt(Encrypt Hash) end prikey(Private Key) --> encrypt pubkey(Public Key) pubkey-->auth signedCert-->embed code(Binary)-->hash encrypt-->embed(Embed Signing Data) embed-->signed(Signed Binary)
Sample Signing Process

One possible process of code signing starts by generating a public and private key pair. The private key is used in the signing process while the public key is used for validation. A signature is generated from the hash of the binary and a certificate is requested from a trusted certificate authority. The hash and certificate become the signature information that is finally embedded into the binary.

graph LR subgraph Decrypt Hash encrypt(Encrypted Hash) --> decrypt(Decrypt) decrypt --> hash(Hash) end subgraph Certificate Authority verify(Verify Cerfiticate) end pkey(Public Key) signed(Signed Binary) --> encrypt signed --> unsigned(Unsigned Binary) unsigned --> hashUnsigned(Unsigned Hash) signed --> cert(Certificate) cert(Certificate) --> verify verify --> pkey pkey --> decrypt hash --> validate hashUnsigned --> validate
Sample Verification Process

To validate a signed binary we need to obtain 3 things

  1. The certificate attached to the signed binary, validated by the signing certificate authority
  2. The signature of the binary which contains the hash which we can use to verify the integrity
  3. The unsigned binary which we can validate

Fortunately, all of these things can be obtained from the signed binary. Since the process of embedding the signing data was additive, removing that data should give us the unsigned binary. Binary file formats are usually divided up into sections that allow us to do this in a deterministic way. The WASM format defines 12 types of sections that may appear in a binary which all have a given section ID. For our purpose, we can use custom sections.

Custom Sections have a section ID of 0. They are intended to hold metadata and debugging information relevant to tooling. This makes them perfect for holding onto our signature information.

Exploring a WASM module

Let's start by breaking down a simple wasm binary. This will allow us to see how sections are laid out.

$ cat sample.wat

(module (func $helloWasm))

Serializing this into the binary format gives us this module yields the following output

$ wat2wasm sample.wat
$ xxd -c 4 -g 1 sample.wasm

00000000: 00 61 73 6d 01 00 00 00  .asm....
00000008: 01 04 01 60 00 00 03 02  ...`....
00000010: 01 00 0a 04 01 02 00 0b  ........

To break this down, the first four bytes of our binary is the preamble 00 61 73 6d which identifies the file as a WASM module. The next 4 bytes represent the version number 01 00 00 00. The next couple of bytes define the type section in our module. The section starts with 01 as its section ID and 04 describes the length of the section data.

01 04 01 60 00 00
|  |  |  |  |  β”” Number of return values
|  |  |  |  β”” Number of parameters
|  |  |  β”” Function type declaration
|  |  β”” Number of types declared
|  β”” Section data length 
β”” Section Id (Types section)

Next, we have the function section.

03 02 01 00
|  |  |  β”” Type section index
|  |  β”” Number of declared functions
|  β”” Section data length 
β”” Section Id (Functions section)

And finally the code section

0a 04 01 02 00 0b
|  |  |  |  |  β”” End of code block
|  |  |  |  β”” Number of declared local variables
|  |  |  β”” Length of next code block
|  |  β”” Number of declared code blocks
|  β”” Section data length 
β”” Section Id (Code section)

Looking at the pattern from the other sections we can guess that our serialized custom section should begin with the section ID 00 followed by the length of data in the section and the section data which should contain our signature information.

Generating signing keys

To generate our public and private key pair, we can use openssl.

$ openssl genpkey -algorithm ed25519 -outform PEM -out private.pem
$ cat private.pem


This is for demonstration purposes. Do not use this!

We can then use our private key to generate a public key

$ openssl pkey -in private.pem -outform PEM -pubout -out public.pem
$ cat public.pem

-----END PUBLIC KEY-----

This is for demonstration purposes. Do not use this!

For this demo, we can skip over the certificate creation and just sign our binary using our public/private key pair.

Signing our module

We can deserialize our WASM module using the excellent parity_wasm crate.

let module_bytes = include_bytes!("../sample.wasm");
let mut module = parity_wasm::deserialize_buffer::<Module>(module_bytes).unwrap();

Next using the rust_openssl crate we can parse our generated private key

let private_key_bytes = include_bytes!("../keys/private.pem");
let private_key = PKey::private_key_from_pem(private_key_bytes).unwrap();

At this point, we're ready to compute the cryptographic signature for our module using our private key.

let mut signer = Signer::new_without_digest(&private_key).unwrap();
let mut signature = signer.sign_oneshot_to_vec(module_bytes).unwrap();

Now we need to generate the custom section with our signature module for our module. Fortunately parity_wasm has a handy set of APIs for this.

let mut custom = CustomSection::default();
custom.payload_mut().push(signature.len() as u8); 
custom.payload_mut().append(&mut signature);

Finally, we can embed that into our module and generate the signed binary

parity_wasm::serialize_to_file("sample-signed.wasm", module).unwrap();

Inspecting the binary data for our module, we can see that our custom section has been inserted after the code section.

$ xxd -c 8 -g 1 sample-signed.wasm

00000000: 00 61 73 6d 01 00 00 00  .asm....
00000008: 01 04 01 60 00 00 03 02  ...`....
00000010: 01 00 0a 04 01 02 00 0b  ........
00000018: 00 4b 09 73 69 67 6e 61  .K.signa
00000020: 74 75 72 65 40 52 92 68  ture@R.h
00000028: 54 e3 76 a0 47 4e 55 3d  T.v.GNU=
00000030: 68 e9 c3 25 52 22 41 d8  h..%R"A.
00000038: cf 4e f3 cf 81 4d 5a 80  .N...MZ.
00000040: a1 1a 80 56 60 47 af 85  ...V`G..
00000048: 11 03 6a 6f c1 f6 39 c9  ..jo..9.
00000050: ff 74 80 f9 ac 9c 5f 33  .t...._3
00000058: 9d 14 27 ca 7a 01 fc 29  ..'.z..)
00000060: 17 50 88 a8 02           .P...

The section starts with the section header which has 00 as the custom ID and 4b as the size of the section. Next, we have the name of the section which starts with 09 the size of the string followed by 73 69 67 6e 61 74 75 72 65 which corresponds to signature. Finally, we have our constructed payload which as we expect, starts with the size of the signature 40, followed by the signature body.

Verifying our module

To go through the verification process, we can start by deserializing our signed binary as well as our public key.

let mut module = parity_wasm::deserialize_file("sample-signed.wasm").unwrap();
let public_key_bytes = include_bytes!("../keys/public.pem");
let public_key = PKey::public_key_from_pem(public_key_bytes).unwrap();

Next, we want to extract our signature and remove it from the deserialized module. This is because we want to verify the module in the exact state that it was in when we generated the signature. Fortunately, the clear_custom_section function does exactly this for us.

let mut signature_section = module.clear_custom_section("signature").unwrap();

Now we can grab the payload from the section and use the signature to verify our module.

let payload = signature_section.payload_mut();
let size = payload[0] as usize;
let signature = &signature_section.payload_mut()[1..=size];

let mut verifier = Verifier::new_without_digest(&public_key).unwrap();

let mut buf = vec![];
module.serialize(&mut buf).unwrap();

assert!(verifier.verify_oneshot(&signature, &buf).unwrap());

We can test that this process works by using a hex editor to modify the binary. Changing any part of it including the existing sections and signature results in a failed verification. Let's edit one byte in the binary to see this in action.

$ xxd -c 8 -g 1 sample-signed.wasm

00000000: 00 61 73 6d 01 00 00 00  .asm....
00000008: 01 04 01 60 00 00 03 02  ...`....
00000010: 01 00 0a 04 01 02 00 0b  ........
00000018: 00 4b 09 73 69 67 6e 61  .K.signa
00000020: 74 75 72 65 39 52 92 68  ture9R.h
00000028: 54 e3 76 a0 47 4e 55 3d  T.v.GNU=
00000030: 68 e9 c3 25 52 22 41 d8  h..%R"A.
00000038: cf 4e f3 cf 81 4d 5a 80  .N...MZ.
00000040: a1 1a 80 56 60 47 af 85  ...V`G..
00000048: 11 03 6a 6f c1 f6 39 c9  ..jo..9.
00000050: ff 74 80 f9 ac 9c 5f 33  .t...._3
00000058: 9d 14 27 ca 7a 01 fc 29  ..'.z..)
00000060: 17 50 88 a8 02           .P...

$ cargo run
thread 'main' panicked at 'assertion failed: verifier.verify_oneshot(&signature, &buf).unwrap()', src/main.rs:50:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  1. www.samlogic.net. (n.d.). What is Code Signing / Digital Signature / Digital Certificate? (Q&A). [online] Available at: https://www.samlogic.net/articles/code-signing.htm [Accessed 17 Feb. 2022].
  2. β€Œwww.samlogic.net. (n.d.). Extended Validation (EV) Code Signing in Windows 8 / Windows 10. [online] Available at: https://www.samlogic.net/articles/windows-8-windows-10-extended-validation-code-signing.htm [Accessed 17 Feb. 2022].
  3. webassembly.github.io. (n.d.). Modules β€” WebAssembly 1.1 (Draft 2022-02-15). [online] Available at: https://webassembly.github.io/spec/core/binary/modules.html#binary-customsec [Accessed 17 Feb. 2022].
  4. Brazhnikov, K. (2019). Using digital signatures for data integrity checking in Linux. [online] Embedded.com. Available at: https://www.embedded.com/using-digital-signatures-for-data-integrity-checking-in-linux/ [Accessed 17 Feb. 2022].
  5. webassembly.github.io. (n.d.). Custom Sections β€” WebAssembly 1.1 (Draft 2022-02-15). [online] Available at: https://webassembly.github.io/spec/core/appendix/custom.html [Accessed 17 Feb. 2022].
  6. Rietta, F. (2012). Generate OpenSSL RSA Key Pair from the Command Line. [online] Rietta.com. Available at: https://rietta.com/blog/openssl-generating-rsa-key-from-command/ [Accessed 17 Feb. 2022].

Subscribe to Another Dev's Two Cents

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.