use std::path::{Path, PathBuf};
use osslsigncode::{
Credential, Digest, JpLevel, Secret, Signed, Timestamp, TrustAnchors, Unsigned, VENDOR_COMMIT,
};
use tempfile::TempDir;
struct Fixture {
dir: TempDir,
input: PathBuf,
pkcs12: PathBuf,
ca: PathBuf,
}
impl Fixture {
fn new() -> Self {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let dir = TempDir::new().expect("tempdir");
let input = dir.path().join("app.js");
std::fs::copy(
root.join("vendor/osslsigncode/tests/files/unsigned.js"),
&input,
)
.expect("copy unsigned.js");
Self {
dir,
input,
pkcs12: root.join("tests/fixtures/publisher.p12"),
ca: root.join("tests/fixtures/ca.pem"),
}
}
fn credential(&self) -> Credential {
Credential::pkcs12(&self.pkcs12, Secret::value("secret"))
}
fn sign(&self, output_name: &str) -> Signed {
Unsigned::open(&self.input)
.unwrap()
.sign(self.credential())
.digest(Digest::Sha256)
.description("test")
.output(self.dir.path().join(output_name))
.sign()
.expect("sign")
}
fn trust(&self) -> TrustAnchors {
TrustAnchors {
ca_file: Some(self.ca.clone()),
..TrustAnchors::default()
}
}
}
fn read_all(mut reader: impl std::io::Read) -> Vec<u8> {
let mut buf = Vec::new();
reader
.read_to_end(&mut buf)
.expect("read extracted content");
buf
}
#[test]
fn vendor_commit_is_git_sha() {
assert!(VENDOR_COMMIT.len() >= 7);
assert!(VENDOR_COMMIT.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn sign_verify_extract_strip_roundtrip() {
let fx = Fixture::new();
let signed = fx.sign("signed.js");
assert!(signed.path().is_file());
signed
.verify()
.trust(fx.trust())
.ignore_timestamp()
.check()
.expect("verify");
let signature = read_all(
signed
.extract_signature()
.pem()
.reader()
.expect("extract signature"),
);
assert!(signature.starts_with(b"-----BEGIN"));
let data = read_all(signed.extract_data().reader().expect("extract data"));
assert!(!data.is_empty());
let stripped = signed
.strip()
.output(fx.dir.path().join("stripped.js"))
.strip()
.expect("strip");
assert_eq!(
std::fs::read(stripped.path()).unwrap(),
std::fs::read(&fx.input).unwrap()
);
}
#[test]
fn attach_signature_roundtrip() {
let fx = Fixture::new();
let signed = fx.sign("attach-src.js");
let sig_path = fx.dir.path().join("detached.pem");
let mut sig_file = std::fs::File::create(&sig_path).expect("create sig file");
std::io::copy(
&mut signed
.extract_signature()
.pem()
.reader()
.expect("extract sig"),
&mut sig_file,
)
.expect("write sig");
let attach_in = fx.dir.path().join("attach-in.js");
std::fs::copy(&fx.input, &attach_in).unwrap();
let attached = Unsigned::open(&attach_in)
.unwrap()
.attach_signature(&sig_path)
.digest(Digest::Sha256)
.output(fx.dir.path().join("attached.js"))
.attach()
.expect("attach");
attached
.verify()
.trust(TrustAnchors {
ca_file: Some(fx.ca.clone()),
..TrustAnchors::default()
})
.ignore_timestamp()
.check()
.expect("verify attached");
}
#[test]
fn verify_rejects_untrusted_signer() {
let fx = Fixture::new();
let signed = fx.sign("untrusted.js");
let result = signed.verify().ignore_timestamp().check();
assert!(
result.is_err(),
"verification should fail without the signer's CA, got {result:?}"
);
}
#[test]
fn verify_rejects_tampered_payload() {
let fx = Fixture::new();
let signed = fx.sign("tamper-src.js");
let mut bytes = std::fs::read(signed.path()).expect("read signed file");
let index = bytes.len() / 4;
bytes[index] ^= 0xff;
let tampered_path = fx.dir.path().join("tampered.js");
std::fs::write(&tampered_path, &bytes).expect("write tampered file");
let tampered = Signed::open(&tampered_path).expect("open tampered file");
let result = tampered
.verify()
.trust(fx.trust())
.ignore_timestamp()
.check();
assert!(
result.is_err(),
"verification should fail on a tampered payload, got {result:?}"
);
}
#[test]
fn verify_rejects_unsigned_input() {
let fx = Fixture::new();
let unsigned = Signed::open(&fx.input).expect("open unsigned input");
let result = unsigned
.verify()
.trust(fx.trust())
.ignore_timestamp()
.check();
assert!(
result.is_err(),
"unsigned input must not verify, got {result:?}"
);
}
#[test]
fn extracted_signature_is_pkcs7_pem_and_reattaches() {
let fx = Fixture::new();
let signed = fx.sign("extract-src.js");
let pem = read_all(
signed
.extract_signature()
.pem()
.reader()
.expect("extract PEM"),
);
let text = String::from_utf8(pem).expect("PEM is ASCII");
assert!(
text.contains("-----BEGIN PKCS7-----"),
"unexpected PEM:\n{text}"
);
assert!(text.contains("-----END PKCS7-----"));
let der = read_all(signed.extract_signature().reader().expect("extract DER"));
assert!(!der.is_empty());
assert_ne!(der, text.into_bytes());
}
#[test]
fn inspect_reports_signer_identity() {
let fx = Fixture::new();
let signed = fx.sign("inspect-src.js");
let info = signed.inspect().expect("inspect");
assert_eq!(info.signers.len(), 1);
let signer = &info.signers[0];
assert!(
signer.subject.contains("CN=osslsigncode-doctest"),
"{signer:?}"
);
assert_eq!(signer.issuer, signer.subject);
assert!(!signer.serial.is_empty());
assert!(!signer.not_before.is_empty() && !signer.not_after.is_empty());
assert_eq!(info.digest, Some(Digest::Sha256));
assert!(!info.timestamped, "fixture signs without a timestamp");
}
#[test]
fn inspect_reflects_the_signing_digest() {
let fx = Fixture::new();
let signed = Unsigned::open(&fx.input)
.unwrap()
.sign(fx.credential())
.digest(Digest::Sha512)
.output(fx.dir.path().join("sha512.js"))
.sign()
.expect("sign");
assert_eq!(
signed.inspect().expect("inspect").digest,
Some(Digest::Sha512)
);
}
#[test]
fn additional_certs_work_with_pkcs12() {
let fx = Fixture::new();
let signed = Unsigned::open(&fx.input)
.unwrap()
.sign(fx.credential())
.additional_certs(&fx.ca)
.output(fx.dir.path().join("extra-certs.js"))
.sign()
.expect("sign with -ac and pkcs12");
signed
.verify()
.trust(fx.trust())
.ignore_timestamp()
.check()
.expect("verify");
}
#[test]
fn type_state_builders_are_exhaustive() {
let fx = Fixture::new();
let _ = Unsigned::open(&fx.input)
.unwrap()
.sign(Credential::pkcs12(&fx.pkcs12, Secret::value("secret")))
.digest(Digest::Sha384)
.jp(JpLevel::Low)
.additional_certs("extra.pem")
.page_hashes()
.commercial()
.pem()
.nest()
.verbose()
.no_legacy()
.time(1_700_000_000)
.timestamp(Timestamp::rfc3161("http://timestamp.example"))
.network(osslsigncode::Network {
verify_peer: false,
..osslsigncode::Network::default()
})
.options(osslsigncode::AuthenticodeOptions {
description: Some("opts".into()),
url: Some("https://example.test".into()),
..osslsigncode::AuthenticodeOptions::default()
})
.output(fx.dir.path().join("unused-builder-only.js"));
}