Integrity validation

Integrity validation is the process of verifying the authenticity of an app and ensuring that its code hasn’t been tampered with.

You can’t trust your own code if it has been modified.

While built-in system security in macOS automatically checks the integrity of every app being launched, apps themselves can (and should) validate their own integrity during runtime.

App integrity

Data checksumming and code signing are two closely related methods that help ensure the integrity and authenticity of an app.

File checksumming

A checksum is small block of data, also known as a hash or digest, computed from another block of data using a cryptographic hash function. Confirming that two checksums computed at different times are the same verifies that the data has not been corrupted or modified during that period. Checksumming is useful for verifying the integrity of a downloaded app when the checksum is known prior to downloading it.

For example, some directly distributed apps, such as Blender and VLC media player, provide checksum hashes that can be verified after downloading, for example, using shasum:

$ shasum -a 256 'vlc-3.0.18-intel64.dmg'
88edcdfceb3bd2f680367d9009d38a0c147fa758f2dd395e43412c1e08ce1ccb

Code signing

Code signing is the process of creating and embedding a digital signature to certify that an app was created by a trusted developer. The code signature includes a digest of the app's contents and a digital certificate that verifies the signer's identity. In the Apple ecosystem, this certificate is issued directly by Apple, acting as a trusted third party known as a certification authority (CA).

The process of signing a macOS app involves creating a digital signature using a developer's private key, obtained from Apple’s Developer Program. This signature is embedded into the app's binary. When the app is downloaded or executed, the macOS operating system checks the signature, verifying it with the corresponding public key. If the signature is valid, the system confirms that the app has not been altered since signing, ensuring its integrity and authenticity.

An app’s code signature can be verified using the codesign command:

$ codesign --deep --verify --verbose '/System/Applications/Calculator.app'
/System/Applications/Calculator.app: valid on disk
/System/Applications/Calculator.app: satisfies its Designated Requirement

Gatekeeper protection

MacOS includes a layered security system that offers comprehensive protection against various threats. One of its components is Gatekeeper, which prevents unauthorized code execution by allowing only apps downloaded from the App Store or signed by verified developers to run on the system. Prior to launching an app, Gatekeeper examines the signing developer's certificate and verifies that the app's content hasn't been modified since it was signed.

It is possible to disable Gatekeeper, which allows for the unrestricted launching of apps with invalid or unverified code signatures. In fact, often, the use or pirated apps requires disabling Gatekeeper, which opens a door into a bunch of security issues.

Digest:

Checksum validation is an effective method for verifying an app's integrity, but unlike code-signing, checksumming cannot validate the app’s authenticity.

Code-signing provides a higher level of security by verifying the authenticity of the app and ensuring that it hasn't been tampered with since being signed.

App integrity and authenticity validation is critical part of any operating system, as execution of unauthorized code can compromise the system's behavior and exploit user's data.

Integrity validation is equally critical for paid apps, as tampered code can disable license validation or introduce malicious behavior, impacting revenue and damaging reputation.

File checksum validation

Checksums are an effective way to manually verify the integrity of a file. However, it is not possible to embed a checksum hash value within the file that it was computed for. This creates a logical paradox because any changes made to the file will result in a change to the checksum, which would render the embedded hash value invalid. One approach for validating checksums would be to use server-side checks.

  1. An app calculates the checksum hash of it’s binary and any sensitive resources, like embedded libraries and frameworks.

  2. The hash value is sent to the server where the original correct hash value is already known, and compares both.

Digest:

Server-side checksum validation can be a good option for enhancing security, but it would be inefficient as a first step solution.

Introducing networking for handling sensitive information would require additional security enhancements, which can further complicate protection measures.

Code signature validation

Validating the code signature is the most important security check that every app should implement, but it can also be the most challenging one. The main issue with code signing is that anyone can re-sign a modified app with their own certificate, which can bypass the loose integrity validation. Furthermore, when a verified developer re-signs the app, it is even possible to avoid triggering Gatekeeper.

Verified Apple developers would not want to sign and distribute apps created by others, as it could result in legal implications. However, it is very easy to re-sign pirated apps for personal use and trick Gatekeeper into treating them as legit.

The simplest code signature validation can be implemented using the Apple’s Security framework using SecStaticCodeCheckValidity function. However, to counteract unauthorized re-signing, the signer's identity must also be validated. This can be done by validating the public key of the signing certificate.

Public key validation

A public key is contained within a signing certificate that is embedded into a code signature. This public key has several purposes, including identifying the developer who signed the code and establishing if that developer can be a trusted.

To ensure that the app is not only correctly signed, but also signed by the correct developer or app store, it can compare the public key of its signing certificate with the expected value. This is similar to SSL pinning in network security, where the public key of a trusted certificate is hardcoded into the app and compared with the server's key to establish trust in the connection.

import Foundation
import Security
// 01. Declare placeholders for error handling. Security error details can be found at https://opensource.apple.com/source/Security/Security-59754.80.3/base/SecBase.h.auto.html
var result: OSStatus
var error: CFError?
// 02. Create static code object for the standard Calculator app.
var staticCode: SecStaticCode?
result = SecStaticCodeCreateWithPath(URL(filePath: "/System/Applications/Calculator.app") as CFURL, SecCSFlags(), &staticCode)
guard result == errSecSuccess else { fatalError("Failed to create static code with error code: \(result)") }
// 03. Check static code validity.
result = SecStaticCodeCheckValidity(staticCode!, SecCSFlags(), nil)
guard result == errSecSuccess else { fatalError("Failed to check static code validity with error code: \(result)") }
// 04. Extract code signing information from the static code object.
var information: CFDictionary?
result = SecCodeCopySigningInformation(staticCode!, SecCSFlags(rawValue: kSecCSSigningInformation), &information)
guard result == errSecSuccess, let information else { fatalError("Failed to copy code signing information with error code: \(result)") }
// 05. Extract certificates, this can be nil, for example, if a code was signed for running locally.
let certificates = (information as? [CFString: Any])?[kSecCodeInfoCertificates] as? [SecCertificate]
guard let certificates else { fatalError("Failed to get certificates from code signing information") }
// 06. Create trust object from certificates.
let policy = SecPolicyCreateBasicX509()
var trust: SecTrust?
result = SecTrustCreateWithCertificates(certificates as CFArray, policy, &trust)
guard result == errSecSuccess, let trust else { fatalError("Failed to create trust object with error code: \(result)") }
// 07. Configure trust object to allow expired certificates.
result = SecTrustSetOptions(trust, [.allowExpired, .allowExpiredRoot])
guard result == errSecSuccess else { fatalError() }
// 08. Evaluate trust object.
let isTrusted = SecTrustEvaluateWithError(trust, &error)
guard isTrusted else { fatalError("Failed trust evaluation with error: \(error!)") }
// 09. Extract leaf certificate public key object from the trust object.
let publicKey = SecTrustCopyKey(trust)
guard let publicKey else { fatalError("Failed to copy leaf certificate public key") }
// 10. Extract external representation data from the key object as Base64 string.
let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data?
guard let publicKeyData else { fatalError("Failed to copy external representation of the public key") }
let publicKeyString = publicKeyData.base64EncodedString()
// 11. Compare actual and expected public keys. The Calculator app is part macOS
// and signed with the Software Signing certificate, see below for details…
print("Public key is valid:", publicKeyString == "MIIBCgKCAQEAvzC4dJhPrgYLpJRuMVRtCdE8o7A5EKnQ5qpgIl3T5ISiQEGQZMZgPZFh2lKoRSeeLGTL5B6oJApSOrPT7BZBeMGe0QbmeCVjCPPfykHmHf416VFcExWL6dGdcXvRyapLnpnaj8ApuC+Qb6S7ZY+Aatc9aG3keMKlEi/4Mul+yV1cjD4WIBvXXp0bhi0I97wZpE0OQaRK22sagEVQBKlMjRUH6monvbuInjw/slI5l+O3yOtUUpiKLyNWZeXpKtlRtCP6BN3366Df1ZryXgjxX4fwsUdsdbGxjNET43rB7GJBdKaDNPH5WnRkOT9Zbs8swGCDKTzn3k9fDfOTOV8wjQIDAQAB")
// Apple usually uses the following signing certificates:
//
// - Certificate: Software Signing
//   Public key:  MIIBCgKCAQEAvzC4dJhPrgYLpJRuMVRtCdE8o7A5EKnQ5qpgIl3T5ISiQEGQZMZgPZFh2lKoRSeeLGTL5B6oJApSOrPT7BZBeMGe0QbmeCVjCPPfykHmHf416VFcExWL6dGdcXvRyapLnpnaj8ApuC+Qb6S7ZY+Aatc9aG3keMKlEi/4Mul+yV1cjD4WIBvXXp0bhi0I97wZpE0OQaRK22sagEVQBKlMjRUH6monvbuInjw/slI5l+O3yOtUUpiKLyNWZeXpKtlRtCP6BN3366Df1ZryXgjxX4fwsUdsdbGxjNET43rB7GJBdKaDNPH5WnRkOT9Zbs8swGCDKTzn3k9fDfOTOV8wjQIDAQAB
//   Expires:     24 October 2026
//   Details:     Apple-signed, used for macOS apps and components.
//
// - Certificate: Apple Mac OS Application Signing
//   Public key:  MIIBCgKCAQEAoScULiMkhV8fkhdlMoq1V2kftKUbQztiXBRF9hz6KInqZd4DPf858OS7UZ6+DHDFlBV2FBIxatR5lRYBMDLXPQiqvEmRmFqc6bdpgdjjnjDUaZiYSb0PB+25Ib8ekF8+AhR3PdAgegq2RTzB1wwlRNXxzALVyI6HpLPl9DogGUv9f/mFYycY7jSddj1el1slhWYmyCsZ00CBDGxOMrqilIFQ8SBaDwwtQVfkC1UuOiTL4AegNTyiAcQOBZUz7j7iaK4R9UmvWBsoZjXpgnvIl+QJ5xFGJJmiJQHNH42VNXiJXmeeU2L2SuhnmbCbqwv7azakzOCoSm5gHWT9cV8uBwIDAQAB
//   Expires:     31 August 2024
//   Details:     Apple-signed, used for macOS apps available on the Mac App Store.

With great power comes great responsibility... The above code uses two crucial trust option flags: .allowExpired and .allowExpiredRoot. The reason for this will be discussed in more detail below.

Public key extraction

On macOS, there is no straightforward way to obtain a public key from a certificate in the same format as returned by SecKeyCopyExternalRepresentation. The following OpenSSL command comes to help, it takes a certificate file in DER format and prints a trimmed public key:

$ openssl x509 -pubkey -noout -in='certificate.cer' | \
awk '/-END PUBLIC KEY-/ { p = 0 }; p; /-BEGIN PUBLIC KEY-/ { p = 1 }' | \
awk -v d='' '{s=(NR==1?s:s d)$0}END{print s}' | \
sed 's/^MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A//'
MIIBCgKCAQEAoScULiMkhV8fkhdlMoq1V2kftKUbQztiXBRF9hz6KInqZd4DPf858OS7UZ6+DHDFlBV2FBIxatR5lRYBMDLXPQiqvEmRmFqc6bdpgdjjnjDUaZiYSb0PB+25Ib8ekF8+AhR3PdAgegq2RTzB1wwlRNXxzALVyI6HpLPl9DogGUv9f/mFYycY7jSddj1el1slhWYmyCsZ00CBDGxOMrqilIFQ8SBaDwwtQVfkC1UuOiTL4AegNTyiAcQOBZUz7j7iaK4R9UmvWBsoZjXpgnvIl+QJ5xFGJJmiJQHNH42VNXiJXmeeU2L2SuhnmbCbqwv7azakzOCoSm5gHWT9cV8uBwIDAQAB

A Developer ID Application — type certificate, used for code signing apps distributed outside of Apple's App Stores, can be downloaded directly from the Apple Developer portal. However, Software Signing and Apple Mac OS Application Signing certificates are not openly published and can only be extracted from existing signed apps using, for example, the codesign command:

$ codesign --display --extract-certificates='certificate_' '/System/Applications/Calculator.app'
Executable=/System/Applications/Calculator.app/Contents/MacOS/Calculator

This will export all signing certificates in the chain prefixed with certificate_, where certificate_0 would be the leaf certificate, and one with the highest index, usually certificate_2, the creative authority (CA) certificate.

App store re-signing

While directly distributed apps are signed with the developer's certificate, apps distributed via app stores can be re-signed with their own certificates. For example, this is the case with Apple's App Stores, where 3rd party macOS apps are signed with Apple Mac OS Application Signing certificate.

When validating the signer's identity and verifying the public key, it is important to remember that they may differ due to this re-signing process.

Certificate expiration support

Code signing certificates have a predetermined lifetime, and expired certificates, by default, would cause the SecTrustEvaluate… calls and, consequently, the validation to fail.

Not allowing expired signing certificates in integrity validation can result in serious issues. Depending on the severity of the validation failure action, the app may become completely dysfunctional. Fixing the issue can be extremely challenging, especially in older app versions that are no longer actively maintained.

To not wake up one morning and find hundreds of support emails saying that the app is no longer working, the SecTrust object must be configured to accept expired certificates, including the root certificate:

SecTrustSetOptions(trust, [.allowExpired, .allowExpiredRoot])

Digest:

If the app is pirated and does not verify its own integrity, Gatekeeper might not be able to prevent it from being launched and used, as it can be easily disabled.

Code signature validation alone doesn’t check if an app was re-signed, the signer identify must be explicitly validated.

Not allowing expired signing certificates in integrity validation can result in a dysfunctional app that is challenging to fix.

Computing a data digest and validating a code signature can be computationally intensive tasks, which can indirectly aid in identifying them during reverse engineering.

Calling standard Security functions directly can be easy to detect during reverse engineering and should be obfuscated to counteract it.