use std::cell::RefCell;
use std::ffi::{CStr, CString, NulError, OsStr};
use std::os::raw::{c_char, c_int};
use std::path::{Path, PathBuf};
use std::ptr;
use std::sync::{Mutex, Once};
use openssl::hash::MessageDigest;
use openssl::nid::Nid;
use openssl_sys::CRYPTO_malloc;
use crate::credential::{Credential, Secret};
use crate::digest::{Digest, JpLevel};
use crate::error::Error;
use crate::format::AuthenticodeOptions;
use crate::policy::{Network, Timestamp};
use crate::sys::{GLOBAL_OPTIONS, cmd_type_t, engine_control_set, free_options};
#[inline]
fn c_bool(value: bool) -> c_int {
c_int::from(value)
}
pub(crate) static RUN_LOCK: Mutex<()> = Mutex::new(());
const MAX_TS_SERVERS: usize = 256;
const INVALID_TIME: i64 = -1;
thread_local! {
static LAST_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
}
static OPENSSL_READY: Once = Once::new();
#[cfg(not(windows))]
const DEFAULT_CA_FILES: &[&str] = &[
"/etc/ssl/certs/ca-certificates.crt",
"/etc/pki/tls/certs/ca-bundle.crt",
"/usr/share/ssl/certs/ca-bundle.crt",
"/usr/local/share/certs/ca-root-nss.crt",
"/etc/ssl/cert.pem",
];
#[derive(Clone, Debug)]
pub(crate) struct NativeJob {
pub cmd: cmd_type_t,
pub digest: Digest,
pub input: PathBuf,
pub output: Option<PathBuf>,
pub signature: Option<PathBuf>,
pub credential: Option<Credential>,
pub additional_certs: Option<PathBuf>,
pub timestamps: Vec<Timestamp>,
pub network: Network,
pub options: AuthenticodeOptions,
pub catalog: Option<PathBuf>,
pub ca_file: Option<PathBuf>,
pub crl_file: Option<PathBuf>,
pub tsa_ca: Option<PathBuf>,
pub tsa_crl: Option<PathBuf>,
pub leafhash: Option<String>,
pub index: Option<i32>,
pub time: Option<i64>,
pub ignore_timestamp: bool,
pub ignore_cdp: bool,
pub ignore_crl: bool,
pub verbose: bool,
pub no_legacy: bool,
}
impl NativeJob {
pub(crate) fn new(cmd: cmd_type_t, input: PathBuf) -> Self {
Self {
cmd,
digest: Digest::Sha256,
input,
output: None,
signature: None,
credential: None,
additional_certs: None,
timestamps: Vec::new(),
network: Network::default(),
options: AuthenticodeOptions::default(),
catalog: None,
ca_file: None,
crl_file: None,
tsa_ca: None,
tsa_crl: None,
leafhash: None,
index: None,
time: None,
ignore_timestamp: false,
ignore_cdp: false,
ignore_crl: false,
verbose: false,
no_legacy: false,
}
}
pub(crate) fn run(self, operation: &'static str) -> Result<(), Error> {
require_readable(&self.input, "input")?;
if let Some(credential) = &self.credential {
require_credential(credential)?;
}
if let Some(signature) = &self.signature {
require_readable(signature, "signature")?;
}
if let Some(catalog) = &self.catalog {
require_readable(catalog, "catalog")?;
}
let mut arena = Arena::new();
let mut prepared = PreparedOptions {
options: unsafe { std::mem::zeroed() },
ran: false,
};
apply_job(&mut prepared.options, &mut arena, &self)?;
let _guard = RUN_LOCK.lock().unwrap_or_else(|error| error.into_inner());
init_openssl()?;
LAST_ERROR.with(|slot| slot.borrow_mut().clear());
let status = unsafe { crate::drive::run(&mut prepared.options) };
prepared.ran = true;
if status == 0 {
Ok(())
} else {
Err(crate::error::failed(operation, status, last_error()))
}
}
}
struct PreparedOptions {
options: GLOBAL_OPTIONS,
ran: bool,
}
impl Drop for PreparedOptions {
fn drop(&mut self) {
if !self.ran {
unsafe { free_options(&mut self.options) };
}
}
}
pub(crate) fn last_error() -> Option<String> {
LAST_ERROR.with(|slot| {
let message = slot.borrow();
(!message.is_empty()).then(|| message.clone())
})
}
pub(crate) fn set_last_error(message: impl Into<String>) {
LAST_ERROR.with(|slot| *slot.borrow_mut() = message.into());
}
fn init_openssl() -> Result<(), Error> {
let mut failed = None;
OPENSSL_READY.call_once(|| {
openssl::init();
const OIDS: &[(&str, &str, &str)] = &[
(
"1.3.6.1.4.1.311.2.1.11",
"spcStatementType",
"spcStatementType",
),
("1.3.6.1.4.1.311.15.1", "msJavaSomething", "msJavaSomething"),
("1.3.6.1.4.1.311.2.1.12", "spcSpOpusInfo", "spcSpOpusInfo"),
(
"1.3.6.1.4.1.311.2.4.1",
"spcNestedSignature",
"spcNestedSignature",
),
(
"1.3.6.1.4.1.42921.1.2.1",
"spcUnauthenticatedData",
"spcUnauthenticatedData",
),
("1.3.6.1.4.1.311.3.3.1", "spcRfc3161", "spcRfc3161"),
(
"1.2.840.113549.1.9.25.4",
"pkcs9SequenceNumber",
"pkcs9SequenceNumber",
),
];
for &(oid, sn, ln) in OIDS {
if Nid::create(oid, sn, ln).is_err() {
failed = Some("failed to create Authenticode OpenSSL objects");
return;
}
}
});
match failed {
Some(message) => Err(Error::Runtime {
message: message.to_owned(),
}),
None => Ok(()),
}
}
struct Arena {
strings: Vec<CString>,
}
impl Arena {
fn new() -> Self {
Self {
strings: Vec::new(),
}
}
fn borrowed(
&mut self,
value: impl AsRef<OsStr>,
field: &'static str,
) -> Result<*mut c_char, Error> {
let cstr = cstring(value.as_ref(), field)?;
let pointer = cstr.as_ptr() as *mut c_char;
self.strings.push(cstr);
Ok(pointer)
}
fn borrowed_opt(
&mut self,
value: Option<impl AsRef<OsStr>>,
field: &'static str,
) -> Result<*mut c_char, Error> {
match value {
Some(value) => self.borrowed(value, field),
None => Ok(ptr::null_mut()),
}
}
}
fn cstring(value: &OsStr, field: &'static str) -> Result<CString, Error> {
os_to_cstring(value).map_err(|source| Error::InvalidCString { field, source })
}
fn apply_job(
options: &mut GLOBAL_OPTIONS,
arena: &mut Arena,
job: &NativeJob,
) -> Result<(), Error> {
options.cmd = job.cmd;
options.md = digest_md(job.digest);
options.time = job.time.unwrap_or(INVALID_TIME);
options.tsa_time = 0;
options.jp = job.options.jp.map(c_int::from).unwrap_or(-1);
options.index = job.index.unwrap_or(-1);
options.nested_number = -1;
options.legacy = c_bool(!job.no_legacy);
options.infile = arena.borrowed(&job.input, "input")?;
options.outfile = arena.borrowed_opt(job.output.as_deref(), "output")?;
options.sigfile = arena.borrowed_opt(job.signature.as_deref(), "signature")?;
options.catalog = arena.borrowed_opt(job.catalog.as_deref(), "catalog")?;
options.leafhash = arena.borrowed_opt(job.leafhash.as_deref(), "leafhash")?;
options.desc = arena.borrowed_opt(job.options.description.as_deref(), "description")?;
options.url = arena.borrowed_opt(job.options.url.as_deref(), "url")?;
options.proxy = arena.borrowed_opt(job.network.proxy.as_deref(), "proxy")?;
options.blob_file = arena.borrowed_opt(job.options.blob.as_deref(), "blob")? as *const c_char;
apply_credential(options, arena, job.credential.as_ref())?;
if job.additional_certs.is_some() {
options.xcertfile =
arena.borrowed_opt(job.additional_certs.as_deref(), "additional_certs")?;
}
apply_timestamps(options, arena, &job.timestamps)?;
options.output_pkcs7 = c_bool(job.options.pem);
options.comm = c_bool(job.options.commercial);
options.pagehash = c_bool(job.options.page_hashes);
options.noverifypeer = c_bool(!job.network.verify_peer);
options.addBlob = c_bool(job.options.blob.is_some());
options.nest = c_bool(job.options.nest);
options.ignore_timestamp = c_bool(job.ignore_timestamp);
options.ignore_cdp = c_bool(job.ignore_cdp);
options.ignore_crl = c_bool(job.ignore_crl);
options.verbose = c_bool(job.verbose);
options.add_msi_dse = c_bool(job.options.msi_dse);
options.cafile = ca_field(job.ca_file.as_deref(), job.cmd, "ca_file")?;
options.https_cafile = ca_field(job.network.https_ca.as_deref(), job.cmd, "https_ca")?;
options.tsa_cafile = ca_field(job.tsa_ca.as_deref(), job.cmd, "tsa_ca")?;
options.crlfile = owned_opt(job.crl_file.as_deref(), "crl_file")?;
options.https_crlfile = owned_opt(job.network.https_crl.as_deref(), "https_crl")?;
options.tsa_crlfile = owned_opt(job.tsa_crl.as_deref(), "tsa_crl")?;
if !matches!(job.cmd, cmd_type_t::CMD_VERIFY) {
if options.outfile.is_null() {
return Err(Error::Runtime {
message: "output path is required".to_owned(),
});
}
let out = unsafe { CStr::from_ptr(options.outfile) }.to_string_lossy();
if Path::new(out.as_ref()).exists() {
return Err(Error::Runtime {
message: "refusing to overwrite existing output file".to_owned(),
});
}
}
if matches!(job.cmd, cmd_type_t::CMD_SIGN)
&& options.pkcs12file.is_null()
&& (options.certfile.is_null() || options.keyfile.is_null())
&& options.p11module.is_null()
&& options.p11engine.is_null()
&& options.provider.is_null()
{
return Err(Error::Runtime {
message: "signing requires a PKCS#12 file, certificate+key, or PKCS#11 credential"
.to_owned(),
});
}
Ok(())
}
fn apply_credential(
options: &mut GLOBAL_OPTIONS,
arena: &mut Arena,
credential: Option<&Credential>,
) -> Result<(), Error> {
let Some(credential) = credential else {
return Ok(());
};
match credential {
Credential::Pkcs12 { path, secret } => {
options.pkcs12file = arena.borrowed(path, "pkcs12")?;
apply_secret(options, arena, secret)?;
}
Credential::CertificateKey {
certificates,
key,
additional,
secret,
} => {
options.certfile = arena.borrowed(certificates, "certificates")?;
options.keyfile = arena.borrowed(key, "key")?;
options.xcertfile = arena.borrowed_opt(additional.as_deref(), "additional_certs")?;
apply_secret(options, arena, secret)?;
}
Credential::Pkcs11(pkcs11) => {
options.p11module = arena.borrowed(&pkcs11.module, "pkcs11module")?;
options.p11cert = arena.borrowed_opt(pkcs11.cert.as_deref(), "pkcs11cert")?;
options.p11engine = arena.borrowed_opt(pkcs11.engine.as_deref(), "engine")?;
options.provider = arena.borrowed_opt(pkcs11.provider.as_deref(), "provider")?;
options.login = c_bool(pkcs11.login);
apply_secret(options, arena, &pkcs11.secret)?;
for ctrl in &pkcs11.engine_ctrls {
let cstr = cstring(OsStr::new(ctrl), "engine_ctrl")?;
let ptr = cstr.into_raw();
unsafe {
engine_control_set(options, ptr);
let _ = CString::from_raw(ptr);
}
}
}
}
Ok(())
}
fn apply_secret(
options: &mut GLOBAL_OPTIONS,
arena: &mut Arena,
secret: &Secret,
) -> Result<(), Error> {
match secret {
Secret::Prompt => options.askpass = 1,
Secret::Stdin => options.readpass = arena.borrowed("-", "readpass")?,
Secret::File(path) => options.readpass = arena.borrowed(path, "readpass")?,
Secret::Value(value) => options.pass = owned(value.as_str(), "password")?,
}
Ok(())
}
fn apply_timestamps(
options: &mut GLOBAL_OPTIONS,
arena: &mut Arena,
timestamps: &[Timestamp],
) -> Result<(), Error> {
let mut authenticode = 0usize;
let mut rfc3161 = 0usize;
for timestamp in timestamps {
match timestamp {
Timestamp::Authenticode(url) => {
if authenticode >= MAX_TS_SERVERS {
return Err(Error::Runtime {
message: "too many Authenticode timestamp URLs".to_owned(),
});
}
options.turl[authenticode] =
arena.borrowed(url.as_str(), "authenticode_timestamp")?;
authenticode += 1;
}
Timestamp::Rfc3161(url) => {
if rfc3161 >= MAX_TS_SERVERS {
return Err(Error::Runtime {
message: "too many RFC-3161 timestamp URLs".to_owned(),
});
}
options.tsurl[rfc3161] = arena.borrowed(url.as_str(), "rfc3161_timestamp")?;
rfc3161 += 1;
}
Timestamp::Authority {
certificates,
key,
unix_time,
} => {
options.tsa_certfile = arena.borrowed(certificates, "tsa_certs")?;
options.tsa_keyfile = arena.borrowed(key, "tsa_key")?;
if let Some(time) = unix_time {
options.tsa_time = *time;
}
}
}
}
options.nturl = authenticode as c_int;
options.ntsurl = rfc3161 as c_int;
Ok(())
}
fn owned(value: impl AsRef<OsStr>, field: &'static str) -> Result<*mut c_char, Error> {
let cstr = cstring(value.as_ref(), field)?;
openssl_dup(cstr.as_bytes_with_nul()).ok_or_else(|| Error::Runtime {
message: format!("out of memory copying {field}"),
})
}
fn owned_opt(value: Option<impl AsRef<OsStr>>, field: &'static str) -> Result<*mut c_char, Error> {
match value {
Some(value) => owned(value, field),
None => Ok(ptr::null_mut()),
}
}
fn ca_field(
path: Option<&Path>,
cmd: cmd_type_t,
field: &'static str,
) -> Result<*mut c_char, Error> {
match path {
Some(path) => owned(path, field),
None if matches!(cmd, cmd_type_t::CMD_SIGN | cmd_type_t::CMD_VERIFY) => {
default_cafile(field)
}
None => Ok(ptr::null_mut()),
}
}
fn default_cafile(field: &'static str) -> Result<*mut c_char, Error> {
#[cfg(windows)]
{
let _ = field;
Ok(ptr::null_mut())
}
#[cfg(not(windows))]
match DEFAULT_CA_FILES
.iter()
.find(|path| Path::new(path).is_file())
{
Some(path) => owned(*path, field),
None => Ok(ptr::null_mut()),
}
}
fn openssl_dup(bytes: &[u8]) -> Option<*mut c_char> {
let copy = unsafe {
CRYPTO_malloc(
bytes.len(),
c"osslsigncode/native.rs".as_ptr(),
line!() as c_int,
)
};
if copy.is_null() {
return None;
}
unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), copy.cast::<u8>(), bytes.len()) };
Some(copy.cast::<c_char>())
}
fn digest_md(digest: Digest) -> *const openssl_sys::EVP_MD {
let md = match digest {
Digest::Md5 => MessageDigest::md5(),
Digest::Sha1 => MessageDigest::sha1(),
Digest::Sha256 => MessageDigest::sha256(),
Digest::Sha384 => MessageDigest::sha384(),
Digest::Sha512 => MessageDigest::sha512(),
};
md.as_ptr()
}
impl From<JpLevel> for c_int {
fn from(level: JpLevel) -> Self {
level as Self
}
}
pub(crate) fn require_readable(path: &Path, field: &'static str) -> Result<(), Error> {
std::fs::metadata(path)
.map(|_| ())
.map_err(|source| crate::error::io(field, path, source))
}
fn require_credential(credential: &Credential) -> Result<(), Error> {
match credential {
Credential::Pkcs12 { path, .. } => require_readable(path, "pkcs12"),
Credential::CertificateKey {
certificates,
key,
additional,
..
} => {
require_readable(certificates, "certificates")?;
require_readable(key, "key")?;
if let Some(additional) = additional {
require_readable(additional, "additional_certs")?;
}
Ok(())
}
Credential::Pkcs11(pkcs11) => require_readable(&pkcs11.module, "pkcs11module"),
}
}
#[cfg(unix)]
fn os_to_cstring(value: &OsStr) -> Result<CString, NulError> {
use std::os::unix::ffi::OsStrExt;
CString::new(value.as_bytes())
}
#[cfg(not(unix))]
fn os_to_cstring(value: &OsStr) -> Result<CString, NulError> {
CString::new(value.to_string_lossy().as_bytes())
}