diff --git a/Cargo.lock b/Cargo.lock index 42a01a9..a8715f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,15 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.10.0" @@ -139,10 +148,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.22" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -249,6 +259,26 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.13.0" @@ -315,6 +345,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -451,6 +487,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1597,6 +1643,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1662,6 +1719,7 @@ dependencies = [ "regex", "serde", "serde_json", + "sha2", "static_web_server_utils", "tempfile", "test_support", @@ -2056,6 +2114,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unarray" version = "0.1.4" @@ -2160,6 +2224,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/buildpacks/static-web-server/Cargo.toml b/buildpacks/static-web-server/Cargo.toml index c11d55b..3581e0a 100644 --- a/buildpacks/static-web-server/Cargo.toml +++ b/buildpacks/static-web-server/Cargo.toml @@ -23,6 +23,7 @@ bullet_stream = "0.10.0" tracing = "0.1.44" const_format = "0.2.35" glob = "0.3.3" +sha2 = "0.10" [dev-dependencies] libcnb-test = "=0.30.2" diff --git a/buildpacks/static-web-server/src/errors.rs b/buildpacks/static-web-server/src/errors.rs index cc976ea..4a8bfc4 100644 --- a/buildpacks/static-web-server/src/errors.rs +++ b/buildpacks/static-web-server/src/errors.rs @@ -24,6 +24,11 @@ pub(crate) enum StaticWebServerBuildpackError { CannotCreateWebExecD(std::io::Error), CannotInstallEnvAsHtmlData(std::io::Error), ConfigurationConstraint(String), + ChecksumVerificationFailed(String), + CannotReadChecksums { + filename: String, + error: std::io::Error, + }, } pub(crate) struct ErrorMessage { @@ -58,11 +63,12 @@ pub(crate) fn on_error(error: libcnb::Error) { eprintln!(); } +#[allow(clippy::too_many_lines)] fn buildpack_error_message(error: StaticWebServerBuildpackError) -> ErrorMessage { match error { StaticWebServerBuildpackError::Download(e) => ErrorMessage { message: formatdoc! {" - Unable to download the static web server for {buildpack_name}. + Failed to download Caddy web server for {buildpack_name}. ", buildpack_name = style::value(BUILDPACK_NAME) }, error_string: e.to_string(), error_id: "download_error".to_string(), @@ -144,6 +150,23 @@ fn buildpack_error_message(error: StaticWebServerBuildpackError) -> ErrorMessage error_string: e, error_id: "configuration_constraint_error".to_string(), }, + StaticWebServerBuildpackError::ChecksumVerificationFailed(e) => ErrorMessage { + message: formatdoc! {" + Failed to verify Caddy checksum for {buildpack_name} + + The downloaded Caddy binary's checksum does not match the expected value. + This could indicate a corrupted download or network tampering. + ", buildpack_name = style::value(BUILDPACK_NAME) }, + error_string: e, + error_id: "checksum_verification_failed_error".to_string(), + }, + StaticWebServerBuildpackError::CannotReadChecksums { filename, error } => ErrorMessage { + message: formatdoc! {" + Failed to verify Caddy checksum, reading {filename}, for {buildpack_name} + ", buildpack_name = style::value(BUILDPACK_NAME), filename = style::value(filename) }, + error_string: error.to_string(), + error_id: "cannot_read_checksums_error".to_string(), + }, } } diff --git a/buildpacks/static-web-server/src/install_web_server.rs b/buildpacks/static-web-server/src/install_web_server.rs index c57ffb1..e3e5892 100644 --- a/buildpacks/static-web-server/src/install_web_server.rs +++ b/buildpacks/static-web-server/src/install_web_server.rs @@ -1,4 +1,6 @@ use std::fs; +use std::io::{BufRead, BufReader}; +use std::path::Path; use libcnb::build::BuildContext; use libcnb::data::layer_name; @@ -10,6 +12,7 @@ use libherokubuildpack::download::download_file; use libherokubuildpack::log::log_info; use libherokubuildpack::tar::decompress_tarball; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha512}; use tempfile::NamedTempFile; use crate::o11y::*; @@ -79,8 +82,18 @@ pub(crate) fn install_web_server( { INSTALLATION_WEB_SERVER_VERSION } = web_server_version, "downloading web server" ); - download_file(artifact_url, web_server_tgz.path()) + download_file(&artifact_url, web_server_tgz.path()) .map_err(StaticWebServerBuildpackError::Download)?; + + // Verify the checksum + log_info("Verifying web server checksum"); + verify_caddy_checksum( + web_server_version, + &context.target.os, + &context.target.arch, + web_server_tgz.path(), + )?; + decompress_tarball(&mut web_server_tgz.into_file(), &web_server_dir) .map_err(StaticWebServerBuildpackError::CannotUnpackCaddyTarball)?; } @@ -117,3 +130,98 @@ pub(crate) struct WebServerLayerMetadata { arch: String, os: String, } + +/// Verifies the Caddy binary checksum against the official checksums file +fn verify_caddy_checksum( + version: &str, + os: &str, + arch: &str, + tarball_path: &Path, +) -> Result<(), libcnb::Error> { + let base_url = format!("https://github.com/caddyserver/caddy/releases/download/v{version}"); + let checksums_filename = format!("caddy_{version}_checksums.txt"); + let artifact_filename = format!("caddy_{version}_{os}_{arch}.tar.gz"); + + // Download checksums file + let checksums_file = NamedTempFile::new() + .map_err(StaticWebServerBuildpackError::CannotCreateCaddyTarballFile)?; + download_file( + format!("{base_url}/{checksums_filename}"), + checksums_file.path(), + ) + .map_err(StaticWebServerBuildpackError::Download)?; + + // Verify the tarball checksum against the checksums file + verify_checksum(tarball_path, checksums_file.path(), &artifact_filename)?; + + tracing::info!("Successfully verified Caddy checksum for version {version}"); + + Ok(()) +} + +/// Verifies the checksum of a file against a checksums file +fn verify_checksum( + file_path: &Path, + checksums_path: &Path, + expected_filename: &str, +) -> Result<(), libcnb::Error> { + // Calculate the SHA512 hash of the downloaded file + let mut file = fs::File::open(file_path).map_err(|error| { + StaticWebServerBuildpackError::CannotReadChecksums { + filename: expected_filename.to_string(), + error, + } + })?; + let mut hasher = Sha512::new(); + std::io::copy(&mut file, &mut hasher).map_err(|error| { + StaticWebServerBuildpackError::CannotReadChecksums { + filename: expected_filename.to_string(), + error, + } + })?; + let calculated_hash = format!("{:x}", hasher.finalize()); + + // Parse the checksums file to find the expected checksum + let checksums_filename = checksums_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("checksums.txt"); + let checksums_file = fs::File::open(checksums_path).map_err(|error| { + StaticWebServerBuildpackError::CannotReadChecksums { + filename: checksums_filename.to_string(), + error, + } + })?; + let reader = BufReader::new(checksums_file); + + let mut found_checksum = None; + for line in reader.lines() { + let line = line.map_err(|error| StaticWebServerBuildpackError::CannotReadChecksums { + filename: checksums_filename.to_string(), + error, + })?; + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 && parts[1] == expected_filename { + found_checksum = Some(parts[0].to_string()); + break; + } + } + + let expected_hash = found_checksum.ok_or_else(|| { + StaticWebServerBuildpackError::ChecksumVerificationFailed(format!( + "Checksum for {expected_filename} not found in checksums file" + )) + })?; + + // Compare checksums + if calculated_hash != expected_hash { + return Err( + StaticWebServerBuildpackError::ChecksumVerificationFailed(format!( + "Checksum mismatch for {expected_filename}: expected {expected_hash}, got {calculated_hash}" + )) + .into(), + ); + } + + Ok(()) +} diff --git a/buildpacks/static-web-server/src/main.rs b/buildpacks/static-web-server/src/main.rs index 93395a3..a7aa12c 100644 --- a/buildpacks/static-web-server/src/main.rs +++ b/buildpacks/static-web-server/src/main.rs @@ -32,7 +32,7 @@ use ureq as _; const BUILDPACK_NAME: &str = "Heroku Static Web Server Buildpack"; const BUILD_PLAN_ID: &str = "static-web-server"; pub(crate) const WEB_SERVER_NAME: &str = "caddy"; -pub(crate) const WEB_SERVER_VERSION: &str = "2.10.2"; +pub(crate) const WEB_SERVER_VERSION: &str = "2.11.1"; pub(crate) struct StaticWebServerBuildpack;