Signing and Encrypting WebAssembly Modules

In the previous section I talked about taking a paranoia approach to defense against external client tampering with WebAssembly modules. The consequences for module tampering can be far, far worse if you’re distributing and using modules within your data center, cloud, or enterprise.

Let’s say you’ve adopted WebAssembly and you’re running some form of FaaS (Functions as a Service) infrastructure. Everything is great and you’re loving the ease and portability you get, and you love the nearly crash-proof nature of running an interpreter on WebAssembly modules.

What if someone were to manage to slip a bad WebAssembly module into the environment. If your host protocol allows those modules to do things that could corrupt data, download data, or make outbound network calls, you could have all sorts of malicious actors controlling your system. What we need is some way to ensure the digital provenance of WebAssembly modules—to ensure that they haven’t been tampered with and that they were created by only authorized personnel or processes.

We can use cryptographic signatures to handle this problem. A simple hash of an array of bytes will allow us to tell if a file has been tampered with. We see this pattern used all the time on public downloads when the official sites also publish a checksum so you can verify that the file is legitimate.

A cryptographic signature takes that concept one step further. It produces a value that will not only change if the source file bytes change, but it is entirely dependent on some asymmetric, private key. In other words, you have to be in possession of a very carefully guarded secret in order to produce a valid signature.

Before your host code—whether it’s a browser or a Rust console application or an OpenFaaS container—executes any code in a WebAssembly module, it should verify the signature that accompanies the file. If the verification fails, then the party who signed that file was not in posession of the right key and you should not only reject the file, but probably create some form of alert to trigger an investigation.

There are quite a few techniques to sign an array of bytes, but I am particularly partial to using Ed25519[61] keys. They’re fast, simple, easy to use, and generally less vulnerable to certain types of attacks that have caused havoc in the past. There are more mathematical reasons to like this signature algorithm that I won’t go into here because they make my head hurt.

With the EdDSA signature algorithm, you use a single private key called the seed key to sign an arbitrary binary payload. You can then use public keys generated from the seed to verify that signature. The availability of many public keys for a single binary key often creates some very powerful and elegant security solutions.

In this small Rust example (also included with the book code downloads), I illustrate using the signatory and signatory-dalek crates to sign and verify the contents of a WebAssembly file passed as the first argument on the command line:

 use​ ​std​::env;
 use​ ​std​::​fs​::File;
 use​ ​std​::​io​::​prelude​::*;
 
 use​ ​signatory​::{ed25519, Encode};
 use​ ​signatory_dalek​::{Ed25519Signer, Ed25519Verifier};
 
 fn​ ​main​() ​->​ Result<(), ​std​::​io​::Error> {
 let​ args: Vec<String> = ​env​::​args​()​.collect​();
 let​ path = &args[1];
 
 let​ ​mut​ file = ​File​::​open​(path)?;
 let​ ​mut​ wasm_buf = ​Vec​::​new​();
 let​ _bytes_read = file​.read_to_end​(&​mut​ wasm_buf)?;
 let​ buf: &[u8] = &wasm_buf;
 
 let​ seed = ​ed25519​::​Seed​::​generate​();
 let​ base64 = ​signatory​::​encoding​::Base64 {};
  println!(
 "Generated a seed/private key: {}"​,
  seed​.encode_to_string​(&base64)​.unwrap​()
  );
 let​ signer = ​Ed25519Signer​::​from​(&seed);
 
 let​ sig = ​ed25519​::​sign​(&signer, buf)​.unwrap​();
  println!(
 "Signature for {} created: {}"​,
  path,
  sig​.encode_to_string​(&base64)​.unwrap​()
  );
 
 let​ pk = ​ed25519​::​public_key​(&signer)​.unwrap​();
 let​ verifier = ​Ed25519Verifier​::​from​(&pk);
 
 let​ verified = ​ed25519​::​verify​(&verifier, buf, &sig)​.is_ok​();
  println!(​"Signature verified: {}"​, verified);
 
 Ok​(())
 }

Here’s a sample run of the application:

 $ ​​target/debug/signer​​ ​​../checkers/checkers.wasm
 Generated a seed/private key: AOdRsk3TwSMO4chzRv4+EvXoGPuP25eMughoBRIGoKw=
 Signature for ../checkers/checkers.wasm created:
 wzphNwHoiPJvgooU8j36H0t8M4DjJ5Q0jyAbRPmIAmqZw+mBYG2dfCLgvCLd1b66
 qGncRQk0UfhcAbaO7oqvBw==
 Signature verified: true

The hard part with signing files isn’t really the encryption code—others have already done the hard work for us by writing re-usable libraries. The difficulty with signatures is ensuring that the signature always accompany the file being signed.

There are a number of ways to do this. You could put the signature at the beginning of the binary file and then parse that out as you read it, treating the remainder of the binary file as the regular WebAssembly module. This has an added advantage of ensuring that the signature will never be apart from a file. This works because we know that ed25519 signatures are always 64 bytes, so we can simply read a fixed number of bytes from the beginning of the file.

I’ve coded up a sample (not refactored for cleanliness) that illustrates how to read a WebAssembly file, generate a signature from it, embed that signature in an output file, and then verify that the signature from a file matches the WebAssembly bytes—all of the tasks you would need to know how to do in order to secure the provenance of your modules. I’m only using base 64 encoding so I can print the signature out to the console:

 use​ ​std​::env;
 use​ ​std​::​fs​::File;
 use​ ​std​::​io​::​prelude​::*;
 use​ ​std​::​io​::BufReader;
 
 use​ ​signatory​::{ed25519, Encode};
 use​ ​signatory_dalek​::{Ed25519Signer, Ed25519Verifier};
 
 fn​ ​main​() ​->​ ​std​::​io​::Result<()> {
 let​ args: Vec<String> = ​env​::​args​()​.collect​();
 
 let​ input = &args[1];
 let​ output = &args[2];
 
 let​ infile = ​load_file​(input)?;
 let​ inbytes: &[u8] = &infile;
 let​ base64 = ​signatory​::​encoding​::Base64 {};
 
 // This seed is a private key - store this in a safe place,
 // Obviously, you'll want to persist this somewhere instead of
 // just using it once in memory...
 let​ seed = ​ed25519​::​Seed​::​generate​();
 let​ signer = ​Ed25519Signer​::​from​(&seed);
 let​ sig = ​ed25519​::​sign​(&signer, inbytes)​.unwrap​();
 let​ sig_encoded = sig​.encode_to_string​(&base64)​.unwrap​();
 
 let​ pk = ​ed25519​::​public_key​(&signer)​.unwrap​();
 let​ verifier = ​Ed25519Verifier​::​from​(&pk);
 
  {
 let​ ​mut​ out_file = ​File​::​create​(output)?;
  out_file​.write​(sig​.as_bytes​())?;
  out_file​.write_all​(inbytes)?;
  }
 
  println!(
 "Embedded signature into {} - output {}​​ ​​-->{}"​,
  input, output, sig_encoded
  );
 let​ ​mut​ sigbuf = [0; 64];
 let​ ​mut​ wasmbuf = vec![];
  {
 let​ in_file = ​File​::​open​(output)?;
 let​ ​mut​ br = ​BufReader​::​new​(in_file);
  br​.read_exact​(&​mut​ sigbuf)?;
  br​.read_to_end​(&​mut​ wasmbuf)?;
  }
 
 let​ wasmbytes: &[u8] = &wasmbuf;
 let​ insig = ​ed25519​::​Signature​::​new​(sigbuf);
 
 let​ verify_res = ​ed25519​::​verify​(&verifier, wasmbytes, &insig)​.is_ok​();
  println!(​"Verification result on new bytes - {}"​, verify_res);
 
 Ok​(())
 }
 
 fn​ ​load_file​(path: &str) ​->​ ​std​::​io​::Result<Vec<u8>> {
 let​ ​mut​ file = ​File​::​open​(path)?;
 let​ ​mut​ buf = ​Vec​::​new​();
 let​ _bytes_read = file​.read_to_end​(&​mut​ buf)?;
 
 Ok​(buf)
 }

You could also use this same embedding technique to add additional metadata to your WebAssembly file that might come from your build process or developers. Or, you could wrap the WebAssembly module bytes in a protocol buffer[62] binary file with fields for both raw bytes and a signature, which would save you the trouble of manually reading and writing custom binary file formats as you could just re-use the protocol buffer libraries available.

Thank you!

How did you enjoy this book? Please let us know. Take a moment and email us at [email protected] with your feedback. Tell us your story and you could win free ebooks. Please use the subject line “Book Feedback.”

Ready for your next great Pragmatic Bookshelf book? Come on over to https://pragprog.com and use the coupon code BUYANOTHER2019 to save 30% on your next ebook.

Void where prohibited, restricted, or otherwise unwelcome. Do not use ebooks near water. If rash persists, see a doctor. Doesn’t apply to The Pragmatic Programmer ebook because it’s older than the Pragmatic Bookshelf itself. Side effects may include increased knowledge and skill, increased marketability, and deep satisfaction. Increase dosage regularly.

And thank you for your continued support,

Andy Hunt, Publisher

images/Coupon.png
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.110.155