├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── logo.png └── src ├── cli └── main.rs ├── lib.rs └── lib ├── Cargo.toml └── src ├── error.rs ├── lib.rs ├── signature ├── hash.rs ├── keys.rs ├── matrix.rs ├── mod.rs ├── multi.rs ├── sig_sections.rs └── simple.rs ├── split.rs └── wasm_module ├── mod.rs └── varint.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *~ 3 | Cargo.lock 4 | *.wasm 5 | *.key 6 | *.key2 7 | *.sh 8 | signature 9 | src/lib/target 10 | wasmsign2 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmsign2-cli" 3 | version = "0.2.6" 4 | edition = "2021" 5 | authors = ["Frank Denis "] 6 | description = "CLI tool to sign and verify WebAssembly modules" 7 | readme = "README.md" 8 | keywords = ["webassembly", "modules", "signatures"] 9 | license = "MIT" 10 | homepage = "https://github.com/wasm-signatures/design" 11 | repository = "https://github.com/wasm-signatures/wasmsign2" 12 | categories = ["cryptography", "wasm"] 13 | 14 | [[bin]] 15 | name = "wasmsign2" 16 | path = "src/cli/main.rs" 17 | 18 | [dependencies] 19 | clap = { version = "3.2.25", default-features = false, features = [ 20 | "std", 21 | "cargo", 22 | "wrap_help", 23 | ] } 24 | env_logger = { version = "0.11.7", default-features = false, features = [ 25 | "humantime", 26 | ] } 27 | regex = "1.11.1" 28 | ureq = "3.0.8" 29 | uri_encode = "1.0.3" 30 | wasmsign2 = { version = "0.2.6", path = "src/lib" } 31 | 32 | [profile.release] 33 | codegen-units = 1 34 | incremental = false 35 | panic = "abort" 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub CI](https://github.com/wasm-signatures/wasmsign2/actions/workflows/rust.yml/badge.svg)](https://github.com/wasm-signatures/wasmsign2/actions/workflows/rust.yml) 2 | [![docs.rs](https://docs.rs/wasmsign2/badge.svg)](https://docs.rs/wasmsign2/) 3 | [![crates.io](https://img.shields.io/crates/v/wasmsign2.svg)](https://crates.io/crates/wasmsign2) 4 | 5 | # ![Wasmsign2](https://raw.github.com/wasm-signatures/wasmsign2/master/logo.png) 6 | 7 | A tool and library for signing WebAssembly modules. 8 | 9 | - [!Wasmsign2](#) 10 | - [WASM signatures](#wasm-signatures) 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Inspecting a module](#inspecting-a-module) 14 | - [Creating a key pair](#creating-a-key-pair) 15 | - [Signing a WebAssembly module](#signing-a-webassembly-module) 16 | - [Verifying a WebAssembly module](#verifying-a-webassembly-module) 17 | - [Verifying a WebAssembly module against multiple public keys](#verifying-a-webassembly-module-against-multiple-public-keys) 18 | - [Detaching a signature from a module](#detaching-a-signature-from-a-module) 19 | - [Embedding a detached signature in a module](#embedding-a-detached-signature-in-a-module) 20 | - [Partial verification](#partial-verification) 21 | - [OpenSSH keys support](#openssh-keys-support) 22 | - [GitHub integration](#github-integration) 23 | 24 | ## WASM signatures 25 | 26 | Unlike typical desktop and mobile applications, WebAssembly binaries do not embed any kind of digital signatures to verify that they come from a trusted source, and haven't been tampered with. 27 | 28 | Wasmsign2 takes an existing WebAssembly module, computes a signature for its content, and stores the signature in a custom section. 29 | 30 | The resulting binary remains a standalone, valid WebAssembly module, but signatures can be verified prior to executing it. 31 | 32 | Wasmsign2 is a proof of concept implementation of the [WebAssembly modules signatures](https://github.com/wasm-signatures/design) proposal. 33 | The file format is documented in the [WebAssembly tool conventions repository](https://github.com/WebAssembly/tool-conventions/blob/main/Signatures.md). 34 | 35 | The proposal, and this implementation, support domain-specific features such as: 36 | 37 | - The ability to have multiple signatures for a single module, with a compact representation 38 | - The ability to sign a module which was already signed with different keys 39 | - The ability to extend an existing module with additional custom sections, without invalidating existing signatures 40 | - The ability to verify multiple subsets of a module's sections with a single signature 41 | - The ability to turn an embedded signature into a detached one, and the other way round. 42 | 43 | ## Installation 44 | 45 | `wasmsign2` is a Rust crate, that can be used in other applications. 46 | 47 | See the [API documentation](https://docs.rs/wasmsign2) for details. 48 | 49 | It is also a CLI tool to perform common operations, whose usage is summarized below. 50 | 51 | The tool requires the Rust compiler, and can be installed with the following command: 52 | 53 | ```sh 54 | cargo install wasmsign2-cli 55 | ``` 56 | 57 | ## Usage 58 | 59 | ```text 60 | USAGE: 61 | wasmsign2 [FLAGS] [SUBCOMMAND] 62 | 63 | FLAGS: 64 | -d Print debugging information 65 | -h, --help Prints help information 66 | -V, --version Prints version information 67 | -v Verbose output 68 | 69 | SUBCOMMANDS: 70 | attach Embed a detach signature into a module 71 | detach Detach the signature from a module 72 | help Prints this message or the help of the given 73 | subcommand(s) 74 | keygen Generate a new key pair 75 | show Print the structure of a module 76 | sign Sign a module 77 | split Add cutting points to a module to enable partial 78 | verification 79 | verify Verify a module's signatures 80 | verify_matrix Batch verification against multiple public keys 81 | ``` 82 | 83 | ## Inspecting a module 84 | 85 | ```text 86 | wasmsign2 show --input-file 87 | ``` 88 | 89 | Example: 90 | 91 | ```sh 92 | wasmsign2 show -i z.wasm 93 | ``` 94 | 95 | The `-v` switch prints additional details about signature data. 96 | 97 | ## Creating a key pair 98 | 99 | ```text 100 | wasmsign2 keygen --public-key --secret-key 101 | 102 | -K, --public-key Public key file 103 | -k, --secret-key Secret key file 104 | ``` 105 | 106 | Example: 107 | 108 | ```sh 109 | wasmsign2 keygen --public-key key.public --secret-key key.secret 110 | ``` 111 | 112 | ## Signing a WebAssembly module 113 | 114 | ```text 115 | wasmsign2 sign [OPTIONS] --input-file --output-file --secret-key 116 | 117 | -i, --input-file Input file 118 | -o, --output-file Output file 119 | -K, --public-key Public key file 120 | -k, --secret-key Secret key file 121 | -S, --signature-file Signature file 122 | -Z, --ssh Parse OpenSSH keys 123 | ``` 124 | 125 | Example: 126 | 127 | ```sh 128 | wasmsign2 sign -i z.wasm -o z2.wasm -k secret.key 129 | ``` 130 | 131 | The public key is optional. It is only used to include a key identifier into the signature in order to speed up signature verification when a module includes multiple signatures made with different keys. 132 | 133 | By default, signatures are assumed to be embedded in modules. Detached signatures can be provided with the optional `--signature-file` argument. 134 | 135 | A module that was already signed can be signed with other keys, and can then be verified by any of the corresponding public keys. 136 | 137 | ## Verifying a WebAssembly module 138 | 139 | ```text 140 | wasmsign2 verify [FLAGS] [OPTIONS] --input-file 141 | 142 | -i, --input-file Input file 143 | -K, --public-key Public key file 144 | -S, --signature-file Signature file 145 | -s, --split Custom section names to be verified 146 | -G, --from-github GitHub account to retrieve public keys from 147 | -Z, --ssh Parse OpenSSH keys 148 | ``` 149 | 150 | Example: 151 | 152 | ```sh 153 | wasmsign2 verify -i z2.wasm -K public.key 154 | ``` 155 | 156 | The optional `-s/--split` parameter is documented in the "partial verification" section down below. 157 | 158 | ## Verifying a WebAssembly module against multiple public keys 159 | 160 | ```text 161 | wasmsign2 verify_matrix [FLAGS] [OPTIONS] --input-file 162 | 163 | -i, --input-file Input file 164 | -K, --public-keys ... Public key files 165 | -s, --split Custom section names to be verified 166 | -G, --from-github GitHub account to retrieve public keys from 167 | -Z, --ssh Parse OpenSSH keys 168 | ``` 169 | 170 | The command verifies a module's signatures against multiple keys simultaneously, and reports the set of public keys for which a valid signature was found. 171 | 172 | The optional `-s/--split` parameter is documented in the "partial verification" section down below. 173 | 174 | Example: 175 | 176 | ```sh 177 | wasmsign2 verify_matrix -i z2.wasm -K public.key -K public.key2 178 | ``` 179 | 180 | ## Detaching a signature from a module 181 | 182 | ```text 183 | wasmsign2 detach --input-file --output-file --signature-file 184 | 185 | -i, --input-file Input file 186 | -o, --output-file Output file 187 | -S, --signature-file Signature file 188 | ``` 189 | 190 | The command extracts and removes the signature from a module, and stores it in a distinct file. 191 | 192 | Example: 193 | 194 | ```sh 195 | wasmsign2 detach -i z2.wasm -o z3.wasm -S signature 196 | ``` 197 | 198 | ## Embedding a detached signature in a module 199 | 200 | ```text 201 | wasmsign2 attach --input-file --output-file --signature-file 202 | 203 | -i, --input-file Input file 204 | -o, --output-file Output file 205 | -S, --signature-file Signature file 206 | ``` 207 | 208 | The command embeds a detached signature into a module. 209 | 210 | Example: 211 | 212 | ```sh 213 | wasmsign2 attach -i z2.wasm -o z3.wasm -S signature 214 | ``` 215 | 216 | ## Partial verification 217 | 218 | A signature can verify an entire module, but also one or more subsets of it. 219 | 220 | This requires "cutting points" to be defined before the signature process. It is impossible to verify a signature beyond cutting point boudaries. 221 | 222 | Cutting points can be added to a module with the `split` command: 223 | 224 | ```text 225 | wasmsign2 split [OPTIONS] --input-file --output-file 226 | 227 | -i, --input-file Input file 228 | -o, --output-file Output file 229 | -s, --split Custom section names to be signed 230 | ``` 231 | 232 | This adds cutting points so that it is possible to verify only the subset of custom sections whose name matches the regular expression, in addition to standard sections. 233 | 234 | This command can be repeated, to add new cutting points to a module that was already prepared for partial verification. 235 | 236 | Example: 237 | 238 | ```sh 239 | wasmsign2 split -i z2.wasm -o z3.wasm -s '^.debug_' 240 | ``` 241 | 242 | The above command makes it possible to verify only the custom sections whose name starts with `.debug_`, even though the entire module was signed. 243 | 244 | In order to do partial verification, the `--split` parameter is also available in the verification commands: 245 | 246 | ```sh 247 | wasmsign2 verify -i z3.wasm -K public.key -s '^.debug_' 248 | ``` 249 | 250 | ```sh 251 | wasmsign2 verify_matrix -i z3.wasm -K public.key -K public.key2 -s '^.debug_' 252 | ``` 253 | 254 | ## OpenSSH keys support 255 | 256 | In addition to the compact key format documented in the proposal, the API allows loading/saving public and secret keys with DER and PEM encoding. 257 | 258 | OpenSSH keys can also be used by adding the `--ssh` flag to the `sign`, `verify` and `verify_matrix` commands, provided that they are Ed25519 (EdDSA) keys. 259 | 260 | Examples: 261 | 262 | ```sh 263 | wasmsign2 sign --ssh -k ~/.ssh/id_ed25519 -i z.wasm -o z2.wasm 264 | ``` 265 | 266 | ```sh 267 | wasmsign2 verify --ssh -K ~/.ssh/id_ed25519.pub -i z2.wasm 268 | ``` 269 | 270 | If a file contains more than a single public key, the `verify_matrix` command will check the signature against all discovered Ed25519 keys. 271 | 272 | Public key sets from GitHub accounts can be downloaded at `https://github.com/.keys`, replacing `` with an actual GitHub account name. 273 | 274 | Keys downloaded from such URL can be directly used to verify WebAssembly signatures. 275 | 276 | ## GitHub integration 277 | 278 | Public keys can also automatically be retrieved from GitHub accounts, using the `--from-github` parameter. 279 | 280 | Examples: 281 | 282 | ```sh 283 | wasmsign2 verify -G example_account -i z2.wasm 284 | ``` 285 | 286 | ```sh 287 | wasmsign2 matrix_verify -G example_account -i z2.wasm 288 | ``` 289 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasm-signatures/wasmsign2/2499e3cc89ce3443fefaa91e5179905b69df73ba/logo.png -------------------------------------------------------------------------------- /src/cli/main.rs: -------------------------------------------------------------------------------- 1 | use wasmsign2::{ 2 | BoxedPredicate, KeyPair, Module, PublicKey, PublicKeySet, SecretKey, Section, WSError, 3 | }; 4 | 5 | use wasmsign2::reexports::log; 6 | 7 | #[macro_use] 8 | extern crate clap; 9 | 10 | use clap::{App, Arg}; 11 | use regex::RegexBuilder; 12 | use std::fs::File; 13 | use std::io::{prelude::*, BufReader}; 14 | 15 | fn start() -> Result<(), WSError> { 16 | let matches = app_from_crate!() 17 | .arg(Arg::new("verbose").short('v').help("Verbose output")) 18 | .arg( 19 | Arg::new("debug") 20 | .short('d') 21 | .help("Prints debugging information"), 22 | ) 23 | .subcommand( 24 | App::new("keygen") 25 | .about("Generate a new key pair") 26 | .arg( 27 | Arg::new("secret_key") 28 | .value_name("secret_key_file") 29 | .long("--secret-key") 30 | .short('k') 31 | .multiple_occurrences(false) 32 | .required(true) 33 | .help("Secret key file"), 34 | ) 35 | .arg( 36 | Arg::new("public_key") 37 | .value_name("public_key_file") 38 | .long("--public-key") 39 | .short('K') 40 | .multiple_occurrences(false) 41 | .required(true) 42 | .help("Public key file"), 43 | ), 44 | ) 45 | .subcommand( 46 | App::new("show") 47 | .about("Print the structure of a module") 48 | .arg( 49 | Arg::new("in") 50 | .value_name("input_file") 51 | .long("--input-file") 52 | .short('i') 53 | .multiple_occurrences(false) 54 | .required(true) 55 | .help("Input file"), 56 | ), 57 | ) 58 | .subcommand( 59 | App::new("split") 60 | .about("Add cutting points to a module to enable partial verification") 61 | .arg( 62 | Arg::new("in") 63 | .value_name("input_file") 64 | .long("--input-file") 65 | .short('i') 66 | .multiple_occurrences(false) 67 | .required(true) 68 | .help("Input file"), 69 | ) 70 | .arg( 71 | Arg::new("out") 72 | .value_name("output_file") 73 | .long("--output-file") 74 | .short('o') 75 | .multiple_occurrences(false) 76 | .required(true) 77 | .help("Output file"), 78 | ) 79 | .arg( 80 | Arg::new("splits") 81 | .long("--split") 82 | .short('s') 83 | .value_name("regex") 84 | .multiple_occurrences(false) 85 | .help("Custom section names to be signed"), 86 | ), 87 | ) 88 | .subcommand( 89 | App::new("sign") 90 | .about("Sign a module") 91 | .arg( 92 | Arg::new("in") 93 | .value_name("input_file") 94 | .long("--input-file") 95 | .short('i') 96 | .multiple_occurrences(false) 97 | .required(true) 98 | .help("Input file"), 99 | ) 100 | .arg( 101 | Arg::new("out") 102 | .value_name("output_file") 103 | .long("--output-file") 104 | .short('o') 105 | .multiple_occurrences(false) 106 | .required(true) 107 | .help("Output file"), 108 | ) 109 | .arg( 110 | Arg::new("secret_key") 111 | .value_name("secret_key_file") 112 | .long("--secret-key") 113 | .short('k') 114 | .multiple_occurrences(false) 115 | .required(true) 116 | .help("Secret key file"), 117 | ) 118 | .arg( 119 | Arg::new("public_key") 120 | .value_name("public_key_file") 121 | .long("--public-key") 122 | .short('K') 123 | .multiple_occurrences(false) 124 | .help("Public key file"), 125 | ) 126 | .arg( 127 | Arg::new("ssh") 128 | .long("--ssh") 129 | .short('Z') 130 | .help("Parse OpenSSH keys"), 131 | ) 132 | .arg( 133 | Arg::new("signature_file") 134 | .value_name("signature_file") 135 | .long("--signature-file") 136 | .short('S') 137 | .multiple_occurrences(false) 138 | .help("Signature file"), 139 | ), 140 | ) 141 | .subcommand( 142 | App::new("verify") 143 | .about("Verify a module's signature") 144 | .arg( 145 | Arg::new("in") 146 | .value_name("input_file") 147 | .long("--input-file") 148 | .short('i') 149 | .multiple_occurrences(false) 150 | .required(true) 151 | .help("Input file"), 152 | ) 153 | .arg( 154 | Arg::new("public_key") 155 | .value_name("public_key_file") 156 | .long("--public-key") 157 | .short('K') 158 | .multiple_occurrences(false) 159 | .required(false) 160 | .help("Public key file"), 161 | ) 162 | .arg( 163 | Arg::new("from_github") 164 | .value_name("from_github") 165 | .long("--from-github") 166 | .short('G') 167 | .multiple_occurrences(false) 168 | .required(false) 169 | .help("GitHub account to retrieve public keys from"), 170 | ) 171 | .arg( 172 | Arg::new("ssh") 173 | .long("--ssh") 174 | .short('Z') 175 | .help("Parse OpenSSH keys"), 176 | ) 177 | .arg( 178 | Arg::new("signature_file") 179 | .value_name("signature_file") 180 | .long("--signature-file") 181 | .short('S') 182 | .multiple_occurrences(false) 183 | .help("Signature file"), 184 | ) 185 | .arg( 186 | Arg::new("splits") 187 | .long("--split") 188 | .short('s') 189 | .value_name("regex") 190 | .multiple_occurrences(false) 191 | .help("Custom section names to be verified"), 192 | ), 193 | ) 194 | .subcommand( 195 | App::new("detach") 196 | .about("Detach the signature from a module") 197 | .arg( 198 | Arg::new("in") 199 | .value_name("input_file") 200 | .long("--input-file") 201 | .short('i') 202 | .multiple_occurrences(false) 203 | .required(true) 204 | .help("Input file"), 205 | ) 206 | .arg( 207 | Arg::new("out") 208 | .value_name("output_file") 209 | .long("--output-file") 210 | .short('o') 211 | .multiple_occurrences(false) 212 | .required(true) 213 | .help("Output file"), 214 | ) 215 | .arg( 216 | Arg::new("signature_file") 217 | .value_name("signature_file") 218 | .long("--signature-file") 219 | .short('S') 220 | .multiple_occurrences(false) 221 | .required(true) 222 | .help("Signature file"), 223 | ), 224 | ) 225 | .subcommand( 226 | App::new("attach") 227 | .about("Embed a detach signature into a module") 228 | .arg( 229 | Arg::new("in") 230 | .value_name("input_file") 231 | .long("--input-file") 232 | .short('i') 233 | .multiple_occurrences(false) 234 | .required(true) 235 | .help("Input file"), 236 | ) 237 | .arg( 238 | Arg::new("out") 239 | .value_name("output_file") 240 | .long("--output-file") 241 | .short('o') 242 | .multiple_occurrences(false) 243 | .required(true) 244 | .help("Output file"), 245 | ) 246 | .arg( 247 | Arg::new("signature_file") 248 | .value_name("signature_file") 249 | .long("--signature-file") 250 | .short('S') 251 | .multiple_occurrences(false) 252 | .required(true) 253 | .help("Signature file"), 254 | ), 255 | ) 256 | .subcommand( 257 | App::new("verify_matrix") 258 | .about("Batch verification against multiple public keys") 259 | .arg( 260 | Arg::new("in") 261 | .value_name("input_file") 262 | .long("--input-file") 263 | .short('i') 264 | .multiple_occurrences(false) 265 | .required(true) 266 | .help("Input file"), 267 | ) 268 | .arg( 269 | Arg::new("public_keys") 270 | .value_name("public_key_files") 271 | .long("--public-keys") 272 | .short('K') 273 | .multiple_occurrences(true) 274 | .required(false) 275 | .help("Public key files"), 276 | ) 277 | .arg( 278 | Arg::new("from_github") 279 | .value_name("from_github") 280 | .long("--from-github") 281 | .short('G') 282 | .multiple_occurrences(false) 283 | .required(false) 284 | .help("GitHub account to retrieve public keys from"), 285 | ) 286 | .arg( 287 | Arg::new("ssh") 288 | .long("--ssh") 289 | .short('Z') 290 | .help("Parse OpenSSH keys"), 291 | ) 292 | .arg( 293 | Arg::new("splits") 294 | .long("--split") 295 | .short('s') 296 | .value_name("regex") 297 | .multiple_occurrences(false) 298 | .help("Custom section names to be verified"), 299 | ), 300 | ) 301 | .get_matches(); 302 | 303 | let verbose = matches.is_present("verbose"); 304 | let debug = matches.is_present("debug"); 305 | 306 | env_logger::builder() 307 | .format_timestamp(None) 308 | .format_level(false) 309 | .format_module_path(false) 310 | .format_target(false) 311 | .filter_level(if debug { 312 | log::LevelFilter::Debug 313 | } else { 314 | log::LevelFilter::Info 315 | }) 316 | .init(); 317 | 318 | if let Some(matches) = matches.subcommand_matches("show") { 319 | let input_file = matches.value_of("in"); 320 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 321 | let module = Module::deserialize_from_file(input_file)?; 322 | module.show(verbose)?; 323 | } else if let Some(matches) = matches.subcommand_matches("keygen") { 324 | let kp = KeyPair::generate(); 325 | let sk_file = matches 326 | .value_of("secret_key") 327 | .ok_or(WSError::UsageError("Missing secret key file"))?; 328 | let pk_file = matches 329 | .value_of("public_key") 330 | .ok_or(WSError::UsageError("Missing public key file"))?; 331 | kp.sk.to_file(sk_file)?; 332 | println!("Secret key saved to [{sk_file}]"); 333 | kp.pk.to_file(pk_file)?; 334 | println!("Public key saved to [{pk_file}]"); 335 | } else if let Some(matches) = matches.subcommand_matches("split") { 336 | let input_file = matches.value_of("in"); 337 | let output_file = matches.value_of("out"); 338 | let splits = matches.value_of("splits"); 339 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 340 | let output_file = output_file.ok_or(WSError::UsageError("Missing output file"))?; 341 | let signed_sections_rx = match splits { 342 | None => None, 343 | Some(splits) => Some( 344 | RegexBuilder::new(splits) 345 | .case_insensitive(false) 346 | .multi_line(false) 347 | .dot_matches_new_line(false) 348 | .size_limit(1_000_000) 349 | .dfa_size_limit(1_000_000) 350 | .nest_limit(1000) 351 | .build() 352 | .map_err(|_| WSError::InvalidArgument)?, 353 | ), 354 | }; 355 | let mut module = Module::deserialize_from_file(input_file)?; 356 | module = module.split(|section| match section { 357 | Section::Standard(_) => true, 358 | Section::Custom(custom_section) => { 359 | if let Some(signed_sections_rx) = &signed_sections_rx { 360 | signed_sections_rx.is_match(custom_section.name()) 361 | } else { 362 | true 363 | } 364 | } 365 | })?; 366 | module.serialize_to_file(output_file)?; 367 | println!("* Split module structure:\n"); 368 | module.show(verbose)?; 369 | } else if let Some(matches) = matches.subcommand_matches("sign") { 370 | let input_file = matches.value_of("in"); 371 | let output_file = matches.value_of("out"); 372 | let signature_file = matches.value_of("signature_file"); 373 | let sk_file = matches 374 | .value_of("secret_key") 375 | .ok_or(WSError::UsageError("Missing secret key file"))?; 376 | let sk = match matches.is_present("ssh") { 377 | false => SecretKey::from_file(sk_file)?, 378 | true => SecretKey::from_openssh_file(sk_file)?, 379 | }; 380 | let pk_file = matches.value_of("public_key"); 381 | let key_id = if let Some(pk_file) = pk_file { 382 | let pk = match matches.is_present("ssh") { 383 | false => PublicKey::from_file(pk_file)?, 384 | true => PublicKey::from_openssh_file(pk_file)?, 385 | } 386 | .attach_default_key_id(); 387 | pk.key_id().cloned() 388 | } else { 389 | None 390 | }; 391 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 392 | let output_file = output_file.ok_or(WSError::UsageError("Missing output file"))?; 393 | let module = Module::deserialize_from_file(input_file)?; 394 | let (module, signature) = 395 | sk.sign_multi(module, key_id.as_ref(), signature_file.is_some(), false)?; 396 | if let Some(signature_file) = signature_file { 397 | module.serialize_to_file(output_file)?; 398 | File::create(signature_file)?.write_all(&signature)?; 399 | } else { 400 | module.serialize_to_file(output_file)?; 401 | } 402 | println!("* Signed module structure:\n"); 403 | module.show(verbose)?; 404 | } else if let Some(matches) = matches.subcommand_matches("verify") { 405 | let input_file = matches.value_of("in"); 406 | let signature_file = matches.value_of("signature_file"); 407 | let splits = matches.value_of("splits"); 408 | let signed_sections_rx = match splits { 409 | None => None, 410 | Some(splits) => Some( 411 | RegexBuilder::new(splits) 412 | .case_insensitive(false) 413 | .multi_line(false) 414 | .dot_matches_new_line(false) 415 | .size_limit(1_000_000) 416 | .dfa_size_limit(1_000_000) 417 | .nest_limit(1000) 418 | .build() 419 | .map_err(|_| WSError::InvalidArgument)?, 420 | ), 421 | }; 422 | let pk = if let Some(github_account) = matches.value_of("from_github") { 423 | PublicKey::from_openssh(&get_pks_from_github(github_account)?)? 424 | } else { 425 | let pk_file = matches 426 | .value_of("public_key") 427 | .ok_or(WSError::UsageError("Missing public key file"))?; 428 | match matches.is_present("ssh") { 429 | false => PublicKey::from_file(pk_file)?, 430 | true => PublicKey::from_openssh_file(pk_file)?, 431 | } 432 | } 433 | .attach_default_key_id(); 434 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 435 | let mut detached_signatures_ = vec![]; 436 | let detached_signatures = match signature_file { 437 | None => None, 438 | Some(signature_file) => { 439 | File::open(signature_file)?.read_to_end(&mut detached_signatures_)?; 440 | Some(detached_signatures_.as_slice()) 441 | } 442 | }; 443 | let mut reader = BufReader::new(File::open(input_file)?); 444 | if let Some(signed_sections_rx) = &signed_sections_rx { 445 | pk.verify_multi(&mut reader, detached_signatures, |section| match section { 446 | Section::Standard(_) => true, 447 | Section::Custom(custom_section) => { 448 | signed_sections_rx.is_match(custom_section.name()) 449 | } 450 | })?; 451 | } else { 452 | pk.verify(&mut reader, detached_signatures)?; 453 | } 454 | println!("Signature is valid."); 455 | } else if let Some(matches) = matches.subcommand_matches("detach") { 456 | let input_file = matches.value_of("in"); 457 | let output_file = matches.value_of("out"); 458 | let signature_file = matches.value_of("signature_file"); 459 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 460 | let output_file = output_file.ok_or(WSError::UsageError("Missing output file"))?; 461 | let signature_file = 462 | signature_file.ok_or(WSError::UsageError("Missing detached signature file"))?; 463 | let module = Module::deserialize_from_file(input_file)?; 464 | let (module, detached_signature) = module.detach_signature()?; 465 | File::create(signature_file)?.write_all(&detached_signature)?; 466 | module.serialize_to_file(output_file)?; 467 | println!("Signature is now detached."); 468 | } else if let Some(matches) = matches.subcommand_matches("attach") { 469 | let input_file = matches.value_of("in"); 470 | let output_file = matches.value_of("out"); 471 | let signature_file = matches.value_of("signature_file"); 472 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 473 | let output_file = output_file.ok_or(WSError::UsageError("Missing output file"))?; 474 | let signature_file = 475 | signature_file.ok_or(WSError::UsageError("Missing detached signature file"))?; 476 | let mut detached_signature = vec![]; 477 | File::open(signature_file)?.read_to_end(&mut detached_signature)?; 478 | let mut module = Module::deserialize_from_file(input_file)?; 479 | module = module.attach_signature(&detached_signature)?; 480 | module.serialize_to_file(output_file)?; 481 | println!("Signature is now embedded as a custom section."); 482 | } else if let Some(matches) = matches.subcommand_matches("verify_matrix") { 483 | let input_file = matches.value_of("in"); 484 | let signature_file = matches.value_of("signature_file"); 485 | let splits = matches.value_of("splits"); 486 | let signed_sections_rx = match splits { 487 | None => None, 488 | Some(splits) => Some( 489 | RegexBuilder::new(splits) 490 | .case_insensitive(false) 491 | .multi_line(false) 492 | .dot_matches_new_line(false) 493 | .size_limit(1_000_000) 494 | .dfa_size_limit(1_000_000) 495 | .nest_limit(1000) 496 | .build() 497 | .map_err(|_| WSError::InvalidArgument)?, 498 | ), 499 | }; 500 | let pks = if let Some(github_account) = matches.value_of("from_github") { 501 | PublicKeySet::from_openssh(&get_pks_from_github(github_account)?)? 502 | } else { 503 | let pk_files = matches 504 | .values_of("public_keys") 505 | .ok_or(WSError::UsageError("Missing public key files"))?; 506 | match matches.is_present("ssh") { 507 | false => { 508 | let mut pks = std::collections::HashSet::new(); 509 | for pk_file in pk_files { 510 | let pk = PublicKey::from_file(pk_file)?; 511 | pks.insert(pk); 512 | } 513 | PublicKeySet::new(pks) 514 | } 515 | true => PublicKeySet::from_openssh_file( 516 | pk_files 517 | .into_iter() 518 | .next() 519 | .ok_or(WSError::UsageError("Missing public keys file"))?, 520 | )?, 521 | } 522 | } 523 | .attach_default_key_id(); 524 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 525 | let mut detached_signatures_ = vec![]; 526 | let detached_signatures = match signature_file { 527 | None => None, 528 | Some(signature_file) => { 529 | File::open(signature_file)?.read_to_end(&mut detached_signatures_)?; 530 | Some(detached_signatures_.as_slice()) 531 | } 532 | }; 533 | let mut reader = BufReader::new(File::open(input_file)?); 534 | let predicates: Vec = if let Some(signed_sections_rx) = signed_sections_rx { 535 | vec![Box::new(move |section| match section { 536 | Section::Standard(_) => true, 537 | Section::Custom(custom_section) => { 538 | signed_sections_rx.is_match(custom_section.name()) 539 | } 540 | })] 541 | } else { 542 | vec![Box::new(|_| true)] 543 | }; 544 | let matrix = pks.verify_matrix(&mut reader, detached_signatures, &predicates)?; 545 | let valid_pks = matrix.first().ok_or(WSError::UsageError("No predicates"))?; 546 | if valid_pks.is_empty() { 547 | println!("No valid public keys found"); 548 | } else { 549 | println!("Valid public keys:"); 550 | for pk in valid_pks { 551 | println!(" - {pk:x?}"); 552 | } 553 | } 554 | } else { 555 | return Err(WSError::UsageError("No subcommand specified")); 556 | } 557 | Ok(()) 558 | } 559 | 560 | fn get_pks_from_github(account: impl AsRef) -> Result { 561 | let account_rawurlencoded = uri_encode::encode_uri_component(account.as_ref()); 562 | let url = format!("https://github.com/{account_rawurlencoded}.keys"); 563 | let response = ureq::get(&url) 564 | .call() 565 | .map_err(|_| WSError::UsageError("Keys couldn't be retrieved from GitHub"))?; 566 | let s = response 567 | .into_body() 568 | .read_to_vec() 569 | .map_err(|_| WSError::UsageError("Keys couldn't be retrieved from GitHub"))?; 570 | String::from_utf8(s).map_err(|_| { 571 | WSError::UsageError("Unexpected characters in the public keys retrieved from GitHub") 572 | }) 573 | } 574 | 575 | fn main() -> Result<(), WSError> { 576 | let res = start(); 577 | match res { 578 | Ok(_) => {} 579 | Err(e) => { 580 | eprintln!("{e}"); 581 | std::process::exit(1); 582 | } 583 | } 584 | Ok(()) 585 | } 586 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Exists for the sake of crate_universe: 2 | // https://bazelbuild.github.io/rules_rust/crate_universe.html#binary-dependencies 3 | -------------------------------------------------------------------------------- /src/lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmsign2" 3 | version = "0.2.6" 4 | edition = "2021" 5 | authors = ["Frank Denis "] 6 | description = "An implementation of the WebAssembly modules signatures proposal" 7 | readme = "../../README.md" 8 | keywords = ["webassembly", "modules", "signatures"] 9 | license = "MIT" 10 | homepage = "https://github.com/wasm-signatures/design" 11 | repository = "https://github.com/wasm-signatures/wasmsign2" 12 | categories = ["cryptography", "wasm"] 13 | 14 | [dependencies] 15 | anyhow = "1.0.93" 16 | ct-codecs = "1.1.2" 17 | ed25519-compact = { version = "2.1.1", features = ["pem"] } 18 | getrandom = { version = "0.2.15", features = ["js"] } 19 | hmac-sha256 = "1.1.7" 20 | log = "0.4.22" 21 | regex = "1.11.1" 22 | ssh-keys = "0.1.4" 23 | thiserror = "2.0.3" 24 | 25 | [profile.release] 26 | codegen-units = 1 27 | incremental = false 28 | panic = "abort" 29 | -------------------------------------------------------------------------------- /src/lib/src/error.rs: -------------------------------------------------------------------------------- 1 | /// The WasmSign2 error type. 2 | #[derive(Debug, thiserror::Error)] 3 | pub enum WSError { 4 | #[error("Internal error: [{0}]")] 5 | InternalError(String), 6 | 7 | #[error("Parse error")] 8 | ParseError, 9 | 10 | #[error("I/O error")] 11 | IOError(#[from] std::io::Error), 12 | 13 | #[error("EOF")] 14 | Eof, 15 | 16 | #[error("UTF-8 error")] 17 | UTF8Error(#[from] std::str::Utf8Error), 18 | 19 | #[error("Ed25519 signature function error")] 20 | CryptoError(#[from] ed25519_compact::Error), 21 | 22 | #[error("Unsupported module type")] 23 | UnsupportedModuleType, 24 | 25 | #[error("No valid signatures")] 26 | VerificationFailed, 27 | 28 | #[error("No valid signatures for the given predicates")] 29 | VerificationFailedForPredicates, 30 | 31 | #[error("No signatures found")] 32 | NoSignatures, 33 | 34 | #[error("Unsupported key type")] 35 | UnsupportedKeyType, 36 | 37 | #[error("Invalid argument")] 38 | InvalidArgument, 39 | 40 | #[error("Incompatible signature version")] 41 | IncompatibleSignatureVersion, 42 | 43 | #[error("Duplicate signature")] 44 | DuplicateSignature, 45 | 46 | #[error("Sections can only be verified between pre-defined boundaries")] 47 | InvalidVerificationPredicate, 48 | 49 | #[error("Signature already attached")] 50 | SignatureAlreadyAttached, 51 | 52 | #[error("Duplicate public key")] 53 | DuplicatePublicKey, 54 | 55 | #[error("Unknown public key")] 56 | UnknownPublicKey, 57 | 58 | #[error("Too many hashes (max: {0})")] 59 | TooManyHashes(usize), 60 | 61 | #[error("Too many signatures (max: {0})")] 62 | TooManySignatures(usize), 63 | 64 | #[error("Usage error: {0}")] 65 | UsageError(&'static str), 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A proof of concept implementation of the WebAssembly module signature proposal. 2 | 3 | // The `PublicKey::verify()` function is what most runtimes should use or reimplement if they don't need partial verification. 4 | // The `SecretKey::sign()` function is what most 3rd-party signing tools can use or reimplement if they don't need support for multiple signatures. 5 | 6 | #![allow(clippy::vec_init_then_push)] 7 | #![forbid(unsafe_code)] 8 | 9 | mod error; 10 | mod signature; 11 | mod split; 12 | mod wasm_module; 13 | 14 | #[allow(unused_imports)] 15 | pub use error::*; 16 | #[allow(unused_imports)] 17 | pub use signature::*; 18 | #[allow(unused_imports)] 19 | pub use split::*; 20 | #[allow(unused_imports)] 21 | pub use wasm_module::*; 22 | 23 | pub mod reexports { 24 | pub use {anyhow, ct_codecs, getrandom, hmac_sha256, log, regex, thiserror}; 25 | } 26 | 27 | const SIGNATURE_WASM_DOMAIN: &str = "wasmsig"; 28 | const SIGNATURE_VERSION: u8 = 0x01; 29 | const SIGNATURE_WASM_MODULE_CONTENT_TYPE: u8 = 0x01; 30 | const SIGNATURE_HASH_FUNCTION: u8 = 0x01; 31 | -------------------------------------------------------------------------------- /src/lib/src/signature/hash.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub(crate) struct Hash { 5 | hash: hmac_sha256::Hash, 6 | } 7 | 8 | impl Hash { 9 | pub fn new() -> Self { 10 | Hash { 11 | hash: hmac_sha256::Hash::new(), 12 | } 13 | } 14 | 15 | pub fn update>(&mut self, data: T) { 16 | self.hash.update(data); 17 | } 18 | 19 | pub fn finalize(&self) -> [u8; 32] { 20 | self.hash.finalize() 21 | } 22 | } 23 | 24 | impl Write for Hash { 25 | fn write(&mut self, buf: &[u8]) -> io::Result { 26 | self.hash.update(buf); 27 | Ok(buf.len()) 28 | } 29 | 30 | fn flush(&mut self) -> io::Result<()> { 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/src/signature/keys.rs: -------------------------------------------------------------------------------- 1 | pub use crate::error::*; 2 | 3 | use ct_codecs::{Encoder, Hex}; 4 | use ssh_keys::{self, openssh}; 5 | use std::collections::HashSet; 6 | use std::fs::File; 7 | use std::io::{self, prelude::*}; 8 | use std::path::Path; 9 | use std::{fmt, str}; 10 | 11 | pub(crate) const ED25519_PK_ID: u8 = 0x01; 12 | pub(crate) const ED25519_SK_ID: u8 = 0x81; 13 | 14 | /// A public key. 15 | #[derive(Clone, Eq, PartialEq, Hash)] 16 | pub struct PublicKey { 17 | pub pk: ed25519_compact::PublicKey, 18 | pub key_id: Option>, 19 | } 20 | 21 | impl PublicKey { 22 | /// Create a public key from raw bytes. 23 | pub fn from_bytes(pk: &[u8]) -> Result { 24 | let mut reader = io::Cursor::new(pk); 25 | let mut id = [0u8]; 26 | reader.read_exact(&mut id)?; 27 | if id[0] != ED25519_PK_ID { 28 | return Err(WSError::UnsupportedKeyType); 29 | } 30 | let mut bytes = vec![]; 31 | reader.read_to_end(&mut bytes)?; 32 | Ok(Self { 33 | pk: ed25519_compact::PublicKey::from_slice(&bytes)?, 34 | key_id: None, 35 | }) 36 | } 37 | 38 | /// Deserialize a PEM-encoded public key. 39 | pub fn from_pem(pem: &str) -> Result { 40 | let pk = ed25519_compact::PublicKey::from_pem(pem)?; 41 | Ok(Self { pk, key_id: None }) 42 | } 43 | 44 | /// Deserialize a DER-encoded public key. 45 | pub fn from_der(der: &[u8]) -> Result { 46 | let pk = ed25519_compact::PublicKey::from_der(der)?; 47 | Ok(Self { pk, key_id: None }) 48 | } 49 | 50 | /// Return the public key as raw bytes. 51 | pub fn to_bytes(&self) -> Vec { 52 | let mut bytes = vec![ED25519_PK_ID]; 53 | bytes.extend_from_slice(self.pk.as_ref()); 54 | bytes 55 | } 56 | 57 | /// Serialize the public key using PEM encoding. 58 | pub fn to_pem(&self) -> String { 59 | self.pk.to_pem() 60 | } 61 | 62 | /// Serialize the public key using DER encoding. 63 | pub fn to_der(&self) -> Vec { 64 | self.pk.to_der() 65 | } 66 | 67 | /// Read public key from a file. 68 | pub fn from_file(file: impl AsRef) -> Result { 69 | let mut fp = File::open(file)?; 70 | let mut bytes = vec![]; 71 | fp.read_to_end(&mut bytes)?; 72 | Self::from_bytes(&bytes) 73 | } 74 | 75 | /// Save the public key to a file. 76 | pub fn to_file(&self, file: impl AsRef) -> Result<(), WSError> { 77 | let mut fp = File::create(file)?; 78 | fp.write_all(&self.to_bytes())?; 79 | Ok(()) 80 | } 81 | 82 | /// Parse a single OpenSSH public key. 83 | pub fn from_openssh(lines: &str) -> Result { 84 | for line in lines.lines() { 85 | let line = line.trim(); 86 | if let Ok(ssh_keys::PublicKey::Ed25519(raw)) = openssh::parse_public_key(line) { 87 | let mut bytes = vec![ED25519_PK_ID]; 88 | bytes.extend_from_slice(&raw); 89 | if let Ok(pk) = PublicKey::from_bytes(&bytes) { 90 | return Ok(pk); 91 | } 92 | }; 93 | } 94 | Err(WSError::ParseError) 95 | } 96 | 97 | /// Parse a single OpenSSH public key from a file. 98 | pub fn from_openssh_file(file: impl AsRef) -> Result { 99 | let mut fp = File::open(file)?; 100 | let mut lines = String::new(); 101 | fp.read_to_string(&mut lines)?; 102 | Self::from_openssh(&lines) 103 | } 104 | 105 | /// Try to guess the public key format. 106 | pub fn from_any(data: &[u8]) -> Result { 107 | if let Ok(pk) = Self::from_bytes(data) { 108 | return Ok(pk); 109 | } 110 | if let Ok(pk) = Self::from_der(data) { 111 | return Ok(pk); 112 | } 113 | let s = str::from_utf8(data).map_err(|_| WSError::ParseError)?; 114 | if let Ok(pk) = Self::from_pem(s) { 115 | return Ok(pk); 116 | } 117 | if let Ok(pk) = Self::from_openssh(s) { 118 | return Ok(pk); 119 | } 120 | Err(WSError::ParseError) 121 | } 122 | 123 | /// Load a key from a file, trying to guess its format. 124 | pub fn from_any_file(file: impl AsRef) -> Result { 125 | let mut fp = File::open(file)?; 126 | let mut bytes = vec![]; 127 | fp.read_to_end(&mut bytes)?; 128 | Self::from_any(&bytes) 129 | } 130 | 131 | /// Return the key identifier associated with this public key, if there is one. 132 | pub fn key_id(&self) -> Option<&Vec> { 133 | self.key_id.as_ref() 134 | } 135 | 136 | /// Compute a deterministic key identifier for this public key, if it doesn't already have one. 137 | pub fn attach_default_key_id(mut self) -> Self { 138 | if self.key_id.is_none() { 139 | self.key_id = Some(hmac_sha256::HMAC::mac(b"key_id", self.pk.as_ref())[0..12].to_vec()); 140 | } 141 | self 142 | } 143 | } 144 | 145 | impl fmt::Debug for PublicKey { 146 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 147 | write!( 148 | f, 149 | "PublicKey {{ [{}] - key_id: {:?} }}", 150 | Hex::encode_to_string(self.pk.as_ref()).unwrap(), 151 | self.key_id() 152 | .map(|key_id| format!("[{}]", Hex::encode_to_string(key_id).unwrap())) 153 | ) 154 | } 155 | } 156 | 157 | /// A secret key. 158 | #[derive(Clone, Eq, PartialEq, Hash)] 159 | pub struct SecretKey { 160 | pub sk: ed25519_compact::SecretKey, 161 | } 162 | 163 | impl SecretKey { 164 | /// Create a secret key from raw bytes. 165 | pub fn from_bytes(sk: &[u8]) -> Result { 166 | let mut reader = io::Cursor::new(sk); 167 | let mut id = [0u8]; 168 | reader.read_exact(&mut id)?; 169 | if id[0] != ED25519_SK_ID { 170 | return Err(WSError::UnsupportedKeyType); 171 | } 172 | let mut bytes = vec![]; 173 | reader.read_to_end(&mut bytes)?; 174 | Ok(Self { 175 | sk: ed25519_compact::SecretKey::from_slice(&bytes)?, 176 | }) 177 | } 178 | 179 | /// Deserialize a PEM-encoded secret key. 180 | pub fn from_pem(pem: &str) -> Result { 181 | let sk = ed25519_compact::SecretKey::from_pem(pem)?; 182 | Ok(Self { sk }) 183 | } 184 | 185 | /// Deserialize a DER-encoded secret key. 186 | pub fn from_der(der: &[u8]) -> Result { 187 | let sk = ed25519_compact::SecretKey::from_der(der)?; 188 | Ok(Self { sk }) 189 | } 190 | 191 | /// Return the secret key as raw bytes. 192 | pub fn to_bytes(&self) -> Vec { 193 | let mut bytes = vec![ED25519_SK_ID]; 194 | bytes.extend_from_slice(self.sk.as_ref()); 195 | bytes 196 | } 197 | 198 | /// Serialize the secret key using PEM encoding. 199 | pub fn to_pem(&self) -> String { 200 | self.sk.to_pem() 201 | } 202 | 203 | /// Serialize the secret key using DER encoding. 204 | pub fn to_der(&self) -> Vec { 205 | self.sk.to_der() 206 | } 207 | 208 | /// Read a secret key from a file. 209 | pub fn from_file(file: impl AsRef) -> Result { 210 | let mut fp = File::open(file)?; 211 | let mut bytes = vec![]; 212 | fp.read_to_end(&mut bytes)?; 213 | Self::from_bytes(&bytes) 214 | } 215 | 216 | /// Save a secret key to a file. 217 | pub fn to_file(&self, file: impl AsRef) -> Result<(), WSError> { 218 | let mut fp = File::create(file)?; 219 | fp.write_all(&self.to_bytes())?; 220 | Ok(()) 221 | } 222 | 223 | /// Parse an OpenSSH secret key. 224 | pub fn from_openssh(lines: &str) -> Result { 225 | for sk in openssh::parse_private_key(lines).map_err(|_| WSError::ParseError)? { 226 | if let ssh_keys::PrivateKey::Ed25519(raw) = sk { 227 | let mut bytes = vec![ED25519_SK_ID]; 228 | bytes.extend_from_slice(&raw); 229 | return Self::from_bytes(&bytes); 230 | } 231 | } 232 | Err(WSError::UnsupportedKeyType) 233 | } 234 | 235 | /// Read an OpenSSH key from a file. 236 | pub fn from_openssh_file(file: impl AsRef) -> Result { 237 | let mut fp = File::open(file)?; 238 | let mut lines = String::new(); 239 | fp.read_to_string(&mut lines)?; 240 | Self::from_openssh(&lines) 241 | } 242 | } 243 | 244 | impl fmt::Debug for SecretKey { 245 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 246 | write!( 247 | f, 248 | "SecretKey {{ [{}] }}", 249 | Hex::encode_to_string(self.sk.as_ref()).unwrap(), 250 | ) 251 | } 252 | } 253 | 254 | /// A key pair. 255 | #[derive(Clone, Eq, PartialEq, Hash, Debug)] 256 | pub struct KeyPair { 257 | /// The public key. 258 | pub pk: PublicKey, 259 | /// The secret key. 260 | pub sk: SecretKey, 261 | } 262 | 263 | impl KeyPair { 264 | /// Generate a new key pair. 265 | pub fn generate() -> Self { 266 | let kp = ed25519_compact::KeyPair::from_seed(ed25519_compact::Seed::generate()); 267 | KeyPair { 268 | pk: PublicKey { 269 | pk: kp.pk, 270 | key_id: None, 271 | }, 272 | sk: SecretKey { sk: kp.sk }, 273 | } 274 | } 275 | } 276 | 277 | /// A set of multiple public keys. 278 | #[derive(Debug, Clone)] 279 | pub struct PublicKeySet { 280 | pub pks: HashSet, 281 | } 282 | 283 | impl PublicKeySet { 284 | /// Create an empty public key set. 285 | pub fn empty() -> Self { 286 | PublicKeySet { 287 | pks: HashSet::new(), 288 | } 289 | } 290 | 291 | /// Create a new public key set. 292 | pub fn new(pks: HashSet) -> Self { 293 | PublicKeySet { pks } 294 | } 295 | 296 | /// Parse an OpenSSH public key set. 297 | pub fn from_openssh(lines: &str) -> Result { 298 | let mut pks = PublicKeySet::empty(); 299 | for line in lines.lines() { 300 | let line = line.trim(); 301 | if let Ok(ssh_keys::PublicKey::Ed25519(raw)) = openssh::parse_public_key(line) { 302 | let mut bytes = vec![ED25519_PK_ID]; 303 | bytes.extend_from_slice(&raw); 304 | if let Ok(pk) = PublicKey::from_bytes(&bytes) { 305 | pks.pks.insert(pk); 306 | } 307 | }; 308 | } 309 | Ok(pks) 310 | } 311 | 312 | /// Parse an OpenSSH public key set from a file. 313 | pub fn from_openssh_file(file: impl AsRef) -> Result { 314 | let mut fp = File::open(file)?; 315 | let mut lines = String::new(); 316 | fp.read_to_string(&mut lines)?; 317 | Self::from_openssh(&lines) 318 | } 319 | 320 | /// Return the number of keys in the set. 321 | pub fn len(&self) -> usize { 322 | self.pks.len() 323 | } 324 | 325 | /// Return true if the set is empty. 326 | pub fn is_empty(&self) -> bool { 327 | self.pks.is_empty() 328 | } 329 | 330 | /// Add a public key to the set. 331 | pub fn insert(&mut self, pk: PublicKey) -> Result<(), WSError> { 332 | if !self.pks.insert(pk) { 333 | return Err(WSError::DuplicatePublicKey); 334 | } 335 | Ok(()) 336 | } 337 | 338 | /// Parse and add a key to the set, trying to guess its format. 339 | pub fn insert_any(&mut self, data: &[u8]) -> Result<(), WSError> { 340 | if let Ok(s) = str::from_utf8(data) { 341 | if let Ok(pk) = PublicKey::from_openssh(s) { 342 | self.insert(pk)?; 343 | return Ok(()); 344 | } 345 | } 346 | let pk = PublicKey::from_any(data)?; 347 | self.insert(pk) 348 | } 349 | 350 | /// Load, parse and add a key to the set, trying to guess its format. 351 | pub fn insert_any_file(&mut self, file: impl AsRef) -> Result<(), WSError> { 352 | let mut fp = File::open(file)?; 353 | let mut data = vec![]; 354 | fp.read_to_end(&mut data)?; 355 | self.insert_any(&data) 356 | } 357 | 358 | /// Merge another public key set into this one. 359 | pub fn merge(&mut self, other: &PublicKeySet) -> Result<(), WSError> { 360 | for pk in other.pks.iter() { 361 | self.insert(pk.clone())?; 362 | } 363 | Ok(()) 364 | } 365 | 366 | /// Remove a key from the set. 367 | pub fn remove(&mut self, pk: &PublicKey) -> Result<(), WSError> { 368 | if !self.pks.remove(pk) { 369 | return Err(WSError::UnknownPublicKey); 370 | } 371 | Ok(()) 372 | } 373 | 374 | /// Return the hash set storing the keys. 375 | pub fn items(&self) -> &HashSet { 376 | &self.pks 377 | } 378 | 379 | /// Return the mutable hash set storing the keys. 380 | pub fn items_mut(&mut self) -> &mut HashSet { 381 | &mut self.pks 382 | } 383 | 384 | /// Add a deterministic key identifier to all the keys that don't have one already. 385 | pub fn attach_default_key_id(mut self) -> Self { 386 | self.pks = self 387 | .pks 388 | .into_iter() 389 | .map(|pk| pk.attach_default_key_id()) 390 | .collect(); 391 | self 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/lib/src/signature/matrix.rs: -------------------------------------------------------------------------------- 1 | use crate::signature::*; 2 | use crate::wasm_module::*; 3 | use crate::*; 4 | 5 | use log::*; 6 | use std::collections::{HashMap, HashSet}; 7 | use std::io::Read; 8 | 9 | /// A sized predicate, used to verify a predicate*public_key matrix. 10 | pub type BoxedPredicate = Box bool>; 11 | 12 | impl PublicKeySet { 13 | /// Given a set of predicates and a set of public keys, check which public keys verify a signature over sections matching each predicate. 14 | /// 15 | /// `reader` is a reader over the raw module data. 16 | /// 17 | /// `detached_signature` is the detached signature of the module, if any. 18 | /// 19 | /// `predicates` is a set of predicates. 20 | /// 21 | /// The function returns a vector which maps every predicate to a set public keys verifying a signature over sections matching the predicate. 22 | /// The vector is sorted by predicate index. 23 | pub fn verify_matrix( 24 | &self, 25 | reader: &mut impl Read, 26 | detached_signature: Option<&[u8]>, 27 | predicates: &[impl Fn(&Section) -> bool], 28 | ) -> Result>, WSError> { 29 | let mut sections = Module::iterate(Module::init_from_reader(reader)?)?; 30 | let signature_header_section = if let Some(detached_signature) = &detached_signature { 31 | Section::Custom(CustomSection::new( 32 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 33 | detached_signature.to_vec(), 34 | )) 35 | } else { 36 | sections.next().ok_or(WSError::ParseError)?? 37 | }; 38 | let signature_header = match signature_header_section { 39 | Section::Custom(custom_section) if custom_section.is_signature_header() => { 40 | custom_section 41 | } 42 | _ => { 43 | debug!("This module is not signed"); 44 | return Err(WSError::NoSignatures); 45 | } 46 | }; 47 | 48 | let signature_data = signature_header.signature_data()?; 49 | if signature_data.hash_function != SIGNATURE_HASH_FUNCTION { 50 | debug!( 51 | "Unsupported hash function: {:02x}", 52 | signature_data.hash_function, 53 | ); 54 | return Err(WSError::ParseError); 55 | } 56 | if signature_data.content_type != SIGNATURE_WASM_MODULE_CONTENT_TYPE { 57 | debug!( 58 | "Unsupported content type: {:02x}", 59 | signature_data.content_type, 60 | ); 61 | return Err(WSError::ParseError); 62 | } 63 | 64 | let signed_hashes_set = signature_data.signed_hashes_set; 65 | let mut valid_hashes_for_pks = HashMap::new(); 66 | for pk in &self.pks { 67 | let valid_hashes = pk.valid_hashes_for_pk(&signed_hashes_set)?; 68 | if !valid_hashes.is_empty() { 69 | valid_hashes_for_pks.insert(pk.clone(), valid_hashes); 70 | } 71 | } 72 | if valid_hashes_for_pks.is_empty() { 73 | debug!("No valid signatures"); 74 | return Err(WSError::VerificationFailed); 75 | } 76 | 77 | let mut section_sequence_must_be_signed_for_pks: HashMap> = 78 | HashMap::new(); 79 | for pk in valid_hashes_for_pks.keys() { 80 | section_sequence_must_be_signed_for_pks.insert(pk.clone(), None); 81 | } 82 | 83 | let mut verify_failures_for_predicates: Vec> = vec![]; 84 | for _predicate in predicates { 85 | verify_failures_for_predicates.push(HashSet::new()); 86 | } 87 | 88 | let mut hasher = Hash::new(); 89 | for section in sections { 90 | let section = section?; 91 | section.serialize(&mut hasher)?; 92 | if section.is_signature_delimiter() { 93 | let h = hasher.finalize().to_vec(); 94 | for (pk, section_sequence_must_be_signed) in 95 | section_sequence_must_be_signed_for_pks.iter_mut() 96 | { 97 | if let Some(false) = section_sequence_must_be_signed { 98 | *section_sequence_must_be_signed = None; 99 | continue; 100 | } 101 | let valid_hashes = match valid_hashes_for_pks.get(pk) { 102 | None => continue, 103 | Some(valid_hashes) => valid_hashes, 104 | }; 105 | if !valid_hashes.contains(&h) { 106 | valid_hashes_for_pks.remove(pk); 107 | } 108 | *section_sequence_must_be_signed = None; 109 | } 110 | } else { 111 | for (idx, predicate) in predicates.iter().enumerate() { 112 | let section_must_be_signed = predicate(§ion); 113 | for (pk, section_sequence_must_be_signed) in 114 | section_sequence_must_be_signed_for_pks.iter_mut() 115 | { 116 | match section_sequence_must_be_signed { 117 | None => *section_sequence_must_be_signed = Some(section_must_be_signed), 118 | Some(false) if section_must_be_signed => { 119 | verify_failures_for_predicates[idx].insert(pk.clone()); 120 | } 121 | Some(true) if !section_must_be_signed => { 122 | verify_failures_for_predicates[idx].insert(pk.clone()); 123 | } 124 | _ => {} 125 | } 126 | } 127 | } 128 | } 129 | } 130 | 131 | let mut res: Vec> = vec![]; 132 | for _predicate in predicates { 133 | let mut result_for_predicate: HashSet<&PublicKey> = HashSet::new(); 134 | for pk in &self.pks { 135 | if !valid_hashes_for_pks.contains_key(pk) { 136 | continue; 137 | } 138 | if !verify_failures_for_predicates[res.len()].contains(pk) { 139 | result_for_predicate.insert(pk); 140 | } 141 | } 142 | res.push(result_for_predicate); 143 | } 144 | 145 | if res.is_empty() { 146 | debug!("No valid signatures"); 147 | return Err(WSError::VerificationFailedForPredicates); 148 | } 149 | Ok(res) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/lib/src/signature/mod.rs: -------------------------------------------------------------------------------- 1 | mod hash; 2 | mod keys; 3 | mod matrix; 4 | mod multi; 5 | mod sig_sections; 6 | mod simple; 7 | 8 | pub use keys::*; 9 | pub use matrix::*; 10 | pub use multi::*; 11 | pub use simple::*; 12 | 13 | pub(crate) use hash::*; 14 | pub(crate) use sig_sections::*; 15 | -------------------------------------------------------------------------------- /src/lib/src/signature/multi.rs: -------------------------------------------------------------------------------- 1 | use crate::signature::*; 2 | use crate::wasm_module::*; 3 | use crate::*; 4 | 5 | use ct_codecs::{Encoder, Hex}; 6 | use log::*; 7 | use std::collections::HashSet; 8 | use std::io::Read; 9 | 10 | impl SecretKey { 11 | /// Sign a module with the secret key. 12 | /// 13 | /// If the module was already signed, the new signature is added to the existing ones. 14 | /// `key_id` is the key identifier of the public key, to be stored with the signature. 15 | /// This parameter is optional. 16 | /// 17 | /// `detached` prevents the signature from being embedded. 18 | /// 19 | /// `allow_extensions` allows new sections to be added to the module later, while retaining the ability for the original module to be verified. 20 | pub fn sign_multi( 21 | &self, 22 | mut module: Module, 23 | key_id: Option<&Vec>, 24 | detached: bool, 25 | allow_extensions: bool, 26 | ) -> Result<(Module, Vec), WSError> { 27 | let mut hasher = Hash::new(); 28 | let mut hashes = vec![]; 29 | 30 | let mut out_sections = vec![]; 31 | let header_section = Section::Custom(CustomSection::default()); 32 | if !detached { 33 | if allow_extensions { 34 | module = module.split(|_| true)?; 35 | } 36 | out_sections.push(header_section); 37 | } 38 | let mut previous_signature_data = None; 39 | let mut last_section_was_a_signature = false; 40 | for (idx, section) in module.sections.iter().enumerate() { 41 | if let Section::Custom(custom_section) = section { 42 | if custom_section.is_signature_header() { 43 | debug!("A signature section was already present."); 44 | if idx != 0 { 45 | error!("The signature section was not the first module section"); 46 | continue; 47 | } 48 | assert_eq!(previous_signature_data, None); 49 | previous_signature_data = Some(custom_section.signature_data()?); 50 | continue; 51 | } 52 | if custom_section.is_signature_delimiter() { 53 | section.serialize(&mut hasher)?; 54 | out_sections.push(section.clone()); 55 | hashes.push(hasher.finalize().to_vec()); 56 | last_section_was_a_signature = true; 57 | continue; 58 | } 59 | last_section_was_a_signature = false; 60 | } 61 | section.serialize(&mut hasher)?; 62 | out_sections.push(section.clone()); 63 | } 64 | if !last_section_was_a_signature { 65 | hashes.push(hasher.finalize().to_vec()); 66 | } 67 | let header_section = 68 | Self::build_header_section(previous_signature_data, self, key_id, hashes)?; 69 | if detached { 70 | Ok((module, header_section.payload().to_vec())) 71 | } else { 72 | out_sections[0] = header_section; 73 | module.sections = out_sections; 74 | let signature = module.sections[0].payload().to_vec(); 75 | Ok((module, signature)) 76 | } 77 | } 78 | 79 | fn build_header_section( 80 | previous_signature_data: Option, 81 | sk: &SecretKey, 82 | key_id: Option<&Vec>, 83 | hashes: Vec>, 84 | ) -> Result { 85 | let mut msg: Vec = vec![]; 86 | msg.extend_from_slice(SIGNATURE_WASM_DOMAIN.as_bytes()); 87 | msg.extend_from_slice(&[ 88 | SIGNATURE_VERSION, 89 | SIGNATURE_WASM_MODULE_CONTENT_TYPE, 90 | SIGNATURE_HASH_FUNCTION, 91 | ]); 92 | for hash in &hashes { 93 | msg.extend_from_slice(hash); 94 | } 95 | 96 | debug!("* Adding signature:\n"); 97 | 98 | debug!( 99 | "sig = Ed25519(sk, \"{}\" ‖ {:02x} ‖ {:02x} ‖ {:02x} ‖ {})\n", 100 | SIGNATURE_WASM_DOMAIN, 101 | SIGNATURE_VERSION, 102 | SIGNATURE_WASM_MODULE_CONTENT_TYPE, 103 | SIGNATURE_HASH_FUNCTION, 104 | Hex::encode_to_string(&msg[SIGNATURE_WASM_DOMAIN.len() + 2..]).unwrap() 105 | ); 106 | 107 | let signature = sk.sk.sign(msg, None).to_vec(); 108 | 109 | debug!(" = {}\n\n", Hex::encode_to_string(&signature).unwrap()); 110 | 111 | let signature_for_hashes = SignatureForHashes { 112 | key_id: key_id.cloned(), 113 | alg_id: ED25519_PK_ID, 114 | signature, 115 | }; 116 | let mut signed_hashes_set = match &previous_signature_data { 117 | None => vec![], 118 | Some(previous_signature_data) 119 | if previous_signature_data.specification_version == SIGNATURE_VERSION 120 | && previous_signature_data.content_type 121 | == SIGNATURE_WASM_MODULE_CONTENT_TYPE 122 | && previous_signature_data.hash_function == SIGNATURE_HASH_FUNCTION => 123 | { 124 | previous_signature_data.signed_hashes_set.clone() 125 | } 126 | _ => return Err(WSError::IncompatibleSignatureVersion), 127 | }; 128 | 129 | let mut new_hashes = true; 130 | for previous_signed_hashes_set in &mut signed_hashes_set { 131 | if previous_signed_hashes_set.hashes == hashes { 132 | if previous_signed_hashes_set.signatures.iter().any(|sig| { 133 | sig.key_id == signature_for_hashes.key_id 134 | && sig.signature == signature_for_hashes.signature 135 | }) { 136 | debug!("A matching hash set was already signed with that key."); 137 | return Err(WSError::DuplicateSignature); 138 | } 139 | debug!("A matching hash set was already signed."); 140 | previous_signed_hashes_set 141 | .signatures 142 | .push(signature_for_hashes.clone()); 143 | new_hashes = false; 144 | break; 145 | } 146 | } 147 | if new_hashes { 148 | debug!("No matching hash was previously signed."); 149 | let signatures = vec![signature_for_hashes]; 150 | let new_signed_section_sequences = SignedHashes { hashes, signatures }; 151 | signed_hashes_set.push(new_signed_section_sequences); 152 | } 153 | let signature_data = SignatureData { 154 | specification_version: SIGNATURE_VERSION, 155 | content_type: SIGNATURE_WASM_MODULE_CONTENT_TYPE, 156 | hash_function: SIGNATURE_HASH_FUNCTION, 157 | signed_hashes_set, 158 | }; 159 | let header_section = Section::Custom(CustomSection::new( 160 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 161 | signature_data.serialize()?, 162 | )); 163 | Ok(header_section) 164 | } 165 | } 166 | 167 | impl PublicKey { 168 | /// Verify the signature of a module, or module subset. 169 | /// 170 | /// `reader` is a reader over the raw module data. 171 | /// 172 | /// `detached_signature` allows the caller to verify a module without an embedded signature. 173 | /// 174 | /// `predicate` should return `true` for each section that needs to be included in the signature verification. 175 | pub fn verify_multi

( 176 | &self, 177 | reader: &mut impl Read, 178 | detached_signature: Option<&[u8]>, 179 | mut predicate: P, 180 | ) -> Result<(), WSError> 181 | where 182 | P: FnMut(&Section) -> bool, 183 | { 184 | let mut sections = Module::iterate(Module::init_from_reader(reader)?)?.enumerate(); 185 | let signature_header_section = if let Some(detached_signature) = &detached_signature { 186 | Section::Custom(CustomSection::new( 187 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 188 | detached_signature.to_vec(), 189 | )) 190 | } else { 191 | sections.next().ok_or(WSError::ParseError)?.1? 192 | }; 193 | let signature_header = match signature_header_section { 194 | Section::Custom(custom_section) if custom_section.is_signature_header() => { 195 | custom_section 196 | } 197 | _ => { 198 | debug!("This module is not signed"); 199 | return Err(WSError::NoSignatures); 200 | } 201 | }; 202 | 203 | let signature_data = signature_header.signature_data()?; 204 | if signature_data.hash_function != SIGNATURE_HASH_FUNCTION { 205 | debug!( 206 | "Unsupported hash function: {:02x}", 207 | signature_data.hash_function 208 | ); 209 | return Err(WSError::ParseError); 210 | } 211 | 212 | let signed_hashes_set = signature_data.signed_hashes_set; 213 | let valid_hashes = self.valid_hashes_for_pk(&signed_hashes_set)?; 214 | if valid_hashes.is_empty() { 215 | debug!("No valid signatures"); 216 | return Err(WSError::VerificationFailed); 217 | } 218 | debug!("Hashes matching the signature:"); 219 | for valid_hash in &valid_hashes { 220 | debug!(" - [{}]", Hex::encode_to_string(valid_hash).unwrap()); 221 | } 222 | let mut hasher = Hash::new(); 223 | let mut matching_section_ranges = vec![]; 224 | debug!("Computed hashes:"); 225 | let mut section_sequence_must_be_signed: Option = None; 226 | for (idx, section) in sections { 227 | let section = section?; 228 | section.serialize(&mut hasher)?; 229 | if section.is_signature_delimiter() { 230 | if section_sequence_must_be_signed == Some(false) { 231 | section_sequence_must_be_signed = None; 232 | continue; 233 | } 234 | let h = hasher.finalize().to_vec(); 235 | debug!(" - [{}]", Hex::encode_to_string(&h).unwrap()); 236 | if !valid_hashes.contains(&h) { 237 | return Err(WSError::VerificationFailedForPredicates); 238 | } 239 | matching_section_ranges.push(0..=idx); 240 | section_sequence_must_be_signed = None; 241 | } else { 242 | let section_must_be_signed = predicate(§ion); 243 | match section_sequence_must_be_signed { 244 | None => section_sequence_must_be_signed = Some(section_must_be_signed), 245 | Some(false) if section_must_be_signed => { 246 | return Err(WSError::VerificationFailedForPredicates); 247 | } 248 | Some(true) if !section_must_be_signed => { 249 | return Err(WSError::VerificationFailedForPredicates); 250 | } 251 | _ => {} 252 | } 253 | } 254 | } 255 | debug!("Valid, signed ranges:"); 256 | for range in &matching_section_ranges { 257 | debug!(" - {}...{}", range.start(), range.end()); 258 | } 259 | Ok(()) 260 | } 261 | 262 | pub(crate) fn valid_hashes_for_pk<'t>( 263 | &self, 264 | signed_hashes_set: &'t [SignedHashes], 265 | ) -> Result>, WSError> { 266 | let mut valid_hashes = HashSet::new(); 267 | for signed_section_sequence in signed_hashes_set { 268 | let mut msg: Vec = vec![]; 269 | msg.extend_from_slice(SIGNATURE_WASM_DOMAIN.as_bytes()); 270 | msg.extend_from_slice(&[ 271 | SIGNATURE_VERSION, 272 | SIGNATURE_WASM_MODULE_CONTENT_TYPE, 273 | SIGNATURE_HASH_FUNCTION, 274 | ]); 275 | let hashes = &signed_section_sequence.hashes; 276 | for hash in hashes { 277 | msg.extend_from_slice(hash); 278 | } 279 | for signature in &signed_section_sequence.signatures { 280 | match (&signature.key_id, &self.key_id) { 281 | (Some(signature_key_id), Some(pk_key_id)) if signature_key_id != pk_key_id => { 282 | continue; 283 | } 284 | _ => {} 285 | } 286 | if self 287 | .pk 288 | .verify( 289 | &msg, 290 | &ed25519_compact::Signature::from_slice(&signature.signature)?, 291 | ) 292 | .is_err() 293 | { 294 | continue; 295 | } 296 | debug!( 297 | "Hash signature is valid for key [{}]", 298 | Hex::encode_to_string(*self.pk).unwrap() 299 | ); 300 | for hash in hashes { 301 | valid_hashes.insert(hash); 302 | } 303 | } 304 | } 305 | Ok(valid_hashes) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/lib/src/signature/sig_sections.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | use std::io::{prelude::*, BufReader, BufWriter}; 3 | 4 | use crate::error::*; 5 | use crate::wasm_module::*; 6 | use crate::ED25519_PK_ID; 7 | use crate::SIGNATURE_VERSION; 8 | use crate::SIGNATURE_WASM_MODULE_CONTENT_TYPE; 9 | 10 | pub const SIGNATURE_SECTION_HEADER_NAME: &str = "signature"; 11 | pub const SIGNATURE_SECTION_DELIMITER_NAME: &str = "signature_delimiter"; 12 | 13 | pub const MAX_HASHES: usize = 64; 14 | pub const MAX_SIGNATURES: usize = 256; 15 | 16 | #[derive(PartialEq, Debug, Clone, Eq)] 17 | pub struct SignatureForHashes { 18 | pub key_id: Option>, 19 | pub alg_id: u8, 20 | pub signature: Vec, 21 | } 22 | 23 | #[derive(PartialEq, Debug, Clone, Eq)] 24 | pub struct SignedHashes { 25 | pub hashes: Vec>, 26 | pub signatures: Vec, 27 | } 28 | 29 | #[derive(PartialEq, Debug, Clone, Eq)] 30 | pub struct SignatureData { 31 | pub specification_version: u8, 32 | pub content_type: u8, 33 | pub hash_function: u8, 34 | pub signed_hashes_set: Vec, 35 | } 36 | 37 | impl SignatureForHashes { 38 | pub fn serialize(&self) -> Result, WSError> { 39 | let mut writer = BufWriter::new(Vec::new()); 40 | if let Some(key_id) = &self.key_id { 41 | varint::put_slice(&mut writer, key_id)?; 42 | } else { 43 | varint::put(&mut writer, 0)?; 44 | } 45 | writer.write_all(&[self.alg_id])?; 46 | varint::put_slice(&mut writer, &self.signature)?; 47 | Ok(writer.into_inner().unwrap()) 48 | } 49 | 50 | pub fn deserialize(bin: impl AsRef<[u8]>) -> Result { 51 | let mut reader = BufReader::new(bin.as_ref()); 52 | let key_id = varint::get_slice(&mut reader)?; 53 | let key_id = if key_id.is_empty() { 54 | None 55 | } else { 56 | Some(key_id) 57 | }; 58 | let mut alg_id = [0u8; 1]; 59 | reader.read_exact(&mut alg_id)?; 60 | let alg_id = alg_id[0]; 61 | if alg_id != ED25519_PK_ID { 62 | debug!("Unsupported algorithm: {:02x}", alg_id); 63 | return Err(WSError::ParseError); 64 | } 65 | let signature = varint::get_slice(&mut reader)?; 66 | Ok(Self { 67 | key_id, 68 | alg_id, 69 | signature, 70 | }) 71 | } 72 | } 73 | 74 | impl SignedHashes { 75 | pub fn serialize(&self) -> Result, WSError> { 76 | let mut writer = BufWriter::new(Vec::new()); 77 | varint::put(&mut writer, self.hashes.len() as _)?; 78 | for hash in &self.hashes { 79 | writer.write_all(hash)?; 80 | } 81 | varint::put(&mut writer, self.signatures.len() as _)?; 82 | for signature in &self.signatures { 83 | varint::put_slice(&mut writer, &signature.serialize()?)?; 84 | } 85 | Ok(writer.into_inner().unwrap()) 86 | } 87 | 88 | pub fn deserialize(bin: impl AsRef<[u8]>) -> Result { 89 | let mut reader = BufReader::new(bin.as_ref()); 90 | let hashes_count = varint::get32(&mut reader)? as _; 91 | if hashes_count > MAX_HASHES { 92 | debug!("Too many hashes: {} (max: {})", hashes_count, MAX_HASHES); 93 | return Err(WSError::TooManyHashes(MAX_HASHES)); 94 | } 95 | let mut hashes = Vec::with_capacity(hashes_count); 96 | for _ in 0..hashes_count { 97 | let mut hash = vec![0; 32]; 98 | reader.read_exact(&mut hash)?; 99 | hashes.push(hash); 100 | } 101 | let signatures_count = varint::get32(&mut reader)? as _; 102 | if signatures_count > MAX_SIGNATURES { 103 | debug!( 104 | "Too many signatures: {} (max: {})", 105 | signatures_count, MAX_SIGNATURES 106 | ); 107 | return Err(WSError::TooManySignatures(MAX_SIGNATURES)); 108 | } 109 | let mut signatures = Vec::with_capacity(signatures_count); 110 | for _ in 0..signatures_count { 111 | let bin = varint::get_slice(&mut reader)?; 112 | if let Ok(signature) = SignatureForHashes::deserialize(bin) { 113 | signatures.push(signature); 114 | } 115 | } 116 | Ok(Self { hashes, signatures }) 117 | } 118 | } 119 | 120 | impl SignatureData { 121 | pub fn serialize(&self) -> Result, WSError> { 122 | let mut writer = BufWriter::new(Vec::new()); 123 | varint::put(&mut writer, self.specification_version as _)?; 124 | varint::put(&mut writer, self.content_type as _)?; 125 | varint::put(&mut writer, self.hash_function as _)?; 126 | varint::put(&mut writer, self.signed_hashes_set.len() as _)?; 127 | for signed_hashes in &self.signed_hashes_set { 128 | varint::put_slice(&mut writer, &signed_hashes.serialize()?)?; 129 | } 130 | Ok(writer.into_inner().unwrap()) 131 | } 132 | 133 | pub fn deserialize(bin: impl AsRef<[u8]>) -> Result { 134 | let mut reader = BufReader::new(bin.as_ref()); 135 | let specification_version = varint::get7(&mut reader)?; 136 | if specification_version != SIGNATURE_VERSION { 137 | debug!( 138 | "Unsupported specification version: {:02x}", 139 | specification_version 140 | ); 141 | return Err(WSError::ParseError); 142 | } 143 | let content_type = varint::get7(&mut reader)?; 144 | if content_type != SIGNATURE_WASM_MODULE_CONTENT_TYPE { 145 | debug!("Unsupported content type: {:02x}", content_type); 146 | return Err(WSError::ParseError); 147 | } 148 | let hash_function = varint::get7(&mut reader)?; 149 | let signed_hashes_count = varint::get32(&mut reader)? as _; 150 | if signed_hashes_count > MAX_HASHES { 151 | debug!( 152 | "Too many hashes: {} (max: {})", 153 | signed_hashes_count, MAX_HASHES 154 | ); 155 | return Err(WSError::TooManyHashes(MAX_HASHES)); 156 | } 157 | let mut signed_hashes_set = Vec::with_capacity(signed_hashes_count); 158 | for _ in 0..signed_hashes_count { 159 | let bin = varint::get_slice(&mut reader)?; 160 | let signed_hashes = SignedHashes::deserialize(bin)?; 161 | signed_hashes_set.push(signed_hashes); 162 | } 163 | Ok(Self { 164 | specification_version, 165 | content_type, 166 | hash_function, 167 | signed_hashes_set, 168 | }) 169 | } 170 | } 171 | 172 | pub fn new_delimiter_section() -> Result { 173 | let mut custom_payload = vec![0u8; 16]; 174 | getrandom::getrandom(&mut custom_payload) 175 | .map_err(|_| WSError::InternalError("RNG error".to_string()))?; 176 | Ok(Section::Custom(CustomSection::new( 177 | SIGNATURE_SECTION_DELIMITER_NAME.to_string(), 178 | custom_payload, 179 | ))) 180 | } 181 | -------------------------------------------------------------------------------- /src/lib/src/signature/simple.rs: -------------------------------------------------------------------------------- 1 | use crate::signature::*; 2 | use crate::wasm_module::*; 3 | use crate::*; 4 | 5 | use log::*; 6 | use std::collections::{HashMap, HashSet}; 7 | use std::io::Read; 8 | 9 | impl SecretKey { 10 | /// Sign a module with the secret key. 11 | /// 12 | /// If the module was already signed, the signature is replaced. 13 | /// 14 | /// `key_id` is the key identifier of the public key, to be stored with the signature. 15 | /// This parameter is optional. 16 | pub fn sign(&self, mut module: Module, key_id: Option<&Vec>) -> Result { 17 | let mut out_sections = vec![Section::Custom(CustomSection::default())]; 18 | let mut hasher = Hash::new(); 19 | for section in module.sections.into_iter() { 20 | if section.is_signature_header() { 21 | continue; 22 | } 23 | section.serialize(&mut hasher)?; 24 | out_sections.push(section); 25 | } 26 | let h = hasher.finalize().to_vec(); 27 | 28 | let mut msg: Vec = vec![]; 29 | msg.extend_from_slice(SIGNATURE_WASM_DOMAIN.as_bytes()); 30 | msg.extend_from_slice(&[ 31 | SIGNATURE_VERSION, 32 | SIGNATURE_WASM_MODULE_CONTENT_TYPE, 33 | SIGNATURE_HASH_FUNCTION, 34 | ]); 35 | msg.extend_from_slice(&h); 36 | 37 | let signature = self.sk.sign(msg, None).to_vec(); 38 | 39 | let signature_for_hashes = SignatureForHashes { 40 | key_id: key_id.cloned(), 41 | alg_id: ED25519_PK_ID, 42 | signature, 43 | }; 44 | let signed_hashes_set = vec![SignedHashes { 45 | hashes: vec![h], 46 | signatures: vec![signature_for_hashes], 47 | }]; 48 | let signature_data = SignatureData { 49 | specification_version: SIGNATURE_VERSION, 50 | content_type: SIGNATURE_WASM_MODULE_CONTENT_TYPE, 51 | hash_function: SIGNATURE_HASH_FUNCTION, 52 | signed_hashes_set, 53 | }; 54 | out_sections[0] = Section::Custom(CustomSection::new( 55 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 56 | signature_data.serialize()?, 57 | )); 58 | 59 | module.sections = out_sections; 60 | Ok(module) 61 | } 62 | } 63 | 64 | impl PublicKey { 65 | /// Verify a module's signature. 66 | /// 67 | /// `reader` is a reader over the raw module data. 68 | /// 69 | /// `detached_signature` allows the caller to verify a module without an embedded signature. 70 | /// 71 | /// This simplified interface verifies the entire module, with a single public key. 72 | pub fn verify( 73 | &self, 74 | reader: &mut impl Read, 75 | detached_signature: Option<&[u8]>, 76 | ) -> Result<(), WSError> { 77 | let stream = Module::init_from_reader(reader)?; 78 | let mut sections = Module::iterate(stream)?; 79 | 80 | // Read the signature header from the module, or reconstruct it from the detached signature. 81 | let signature_header_section = if let Some(detached_signature) = &detached_signature { 82 | Section::Custom(CustomSection::new( 83 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 84 | detached_signature.to_vec(), 85 | )) 86 | } else { 87 | sections.next().ok_or(WSError::ParseError)?? 88 | }; 89 | let signature_header = match signature_header_section { 90 | Section::Custom(custom_section) if custom_section.is_signature_header() => { 91 | custom_section 92 | } 93 | _ => { 94 | debug!("This module is not signed"); 95 | return Err(WSError::NoSignatures); 96 | } 97 | }; 98 | 99 | // Actual signature verification starts here. 100 | let signature_data = signature_header.signature_data()?; 101 | if signature_data.hash_function != SIGNATURE_HASH_FUNCTION { 102 | debug!( 103 | "Unsupported hash function: {:02x}", 104 | signature_data.specification_version 105 | ); 106 | return Err(WSError::ParseError); 107 | } 108 | 109 | let signed_hashes_set = signature_data.signed_hashes_set; 110 | let valid_hashes = self.valid_hashes_for_pk(&signed_hashes_set)?; 111 | if valid_hashes.is_empty() { 112 | debug!("No valid signatures"); 113 | return Err(WSError::VerificationFailed); 114 | } 115 | 116 | let mut hasher = Hash::new(); 117 | let mut buf = vec![0u8; 65536]; 118 | loop { 119 | match reader.read(&mut buf)? { 120 | 0 => break, 121 | n => { 122 | hasher.update(&buf[..n]); 123 | } 124 | } 125 | } 126 | let h = hasher.finalize().to_vec(); 127 | 128 | if valid_hashes.contains(&h) { 129 | Ok(()) 130 | } else { 131 | Err(WSError::VerificationFailed) 132 | } 133 | } 134 | } 135 | 136 | impl PublicKeySet { 137 | /// Verify a module's signature with multiple public keys. 138 | /// 139 | /// `reader` is a reader over the raw module data. 140 | /// 141 | /// `detached_signature` allows the caller to verify a module without an embedded signature. 142 | /// 143 | /// This simplified interface verifies the entire module, with all public keys from the set. 144 | /// It returns the set of public keys for which a valid signature was found. 145 | pub fn verify( 146 | &self, 147 | reader: &mut impl Read, 148 | detached_signature: Option<&[u8]>, 149 | ) -> Result, WSError> { 150 | let mut sections = Module::iterate(Module::init_from_reader(reader)?)?; 151 | 152 | // Read the signature header from the module, or reconstruct it from the detached signature. 153 | let signature_header: &Section; 154 | let signature_header_from_detached_signature; 155 | let signature_header_from_stream; 156 | if let Some(detached_signature) = &detached_signature { 157 | signature_header_from_detached_signature = Section::Custom(CustomSection::new( 158 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 159 | detached_signature.to_vec(), 160 | )); 161 | signature_header = &signature_header_from_detached_signature; 162 | } else { 163 | signature_header_from_stream = sections.next().ok_or(WSError::ParseError)??; 164 | signature_header = &signature_header_from_stream; 165 | } 166 | let signature_header = match signature_header { 167 | Section::Custom(custom_section) if custom_section.is_signature_header() => { 168 | custom_section 169 | } 170 | _ => { 171 | debug!("This module is not signed"); 172 | return Err(WSError::NoSignatures); 173 | } 174 | }; 175 | 176 | // Actual signature verification starts here. 177 | let signature_data = signature_header.signature_data()?; 178 | if signature_data.content_type != SIGNATURE_WASM_MODULE_CONTENT_TYPE { 179 | debug!( 180 | "Unsupported content type: {:02x}", 181 | signature_data.content_type 182 | ); 183 | return Err(WSError::ParseError); 184 | } 185 | if signature_data.hash_function != SIGNATURE_HASH_FUNCTION { 186 | debug!( 187 | "Unsupported hash function: {:02x}", 188 | signature_data.specification_version 189 | ); 190 | return Err(WSError::ParseError); 191 | } 192 | let signed_hashes_set = signature_data.signed_hashes_set; 193 | let valid_hashes_for_pks: HashMap<&PublicKey, HashSet<&Vec>> = self 194 | .pks 195 | .iter() 196 | .filter_map(|pk| match pk.valid_hashes_for_pk(&signed_hashes_set) { 197 | Ok(valid_hashes) if !valid_hashes.is_empty() => Some((pk, valid_hashes)), 198 | _ => None, 199 | }) 200 | .collect(); 201 | if valid_hashes_for_pks.is_empty() { 202 | debug!("No valid signatures"); 203 | return Err(WSError::VerificationFailed); 204 | } 205 | 206 | let mut hasher = Hash::new(); 207 | let mut buf = vec![0u8; 65536]; 208 | loop { 209 | match reader.read(&mut buf)? { 210 | 0 => break, 211 | n => { 212 | hasher.update(&buf[..n]); 213 | } 214 | } 215 | } 216 | let h = hasher.finalize().to_vec(); 217 | let mut valid_pks = HashSet::new(); 218 | for (pk, valid_hashes) in valid_hashes_for_pks { 219 | if valid_hashes.contains(&h) { 220 | valid_pks.insert(pk); 221 | } 222 | } 223 | if valid_pks.is_empty() { 224 | debug!("No valid signatures"); 225 | return Err(WSError::VerificationFailed); 226 | } 227 | Ok(valid_pks) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/lib/src/split.rs: -------------------------------------------------------------------------------- 1 | use crate::signature::*; 2 | use crate::wasm_module::*; 3 | 4 | use log::*; 5 | 6 | impl Module { 7 | /// Print the structure of a module to the standard output, mainly for debugging purposes. 8 | /// 9 | /// Set `verbose` to `true` in order to also print details about signature data. 10 | pub fn show(&self, verbose: bool) -> Result<(), WSError> { 11 | for (idx, section) in self.sections.iter().enumerate() { 12 | println!("{}:\t{}", idx, section.display(verbose)); 13 | } 14 | Ok(()) 15 | } 16 | 17 | /// Prepare a module for partial verification. 18 | /// 19 | /// The predicate should return `true` if a section is part of a set that can be verified, 20 | /// and `false` if the section can be ignored during verification. 21 | /// 22 | /// It is highly recommended to always include the standard sections in the signed set. 23 | pub fn split

(self, mut predicate: P) -> Result 24 | where 25 | P: FnMut(&Section) -> bool, 26 | { 27 | let mut out_sections = vec![]; 28 | let mut flip = false; 29 | let mut last_was_delimiter = false; 30 | for (idx, section) in self.sections.into_iter().enumerate() { 31 | if section.is_signature_header() { 32 | info!("Module is already signed"); 33 | out_sections.push(section); 34 | continue; 35 | } 36 | if section.is_signature_delimiter() { 37 | out_sections.push(section); 38 | last_was_delimiter = true; 39 | continue; 40 | } 41 | let section_can_be_signed = predicate(§ion); 42 | if idx == 0 { 43 | flip = !section_can_be_signed; 44 | } else if section_can_be_signed == flip { 45 | if !last_was_delimiter { 46 | let delimiter = new_delimiter_section()?; 47 | out_sections.push(delimiter); 48 | } 49 | flip = !flip; 50 | } 51 | out_sections.push(section); 52 | last_was_delimiter = false; 53 | } 54 | if let Some(last_section) = out_sections.last() { 55 | if !last_section.is_signature_delimiter() { 56 | let delimiter = new_delimiter_section()?; 57 | out_sections.push(delimiter); 58 | } 59 | } 60 | Ok(Module { 61 | header: self.header, 62 | sections: out_sections, 63 | }) 64 | } 65 | 66 | /// Detach the signature from a signed module. 67 | /// 68 | /// This function returns the module without the embedded signature, 69 | /// as well as the detached signature as a byte string. 70 | pub fn detach_signature(mut self) -> Result<(Module, Vec), WSError> { 71 | let mut out_sections = vec![]; 72 | let mut sections = self.sections.into_iter(); 73 | let detached_signature = match sections.next() { 74 | None => return Err(WSError::NoSignatures), 75 | Some(section) => { 76 | if !section.is_signature_header() { 77 | return Err(WSError::NoSignatures); 78 | } 79 | section.payload().to_vec() 80 | } 81 | }; 82 | for section in sections { 83 | out_sections.push(section); 84 | } 85 | self.sections = out_sections; 86 | debug!("Signature detached"); 87 | Ok((self, detached_signature)) 88 | } 89 | 90 | /// Embed a detached signature into a module. 91 | /// This function returns the module with embedded signature. 92 | pub fn attach_signature(mut self, detached_signature: &[u8]) -> Result { 93 | let mut out_sections = vec![]; 94 | let sections = self.sections.into_iter(); 95 | let signature_header = Section::Custom(CustomSection::new( 96 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 97 | detached_signature.to_vec(), 98 | )); 99 | out_sections.push(signature_header); 100 | for section in sections { 101 | if section.is_signature_header() { 102 | return Err(WSError::SignatureAlreadyAttached); 103 | } 104 | out_sections.push(section); 105 | } 106 | self.sections = out_sections; 107 | debug!("Signature attached"); 108 | Ok(self) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/lib/src/wasm_module/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod varint; 2 | 3 | use crate::signature::*; 4 | 5 | use ct_codecs::{Encoder, Hex}; 6 | use std::fmt::{self, Write as _}; 7 | use std::fs::File; 8 | use std::io::{self, prelude::*, BufReader, BufWriter}; 9 | use std::path::Path; 10 | use std::str; 11 | 12 | const WASM_HEADER: [u8; 8] = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; 13 | const WASM_COMPONENT_HEADER: [u8; 8] = [0x00, 0x61, 0x73, 0x6d, 0x0d, 0x00, 0x01, 0x00]; 14 | pub type Header = [u8; 8]; 15 | 16 | /// A section identifier. 17 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 18 | #[repr(u8)] 19 | pub enum SectionId { 20 | CustomSection, 21 | Type, 22 | Import, 23 | Function, 24 | Table, 25 | Memory, 26 | Global, 27 | Export, 28 | Start, 29 | Element, 30 | Code, 31 | Data, 32 | Extension(u8), 33 | } 34 | 35 | impl From for SectionId { 36 | fn from(v: u8) -> Self { 37 | match v { 38 | 0 => SectionId::CustomSection, 39 | 1 => SectionId::Type, 40 | 2 => SectionId::Import, 41 | 3 => SectionId::Function, 42 | 4 => SectionId::Table, 43 | 5 => SectionId::Memory, 44 | 6 => SectionId::Global, 45 | 7 => SectionId::Export, 46 | 8 => SectionId::Start, 47 | 9 => SectionId::Element, 48 | 10 => SectionId::Code, 49 | 11 => SectionId::Data, 50 | x => SectionId::Extension(x), 51 | } 52 | } 53 | } 54 | 55 | impl From for u8 { 56 | fn from(v: SectionId) -> Self { 57 | match v { 58 | SectionId::CustomSection => 0, 59 | SectionId::Type => 1, 60 | SectionId::Import => 2, 61 | SectionId::Function => 3, 62 | SectionId::Table => 4, 63 | SectionId::Memory => 5, 64 | SectionId::Global => 6, 65 | SectionId::Export => 7, 66 | SectionId::Start => 8, 67 | SectionId::Element => 9, 68 | SectionId::Code => 10, 69 | SectionId::Data => 11, 70 | SectionId::Extension(x) => x, 71 | } 72 | } 73 | } 74 | 75 | impl fmt::Display for SectionId { 76 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 77 | match self { 78 | SectionId::CustomSection => write!(f, "custom section"), 79 | SectionId::Type => write!(f, "types section"), 80 | SectionId::Import => write!(f, "imports section"), 81 | SectionId::Function => write!(f, "functions section"), 82 | SectionId::Table => write!(f, "table section"), 83 | SectionId::Memory => write!(f, "memory section"), 84 | SectionId::Global => write!(f, "global section"), 85 | SectionId::Export => write!(f, "exports section"), 86 | SectionId::Start => write!(f, "start section"), 87 | SectionId::Element => write!(f, "elements section"), 88 | SectionId::Code => write!(f, "code section"), 89 | SectionId::Data => write!(f, "data section"), 90 | SectionId::Extension(x) => write!(f, "section id#{x}"), 91 | } 92 | } 93 | } 94 | 95 | /// Common functions for a module section. 96 | pub trait SectionLike { 97 | fn id(&self) -> SectionId; 98 | fn payload(&self) -> &[u8]; 99 | fn display(&self, verbose: bool) -> String; 100 | } 101 | 102 | /// A standard section. 103 | #[derive(Debug, Clone)] 104 | pub struct StandardSection { 105 | id: SectionId, 106 | payload: Vec, 107 | } 108 | 109 | impl StandardSection { 110 | /// Create a new standard section. 111 | pub fn new(id: SectionId, payload: Vec) -> Self { 112 | Self { id, payload } 113 | } 114 | } 115 | 116 | impl SectionLike for StandardSection { 117 | /// Return the identifier of the section. 118 | fn id(&self) -> SectionId { 119 | self.id 120 | } 121 | 122 | /// Return the payload of the section. 123 | fn payload(&self) -> &[u8] { 124 | &self.payload 125 | } 126 | 127 | /// Human-readable representation of the section. 128 | fn display(&self, _verbose: bool) -> String { 129 | self.id().to_string() 130 | } 131 | } 132 | 133 | /// A custom section. 134 | #[derive(Debug, Clone, Default)] 135 | pub struct CustomSection { 136 | name: String, 137 | payload: Vec, 138 | } 139 | 140 | impl CustomSection { 141 | /// Create a new custom section. 142 | pub fn new(name: String, payload: Vec) -> Self { 143 | Self { name, payload } 144 | } 145 | 146 | /// Return the name of the custom section. 147 | pub fn name(&self) -> &str { 148 | &self.name 149 | } 150 | 151 | /// Return the custom section as an array of bytes. 152 | /// 153 | /// This includes the data itself, but also the size and name of the custom section. 154 | pub fn outer_payload(&self) -> Result, WSError> { 155 | let mut writer = io::Cursor::new(vec![]); 156 | varint::put(&mut writer, self.name.len() as _)?; 157 | writer.write_all(self.name.as_bytes())?; 158 | writer.write_all(&self.payload)?; 159 | Ok(writer.into_inner()) 160 | } 161 | } 162 | 163 | impl SectionLike for CustomSection { 164 | fn id(&self) -> SectionId { 165 | SectionId::CustomSection 166 | } 167 | 168 | fn payload(&self) -> &[u8] { 169 | &self.payload 170 | } 171 | 172 | fn display(&self, verbose: bool) -> String { 173 | if !verbose { 174 | return format!("custom section: [{}]", self.name()); 175 | } 176 | 177 | match self.name() { 178 | SIGNATURE_SECTION_DELIMITER_NAME => format!( 179 | "custom section: [{}]\n- delimiter: [{}]\n", 180 | self.name, 181 | Hex::encode_to_string(self.payload()).unwrap() 182 | ), 183 | SIGNATURE_SECTION_HEADER_NAME => { 184 | let signature_data = match SignatureData::deserialize(self.payload()) { 185 | Ok(signature_data) => signature_data, 186 | _ => return "undecodable signature header".to_string(), 187 | }; 188 | let mut s = String::new(); 189 | writeln!( 190 | s, 191 | "- specification version: 0x{:02x}", 192 | signature_data.specification_version, 193 | ) 194 | .unwrap(); 195 | writeln!(s, "- content_type: 0x{:02x}", signature_data.content_type,).unwrap(); 196 | writeln!( 197 | s, 198 | "- hash function: 0x{:02x} (SHA-256)", 199 | signature_data.hash_function 200 | ) 201 | .unwrap(); 202 | writeln!(s, "- (hashes,signatures) set:").unwrap(); 203 | for signed_parts in &signature_data.signed_hashes_set { 204 | writeln!(s, " - hashes:").unwrap(); 205 | for hash in &signed_parts.hashes { 206 | writeln!(s, " - [{}]", Hex::encode_to_string(hash).unwrap()).unwrap(); 207 | } 208 | writeln!(s, " - signatures:").unwrap(); 209 | for signature in &signed_parts.signatures { 210 | write!( 211 | s, 212 | " - [{}]", 213 | Hex::encode_to_string(&signature.signature).unwrap() 214 | ) 215 | .unwrap(); 216 | match &signature.key_id { 217 | None => writeln!(s, " (no key id)").unwrap(), 218 | Some(key_id) => writeln!( 219 | s, 220 | " (key id: [{}])", 221 | Hex::encode_to_string(key_id).unwrap() 222 | ) 223 | .unwrap(), 224 | } 225 | } 226 | } 227 | format!("custom section: [{}]\n{}", self.name(), s) 228 | } 229 | _ => format!("custom section: [{}]", self.name()), 230 | } 231 | } 232 | } 233 | 234 | /// A WebAssembly module section. 235 | /// 236 | /// It is recommended to import the `SectionLike` trait for additional functions. 237 | #[derive(Clone)] 238 | pub enum Section { 239 | /// A standard section. 240 | Standard(StandardSection), 241 | /// A custom section. 242 | Custom(CustomSection), 243 | } 244 | 245 | impl SectionLike for Section { 246 | fn id(&self) -> SectionId { 247 | match self { 248 | Section::Standard(s) => s.id(), 249 | Section::Custom(s) => s.id(), 250 | } 251 | } 252 | 253 | fn payload(&self) -> &[u8] { 254 | match self { 255 | Section::Standard(s) => s.payload(), 256 | Section::Custom(s) => s.payload(), 257 | } 258 | } 259 | 260 | fn display(&self, verbose: bool) -> String { 261 | match self { 262 | Section::Standard(s) => s.display(verbose), 263 | Section::Custom(s) => s.display(verbose), 264 | } 265 | } 266 | } 267 | 268 | impl Section { 269 | /// Create a new section with the given identifier and payload. 270 | pub fn new(id: SectionId, payload: Vec) -> Result { 271 | match id { 272 | SectionId::CustomSection => { 273 | let mut reader = io::Cursor::new(payload); 274 | let name_len = varint::get32(&mut reader)? as usize; 275 | let mut name_slice = vec![0u8; name_len]; 276 | reader.read_exact(&mut name_slice)?; 277 | let name = str::from_utf8(&name_slice)?.to_string(); 278 | let mut payload = Vec::new(); 279 | let len = reader.read_to_end(&mut payload)?; 280 | payload.truncate(len); 281 | Ok(Section::Custom(CustomSection::new(name, payload))) 282 | } 283 | _ => Ok(Section::Standard(StandardSection::new(id, payload))), 284 | } 285 | } 286 | 287 | /// Create a section from its standard serialized representation. 288 | pub fn deserialize(reader: &mut impl Read) -> Result, WSError> { 289 | let id = match varint::get7(reader) { 290 | Ok(id) => SectionId::from(id), 291 | Err(WSError::Eof) => return Ok(None), 292 | Err(e) => return Err(e), 293 | }; 294 | let len = varint::get32(reader)? as usize; 295 | let mut payload = vec![0u8; len]; 296 | reader.read_exact(&mut payload)?; 297 | let section = Section::new(id, payload)?; 298 | Ok(Some(section)) 299 | } 300 | 301 | /// Serialize a section. 302 | pub fn serialize(&self, writer: &mut impl Write) -> Result<(), WSError> { 303 | let outer_payload; 304 | let payload = match self { 305 | Section::Standard(s) => s.payload(), 306 | Section::Custom(s) => { 307 | outer_payload = s.outer_payload()?; 308 | &outer_payload 309 | } 310 | }; 311 | varint::put(writer, u8::from(self.id()) as _)?; 312 | varint::put(writer, payload.len() as _)?; 313 | writer.write_all(payload)?; 314 | Ok(()) 315 | } 316 | 317 | /// Return `true` if the section contains the module's signatures. 318 | pub fn is_signature_header(&self) -> bool { 319 | match self { 320 | Section::Standard(_) => false, 321 | Section::Custom(s) => s.is_signature_header(), 322 | } 323 | } 324 | 325 | /// Return `true` if the section is a signature delimiter. 326 | pub fn is_signature_delimiter(&self) -> bool { 327 | match self { 328 | Section::Standard(_) => false, 329 | Section::Custom(s) => s.is_signature_delimiter(), 330 | } 331 | } 332 | } 333 | 334 | impl CustomSection { 335 | /// Return `true` if the section contains the module's signatures. 336 | pub fn is_signature_header(&self) -> bool { 337 | self.name() == SIGNATURE_SECTION_HEADER_NAME 338 | } 339 | 340 | /// Return `true` if the section is a signature delimiter. 341 | pub fn is_signature_delimiter(&self) -> bool { 342 | self.name() == SIGNATURE_SECTION_DELIMITER_NAME 343 | } 344 | 345 | /// If the section contains the module's signature, deserializes it into a `SignatureData` object 346 | /// containing the signatures and the hashes. 347 | pub fn signature_data(&self) -> Result { 348 | let header_payload = 349 | SignatureData::deserialize(self.payload()).map_err(|_| WSError::ParseError)?; 350 | Ok(header_payload) 351 | } 352 | } 353 | 354 | impl fmt::Display for Section { 355 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 356 | write!(f, "{}", self.display(false)) 357 | } 358 | } 359 | 360 | impl fmt::Debug for Section { 361 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 362 | write!(f, "{}", self.display(true)) 363 | } 364 | } 365 | 366 | /// A WebAssembly module. 367 | #[derive(Debug, Clone, Default)] 368 | pub struct Module { 369 | pub header: Header, 370 | pub sections: Vec

, 371 | } 372 | 373 | impl Module { 374 | /// Deserialize a WebAssembly module from the given reader. 375 | pub fn deserialize(reader: &mut impl Read) -> Result { 376 | let stream = Self::init_from_reader(reader)?; 377 | let header = stream.header; 378 | let it = Self::iterate(stream)?; 379 | let mut sections = Vec::new(); 380 | for section in it { 381 | sections.push(section?); 382 | } 383 | Ok(Module { header, sections }) 384 | } 385 | 386 | /// Deserialize a WebAssembly module from the given file. 387 | pub fn deserialize_from_file(file: impl AsRef) -> Result { 388 | let fp = File::open(file.as_ref())?; 389 | Self::deserialize(&mut BufReader::new(fp)) 390 | } 391 | 392 | /// Serialize a WebAssembly module to the given writer. 393 | pub fn serialize(&self, writer: &mut impl Write) -> Result<(), WSError> { 394 | writer.write_all(&self.header)?; 395 | for section in &self.sections { 396 | section.serialize(writer)?; 397 | } 398 | Ok(()) 399 | } 400 | 401 | /// Serialize a WebAssembly module to the given file. 402 | pub fn serialize_to_file(&self, file: impl AsRef) -> Result<(), WSError> { 403 | let fp = File::create(file.as_ref())?; 404 | self.serialize(&mut BufWriter::new(fp)) 405 | } 406 | 407 | /// Parse the module's header. This function must be called before `stream()`. 408 | pub fn init_from_reader(reader: &mut T) -> Result, WSError> { 409 | let mut header = Header::default(); 410 | reader.read_exact(&mut header)?; 411 | if header != WASM_HEADER && header != WASM_COMPONENT_HEADER { 412 | return Err(WSError::UnsupportedModuleType); 413 | } 414 | Ok(ModuleStreamReader { reader, header }) 415 | } 416 | 417 | /// Return an iterator over the sections of a WebAssembly module. 418 | /// 419 | /// The module is read in a streaming fashion, and doesn't have to be fully loaded into memory. 420 | pub fn iterate( 421 | module_stream: ModuleStreamReader, 422 | ) -> Result, WSError> { 423 | Ok(SectionsIterator { 424 | reader: module_stream.reader, 425 | }) 426 | } 427 | } 428 | 429 | pub struct ModuleStreamReader<'t, T: Read> { 430 | reader: &'t mut T, 431 | header: Header, 432 | } 433 | 434 | /// An iterator over the sections of a WebAssembly module. 435 | pub struct SectionsIterator<'t, T: Read> { 436 | reader: &'t mut T, 437 | } 438 | 439 | impl<'t, T: Read> Iterator for SectionsIterator<'t, T> { 440 | type Item = Result; 441 | 442 | fn next(&mut self) -> Option { 443 | match Section::deserialize(self.reader) { 444 | Err(e) => Some(Err(e)), 445 | Ok(None) => None, 446 | Ok(Some(section)) => Some(Ok(section)), 447 | } 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /src/lib/src/wasm_module/varint.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, prelude::*}; 2 | 3 | use crate::error::*; 4 | 5 | pub fn get7(reader: &mut impl Read) -> Result { 6 | let mut v: u8 = 0; 7 | for i in 0..1 { 8 | let mut byte = [0u8; 1]; 9 | if let Err(e) = reader.read_exact(&mut byte) { 10 | return Err(if e.kind() == io::ErrorKind::UnexpectedEof { 11 | WSError::Eof 12 | } else { 13 | e.into() 14 | }); 15 | }; 16 | v |= (byte[0] & 0x7f) << (i * 7); 17 | if (byte[0] & 0x80) == 0 { 18 | return Ok(v); 19 | } 20 | } 21 | Err(WSError::ParseError) 22 | } 23 | 24 | pub fn get32(reader: &mut impl Read) -> Result { 25 | let mut v: u32 = 0; 26 | for i in 0..5 { 27 | let mut byte = [0u8; 1]; 28 | reader.read_exact(&mut byte)?; 29 | v |= ((byte[0] & 0x7f) as u32) << (i * 7); 30 | if (byte[0] & 0x80) == 0 { 31 | return Ok(v); 32 | } 33 | } 34 | Err(WSError::ParseError) 35 | } 36 | 37 | pub fn put(writer: &mut impl Write, mut v: u64) -> Result<(), WSError> { 38 | let mut byte = [0u8; 1]; 39 | loop { 40 | byte[0] = (v & 0x7f) as u8; 41 | if v > 0x7f { 42 | byte[0] |= 0x80; 43 | } 44 | writer.write_all(&byte)?; 45 | v >>= 7; 46 | if v == 0 { 47 | return Ok(()); 48 | } 49 | } 50 | } 51 | 52 | pub fn put_slice(writer: &mut impl Write, bytes: impl AsRef<[u8]>) -> Result<(), WSError> { 53 | let bytes = bytes.as_ref(); 54 | put(writer, bytes.len() as _)?; 55 | writer.write_all(bytes)?; 56 | Ok(()) 57 | } 58 | 59 | pub fn get_slice(reader: &mut impl Read) -> Result, WSError> { 60 | let len = get32(reader)? as _; 61 | let mut bytes = vec![0u8; len]; 62 | reader.read_exact(&mut bytes)?; 63 | Ok(bytes) 64 | } 65 | --------------------------------------------------------------------------------