Skip to content
Open
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
2 changes: 1 addition & 1 deletion crates/psign-authenticode-trust/src/trust_verify_pe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pub fn load_trust_material(opts: &TrustVerifyPeOptions) -> Result<(AnchorStore,

if anchor_store.thumbprint_count() == 0 {
return Err(anyhow!(
"no trust anchors configured (use --anchor-dir and/or --authroot-cab)"
"no trust anchors configured (use --anchor-dir, --trusted-ca, --additional-trusted-ca, and/or --authroot-cab)"
));
}

Expand Down
113 changes: 110 additions & 3 deletions crates/psign-digest-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ struct TrustVerifySharedArgs {
/// Trust this CA certificate file as an anchor (repeatable, PEM or DER).
#[arg(long, value_name = "PATH", action = clap::ArgAction::Append)]
trusted_ca: Vec<PathBuf>,
/// Add a CA without replacing the selected or automatically discovered trust anchors
/// (repeatable, PEM or DER).
#[arg(long, value_name = "PATH", action = clap::ArgAction::Append)]
additional_trusted_ca: Vec<PathBuf>,
#[arg(long, value_name = "PATH")]
authroot_cab: Option<PathBuf>,
/// Require **`--authroot-cab`** file SHA-256 (64 lowercase hex chars) to match before ingest.
Expand Down Expand Up @@ -160,7 +164,7 @@ fn trust_verify_options_from_shared(a: &TrustVerifySharedArgs) -> Result<TrustVe
let effective_aia = a.online_aia || authroot_cab.is_some();
Ok(TrustVerifyPeOptions {
anchor_dir: a.anchor_dir.clone(),
trusted_ca_files: a.trusted_ca.clone(),
trusted_ca_files: combined_trusted_ca_files(&a.trusted_ca, &a.additional_trusted_ca),
authroot_cab,
expect_authroot_cab_sha256,
verification_instant_override,
Expand All @@ -187,7 +191,7 @@ fn resolve_authroot_cab_for_shared(a: &TrustVerifySharedArgs) -> Result<Option<P
if let Some(cab) = &a.authroot_cab {
return Ok(Some(cab.clone()));
}
if a.anchor_dir.is_some() || !a.trusted_ca.is_empty() {
if explicit_anchors_replace_automatic_authroot(a) {
return Ok(None);
}
Ok(
Expand All @@ -196,9 +200,112 @@ fn resolve_authroot_cab_for_shared(a: &TrustVerifySharedArgs) -> Result<Option<P
)
}

fn combined_trusted_ca_files(
trusted_ca: &[PathBuf],
additional_trusted_ca: &[PathBuf],
) -> Vec<PathBuf> {
trusted_ca
.iter()
.chain(additional_trusted_ca)
.cloned()
.collect()
}

fn explicit_anchors_replace_automatic_authroot(a: &TrustVerifySharedArgs) -> bool {
a.anchor_dir.is_some() || !a.trusted_ca.is_empty()
}

#[cfg(test)]
mod trust_source_tests {
use super::*;

#[derive(Parser)]
struct SharedTrustArgsParser {
#[command(flatten)]
shared: TrustVerifySharedArgs,
}

fn path(name: &str) -> PathBuf {
PathBuf::from(name)
}

fn parse_shared_args(args: &[&str]) -> TrustVerifySharedArgs {
SharedTrustArgsParser::try_parse_from(
std::iter::once("trust-args").chain(args.iter().copied()),
)
.expect("parse shared trust arguments")
.shared
}

#[test]
fn additional_ca_does_not_replace_automatic_authroot() {
let additional_only = parse_shared_args(&[
"--additional-trusted-ca",
"additional-root.cer",
]);
assert!(!explicit_anchors_replace_automatic_authroot(
&additional_only
));

let with_trusted_ca = parse_shared_args(&[
"--additional-trusted-ca",
"additional-root.cer",
"--trusted-ca",
"explicit-root.cer",
]);
assert!(explicit_anchors_replace_automatic_authroot(
&with_trusted_ca
));

let with_anchor_dir = parse_shared_args(&[
"--additional-trusted-ca",
"additional-root.cer",
"--anchor-dir",
"anchors",
]);
assert!(explicit_anchors_replace_automatic_authroot(
&with_anchor_dir
));
}

#[test]
fn trusted_and_additional_ca_files_are_merged_in_argument_order() {
let trusted_ca = [path("explicit-a.cer"), path("explicit-b.cer")];
let additional_trusted_ca = [path("additional-a.cer"), path("additional-b.cer")];

let merged = combined_trusted_ca_files(&trusted_ca, &additional_trusted_ca);

assert_eq!(
merged,
vec![
path("explicit-a.cer"),
path("explicit-b.cer"),
path("additional-a.cer"),
path("additional-b.cer")
]
);
}

#[test]
fn additional_trusted_ca_is_repeatable_on_trust_commands() {
let args = parse_shared_args(&[
"--additional-trusted-ca",
"test-root-a.cer",
"--additional-trusted-ca",
"test-root-b.cer",
]);

assert_eq!(
args.additional_trusted_ca,
vec![path("test-root-a.cer"), path("test-root-b.cer")]
);
}
}

fn trust_verify_args_present(a: &TrustVerifySharedArgs) -> bool {
a.anchor_dir.is_some()
|| !a.trusted_ca.is_empty()
|| !a.additional_trusted_ca.is_empty()
|| a.authroot_cab.is_some()
|| a.expect_authroot_cab_sha256.is_some()
|| a.as_of.is_some()
Expand Down Expand Up @@ -1913,7 +2020,7 @@ enum Command {
VerifyPe { path: PathBuf },
/// Verify PE Authenticode **trust**: PKCS#7 CMS validation + certificate chain to portable anchors (no OS store).
///
/// Uses the automatic Microsoft AuthRoot CAB cache when no anchors are supplied. Supply **`--anchor-dir`** (Phase A: `.crt`/`.cer`/`.pem`) and/or **`--authroot-cab`** (extract certs + CTL thumbs from AuthRoot-style CAB `.stl` payloads) for explicit trust inputs. **`verify-pe`** remains digest-only; this subcommand adds chain + policy checks.
/// Uses the automatic Microsoft AuthRoot CAB cache when no replacing anchors are supplied. Supply **`--anchor-dir`** (Phase A: `.crt`/`.cer`/`.pem`) and/or **`--authroot-cab`** (extract certs + CTL thumbs from AuthRoot-style CAB `.stl` payloads) for explicit trust inputs, or **`--additional-trusted-ca`** to augment the selected trust set. **`verify-pe`** remains digest-only; this subcommand adds chain + policy checks.
TrustVerifyPe {
path: PathBuf,
#[command(flatten)]
Expand Down
2 changes: 1 addition & 1 deletion docs/authenticode-trust-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This describes how **`crates/psign-authenticode-trust`** composes crates for **L
| PKCS#7 shell, **`SignerInfo`**, authenticated attributes | **`cms`**, **`der`** (via **`authenticode`** / **`picky`**) | Parse **`SignedData`**, locate **`messageDigest`**, carry DER blobs. |
| PE layout, indirect **`SpcIndirectData`**, image digest | **`authenticode`**, **`psign-sip-digest`** | Enumerate embedded PKCS#7 from the PE certificate table; recompute **`pe_authenticode_digest`** for the embedded hash algorithm. |
| CMS Authenticode rules + X.509 chain verification | **`picky`** (`AuthenticodeSignature`, `authenticode_verifier`, `Cert::verifier`) | Validate **`messageDigest`** vs provided digest, signature over authenticated attributes, TBSCertificate signatures along **`issuer_chain`**, Basic Constraints / dates / EKU policy hooks. |
| Trust anchors | This crate (**`anchor`**, **`authroot_cache`**, **`authroot_cab`**, **`authroot_ctl`**) | Phase A: load **`*.crt`** / **`*.cer`** / **`*.pem`** from **`--anchor-dir`** or repeatable **`--trusted-ca`** files. Phase B: automatically cache Microsoft **`authrootstl.cab`** at **`~/.psign/authroot/`** when no explicit anchors are supplied, then parse CAB **`*.stl`** → PKCS#7 **`SignedData`** **`eContent`** CTL **SHA-1 subject identifiers** plus PKCS#7-embedded certs. |
| Trust anchors | This crate (**`anchor`**, **`authroot_cache`**, **`authroot_cab`**, **`authroot_ctl`**) | Phase A: load **`*.crt`** / **`*.cer`** / **`*.pem`** from **`--anchor-dir`** or repeatable **`--trusted-ca`** files. Phase B: automatically cache Microsoft **`authrootstl.cab`** at **`~/.psign/authroot/`** when no replacing explicit anchors are supplied, then parse CAB **`*.stl`** → PKCS#7 **`SignedData`** **`eContent`** CTL **SHA-1 subject identifiers** plus PKCS#7-embedded certs. Repeatable **`--additional-trusted-ca`** files augment either selected trust set without suppressing automatic AuthRoot discovery. |
| Policy knobs | **`policy::AuthenticodeTrustPolicy`** | Default **strict** code-signing EKU; CLI **`allow-loose-signing-cert`**, **`--prefer-timestamp-signing-time`** / **`--require-valid-timestamp`** (see [**Verification instant / timestamps**](#verification-instant--timestamps)), **`--as-of YYYY-MM-DD`** for **`exact_date`**. |
| Portable CLI | **`psign-tool portable`** | **`trust-verify-pe`**, **`trust-verify-cab`**, **`trust-verify-catalog`**, **`trust-verify-detached`** share anchor, AuthRoot CAB/cache, AIA, OCSP, CRL revocation, timestamp-policy, and chain-diagnostic flags; detached uses [`pkcs7_wire::normalize_pkcs7_der_for_authenticode`](../crates/psign-sip-digest/src/pkcs7_wire.rs). **`inspect-authenticode`** emits JSON for PKCS#7 signers, timestamp-related OIDs, and nested signatures (**`1.3.6.1.4.1.311.2.4.1`**). Unified **`psign-tool --mode portable verify`** uses the portable trust commands by default for supported formats when automatic AuthRoot is enabled; **`PSIGN_NO_AUTO_TRUST=1`** restores digest-only routing unless explicit trust inputs are present. |
| CMS inspection (no trust decision) | This crate **`inspect`** | Uses **`cms`** **`SignedData`** + **`authenticode`** digest probe; complements picky **`trust_*`** paths. See [**psa-interoperability.md**](psa-interoperability.md). |
Expand Down
12 changes: 11 additions & 1 deletion docs/authroot-linux-verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ By default, **`psign-tool portable trust-verify-*`** and bare **`psign-tool --mo

Set **`PSIGN_NO_AUTO_TRUST=1`** (also accepts `true` or `yes`) to disable automatic AuthRoot use. Set **`PSIGN_AUTHROOT_MAX_AGE_DAYS=<days>`** to change the staleness window. Advanced/offline environments can set **`PSIGN_AUTHROOT_CACHE_DIR`** or **`PSIGN_AUTHROOT_URL`** for an alternate cache location or mirror. Explicit **`--authroot-cab`**, **`--anchor-dir`**, or repeatable **`--trusted-ca`** inputs take precedence and suppress automatic AuthRoot resolution.

Use repeatable **`--additional-trusted-ca`** inputs when a private or test root should augment, rather than replace, the selected trust set. With no explicit **`--authroot-cab`**, **`--anchor-dir`**, or **`--trusted-ca`**, automatic Microsoft AuthRoot discovery remains enabled. Because every additional certificate becomes a trust anchor for that invocation, obtain it from an authenticated source and validate its expected identity in security-sensitive automation.

## Phase A — anchor directory (recommended first ship)

1. On a Windows machine with updates, sync roots you trust, for example:
Expand Down Expand Up @@ -59,7 +61,15 @@ psign-tool portable trust-verify-pe \
./signed.exe
```

The unified CLI uses the same trust path without writing to the Windows or Linux OS trust store. With **`--mode portable verify`**, supported formats route to the corresponding portable **`trust-verify-*`** command by default when automatic AuthRoot is enabled. Explicit trust inputs such as **`--trusted-ca`**, **`--anchor-dir`**, **`--authroot-cab`**, AIA/OCSP/CRL flags, and timestamp policy flags still route to the same trust commands:
Add a test or private root while retaining automatic Microsoft AuthRoot trust:

```bash
psign-tool portable trust-verify-pe \
--additional-trusted-ca ./downloaded-test-root.cer \
./signed.exe
```

The unified CLI uses the same trust path without writing to the Windows or Linux OS trust store. With **`--mode portable verify`**, supported formats route to the corresponding portable **`trust-verify-*`** command by default when automatic AuthRoot is enabled. Trust inputs such as **`--trusted-ca`**, **`--additional-trusted-ca`**, **`--anchor-dir`**, **`--authroot-cab`**, AIA/OCSP/CRL flags, and timestamp policy flags still route to the same trust commands:

```bash
psign-tool --mode portable verify \
Expand Down
1 change: 1 addition & 0 deletions docs/psign-cli-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
{"native": "(detached sig file)", "rust": "--detached-pkcs7 (--p7s)", "tier": "P0", "status": "implemented", "notes": "Alias p7s for detached PKCS#7 path. In `--mode portable verify`, this routes to one portable CMS/chain validation via `trust-verify-detached` and requires exactly one verify target."},
{"native": "(allow test roots)", "rust": "--allow-test-root (--testroot)", "tier": "P1", "status": "implemented", "notes": "Windows argv /testroot supported"},
{"native": "(portable explicit root)", "rust": "--trusted-ca", "tier": "P1", "status": "implemented", "notes": "Portable trust only; repeatable PEM/DER root files, no OS trust-store writes. In `--mode portable verify`, supported formats route to portable trust by default when automatic AuthRoot is enabled; this flag supplies explicit roots and suppresses auto AuthRoot resolution, including detached PKCS#7 and explicit catalog routing."},
{"native": "(portable additive root)", "rust": "--additional-trusted-ca", "tier": "P1", "status": "implemented", "notes": "Portable trust only; repeatable PEM/DER root files that augment the selected trust set without suppressing automatic Microsoft AuthRoot discovery. Each supplied certificate is trusted as an anchor for that invocation."},
{"native": "(portable anchor directory)", "rust": "--anchor-dir", "tier": "P1", "status": "implemented", "notes": "Portable trust only; loads .crt/.cer/.pem files as anchors without elevation or persistent store changes."},
{"native": "(portable AIA)", "rust": "--online-aia", "tier": "P2", "status": "partial", "notes": "Portable trust only; explicit in-memory HTTP AIA caIssuers fetch for missing issuers. Revocation is available through OCSP/CRL HTTP overrides and CRL Distribution Points."},
{"native": "(portable AIA test override)", "rust": "--aia-url-override", "tier": "P2", "status": "partial", "notes": "Portable trust only; deterministic local test override used before certificate AIA URLs."},
Expand Down