From d268766d90fcc958c1f294568922554a0da5dcfa Mon Sep 17 00:00:00 2001 From: John Reese Date: Mon, 1 Dec 2025 09:41:17 +0000 Subject: [PATCH 1/3] Prepare repo for submodule embedding --- README.md | 450 +-------------- docs/INTEGRATION.md | 71 +++ .../archive/ADDITIONAL_FORMATS.md | 0 .../archive/AUDIO_HARDENING.md | 0 .../archive/COCKPIT_INTEGRATION.md | 0 .../archive/KERNEL_BUILD.md | 0 .../archive/LOAD_TESTING.md | 0 .../archive/METEOR_LAKE_BUILD.md | 0 .../archive/PRODUCTION_DEPLOYMENT.md | 0 .../archive/PR_DESCRIPTION.md | 0 QUICKSTART.md => docs/archive/QUICKSTART.md | 0 docs/archive/README.md | 18 + .../archive/SECURITY_ARCHITECTURE.md | 0 .../archive/VIDEO_HARDENING.md | 0 mission.md => docs/archive/mission.md | 0 image_harden/src/api.rs | 159 ++++++ image_harden/src/lib.rs | 530 +++++++++++------- 17 files changed, 599 insertions(+), 629 deletions(-) create mode 100644 docs/INTEGRATION.md rename ADDITIONAL_FORMATS.md => docs/archive/ADDITIONAL_FORMATS.md (100%) rename AUDIO_HARDENING.md => docs/archive/AUDIO_HARDENING.md (100%) rename COCKPIT_INTEGRATION.md => docs/archive/COCKPIT_INTEGRATION.md (100%) rename KERNEL_BUILD.md => docs/archive/KERNEL_BUILD.md (100%) rename LOAD_TESTING.md => docs/archive/LOAD_TESTING.md (100%) rename METEOR_LAKE_BUILD.md => docs/archive/METEOR_LAKE_BUILD.md (100%) rename PRODUCTION_DEPLOYMENT.md => docs/archive/PRODUCTION_DEPLOYMENT.md (100%) rename PR_DESCRIPTION.md => docs/archive/PR_DESCRIPTION.md (100%) rename QUICKSTART.md => docs/archive/QUICKSTART.md (100%) create mode 100644 docs/archive/README.md rename SECURITY_ARCHITECTURE.md => docs/archive/SECURITY_ARCHITECTURE.md (100%) rename VIDEO_HARDENING.md => docs/archive/VIDEO_HARDENING.md (100%) rename mission.md => docs/archive/mission.md (100%) create mode 100644 image_harden/src/api.rs diff --git a/README.md b/README.md index 5e0ab8b..0df200e 100644 --- a/README.md +++ b/README.md @@ -1,448 +1,52 @@ # IMAGEHARDER -**Comprehensive Hardened Media Decoder with Extended Format Support** +Hardened media decoding stack that prioritizes safety, observability, and predictable integration. The repo is now organized for use as a Git submodule so parent projects can embed the `image_harden` crate and reuse the hardened build tooling. -IMAGEHARDER is a production-grade system for hardening image, audio, and video decoding libraries. It provides comprehensive security measures including CPU-optimized hardening flags, sandboxing, fuzzing infrastructure, and support for extended modern formats. - ---- - -## πŸ“‹ Table of Contents - -- [Supported Formats](#supported-formats) -- [Security Features](#security-features) -- [CPU Optimization](#cpu-optimization) -- [Getting Started](#getting-started) -- [Build Instructions](#build-instructions) -- [Usage](#usage) -- [Fuzzing](#fuzzing) -- [Architecture](#architecture) -- [Documentation](#documentation) - ---- - -## 🎨 Supported Formats - -### Core Image Formats -- **PNG** - libpng with strict limits (CVE-2015-8540, CVE-2019-7317 mitigations) -- **JPEG** - libjpeg-turbo (CVE-2018-14498 mitigation) -- **GIF** - giflib (CVE-2019-15133, CVE-2016-3977 mitigations) -- **WebP** - Pure Rust decoder (CVE-2023-4863 mitigation, HIGH PRIORITY) -- **HEIF/HEIC** - Apple format (iOS/macOS image format) -- **SVG** - resvg (Pure Rust, memory-safe with sanitization) - -### Extended Image Formats (NEW) -- **AVIF** - AV1 Image File Format (libavif + dav1d) -- **JPEG XL** - Next-gen lossy/lossless (libjxl) -- **TIFF** - Tagged Image File Format (libtiff, CVE-hardened) -- **OpenEXR** - High Dynamic Range images (VFX/HDR workflows) - -### Hidden-Path Components (NEW) -- **ICC Profiles** - Color management (lcms2, with stripping option) -- **EXIF Metadata** - Photo metadata (libexif, with privacy stripping) - -### Audio Formats (Pure Rust) -- **MP3** - minimp3 (Rust wrapper) -- **Vorbis** - lewton (pure Rust) -- **FLAC** - claxon (pure Rust) -- **Opus** - opus crate -- **Ogg** - ogg container (pure Rust) - -### Video Formats -- **MP4/MOV** - mp4parse (Firefox's Rust implementation) -- **MKV/WebM** - matroska (pure Rust EBML) -- **FFmpeg** - WebAssembly sandboxed (MPEG-TS and others) - ---- - -## πŸ”’ Security Features - -### Compile-Time Hardening -- **Stack Protection**: `-fstack-protector-strong`, `-fstack-clash-protection` -- **Memory Safety**: `-D_FORTIFY_SOURCE=3`, `-fPIE`, `-pie` -- **RELRO**: `-Wl,-z,relro,-z,now` -- **No Executable Stack**: `-Wl,-z,noexecstack` -- **Control Flow**: `-fcf-protection=full` (CET on x86_64) -- **Hidden Symbols**: `-fvisibility=hidden` - -### Runtime Protection -- **Strict Resource Limits**: - - Image dimensions (default: 8192x8192, configurable up to 16384x16384) - - File sizes (256-500 MB depending on format) - - IFD counts (TIFF: max 100 IFDs) - - Tag counts (ICC: max 256, EXIF: max 512) - - Memory quotas enforced before allocation - -- **Input Validation**: - - Magic byte verification - - Header sanity checks - - Dimension bounds checking - - Container structure validation - -- **Fail-Closed Error Handling**: - - No best-effort decoding - - Hard errors on warnings - - No partial data leakage - -### Sandboxing -- **Kernel Namespaces** (PID, mount, user, network) -- **seccomp-bpf** syscall filtering -- **Landlock** filesystem access control -- **WebAssembly** sandbox for FFmpeg - -### Privacy Protection -- **Default ICC Profile Stripping**: Removes color profiles in hardened mode -- **Default EXIF Stripping**: Removes all metadata including GPS -- **Selective Retention**: Optional validated ICC/EXIF with strict limits - ---- - -## ⚑ CPU Optimization - -IMAGEHARDER supports CPU-tuned builds for maximum performance: - -### CPU Profiles - -| Profile | Target | Use Case | -|---------|--------|----------| -| `generic` | x86-64 baseline | Maximum compatibility | -| `v3` | x86-64-v3 (AVX2) | CI/production (Haswell+) | -| `host` | Native CPU | Development (Intel Core Ultra 7 165H) | - -### Build Examples - -```bash -# Generic (portable) -IMAGEHARDEN_CPU=generic ./build.sh - -# AVX2 baseline (recommended for production) -IMAGEHARDEN_CPU=v3 ./build_extended_formats.sh - -# Host-optimized (Meteor Lake: AVX2, AVX-VNNI, AES-NI, SHA) -IMAGEHARDEN_CPU=host ./build_extended_formats.sh -``` - -### Optimizations Applied -- **Meteor Lake** (`host`): `-march=native -mavx2 -mfma -mbmi -mbmi2 -maes -msha -mpclmul` -- **AVX2** (`v3`): `-march=x86-64-v3 -mtune=core-avx2` -- **Generic**: `-march=x86-64 -mtune=generic` - ---- - -## πŸš€ Getting Started - -### Prerequisites - -#### System Requirements -- Debian-based Linux (Ubuntu, Debian) -- Kernel 5.13+ (for Landlock support) -- 64-bit x86_64 architecture - -#### Build Dependencies -```bash -sudo apt-get update && sudo apt-get install -y \ - build-essential clang cmake nasm meson ninja-build \ - autoconf automake libtool git pkg-config \ - libseccomp-dev yasm python3-pip \ - rustc cargo -``` - ---- - -## πŸ”¨ Build Instructions - -### 1. Clone and Initialize +## Highlights +- Defensive decoders for common image formats (PNG, JPEG, GIF, WebP, HEIC/HEIF, SVG) plus pure-Rust audio codecs (MP3, Vorbis, FLAC) and video container validation. +- Extended format modules (AVIF, JPEG XL, TIFF, OpenEXR, ICC/EXIF) are feature-gated for projects that need them. +- Runtime hardening: strict dimension/file limits, fail-closed error handling, sandboxed video validation, and optional Prometheus metrics server. +- Drop-in CLI (`image_harden_cli`) for local validation using the same code paths as the library. +## Quick start ```bash +# Clone as a standalone repo git clone https://github.com/SWORDIntel/IMAGEHARDER.git -cd IMAGEHARDER -git submodule update --init --recursive -``` - -### 2. Build Core Libraries - -```bash -# Build core image libraries (GIF, etc.) -./build.sh - -# Build extended formats (AVIF, JXL, TIFF, OpenEXR, ICC, EXIF) -./build_extended_formats.sh - -# Build audio codecs (optional, Rust uses pure implementations) -./build_audio.sh -# Build FFmpeg WebAssembly sandbox -./setup_emsdk.sh -./build_ffmpeg_wasm.sh +# or add as a submodule inside an existing project +git submodule add https://github.com/SWORDIntel/IMAGEHARDER.git external/imageharder ``` -### 3. Build Rust Components - -```bash -cd image_harden -cargo build --release -``` - -### 4. Run Tests - -```bash -cargo test --release -``` - ---- - -## πŸ“– Usage - -### Rust API - -Add to your `Cargo.toml`: +## Public API (library) +Add the crate to your workspace and call the unified API surface: ```toml +# parent-project/Cargo.toml [dependencies] -image_harden = { path = "../image_harden" } -``` - -### Example: Decoding Images - -```rust -use image_harden::{decode_png, decode_jpeg, ImageHardenError}; - -fn main() -> Result<(), ImageHardenError> { - // Decode PNG with hardening - let png_data = std::fs::read("image.png")?; - let decoded = decode_png(&png_data)?; - - // Decode JPEG - let jpeg_data = std::fs::read("photo.jpg")?; - let decoded = decode_jpeg(&jpeg_data)?; - - Ok(()) -} +image_harden = { path = "external/imageharder/image_harden" } ``` -### Example: Extended Formats - ```rust -use image_harden::formats::{avif, jxl, tiff, exr}; - -fn decode_modern_formats() -> Result<(), ImageHardenError> { - // AVIF (AV1 images) - #[cfg(feature = "avif")] - { - let avif_data = std::fs::read("image.avif")?; - let decoded = avif::decode_avif(&avif_data)?; - } +use image_harden::api::{DecodedMedia, HardenedDecoder, MediaFormat}; - // JPEG XL - #[cfg(feature = "jxl")] - { - let jxl_data = std::fs::read("image.jxl")?; - let decoded = jxl::decode_jxl(&jxl_data)?; - } - - // TIFF - #[cfg(feature = "tiff")] - { - let tiff_data = std::fs::read("scan.tiff")?; - let decoded = tiff::decode_tiff(&tiff_data)?; - } - - // OpenEXR (HDR) - #[cfg(feature = "openexr")] - { - let exr_data = std::fs::read("render.exr")?; - let decoded = exr::decode_exr(&exr_data)?; - } - - Ok(()) +fn decode(bytes: &[u8]) -> Result { + HardenedDecoder::decode(MediaFormat::Png, bytes) } ``` -### Example: Metadata Handling - -```rust -use image_harden::formats::{icc, exif}; - -fn handle_metadata() -> Result<(), ImageHardenError> { - // Validate ICC profile - #[cfg(feature = "icc")] - { - let profile_data = std::fs::read("profile.icc")?; - let info = icc::validate_icc_profile(&profile_data)?; - println!("ICC version: {}.{}", info.version_major, info.version_minor); - } - - // Validate EXIF (or strip for privacy) - #[cfg(feature = "exif")] - { - let exif_data = extract_exif_from_jpeg(&jpeg_data)?; - let info = exif::validate_exif(&exif_data)?; - - // Strip GPS data for privacy - let sanitized = exif::strip_gps_from_exif(&exif_data)?; - } - - Ok(()) -} -``` - ---- - -## πŸ› Fuzzing - -IMAGEHARDER includes comprehensive fuzzing infrastructure using `cargo-fuzz`. - -### Available Fuzz Targets - -#### Core Formats -- `fuzz_png`, `fuzz_jpeg`, `fuzz_gif` -- `fuzz_webp`, `fuzz_heif`, `fuzz_svg` - -#### Extended Formats -- `fuzz_avif`, `fuzz_jxl`, `fuzz_tiff`, `fuzz_exr` - -#### Hidden-Path Components -- `fuzz_icc`, `fuzz_exif` - -#### Audio -- `fuzz_mp3`, `fuzz_vorbis`, `fuzz_flac`, `fuzz_opus` - -#### Video -- `fuzz_video_mp4`, `fuzz_video_mkv` - -### Running Fuzz Tests +See [`docs/INTEGRATION.md`](docs/INTEGRATION.md) for submodule workflow, feature flags, and metrics exposure. +## Building the CLI ```bash cd image_harden - -# Install cargo-fuzz -cargo install cargo-fuzz - -# Run a specific target -cargo fuzz run fuzz_avif - -# Run with sanitizers -cargo fuzz run fuzz_tiff -- -max_total_time=60 - -# Run all targets (CI) -./run_all_fuzz_tests.sh -``` - ---- - -## πŸ—οΈ Architecture - -### Directory Structure - -``` -IMAGEHARDER/ -β”œβ”€β”€ config/ -β”‚ └── hardening-flags.mk # Centralized hardening configuration -β”œβ”€β”€ image_harden/ -β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ lib.rs # Core decoding functions -β”‚ β”‚ β”œβ”€β”€ formats/ # Extended format modules -β”‚ β”‚ β”‚ β”œβ”€β”€ avif.rs -β”‚ β”‚ β”‚ β”œβ”€β”€ jxl.rs -β”‚ β”‚ β”‚ β”œβ”€β”€ tiff.rs -β”‚ β”‚ β”‚ β”œβ”€β”€ exr.rs -β”‚ β”‚ β”‚ β”œβ”€β”€ icc.rs -β”‚ β”‚ β”‚ └── exif.rs -β”‚ β”‚ β”œβ”€β”€ metrics.rs # Prometheus metrics -β”‚ β”‚ └── metrics_server.rs -β”‚ β”œβ”€β”€ fuzz/ -β”‚ β”‚ └── fuzz_targets/ # Fuzzing harnesses -β”‚ β”œβ”€β”€ build.rs # C library FFI binding generation -β”‚ └── Cargo.toml -β”œβ”€β”€ docs/ -β”‚ └── HARDENING_EXTRAS.md # Extended hardening specification -β”œβ”€β”€ build.sh # Core library builder -β”œβ”€β”€ build_extended_formats.sh # Extended format builder -β”œβ”€β”€ build_audio.sh # Audio codec builder -└── build_ffmpeg_wasm.sh # FFmpeg WASM builder +cargo build --release ``` -### Format Coverage Matrix - -| Format | Decoder | Hardening | Fuzzing | Sandboxing | -|--------|---------|-----------|---------|------------| -| PNG | libpng | βœ… | βœ… | βœ… | -| JPEG | libjpeg-turbo | βœ… | βœ… | βœ… | -| GIF | giflib | βœ… | βœ… | βœ… | -| WebP | Pure Rust | βœ… | βœ… | βœ… | -| HEIF | libheif-rs | βœ… | βœ… | βœ… | -| SVG | resvg (Rust) | βœ… | βœ… | βœ… | -| AVIF | libavif+dav1d | βœ… | βœ… | 🚧 | -| JPEG XL | libjxl | βœ… | βœ… | 🚧 | -| TIFF | libtiff | βœ… | βœ… | 🚧 | -| OpenEXR | openexr | βœ… | βœ… | 🚧 | -| MP3 | minimp3 (Rust) | βœ… | βœ… | βœ… | -| Vorbis | lewton (Rust) | βœ… | βœ… | βœ… | -| FLAC | claxon (Rust) | βœ… | βœ… | βœ… | -| Opus | opus (Rust) | βœ… | βœ… | βœ… | -| MP4 | mp4parse (Rust) | βœ… | βœ… | βœ… | -| FFmpeg | WASM | βœ… | βœ… | βœ… | - ---- - -## πŸ“š Documentation - -- **[HARDENING_EXTRAS.md](docs/HARDENING_EXTRAS.md)** - Extended format hardening specification -- **[KERNEL_BUILD.md](KERNEL_BUILD.md)** - Kernel configuration for Landlock support -- **[config/hardening-flags.mk](config/hardening-flags.mk)** - Hardening flag reference - -### Hardening Specification - -The [HARDENING_EXTRAS.md](docs/HARDENING_EXTRAS.md) document defines: -- Extended media surface (AVIF, JXL, TIFF, OpenEXR, MPEG-TS) -- Hidden-path component policies (ICC, EXIF, fonts) -- CPU-tuned compilation profiles -- Sanitizer and fuzzing configurations -- Sandboxing models - ---- - -## 🀝 Contributing - -Contributions are welcome! Please ensure: -- All C/C++ code uses hardening flags from `config/hardening-flags.mk` -- New formats include Rust FFI wrappers with validation -- Fuzzing targets are added for new decoders -- Tests pass with sanitizers enabled - ---- - -## πŸ“„ License - -MIT License - see LICENSE file for details - ---- - -## πŸ™ Acknowledgments - -Built on: -- VideoLAN's dav1d (AV1 decoder) -- AOMediaCodec's libavif -- libjxl (JPEG XL Reference Implementation) -- Little CMS (lcms2) -- OpenEXR (Academy Software Foundation) -- FFmpeg Project -- Rust ecosystem (resvg, lewton, claxon, mp4parse, matroska) - ---- - -## πŸ” Security Contact - -For security issues, please contact: [security contact information] - -**CVEs Addressed**: -- CVE-2023-4863 (WebP) -- CVE-2019-7317, CVE-2015-8540 (libpng) -- CVE-2018-14498 (libjpeg) -- CVE-2019-15133, CVE-2016-3977 (giflib) -- And many more through comprehensive hardening - ---- - -**Status**: Production-Ready with Extended Format Support (v0.2.0) +## Repository layout +- `image_harden/` – Rust crate with the hardened decoders and CLI entrypoint. +- `docs/INTEGRATION.md` – concise guide for embedding as a submodule. +- `docs/archive/` – preserved deep-dives (build notes, platform guides, extended hardening writeups). +- `config/`, `ffmpeg/`, `docker-compose.yml` – deployment aids retained for downstream integrators. -**Platform**: Debian-based Linux (x86-64, Intel Core Ultra 7 165H optimized) +## Security +Security contacts and policy are tracked in [`SECURITY.md`](SECURITY.md). Runtime mitigations are enabled by default; feature flags in `image_harden/Cargo.toml` gate optional codecs. diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md new file mode 100644 index 0000000..fa85bd5 --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,71 @@ +# Integration guide + +This guide shows how to embed IMAGEHARDER as a Git submodule and call the hardened decoder API from a parent project. + +## 1) Add the repository as a submodule +```bash +git submodule add https://github.com/SWORDIntel/IMAGEHARDER.git external/imageharder +git submodule update --init --recursive +``` + +## 2) Wire the crate into your workspace +Add the crate using a path dependency so the parent project builds against the submodule source. + +```toml +# parent-project/Cargo.toml +[dependencies] +image_harden = { path = "external/imageharder/image_harden" } +``` + +If you keep a Cargo workspace, also append the crate to `members`: +```toml +[workspace] +members = [ + "image_harden", # if IMAGEHARDER lives at repo root + "external/imageharder/image_harden" +] +``` + +## 3) Call the public API +The `api` module exposes a minimal surface that wraps the internal decoders and feature-gated formats. + +```rust +use image_harden::api::{DecodedMedia, HardenedDecoder, MediaFormat}; + +fn decode_png(bytes: &[u8]) -> Result { + HardenedDecoder::decode(MediaFormat::Png, bytes) +} +``` + +You can check what is currently compiled in (based on enabled features) at runtime: +```rust +let advertised = image_harden::api::supported_formats(); +assert!(advertised.contains(&"png")); +``` + +## 4) Feature flags +Optional formats are disabled by default to keep builds lean. Enable only what you need in the dependency declaration: + +```toml +[dependencies] +image_harden = { path = "external/imageharder/image_harden", features = ["avif", "jxl", "tiff", "openexr", "icc", "exif"] } +``` + +## 5) Metrics (optional) +The crate ships a tiny Prometheus exporter to surface decode metrics. + +```rust +// Expose metrics on 0.0.0.0:9898 +image_harden::metrics_server::start_metrics_server("0.0.0.0:9898")?; +``` + +## 6) Updating the submodule +Pull upstream changes and propagate to your workspace: +```bash +git submodule update --remote external/imageharder +cd external/imageharder && git checkout +``` + +## Notes for Xen domU/CI builders +- The build is pure Rust by default; C-based optional codecs (e.g., libavif) require system headers and are isolated behind feature flags. +- The CLI entrypoint (`image_harden_cli`) exercises the same hardened limits as the library for parity testing. diff --git a/ADDITIONAL_FORMATS.md b/docs/archive/ADDITIONAL_FORMATS.md similarity index 100% rename from ADDITIONAL_FORMATS.md rename to docs/archive/ADDITIONAL_FORMATS.md diff --git a/AUDIO_HARDENING.md b/docs/archive/AUDIO_HARDENING.md similarity index 100% rename from AUDIO_HARDENING.md rename to docs/archive/AUDIO_HARDENING.md diff --git a/COCKPIT_INTEGRATION.md b/docs/archive/COCKPIT_INTEGRATION.md similarity index 100% rename from COCKPIT_INTEGRATION.md rename to docs/archive/COCKPIT_INTEGRATION.md diff --git a/KERNEL_BUILD.md b/docs/archive/KERNEL_BUILD.md similarity index 100% rename from KERNEL_BUILD.md rename to docs/archive/KERNEL_BUILD.md diff --git a/LOAD_TESTING.md b/docs/archive/LOAD_TESTING.md similarity index 100% rename from LOAD_TESTING.md rename to docs/archive/LOAD_TESTING.md diff --git a/METEOR_LAKE_BUILD.md b/docs/archive/METEOR_LAKE_BUILD.md similarity index 100% rename from METEOR_LAKE_BUILD.md rename to docs/archive/METEOR_LAKE_BUILD.md diff --git a/PRODUCTION_DEPLOYMENT.md b/docs/archive/PRODUCTION_DEPLOYMENT.md similarity index 100% rename from PRODUCTION_DEPLOYMENT.md rename to docs/archive/PRODUCTION_DEPLOYMENT.md diff --git a/PR_DESCRIPTION.md b/docs/archive/PR_DESCRIPTION.md similarity index 100% rename from PR_DESCRIPTION.md rename to docs/archive/PR_DESCRIPTION.md diff --git a/QUICKSTART.md b/docs/archive/QUICKSTART.md similarity index 100% rename from QUICKSTART.md rename to docs/archive/QUICKSTART.md diff --git a/docs/archive/README.md b/docs/archive/README.md new file mode 100644 index 0000000..b10d3b4 --- /dev/null +++ b/docs/archive/README.md @@ -0,0 +1,18 @@ +# Archived root documents + +The following documents were moved out of the repository root to keep the entrypoint clean for submodule consumers. Content is unchanged: + +- `ADDITIONAL_FORMATS.md` +- `AUDIO_HARDENING.md` +- `COCKPIT_INTEGRATION.md` +- `KERNEL_BUILD.md` +- `LOAD_TESTING.md` +- `METEOR_LAKE_BUILD.md` +- `PRODUCTION_DEPLOYMENT.md` +- `PR_DESCRIPTION.md` +- `QUICKSTART.md` +- `SECURITY_ARCHITECTURE.md` +- `VIDEO_HARDENING.md` +- `mission.md` + +Refer to these files for the original deep-dives on platform-specific builds, production deployment, and project background. diff --git a/SECURITY_ARCHITECTURE.md b/docs/archive/SECURITY_ARCHITECTURE.md similarity index 100% rename from SECURITY_ARCHITECTURE.md rename to docs/archive/SECURITY_ARCHITECTURE.md diff --git a/VIDEO_HARDENING.md b/docs/archive/VIDEO_HARDENING.md similarity index 100% rename from VIDEO_HARDENING.md rename to docs/archive/VIDEO_HARDENING.md diff --git a/mission.md b/docs/archive/mission.md similarity index 100% rename from mission.md rename to docs/archive/mission.md diff --git a/image_harden/src/api.rs b/image_harden/src/api.rs new file mode 100644 index 0000000..21b125b --- /dev/null +++ b/image_harden/src/api.rs @@ -0,0 +1,159 @@ +//! Public API surface for embedding IMAGEHARDER as a library. +//! This module wraps the individual decoders into a small, stable interface +//! that parent projects can depend on when the repository is consumed as a +//! Git submodule. + +use crate::{ + decode_flac, decode_gif, decode_heif, decode_jpeg, decode_mp3, decode_png, decode_svg, + decode_video, decode_vorbis, decode_webp, AudioData, ImageHardenError, +}; + +#[cfg(feature = "avif")] +use crate::formats::avif::decode_avif; +#[cfg(feature = "exif")] +use crate::formats::exif::validate_exif; +#[cfg(feature = "openexr")] +use crate::formats::exr::decode_exr; +#[cfg(feature = "icc")] +use crate::formats::icc::validate_icc_profile; +#[cfg(feature = "jxl")] +use crate::formats::jxl::decode_jxl; +#[cfg(feature = "tiff")] +use crate::formats::tiff::decode_tiff; + +/// Supported media types for the unified decoder entrypoint. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MediaFormat { + Png, + Jpeg, + Gif, + WebP, + Heif, + Svg, + #[cfg(feature = "avif")] + Avif, + #[cfg(feature = "jxl")] + JpegXl, + #[cfg(feature = "tiff")] + Tiff, + #[cfg(feature = "openexr")] + OpenExr, + AudioMp3, + AudioVorbis, + AudioFlac, + VideoContainer, +} + +/// Decoder output variants. +#[derive(Debug, Clone)] +pub enum DecodedMedia { + Image(Vec), + Audio(AudioData), + Video(Vec), +} + +/// Optional knobs for decoding. Currently only video uses an option +/// to specify the sandboxed WASM path. +#[derive(Debug, Default, Clone)] +pub struct DecoderOptions { + pub video_wasm_path: Option, +} + +/// Convenience wrapper that exposes a minimal surface area for downstream +/// consumers. Use `decode` for sensible defaults or `decode_with_options` to +/// pass explicit configuration. +pub struct HardenedDecoder; + +impl HardenedDecoder { + /// Returns the crate version to make it easy to verify the linked build. + pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION") + } + + /// Decode using defaults. + pub fn decode(format: MediaFormat, data: &[u8]) -> Result { + Self::decode_with_options(format, data, &DecoderOptions::default()) + } + + /// Decode with explicit options (e.g., WASM path for video validation). + pub fn decode_with_options( + format: MediaFormat, + data: &[u8], + options: &DecoderOptions, + ) -> Result { + match format { + MediaFormat::Png => decode_png(data).map(DecodedMedia::Image), + MediaFormat::Jpeg => decode_jpeg(data).map(DecodedMedia::Image), + MediaFormat::Gif => decode_gif(data).map(DecodedMedia::Image), + MediaFormat::WebP => decode_webp(data).map(DecodedMedia::Image), + MediaFormat::Heif => decode_heif(data).map(DecodedMedia::Image), + MediaFormat::Svg => decode_svg(data).map(DecodedMedia::Image), + #[cfg(feature = "avif")] + MediaFormat::Avif => decode_avif(data).map(DecodedMedia::Image), + #[cfg(feature = "jxl")] + MediaFormat::JpegXl => decode_jxl(data).map(DecodedMedia::Image), + #[cfg(feature = "tiff")] + MediaFormat::Tiff => decode_tiff(data).map(DecodedMedia::Image), + #[cfg(feature = "openexr")] + MediaFormat::OpenExr => decode_exr(data).map(DecodedMedia::Image), + MediaFormat::AudioMp3 => decode_mp3(data).map(DecodedMedia::Audio), + MediaFormat::AudioVorbis => decode_vorbis(data).map(DecodedMedia::Audio), + MediaFormat::AudioFlac => decode_flac(data).map(DecodedMedia::Audio), + MediaFormat::VideoContainer => { + decode_video(data, options.video_wasm_path.as_deref().unwrap_or("")) + .map(DecodedMedia::Video) + } + } + } +} + +/// Report which formats are available in the current build based on feature +/// flags. Useful for capability advertisement in parent applications. +pub fn supported_formats() -> Vec<&'static str> { + let mut formats = vec![ + "png", "jpeg", "gif", "webp", "heif", "svg", "mp3", "vorbis", "flac", "video", + ]; + + #[cfg(feature = "avif")] + { + formats.push("avif"); + } + #[cfg(feature = "jxl")] + { + formats.push("jpegxl"); + } + #[cfg(feature = "tiff")] + { + formats.push("tiff"); + } + #[cfg(feature = "openexr")] + { + formats.push("openexr"); + } + #[cfg(feature = "icc")] + { + formats.push("icc"); + } + #[cfg(feature = "exif")] + { + formats.push("exif"); + } + + formats +} + +/// Validate metadata payloads without decoding image data. +#[cfg(any(feature = "icc", feature = "exif"))] +pub fn validate_metadata(data: &[u8]) -> Result<(), ImageHardenError> { + #[cfg(feature = "icc")] + { + validate_icc_profile(data)?; + } + + #[cfg(feature = "exif")] + { + validate_exif(data)?; + } + + Ok(()) +} diff --git a/image_harden/src/lib.rs b/image_harden/src/lib.rs index 53d6474..64981cf 100644 --- a/image_harden/src/lib.rs +++ b/image_harden/src/lib.rs @@ -4,16 +4,19 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +use ammonia::clean; use std::ffi::CStr; use std::io::Read; use std::mem; use thiserror::Error; -use ammonia::clean; // Metrics and monitoring modules pub mod metrics; pub mod metrics_server; +// Public API surface +pub mod api; + // Extended format support pub mod formats; @@ -103,14 +106,24 @@ pub fn decode_png(data: &[u8]) -> Result, ImageHardenError> { let info_ptr = png_create_info_struct(png_ptr); if info_ptr.is_null() { - png_destroy_read_struct(&mut (png_ptr as png_structp), std::ptr::null_mut(), std::ptr::null_mut()); + png_destroy_read_struct( + &mut (png_ptr as png_structp), + std::ptr::null_mut(), + std::ptr::null_mut(), + ); return Err(ImageHardenError::NullPointer); } let jmp_buf_ptr = png_jmpbuf_wrapper(png_ptr) as *mut jmp_buf; if setjmp(mem::transmute(jmp_buf_ptr)) != 0 { - png_destroy_read_struct(&mut (png_ptr as png_structp), &mut (info_ptr as png_infop), std::ptr::null_mut()); - return Err(ImageHardenError::PngError("PNG decoding failed".to_string())); + png_destroy_read_struct( + &mut (png_ptr as png_structp), + &mut (info_ptr as png_infop), + std::ptr::null_mut(), + ); + return Err(ImageHardenError::PngError( + "PNG decoding failed".to_string(), + )); } png_set_user_limits(png_ptr, 8192, 8192); @@ -118,7 +131,11 @@ pub fn decode_png(data: &[u8]) -> Result, ImageHardenError> { png_set_chunk_malloc_max(png_ptr, 256 * 1024); let mut cursor = std::io::Cursor::new(data); - png_set_read_fn(png_ptr, &mut cursor as *mut _ as png_voidp, Some(read_data_fn)); + png_set_read_fn( + png_ptr, + &mut cursor as *mut _ as png_voidp, + Some(read_data_fn), + ); png_read_info(png_ptr, info_ptr); @@ -154,7 +171,11 @@ pub fn decode_png(data: &[u8]) -> Result, ImageHardenError> { png_read_image(png_ptr, row_pointers.as_mut_ptr()); - png_destroy_read_struct(&mut (png_ptr as png_structp), &mut (info_ptr as png_infop), std::ptr::null_mut()); + png_destroy_read_struct( + &mut (png_ptr as png_structp), + &mut (info_ptr as png_infop), + std::ptr::null_mut(), + ); Ok(image_data) } @@ -179,10 +200,16 @@ pub fn decode_jpeg(data: &[u8]) -> Result, ImageHardenError> { if setjmp(err_mgr.jmp_buf.as_mut_ptr()) != 0 { jpeg_destroy_decompress(&mut cinfo); - return Err(ImageHardenError::JpegError("JPEG decoding failed".to_string())); + return Err(ImageHardenError::JpegError( + "JPEG decoding failed".to_string(), + )); } - jpeg_CreateDecompress(&mut cinfo, JPEG_LIB_VERSION as i32, std::mem::size_of::()); + jpeg_CreateDecompress( + &mut cinfo, + JPEG_LIB_VERSION as i32, + std::mem::size_of::(), + ); (*cinfo.mem).max_memory_to_use = 64 * 1024 * 1024; // 64 MB for m in 0xE0..=0xEF { @@ -190,13 +217,14 @@ pub fn decode_jpeg(data: &[u8]) -> Result, ImageHardenError> { } jpeg_save_markers(&mut cinfo, JPEG_COM as i32, 0); - jpeg_mem_src(&mut cinfo, data.as_ptr(), data.len() as u64); jpeg_read_header(&mut cinfo, 1); if cinfo.image_width > 10000 || cinfo.image_height > 10000 { - return Err(ImageHardenError::JpegError("Image dimensions exceed limits".to_string())); + return Err(ImageHardenError::JpegError( + "Image dimensions exceed limits".to_string(), + )); } cinfo.out_color_space = J_COLOR_SPACE_JCS_RGB; @@ -206,7 +234,9 @@ pub fn decode_jpeg(data: &[u8]) -> Result, ImageHardenError> { let mut image_data = vec![0u8; row_stride * cinfo.output_height as usize]; while cinfo.output_scanline < cinfo.output_height { - let mut buffer = [image_data.as_mut_ptr().add(cinfo.output_scanline as usize * row_stride)]; + let mut buffer = [image_data + .as_mut_ptr() + .add(cinfo.output_scanline as usize * row_stride)]; jpeg_read_scanlines(&mut cinfo, buffer.as_mut_ptr(), 1); } @@ -232,7 +262,11 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { unsafe impl Sync for GifMemoryReader {} // GIF read function for memory buffer - unsafe extern "C" fn gif_read_fn(gif_file: *mut GifFileType, buf: *mut GifByteType, size: i32) -> i32 { + unsafe extern "C" fn gif_read_fn( + gif_file: *mut GifFileType, + buf: *mut GifByteType, + size: i32, + ) -> i32 { let reader = (*gif_file).UserData as *mut GifMemoryReader; if reader.is_null() { return 0; @@ -247,11 +281,7 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { return 0; } - std::ptr::copy_nonoverlapping( - reader_ref.data.add(pos), - buf, - to_read, - ); + std::ptr::copy_nonoverlapping(reader_ref.data.add(pos), buf, to_read); reader_ref.pos.store(pos + to_read, Ordering::Relaxed); to_read as i32 @@ -262,10 +292,14 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { return Err(ImageHardenError::GifError("File too small".to_string())); } if &data[0..3] != b"GIF" { - return Err(ImageHardenError::GifError("Invalid GIF signature".to_string())); + return Err(ImageHardenError::GifError( + "Invalid GIF signature".to_string(), + )); } if &data[3..6] != b"87a" && &data[3..6] != b"89a" { - return Err(ImageHardenError::GifError("Unknown GIF version".to_string())); + return Err(ImageHardenError::GifError( + "Unknown GIF version".to_string(), + )); } unsafe { @@ -293,7 +327,10 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { let msg = std::ffi::CStr::from_ptr(error_info.error_msg.as_ptr()) .to_string_lossy() .into_owned(); - return Err(ImageHardenError::GifError(format!("Failed to open GIF: {}", msg))); + return Err(ImageHardenError::GifError(format!( + "Failed to open GIF: {}", + msg + ))); } // Slurp GIF with comprehensive validation @@ -302,7 +339,10 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { .to_string_lossy() .into_owned(); safe_DGifClose(gif_file); - return Err(ImageHardenError::GifError(format!("Failed to decode GIF: {}", msg))); + return Err(ImageHardenError::GifError(format!( + "Failed to decode GIF: {}", + msg + ))); } let gif = &*gif_file; @@ -339,9 +379,10 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { // Validate color map if cmap.ColorCount <= 0 || cmap.ColorCount > 256 { safe_DGifClose(gif_file); - return Err(ImageHardenError::GifError( - format!("Invalid color count: {}", cmap.ColorCount) - )); + return Err(ImageHardenError::GifError(format!( + "Invalid color count: {}", + cmap.ColorCount + ))); } if cmap.Colors.is_null() { @@ -358,7 +399,9 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { // Validate bounds if img_left + img_width > width || img_top + img_height > height { safe_DGifClose(gif_file); - return Err(ImageHardenError::GifError("Image out of bounds".to_string())); + return Err(ImageHardenError::GifError( + "Image out of bounds".to_string(), + )); } // Copy pixels with bounds checking @@ -380,10 +423,11 @@ pub fn decode_gif(data: &[u8]) -> Result, ImageHardenError> { // Validate color index (CVE-2019-15133 mitigation) if color_idx >= cmap.ColorCount as usize { safe_DGifClose(gif_file); - return Err(ImageHardenError::GifError( - format!("Color index {} out of range (max: {})", - color_idx, cmap.ColorCount - 1) - )); + return Err(ImageHardenError::GifError(format!( + "Color index {} out of range (max: {})", + color_idx, + cmap.ColorCount - 1 + ))); } // Get color from color map @@ -413,41 +457,52 @@ pub fn decode_webp(data: &[u8]) -> Result, ImageHardenError> { return Err(ImageHardenError::WebPError("File too small".to_string())); } if &data[0..4] != b"RIFF" { - return Err(ImageHardenError::WebPError("Invalid WebP signature: missing RIFF header".to_string())); + return Err(ImageHardenError::WebPError( + "Invalid WebP signature: missing RIFF header".to_string(), + )); } if &data[8..12] != b"WEBP" { - return Err(ImageHardenError::WebPError("Invalid WebP signature: missing WEBP marker".to_string())); + return Err(ImageHardenError::WebPError( + "Invalid WebP signature: missing WEBP marker".to_string(), + )); } // Check file size matches RIFF header let declared_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize; if declared_size + 8 != data.len() { - return Err(ImageHardenError::WebPError( - format!("WebP size mismatch: declared {} bytes, got {} bytes", - declared_size + 8, data.len()) - )); + return Err(ImageHardenError::WebPError(format!( + "WebP size mismatch: declared {} bytes, got {} bytes", + declared_size + 8, + data.len() + ))); } // Enforce reasonable file size limit (50 MB) const MAX_WEBP_FILE_SIZE: usize = 50 * 1024 * 1024; if data.len() > MAX_WEBP_FILE_SIZE { - return Err(ImageHardenError::WebPError( - format!("WebP file too large: {} bytes (max: {})", data.len(), MAX_WEBP_FILE_SIZE) - )); + return Err(ImageHardenError::WebPError(format!( + "WebP file too large: {} bytes (max: {})", + data.len(), + MAX_WEBP_FILE_SIZE + ))); } // Decode with webp crate let decoder = Decoder::new(data); - let decoded = decoder.decode() + let decoded = decoder + .decode() .ok_or_else(|| ImageHardenError::WebPError("WebP decoding failed".to_string()))?; // Validate dimensions - const MAX_WEBP_DIMENSION: u32 = 16384; // 16K max dimension + const MAX_WEBP_DIMENSION: u32 = 16384; // 16K max dimension if decoded.width() > MAX_WEBP_DIMENSION || decoded.height() > MAX_WEBP_DIMENSION { - return Err(ImageHardenError::WebPError( - format!("WebP dimensions too large: {}x{} (max: {}x{})", - decoded.width(), decoded.height(), MAX_WEBP_DIMENSION, MAX_WEBP_DIMENSION) - )); + return Err(ImageHardenError::WebPError(format!( + "WebP dimensions too large: {}x{} (max: {}x{})", + decoded.width(), + decoded.height(), + MAX_WEBP_DIMENSION, + MAX_WEBP_DIMENSION + ))); } // Return raw RGBA data @@ -457,60 +512,71 @@ pub fn decode_webp(data: &[u8]) -> Result, ImageHardenError> { // HEIF/HEIC decoder (Apple iOS/macOS format) // HEIF uses complex codec chains and requires careful validation pub fn decode_heif(data: &[u8]) -> Result, ImageHardenError> { - use libheif_rs::{HeifContext, RgbChroma, ColorSpace}; + use libheif_rs::{ColorSpace, HeifContext, RgbChroma}; // Validate HEIF signature (ISO Base Media File Format) if data.len() < 12 { return Err(ImageHardenError::HeifError("File too small".to_string())); } if &data[4..8] != b"ftyp" { - return Err(ImageHardenError::HeifError("Invalid HEIF signature: missing ftyp box".to_string())); + return Err(ImageHardenError::HeifError( + "Invalid HEIF signature: missing ftyp box".to_string(), + )); } // Check for Apple brand codes (heic, heix, mif1, msf1, hevc, hevx) let brand = &data[8..12]; let valid_brands = [b"heic", b"heix", b"mif1", b"msf1", b"hevc", b"hevx"]; if !valid_brands.iter().any(|&b| brand == b) { - return Err(ImageHardenError::HeifError( - format!("Unsupported HEIF brand: {:?}", std::str::from_utf8(brand).unwrap_or("invalid")) - )); + return Err(ImageHardenError::HeifError(format!( + "Unsupported HEIF brand: {:?}", + std::str::from_utf8(brand).unwrap_or("invalid") + ))); } // Enforce reasonable file size limit (100 MB) const MAX_HEIF_FILE_SIZE: usize = 100 * 1024 * 1024; if data.len() > MAX_HEIF_FILE_SIZE { - return Err(ImageHardenError::HeifError( - format!("HEIF file too large: {} bytes (max: {})", data.len(), MAX_HEIF_FILE_SIZE) - )); + return Err(ImageHardenError::HeifError(format!( + "HEIF file too large: {} bytes (max: {})", + data.len(), + MAX_HEIF_FILE_SIZE + ))); } // Create context and read from memory - let ctx = HeifContext::read_from_bytes(data) - .map_err(|e| ImageHardenError::HeifError(format!("Failed to read HEIF context: {:?}", e)))?; + let ctx = HeifContext::read_from_bytes(data).map_err(|e| { + ImageHardenError::HeifError(format!("Failed to read HEIF context: {:?}", e)) + })?; // Get primary image handle - let handle = ctx.primary_image_handle() - .map_err(|e| ImageHardenError::HeifError(format!("Failed to get primary image: {:?}", e)))?; + let handle = ctx.primary_image_handle().map_err(|e| { + ImageHardenError::HeifError(format!("Failed to get primary image: {:?}", e)) + })?; // Validate dimensions - const MAX_HEIF_DIMENSION: u32 = 16384; // 16K max dimension + const MAX_HEIF_DIMENSION: u32 = 16384; // 16K max dimension let width = handle.width() as u32; let height = handle.height() as u32; if width > MAX_HEIF_DIMENSION || height > MAX_HEIF_DIMENSION { - return Err(ImageHardenError::HeifError( - format!("HEIF dimensions too large: {}x{} (max: {}x{})", - width, height, MAX_HEIF_DIMENSION, MAX_HEIF_DIMENSION) - )); + return Err(ImageHardenError::HeifError(format!( + "HEIF dimensions too large: {}x{} (max: {}x{})", + width, height, MAX_HEIF_DIMENSION, MAX_HEIF_DIMENSION + ))); } // Decode image to RGB - let image = handle.decode(ColorSpace::Rgb(RgbChroma::Rgb), None) - .map_err(|e| ImageHardenError::HeifError(format!("Failed to decode HEIF image: {:?}", e)))?; + let image = handle + .decode(ColorSpace::Rgb(RgbChroma::Rgb), None) + .map_err(|e| { + ImageHardenError::HeifError(format!("Failed to decode HEIF image: {:?}", e)) + })?; // Get image planes and convert to raw bytes let planes = image.planes(); - let interleaved = planes.interleaved + let interleaved = planes + .interleaved .ok_or_else(|| ImageHardenError::HeifError("No interleaved plane data".to_string()))?; Ok(interleaved.data.to_vec()) @@ -519,8 +585,9 @@ pub fn decode_heif(data: &[u8]) -> Result, ImageHardenError> { // SVG wrapper using pure Rust resvg (memory-safe) pub fn decode_svg(data: &[u8]) -> Result, ImageHardenError> { // Sanitize SVG to remove malicious content - let sanitized_svg = clean(std::str::from_utf8(data) - .map_err(|e| ImageHardenError::SvgError(e.to_string()))?).to_string(); + let sanitized_svg = + clean(std::str::from_utf8(data).map_err(|e| ImageHardenError::SvgError(e.to_string()))?) + .to_string(); // Parse SVG with usvg let opt = usvg::Options::default(); @@ -545,7 +612,8 @@ pub fn decode_svg(data: &[u8]) -> Result, ImageHardenError> { resvg::render(&tree, transform, &mut pixmap.as_mut()); // Encode as PNG - pixmap.encode_png() + pixmap + .encode_png() .map_err(|e| ImageHardenError::SvgError(format!("Failed to encode PNG: {:?}", e))) } @@ -567,16 +635,12 @@ pub fn decode_video(data: &[u8], _wasm_path: &str) -> Result, ImageHarde // Return metadata as proof of validation let result = format!( "Video validated: {:?} {}x{} {:.1}s", - metadata.container_format, - metadata.width, - metadata.height, - metadata.duration_secs + metadata.container_format, metadata.width, metadata.height, metadata.duration_secs ); Ok(result.into_bytes()) } - extern "C" fn error_fn(png_ptr: png_structp, error_msg: png_const_charp) { let msg = unsafe { CStr::from_ptr(error_msg).to_string_lossy().into_owned() }; eprintln!("PNG error: {}", msg); @@ -617,18 +681,18 @@ unsafe extern "C" fn jpeg_error_exit(cinfo: j_common_ptr) { // like Telegram, Discord, email attachments, etc. // Security limits for audio decoding -const MAX_AUDIO_FILE_SIZE: usize = 100 * 1024 * 1024; // 100 MB -const MAX_AUDIO_DURATION_SECS: u64 = 600; // 10 minutes -const MAX_SAMPLE_RATE: u32 = 192000; // 192 kHz -const MAX_CHANNELS: u16 = 8; // 8 channels +const MAX_AUDIO_FILE_SIZE: usize = 100 * 1024 * 1024; // 100 MB +const MAX_AUDIO_DURATION_SECS: u64 = 600; // 10 minutes +const MAX_SAMPLE_RATE: u32 = 192000; // 192 kHz +const MAX_CHANNELS: u16 = 8; // 8 channels // Audio sample output format #[derive(Debug, Clone)] pub struct AudioData { - pub samples: Vec, // Interleaved samples - pub sample_rate: u32, // Hz - pub channels: u16, // 1=mono, 2=stereo, etc. - pub duration_secs: f64, // Total duration + pub samples: Vec, // Interleaved samples + pub sample_rate: u32, // Hz + pub channels: u16, // 1=mono, 2=stereo, etc. + pub duration_secs: f64, // Total duration } // MP3 decoder (using minimp3 - Rust wrapper around C minimp3) @@ -638,14 +702,18 @@ pub fn decode_mp3(data: &[u8]) -> Result { // Validate input size if data.len() > MAX_AUDIO_FILE_SIZE { - return Err(ImageHardenError::Mp3Error( - format!("File too large: {} bytes (max: {})", data.len(), MAX_AUDIO_FILE_SIZE) - )); + return Err(ImageHardenError::Mp3Error(format!( + "File too large: {} bytes (max: {})", + data.len(), + MAX_AUDIO_FILE_SIZE + ))); } // Validate MP3 signature (MPEG frame sync) if data.len() < 2 || (data[0] != 0xFF || (data[1] & 0xE0) != 0xE0) { - return Err(ImageHardenError::Mp3Error("Invalid MP3 signature".to_string())); + return Err(ImageHardenError::Mp3Error( + "Invalid MP3 signature".to_string(), + )); } let mut decoder = Decoder::new(data); @@ -656,17 +724,24 @@ pub fn decode_mp3(data: &[u8]) -> Result { loop { match decoder.next_frame() { - Ok(Frame { data: samples, sample_rate: rate, channels: ch, .. }) => { + Ok(Frame { + data: samples, + sample_rate: rate, + channels: ch, + .. + }) => { // Validate audio parameters if rate as u32 > MAX_SAMPLE_RATE { - return Err(ImageHardenError::Mp3Error( - format!("Sample rate too high: {} Hz", rate) - )); + return Err(ImageHardenError::Mp3Error(format!( + "Sample rate too high: {} Hz", + rate + ))); } if ch as u16 > MAX_CHANNELS { - return Err(ImageHardenError::Mp3Error( - format!("Too many channels: {}", ch) - )); + return Err(ImageHardenError::Mp3Error(format!( + "Too many channels: {}", + ch + ))); } // Set format from first frame @@ -679,9 +754,10 @@ pub fn decode_mp3(data: &[u8]) -> Result { total_samples += samples.len(); let duration_secs = total_samples as u64 / (sample_rate as u64 * channels as u64); if duration_secs > MAX_AUDIO_DURATION_SECS { - return Err(ImageHardenError::Mp3Error( - format!("Audio too long: {} seconds (max: {})", duration_secs, MAX_AUDIO_DURATION_SECS) - )); + return Err(ImageHardenError::Mp3Error(format!( + "Audio too long: {} seconds (max: {})", + duration_secs, MAX_AUDIO_DURATION_SECS + ))); } all_samples.extend_from_slice(&samples); @@ -692,7 +768,9 @@ pub fn decode_mp3(data: &[u8]) -> Result { } if all_samples.is_empty() { - return Err(ImageHardenError::Mp3Error("No audio data decoded".to_string())); + return Err(ImageHardenError::Mp3Error( + "No audio data decoded".to_string(), + )); } let duration_secs = all_samples.len() as f64 / (sample_rate as f64 * channels as f64); @@ -711,56 +789,66 @@ pub fn decode_vorbis(data: &[u8]) -> Result { // Validate input size if data.len() > MAX_AUDIO_FILE_SIZE { - return Err(ImageHardenError::VorbisError( - format!("File too large: {} bytes", data.len()) - )); + return Err(ImageHardenError::VorbisError(format!( + "File too large: {} bytes", + data.len() + ))); } // Validate Ogg signature if data.len() < 4 || &data[0..4] != b"OggS" { - return Err(ImageHardenError::VorbisError("Invalid Ogg signature".to_string())); + return Err(ImageHardenError::VorbisError( + "Invalid Ogg signature".to_string(), + )); } let cursor = std::io::Cursor::new(data); - let mut reader = OggStreamReader::new(cursor) - .map_err(|e| ImageHardenError::VorbisError(format!("Failed to initialize reader: {:?}", e)))?; + let mut reader = OggStreamReader::new(cursor).map_err(|e| { + ImageHardenError::VorbisError(format!("Failed to initialize reader: {:?}", e)) + })?; // Validate audio parameters let sample_rate = reader.ident_hdr.audio_sample_rate; let channels = reader.ident_hdr.audio_channels as u16; if sample_rate > MAX_SAMPLE_RATE { - return Err(ImageHardenError::VorbisError( - format!("Sample rate too high: {} Hz", sample_rate) - )); + return Err(ImageHardenError::VorbisError(format!( + "Sample rate too high: {} Hz", + sample_rate + ))); } if channels > MAX_CHANNELS { - return Err(ImageHardenError::VorbisError( - format!("Too many channels: {}", channels) - )); + return Err(ImageHardenError::VorbisError(format!( + "Too many channels: {}", + channels + ))); } let mut all_samples = Vec::new(); let mut total_samples = 0usize; - while let Some(packet) = reader.read_dec_packet_itl() - .map_err(|e| ImageHardenError::VorbisError(format!("Decode error: {:?}", e)))? { - + while let Some(packet) = reader + .read_dec_packet_itl() + .map_err(|e| ImageHardenError::VorbisError(format!("Decode error: {:?}", e)))? + { total_samples += packet.len(); // Check duration limit let duration_secs = total_samples as u64 / (sample_rate as u64 * channels as u64); if duration_secs > MAX_AUDIO_DURATION_SECS { - return Err(ImageHardenError::VorbisError( - format!("Audio too long: {} seconds", duration_secs) - )); + return Err(ImageHardenError::VorbisError(format!( + "Audio too long: {} seconds", + duration_secs + ))); } all_samples.extend_from_slice(&packet); } if all_samples.is_empty() { - return Err(ImageHardenError::VorbisError("No audio data decoded".to_string())); + return Err(ImageHardenError::VorbisError( + "No audio data decoded".to_string(), + )); } let duration_secs = all_samples.len() as f64 / (sample_rate as f64 * channels as f64); @@ -779,32 +867,38 @@ pub fn decode_flac(data: &[u8]) -> Result { // Validate input size if data.len() > MAX_AUDIO_FILE_SIZE { - return Err(ImageHardenError::FlacError( - format!("File too large: {} bytes", data.len()) - )); + return Err(ImageHardenError::FlacError(format!( + "File too large: {} bytes", + data.len() + ))); } // Validate FLAC signature if data.len() < 4 || &data[0..4] != b"fLaC" { - return Err(ImageHardenError::FlacError("Invalid FLAC signature".to_string())); + return Err(ImageHardenError::FlacError( + "Invalid FLAC signature".to_string(), + )); } let cursor = std::io::Cursor::new(data); - let mut reader = FlacReader::new(cursor) - .map_err(|e| ImageHardenError::FlacError(format!("Failed to initialize reader: {:?}", e)))?; + let mut reader = FlacReader::new(cursor).map_err(|e| { + ImageHardenError::FlacError(format!("Failed to initialize reader: {:?}", e)) + })?; let streaminfo = reader.streaminfo(); // Validate audio parameters if streaminfo.sample_rate > MAX_SAMPLE_RATE { - return Err(ImageHardenError::FlacError( - format!("Sample rate too high: {} Hz", streaminfo.sample_rate) - )); + return Err(ImageHardenError::FlacError(format!( + "Sample rate too high: {} Hz", + streaminfo.sample_rate + ))); } if streaminfo.channels as u16 > MAX_CHANNELS { - return Err(ImageHardenError::FlacError( - format!("Too many channels: {}", streaminfo.channels) - )); + return Err(ImageHardenError::FlacError(format!( + "Too many channels: {}", + streaminfo.channels + ))); } let mut all_samples = Vec::new(); @@ -812,8 +906,8 @@ pub fn decode_flac(data: &[u8]) -> Result { let mut sample_count = 0usize; while let Some(sample) = samples.next() { - let sample = sample - .map_err(|e| ImageHardenError::FlacError(format!("Decode error: {:?}", e)))?; + let sample = + sample.map_err(|e| ImageHardenError::FlacError(format!("Decode error: {:?}", e)))?; // Convert to i16 (FLAC can have various bit depths) let sample_i16 = if streaminfo.bits_per_sample <= 16 { @@ -826,19 +920,24 @@ pub fn decode_flac(data: &[u8]) -> Result { sample_count += 1; // Check duration limit - let duration_secs = sample_count as u64 / (streaminfo.sample_rate as u64 * streaminfo.channels as u64); + let duration_secs = + sample_count as u64 / (streaminfo.sample_rate as u64 * streaminfo.channels as u64); if duration_secs > MAX_AUDIO_DURATION_SECS { - return Err(ImageHardenError::FlacError( - format!("Audio too long: {} seconds", duration_secs) - )); + return Err(ImageHardenError::FlacError(format!( + "Audio too long: {} seconds", + duration_secs + ))); } } if all_samples.is_empty() { - return Err(ImageHardenError::FlacError("No audio data decoded".to_string())); + return Err(ImageHardenError::FlacError( + "No audio data decoded".to_string(), + )); } - let duration_secs = all_samples.len() as f64 / (streaminfo.sample_rate as f64 * streaminfo.channels as f64); + let duration_secs = + all_samples.len() as f64 / (streaminfo.sample_rate as f64 * streaminfo.channels as f64); Ok(AudioData { samples: all_samples, @@ -862,7 +961,9 @@ pub fn decode_audio(data: &[u8]) -> Result { } else if data.len() >= 2 && data[0] == 0xFF && (data[1] & 0xE0) == 0xE0 { decode_mp3(data) } else { - Err(ImageHardenError::AudioError("Unknown audio format".to_string())) + Err(ImageHardenError::AudioError( + "Unknown audio format".to_string(), + )) } } @@ -887,13 +988,13 @@ pub fn decode_audio(data: &[u8]) -> Result { // 6. Rate-limit and resource-bound all operations // Security limits for video validation -const MAX_VIDEO_FILE_SIZE: usize = 500 * 1024 * 1024; // 500 MB -const MAX_VIDEO_DURATION_SECS: u64 = 3600; // 1 hour -const MAX_VIDEO_WIDTH: u32 = 3840; // 4K width -const MAX_VIDEO_HEIGHT: u32 = 2160; // 4K height -const MAX_VIDEO_FRAMERATE: u32 = 120; // 120 fps -const MAX_VIDEO_BITRATE: u64 = 50_000_000; // 50 Mbps -const MAX_VIDEO_TRACKS: usize = 8; // Max audio/video/subtitle tracks +const MAX_VIDEO_FILE_SIZE: usize = 500 * 1024 * 1024; // 500 MB +const MAX_VIDEO_DURATION_SECS: u64 = 3600; // 1 hour +const MAX_VIDEO_WIDTH: u32 = 3840; // 4K width +const MAX_VIDEO_HEIGHT: u32 = 2160; // 4K height +const MAX_VIDEO_FRAMERATE: u32 = 120; // 120 fps +const MAX_VIDEO_BITRATE: u64 = 50_000_000; // 50 Mbps +const MAX_VIDEO_TRACKS: usize = 8; // Max audio/video/subtitle tracks #[derive(Debug, Clone)] pub struct VideoMetadata { @@ -919,14 +1020,16 @@ pub enum VideoContainerFormat { pub fn validate_video_container(data: &[u8]) -> Result { // File size check if data.len() > MAX_VIDEO_FILE_SIZE { - return Err(ImageHardenError::VideoValidationError( - format!("Video file too large: {} bytes (max: {})", data.len(), MAX_VIDEO_FILE_SIZE) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Video file too large: {} bytes (max: {})", + data.len(), + MAX_VIDEO_FILE_SIZE + ))); } if data.len() < 12 { return Err(ImageHardenError::VideoValidationError( - "Video file too small".to_string() + "Video file too small".to_string(), )); } @@ -938,7 +1041,7 @@ pub fn validate_video_container(data: &[u8]) -> Result validate_mkv_container(data), VideoContainerFormat::AVI => validate_avi_container(data), VideoContainerFormat::Unknown => Err(ImageHardenError::VideoValidationError( - "Unknown or unsupported video container format".to_string() + "Unknown or unsupported video container format".to_string(), )), } } @@ -982,16 +1085,17 @@ fn validate_mp4_container(data: &[u8]) -> Result MAX_VIDEO_TRACKS { - return Err(ImageHardenError::VideoValidationError( - format!("Too many tracks: {} (max: {})", context.tracks.len(), MAX_VIDEO_TRACKS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Too many tracks: {} (max: {})", + context.tracks.len(), + MAX_VIDEO_TRACKS + ))); } let mut video_tracks = 0; @@ -1007,18 +1111,20 @@ fn validate_mp4_container(data: &[u8]) -> Result> 16; // Fixed-point to integer + let width = tkhd.width >> 16; // Fixed-point to integer let height = tkhd.height >> 16; if width > MAX_VIDEO_WIDTH { - return Err(ImageHardenError::VideoValidationError( - format!("Video width too large: {} (max: {})", width, MAX_VIDEO_WIDTH) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Video width too large: {} (max: {})", + width, MAX_VIDEO_WIDTH + ))); } if height > MAX_VIDEO_HEIGHT { - return Err(ImageHardenError::VideoValidationError( - format!("Video height too large: {} (max: {})", height, MAX_VIDEO_HEIGHT) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Video height too large: {} (max: {})", + height, MAX_VIDEO_HEIGHT + ))); } max_width = max_width.max(width); @@ -1036,10 +1142,10 @@ fn validate_mp4_container(data: &[u8]) -> Result MAX_VIDEO_DURATION_SECS as f64 { - return Err(ImageHardenError::VideoValidationError( - format!("Video too long: {:.1} seconds (max: {})", - duration_secs, MAX_VIDEO_DURATION_SECS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Video too long: {:.1} seconds (max: {})", + duration_secs, MAX_VIDEO_DURATION_SECS + ))); } } } @@ -1053,7 +1159,7 @@ fn validate_mp4_container(data: &[u8]) -> Result Result Result MAX_VIDEO_TRACKS { - return Err(ImageHardenError::VideoValidationError( - format!("Too many tracks: {} (max: {})", - video_tracks + audio_tracks, MAX_VIDEO_TRACKS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "Too many tracks: {} (max: {})", + video_tracks + audio_tracks, + MAX_VIDEO_TRACKS + ))); } if video_tracks == 0 { return Err(ImageHardenError::VideoValidationError( - "No video tracks found in MKV/WebM".to_string() + "No video tracks found in MKV/WebM".to_string(), )); } @@ -1125,10 +1231,10 @@ fn validate_mkv_container(data: &[u8]) -> Result MAX_VIDEO_DURATION_SECS as f64 { - return Err(ImageHardenError::VideoValidationError( - format!("MKV video too long: {:.1} seconds (max: {})", - duration_secs, MAX_VIDEO_DURATION_SECS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "MKV video too long: {:.1} seconds (max: {})", + duration_secs, MAX_VIDEO_DURATION_SECS + ))); } Ok(VideoMetadata { @@ -1149,7 +1255,7 @@ fn validate_avi_container(data: &[u8]) -> Result Result Result data.len() { - break; // Chunk extends past file end + break; // Chunk extends past file end } if chunk_id == b"avih" && chunk_size >= 56 { found_avih = true; // Parse AVI main header (56 bytes minimum) - let header_data = &data[pos+8..pos+8+56]; + let header_data = &data[pos + 8..pos + 8 + 56]; duration_microsecs = u32::from_le_bytes([ - header_data[0], header_data[1], header_data[2], header_data[3] + header_data[0], + header_data[1], + header_data[2], + header_data[3], ]); width = u32::from_le_bytes([ - header_data[32], header_data[33], header_data[34], header_data[35] + header_data[32], + header_data[33], + header_data[34], + header_data[35], ]); height = u32::from_le_bytes([ - header_data[36], header_data[37], header_data[38], header_data[39] + header_data[36], + header_data[37], + header_data[38], + header_data[39], ]); // Validate dimensions if width > MAX_VIDEO_WIDTH { - return Err(ImageHardenError::VideoValidationError( - format!("AVI width too large: {} (max: {})", width, MAX_VIDEO_WIDTH) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "AVI width too large: {} (max: {})", + width, MAX_VIDEO_WIDTH + ))); } if height > MAX_VIDEO_HEIGHT { - return Err(ImageHardenError::VideoValidationError( - format!("AVI height too large: {} (max: {})", height, MAX_VIDEO_HEIGHT) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "AVI height too large: {} (max: {})", + height, MAX_VIDEO_HEIGHT + ))); } break; @@ -1219,17 +1337,17 @@ fn validate_avi_container(data: &[u8]) -> Result MAX_VIDEO_DURATION_SECS as f64 { - return Err(ImageHardenError::VideoValidationError( - format!("AVI video too long: {:.1} seconds (max: {})", - duration_secs, MAX_VIDEO_DURATION_SECS) - )); + return Err(ImageHardenError::VideoValidationError(format!( + "AVI video too long: {:.1} seconds (max: {})", + duration_secs, MAX_VIDEO_DURATION_SECS + ))); } Ok(VideoMetadata { @@ -1237,8 +1355,8 @@ fn validate_avi_container(data: &[u8]) -> Result Date: Mon, 1 Dec 2025 09:57:26 +0000 Subject: [PATCH 2/3] Document libsodium hardening and simplify API surface --- README.md | 9 ++-- docs/HARDENING_LIBSODIUM.md | 90 +++++++++++++++++++++++++++++++++++++ docs/INTEGRATION.md | 14 ++---- image_harden/src/lib.rs | 3 -- 4 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 docs/HARDENING_LIBSODIUM.md diff --git a/README.md b/README.md index 0df200e..9a61d5d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ git submodule add https://github.com/SWORDIntel/IMAGEHARDER.git external/imageha ``` ## Public API (library) -Add the crate to your workspace and call the unified API surface: +Add the crate to your workspace and call the hardened helpers directly: ```toml # parent-project/Cargo.toml @@ -27,10 +27,10 @@ image_harden = { path = "external/imageharder/image_harden" } ``` ```rust -use image_harden::api::{DecodedMedia, HardenedDecoder, MediaFormat}; +use image_harden::decode_png; -fn decode(bytes: &[u8]) -> Result { - HardenedDecoder::decode(MediaFormat::Png, bytes) +fn decode(bytes: &[u8]) -> Result, image_harden::ImageHardenError> { + decode_png(bytes) } ``` @@ -46,6 +46,7 @@ cargo build --release - `image_harden/` – Rust crate with the hardened decoders and CLI entrypoint. - `docs/INTEGRATION.md` – concise guide for embedding as a submodule. - `docs/archive/` – preserved deep-dives (build notes, platform guides, extended hardening writeups). +- `docs/HARDENING_LIBSODIUM.md` – exploratory notes on applying similar hardening patterns to libsodium. - `config/`, `ffmpeg/`, `docker-compose.yml` – deployment aids retained for downstream integrators. ## Security diff --git a/docs/HARDENING_LIBSODIUM.md b/docs/HARDENING_LIBSODIUM.md new file mode 100644 index 0000000..6fffdd4 --- /dev/null +++ b/docs/HARDENING_LIBSODIUM.md @@ -0,0 +1,90 @@ +# Applying IMAGEHARDER-style hardening to libsodium + +The same practices used in IMAGEHARDER (bounded inputs, explicit error mapping, metrics, and sandboxing) can be mirrored when embedding +[libsodium](https://github.com/jedisct1/libsodium) for cryptographic workloads. This note outlines a secure-integration profile +suitable for Xen domU/CI builders. + +## Goals +- Stable, minimal FFI boundary with explicit error types. +- Deterministic builds that pin versions and compiler flags. +- Isolation of key material and misuse-resistant APIs. +- Observability: metrics, tamper-evident audit logs, and build provenance. + +## Build strategy +- **Version pinning**: vendor a known-good libsodium release and verify with `sha256sum` before build. +- **Compiler flags**: prefer `-fstack-protector-strong -D_FORTIFY_SOURCE=3 -fPIC -O2 -pipe -fno-plt` plus + `-fstack-clash-protection` on supported toolchains. Enable `-fcf-protection=full` for CET-capable targets. +- **Linking**: prefer static linking in minimal domU images; use `RUSTFLAGS="-C target-feature=+crt-static"` when pairing with + Rust consumers to avoid dynamic search-path issues. +- **Reproducibility**: capture build metadata (compiler, flags, git SHA) in an SBOM; wire `generate-sbom.sh` as part of CI. + +## FFI surface design +- Wrap each libsodium primitive in narrow functions that accept length-checked slices and return `Result`. +- Prohibit raw pointer exposure; translate C error codes into typed Rust errors with context. +- Add zeroization hooks (`zeroize::Zeroize`) for buffers that carry keys, nonces, or secrets. +- Enforce domain separation by tagging APIs per purpose (e.g., `Auth`, `Aead`, `Kdf`) to discourage misuse. + +## Runtime safeguards +- **Initialization guard**: perform a one-time `sodium_init()` with failure handling before exposing any primitive. +- **Key lifecycle**: keep keys in guarded memory (e.g., `secrecy::SecretVec`) and avoid serialization. Provide key-generation + helpers that default to high-entropy RNGs (libsodium already defaults to `randombytes_buf`), and expose optional hardware RNG + seeding for dom0/domU if available. +- **Parameter validation**: validate nonce sizes, tag lengths, and buffer boundaries before calling into libsodium. +- **Constant-time expectations**: document which APIs are constant-time and block callers from using them for unrelated purposes + (e.g., no generic equality on MACs without constant-time compare). + +## Observability and policy +- Emit Prometheus counters for successes/failures per primitive (without leaking secrets) and gauge unexpected parameter + rejections. +- Capture audit logs that include operation type, key identifier, and policy decisions; ship logs over mTLS to central storage. +- Include a policy module that enforces approved cipher suites (e.g., `xchacha20poly1305_ietf`) and blocks deprecated ones. + +## Testing and validation +- **KATs**: integrate libsodium's known-answer tests and add fuzzers around the FFI boundary to catch length/parameter issues. +- **Memory checks**: run under `valgrind`/`ASan` in CI; ensure zeroization paths are covered. +- **Cross-platform**: validate builds under Xen domU, containers, and bare-metal to confirm consistent instruction sets and + `RDRAND` availability. + +## Example integration sketch + +```rust +// Pseudocode illustrating a narrow AEAD wrapper +pub fn seal(key: &SecretVec, nonce: &[u8; crypto_aead_xchacha20poly1305_ietf_NPUBBYTES], + aad: &[u8], plaintext: &[u8]) -> Result, CryptoError> { + if plaintext.len() > MAX_MESSAGE || aad.len() > MAX_AAD { + return Err(CryptoError::InputTooLarge); + } + + let mut ciphertext = vec![0u8; plaintext.len() + TAG_LEN]; + let mut clen = 0usize; + let rc = unsafe { + crypto_aead_xchacha20poly1305_ietf_encrypt( + ciphertext.as_mut_ptr(), + &mut clen, + plaintext.as_ptr(), + plaintext.len() as u64, + aad.as_ptr(), + aad.len() as u64, + std::ptr::null(), + nonce.as_ptr(), + key.expose_secret().as_ptr(), + ) + }; + + if rc != 0 { + return Err(CryptoError::EncryptionFailed); + } + + ciphertext.truncate(clen); + Ok(ciphertext) +} +``` + +## Operational checklist +- Verify SBOM provenance for each release and store alongside build artifacts. +- Rotate keys on a fixed schedule; enforce non-reuse of nonces via per-key counters. +- Run `cargo deny` (or equivalent) to flag transitive CVEs in Rust bindings. +- Add a chaos test that simulates allocator failures to ensure safe unwinding. + +Following this pattern mirrors IMAGEHARDER's focus on bounded inputs, explicit error handling, and strong observabilityβ€”adapted to the +cryptographic domain served by libsodium. diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index fa85bd5..2352f4e 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -27,22 +27,16 @@ members = [ ``` ## 3) Call the public API -The `api` module exposes a minimal surface that wraps the internal decoders and feature-gated formats. +Use the hardened helpers directly. Each decoder returns either raw bytes (images/video) or `AudioData` for audio formats. ```rust -use image_harden::api::{DecodedMedia, HardenedDecoder, MediaFormat}; +use image_harden::decode_png; -fn decode_png(bytes: &[u8]) -> Result { - HardenedDecoder::decode(MediaFormat::Png, bytes) +fn decode_png(bytes: &[u8]) -> Result, image_harden::ImageHardenError> { + decode_png(bytes) } ``` -You can check what is currently compiled in (based on enabled features) at runtime: -```rust -let advertised = image_harden::api::supported_formats(); -assert!(advertised.contains(&"png")); -``` - ## 4) Feature flags Optional formats are disabled by default to keep builds lean. Enable only what you need in the dependency declaration: diff --git a/image_harden/src/lib.rs b/image_harden/src/lib.rs index 64981cf..a7eaddb 100644 --- a/image_harden/src/lib.rs +++ b/image_harden/src/lib.rs @@ -14,9 +14,6 @@ use thiserror::Error; pub mod metrics; pub mod metrics_server; -// Public API surface -pub mod api; - // Extended format support pub mod formats; From 136d38ec2dd1c17539949abe39b308ca0cb1e45d Mon Sep 17 00:00:00 2001 From: John Reese Date: Mon, 1 Dec 2025 10:16:57 +0000 Subject: [PATCH 3/3] Restore original README --- README.md | 451 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 423 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 9a61d5d..5e0ab8b 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,448 @@ # IMAGEHARDER -Hardened media decoding stack that prioritizes safety, observability, and predictable integration. The repo is now organized for use as a Git submodule so parent projects can embed the `image_harden` crate and reuse the hardened build tooling. +**Comprehensive Hardened Media Decoder with Extended Format Support** -## Highlights -- Defensive decoders for common image formats (PNG, JPEG, GIF, WebP, HEIC/HEIF, SVG) plus pure-Rust audio codecs (MP3, Vorbis, FLAC) and video container validation. -- Extended format modules (AVIF, JPEG XL, TIFF, OpenEXR, ICC/EXIF) are feature-gated for projects that need them. -- Runtime hardening: strict dimension/file limits, fail-closed error handling, sandboxed video validation, and optional Prometheus metrics server. -- Drop-in CLI (`image_harden_cli`) for local validation using the same code paths as the library. +IMAGEHARDER is a production-grade system for hardening image, audio, and video decoding libraries. It provides comprehensive security measures including CPU-optimized hardening flags, sandboxing, fuzzing infrastructure, and support for extended modern formats. + +--- + +## πŸ“‹ Table of Contents + +- [Supported Formats](#supported-formats) +- [Security Features](#security-features) +- [CPU Optimization](#cpu-optimization) +- [Getting Started](#getting-started) +- [Build Instructions](#build-instructions) +- [Usage](#usage) +- [Fuzzing](#fuzzing) +- [Architecture](#architecture) +- [Documentation](#documentation) + +--- + +## 🎨 Supported Formats + +### Core Image Formats +- **PNG** - libpng with strict limits (CVE-2015-8540, CVE-2019-7317 mitigations) +- **JPEG** - libjpeg-turbo (CVE-2018-14498 mitigation) +- **GIF** - giflib (CVE-2019-15133, CVE-2016-3977 mitigations) +- **WebP** - Pure Rust decoder (CVE-2023-4863 mitigation, HIGH PRIORITY) +- **HEIF/HEIC** - Apple format (iOS/macOS image format) +- **SVG** - resvg (Pure Rust, memory-safe with sanitization) + +### Extended Image Formats (NEW) +- **AVIF** - AV1 Image File Format (libavif + dav1d) +- **JPEG XL** - Next-gen lossy/lossless (libjxl) +- **TIFF** - Tagged Image File Format (libtiff, CVE-hardened) +- **OpenEXR** - High Dynamic Range images (VFX/HDR workflows) + +### Hidden-Path Components (NEW) +- **ICC Profiles** - Color management (lcms2, with stripping option) +- **EXIF Metadata** - Photo metadata (libexif, with privacy stripping) + +### Audio Formats (Pure Rust) +- **MP3** - minimp3 (Rust wrapper) +- **Vorbis** - lewton (pure Rust) +- **FLAC** - claxon (pure Rust) +- **Opus** - opus crate +- **Ogg** - ogg container (pure Rust) + +### Video Formats +- **MP4/MOV** - mp4parse (Firefox's Rust implementation) +- **MKV/WebM** - matroska (pure Rust EBML) +- **FFmpeg** - WebAssembly sandboxed (MPEG-TS and others) + +--- + +## πŸ”’ Security Features + +### Compile-Time Hardening +- **Stack Protection**: `-fstack-protector-strong`, `-fstack-clash-protection` +- **Memory Safety**: `-D_FORTIFY_SOURCE=3`, `-fPIE`, `-pie` +- **RELRO**: `-Wl,-z,relro,-z,now` +- **No Executable Stack**: `-Wl,-z,noexecstack` +- **Control Flow**: `-fcf-protection=full` (CET on x86_64) +- **Hidden Symbols**: `-fvisibility=hidden` + +### Runtime Protection +- **Strict Resource Limits**: + - Image dimensions (default: 8192x8192, configurable up to 16384x16384) + - File sizes (256-500 MB depending on format) + - IFD counts (TIFF: max 100 IFDs) + - Tag counts (ICC: max 256, EXIF: max 512) + - Memory quotas enforced before allocation + +- **Input Validation**: + - Magic byte verification + - Header sanity checks + - Dimension bounds checking + - Container structure validation + +- **Fail-Closed Error Handling**: + - No best-effort decoding + - Hard errors on warnings + - No partial data leakage + +### Sandboxing +- **Kernel Namespaces** (PID, mount, user, network) +- **seccomp-bpf** syscall filtering +- **Landlock** filesystem access control +- **WebAssembly** sandbox for FFmpeg + +### Privacy Protection +- **Default ICC Profile Stripping**: Removes color profiles in hardened mode +- **Default EXIF Stripping**: Removes all metadata including GPS +- **Selective Retention**: Optional validated ICC/EXIF with strict limits + +--- + +## ⚑ CPU Optimization + +IMAGEHARDER supports CPU-tuned builds for maximum performance: + +### CPU Profiles + +| Profile | Target | Use Case | +|---------|--------|----------| +| `generic` | x86-64 baseline | Maximum compatibility | +| `v3` | x86-64-v3 (AVX2) | CI/production (Haswell+) | +| `host` | Native CPU | Development (Intel Core Ultra 7 165H) | + +### Build Examples + +```bash +# Generic (portable) +IMAGEHARDEN_CPU=generic ./build.sh + +# AVX2 baseline (recommended for production) +IMAGEHARDEN_CPU=v3 ./build_extended_formats.sh + +# Host-optimized (Meteor Lake: AVX2, AVX-VNNI, AES-NI, SHA) +IMAGEHARDEN_CPU=host ./build_extended_formats.sh +``` + +### Optimizations Applied +- **Meteor Lake** (`host`): `-march=native -mavx2 -mfma -mbmi -mbmi2 -maes -msha -mpclmul` +- **AVX2** (`v3`): `-march=x86-64-v3 -mtune=core-avx2` +- **Generic**: `-march=x86-64 -mtune=generic` + +--- + +## πŸš€ Getting Started + +### Prerequisites + +#### System Requirements +- Debian-based Linux (Ubuntu, Debian) +- Kernel 5.13+ (for Landlock support) +- 64-bit x86_64 architecture + +#### Build Dependencies +```bash +sudo apt-get update && sudo apt-get install -y \ + build-essential clang cmake nasm meson ninja-build \ + autoconf automake libtool git pkg-config \ + libseccomp-dev yasm python3-pip \ + rustc cargo +``` + +--- + +## πŸ”¨ Build Instructions + +### 1. Clone and Initialize -## Quick start ```bash -# Clone as a standalone repo git clone https://github.com/SWORDIntel/IMAGEHARDER.git +cd IMAGEHARDER +git submodule update --init --recursive +``` + +### 2. Build Core Libraries + +```bash +# Build core image libraries (GIF, etc.) +./build.sh + +# Build extended formats (AVIF, JXL, TIFF, OpenEXR, ICC, EXIF) +./build_extended_formats.sh + +# Build audio codecs (optional, Rust uses pure implementations) +./build_audio.sh -# or add as a submodule inside an existing project -git submodule add https://github.com/SWORDIntel/IMAGEHARDER.git external/imageharder +# Build FFmpeg WebAssembly sandbox +./setup_emsdk.sh +./build_ffmpeg_wasm.sh ``` -## Public API (library) -Add the crate to your workspace and call the hardened helpers directly: +### 3. Build Rust Components + +```bash +cd image_harden +cargo build --release +``` + +### 4. Run Tests + +```bash +cargo test --release +``` + +--- + +## πŸ“– Usage + +### Rust API + +Add to your `Cargo.toml`: ```toml -# parent-project/Cargo.toml [dependencies] -image_harden = { path = "external/imageharder/image_harden" } +image_harden = { path = "../image_harden" } +``` + +### Example: Decoding Images + +```rust +use image_harden::{decode_png, decode_jpeg, ImageHardenError}; + +fn main() -> Result<(), ImageHardenError> { + // Decode PNG with hardening + let png_data = std::fs::read("image.png")?; + let decoded = decode_png(&png_data)?; + + // Decode JPEG + let jpeg_data = std::fs::read("photo.jpg")?; + let decoded = decode_jpeg(&jpeg_data)?; + + Ok(()) +} ``` +### Example: Extended Formats + ```rust -use image_harden::decode_png; +use image_harden::formats::{avif, jxl, tiff, exr}; + +fn decode_modern_formats() -> Result<(), ImageHardenError> { + // AVIF (AV1 images) + #[cfg(feature = "avif")] + { + let avif_data = std::fs::read("image.avif")?; + let decoded = avif::decode_avif(&avif_data)?; + } -fn decode(bytes: &[u8]) -> Result, image_harden::ImageHardenError> { - decode_png(bytes) + // JPEG XL + #[cfg(feature = "jxl")] + { + let jxl_data = std::fs::read("image.jxl")?; + let decoded = jxl::decode_jxl(&jxl_data)?; + } + + // TIFF + #[cfg(feature = "tiff")] + { + let tiff_data = std::fs::read("scan.tiff")?; + let decoded = tiff::decode_tiff(&tiff_data)?; + } + + // OpenEXR (HDR) + #[cfg(feature = "openexr")] + { + let exr_data = std::fs::read("render.exr")?; + let decoded = exr::decode_exr(&exr_data)?; + } + + Ok(()) } ``` -See [`docs/INTEGRATION.md`](docs/INTEGRATION.md) for submodule workflow, feature flags, and metrics exposure. +### Example: Metadata Handling + +```rust +use image_harden::formats::{icc, exif}; + +fn handle_metadata() -> Result<(), ImageHardenError> { + // Validate ICC profile + #[cfg(feature = "icc")] + { + let profile_data = std::fs::read("profile.icc")?; + let info = icc::validate_icc_profile(&profile_data)?; + println!("ICC version: {}.{}", info.version_major, info.version_minor); + } + + // Validate EXIF (or strip for privacy) + #[cfg(feature = "exif")] + { + let exif_data = extract_exif_from_jpeg(&jpeg_data)?; + let info = exif::validate_exif(&exif_data)?; + + // Strip GPS data for privacy + let sanitized = exif::strip_gps_from_exif(&exif_data)?; + } + + Ok(()) +} +``` + +--- + +## πŸ› Fuzzing + +IMAGEHARDER includes comprehensive fuzzing infrastructure using `cargo-fuzz`. + +### Available Fuzz Targets + +#### Core Formats +- `fuzz_png`, `fuzz_jpeg`, `fuzz_gif` +- `fuzz_webp`, `fuzz_heif`, `fuzz_svg` + +#### Extended Formats +- `fuzz_avif`, `fuzz_jxl`, `fuzz_tiff`, `fuzz_exr` + +#### Hidden-Path Components +- `fuzz_icc`, `fuzz_exif` + +#### Audio +- `fuzz_mp3`, `fuzz_vorbis`, `fuzz_flac`, `fuzz_opus` + +#### Video +- `fuzz_video_mp4`, `fuzz_video_mkv` + +### Running Fuzz Tests -## Building the CLI ```bash cd image_harden -cargo build --release + +# Install cargo-fuzz +cargo install cargo-fuzz + +# Run a specific target +cargo fuzz run fuzz_avif + +# Run with sanitizers +cargo fuzz run fuzz_tiff -- -max_total_time=60 + +# Run all targets (CI) +./run_all_fuzz_tests.sh +``` + +--- + +## πŸ—οΈ Architecture + +### Directory Structure + ``` +IMAGEHARDER/ +β”œβ”€β”€ config/ +β”‚ └── hardening-flags.mk # Centralized hardening configuration +β”œβ”€β”€ image_harden/ +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ lib.rs # Core decoding functions +β”‚ β”‚ β”œβ”€β”€ formats/ # Extended format modules +β”‚ β”‚ β”‚ β”œβ”€β”€ avif.rs +β”‚ β”‚ β”‚ β”œβ”€β”€ jxl.rs +β”‚ β”‚ β”‚ β”œβ”€β”€ tiff.rs +β”‚ β”‚ β”‚ β”œβ”€β”€ exr.rs +β”‚ β”‚ β”‚ β”œβ”€β”€ icc.rs +β”‚ β”‚ β”‚ └── exif.rs +β”‚ β”‚ β”œβ”€β”€ metrics.rs # Prometheus metrics +β”‚ β”‚ └── metrics_server.rs +β”‚ β”œβ”€β”€ fuzz/ +β”‚ β”‚ └── fuzz_targets/ # Fuzzing harnesses +β”‚ β”œβ”€β”€ build.rs # C library FFI binding generation +β”‚ └── Cargo.toml +β”œβ”€β”€ docs/ +β”‚ └── HARDENING_EXTRAS.md # Extended hardening specification +β”œβ”€β”€ build.sh # Core library builder +β”œβ”€β”€ build_extended_formats.sh # Extended format builder +β”œβ”€β”€ build_audio.sh # Audio codec builder +└── build_ffmpeg_wasm.sh # FFmpeg WASM builder +``` + +### Format Coverage Matrix + +| Format | Decoder | Hardening | Fuzzing | Sandboxing | +|--------|---------|-----------|---------|------------| +| PNG | libpng | βœ… | βœ… | βœ… | +| JPEG | libjpeg-turbo | βœ… | βœ… | βœ… | +| GIF | giflib | βœ… | βœ… | βœ… | +| WebP | Pure Rust | βœ… | βœ… | βœ… | +| HEIF | libheif-rs | βœ… | βœ… | βœ… | +| SVG | resvg (Rust) | βœ… | βœ… | βœ… | +| AVIF | libavif+dav1d | βœ… | βœ… | 🚧 | +| JPEG XL | libjxl | βœ… | βœ… | 🚧 | +| TIFF | libtiff | βœ… | βœ… | 🚧 | +| OpenEXR | openexr | βœ… | βœ… | 🚧 | +| MP3 | minimp3 (Rust) | βœ… | βœ… | βœ… | +| Vorbis | lewton (Rust) | βœ… | βœ… | βœ… | +| FLAC | claxon (Rust) | βœ… | βœ… | βœ… | +| Opus | opus (Rust) | βœ… | βœ… | βœ… | +| MP4 | mp4parse (Rust) | βœ… | βœ… | βœ… | +| FFmpeg | WASM | βœ… | βœ… | βœ… | + +--- + +## πŸ“š Documentation + +- **[HARDENING_EXTRAS.md](docs/HARDENING_EXTRAS.md)** - Extended format hardening specification +- **[KERNEL_BUILD.md](KERNEL_BUILD.md)** - Kernel configuration for Landlock support +- **[config/hardening-flags.mk](config/hardening-flags.mk)** - Hardening flag reference + +### Hardening Specification + +The [HARDENING_EXTRAS.md](docs/HARDENING_EXTRAS.md) document defines: +- Extended media surface (AVIF, JXL, TIFF, OpenEXR, MPEG-TS) +- Hidden-path component policies (ICC, EXIF, fonts) +- CPU-tuned compilation profiles +- Sanitizer and fuzzing configurations +- Sandboxing models + +--- + +## 🀝 Contributing + +Contributions are welcome! Please ensure: +- All C/C++ code uses hardening flags from `config/hardening-flags.mk` +- New formats include Rust FFI wrappers with validation +- Fuzzing targets are added for new decoders +- Tests pass with sanitizers enabled + +--- + +## πŸ“„ License + +MIT License - see LICENSE file for details + +--- + +## πŸ™ Acknowledgments + +Built on: +- VideoLAN's dav1d (AV1 decoder) +- AOMediaCodec's libavif +- libjxl (JPEG XL Reference Implementation) +- Little CMS (lcms2) +- OpenEXR (Academy Software Foundation) +- FFmpeg Project +- Rust ecosystem (resvg, lewton, claxon, mp4parse, matroska) + +--- + +## πŸ” Security Contact + +For security issues, please contact: [security contact information] + +**CVEs Addressed**: +- CVE-2023-4863 (WebP) +- CVE-2019-7317, CVE-2015-8540 (libpng) +- CVE-2018-14498 (libjpeg) +- CVE-2019-15133, CVE-2016-3977 (giflib) +- And many more through comprehensive hardening + +--- -## Repository layout -- `image_harden/` – Rust crate with the hardened decoders and CLI entrypoint. -- `docs/INTEGRATION.md` – concise guide for embedding as a submodule. -- `docs/archive/` – preserved deep-dives (build notes, platform guides, extended hardening writeups). -- `docs/HARDENING_LIBSODIUM.md` – exploratory notes on applying similar hardening patterns to libsodium. -- `config/`, `ffmpeg/`, `docker-compose.yml` – deployment aids retained for downstream integrators. +**Status**: Production-Ready with Extended Format Support (v0.2.0) -## Security -Security contacts and policy are tracked in [`SECURITY.md`](SECURITY.md). Runtime mitigations are enabled by default; feature flags in `image_harden/Cargo.toml` gate optional codecs. +**Platform**: Debian-based Linux (x86-64, Intel Core Ultra 7 165H optimized)