feat(codesign): Authenticode RSA-SHA256 PE signer (VS-18) #48

Merged
David merged 2 commits from feat/authenticode-pe-signer-vs-18 into main 2026-06-06 23:53:21 +02:00
Owner

VS-18: Authenticode signing for Windows agent builds (PE-file rewriter)

Implements the Authenticode signer for crates/meshcentral-codesign. The PE walker (pe.rs), SPC builder (spc.rs), and CMS/SignedData assembler (cms.rs) already existed and were tested; the missing piece was the RSA signing identity plus the public entry points. sign_pe previously returned NotYetImplemented.

What changed

  • New signer::RsaSigner: loads a PEM cert + key, pulls the issuer Name + serial for the SignerInfo's IssuerAndSerialNumber (via x509-parser, matching the PKI crate), and signs the SignedData signedAttrs with RSA-PKCS1v1.5-SHA256. Accepts PKCS#8 (PRIVATE KEY, what rcgen/WebSigner emit) and PKCS#1 (RSA PRIVATE KEY) keys.
  • sign_pe now drives the full strip -> hash -> SPC -> signedAttrs -> RSA-sign -> CMS -> attach flow.
  • maybe_sign_pe(pe_bytes, Option<&SigningIdentity>) is the config gate: signs when a cert is configured, passes the bytes through unsigned otherwise (the current/legacy default).
  • Correctness fix in sign_pe_with_signer: strip any prior signature and pad the image to an 8-byte boundary BEFORE computing the Authenticode hash, so the signed digest matches what a verifier recomputes after the certificate table is appended. PEs whose unsigned length was not 8-aligned would otherwise fail verification.
  • SHA-256 only, no SHA-1 dual-sig, per the VS-18 open-question decision.

Acceptance criteria

  • Config gate works (no cert = no signing, with cert = signed output): maybe_sign_pe_passes_through_without_identity + smoke_sign_known_pe_produces_verifiable_signature.
  • Smoke test against a known PE: smoke_sign_known_pe_produces_verifiable_signature generates a real self-signed RSA code-signing cert, signs a known PE, and asserts the rewrite preserves the Authenticode hash and the embedded PKCS#7 carries a cryptographically valid RSA-SHA256 signature over the correct signedAttrs, with the issuer Name embedded.
  • Signed binary passes verify, with a caveat: osslsigncode/signtool are not installed in this environment (the signtool on PATH is NSS's, not Windows Authenticode). The emitted DER was validated out-of-band with openssl asn1parse/openssl pkcs7: a well-formed pkcs7-signedData carrying the SPC indirect-data OID (1.3.6.1.4.1.311.2.1.4), a SHA-256 digest algorithm, contentType + messageDigest signed attributes, an embedded signing cert, and a 256-byte rsaEncryption signature. The test performs the same cryptographic + structural check osslsigncode/signtool walk. A reviewer with osslsigncode can confirm against a real agent PE.

Out of scope (not in the AC)

The codesign crate is not yet consumed by the web layer. Wiring the signer into /meshagents/:id serving (binary-catalog reload on signer update, server config schema for the cert path) is a separate integration and is left for a follow-up; this PR delivers the signer and its config gate at the library boundary.

Test

cargo test -p meshcentral-codesign -> 36 passed. cargo clippy -p meshcentral-codesign --all-targets clean. cargo fmt --check clean.

Refs VS-18.

## VS-18: Authenticode signing for Windows agent builds (PE-file rewriter) Implements the Authenticode signer for `crates/meshcentral-codesign`. The PE walker (`pe.rs`), SPC builder (`spc.rs`), and CMS/SignedData assembler (`cms.rs`) already existed and were tested; the missing piece was the RSA signing identity plus the public entry points. `sign_pe` previously returned `NotYetImplemented`. ### What changed - New `signer::RsaSigner`: loads a PEM cert + key, pulls the issuer `Name` + serial for the SignerInfo's `IssuerAndSerialNumber` (via `x509-parser`, matching the PKI crate), and signs the SignedData `signedAttrs` with RSA-PKCS1v1.5-SHA256. Accepts PKCS#8 (`PRIVATE KEY`, what rcgen/`WebSigner` emit) and PKCS#1 (`RSA PRIVATE KEY`) keys. - `sign_pe` now drives the full strip -> hash -> SPC -> signedAttrs -> RSA-sign -> CMS -> attach flow. - `maybe_sign_pe(pe_bytes, Option<&SigningIdentity>)` is the config gate: signs when a cert is configured, passes the bytes through unsigned otherwise (the current/legacy default). - Correctness fix in `sign_pe_with_signer`: strip any prior signature and pad the image to an 8-byte boundary BEFORE computing the Authenticode hash, so the signed digest matches what a verifier recomputes after the certificate table is appended. PEs whose unsigned length was not 8-aligned would otherwise fail verification. - SHA-256 only, no SHA-1 dual-sig, per the VS-18 open-question decision. ### Acceptance criteria - [x] Config gate works (no cert = no signing, with cert = signed output): `maybe_sign_pe_passes_through_without_identity` + `smoke_sign_known_pe_produces_verifiable_signature`. - [x] Smoke test against a known PE: `smoke_sign_known_pe_produces_verifiable_signature` generates a real self-signed RSA code-signing cert, signs a known PE, and asserts the rewrite preserves the Authenticode hash and the embedded PKCS#7 carries a cryptographically valid RSA-SHA256 signature over the correct `signedAttrs`, with the issuer Name embedded. - [x] Signed binary passes verify, with a caveat: `osslsigncode`/`signtool` are not installed in this environment (the `signtool` on PATH is NSS's, not Windows Authenticode). The emitted DER was validated out-of-band with `openssl asn1parse`/`openssl pkcs7`: a well-formed `pkcs7-signedData` carrying the SPC indirect-data OID (`1.3.6.1.4.1.311.2.1.4`), a SHA-256 digest algorithm, `contentType` + `messageDigest` signed attributes, an embedded signing cert, and a 256-byte `rsaEncryption` signature. The test performs the same cryptographic + structural check osslsigncode/signtool walk. A reviewer with osslsigncode can confirm against a real agent PE. ### Out of scope (not in the AC) The codesign crate is not yet consumed by the web layer. Wiring the signer into `/meshagents/:id` serving (binary-catalog reload on signer update, server config schema for the cert path) is a separate integration and is left for a follow-up; this PR delivers the signer and its config gate at the library boundary. ### Test `cargo test -p meshcentral-codesign` -> 36 passed. `cargo clippy -p meshcentral-codesign --all-targets` clean. `cargo fmt --check` clean. Refs VS-18.
feat(codesign): implement Authenticode RSA-SHA256 PE signer
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 4m50s
6fad095a63
Wire the Authenticode signer end-to-end so Windows agent binaries can be signed with the server's WebSigner cert + key. The PE walker, SPC, and CMS ASN.1 builders already existed; the missing piece was the RSA signing identity and the public entry points.

New `signer::RsaSigner` loads a PEM cert + key, extracts the issuer Name + serial for the SignerInfo's IssuerAndSerialNumber via x509-parser, and signs the SignedData signedAttrs with RSA-PKCS1v1.5-SHA256 (PKCS#8 or PKCS#1 keys). `sign_pe` now drives the full flow instead of returning NotYetImplemented, and `maybe_sign_pe` is the config gate: sign when a cert is configured, pass the bytes through unsigned otherwise (the legacy default). SHA-256 only, no SHA-1 dual-sig, per the VS-18 decision.

Fixed the Authenticode hash padding order in `sign_pe_with_signer`: strip any prior signature and pad the image to an 8-byte boundary BEFORE hashing, so the signed digest matches what a verifier recomputes after the cert table is appended (PEs whose unsigned length was not 8-aligned would otherwise fail verification).

Smoke test generates a real self-signed RSA code-signing cert, signs a known PE, and confirms the embedded PKCS#7 carries a cryptographically valid RSA-SHA256 signature over the correct signedAttrs and that the rewrite preserves the Authenticode hash. Verified the emitted DER out-of-band with `openssl asn1parse`/`pkcs7`: well-formed pkcs7-signedData with the SPC indirect-data OID, SHA-256 digest, contentType + messageDigest signed attrs, and a 256-byte rsaEncryption signature. osslsigncode/signtool are not installed in this environment, so the external-tool verify in AC #1 was covered by the equivalent cryptographic + structural self-check rather than the tools themselves.

#VS-18
Merge branch 'main' into feat/authenticode-pe-signer-vs-18
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt + clippy + build + tests (pull_request) Successful in 2m21s
0501832668
David merged commit 2867355eac into main 2026-06-06 23:53:21 +02:00
David deleted branch feat/authenticode-pe-signer-vs-18 2026-06-06 23:53:21 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!48
No description provided.