Allow use of a single cert for multiple provisioning profiles (#2193)

* Use correct security call

Was using wrong syntax and therefore failed

* Do not check for duplicate import of keys as KeyChain takes care of that itself

* Adjust tests

* Adjust test

* Formatting

* Parse certificate fingerprint from certs and provisioning profiles

* Address issues from PR review

* Reformat

* Adapt fixture to new requirments

* Remove targetName and configurationName from cert and match using fingerprint

* Update changelog

* Update documentation.

* Improve certificate lookup (faster and deterministic)

* Revert Gemfile

* Remove duplicate declaration

* Catch openssl parsing errors

* Remove unused error

* Apply suggestions from code review

Co-authored-by: Marek Fořt <marek.fort@ackee.cz>

* Update documentation

* Apply PR review comments

* Log detailed openssl error when parsing fails

* Update changelog

* Remove duplicate switch case

Co-authored-by: Marek Fořt <marek.fort@ackee.cz>
This commit is contained in:
Stefan Rinner 2021-01-18 09:41:29 +01:00 committed by GitHub
parent 61a3bc54b0
commit 4d7b9f2c17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 195 additions and 97 deletions

View File

@ -9,6 +9,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
### Added
- Add linting for paths of local packages and for URL validity of remote packages [#2255](https://github.com/tuist/tuist/pull/2255) by [@adellibovi](https://github.com/adellibovi).
- Allow use of a single cert for multiple provisioning profiles [#2193](https://github.com/tuist/tuist/pull/2193) by [@rist](https://github.com/rist).
### Fixed

View File

@ -4,9 +4,9 @@ import TSCBasic
struct Certificate: Equatable {
let publicKey: AbsolutePath
let privateKey: AbsolutePath
/// Content of the fingerprint property of the public key
let fingerprint: String
let developmentTeam: String
let name: String
let targetName: String
let configurationName: String
let isRevoked: Bool
}

View File

@ -5,23 +5,23 @@ import TuistSupport
enum CertificateParserError: FatalError, Equatable {
case nameParsingFailed(AbsolutePath, String)
case developmentTeamParsingFailed(AbsolutePath, String)
case invalidFormat(String)
case fileParsingFailed(AbsolutePath)
var type: ErrorType {
switch self {
case .nameParsingFailed, .developmentTeamParsingFailed, .invalidFormat:
case .nameParsingFailed, .developmentTeamParsingFailed, .fileParsingFailed:
return .abort
}
}
var description: String {
switch self {
case let .invalidFormat(certificate):
return "Certificate \(certificate) is in invalid format. Please name your certificates in the following way: Target.Configuration.p12"
case let .nameParsingFailed(path, input):
return "We couldn't parse the name while parsing the following output from the file \(path.pathString): \(input)"
case let .developmentTeamParsingFailed(path, input):
return "We couldn't parse the development team while parsing the following output from the file \(path.pathString): \(input)"
case let .fileParsingFailed(path):
return "We couldn't parse the file \(path.pathString)"
}
}
}
@ -31,6 +31,9 @@ protocol CertificateParsing {
/// Parse public-private key pair
/// - Returns: Parse `Certificate`
func parse(publicKey: AbsolutePath, privateKey: AbsolutePath) throws -> Certificate
/// Retrieve fingerprint of a public key
func parseFingerPrint(developerCertificate: Data) throws -> String
}
/// Subject attributes that are returnen with `openssl x509 -subject`
@ -48,12 +51,8 @@ private enum SubjectAttribute: String {
final class CertificateParser: CertificateParsing {
func parse(publicKey: AbsolutePath, privateKey: AbsolutePath) throws -> Certificate {
let publicKeyComponents = publicKey.basenameWithoutExt.components(separatedBy: ".")
guard publicKeyComponents.count == 2 else { throw CertificateParserError.invalidFormat(publicKey.pathString) }
let targetName = publicKeyComponents[0]
let configurationName = publicKeyComponents[1]
let subject = try self.subject(at: publicKey)
let fingerprint = try self.fingerprint(at: publicKey)
let isRevoked = subject.contains("REVOKED")
let nameRegex = try NSRegularExpression(
@ -61,9 +60,9 @@ final class CertificateParser: CertificateParsing {
options: []
)
guard
let result = nameRegex.firstMatch(in: subject, options: [], range: NSRange(location: 0, length: subject.count))
let nameResult = nameRegex.firstMatch(in: subject, options: [], range: NSRange(location: 0, length: subject.count))
else { throw CertificateParserError.nameParsingFailed(publicKey, subject) }
let name = NSString(string: subject).substring(with: result.range(at: 1)).spm_chomp()
let name = NSString(string: subject).substring(with: nameResult.range(at: 1)).spm_chomp()
let developmentTeamRegex = try NSRegularExpression(
pattern: SubjectAttribute.organizationalUnit.rawValue + " *= *([^/,]+)",
@ -77,18 +76,46 @@ final class CertificateParser: CertificateParsing {
return Certificate(
publicKey: publicKey,
privateKey: privateKey,
fingerprint: fingerprint,
developmentTeam: developmentTeam,
name: name.sanitizeEncoding(),
targetName: targetName,
configurationName: configurationName,
isRevoked: isRevoked
)
}
func parseFingerPrint(developerCertificate: Data) throws -> String {
let temporaryFile = try FileHandler.shared.temporaryDirectory().appending(component: "developerCertificate.cer")
try developerCertificate.write(to: temporaryFile.asURL)
return try fingerprint(at: temporaryFile)
}
// MARK: - Helpers
private func subject(at path: AbsolutePath) throws -> String {
try System.shared.capture("openssl", "x509", "-inform", "der", "-in", path.pathString, "-noout", "-subject")
do {
return try System.shared.capture("openssl", "x509", "-inform", "der", "-in", path.pathString, "-noout", "-subject")
} catch let TuistSupport.SystemError.terminated(_, _, standardError) {
if let string = String(data: standardError, encoding: .utf8) {
logger.warning("Parsing subject of \(path) failed with: \(string)")
}
throw CertificateParserError.fileParsingFailed(path)
} catch {
throw CertificateParserError.fileParsingFailed(path)
}
}
private func fingerprint(at path: AbsolutePath) throws -> String {
do {
return try System.shared.capture("openssl", "x509", "-inform", "der", "-in", path.pathString, "-noout", "-fingerprint").spm_chomp()
} catch let TuistSupport.SystemError.terminated(_, _, standardError) {
if let string = String(data: standardError, encoding: .utf8) {
logger.warning("Parsing fingerprint of \(path) failed with: \(string)")
}
throw CertificateParserError.fileParsingFailed(path)
} catch {
throw CertificateParserError.fileParsingFailed(path)
}
}
}

View File

@ -0,0 +1,7 @@
import Foundation
extension Dictionary where Key == Fingerprint, Value == Certificate {
func first(for provisioningProfile: ProvisioningProfile) -> Certificate? {
provisioningProfile.developerCertificateFingerprints.compactMap { self[$0] }.first
}
}

View File

@ -59,7 +59,7 @@ public class SigningMapper: ProjectMapping {
private func map(target: Target,
project: Project,
keychainPath: AbsolutePath,
certificates: [TargetName: [ConfigurationName: Certificate]],
certificates: [Fingerprint: Certificate],
provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]]) throws -> Target
{
var target = target
@ -70,7 +70,7 @@ public class SigningMapper: ProjectMapping {
.reduce(into: [:]) { dict, configurationPair in
guard
let provisioningProfile = provisioningProfiles[target.name]?[configurationPair.key.name],
let certificate = certificates[target.name]?[configurationPair.key.name]
let certificate = certificates.first(for: provisioningProfile)
else {
dict[configurationPair.key] = configurationPair.value
return

View File

@ -16,6 +16,7 @@ struct ProvisioningProfile: Equatable {
let applicationIdPrefix: [String]
let platforms: [String]
let expirationDate: Date
let developerCertificateFingerprints: [String]
struct Content {
let name: String
@ -26,6 +27,7 @@ struct ProvisioningProfile: Equatable {
let applicationIdPrefix: [String]
let platforms: [String]
let expirationDate: Date
let developerCertificates: [Data]
}
}
@ -52,6 +54,7 @@ extension ProvisioningProfile.Content: Decodable {
case platforms = "Platform"
case entitlements = "Entitlements"
case expirationDate = "ExpirationDate"
case developerCertificates = "DeveloperCertificates"
}
init(from decoder: Decoder) throws {
@ -65,5 +68,6 @@ extension ProvisioningProfile.Content: Decodable {
let entitlements = try container.decode(Entitlements.self, forKey: .entitlements)
appId = entitlements.appId
expirationDate = try container.decode(Date.self, forKey: .expirationDate)
developerCertificates = try container.decode([Data].self, forKey: .developerCertificates)
}
}

View File

@ -29,9 +29,11 @@ protocol ProvisioningProfileParsing {
final class ProvisioningProfileParser: ProvisioningProfileParsing {
private let securityController: SecurityControlling
private let certificateParser: CertificateParsing
init(securityController: SecurityControlling = SecurityController()) {
init(securityController: SecurityControlling = SecurityController(), certificateParser: CertificateParsing = CertificateParser()) {
self.securityController = securityController
self.certificateParser = certificateParser
}
func parse(at path: AbsolutePath) throws -> ProvisioningProfile {
@ -44,6 +46,10 @@ final class ProvisioningProfileParser: ProvisioningProfileParsing {
let plistData = Data(unencryptedProvisioningProfile.utf8)
let provisioningProfileContent = try PropertyListDecoder().decode(ProvisioningProfile.Content.self, from: plistData)
let developerCertificateFingerprints = try provisioningProfileContent.developerCertificates.map {
try certificateParser.parseFingerPrint(developerCertificate: $0)
}
return ProvisioningProfile(path: path,
name: provisioningProfileContent.name,
targetName: targetName,
@ -54,6 +60,7 @@ final class ProvisioningProfileParser: ProvisioningProfileParsing {
appIdName: provisioningProfileContent.appIdName,
applicationIdPrefix: provisioningProfileContent.applicationIdPrefix,
platforms: provisioningProfileContent.platforms,
expirationDate: provisioningProfileContent.expirationDate)
expirationDate: provisioningProfileContent.expirationDate,
developerCertificateFingerprints: developerCertificateFingerprints)
}
}

View File

@ -83,7 +83,7 @@ public final class SigningInteractor: SigningInteracting {
private func install(target: Target,
project: Project,
keychainPath: AbsolutePath,
certificates: [TargetName: [ConfigurationName: Certificate]],
certificates: [Fingerprint: Certificate],
provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]]) throws
{
let targetConfigurations = target.settings?.configurations ?? [:]
@ -97,7 +97,7 @@ public final class SigningInteractor: SigningInteracting {
.compactMap { configuration -> (certificate: Certificate, provisioningProfile: ProvisioningProfile)? in
guard
let provisioningProfile = provisioningProfiles[target.name]?[configuration.name],
let certificate = certificates[target.name]?[configuration.name]
let certificate = certificates.first(for: provisioningProfile)
else {
return nil
}

View File

@ -2,6 +2,7 @@ import Foundation
import TSCBasic
import TuistCore
typealias Fingerprint = String
typealias TargetName = String
typealias ConfigurationName = String
@ -10,7 +11,7 @@ protocol SigningMatching {
/// - Returns: Certificates and provisioning profiles matched with their configuration and target
/// - Warning: Expects certificates and provisioning profiles already decrypted
func match(from path: AbsolutePath) throws -> (
certificates: [TargetName: [ConfigurationName: Certificate]],
certificates: [Fingerprint: Certificate],
provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]]
)
}
@ -30,19 +31,17 @@ final class SigningMatcher: SigningMatching {
}
func match(from path: AbsolutePath) throws -> (
certificates: [TargetName: [ConfigurationName: Certificate]],
certificates: [Fingerprint: Certificate],
provisioningProfiles: [TargetName: [ConfigurationName: ProvisioningProfile]]
) {
let certificateFiles = try signingFilesLocator.locateUnencryptedCertificates(from: path)
.sorted()
let privateKeyFiles = try signingFilesLocator.locateUnencryptedPrivateKeys(from: path)
.sorted()
let certificates: [TargetName: [ConfigurationName: Certificate]] = try zip(certificateFiles, privateKeyFiles)
let certificates: [Fingerprint: Certificate] = try zip(certificateFiles, privateKeyFiles)
.map(certificateParser.parse)
.reduce(into: [:]) { dict, certificate in
var currentTargetDict = dict[certificate.targetName] ?? [:]
currentTargetDict[certificate.configurationName] = certificate
dict[certificate.targetName] = currentTargetDict
dict[certificate.fingerprint] = certificate
}
// swiftlint:disable:next line_length

View File

@ -5,18 +5,16 @@ import TSCBasic
extension Certificate {
static func test(publicKey: AbsolutePath = AbsolutePath("/"),
privateKey: AbsolutePath = AbsolutePath("/"),
fingerprint: String = "",
developmentTeam: String = "",
name: String = "",
targetName: String = "",
configurationName: String = "",
isRevoked: Bool = false) -> Certificate
{
Certificate(publicKey: publicKey,
privateKey: privateKey,
fingerprint: fingerprint,
developmentTeam: developmentTeam,
name: name,
targetName: targetName,
configurationName: configurationName,
isRevoked: isRevoked)
}
}

View File

@ -14,7 +14,8 @@ extension ProvisioningProfile {
appIdName: String = "appIdName",
applicationIdPrefix: [String] = [],
platforms: [String] = ["iOS"],
expirationDate: Date = Date().addingTimeInterval(100)
expirationDate: Date = Date().addingTimeInterval(100),
developerCertificateFingerprints: [String] = ["developerCertificateFingerprint"]
) -> ProvisioningProfile {
ProvisioningProfile(
path: path,
@ -27,7 +28,8 @@ extension ProvisioningProfile {
appIdName: appIdName,
applicationIdPrefix: applicationIdPrefix,
platforms: platforms,
expirationDate: expirationDate
expirationDate: expirationDate,
developerCertificateFingerprints: developerCertificateFingerprints
)
}
}

View File

@ -5,18 +5,18 @@ import XCTest
@testable import TuistSupportTesting
final class CertificateParserIntegrationTests: TuistTestCase {
var subject: CertificateParser!
var certificateParser: CertificateParser!
override func setUp() {
super.setUp()
subject = CertificateParser()
certificateParser = CertificateParser()
}
override func tearDown() {
super.tearDown()
subject = nil
certificateParser = nil
}
func test_parse_certificate() throws {
@ -27,15 +27,14 @@ final class CertificateParserIntegrationTests: TuistTestCase {
let expectedCertificate = Certificate(
publicKey: publicKey,
privateKey: privateKey,
fingerprint: "SHA1 Fingerprint=80:B8:6E:55:1D:8E:F2:38:CD:95:3C:7E:72:3B:DB:B1:A5:B0:5D:60",
developmentTeam: "QH95ER52SG",
name: "Apple Development: Marek Fort (54GSF6G47V)",
targetName: "Target",
configurationName: "Debug",
isRevoked: false
)
// When
let certificate = try subject.parse(publicKey: publicKey, privateKey: privateKey)
let certificate = try certificateParser.parse(publicKey: publicKey, privateKey: privateKey)
// Then
XCTAssertEqual(certificate, expectedCertificate)

View File

@ -0,0 +1,47 @@
import TSCBasic
import XCTest
@testable import TuistSigning
@testable import TuistSigningTesting
@testable import TuistSupportTesting
final class ProvisioningProfileParserTests: TuistTestCase {
var provisioningProfileParser: ProvisioningProfileParser!
override func setUp() {
super.setUp()
provisioningProfileParser = ProvisioningProfileParser()
}
override func tearDown() {
super.tearDown()
provisioningProfileParser = nil
}
func test_parse_provisioningProfile() throws {
// Given
let currentDirectory = AbsolutePath(#file.replacingOccurrences(of: "file://", with: "")).removingLastComponent()
let provisioningProfileFile = currentDirectory.appending(component: "SignApp.debug.mobileprovision")
let expectedProvisioningProfile = ProvisioningProfile(
path: provisioningProfileFile,
name: "SignApp.Debug",
targetName: "SignApp",
configurationName: "debug",
uuid: "d34fb066-f494-4d85-a556-d469c2196f46",
teamId: "QH95ER52SG",
appId: "QH95ER52SG.io.tuist.SignApp",
appIdName: "SignApp",
applicationIdPrefix: ["QH95ER52SG"],
platforms: ["iOS"],
expirationDate: Date(timeIntervalSince1970: 1_619_208_757.0),
developerCertificateFingerprints: ["SHA1 Fingerprint=80:B8:6E:55:1D:8E:F2:38:CD:95:3C:7E:72:3B:DB:B1:A5:B0:5D:60"]
)
// When
let provisioningProfile = try provisioningProfileParser.parse(at: provisioningProfileFile)
// Then
XCTAssertEqual(provisioningProfile, expectedProvisioningProfile)
}
}

View File

@ -27,6 +27,11 @@ final class CertificateParserTests: TuistUnitTestCase {
"openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-subject",
output: subjectOutput
)
let fingerprintOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n"
system.succeedCommand(
"openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-fingerprint",
output: fingerprintOutput
)
// When
XCTAssertThrowsSpecific(
@ -44,6 +49,11 @@ final class CertificateParserTests: TuistUnitTestCase {
"openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-subject",
output: subjectOutput
)
let fingerprintOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n"
system.succeedCommand(
"openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-fingerprint",
output: fingerprintOutput
)
// When
XCTAssertThrowsSpecific(
@ -52,18 +62,6 @@ final class CertificateParserTests: TuistUnitTestCase {
)
}
func test_throws_invalid_name_when_wrong_format() throws {
// Given
let publicKey = try temporaryPath()
let privateKey = try temporaryPath()
// When
XCTAssertThrowsSpecific(
try subject.parse(publicKey: publicKey, privateKey: privateKey),
CertificateParserError.invalidFormat(publicKey.pathString)
)
}
func test_parsing_succeeds() throws {
// Given
let publicKey = try temporaryPath().appending(component: "Target.Debug.p12")
@ -73,13 +71,17 @@ final class CertificateParserTests: TuistUnitTestCase {
"openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-subject",
output: subjectOutput
)
let fingerprintOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n"
system.succeedCommand(
"openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-fingerprint",
output: fingerprintOutput
)
let expectedCertificate = Certificate(
publicKey: publicKey,
privateKey: privateKey,
fingerprint: "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US",
developmentTeam: "QH95ER52SG",
name: "Apple Development: Name (54GSF6G47V)",
targetName: "Target",
configurationName: "Debug",
isRevoked: false
)
@ -99,13 +101,17 @@ final class CertificateParserTests: TuistUnitTestCase {
"openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-subject",
output: subjectOutput
)
let fingerprintOutput = "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US\n"
system.succeedCommand(
"openssl", "x509", "-inform", "der", "-in", publicKey.pathString, "-noout", "-fingerprint",
output: fingerprintOutput
)
let expectedCertificate = Certificate(
publicKey: publicKey,
privateKey: privateKey,
fingerprint: "subject= /UID=VD55TKL3V6/CN=Apple Development: Name (54GSF6G47V)/OU=QH95ER52SG/O=Name/C=US",
developmentTeam: "QH95ER52SG",
name: "Apple Development: Name (54GSF6G47V)",
targetName: "Target",
configurationName: "Debug",
isRevoked: false
)

View File

@ -1,3 +1,4 @@
import Foundation
import TSCBasic
@testable import TuistSigning
@testable import TuistSigningTesting
@ -7,4 +8,9 @@ final class MockCertificateParser: CertificateParsing {
func parse(publicKey: AbsolutePath, privateKey: AbsolutePath) throws -> Certificate {
try parseStub?(publicKey, privateKey) ?? Certificate.test()
}
var parseFingerPrintStub: ((Data) throws -> String)?
func parseFingerPrint(developerCertificate: Data) throws -> String {
try parseFingerPrintStub?(developerCertificate) ?? ""
}
}

View File

@ -3,8 +3,8 @@ import TuistCore
@testable import TuistSigning
final class MockSigningMatcher: SigningMatching {
var matchStub: ((AbsolutePath) throws -> (certificates: [String: [String: Certificate]], provisioningProfiles: [String: [String: ProvisioningProfile]]))?
func match(from path: AbsolutePath) throws -> (certificates: [String: [String: Certificate]], provisioningProfiles: [String: [String: ProvisioningProfile]]) {
var matchStub: ((AbsolutePath) throws -> (certificates: [String: Certificate], provisioningProfiles: [String: [String: ProvisioningProfile]]))?
func match(from path: AbsolutePath) throws -> (certificates: [String: Certificate], provisioningProfiles: [String: [String: ProvisioningProfile]]) {
try matchStub?(path) ?? (certificates: [:], provisioningProfiles: [:])
}
}

View File

@ -37,7 +37,8 @@ final class ProvisioningProfileParserTests: TuistUnitTestCase {
appIdName: "AppIDName",
applicationIdPrefix: ["Prefix"],
platforms: ["iOS"],
expirationDate: Date(timeIntervalSinceReferenceDate: 640_729_461)
expirationDate: Date(timeIntervalSinceReferenceDate: 640_729_461),
developerCertificateFingerprints: []
)
securityController.decodeFileStub = { _ in
.testProvisioningProfile(
@ -110,6 +111,9 @@ private extension String {
<date>\(expirationDate)</date>
<key>Name</key>
<string>\(name)</string>
<key>DeveloperCertificates</key>
<array>
</array>
<key>ProvisionedDevices</key>
<array>
<string>2b41533fd2df499800f493b261d060fe6d60838b</string>

View File

@ -207,14 +207,11 @@ final class SigningInteractorTests: TuistUnitTestCase {
let targetName = "target"
let configuration = "configuration"
let expectedCertificate = Certificate.test(name: "certA")
let expectedProvisioningProfile = ProvisioningProfile.test(name: "profileA")
let expectedProvisioningProfile = ProvisioningProfile.test(name: "profileA", developerCertificateFingerprints: ["fingerprint"])
signingMatcher.matchStub = { _ in
(certificates: [
targetName: [
configuration: expectedCertificate,
// Used to ensure only certificates that have configuration are installed
"other-config": Certificate.test(name: "certB"),
],
"fingerprint": expectedCertificate,
"otherFingerprint": Certificate.test(name: "certB"),
],
provisioningProfiles: [
targetName: [

View File

@ -44,16 +44,16 @@ final class SigningMapperTests: TuistUnitTestCase {
let targetName = "target"
let configuration = "configuration"
let certificate = Certificate.test(name: "certA")
let fingerprint = "fingerprint"
let provisioningProfile = ProvisioningProfile.test(
name: "profileA",
teamId: "TeamID",
appId: "TeamID.BundleID"
appId: "TeamID.BundleID",
developerCertificateFingerprints: ["otherFingerPrint", fingerprint]
)
signingMatcher.matchStub = { _ in
(certificates: [
targetName: [
configuration: certificate,
],
fingerprint: certificate,
],
provisioningProfiles: [
targetName: [

View File

@ -73,35 +73,30 @@ final class SigningMatcherTests: TuistUnitTestCase {
]
}
certificateParser.parseStub = { publicKey, privateKey in
let configurationName: String
let fingerprint: String
if publicKey == publicKeyPath {
configurationName = debugConfiguration
fingerprint = "fingerprint"
} else {
configurationName = releaseConfiguration
fingerprint = "otherFingerprint"
}
return Certificate.test(
publicKey: publicKey,
privateKey: privateKey,
targetName: targetName,
configurationName: configurationName
fingerprint: fingerprint
)
}
let expectedCertificates: [String: [String: Certificate]] = [
targetName: [
debugConfiguration: Certificate.test(
publicKey: publicKeyPath,
privateKey: privateKeyPath,
targetName: targetName,
configurationName: debugConfiguration
),
releaseConfiguration: Certificate.test(
publicKey: releasePublicKeyPath,
privateKey: releasePrivateKeyPath,
targetName: targetName,
configurationName: releaseConfiguration
),
],
let expectedCertificates: [String: Certificate] = [
"fingerprint": Certificate.test(
publicKey: publicKeyPath,
privateKey: privateKeyPath,
fingerprint: "fingerprint"
),
"otherFingerprint": Certificate.test(
publicKey: releasePublicKeyPath,
privateKey: releasePrivateKeyPath,
fingerprint: "otherFingerprint"
),
]
let debugProvisioningProfilePath = AbsolutePath("/\(targetName).\(debugConfiguration).mobileprovision")

View File

@ -1 +0,0 @@
5AHwOTcfzNr1TYeAnL/pAg==-we3696+agUK2mv4hFbJqvLgBADWtUknOKJLZ+UhMFaNfOx/hBSbLtTwE56+3K1vDrY1RBiSg8zkWjm/aXpLtgCwGekvNWwLGBwOvPL5r+fxp96QvYUbvWMTZBvA1yVGFwptgGSYZ4xCqqOZdLNzB8UtuB6c+WlSKSlUdcx6wbjNoEMPVYxP7APkJnlZTZqqgXn27IqWSrD5zCahshxep8F4xefOboHQL4aUe/KdxX0dZTbnE2OrrAFbNmOcc7kwTL0pSdEhBZDjCSYB9gK+90P+t9HZWcYzDnYQbR73wq7QEDONoF0VgwUGYtsTd3c4wPddezEo0Gw07o/CEnccho+RD9t1gwKuWAXKZxnM3jI46wGVJ4v4qHZuZy+t/Q/GxhsaFDhrIrPTsthw5fzZbOm20BDL2BCH4GiWSW2QCHBvctsTXQ9JKXHiQsW72UlVMKz1oj5Xh3+QGyiDZZxrfAQmN0SwI4WGsOateLEOV5lfQ57+9oS3VewC60lXL8iYSzq0/w2ecOZxCu3NLECQLN83xFJYKeloZT8g0eZXgNeLPs0eaOkRZN8NBl/ZvOBZKitk/L/G1OSFHTL4J0zv3ykDxV4kfHxMIgxE9BCYEvIOCSOrhDB5KjYQc9Phv6bL9jXjlyScPnKsl6t7+7r58R9P9fY5FNoMs1YZEmBbny1UjZ/Ny6KL2ADYTlJs0MKikJvFi3SLgzm3tT+l9G1yximpwwETMAT473oAspIKJcaNdxFtv0ctEonyDTy8svvQ+IvZjkdWMtv91Ze5Ht3YG8iR2CAOhlr1JcPz9B/+UuxO8hWMNYI19Ff4cqYy8dZSI+DJ3IgoDzEUnUUDTagUTWVG3pEJG0uoKYKedNCR8quLf+XzABSe8FP0/u1xMCKWVxgVbFdUfgEvxPQKFmkyKhF0PtFHQnQRtHo66AdfqdjPqDb/WULTQeXBnv2vIXt4DtIj7w7Z0xNuMCjxEDYREOwglMMGHqfAkbXC2XL4bVKmVRleC3LdjpISh2308YqBTJwlEInIBNAOS02+0BJdp3/vP4I83FXJWHhQdT9wrzaXsysn1jVlD+GPh3pxjUfOWj45nuPO+Mt/KhO0HM/o4/1qC75vsvsvOjnEU6EWucBUy5GCcV+TYIzKJvyJ+k81R90JXILSkZPkFpNMXjCCD22r7fbyWfZXn9hgUHjZ02DhBGpfcHszIMVP8hxv5Ptt9kFJekceMBPLiRLZh/C2skLDE7rkzYPEnVzGIZ0oUT8rYZJ7kjjc56eXvPQ7oCwMPQQeayzwJVobREQnn6ypxvaWpRscq39uWl8kC7OiS3T1mt+75e+kQ5jqY7nf8fsQlPKn8w/hUL32f80FOR11iZ3/aid1/gt5FpT8QoLpZ6R3CSM+spedi3XWUvPMdhUOeEvzg8qoBVuzq/aDTo1qIQ6hNcEmlT+TA9bK4LraTmkHguZ8IW+Vej+yFucfEh+zqqwte+FTwjCwrHlRvcx+Ks5nykVz6ubZAXZ3szat7y6ltjEMFpRvTjZ3hPcLwsBFv88h/1pnNS2oRDU/RikgQJ+b0nyRQF7m170MCzmmMGnIV+ZFvEaCfCTleay4V5IsC1uHA04UtPANsUig7Pm71o5NpwHckWykvcf6LdtCpInJmVlWDoTcFmykxWuQGogz6t3/xkVNULkIQtmQilI+usPBoNm3YM62orkks4hVFqgHqDvN5VC7Bt/35yzumIAOCedqmDRltrzbHTjt66Gfuh9FQIqFEE1muSuuCXsW9erD1Oi7VKuYfg9/i3yyCQyAT05wM5anRGjVbLlg9r4O1m30WfVB7A6/Je47gKbYzVq40PR37YyGrs/2hHLqiUv6Kan1fGS2DjwUzH8xpGWFy6jNzVzMUYH7Y0V15n+rticuki6L5AHn8OgoNEwivLmRxG7/mKrRV

View File

@ -1 +0,0 @@
dThPH8pFcEHgwpG1mqFDPg==-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

View File

@ -18,12 +18,13 @@ Tuist aims to solve all of the above pain points in a simple and _deterministic_
## How to Get Started
As the first step, download all your provisioning profiles and certficates that you'll need.
Once you download them, make sure to rename the files, so they abide by the following convention:
`Target.Configuration.extension`
Where `Target` should be a name of the target, `Configuration` a name of the configuration and leave the default extension
that you downloaded the file with (eg. `p12` for certificate, etc.).
Now you can put all those files in `Tuist/Signing` directory.
As the first step, download all your provisioning profiles that you'll need.
Once you downloaded them, make sure to rename the provisioning profiles, so they abide by the following convention:
`Target.Configuration.mobileprovision` for iOS apps or `Target.Configuration.provisionprofile` for macOS apps.
Where `Target` should be a name of the target, `Configuration` a name of the configuration.
Export the public (as .cer file) and private key (as .p12 file with an empty string as a password) from your keychain. Use the same basename for these two files: `SomeName.cer` and `SomeName.p12` - this name doesn't need to match any target or configuration.
If multiple provisioning profiles use the same certificate, it's fine to have `.p12` and `.cer` files just once in the folder - Tuist will find the matching one based on information embedded into the provisioning profile.
Now you can put all those files in `Tuist/Signing` directory within your project.
To make it all work, create a secure password by running [tuist secret](/docs/commands/secret/) and place its contents into `Tuist/master.key` that will be used
for encrypting and decrypting your files.