use std::collections::HashSet;
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");
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(())
}
enum Patch {
Elf(usize),
MachO(usize),
}
fn promote_symbols(path: &Path, want: &[&str]) -> Result<HashSet<String>> {
let mut data = fs::read(path)?;
let targets: HashSet<&str> = want.iter().copied().collect();
let mut patches = Vec::new();
let mut promoted = HashSet::new();
{
match goblin::Object::parse(&data)? {
goblin::Object::Elf(elf) => {
let shdr = elf
.section_headers
.iter()
.find(|s| s.sh_type == goblin::elf::section_header::SHT_SYMTAB)
.ok_or("ELF missing symtab")?;
let entsize = shdr.sh_entsize as usize;
let base = shdr.sh_offset as usize;
for (index, sym) in elf.syms.iter().enumerate() {
if let Some(name) = elf.strtab.get_at(sym.st_name) {
let clean_name = name.trim_start_matches('_');
if targets.contains(clean_name) {
let info_off = base + index * entsize + if elf.is_64 { 4 } else { 12 };
patches.push(Patch::Elf(info_off));
promoted.insert(clean_name.to_owned());
}
}
}
}
goblin::Object::Mach(goblin::mach::Mach::Binary(obj)) => {
let symoff = obj
.load_commands
.iter()
.find_map(|lc| match lc.command {
goblin::mach::load_command::CommandVariant::Symtab(cmd) => {
Some(cmd.symoff as usize)
}
_ => None,
})
.ok_or("Mach-O missing LC_SYMTAB")?;
let nlist_size = if obj.is_64 { 16 } else { 12 };
let symbols = obj.symbols.as_ref().ok_or("Mach-O missing symbols")?;
for (index, sym) in symbols.into_iter().enumerate() {
if let Ok((name, _)) = sym {
let clean_name = name.trim_start_matches('_');
if targets.contains(clean_name) {
let n_type_off = symoff + index * nlist_size + 4;
patches.push(Patch::MachO(n_type_off));
promoted.insert(clean_name.to_owned());
}
}
}
}
_ => return Err("Unsupported object format for symbol promotion".into()),
}
}
for patch in patches {
match patch {
Patch::Elf(off) => {
let typ = data[off] & 0x0f;
data[off] = (1u8 << 4) | typ; }
Patch::MachO(off) => {
data[off] |= 0x01; }
}
}
for name in want {
if !promoted.contains(*name) {
println!(
"cargo:warning=could not promote `{name}` in {} (may already be global)",
path.display()
);
}
}
fs::write(path, &data)?;
Ok(promoted)
}
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(())
}