├── logo.png ├── .gitignore ├── src ├── lib.rs ├── lib │ ├── src │ │ ├── signature │ │ │ ├── mod.rs │ │ │ ├── hash.rs │ │ │ ├── matrix.rs │ │ │ ├── info.rs │ │ │ ├── sig_sections.rs │ │ │ ├── simple.rs │ │ │ ├── multi.rs │ │ │ └── keys.rs │ │ ├── lib.rs │ │ ├── wasm_module │ │ │ ├── varint.rs │ │ │ └── mod.rs │ │ ├── error.rs │ │ └── split.rs │ └── Cargo.toml └── cli │ └── main.rs ├── .github └── workflows │ └── ci.yml ├── Cargo.toml ├── .ci ├── test_show.sh ├── test_keygen.sh ├── run_all.sh ├── test_split.sh ├── test_verify_matrix.sh ├── test_multi_sign.sh ├── test_sign_verify.sh ├── test_detach_attach.sh └── common.sh └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasm-signatures/wasmsign2/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *~ 3 | Cargo.lock 4 | *.wasm 5 | *.key 6 | *.key2 7 | src/lib/target 8 | wasmsign2 9 | -------------------------------------------------------------------------------- /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/src/signature/mod.rs: -------------------------------------------------------------------------------- 1 | mod hash; 2 | mod info; 3 | mod keys; 4 | mod matrix; 5 | mod multi; 6 | mod sig_sections; 7 | mod simple; 8 | 9 | pub use info::*; 10 | pub use keys::*; 11 | pub use matrix::*; 12 | #[allow(unused_imports)] 13 | pub use multi::*; 14 | #[allow(unused_imports)] 15 | pub use simple::*; 16 | pub use sig_sections::{SignatureData, SignedHashes, SignatureForHashes}; 17 | 18 | pub(crate) use hash::*; 19 | pub(crate) use sig_sections::*; 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.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 | - name: Build release 24 | run: cargo build --release 25 | - name: Run functional tests 26 | run: ./.ci/run_all.sh 27 | -------------------------------------------------------------------------------- /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/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 | ct-codecs = "1.1.6" 16 | ed25519-compact = { version = "2.2.0", features = ["pem"] } 17 | getrandom = "0.3.4" 18 | hmac-sha256 = "1.1.12" 19 | log = "0.4.29" 20 | ssh-keys = { version = "0.1.4", optional = true } 21 | thiserror = "2.0.17" 22 | 23 | [features] 24 | default = ["openssh", "wasm_js"] 25 | openssh = ["ssh-keys"] 26 | wasm_js = ["getrandom/wasm_js"] 27 | 28 | [profile.release] 29 | codegen-units = 1 30 | incremental = false 31 | panic = "abort" 32 | -------------------------------------------------------------------------------- /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 = "4.5.53", default-features = false, features = [ 20 | "std", 21 | "cargo", 22 | "wrap_help", 23 | ] } 24 | env_logger = { version = "0.11.8", default-features = false, features = [ 25 | "humantime", 26 | ] } 27 | regex = "1.12.2" 28 | ureq = "3.1.4" 29 | uri_encode = "1.0.4" 30 | wasmsign2 = { version = "0.2.6", path = "src/lib" } 31 | 32 | [profile.release] 33 | codegen-units = 1 34 | incremental = false 35 | panic = "abort" 36 | -------------------------------------------------------------------------------- /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 {ct_codecs, getrandom, hmac_sha256, log, 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/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 byte = [0u8; 1]; 7 | if let Err(e) = reader.read_exact(&mut byte) { 8 | if e.kind() == io::ErrorKind::UnexpectedEof { 9 | return Err(WSError::Eof); 10 | } 11 | return Err(e.into()); 12 | } 13 | if (byte[0] & 0x80) != 0 { 14 | return Err(WSError::ParseError); 15 | } 16 | Ok(byte[0] & 0x7f) 17 | } 18 | 19 | pub fn get32(reader: &mut impl Read) -> Result { 20 | let mut v: u32 = 0; 21 | for i in 0..5 { 22 | let mut byte = [0u8; 1]; 23 | reader.read_exact(&mut byte)?; 24 | v |= ((byte[0] & 0x7f) as u32) << (i * 7); 25 | if (byte[0] & 0x80) == 0 { 26 | return Ok(v); 27 | } 28 | } 29 | Err(WSError::ParseError) 30 | } 31 | 32 | pub fn put(writer: &mut impl Write, mut v: u64) -> Result<(), WSError> { 33 | let mut byte = [0u8; 1]; 34 | loop { 35 | byte[0] = (v & 0x7f) as u8; 36 | if v > 0x7f { 37 | byte[0] |= 0x80; 38 | } 39 | writer.write_all(&byte)?; 40 | v >>= 7; 41 | if v == 0 { 42 | return Ok(()); 43 | } 44 | } 45 | } 46 | 47 | pub fn put_slice(writer: &mut impl Write, bytes: impl AsRef<[u8]>) -> Result<(), WSError> { 48 | let bytes = bytes.as_ref(); 49 | put(writer, bytes.len() as _)?; 50 | writer.write_all(bytes)?; 51 | Ok(()) 52 | } 53 | 54 | pub fn get_slice(reader: &mut impl Read) -> Result, WSError> { 55 | let len = get32(reader)? as _; 56 | let mut bytes = vec![0u8; len]; 57 | reader.read_exact(&mut bytes)?; 58 | Ok(bytes) 59 | } 60 | -------------------------------------------------------------------------------- /.ci/test_show.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Test show command functionality 3 | 4 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 5 | . "${SCRIPT_DIR}/common.sh" 6 | 7 | setup_test_env 8 | 9 | test_header "Show Command Tests" 10 | 11 | if [ -z "$SAMPLE_WASM" ]; then 12 | skip "No sample WASM file available" 13 | print_summary 14 | exit 0 15 | fi 16 | 17 | # Generate keys and create signed module 18 | "$WASMSIGN2" keygen -k "${TEST_DIR}/secret.key" -K "${TEST_DIR}/public.key" >/dev/null 2>&1 19 | "$WASMSIGN2" sign -k "${TEST_DIR}/secret.key" -i "$SAMPLE_WASM" -o "${TEST_DIR}/signed.wasm" >/dev/null 2>&1 20 | 21 | # Test basic show command 22 | assert_success "Show unsigned module structure" \ 23 | "$WASMSIGN2" show -i "$SAMPLE_WASM" 24 | 25 | assert_success "Show signed module structure" \ 26 | "$WASMSIGN2" show -i "${TEST_DIR}/signed.wasm" 27 | 28 | # Test verbose output 29 | assert_success "Show with verbose flag" \ 30 | "$WASMSIGN2" -v show -i "$SAMPLE_WASM" 31 | 32 | # Signed module should show signature section 33 | assert_output_contains "Show signed module includes signature section" "signature" \ 34 | "$WASMSIGN2" show -i "${TEST_DIR}/signed.wasm" 35 | 36 | # Test show on split module 37 | "$WASMSIGN2" split -i "$SAMPLE_WASM" -o "${TEST_DIR}/split.wasm" -s "name" >/dev/null 2>&1 38 | 39 | assert_success "Show split module structure" \ 40 | "$WASMSIGN2" show -i "${TEST_DIR}/split.wasm" 41 | 42 | # Test missing input file 43 | assert_failure "Show fails with missing input file" \ 44 | "$WASMSIGN2" show -i "${TEST_DIR}/nonexistent.wasm" 45 | 46 | # Test with invalid WASM file 47 | echo "not a wasm file" > "${TEST_DIR}/invalid.wasm" 48 | assert_failure "Show fails with invalid WASM file" \ 49 | "$WASMSIGN2" show -i "${TEST_DIR}/invalid.wasm" 50 | 51 | print_summary 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.ci/test_keygen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Test key generation functionality 3 | 4 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 5 | . "${SCRIPT_DIR}/common.sh" 6 | 7 | setup_test_env 8 | 9 | test_header "Key Generation Tests" 10 | 11 | # Test basic key generation 12 | assert_success "Generate key pair" \ 13 | "$WASMSIGN2" keygen -k "${TEST_DIR}/secret.key" -K "${TEST_DIR}/public.key" 14 | 15 | assert_file_exists "${TEST_DIR}/secret.key" 16 | assert_file_exists "${TEST_DIR}/public.key" 17 | 18 | # Check key file sizes (Ed25519 keys have specific sizes) 19 | sk_size=$(wc -c < "${TEST_DIR}/secret.key" | tr -d ' ') 20 | pk_size=$(wc -c < "${TEST_DIR}/public.key" | tr -d ' ') 21 | 22 | if [ "$sk_size" -eq 65 ]; then 23 | pass "Secret key has correct size (65 bytes)" 24 | else 25 | fail "Secret key has incorrect size: $sk_size (expected 65)" 26 | fi 27 | 28 | if [ "$pk_size" -eq 33 ]; then 29 | pass "Public key has correct size (33 bytes)" 30 | else 31 | fail "Public key has incorrect size: $pk_size (expected 33)" 32 | fi 33 | 34 | # Test generating multiple key pairs 35 | assert_success "Generate second key pair" \ 36 | "$WASMSIGN2" keygen -k "${TEST_DIR}/secret2.key" -K "${TEST_DIR}/public2.key" 37 | 38 | # Keys should be different 39 | assert_files_differ "${TEST_DIR}/secret.key" "${TEST_DIR}/secret2.key" 40 | assert_files_differ "${TEST_DIR}/public.key" "${TEST_DIR}/public2.key" 41 | 42 | # Test overwriting existing keys 43 | assert_success "Overwrite existing key pair" \ 44 | "$WASMSIGN2" keygen -k "${TEST_DIR}/secret.key" -K "${TEST_DIR}/public.key" 45 | 46 | # Test missing arguments 47 | assert_failure "Fail with missing secret key arg" \ 48 | "$WASMSIGN2" keygen -K "${TEST_DIR}/public3.key" 49 | 50 | assert_failure "Fail with missing public key arg" \ 51 | "$WASMSIGN2" keygen -k "${TEST_DIR}/secret3.key" 52 | 53 | print_summary 54 | -------------------------------------------------------------------------------- /.ci/run_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Main test runner for wasmsign2 functional tests 3 | 4 | set -e 5 | 6 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 7 | PROJECT_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) 8 | 9 | # Colors for output 10 | if [ -t 1 ]; then 11 | RED='\033[0;31m' 12 | GREEN='\033[0;32m' 13 | YELLOW='\033[0;33m' 14 | BLUE='\033[0;34m' 15 | NC='\033[0m' 16 | else 17 | RED='' 18 | GREEN='' 19 | YELLOW='' 20 | BLUE='' 21 | NC='' 22 | fi 23 | 24 | echo "${BLUE}========================================${NC}" 25 | echo "${BLUE} wasmsign2 Functional Test Suite${NC}" 26 | echo "${BLUE}========================================${NC}" 27 | 28 | # Build the project first 29 | echo "\n${YELLOW}Building project...${NC}" 30 | cd "$PROJECT_ROOT" 31 | if cargo build --release >/dev/null 2>&1; then 32 | echo "${GREEN}Build successful${NC}" 33 | else 34 | echo "${RED}Build failed${NC}" 35 | exit 1 36 | fi 37 | 38 | # Track overall results 39 | TOTAL_SUITES=0 40 | PASSED_SUITES=0 41 | FAILED_SUITES=0 42 | 43 | run_test_suite() { 44 | suite_name="$1" 45 | script_path="$2" 46 | 47 | TOTAL_SUITES=$((TOTAL_SUITES + 1)) 48 | 49 | echo "\n${BLUE}Running: $suite_name${NC}" 50 | echo "----------------------------------------" 51 | 52 | if sh "$script_path"; then 53 | PASSED_SUITES=$((PASSED_SUITES + 1)) 54 | echo "${GREEN}$suite_name: PASSED${NC}" 55 | else 56 | FAILED_SUITES=$((FAILED_SUITES + 1)) 57 | echo "${RED}$suite_name: FAILED${NC}" 58 | fi 59 | } 60 | 61 | # Run all test suites 62 | run_test_suite "Key Generation Tests" "$SCRIPT_DIR/test_keygen.sh" 63 | run_test_suite "Sign/Verify Tests" "$SCRIPT_DIR/test_sign_verify.sh" 64 | run_test_suite "Multi-Signature Tests" "$SCRIPT_DIR/test_multi_sign.sh" 65 | run_test_suite "Split Tests" "$SCRIPT_DIR/test_split.sh" 66 | run_test_suite "Detach/Attach Tests" "$SCRIPT_DIR/test_detach_attach.sh" 67 | run_test_suite "Verify Matrix Tests" "$SCRIPT_DIR/test_verify_matrix.sh" 68 | run_test_suite "Show Command Tests" "$SCRIPT_DIR/test_show.sh" 69 | 70 | # Print overall summary 71 | echo "\n${BLUE}========================================${NC}" 72 | echo "${BLUE} Overall Summary${NC}" 73 | echo "${BLUE}========================================${NC}" 74 | echo "${GREEN}Passed Suites${NC}: $PASSED_SUITES" 75 | echo "${RED}Failed Suites${NC}: $FAILED_SUITES" 76 | echo "Total Suites: $TOTAL_SUITES" 77 | 78 | if [ "$FAILED_SUITES" -gt 0 ]; then 79 | echo "\n${RED}Some tests failed!${NC}" 80 | exit 1 81 | else 82 | echo "\n${GREEN}All tests passed!${NC}" 83 | exit 0 84 | fi 85 | -------------------------------------------------------------------------------- /.ci/test_split.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Test cutting points (split) and partial verification 3 | 4 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 5 | . "${SCRIPT_DIR}/common.sh" 6 | 7 | setup_test_env 8 | 9 | test_header "Split and Partial Verification Tests" 10 | 11 | if [ -z "$SAMPLE_WASM" ]; then 12 | skip "No sample WASM file available" 13 | print_summary 14 | exit 0 15 | fi 16 | 17 | # Generate keys 18 | "$WASMSIGN2" keygen -k "${TEST_DIR}/secret.key" -K "${TEST_DIR}/public.key" >/dev/null 2>&1 19 | 20 | # Test basic split 21 | assert_success "Add cutting point" \ 22 | "$WASMSIGN2" split -i "$SAMPLE_WASM" -o "${TEST_DIR}/split.wasm" -s "name" 23 | 24 | assert_file_exists "${TEST_DIR}/split.wasm" 25 | 26 | # Split file may be slightly larger due to cutting point markers 27 | assert_files_differ "$SAMPLE_WASM" "${TEST_DIR}/split.wasm" 28 | 29 | # Test split with regex pattern 30 | assert_success "Add cutting point with regex" \ 31 | "$WASMSIGN2" split -i "$SAMPLE_WASM" -o "${TEST_DIR}/split_regex.wasm" -s '^[.]debug' 32 | 33 | # Sign the split module 34 | assert_success "Sign split module" \ 35 | "$WASMSIGN2" sign -k "${TEST_DIR}/secret.key" -i "${TEST_DIR}/split.wasm" -o "${TEST_DIR}/split_signed.wasm" 36 | 37 | # Verify full module 38 | assert_success "Verify full split module" \ 39 | "$WASMSIGN2" verify -i "${TEST_DIR}/split_signed.wasm" -K "${TEST_DIR}/public.key" 40 | 41 | # Verify with partial split pattern 42 | assert_success "Verify with split pattern 'name'" \ 43 | "$WASMSIGN2" verify -i "${TEST_DIR}/split_signed.wasm" -K "${TEST_DIR}/public.key" -s "name" 44 | 45 | # Test multiple cutting points 46 | assert_success "Add multiple cutting points" \ 47 | "$WASMSIGN2" split -i "$SAMPLE_WASM" -o "${TEST_DIR}/multi_split.wasm" -s "debug|name" 48 | 49 | assert_success "Sign multi-split module" \ 50 | "$WASMSIGN2" sign -k "${TEST_DIR}/secret.key" -i "${TEST_DIR}/multi_split.wasm" -o "${TEST_DIR}/multi_split_signed.wasm" 51 | 52 | assert_success "Verify multi-split with pattern" \ 53 | "$WASMSIGN2" verify -i "${TEST_DIR}/multi_split_signed.wasm" -K "${TEST_DIR}/public.key" -s "debug|name" 54 | 55 | # Test chained splits 56 | assert_success "First split" \ 57 | "$WASMSIGN2" split -i "$SAMPLE_WASM" -o "${TEST_DIR}/chain1.wasm" -s "first_pattern" 58 | 59 | assert_success "Second split on already-split module" \ 60 | "$WASMSIGN2" split -i "${TEST_DIR}/chain1.wasm" -o "${TEST_DIR}/chain2.wasm" -s "second_pattern" 61 | 62 | # Show command should work on split modules 63 | assert_success "Show split module structure" \ 64 | "$WASMSIGN2" show -i "${TEST_DIR}/split.wasm" 65 | 66 | print_summary 67 | -------------------------------------------------------------------------------- /.ci/test_verify_matrix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Test batch verification (verify_matrix) functionality 3 | 4 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 5 | . "${SCRIPT_DIR}/common.sh" 6 | 7 | setup_test_env 8 | 9 | test_header "Verify Matrix Tests" 10 | 11 | if [ -z "$SAMPLE_WASM" ]; then 12 | skip "No sample WASM file available" 13 | print_summary 14 | exit 0 15 | fi 16 | 17 | # Generate multiple key pairs 18 | "$WASMSIGN2" keygen -k "${TEST_DIR}/key1.sk" -K "${TEST_DIR}/key1.pk" >/dev/null 2>&1 19 | "$WASMSIGN2" keygen -k "${TEST_DIR}/key2.sk" -K "${TEST_DIR}/key2.pk" >/dev/null 2>&1 20 | "$WASMSIGN2" keygen -k "${TEST_DIR}/key3.sk" -K "${TEST_DIR}/key3.pk" >/dev/null 2>&1 21 | 22 | # Sign with first two keys 23 | "$WASMSIGN2" sign -k "${TEST_DIR}/key1.sk" -i "$SAMPLE_WASM" -o "${TEST_DIR}/signed1.wasm" >/dev/null 2>&1 24 | "$WASMSIGN2" sign -k "${TEST_DIR}/key2.sk" -i "${TEST_DIR}/signed1.wasm" -o "${TEST_DIR}/signed2.wasm" >/dev/null 2>&1 25 | 26 | # Test verify_matrix with multiple public keys 27 | assert_success "Verify matrix with multiple keys" \ 28 | "$WASMSIGN2" verify_matrix -i "${TEST_DIR}/signed2.wasm" \ 29 | -K "${TEST_DIR}/key1.pk" "${TEST_DIR}/key2.pk" "${TEST_DIR}/key3.pk" 30 | 31 | # Test that output shows valid keys 32 | assert_output_contains "Matrix output shows valid keys" "Valid public keys" \ 33 | "$WASMSIGN2" verify_matrix -i "${TEST_DIR}/signed2.wasm" \ 34 | -K "${TEST_DIR}/key1.pk" "${TEST_DIR}/key2.pk" "${TEST_DIR}/key3.pk" 35 | 36 | # Test verify_matrix with only non-signer keys 37 | output=$("$WASMSIGN2" verify_matrix -i "${TEST_DIR}/signed2.wasm" -K "${TEST_DIR}/key3.pk" 2>&1) 38 | if echo "$output" | grep -q "No valid public keys"; then 39 | pass "Matrix shows no valid keys when none match" 40 | else 41 | # The output format might vary - just check it runs successfully 42 | if echo "$output" | grep -qv "Valid"; then 43 | pass "Matrix correctly handles non-signer keys" 44 | else 45 | fail "Matrix should indicate no valid keys for non-signer" 46 | fi 47 | fi 48 | 49 | # Test with split pattern 50 | "$WASMSIGN2" split -i "$SAMPLE_WASM" -o "${TEST_DIR}/split.wasm" -s "name" >/dev/null 2>&1 51 | "$WASMSIGN2" sign -k "${TEST_DIR}/key1.sk" -i "${TEST_DIR}/split.wasm" -o "${TEST_DIR}/split_signed.wasm" >/dev/null 2>&1 52 | 53 | assert_success "Verify matrix with split pattern" \ 54 | "$WASMSIGN2" verify_matrix -i "${TEST_DIR}/split_signed.wasm" \ 55 | -K "${TEST_DIR}/key1.pk" "${TEST_DIR}/key2.pk" -s "name" 56 | 57 | # Test missing public keys argument 58 | assert_failure "Verify matrix fails without public keys" \ 59 | "$WASMSIGN2" verify_matrix -i "${TEST_DIR}/signed2.wasm" 60 | 61 | print_summary 62 | -------------------------------------------------------------------------------- /.ci/test_multi_sign.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Test multiple signature functionality 3 | 4 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 5 | . "${SCRIPT_DIR}/common.sh" 6 | 7 | setup_test_env 8 | 9 | test_header "Multi-Signature Tests" 10 | 11 | if [ -z "$SAMPLE_WASM" ]; then 12 | skip "No sample WASM file available" 13 | print_summary 14 | exit 0 15 | fi 16 | 17 | # Generate multiple key pairs 18 | "$WASMSIGN2" keygen -k "${TEST_DIR}/key1.sk" -K "${TEST_DIR}/key1.pk" >/dev/null 2>&1 19 | "$WASMSIGN2" keygen -k "${TEST_DIR}/key2.sk" -K "${TEST_DIR}/key2.pk" >/dev/null 2>&1 20 | "$WASMSIGN2" keygen -k "${TEST_DIR}/key3.sk" -K "${TEST_DIR}/key3.pk" >/dev/null 2>&1 21 | 22 | # Sign with first key 23 | assert_success "Sign with first key" \ 24 | "$WASMSIGN2" sign -k "${TEST_DIR}/key1.sk" -i "$SAMPLE_WASM" -o "${TEST_DIR}/signed1.wasm" 25 | 26 | # Add second signature 27 | assert_success "Add second signature" \ 28 | "$WASMSIGN2" sign -k "${TEST_DIR}/key2.sk" -i "${TEST_DIR}/signed1.wasm" -o "${TEST_DIR}/signed2.wasm" 29 | 30 | # Verify with first key 31 | assert_success "Verify multi-signed module with first key" \ 32 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed2.wasm" -K "${TEST_DIR}/key1.pk" 33 | 34 | # Verify with second key 35 | assert_success "Verify multi-signed module with second key" \ 36 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed2.wasm" -K "${TEST_DIR}/key2.pk" 37 | 38 | # Verification should fail with third key (not a signer) 39 | assert_failure "Verify fails with non-signer key" \ 40 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed2.wasm" -K "${TEST_DIR}/key3.pk" 41 | 42 | # Add third signature 43 | assert_success "Add third signature" \ 44 | "$WASMSIGN2" sign -k "${TEST_DIR}/key3.sk" -i "${TEST_DIR}/signed2.wasm" -o "${TEST_DIR}/signed3.wasm" 45 | 46 | # All three keys should now verify 47 | assert_success "Verify triple-signed with key1" \ 48 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed3.wasm" -K "${TEST_DIR}/key1.pk" 49 | 50 | assert_success "Verify triple-signed with key2" \ 51 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed3.wasm" -K "${TEST_DIR}/key2.pk" 52 | 53 | assert_success "Verify triple-signed with key3" \ 54 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed3.wasm" -K "${TEST_DIR}/key3.pk" 55 | 56 | # File sizes should increase with each signature 57 | size1=$(wc -c < "${TEST_DIR}/signed1.wasm" | tr -d ' ') 58 | size2=$(wc -c < "${TEST_DIR}/signed2.wasm" | tr -d ' ') 59 | size3=$(wc -c < "${TEST_DIR}/signed3.wasm" | tr -d ' ') 60 | 61 | if [ "$size2" -gt "$size1" ] && [ "$size3" -gt "$size2" ]; then 62 | pass "File size increases with each signature" 63 | else 64 | fail "File size should increase with each signature" 65 | fi 66 | 67 | print_summary 68 | -------------------------------------------------------------------------------- /.ci/test_sign_verify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Test basic signing and verification functionality 3 | 4 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 5 | . "${SCRIPT_DIR}/common.sh" 6 | 7 | setup_test_env 8 | 9 | test_header "Sign and Verify Tests" 10 | 11 | if [ -z "$SAMPLE_WASM" ]; then 12 | skip "No sample WASM file available" 13 | print_summary 14 | exit 0 15 | fi 16 | 17 | # Generate a key pair for testing 18 | "$WASMSIGN2" keygen -k "${TEST_DIR}/secret.key" -K "${TEST_DIR}/public.key" >/dev/null 2>&1 19 | 20 | # Test signing a module 21 | assert_success "Sign WASM module" \ 22 | "$WASMSIGN2" sign -k "${TEST_DIR}/secret.key" -i "$SAMPLE_WASM" -o "${TEST_DIR}/signed.wasm" 23 | 24 | assert_file_exists "${TEST_DIR}/signed.wasm" 25 | 26 | # Signed file should be larger (includes signature section) 27 | orig_size=$(wc -c < "$SAMPLE_WASM" | tr -d ' ') 28 | signed_size=$(wc -c < "${TEST_DIR}/signed.wasm" | tr -d ' ') 29 | 30 | if [ "$signed_size" -gt "$orig_size" ]; then 31 | pass "Signed file is larger than original" 32 | else 33 | fail "Signed file should be larger than original" 34 | fi 35 | 36 | # Test verifying a signed module 37 | assert_success "Verify signed module with correct key" \ 38 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed.wasm" -K "${TEST_DIR}/public.key" 39 | 40 | # Test verification with wrong key fails 41 | "$WASMSIGN2" keygen -k "${TEST_DIR}/wrong_secret.key" -K "${TEST_DIR}/wrong_public.key" >/dev/null 2>&1 42 | 43 | assert_failure "Verify fails with wrong key" \ 44 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed.wasm" -K "${TEST_DIR}/wrong_public.key" 45 | 46 | # Test verification of unsigned module fails 47 | assert_failure "Verify fails on unsigned module" \ 48 | "$WASMSIGN2" verify -i "$SAMPLE_WASM" -K "${TEST_DIR}/public.key" 49 | 50 | # Test signing with key ID (public key provided) 51 | assert_success "Sign with key ID" \ 52 | "$WASMSIGN2" sign -k "${TEST_DIR}/secret.key" -K "${TEST_DIR}/public.key" \ 53 | -i "$SAMPLE_WASM" -o "${TEST_DIR}/signed_with_id.wasm" 54 | 55 | assert_success "Verify module signed with key ID" \ 56 | "$WASMSIGN2" verify -i "${TEST_DIR}/signed_with_id.wasm" -K "${TEST_DIR}/public.key" 57 | 58 | # Test verbose output 59 | assert_output_contains "Verbose sign shows structure" "signature" \ 60 | "$WASMSIGN2" -v sign -k "${TEST_DIR}/secret.key" -i "$SAMPLE_WASM" -o "${TEST_DIR}/verbose_signed.wasm" 61 | 62 | # Test missing input file 63 | assert_failure "Fail with missing input file" \ 64 | "$WASMSIGN2" sign -k "${TEST_DIR}/secret.key" -i "${TEST_DIR}/nonexistent.wasm" -o "${TEST_DIR}/out.wasm" 65 | 66 | # Test missing secret key 67 | assert_failure "Fail with missing secret key file" \ 68 | "$WASMSIGN2" sign -k "${TEST_DIR}/nonexistent.key" -i "$SAMPLE_WASM" -o "${TEST_DIR}/out.wasm" 69 | 70 | print_summary 71 | -------------------------------------------------------------------------------- /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 | if self.sections.is_empty() { 72 | return Err(WSError::NoSignatures); 73 | } 74 | let first_section = self.sections.remove(0); 75 | if !first_section.is_signature_header() { 76 | return Err(WSError::NoSignatures); 77 | } 78 | let detached_signature = first_section.payload().to_vec(); 79 | debug!("Signature detached"); 80 | Ok((self, detached_signature)) 81 | } 82 | 83 | /// Embed a detached signature into a module. 84 | /// This function returns the module with embedded signature. 85 | pub fn attach_signature(mut self, detached_signature: &[u8]) -> Result { 86 | for section in &self.sections { 87 | if section.is_signature_header() { 88 | return Err(WSError::SignatureAlreadyAttached); 89 | } 90 | } 91 | let signature_header = Section::Custom(CustomSection::new( 92 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 93 | detached_signature.to_vec(), 94 | )); 95 | self.sections.insert(0, signature_header); 96 | debug!("Signature attached"); 97 | Ok(self) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.ci/test_detach_attach.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Test detached signature functionality 3 | 4 | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) 5 | . "${SCRIPT_DIR}/common.sh" 6 | 7 | setup_test_env 8 | 9 | test_header "Detach and Attach Signature Tests" 10 | 11 | if [ -z "$SAMPLE_WASM" ]; then 12 | skip "No sample WASM file available" 13 | print_summary 14 | exit 0 15 | fi 16 | 17 | # Generate keys 18 | "$WASMSIGN2" keygen -k "${TEST_DIR}/secret.key" -K "${TEST_DIR}/public.key" >/dev/null 2>&1 19 | 20 | # Sign the module first 21 | "$WASMSIGN2" sign -k "${TEST_DIR}/secret.key" -i "$SAMPLE_WASM" -o "${TEST_DIR}/signed.wasm" >/dev/null 2>&1 22 | 23 | # Test detaching signature 24 | assert_success "Detach signature from module" \ 25 | "$WASMSIGN2" detach -i "${TEST_DIR}/signed.wasm" -o "${TEST_DIR}/unsigned.wasm" -S "${TEST_DIR}/signature.bin" 26 | 27 | assert_file_exists "${TEST_DIR}/unsigned.wasm" 28 | assert_file_exists "${TEST_DIR}/signature.bin" 29 | 30 | # Unsigned module should be smaller than signed module 31 | signed_size=$(wc -c < "${TEST_DIR}/signed.wasm" | tr -d ' ') 32 | unsigned_size=$(wc -c < "${TEST_DIR}/unsigned.wasm" | tr -d ' ') 33 | 34 | if [ "$unsigned_size" -lt "$signed_size" ]; then 35 | pass "Detached module is smaller than signed module" 36 | else 37 | fail "Detached module should be smaller than signed module" 38 | fi 39 | 40 | # Signature file should have content 41 | sig_size=$(wc -c < "${TEST_DIR}/signature.bin" | tr -d ' ') 42 | if [ "$sig_size" -gt 0 ]; then 43 | pass "Signature file has content" 44 | else 45 | fail "Signature file is empty" 46 | fi 47 | 48 | # Verification with detached signature should work 49 | assert_success "Verify with detached signature" \ 50 | "$WASMSIGN2" verify -i "${TEST_DIR}/unsigned.wasm" -K "${TEST_DIR}/public.key" -S "${TEST_DIR}/signature.bin" 51 | 52 | # Verification without detached signature should fail (module is unsigned) 53 | assert_failure "Verify unsigned module without detached sig fails" \ 54 | "$WASMSIGN2" verify -i "${TEST_DIR}/unsigned.wasm" -K "${TEST_DIR}/public.key" 55 | 56 | # Test attaching signature back 57 | assert_success "Attach signature to module" \ 58 | "$WASMSIGN2" attach -i "${TEST_DIR}/unsigned.wasm" -o "${TEST_DIR}/reattached.wasm" -S "${TEST_DIR}/signature.bin" 59 | 60 | assert_file_exists "${TEST_DIR}/reattached.wasm" 61 | 62 | # Reattached module should verify without detached signature 63 | assert_success "Verify reattached module" \ 64 | "$WASMSIGN2" verify -i "${TEST_DIR}/reattached.wasm" -K "${TEST_DIR}/public.key" 65 | 66 | # Reattached and original signed should be similar in size 67 | reattached_size=$(wc -c < "${TEST_DIR}/reattached.wasm" | tr -d ' ') 68 | 69 | if [ "$reattached_size" -eq "$signed_size" ]; then 70 | pass "Reattached module same size as original signed" 71 | else 72 | # They might differ slightly due to section ordering 73 | diff=$((reattached_size - signed_size)) 74 | if [ "$diff" -lt 0 ]; then 75 | diff=$((diff * -1)) 76 | fi 77 | if [ "$diff" -lt 100 ]; then 78 | pass "Reattached module approximately same size as original signed (diff: $diff bytes)" 79 | else 80 | fail "Reattached module significantly different size from original signed" 81 | fi 82 | fi 83 | 84 | # Test signing directly to detached signature 85 | assert_success "Sign with detached signature output" \ 86 | "$WASMSIGN2" sign -k "${TEST_DIR}/secret.key" -i "$SAMPLE_WASM" \ 87 | -o "${TEST_DIR}/module_for_detached.wasm" -S "${TEST_DIR}/direct_sig.bin" 88 | 89 | assert_file_exists "${TEST_DIR}/direct_sig.bin" 90 | 91 | # Verify the direct detached signature 92 | assert_success "Verify directly created detached signature" \ 93 | "$WASMSIGN2" verify -i "${TEST_DIR}/module_for_detached.wasm" -K "${TEST_DIR}/public.key" -S "${TEST_DIR}/direct_sig.bin" 94 | 95 | # Test missing signature file in detach 96 | assert_failure "Detach fails without signature file arg" \ 97 | "$WASMSIGN2" detach -i "${TEST_DIR}/signed.wasm" -o "${TEST_DIR}/out.wasm" 98 | 99 | # Test missing signature file in attach 100 | assert_failure "Attach fails without signature file arg" \ 101 | "$WASMSIGN2" attach -i "${TEST_DIR}/unsigned.wasm" -o "${TEST_DIR}/out.wasm" 102 | 103 | print_summary 104 | -------------------------------------------------------------------------------- /.ci/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Common test utilities for wasmsign2 functional tests 3 | 4 | # Colors for output (disabled if not a terminal) 5 | if [ -t 1 ]; then 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[0;33m' 9 | NC='\033[0m' 10 | else 11 | RED='' 12 | GREEN='' 13 | YELLOW='' 14 | NC='' 15 | fi 16 | 17 | # Test counters 18 | TESTS_PASSED=0 19 | TESTS_FAILED=0 20 | TESTS_SKIPPED=0 21 | 22 | # Get script directory 23 | get_script_dir() { 24 | cd "$(dirname "$0")" && pwd 25 | } 26 | 27 | # Get project root directory 28 | get_project_root() { 29 | cd "$(get_script_dir)/.." && pwd 30 | } 31 | 32 | # Print test header 33 | test_header() { 34 | printf "\n${YELLOW}=== %s ===${NC}\n" "$1" 35 | } 36 | 37 | # Print success message 38 | pass() { 39 | TESTS_PASSED=$((TESTS_PASSED + 1)) 40 | printf "${GREEN}PASS${NC}: %s\n" "$1" 41 | } 42 | 43 | # Print failure message 44 | fail() { 45 | TESTS_FAILED=$((TESTS_FAILED + 1)) 46 | printf "${RED}FAIL${NC}: %s\n" "$1" 47 | } 48 | 49 | # Print skip message 50 | skip() { 51 | TESTS_SKIPPED=$((TESTS_SKIPPED + 1)) 52 | printf "${YELLOW}SKIP${NC}: %s\n" "$1" 53 | } 54 | 55 | # Assert command succeeds 56 | assert_success() { 57 | msg="$1" 58 | shift 59 | if "$@" >/dev/null 2>&1; then 60 | pass "$msg" 61 | return 0 62 | else 63 | fail "$msg" 64 | return 1 65 | fi 66 | } 67 | 68 | # Assert command fails 69 | assert_failure() { 70 | msg="$1" 71 | shift 72 | if "$@" >/dev/null 2>&1; then 73 | fail "$msg (expected failure but succeeded)" 74 | return 1 75 | else 76 | pass "$msg" 77 | return 0 78 | fi 79 | } 80 | 81 | # Assert file exists 82 | assert_file_exists() { 83 | if [ -f "$1" ]; then 84 | pass "File exists: $1" 85 | return 0 86 | else 87 | fail "File does not exist: $1" 88 | return 1 89 | fi 90 | } 91 | 92 | # Assert file does not exist 93 | assert_file_not_exists() { 94 | if [ ! -f "$1" ]; then 95 | pass "File does not exist: $1" 96 | return 0 97 | else 98 | fail "File exists but should not: $1" 99 | return 1 100 | fi 101 | } 102 | 103 | # Assert files are identical 104 | assert_files_equal() { 105 | if cmp -s "$1" "$2"; then 106 | pass "Files are equal: $1 == $2" 107 | return 0 108 | else 109 | fail "Files differ: $1 != $2" 110 | return 1 111 | fi 112 | } 113 | 114 | # Assert files are different 115 | assert_files_differ() { 116 | if cmp -s "$1" "$2"; then 117 | fail "Files are equal but should differ: $1 == $2" 118 | return 1 119 | else 120 | pass "Files differ: $1 != $2" 121 | return 0 122 | fi 123 | } 124 | 125 | # Assert output contains string 126 | assert_output_contains() { 127 | msg="$1" 128 | expected="$2" 129 | shift 2 130 | output=$("$@" 2>&1) 131 | if echo "$output" | grep -q "$expected"; then 132 | pass "$msg" 133 | return 0 134 | else 135 | fail "$msg (output did not contain: $expected)" 136 | return 1 137 | fi 138 | } 139 | 140 | # Setup test environment 141 | setup_test_env() { 142 | PROJECT_ROOT=$(get_project_root) 143 | WASMSIGN2="${PROJECT_ROOT}/target/release/wasmsign2" 144 | 145 | if [ ! -x "$WASMSIGN2" ]; then 146 | # Try debug build 147 | WASMSIGN2="${PROJECT_ROOT}/target/debug/wasmsign2" 148 | fi 149 | 150 | if [ ! -x "$WASMSIGN2" ]; then 151 | printf "${RED}ERROR${NC}: wasmsign2 binary not found. Run 'cargo build --release' first.\n" 152 | exit 1 153 | fi 154 | 155 | # Create temporary directory for test files 156 | TEST_DIR=$(mktemp -d) 157 | trap 'rm -rf "$TEST_DIR"' EXIT 158 | 159 | # Copy sample WASM file if available 160 | if [ -f "${PROJECT_ROOT}/z.wasm" ]; then 161 | cp "${PROJECT_ROOT}/z.wasm" "${TEST_DIR}/test.wasm" 162 | SAMPLE_WASM="${TEST_DIR}/test.wasm" 163 | else 164 | SAMPLE_WASM="" 165 | fi 166 | 167 | export WASMSIGN2 TEST_DIR SAMPLE_WASM PROJECT_ROOT 168 | } 169 | 170 | # Create a minimal valid WASM module for testing 171 | create_minimal_wasm() { 172 | output_file="$1" 173 | # Minimal valid WASM: magic number + version 174 | # \x00asm = magic number, \x01\x00\x00\x00 = version 1 175 | printf '\x00asm\x01\x00\x00\x00' > "$output_file" 176 | } 177 | 178 | # Print test summary 179 | print_summary() { 180 | total=$((TESTS_PASSED + TESTS_FAILED + TESTS_SKIPPED)) 181 | printf "\n${YELLOW}=== Test Summary ===${NC}\n" 182 | printf "${GREEN}Passed${NC}: %d\n" "$TESTS_PASSED" 183 | printf "${RED}Failed${NC}: %d\n" "$TESTS_FAILED" 184 | printf "${YELLOW}Skipped${NC}: %d\n" "$TESTS_SKIPPED" 185 | printf "Total: %d\n" "$total" 186 | 187 | if [ "$TESTS_FAILED" -gt 0 ]; then 188 | return 1 189 | fi 190 | return 0 191 | } 192 | -------------------------------------------------------------------------------- /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 of 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_state_for_pks: HashMap> = HashMap::new(); 78 | for pk in valid_hashes_for_pks.keys() { 79 | section_state_for_pks.insert(pk.clone(), None); 80 | } 81 | 82 | let mut verify_failures: Vec> = vec![HashSet::new(); predicates.len()]; 83 | 84 | let mut hasher = Hash::new(); 85 | for section in sections { 86 | let section = section?; 87 | section.serialize(&mut hasher)?; 88 | if section.is_signature_delimiter() { 89 | let h = hasher.finalize().to_vec(); 90 | for (pk, state) in section_state_for_pks.iter_mut() { 91 | if *state == Some(false) { 92 | *state = None; 93 | continue; 94 | } 95 | if let Some(valid_hashes) = valid_hashes_for_pks.get(pk) { 96 | if !valid_hashes.contains(&h) { 97 | valid_hashes_for_pks.remove(pk); 98 | } 99 | } 100 | *state = None; 101 | } 102 | } else { 103 | for (idx, predicate) in predicates.iter().enumerate() { 104 | let section_must_be_signed = predicate(§ion); 105 | for (pk, state) in section_state_for_pks.iter_mut() { 106 | if let Some(expected) = *state { 107 | if section_must_be_signed != expected { 108 | verify_failures[idx].insert(pk.clone()); 109 | } 110 | } else { 111 | *state = Some(section_must_be_signed); 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | let mut results: Vec> = Vec::new(); 119 | for (idx, _predicate) in predicates.iter().enumerate() { 120 | let mut valid_for_predicate: HashSet<&PublicKey> = HashSet::new(); 121 | for pk in &self.pks { 122 | if !valid_hashes_for_pks.contains_key(pk) { 123 | continue; 124 | } 125 | if !verify_failures[idx].contains(pk) { 126 | valid_for_predicate.insert(pk); 127 | } 128 | } 129 | results.push(valid_for_predicate); 130 | } 131 | 132 | if results.is_empty() { 133 | debug!("No valid signatures"); 134 | return Err(WSError::VerificationFailedForPredicates); 135 | } 136 | Ok(results) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/lib/src/signature/info.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::signature::sig_sections::{SIGNATURE_SECTION_HEADER_NAME, SignatureData}; 3 | use crate::wasm_module::{Module, Section}; 4 | use std::fs::File; 5 | use std::io::{BufReader, Read}; 6 | use std::path::Path; 7 | 8 | /// Information about signatures in a WebAssembly module. 9 | /// 10 | /// This struct provides easy access to signature metadata. 11 | #[derive(Debug, Clone)] 12 | pub struct SignatureInfo { 13 | key_ids: Vec>, 14 | signature_count: usize, 15 | /// The specification version of the signature format. 16 | pub specification_version: u8, 17 | /// The content type (0x01 for WebAssembly modules). 18 | pub content_type: u8, 19 | /// The hash function used (0x01 for SHA-256). 20 | pub hash_function: u8, 21 | } 22 | 23 | impl SignatureInfo { 24 | fn from_signature_data(data: &SignatureData) -> Self { 25 | let mut key_ids = Vec::new(); 26 | let mut signature_count = 0; 27 | 28 | for signed_hashes in &data.signed_hashes_set { 29 | for sig in &signed_hashes.signatures { 30 | if let Some(key_id) = &sig.key_id { 31 | key_ids.push(key_id.clone()); 32 | } 33 | signature_count += 1; 34 | } 35 | } 36 | 37 | SignatureInfo { 38 | key_ids, 39 | signature_count, 40 | specification_version: data.specification_version, 41 | content_type: data.content_type, 42 | hash_function: data.hash_function, 43 | } 44 | } 45 | 46 | /// Returns the key IDs from signatures that have them. 47 | /// 48 | /// Signatures created without key IDs are not included. To detect 49 | /// anonymous signatures, compare `key_ids().len()` with `signature_count()`. 50 | pub fn key_ids(&self) -> &[Vec] { 51 | &self.key_ids 52 | } 53 | 54 | /// Returns the total number of signatures (with or without key IDs). 55 | pub fn signature_count(&self) -> usize { 56 | self.signature_count 57 | } 58 | 59 | /// Returns true if the module has at least one signature. 60 | pub fn is_signed(&self) -> bool { 61 | self.signature_count > 0 62 | } 63 | } 64 | 65 | impl Module { 66 | /// Get signature information from this module. 67 | /// 68 | /// Returns `SignatureInfo` containing key IDs and other signature metadata. 69 | /// 70 | /// # Example 71 | /// 72 | /// ```ignore 73 | /// let module = Module::deserialize_from_file("signed.wasm")?; 74 | /// let info = module.signature_info()?; 75 | /// for key_id in info.key_ids() { 76 | /// println!("Key ID: {:02x?}", key_id); 77 | /// } 78 | /// ``` 79 | pub fn signature_info(&self) -> Result { 80 | for section in &self.sections { 81 | if let Section::Custom(custom) = section { 82 | if custom.is_signature_header() { 83 | let data = custom.signature_data()?; 84 | return Ok(SignatureInfo::from_signature_data(&data)); 85 | } 86 | } 87 | } 88 | Err(WSError::NoSignatures) 89 | } 90 | } 91 | 92 | /// Get signature information from a WebAssembly module file. 93 | /// 94 | /// This is a convenience function that opens and parses a file to extract 95 | /// signature information without loading the entire module into memory. 96 | /// 97 | /// # Example 98 | /// 99 | /// ```ignore 100 | /// let info = wasmsign2::signature_info_from_file("signed.wasm")?; 101 | /// println!("Module has {} signatures", info.signature_count()); 102 | /// for key_id in info.key_ids() { 103 | /// println!("Key ID: {:02x?}", key_id); 104 | /// } 105 | /// ``` 106 | pub fn signature_info_from_file(path: impl AsRef) -> Result { 107 | let fp = File::open(path.as_ref())?; 108 | signature_info_from_reader(&mut BufReader::new(fp), None) 109 | } 110 | 111 | /// Get signature information from a reader in streaming fashion. 112 | /// 113 | /// This function reads only the signature section without loading the entire 114 | /// module, making it efficient for large modules. 115 | /// 116 | /// `detached_signature` allows reading signature info from a detached signature 117 | /// instead of an embedded one. 118 | /// 119 | /// # Example 120 | /// 121 | /// ```ignore 122 | /// let mut file = File::open("signed.wasm")?; 123 | /// let info = wasmsign2::signature_info_from_reader(&mut file, None)?; 124 | /// println!("Found {} key IDs", info.key_ids().len()); 125 | /// ``` 126 | pub fn signature_info_from_reader( 127 | reader: &mut impl Read, 128 | detached_signature: Option<&[u8]>, 129 | ) -> Result { 130 | if let Some(detached) = detached_signature { 131 | let data = SignatureData::deserialize(detached)?; 132 | return Ok(SignatureInfo::from_signature_data(&data)); 133 | } 134 | 135 | let stream = Module::init_from_reader(reader)?; 136 | let mut sections = Module::iterate(stream)?; 137 | 138 | let first_section = sections.next().ok_or(WSError::ParseError)??; 139 | 140 | match first_section { 141 | Section::Custom(custom) if custom.name() == SIGNATURE_SECTION_HEADER_NAME => { 142 | let data = custom.signature_data()?; 143 | Ok(SignatureInfo::from_signature_data(&data)) 144 | } 145 | _ => Err(WSError::NoSignatures), 146 | } 147 | } 148 | 149 | /// Get signature information from a detached signature. 150 | /// 151 | /// This function parses a detached signature blob directly without needing 152 | /// access to the original module. 153 | /// 154 | /// # Example 155 | /// 156 | /// ```ignore 157 | /// let signature_bytes = std::fs::read("signature.bin")?; 158 | /// let info = wasmsign2::signature_info_from_detached(&signature_bytes)?; 159 | /// println!("Detached signature has {} keys", info.key_ids().len()); 160 | /// ``` 161 | pub fn signature_info_from_detached(detached_signature: &[u8]) -> Result { 162 | let data = SignatureData::deserialize(detached_signature)?; 163 | Ok(SignatureInfo::from_signature_data(&data)) 164 | } 165 | -------------------------------------------------------------------------------- /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_bytes = varint::get_slice(&mut reader)?; 53 | let key_id = if key_id_bytes.is_empty() { None } else { Some(key_id_bytes) }; 54 | let mut alg_id_buf = [0u8; 1]; 55 | reader.read_exact(&mut alg_id_buf)?; 56 | let alg_id = alg_id_buf[0]; 57 | if alg_id != ED25519_PK_ID { 58 | debug!("Unsupported algorithm: {:02x}", alg_id); 59 | return Err(WSError::ParseError); 60 | } 61 | let signature = varint::get_slice(&mut reader)?; 62 | Ok(Self { key_id, alg_id, signature }) 63 | } 64 | } 65 | 66 | impl SignedHashes { 67 | pub fn serialize(&self) -> Result, WSError> { 68 | let mut writer = BufWriter::new(Vec::new()); 69 | varint::put(&mut writer, self.hashes.len() as _)?; 70 | for hash in &self.hashes { 71 | writer.write_all(hash)?; 72 | } 73 | varint::put(&mut writer, self.signatures.len() as _)?; 74 | for signature in &self.signatures { 75 | varint::put_slice(&mut writer, &signature.serialize()?)?; 76 | } 77 | Ok(writer.into_inner().unwrap()) 78 | } 79 | 80 | pub fn deserialize(bin: impl AsRef<[u8]>) -> Result { 81 | let mut reader = BufReader::new(bin.as_ref()); 82 | let hashes_count = varint::get32(&mut reader)? as _; 83 | if hashes_count > MAX_HASHES { 84 | debug!("Too many hashes: {} (max: {})", hashes_count, MAX_HASHES); 85 | return Err(WSError::TooManyHashes(MAX_HASHES)); 86 | } 87 | let mut hashes = Vec::with_capacity(hashes_count); 88 | for _ in 0..hashes_count { 89 | let mut hash = vec![0; 32]; 90 | reader.read_exact(&mut hash)?; 91 | hashes.push(hash); 92 | } 93 | let signatures_count = varint::get32(&mut reader)? as _; 94 | if signatures_count > MAX_SIGNATURES { 95 | debug!( 96 | "Too many signatures: {} (max: {})", 97 | signatures_count, MAX_SIGNATURES 98 | ); 99 | return Err(WSError::TooManySignatures(MAX_SIGNATURES)); 100 | } 101 | let mut signatures = Vec::with_capacity(signatures_count); 102 | for _ in 0..signatures_count { 103 | let bin = varint::get_slice(&mut reader)?; 104 | if let Ok(signature) = SignatureForHashes::deserialize(bin) { 105 | signatures.push(signature); 106 | } 107 | } 108 | Ok(Self { hashes, signatures }) 109 | } 110 | } 111 | 112 | impl SignatureData { 113 | pub fn serialize(&self) -> Result, WSError> { 114 | let mut writer = BufWriter::new(Vec::new()); 115 | varint::put(&mut writer, self.specification_version as _)?; 116 | varint::put(&mut writer, self.content_type as _)?; 117 | varint::put(&mut writer, self.hash_function as _)?; 118 | varint::put(&mut writer, self.signed_hashes_set.len() as _)?; 119 | for signed_hashes in &self.signed_hashes_set { 120 | varint::put_slice(&mut writer, &signed_hashes.serialize()?)?; 121 | } 122 | Ok(writer.into_inner().unwrap()) 123 | } 124 | 125 | pub fn deserialize(bin: impl AsRef<[u8]>) -> Result { 126 | let mut reader = BufReader::new(bin.as_ref()); 127 | let specification_version = varint::get7(&mut reader)?; 128 | if specification_version != SIGNATURE_VERSION { 129 | debug!( 130 | "Unsupported specification version: {:02x}", 131 | specification_version 132 | ); 133 | return Err(WSError::ParseError); 134 | } 135 | let content_type = varint::get7(&mut reader)?; 136 | if content_type != SIGNATURE_WASM_MODULE_CONTENT_TYPE { 137 | debug!("Unsupported content type: {:02x}", content_type); 138 | return Err(WSError::ParseError); 139 | } 140 | let hash_function = varint::get7(&mut reader)?; 141 | let signed_hashes_count = varint::get32(&mut reader)? as _; 142 | if signed_hashes_count > MAX_HASHES { 143 | debug!( 144 | "Too many hashes: {} (max: {})", 145 | signed_hashes_count, MAX_HASHES 146 | ); 147 | return Err(WSError::TooManyHashes(MAX_HASHES)); 148 | } 149 | let mut signed_hashes_set = Vec::with_capacity(signed_hashes_count); 150 | for _ in 0..signed_hashes_count { 151 | let bin = varint::get_slice(&mut reader)?; 152 | let signed_hashes = SignedHashes::deserialize(bin)?; 153 | signed_hashes_set.push(signed_hashes); 154 | } 155 | Ok(Self { 156 | specification_version, 157 | content_type, 158 | hash_function, 159 | signed_hashes_set, 160 | }) 161 | } 162 | } 163 | 164 | pub fn new_delimiter_section() -> Result { 165 | let mut custom_payload = vec![0u8; 16]; 166 | getrandom::fill(&mut custom_payload) 167 | .map_err(|_| WSError::InternalError("RNG error".to_string()))?; 168 | Ok(Section::Custom(CustomSection::new( 169 | SIGNATURE_SECTION_DELIMITER_NAME.to_string(), 170 | custom_payload, 171 | ))) 172 | } 173 | -------------------------------------------------------------------------------- /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.hash_function 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 | let n = reader.read(&mut buf)?; 120 | if n == 0 { 121 | break; 122 | } 123 | hasher.update(&buf[..n]); 124 | } 125 | let h = hasher.finalize().to_vec(); 126 | 127 | if !valid_hashes.contains(&h) { 128 | return Err(WSError::VerificationFailed); 129 | } 130 | Ok(()) 131 | } 132 | } 133 | 134 | impl PublicKeySet { 135 | /// Verify a module's signature with multiple public keys. 136 | /// 137 | /// `reader` is a reader over the raw module data. 138 | /// 139 | /// `detached_signature` allows the caller to verify a module without an embedded signature. 140 | /// 141 | /// This simplified interface verifies the entire module, with all public keys from the set. 142 | /// It returns the set of public keys for which a valid signature was found. 143 | pub fn verify( 144 | &self, 145 | reader: &mut impl Read, 146 | detached_signature: Option<&[u8]>, 147 | ) -> Result, WSError> { 148 | let mut sections = Module::iterate(Module::init_from_reader(reader)?)?; 149 | 150 | let signature_header_section = if let Some(detached_signature) = detached_signature { 151 | Section::Custom(CustomSection::new( 152 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 153 | detached_signature.to_vec(), 154 | )) 155 | } else { 156 | sections.next().ok_or(WSError::ParseError)?? 157 | }; 158 | 159 | let signature_header = match signature_header_section { 160 | Section::Custom(custom_section) if custom_section.is_signature_header() => { 161 | custom_section 162 | } 163 | _ => { 164 | debug!("This module is not signed"); 165 | return Err(WSError::NoSignatures); 166 | } 167 | }; 168 | 169 | let signature_data = signature_header.signature_data()?; 170 | if signature_data.content_type != SIGNATURE_WASM_MODULE_CONTENT_TYPE { 171 | debug!("Unsupported content type: {:02x}", signature_data.content_type); 172 | return Err(WSError::ParseError); 173 | } 174 | if signature_data.hash_function != SIGNATURE_HASH_FUNCTION { 175 | debug!("Unsupported hash function: {:02x}", signature_data.hash_function); 176 | return Err(WSError::ParseError); 177 | } 178 | 179 | let signed_hashes_set = signature_data.signed_hashes_set; 180 | let mut valid_hashes_for_pks: HashMap<&PublicKey, HashSet<&Vec>> = HashMap::new(); 181 | for pk in &self.pks { 182 | if let Ok(valid_hashes) = pk.valid_hashes_for_pk(&signed_hashes_set) { 183 | if !valid_hashes.is_empty() { 184 | valid_hashes_for_pks.insert(pk, valid_hashes); 185 | } 186 | } 187 | } 188 | if valid_hashes_for_pks.is_empty() { 189 | debug!("No valid signatures"); 190 | return Err(WSError::VerificationFailed); 191 | } 192 | 193 | let mut hasher = Hash::new(); 194 | let mut buf = vec![0u8; 65536]; 195 | loop { 196 | let n = reader.read(&mut buf)?; 197 | if n == 0 { 198 | break; 199 | } 200 | hasher.update(&buf[..n]); 201 | } 202 | let h = hasher.finalize().to_vec(); 203 | 204 | let mut valid_pks = HashSet::new(); 205 | for (pk, valid_hashes) in valid_hashes_for_pks { 206 | if valid_hashes.contains(&h) { 207 | valid_pks.insert(pk); 208 | } 209 | } 210 | if valid_pks.is_empty() { 211 | debug!("No valid signatures"); 212 | return Err(WSError::VerificationFailed); 213 | } 214 | Ok(valid_pks) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub CI](https://github.com/wasm-signatures/wasmsign2/actions/workflows/ci.yml/badge.svg)](https://github.com/wasm-signatures/wasmsign2/actions/workflows/ci.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 | Commands: 61 | keygen Generate a new key pair 62 | show Print the structure of a module 63 | split Add cutting points to a module to enable partial verification 64 | sign Sign a module 65 | verify Verify a module's signature 66 | detach Detach the signature from a module 67 | attach Embed a detached signature into a module 68 | verify_matrix Batch verification against multiple public keys 69 | help Print this message or the help of the given subcommand(s) 70 | 71 | Options: 72 | -v Verbose output 73 | -d Prints debugging information 74 | -h, --help Print help 75 | -V, --version Print version 76 | ``` 77 | 78 | ## Inspecting a module 79 | 80 | ```text 81 | wasmsign2 show --input-file 82 | ``` 83 | 84 | Example: 85 | 86 | ```sh 87 | wasmsign2 show -i z.wasm 88 | ``` 89 | 90 | The `-v` switch prints additional details about signature data. 91 | 92 | ## Creating a key pair 93 | 94 | ```text 95 | wasmsign2 keygen --public-key --secret-key 96 | 97 | -K, --public-key Public key file 98 | -k, --secret-key Secret key file 99 | ``` 100 | 101 | Example: 102 | 103 | ```sh 104 | wasmsign2 keygen --public-key key.public --secret-key key.secret 105 | ``` 106 | 107 | ## Signing a WebAssembly module 108 | 109 | ```text 110 | wasmsign2 sign [OPTIONS] --input-file --output-file --secret-key 111 | 112 | -i, --input-file Input file 113 | -o, --output-file Output file 114 | -K, --public-key Public key file 115 | -k, --secret-key Secret key file 116 | -S, --signature-file Signature file 117 | -Z, --ssh Parse OpenSSH keys 118 | ``` 119 | 120 | Example: 121 | 122 | ```sh 123 | wasmsign2 sign -i z.wasm -o z2.wasm -k secret.key 124 | ``` 125 | 126 | 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. 127 | 128 | By default, signatures are assumed to be embedded in modules. Detached signatures can be provided with the optional `--signature-file` argument. 129 | 130 | A module that was already signed can be signed with other keys, and can then be verified by any of the corresponding public keys. 131 | 132 | ## Verifying a WebAssembly module 133 | 134 | ```text 135 | wasmsign2 verify [FLAGS] [OPTIONS] --input-file 136 | 137 | -i, --input-file Input file 138 | -K, --public-key Public key file 139 | -S, --signature-file Signature file 140 | -s, --split Custom section names to be verified 141 | -G, --from-github GitHub account to retrieve public keys from 142 | -Z, --ssh Parse OpenSSH keys 143 | ``` 144 | 145 | Example: 146 | 147 | ```sh 148 | wasmsign2 verify -i z2.wasm -K public.key 149 | ``` 150 | 151 | The optional `-s/--split` parameter is documented in the "partial verification" section down below. 152 | 153 | ## Verifying a WebAssembly module against multiple public keys 154 | 155 | ```text 156 | wasmsign2 verify_matrix [FLAGS] [OPTIONS] --input-file 157 | 158 | -i, --input-file Input file 159 | -K, --public-keys ... Public key files 160 | -s, --split Custom section names to be verified 161 | -G, --from-github GitHub account to retrieve public keys from 162 | -Z, --ssh Parse OpenSSH keys 163 | ``` 164 | 165 | 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. 166 | 167 | The optional `-s/--split` parameter is documented in the "partial verification" section down below. 168 | 169 | Example: 170 | 171 | ```sh 172 | wasmsign2 verify_matrix -i z2.wasm -K public.key -K public.key2 173 | ``` 174 | 175 | ## Detaching a signature from a module 176 | 177 | ```text 178 | wasmsign2 detach --input-file --output-file --signature-file 179 | 180 | -i, --input-file Input file 181 | -o, --output-file Output file 182 | -S, --signature-file Signature file 183 | ``` 184 | 185 | The command extracts and removes the signature from a module, and stores it in a distinct file. 186 | 187 | Example: 188 | 189 | ```sh 190 | wasmsign2 detach -i z2.wasm -o z3.wasm -S signature 191 | ``` 192 | 193 | ## Embedding a detached signature in a module 194 | 195 | ```text 196 | wasmsign2 attach --input-file --output-file --signature-file 197 | 198 | -i, --input-file Input file 199 | -o, --output-file Output file 200 | -S, --signature-file Signature file 201 | ``` 202 | 203 | The command embeds a detached signature into a module. 204 | 205 | Example: 206 | 207 | ```sh 208 | wasmsign2 attach -i z2.wasm -o z3.wasm -S signature 209 | ``` 210 | 211 | ## Partial verification 212 | 213 | A signature can verify an entire module, but also one or more subsets of it. 214 | 215 | This requires "cutting points" to be defined before the signature process. It is impossible to verify a signature beyond cutting point boundaries. 216 | 217 | Cutting points can be added to a module with the `split` command: 218 | 219 | ```text 220 | wasmsign2 split [OPTIONS] --input-file --output-file 221 | 222 | -i, --input-file Input file 223 | -o, --output-file Output file 224 | -s, --split Custom section names to be signed 225 | ``` 226 | 227 | 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. 228 | 229 | This command can be repeated, to add new cutting points to a module that was already prepared for partial verification. 230 | 231 | Example: 232 | 233 | ```sh 234 | wasmsign2 split -i z2.wasm -o z3.wasm -s '^.debug_' 235 | ``` 236 | 237 | The above command makes it possible to verify only the custom sections whose name starts with `.debug_`, even though the entire module was signed. 238 | 239 | In order to do partial verification, the `--split` parameter is also available in the verification commands: 240 | 241 | ```sh 242 | wasmsign2 verify -i z3.wasm -K public.key -s '^.debug_' 243 | ``` 244 | 245 | ```sh 246 | wasmsign2 verify_matrix -i z3.wasm -K public.key -K public.key2 -s '^.debug_' 247 | ``` 248 | 249 | ## OpenSSH keys support 250 | 251 | 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. 252 | 253 | 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. 254 | 255 | Examples: 256 | 257 | ```sh 258 | wasmsign2 sign --ssh -k ~/.ssh/id_ed25519 -i z.wasm -o z2.wasm 259 | ``` 260 | 261 | ```sh 262 | wasmsign2 verify --ssh -K ~/.ssh/id_ed25519.pub -i z2.wasm 263 | ``` 264 | 265 | If a file contains more than a single public key, the `verify_matrix` command will check the signature against all discovered Ed25519 keys. 266 | 267 | Public key sets from GitHub accounts can be downloaded at `https://github.com/.keys`, replacing `` with an actual GitHub account name. 268 | 269 | Keys downloaded from such URL can be directly used to verify WebAssembly signatures. 270 | 271 | ## GitHub integration 272 | 273 | Public keys can also automatically be retrieved from GitHub accounts, using the `--from-github` parameter. 274 | 275 | Examples: 276 | 277 | ```sh 278 | wasmsign2 verify -G example_account -i z2.wasm 279 | ``` 280 | 281 | ```sh 282 | wasmsign2 verify_matrix -G example_account -i z2.wasm 283 | ``` 284 | -------------------------------------------------------------------------------- /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_was_delimiter = false; 40 | for (idx, section) in module.sections.iter().enumerate() { 41 | if section.is_signature_header() { 42 | debug!("A signature section was already present."); 43 | if idx != 0 { 44 | error!("The signature section was not the first module section"); 45 | continue; 46 | } 47 | if let Section::Custom(custom_section) = section { 48 | previous_signature_data = Some(custom_section.signature_data()?); 49 | } 50 | continue; 51 | } 52 | if section.is_signature_delimiter() { 53 | section.serialize(&mut hasher)?; 54 | out_sections.push(section.clone()); 55 | hashes.push(hasher.finalize().to_vec()); 56 | last_was_delimiter = true; 57 | continue; 58 | } 59 | section.serialize(&mut hasher)?; 60 | out_sections.push(section.clone()); 61 | last_was_delimiter = false; 62 | } 63 | if !last_was_delimiter { 64 | hashes.push(hasher.finalize().to_vec()); 65 | } 66 | let header_section = 67 | Self::build_header_section(previous_signature_data, self, key_id, hashes)?; 68 | if detached { 69 | Ok((module, header_section.payload().to_vec())) 70 | } else { 71 | out_sections[0] = header_section; 72 | module.sections = out_sections; 73 | let signature = module.sections[0].payload().to_vec(); 74 | Ok((module, signature)) 75 | } 76 | } 77 | 78 | fn build_header_section( 79 | previous_signature_data: Option, 80 | sk: &SecretKey, 81 | key_id: Option<&Vec>, 82 | hashes: Vec>, 83 | ) -> Result { 84 | let mut msg: Vec = vec![]; 85 | msg.extend_from_slice(SIGNATURE_WASM_DOMAIN.as_bytes()); 86 | msg.extend_from_slice(&[ 87 | SIGNATURE_VERSION, 88 | SIGNATURE_WASM_MODULE_CONTENT_TYPE, 89 | SIGNATURE_HASH_FUNCTION, 90 | ]); 91 | for hash in &hashes { 92 | msg.extend_from_slice(hash); 93 | } 94 | 95 | debug!("* Adding signature:\n"); 96 | 97 | debug!( 98 | "sig = Ed25519(sk, \"{}\" ‖ {:02x} ‖ {:02x} ‖ {:02x} ‖ {})\n", 99 | SIGNATURE_WASM_DOMAIN, 100 | SIGNATURE_VERSION, 101 | SIGNATURE_WASM_MODULE_CONTENT_TYPE, 102 | SIGNATURE_HASH_FUNCTION, 103 | Hex::encode_to_string(&msg[SIGNATURE_WASM_DOMAIN.len() + 3..]).unwrap() 104 | ); 105 | 106 | let signature = sk.sk.sign(msg, None).to_vec(); 107 | 108 | debug!(" = {}\n\n", Hex::encode_to_string(&signature).unwrap()); 109 | 110 | let signature_for_hashes = SignatureForHashes { 111 | key_id: key_id.cloned(), 112 | alg_id: ED25519_PK_ID, 113 | signature, 114 | }; 115 | 116 | let mut signed_hashes_set = if let Some(prev) = &previous_signature_data { 117 | if prev.specification_version != SIGNATURE_VERSION 118 | || prev.content_type != SIGNATURE_WASM_MODULE_CONTENT_TYPE 119 | || prev.hash_function != SIGNATURE_HASH_FUNCTION 120 | { 121 | return Err(WSError::IncompatibleSignatureVersion); 122 | } 123 | prev.signed_hashes_set.clone() 124 | } else { 125 | vec![] 126 | }; 127 | 128 | let mut new_hashes = true; 129 | for previous_signed_hashes_set in &mut signed_hashes_set { 130 | if previous_signed_hashes_set.hashes == hashes { 131 | if previous_signed_hashes_set.signatures.iter().any(|sig| { 132 | sig.key_id == signature_for_hashes.key_id 133 | && sig.signature == signature_for_hashes.signature 134 | }) { 135 | debug!("A matching hash set was already signed with that key."); 136 | return Err(WSError::DuplicateSignature); 137 | } 138 | debug!("A matching hash set was already signed."); 139 | previous_signed_hashes_set 140 | .signatures 141 | .push(signature_for_hashes.clone()); 142 | new_hashes = false; 143 | break; 144 | } 145 | } 146 | if new_hashes { 147 | debug!("No matching hash was previously signed."); 148 | let signatures = vec![signature_for_hashes]; 149 | let new_signed_section_sequences = SignedHashes { hashes, signatures }; 150 | signed_hashes_set.push(new_signed_section_sequences); 151 | } 152 | let signature_data = SignatureData { 153 | specification_version: SIGNATURE_VERSION, 154 | content_type: SIGNATURE_WASM_MODULE_CONTENT_TYPE, 155 | hash_function: SIGNATURE_HASH_FUNCTION, 156 | signed_hashes_set, 157 | }; 158 | let header_section = Section::Custom(CustomSection::new( 159 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 160 | signature_data.serialize()?, 161 | )); 162 | Ok(header_section) 163 | } 164 | } 165 | 166 | impl PublicKey { 167 | /// Verify the signature of a module, or module subset. 168 | /// 169 | /// `reader` is a reader over the raw module data. 170 | /// 171 | /// `detached_signature` allows the caller to verify a module without an embedded signature. 172 | /// 173 | /// `predicate` should return `true` for each section that needs to be included in the signature verification. 174 | pub fn verify_multi

( 175 | &self, 176 | reader: &mut impl Read, 177 | detached_signature: Option<&[u8]>, 178 | mut predicate: P, 179 | ) -> Result<(), WSError> 180 | where 181 | P: FnMut(&Section) -> bool, 182 | { 183 | let mut sections = Module::iterate(Module::init_from_reader(reader)?)?.enumerate(); 184 | let signature_header_section = if let Some(detached_signature) = &detached_signature { 185 | Section::Custom(CustomSection::new( 186 | SIGNATURE_SECTION_HEADER_NAME.to_string(), 187 | detached_signature.to_vec(), 188 | )) 189 | } else { 190 | sections.next().ok_or(WSError::ParseError)?.1? 191 | }; 192 | let signature_header = match signature_header_section { 193 | Section::Custom(custom_section) if custom_section.is_signature_header() => { 194 | custom_section 195 | } 196 | _ => { 197 | debug!("This module is not signed"); 198 | return Err(WSError::NoSignatures); 199 | } 200 | }; 201 | 202 | let signature_data = signature_header.signature_data()?; 203 | if signature_data.hash_function != SIGNATURE_HASH_FUNCTION { 204 | debug!( 205 | "Unsupported hash function: {:02x}", 206 | signature_data.hash_function 207 | ); 208 | return Err(WSError::ParseError); 209 | } 210 | 211 | let signed_hashes_set = signature_data.signed_hashes_set; 212 | let valid_hashes = self.valid_hashes_for_pk(&signed_hashes_set)?; 213 | if valid_hashes.is_empty() { 214 | debug!("No valid signatures"); 215 | return Err(WSError::VerificationFailed); 216 | } 217 | debug!("Hashes matching the signature:"); 218 | for valid_hash in &valid_hashes { 219 | debug!(" - [{}]", Hex::encode_to_string(valid_hash).unwrap()); 220 | } 221 | let mut hasher = Hash::new(); 222 | let mut matching_section_ranges = vec![]; 223 | debug!("Computed hashes:"); 224 | let mut section_sequence_must_be_signed: Option = None; 225 | for (idx, section) in sections { 226 | let section = section?; 227 | section.serialize(&mut hasher)?; 228 | if section.is_signature_delimiter() { 229 | if section_sequence_must_be_signed == Some(false) { 230 | section_sequence_must_be_signed = None; 231 | continue; 232 | } 233 | let h = hasher.finalize().to_vec(); 234 | debug!(" - [{}]", Hex::encode_to_string(&h).unwrap()); 235 | if !valid_hashes.contains(&h) { 236 | return Err(WSError::VerificationFailedForPredicates); 237 | } 238 | matching_section_ranges.push(0..=idx); 239 | section_sequence_must_be_signed = None; 240 | } else { 241 | let section_must_be_signed = predicate(§ion); 242 | if let Some(expected) = section_sequence_must_be_signed { 243 | if section_must_be_signed != expected { 244 | return Err(WSError::VerificationFailedForPredicates); 245 | } 246 | } else { 247 | section_sequence_must_be_signed = Some(section_must_be_signed); 248 | } 249 | } 250 | } 251 | debug!("Valid, signed ranges:"); 252 | for range in &matching_section_ranges { 253 | debug!(" - {}...{}", range.start(), range.end()); 254 | } 255 | Ok(()) 256 | } 257 | 258 | pub(crate) fn valid_hashes_for_pk<'t>( 259 | &self, 260 | signed_hashes_set: &'t [SignedHashes], 261 | ) -> Result>, WSError> { 262 | let mut valid_hashes = HashSet::new(); 263 | for signed_section_sequence in signed_hashes_set { 264 | let mut msg: Vec = vec![]; 265 | msg.extend_from_slice(SIGNATURE_WASM_DOMAIN.as_bytes()); 266 | msg.extend_from_slice(&[ 267 | SIGNATURE_VERSION, 268 | SIGNATURE_WASM_MODULE_CONTENT_TYPE, 269 | SIGNATURE_HASH_FUNCTION, 270 | ]); 271 | let hashes = &signed_section_sequence.hashes; 272 | for hash in hashes { 273 | msg.extend_from_slice(hash); 274 | } 275 | for signature in &signed_section_sequence.signatures { 276 | if let (Some(sig_key_id), Some(pk_key_id)) = (&signature.key_id, &self.key_id) { 277 | if sig_key_id != pk_key_id { 278 | continue; 279 | } 280 | } 281 | let sig = ed25519_compact::Signature::from_slice(&signature.signature)?; 282 | if self.pk.verify(&msg, &sig).is_err() { 283 | continue; 284 | } 285 | debug!("Hash signature is valid for key [{}]", Hex::encode_to_string(*self.pk).unwrap()); 286 | for hash in hashes { 287 | valid_hashes.insert(hash); 288 | } 289 | } 290 | } 291 | Ok(valid_hashes) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/lib/src/signature/keys.rs: -------------------------------------------------------------------------------- 1 | pub use crate::error::*; 2 | 3 | use ct_codecs::{Encoder, Hex}; 4 | #[cfg(feature = "openssh")] 5 | use ssh_keys::{self, openssh}; 6 | use std::collections::HashSet; 7 | use std::fs::File; 8 | use std::io::{self, prelude::*}; 9 | use std::path::Path; 10 | use std::{fmt, str}; 11 | 12 | pub(crate) const ED25519_PK_ID: u8 = 0x01; 13 | pub(crate) const ED25519_SK_ID: u8 = 0x81; 14 | 15 | /// A public key. 16 | #[derive(Clone, Eq, PartialEq, Hash)] 17 | pub struct PublicKey { 18 | pub pk: ed25519_compact::PublicKey, 19 | pub key_id: Option>, 20 | } 21 | 22 | impl PublicKey { 23 | /// Create a public key from raw bytes. 24 | pub fn from_bytes(pk: &[u8]) -> Result { 25 | let mut reader = io::Cursor::new(pk); 26 | let mut id = [0u8]; 27 | reader.read_exact(&mut id)?; 28 | if id[0] != ED25519_PK_ID { 29 | return Err(WSError::UnsupportedKeyType); 30 | } 31 | let mut bytes = vec![]; 32 | reader.read_to_end(&mut bytes)?; 33 | Ok(Self { 34 | pk: ed25519_compact::PublicKey::from_slice(&bytes)?, 35 | key_id: None, 36 | }) 37 | } 38 | 39 | /// Deserialize a PEM-encoded public key. 40 | pub fn from_pem(pem: &str) -> Result { 41 | let pk = ed25519_compact::PublicKey::from_pem(pem)?; 42 | Ok(Self { pk, key_id: None }) 43 | } 44 | 45 | /// Deserialize a DER-encoded public key. 46 | pub fn from_der(der: &[u8]) -> Result { 47 | let pk = ed25519_compact::PublicKey::from_der(der)?; 48 | Ok(Self { pk, key_id: None }) 49 | } 50 | 51 | /// Return the public key as raw bytes. 52 | pub fn to_bytes(&self) -> Vec { 53 | let mut bytes = vec![ED25519_PK_ID]; 54 | bytes.extend_from_slice(self.pk.as_ref()); 55 | bytes 56 | } 57 | 58 | /// Serialize the public key using PEM encoding. 59 | pub fn to_pem(&self) -> String { 60 | self.pk.to_pem() 61 | } 62 | 63 | /// Serialize the public key using DER encoding. 64 | pub fn to_der(&self) -> Vec { 65 | self.pk.to_der() 66 | } 67 | 68 | /// Read public key from a file. 69 | pub fn from_file(file: impl AsRef) -> Result { 70 | let mut fp = File::open(file)?; 71 | let mut bytes = vec![]; 72 | fp.read_to_end(&mut bytes)?; 73 | Self::from_bytes(&bytes) 74 | } 75 | 76 | /// Save the public key to a file. 77 | pub fn to_file(&self, file: impl AsRef) -> Result<(), WSError> { 78 | let mut fp = File::create(file)?; 79 | fp.write_all(&self.to_bytes())?; 80 | Ok(()) 81 | } 82 | 83 | /// Parse a single OpenSSH public key. 84 | #[cfg(feature = "openssh")] 85 | pub fn from_openssh(lines: &str) -> Result { 86 | for line in lines.lines() { 87 | let line = line.trim(); 88 | if let Ok(ssh_keys::PublicKey::Ed25519(raw)) = openssh::parse_public_key(line) { 89 | let mut bytes = vec![ED25519_PK_ID]; 90 | bytes.extend_from_slice(&raw); 91 | if let Ok(pk) = PublicKey::from_bytes(&bytes) { 92 | return Ok(pk); 93 | } 94 | }; 95 | } 96 | Err(WSError::ParseError) 97 | } 98 | 99 | /// Parse a single OpenSSH public key from a file. 100 | #[cfg(feature = "openssh")] 101 | pub fn from_openssh_file(file: impl AsRef) -> Result { 102 | let mut fp = File::open(file)?; 103 | let mut lines = String::new(); 104 | fp.read_to_string(&mut lines)?; 105 | Self::from_openssh(&lines) 106 | } 107 | 108 | /// Try to guess the public key format. 109 | pub fn from_any(data: &[u8]) -> Result { 110 | if let Ok(pk) = Self::from_bytes(data) { 111 | return Ok(pk); 112 | } 113 | if let Ok(pk) = Self::from_der(data) { 114 | return Ok(pk); 115 | } 116 | let s = str::from_utf8(data).map_err(|_| WSError::ParseError)?; 117 | if let Ok(pk) = Self::from_pem(s) { 118 | return Ok(pk); 119 | } 120 | #[cfg(feature = "openssh")] 121 | if let Ok(pk) = Self::from_openssh(s) { 122 | return Ok(pk); 123 | } 124 | Err(WSError::ParseError) 125 | } 126 | 127 | /// Load a key from a file, trying to guess its format. 128 | pub fn from_any_file(file: impl AsRef) -> Result { 129 | let mut fp = File::open(file)?; 130 | let mut bytes = vec![]; 131 | fp.read_to_end(&mut bytes)?; 132 | Self::from_any(&bytes) 133 | } 134 | 135 | /// Return the key identifier associated with this public key, if there is one. 136 | pub fn key_id(&self) -> Option<&Vec> { 137 | self.key_id.as_ref() 138 | } 139 | 140 | /// Compute a deterministic key identifier for this public key, if it doesn't already have one. 141 | pub fn attach_default_key_id(mut self) -> Self { 142 | if self.key_id.is_none() { 143 | self.key_id = Some(hmac_sha256::HMAC::mac(b"key_id", self.pk.as_ref())[0..12].to_vec()); 144 | } 145 | self 146 | } 147 | } 148 | 149 | impl fmt::Debug for PublicKey { 150 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 151 | write!( 152 | f, 153 | "PublicKey {{ [{}] - key_id: {:?} }}", 154 | Hex::encode_to_string(self.pk.as_ref()).unwrap(), 155 | self.key_id() 156 | .map(|key_id| format!("[{}]", Hex::encode_to_string(key_id).unwrap())) 157 | ) 158 | } 159 | } 160 | 161 | /// A secret key. 162 | #[derive(Clone, Eq, PartialEq, Hash)] 163 | pub struct SecretKey { 164 | pub sk: ed25519_compact::SecretKey, 165 | } 166 | 167 | impl SecretKey { 168 | /// Create a secret key from raw bytes. 169 | pub fn from_bytes(sk: &[u8]) -> Result { 170 | let mut reader = io::Cursor::new(sk); 171 | let mut id = [0u8]; 172 | reader.read_exact(&mut id)?; 173 | if id[0] != ED25519_SK_ID { 174 | return Err(WSError::UnsupportedKeyType); 175 | } 176 | let mut bytes = vec![]; 177 | reader.read_to_end(&mut bytes)?; 178 | Ok(Self { 179 | sk: ed25519_compact::SecretKey::from_slice(&bytes)?, 180 | }) 181 | } 182 | 183 | /// Deserialize a PEM-encoded secret key. 184 | pub fn from_pem(pem: &str) -> Result { 185 | let sk = ed25519_compact::SecretKey::from_pem(pem)?; 186 | Ok(Self { sk }) 187 | } 188 | 189 | /// Deserialize a DER-encoded secret key. 190 | pub fn from_der(der: &[u8]) -> Result { 191 | let sk = ed25519_compact::SecretKey::from_der(der)?; 192 | Ok(Self { sk }) 193 | } 194 | 195 | /// Return the secret key as raw bytes. 196 | pub fn to_bytes(&self) -> Vec { 197 | let mut bytes = vec![ED25519_SK_ID]; 198 | bytes.extend_from_slice(self.sk.as_ref()); 199 | bytes 200 | } 201 | 202 | /// Serialize the secret key using PEM encoding. 203 | pub fn to_pem(&self) -> String { 204 | self.sk.to_pem() 205 | } 206 | 207 | /// Serialize the secret key using DER encoding. 208 | pub fn to_der(&self) -> Vec { 209 | self.sk.to_der() 210 | } 211 | 212 | /// Read a secret key from a file. 213 | pub fn from_file(file: impl AsRef) -> Result { 214 | let mut fp = File::open(file)?; 215 | let mut bytes = vec![]; 216 | fp.read_to_end(&mut bytes)?; 217 | Self::from_bytes(&bytes) 218 | } 219 | 220 | /// Save a secret key to a file. 221 | pub fn to_file(&self, file: impl AsRef) -> Result<(), WSError> { 222 | let mut fp = File::create(file)?; 223 | fp.write_all(&self.to_bytes())?; 224 | Ok(()) 225 | } 226 | 227 | /// Parse an OpenSSH secret key. 228 | #[cfg(feature = "openssh")] 229 | pub fn from_openssh(lines: &str) -> Result { 230 | for sk in openssh::parse_private_key(lines).map_err(|_| WSError::ParseError)? { 231 | if let ssh_keys::PrivateKey::Ed25519(raw) = sk { 232 | let mut bytes = vec![ED25519_SK_ID]; 233 | bytes.extend_from_slice(&raw); 234 | return Self::from_bytes(&bytes); 235 | } 236 | } 237 | Err(WSError::UnsupportedKeyType) 238 | } 239 | 240 | /// Read an OpenSSH key from a file. 241 | #[cfg(feature = "openssh")] 242 | pub fn from_openssh_file(file: impl AsRef) -> Result { 243 | let mut fp = File::open(file)?; 244 | let mut lines = String::new(); 245 | fp.read_to_string(&mut lines)?; 246 | Self::from_openssh(&lines) 247 | } 248 | 249 | /// Try to guess the secret key format. 250 | pub fn from_any(data: &[u8]) -> Result { 251 | if let Ok(sk) = Self::from_bytes(data) { 252 | return Ok(sk); 253 | } 254 | if let Ok(sk) = Self::from_der(data) { 255 | return Ok(sk); 256 | } 257 | let s = str::from_utf8(data).map_err(|_| WSError::ParseError)?; 258 | if let Ok(sk) = Self::from_pem(s) { 259 | return Ok(sk); 260 | } 261 | #[cfg(feature = "openssh")] 262 | if let Ok(sk) = Self::from_openssh(s) { 263 | return Ok(sk); 264 | } 265 | Err(WSError::ParseError) 266 | } 267 | 268 | /// Load a key from a file, trying to guess its format. 269 | pub fn from_any_file(file: impl AsRef) -> Result { 270 | let mut fp = File::open(file)?; 271 | let mut bytes = vec![]; 272 | fp.read_to_end(&mut bytes)?; 273 | Self::from_any(&bytes) 274 | } 275 | } 276 | 277 | impl fmt::Debug for SecretKey { 278 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 279 | write!( 280 | f, 281 | "SecretKey {{ [{}] }}", 282 | Hex::encode_to_string(self.sk.as_ref()).unwrap(), 283 | ) 284 | } 285 | } 286 | 287 | /// A key pair. 288 | #[derive(Clone, Eq, PartialEq, Hash, Debug)] 289 | pub struct KeyPair { 290 | /// The public key. 291 | pub pk: PublicKey, 292 | /// The secret key. 293 | pub sk: SecretKey, 294 | } 295 | 296 | impl KeyPair { 297 | /// Generate a new key pair. 298 | pub fn generate() -> Self { 299 | let kp = ed25519_compact::KeyPair::from_seed(ed25519_compact::Seed::generate()); 300 | KeyPair { 301 | pk: PublicKey { 302 | pk: kp.pk, 303 | key_id: None, 304 | }, 305 | sk: SecretKey { sk: kp.sk }, 306 | } 307 | } 308 | } 309 | 310 | /// A set of multiple public keys. 311 | #[derive(Debug, Clone)] 312 | pub struct PublicKeySet { 313 | pub pks: HashSet, 314 | } 315 | 316 | impl PublicKeySet { 317 | /// Create an empty public key set. 318 | pub fn empty() -> Self { 319 | PublicKeySet { 320 | pks: HashSet::new(), 321 | } 322 | } 323 | 324 | /// Create a new public key set. 325 | pub fn new(pks: HashSet) -> Self { 326 | PublicKeySet { pks } 327 | } 328 | 329 | /// Parse an OpenSSH public key set. 330 | #[cfg(feature = "openssh")] 331 | pub fn from_openssh(lines: &str) -> Result { 332 | let mut pks = PublicKeySet::empty(); 333 | for line in lines.lines() { 334 | let line = line.trim(); 335 | if let Ok(ssh_keys::PublicKey::Ed25519(raw)) = openssh::parse_public_key(line) { 336 | let mut bytes = vec![ED25519_PK_ID]; 337 | bytes.extend_from_slice(&raw); 338 | if let Ok(pk) = PublicKey::from_bytes(&bytes) { 339 | pks.pks.insert(pk); 340 | } 341 | }; 342 | } 343 | Ok(pks) 344 | } 345 | 346 | /// Parse an OpenSSH public key set from a file. 347 | #[cfg(feature = "openssh")] 348 | pub fn from_openssh_file(file: impl AsRef) -> Result { 349 | let mut fp = File::open(file)?; 350 | let mut lines = String::new(); 351 | fp.read_to_string(&mut lines)?; 352 | Self::from_openssh(&lines) 353 | } 354 | 355 | /// Return the number of keys in the set. 356 | pub fn len(&self) -> usize { 357 | self.pks.len() 358 | } 359 | 360 | /// Return true if the set is empty. 361 | pub fn is_empty(&self) -> bool { 362 | self.pks.is_empty() 363 | } 364 | 365 | /// Add a public key to the set. 366 | pub fn insert(&mut self, pk: PublicKey) -> Result<(), WSError> { 367 | if !self.pks.insert(pk) { 368 | return Err(WSError::DuplicatePublicKey); 369 | } 370 | Ok(()) 371 | } 372 | 373 | /// Parse and add a key to the set, trying to guess its format. 374 | pub fn insert_any(&mut self, data: &[u8]) -> Result<(), WSError> { 375 | #[cfg(feature = "openssh")] 376 | if let Ok(s) = str::from_utf8(data) { 377 | if let Ok(pk) = PublicKey::from_openssh(s) { 378 | self.insert(pk)?; 379 | return Ok(()); 380 | } 381 | } 382 | let pk = PublicKey::from_any(data)?; 383 | self.insert(pk) 384 | } 385 | 386 | /// Load, parse and add a key to the set, trying to guess its format. 387 | pub fn insert_any_file(&mut self, file: impl AsRef) -> Result<(), WSError> { 388 | let mut fp = File::open(file)?; 389 | let mut data = vec![]; 390 | fp.read_to_end(&mut data)?; 391 | self.insert_any(&data) 392 | } 393 | 394 | /// Merge another public key set into this one. 395 | pub fn merge(&mut self, other: &PublicKeySet) -> Result<(), WSError> { 396 | for pk in other.pks.iter() { 397 | self.insert(pk.clone())?; 398 | } 399 | Ok(()) 400 | } 401 | 402 | /// Remove a key from the set. 403 | pub fn remove(&mut self, pk: &PublicKey) -> Result<(), WSError> { 404 | if !self.pks.remove(pk) { 405 | return Err(WSError::UnknownPublicKey); 406 | } 407 | Ok(()) 408 | } 409 | 410 | /// Return the hash set storing the keys. 411 | pub fn items(&self) -> &HashSet { 412 | &self.pks 413 | } 414 | 415 | /// Return the mutable hash set storing the keys. 416 | pub fn items_mut(&mut self) -> &mut HashSet { 417 | &mut self.pks 418 | } 419 | 420 | /// Add a deterministic key identifier to all the keys that don't have one already. 421 | pub fn attach_default_key_id(mut self) -> Self { 422 | self.pks = self 423 | .pks 424 | .into_iter() 425 | .map(|pk| pk.attach_default_key_id()) 426 | .collect(); 427 | self 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /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 | if self.name() == SIGNATURE_SECTION_DELIMITER_NAME { 178 | let hex = Hex::encode_to_string(self.payload()).unwrap(); 179 | return format!("custom section: [{}]\n- delimiter: [{}]\n", self.name, hex); 180 | } 181 | 182 | if self.name() == SIGNATURE_SECTION_HEADER_NAME { 183 | let signature_data = match SignatureData::deserialize(self.payload()) { 184 | Ok(data) => data, 185 | Err(_) => return "undecodable signature header".to_string(), 186 | }; 187 | let mut s = String::new(); 188 | writeln!(s, "- specification version: 0x{:02x}", signature_data.specification_version).unwrap(); 189 | writeln!(s, "- content_type: 0x{:02x}", signature_data.content_type).unwrap(); 190 | writeln!(s, "- hash function: 0x{:02x} (SHA-256)", signature_data.hash_function).unwrap(); 191 | writeln!(s, "- (hashes,signatures) set:").unwrap(); 192 | for signed_parts in &signature_data.signed_hashes_set { 193 | writeln!(s, " - hashes:").unwrap(); 194 | for hash in &signed_parts.hashes { 195 | writeln!(s, " - [{}]", Hex::encode_to_string(hash).unwrap()).unwrap(); 196 | } 197 | writeln!(s, " - signatures:").unwrap(); 198 | for signature in &signed_parts.signatures { 199 | let sig_hex = Hex::encode_to_string(&signature.signature).unwrap(); 200 | if let Some(key_id) = &signature.key_id { 201 | let key_hex = Hex::encode_to_string(key_id).unwrap(); 202 | writeln!(s, " - [{}] (key id: [{}])", sig_hex, key_hex).unwrap(); 203 | } else { 204 | writeln!(s, " - [{}] (no key id)", sig_hex).unwrap(); 205 | } 206 | } 207 | } 208 | return format!("custom section: [{}]\n{}", self.name(), s); 209 | } 210 | 211 | format!("custom section: [{}]", self.name()) 212 | } 213 | } 214 | 215 | /// A WebAssembly module section. 216 | /// 217 | /// It is recommended to import the `SectionLike` trait for additional functions. 218 | #[derive(Clone)] 219 | pub enum Section { 220 | /// A standard section. 221 | Standard(StandardSection), 222 | /// A custom section. 223 | Custom(CustomSection), 224 | } 225 | 226 | impl SectionLike for Section { 227 | fn id(&self) -> SectionId { 228 | match self { 229 | Section::Standard(s) => s.id(), 230 | Section::Custom(s) => s.id(), 231 | } 232 | } 233 | 234 | fn payload(&self) -> &[u8] { 235 | match self { 236 | Section::Standard(s) => s.payload(), 237 | Section::Custom(s) => s.payload(), 238 | } 239 | } 240 | 241 | fn display(&self, verbose: bool) -> String { 242 | match self { 243 | Section::Standard(s) => s.display(verbose), 244 | Section::Custom(s) => s.display(verbose), 245 | } 246 | } 247 | } 248 | 249 | impl Section { 250 | /// Create a new section with the given identifier and payload. 251 | pub fn new(id: SectionId, payload: Vec) -> Result { 252 | if id != SectionId::CustomSection { 253 | return Ok(Section::Standard(StandardSection::new(id, payload))); 254 | } 255 | let mut reader = io::Cursor::new(payload); 256 | let name_len = varint::get32(&mut reader)? as usize; 257 | let mut name_bytes = vec![0u8; name_len]; 258 | reader.read_exact(&mut name_bytes)?; 259 | let name = str::from_utf8(&name_bytes)?.to_string(); 260 | let mut payload = Vec::new(); 261 | reader.read_to_end(&mut payload)?; 262 | Ok(Section::Custom(CustomSection::new(name, payload))) 263 | } 264 | 265 | /// Create a section from its standard serialized representation. 266 | pub fn deserialize(reader: &mut impl Read) -> Result, WSError> { 267 | let id = match varint::get7(reader) { 268 | Ok(id) => SectionId::from(id), 269 | Err(WSError::Eof) => return Ok(None), 270 | Err(e) => return Err(e), 271 | }; 272 | let len = varint::get32(reader)? as usize; 273 | let mut payload = vec![0u8; len]; 274 | reader.read_exact(&mut payload)?; 275 | let section = Section::new(id, payload)?; 276 | Ok(Some(section)) 277 | } 278 | 279 | /// Serialize a section. 280 | pub fn serialize(&self, writer: &mut impl Write) -> Result<(), WSError> { 281 | let outer_payload; 282 | let payload = match self { 283 | Section::Standard(s) => s.payload(), 284 | Section::Custom(s) => { 285 | outer_payload = s.outer_payload()?; 286 | &outer_payload 287 | } 288 | }; 289 | varint::put(writer, u8::from(self.id()) as _)?; 290 | varint::put(writer, payload.len() as _)?; 291 | writer.write_all(payload)?; 292 | Ok(()) 293 | } 294 | 295 | /// Return `true` if the section contains the module's signatures. 296 | pub fn is_signature_header(&self) -> bool { 297 | if let Section::Custom(s) = self { 298 | return s.is_signature_header(); 299 | } 300 | false 301 | } 302 | 303 | /// Return `true` if the section is a signature delimiter. 304 | pub fn is_signature_delimiter(&self) -> bool { 305 | if let Section::Custom(s) = self { 306 | return s.is_signature_delimiter(); 307 | } 308 | false 309 | } 310 | } 311 | 312 | impl CustomSection { 313 | /// Return `true` if the section contains the module's signatures. 314 | pub fn is_signature_header(&self) -> bool { 315 | self.name() == SIGNATURE_SECTION_HEADER_NAME 316 | } 317 | 318 | /// Return `true` if the section is a signature delimiter. 319 | pub fn is_signature_delimiter(&self) -> bool { 320 | self.name() == SIGNATURE_SECTION_DELIMITER_NAME 321 | } 322 | 323 | /// If the section contains the module's signature, deserializes it into a `SignatureData` object 324 | /// containing the signatures and the hashes. 325 | pub fn signature_data(&self) -> Result { 326 | let header_payload = 327 | SignatureData::deserialize(self.payload()).map_err(|_| WSError::ParseError)?; 328 | Ok(header_payload) 329 | } 330 | } 331 | 332 | impl fmt::Display for Section { 333 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 334 | write!(f, "{}", self.display(false)) 335 | } 336 | } 337 | 338 | impl fmt::Debug for Section { 339 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 340 | write!(f, "{}", self.display(true)) 341 | } 342 | } 343 | 344 | /// A WebAssembly module. 345 | #[derive(Debug, Clone, Default)] 346 | pub struct Module { 347 | pub header: Header, 348 | pub sections: Vec

, 349 | } 350 | 351 | impl Module { 352 | /// Deserialize a WebAssembly module from the given reader. 353 | pub fn deserialize(reader: &mut impl Read) -> Result { 354 | let stream = Self::init_from_reader(reader)?; 355 | let header = stream.header; 356 | let it = Self::iterate(stream)?; 357 | let mut sections = Vec::new(); 358 | for section in it { 359 | sections.push(section?); 360 | } 361 | Ok(Module { header, sections }) 362 | } 363 | 364 | /// Deserialize a WebAssembly module from the given file. 365 | pub fn deserialize_from_file(file: impl AsRef) -> Result { 366 | let fp = File::open(file.as_ref())?; 367 | Self::deserialize(&mut BufReader::new(fp)) 368 | } 369 | 370 | /// Serialize a WebAssembly module to the given writer. 371 | pub fn serialize(&self, writer: &mut impl Write) -> Result<(), WSError> { 372 | writer.write_all(&self.header)?; 373 | for section in &self.sections { 374 | section.serialize(writer)?; 375 | } 376 | Ok(()) 377 | } 378 | 379 | /// Serialize a WebAssembly module to the given file. 380 | pub fn serialize_to_file(&self, file: impl AsRef) -> Result<(), WSError> { 381 | let fp = File::create(file.as_ref())?; 382 | self.serialize(&mut BufWriter::new(fp)) 383 | } 384 | 385 | /// Parse the module's header. This function must be called before `stream()`. 386 | pub fn init_from_reader(reader: &mut T) -> Result, WSError> { 387 | let mut header = Header::default(); 388 | reader.read_exact(&mut header)?; 389 | if header != WASM_HEADER && header != WASM_COMPONENT_HEADER { 390 | return Err(WSError::UnsupportedModuleType); 391 | } 392 | Ok(ModuleStreamReader { reader, header }) 393 | } 394 | 395 | /// Return an iterator over the sections of a WebAssembly module. 396 | /// 397 | /// The module is read in a streaming fashion, and doesn't have to be fully loaded into memory. 398 | pub fn iterate( 399 | module_stream: ModuleStreamReader, 400 | ) -> Result, WSError> { 401 | Ok(SectionsIterator { 402 | reader: module_stream.reader, 403 | }) 404 | } 405 | } 406 | 407 | pub struct ModuleStreamReader<'t, T: Read> { 408 | reader: &'t mut T, 409 | header: Header, 410 | } 411 | 412 | /// An iterator over the sections of a WebAssembly module. 413 | pub struct SectionsIterator<'t, T: Read> { 414 | reader: &'t mut T, 415 | } 416 | 417 | impl<'t, T: Read> Iterator for SectionsIterator<'t, T> { 418 | type Item = Result; 419 | 420 | fn next(&mut self) -> Option { 421 | match Section::deserialize(self.reader) { 422 | Err(e) => Some(Err(e)), 423 | Ok(None) => None, 424 | Ok(Some(section)) => Some(Ok(section)), 425 | } 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /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 | use clap::{command, Arg, Command}; 8 | use regex::RegexBuilder; 9 | use std::fs::File; 10 | use std::io::{prelude::*, BufReader}; 11 | 12 | fn start() -> Result<(), WSError> { 13 | let matches = command!() 14 | .arg( 15 | Arg::new("verbose") 16 | .short('v') 17 | .action(clap::ArgAction::SetTrue) 18 | .help("Verbose output"), 19 | ) 20 | .arg( 21 | Arg::new("debug") 22 | .short('d') 23 | .action(clap::ArgAction::SetTrue) 24 | .help("Prints debugging information"), 25 | ) 26 | .subcommand( 27 | Command::new("keygen") 28 | .about("Generate a new key pair") 29 | .arg( 30 | Arg::new("secret_key") 31 | .value_name("secret_key_file") 32 | .long("secret-key") 33 | .short('k') 34 | .required(true) 35 | .help("Secret key file"), 36 | ) 37 | .arg( 38 | Arg::new("public_key") 39 | .value_name("public_key_file") 40 | .long("public-key") 41 | .short('K') 42 | .required(true) 43 | .help("Public key file"), 44 | ), 45 | ) 46 | .subcommand( 47 | Command::new("show") 48 | .about("Print the structure of a module") 49 | .arg( 50 | Arg::new("in") 51 | .value_name("input_file") 52 | .long("input-file") 53 | .short('i') 54 | .required(true) 55 | .help("Input file"), 56 | ), 57 | ) 58 | .subcommand( 59 | Command::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 | .required(true) 67 | .help("Input file"), 68 | ) 69 | .arg( 70 | Arg::new("out") 71 | .value_name("output_file") 72 | .long("output-file") 73 | .short('o') 74 | .required(true) 75 | .help("Output file"), 76 | ) 77 | .arg( 78 | Arg::new("splits") 79 | .long("split") 80 | .short('s') 81 | .value_name("regex") 82 | .help("Custom section names to be signed"), 83 | ), 84 | ) 85 | .subcommand( 86 | Command::new("sign") 87 | .about("Sign a module") 88 | .arg( 89 | Arg::new("in") 90 | .value_name("input_file") 91 | .long("input-file") 92 | .short('i') 93 | .required(true) 94 | .help("Input file"), 95 | ) 96 | .arg( 97 | Arg::new("out") 98 | .value_name("output_file") 99 | .long("output-file") 100 | .short('o') 101 | .required(true) 102 | .help("Output file"), 103 | ) 104 | .arg( 105 | Arg::new("secret_key") 106 | .value_name("secret_key_file") 107 | .long("secret-key") 108 | .short('k') 109 | .required(true) 110 | .help("Secret key file"), 111 | ) 112 | .arg( 113 | Arg::new("public_key") 114 | .value_name("public_key_file") 115 | .long("public-key") 116 | .short('K') 117 | .help("Public key file"), 118 | ) 119 | .arg( 120 | Arg::new("ssh") 121 | .long("ssh") 122 | .short('Z') 123 | .action(clap::ArgAction::SetTrue) 124 | .help("Parse OpenSSH keys"), 125 | ) 126 | .arg( 127 | Arg::new("signature_file") 128 | .value_name("signature_file") 129 | .long("signature-file") 130 | .short('S') 131 | .help("Signature file"), 132 | ), 133 | ) 134 | .subcommand( 135 | Command::new("verify") 136 | .about("Verify a module's signature") 137 | .arg( 138 | Arg::new("in") 139 | .value_name("input_file") 140 | .long("input-file") 141 | .short('i') 142 | .required(true) 143 | .help("Input file"), 144 | ) 145 | .arg( 146 | Arg::new("public_key") 147 | .value_name("public_key_file") 148 | .long("public-key") 149 | .short('K') 150 | .required(false) 151 | .help("Public key file"), 152 | ) 153 | .arg( 154 | Arg::new("from_github") 155 | .value_name("from_github") 156 | .long("from-github") 157 | .short('G') 158 | .required(false) 159 | .help("GitHub account to retrieve public keys from"), 160 | ) 161 | .arg( 162 | Arg::new("ssh") 163 | .long("ssh") 164 | .short('Z') 165 | .action(clap::ArgAction::SetTrue) 166 | .help("Parse OpenSSH keys"), 167 | ) 168 | .arg( 169 | Arg::new("signature_file") 170 | .value_name("signature_file") 171 | .long("signature-file") 172 | .short('S') 173 | .help("Signature file"), 174 | ) 175 | .arg( 176 | Arg::new("splits") 177 | .long("split") 178 | .short('s') 179 | .value_name("regex") 180 | .help("Custom section names to be verified"), 181 | ), 182 | ) 183 | .subcommand( 184 | Command::new("detach") 185 | .about("Detach the signature from a module") 186 | .arg( 187 | Arg::new("in") 188 | .value_name("input_file") 189 | .long("input-file") 190 | .short('i') 191 | .required(true) 192 | .help("Input file"), 193 | ) 194 | .arg( 195 | Arg::new("out") 196 | .value_name("output_file") 197 | .long("output-file") 198 | .short('o') 199 | .required(true) 200 | .help("Output file"), 201 | ) 202 | .arg( 203 | Arg::new("signature_file") 204 | .value_name("signature_file") 205 | .long("signature-file") 206 | .short('S') 207 | .required(true) 208 | .help("Signature file"), 209 | ), 210 | ) 211 | .subcommand( 212 | Command::new("attach") 213 | .about("Embed a detached signature into a module") 214 | .arg( 215 | Arg::new("in") 216 | .value_name("input_file") 217 | .long("input-file") 218 | .short('i') 219 | .required(true) 220 | .help("Input file"), 221 | ) 222 | .arg( 223 | Arg::new("out") 224 | .value_name("output_file") 225 | .long("output-file") 226 | .short('o') 227 | .required(true) 228 | .help("Output file"), 229 | ) 230 | .arg( 231 | Arg::new("signature_file") 232 | .value_name("signature_file") 233 | .long("signature-file") 234 | .short('S') 235 | .required(true) 236 | .help("Signature file"), 237 | ), 238 | ) 239 | .subcommand( 240 | Command::new("verify_matrix") 241 | .about("Batch verification against multiple public keys") 242 | .arg( 243 | Arg::new("in") 244 | .value_name("input_file") 245 | .long("input-file") 246 | .short('i') 247 | .required(true) 248 | .help("Input file"), 249 | ) 250 | .arg( 251 | Arg::new("public_keys") 252 | .value_name("public_key_files") 253 | .long("public-keys") 254 | .short('K') 255 | .num_args(1..) 256 | .required(false) 257 | .help("Public key files"), 258 | ) 259 | .arg( 260 | Arg::new("from_github") 261 | .value_name("from_github") 262 | .long("from-github") 263 | .short('G') 264 | .required(false) 265 | .help("GitHub account to retrieve public keys from"), 266 | ) 267 | .arg( 268 | Arg::new("ssh") 269 | .long("ssh") 270 | .short('Z') 271 | .action(clap::ArgAction::SetTrue) 272 | .help("Parse OpenSSH keys"), 273 | ) 274 | .arg( 275 | Arg::new("splits") 276 | .long("split") 277 | .short('s') 278 | .value_name("regex") 279 | .help("Custom section names to be verified"), 280 | ), 281 | ) 282 | .get_matches(); 283 | 284 | let verbose = matches.get_flag("verbose"); 285 | let debug = matches.get_flag("debug"); 286 | 287 | env_logger::builder() 288 | .format_timestamp(None) 289 | .format_level(false) 290 | .format_module_path(false) 291 | .format_target(false) 292 | .filter_level(if debug { 293 | log::LevelFilter::Debug 294 | } else { 295 | log::LevelFilter::Info 296 | }) 297 | .init(); 298 | 299 | if let Some(matches) = matches.subcommand_matches("show") { 300 | let input_file = matches.get_one::("in"); 301 | let input_file = input_file 302 | .ok_or(WSError::UsageError("Missing input file"))? 303 | .as_str(); 304 | let module = Module::deserialize_from_file(input_file)?; 305 | module.show(verbose)?; 306 | } else if let Some(matches) = matches.subcommand_matches("keygen") { 307 | let kp = KeyPair::generate(); 308 | let sk_file = matches 309 | .get_one::("secret_key") 310 | .ok_or(WSError::UsageError("Missing secret key file"))? 311 | .as_str(); 312 | let pk_file = matches 313 | .get_one::("public_key") 314 | .ok_or(WSError::UsageError("Missing public key file"))? 315 | .as_str(); 316 | kp.sk.to_file(sk_file)?; 317 | println!("Secret key saved to [{sk_file}]"); 318 | kp.pk.to_file(pk_file)?; 319 | println!("Public key saved to [{pk_file}]"); 320 | } else if let Some(matches) = matches.subcommand_matches("split") { 321 | let input_file = matches.get_one::("in").map(|s| s.as_str()); 322 | let output_file = matches.get_one::("out").map(|s| s.as_str()); 323 | let splits = matches.get_one::("splits").map(|s| s.as_str()); 324 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 325 | let output_file = output_file.ok_or(WSError::UsageError("Missing output file"))?; 326 | let signed_sections_rx = match splits { 327 | None => None, 328 | Some(splits) => Some( 329 | RegexBuilder::new(splits) 330 | .case_insensitive(false) 331 | .multi_line(false) 332 | .dot_matches_new_line(false) 333 | .size_limit(1_000_000) 334 | .dfa_size_limit(1_000_000) 335 | .nest_limit(1000) 336 | .build() 337 | .map_err(|_| WSError::InvalidArgument)?, 338 | ), 339 | }; 340 | let mut module = Module::deserialize_from_file(input_file)?; 341 | module = module.split(|section| match section { 342 | Section::Standard(_) => true, 343 | Section::Custom(custom_section) => { 344 | if let Some(signed_sections_rx) = &signed_sections_rx { 345 | signed_sections_rx.is_match(custom_section.name()) 346 | } else { 347 | true 348 | } 349 | } 350 | })?; 351 | module.serialize_to_file(output_file)?; 352 | println!("* Split module structure:\n"); 353 | module.show(verbose)?; 354 | } else if let Some(matches) = matches.subcommand_matches("sign") { 355 | let input_file = matches.get_one::("in").map(|s| s.as_str()); 356 | let output_file = matches.get_one::("out").map(|s| s.as_str()); 357 | let signature_file = matches 358 | .get_one::("signature_file") 359 | .map(|s| s.as_str()); 360 | let sk_file = matches 361 | .get_one::("secret_key") 362 | .ok_or(WSError::UsageError("Missing secret key file"))? 363 | .as_str(); 364 | let sk = match matches.get_flag("ssh") { 365 | false => SecretKey::from_file(sk_file)?, 366 | true => SecretKey::from_openssh_file(sk_file)?, 367 | }; 368 | let pk_file = matches.get_one::("public_key").map(|s| s.as_str()); 369 | let key_id = if let Some(pk_file) = pk_file { 370 | let pk = match matches.get_flag("ssh") { 371 | false => PublicKey::from_file(pk_file)?, 372 | true => PublicKey::from_openssh_file(pk_file)?, 373 | } 374 | .attach_default_key_id(); 375 | pk.key_id().cloned() 376 | } else { 377 | None 378 | }; 379 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 380 | let output_file = output_file.ok_or(WSError::UsageError("Missing output file"))?; 381 | let module = Module::deserialize_from_file(input_file)?; 382 | let (module, signature) = 383 | sk.sign_multi(module, key_id.as_ref(), signature_file.is_some(), false)?; 384 | if let Some(signature_file) = signature_file { 385 | module.serialize_to_file(output_file)?; 386 | File::create(signature_file)?.write_all(&signature)?; 387 | } else { 388 | module.serialize_to_file(output_file)?; 389 | } 390 | println!("* Signed module structure:\n"); 391 | module.show(verbose)?; 392 | } else if let Some(matches) = matches.subcommand_matches("verify") { 393 | let input_file = matches.get_one::("in").map(|s| s.as_str()); 394 | let signature_file = matches 395 | .get_one::("signature_file") 396 | .map(|s| s.as_str()); 397 | let splits = matches.get_one::("splits").map(|s| s.as_str()); 398 | let signed_sections_rx = match splits { 399 | None => None, 400 | Some(splits) => Some( 401 | RegexBuilder::new(splits) 402 | .case_insensitive(false) 403 | .multi_line(false) 404 | .dot_matches_new_line(false) 405 | .size_limit(1_000_000) 406 | .dfa_size_limit(1_000_000) 407 | .nest_limit(1000) 408 | .build() 409 | .map_err(|_| WSError::InvalidArgument)?, 410 | ), 411 | }; 412 | let pk = if let Some(github_account) = 413 | matches.get_one::("from_github").map(|s| s.as_str()) 414 | { 415 | PublicKey::from_openssh(&get_pks_from_github(github_account)?)? 416 | } else { 417 | let pk_file = matches 418 | .get_one::("public_key") 419 | .ok_or(WSError::UsageError("Missing public key file"))? 420 | .as_str(); 421 | match matches.get_flag("ssh") { 422 | false => PublicKey::from_file(pk_file)?, 423 | true => PublicKey::from_openssh_file(pk_file)?, 424 | } 425 | } 426 | .attach_default_key_id(); 427 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 428 | let mut detached_signatures_ = vec![]; 429 | let detached_signatures = match signature_file { 430 | None => None, 431 | Some(signature_file) => { 432 | File::open(signature_file)?.read_to_end(&mut detached_signatures_)?; 433 | Some(detached_signatures_.as_slice()) 434 | } 435 | }; 436 | let mut reader = BufReader::new(File::open(input_file)?); 437 | if let Some(signed_sections_rx) = &signed_sections_rx { 438 | pk.verify_multi(&mut reader, detached_signatures, |section| match section { 439 | Section::Standard(_) => true, 440 | Section::Custom(custom_section) => { 441 | signed_sections_rx.is_match(custom_section.name()) 442 | } 443 | })?; 444 | } else { 445 | pk.verify(&mut reader, detached_signatures)?; 446 | } 447 | println!("Signature is valid."); 448 | } else if let Some(matches) = matches.subcommand_matches("detach") { 449 | let input_file = matches.get_one::("in").map(|s| s.as_str()); 450 | let output_file = matches.get_one::("out").map(|s| s.as_str()); 451 | let signature_file = matches 452 | .get_one::("signature_file") 453 | .map(|s| s.as_str()); 454 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 455 | let output_file = output_file.ok_or(WSError::UsageError("Missing output file"))?; 456 | let signature_file = 457 | signature_file.ok_or(WSError::UsageError("Missing detached signature file"))?; 458 | let module = Module::deserialize_from_file(input_file)?; 459 | let (module, detached_signature) = module.detach_signature()?; 460 | File::create(signature_file)?.write_all(&detached_signature)?; 461 | module.serialize_to_file(output_file)?; 462 | println!("Signature is now detached."); 463 | } else if let Some(matches) = matches.subcommand_matches("attach") { 464 | let input_file = matches.get_one::("in").map(|s| s.as_str()); 465 | let output_file = matches.get_one::("out").map(|s| s.as_str()); 466 | let signature_file = matches 467 | .get_one::("signature_file") 468 | .map(|s| s.as_str()); 469 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 470 | let output_file = output_file.ok_or(WSError::UsageError("Missing output file"))?; 471 | let signature_file = 472 | signature_file.ok_or(WSError::UsageError("Missing detached signature file"))?; 473 | let mut detached_signature = vec![]; 474 | File::open(signature_file)?.read_to_end(&mut detached_signature)?; 475 | let mut module = Module::deserialize_from_file(input_file)?; 476 | module = module.attach_signature(&detached_signature)?; 477 | module.serialize_to_file(output_file)?; 478 | println!("Signature is now embedded as a custom section."); 479 | } else if let Some(matches) = matches.subcommand_matches("verify_matrix") { 480 | let input_file = matches.get_one::("in").map(|s| s.as_str()); 481 | let signature_file = matches 482 | .get_one::("signature_file") 483 | .map(|s| s.as_str()); 484 | let splits = matches.get_one::("splits").map(|s| s.as_str()); 485 | let signed_sections_rx = match splits { 486 | None => None, 487 | Some(splits) => Some( 488 | RegexBuilder::new(splits) 489 | .case_insensitive(false) 490 | .multi_line(false) 491 | .dot_matches_new_line(false) 492 | .size_limit(1_000_000) 493 | .dfa_size_limit(1_000_000) 494 | .nest_limit(1000) 495 | .build() 496 | .map_err(|_| WSError::InvalidArgument)?, 497 | ), 498 | }; 499 | let pks = if let Some(github_account) = 500 | matches.get_one::("from_github").map(|s| s.as_str()) 501 | { 502 | PublicKeySet::from_openssh(&get_pks_from_github(github_account)?)? 503 | } else { 504 | let pk_files = matches 505 | .get_many::("public_keys") 506 | .ok_or(WSError::UsageError("Missing public key files"))?; 507 | match matches.get_flag("ssh") { 508 | false => { 509 | let mut pks = std::collections::HashSet::new(); 510 | for pk_file in pk_files { 511 | let pk = PublicKey::from_file(pk_file.as_str())?; 512 | pks.insert(pk); 513 | } 514 | PublicKeySet::new(pks) 515 | } 516 | true => PublicKeySet::from_openssh_file( 517 | pk_files 518 | .map(|s| s.as_str()) 519 | .next() 520 | .ok_or(WSError::UsageError("Missing public keys file"))?, 521 | )?, 522 | } 523 | } 524 | .attach_default_key_id(); 525 | let input_file = input_file.ok_or(WSError::UsageError("Missing input file"))?; 526 | let mut detached_signatures_ = vec![]; 527 | let detached_signatures = match signature_file { 528 | None => None, 529 | Some(signature_file) => { 530 | File::open(signature_file)?.read_to_end(&mut detached_signatures_)?; 531 | Some(detached_signatures_.as_slice()) 532 | } 533 | }; 534 | let mut reader = BufReader::new(File::open(input_file)?); 535 | let predicates: Vec = if let Some(signed_sections_rx) = signed_sections_rx { 536 | vec![Box::new(move |section| match section { 537 | Section::Standard(_) => true, 538 | Section::Custom(custom_section) => { 539 | signed_sections_rx.is_match(custom_section.name()) 540 | } 541 | })] 542 | } else { 543 | vec![Box::new(|_| true)] 544 | }; 545 | let matrix = pks.verify_matrix(&mut reader, detached_signatures, &predicates)?; 546 | let valid_pks = matrix.first().ok_or(WSError::UsageError("No predicates"))?; 547 | if valid_pks.is_empty() { 548 | println!("No valid public keys found"); 549 | } else { 550 | println!("Valid public keys:"); 551 | for pk in valid_pks { 552 | println!(" - {pk:x?}"); 553 | } 554 | } 555 | } else { 556 | return Err(WSError::UsageError("No subcommand specified")); 557 | } 558 | Ok(()) 559 | } 560 | 561 | fn get_pks_from_github(account: impl AsRef) -> Result { 562 | let account_rawurlencoded = uri_encode::encode_uri_component(account.as_ref()); 563 | let url = format!("https://github.com/{account_rawurlencoded}.keys"); 564 | let response = ureq::get(&url) 565 | .call() 566 | .map_err(|_| WSError::UsageError("Keys couldn't be retrieved from GitHub"))?; 567 | let s = response 568 | .into_body() 569 | .read_to_vec() 570 | .map_err(|_| WSError::UsageError("Keys couldn't be retrieved from GitHub"))?; 571 | String::from_utf8(s).map_err(|_| { 572 | WSError::UsageError("Unexpected characters in the public keys retrieved from GitHub") 573 | }) 574 | } 575 | 576 | fn main() -> Result<(), WSError> { 577 | let res = start(); 578 | match res { 579 | Ok(_) => {} 580 | Err(e) => { 581 | eprintln!("{e}"); 582 | std::process::exit(1); 583 | } 584 | } 585 | Ok(()) 586 | } 587 | --------------------------------------------------------------------------------