Skip to content

Commit

Permalink
apple-codesign: implement a sign --for-notarization mode
Browse files Browse the repository at this point in the history
Getting software to pass Apple's notarization requirements can be subtly
difficult and often requires multiple failed notarization attempts
before success.

This commit introduces a `sign --for-notarization` flag that attempts
to validate and engage signing settings to help ensure signed software
passes notarization. The logic is best effort and I'm sure there are
gaps. But for now, it seems better than nothing.

Related to #118.
  • Loading branch information
indygreg committed Jan 17, 2024
1 parent e1c5107 commit 8e11106
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 3 deletions.
5 changes: 5 additions & 0 deletions apple-codesign/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Released on ReleaseDate.
additional Mach-O binaries inside bundles, among other potential changes.
Ultimately we want this signing mode to converge with the default behavior of
Apple's tooling.
* The `sign` command has gained a `--for-notarization` argument that attempts to
engage and enforce signing settings required for Apple notarization. The goal
of the feature is to cut down on notarization failures after successful
signing operations. If you encounter a notarization failure when using this
new flag, consider filing a bug report.
* (API) `BundleSigner` now requires calling `collect_nested_bundles()` to register
child bundles for signing instead of signing all nested bundles by default.
* aws-config 0.57 -> 1.1.
Expand Down
24 changes: 24 additions & 0 deletions apple-codesign/docs/apple_codesign_rcodesign_notarizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,30 @@ them. They somewhat mirror Apple's official guidance at
`Resolving common notarization issues <https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/resolving_common_notarization_issues>`_,
which we highly recommend you read before this documentation.

.. _apple_codesign_notarization_for_notarization:

Using `sign --for-notarization`
-------------------------------

The ``rcodesign sign`` command has a ``--for-notarization`` argument that
attempts to engage *Apple notarization compatibility mode*.

When this flag is used:

* The signing configuration is validated for notarization compatibility.
* Signing settings are automatically changed to help ensure notarization compatibility.

This flag is best effort. If you encounter notarization failures when using
this flag that you think could be automatically detected or prevented,
please consider filing a bug report.

Usage example::

rcodesign sign \
--for-notarization \
--pem-source developer-id-application.pem \
MyApp.app

.. _apple_codesign_notarization_problem_apple_first:

Notarize with Apple Tooling First
Expand Down
12 changes: 12 additions & 0 deletions apple-codesign/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,9 @@ pub trait AppleCertificate: Sized {
/// certificates as the Organizational Unit field of the subject. So this
/// function is just a shortcut for retrieving that.
fn apple_team_id(&self) -> Option<String>;

/// Whether this is a certificate pretending to be signed by an Apple CA but isn't really.
fn is_test_apple_signed_certificate(&self) -> bool;
}

impl AppleCertificate for CapturedX509Certificate {
Expand Down Expand Up @@ -1029,6 +1032,15 @@ impl AppleCertificate for CapturedX509Certificate {
))
.unwrap_or(None)
}

fn is_test_apple_signed_certificate(&self) -> bool {
if let Ok(digest) = self.sha256_fingerprint() {
hex::encode(digest)
== "5939ad5770d8b977b38d07533754371314744e87a8d606433f689e9bc6b980a0"
} else {
false
}
}
}

/// Extensions to [X509CertificateBuilder] specializing in Apple certificate behavior.
Expand Down
24 changes: 24 additions & 0 deletions apple-codesign/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1477,6 +1477,27 @@ struct Sign {
#[arg(long)]
shallow: bool,

/// Indicate that the entity being signed will later be notarized.
///
/// Notarized software is subject to specific requirements, such as enabling the
/// hardened runtime.
///
/// The presence of this flag influences signing settings and engages additional
/// checks to help ensure that signed software can be successfully notarized.
///
/// This flag is best effort. Notarization failures of software signed with
/// this flag may be indicative of bugs in this software.
///
/// The behavior of this flag is subject to change. As currently implemented,
/// it will:
///
/// * Require the use of a "Developer ID" signing certificate issued by Apple.
/// * Require the use of a time-stamp server.
/// * Enable the hardened runtime code signature flag on all Mach-O binaries
/// (equivalent to `--code-signature-flags runtime` for all signed paths).
#[arg(long)]
for_notarization: bool,

/// Path to Mach-O binary to sign
input_path: PathBuf,

Expand Down Expand Up @@ -1535,13 +1556,16 @@ impl CliCommand for Sign {
}

settings.set_shallow(self.shallow);
settings.set_for_notarization(self.for_notarization);

for pattern in &self.exclude {
settings.add_path_exclusion(pattern)?;
}

ScopedSigningSettings(c.paths.clone()).load_into_settings(&mut settings)?;

settings.ensure_for_notarization_settings()?;

// Settings are locked in. Proceed to sign.

let signer = UnifiedSigner::new(settings);
Expand Down
3 changes: 3 additions & 0 deletions apple-codesign/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ pub enum AppleCodesignError {
#[error("Could not find App Store Connect API key in default search locations")]
AppStoreConnectApiKeyNotFound,

#[error("signing settings are not compatible with notarization")]
ForNotarizationInvalidSettings,

#[error("do not know how to notarize {0}")]
NotarizeUnsupportedPath(PathBuf),

Expand Down
98 changes: 95 additions & 3 deletions apple-codesign/src/signing_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use {
crate::{
certificate::AppleCertificate,
certificate::{AppleCertificate, CodeSigningCertificateExtension},
code_directory::CodeSignatureFlags,
code_requirement::CodeRequirementExpression,
cryptography::DigestType,
Expand All @@ -19,7 +19,7 @@ use {
goblin::mach::cputype::{
CpuType, CPU_TYPE_ARM, CPU_TYPE_ARM64, CPU_TYPE_ARM64_32, CPU_TYPE_X86_64,
},
log::info,
log::{error, info},
reqwest::{IntoUrl, Url},
std::{
collections::{BTreeMap, BTreeSet},
Expand Down Expand Up @@ -303,6 +303,7 @@ pub struct SigningSettings<'key> {
signing_time: Option<chrono::DateTime<chrono::Utc>>,
path_exclusion_patterns: Vec<Pattern>,
shallow: bool,
for_notarization: bool,

// Scope-specific settings.
// These are BTreeMap so when we filter the keys, keys with higher precedence come
Expand Down Expand Up @@ -526,6 +527,19 @@ impl<'key> SigningSettings<'key> {
self.shallow = v;
}

/// Whether the signed asset will later be notarized.
///
/// This serves as a hint to engage additional signing settings that are required
/// for an asset to be successfully notarized by Apple.
pub fn for_notarization(&self) -> bool {
self.for_notarization
}

/// Set whether to engage notarization compatibility mode.
pub fn set_for_notarization(&mut self, v: bool) {
self.for_notarization = v;
}

/// Obtain the primary digest type to use.
pub fn digest_type(&self, scope: impl AsRef<SettingsScope>) -> DigestType {
self.digest_type
Expand Down Expand Up @@ -680,7 +694,21 @@ impl<'key> SigningSettings<'key> {
&self,
scope: impl AsRef<SettingsScope>,
) -> Option<CodeSignatureFlags> {
self.code_signature_flags.get(scope.as_ref()).copied()
let mut flags = self.code_signature_flags.get(scope.as_ref()).copied();

if self.for_notarization {
flags.get_or_insert(CodeSignatureFlags::default());

flags.as_mut().map(|flags| {
if !flags.contains(CodeSignatureFlags::RUNTIME) {
info!("adding hardened runtime flag because notarization mode enabled");
}

flags.insert(CodeSignatureFlags::RUNTIME);
});
}

flags
}

/// Set code signature flags for signed Mach-O binaries.
Expand Down Expand Up @@ -1245,6 +1273,7 @@ impl<'key> SigningSettings<'key> {
team_id: self.team_id.clone(),
path_exclusion_patterns: self.path_exclusion_patterns.clone(),
shallow: self.shallow,
for_notarization: self.for_notarization,
digest_type: self
.digest_type
.clone()
Expand Down Expand Up @@ -1352,6 +1381,49 @@ impl<'key> SigningSettings<'key> {
.collect::<BTreeMap<_, _>>(),
}
}

/// Attempt to validate the settings consistency when the `for notarization` flag is set.
///
/// On error, logs errors at error level and returns an Err.
pub fn ensure_for_notarization_settings(&self) -> Result<(), AppleCodesignError> {
if !self.for_notarization {
return Ok(());
}

let mut have_error = false;

if let Some((_, cert)) = self.signing_key() {
if !cert.chains_to_apple_root_ca() && !cert.is_test_apple_signed_certificate() {
error!("--for-notarization requires use of an Apple-issued signing certificate; current certificate is not signed by Apple");
error!("hint: use a signing certificate issued by Apple that is signed by an Apple certificate authority");
have_error = true;
}

if !cert.apple_code_signing_extensions().into_iter().any(|e| {
e == CodeSigningCertificateExtension::DeveloperIdApplication
|| e == CodeSigningCertificateExtension::DeveloperIdInstaller
|| e == CodeSigningCertificateExtension::DeveloperIdKernel {}
}) {
error!("--for-notarization requires use of a Developer ID signing certificate; current certificate doesn't appear to be such a certificate");
error!("hint: use a `Developer ID Application`, `Developer ID Installer`, or `Developer ID Kernel` certificate");
have_error = true;
}

if self.time_stamp_url().is_none() {
error!("--for-notarization requires use of a time-stamp protocol server; none configured");
have_error = true;
}
} else {
error!("--for-notarization requires use of a Developer ID signing certificate; no signing certificate was provided");
have_error = true;
}

if have_error {
Err(AppleCodesignError::ForNotarizationInvalidSettings)
} else {
Ok(())
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1602,4 +1674,24 @@ mod tests {

Ok(())
}

#[test]
fn for_notarization_handling() -> Result<(), AppleCodesignError> {
let mut settings = SigningSettings::default();
settings.set_for_notarization(true);

assert_eq!(
settings.code_signature_flags(SettingsScope::Main),
Some(CodeSignatureFlags::RUNTIME)
);

assert_eq!(
settings
.as_bundle_macho_settings("")
.code_signature_flags(SettingsScope::Main),
Some(CodeSignatureFlags::RUNTIME)
);

Ok(())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCxB3u9PkOjy9K7
vaqeAQ7OUmBEtx01NjzUn4LEe5BTL0/bh02KxG+edlaVOmdeMF8nff89yrP6k3L+
lA7p19uzACxjEYowytUyeLf1RCF9z+6rEeIAqW7+PEYH+yeiOrdSzd5DIaf/PGYk
KxhHyp9OuAS8CDimfvXBRV7PmpKMj+ostOy0Tlj6po1+Nci+SepZRNP/YdFTi0Yd
q9B/FztslFhvX/Ffc8rhmP8GQ8lPJqJOBcLss+aKAz9slFCzVahqmqXu8rz1b3E/
5JHv5RFFBxgvwuXWaiO1mLTn62tIWTiG5rmR0xqJ4VucXk9DvOEAt1CxOBVupXOd
5IDt+qZ7AgMBAAECggEAH8IU6704yzCsjGuZKSFNc6wJgypKfhpNzWMURYVZPeMV
828RdRyKXaYjIEBK/PW2jFIpMP+lTAWZspwDFOZZjoIwdFFYNiqdFqHbdo+TZouf
6Gab4byDoe5ULehbktnvu1YdUnO+PKasOD7W60IpVCjlCIp9Bzltgw+b06iKM9bt
KIPvBXDRgIp6eSBzJFxzj3zmdnSQn1OSF4V4oQt6gbR3NCJIcNyLXL1gEqtKg/AS
swbpOVrPpXWd2x3gND8zvHD90vEiIesDNoSXKlclvLpQ30tkhkFh78xSe5sW7XEL
hIKOExm+VCHoXTdjZnuCkXBkUIkek8T0KNs7A3LgeQKBgQDQXIDvGwzDhR+bdG5C
azKI7SSqvIVpSngfGDCL8OLkYcoOwSClPOwZbY+UCXLppjrkoCD6QwJQ6CyCJwCK
n9hAAWazOQPaerS3zbq2ig+HCQg12zZZ9/oXe7C/sRsq02phxV2+Km+v4uIyalQ9
qGsywA1PN4/mFJ3H0w0QZXV3bwKBgQDZgRhegfulaWvK6x93EJ+UjLYv2d1oBBsn
/87Vif/q14Ms0RNSmUPy1c8WuF8ZYOD4d7S+jcc/92JZWwJK+8pm6pHazdIIPCAP
GCcp9fkS6gOu4twdC6oz34mJ3lqdmHKHz+0hhC2JqV1uwvPS5k/DcKfKR2+ifxQE
xCBocLObtQKBgGWLH17n3OGQiCXXqUB/Q6KNh9gZhh8ZJs9ol4grvje1HKbyIfnF
Zf7CcT2hGTqbQ4pWK5wref56F+7aGR515grTY/ymJaWdNWN6RKtfP0/8695rVeKk
wmIdarcRFf9aBzdc22GpBsM+HCSbwzBFWvDhvdrEZkGn/Hj89xntiEDLAoGAb1sR
r+kafkBv6I7iKCJBoVs9N1hya3uWr67fJSKm/IPj68ELBIHlcOEYSkiQn7yi0XLv
/ZM2zMAKATeAAAXTRUeY7w3rFz45J6E1A92j7JQU2KfbC5/aPv6WOxi1CfRvxqqk
fEFg0xb79+Yl0PcLJUN7FCvosqgfBqWm9fGlcvUCgYAYvALoZPz2z6zzqURDKPWk
lkrW8nQVFg0eeTJNBiU1ipXS28oEGQISdjlYCHD1Ds3W8JHav/i6QnCaAXb5PtHd
DlylI7U+tSZaC83+oPu/BaKVUO9n9e0mqj2oCD7W2gTBazac3d9F1ncfSkEhpDSB
i6+0+GL62SGnzAF52ajf0g==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID4DCCAsigAwIBAgIBATANBgkqhkiG9w0BAQsFADCBizEYMBYGCgmSJomT8ixk
AQEMCGRlYWRiZWVmMTkwNwYDVQQDDDBEZXZlbG9wZXIgSUQgQXBwbGljYXRpb246
IEpvaG4gU2lnbmVyIChkZWFkYmVlZikxETAPBgNVBAsMCGRlYWRiZWVmMRQwEgYD
VQQKDAtKb2huIFNpZ25lcjELMAkGA1UEBhMCWFgwHhcNMjQwMTE3MDI0ODE2WhcN
MzcwOTI1MDI0ODE2WjCBizEYMBYGCgmSJomT8ixkAQEMCGRlYWRiZWVmMTkwNwYD
VQQDDDBEZXZlbG9wZXIgSUQgQXBwbGljYXRpb246IEpvaG4gU2lnbmVyIChkZWFk
YmVlZikxETAPBgNVBAsMCGRlYWRiZWVmMRQwEgYDVQQKDAtKb2huIFNpZ25lcjEL
MAkGA1UEBhMCWFgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxB3u9
PkOjy9K7vaqeAQ7OUmBEtx01NjzUn4LEe5BTL0/bh02KxG+edlaVOmdeMF8nff89
yrP6k3L+lA7p19uzACxjEYowytUyeLf1RCF9z+6rEeIAqW7+PEYH+yeiOrdSzd5D
Iaf/PGYkKxhHyp9OuAS8CDimfvXBRV7PmpKMj+ostOy0Tlj6po1+Nci+SepZRNP/
YdFTi0Ydq9B/FztslFhvX/Ffc8rhmP8GQ8lPJqJOBcLss+aKAz9slFCzVahqmqXu
8rz1b3E/5JHv5RFFBxgvwuXWaiO1mLTn62tIWTiG5rmR0xqJ4VucXk9DvOEAt1Cx
OBVupXOd5IDt+qZ7AgMBAAGjTTBLMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAww
CgYIKwYBBQUHAwMwDgYDVR0PAQH/BAQDAgeAMBMGCiqGSIb3Y2QGAQ0BAf8EAgUA
MA0GCSqGSIb3DQEBCwUAA4IBAQCGzLhwja5AWKrla/VNjhXdKFiendu1VnGBwFQr
WtMVSlXirqjG32WaayFJAjtp3MfXgLD6Yu2isB6I06LimuET4e2jR9nHcxtgZZ0B
iowzgoNi+OA9sUSuZyrgzHwO7+tRmMroaMSURrzwCsffFIiiL8thgvZFZvtCRKm8
7pHzhvIGmjc5BYY8TB3BoWPZiqZAXY1cpw66gPVi2pQ6zitUtv1C3gzwLxQMDtnn
SWm4XWWeihFcL6KgHxPqgnJ9YsRGjDbJ5LcrYk+uinXxSgzhv2Jlq+KD0VxvM6xr
foxvH3jp8ISnTYjajhPQ8zekYE5iZN+GPtF0RjmtQ6RMlweR
-----END CERTIFICATE-----
Loading

0 comments on commit 8e11106

Please sign in to comment.