Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Canonical repository: <https://github.com/Devolutions/psign>.
## CLI surface

- `verify`, `remove`, `catdb`: Windows-compatible `signtool.exe` flows backed by WinTrust and CryptSIP where native APIs are required.
- `sign`: Rust mssign32 core (`SignerSignEx3`) with PFX/system-store cert selection, RFC3161 sign-time timestamping, and decoupled-digest bridge flow (`--dlib` or `--trusted-signing-dlib-root` + `--dmdf`) for MSIX parity and [Azure Artifact Signing / Trusted Signing](docs/migration-artifact-signing.md).
- `sign`: Rust mssign32 core (`SignerSignEx3`) with PFX/system-store cert selection, RFC3161 sign-time timestamping, first-class custom ZIP Authenticode support ([docs/zip-authenticode-signing.md](docs/zip-authenticode-signing.md)), and decoupled-digest bridge flow (`--dlib` or `--trusted-signing-dlib-root` + `--dmdf`) for MSIX parity and [Azure Artifact Signing / Trusted Signing](docs/migration-artifact-signing.md).
- `inspect-signature`: JSON dump of PKCS#7 signers, timestamp OIDs, and nested signatures (`1.3.6.1.4.1.311.2.4.1`) — same parser as **`psign-tool portable inspect-authenticode`** ([docs/psa-interoperability.md](docs/psa-interoperability.md)).
- `timestamp`: Rust mssign32 core (`SignerTimeStampEx3`/`SignerTimeStampEx2`) plus AppX restrictions.
- `rdp`: Rust port of **`rdpsign.exe`** for `.rdp` files (`SignScope` / `Signature` records, detached PKCS#7 over the secure-settings blob).
Expand Down Expand Up @@ -86,6 +86,8 @@ cargo build -p psign --bin psign-tool --locked
# psign-tool portable sign-pe --cert cert.der --key key.pk8 --output signed.exe unsigned.exe
# Portable trust verification with explicit anchors:
# psign-tool portable trust-verify-pe signed.exe --anchor-dir anchors
# Portable custom ZIP Authenticode verification:
# psign-tool portable trust-verify-zip archive.zip --anchor-dir anchors
# Portable unsigned CAB signing with a local RSA key:
# psign-tool portable sign-cab --cert cert.der --key key.pk8 --output signed.cab unsigned.cab
# Portable MSI/MSP signing with a local RSA key:
Expand Down
2 changes: 2 additions & 0 deletions crates/psign-authenticode-trust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod trust_verify_detached;
pub mod trust_verify_esd;
pub mod trust_verify_msi;
pub mod trust_verify_pe;
pub mod trust_verify_zip;
pub mod verification_instant;

pub use inspect::{
Expand All @@ -37,4 +38,5 @@ pub use trust_verify_pe::{
TrustVerifyPeOptions, TrustVerifyPeReport, load_trust_material, pe_first_pkcs7_terminal_root,
pkcs7_signed_data_der_terminal_root, trust_verify_pe_bytes,
};
pub use trust_verify_zip::trust_verify_zip_bytes;
pub use verification_instant::parse_verification_date_ymd;
44 changes: 44 additions & 0 deletions crates/psign-authenticode-trust/src/trust_verify_zip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Custom ZIP Authenticode trust (Devolutions ZIP comment convention).

use crate::trust_pkcs7::verify_authenticode_pkcs7_trust;
use crate::trust_verify_pe::{TrustVerifyPeOptions, TrustVerifyPeReport, load_trust_material};
use crate::verification_instant::resolve_verification_instant_for_pkcs7_with_trust;
use anyhow::Result;
use psign_sip_digest::ps_script::powershell_class_digest_report;
use psign_sip_digest::zip_authenticode;

pub fn trust_verify_zip_bytes(
data: &[u8],
opts: &TrustVerifyPeOptions,
) -> Result<TrustVerifyPeReport> {
let (anchors, anchor_certs) = load_trust_material(opts)?;
let sig = zip_authenticode::verify_zip_digest_binding(data)?;
let script = zip_authenticode::signature_script_from_parts(&sig.digest, &sig.pkcs7_base64);
let script_report = powershell_class_digest_report(script.as_bytes(), "ps1")?;

let verification_instant = resolve_verification_instant_for_pkcs7_with_trust(
&script_report.pkcs7_der,
&opts.policy,
opts.verification_instant_override.as_ref(),
&anchors,
&anchor_certs,
&opts.online,
opts.verbose_chain,
)?;
verify_authenticode_pkcs7_trust(
&script_report.pkcs7_der,
0,
&script_report.computed_digest,
&anchors,
&anchor_certs,
&opts.policy,
&opts.online,
&verification_instant,
opts.verbose_chain,
)?;

Ok(TrustVerifyPeReport {
pkcs7_entries_verified: 1,
anchor_thumbprints: anchors.thumbprint_count(),
})
}
26 changes: 26 additions & 0 deletions crates/psign-digest-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use psign_authenticode_trust::{
policy::{OnlineTrustOptions, RevocationMode},
trust_verify_cab_bytes, trust_verify_catalog_bytes, trust_verify_detached_bytes,
trust_verify_msi_bytes, trust_verify_pe_bytes, trust_verify_wim_esd_path,
trust_verify_zip_bytes,
};
#[cfg(feature = "azure-kv-sign-portable")]
use psign_azure_kv_rest::{
Expand Down Expand Up @@ -48,6 +49,7 @@ use psign_sip_digest::timestamp::{
};
use psign_sip_digest::verify_pe;
use psign_sip_digest::verify_script_digest_consistency;
use psign_sip_digest::zip_authenticode;
use serde::Deserialize;
use sha1::Sha1;
use sha2::{Digest as _, Sha256, Sha384, Sha512};
Expand Down Expand Up @@ -596,6 +598,12 @@ enum Command {
#[command(flatten)]
shared: TrustVerifySharedArgs,
},
/// Custom ZIP Authenticode comment signature: verify ZIP digest binding plus PKCS#7 chain to anchors.
TrustVerifyZip {
path: PathBuf,
#[command(flatten)]
shared: TrustVerifySharedArgs,
},
/// Print whether embedded PKCS#7 bytes contain **SPC_PE_IMAGE_PAGE_HASHES** attribute OIDs (V1/V2 DER scan).
///
/// Outputs `yes` or `no` (does **not** validate page segments vs file bytes — use **`verify-pe-page-hashes`** for the experimental Rust check).
Expand Down Expand Up @@ -884,6 +892,8 @@ enum Command {
},
/// CAB with embedded PKCS#7: compare indirect digest to Rust CAB hash.
VerifyCab { path: PathBuf },
/// Custom ZIP Authenticode comment signature: compare ZIP digest binding and reconstructed script digest.
VerifyZip { path: PathBuf },
/// Write **`\\u{5}DigitalSignature`** stream (**raw PKCS#7 DER**) from an **`.msi`** to stdout or **`--output`**.
///
/// Same blob as **`pkcs7-signer-rs256-prehash`** input for that signature. For real signed MSIs only; see **`tests/fixtures/msi-authenticode-upstream/README.md`** for the PKCS#7-only stub used in CI.
Expand Down Expand Up @@ -2088,6 +2098,13 @@ where
.with_context(|| format!("trust-verify-detached {}", content.display()))?;
print_trust_ok("trust-verify-detached", &report);
}
Command::TrustVerifyZip { path, shared } => {
let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
let opts = trust_verify_options_from_shared(&shared)?;
let report = trust_verify_zip_bytes(&bytes, &opts)
.with_context(|| format!("trust-verify-zip {}", path.display()))?;
print_trust_ok("trust-verify-zip", &report);
}
Command::PeHasPageHashes { path } => {
let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
let present = page_hashes::pe_embedded_pkcs7_contains_page_hash_attribute(&bytes)
Expand Down Expand Up @@ -2748,6 +2765,15 @@ where
verify_cab_digest_consistency(&path)
.with_context(|| format!("verify-cab {}", path.display()))?;
}
Command::VerifyZip { path } => {
let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
let sig = zip_authenticode::verify_zip_digest_binding(&bytes)
.with_context(|| format!("verify-zip {}", path.display()))?;
let script =
zip_authenticode::signature_script_from_parts(&sig.digest, &sig.pkcs7_base64);
verify_script_digest_consistency(script.as_bytes(), "ps1")
.with_context(|| format!("verify-zip reconstructed signature {}", path.display()))?;
}
Command::ExtractMsiPkcs7 { path, output } => {
use std::io::Write;
let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
Expand Down
1 change: 1 addition & 0 deletions crates/psign-sip-digest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod rdp;
pub mod timestamp;
pub mod verify_pe;
pub mod wsh_script;
pub mod zip_authenticode;

use anyhow::Result;

Expand Down
44 changes: 44 additions & 0 deletions crates/psign-sip-digest/src/pkcs7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ const SPC_MSI_SIGINFO_VALUE_DER: &[u8] = &[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00,
0x02, 0x01, 0x00, 0x02, 0x01, 0x00,
];
/// Stable PowerShell `SpcSigInfo` value DER observed in `signtool sign` script signatures.
const SPC_SCRIPT_SIGINFO_VALUE_DER: &[u8] = &[
0x30, 0x26, 0x02, 0x03, 0x01, 0x00, 0x00, 0x04, 0x10, 0x1f, 0xcc, 0x3b, 0x60, 0x59, 0x4b, 0x08,
0x4e, 0xb7, 0x24, 0xd2, 0xc6, 0x29, 0x7e, 0xf3, 0x51, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x02,
0x01, 0x00, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00,
];

/// **`SignerInfo.digestAlgorithm`** / **`DigestInfo.digestAlgorithm`** SHA-1 OID.
const DIGEST_OID_SHA1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.14.3.2.26");
Expand Down Expand Up @@ -184,6 +190,21 @@ pub fn msi_spc_indirect_data(
)
}

/// Build the Authenticode `SpcIndirectDataContent` for a PowerShell-class script digest.
pub fn script_spc_indirect_data(
digest_algorithm: AuthenticodeSigningDigest,
script_digest: &[u8],
) -> Result<SpcIndirectDataContent> {
spc_indirect_data(
digest_algorithm,
script_digest,
SPC_MSI_SIGINFO_OBJID,
Any::from_der(SPC_SCRIPT_SIGINFO_VALUE_DER)
.map_err(|e| anyhow!("SPC_SCRIPT_SIGINFO Any: {e}"))?,
"script",
)
}

fn spc_indirect_data(
digest_algorithm: AuthenticodeSigningDigest,
subject_digest: &[u8],
Expand Down Expand Up @@ -277,6 +298,29 @@ pub fn create_pe_authenticode_pkcs7_der_rsa(
)
}

/// Create PKCS#7 `ContentInfo(SignedData)` DER for a PowerShell-class Authenticode script.
pub fn create_script_authenticode_pkcs7_der_rsa(
script: &[u8],
digest_algorithm: AuthenticodeSigningDigest,
signer_cert: Certificate,
chain_certs: Vec<Certificate>,
private_key: RsaPrivateKey,
) -> Result<Vec<u8>> {
let units = crate::ps_script::file_utf16_units(script);
let script_digest = crate::ps_script::hash_payload(
digest_algorithm.pe_hash_kind(),
&crate::ps_script::utf16le_bytes(&units),
)?;
let indirect = script_spc_indirect_data(digest_algorithm, &script_digest)?;
create_authenticode_pkcs7_der_rsa(
indirect,
digest_algorithm,
signer_cert,
chain_certs,
private_key,
)
}

/// Create PKCS#7 `ContentInfo(SignedData)` DER for an Authenticode `SpcIndirectDataContent`.
pub fn create_authenticode_pkcs7_der_rsa(
indirect: SpcIndirectDataContent,
Expand Down
24 changes: 22 additions & 2 deletions crates/psign-sip-digest/src/ps_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ use digest::Digest;
use sha1::Sha1;
use sha2::{Sha256, Sha384, Sha512};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ScriptAuthenticodeDigestReport {
pub pkcs7_der: Vec<u8>,
pub embedded_digest: Vec<u8>,
pub computed_digest: Vec<u8>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MarkerFamily {
Hash,
Expand Down Expand Up @@ -142,7 +149,7 @@ pub(crate) fn hash_payload(kind: PeAuthenticodeHashKind, payload: &[u8]) -> Resu
}

fn looks_like_b64_token(s: &str) -> bool {
s.len() >= 16
s.len() >= 4
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
}
Expand Down Expand Up @@ -236,7 +243,10 @@ pub fn is_wsh_extension(ext: &str) -> bool {
}

/// Compare PKCS#7 indirect digest with a heuristic UTF-16 hash over the file excluding the sig block.
pub(crate) fn verify_powershell_class_digest(raw: &[u8], ext: &str) -> Result<()> {
pub fn powershell_class_digest_report(
raw: &[u8],
ext: &str,
) -> Result<ScriptAuthenticodeDigestReport> {
let ext_l = ext.to_ascii_lowercase();
if !extension_supported(&ext_l) {
return Err(anyhow!(
Expand All @@ -261,6 +271,16 @@ pub(crate) fn verify_powershell_class_digest(raw: &[u8], ext: &str) -> Result<()
"script Authenticode digest mismatch (experimental UTF-16 strip heuristic vs PKCS#7)"
));
}
Ok(ScriptAuthenticodeDigestReport {
pkcs7_der: pkcs7,
embedded_digest: embedded.to_vec(),
computed_digest: computed,
})
}

/// Compare PKCS#7 indirect digest with a heuristic UTF-16 hash over the file excluding the sig block.
pub(crate) fn verify_powershell_class_digest(raw: &[u8], ext: &str) -> Result<()> {
powershell_class_digest_report(raw, ext)?;
Ok(())
}

Expand Down
Loading