The Complete Guide to Code Signing: From Keys to Deployment

Code signing has become a crucial cornerstone of modern software distribution, yet many developers still find the process mysterious or intimidating. Think of code signing as a digital passport for your software—it proves the authenticity of your code and ensures it hasn’t been tampered with since its creation. In today’s landscape of increasing security threats and malware attacks, understanding and implementing proper code signing isn’t just a nice-to-have—it’s essential for building trust with users and meeting platform requirements.

In this comprehensive guide, we’ll peel back the layers of the code signing process, exploring everything from obtaining certificates to implementing signing in your deployment pipeline. Whether you’re a seasoned developer looking to strengthen your security practices or a newcomer trying to publish your first application, this guide will equip you with the knowledge and practical steps needed to master code signing.

https://developer.apple.com

Basics & Pre-requisites:
  1. MacBook
  2. Xcode
  3. Apple Developer Program
  4. CSR (Certificate Signing Request)
  5. Certificates
  6. App Identifier 
  7. Provisioning profiles
  8. IPA to be signed

Let’s get going, then…


1. Why Code Signing?

When you install a new version of a code-signed application, the operating system recognizes that the updated version carries the same digital signature as its predecessor. As a result, macOS does not repeatedly prompt the user for permissions—such as access to the keychain—if those permissions were already granted. Because the digital signature is consistent, the system treats the updated app the same as the old one.

Other macOS security features, including the App Sandbox and parental controls, also depend on code signing. By verifying code signatures, the operating system can:

1. Confirm code integrity:

The system can detect even the smallest unauthorized modification to the code—whether caused by a malicious attacker or an accidental file corruption. If the code signature is intact, the system can be confident that the code is unchanged and trustworthy.

2. Identify code origin:

The code signature embeds cryptographic information linking the code to a specific developer or signer, allowing the system and the user to know exactly who created it.

3. Establish trust across versions:

By maintaining a consistent signature, a developer can indicate that a new app version should be trusted as much as its predecessor, ensuring a seamless update process without re-prompting the user for permissions.

Limitations of Code Signing

While code signing is an essential part of a broader security strategy, it does not cover every possible threat. Specifically, it does not:

• Ensure code is free from security vulnerabilities.

• Prevent an application from loading unsafe or modified code (such as untrusted plug-ins) after it has started running.

• Serve as a form of digital rights management (DRM) or copy protection. Code signing does not hide or encrypt the application’s content.


2. How Code Signing Works

When we talk about code signing, we are referring to a verification process that ensures any piece of software—be it an application, script, or update—has not been tampered with and truly originates from the source it claims to. This concept can be broken down into three integral parts: the seal, the digital signature, and the code requirements. Each plays a crucial role in maintaining integrity, authenticity, and alignment with established security policies.

1. The Seal: Imagine you have a sealed envelope containing a confidential document. If the seal is broken, you immediately know someone might have interfered with the contents. In code signing, this “seal” represents the guaranteed, intact state of the code at the moment it was signed by the developer. Just as a wax seal on a letter provides a physical guarantee that no one else has peeked inside, the code’s cryptographic seal ensures that from the time the developer finalized the code to the moment it reaches the end user’s system, no unauthorized changes have occurred. If someone tries to alter even a single character, the seal “breaks” and the receiving system will detect the modification, treating it as suspicious or outright rejecting the code.

2. The Digital Signature: Beneath the concept of the seal lies the more technical mechanism of a digital signature. This is a mathematical process that uses a pair of cryptographic keys—a private key held secretly by the developer and a corresponding public key usually verified through a trusted authority (often a Certificate Authority). When code is signed, it is combined with the developer’s private key to create a unique cryptographic signature. On the user’s end, the public key is used to verify this signature, confirming that the code indeed comes from the stated developer and has not been tampered with. This process works like a fingerprint: just as each individual’s fingerprint is unique and can confirm their identity, the digital signature uniquely identifies the developer and the integrity of their code. If anyone tries to alter the code after signing, the verification will fail, signaling to users or automated systems that the software is not trustworthy.

3. Code Requirements: Beyond authenticity and integrity checks, code signing also integrates the concept of code requirements. These requirements are a set of predefined rules and conditions that the code must meet before it’s allowed to run on a device or system. They can dictate which frameworks the application can use, which permissions it can request, or even the specific environments in which it can safely operate. For example, one requirement might ensure the code only runs on certain operating system versions or only when the application does not access restricted hardware components. These conditions help maintain a stable, secure ecosystem by enforcing policies that guard against malicious behavior, excessive resource usage, or instability caused by incompatible components. Essentially, code requirements act like a checklist, ensuring that even if the code is authentic and untampered, it must also adhere to established best practices and security guidelines before being trusted to run.

By combining the protective seal, the cryptographic certainty of a digital signature, and the orderly enforcement of code requirements, code signing provides a holistic security framework. It reassures end users that the software they install is genuine and safe, while also helping developers maintain control and ensure that their code runs only in the ways and places they intend.

Code Signature Evaluation: Evaluating a code signature involves verifying both the integrity of the signed software and the authenticity of its origin. When a user or a system receives signed code—such as an application installer, an operating system patch, or a library—it conducts a thorough check against a trusted certificate authority’s public key. First, it verifies that the digital signature mathematically matches what the developer’s private key created, ensuring that the code hasn’t been modified since it was signed. If this verification fails due to any unauthorized change, the code’s integrity is questioned and typically rejected. Next, the evaluator checks the code’s adherence to predefined code requirements, confirming it meets conditions like proper entitlements, approved frameworks, or designated execution environments. By ensuring both authenticity and compliance, the evaluation process not only detects potentially malicious or tampered code before it runs, but also safeguards user trust in the broader software ecosystem.


3. Securing Your Code with Digital Signatures

Code signing provides this assurance through digital signatures that cryptographically verify a program’s origin and integrity. These signatures create a verifiable link between software and its developer, backed by trusted certificate authorities. This chain of trust protects users from modified or counterfeit software while giving developers a robust way to establish their software’s legitimacy and distribute it securely.

Establishing a Developer Identity: Before you can confidently sign and distribute your software, you need to establish a verifiable identity as a trusted developer. This process typically involves obtaining digital certificates from a recognized authority, such as a platform vendor or a third-party certificate provider, which act as cryptographic proof linking your code to your unique credentials. By securing a legitimate identity, you reassure users and the operating system that your applications come from a known and reliable source, ultimately laying the groundwork for building user trust and maintaining the integrity of your software distribution.

Obtaining Your Signing Identities: Getting started with code signing begins with acquiring the appropriate digital certificates through the Apple Developer Program. Upon joining, you gain access to the developer portal where you can generate various types of certificates. These include Developer ID certificates for direct Mac app distribution, Mac App Distribution certificates for Mac App Store submissions, and iOS Distribution certificates for iOS App Store deployment.

Managing these certificates can be approached in two ways. The simpler route is through Xcode, which handles the entire process automatically – from generating keys to managing renewals. This streamlined approach integrates seamlessly with the developer portal, making it ideal for developers focused on Apple platforms. Alternatively, you can manage certificates manually using the Keychain Access Certificate Assistant, which gives you more control over the process. This tool helps you generate public/private key pairs, create certificate signing requests, and maintain your credentials in your system keychain.

For enterprises and organizations with existing certificate infrastructure, there’s flexibility in certificate sourcing. The macOS codesign command supports standard certificate formats, allowing you to use internal or third-party certificates for code signing. However, it’s crucial to note that while these certificates work for signing code, distributing through the App Store or participating in the Developer ID program specifically requires Apple-issued certificates.

Requesting a verified certificate using certificate assistant
  • Open Applications > Utilities > Keychain Access.
  • From the Keychain Access menu, choose Certificate Assistant > Request a Certificate from a Certificate Authority.
  • Fill in your email address and a name for the certificate, and select Saved to disk. Then click Continue.
  • Certificate Assistant generates the public and private keys and stores them in your keychain, while storing the matching certificate request on disk.
  • Upload the certificate request to the certificate authority (for example, Apple developer portal).
  • Download the generated certificate (a file with a cer extension).
  • Open the certificate file by double clicking on it. Keychain Access imports the certificate and associates it with the corresponding private key you created earlier.
Creating a Self-Issued Certificate for Testing
  • Open Applications > Utilities > Keychain Access.
  • From the Keychain Access menu, choose Certificate Assistant > Create a Certificate.
  • Fill in a name for the certificate. This name appears in the Keychain Access utility as the name of the certificate.
  • Choose Self Signed Root from the Identity Type pop-up menu.
  • Choose Code Signing from the Certificate Type pop-up menu.
  • Check the Let me override defaults checkbox. Click Continue.
  • Click “Continue” when prompted with warning.
  • Specify a serial number for the certificate.Any number will do as long as you have no other certificate with the same name and serial number.
  • Click Continue.
  • Fill in the information for the certificate. Click Continue.
  • Accept the defaults for the rest of the screens.
Integrating Info.plist

The system reads code requirements from an application bundle’s Info.plist file to establish the designated requirement by default. While single-file tools typically don’t include an Info.plist, you have the option to add one to specify custom requirements.

To configure code signing requirements:

  1. Create and Set Up Info.plist
    • Add a new Info.plist file to your project structure
    • Configure the essential identifiers:
      • Set CFBundleIdentifier – This acts as your program’s unique signature
        • Use reverse DNS format (e.g., com.companyname.productname)
        • Must be globally unique
        • Follow pattern: domain.company.product
    • Set CFBundleName – Your app’s display name
      • Keep it under 16 characters
      • Typically matches the target name
  2. Configure Xcode Settings
    • Navigate to target’s Build Settings
    • Enable “Create Info.plist Section in Binary”
    • Specify the path to your Info.plist file
Code Signing Distribution Packages

Creating securely signed distribution packages requires attention to multiple package types. Here’s a comprehensive look at how to implement signing for different distribution formats:

Package Installers:

When preparing installer packages for distribution, you’ll need to handle signing manually since this falls outside of Xcode’s automated processes. The productbuild command line tool enables you to create and sign installation packages. The process combines your distribution file with your signing identity to generate a secure .pkg file:

productbuild --distribution YourProduct.dist --sign "Your Identity" YourProduct.pkg
Remember that package integrity is crucial - any modification to a signed package will invalidate its signature, protecting users from tampered installers.

Disk Image Distribution:

For distributing software bundles, signed disk images provide a secure alternative to older archive formats. Modern macOS versions support code signing for read-only compressed disk images, enhancing security during software distribution. To apply a signature to your disk image, use the codesign utility:

codesign -s "Your Identity" YourDiskImage.dmg

An important security consideration arises when distributing applications via disk images. If your application references external resources (like libraries or scripts) using relative paths, you introduce potential security vulnerabilities. This is because these external resources, living outside the app bundle, aren’t protected by the application’s signature.

Modern macOS addresses this security concern through path randomization – when launching an application from an unsigned disk image, the system copies it to a random location, preventing potentially malicious external resource substitution. However, by signing your disk image, you create a trusted container where path randomization isn’t necessary since all contents are protected by the signature.

Testing Your App’s Security Compliance

Before distributing your application, it’s essential to verify its compliance with macOS security measures, particularly Gatekeeper. This security framework serves as a vital checkpoint for applications downloaded from the internet, whether through websites or email attachments. Its primary role is to protect users by validating applications before their first launch on any system.

Understanding Gatekeeper’s Role:

Gatekeeper acts as a security expert for your Mac, examining code signatures to verify both the integrity and authenticity of applications. By default, it permits applications from two trusted sources: the Mac App Store and developers with valid Developer ID signatures. This verification process includes extensive checks of the application bundle’s structure and dependencies.

Key Security Requirements

Modern versions of macOS enforce strict security requirements through Gatekeeper. All code resources must carry valid signatures, and applications need proper structural organization. External dependencies face particular scrutiny – applications must contain all required libraries within their bundle, as Gatekeeper prevents loading libraries from external locations.

Bundle Structure Validation

The system performs thorough verification of your application bundle’s structure. Symbolic links within your application receive special attention – they must either point to valid internal locations or specifically approved system directories like /System or /Library. Any broken links or references to unauthorized locations will trigger rejection.

Testing Best Practices

For applications distributed outside the Mac App Store, thorough testing against Gatekeeper’s requirements becomes crucial. This involves verifying:

  • Complete code signature coverage across all bundle components
  • Proper containment of all required libraries within the bundle
  • Valid symbolic link configuration
  • Appropriate bundle structure and resource organization

4. Manual Code Signing Guide

While Xcode typically handles code signing automatically through its interface, understanding how to sign code manually gives you greater control over the process. Let’s explore how to use command-line tools for code signing when you need more precise control over the signing process.

Basic Code Signing Command

The fundamental command for code signing uses the codesign utility. Here’s the basic syntax:

codesign -s "Your Identity" "Path/To/Your/Code"

This command can sign either a bundle folder or a specific binary file. To receive feedback about the signing process, add the verbose flag:

codesign -s "Your Identity" -v "Path/To/Your/Code"
Working with Identities

When specifying your signing identity, you can use any unique portion of your certificate’s common name. The system will search your keychains for a matching certificate. Remember that these matches are case-sensitive, so accuracy is important.

Adding Requirements

You can enhance security by adding specific requirements to your code signature. These requirements define conditions under which your code can run. For instance, to specify requirements from a file:

codesign -s "Your Identity" -r @requirements.txt "Path/To/Your/Code"

For immediate requirement specification, use the equals prefix:

codesign -s "Your Identity" -r="your requirements here" "Path/To/Your/Code"

Custom Certificate Integration

When working with custom certificate hierarchies, you can incorporate your certificate anchor into the signing process. This creates a designated requirement specific to your organization:

codesign -s "Your Identity" -r="designated => anchor path/to/cert and identifier com.yourcompany.appname" "Path/To/Your/Code"

5. Application Notarization

Mac software notarization is a critical security feature that enhances user trust in applications distributed outside the App Store. When you notarize your application, you’re submitting it to Apple’s automated security screening service, which thoroughly examines the software for potential security risks and malicious content.

Unlike the comprehensive App Store review process, notarization is a streamlined, automated security check. The service rapidly analyzes your code-signed software and provides quick feedback. Upon successful verification, the service generates a special verification ticket. This ticket can be attached directly to your software (a process called “stapling”) and is also stored on Apple’s servers for online verification.

The benefits become clear when users interact with your software. When someone downloads and attempts to run your notarized application, macOS Gatekeeper checks for this verification ticket either locally (if stapled) or online. Finding a valid ticket allows Gatekeeper to confirm that the software has passed Apple’s security checks, providing users with clear information about the software’s origin and security status before they decide to run it.

This security measure serves two important purposes:

  • It provides developers a way to distribute software outside the App Store while maintaining user trust
  • It helps protect users from potentially harmful software
Notarization Requirements

For successful notarization of your macOS software, you’ll need to meet several key security requirements:

  • Code Signing Configuration:
    • All distributed executables must be code-signed
    • Signatures must use Developer ID certificates
    • Avoid using Mac Distribution, ad hoc, or local development certificates
    • Include secure timestamps in signatures (automatic in Xcode distribution)
  • Runtime Security:
    • Enable Hardened Runtime for all applications and command-line tools
    • Avoid enabling the task-allow entitlement (com.apple.security.get-task-allow)
    • Configure proper XML formatting for ASCII-encoded entitlements
  • Technical Requirements
    • Ensure valid code signatures across all components
    • Maintain proper formatting for all security entitlements

The notary service employs various methods to evaluate older software, potentially applying different criteria based on the software’s age and characteristics.

Automatic App Notarization with Xcode

Here’s how to set up automatic notarization during your app’s distribution process:

Using Xcode’s Organizer

  1. Prepare for Distribution
  • Open Xcode’s Organizer window
  • Select your archived app
  • Choose “Distribute App”
  1. Configure Distribution Settings
  • Select “Developer ID” as your distribution method
  • Enable automatic notarization option
  • Ensure your Apple ID credentials are properly configured
  1. Distribution Process
  • Xcode handles code signing with your Developer ID
  • Automatically submits the app for notarization
  • Manages the notarization ticket stapling process
  • Creates the final distributable version

The advantage of this automated approach is that it combines signing, notarization, and stapling into a single workflow, reducing the chance of errors that might occur with manual processes.

Let’s look at how notarization works using sdnotary2. This tool simplifies the notarization process by handling the communication with Apple’s notary service. Here’s the workflow:

Using SD Notary 2

The SD Notary 2 window consists of three parts.

At the top, you choose the name of the Developer ID signing identity you wish to use. Unless you are a member of more than one team, there will only be a single entry.

Below that you can choose whether to create and submit a disk image of your app, and if so whether to use DropDMG.app, and advanced settings for enclosures and extra entitlements. To the right, you can set the basic entitlements the notarized file will have.

The next part is the log area, where progress and feedback occurs. This is especially important when problems are encountered.

At the bottom are four buttons, for the four tasks SD Notary 2 can perform.

The Fetch History button will fetch the details of recent notarization sessions, and whether they succeeded.

The Sign Only… button lets you sign a file for notarization, without submitting it for notarization. You might do this before assembling files on a disk image, for example.

The Staple Only… button lets you staple a file that has already been notarized. You might use this when a session times out, or you lose your Internet connection after the file has been sent for notarization.

The Submit… but is where you select a file and submit it for notarization.

The first time you submit a file, or fetch details of recent sessions, you will see a dialog asking you to enter your Apple ID and the app-specific password you have set up for notarytool as described above. These credentials will be verified, and if correct will be securely stored in your login keychain. The only time you may be asked for them again is if you use a different Developer ID.

(These credentials are not stored in the application, and cannot be retrieved from your login keychain by either SD Notary 2 or Keychain Access. If you think you need to use your app-specific password again – on a different computer, for example, or a different account – you should ensure you also keep a copy elsewhere.)

When notarization succeeds, macOS will recognize your application as verified by Apple. This verification appears to users in several ways – from the initial download, where they’ll see that your application is verified by Apple, to the first launch, where macOS confirms the application’s security credentials.

This process becomes particularly important when distributing your application outside the Mac App Store. Without notarization, users might face additional security warnings, or on newer versions of macOS, might not be able to run your application at all.


Packaging Software for Mac Distribution

When building with Xcode or third-party tools, you need to consider various packaging methods for Mac software distribution. While Xcode handles common scenarios like App Store submissions, you might need additional steps for:

  • Non-app products
  • Multi-component software (like apps with daemons)
  • Direct distribution products
  • Third-party tool builds

For these cases, you’ll need to select an appropriate container format and prepare your product for distribution. This involves signing your code, creating a distribution container, and notarizing the final package. Consider automating this process for consistent handling across different versions.

When building with Xcode or third-party tools, you need to consider various packaging methods for Mac software distribution. While Xcode handles common scenarios like App Store submissions, you might need additional steps for:

  • Non-app products
  • Multi-component software (like apps with daemons)
  • Direct distribution products
  • Third-party tool builds

For these cases, you’ll need to select an appropriate container format and prepare your product for distribution. This involves signing your code, creating a distribution container, and notarizing the final package. Consider automating this process for consistent handling across different versions.

App Store Distribution

  • Uses installer package format
  • Managed through standard submission process

Direct Distribution

You have three main container choices:

  • ZIP Archive (.zip)
    • Basic compression format
    • No signing capability
    • User extracts via Finder
    • Note: Unsigned components risk tampering
  • Disk Image (.dmg)
    • Supports code signing
    • Protects contents post-signing
    • Ideal for single-file products
    • Simple user experience
  1. Installer Package (.pkg)
  • Required signing
  • Guided installation process
  • Best for:
    • Multiple components
    • Specific location requirements
    • Custom installation scripts

Testing Your Distribution Package

Before releasing your directly-distributed software, conduct thorough testing across different scenarios. Use a separate testing machine to avoid development environment interference.

Basic Testing Scenarios

  • Fresh Installation
    • Test on a Mac without previous versions
    • Verify first-time user experience
    • Check all functionality works as expected
  • Upgrade Testing
    • Install over existing older version
    • Install alongside older version
    • Verify data migration if applicable
    • Check for version conflicts
  • User Account Testing
    • Test with different user accounts
    • Verify installer behavior across accounts
    • Check application permissions

For ZIP and DMG Distributions

  • Launch Location Testing
    • Test direct launch from distribution location
    • Verify behavior with Gatekeeper path randomization
    • Check subsequent launches for consistency
  • Relocated Installation Testing
    • Move application to different locations
    • Test functionality after relocation
    • Verify resource access and permissions
  • Check for path-dependent issues

Key Verification Points

  • Resource accessibility
  • File permissions
  • External component connections
  • Data storage and access
  • Update mechanism functionality

Conclusion

Code signing and notarization are essential steps in creating a trusted, secure distribution pipeline for your Mac software. By implementing these security measures, you not only protect your users but also demonstrate your commitment to software security and integrity.

Let’s recap the key points:

  1. Code Signing Fundamentals
  • Provides cryptographic validation of your software
  • Ensures code integrity and authenticity
  • Establishes developer identity and accountability
  1. Notarization Process
  • Adds Apple’s security verification layer
  • Protects users from malicious software
  • Streamlines the user trust experience
  1. Distribution Packaging
  • Choose appropriate container formats
  • Implement proper signing at each level
  • Consider user installation experience
  1. Comprehensive Testing
  • Verify across multiple scenarios
  • Test different installation patterns
  • Ensure consistent security behavior

Looking ahead, these security practices will continue to evolve as new threats emerge. Staying current with best practices and security requirements isn’t just about meeting technical specifications—it’s about maintaining user trust and providing a secure, reliable software experience.

Remember: security isn’t a final destination but an ongoing journey. Regular updates, proactive security measures, and thorough testing will help ensure your software remains both secure and accessible to your users.


Categories: Intune, iOS-iPadOS, macOS, Swift, Xcode

Leave a Reply

Cookies Notice

Intune - In Real Life, uses cookies. If you continue to use this site it is assumed that you are happy with this.

Discover more from Intune - In Real Life

Subscribe now to keep reading and get access to the full archive.

Continue reading