diff --git a/Documentation/xtool.docc/Distribution.md b/Documentation/xtool.docc/Distribution.md new file mode 100644 index 00000000..b89d727d --- /dev/null +++ b/Documentation/xtool.docc/Distribution.md @@ -0,0 +1,60 @@ +# Sign for distribution + +Produce an `.ipa` for TestFlight, the App Store or Ad Hoc distribution, entirely offline. + +## Overview + +`xtool dev` normally signs your app with a development certificate that xtool creates for you through your Apple ID. That is what you want for running on your own device, but an App Store or Ad Hoc build needs a **distribution** certificate and a matching provisioning profile. + +xtool can sign with a distribution identity you already have. It never contacts Apple in this mode: it doesn't create, renew or revoke any certificate or profile, so it is safe to run in CI. + +You need: + +- An **Apple Distribution** certificate with its private key, exported as a `.p12` file (in Keychain Access, or with `openssl pkcs12 -export`). +- An **App Store** or **Ad Hoc** provisioning profile (`.mobileprovision`) for your app's bundle ID, created with that certificate on the Apple Developer portal. + +## Sign a build + +Pass the identity and the profile to `xtool dev build`: + +```bash +export XTOOL_P12_PASSWORD='' +xtool dev build --configuration release --ipa \ + --p12 Distribution.p12 \ + --profile MyApp_AppStore.mobileprovision +``` + +The password is read from the `XTOOL_P12_PASSWORD` environment variable, or from a file with `--p12-password-file`. Use `--p12-password-env` to read a different variable. It is deliberately not accepted as a command-line argument, since arguments are visible to other users of the machine and end up in shell history and CI logs. + +## Re-sign an existing `.ipa` + +`xtool sign` re-signs an `.ipa` (or an `.app`) that was built elsewhere, and writes a new `.ipa`. The input is left untouched. + +```bash +xtool sign MyApp.ipa \ + --p12 Distribution.p12 \ + --profile MyApp_AppStore.mobileprovision \ + --output MyApp-signed.ipa +``` + +## What gets signed + +xtool checks the inputs before it signs anything, and refuses to continue if: + +- the certificate is not a distribution certificate, or has expired; +- the profile is a development profile, has expired, belongs to another team, or doesn't include your certificate; +- no profile matches the app's bundle ID. + +The entitlements are derived from the profile, so that the signature matches what the profile allows: + +- `application-identifier` and `com.apple.developer.team-identifier` always come from the profile. +- `get-task-allow` is always `false`. +- Like Xcode, xtool claims what the app asks for (the entitlements already in its signature, for instance those from your `.entitlements` file), checked against what the profile allows. Asking for something the profile doesn't allow is an error. `beta-reports-active` is carried over when the profile has it. +- If the app has no entitlements of its own, everything the profile grants is claimed, such as `aps-environment` (`production` in a distribution profile) and app groups. +- Wildcards in the profile are resolved for your app. For example, `keychain-access-groups` of `.*` becomes your app's own group. + +To use a specific set of entitlements instead of the ones already in the app, pass `--entitlements Entitlements.plist`. + +Extensions (`.appex`) are signed with their own profile and entitlements. Pass one `--profile` per bundle: each bundle uses the profile that best matches its bundle ID. + +The profile is copied into each bundle as `embedded.mobileprovision`. diff --git a/Documentation/xtool.docc/xtool.md b/Documentation/xtool.docc/xtool.md index fea43522..f97896d3 100644 --- a/Documentation/xtool.docc/xtool.md +++ b/Documentation/xtool.docc/xtool.md @@ -32,3 +32,4 @@ xtool is a cross-platform (Linux/WSL/macOS) tool that replicates Xcode functiona - - +- diff --git a/Package.resolved b/Package.resolved index 4cc2f508..42e4c3ba 100644 --- a/Package.resolved +++ b/Package.resolved @@ -454,10 +454,9 @@ { "identity" : "zsign", "kind" : "remoteSourceControl", - "location" : "https://github.com/xtool-org/zsign", + "location" : "https://github.com/TartanLeGrand/zsign", "state" : { - "revision" : "f6158be9e62a5ce2a53d72b1a6d880880da956c8", - "version" : "1.9.0" + "revision" : "4d2eb4c07e26cf56b6a161fa4a1c4eb4eb56cbc2" } } ], diff --git a/Package.swift b/Package.swift index 4a7766f7..7089f975 100644 --- a/Package.swift +++ b/Package.swift @@ -50,7 +50,9 @@ let package = Package( dependencies: [ .package(url: "https://github.com/xtool-org/xtool-core", .upToNextMinor(from: "1.5.0")), .package(url: "https://github.com/xtool-org/SwiftyMobileDevice", .upToNextMinor(from: "1.7.0")), - .package(url: "https://github.com/xtool-org/zsign", .upToNextMinor(from: "1.9.0")), + // TODO: temporary, points at a branch combining xtool-org/zsign#3, #4 and #5. Switch to + // .upToNextMinor(from: "") once zsign tags a release including them. + .package(url: "https://github.com/TartanLeGrand/zsign", revision: "4d2eb4c07e26cf56b6a161fa4a1c4eb4eb56cbc2"), .package(url: "https://github.com/xtool-org/xadi", .upToNextMinor(from: "0.4.2")), .package(url: "https://github.com/apple/swift-system", from: "1.4.0"), diff --git a/Sources/CXKit/include/pkcs12_identity.h b/Sources/CXKit/include/pkcs12_identity.h new file mode 100644 index 00000000..e152f84d --- /dev/null +++ b/Sources/CXKit/include/pkcs12_identity.h @@ -0,0 +1,26 @@ +#ifndef pkcs12_identity_h +#define pkcs12_identity_h + +#include + +#pragma clang assume_nonnull begin + +typedef enum { + PKCS12_IDENTITY_OK = 0, + PKCS12_IDENTITY_INVALID = 1, + PKCS12_IDENTITY_BAD_PASSWORD = 2, + PKCS12_IDENTITY_MISSING_CONTENT = 3, +} pkcs12_identity_status_t; + +/// Extracts the leaf certificate (DER) and its private key (unencrypted PKCS#8 PEM) +/// from a PKCS#12 archive. On success both outputs are malloc'd and owned by the caller. +pkcs12_identity_status_t pkcs12_identity_extract( + const void *data, size_t len, + const char *password, + void * _Nullable * _Nonnull cert_der, size_t *cert_len, + void * _Nullable * _Nonnull key_pem, size_t *key_len +); + +#pragma clang assume_nonnull end + +#endif /* pkcs12_identity_h */ diff --git a/Sources/CXKit/pkcs12_identity.c b/Sources/CXKit/pkcs12_identity.c new file mode 100644 index 00000000..c1d67de8 --- /dev/null +++ b/Sources/CXKit/pkcs12_identity.c @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pkcs12_identity.h" + +static pthread_once_t providers_once = PTHREAD_ONCE_INIT; + +static void load_providers_once(void) { + // archives exported by older keychains use RC2/3DES, which OpenSSL 3 only + // provides through the legacy provider. It's fine if it isn't available. + OSSL_PROVIDER_load(NULL, "default"); + OSSL_PROVIDER_load(NULL, "legacy"); +} + +static void load_providers(void) { + // loading providers concurrently races with PKCS12_parse on other threads + // (seen as a parse that succeeds without a key or certificate), so do it once + pthread_once(&providers_once, load_providers_once); +} + +pkcs12_identity_status_t pkcs12_identity_extract( + const void *data, size_t len, + const char *password, + void **cert_der, size_t *cert_len, + void **key_pem, size_t *key_len +) { + *cert_der = NULL; + *key_pem = NULL; + *cert_len = 0; + *key_len = 0; + + load_providers(); + ERR_clear_error(); + + pkcs12_identity_status_t status = PKCS12_IDENTITY_INVALID; + PKCS12 *p12 = NULL; + EVP_PKEY *pkey = NULL; + X509 *cert = NULL; + STACK_OF(X509) *ca = NULL; + BIO *mem = NULL; + + const unsigned char *cursor = data; + p12 = d2i_PKCS12(NULL, &cursor, (long)len); + if (!p12) goto done; + + if (!PKCS12_parse(p12, password, &pkey, &cert, &ca)) { + unsigned long err = ERR_peek_last_error(); + if (ERR_GET_LIB(err) == ERR_LIB_PKCS12 && ERR_GET_REASON(err) == PKCS12_R_MAC_VERIFY_FAILURE) { + status = PKCS12_IDENTITY_BAD_PASSWORD; + } + goto done; + } + if (!pkey || !cert) { + status = PKCS12_IDENTITY_MISSING_CONTENT; + goto done; + } + + unsigned char *der = NULL; + int der_len = i2d_X509(cert, &der); + if (der_len <= 0) goto done; + + mem = BIO_new(BIO_s_mem()); + if (!mem || !PEM_write_bio_PKCS8PrivateKey(mem, pkey, NULL, NULL, 0, NULL, NULL)) { + OPENSSL_free(der); + goto done; + } + char *pem = NULL; + long pem_len = BIO_get_mem_data(mem, &pem); + + *cert_der = malloc((size_t)der_len); + *key_pem = malloc((size_t)pem_len); + if (!*cert_der || !*key_pem) { + free(*cert_der); + free(*key_pem); + *cert_der = NULL; + *key_pem = NULL; + OPENSSL_free(der); + goto done; + } + memcpy(*cert_der, der, (size_t)der_len); + memcpy(*key_pem, pem, (size_t)pem_len); + *cert_len = (size_t)der_len; + *key_len = (size_t)pem_len; + OPENSSL_free(der); + status = PKCS12_IDENTITY_OK; + +done: + BIO_free_all(mem); + sk_X509_pop_free(ca, X509_free); + X509_free(cert); + EVP_PKEY_free(pkey); + PKCS12_free(p12); + return status; +} diff --git a/Sources/XKit/Model/Certificate.swift b/Sources/XKit/Model/Certificate.swift index 7cf2fb94..75c5203d 100644 --- a/Sources/XKit/Model/Certificate.swift +++ b/Sources/XKit/Model/Certificate.swift @@ -65,6 +65,17 @@ public struct Certificate: Codable, Sendable { return String(content) } + public var expirationDate: Date { + raw.notValidAfter + } + + /// Whether the certificate is an Apple Distribution (or legacy iPhone Distribution) identity, + /// as opposed to a development one. Apple encodes this in the common name. + public func isDistribution() throws -> Bool { + let name = try developerIdentity() + return name.hasPrefix("Apple Distribution:") || name.hasPrefix("iPhone Distribution:") + } + public func wasIssuedBefore(_ other: Certificate) -> Bool { raw.notValidBefore < other.raw.notValidBefore } diff --git a/Sources/XKit/Model/Mobileprovision.swift b/Sources/XKit/Model/Mobileprovision.swift index 79d88192..2fee3375 100644 --- a/Sources/XKit/Model/Mobileprovision.swift +++ b/Sources/XKit/Model/Mobileprovision.swift @@ -15,15 +15,26 @@ public final class Mobileprovision: @unchecked Sendable { case invalidProfile } - public struct Digest: Decodable { + /// The distribution method a profile is meant for, as Xcode's export dialog would call it. + public enum Kind: Sendable, Equatable { + case development + case adHoc + case appStore + case enterprise + } + + public struct Digest: Decodable, Sendable { public let name: String public let uuid: String public let teamIdentifiers: [DeveloperServicesTeam.ID] public let creationDate: Date public let expirationDate: Date + /// The UDIDs the profile is restricted to. Empty for App Store and Enterprise profiles. public let devices: [String] + public let platforms: [String] public let certificates: [Certificate] public let entitlements: Entitlements + public let kind: Kind private enum CodingKeys: String, CodingKey { case name = "Name" @@ -32,9 +43,38 @@ public final class Mobileprovision: @unchecked Sendable { case creationDate = "CreationDate" case expirationDate = "ExpirationDate" case devices = "ProvisionedDevices" + case provisionsAllDevices = "ProvisionsAllDevices" + case platforms = "Platform" case certificates = "DeveloperCertificates" case entitlements = "Entitlements" } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + uuid = try container.decode(String.self, forKey: .uuid) + teamIdentifiers = try container.decode([DeveloperServicesTeam.ID].self, forKey: .teamIdentifiers) + creationDate = try container.decode(Date.self, forKey: .creationDate) + expirationDate = try container.decode(Date.self, forKey: .expirationDate) + // App Store and Enterprise profiles have no ProvisionedDevices key + let provisionedDevices = try container.decodeIfPresent([String].self, forKey: .devices) + devices = provisionedDevices ?? [] + platforms = try container.decodeIfPresent([String].self, forKey: .platforms) ?? [] + certificates = try container.decode([Certificate].self, forKey: .certificates) + entitlements = try container.decode(Entitlements.self, forKey: .entitlements) + + let provisionsAllDevices = try container.decodeIfPresent(Bool.self, forKey: .provisionsAllDevices) ?? false + if provisionsAllDevices { + kind = .enterprise + } else if provisionedDevices == nil { + kind = .appStore + } else { + let getTaskAllow = try entitlements.entitlements() + .compactMap { $0 as? GetTaskAllowEntitlement } + .first?.rawValue ?? false + kind = getTaskAllow ? .development : .adHoc + } + } } let raw: mobileprovision_t @@ -53,6 +93,10 @@ public final class Mobileprovision: @unchecked Sendable { mobileprovision_free(raw) } + public convenience init(contentsOf url: URL) throws { + try self.init(data: Data(contentsOf: url)) + } + public func digest() throws -> Digest { var len = 0 guard let ptr = mobileprovision_get_digest(raw, &len), len > 0 else { diff --git a/Sources/XKit/Signer/Distribution/DistributionEntitlements.swift b/Sources/XKit/Signer/Distribution/DistributionEntitlements.swift new file mode 100644 index 00000000..a1b59ff4 --- /dev/null +++ b/Sources/XKit/Signer/Distribution/DistributionEntitlements.swift @@ -0,0 +1,116 @@ +import Foundation + +/// Computes the entitlements an app is signed with for distribution. +/// +/// The profile is the source of truth for what the app is allowed to claim. The app (or the user, via +/// an entitlements file) may narrow what the profile grants, but asking for something the profile does not +/// allow is an error: App Store Connect and installd would reject the result anyway. +public enum DistributionEntitlements { + + /// Keys that always come from the profile, whatever the app asked for. + private static let profileOwnedKeys: Set = [ + "application-identifier", + "com.apple.developer.team-identifier", + "get-task-allow", + ] + + /// Granted by the profile and claimed whether or not the app asks for them (Xcode does the same). + private static let alwaysClaimedKeys: Set = ["beta-reports-active"] + + public static func resolve( + profile: DistributionProfile, + bundleID: String, + requested: Entitlements? + ) throws -> Entitlements { + let granted = try dictionary(of: profile.digest.entitlements) + let requested = try requested.map(dictionary(of:)) ?? [:] + + let team = profile.digest.teamIdentifiers.first?.rawValue ?? "" + let applicationIdentifier = "\(team).\(bundleID)" + + // Like Xcode, only claim what the app asked for. If we know nothing about the app + // (it carries no entitlements of its own), fall back to what the profile grants. + let appAsksForSomething = requested.keys.contains { !profileOwnedKeys.contains($0) } + + var result: [String: Any] = [:] + for (key, grant) in granted where !profileOwnedKeys.contains(key) { + if let request = requested[key] { + guard permits(grant: grant, request: request) else { + throw DistributionSigningError.entitlementNotInProfile(key: key, profile: profile.name) + } + result[key] = request + } else if !appAsksForSomething || alwaysClaimedKeys.contains(key) { + if let value = narrow(key: key, grant: grant, applicationIdentifier: applicationIdentifier) { + result[key] = value + } + } + } + + for key in requested.keys where granted[key] == nil && !profileOwnedKeys.contains(key) { + throw DistributionSigningError.entitlementNotInProfile(key: key, profile: profile.name) + } + + // a wildcard profile has `.*`: the signed entitlement must name the actual app + result["application-identifier"] = applicationIdentifier + // distribution builds explicitly opt out of debugging, as Xcode's do + result["get-task-allow"] = false + result["com.apple.developer.team-identifier"] = team + + return try entitlements(from: result) + } + + /// Resolves a wildcard grant to a concrete value, or `nil` if there is no sensible one to claim. + private static func narrow(key: String, grant: Any, applicationIdentifier: String) -> Any? { + switch grant { + case let string as String: + return string == "*" ? nil : string + case let strings as [String]: + let concrete = strings.compactMap { entry -> String? in + guard entry.hasSuffix("*") else { return entry } + // keychain groups are commonly granted as `.*`, which means "this app's group" + return key == "keychain-access-groups" ? applicationIdentifier : nil + } + var seen = Set() + let unique = concrete.filter { seen.insert($0).inserted } + return unique.isEmpty ? nil : unique + default: + return grant + } + } + + private static func permits(grant: Any, request: Any) -> Bool { + switch (grant, request) { + case let (grant as [String], request as [String]): + return request.allSatisfy { item in + grant.contains { $0 == item || ($0.hasSuffix("*") && item.hasPrefix(String($0.dropLast()))) } + } + case let (grant as String, request as String): + return grant == request || (grant.hasSuffix("*") && request.hasPrefix(String(grant.dropLast()))) + case let (grant as Bool, request as Bool): + return grant || !request + default: + return (grant as? NSObject)?.isEqual(request) ?? false + } + } + + // MARK: - Entitlements <-> plist dictionary + + static func dictionary(of entitlements: Entitlements) throws -> [String: Any] { + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let data = try encoder.encode(entitlements) + return try PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] ?? [:] + } + + static func entitlements(from dictionary: [String: Any]) throws -> Entitlements { + let data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0) + return try PropertyListDecoder().decode(Entitlements.self, from: data) + } + + /// Parses the entitlements plist embedded in a code signature, as returned by `Signer.analyze`. + public static func parse(plist data: Data) -> Entitlements? { + guard !data.isEmpty else { return nil } + return try? PropertyListDecoder().decode(Entitlements.self, from: data) + } + +} diff --git a/Sources/XKit/Signer/Distribution/DistributionIdentity.swift b/Sources/XKit/Signer/Distribution/DistributionIdentity.swift new file mode 100644 index 00000000..c13aa2b9 --- /dev/null +++ b/Sources/XKit/Signer/Distribution/DistributionIdentity.swift @@ -0,0 +1,64 @@ +import Foundation +import CXKit + +/// A certificate and private key supplied by the user (typically an Apple Distribution identity +/// exported as a `.p12`). Nothing is created or revoked on Apple's side when using this. +public struct DistributionIdentity: Sendable { + + public let certificate: Certificate + public let privateKey: PrivateKey + + public init(certificate: Certificate, privateKey: PrivateKey) { + self.certificate = certificate + self.privateKey = privateKey + } + + public init(p12: Data, password: String) throws { + var certDER: UnsafeMutableRawPointer? + var certLength = 0 + var keyPEM: UnsafeMutableRawPointer? + var keyLength = 0 + + let status = p12.withUnsafeBytes { buffer in + // an empty Data has no base address; OpenSSL then fails to parse it, which is the right outcome + pkcs12_identity_extract( + buffer.baseAddress ?? UnsafeRawPointer(bitPattern: 1)!, buffer.count, + password, + &certDER, &certLength, + &keyPEM, &keyLength + ) + } + + switch status { + case PKCS12_IDENTITY_OK: + break + case PKCS12_IDENTITY_BAD_PASSWORD: + throw DistributionSigningError.wrongP12Password + case PKCS12_IDENTITY_MISSING_CONTENT: + throw DistributionSigningError.p12MissingContent + default: + throw DistributionSigningError.invalidP12 + } + guard let certDER, let keyPEM else { throw DistributionSigningError.invalidP12 } + + let certData = Data(bytesNoCopy: certDER, count: certLength, deallocator: .free) + let keyData = Data(bytesNoCopy: keyPEM, count: keyLength, deallocator: .free) + self.init(certificate: try Certificate(data: certData), privateKey: PrivateKey(data: keyData)) + } + + public init(p12 url: URL, password: String) throws { + try self.init(p12: Data(contentsOf: url), password: password) + } + + /// Checks that the certificate is a usable distribution certificate. + public func validate(now: Date = Date()) throws { + let name = try certificate.developerIdentity() + guard try certificate.isDistribution() else { + throw DistributionSigningError.notADistributionCertificate(name) + } + guard certificate.expirationDate > now else { + throw DistributionSigningError.certificateExpired(name, certificate.expirationDate) + } + } + +} diff --git a/Sources/XKit/Signer/Distribution/DistributionProfile.swift b/Sources/XKit/Signer/Distribution/DistributionProfile.swift new file mode 100644 index 00000000..df5c2292 --- /dev/null +++ b/Sources/XKit/Signer/Distribution/DistributionProfile.swift @@ -0,0 +1,101 @@ +import Foundation + +/// A user-supplied App Store or Ad Hoc provisioning profile. +public struct DistributionProfile: Sendable { + + public let mobileprovision: Mobileprovision + public let digest: Mobileprovision.Digest + + public init(mobileprovision: Mobileprovision) throws { + self.mobileprovision = mobileprovision + do { + self.digest = try mobileprovision.digest() + } catch { + throw DistributionSigningError.invalidProfile("embedded profile") + } + } + + public init(data: Data) throws { + try self.init(mobileprovision: try Mobileprovision(data: data)) + } + + public init(contentsOf url: URL) throws { + let mobileprovision: Mobileprovision + do { + mobileprovision = try Mobileprovision(contentsOf: url) + } catch { + throw DistributionSigningError.invalidProfile(url.lastPathComponent) + } + do { + self.digest = try mobileprovision.digest() + } catch { + throw DistributionSigningError.invalidProfile(url.lastPathComponent) + } + self.mobileprovision = mobileprovision + } + + public var name: String { digest.name } + + /// The `application-identifier` entitlement, `.` (the bundle ID may be a wildcard). + public var applicationIdentifier: String? { + (try? digest.entitlements.entitlements())? + .compactMap { $0 as? ApplicationIdentifierEntitlement } + .first?.rawValue + } + + /// Whether this profile can sign an app with the given bundle ID. + public func matches(bundleID: String) -> Bool { + specificity(forBundleID: bundleID) != nil + } + + /// `nil` if the profile doesn't match; otherwise larger values are more specific + /// (an exact match beats a wildcard, a longer wildcard prefix beats a shorter one). + func specificity(forBundleID bundleID: String) -> Int? { + guard let appID = applicationIdentifier, + let team = digest.teamIdentifiers.first?.rawValue, + appID.hasPrefix(team + ".") + else { return nil } + let pattern = String(appID.dropFirst(team.count + 1)) + if pattern == bundleID { return Int.max } + if pattern.hasSuffix("*") { + let prefix = String(pattern.dropLast()) + return bundleID.hasPrefix(prefix) ? prefix.count : nil + } + return nil + } + + /// Verifies that the profile can be used to sign with `identity`. + public func validate(for identity: DistributionIdentity, now: Date = Date()) throws { + switch digest.kind { + case .development: + throw DistributionSigningError.notADistributionProfile(profile: name) + case .appStore, .adHoc, .enterprise: + break + } + guard digest.expirationDate > now else { + throw DistributionSigningError.profileExpired(profile: name, digest.expirationDate) + } + guard digest.platforms.isEmpty || digest.platforms.contains("iOS") else { + throw DistributionSigningError.profileWrongPlatform(profile: name, digest.platforms) + } + + let certTeam = try identity.certificate.teamID() + let profileTeams = digest.teamIdentifiers.map(\.rawValue) + guard profileTeams.contains(certTeam) else { + throw DistributionSigningError.profileTeamMismatch( + profile: name, + certificateTeam: certTeam, + profileTeams: profileTeams + ) + } + + let certData = try identity.certificate.data() + guard try digest.certificates.contains(where: { try $0.data() == certData }) else { + throw DistributionSigningError.certificateNotInProfile( + profile: name, + certificate: try identity.certificate.developerIdentity() + ) + } + } + +} diff --git a/Sources/XKit/Signer/Distribution/DistributionSigner.swift b/Sources/XKit/Signer/Distribution/DistributionSigner.swift new file mode 100644 index 00000000..6ae9d452 --- /dev/null +++ b/Sources/XKit/Signer/Distribution/DistributionSigner.swift @@ -0,0 +1,131 @@ +import Foundation + +/// Signs an app for App Store, TestFlight or Ad Hoc distribution using an identity and profiles +/// that the user already owns. Unlike ``AutoSigner`` it never talks to Apple and never creates +/// or revokes certificates or profiles. +public struct DistributionSigner: Sendable { + + public struct Result: Sendable { + public let bundleID: String + public let entitlements: Entitlements + } + + public let identity: DistributionIdentity + public let profiles: [DistributionProfile] + /// Entitlements to use instead of the ones found in the app's existing signature. + public let entitlements: Entitlements? + public let signer: Signer + + public init( + identity: DistributionIdentity, + profiles: [DistributionProfile], + entitlements: Entitlements? = nil, + signer: Signer? = nil + ) throws { + self.identity = identity + self.profiles = profiles + self.entitlements = entitlements + self.signer = try signer ?? .first() + } + + private struct Bundle { + var url: URL + var bundleID: String + var profile: DistributionProfile + var entitlements: Entitlements + } + + /// Signs `app` in place. + @discardableResult + public func sign( + app: URL, + status: @escaping (String) -> Void = { _ in }, + progress: @escaping @Sendable (Double?) -> Void = { _ in }, + now: Date = Date() + ) async throws -> Result { + status(NSLocalizedString("signer.provisioning", value: "Provisioning", comment: "")) + + try identity.validate(now: now) + for profile in profiles { + try profile.validate(for: identity, now: now) + } + + let bundles = try Self.appBundles(in: app) + var prepared: [Bundle] = [] + for url in bundles { + let bundleID = try Self.bundleID(of: url) + guard let profile = profiles + .compactMap({ profile in profile.specificity(forBundleID: bundleID).map { (profile, $0) } }) + .max(by: { $0.1 < $1.1 })?.0 + else { throw DistributionSigningError.noMatchingProfile(bundleID: bundleID) } + + let requested: Entitlements? + if url == app, let entitlements { + requested = entitlements + } else { + requested = await existingEntitlements(of: url) + } + prepared.append(Bundle( + url: url, + bundleID: bundleID, + profile: profile, + entitlements: try DistributionEntitlements.resolve( + profile: profile, bundleID: bundleID, requested: requested + ) + )) + } + + for bundle in prepared { + let destination = bundle.url.appendingPathComponent("embedded.mobileprovision") + if destination.exists { + try FileManager.default.removeItem(at: destination) + } + try bundle.profile.mobileprovision.data().write(to: destination) + } + + status(NSLocalizedString("signer.signing", value: "Signing", comment: "")) + try await signer.sign( + app: app, + identity: .real(identity.certificate, identity.privateKey), + entitlementMapping: Dictionary(uniqueKeysWithValues: prepared.map { ($0.url, $0.entitlements) }), + progress: progress + ) + progress(1) + + let root = prepared[0] + return Result(bundleID: root.bundleID, entitlements: root.entitlements) + } + + /// The app followed by its extensions + private static func appBundles(in app: URL) throws -> [URL] { + guard app.dirExists else { throw DistributionSigningError.invalidApp("\(app.lastPathComponent) not found") } + var bundles = [app] + for pluginsDir in ["PlugIns", "Extensions"] { + let plugins = app.appendingPathComponent(pluginsDir) + guard plugins.dirExists else { continue } + bundles += plugins.implicitContents.filter { $0.pathExtension.lowercased() == "appex" } + } + return bundles + } + + private static func bundleID(of bundle: URL) throws -> String { + let infoPlist = bundle.appendingPathComponent("Info.plist") + guard let data = try? Data(contentsOf: infoPlist), + let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], + let bundleID = dict["CFBundleIdentifier"] as? String + else { throw DistributionSigningError.invalidApp("cannot read CFBundleIdentifier in \(infoPlist.path)") } + return bundleID + } + + /// The entitlements currently embedded in the bundle's main executable, if it's signed + private func existingEntitlements(of bundle: URL) async -> Entitlements? { + let infoPlist = bundle.appendingPathComponent("Info.plist") + guard let data = try? Data(contentsOf: infoPlist), + let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], + let executable = dict["CFBundleExecutable"] as? String, + let plist = try? await signer.analyze(executable: bundle.appendingPathComponent(executable)) + else { return nil } + return DistributionEntitlements.parse(plist: plist) + } + +} diff --git a/Sources/XKit/Signer/Distribution/DistributionSigningError.swift b/Sources/XKit/Signer/Distribution/DistributionSigningError.swift new file mode 100644 index 00000000..daad3e98 --- /dev/null +++ b/Sources/XKit/Signer/Distribution/DistributionSigningError.swift @@ -0,0 +1,54 @@ +import Foundation + +public enum DistributionSigningError: LocalizedError, Equatable { + case invalidP12 + case wrongP12Password + case p12MissingContent + case notADistributionCertificate(String) + case certificateExpired(String, Date) + case invalidProfile(String) + case notADistributionProfile(profile: String) + case profileExpired(profile: String, Date) + case profileWrongPlatform(profile: String, [String]) + case profileTeamMismatch(profile: String, certificateTeam: String, profileTeams: [String]) + case certificateNotInProfile(profile: String, certificate: String) + case noMatchingProfile(bundleID: String) + case entitlementNotInProfile(key: String, profile: String) + case invalidApp(String) + + public var errorDescription: String? { + switch self { + case .invalidP12: + "The .p12 file could not be read. Make sure it is a PKCS#12 archive." + case .wrongP12Password: + "The .p12 password is incorrect." + case .p12MissingContent: + "The .p12 file must contain both a certificate and its private key." + case .notADistributionCertificate(let name): + "'\(name)' is not a distribution certificate. Export an Apple Distribution identity instead." + case .certificateExpired(let name, let date): + "The certificate '\(name)' expired on \(date.formatted(.iso8601.year().month().day()))." + case .invalidProfile(let path): + "'\(path)' is not a valid provisioning profile." + case .notADistributionProfile(let profile): + "The profile '\(profile)' is a development profile. " + + "Use an App Store or Ad Hoc profile to sign for distribution." + case .profileExpired(let profile, let date): + "The profile '\(profile)' expired on \(date.formatted(.iso8601.year().month().day()))." + case .profileWrongPlatform(let profile, let platforms): + "The profile '\(profile)' is for \(platforms.joined(separator: ", ")), not iOS." + case .profileTeamMismatch(let profile, let certificateTeam, let profileTeams): + "The profile '\(profile)' belongs to team \(profileTeams.joined(separator: ", ")), " + + "but the certificate belongs to team \(certificateTeam)." + case .certificateNotInProfile(let profile, let certificate): + "The profile '\(profile)' does not include the certificate '\(certificate)'. " + + "Regenerate the profile with this certificate selected." + case .noMatchingProfile(let bundleID): + "None of the provided profiles matches the bundle ID '\(bundleID)'." + case .entitlementNotInProfile(let key, let profile): + "The entitlement '\(key)' is requested but not allowed by the profile '\(profile)'." + case .invalidApp(let reason): + "Invalid app: \(reason)" + } + } +} diff --git a/Sources/XKit/Signer/SignerImpl.swift b/Sources/XKit/Signer/SignerImpl.swift index 3217a956..6dda1e9a 100644 --- a/Sources/XKit/Signer/SignerImpl.swift +++ b/Sources/XKit/Signer/SignerImpl.swift @@ -152,9 +152,6 @@ public actor Signer { entitlementMapping: [URL: Entitlements], progress: @escaping (Double?) -> Void ) async throws { - // TODO: #131: update Zupersign to handle multiple entitlements files - // this is a temp hack to just pass along the root app entitlements - let entitlementMapping = entitlementMapping.filter { $0.key.path == app.path } try self._sign( app: app, identity: identity, diff --git a/Sources/XToolSupport/DevCommand.swift b/Sources/XToolSupport/DevCommand.swift index 369bfa8d..39676bb4 100644 --- a/Sources/XToolSupport/DevCommand.swift +++ b/Sources/XToolSupport/DevCommand.swift @@ -144,6 +144,14 @@ struct DevBuildCommand: AsyncParsableCommand { ) ) var triple: String? + @OptionGroup(title: "Distribution signing") var distribution: DistributionSigningOptions + + func validate() throws { + if sign && distribution.isEnabled { + throw ValidationError("--sign (development) cannot be combined with --p12 (distribution).") + } + } + func run() async throws { let signingAuthToken: AuthToken? if sign { @@ -177,6 +185,10 @@ struct DevBuildCommand: AsyncParsableCommand { } } + if distribution.isEnabled { + try await distribution.sign(app: url) + } + let finalURL: URL if ipa { @Dependency(\.zipCompressor) var compressor diff --git a/Sources/XToolSupport/DistributionSigningOptions.swift b/Sources/XToolSupport/DistributionSigningOptions.swift new file mode 100644 index 00000000..1ea9c2f5 --- /dev/null +++ b/Sources/XToolSupport/DistributionSigningOptions.swift @@ -0,0 +1,106 @@ +import Foundation +import ArgumentParser +import XKit + +/// Options to sign with an identity and provisioning profiles that already exist, +/// as needed for App Store, TestFlight and Ad Hoc builds (typically in CI). +struct DistributionSigningOptions: ParsableArguments { + static let defaultPasswordEnvironmentVariable = "XTOOL_P12_PASSWORD" + + @Option( + help: ArgumentHelp( + "Sign for distribution with the identity in this .p12 file", + discussion: """ + The file must hold an Apple Distribution certificate and its private key. Nothing is \ + created or revoked on your Apple Developer account. + """, + valueName: "path" + ) + ) var p12: String? + + @Option( + help: ArgumentHelp( + "Environment variable that holds the .p12 password", + discussion: "Defaults to '\(defaultPasswordEnvironmentVariable)'. An unset variable means an empty password.", + valueName: "name" + ) + ) var p12PasswordEnv: String = defaultPasswordEnvironmentVariable + + @Option( + help: ArgumentHelp( + "File that holds the .p12 password (takes precedence over the environment variable)", + valueName: "path" + ) + ) var p12PasswordFile: String? + + @Option( + name: .customLong("profile"), + help: ArgumentHelp( + "An App Store or Ad Hoc .mobileprovision (repeatable)", + discussion: "Each bundle is signed with the most specific profile that matches its bundle ID.", + valueName: "path" + ) + ) var profiles: [String] = [] + + @Option( + help: ArgumentHelp( + "A plist of entitlements to use instead of the ones already in the app's signature", + discussion: "Entitlements the profile does not allow are rejected.", + valueName: "path" + ) + ) var entitlements: String? + + var isEnabled: Bool { p12 != nil } + + func validate() throws { + if p12 == nil { + if !profiles.isEmpty || entitlements != nil || p12PasswordFile != nil { + throw ValidationError("--profile, --entitlements and --p12-password-file require --p12.") + } + } else if profiles.isEmpty { + throw ValidationError("Distribution signing with --p12 requires at least one --profile.") + } + } + + private func password() throws -> String { + if let p12PasswordFile { + let contents = try String(contentsOfFile: p12PasswordFile, encoding: .utf8) + // tolerate the trailing newline that editors and `echo` add + return contents.hasSuffix("\n") ? String(contents.dropLast()) : contents + } + return ProcessInfo.processInfo.environment[p12PasswordEnv] ?? "" + } + + func makeSigner() throws -> DistributionSigner { + guard let p12 else { throw ValidationError("--p12 is required.") } + let identity = try DistributionIdentity(p12: URL(fileURLWithPath: p12), password: password()) + let profiles = try self.profiles.map { try DistributionProfile(contentsOf: URL(fileURLWithPath: $0)) } + let entitlements = try self.entitlements.map { + try PropertyListDecoder().decode(Entitlements.self, from: Data(contentsOf: URL(fileURLWithPath: $0))) + } + return try DistributionSigner(identity: identity, profiles: profiles, entitlements: entitlements) + } + + /// Signs `app` in place, printing progress. + func sign(app: URL) async throws { + let signer = try makeSigner() + defer { print() } + do { + let result = try await signer.sign( + app: app, + status: { print("\r\u{1B}[K\($0)...", terminator: "") }, + progress: { progress in + if let progress { + print("\r\(Int(progress * 100))%", terminator: "") + } + } + ) + print("\r\u{1B}[KSigned \(result.bundleID)", terminator: "") + } catch let error as CancellationError { + throw error + } catch { + print("\nError: \(error.localizedDescription)") + throw ExitCode.failure + } + } +} diff --git a/Sources/XToolSupport/SignCommand.swift b/Sources/XToolSupport/SignCommand.swift new file mode 100644 index 00000000..d65c1a3b --- /dev/null +++ b/Sources/XToolSupport/SignCommand.swift @@ -0,0 +1,82 @@ +import Foundation +import ArgumentParser +import Dependencies +import XKit +import XUtils + +struct SignCommand: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "sign", + abstract: "Sign an ipa or app for distribution", + discussion: """ + Re-signs an existing .ipa or .app with an Apple Distribution identity and an App Store or \ + Ad Hoc provisioning profile, and writes the result as a new .ipa. \ + The input is not modified. This works offline and never contacts Apple. + + The .p12 password is read from the XTOOL_P12_PASSWORD environment variable \ + (or from the file given to --p12-password-file); it is never taken as an argument. + """ + ) + + @OptionGroup var signing: DistributionSigningOptions + + @Option( + name: .shortAndLong, + help: "Where to write the signed .ipa (defaults to '-signed.ipa' next to the input)" + ) var output: String? + + @Argument(help: "The .ipa or .app to sign") var path: String + + func validate() throws { + guard signing.isEnabled else { + throw ValidationError("'sign' requires --p12 and at least one --profile.") + } + switch URL(fileURLWithPath: path).pathExtension.lowercased() { + case "ipa", "app": break + default: throw ValidationError("The input must be a .ipa or a .app.") + } + } + + func run() async throws { + @Dependency(\.zipCompressor) var compressor + + let input = URL(fileURLWithPath: path) + let outputURL = output.map { URL(fileURLWithPath: $0) } + ?? input.deletingLastPathComponent().appendingPathComponent( + input.deletingPathExtension().lastPathComponent + "-signed.ipa" + ) + + // fail on bad credentials before unpacking anything + _ = try signing.makeSigner() + + let staging = try TemporaryDirectory(name: "sign") + let payload = staging.url.appendingPathComponent("Payload") + + switch input.pathExtension.lowercased() { + case "ipa": + print("Unpacking...") + try await compressor.decompress(file: input, in: staging.url, progress: { _ in }) + default: + try FileManager.default.createDirectory(at: payload, withIntermediateDirectories: false) + try FileManager.default.copyItem(at: input, to: payload.appendingPathComponent(input.lastPathComponent)) + } + + guard let app = payload.implicitContents.first(where: { $0.pathExtension.lowercased() == "app" }) else { + throw Console.Error("No .app found in Payload/") + } + + try await signing.sign(app: app) + + let ipa = try await compressor.compress(directory: payload) { progress in + if let progress { + print("\rPackaging... \(Int(progress * 100))%", terminator: "") + } else { + print("\rPackaging...", terminator: "") + } + } + print() + try? FileManager.default.removeItem(at: outputURL) + try FileManager.default.moveItem(at: ipa, to: outputURL) + print("Wrote to \(outputURL.path)") + } +} diff --git a/Sources/XToolSupport/XTool.swift b/Sources/XToolSupport/XTool.swift index 2cec336e..d8f958fa 100644 --- a/Sources/XToolSupport/XTool.swift +++ b/Sources/XToolSupport/XTool.swift @@ -29,6 +29,7 @@ private struct XToolCommand: AsyncParsableCommand { subcommands: [ NewCommand.self, DevCommand.self, + SignCommand.self, DSCommand.self, ] ), diff --git a/Tests/XToolTests/DistributionSigningTests.swift b/Tests/XToolTests/DistributionSigningTests.swift new file mode 100644 index 00000000..b7de0754 --- /dev/null +++ b/Tests/XToolTests/DistributionSigningTests.swift @@ -0,0 +1,324 @@ +import Foundation +import Testing +@testable import XKit + +private func profile(_ data: Data) throws -> DistributionProfile { + try DistributionProfile(data: data) +} + +private func identity(_ p12: Data = SigningFixtures.distributionP12) throws -> DistributionIdentity { + try DistributionIdentity(p12: p12, password: SigningFixtures.p12Password) +} + +private func resolved( + _ data: Data, + bundleID: String = SigningFixtures.bundleID, + requested: [String: Any]? = nil +) throws -> [String: Any] { + let requested = try requested.map(DistributionEntitlements.entitlements(from:)) + return try DistributionEntitlements.dictionary(of: DistributionEntitlements.resolve( + profile: profile(data), bundleID: bundleID, requested: requested + )) +} + +// MARK: - Profile parsing + +@Suite struct DistributionProfileParsingTests { + @Test func appStoreProfile() throws { + let profile = try profile(SigningFixtures.appstoreProfile) + #expect(profile.digest.kind == .appStore) + #expect(profile.digest.devices.isEmpty) + #expect(profile.digest.teamIdentifiers.map(\.rawValue) == [SigningFixtures.teamID]) + #expect(profile.digest.platforms == ["iOS"]) + #expect(profile.digest.expirationDate > Date()) + #expect(profile.digest.certificates.count == 1) + #expect(profile.applicationIdentifier == "\(SigningFixtures.teamID).\(SigningFixtures.bundleID)") + let entitlements = try DistributionEntitlements.dictionary(of: profile.digest.entitlements) + #expect(entitlements["aps-environment"] as? String == "production") + } + + @Test func adHocProfile() throws { + let profile = try profile(SigningFixtures.adhocProfile) + #expect(profile.digest.kind == .adHoc) + #expect(profile.digest.devices.count == 1) + } + + @Test func developmentProfile() throws { + let profile = try profile(SigningFixtures.developmentProfile) + #expect(profile.digest.kind == .development) + #expect(profile.digest.devices.count == 1) + } + + @Test func invalidProfile() { + #expect(throws: (any Error).self) { try profile(Data("not a profile".utf8)) } + } + + @Test func bundleIDMatching() throws { + let exact = try profile(SigningFixtures.appstoreProfile) + #expect(exact.matches(bundleID: SigningFixtures.bundleID)) + #expect(!exact.matches(bundleID: "\(SigningFixtures.bundleID).widget")) + #expect(!exact.matches(bundleID: "com.example.other")) + + let wildcard = try profile(SigningFixtures.wildcardProfile) + #expect(wildcard.matches(bundleID: SigningFixtures.bundleID)) + #expect(wildcard.matches(bundleID: "com.anything.else")) + + // an exact profile is preferred over a wildcard one + let exactScore = try #require(exact.specificity(forBundleID: SigningFixtures.bundleID)) + let wildcardScore = try #require(wildcard.specificity(forBundleID: SigningFixtures.bundleID)) + #expect(exactScore > wildcardScore) + } +} + +// MARK: - Identity and profile validation + +@Suite struct DistributionValidationTests { + @Test func loadsDistributionIdentity() throws { + let identity = try identity() + #expect(try identity.certificate.teamID() == SigningFixtures.teamID) + #expect(try identity.certificate.isDistribution()) + #expect(try identity.certificate.data() == SigningFixtures.distributionCertificate) + #expect(String(decoding: identity.privateKey.data, as: UTF8.self).hasPrefix("-----BEGIN PRIVATE KEY-----")) + try identity.validate() + } + + @Test func wrongP12Password() { + #expect(throws: DistributionSigningError.wrongP12Password) { + try DistributionIdentity(p12: SigningFixtures.distributionP12, password: "nope") + } + } + + @Test func invalidP12() { + #expect(throws: DistributionSigningError.invalidP12) { + try DistributionIdentity(p12: Data("garbage".utf8), password: "") + } + #expect(throws: DistributionSigningError.invalidP12) { + try DistributionIdentity(p12: Data(), password: "") + } + } + + @Test func rejectsDevelopmentCertificate() throws { + let identity = try identity(SigningFixtures.developmentP12) + #expect(throws: DistributionSigningError.self) { try identity.validate() } + } + + @Test func rejectsExpiredCertificate() throws { + let identity = try identity() + let future = identity.certificate.expirationDate.addingTimeInterval(1) + #expect(throws: DistributionSigningError.self) { try identity.validate(now: future) } + } + + @Test func acceptsAppStoreAndAdHocProfiles() throws { + let identity = try identity() + try profile(SigningFixtures.appstoreProfile).validate(for: identity) + try profile(SigningFixtures.adhocProfile).validate(for: identity) + } + + @Test func rejectsDevelopmentProfile() throws { + let identity = try identity() + let development = try profile(SigningFixtures.developmentProfile) + #expect(throws: DistributionSigningError.notADistributionProfile(profile: development.name)) { + try development.validate(for: identity) + } + } + + @Test func rejectsExpiredProfile() throws { + let identity = try identity() + let expired = try profile(SigningFixtures.expiredProfile) + #expect(throws: DistributionSigningError.profileExpired( + profile: expired.name, expired.digest.expirationDate + )) { + try expired.validate(for: identity) + } + } + + @Test func rejectsTeamMismatch() throws { + let identity = try identity() + let otherTeam = try profile(SigningFixtures.otherteamProfile) + #expect(throws: DistributionSigningError.profileTeamMismatch( + profile: otherTeam.name, certificateTeam: SigningFixtures.teamID, profileTeams: ["OTHERTEAM1"] + )) { + try otherTeam.validate(for: identity) + } + } + + @Test func rejectsProfileWithoutTheCertificate() throws { + let identity = try identity() + let otherCert = try profile(SigningFixtures.othercertProfile) + #expect(throws: DistributionSigningError.self) { try otherCert.validate(for: identity) } + } +} + +// MARK: - Entitlements + +@Suite struct DistributionEntitlementsTests { + private let appID = "\(SigningFixtures.teamID).\(SigningFixtures.bundleID)" + + @Test func derivesEntitlementsFromTheProfile() throws { + let ents = try resolved(SigningFixtures.appstoreProfile) + #expect(ents["application-identifier"] as? String == appID) + #expect(ents["com.apple.developer.team-identifier"] as? String == SigningFixtures.teamID) + #expect(ents["aps-environment"] as? String == "production") + #expect(ents["beta-reports-active"] as? Bool == true) + #expect(ents["com.apple.security.application-groups"] as? [String] == ["group.com.example.app"]) + // `.*` is narrowed to this app's group + #expect(ents["keychain-access-groups"] as? [String] == [appID]) + // a lone wildcard can't be claimed as-is + #expect(ents["com.apple.developer.associated-domains"] == nil) + } + + @Test func getTaskAllowIsNeverTrue() throws { + let profiles = [ + SigningFixtures.appstoreProfile, SigningFixtures.adhocProfile, + SigningFixtures.wildcardProfile, SigningFixtures.developmentProfile, + ] + for data in profiles { + #expect(try resolved(data)["get-task-allow"] as? Bool == false) + } + // even when the app (or a debug signature it came with) asks for it + let ents = try resolved(SigningFixtures.appstoreProfile, requested: ["get-task-allow": true]) + #expect(ents["get-task-allow"] as? Bool == false) + } + + @Test func wildcardProfileNamesTheActualApp() throws { + let ents = try resolved(SigningFixtures.wildcardProfile) + #expect(ents["application-identifier"] as? String == appID) + #expect(ents["keychain-access-groups"] as? [String] == [appID]) + } + + @Test func adHocUsesTheProfilePushEnvironment() throws { + let ents = try resolved(SigningFixtures.adhocProfile) + #expect(ents["aps-environment"] as? String == "production") + #expect(ents["beta-reports-active"] == nil) + } + + @Test func projectEntitlementsNarrowWhatTheProfileGrants() throws { + let ents = try resolved(SigningFixtures.appstoreProfile, requested: [ + "com.apple.developer.associated-domains": ["applinks:example.com"], + "keychain-access-groups": ["\(SigningFixtures.teamID).shared"], + "application-identifier": "stale.value", + ]) + #expect(ents["com.apple.developer.associated-domains"] as? [String] == ["applinks:example.com"]) + #expect(ents["keychain-access-groups"] as? [String] == ["\(SigningFixtures.teamID).shared"]) + // identity keys always come from the profile + #expect(ents["application-identifier"] as? String == appID) + // like Xcode, we don't claim capabilities the app did not ask for... + #expect(ents["aps-environment"] == nil) + #expect(ents["com.apple.security.application-groups"] == nil) + // ...except the ones the profile enables for every build + #expect(ents["beta-reports-active"] as? Bool == true) + } + + @Test func rejectsEntitlementsTheProfileDoesNotAllow() throws { + #expect(throws: DistributionSigningError.self) { + try resolved(SigningFixtures.appstoreProfile, requested: ["com.apple.developer.healthkit": true]) + } + // a development push environment in a production profile + #expect(throws: DistributionSigningError.self) { + try resolved(SigningFixtures.appstoreProfile, requested: ["aps-environment": "development"]) + } + // an app group the profile doesn't list + #expect(throws: DistributionSigningError.self) { + try resolved(SigningFixtures.appstoreProfile, requested: [ + "com.apple.security.application-groups": ["group.com.example.other"], + ]) + } + } +} + +// MARK: - Signer preconditions + +@Suite struct DistributionSignerTests { + private func makeApp(extensions: [String] = []) throws -> URL { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + let app = root.appendingPathComponent("Test.app", isDirectory: true) + func writeBundle(at url: URL, id: String) throws { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + let info: [String: Any] = ["CFBundleIdentifier": id, "CFBundleExecutable": "main"] + try PropertyListSerialization.data(fromPropertyList: info, format: .xml, options: 0) + .write(to: url.appendingPathComponent("Info.plist")) + } + try writeBundle(at: app, id: SigningFixtures.bundleID) + for id in extensions { + try writeBundle(at: app.appendingPathComponent("PlugIns/\(id).appex"), id: id) + } + return app + } + + @Test func failsWithoutAMatchingProfile() async throws { + let app = try makeApp() + defer { try? FileManager.default.removeItem(at: app.deletingLastPathComponent()) } + + let signer = try DistributionSigner( + identity: identity(), + profiles: [profile(SigningFixtures.appextensionProfile)] + ) + await #expect(throws: DistributionSigningError.noMatchingProfile(bundleID: SigningFixtures.bundleID)) { + try await signer.sign(app: app) + } + } + + @Test func refusesDevelopmentProfiles() async throws { + let app = try makeApp() + defer { try? FileManager.default.removeItem(at: app.deletingLastPathComponent()) } + + let development = try profile(SigningFixtures.developmentProfile) + let signer = try DistributionSigner(identity: identity(), profiles: [development]) + await #expect(throws: DistributionSigningError.notADistributionProfile(profile: development.name)) { + try await signer.sign(app: app) + } + #expect(!app.appendingPathComponent("embedded.mobileprovision").exists) + } + + @Test func signsEachBundleWithItsOwnProfileAndEntitlements() async throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + defer { try? FileManager.default.removeItem(at: root) } + let app = try FakeApp.writeBundle( + at: root.appendingPathComponent("Test.app"), id: SigningFixtures.bundleID + ) + let widgetID = "\(SigningFixtures.bundleID).widget" + let widget = try FakeApp.writeBundle( + at: app.appendingPathComponent("PlugIns/Widget.appex"), id: widgetID + ) + + let signer = try DistributionSigner( + identity: identity(), + profiles: [profile(SigningFixtures.appstoreProfile), profile(SigningFixtures.appextensionProfile)] + ) + let result = try await signer.sign(app: app) + #expect(result.bundleID == SigningFixtures.bundleID) + + let appEnts = try #require(try FakeApp.entitlements(ofExecutable: app.appendingPathComponent("main"))) + let widgetEnts = try #require(try FakeApp.entitlements(ofExecutable: widget.appendingPathComponent("main"))) + let team = SigningFixtures.teamID + #expect(appEnts["application-identifier"] as? String == "\(team).\(SigningFixtures.bundleID)") + #expect(widgetEnts["application-identifier"] as? String == "\(team).\(widgetID)") + #expect(appEnts["keychain-access-groups"] as? [String] == ["\(team).\(SigningFixtures.bundleID)"]) + #expect(widgetEnts["keychain-access-groups"] as? [String] == ["\(team).\(widgetID)"]) + for ents in [appEnts, widgetEnts] { + #expect(ents["get-task-allow"] as? Bool == false) + #expect(ents["aps-environment"] as? String == "production") + } + + // a single SHA-256 CodeDirectory, as Xcode emits: App Store Connect rejects SHA-1 + SHA-256 (90034) + for bundle in [app, widget] { + #expect(try FakeApp.codeDirectoryHashTypes(ofExecutable: bundle.appendingPathComponent("main")) == [2]) + } + + // each bundle carries the profile that matches it + let appProfile = try Mobileprovision(contentsOf: app.appendingPathComponent("embedded.mobileprovision")) + let widgetProfile = try Mobileprovision(contentsOf: widget.appendingPathComponent("embedded.mobileprovision")) + #expect(try appProfile.data() == profile(SigningFixtures.appstoreProfile).mobileprovision.data()) + #expect(try widgetProfile.data() == profile(SigningFixtures.appextensionProfile).mobileprovision.data()) + #expect(app.appendingPathComponent("_CodeSignature/CodeResources").exists) + + // rewriting an executable to make room for the signature must not drop its permissions + for bundle in [app, widget] { + let attributes = try FileManager.default.attributesOfItem(atPath: bundle.appendingPathComponent("main").path) + #expect((attributes[.posixPermissions] as? Int).map { $0 & 0o111 != 0 } == true) + } + #expect(widget.appendingPathComponent("_CodeSignature/CodeResources").exists) + } +} diff --git a/Tests/XToolTests/FakeApp.swift b/Tests/XToolTests/FakeApp.swift new file mode 100644 index 00000000..ea695823 --- /dev/null +++ b/Tests/XToolTests/FakeApp.swift @@ -0,0 +1,119 @@ +import Foundation +@testable import XKit + +/// Builds minimal but signable arm64 apps on any host, and reads back what a signature contains. +enum FakeApp { + + /// A just-valid arm64 `MH_EXECUTE` with room for a code signature + static func machO(sectionName: String = "__text", contentOffset: UInt32 = 0x1000) -> Data { + var data = Data() + func append(_ value: T) { + withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) } + } + func name(_ string: String) { + data.append(contentsOf: Array(string.utf8) + Array(repeating: 0, count: 16 - string.utf8.count)) + } + func segment( + _ segName: String, vmaddr: UInt64, vmsize: UInt64, fileoff: UInt64, filesize: UInt64, + prot: Int32, sectionCount: UInt32 = 0 + ) { + append(UInt32(0x19)) + append(UInt32(72 + 80 * sectionCount)) + name(segName) + append(vmaddr); append(vmsize); append(fileoff); append(filesize) + append(prot); append(prot); append(sectionCount); append(UInt32(0)) + if sectionCount > 0 { + name(sectionName); name("__TEXT") + append(UInt64(0x100001000)); append(UInt64(0x100)) + append(contentOffset); append(UInt32(2)); append(UInt32(0)); append(UInt32(0)) + append(UInt32(0x80000400)); append(UInt32(0)); append(UInt32(0)); append(UInt32(0)) + } + } + + var commands = Data() + swap(&data, &commands) + segment("__PAGEZERO", vmaddr: 0, vmsize: 0x100000000, fileoff: 0, filesize: 0, prot: 0) + segment("__TEXT", vmaddr: 0x100000000, vmsize: 0x4000, fileoff: 0, filesize: 0x4000, prot: 5, sectionCount: 1) + segment("__LINKEDIT", vmaddr: 0x100004000, vmsize: 0x4000, fileoff: 0x4000, filesize: 0x1000, prot: 1) + for value: UInt32 in [2, 24, 0x4000, 0, 0x4000, 0] { append(value) } + swap(&data, &commands) + + append(UInt32(0xfeedfacf)); append(Int32(0x0100000c)); append(Int32(0)); append(UInt32(2)) + append(UInt32(4)); append(UInt32(commands.count)); append(UInt32(0x200085)); append(UInt32(0)) + data.append(commands) + data.append(Data(count: 0x5000 - data.count)) + return data + } + + @discardableResult + static func writeBundle( + at url: URL, id: String, executable: String = "main", machO: Data = FakeApp.machO() + ) throws -> URL { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + let info: [String: Any] = ["CFBundleIdentifier": id, "CFBundleExecutable": executable] + try PropertyListSerialization.data(fromPropertyList: info, format: .xml, options: 0) + .write(to: url.appendingPathComponent("Info.plist")) + try machO.write(to: url.appendingPathComponent(executable)) + try FileManager.default.setAttributes( + [.posixPermissions: 0o755], ofItemAtPath: url.appendingPathComponent(executable).path + ) + return url + } + + /// The hash type (1 = SHA-1, 2 = SHA-256) of every CodeDirectory in the executable's signature, in slot order + static func codeDirectoryHashTypes(ofExecutable url: URL) throws -> [Int] { + let data = try Data(contentsOf: url) + func u32(_ offset: Int, bigEndian: Bool = false) -> Int { + let value = (0..<4).reduce(UInt32(0)) { $0 | UInt32(data[offset + $1]) << (8 * UInt32($1)) } + return Int(bigEndian ? value.byteSwapped : value) + } + var offset = 32 + for _ in 0.. [String: Any]? { + let data = try Data(contentsOf: url) + func u32(_ offset: Int, bigEndian: Bool = false) -> Int { + let value = (0..<4).reduce(UInt32(0)) { $0 | UInt32(data[offset + $1]) << (8 * UInt32($1)) } + return Int(bigEndian ? value.byteSwapped : value) + } + // walk the load commands to LC_CODE_SIGNATURE (0x1d) + var offset = 32 + for _ in 0.. (executable: URL, error: (any Error)?) { + let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + let app = try FakeApp.writeBundle( + at: root.appendingPathComponent("Test.app"), id: "com.example.layout", machO: machO + ) + let ents = try PropertyListDecoder().decode( + Entitlements.self, + from: PropertyListSerialization.data(fromPropertyList: ["get-task-allow": false], format: .xml, options: 0) + ) + do { + try await Signer.first().sign( + app: app, identity: .adhoc, entitlementMapping: [app: ents], progress: { _ in } + ) + return (app.appendingPathComponent("main"), nil) + } catch { + return (app.appendingPathComponent("main"), error) + } + } + + @Test func failureLeavesTheExecutableUntouched() async throws { + // the first section starts right after the load commands: no room for LC_CODE_SIGNATURE + let cramped = FakeApp.machO(contentOffset: 32 + 72 + 152 + 72 + 24) + let result = try await sign(machO: cramped) + defer { try? FileManager.default.removeItem(at: result.executable.deletingLastPathComponent().deletingLastPathComponent()) } + #expect(result.error != nil) + #expect(try Data(contentsOf: result.executable) == cramped) + } + + @Test func freeSpaceIsMeasuredFromTheFirstSectionWhateverItsName() async throws { + let result = try await sign(machO: FakeApp.machO(sectionName: "__const")) + defer { try? FileManager.default.removeItem(at: result.executable.deletingLastPathComponent().deletingLastPathComponent()) } + #expect(result.error == nil) + #expect(try FakeApp.entitlements(ofExecutable: result.executable)?["get-task-allow"] as? Bool == false) + } +} diff --git a/Tests/XToolTests/SigningFixtures.swift b/Tests/XToolTests/SigningFixtures.swift new file mode 100644 index 00000000..a8e87fea --- /dev/null +++ b/Tests/XToolTests/SigningFixtures.swift @@ -0,0 +1,33 @@ +// Generated by scripts/generate-signing-fixtures.sh. Do not edit. +// Throwaway self-signed identities and hand-written profiles: no real secret is involved. + +import Foundation + +enum SigningFixtures { + static let teamID = "TEAM123456" + static let bundleID = "com.example.app" + static let p12Password = "xtool-test-password" + + // swiftlint:disable:next line_length + static let distributionP12 = Data(base64Encoded: "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")! + // swiftlint:disable:next line_length + static let developmentP12 = Data(base64Encoded: "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")! + // swiftlint:disable:next line_length + static let distributionCertificate = Data(base64Encoded: "MIIDvTCCAqWgAwIBAgIUFpAMEWUWo7XdkeQhB1bpQkiNMQ0wDQYJKoZIhvcNAQELBQAwbTELMAkGA1UEBhMCVVMxEzARBgNVBAoMClh0b29sIFRlc3QxEzARBgNVBAsMClRFQU0xMjM0NTYxNDAyBgNVBAMMK0FwcGxlIERpc3RyaWJ1dGlvbjogWHRvb2wgVGVzdCAoVEVBTTEyMzQ1NikwIBcNMjYxMDEwMjI0MjA4WhgPMjEyNjA5MTYyMjQyMDhaMG0xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApYdG9vbCBUZXN0MRMwEQYDVQQLDApURUFNMTIzNDU2MTQwMgYDVQQDDCtBcHBsZSBEaXN0cmlidXRpb246IFh0b29sIFRlc3QgKFRFQU0xMjM0NTYpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlXXmA0oBH+dZ5y7txaMxQLJuuWh4TogxWkoStrG1UIetInFHTpHl6OIrtIUehYnSlAaTrh2G3gPs9jr3fBM8R4vemMMl5EnNVb62JIoOFewOBzE7XJGL6dmoNcXp8YDnIc2Ad78wkRryaz2ywHLTwPQKC69oXxlTLJREzHfJIcyq1reqpCqDDLP6FIVK0mL3covormHgxasOkISMBUC1UdyplG3MudabVo0RqX/IrpJJGi2vCFaCDPFZSC6bm4H6jELE8Eo9V4kiL/Bu+2PJFjoWMGPlurW6Bht7W6d1xyRkK/+AsqhxWfJh3BMNBHsAed09W2pZF2kJmBm8lrkYtQIDAQABo1MwUTAdBgNVHQ4EFgQU2Ng414oJagv7vIgGNcSwhPcievMwHwYDVR0jBBgwFoAU2Ng414oJagv7vIgGNcSwhPcievMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcRF1aUDGo0B/63rr3s2MSdW94t1JSoGAP0l0zL+J3gbuAv/ge0rWKMHcbFtl/FRYQ1afbgqN1686pmuYvT2HmdmvU7Rct56C6ehbIEaY+YKcj3m791Z78LY0wbYGp1pQc3eaeW0Ez+D+SO4pPxoTY0IDRb5YkAMEQw3X8lRd4s4zEkpXFDFJYg3/Ua4n4krY+agjMLQyYiEBO54/9KVtkae5j8a+wpuoWOkZcNV08LJmsfkgF286+h2ZhtoPiPsZEp9NNwhn1svBS7AB66NAsEfWeqrHjNnlJp7rQwoZR9pzw4zbJ4SNhcvYJJJsfkhHgDwPmIqwHYGsgpJ8rK9kSA==")! + // swiftlint:disable:next line_length + static let appstoreProfile = Data(base64Encoded: "MIIQqwYJKoZIhvcNAQcCoIIQnDCCEJgCAQExDTALBglghkgBZQMEAgEwggojBgkqhkiG9w0BBwGgggoUBIIKEDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+CiAgICA8a2V5Pk5hbWU8L2tleT48c3RyaW5nPnh0b29sIGFwcHN0b3JlPC9zdHJpbmc+CiAgICA8a2V5PlVVSUQ8L2tleT48c3RyaW5nPjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAxMDM5Mzwvc3RyaW5nPgogICAgPGtleT5UZWFtSWRlbnRpZmllcjwva2V5PjxhcnJheT48c3RyaW5nPlRFQU0xMjM0NTY8L3N0cmluZz48L2FycmF5PgogICAgPGtleT5QbGF0Zm9ybTwva2V5PjxhcnJheT48c3RyaW5nPmlPUzwvc3RyaW5nPjwvYXJyYXk+CiAgICA8a2V5PkNyZWF0aW9uRGF0ZTwva2V5PjxkYXRlPjIwMjQtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgogICAgPGtleT5FeHBpcmF0aW9uRGF0ZTwva2V5PjxkYXRlPjIxMjYtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgogICAgPGtleT5EZXZlbG9wZXJDZXJ0aWZpY2F0ZXM8L2tleT48YXJyYXk+PGRhdGE+TUlJRHZUQ0NBcVdnQXdJQkFnSVVGcEFNRVdVV283WGRrZVFoQjFicFFraU5NUTB3RFFZSktvWklodmNOQVFFTEJRQXdiVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFvTUNsaDBiMjlzSUZSbGMzUXhFekFSQmdOVkJBc01DbFJGUVUweE1qTTBOVFl4TkRBeUJnTlZCQU1NSzBGd2NHeGxJRVJwYzNSeWFXSjFkR2x2YmpvZ1dIUnZiMndnVkdWemRDQW9WRVZCVFRFeU16UTFOaWt3SUJjTk1qWXhNREV3TWpJME1qQTRXaGdQTWpFeU5qQTVNVFl5TWpReU1EaGFNRzB4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRS0RBcFlkRzl2YkNCVVpYTjBNUk13RVFZRFZRUUxEQXBVUlVGTk1USXpORFUyTVRRd01nWURWUVFEREN0QmNIQnNaU0JFYVhOMGNtbGlkWFJwYjI0NklGaDBiMjlzSUZSbGMzUWdLRlJGUVUweE1qTTBOVFlwTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFsWFhtQTBvQkgrZFo1eTd0eGFNeFFMSnV1V2g0VG9neFdrb1N0ckcxVUlldEluRkhUcEhsNk9JcnRJVWVoWW5TbEFhVHJoMkczZ1BzOWpyM2ZCTThSNHZlbU1NbDVFbk5WYjYySklvT0Zld09CekU3WEpHTDZkbW9OY1hwOFlEbkljMkFkNzh3a1JyeWF6Mnl3SExUd1BRS0M2OW9YeGxUTEpSRXpIZkpJY3lxMXJlcXBDcURETFA2RklWSzBtTDNjb3Zvcm1IZ3hhc09rSVNNQlVDMVVkeXBsRzNNdWRhYlZvMFJxWC9JcnBKSkdpMnZDRmFDRFBGWlNDNmJtNEg2akVMRThFbzlWNGtpTC9CdSsyUEpGam9XTUdQbHVyVzZCaHQ3VzZkMXh5UmtLLytBc3FoeFdmSmgzQk1OQkhzQWVkMDlXMnBaRjJrSm1CbThscmtZdFFJREFRQUJvMU13VVRBZEJnTlZIUTRFRmdRVTJOZzQxNG9KYWd2N3ZJZ0dOY1N3aFBjaWV2TXdId1lEVlIwakJCZ3dGb0FVMk5nNDE0b0phZ3Y3dklnR05jU3doUGNpZXZNd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWNSRjFhVURHbzBCLzYzcnIzczJNU2RXOTR0MUpTb0dBUDBsMHpMK0ozZ2J1QXYvZ2UwcldLTUhjYkZ0bC9GUllRMWFmYmdxTjE2ODZwbXVZdlQySG1kbXZVN1JjdDU2QzZlaGJJRWFZK1lLY2ozbTc5MVo3OExZMHdiWUdwMXBRYzNlYWVXMEV6K0QrU080cFB4b1RZMElEUmI1WWtBTUVRdzNYOGxSZDRzNHpFa3BYRkRGSllnMy9VYTRuNGtyWSthZ2pNTFF5WWlFQk81NC85S1Z0a2FlNWo4YSt3cHVvV09rWmNOVjA4TEptc2ZrZ0YyODYraDJaaHRvUGlQc1pFcDlOTndobjFzdkJTN0FCNjZOQXNFZldlcXJIak5ubEpwN3JRd29aUjlwenc0emJKNFNOaGN2WUpKSnNma2hIZ0R3UG1JcXdIWUdzZ3BKOHJLOWtTQT09PC9kYXRhPjwvYXJyYXk+CiAgICAKICAgIDxrZXk+RW50aXRsZW1lbnRzPC9rZXk+CiAgICA8ZGljdD4KICAgICAgICA8a2V5PmFwcGxpY2F0aW9uLWlkZW50aWZpZXI8L2tleT48c3RyaW5nPlRFQU0xMjM0NTYuY29tLmV4YW1wbGUuYXBwPC9zdHJpbmc+CiAgICAgICAgPGtleT5jb20uYXBwbGUuZGV2ZWxvcGVyLnRlYW0taWRlbnRpZmllcjwva2V5PjxzdHJpbmc+VEVBTTEyMzQ1Njwvc3RyaW5nPgogICAgICAgIDxrZXk+Z2V0LXRhc2stYWxsb3c8L2tleT48ZmFsc2UvPgogICAgICAgIDxrZXk+YXBzLWVudmlyb25tZW50PC9rZXk+PHN0cmluZz5wcm9kdWN0aW9uPC9zdHJpbmc+CiAgICAgICAgPGtleT5iZXRhLXJlcG9ydHMtYWN0aXZlPC9rZXk+PHRydWUvPgogICAgICAgIDxrZXk+Y29tLmFwcGxlLmRldmVsb3Blci5hc3NvY2lhdGVkLWRvbWFpbnM8L2tleT48YXJyYXk+PHN0cmluZz4qPC9zdHJpbmc+PC9hcnJheT4KICAgICAgICA8a2V5PmNvbS5hcHBsZS5zZWN1cml0eS5hcHBsaWNhdGlvbi1ncm91cHM8L2tleT48YXJyYXk+PHN0cmluZz5ncm91cC5jb20uZXhhbXBsZS5hcHA8L3N0cmluZz48L2FycmF5PgogICAgICAgIDxrZXk+a2V5Y2hhaW4tYWNjZXNzLWdyb3Vwczwva2V5PjxhcnJheT48c3RyaW5nPlRFQU0xMjM0NTYuKjwvc3RyaW5nPjwvYXJyYXk+CiAgICA8L2RpY3Q+CjwvZGljdD4KPC9wbGlzdD4KoIIDwTCCA70wggKloAMCAQICFBaQDBFlFqO13ZHkIQdW6UJIjTENMA0GCSqGSIb3DQEBCwUAMG0xCzAJ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")! + // swiftlint:disable:next line_length + static let wildcardProfile = Data(base64Encoded: "MIIQnQYJKoZIhvcNAQcCoIIQjjCCEIoCAQExDTALBglghkgBZQMEAgEwggoVBgkqhkiG9w0BBwGgggoGBIIKAjw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+CiAgICA8a2V5Pk5hbWU8L2tleT48c3RyaW5nPnh0b29sIHdpbGRjYXJkPC9zdHJpbmc+CiAgICA8a2V5PlVVSUQ8L2tleT48c3RyaW5nPjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAyMDc4ODwvc3RyaW5nPgogICAgPGtleT5UZWFtSWRlbnRpZmllcjwva2V5PjxhcnJheT48c3RyaW5nPlRFQU0xMjM0NTY8L3N0cmluZz48L2FycmF5PgogICAgPGtleT5QbGF0Zm9ybTwva2V5PjxhcnJheT48c3RyaW5nPmlPUzwvc3RyaW5nPjwvYXJyYXk+CiAgICA8a2V5PkNyZWF0aW9uRGF0ZTwva2V5PjxkYXRlPjIwMjQtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgogICAgPGtleT5FeHBpcmF0aW9uRGF0ZTwva2V5PjxkYXRlPjIxMjYtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgogICAgPGtleT5EZXZlbG9wZXJDZXJ0aWZpY2F0ZXM8L2tleT48YXJyYXk+PGRhdGE+TUlJRHZUQ0NBcVdnQXdJQkFnSVVGcEFNRVdVV283WGRrZVFoQjFicFFraU5NUTB3RFFZSktvWklodmNOQVFFTEJRQXdiVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFvTUNsaDBiMjlzSUZSbGMzUXhFekFSQmdOVkJBc01DbFJGUVUweE1qTTBOVFl4TkRBeUJnTlZCQU1NSzBGd2NHeGxJRVJwYzNSeWFXSjFkR2x2YmpvZ1dIUnZiMndnVkdWemRDQW9WRVZCVFRFeU16UTFOaWt3SUJjTk1qWXhNREV3TWpJME1qQTRXaGdQTWpFeU5qQTVNVFl5TWpReU1EaGFNRzB4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRS0RBcFlkRzl2YkNCVVpYTjBNUk13RVFZRFZRUUxEQXBVUlVGTk1USXpORFUyTVRRd01nWURWUVFEREN0QmNIQnNaU0JFYVhOMGNtbGlkWFJwYjI0NklGaDBiMjlzSUZSbGMzUWdLRlJGUVUweE1qTTBOVFlwTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFsWFhtQTBvQkgrZFo1eTd0eGFNeFFMSnV1V2g0VG9neFdrb1N0ckcxVUlldEluRkhUcEhsNk9JcnRJVWVoWW5TbEFhVHJoMkczZ1BzOWpyM2ZCTThSNHZlbU1NbDVFbk5WYjYySklvT0Zld09CekU3WEpHTDZkbW9OY1hwOFlEbkljMkFkNzh3a1JyeWF6Mnl3SExUd1BRS0M2OW9YeGxUTEpSRXpIZkpJY3lxMXJlcXBDcURETFA2RklWSzBtTDNjb3Zvcm1IZ3hhc09rSVNNQlVDMVVkeXBsRzNNdWRhYlZvMFJxWC9JcnBKSkdpMnZDRmFDRFBGWlNDNmJtNEg2akVMRThFbzlWNGtpTC9CdSsyUEpGam9XTUdQbHVyVzZCaHQ3VzZkMXh5UmtLLytBc3FoeFdmSmgzQk1OQkhzQWVkMDlXMnBaRjJrSm1CbThscmtZdFFJREFRQUJvMU13VVRBZEJnTlZIUTRFRmdRVTJOZzQxNG9KYWd2N3ZJZ0dOY1N3aFBjaWV2TXdId1lEVlIwakJCZ3dGb0FVMk5nNDE0b0phZ3Y3dklnR05jU3doUGNpZXZNd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWNSRjFhVURHbzBCLzYzcnIzczJNU2RXOTR0MUpTb0dBUDBsMHpMK0ozZ2J1QXYvZ2UwcldLTUhjYkZ0bC9GUllRMWFmYmdxTjE2ODZwbXVZdlQySG1kbXZVN1JjdDU2QzZlaGJJRWFZK1lLY2ozbTc5MVo3OExZMHdiWUdwMXBRYzNlYWVXMEV6K0QrU080cFB4b1RZMElEUmI1WWtBTUVRdzNYOGxSZDRzNHpFa3BYRkRGSllnMy9VYTRuNGtyWSthZ2pNTFF5WWlFQk81NC85S1Z0a2FlNWo4YSt3cHVvV09rWmNOVjA4TEptc2ZrZ0YyODYraDJaaHRvUGlQc1pFcDlOTndobjFzdkJTN0FCNjZOQXNFZldlcXJIak5ubEpwN3JRd29aUjlwenc0emJKNFNOaGN2WUpKSnNma2hIZ0R3UG1JcXdIWUdzZ3BKOHJLOWtTQT09PC9kYXRhPjwvYXJyYXk+CiAgICAKICAgIDxrZXk+RW50aXRsZW1lbnRzPC9rZXk+CiAgICA8ZGljdD4KICAgICAgICA8a2V5PmFwcGxpY2F0aW9uLWlkZW50aWZpZXI8L2tleT48c3RyaW5nPlRFQU0xMjM0NTYuKjwvc3RyaW5nPgogICAgICAgIDxrZXk+Y29tLmFwcGxlLmRldmVsb3Blci50ZWFtLWlkZW50aWZpZXI8L2tleT48c3RyaW5nPlRFQU0xMjM0NTY8L3N0cmluZz4KICAgICAgICA8a2V5PmdldC10YXNrLWFsbG93PC9rZXk+PGZhbHNlLz4KICAgICAgICA8a2V5PmFwcy1lbnZpcm9ubWVudDwva2V5PjxzdHJpbmc+cHJvZHVjdGlvbjwvc3RyaW5nPgogICAgICAgIDxrZXk+YmV0YS1yZXBvcnRzLWFjdGl2ZTwva2V5Pjx0cnVlLz4KICAgICAgICA8a2V5PmNvbS5hcHBsZS5kZXZlbG9wZXIuYXNzb2NpYXRlZC1kb21haW5zPC9rZXk+PGFycmF5PjxzdHJpbmc+Kjwvc3RyaW5nPjwvYXJyYXk+CiAgICAgICAgPGtleT5jb20uYXBwbGUuc2VjdXJpdHkuYXBwbGljYXRpb24tZ3JvdXBzPC9rZXk+PGFycmF5PjxzdHJpbmc+Z3JvdXAuY29tLmV4YW1wbGUuYXBwPC9zdHJpbmc+PC9hcnJheT4KICAgICAgICA8a2V5PmtleWNoYWluLWFjY2Vzcy1ncm91cHM8L2tleT48YXJyYXk+PHN0cmluZz5URUFNMTIzNDU2Lio8L3N0cmluZz48L2FycmF5PgogICAgPC9kaWN0Pgo8L2RpY3Q+CjwvcGxpc3Q+CqCCA8EwggO9MIICpaADAgECAhQWkAwRZRajtd2R5CEHVulCSI0xDTANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQGEwJVUzETMBEG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")! + // swiftlint:disable:next line_length + static let adhocProfile = Data(base64Encoded: "MIIQBwYJKoZIhvcNAQcCoIIP+DCCD/QCAQExDTALBglghkgBZQMEAgEwggl/BgkqhkiG9w0BBwGggglwBIIJbDw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+CiAgICA8a2V5Pk5hbWU8L2tleT48c3RyaW5nPnh0b29sIGFkaG9jPC9zdHJpbmc+CiAgICA8a2V5PlVVSUQ8L2tleT48c3RyaW5nPjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAxMTIyNDwvc3RyaW5nPgogICAgPGtleT5UZWFtSWRlbnRpZmllcjwva2V5PjxhcnJheT48c3RyaW5nPlRFQU0xMjM0NTY8L3N0cmluZz48L2FycmF5PgogICAgPGtleT5QbGF0Zm9ybTwva2V5PjxhcnJheT48c3RyaW5nPmlPUzwvc3RyaW5nPjwvYXJyYXk+CiAgICA8a2V5PkNyZWF0aW9uRGF0ZTwva2V5PjxkYXRlPjIwMjQtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgogICAgPGtleT5FeHBpcmF0aW9uRGF0ZTwva2V5PjxkYXRlPjIxMjYtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgogICAgPGtleT5EZXZlbG9wZXJDZXJ0aWZpY2F0ZXM8L2tleT48YXJyYXk+PGRhdGE+TUlJRHZUQ0NBcVdnQXdJQkFnSVVGcEFNRVdVV283WGRrZVFoQjFicFFraU5NUTB3RFFZSktvWklodmNOQVFFTEJRQXdiVEVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFvTUNsaDBiMjlzSUZSbGMzUXhFekFSQmdOVkJBc01DbFJGUVUweE1qTTBOVFl4TkRBeUJnTlZCQU1NSzBGd2NHeGxJRVJwYzNSeWFXSjFkR2x2YmpvZ1dIUnZiMndnVkdWemRDQW9WRVZCVFRFeU16UTFOaWt3SUJjTk1qWXhNREV3TWpJME1qQTRXaGdQTWpFeU5qQTVNVFl5TWpReU1EaGFNRzB4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRS0RBcFlkRzl2YkNCVVpYTjBNUk13RVFZRFZRUUxEQXBVUlVGTk1USXpORFUyTVRRd01nWURWUVFEREN0QmNIQnNaU0JFYVhOMGNtbGlkWFJwYjI0NklGaDBiMjlzSUZSbGMzUWdLRlJGUVUweE1qTTBOVFlwTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFsWFhtQTBvQkgrZFo1eTd0eGFNeFFMSnV1V2g0VG9neFdrb1N0ckcxVUlldEluRkhUcEhsNk9JcnRJVWVoWW5TbEFhVHJoMkczZ1BzOWpyM2ZCTThSNHZlbU1NbDVFbk5WYjYySklvT0Zld09CekU3WEpHTDZkbW9OY1hwOFlEbkljMkFkNzh3a1JyeWF6Mnl3SExUd1BRS0M2OW9YeGxUTEpSRXpIZkpJY3lxMXJlcXBDcURETFA2RklWSzBtTDNjb3Zvcm1IZ3hhc09rSVNNQlVDMVVkeXBsRzNNdWRhYlZvMFJxWC9JcnBKSkdpMnZDRmFDRFBGWlNDNmJtNEg2akVMRThFbzlWNGtpTC9CdSsyUEpGam9XTUdQbHVyVzZCaHQ3VzZkMXh5UmtLLytBc3FoeFdmSmgzQk1OQkhzQWVkMDlXMnBaRjJrSm1CbThscmtZdFFJREFRQUJvMU13VVRBZEJnTlZIUTRFRmdRVTJOZzQxNG9KYWd2N3ZJZ0dOY1N3aFBjaWV2TXdId1lEVlIwakJCZ3dGb0FVMk5nNDE0b0phZ3Y3dklnR05jU3doUGNpZXZNd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWNSRjFhVURHbzBCLzYzcnIzczJNU2RXOTR0MUpTb0dBUDBsMHpMK0ozZ2J1QXYvZ2UwcldLTUhjYkZ0bC9GUllRMWFmYmdxTjE2ODZwbXVZdlQySG1kbXZVN1JjdDU2QzZlaGJJRWFZK1lLY2ozbTc5MVo3OExZMHdiWUdwMXBRYzNlYWVXMEV6K0QrU080cFB4b1RZMElEUmI1WWtBTUVRdzNYOGxSZDRzNHpFa3BYRkRGSllnMy9VYTRuNGtyWSthZ2pNTFF5WWlFQk81NC85S1Z0a2FlNWo4YSt3cHVvV09rWmNOVjA4TEptc2ZrZ0YyODYraDJaaHRvUGlQc1pFcDlOTndobjFzdkJTN0FCNjZOQXNFZldlcXJIak5ubEpwN3JRd29aUjlwenc0emJKNFNOaGN2WUpKSnNma2hIZ0R3UG1JcXdIWUdzZ3BKOHJLOWtTQT09PC9kYXRhPjwvYXJyYXk+CiAgICA8a2V5PlByb3Zpc2lvbmVkRGV2aWNlczwva2V5PjxhcnJheT48c3RyaW5nPjAwMDA4MDMwLTAwMUEyQjNDNEQ1RTZGNzg8L3N0cmluZz48L2FycmF5PgogICAgPGtleT5FbnRpdGxlbWVudHM8L2tleT4KICAgIDxkaWN0PgogICAgICAgIDxrZXk+YXBwbGljYXRpb24taWRlbnRpZmllcjwva2V5PjxzdHJpbmc+VEVBTTEyMzQ1Ni5jb20uZXhhbXBsZS5hcHA8L3N0cmluZz4KICAgICAgICA8a2V5PmNvbS5hcHBsZS5kZXZlbG9wZXIudGVhbS1pZGVudGlmaWVyPC9rZXk+PHN0cmluZz5URUFNMTIzNDU2PC9zdHJpbmc+CiAgICAgICAgPGtleT5nZXQtdGFzay1hbGxvdzwva2V5PjxmYWxzZS8+CiAgICAgICAgPGtleT5hcHMtZW52aXJvbm1lbnQ8L2tleT48c3RyaW5nPnByb2R1Y3Rpb248L3N0cmluZz4KICAgICAgICA8a2V5PmtleWNoYWluLWFjY2Vzcy1ncm91cHM8L2tleT48YXJyYXk+PHN0cmluZz5URUFNMTIzNDU2Lio8L3N0cmluZz48L2FycmF5PgogICAgPC9kaWN0Pgo8L2RpY3Q+CjwvcGxpc3Q+CqCCA8EwggO9MIICpaADAgECAhQWkAwRZRajtd2R5CEHVulCSI0xDTANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQGEwJVUzETMBEGA1UECgwKWHRvb2wgVGVzdDETMBEGA1UECwwKVEVBTTEyMzQ1NjE0MDIGA1UEAwwrQXBwbGUgRGlzdHJpYnV0aW9uOiBYdG9vbCBUZXN0IChURUFNMTIzNDU2KTAgFw0yNjEwMTAyMjQyMDhaGA8yMTI2MDkxNjIyNDIwOFowbTELMAkGA1UEBhMCVVMxEzARBgNVBAoMClh0b29sIFRlc3QxEzARBgNVBAsMClRFQU0xMjM0NTYxNDAyBgNVBAMMK0FwcGxlIERpc3RyaWJ1dGlvbjogWHRvb2wgVGVzdCAoVEVBTTEyMzQ1NikwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVdeYDSgEf51nnLu3FozFAsm65aHhOiDFaShK2sbVQh60icUdOkeXo4iu0hR6FidKUBpOuHYbeA+z2Ovd8EzxHi96YwyXkSc1VvrYkig4V7A4HMTtckYvp2ag1xenxgOchzYB3vzCRGvJrPbLActPA9AoLr2hfGVMslETMd8khzKrWt6qkKoMMs/oUhUrSYvdyi+iuYeDFqw6QhIwFQLVR3KmUbcy51ptWjRGpf8iukkkaLa8IVoIM8VlILpubgfqMQsTwSj1XiSIv8G77Y8kWOhYwY+W6tboGG3tbp3XHJGQr/4CyqHFZ8mHcEw0EewB53T1balkXaQmYGbyWuRi1AgMBAAGjUzBRMB0GA1UdDgQWBBTY2DjXiglqC/u8iAY1xLCE9yJ68zAfBgNVHSMEGDAWgBTY2DjXiglqC/u8iAY1xLCE9yJ68zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBxEXVpQMajQH/reuvezYxJ1b3i3UlKgYA/SXTMv4neBu4C/+B7StYowdxsW2X8VFhDVp9uCo3Xrzqma5i9PYeZ2a9TtFy3noLp6FsgRpj5gpyPebv3VnvwtjTBtganWlBzd5p5bQTP4P5I7ik/GhNjQgNFvliQAwRDDdfyVF3izjMSSlcUMUliDf9RrifiStj5qCMwtDJiIQE7nj/0pW2Rp7mPxr7Cm6hY6Rlw1XTwsmax+SAXbzr6HZmG2g+I+xkSn003CGfWy8FLsAHro0CwR9Z6qseM2eUmnutDChlH2nPDjNsnhI2Fy9gkkmx+SEeAPA+YirAdgayCknysr2RIMYICljCCApICAQEwgYUwbTELMAkGA1UEBhMCVVMxEzARBgNVBAoMClh0b29sIFRlc3QxEzARBgNVBAsMClRFQU0xMjM0NTYxNDAyBgNVBAMMK0FwcGxlIERpc3RyaWJ1dGlvbjogWHRvb2wgVGVzdCAoVEVBTTEyMzQ1NikCFBaQDBFlFqO13ZHkIQdW6UJIjTENMAsGCWCGSAFlAwQCAaCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yNjEwMTAyMjQyMDlaMC8GCSqGSIb3DQEJBDEiBCDbzzVKJN84WeGGRdb0X4t70RRG50Hv6mnkDLaCDY5bYTB5BgkqhkiG9w0BCQ8xbDBqMAsGCWCGSAFlAwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIBKDANBgkqhkiG9w0BAQEFAASCAQCDk2cyQq0kuT9Hz3V1eoYC26X44iPBduvhH73ld5Ay4sSuHeY90VDBwwrzhm33Olb3nBnegynTApJmezVSEqc9ohLY0S0UI6aj2EpxtkcZLocBNsZYxYYn727Mi663ckp6qNCnjYXbmslJ0fDXO7yrTfc+uTy0E0n7nutfEgBITK9k5QpVCSQd8zPQWnkftFZus78ttnUolQW1IFf9cZody7XE+WzXnzn0s5rMMvuxC8xCIuBEDssZry8TUV1sWr/qOOQQ50Tk2yFWl1Fz4ln0C995wJ2Z7BZVpdWOwpLPra9W9ZvDF3UVtHxc3MF3iFskcW/0gNkdZxcEb/aH3s97")! + // swiftlint:disable:next line_length + static let developmentProfile = Data(base64Encoded: "MIIQBgYJKoZIhvcNAQcCoIIP9zCCD/MCAQExDTALBglghkgBZQMEAgEwggmBBgkqhkiG9w0BBwGggglyBIIJbjw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+CiAgICA8a2V5Pk5hbWU8L2tleT48c3RyaW5nPnh0b29sIGRldmVsb3BtZW50PC9zdHJpbmc+CiAgICA8a2V5PlVVSUQ8L2tleT48c3RyaW5nPjAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAyNjYwMjwvc3RyaW5nPgogICAgPGtleT5UZWFtSWRlbnRpZmllcjwva2V5PjxhcnJheT48c3RyaW5nPlRFQU0xMjM0NTY8L3N0cmluZz48L2FycmF5PgogICAgPGtleT5QbGF0Zm9ybTwva2V5PjxhcnJheT48c3RyaW5nPmlPUzwvc3RyaW5nPjwvYXJyYXk+CiAgICA8a2V5PkNyZWF0aW9uRGF0ZTwva2V5PjxkYXRlPjIwMjQtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgogICAgPGtleT5FeHBpcmF0aW9uRGF0ZTwva2V5PjxkYXRlPjIxMjYtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgogICAgPGtleT5EZXZlbG9wZXJDZXJ0aWZpY2F0ZXM8L2tleT48YXJyYXk+PGRhdGE+TUlJRHV6Q0NBcU9nQXdJQkFnSVVVVTg3cUdQK3VKS1RTTk9MOGYwVjc1NExQeFl3RFFZSktvWklodmNOQVFFTEJRQXdiREVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFvTUNsaDBiMjlzSUZSbGMzUXhFekFSQmdOVkJBc01DbFJGUVUweE1qTTBOVFl4TXpBeEJnTlZCQU1NS2tGd2NHeGxJRVJsZG1Wc2IzQnRaVzUwT2lCWWRHOXZiQ0JVWlhOMElDaEJRa05FUlRFeU16UTFLVEFnRncweU5qRXdNVEF5TWpReU1EbGFHQTh5TVRJMk1Ea3hOakl5TkRJd09Wb3diREVMTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFvTUNsaDBiMjlzSUZSbGMzUXhFekFSQmdOVkJBc01DbFJGUVUweE1qTTBOVFl4TXpBeEJnTlZCQU1NS2tGd2NHeGxJRVJsZG1Wc2IzQnRaVzUwT2lCWWRHOXZiQ0JVWlhOMElDaEJRa05FUlRFeU16UTFLVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFNQlF3a054YUN3UisyZzFuRklEdmRFTXpQSE9XWDM4bHV2MzZBTXFsZ0EzWDE3YkVLdFZjT2ErNjNtQVhraGluL3h3OXR2eUdXYWNrWlp4emx6QkhmZDIrU2hrdWhTNmlyeXEzUjBGQlBBU3AzekUvVU9wMGVMM2UzbVNqNXhsMElucjN5blZLS0sxNmNpOFVWd3h0UVBzMTdaY2VsZ0FBY1FUc1V1WU5OaFFTdDIrQ1d5ZTdkMEY0NWhqNk5NWTVud0E0V2lSL2xtLzlFZTI5SU9SaE5RbDFhWk1YZEsrTng5SXJHQnYxQXdpSDJ6dHFWSVc0NnBQWVE1TSt4UkZZUDVqVzMxa3Vta3NZNnMreDVFeldoM1RlVHdpMzhZN2c2dWd1RVNUZy9jMW9qWXNGSlhCWXNWQUdyYlUwaUlOUWdWYWZYQ1M1MGIyWWNHYnBkbi92QjBDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkM5NlJBV3FENUs3WUVJYjh6bm1FYTQ0MjRtck1COEdBMVVkSXdRWU1CYUFGQzk2UkFXcUQ1SzdZRUliOHpubUVhNDQyNG1yTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUp2MzA5QTdoY2pRRVI0ajNGWEtnbk9sRGN3akozNGo0MEFmQkxNNGlXRXBoSDQydERvTWxFakVUa0FtZlM0Rzd4NWdFWmY2M0xyQXZuYXdjNDg3aCtsY211ODc1bjc0T0d0b3VXUkYyNXQ0QkRFV2xhUjRkazBMQks3UVZzZklTYm1CbmM4MlhrS0JvYlRNUFRCYXh1TkQ0QjFwdTJpcWVYR2dCVDFQeE00M2pRNEI0OXk3Yzhkd2xYbnNpSnRIT0lsUjR1MHFobjFVclo4ZGx1d1AyWFRwbFVHNVVVb251MjJoUjhQWkoyclBrRmdaUUFrZ2dzeVFjbnpXdGVsanlaTUJZTWViRWFzSFphRTFLeC9GS2V5MGdZRndEUVJnSE1zeUUwaVBwUnZmb1FSMVQ5TFlqRk9XU0cwbjQ5M3hnQlF5TGUxbURMM01MU1ExVG4xanA0az08L2RhdGE+PC9hcnJheT4KICAgIDxrZXk+UHJvdmlzaW9uZWREZXZpY2VzPC9rZXk+PGFycmF5PjxzdHJpbmc+MDAwMDgwMzAtMDAxQTJCM0M0RDVFNkY3ODwvc3RyaW5nPjwvYXJyYXk+CiAgICA8a2V5PkVudGl0bGVtZW50czwva2V5PgogICAgPGRpY3Q+CiAgICAgICAgPGtleT5hcHBsaWNhdGlvbi1pZGVudGlmaWVyPC9rZXk+PHN0cmluZz5URUFNMTIzNDU2LmNvbS5leGFtcGxlLmFwcDwvc3RyaW5nPgogICAgICAgIDxrZXk+Y29tLmFwcGxlLmRldmVsb3Blci50ZWFtLWlkZW50aWZpZXI8L2tleT48c3RyaW5nPlRFQU0xMjM0NTY8L3N0cmluZz4KICAgICAgICA8a2V5PmdldC10YXNrLWFsbG93PC9rZXk+PHRydWUvPgogICAgICAgIDxrZXk+YXBzLWVudmlyb25tZW50PC9rZXk+PHN0cmluZz5kZXZlbG9wbWVudDwvc3RyaW5nPgogICAgICAgIDxrZXk+a2V5Y2hhaW4tYWNjZXNzLWdyb3Vwczwva2V5PjxhcnJheT48c3RyaW5nPlRFQU0xMjM0NTYuKjwvc3RyaW5nPjwvYXJyYXk+CiAgICA8L2RpY3Q+CjwvZGljdD4KPC9wbGlzdD4KoIIDvzCCA7swggKjoAMCAQICFFFPO6hj/riSk0jTi/H9Fe+eCz8WMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApYdG9vbCBUZXN0MRMwEQYDVQQLDApURUFNMTIzNDU2MTMwMQYDVQQDDCpBcHBsZSBEZXZlbG9wbWVudDogWHRvb2wgVGVzdCAoQUJDREUxMjM0NSkwIBcNMjYxMDEwMjI0MjA5WhgPMjEyNjA5MTYyMjQyMDlaMGwxCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApYdG9vbCBUZXN0MRMwEQYDVQQLDApURUFNMTIzNDU2MTMwMQYDVQQDDCpBcHBsZSBEZXZlbG9wbWVudDogWHRvb2wgVGVzdCAoQUJDREUxMjM0NSkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAUMJDcWgsEftoNZxSA73RDMzxzll9/Jbr9+gDKpYAN19e2xCrVXDmvut5gF5IYp/8cPbb8hlmnJGWcc5cwR33dvkoZLoUuoq8qt0dBQTwEqd8xP1DqdHi93t5ko+cZdCJ698p1SiitenIvFFcMbUD7Ne2XHpYAAHEE7FLmDTYUErdvglsnu3dBeOYY+jTGOZ8AOFokf5Zv/RHtvSDkYTUJdWmTF3SvjcfSKxgb9QMIh9s7alSFuOqT2EOTPsURWD+Y1t9ZLppLGOrPseRM1od03k8It/GO4OroLhEk4P3NaI2LBSVwWLFQBq21NIiDUIFWn1wkudG9mHBm6XZ/7wdAgMBAAGjUzBRMB0GA1UdDgQWBBQvekQFqg+Su2BCG/M55hGuONuJqzAfBgNVHSMEGDAWgBQvekQFqg+Su2BCG/M55hGuONuJqzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCb99PQO4XI0BEeI9xVyoJzpQ3MIyd+I+NAHwSzOIlhKYR+NrQ6DJRIxE5AJn0uBu8eYBGX+ty6wL52sHOPO4fpXJrvO+Z++DhraLlkRdubeAQxFpWkeHZNCwSu0FbHyEm5gZ3PNl5CgaG0zD0wWsbjQ+AdabtoqnlxoAU9T8TON40OAePcu3PHcJV57IibRziJUeLtKoZ9VK2fHZbsD9l06ZVBuVFKJ7ttoUfD2Sdqz5BYGUAJIILMkHJ81rXpY8mTAWDHmxGrB2WhNSsfxSnstIGBcA0EYBzLMhNIj6Ub36EEdU/S2IxTlkhtJ+Pd8YAUMi3tZgy9zC0kNU59Y6eJMYIClTCCApECAQEwgYQwbDELMAkGA1UEBhMCVVMxEzARBgNVBAoMClh0b29sIFRlc3QxEzARBgNVBAsMClRFQU0xMjM0NTYxMzAxBgNVBAMMKkFwcGxlIERldmVsb3BtZW50OiBYdG9vbCBUZXN0IChBQkNERTEyMzQ1KQIUUU87qGP+uJKTSNOL8f0V754LPxYwCwYJYIZIAWUDBAIBoIHkMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTI2MTAxMDIyNDIwOVowLwYJKoZIhvcNAQkEMSIEIMRe+KMMGQKDzfiWDcWNpGXZR+kJKlUwbdQQNf/SnK+IMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIBAJLqZn4I0oJm2LhwzyRU4Yz7ej8pC/rtpts5fS6CJlgAkOeuc+Y3rWhsz4SVAys6/TDlMZwvqCtxMP/UNCCkLIqB40oQlo3SF73lmQWMBgp6bIxiAlXFWWoE95TOKZo7bLVxvHXFhDPZXDjaIWUMST35hB6bD6wXpopCvOJ80gwJPckScEClWV6jaYlxQ7xyiwcCkHL7nJAVkjZ2MSoasAGzZ7mNcaEPb8feu0BYInVuT3HHC6CH96PW4mHP3Cg7vMauCF/mB2G+AasCec5ATtG5cjSPbxI+zQwrX9REw6pHClB7jSNoFWQxjB1KwdQUgnJRBxMrqYsL9q3HmQRHN0M=")! + // swiftlint:disable:next line_length + static let expiredProfile = Data(base64Encoded: "MIIQqgYJKoZIhvcNAQcCoIIQmzCCEJcCAQExDTALBglghkgBZQMEAgEwggoiBgkqhkiG9w0BBwGgggoTBIIKDzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+CiAgICA8a2V5Pk5hbWU8L2tleT48c3RyaW5nPnh0b29sIGV4cGlyZWQ8L3N0cmluZz4KICAgIDxrZXk+VVVJRDwva2V5PjxzdHJpbmc+MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDE3MTE1PC9zdHJpbmc+CiAgICA8a2V5PlRlYW1JZGVudGlmaWVyPC9rZXk+PGFycmF5PjxzdHJpbmc+VEVBTTEyMzQ1Njwvc3RyaW5nPjwvYXJyYXk+CiAgICA8a2V5PlBsYXRmb3JtPC9rZXk+PGFycmF5PjxzdHJpbmc+aU9TPC9zdHJpbmc+PC9hcnJheT4KICAgIDxrZXk+Q3JlYXRpb25EYXRlPC9rZXk+PGRhdGU+MjAyNC0wMS0wMVQwMDowMDowMFo8L2RhdGU+CiAgICA8a2V5PkV4cGlyYXRpb25EYXRlPC9rZXk+PGRhdGU+MjAyMC0wMS0wMVQwMDowMDowMFo8L2RhdGU+CiAgICA8a2V5PkRldmVsb3BlckNlcnRpZmljYXRlczwva2V5PjxhcnJheT48ZGF0YT5NSUlEdlRDQ0FxV2dBd0lCQWdJVUZwQU1FV1VXbzdYZGtlUWhCMWJwUWtpTk1RMHdEUVlKS29aSWh2Y05BUUVMQlFBd2JURUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2xoMGIyOXNJRlJsYzNReEV6QVJCZ05WQkFzTUNsUkZRVTB4TWpNME5UWXhOREF5QmdOVkJBTU1LMEZ3Y0d4bElFUnBjM1J5YVdKMWRHbHZiam9nV0hSdmIyd2dWR1Z6ZENBb1ZFVkJUVEV5TXpRMU5pa3dJQmNOTWpZeE1ERXdNakkwTWpBNFdoZ1BNakV5TmpBNU1UWXlNalF5TURoYU1HMHhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFLREFwWWRHOXZiQ0JVWlhOME1STXdFUVlEVlFRTERBcFVSVUZOTVRJek5EVTJNVFF3TWdZRFZRUUREQ3RCY0hCc1pTQkVhWE4wY21saWRYUnBiMjQ2SUZoMGIyOXNJRlJsYzNRZ0tGUkZRVTB4TWpNME5UWXBNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQWxYWG1BMG9CSCtkWjV5N3R4YU14UUxKdXVXaDRUb2d4V2tvU3RyRzFVSWV0SW5GSFRwSGw2T0lydElVZWhZblNsQWFUcmgyRzNnUHM5anIzZkJNOFI0dmVtTU1sNUVuTlZiNjJKSW9PRmV3T0J6RTdYSkdMNmRtb05jWHA4WURuSWMyQWQ3OHdrUnJ5YXoyeXdITFR3UFFLQzY5b1h4bFRMSlJFekhmSkljeXExcmVxcENxRERMUDZGSVZLMG1MM2Nvdm9ybUhneGFzT2tJU01CVUMxVWR5cGxHM011ZGFiVm8wUnFYL0lycEpKR2kydkNGYUNEUEZaU0M2Ym00SDZqRUxFOEVvOVY0a2lML0J1KzJQSkZqb1dNR1BsdXJXNkJodDdXNmQxeHlSa0svK0FzcWh4V2ZKaDNCTU5CSHNBZWQwOVcycFpGMmtKbUJtOGxya1l0UUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVMk5nNDE0b0phZ3Y3dklnR05jU3doUGNpZXZNd0h3WURWUjBqQkJnd0ZvQVUyTmc0MTRvSmFndjd2SWdHTmNTd2hQY2lldk13RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBY1JGMWFVREdvMEIvNjNycjNzMk1TZFc5NHQxSlNvR0FQMGwwekwrSjNnYnVBdi9nZTByV0tNSGNiRnRsL0ZSWVExYWZiZ3FOMTY4NnBtdVl2VDJIbWRtdlU3UmN0NTZDNmVoYklFYVkrWUtjajNtNzkxWjc4TFkwd2JZR3AxcFFjM2VhZVcwRXorRCtTTzRwUHhvVFkwSURSYjVZa0FNRVF3M1g4bFJkNHM0ekVrcFhGREZKWWczL1VhNG40a3JZK2Fnak1MUXlZaUVCTzU0LzlLVnRrYWU1ajhhK3dwdW9XT2taY05WMDhMSm1zZmtnRjI4NitoMlpodG9QaVBzWkVwOU5Od2huMXN2QlM3QUI2Nk5Bc0VmV2VxckhqTm5sSnA3clF3b1pSOXB6dzR6Yko0U05oY3ZZSkpKc2ZraEhnRHdQbUlxd0hZR3NncEo4cks5a1NBPT08L2RhdGE+PC9hcnJheT4KICAgIAogICAgPGtleT5FbnRpdGxlbWVudHM8L2tleT4KICAgIDxkaWN0PgogICAgICAgIDxrZXk+YXBwbGljYXRpb24taWRlbnRpZmllcjwva2V5PjxzdHJpbmc+VEVBTTEyMzQ1Ni5jb20uZXhhbXBsZS5hcHA8L3N0cmluZz4KICAgICAgICA8a2V5PmNvbS5hcHBsZS5kZXZlbG9wZXIudGVhbS1pZGVudGlmaWVyPC9rZXk+PHN0cmluZz5URUFNMTIzNDU2PC9zdHJpbmc+CiAgICAgICAgPGtleT5nZXQtdGFzay1hbGxvdzwva2V5PjxmYWxzZS8+CiAgICAgICAgPGtleT5hcHMtZW52aXJvbm1lbnQ8L2tleT48c3RyaW5nPnByb2R1Y3Rpb248L3N0cmluZz4KICAgICAgICA8a2V5PmJldGEtcmVwb3J0cy1hY3RpdmU8L2tleT48dHJ1ZS8+CiAgICAgICAgPGtleT5jb20uYXBwbGUuZGV2ZWxvcGVyLmFzc29jaWF0ZWQtZG9tYWluczwva2V5PjxhcnJheT48c3RyaW5nPio8L3N0cmluZz48L2FycmF5PgogICAgICAgIDxrZXk+Y29tLmFwcGxlLnNlY3VyaXR5LmFwcGxpY2F0aW9uLWdyb3Vwczwva2V5PjxhcnJheT48c3RyaW5nPmdyb3VwLmNvbS5leGFtcGxlLmFwcDwvc3RyaW5nPjwvYXJyYXk+CiAgICAgICAgPGtleT5rZXljaGFpbi1hY2Nlc3MtZ3JvdXBzPC9rZXk+PGFycmF5PjxzdHJpbmc+VEVBTTEyMzQ1Ni4qPC9zdHJpbmc+PC9hcnJheT4KICAgIDwvZGljdD4KPC9kaWN0Pgo8L3BsaXN0PgqgggPBMIIDvTCCAqWgAwIBAgIUFpAMEWUWo7XdkeQhB1bpQkiNMQ0wDQYJKoZIhvcNAQELBQAwbTELMAkG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")! + // swiftlint:disable:next line_length + static let otherteamProfile = Data(base64Encoded: "MIIQrAYJKoZIhvcNAQcCoIIQnTCCEJkCAQExDTALBglghkgBZQMEAgEwggokBgkqhkiG9w0BBwGgggoVBIIKETw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+CiAgICA8a2V5Pk5hbWU8L2tleT48c3RyaW5nPnh0b29sIG90aGVydGVhbTwvc3RyaW5nPgogICAgPGtleT5VVUlEPC9rZXk+PHN0cmluZz4wMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMTY0MDI8L3N0cmluZz4KICAgIDxrZXk+VGVhbUlkZW50aWZpZXI8L2tleT48YXJyYXk+PHN0cmluZz5PVEhFUlRFQU0xPC9zdHJpbmc+PC9hcnJheT4KICAgIDxrZXk+UGxhdGZvcm08L2tleT48YXJyYXk+PHN0cmluZz5pT1M8L3N0cmluZz48L2FycmF5PgogICAgPGtleT5DcmVhdGlvbkRhdGU8L2tleT48ZGF0ZT4yMDI0LTAxLTAxVDAwOjAwOjAwWjwvZGF0ZT4KICAgIDxrZXk+RXhwaXJhdGlvbkRhdGU8L2tleT48ZGF0ZT4yMTI2LTAxLTAxVDAwOjAwOjAwWjwvZGF0ZT4KICAgIDxrZXk+RGV2ZWxvcGVyQ2VydGlmaWNhdGVzPC9rZXk+PGFycmF5PjxkYXRhPk1JSUR2VENDQXFXZ0F3SUJBZ0lVRnBBTUVXVVdvN1hka2VRaEIxYnBRa2lOTVEwd0RRWUpLb1pJaHZjTkFRRUxCUUF3YlRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBb01DbGgwYjI5c0lGUmxjM1F4RXpBUkJnTlZCQXNNQ2xSRlFVMHhNak0wTlRZeE5EQXlCZ05WQkFNTUswRndjR3hsSUVScGMzUnlhV0oxZEdsdmJqb2dXSFJ2YjJ3Z1ZHVnpkQ0FvVkVWQlRURXlNelExTmlrd0lCY05Nall4TURFd01qSTBNakE0V2hnUE1qRXlOakE1TVRZeU1qUXlNRGhhTUcweEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBZZEc5dmJDQlVaWE4wTVJNd0VRWURWUVFMREFwVVJVRk5NVEl6TkRVMk1UUXdNZ1lEVlFRRERDdEJjSEJzWlNCRWFYTjBjbWxpZFhScGIyNDZJRmgwYjI5c0lGUmxjM1FnS0ZSRlFVMHhNak0wTlRZcE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFhYbUEwb0JIK2RaNXk3dHhhTXhRTEp1dVdoNFRvZ3hXa29TdHJHMVVJZXRJbkZIVHBIbDZPSXJ0SVVlaFluU2xBYVRyaDJHM2dQczlqcjNmQk04UjR2ZW1NTWw1RW5OVmI2MkpJb09GZXdPQnpFN1hKR0w2ZG1vTmNYcDhZRG5JYzJBZDc4d2tScnlhejJ5d0hMVHdQUUtDNjlvWHhsVExKUkV6SGZKSWN5cTFyZXFwQ3FERExQNkZJVkswbUwzY292b3JtSGd4YXNPa0lTTUJVQzFVZHlwbEczTXVkYWJWbzBScVgvSXJwSkpHaTJ2Q0ZhQ0RQRlpTQzZibTRINmpFTEU4RW85VjRraUwvQnUrMlBKRmpvV01HUGx1clc2Qmh0N1c2ZDF4eVJrSy8rQXNxaHhXZkpoM0JNTkJIc0FlZDA5VzJwWkYya0ptQm04bHJrWXRRSURBUUFCbzFNd1VUQWRCZ05WSFE0RUZnUVUyTmc0MTRvSmFndjd2SWdHTmNTd2hQY2lldk13SHdZRFZSMGpCQmd3Rm9BVTJOZzQxNG9KYWd2N3ZJZ0dOY1N3aFBjaWV2TXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFjUkYxYVVER28wQi82M3JyM3MyTVNkVzk0dDFKU29HQVAwbDB6TCtKM2didUF2L2dlMHJXS01IY2JGdGwvRlJZUTFhZmJncU4xNjg2cG11WXZUMkhtZG12VTdSY3Q1NkM2ZWhiSUVhWStZS2NqM203OTFaNzhMWTB3YllHcDFwUWMzZWFlVzBFeitEK1NPNHBQeG9UWTBJRFJiNVlrQU1FUXczWDhsUmQ0czR6RWtwWEZERkpZZzMvVWE0bjRrclkrYWdqTUxReVlpRUJPNTQvOUtWdGthZTVqOGErd3B1b1dPa1pjTlYwOExKbXNma2dGMjg2K2gyWmh0b1BpUHNaRXA5Tk53aG4xc3ZCUzdBQjY2TkFzRWZXZXFySGpObmxKcDdyUXdvWlI5cHp3NHpiSjRTTmhjdllKSkpzZmtoSGdEd1BtSXF3SFlHc2dwSjhySzlrU0E9PTwvZGF0YT48L2FycmF5PgogICAgCiAgICA8a2V5PkVudGl0bGVtZW50czwva2V5PgogICAgPGRpY3Q+CiAgICAgICAgPGtleT5hcHBsaWNhdGlvbi1pZGVudGlmaWVyPC9rZXk+PHN0cmluZz5PVEhFUlRFQU0xLmNvbS5leGFtcGxlLmFwcDwvc3RyaW5nPgogICAgICAgIDxrZXk+Y29tLmFwcGxlLmRldmVsb3Blci50ZWFtLWlkZW50aWZpZXI8L2tleT48c3RyaW5nPk9USEVSVEVBTTE8L3N0cmluZz4KICAgICAgICA8a2V5PmdldC10YXNrLWFsbG93PC9rZXk+PGZhbHNlLz4KICAgICAgICA8a2V5PmFwcy1lbnZpcm9ubWVudDwva2V5PjxzdHJpbmc+cHJvZHVjdGlvbjwvc3RyaW5nPgogICAgICAgIDxrZXk+YmV0YS1yZXBvcnRzLWFjdGl2ZTwva2V5Pjx0cnVlLz4KICAgICAgICA8a2V5PmNvbS5hcHBsZS5kZXZlbG9wZXIuYXNzb2NpYXRlZC1kb21haW5zPC9rZXk+PGFycmF5PjxzdHJpbmc+Kjwvc3RyaW5nPjwvYXJyYXk+CiAgICAgICAgPGtleT5jb20uYXBwbGUuc2VjdXJpdHkuYXBwbGljYXRpb24tZ3JvdXBzPC9rZXk+PGFycmF5PjxzdHJpbmc+Z3JvdXAuY29tLmV4YW1wbGUuYXBwPC9zdHJpbmc+PC9hcnJheT4KICAgICAgICA8a2V5PmtleWNoYWluLWFjY2Vzcy1ncm91cHM8L2tleT48YXJyYXk+PHN0cmluZz5URUFNMTIzNDU2Lio8L3N0cmluZz48L2FycmF5PgogICAgPC9kaWN0Pgo8L2RpY3Q+CjwvcGxpc3Q+CqCCA8EwggO9MIICpaADAgECAhQWkAwRZRajtd2R5CEHVulCSI0xDTANBgkqhkiG9w0BAQsFADBtMQsw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")! + // swiftlint:disable:next line_length + static let othercertProfile = Data(base64Encoded: "MIIQqAYJKoZIhvcNAQcCoIIQmTCCEJUCAQExDTALBglghkgBZQMEAgEwggogBgkqhkiG9w0BBwGgggoRBIIKDTw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+CiAgICA8a2V5Pk5hbWU8L2tleT48c3RyaW5nPnh0b29sIG90aGVyY2VydDwvc3RyaW5nPgogICAgPGtleT5VVUlEPC9rZXk+PHN0cmluZz4wMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMTQ1NzU8L3N0cmluZz4KICAgIDxrZXk+VGVhbUlkZW50aWZpZXI8L2tleT48YXJyYXk+PHN0cmluZz5URUFNMTIzNDU2PC9zdHJpbmc+PC9hcnJheT4KICAgIDxrZXk+UGxhdGZvcm08L2tleT48YXJyYXk+PHN0cmluZz5pT1M8L3N0cmluZz48L2FycmF5PgogICAgPGtleT5DcmVhdGlvbkRhdGU8L2tleT48ZGF0ZT4yMDI0LTAxLTAxVDAwOjAwOjAwWjwvZGF0ZT4KICAgIDxrZXk+RXhwaXJhdGlvbkRhdGU8L2tleT48ZGF0ZT4yMTI2LTAxLTAxVDAwOjAwOjAwWjwvZGF0ZT4KICAgIDxrZXk+RGV2ZWxvcGVyQ2VydGlmaWNhdGVzPC9rZXk+PGFycmF5PjxkYXRhPk1JSUR1ekNDQXFPZ0F3SUJBZ0lVVVU4N3FHUCt1SktUU05PTDhmMFY3NTRMUHhZd0RRWUpLb1pJaHZjTkFRRUxCUUF3YkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBb01DbGgwYjI5c0lGUmxjM1F4RXpBUkJnTlZCQXNNQ2xSRlFVMHhNak0wTlRZeE16QXhCZ05WQkFNTUtrRndjR3hsSUVSbGRtVnNiM0J0Wlc1ME9pQllkRzl2YkNCVVpYTjBJQ2hCUWtORVJURXlNelExS1RBZ0Z3MHlOakV3TVRBeU1qUXlNRGxhR0E4eU1USTJNRGt4TmpJeU5ESXdPVm93YkRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBb01DbGgwYjI5c0lGUmxjM1F4RXpBUkJnTlZCQXNNQ2xSRlFVMHhNak0wTlRZeE16QXhCZ05WQkFNTUtrRndjR3hsSUVSbGRtVnNiM0J0Wlc1ME9pQllkRzl2YkNCVVpYTjBJQ2hCUWtORVJURXlNelExS1RDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUJRd2tOeGFDd1IrMmcxbkZJRHZkRU16UEhPV1gzOGx1djM2QU1xbGdBM1gxN2JFS3RWY09hKzYzbUFYa2hpbi94dzl0dnlHV2Fja1paeHpsekJIZmQyK1Noa3VoUzZpcnlxM1IwRkJQQVNwM3pFL1VPcDBlTDNlM21TajV4bDBJbnIzeW5WS0tLMTZjaThVVnd4dFFQczE3WmNlbGdBQWNRVHNVdVlOTmhRU3QyK0NXeWU3ZDBGNDVoajZOTVk1bndBNFdpUi9sbS85RWUyOUlPUmhOUWwxYVpNWGRLK054OUlyR0J2MUF3aUgyenRxVklXNDZwUFlRNU0reFJGWVA1alczMWt1bWtzWTZzK3g1RXpXaDNUZVR3aTM4WTdnNnVndUVTVGcvYzFvallzRkpYQllzVkFHcmJVMGlJTlFnVmFmWENTNTBiMlljR2JwZG4vdkIwQ0F3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZDOTZSQVdxRDVLN1lFSWI4em5tRWE0NDI0bXJNQjhHQTFVZEl3UVlNQmFBRkM5NlJBV3FENUs3WUVJYjh6bm1FYTQ0MjRtck1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKdjMwOUE3aGNqUUVSNGozRlhLZ25PbERjd2pKMzRqNDBBZkJMTTRpV0VwaEg0MnREb01sRWpFVGtBbWZTNEc3eDVnRVpmNjNMckF2bmF3YzQ4N2grbGNtdTg3NW43NE9HdG91V1JGMjV0NEJERVdsYVI0ZGswTEJLN1FWc2ZJU2JtQm5jODJYa0tCb2JUTVBUQmF4dU5ENEIxcHUyaXFlWEdnQlQxUHhNNDNqUTRCNDl5N2M4ZHdsWG5zaUp0SE9JbFI0dTBxaG4xVXJaOGRsdXdQMlhUcGxVRzVVVW9udTIyaFI4UFpKMnJQa0ZnWlFBa2dnc3lRY256V3RlbGp5Wk1CWU1lYkVhc0haYUUxS3gvRktleTBnWUZ3RFFSZ0hNc3lFMGlQcFJ2Zm9RUjFUOUxZakZPV1NHMG40OTN4Z0JReUxlMW1ETDNNTFNRMVRuMWpwNGs9PC9kYXRhPjwvYXJyYXk+CiAgICAKICAgIDxrZXk+RW50aXRsZW1lbnRzPC9rZXk+CiAgICA8ZGljdD4KICAgICAgICA8a2V5PmFwcGxpY2F0aW9uLWlkZW50aWZpZXI8L2tleT48c3RyaW5nPlRFQU0xMjM0NTYuY29tLmV4YW1wbGUuYXBwPC9zdHJpbmc+CiAgICAgICAgPGtleT5jb20uYXBwbGUuZGV2ZWxvcGVyLnRlYW0taWRlbnRpZmllcjwva2V5PjxzdHJpbmc+VEVBTTEyMzQ1Njwvc3RyaW5nPgogICAgICAgIDxrZXk+Z2V0LXRhc2stYWxsb3c8L2tleT48ZmFsc2UvPgogICAgICAgIDxrZXk+YXBzLWVudmlyb25tZW50PC9rZXk+PHN0cmluZz5wcm9kdWN0aW9uPC9zdHJpbmc+CiAgICAgICAgPGtleT5iZXRhLXJlcG9ydHMtYWN0aXZlPC9rZXk+PHRydWUvPgogICAgICAgIDxrZXk+Y29tLmFwcGxlLmRldmVsb3Blci5hc3NvY2lhdGVkLWRvbWFpbnM8L2tleT48YXJyYXk+PHN0cmluZz4qPC9zdHJpbmc+PC9hcnJheT4KICAgICAgICA8a2V5PmNvbS5hcHBsZS5zZWN1cml0eS5hcHBsaWNhdGlvbi1ncm91cHM8L2tleT48YXJyYXk+PHN0cmluZz5ncm91cC5jb20uZXhhbXBsZS5hcHA8L3N0cmluZz48L2FycmF5PgogICAgICAgIDxrZXk+a2V5Y2hhaW4tYWNjZXNzLWdyb3Vwczwva2V5PjxhcnJheT48c3RyaW5nPlRFQU0xMjM0NTYuKjwvc3RyaW5nPjwvYXJyYXk+CiAgICA8L2RpY3Q+CjwvZGljdD4KPC9wbGlzdD4KoIIDwTCCA70wggKloAMCAQICFBaQDBFlFqO13ZHkIQdW6UJIjTENMA0GCSqGSIb3DQEBCwUAMG0xCzAJBgNV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")! + // swiftlint:disable:next line_length + static let appextensionProfile = Data(base64Encoded: "MIIQtgYJKoZIhvcNAQcCoIIQpzCCEKMCAQExDTALBglghkgBZQMEAgEwggouBgkqhkiG9w0BBwGgggofBIIKGzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+CjwhRE9DVFlQRSBwbGlzdCBQVUJMSUMgIi0vL0FwcGxlLy9EVEQgUExJU1QgMS4wLy9FTiIgImh0dHA6Ly93d3cuYXBwbGUuY29tL0RURHMvUHJvcGVydHlMaXN0LTEuMC5kdGQiPgo8cGxpc3QgdmVyc2lvbj0iMS4wIj4KPGRpY3Q+CiAgICA8a2V5Pk5hbWU8L2tleT48c3RyaW5nPnh0b29sIGFwcGV4dGVuc2lvbjwvc3RyaW5nPgogICAgPGtleT5VVUlEPC9rZXk+PHN0cmluZz4wMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMTMxODM8L3N0cmluZz4KICAgIDxrZXk+VGVhbUlkZW50aWZpZXI8L2tleT48YXJyYXk+PHN0cmluZz5URUFNMTIzNDU2PC9zdHJpbmc+PC9hcnJheT4KICAgIDxrZXk+UGxhdGZvcm08L2tleT48YXJyYXk+PHN0cmluZz5pT1M8L3N0cmluZz48L2FycmF5PgogICAgPGtleT5DcmVhdGlvbkRhdGU8L2tleT48ZGF0ZT4yMDI0LTAxLTAxVDAwOjAwOjAwWjwvZGF0ZT4KICAgIDxrZXk+RXhwaXJhdGlvbkRhdGU8L2tleT48ZGF0ZT4yMTI2LTAxLTAxVDAwOjAwOjAwWjwvZGF0ZT4KICAgIDxrZXk+RGV2ZWxvcGVyQ2VydGlmaWNhdGVzPC9rZXk+PGFycmF5PjxkYXRhPk1JSUR2VENDQXFXZ0F3SUJBZ0lVRnBBTUVXVVdvN1hka2VRaEIxYnBRa2lOTVEwd0RRWUpLb1pJaHZjTkFRRUxCUUF3YlRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBb01DbGgwYjI5c0lGUmxjM1F4RXpBUkJnTlZCQXNNQ2xSRlFVMHhNak0wTlRZeE5EQXlCZ05WQkFNTUswRndjR3hsSUVScGMzUnlhV0oxZEdsdmJqb2dXSFJ2YjJ3Z1ZHVnpkQ0FvVkVWQlRURXlNelExTmlrd0lCY05Nall4TURFd01qSTBNakE0V2hnUE1qRXlOakE1TVRZeU1qUXlNRGhhTUcweEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBZZEc5dmJDQlVaWE4wTVJNd0VRWURWUVFMREFwVVJVRk5NVEl6TkRVMk1UUXdNZ1lEVlFRRERDdEJjSEJzWlNCRWFYTjBjbWxpZFhScGIyNDZJRmgwYjI5c0lGUmxjM1FnS0ZSRlFVMHhNak0wTlRZcE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFhYbUEwb0JIK2RaNXk3dHhhTXhRTEp1dVdoNFRvZ3hXa29TdHJHMVVJZXRJbkZIVHBIbDZPSXJ0SVVlaFluU2xBYVRyaDJHM2dQczlqcjNmQk04UjR2ZW1NTWw1RW5OVmI2MkpJb09GZXdPQnpFN1hKR0w2ZG1vTmNYcDhZRG5JYzJBZDc4d2tScnlhejJ5d0hMVHdQUUtDNjlvWHhsVExKUkV6SGZKSWN5cTFyZXFwQ3FERExQNkZJVkswbUwzY292b3JtSGd4YXNPa0lTTUJVQzFVZHlwbEczTXVkYWJWbzBScVgvSXJwSkpHaTJ2Q0ZhQ0RQRlpTQzZibTRINmpFTEU4RW85VjRraUwvQnUrMlBKRmpvV01HUGx1clc2Qmh0N1c2ZDF4eVJrSy8rQXNxaHhXZkpoM0JNTkJIc0FlZDA5VzJwWkYya0ptQm04bHJrWXRRSURBUUFCbzFNd1VUQWRCZ05WSFE0RUZnUVUyTmc0MTRvSmFndjd2SWdHTmNTd2hQY2lldk13SHdZRFZSMGpCQmd3Rm9BVTJOZzQxNG9KYWd2N3ZJZ0dOY1N3aFBjaWV2TXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFjUkYxYVVER28wQi82M3JyM3MyTVNkVzk0dDFKU29HQVAwbDB6TCtKM2didUF2L2dlMHJXS01IY2JGdGwvRlJZUTFhZmJncU4xNjg2cG11WXZUMkhtZG12VTdSY3Q1NkM2ZWhiSUVhWStZS2NqM203OTFaNzhMWTB3YllHcDFwUWMzZWFlVzBFeitEK1NPNHBQeG9UWTBJRFJiNVlrQU1FUXczWDhsUmQ0czR6RWtwWEZERkpZZzMvVWE0bjRrclkrYWdqTUxReVlpRUJPNTQvOUtWdGthZTVqOGErd3B1b1dPa1pjTlYwOExKbXNma2dGMjg2K2gyWmh0b1BpUHNaRXA5Tk53aG4xc3ZCUzdBQjY2TkFzRWZXZXFySGpObmxKcDdyUXdvWlI5cHp3NHpiSjRTTmhjdllKSkpzZmtoSGdEd1BtSXF3SFlHc2dwSjhySzlrU0E9PTwvZGF0YT48L2FycmF5PgogICAgCiAgICA8a2V5PkVudGl0bGVtZW50czwva2V5PgogICAgPGRpY3Q+CiAgICAgICAgPGtleT5hcHBsaWNhdGlvbi1pZGVudGlmaWVyPC9rZXk+PHN0cmluZz5URUFNMTIzNDU2LmNvbS5leGFtcGxlLmFwcC53aWRnZXQ8L3N0cmluZz4KICAgICAgICA8a2V5PmNvbS5hcHBsZS5kZXZlbG9wZXIudGVhbS1pZGVudGlmaWVyPC9rZXk+PHN0cmluZz5URUFNMTIzNDU2PC9zdHJpbmc+CiAgICAgICAgPGtleT5nZXQtdGFzay1hbGxvdzwva2V5PjxmYWxzZS8+CiAgICAgICAgPGtleT5hcHMtZW52aXJvbm1lbnQ8L2tleT48c3RyaW5nPnByb2R1Y3Rpb248L3N0cmluZz4KICAgICAgICA8a2V5PmJldGEtcmVwb3J0cy1hY3RpdmU8L2tleT48dHJ1ZS8+CiAgICAgICAgPGtleT5jb20uYXBwbGUuZGV2ZWxvcGVyLmFzc29jaWF0ZWQtZG9tYWluczwva2V5PjxhcnJheT48c3RyaW5nPio8L3N0cmluZz48L2FycmF5PgogICAgICAgIDxrZXk+Y29tLmFwcGxlLnNlY3VyaXR5LmFwcGxpY2F0aW9uLWdyb3Vwczwva2V5PjxhcnJheT48c3RyaW5nPmdyb3VwLmNvbS5leGFtcGxlLmFwcDwvc3RyaW5nPjwvYXJyYXk+CiAgICAgICAgPGtleT5rZXljaGFpbi1hY2Nlc3MtZ3JvdXBzPC9rZXk+PGFycmF5PjxzdHJpbmc+VEVBTTEyMzQ1Ni4qPC9zdHJpbmc+PC9hcnJheT4KICAgIDwvZGljdD4KPC9kaWN0Pgo8L3BsaXN0PgqgggPBMIIDvTCCAqWgAwIBAgIUFpAMEWUWo7XdkeQhB1bpQkiNMQ0wDQYJKoZIhvcN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")! +} diff --git a/scripts/generate-signing-fixtures.sh b/scripts/generate-signing-fixtures.sh new file mode 100755 index 00000000..a37d09b7 --- /dev/null +++ b/scripts/generate-signing-fixtures.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# +# Regenerates Tests/XToolTests/SigningFixtures.swift. +# +# Everything here is throwaway: self-signed certificates with freshly generated keys and +# hand-written provisioning profiles. Nothing is (or could be) valid for Apple's services. +# +# Usage: scripts/generate-signing-fixtures.sh (needs openssl >= 1.1.1) + +set -euo pipefail + +cd "$(dirname "$0")/.." +OUT="Tests/XToolTests/SigningFixtures.swift" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +PASSWORD="xtool-test-password" +TEAM="TEAM123456" +BUNDLE_ID="com.example.app" + +# make_identity +make_identity() { + openssl req -x509 -newkey rsa:2048 -nodes -days 36500 \ + -keyout "$WORK/$1.key" -out "$WORK/$1.pem" \ + -subj "/C=US/O=Xtool Test/OU=$3/CN=$2" 2>/dev/null + openssl x509 -in "$WORK/$1.pem" -outform DER -out "$WORK/$1.der" + openssl pkcs12 -export -inkey "$WORK/$1.key" -in "$WORK/$1.pem" \ + -out "$WORK/$1.p12" -passout "pass:$PASSWORD" -name "$2" +} + +# make_profile +# +make_profile() { + local name="$1" signer="$2" embedded="$3" team="$4" appid="$5" extra="$6" ents="$7" expires="$8" + cat > "$WORK/$name.plist" < + + + + Namextool $name + UUID00000000-0000-0000-0000-$(printf '%012d' "$RANDOM") + TeamIdentifier$team + PlatformiOS + CreationDate2024-01-01T00:00:00Z + ExpirationDate$expires + DeveloperCertificates$(base64 -w0 < "$WORK/$embedded.der") + $extra + Entitlements + + application-identifier$team.$appid + com.apple.developer.team-identifier$team + $ents + + + +PLIST + openssl cms -sign -nodetach -binary -outform DER \ + -in "$WORK/$name.plist" -signer "$WORK/$signer.pem" -inkey "$WORK/$signer.key" \ + -out "$WORK/$name.mobileprovision" +} + +make_identity distribution "Apple Distribution: Xtool Test ($TEAM)" "$TEAM" +make_identity development "Apple Development: Xtool Test (ABCDE12345)" "$TEAM" + +FAR="2126-01-01T00:00:00Z" +DEVICES='ProvisionedDevices00008030-001A2B3C4D5E6F78' + +STORE_ENTS='get-task-allow + aps-environmentproduction + beta-reports-active + com.apple.developer.associated-domains* + com.apple.security.application-groupsgroup.com.example.app + keychain-access-groups'"$TEAM"'.*' +ADHOC_ENTS='get-task-allow + aps-environmentproduction + keychain-access-groups'"$TEAM"'.*' +DEV_ENTS='get-task-allow + aps-environmentdevelopment + keychain-access-groups'"$TEAM"'.*' + +make_profile appstore distribution distribution "$TEAM" "$BUNDLE_ID" "" "$STORE_ENTS" "$FAR" +make_profile wildcard distribution distribution "$TEAM" "*" "" "$STORE_ENTS" "$FAR" +make_profile adhoc distribution distribution "$TEAM" "$BUNDLE_ID" "$DEVICES" "$ADHOC_ENTS" "$FAR" +make_profile development development development "$TEAM" "$BUNDLE_ID" "$DEVICES" "$DEV_ENTS" "$FAR" +make_profile expired distribution distribution "$TEAM" "$BUNDLE_ID" "" "$STORE_ENTS" "2020-01-01T00:00:00Z" +make_profile otherteam distribution distribution "OTHERTEAM1" "$BUNDLE_ID" "" "$STORE_ENTS" "$FAR" +make_profile othercert distribution development "$TEAM" "$BUNDLE_ID" "" "$STORE_ENTS" "$FAR" +make_profile appextension distribution distribution "$TEAM" "$BUNDLE_ID.widget" "" "$STORE_ENTS" "$FAR" + +b64() { base64 -w0 < "$WORK/$1"; } + +{ + echo "// Generated by scripts/generate-signing-fixtures.sh. Do not edit." + echo "// Throwaway self-signed identities and hand-written profiles: no real secret is involved." + echo + echo "import Foundation" + echo + echo "enum SigningFixtures {" + echo " static let teamID = \"$TEAM\"" + echo " static let bundleID = \"$BUNDLE_ID\"" + echo " static let p12Password = \"$PASSWORD\"" + echo + for f in distribution.p12 development.p12 distribution.der \ + appstore.mobileprovision wildcard.mobileprovision adhoc.mobileprovision development.mobileprovision \ + expired.mobileprovision otherteam.mobileprovision othercert.mobileprovision appextension.mobileprovision + do + name="$(echo "${f%%.*}" | sed -E 's/^(.)/\L\1/')" + case "$f" in + *.p12) name="${name}P12" ;; + *.der) name="${name}Certificate" ;; + *.mobileprovision) name="${name}Profile" ;; + esac + echo " // swiftlint:disable:next line_length" + echo " static let $name = Data(base64Encoded: \"$(b64 "$f")\")!" + done + echo "}" +} > "$OUT" + +echo "Wrote $OUT"