Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions buildpacks/static-web-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 24 additions & 1 deletion buildpacks/static-web-server/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -58,11 +63,12 @@ pub(crate) fn on_error(error: libcnb::Error<StaticWebServerBuildpackError>) {
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(),
Expand Down Expand Up @@ -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(),
},
}
}

Expand Down
110 changes: 109 additions & 1 deletion buildpacks/static-web-server/src/install_web_server.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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::*;
Expand Down Expand Up @@ -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)?;
}
Expand Down Expand Up @@ -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<StaticWebServerBuildpackError>> {
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<StaticWebServerBuildpackError>> {
// 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(())
}
2 changes: 1 addition & 1 deletion buildpacks/static-web-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading