Let's explore code signing with WebAssembly
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 Identifier=com.microsoft.VSCode 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 CMSDigest=77e7708d72bda737cd8071180d4b397d24355ca38daca7f0186de6f7350f1805 CMSDigestType=2 CDHash=8ac7d94ab14fa31d50d60384648f1220ab07ba85 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 TeamIdentifier=UBF8T346G9 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).
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.
In order to validate a signed binary we need to obtain 3 things
- The certificate attached to the signed binary, validated by the signing certificate authority
- The signature of the binary which contains the hash which we can use to verify the integrity
- 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 is the version number
01 00 00 00. The next couple of bytes defines 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
We can then use our private key to generate a public key
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
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.name_mut().push_str("signature"); 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
module.sections_mut().push(Section::Custom(custom)); 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 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
- 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].
- 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].
- 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].
- 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].
- 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].
- 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].