diff --git a/crates/psign-digest-cli/src/main.rs b/crates/psign-digest-cli/src/main.rs index ea548b7..531158d 100644 --- a/crates/psign-digest-cli/src/main.rs +++ b/crates/psign-digest-cli/src/main.rs @@ -3539,37 +3539,6 @@ fn artifact_signing_params_for_digest( }) } -#[cfg(feature = "artifact-signing-rest")] -fn parse_artifact_signing_certificates(bytes: &[u8]) -> Result<(x509_cert::Certificate, Vec)> { - if let Ok(text) = std::str::from_utf8(bytes) - && text.contains("-----BEGIN CERTIFICATE-----") - { - let mut certs = Vec::new(); - let mut rest = text; - while let Some(start) = rest.find("-----BEGIN CERTIFICATE-----") { - rest = &rest[start..]; - let Some(end) = rest.find("-----END CERTIFICATE-----") else { - return Err(anyhow!("unterminated PEM certificate in Artifact Signing signingCertificate")); - }; - let end = end + "-----END CERTIFICATE-----".len(); - certs.push( - rdp::parse_certificate(&rest.as_bytes()[..end]) - .context("parse Artifact Signing PEM certificate")?, - ); - rest = &rest[end..]; - } - let mut iter = certs.into_iter(); - let signer = iter - .next() - .ok_or_else(|| anyhow!("Artifact Signing signingCertificate did not contain a certificate"))?; - return Ok((signer, iter.collect())); - } - Ok(( - rdp::parse_certificate(bytes).context("parse Artifact Signing DER signing certificate")?, - Vec::new(), - )) -} - #[cfg(feature = "artifact-signing-rest")] fn create_pe_authenticode_pkcs7_der_artifact_signing( pe: &[u8], @@ -3593,7 +3562,8 @@ fn create_pe_authenticode_pkcs7_der_artifact_signing( eprintln!("[debug] {msg}"); } })?; - let (signer_cert, mut chain) = parse_artifact_signing_certificates(&signed.signing_certificate)?; + let (signer_cert, mut chain) = + pkcs7::parse_artifact_signing_certificates(&signed.signing_certificate)?; for chain_cert in chain_certs { let bytes = std::fs::read(&chain_cert).with_context(|| format!("read {}", chain_cert.display()))?; diff --git a/crates/psign-opc-sign/src/nuget.rs b/crates/psign-opc-sign/src/nuget.rs index 3ac9675..69824ed 100644 --- a/crates/psign-opc-sign/src/nuget.rs +++ b/crates/psign-opc-sign/src/nuget.rs @@ -10,6 +10,72 @@ use zip::write::FileOptions; pub const PACKAGE_SIGNATURE_FILE_NAME: &str = ".signature.p7s"; pub const SIGNATURE_CONTENT_VERSION: &str = "1"; +fn nuget_zip_options(compression: zip::CompressionMethod) -> FileOptions { + FileOptions::default().compression_method(compression) +} + +fn normalize_nuget_zip_metadata(bytes: &mut [u8]) -> Result<()> { + let eocd = bytes + .windows(4) + .rposition(|window| window == [0x50, 0x4b, 0x05, 0x06]) + .ok_or_else(|| anyhow!("ZIP central directory end not found"))?; + if eocd + 22 > bytes.len() { + return Err(anyhow!("truncated ZIP central directory end")); + } + + let central_dir_size = u32::from_le_bytes( + bytes[eocd + 12..eocd + 16] + .try_into() + .expect("central directory size slice"), + ) as usize; + let central_dir_offset = u32::from_le_bytes( + bytes[eocd + 16..eocd + 20] + .try_into() + .expect("central directory offset slice"), + ) as usize; + let central_dir_end = central_dir_offset + .checked_add(central_dir_size) + .ok_or_else(|| anyhow!("ZIP central directory size overflow"))?; + if central_dir_end > bytes.len() { + return Err(anyhow!("ZIP central directory extends past end of file")); + } + + let mut pos = central_dir_offset; + while pos < central_dir_end { + if pos + 46 > bytes.len() || bytes[pos..pos + 4] != [0x50, 0x4b, 0x01, 0x02] { + return Err(anyhow!("invalid ZIP central directory entry")); + } + bytes[pos + 5] = 0; + bytes[pos + 38..pos + 42].fill(0); + + let name_len = u16::from_le_bytes( + bytes[pos + 28..pos + 30] + .try_into() + .expect("file name length slice"), + ) as usize; + let extra_len = u16::from_le_bytes( + bytes[pos + 30..pos + 32] + .try_into() + .expect("extra field length slice"), + ) as usize; + let comment_len = u16::from_le_bytes( + bytes[pos + 32..pos + 34] + .try_into() + .expect("file comment length slice"), + ) as usize; + pos = pos + .checked_add(46) + .and_then(|n| n.checked_add(name_len)) + .and_then(|n| n.checked_add(extra_len)) + .and_then(|n| n.checked_add(comment_len)) + .ok_or_else(|| anyhow!("ZIP central directory entry size overflow"))?; + } + if pos != central_dir_end { + return Err(anyhow!("ZIP central directory entry length mismatch")); + } + Ok(()) +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum NuGetHashAlgorithm { Sha256, @@ -243,7 +309,7 @@ where fn write_package_without_signature_impl( reader: R, - writer: W, + mut writer: W, require_signature: bool, ) -> Result<()> where @@ -251,33 +317,39 @@ where W: Write + Seek, { let mut input = zip::ZipArchive::new(reader).context("open NuGet ZIP")?; - let mut output = zip::ZipWriter::new(writer); + let mut out = std::io::Cursor::new(Vec::new()); let mut had_signature = false; - for i in 0..input.len() { - let mut file = input.by_index(i).context("read NuGet ZIP entry")?; - let name = normalize_zip_part_name(file.name())?; - if name == PACKAGE_SIGNATURE_FILE_NAME { - had_signature = true; - continue; + { + let mut output = zip::ZipWriter::new(&mut out); + for i in 0..input.len() { + let mut file = input.by_index(i).context("read NuGet ZIP entry")?; + let name = normalize_zip_part_name(file.name())?; + if name == PACKAGE_SIGNATURE_FILE_NAME { + had_signature = true; + continue; + } + + let options = nuget_zip_options(file.compression()); + if file.is_dir() { + output.add_directory(name, options)?; + } else { + output.start_file(name, options)?; + std::io::copy(&mut file, &mut output)?; + } } - let options = FileOptions::default().compression_method(file.compression()); - if file.is_dir() { - output.add_directory(name, options)?; - } else { - output.start_file(name, options)?; - std::io::copy(&mut file, &mut output)?; + if require_signature && !had_signature { + return Err(anyhow!( + "package does not contain {PACKAGE_SIGNATURE_FILE_NAME}" + )); } - } - if require_signature && !had_signature { - return Err(anyhow!( - "package does not contain {PACKAGE_SIGNATURE_FILE_NAME}" - )); + output.finish()?; } - - output.finish()?; + let mut bytes = out.into_inner(); + normalize_nuget_zip_metadata(&mut bytes)?; + writer.write_all(&bytes)?; Ok(()) } @@ -318,7 +390,7 @@ pub fn embed_signature_path( pub fn embed_signature( reader: R, - writer: W, + mut writer: W, signature_der: &[u8], overwrite: bool, ) -> Result<()> @@ -330,40 +402,47 @@ where return Err(anyhow!("NuGet package signature payload is empty")); } let mut input = zip::ZipArchive::new(reader).context("open NuGet ZIP")?; - let mut output = zip::ZipWriter::new(writer); + let mut out = std::io::Cursor::new(Vec::new()); let mut had_signature = false; - for i in 0..input.len() { - let mut file = input.by_index(i).context("read NuGet ZIP entry")?; - let name = normalize_zip_part_name(file.name())?; - if name == PACKAGE_SIGNATURE_FILE_NAME { - had_signature = true; - if overwrite { - continue; + { + let mut output = zip::ZipWriter::new(&mut out); + for i in 0..input.len() { + let mut file = input.by_index(i).context("read NuGet ZIP entry")?; + let name = normalize_zip_part_name(file.name())?; + if name == PACKAGE_SIGNATURE_FILE_NAME { + had_signature = true; + if overwrite { + continue; + } + return Err(anyhow!( + "package already contains {}; pass overwrite to replace it", + PACKAGE_SIGNATURE_FILE_NAME + )); + } + + let options = nuget_zip_options(file.compression()); + if file.is_dir() { + output.add_directory(name, options)?; + } else { + output.start_file(name, options)?; + std::io::copy(&mut file, &mut output)?; } - return Err(anyhow!( - "package already contains {}; pass overwrite to replace it", - PACKAGE_SIGNATURE_FILE_NAME - )); } - let options = FileOptions::default().compression_method(file.compression()); - if file.is_dir() { - output.add_directory(name, options)?; - } else { - output.start_file(name, options)?; - std::io::copy(&mut file, &mut output)?; + if !had_signature || overwrite { + output.start_file( + PACKAGE_SIGNATURE_FILE_NAME, + nuget_zip_options(zip::CompressionMethod::Stored), + )?; + output.write_all(signature_der)?; } - } - if !had_signature || overwrite { - output.start_file( - PACKAGE_SIGNATURE_FILE_NAME, - FileOptions::default().compression_method(zip::CompressionMethod::Stored), - )?; - output.write_all(signature_der)?; + output.finish()?; } - output.finish()?; + let mut bytes = out.into_inner(); + normalize_nuget_zip_metadata(&mut bytes)?; + writer.write_all(&bytes)?; Ok(()) } @@ -476,7 +555,8 @@ mod tests { let mut out = Cursor::new(Vec::new()); embed_signature(Cursor::new(zip), &mut out, b"cms", false).unwrap(); - let info = inspect_package_reader_for_test(out.into_inner()); + let signed = out.into_inner(); + let info = inspect_package_reader_for_test(signed.clone()); assert_eq!( info.entry(PACKAGE_SIGNATURE_FILE_NAME) @@ -488,6 +568,18 @@ mod tests { .map(|e| e.compression.as_str()), Some("Stored") ); + let mut archive = zip::ZipArchive::new(Cursor::new(signed)).unwrap(); + assert_eq!( + archive + .by_name(PACKAGE_SIGNATURE_FILE_NAME) + .unwrap() + .unix_mode(), + None + ); + assert_eq!( + archive.by_name("lib/net8.0/a.dll").unwrap().unix_mode(), + None + ); } #[test] diff --git a/crates/psign-sip-digest/src/pkcs7.rs b/crates/psign-sip-digest/src/pkcs7.rs index 7803b80..c9971f4 100644 --- a/crates/psign-sip-digest/src/pkcs7.rs +++ b/crates/psign-sip-digest/src/pkcs7.rs @@ -11,6 +11,7 @@ use anyhow::{Context as _, Result, anyhow}; use authenticode::{DigestInfo, SpcAttributeTypeAndOptionalValue, SpcIndirectDataContent}; +use base64::Engine as _; use cms::builder::{SignedDataBuilder, SignerInfoBuilder}; use cms::cert::CertificateChoices; use cms::cert::IssuerAndSerialNumber; @@ -19,14 +20,14 @@ use cms::signed_data::{ CertificateSet, EncapsulatedContentInfo, SignatureValue, SignedAttributes, SignedData, SignerIdentifier, SignerInfo, SignerInfos, }; -use der::asn1::{Any, AnyRef, ObjectIdentifier, OctetString, OctetStringRef, SetOfVec}; -use der::{Decode, Encode, Reader, SliceReader, Tag}; +use der::asn1::{Any, AnyRef, ObjectIdentifier, OctetString, OctetStringRef, SetOfVec, UtcTime}; +use der::{Decode, Encode, Reader, SliceReader, Tag, TagNumber}; use digest::Digest as _; use rsa::RsaPrivateKey; use sha2::{Sha256, Sha384, Sha512}; use x509_cert::Certificate; use x509_cert::attr::Attribute; -use x509_cert::ext::pkix::SubjectKeyIdentifier; +use x509_cert::ext::pkix::{BasicConstraints, ExtendedKeyUsage, SubjectKeyIdentifier}; use x509_cert::spki::AlgorithmIdentifierOwned; /// CMS **`signedData`** content type OID (`id-signedData`). @@ -84,9 +85,24 @@ pub const PKCS9_MESSAGE_DIGEST_OID: ObjectIdentifier = /// PKCS#9 **`contentType`** authenticated-attribute type OID. pub const PKCS9_CONTENT_TYPE_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.3"); +/// PKCS#9 **`signingTime`** authenticated-attribute type OID. +pub const PKCS9_SIGNING_TIME_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.5"); /// Microsoft Authenticode RFC3161 timestamp-token unsigned attribute. pub const MS_RFC3161_TIMESTAMP_TOKEN_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.311.3.3.1"); +/// CMS/PKCS#9 RFC3161 timestamp-token unsigned attribute (`id-aa-timeStampToken`). +pub const PKCS9_RFC3161_TIMESTAMP_TOKEN_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.2.14"); +/// CAdES commitment-type-indication signed attribute (`id-smime-aa-ets-commitmentType`). +pub const PKCS9_COMMITMENT_TYPE_INDICATION_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.2.16"); +/// ESS signing-certificate-v2 signed attribute (`id-aa-signingCertificateV2`). +pub const PKCS9_SIGNING_CERTIFICATE_V2_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.2.47"); +/// NuGet author signature commitment type (`id-smime-cti-ets-proofOfOrigin`). +pub const COMMITMENT_TYPE_IDENTIFIER_PROOF_OF_ORIGIN_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.16.6.1"); /// CMS **`data`** content type OID (`id-data`). pub const PKCS7_ID_DATA_OID: &str = "1.2.840.113549.1.7.1"; @@ -94,6 +110,16 @@ pub const PKCS7_ID_DATA_OID: &str = "1.2.840.113549.1.7.1"; /// CMS **`signedData`** content type OID (string form). pub const PKCS7_ID_SIGNED_DATA_OID: &str = "1.2.840.113549.1.7.2"; +/// Signed-attribute profile for generic CMS `SignedData` signatures. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Pkcs7SignedAttributeProfile { + /// RFC 5652 minimum attributes generated by RustCrypto CMS: `contentType` and `messageDigest`. + Basic, + /// NuGet author package signature attributes needed for NuGet tooling to classify the primary + /// package signature as a publisher/author signature. + NuGetAuthor, +} + /// Digest algorithms supported by the portable Authenticode CMS producer. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum AuthenticodeSigningDigest { @@ -483,8 +509,22 @@ pub fn pkcs7_remote_rsa_signed_attrs_digest( econtent_der: &[u8], digest_algorithm: AuthenticodeSigningDigest, ) -> Result> { - let attrs = pkcs7_signed_attrs(econtent_type, econtent_der, digest_algorithm)?; - let der = signed_attributes_der(&attrs)?; + let attrs = pkcs7_signed_attrs( + econtent_type, + econtent_der, + digest_algorithm, + Pkcs7SignedAttributeProfile::Basic, + None, + )?; + pkcs7_signed_attrs_digest(&attrs, digest_algorithm) +} + +/// Return the digest a remote RSA signer must sign for already-built CMS signed attributes. +pub fn pkcs7_signed_attrs_digest( + attrs: &SignedAttributes, + digest_algorithm: AuthenticodeSigningDigest, +) -> Result> { + let der = signed_attributes_der(attrs)?; Ok(digest_algorithm.digest_bytes(&der)) } @@ -502,7 +542,39 @@ pub fn create_pkcs7_signed_data_der_with_rsa_signature( encrypted_digest: &[u8], detached: bool, ) -> Result> { - let attrs = pkcs7_signed_attrs(econtent_type, econtent_der, digest_algorithm)?; + let attrs = pkcs7_signed_attrs( + econtent_type, + econtent_der, + digest_algorithm, + Pkcs7SignedAttributeProfile::Basic, + None, + )?; + create_pkcs7_signed_data_der_with_signed_attrs_and_rsa_signature( + econtent_type, + econtent_der, + digest_algorithm, + signer_cert, + chain_certs, + encrypted_digest, + detached, + attrs, + ) +} + +/// Create generic PKCS#7 `ContentInfo(SignedData)` DER from externally produced RSA signature bytes +/// and caller-supplied signed attributes. The caller must ensure `encrypted_digest` signs the DER +/// encoding of `signed_attrs`. +#[allow(clippy::too_many_arguments)] +pub fn create_pkcs7_signed_data_der_with_signed_attrs_and_rsa_signature( + econtent_type: ObjectIdentifier, + econtent_der: &[u8], + digest_algorithm: AuthenticodeSigningDigest, + signer_cert: Certificate, + chain_certs: Vec, + encrypted_digest: &[u8], + detached: bool, + signed_attrs: SignedAttributes, +) -> Result> { let signer_id = SignerIdentifier::IssuerAndSerialNumber(IssuerAndSerialNumber { issuer: signer_cert.tbs_certificate.issuer.clone(), serial_number: signer_cert.tbs_certificate.serial_number.clone(), @@ -511,7 +583,7 @@ pub fn create_pkcs7_signed_data_der_with_rsa_signature( version: CmsVersion::V1, sid: signer_id, digest_alg: digest_algorithm.digest_algorithm(), - signed_attrs: Some(attrs), + signed_attrs: Some(signed_attrs), signature_algorithm: digest_algorithm.rsa_signature_algorithm(), signature: SignatureValue::new(encrypted_digest.to_vec()) .map_err(|e| anyhow!("SignerInfo.signature OCTET STRING: {e}"))?, @@ -553,6 +625,35 @@ pub fn signed_data_add_rfc3161_timestamp_token( sd: &SignedData, signer_index: usize, timestamp_token_der: &[u8], +) -> Result { + signed_data_add_rfc3161_timestamp_token_with_oid( + sd, + signer_index, + timestamp_token_der, + MS_RFC3161_TIMESTAMP_TOKEN_OID, + ) +} + +/// Attach a raw RFC3161 `timeStampToken` `ContentInfo` as a CMS/PKCS#9 unsigned attribute. +pub fn signed_data_add_pkcs9_rfc3161_timestamp_token( + sd: &SignedData, + signer_index: usize, + timestamp_token_der: &[u8], +) -> Result { + signed_data_add_rfc3161_timestamp_token_with_oid( + sd, + signer_index, + timestamp_token_der, + PKCS9_RFC3161_TIMESTAMP_TOKEN_OID, + ) +} + +/// Attach a raw RFC3161 `timeStampToken` `ContentInfo` as an unsigned attribute with the supplied OID. +pub fn signed_data_add_rfc3161_timestamp_token_with_oid( + sd: &SignedData, + signer_index: usize, + timestamp_token_der: &[u8], + timestamp_attr_oid: ObjectIdentifier, ) -> Result { let signers = sd.signer_infos.0.as_slice(); let si = signers.get(signer_index).ok_or_else(|| { @@ -575,7 +676,7 @@ pub fn signed_data_add_rfc3161_timestamp_token( .insert(token_any) .map_err(|e| anyhow!("timestamp AttributeValue SET: {e}"))?; let timestamp_attr = Attribute { - oid: MS_RFC3161_TIMESTAMP_TOKEN_OID, + oid: timestamp_attr_oid, values, }; let mut attrs: Vec = si @@ -583,7 +684,7 @@ pub fn signed_data_add_rfc3161_timestamp_token( .as_ref() .map(|attrs| attrs.iter().cloned().collect()) .unwrap_or_default(); - attrs.retain(|attr| attr.oid != MS_RFC3161_TIMESTAMP_TOKEN_OID); + attrs.retain(|attr| attr.oid != timestamp_attr_oid); attrs.push(timestamp_attr); let mut stamped = si.clone(); stamped.unsigned_attrs = Some( @@ -765,6 +866,12 @@ pub fn parse_pkcs7_signed_data_der(pkcs7_der: &[u8]) -> Result { /// **id-ce-subjectKeyIdentifier** (RFC 5280). const SUBJECT_KEY_IDENTIFIER_EXT_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.14"); +/// **id-ce-basicConstraints** (RFC 5280). +const BASIC_CONSTRAINTS_EXT_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19"); +/// **id-ce-extKeyUsage** (RFC 5280). +const EXTENDED_KEY_USAGE_EXT_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.37"); +/// **id-kp-codeSigning** (RFC 5280). +const CODE_SIGNING_EKU_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.3"); /// Locate the embedded **`Certificate`** matching **`SignerInfo.sid`** (**`IssuerAndSerialNumber`** or **`SubjectKeyIdentifier`**). pub fn signed_data_certificate_for_signer_identifier<'a>( @@ -808,6 +915,212 @@ pub fn signed_data_certificate_for_signer_identifier<'a>( Err(anyhow!("no embedded certificate matches SignerIdentifier")) } +fn same_certificate_identity(a: &Certificate, b: &Certificate) -> bool { + a.tbs_certificate.issuer == b.tbs_certificate.issuer + && a.tbs_certificate.serial_number == b.tbs_certificate.serial_number +} + +// Certificate-bag signer selection uses issuer topology first. Treat optional extension parse +// failures as "not asserted" so nonstandard CA/EKU values don't reject otherwise unambiguous bags. +fn x509_cert_is_ca(cert: &Certificate) -> bool { + let Some(exts) = &cert.tbs_certificate.extensions else { + return false; + }; + for ext in exts + .iter() + .filter(|e| e.extn_id == BASIC_CONSTRAINTS_EXT_OID) + { + if let Ok(basic) = BasicConstraints::from_der(ext.extn_value.as_bytes()) + && basic.ca + { + return true; + } + } + false +} + +fn x509_cert_has_code_signing_eku(cert: &Certificate) -> bool { + let Some(exts) = &cert.tbs_certificate.extensions else { + return false; + }; + for ext in exts + .iter() + .filter(|e| e.extn_id == EXTENDED_KEY_USAGE_EXT_OID) + { + if let Ok(eku) = ExtendedKeyUsage::from_der(ext.extn_value.as_bytes()) + && eku.0.contains(&CODE_SIGNING_EKU_OID) + { + return true; + } + } + false +} + +fn cert_issues_another_cert(candidate: &Certificate, certs: &[Certificate]) -> bool { + certs.iter().any(|other| { + !same_certificate_identity(candidate, other) + && other.tbs_certificate.issuer == candidate.tbs_certificate.subject + }) +} + +fn only_candidate(candidates: Vec<&Certificate>) -> Option { + if candidates.len() == 1 { + Some(candidates[0].clone()) + } else { + None + } +} + +fn select_certificate_bag_signer(certs: &[Certificate]) -> Result { + if certs.is_empty() { + return Err(anyhow!("SignedData has no X.509 certificates")); + } + if certs.len() == 1 { + return Ok(certs[0].clone()); + } + + let leaf_candidates = certs + .iter() + .filter(|cert| !cert_issues_another_cert(cert, certs)) + .collect::>(); + let mut code_signing_leaf_candidates = Vec::new(); + for cert in &leaf_candidates { + if x509_cert_has_code_signing_eku(cert) { + code_signing_leaf_candidates.push(*cert); + } + } + if let Some(cert) = only_candidate(code_signing_leaf_candidates) { + return Ok(cert); + } + if let Some(cert) = only_candidate(leaf_candidates) { + return Ok(cert); + } + + let mut non_ca_code_signing_candidates = Vec::new(); + for cert in certs { + if !x509_cert_is_ca(cert) && x509_cert_has_code_signing_eku(cert) { + non_ca_code_signing_candidates.push(cert); + } + } + if let Some(cert) = only_candidate(non_ca_code_signing_candidates) { + return Ok(cert); + } + + Err(anyhow!( + "PKCS#7 certificate bag has no SignerInfo and signer certificate is ambiguous" + )) +} + +/// Decode PKCS#7 DER and return the primary signer certificate plus any additional certificates. +/// +/// The primary signer is resolved from the first **`SignerInfo.sid`** rather than by certificate-set order. +pub fn parse_pkcs7_signer_and_chain_certificates( + pkcs7_der: &[u8], +) -> Result<(Certificate, Vec)> { + let sd = parse_pkcs7_signed_data_der(pkcs7_der)?; + let certs = sd + .certificates + .as_ref() + .ok_or_else(|| anyhow!("SignedData has no certificates"))? + .0 + .iter() + .filter_map(|choice| match choice { + CertificateChoices::Certificate(cert) => Some(cert.clone()), + _ => None, + }) + .collect::>(); + if let Some(signer_info) = sd.signer_infos.0.as_slice().first() { + let signer = signed_data_certificate_for_signer_identifier(&sd, &signer_info.sid) + .context("resolve signer certificate from SignedData")? + .clone(); + let chain = certs + .into_iter() + .filter(|cert| { + cert.tbs_certificate.issuer != signer.tbs_certificate.issuer + || cert.tbs_certificate.serial_number != signer.tbs_certificate.serial_number + }) + .collect(); + return Ok((signer, chain)); + } + let signer = select_certificate_bag_signer(&certs)?; + let chain = certs + .into_iter() + .filter(|cert| !same_certificate_identity(cert, &signer)) + .collect(); + Ok((signer, chain)) +} + +const ARTIFACT_SIGNING_CERTIFICATE_MAX_BASE64_DEPTH: usize = 4; + +/// Decode Azure Artifact Signing `signingCertificate` bytes into the signer certificate and chain. +/// +/// The service may return a PEM/DER certificate, a PKCS#7 certificate bag, or base64 text wrapping +/// either representation. +pub fn parse_artifact_signing_certificates( + bytes: &[u8], +) -> Result<(Certificate, Vec)> { + parse_artifact_signing_certificates_inner(bytes, 0) +} + +fn parse_artifact_signing_certificates_inner( + bytes: &[u8], + base64_depth: usize, +) -> Result<(Certificate, Vec)> { + if let Ok(text) = std::str::from_utf8(bytes) + && text.contains("-----BEGIN CERTIFICATE-----") + { + let mut certs = Vec::new(); + let mut rest = text; + while let Some(start) = rest.find("-----BEGIN CERTIFICATE-----") { + rest = &rest[start..]; + let Some(end) = rest.find("-----END CERTIFICATE-----") else { + return Err(anyhow!( + "unterminated PEM certificate in Artifact Signing signingCertificate" + )); + }; + let end = end + "-----END CERTIFICATE-----".len(); + certs.push( + crate::rdp::parse_certificate(&rest.as_bytes()[..end]) + .context("parse Artifact Signing PEM certificate")?, + ); + rest = &rest[end..]; + } + let mut iter = certs.into_iter(); + let signer = iter.next().ok_or_else(|| { + anyhow!("Artifact Signing signingCertificate did not contain a certificate") + })?; + return Ok((signer, iter.collect())); + } + if let Ok(text) = std::str::from_utf8(bytes) { + let compact: String = text.chars().filter(|c| !c.is_ascii_whitespace()).collect(); + if !compact.is_empty() + && compact + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'=')) + { + let decoded = base64::engine::general_purpose::STANDARD + .decode(compact.as_bytes()) + .context("decode nested base64 Artifact Signing signingCertificate")?; + if base64_depth >= ARTIFACT_SIGNING_CERTIFICATE_MAX_BASE64_DEPTH { + return Err(anyhow!( + "Artifact Signing signingCertificate has too many nested base64 layers" + )); + } + return parse_artifact_signing_certificates_inner(&decoded, base64_depth + 1); + } + } + + let der_err = match crate::rdp::parse_certificate(bytes) { + Ok(cert) => return Ok((cert, Vec::new())), + Err(err) => err.context("parse Artifact Signing DER signing certificate"), + }; + let pkcs7_err = match parse_pkcs7_signer_and_chain_certificates(bytes) { + Ok(certs) => return Ok(certs), + Err(err) => err.context("parse Artifact Signing PKCS#7 signingCertificate bundle"), + }; + Err(anyhow!("{der_err:#}; {pkcs7_err:#}")) +} + /// PKCS#9 **`messageDigest`** match + **RSA PKCS#1 v1.5** verify over authenticated **`signedAttrs`** /// (**SHA-256** digest algorithm only), using the embedded signer certificate’s public key. /// @@ -1165,6 +1478,109 @@ fn pkcs9_content_type_attribute(content_type: ObjectIdentifier) -> Result Result { + let signing_time_der = UtcTime::from_system_time(std::time::SystemTime::now()) + .map_err(|e| anyhow!("signingTime UTC time: {e}"))? + .to_der() + .map_err(|e| anyhow!("signingTime DER: {e}"))?; + let mut rd = SliceReader::new(signing_time_der.as_slice()) + .map_err(|e| anyhow!("signingTime DER reader: {e}"))?; + let val = Any::decode(&mut rd).map_err(|e| anyhow!("signingTime AttributeValue ANY: {e}"))?; + rd.finish(()) + .map_err(|e| anyhow!("trailing octets after signingTime DER: {e}"))?; + let mut values = SetOfVec::new(); + values + .insert(val) + .map_err(|e| anyhow!("SET OF AttributeValue insert: {e}"))?; + Ok(Attribute { + oid: PKCS9_SIGNING_TIME_OID, + values, + }) +} + +fn commitment_type_indication_attribute(commitment_type: ObjectIdentifier) -> Result { + let commitment_type_der = commitment_type + .to_der() + .map_err(|e| anyhow!("commitmentTypeId OID DER: {e}"))?; + let val = Any::new(Tag::Sequence, commitment_type_der) + .map_err(|e| anyhow!("commitmentTypeIndication AttributeValue ANY: {e}"))?; + let mut values = SetOfVec::new(); + values + .insert(val) + .map_err(|e| anyhow!("SET OF AttributeValue insert: {e}"))?; + Ok(Attribute { + oid: PKCS9_COMMITMENT_TYPE_INDICATION_OID, + values, + }) +} + +fn der_any(tag: Tag, content: Vec, context: &str) -> Result { + Any::new(tag, content).map_err(|e| anyhow!("{context} ANY: {e}")) +} + +fn der_sequence(content: Vec, context: &str) -> Result> { + der_any(Tag::Sequence, content, context)? + .to_der() + .map_err(|e| anyhow!("{context} DER: {e}")) +} + +fn signing_certificate_v2_attribute(signer_cert: &Certificate) -> Result { + let signer_cert_der = signer_cert + .to_der() + .map_err(|e| anyhow!("signer certificate DER for signingCertificateV2: {e}"))?; + let cert_hash = Sha256::digest(&signer_cert_der); + + let hash_algorithm = AuthenticodeSigningDigest::Sha256 + .digest_algorithm() + .to_der() + .map_err(|e| anyhow!("signingCertificateV2 hashAlgorithm DER: {e}"))?; + let cert_hash = OctetString::new(cert_hash.to_vec()) + .map_err(|e| anyhow!("signingCertificateV2 certHash OCTET STRING: {e}"))? + .to_der() + .map_err(|e| anyhow!("signingCertificateV2 certHash DER: {e}"))?; + + let issuer = signer_cert + .tbs_certificate + .issuer + .to_der() + .map_err(|e| anyhow!("signingCertificateV2 issuer Name DER: {e}"))?; + let directory_name = der_any( + Tag::ContextSpecific { + constructed: true, + number: TagNumber::N4, + }, + issuer, + "signingCertificateV2 directoryName", + )? + .to_der() + .map_err(|e| anyhow!("signingCertificateV2 directoryName DER: {e}"))?; + let general_names = der_sequence(directory_name, "signingCertificateV2 GeneralNames")?; + let serial_number = signer_cert + .tbs_certificate + .serial_number + .to_der() + .map_err(|e| anyhow!("signingCertificateV2 serialNumber DER: {e}"))?; + let issuer_serial = der_sequence( + [general_names, serial_number].concat(), + "signingCertificateV2 IssuerSerial", + )?; + let ess_cert_id_v2 = der_sequence( + [hash_algorithm, cert_hash, issuer_serial].concat(), + "signingCertificateV2 ESSCertIDv2", + )?; + let certs = der_sequence(ess_cert_id_v2, "signingCertificateV2 certs")?; + let val = der_any(Tag::Sequence, certs, "signingCertificateV2 AttributeValue")?; + + let mut values = SetOfVec::new(); + values + .insert(val) + .map_err(|e| anyhow!("SET OF AttributeValue insert: {e}"))?; + Ok(Attribute { + oid: PKCS9_SIGNING_CERTIFICATE_V2_OID, + values, + }) +} + fn signed_attributes_der(attrs: &SignedAttributes) -> Result> { let mut out = Vec::new(); attrs @@ -1194,10 +1610,13 @@ fn authenticode_signed_attrs( .map_err(|e| anyhow!("SignedAttributes SET OF Attribute canonicalization: {e}")) } -fn pkcs7_signed_attrs( +/// Build the CMS signed attributes for a generic PKCS#7 signature. +pub fn pkcs7_signed_attrs( econtent_type: ObjectIdentifier, econtent_der: &[u8], digest_algorithm: AuthenticodeSigningDigest, + profile: Pkcs7SignedAttributeProfile, + signer_cert: Option<&Certificate>, ) -> Result { let mut rd = SliceReader::new(econtent_der) .map_err(|e| anyhow!("encapsulated content DER reader: {e}"))?; @@ -1209,11 +1628,22 @@ fn pkcs7_signed_attrs( &digest_algorithm.digest_algorithm().oid, &econtent, )?; - SetOfVec::try_from(vec![ + let mut attrs = vec![ pkcs9_content_type_attribute(econtent_type)?, pkcs9_message_digest_attribute(&econtent_digest)?, - ]) - .map_err(|e| anyhow!("SignedAttributes SET OF Attribute canonicalization: {e}")) + ]; + if profile == Pkcs7SignedAttributeProfile::NuGetAuthor { + let signer_cert = signer_cert.ok_or_else(|| { + anyhow!("NuGet author signed attributes require the signer certificate") + })?; + attrs.push(pkcs9_signing_time_attribute()?); + attrs.push(commitment_type_indication_attribute( + COMMITMENT_TYPE_IDENTIFIER_PROOF_OF_ORIGIN_OID, + )?); + attrs.push(signing_certificate_v2_attribute(signer_cert)?); + } + SetOfVec::try_from(attrs) + .map_err(|e| anyhow!("SignedAttributes SET OF Attribute canonicalization: {e}")) } /// Clone authenticated **`SET OF Attribute`** and replace PKCS#9 **`messageDigest`** (**[`PKCS9_MESSAGE_DIGEST_OID`]**) with **`new_message_digest`**. @@ -1686,14 +2116,92 @@ mod tests { encode_spc_indirect_data_der(&patched).expect("encode patched"); } + #[test] + fn parse_artifact_signing_certificates_accepts_pkcs7_bundle() { + let pkcs7_der = crate::verify_pe::pe_nth_pkcs7_signed_data_der( + include_bytes!("../../../tests/fixtures/pe-authenticode-upstream/tiny32.signed.efi"), + 0, + ) + .expect("pkcs7"); + let expected = + parse_pkcs7_signer_and_chain_certificates(&pkcs7_der).expect("expected signer"); + let actual = parse_artifact_signing_certificates(&pkcs7_der).expect("actual signer"); + + assert_eq!( + actual.0.tbs_certificate.subject, + expected.0.tbs_certificate.subject + ); + assert_eq!( + actual.0.tbs_certificate.serial_number, + expected.0.tbs_certificate.serial_number + ); + assert_eq!(actual.1.len(), expected.1.len()); + } + + #[test] + fn parse_artifact_signing_certificates_accepts_base64_wrapped_pkcs7_bundle() { + let pkcs7_der = crate::verify_pe::pe_nth_pkcs7_signed_data_der( + include_bytes!("../../../tests/fixtures/pe-authenticode-upstream/tiny32.signed.efi"), + 0, + ) + .expect("pkcs7"); + let wrapped = base64::engine::general_purpose::STANDARD.encode(&pkcs7_der); + let expected = + parse_pkcs7_signer_and_chain_certificates(&pkcs7_der).expect("expected signer"); + let actual = + parse_artifact_signing_certificates(wrapped.as_bytes()).expect("actual signer"); + + assert_eq!( + actual.0.tbs_certificate.subject, + expected.0.tbs_certificate.subject + ); + assert_eq!( + actual.0.tbs_certificate.serial_number, + expected.0.tbs_certificate.serial_number + ); + assert_eq!(actual.1.len(), expected.1.len()); + } + + #[test] + fn parse_artifact_signing_certificates_accepts_nested_base64_wrapped_pkcs7_bundle() { + let pkcs7_der = crate::verify_pe::pe_nth_pkcs7_signed_data_der( + include_bytes!("../../../tests/fixtures/pe-authenticode-upstream/tiny32.signed.efi"), + 0, + ) + .expect("pkcs7"); + let wrapped_once = base64::engine::general_purpose::STANDARD.encode(&pkcs7_der); + let wrapped_twice = base64::engine::general_purpose::STANDARD.encode(wrapped_once); + let expected = + parse_pkcs7_signer_and_chain_certificates(&pkcs7_der).expect("expected signer"); + let actual = + parse_artifact_signing_certificates(wrapped_twice.as_bytes()).expect("actual signer"); + + assert_eq!( + actual.0.tbs_certificate.subject, + expected.0.tbs_certificate.subject + ); + assert_eq!( + actual.0.tbs_certificate.serial_number, + expected.0.tbs_certificate.serial_number + ); + assert_eq!(actual.1.len(), expected.1.len()); + } + /// PKCS#1 v1.5 **RS256** prehash parity: [`super::signer_info_sha256_digest_over_signed_attrs`] matches /// **`SignerInfo.signature`** when verified with the embedded **RSA** signer certificate (same contract as Azure KV **`keys/sign`** digest input). mod rsa_pkcs1v15_signed_attrs_verify { + use super::CertificateChoices; + use super::SignerInfos; + use super::encode_pkcs7_content_info_signed_data_der; use super::parse_pkcs7_signed_data_der; + use super::parse_pkcs7_signer_and_chain_certificates; + use super::signed_data_certificate_for_signer_identifier; use super::signed_data_spc_indirect_message_digest_octets; use super::verify_signed_data_authenticode_indirect_digest_and_rsa_sha256_pkcs1v15_signature; use crate::verify_pe::pe_nth_pkcs7_signed_data_der; + use der::Decode; use der::asn1::ObjectIdentifier; + use der::asn1::SetOfVec; const SHA256_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1"); @@ -1727,5 +2235,95 @@ mod tests { "../../../tests/fixtures/pe-authenticode-upstream/tiny64.signed.efi" )); } + + #[test] + fn parse_pkcs7_signer_and_chain_certificates_finds_primary_signer_from_bundle() { + let pe_bytes = include_bytes!( + "../../../tests/fixtures/pe-authenticode-upstream/tiny32.signed.efi" + ); + let pkcs7 = crate::verify_pe::pe_nth_pkcs7_signed_data_der(pe_bytes, 0).expect("pkcs7"); + let sd = parse_pkcs7_signed_data_der(&pkcs7).expect("SignedData"); + let signer_info = sd.signer_infos.0.as_slice().first().expect("SignerInfo"); + let expected = signed_data_certificate_for_signer_identifier(&sd, &signer_info.sid) + .expect("signer cert"); + + let (signer, chain) = + parse_pkcs7_signer_and_chain_certificates(&pkcs7).expect("signer and chain"); + + assert_eq!( + signer.tbs_certificate.subject, + expected.tbs_certificate.subject + ); + assert_eq!( + signer.tbs_certificate.serial_number, + expected.tbs_certificate.serial_number + ); + assert!( + chain.iter().all(|cert| { + cert.tbs_certificate.issuer != signer.tbs_certificate.issuer + || cert.tbs_certificate.serial_number + != signer.tbs_certificate.serial_number + }), + "chain must not include the primary signer certificate" + ); + } + + #[test] + fn parse_pkcs7_signer_and_chain_certificates_accepts_certificate_bag_without_signer_info() { + let pe_bytes = + include_bytes!("../../../tests/fixtures/generated-signed/pe/tiny32-pe-alias.dll"); + let pkcs7 = crate::verify_pe::pe_nth_pkcs7_signed_data_der(pe_bytes, 0).expect("pkcs7"); + let mut sd = parse_pkcs7_signed_data_der(&pkcs7).expect("SignedData"); + let signer_info = sd.signer_infos.0.as_slice().first().expect("SignerInfo"); + let expected_signer = + signed_data_certificate_for_signer_identifier(&sd, &signer_info.sid) + .expect("signer cert") + .clone(); + let ca = x509_cert::Certificate::from_der(include_bytes!( + "../../../tests/fixtures/devolutions-authenticode/authenticode-test-ca.crt" + )) + .expect("CA cert"); + let signer_cert = sd + .certificates + .as_ref() + .expect("certificates") + .0 + .iter() + .find_map(|choice| match choice { + CertificateChoices::Certificate(cert) => Some(cert.clone()), + _ => None, + }) + .expect("signer certificate"); + let certs = vec![ + CertificateChoices::Certificate(ca), + CertificateChoices::Certificate(signer_cert), + ]; + sd.certificates = Some(super::CertificateSet( + SetOfVec::try_from(certs).expect("CertificateSet"), + )); + sd.signer_infos = SignerInfos(SetOfVec::new()); + let bag_der = + encode_pkcs7_content_info_signed_data_der(&sd).expect("encode certificate bag"); + + let (signer, chain) = + parse_pkcs7_signer_and_chain_certificates(&bag_der).expect("signer and chain"); + + assert_eq!( + signer.tbs_certificate.subject, + expected_signer.tbs_certificate.subject + ); + assert_eq!( + signer.tbs_certificate.serial_number, + expected_signer.tbs_certificate.serial_number + ); + assert!( + chain.iter().all(|cert| { + cert.tbs_certificate.issuer != signer.tbs_certificate.issuer + || cert.tbs_certificate.serial_number + != signer.tbs_certificate.serial_number + }), + "fallback chain must not repeat the selected signer certificate" + ); + } } } diff --git a/src/code.rs b/src/code.rs index 86d898e..516d952 100644 --- a/src/code.rs +++ b/src/code.rs @@ -16,7 +16,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 rsa::signature::{SignatureEncoding as _, Signer as _}; +use rsa::signature::{SignatureEncoding as _, Signer as _, hazmat::PrehashSigner as _}; use serde::{Deserialize, Serialize}; use sha2::Digest as _; use std::collections::{BTreeMap, BTreeSet}; @@ -324,6 +324,9 @@ fn execute_code_plan(args: &CodeArgs, plan: &CodePlan) -> Result signing_digest, args.timestamp_url.as_deref(), args.timestamp_digest, + true, + Rfc3161TimestampAttribute::MicrosoftAuthenticode, + pkcs7::Pkcs7SignedAttributeProfile::Basic, ) .with_context(|| { format!( @@ -623,6 +626,9 @@ fn sign_nuget_bytes( signing_digest, timestamp_url, timestamp_digest, + false, + Rfc3161TimestampAttribute::CmsTimeStampToken, + pkcs7::Pkcs7SignedAttributeProfile::NuGetAuthor, )?; let mut out = Cursor::new(Vec::new()); nuget::embed_signature(Cursor::new(unsigned), &mut out, &pkcs7, false) @@ -923,6 +929,9 @@ fn sign_nested_package_entries( signing_digest, timestamp_url, timestamp_digest, + true, + Rfc3161TimestampAttribute::MicrosoftAuthenticode, + pkcs7::Pkcs7SignedAttributeProfile::Basic, ) .with_context(|| { format!("create nested App Installer companion signature for {nested_label}") @@ -1497,6 +1506,7 @@ fn is_unsupported_nested_signable(format: &CodeFormat) -> bool { ) } +#[allow(clippy::too_many_arguments)] fn sign_pkcs7_id_data( content: &[u8], signer: &CodeSigner, @@ -1504,6 +1514,9 @@ fn sign_pkcs7_id_data( digest: pkcs7::AuthenticodeSigningDigest, timestamp_url: Option<&str>, timestamp_digest: Option, + detached: bool, + timestamp_attribute: Rfc3161TimestampAttribute, + signed_attribute_profile: pkcs7::Pkcs7SignedAttributeProfile, ) -> Result> { let chain = load_chain_certs(chain_certs)?; let econtent_der = OctetString::new(content.to_vec()) @@ -1512,21 +1525,33 @@ fn sign_pkcs7_id_data( .map_err(|e| anyhow!("encode CMS id-data DER: {e}"))?; let id_data = ObjectIdentifier::new(pkcs7::PKCS7_ID_DATA_OID) .map_err(|e| anyhow!("parse CMS id-data OID: {e}"))?; - let pkcs7 = signer.sign_pkcs7(id_data, &econtent_der, digest, chain, true)?; - let mut detached = pkcs7::parse_pkcs7_signed_data_der(&pkcs7) - .context("parse generated CMS before detaching eContent")?; - detached.encap_content_info.econtent = None; - let pkcs7 = pkcs7::encode_pkcs7_content_info_signed_data_der(&detached)?; - timestamp_pkcs7_if_requested(&pkcs7, timestamp_url, timestamp_digest) + let pkcs7 = signer.sign_pkcs7( + id_data, + &econtent_der, + digest, + chain, + detached, + signed_attribute_profile, + )?; + timestamp_pkcs7_if_requested(&pkcs7, timestamp_url, timestamp_digest, timestamp_attribute) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Rfc3161TimestampAttribute { + MicrosoftAuthenticode, + CmsTimeStampToken, } fn timestamp_pkcs7_if_requested( pkcs7_der: &[u8], timestamp_url: Option<&str>, timestamp_digest: Option, + timestamp_attribute: Rfc3161TimestampAttribute, ) -> Result> { match (timestamp_url, timestamp_digest) { - (Some(url), Some(digest)) => timestamp_pkcs7_der_rfc3161(pkcs7_der, url, digest), + (Some(url), Some(digest)) => { + timestamp_pkcs7_der_rfc3161(pkcs7_der, url, digest, timestamp_attribute) + } (Some(_), None) => Err(anyhow!( "`psign-tool code` requires --timestamp-digest with --timestamp-url" )), @@ -1542,6 +1567,7 @@ fn timestamp_pkcs7_der_rfc3161( pkcs7_der: &[u8], timestamp_url: &str, timestamp_digest: DigestAlgorithm, + timestamp_attribute: Rfc3161TimestampAttribute, ) -> Result> { let sd = pkcs7::parse_pkcs7_signed_data_der(pkcs7_der).context("parse PKCS#7 SignedData")?; let signer = sd @@ -1563,8 +1589,15 @@ fn timestamp_pkcs7_der_rfc3161( let token = parsed .time_stamp_token .ok_or_else(|| anyhow!("TimeStampResp has no timeStampToken"))?; - let stamped = pkcs7::signed_data_add_rfc3161_timestamp_token(&sd, 0, token) - .context("attach RFC3161 timestamp token")?; + let stamped = match timestamp_attribute { + Rfc3161TimestampAttribute::MicrosoftAuthenticode => { + pkcs7::signed_data_add_rfc3161_timestamp_token(&sd, 0, token) + } + Rfc3161TimestampAttribute::CmsTimeStampToken => { + pkcs7::signed_data_add_pkcs9_rfc3161_timestamp_token(&sd, 0, token) + } + } + .context("attach RFC3161 timestamp token")?; pkcs7::encode_pkcs7_content_info_signed_data_der(&stamped) } @@ -1573,6 +1606,7 @@ fn timestamp_pkcs7_der_rfc3161( _pkcs7_der: &[u8], _timestamp_url: &str, _timestamp_digest: DigestAlgorithm, + _timestamp_attribute: Rfc3161TimestampAttribute, ) -> Result> { Err(anyhow!( "`psign-tool code` RFC3161 timestamping requires the timestamp-http feature" @@ -1732,6 +1766,7 @@ impl CodeSigner { digest: pkcs7::AuthenticodeSigningDigest, chain: Vec, detached: bool, + signed_attribute_profile: pkcs7::Pkcs7SignedAttributeProfile, ) -> Result> { if let Some((cert, key)) = self.local_paths() { let cert_bytes = @@ -1742,39 +1777,116 @@ impl CodeSigner { std::fs::read(key).with_context(|| format!("read {}", key.display()))?; let private_key = rdp::parse_rsa_private_key(&key_bytes) .with_context(|| format!("parse RSA private key {}", key.display()))?; - let pkcs7 = pkcs7::create_pkcs7_signed_data_der_rsa( + let signed_attrs = pkcs7::pkcs7_signed_attrs( + econtent_type, + econtent_der, + digest, + signed_attribute_profile, + Some(&signer_cert), + )?; + let prehash = pkcs7::pkcs7_signed_attrs_digest(&signed_attrs, digest)?; + let signature = sign_pkcs7_signed_attrs_digest(digest, private_key, &prehash)?; + return pkcs7::create_pkcs7_signed_data_der_with_signed_attrs_and_rsa_signature( econtent_type, econtent_der, digest, signer_cert, chain, - private_key, - )?; - if !detached { - return Ok(pkcs7); - } - let mut detached_pkcs7 = pkcs7::parse_pkcs7_signed_data_der(&pkcs7) - .context("parse generated CMS before detaching eContent")?; - detached_pkcs7.encap_content_info.econtent = None; - return pkcs7::encode_pkcs7_content_info_signed_data_der(&detached_pkcs7); + &signature, + detached, + signed_attrs, + ); } - let prehash = - pkcs7::pkcs7_remote_rsa_signed_attrs_digest(econtent_type, econtent_der, digest)?; - let mut remote = - self.sign_remote_digest(code_remote_digest_from_pkcs7(digest), &prehash)?; - remote.chain.extend(chain); - pkcs7::create_pkcs7_signed_data_der_with_rsa_signature( + self.sign_pkcs7_remote( econtent_type, econtent_der, digest, - remote.signer_cert, - remote.chain, - &remote.signature, + chain, detached, + signed_attribute_profile, ) } + fn sign_pkcs7_remote( + &self, + econtent_type: ObjectIdentifier, + econtent_der: &[u8], + digest: pkcs7::AuthenticodeSigningDigest, + chain: Vec, + detached: bool, + signed_attribute_profile: pkcs7::Pkcs7SignedAttributeProfile, + ) -> Result> { + let mut signer_cert_hint = + if signed_attribute_profile == pkcs7::Pkcs7SignedAttributeProfile::NuGetAuthor { + Some(match self.remote_signer_certificate_hint()? { + Some(signer_cert) => signer_cert, + None => self.probe_remote_signer_certificate(digest)?, + }) + } else { + None + }; + + for attempt in 0..2 { + let signed_attrs = pkcs7::pkcs7_signed_attrs( + econtent_type, + econtent_der, + digest, + signed_attribute_profile, + signer_cert_hint.as_ref(), + )?; + let prehash = pkcs7::pkcs7_signed_attrs_digest(&signed_attrs, digest)?; + let mut remote = + self.sign_remote_digest(code_remote_digest_from_pkcs7(digest), &prehash)?; + if let Some(expected_cert) = signer_cert_hint.as_ref() + && !certificates_der_equal(expected_cert, &remote.signer_cert)? + { + if attempt == 0 { + signer_cert_hint = Some(remote.signer_cert); + continue; + } + return Err(anyhow!( + "remote signer certificate changed while building NuGet signing-certificate-v2" + )); + } + remote.chain.extend(chain.clone()); + return pkcs7::create_pkcs7_signed_data_der_with_signed_attrs_and_rsa_signature( + econtent_type, + econtent_der, + digest, + remote.signer_cert, + remote.chain, + &remote.signature, + detached, + signed_attrs, + ); + } + + unreachable!("remote PKCS#7 signing loop returns or errors") + } + + fn probe_remote_signer_certificate( + &self, + digest: pkcs7::AuthenticodeSigningDigest, + ) -> Result { + let probe_digest = vec![0u8; digest.pe_hash_kind().digest_output_len()]; + let remote = + self.sign_remote_digest(code_remote_digest_from_pkcs7(digest), &probe_digest)?; + Ok(remote.signer_cert) + } + + fn remote_signer_certificate_hint(&self) -> Result> { + match &self.backend { + CodeSignerBackend::Local(_) => Ok(None), + #[cfg(feature = "azure-kv-sign")] + CodeSignerBackend::AzureKeyVault(signer) => rdp::parse_certificate(&signer.cert_der) + .context("parse Azure Key Vault signer certificate") + .map(Some), + #[cfg(feature = "artifact-signing-rest")] + CodeSignerBackend::ArtifactSigning(_) => Ok(None), + } + } + fn sign_xml_signed_info( &self, algorithm: vsix::VsixHashAlgorithm, @@ -1885,7 +1997,7 @@ impl CodeArtifactSigningSigner { self.params_for_digest(digest.to_vec(), artifact_signature_algorithm(algorithm))?; let signed = submit_codesign_hash_signature_blocking(¶ms, |_| {})?; let (signer_cert, chain) = - parse_artifact_signing_certificates(&signed.signing_certificate)?; + pkcs7::parse_artifact_signing_certificates(&signed.signing_certificate)?; let signer_cert_der = signer_cert .to_der() .map_err(|e| anyhow!("encode Artifact Signing signer certificate: {e}"))?; @@ -2299,41 +2411,6 @@ struct ArtifactSigningMetadataDoc { ExcludeCredentials: Option>, } -#[cfg(feature = "artifact-signing-rest")] -fn parse_artifact_signing_certificates( - bytes: &[u8], -) -> Result<(x509_cert::Certificate, Vec)> { - if let Ok(text) = std::str::from_utf8(bytes) - && text.contains("-----BEGIN CERTIFICATE-----") - { - let mut certs = Vec::new(); - let mut rest = text; - while let Some(start) = rest.find("-----BEGIN CERTIFICATE-----") { - rest = &rest[start..]; - let Some(end) = rest.find("-----END CERTIFICATE-----") else { - return Err(anyhow!( - "unterminated PEM certificate in Artifact Signing signingCertificate" - )); - }; - let end = end + "-----END CERTIFICATE-----".len(); - certs.push( - rdp::parse_certificate(&rest.as_bytes()[..end]) - .context("parse Artifact Signing PEM certificate")?, - ); - rest = &rest[end..]; - } - let mut iter = certs.into_iter(); - let signer = iter.next().ok_or_else(|| { - anyhow!("Artifact Signing signingCertificate did not contain a certificate") - })?; - return Ok((signer, iter.collect())); - } - Ok(( - rdp::parse_certificate(bytes).context("parse Artifact Signing DER signing certificate")?, - Vec::new(), - )) -} - fn nuget_hash_algorithm(digest: DigestAlgorithm) -> Result { match digest { DigestAlgorithm::Sha256 => Ok(nuget::NuGetHashAlgorithm::Sha256), @@ -2367,6 +2444,47 @@ fn vsix_hash_algorithm(digest: DigestAlgorithm) -> Result Result> { + let signature = match digest { + pkcs7::AuthenticodeSigningDigest::Sha256 => { + let key = rsa::pkcs1v15::SigningKey::::new(private_key); + key.sign_prehash(signed_attrs_digest) + .map_err(|e| anyhow!("RSA/SHA-256 signed attributes prehash sign: {e}"))? + .to_bytes() + .to_vec() + } + pkcs7::AuthenticodeSigningDigest::Sha384 => { + let key = rsa::pkcs1v15::SigningKey::::new(private_key); + key.sign_prehash(signed_attrs_digest) + .map_err(|e| anyhow!("RSA/SHA-384 signed attributes prehash sign: {e}"))? + .to_bytes() + .to_vec() + } + pkcs7::AuthenticodeSigningDigest::Sha512 => { + let key = rsa::pkcs1v15::SigningKey::::new(private_key); + key.sign_prehash(signed_attrs_digest) + .map_err(|e| anyhow!("RSA/SHA-512 signed attributes prehash sign: {e}"))? + .to_bytes() + .to_vec() + } + }; + Ok(signature) +} + +fn certificates_der_equal(a: &x509_cert::Certificate, b: &x509_cert::Certificate) -> Result { + let a_der = a + .to_der() + .map_err(|e| anyhow!("encode expected signer certificate DER: {e}"))?; + let b_der = b + .to_der() + .map_err(|e| anyhow!("encode actual signer certificate DER: {e}"))?; + Ok(a_der == b_der) +} + fn sign_xml_signed_info( algorithm: vsix::VsixHashAlgorithm, private_key: rsa::RsaPrivateKey, diff --git a/tests/cli_pe_digest.rs b/tests/cli_pe_digest.rs index 72fc4c1..4dac7f8 100644 --- a/tests/cli_pe_digest.rs +++ b/tests/cli_pe_digest.rs @@ -1783,7 +1783,7 @@ fn write_test_rsa_cert_key(cert_path: &Path, key_path: &Path) { let builder = CertificateBuilder::new( Profile::Root, SerialNumber::from(42u32), - Validity::from_now(Duration::from_secs(86_400)).expect("validity"), + Validity::from_now(Duration::from_secs(7 * 86_400)).expect("validity"), subject, spki, &signing_key, diff --git a/tests/code_command.rs b/tests/code_command.rs index 0c53dd4..187d8db 100644 --- a/tests/code_command.rs +++ b/tests/code_command.rs @@ -1,6 +1,7 @@ use assert_cmd::Command; use predicates::prelude::*; use psign_opc_sign::nuget; +use psign_sip_digest::pkcs7; use rand::rngs::OsRng; use rsa::RsaPrivateKey; use rsa::pkcs1v15::SigningKey; @@ -173,6 +174,8 @@ fn code_signs_top_level_nupkg_with_local_cert_key() { .success() .stdout(predicate::str::contains("signed=yes")) .stdout(predicate::str::contains("signature_stored=yes")); + + assert_nupkg_signature_has_nuget_author_attrs(&output); } #[test] @@ -313,7 +316,7 @@ fn code_signs_nupkg_with_artifact_signing_identity() { ) .unwrap(); - let (mut guard, endpoint) = spawn_artifact_signing_server(2); + let (mut guard, endpoint) = spawn_artifact_signing_server(4); let mut cmd = psign(); cmd.args(["code", "--base-directory"]) .arg(base) @@ -1727,10 +1730,8 @@ fn code_signs_nupkg_with_rfc3161_timestamp() { inspect .assert() .success() - .stdout(predicate::str::contains( - "microsoft_nested_rfc3161_attribute", - )) - .stdout(predicate::str::contains("1.3.6.1.4.1.311.3.3.1")); + .stdout(predicate::str::contains("id_aa_time_stamp_token")) + .stdout(predicate::str::contains("1.2.840.113549.1.9.16.2.14")); } #[test] @@ -2226,6 +2227,46 @@ fn extract_zip_entry(zip_path: &Path, entry_name: &str, output: &Path) { std::fs::write(output, bytes).unwrap(); } +fn assert_nupkg_signature_has_nuget_author_attrs(path: &Path) { + let signature_der = nuget::extract_signature_path(path).expect("extract NuGet signature"); + let signed_data = + pkcs7::parse_pkcs7_signed_data_der(&signature_der).expect("parse NuGet signature"); + let signer_infos = signed_data.signer_infos.0.as_slice(); + let signer_info = signer_infos.first().expect("NuGet signature signer info"); + let signed_attrs = signer_info + .signed_attrs + .as_ref() + .expect("NuGet signature signed attributes"); + let commitment_attr = signed_attrs + .iter() + .find(|attr| attr.oid == pkcs7::PKCS9_COMMITMENT_TYPE_INDICATION_OID) + .expect("NuGet author commitment-type signed attribute"); + let commitment_values = commitment_attr.values.as_slice(); + assert_eq!(commitment_values.len(), 1); + + let proof_of_origin_oid = pkcs7::COMMITMENT_TYPE_IDENTIFIER_PROOF_OF_ORIGIN_OID + .to_der() + .expect("proofOfOrigin OID DER"); + let mut expected_value = vec![0x30, proof_of_origin_oid.len() as u8]; + expected_value.extend_from_slice(&proof_of_origin_oid); + assert_eq!( + commitment_values[0].to_der().expect("commitment value DER"), + expected_value + ); + + assert!( + signed_attrs + .iter() + .any(|attr| attr.oid == pkcs7::PKCS9_SIGNING_TIME_OID), + "NuGet author signing-time signed attribute" + ); + let signing_certificate_v2 = signed_attrs + .iter() + .find(|attr| attr.oid == pkcs7::PKCS9_SIGNING_CERTIFICATE_V2_OID) + .expect("NuGet author signing-certificate-v2 signed attribute"); + assert_eq!(signing_certificate_v2.values.as_slice().len(), 1); +} + fn sample_clickonce_manifest() -> &'static str { r#"