Skip to content

Commit

Permalink
Freshclam: download .sign files to verify CVDs and CDIFFs
Browse files Browse the repository at this point in the history
Also fix an issue where making a CLD would only include the CFG file for
daily and not if patching any other database.

Also verify CDIFF external digital signatures when applying CDIFFs with
sigtool.
  • Loading branch information
micahsnyder committed Dec 15, 2024
1 parent 6677dc8 commit fd9890a
Show file tree
Hide file tree
Showing 9 changed files with 474 additions and 206 deletions.
1 change: 1 addition & 0 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 libclamav_rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ clam-sigutil = { path = "../../clamav-signature-util" }
tar = "0.4.43"
md5 = "0.7.0"
openssl = "0.10.68"
glob = "0.3.1"

[features]
not_ready = []
Expand Down
1 change: 1 addition & 0 deletions libclamav_rust/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ include = [
"evidence::IndicatorType",
"scanners::scan_onenote",
"scanners::cli_scanalz",
"util::glob_rm",
]

# prefix = "CAPI_"
Expand Down
170 changes: 115 additions & 55 deletions libclamav_rust/src/cdiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ use std::{
str::{self, FromStr},
};

use crate::sys;
use crate::util;
use crate::validate_str_param;
use crate::{codesign, ffi_error, ffi_util::FFIError, sys, validate_str_param};

use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use log::{debug, error, warn};
Expand Down Expand Up @@ -146,6 +144,9 @@ pub enum Error {

#[error("NUL found within CString")]
CstringNulError(#[from] std::ffi::NulError),

#[error("Can't verify: {0}")]
CannotVerify(String),
}

/// Errors particular to input handling (i.e., syntax, or side effects from
Expand Down Expand Up @@ -469,7 +470,7 @@ pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Resu
// Make a copy of the script file name to use for the cdiff file
let cdiff_file_name_string = script_file_name.to_string();
let mut cdiff_file_name = cdiff_file_name_string.as_str();
debug!("script2cdiff() - script file name: {:?}", cdiff_file_name);
debug!("script2cdiff: script file name: {:?}", cdiff_file_name);

// Remove the "".script" suffix
if let Some(file_name) = cdiff_file_name.strip_suffix(".script") {
Expand All @@ -494,7 +495,7 @@ pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Resu

// Add .cdiff suffix
let cdiff_file_name = format!("{}.{}", cdiff_file_name, "cdiff");
debug!("script2cdiff() - writing to: {:?}", &cdiff_file_name);
debug!("script2cdiff: writing to: {:?}", &cdiff_file_name);

// Open cdiff_file_name for writing
let mut cdiff_file: File = File::create(&cdiff_file_name)
Expand Down Expand Up @@ -532,7 +533,7 @@ pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Resu
.map_err(|e| Error::FileMeta(cdiff_file_name.to_owned(), e))?
.len();
debug!(
"script2cdiff() - wrote {} bytes to {}",
"script2cdiff: wrote {} bytes to {}",
cdiff_file_len, cdiff_file_name
);

Expand Down Expand Up @@ -575,27 +576,48 @@ pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Resu
Ok(())
}

/// This function is only meant to be called from sigtool.c
/// C interface for cdiff_apply() (below).
/// This function is for use in sigtool.c and libfreshclam_internal.c
#[export_name = "cdiff_apply"]
pub extern "C" fn _cdiff_apply(fd: i32, mode: u16) -> i32 {
debug!(
"cdiff_apply() - called with file_descriptor={}, mode={}",
fd, mode
);
pub unsafe extern "C" fn _cdiff_apply(
cdiff_file_path_str: *const c_char,
certs_directory_str: *const c_char,
mode: u16,
err: *mut *mut FFIError,
) -> bool {
let cdiff_file_path_str = validate_str_param!(cdiff_file_path_str);
let cdiff_file_path = match Path::new(cdiff_file_path_str).canonicalize() {
Ok(p) => p,
Err(e) => {
return ffi_error!(
err = err,
Error::CannotVerify(format!("Invalid cdiff file path: {}", e))
);
}
};

let certs_directory_str = validate_str_param!(certs_directory_str);
let certs_directory = match PathBuf::from_str(certs_directory_str) {
Ok(p) => p,
Err(e) => {
return ffi_error!(
err = err,
Error::CannotVerify(format!("Invalid certs directory path: {}", e))
);
}
};

let mode = if mode == 1 {
ApplyMode::Cdiff
} else {
ApplyMode::Script
};

let mut file = util::file_from_fd_or_handle(fd);

if let Err(e) = cdiff_apply(&mut file, mode) {
error!("{}", e);
-1
if let Err(e) = cdiff_apply(&cdiff_file_path, &certs_directory, mode) {
error!("Failed to apply {:?}: {}", cdiff_file_path, e);
ffi_error!(err = err, e)
} else {
0
true
}
}

Expand All @@ -612,58 +634,96 @@ pub extern "C" fn _cdiff_apply(fd: i32, mode: u16) -> i32 {
/// A cdiff file contains a footer that is the signed signature of the sha256
/// file contains of the header and the body. The footer begins after the first
/// ':' character to the left of EOF.
pub fn cdiff_apply(file: &mut File, mode: ApplyMode) -> Result<(), Error> {
pub fn cdiff_apply(
cdiff_file_path: &Path,
certs_directory: &Path,
mode: ApplyMode,
) -> Result<(), Error> {
let path = std::env::current_dir().unwrap();
debug!("cdiff_apply() - current directory is {}", path.display());
debug!("cdiff_apply: applying {}", cdiff_file_path.display());
debug!("cdiff_apply: current directory is {}", path.display());

// Open cdiff file for reading
let mut file = File::open(cdiff_file_path).map_err(Error::IoError)?;

// Only read dsig, header, etc. if this is a cdiff file
let header_length = match mode {
ApplyMode::Script => 0,
ApplyMode::Cdiff => {
let dsig = read_dsig(file)?;
debug!("cdiff_apply() - final dsig length is {}", dsig.len());
if is_debug_enabled() {
print_file_data(dsig.clone(), dsig.len());
}

// Get file length
let file_len = file.metadata()?.len() as usize;
let footer_offset = file_len - dsig.len() - 1;

// The SHA is calculated from the contents of the beginning of the file
// up until the ':' before the dsig at the end of the file.
let sha256 = get_hash(file, footer_offset)?;

debug!("cdiff_apply() - sha256: {}", hex::encode(sha256));

// cli_versig2 will expect dsig to be a null-terminated string
let dsig_cstring = CString::new(dsig)?;

// Verify cdiff
let n = CString::new(PUBLIC_KEY_MODULUS).unwrap();
let e = CString::new(PUBLIC_KEY_EXPONENT).unwrap();
let versig_result = unsafe {
sys::cli_versig2(
sha256.to_vec().as_ptr(),
dsig_cstring.as_ptr(),
n.as_ptr() as *const c_char,
e.as_ptr() as *const c_char,
)

// Check if there is an external digital signature
// The filename would be the same as the cdiff file with an extra .sign extension
let sign_file_path = cdiff_file_path.with_extension("cdiff.sign");
let verify_result =
codesign::verify_signed_file(cdiff_file_path, &sign_file_path, certs_directory);
let verified = match verify_result {
Ok(signer) => {
debug!(
"cdiff_apply: external signature verified. Signed by: {}",
signer
);
true
}
Err(codesign::Error::InvalidDigitalSignature(m)) => {
debug!("cdiff_apply: invalid external signature: {}", m);
return Err(Error::InvalidDigitalSignature);
}
Err(e) => {
debug!("cdiff_apply: error validating external signature: {:?}", e);

// If the external signature could not be validated (e.g. does not exist)
// then continue on and try to validate the internal signature.
false
}
};
debug!("cdiff_apply() - cli_versig2() result = {}", versig_result);
if versig_result != 0 {
return Err(Error::InvalidDigitalSignature);

if !verified {
// try to verify the internal (legacy) digital signature
let dsig = read_dsig(&mut file)?;
debug!("cdiff_apply: final dsig length is {}", dsig.len());
if is_debug_enabled() {
print_file_data(dsig.clone(), dsig.len());
}

let footer_offset = file_len - dsig.len() - 1;

// The SHA is calculated from the contents of the beginning of the file
// up until the ':' before the dsig at the end of the file.
let sha256 = get_hash(&mut file, footer_offset)?;

debug!("cdiff_apply: sha256: {}", hex::encode(sha256));

// cli_versig2 will expect dsig to be a null-terminated string
let dsig_cstring = CString::new(dsig)?;

// Verify cdiff
let n = CString::new(PUBLIC_KEY_MODULUS).unwrap();
let e = CString::new(PUBLIC_KEY_EXPONENT).unwrap();
let versig_result = unsafe {
sys::cli_versig2(
sha256.to_vec().as_ptr(),
dsig_cstring.as_ptr(),
n.as_ptr() as *const c_char,
e.as_ptr() as *const c_char,
)
};
debug!("cdiff_apply: cli_versig2() result = {}", versig_result);
if versig_result != 0 {
return Err(Error::InvalidDigitalSignature);
}
}

// Read file length from header
let (header_len, header_offset) = read_size(file)?;
let (header_len, header_offset) = read_size(&mut file)?;
debug!(
"cdiff_apply() - header len = {}, file len = {}, header offset = {}",
"cdiff_apply: header len = {}, file len = {}, header offset = {}",
header_len, file_len, header_offset
);

let current_pos = file.seek(SeekFrom::Start(header_offset as u64))?;
debug!("cdiff_apply() - current file offset = {}", current_pos);
debug!("cdiff_apply: current file offset = {}", current_pos);
header_len as usize
}
};
Expand Down Expand Up @@ -1074,7 +1134,7 @@ fn cmd_close(ctx: &mut Context) -> Result<(), InputError> {
ctx.additions.clear();
}

debug!("cmd_close() - finished");
debug!("cmd_close: finished");

Ok(())
}
Expand Down Expand Up @@ -1183,7 +1243,7 @@ fn read_dsig(file: &mut File) -> Result<Vec<u8>, SignatureError> {
// Read from dsig_offset to EOF
let mut dsig: Vec<u8> = vec![];
file.read_to_end(&mut dsig)?;
debug!("read_dsig() - dsig length is {}", dsig.len());
debug!("read_dsig: dsig length is {}", dsig.len());

// Find the signature
let offset: usize = SIG_SIZE + 1;
Expand Down
2 changes: 1 addition & 1 deletion libclamav_rust/src/cvd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ impl CVD {
let md5_str = std::str::from_utf8(md5_bytes)
.map_err(|_| Error::Parse("MD5 hash string is not valid unicode".to_string()))?;
let md5: Option<String> = if md5_str.len() != 32 {
debug!("MD5 hash string is not 32 characters long. It may be empty");
debug!("MD5 hash string not present.");
None
} else {
Some(md5_str.to_string())
Expand Down
46 changes: 43 additions & 3 deletions libclamav_rust/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@
* MA 02110-1301, USA.
*/

use std::{ffi::CStr, fs::File};
use std::{ffi::CStr, fs::File, os::raw::c_char};

use log::error;
use glob::glob;
use log::{debug, error, warn};

use crate::sys;
use crate::{ffi_error, ffi_util::FFIError, sys, validate_str_param};

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Glob error: {0}")]
GlobError(#[from] glob::GlobError),

#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}

/// Obtain a std::fs::File from an i32 in a platform-independent manner.
///
Expand Down Expand Up @@ -128,3 +138,33 @@ pub unsafe fn scan_archive_metadata(
)
}
}

/// C interface to delete files using a glob pattern.
///
/// # Safety
///
/// No parameters may be NULL.
#[export_name = "glob_rm"]
pub unsafe extern "C" fn glob_rm(
glob_str: *const c_char,
err: *mut *mut FFIError,
) -> bool {
let glob_str = validate_str_param!(glob_str);

for entry in glob(glob_str).expect("Failed to read glob pattern") {
match entry {
Ok(path) => {
debug!("Deleting: {path:?}");
if let Err(e) = std::fs::remove_file(&path) {
warn!("Failed to delete file: {path:?}");
return ffi_error!(err = err, Error::IoError(e));
}
}
Err(e) => {
return ffi_error!(err = err, Error::GlobError(e));
}
}
}

true
}
13 changes: 12 additions & 1 deletion libfreshclam/libfreshclam.c
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,24 @@ fc_error_t fc_prune_database_directory(char **databaseList, uint32_t nDatabases)

while ((dent = readdir(dir))) {
if (dent->d_ino) {
// prune any CVD/CLD files that are not in the database list
if ((NULL != (extension = strstr(dent->d_name, ".cld"))) ||
(NULL != (extension = strstr(dent->d_name, ".cvd")))) {

// find the first '-' or '.' in the filename
// Use this to determine the database name.
// We need this so we can ALSO prune the .sign files for unwanted databases.
// Will also be useful in case the database filename includes a hyphenated version number.
const char * first_dash_or_dot = strchr(dent->d_name, '-');
if (NULL == first_dash_or_dot) {
first_dash_or_dot = extension;
}

uint32_t i;
int bFound = 0;
for (i = 0; i < nDatabases; i++) {
if (0 == strncmp(databaseList[i], dent->d_name, extension - dent->d_name)) {
// check that the database name is in the database list
if (0 == strncmp(databaseList[i], dent->d_name, first_dash_or_dot - dent->d_name)) {
bFound = 1;
}
}
Expand Down
Loading

0 comments on commit fd9890a

Please sign in to comment.