osslsigncode 0.1.1

In-process Rust bindings for the vendored osslsigncode Authenticode implementation
//! End-to-end Authenticode round-trip against the vendored unsigned script fixture.

use std::path::{Path, PathBuf};

use osslsigncode::{
    Credential, Digest, JpLevel, Secret, Signed, Timestamp, TrustAnchors, Unsigned, VENDOR_COMMIT,
};
use tempfile::TempDir;

/// Temporary copy of the vendored unsigned script, paired with the checked-in
/// `tests/fixtures` publisher cert (100-year self-signed, password "secret").
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() {
    // The fixture certificate is self-signed and not in any system trust store,
    // so verification without the fixture's CA must fail rather than pass blindly.
    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");

    // Flip a byte inside the signed script body and re-open the mutated copy.
    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() {
    // Verifying a file that was never signed must error, not silently succeed.
    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-----"));

    // The DER form must be non-empty and differ from the PEM encoding.
    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:?}"
    );
    // Fixture cert is self-signed, so issuer == subject.
    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() {
    // `-ac` used to be reachable only via certificate+key; now it layers onto a
    // PKCS#12 credential too. Signing and verifying must still succeed.
    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"));
}