Skip to content
academia.sh

Lesson 13 / 19

Signing and Provisioning Profiles

A measurement showing that comparing a package's entitlements with a recorded list passes the faulty version and rejects the correct one, while certificate and profile checks stay blind.

Published Updated

In this lesson

  • Distinguish the separate roles of the certificate, the provisioning profile, and the entitlements file in signing
  • Measure how an equality check against a recorded list passes the faulty package and rejects the correct one
  • Verify that checks that look only at the certificate or only at the profile cannot see the fault
  • Show that the same signing check can be blind, noisy, or inverted depending on the counter and the profile
Contents

Until the previous lesson, the checks read code: tests, linters, formatters. From this lesson on, the checks move to the package after it has been built. Before a package reaches a device or the store, it is matched against three things. The certificate says who signed the package. The provisioning profile is a signed file that lists which bundle identifier may run with which entitlements and which certificates. The entitlements file states which entitlements the package itself asks for. Signing looks at whether these three agree.

The question of this course is not what it means when a check passes or fails. The question is whether the two answers differ when the same check runs against the correct version and against the faulty version. The measure is called separation, and it has four categories: separating, where the correct version passes and the faulty version fails; blind, where both pass; noisy, where both fail; and inverted, where the correct version fails and the faulty version passes. In the output they appear as separating, blind, noisy, and inverted. This lesson measures with the core file.

// Separation.swift — classifies whether a check tells the correct version from the faulty one
struct Contrast<Output: CustomStringConvertible> {
    let label: String
    let correct: Output
    let faulty: Output
    let passesCorrect: Bool
    let passesFaulty: Bool

    var separation: String {
        switch (passesCorrect, passesFaulty) {
        case (true, false): return "separating"
        case (true, true): return "blind"
        case (false, false): return "noisy"
        case (false, true): return "inverted"
        }
    }

    func describe() -> String {
        let onCorrect = passesCorrect ? "pass" : "fail"
        let onFaulty = passesFaulty ? "pass" : "fail"
        print("\(label) | correct=\(correct)/\(onCorrect) faulty=\(faulty)/\(onFaulty) -> \(separation)")
        return separation
    }
}

func contrast<Output>(_ label: String, correct: Output, faulty: Output, check: (Output) -> Bool) -> Contrast<Output> {
    Contrast(label: label, correct: correct, faulty: faulty, passesCorrect: check(correct), passesFaulty: check(faulty))
}

func countSeparations(_ results: [String]) {
    for name in ["separating", "blind", "noisy", "inverted"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}

This lesson’s fault is the following: the app asks for the remote notification entitlement, but the package does not carry it. One entry is missing from the entitlements file, and the package still builds, gets signed, and opens. The gap shows up when the app tries to register for remote notifications. A real entitlements file looks like the one below; the model keeps entitlements under its own names, which are not the key names of the real file.

<dict>
    <key>aps-environment</key>
    <string>production</string>
</dict>

The entitlements a package actually carries are read with the codesign -d --entitlements :- Shop.app command. This lesson does not measure the output of that command; what it measures is the signing decision, built as a model in plain Swift. The model builds what the check says about the two versions; it is not the signing tool itself.

The Signing Model

The model holds three values: the package’s identifier and entitlements, the profile’s allowances and expiry counter, and the certificate’s expiry counter. Time is modeled not by a clock but by a version counter: an expiry value counts as expired when the counter reaches or passes it. effective returns the entitlements a signed package can actually use: the intersection of what the package asks for and what the profile allows. sign makes the decision and rejects any entitlement the profile does not match: if the package carries an entitlement that is not in the profile, the signature is not accepted.

// Signing.swift — models a package's entitlements, the certificate, and the provisioning profile
struct Package {
    let identifier: String
    let entitlements: Set<String>
}

struct Profile {
    let identifier: String
    let allowed: Set<String>
    let expiresAt: Int
}

struct Certificate {
    let expiresAt: Int
}

func effective(_ package: Package, profile: Profile) -> [String] {
    package.entitlements.intersection(profile.allowed).sorted()
}

func sign(_ package: Package, profile: Profile, certificate: Certificate, counter: Int) -> String {
    if certificate.expiresAt <= counter { return "reject:certificate" }
    if profile.expiresAt <= counter { return "reject:profile" }
    if package.identifier != profile.identifier { return "reject:identifier" }
    if !package.entitlements.isSubset(of: profile.allowed) { return "reject:entitlement" }
    return "signed"
}

The identifier check looks small but is necessary: a profile with an explicit identifier is bound to a single bundle identifier, and an app packaged under another identifier cannot be signed with that profile, even if it carries the same entitlements. In the model this stands as reject:identifier. No input in this lesson triggers it; identifier matching stays outside the measurement, and the fault does not touch it.

The lesson’s two packages carry the same bundle identifier. The correct package carries two entitlements, and the faulty package carries only one of them. The profile allows both, and the app asks for both.

// Shop.swift — the lesson's two packages, the profile, and the certificate
let requested: Set<String> = ["keychain", "remote-notification"]
let shopProfile = Profile(identifier: "com.example.shop", allowed: ["keychain", "remote-notification"], expiresAt: 8)
let shopCertificate = Certificate(expiresAt: 5)
let correctPackage = Package(identifier: "com.example.shop", entitlements: ["keychain", "remote-notification"])
let faultyPackage = Package(identifier: "com.example.shop", entitlements: ["keychain"])

DS1. This measurement gives the expiry of the certificate and the profile as a version counter. How a real expiry date maps to the counter was not measured.

Four Checks

Four checks are applied to the same two packages. The first compares the entitlements the package carries with a recorded list. The recorded list is an entitlement list that the team recorded once and then treated as the yardstick for correctness; in this lesson, that list was copied from the faulty build. The second looks at whether the effective entitlements of the signed package cover the entitlements the app asks for. The third looks only at whether the profile allows the entitlements the app asks for, and the fourth only at whether the certificate is valid for the counter.

// main.swift — four checks, three separations
let counter = 3
let registered = ["keychain"]

let listed = contrast("registered-list-equals", correct: correctPackage.entitlements.sorted(), faulty: faultyPackage.entitlements.sorted()) { $0 == registered }.describe()
let covered = contrast("effective-entitlements-cover", correct: effective(correctPackage, profile: shopProfile), faulty: effective(faultyPackage, profile: shopProfile)) { requested.isSubset(of: $0) }.describe()
let profileOnly = contrast("profile-covers", correct: shopProfile.allowed.sorted(), faulty: shopProfile.allowed.sorted()) { requested.isSubset(of: $0) }.describe()
let certificateOk = contrast("certificate-valid", correct: shopCertificate.expiresAt, faulty: shopCertificate.expiresAt) { $0 > counter }.describe()

countSeparations([listed, covered, profileOnly, certificateOk])
registered-list-equals | correct=["keychain", "remote-notification"]/fail faulty=["keychain"]/pass -> inverted
effective-entitlements-cover | correct=["keychain", "remote-notification"]/pass faulty=["keychain"]/fail -> separating
profile-covers | correct=["keychain", "remote-notification"]/pass faulty=["keychain", "remote-notification"]/pass -> blind
certificate-valid | correct=5/pass faulty=5/pass -> blind
separating: 1
blind: 2
noisy: 0
inverted: 1

Four lines, three categories. effective-entitlements-cover looks at the value the fault changes: in the faulty package, remote notification is missing from the effective entitlements and the check fails; in the correct package it is present and the check passes. This is the separating check. profile-covers and certificate-valid, on the other hand, see the same value in both versions, because the fault touches neither the profile nor the certificate. On both lines the faulty side says pass; these two blind checks give the confidence common in a green signing pipeline and say nothing about the fault.

The Inversion of the Equality Check

The first line is the interesting one. registered-list-equals rejects the correct package and accepts the faulty one: correct=…/fail, faulty=…/pass, result inverted. The check is not faulty; the problem is the source of the expected value. Because the recorded list was copied from the faulty build, it counted the missing entitlement as “correct”. The team that later adds the entitlement sees the check turn red and, more often than not, updates the list together with the package. From that moment on, the fault has settled into the check itself.

This is a trap specific to equality checks: if a check takes its expected value from a version, it also takes that version’s fault. An equality check protects an expectation, but it does not say whether the expectation is correct. A separating check solves this another way. effective-entitlements-cover takes its expected value from no build; it reads the entitlements the app asks for from their own source, the request list kept together with the app’s code.

It would also be wrong to say the equality check against the recorded list is useless. It is a good way to catch entitlements that quietly grow: if the record was taken correctly, a package with an extra entitlement fails. The fault measured in this lesson is a different one, a missing entitlement. Separation is measured against one fault; the same check can fall into another category against another fault.

DS2. This measurement assumes that the signature is accepted when the profile’s entitlements cover the package’s entitlements. How the real signing tool is affected by extra entitlements in the profile was not measured.

The Comfort of a Blind Check

Why are certificate and profile checks so common? Because their failures are loud and warn the team immediately. A certificate that has expired does not let the package be signed, and a package without a profile is not installed on a device. That is why these checks sit at the start of the pipeline and pass every day, giving the team a habit. But a check that has been green hundreds of times does not show that it saw a difference between the correct version and the faulty one. Both versions are signed with the same certificate and bound to the same profile; blind is therefore unavoidable.

The takeaway is this: a blind check does not have to be deleted, because it catches another fault. What is needed is knowing whether it counts as a check that catches the fault. If the green output of a pipeline consists of three blind checks and one separating check, the fault stays invisible unless it gets past the separating one. In practice this means writing into the pipeline report not only the number of checks that passed but also which fault each check could see; the number can be high while the share of separating checks stays close to zero.

Testing a New Check with Mutation Testing

When a new check is added to the signing pipeline, the first question should not be “does this check pass today”. Passing today gives a green pipeline, and that proves nothing. The right question is this: if I break the package on purpose, will this check fail? This method is called mutation testing. Here, breaking means deliberately removing an entitlement from the package; if the check does not reject that package, it is either blind or inverted.

The lesson’s measurement is a small form of mutation testing: the faulty package was made by removing a single entitlement from the correct package, and four checks were applied to it. The difference is this: mutation testing only asks “was it caught”. This course also looks at the answer of the correct version, because a check that rejects the correct package keeps the team busy with a wrong red every time, whatever its success at catching. Noisy and inverted separate only when the answers of the two versions are put side by side.

Check What it looks at Sees the fault Separation
registered-list-equals the package’s entitlements, the recorded list only as long as the list is correct inverted
effective-entitlements-cover the effective entitlements, the app’s requests yes separating
profile-covers the profile, the app’s requests no blind
certificate-valid the certificate, the counter no blind

The third column in the table states not a result but a condition. In the first row, seeing the fault requires that the recorded list was not taken from the faulty build. That condition does not exist inside the check itself; where the list came from stays outside the check, and only the person who wrote it knows.

Correcting the Recorded List

When the inversion is found, there are two ways. The first is to record the list again from the correct package. That makes the check correct until the next fault, but it again takes the list from a build; if an entitlement is quietly lost in a new build, the inversion returns. The second is to derive the expected value from outside the package, from the app’s request list: this is what effective-entitlements-cover does. The second way removes the work of refreshing the record, because the expected value comes from the app itself every time.

The two ways can also be set up together. The check derived from the request list catches the missing entitlement, and the equality check against the recorded list catches the extra one. The two look at different faults, and the lesson treats them as complementary, not as rivals.

The Bounding Measurement

Separation is not a property of a check alone; it changes with the fault and the input. In the measurement below, a single check, the sign decision being signed, is applied to three inputs. At counter 3, both the certificate and the profile are valid, and both packages get signed. At counter 9, the certificate has expired and both packages are rejected. In the third input, the profile is kept narrow and allows only the keychain entitlement: an entitlement without a match is rejected in the model too, so the correct package that carries remote notification is rejected.

// main.swift — the same signing check gives three separations on three inputs
let narrowProfile = Profile(identifier: "com.example.shop", allowed: ["keychain"], expiresAt: 8)

let early = contrast("counter-3",
    correct: sign(correctPackage, profile: shopProfile, certificate: shopCertificate, counter: 3),
    faulty: sign(faultyPackage, profile: shopProfile, certificate: shopCertificate, counter: 3)) { $0 == "signed" }.describe()
let late = contrast("counter-9",
    correct: sign(correctPackage, profile: shopProfile, certificate: shopCertificate, counter: 9),
    faulty: sign(faultyPackage, profile: shopProfile, certificate: shopCertificate, counter: 9)) { $0 == "signed" }.describe()
let narrow = contrast("narrow-profile",
    correct: sign(correctPackage, profile: narrowProfile, certificate: shopCertificate, counter: 3),
    faulty: sign(faultyPackage, profile: narrowProfile, certificate: shopCertificate, counter: 3)) { $0 == "signed" }.describe()

countSeparations([early, late, narrow])
counter-3 | correct=signed/pass faulty=signed/pass -> blind
counter-9 | correct=reject:certificate/fail faulty=reject:certificate/fail -> noisy
narrow-profile | correct=reject:entitlement/fail faulty=signed/pass -> inverted
separating: 0
blind: 1
noisy: 1
inverted: 1

The same check, three inputs, three categories. At counter-3, the faulty package is signed too; the signing pipeline is green and the fault passes, blind. At counter-9, the time has run out, both packages get reject:certificate, and the check fails on both versions, noisy: the reason for failing is the certificate, not the fault. At narrow-profile, the correct package is rejected with reject:entitlement, because remote notification is not in the profile; the faulty package carries exactly the one entitlement the profile allows, so it gets signed, inverted. In real life too, if a profile does not contain the entitlement, the correct package cannot be signed; the faulty package, which works with a missing entitlement, never meets this obstacle.

This measurement also shows a limit. On none of the three inputs did the signed check come out separating: this check alone cannot separate this fault, because the signing decision only says whether the package fits the profile, not what the app needs. The app’s need is in the requested set, and the only check that reads it is effective-entitlements-cover.

DS3. This measurement gives the order of the signing decision as the model’s order: first the certificate, then the profile, the identifier, and the entitlement. Which mismatch the real signing tool checks first was not measured.

Summary

  • The certificate says who signed, the provisioning profile says which bundle identifier works with which entitlements, and the entitlements file says what the package asks for; signing looks at whether these three agree.
  • A check that compares the package’s entitlements with a recorded list rejects the correct package and passes the faulty one if the list was taken from the faulty build: inverted.
  • A check that looks at whether the effective entitlements cover what the app asks for separates the faulty package: separating.
  • Checks that look only at the certificate or only at the profile pass on both versions: blind.
  • The same signing check can be blind, noisy, or inverted depending on the counter and the profile; separation is a property not of the check but of the check and the input together.
  • This lesson’s separation: inverted.

Next Step

A package that can be signed can still compute the wrong amount; the signing decision does not verify the computation. In the next lesson, Continuous Integration, we will measure these checks running one after another in a pipeline, with the pipeline stopping at the first step that fails.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close