Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 111 additions & 17 deletions src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use psign_codesigning_rest::{
};
use psign_opc_sign::{nuget, opc, vsix};
use psign_sip_digest::timestamp::{build_timestamp_request_bytes, parse_time_stamp_resp_der};
use psign_sip_digest::{pe_digest, pe_embed, pkcs7, rdp};
use psign_sip_digest::{pe_digest, pe_embed, pkcs7, rdp, verify_pe};
use rsa::signature::{SignatureEncoding as _, Signer as _, hazmat::PrehashSigner as _};
use serde::{Deserialize, Serialize};
use sha2::Digest as _;
Expand Down Expand Up @@ -235,11 +235,16 @@ fn execute_code_plan(args: &CodeArgs, plan: &CodePlan) -> Result<CommandOutput>
display_path(&output)
))
} else {
let signed =
sign_pe_bytes(&input_bytes, &node.path, &signer, signing_digest, false)
.with_context(|| {
format!("sign Authenticode payload {}", input.display())
})?;
let signed = sign_pe_bytes(
&input_bytes,
&node.path,
&signer,
signing_digest,
false,
args.timestamp_url.as_deref(),
args.timestamp_digest,
)
.with_context(|| format!("sign Authenticode payload {}", input.display()))?;
std::fs::write(&output, signed).with_context(|| {
format!("write signed Authenticode payload {}", output.display())
})?;
Expand Down Expand Up @@ -503,11 +508,15 @@ fn execute_code_plan(args: &CodeArgs, plan: &CodePlan) -> Result<CommandOutput>
ensure_parent_dir(&output)?;
let input_bytes =
std::fs::read(&input).with_context(|| format!("read {}", input.display()))?;
let signed =
sign_clickonce_deploy_bytes(&input_bytes, &node.path, &signer, signing_digest)
.with_context(|| {
format!("sign ClickOnce deploy payload {}", input.display())
})?;
let signed = sign_clickonce_deploy_bytes(
&input_bytes,
&node.path,
&signer,
signing_digest,
args.timestamp_url.as_deref(),
args.timestamp_digest,
)
.with_context(|| format!("sign ClickOnce deploy payload {}", input.display()))?;
std::fs::write(&output, signed).with_context(|| {
format!("write signed ClickOnce deploy payload {}", output.display())
})?;
Expand Down Expand Up @@ -966,12 +975,27 @@ fn sign_nested_package_entries(
}
CodeFormat::Deploy => entry_updates.push(ZipEntryUpdate {
name,
bytes: sign_clickonce_deploy_bytes(&bytes, &nested_label, signer, signing_digest)?,
bytes: sign_clickonce_deploy_bytes(
&bytes,
&nested_label,
signer,
signing_digest,
timestamp_url,
timestamp_digest,
)?,
compression,
}),
CodeFormat::Pe | CodeFormat::Winmd => entry_updates.push(ZipEntryUpdate {
name,
bytes: sign_pe_bytes(&bytes, &nested_label, signer, signing_digest, skip_signed)?,
bytes: sign_pe_bytes(
&bytes,
&nested_label,
signer,
signing_digest,
skip_signed,
timestamp_url,
timestamp_digest,
)?,
compression,
}),
CodeFormat::Msix
Expand Down Expand Up @@ -1036,6 +1060,8 @@ fn sign_clickonce_deploy_bytes(
label: &str,
signer: &CodeSigner,
signing_digest: pkcs7::AuthenticodeSigningDigest,
timestamp_url: Option<&str>,
timestamp_digest: Option<DigestAlgorithm>,
) -> Result<Vec<u8>> {
if signing_digest != pkcs7::AuthenticodeSigningDigest::Sha256 {
return Err(anyhow!(
Expand All @@ -1054,8 +1080,16 @@ fn sign_clickonce_deploy_bytes(
"ClickOnce .deploy payload {label} maps to unsupported content name {content_name}"
));
}
sign_pe_bytes(input_bytes, label, signer, signing_digest, false)
.with_context(|| format!("sign ClickOnce .deploy PE payload {label}"))
sign_pe_bytes(
input_bytes,
label,
signer,
signing_digest,
false,
timestamp_url,
timestamp_digest,
)
.with_context(|| format!("sign ClickOnce .deploy PE payload {label}"))
}

fn clickonce_deploy_content_name(label: &str) -> Option<String> {
Expand All @@ -1080,16 +1114,76 @@ fn sign_pe_bytes(
signer: &CodeSigner,
signing_digest: pkcs7::AuthenticodeSigningDigest,
skip_signed: bool,
timestamp_url: Option<&str>,
timestamp_digest: Option<DigestAlgorithm>,
) -> Result<Vec<u8>> {
if skip_signed && pe_has_signature(input_bytes) {
return Ok(input_bytes.to_vec());
}
if signing_digest != pkcs7::AuthenticodeSigningDigest::Sha256 {
return Err(anyhow!("PE/WinMD signing currently supports only SHA-256"));
}
signer
let signed = signer
.sign_pe_bytes(input_bytes, signing_digest)
.with_context(|| format!("sign PE/WinMD payload {label}"))
.with_context(|| format!("sign PE/WinMD payload {label}"))?;
timestamp_pe_if_requested(&signed, timestamp_url, timestamp_digest)
.with_context(|| format!("timestamp PE/WinMD payload {label}"))
}

fn timestamp_pe_if_requested(
pe_image: &[u8],
timestamp_url: Option<&str>,
timestamp_digest: Option<DigestAlgorithm>,
) -> Result<Vec<u8>> {
match (timestamp_url, timestamp_digest) {
(Some(url), Some(digest)) => timestamp_pe_rfc3161(pe_image, url, digest),
(Some(_), None) => Err(anyhow!(
"`psign-tool code` requires --timestamp-digest with --timestamp-url"
)),
(None, Some(_)) => Err(anyhow!(
"`psign-tool code` requires --timestamp-url with --timestamp-digest"
)),
(None, None) => Ok(pe_image.to_vec()),
}
}

#[cfg(feature = "timestamp-http")]
fn timestamp_pe_rfc3161(
pe_image: &[u8],
timestamp_url: &str,
timestamp_digest: DigestAlgorithm,
) -> Result<Vec<u8>> {
let pkcs7_count = verify_pe::pe_pkcs7_signed_data_entry_count(pe_image)
.context("inspect PE/WinMD Authenticode PKCS#7 rows")?;
let pkcs7_index = pkcs7_count
.checked_sub(1)
.ok_or_else(|| anyhow!("PE/WinMD has no PKCS#7 Authenticode row to timestamp"))?;
let pkcs7_der = verify_pe::pe_nth_pkcs7_signed_data_der(pe_image, pkcs7_index)
.with_context(|| format!("extract PE/WinMD PKCS#7 row {pkcs7_index}"))?;
let stamped_pkcs7 = timestamp_pkcs7_der_rfc3161(
&pkcs7_der,
timestamp_url,
timestamp_digest,
Rfc3161TimestampAttribute::MicrosoftAuthenticode,
)
.with_context(|| format!("timestamp PE/WinMD PKCS#7 row {pkcs7_index}"))?;
pe_embed::pe_replace_authenticode_pkcs7_certificate_at(
pe_image.to_vec(),
pkcs7_index,
&stamped_pkcs7,
)
.with_context(|| format!("replace PE/WinMD PKCS#7 row {pkcs7_index}"))
}

#[cfg(not(feature = "timestamp-http"))]
fn timestamp_pe_rfc3161(
_pe_image: &[u8],
_timestamp_url: &str,
_timestamp_digest: DigestAlgorithm,
) -> Result<Vec<u8>> {
Err(anyhow!(
"`psign-tool code` RFC3161 timestamping requires the timestamp-http feature"
))
}

#[allow(clippy::too_many_arguments)]
Expand Down
118 changes: 116 additions & 2 deletions tests/code_command.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use assert_cmd::Command;
use predicates::prelude::*;
use psign_opc_sign::nuget;
use psign_sip_digest::pkcs7;
use psign_sip_digest::{pkcs7, verify_pe};
use rand::rngs::OsRng;
use rsa::RsaPrivateKey;
use rsa::pkcs1v15::SigningKey;
Expand Down Expand Up @@ -387,6 +387,59 @@ fn code_signs_top_level_pe_with_local_cert_key() {
.success();
}

#[cfg(all(feature = "timestamp-server", feature = "timestamp-http"))]
#[test]
fn code_signs_top_level_pe_with_rfc3161_timestamp() {
let temp = tempfile::tempdir().unwrap();
let base = temp.path();
let input = base.join("app.exe");
let output = base.join("app.timestamped.exe");
let cert = base.join("signer.der");
let key = base.join("signer.pkcs8");
write_test_rsa_cert_key(&cert, &key);
std::fs::copy(
repo_root().join("tests/fixtures/pe-authenticode-upstream/tiny32.efi"),
&input,
)
.unwrap();
let (mut guard, timestamp_url) = spawn_timestamp_server();

let mut cmd = psign();
cmd.args(["code", "--base-directory"])
.arg(base)
.args([
"--cert",
cert.to_str().unwrap(),
"--key",
key.to_str().unwrap(),
"--timestamp-url",
&timestamp_url,
"--timestamp-digest",
"sha256",
"--output",
])
.arg(&output)
.arg("app.exe");
cmd.assert().success();
let status = guard.0.wait().expect("timestamp server exit");
assert!(status.success(), "timestamp server failed with {status}");

let mut verify = psign();
verify
.args(["portable", "verify-pe"])
.arg(&output)
.assert()
.success();

let pkcs7_der =
verify_pe::pe_nth_pkcs7_signed_data_der(&std::fs::read(&output).unwrap(), 0).unwrap();
assert_pkcs7_has_unsigned_attr(
&pkcs7_der,
pkcs7::MS_RFC3161_TIMESTAMP_TOKEN_OID,
pkcs7::PKCS9_RFC3161_TIMESTAMP_TOKEN_OID,
);
}

#[cfg(all(feature = "timestamp-server", feature = "azure-kv-sign"))]
#[test]
fn code_signs_pe_with_azure_key_vault_identity() {
Expand Down Expand Up @@ -1780,6 +1833,61 @@ fn code_signs_nupkg_with_rfc3161_timestamp() {
.stdout(predicate::str::contains("1.2.840.113549.1.9.16.2.14"));
}

#[cfg(all(feature = "timestamp-server", feature = "timestamp-http"))]
#[test]
fn code_signs_nupkg_nested_pe_with_rfc3161_timestamp() {
let temp = tempfile::tempdir().unwrap();
let base = temp.path();
let input = base.join("with-pe.nupkg");
let output = base.join("with-pe.timestamped.nupkg");
let cert = base.join("signer.der");
let key = base.join("signer.pkcs8");
let nested_pe = base.join("tiny32.timestamped.dll");
write_test_rsa_cert_key(&cert, &key);
std::fs::copy(
repo_root().join("tests/fixtures/package-signing/unsigned/with-pe.nupkg"),
&input,
)
.unwrap();
let (mut guard, timestamp_url) = spawn_timestamp_server_with_max_requests(2);

let mut cmd = psign();
cmd.args(["code", "--base-directory"])
.arg(base)
.args([
"--cert",
cert.to_str().unwrap(),
"--key",
key.to_str().unwrap(),
"--timestamp-url",
&timestamp_url,
"--timestamp-digest",
"sha256",
"--output",
])
.arg(&output)
.arg("with-pe.nupkg");
cmd.assert().success();
let status = guard.0.wait().expect("timestamp server exit");
assert!(status.success(), "timestamp server failed with {status}");

let signature_der = nuget::extract_signature_path(&output).expect("extract NuGet signature");
assert_pkcs7_has_unsigned_attr(
&signature_der,
pkcs7::PKCS9_RFC3161_TIMESTAMP_TOKEN_OID,
pkcs7::MS_RFC3161_TIMESTAMP_TOKEN_OID,
);

extract_zip_entry(&output, "lib/net8.0/tiny32.dll", &nested_pe);
let nested_pkcs7 =
verify_pe::pe_nth_pkcs7_signed_data_der(&std::fs::read(&nested_pe).unwrap(), 0).unwrap();
assert_pkcs7_has_unsigned_attr(
&nested_pkcs7,
pkcs7::MS_RFC3161_TIMESTAMP_TOKEN_OID,
pkcs7::PKCS9_RFC3161_TIMESTAMP_TOKEN_OID,
);
}

#[test]
fn code_rejects_zero_max_concurrency() {
let mut cmd = psign();
Expand Down Expand Up @@ -2378,15 +2486,21 @@ impl Drop for PsignServerGuard {

#[cfg(all(feature = "timestamp-server", feature = "timestamp-http"))]
fn spawn_timestamp_server() -> (PsignServerGuard, String) {
spawn_timestamp_server_with_max_requests(1)
}

#[cfg(all(feature = "timestamp-server", feature = "timestamp-http"))]
fn spawn_timestamp_server_with_max_requests(max_requests: u64) -> (PsignServerGuard, String) {
let mut server_cmd = std::process::Command::new(assert_cmd::cargo::cargo_bin("psign-server"));
let max_requests = max_requests.to_string();
server_cmd.args([
"timestamp-server",
"--listen",
"127.0.0.1:0",
"--gen-time",
"20240102030405Z",
"--max-requests",
"1",
max_requests.as_str(),
]);
server_cmd.stdout(std::process::Stdio::piped());
server_cmd.stderr(std::process::Stdio::piped());
Expand Down