use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
const SOURCES: &[&str] = &[
"osslsigncode.c",
"helpers.c",
"utf.c",
"msi.c",
"pe.c",
"cab.c",
"cat.c",
"appx.c",
"script.c",
"applink.c",
"osslsigncode.h",
"helpers.h",
"utf.h",
"Config.h.in",
];
const PROMOTE: &[&str] = &[
"free_options",
"engine_control_set",
"read_password",
"read_crypto_params",
"verify_signed_file",
"add_timestamp_and_blob",
"add_nested_timestamp_and_blob",
"cursig_set_nested",
"nested_signatures_number_get",
"pkcs7_get_sigfile",
"check_attached_data",
"ui_osslsigncode",
"bio_new_file",
"ui_method",
"providers_cleanup",
];
fn main() -> Result<()> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let vendor_dir = manifest_dir.join("vendor/osslsigncode");
let is_windows = env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows");
emit_rerun_triggers(&vendor_dir);
require_vendored(&vendor_dir)?;
println!(
"cargo:rustc-env=OSSL_VENDOR_COMMIT={}",
vendor_revision(&vendor_dir)
);
println!("cargo:root={}", manifest_dir.display());
println!("cargo:include={}", vendor_dir.display());
if is_windows {
println!("cargo:rustc-link-lib=ws2_32");
println!("cargo:rustc-link-lib=advapi32");
println!("cargo:rustc-link-lib=crypt32");
}
let includes = resolve_includes()?;
write_config_header(&out_dir.join("config.h"), is_windows)?;
generate_bindings(&vendor_dir, &out_dir, &includes)?;
compile_native(&vendor_dir, &out_dir, &includes, is_windows)?;
Ok(())
}
fn emit_rerun_triggers(vendor_dir: &Path) {
println!("cargo:rerun-if-changed=build.rs");
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join(".git").display()
);
for source in SOURCES {
println!(
"cargo:rerun-if-changed={}",
vendor_dir.join(source).display()
);
}
for key in [
"DEP_OPENSSL_INCLUDE",
"DEP_Z_INCLUDE",
"DEP_Z_ROOT",
"OPENSSL_DIR",
"OPENSSL_INCLUDE_DIR",
"LIBCLANG_PATH",
"CC",
"CFLAGS",
"MACOSX_DEPLOYMENT_TARGET",
] {
println!("cargo:rerun-if-env-changed={key}");
}
}
fn require_vendored(vendor_dir: &Path) -> Result<()> {
if vendor_dir.join("osslsigncode.h").is_file() {
return Ok(());
}
Err(
"missing vendor/osslsigncode. Initialize the submodule using: \
`git submodule update --init --recursive`"
.into(),
)
}
fn vendor_revision(vendor: &Path) -> String {
Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(vendor)
.output()
.ok()
.filter(|out| out.status.success())
.map(|out| String::from_utf8_lossy(&out.stdout).trim().to_owned())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".to_owned())
}
fn resolve_includes() -> Result<Vec<PathBuf>> {
let mut dirs: Vec<PathBuf> = [
env::var_os("DEP_OPENSSL_INCLUDE").map(PathBuf::from),
env::var_os("OPENSSL_INCLUDE_DIR").map(PathBuf::from),
env::var_os("OPENSSL_DIR").map(|d| PathBuf::from(d).join("include")),
]
.into_iter()
.flatten()
.collect();
if dirs.is_empty() {
return Err("OpenSSL headers not found. Depend on `openssl-sys` \
with the `vendored` feature, or set `OPENSSL_DIR`."
.into());
}
dirs.extend(
[
env::var_os("DEP_Z_INCLUDE").map(PathBuf::from),
env::var_os("DEP_Z_ROOT").map(|d| PathBuf::from(d).join("include")),
]
.into_iter()
.flatten(),
);
Ok(dirs)
}
fn write_config_header(path: &Path, is_windows: bool) -> Result<()> {
let mut config = vec![
r#"/* Generated by build.rs from the osslsigncode submodule Config.h.in. */"#,
r#"#define VERSION_MAJOR "2""#,
r#"#define VERSION_MINOR "15""#,
r#"#define PACKAGE_STRING "osslsigncode 2.15-dev""#,
r#"#define PACKAGE_BUGREPORT "Michal.Trojnara@stunnel.org""#,
];
if is_windows {
config.push(r#"#define HAVE_MAPVIEWOFFILE 1"#);
} else {
config.extend([
r#"#define HAVE_TERMIOS_H 1"#,
r#"#define HAVE_GETPASS 1"#,
r#"#define HAVE_SYS_MMAN_H 1"#,
r#"#define HAVE_MMAP 1"#,
]);
}
fs::write(path, config.join("\n"))?;
Ok(())
}
fn compile_native(vendor: &Path, out: &Path, includes: &[PathBuf], is_windows: bool) -> Result<()> {
let object = out.join("osslsigncode.o");
let mut base_build = cc::Build::new();
base_build
.std("c11")
.warnings(false)
.include(out)
.include(vendor)
.includes(includes)
.define("HAVE_CONFIG_H", "1")
.flag_if_supported("-Wno-deprecated-declarations")
.flag_if_supported("-fno-addrsig");
if is_windows {
base_build.define("_CRT_SECURE_NO_WARNINGS", "1");
}
let mut cmd = base_build.get_compiler().to_command();
cmd.arg("-c")
.arg(vendor.join("osslsigncode.c"))
.arg("-o")
.arg(&object)
.arg("-Dmain=osslsigncode_cli_main");
let status = cmd.status()?;
if !status.success() {
return Err(format!("failed to compile osslsigncode.c: {status}").into());
}
let promoted = promote_symbols(&object, PROMOTE)?;
let missing: Vec<&str> = [
"free_options",
"read_password",
"read_crypto_params",
"verify_signed_file",
"add_timestamp_and_blob",
"ui_osslsigncode",
"bio_new_file",
"ui_method",
]
.into_iter()
.filter(|&req| !promoted.contains(req))
.collect();
if !missing.is_empty() {
return Err(format!("Required symbols missing from osslsigncode.o: {missing:?}").into());
}
println!("cargo:rustc-check-cfg=cfg(ossl_has_engine_ctrl)");
println!("cargo:rustc-check-cfg=cfg(ossl_has_providers_cleanup)");
if promoted.contains("engine_control_set") {
println!("cargo:rustc-cfg=ossl_has_engine_ctrl");
}
if promoted.contains("providers_cleanup") {
println!("cargo:rustc-cfg=ossl_has_providers_cleanup");
}
let mut library_build = base_build.clone();
library_build.object(&object);
if is_windows {
library_build.file(vendor.join("applink.c"));
}
for source in [
"helpers.c",
"utf.c",
"msi.c",
"pe.c",
"cab.c",
"cat.c",
"appx.c",
"script.c",
] {
library_build.file(vendor.join(source));
}
library_build.compile("osslsigncode");
Ok(())
}
fn promote_symbols(path: &Path, want: &[&str]) -> Result<BTreeSet<String>> {
let mut data = fs::read(path)?;
let targets: BTreeSet<&str> = want.iter().copied().collect();
let plan = symbol_promotion_plan(&data, &targets)?;
for name in want {
if !plan.found.contains(*name) {
println!(
"cargo:warning=could not promote `{name}` in {} (may already be global)",
path.display()
);
}
}
match plan.format {
ObjectFormat::Elf => promote_elf_symbols(&mut data, &targets)?,
ObjectFormat::MachO {
external_flag_offsets,
} => {
for offset in external_flag_offsets {
*data
.get_mut(offset)
.ok_or("Mach-O symbol entry extends past object")? |= 0x01;
}
}
}
fs::write(path, &data)?;
Ok(plan.found)
}
struct PromotionPlan {
format: ObjectFormat,
found: BTreeSet<String>,
}
enum ObjectFormat {
Elf,
MachO { external_flag_offsets: Vec<usize> },
}
fn symbol_promotion_plan(data: &[u8], targets: &BTreeSet<&str>) -> Result<PromotionPlan> {
match goblin::Object::parse(data)? {
goblin::Object::Elf(elf) => Ok(PromotionPlan {
format: ObjectFormat::Elf,
found: elf_symbol_names(&elf, targets),
}),
goblin::Object::Mach(goblin::mach::Mach::Binary(mach)) => {
let symoff = mach
.load_commands
.iter()
.find_map(|command| match command.command {
goblin::mach::load_command::CommandVariant::Symtab(symtab) => {
Some(usize::try_from(symtab.symoff))
}
_ => None,
})
.transpose()?
.ok_or("Mach-O missing LC_SYMTAB")?;
let entry_size = if mach.is_64 { 16 } else { 12 };
let symbols = mach.symbols.as_ref().ok_or("Mach-O missing symbols")?;
let mut found = BTreeSet::new();
let mut external_flag_offsets = Vec::new();
for (index, symbol) in symbols.into_iter().enumerate() {
let Ok((name, _)) = symbol else { continue };
let name = name.trim_start_matches('_');
if targets.contains(name) {
external_flag_offsets.push(
symoff
.checked_add(
index
.checked_mul(entry_size)
.ok_or("Mach-O symbol table size overflow")?,
)
.and_then(|offset| offset.checked_add(4))
.ok_or("Mach-O symbol table size overflow")?,
);
found.insert(name.to_owned());
}
}
Ok(PromotionPlan {
format: ObjectFormat::MachO {
external_flag_offsets,
},
found,
})
}
_ => Err("Unsupported object format for symbol promotion".into()),
}
}
fn elf_symbol_names(elf: &goblin::elf::Elf<'_>, targets: &BTreeSet<&str>) -> BTreeSet<String> {
elf.syms
.iter()
.filter_map(|symbol| elf.strtab.get_at(symbol.st_name))
.map(|name| name.trim_start_matches('_'))
.filter(|name| targets.contains(name))
.map(str::to_owned)
.collect()
}
fn promote_elf_symbols(data: &mut [u8], targets: &BTreeSet<&str>) -> Result<()> {
let snapshot = data.to_vec();
let elf = match goblin::Object::parse(&snapshot)? {
goblin::Object::Elf(elf) => elf,
_ => return Err("expected an ELF object".into()),
};
let symbols = ElfSymbolTable::from_elf(&elf, data.len())?;
let section_headers = ElfSectionHeaders::from_elf(&elf, data.len())?;
let permutation = SymbolPermutation::for_elf(&elf, targets, symbols.count)?;
symbols.rewrite(data, &permutation)?;
section_headers.set_info(data, symbols.section_index, permutation.local_count)?;
for (index, section) in elf.section_headers.iter().enumerate() {
if usize::try_from(section.sh_link)? != symbols.section_index {
continue;
}
rewrite_symbol_references(
data,
section,
index,
&symbols,
§ion_headers,
&permutation,
elf.is_64,
)?;
}
Ok(())
}
struct ElfSymbolTable {
section_index: usize,
offset: usize,
size: usize,
entry_size: usize,
info_offset: usize,
count: usize,
}
impl ElfSymbolTable {
fn from_elf(elf: &goblin::elf::Elf<'_>, data_len: usize) -> Result<Self> {
let (section_index, section) = elf
.section_headers
.iter()
.enumerate()
.find(|(_, section)| section.sh_type == goblin::elf::section_header::SHT_SYMTAB)
.ok_or("ELF missing symtab")?;
let entry_size = usize::try_from(section.sh_entsize)?;
if entry_size == 0 || section.sh_size % section.sh_entsize != 0 {
return Err("invalid ELF symbol table entry size".into());
}
let count = usize::try_from(section.sh_size / section.sh_entsize)?;
if count != elf.syms.len() {
return Err("ELF symbol table does not match parsed symbols".into());
}
let info_offset = if elf.is_64 { 4 } else { 12 };
if entry_size <= info_offset {
return Err("invalid ELF symbol table entry layout".into());
}
let offset = usize::try_from(section.sh_offset)?;
let size = usize::try_from(section.sh_size)?;
checked_range(offset, size, data_len, "ELF symbol table")?;
Ok(Self {
section_index,
offset,
size,
entry_size,
info_offset,
count,
})
}
fn rewrite(&self, data: &mut [u8], permutation: &SymbolPermutation) -> Result<()> {
let range = checked_range(self.offset, self.size, data.len(), "ELF symbol table")?;
let original = data[range].to_vec();
for (new, old) in permutation.old_order.iter().copied().enumerate() {
let destination = self.offset + new * self.entry_size;
let source = old * self.entry_size;
data[destination..destination + self.entry_size]
.copy_from_slice(&original[source..source + self.entry_size]);
if permutation.promoted[old] {
data[destination + self.info_offset] =
(data[destination + self.info_offset] & 0x0f) | 0x10;
}
}
Ok(())
}
}
struct SymbolPermutation {
old_order: Vec<usize>,
new_index_of_old: Vec<usize>,
promoted: Vec<bool>,
local_count: usize,
}
impl SymbolPermutation {
fn for_elf(
elf: &goblin::elf::Elf<'_>,
targets: &BTreeSet<&str>,
symbol_count: usize,
) -> Result<Self> {
if symbol_count != elf.syms.len() {
return Err("ELF symbol table does not match parsed symbols".into());
}
let mut locals = Vec::new();
let mut nonlocals = Vec::new();
let mut promoted = vec![false; symbol_count];
for (index, symbol) in elf.syms.iter().enumerate() {
let name = elf.strtab.get_at(symbol.st_name).unwrap_or_default();
let is_promoted = targets.contains(name.trim_start_matches('_'));
promoted[index] = is_promoted;
if symbol.st_bind() == goblin::elf::sym::STB_LOCAL && !is_promoted {
locals.push(index);
} else {
nonlocals.push(index);
}
}
let local_count = locals.len();
let old_order: Vec<_> = locals.into_iter().chain(nonlocals).collect();
let mut new_index_of_old = vec![0; symbol_count];
for (new, old) in old_order.iter().copied().enumerate() {
new_index_of_old[old] = new;
}
Ok(Self {
old_order,
new_index_of_old,
promoted,
local_count,
})
}
fn remap(&self, old: usize, reference: &str) -> Result<u64> {
self.new_index_of_old
.get(old)
.copied()
.map(|index| u64::try_from(index).map_err(Into::into))
.unwrap_or_else(|| Err(format!("{reference} references unknown symbol").into()))
}
}
struct ElfSectionHeaders {
offset: usize,
entry_size: usize,
info_offset: usize,
little_endian: bool,
}
impl ElfSectionHeaders {
fn from_elf(elf: &goblin::elf::Elf<'_>, data_len: usize) -> Result<Self> {
let offset = usize::try_from(elf.header.e_shoff)?;
let entry_size = usize::from(elf.header.e_shentsize);
let info_offset = if elf.is_64 { 44 } else { 28 };
let size = entry_size
.checked_mul(elf.section_headers.len())
.ok_or("ELF section table size overflow")?;
if entry_size < info_offset + 4 {
return Err("invalid ELF section table".into());
}
checked_range(offset, size, data_len, "ELF section table")?;
Ok(Self {
offset,
entry_size,
info_offset,
little_endian: elf.header.e_ident[goblin::elf::header::EI_DATA]
== goblin::elf::header::ELFDATA2LSB,
})
}
fn set_info(&self, data: &mut [u8], section: usize, value: usize) -> Result<()> {
let offset = self.info_offset(section)?;
write_u32(data, offset, u32::try_from(value)?, self.little_endian)
}
fn info(&self, data: &[u8], section: usize) -> Result<usize> {
Ok(read_uint(data, self.info_offset(section)?, 4, self.little_endian)? as usize)
}
fn info_offset(&self, section: usize) -> Result<usize> {
self.offset
.checked_add(
section
.checked_mul(self.entry_size)
.and_then(|offset| offset.checked_add(self.info_offset))
.ok_or("ELF section table size overflow")?,
)
.ok_or_else(|| "ELF section table size overflow".into())
}
}
fn rewrite_symbol_references(
data: &mut [u8],
section: &goblin::elf::section_header::SectionHeader,
section_index: usize,
symbols: &ElfSymbolTable,
section_headers: &ElfSectionHeaders,
permutation: &SymbolPermutation,
is_64: bool,
) -> Result<()> {
let offset = usize::try_from(section.sh_offset)?;
let size = usize::try_from(section.sh_size)?;
let range = checked_range(offset, size, data.len(), "ELF section")?;
match section.sh_type {
goblin::elf::section_header::SHT_REL | goblin::elf::section_header::SHT_RELA => {
rewrite_relocations(
data,
range.start,
range.end,
section.sh_entsize,
permutation,
is_64,
section_headers.little_endian,
)
}
goblin::elf::section_header::SHT_GROUP => {
let old = section_headers.info(data, section_index)?;
section_headers.set_info(
data,
section_index,
permutation.remap(old, "ELF group")? as usize,
)
}
goblin::elf::section_header::SHT_SYMTAB_SHNDX => rewrite_symtab_shndx(
data,
range.start,
range.end,
section.sh_entsize,
symbols,
permutation,
),
_ => Ok(()),
}
}
fn rewrite_relocations(
data: &mut [u8],
offset: usize,
end: usize,
entry_size: u64,
permutation: &SymbolPermutation,
is_64: bool,
little_endian: bool,
) -> Result<()> {
let entry_size = usize::try_from(entry_size)?;
let (info_offset, info_size) = if is_64 { (8, 8) } else { (4, 4) };
if entry_size < info_offset + info_size || (end - offset) % entry_size != 0 {
return Err("invalid ELF relocation entry size".into());
}
for entry in (offset..end).step_by(entry_size) {
let info = read_uint(data, entry + info_offset, info_size, little_endian)?;
let old = if is_64 {
(info >> 32) as usize
} else {
(info >> 8) as usize
};
let new = permutation.remap(old, "ELF relocation")?;
let rewritten = if is_64 {
(new << 32) | (info & 0xffff_ffff)
} else {
(new << 8) | (info & 0xff)
};
write_uint(
data,
entry + info_offset,
info_size,
rewritten,
little_endian,
)?;
}
Ok(())
}
fn rewrite_symtab_shndx(
data: &mut [u8],
offset: usize,
end: usize,
entry_size: u64,
symbols: &ElfSymbolTable,
permutation: &SymbolPermutation,
) -> Result<()> {
if entry_size != 4 || (end - offset) % 4 != 0 || (end - offset) / 4 != symbols.count {
return Err("invalid ELF symbol section-index table".into());
}
let original = data[offset..end].to_vec();
for (new, old) in permutation.old_order.iter().copied().enumerate() {
data[offset + new * 4..offset + (new + 1) * 4]
.copy_from_slice(&original[old * 4..(old + 1) * 4]);
}
Ok(())
}
fn checked_range(
offset: usize,
size: usize,
data_len: usize,
description: &str,
) -> Result<std::ops::Range<usize>> {
let end = offset
.checked_add(size)
.ok_or_else(|| format!("{description} size overflow"))?;
if end > data_len {
return Err(format!("{description} extends past object").into());
}
Ok(offset..end)
}
fn read_uint(data: &[u8], offset: usize, size: usize, little_endian: bool) -> Result<u64> {
let end = offset
.checked_add(size)
.ok_or("ELF integer size overflow")?;
let bytes = data
.get(offset..end)
.ok_or("ELF integer extends past object")?;
Ok(if little_endian {
bytes.iter().enumerate().fold(0, |value, (index, byte)| {
value | (u64::from(*byte) << (index * 8))
})
} else {
bytes
.iter()
.fold(0, |value, byte| (value << 8) | u64::from(*byte))
})
}
fn write_uint(
data: &mut [u8],
offset: usize,
size: usize,
mut value: u64,
little_endian: bool,
) -> Result<()> {
let end = offset
.checked_add(size)
.ok_or("ELF integer size overflow")?;
let bytes = data
.get_mut(offset..end)
.ok_or("ELF integer extends past object")?;
if little_endian {
for byte in bytes {
*byte = value as u8;
value >>= 8;
}
} else {
for byte in bytes.iter_mut().rev() {
*byte = value as u8;
value >>= 8;
}
}
Ok(())
}
fn write_u32(data: &mut [u8], offset: usize, value: u32, little_endian: bool) -> Result<()> {
write_uint(data, offset, 4, u64::from(value), little_endian)
}
fn generate_bindings(vendor: &Path, out: &Path, includes: &[PathBuf]) -> Result<()> {
let mut builder = bindgen::Builder::default()
.header(vendor.join("osslsigncode.h").display().to_string())
.header(vendor.join("helpers.h").display().to_string())
.clang_arg(format!("-I{}", vendor.display()))
.clang_arg(format!("-I{}", out.display()))
.clang_arg("-DHAVE_CONFIG_H=1")
.blocklist_type("BIO").blocklist_type("bio_st")
.blocklist_type("PKCS7").blocklist_type("pkcs7_st")
.blocklist_type("EVP_MD").blocklist_type("evp_md_st")
.blocklist_type("UI_METHOD").blocklist_type("ui_method_st")
.blocklist_type("X509").blocklist_type("x509_st")
.blocklist_type("EVP_PKEY").blocklist_type("evp_pkey_st")
.blocklist_type("X509_CRL").blocklist_type("X509_crl_st")
.blocklist_type("stack_st_X509").blocklist_type("stack_st_X509_CRL")
.raw_line("pub use openssl_sys::{BIO, bio_st, EVP_MD, EVP_PKEY, PKCS7, X509, X509_CRL, stack_st_X509, stack_st_X509_CRL};")
.raw_line("#[allow(unused_imports)] pub use crate::ffi::UI_METHOD;")
.raw_line("pub type pkcs7_st = openssl_sys::PKCS7;")
.raw_line("pub type evp_md_st = openssl_sys::EVP_MD;")
.raw_line("pub type x509_st = openssl_sys::X509;")
.allowlist_type("GLOBAL_OPTIONS")
.allowlist_type("cmd_type_t")
.allowlist_type("FILE_FORMAT")
.allowlist_type("FILE_FORMAT_CTX")
.allowlist_type("stack_st_EngineControl")
.allowlist_type("EngineControl")
.allowlist_function("data_write_pkcs7")
.allowlist_var("file_format_.*")
.default_enum_style(bindgen::EnumVariation::Rust { non_exhaustive: false })
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.layout_tests(false)
.generate_comments(true)
.merge_extern_blocks(true)
.sort_semantically(true);
for include in includes {
builder = builder.clang_arg(format!("-I{}", include.display()));
}
if let Ok(target) = env::var("TARGET") {
builder = builder.clang_arg(format!("--target={target}"));
}
if cfg!(target_os = "macos") {
if let Ok(output) = Command::new("xcrun").args(["--show-sdk-path"]).output() {
if output.status.success() {
let sdk = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if !sdk.is_empty() {
builder = builder.clang_arg("-isysroot").clang_arg(sdk);
}
}
}
}
let bindings = builder.generate().map_err(|e| {
format!("bindgen failed to parse osslsigncode.h ({e}). Ensure libclang is installed.")
})?;
let rendered = bindings.to_string();
for needle in [
"GLOBAL_OPTIONS",
"cmd_type_t",
"FILE_FORMAT",
"data_write_pkcs7",
"file_format_pe",
] {
if !rendered.contains(needle) {
return Err(format!("bindgen missed expected definition `{needle}`").into());
}
}
fs::write(out.join("bindings.rs"), rendered)?;
Ok(())
}