├── Sample
├── Sources
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── DeviceAuthorityApp.swift
│ └── ContentView.swift
├── Resources
│ ├── SwiftDeviceAuthority-Leaf.cer
│ └── SwiftDeviceAuthority.mobileconfig
├── DeviceAuthoritySample.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── DeviceAuthoritySample.xcscheme
│ └── project.pbxproj
└── README.md
├── .spi.yml
├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ ├── DeviceAuthority.xcscheme
│ ├── swift-device-authority.xcscheme
│ └── DeviceAuthority-Package.xcscheme
├── .swiftformat
├── Tests
└── DeviceAuthorityTests
│ └── DeviceAuthorityTests.swift
├── Package.resolved
├── Sources
├── CommandLine
│ ├── Command.swift
│ ├── MobileConfiguration.swift
│ ├── CreateLeafCommand.swift
│ └── CreateAuthorityCommand.swift
└── DeviceAuthority
│ └── DeviceAuthority.swift
├── LICENSE.md
├── Package.swift
└── README.md
/Sample/Sources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - platform: ios
5 | scheme: DeviceAuthority
6 | - platform: macos
7 | scheme: swift-device-authority
--------------------------------------------------------------------------------
/Sample/Resources/SwiftDeviceAuthority-Leaf.cer:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/getsidetrack/swift-device-authority/HEAD/Sample/Resources/SwiftDeviceAuthority-Leaf.cer
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | xcuserdata/
4 | DerivedData/
5 | .swiftpm/config/registries.json
6 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
--------------------------------------------------------------------------------
/Sample/DeviceAuthoritySample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sample/Sources/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sample/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sample/Sources/DeviceAuthorityApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeviceAuthorityApp.swift
3 | //
4 | // Copyright 2023 • Sidetrack Tech Limited
5 | //
6 |
7 | import SwiftUI
8 |
9 | @main
10 | struct DeviceAuthorityApp: App {
11 | var body: some Scene {
12 | WindowGroup {
13 | ContentView()
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sample/DeviceAuthoritySample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --patternlet inline
2 | --swiftversion 5.6
3 | --trimwhitespace nonblank-lines
4 | --ifdef no-indent
5 |
6 | --disable spaceInsideBrackets
7 | --disable yodaConditions
8 |
9 | --enable isEmpty
10 | --enable sortedSwitchCases
11 | --enable wrapEnumCases
12 | --enable wrapSwitchCases
13 | --enable wrapConditionalBodies
14 | --enable blankLinesBetweenImports
15 | --enable blockComments
16 |
17 | --header "\n{file}\n\nCopyright 2023 • Sidetrack Tech Limited\n"
18 |
--------------------------------------------------------------------------------
/Tests/DeviceAuthorityTests/DeviceAuthorityTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeviceAuthorityTests.swift
3 | //
4 | // Copyright 2023 • Sidetrack Tech Limited
5 | //
6 |
7 | @testable import DeviceAuthority
8 | import XCTest
9 |
10 | final class DeviceAuthorityTests: XCTestCase {
11 | func testExample() throws {
12 | // This is an example of a functional test case.
13 | // Use XCTAssert and related functions to verify your tests produce the correct
14 | // results.
15 | XCTAssertEqual(DeviceAuthority().text, "Hello, World!")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "console-kit",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/vapor/console-kit.git",
7 | "state" : {
8 | "revision" : "a7e67a1719933318b5ab7eaaed355cde020465b1",
9 | "version" : "4.5.0"
10 | }
11 | },
12 | {
13 | "identity" : "shellout",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/JohnSundell/ShellOut",
16 | "state" : {
17 | "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568",
18 | "version" : "2.3.0"
19 | }
20 | },
21 | {
22 | "identity" : "swift-log",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/apple/swift-log.git",
25 | "state" : {
26 | "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c",
27 | "version" : "1.4.4"
28 | }
29 | }
30 | ],
31 | "version" : 2
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/CommandLine/Command.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Command.swift
3 | //
4 | // Copyright 2023 • Sidetrack Tech Limited
5 | //
6 |
7 | import ConsoleKit
8 | import Foundation
9 | import ShellOut
10 |
11 | @main
12 | enum DeviceAuthorityCommandLine {
13 | static func main() async throws {
14 | let console: Console = Terminal()
15 | let input = CommandInput(arguments: CommandLine.arguments)
16 | let context = CommandContext(console: console, input: input)
17 |
18 | var commands = AsyncCommands(enableAutocomplete: false)
19 | commands.use(CreateAuthorityCommand(), as: "create-authority")
20 | commands.use(CreateLeafCommand(), as: "create-leaf")
21 |
22 | do {
23 | let group = commands.group(help: "Helps to create the necessary files to secure functionality in your iOS application.")
24 | try await console.run(group, with: context)
25 | } catch {
26 | console.error("\(error)")
27 | exit(1)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sample/DeviceAuthoritySample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "console-kit",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/vapor/console-kit.git",
7 | "state" : {
8 | "revision" : "a7e67a1719933318b5ab7eaaed355cde020465b1",
9 | "version" : "4.5.0"
10 | }
11 | },
12 | {
13 | "identity" : "shellout",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/JohnSundell/ShellOut.git",
16 | "state" : {
17 | "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568",
18 | "version" : "2.3.0"
19 | }
20 | },
21 | {
22 | "identity" : "swift-log",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/apple/swift-log.git",
25 | "state" : {
26 | "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c",
27 | "version" : "1.4.4"
28 | }
29 | }
30 | ],
31 | "version" : 2
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Sidetrack Tech Limited
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "DeviceAuthority",
7 | platforms: [
8 | .iOS(.v9),
9 | .macOS(.v12),
10 | ],
11 | products: [
12 | .library(name: "DeviceAuthority", targets: ["DeviceAuthority"]),
13 | .executable(name: "swift-device-authority", targets: ["CommandLine"]),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/vapor/console-kit.git", from: "4.5.0"),
17 | .package(url: "https://github.com/JohnSundell/ShellOut.git", from: "2.3.0"),
18 | ],
19 | targets: [
20 | .target(
21 | name: "DeviceAuthority",
22 | dependencies: []
23 | ),
24 |
25 | .testTarget(
26 | name: "DeviceAuthorityTests",
27 | dependencies: ["DeviceAuthority"]
28 | ),
29 |
30 | .executableTarget(
31 | name: "CommandLine",
32 | dependencies: [
33 | .product(name: "ConsoleKit", package: "console-kit"),
34 | .product(name: "ShellOut", package: "ShellOut"),
35 | ]
36 | ),
37 | ]
38 | )
39 |
--------------------------------------------------------------------------------
/Sample/README.md:
--------------------------------------------------------------------------------
1 | # DeviceAuthority - Sample App
2 |
3 | This is a very basic iOS application which provides barebones usage of the available APIs and acts as a simple test of functionality.
4 |
5 | For your convenience, we have provided a mobileconfig and certificate to be used in this project. Do NOT use these in your own projects as it negates all security benefits - follow instructions in the root README on how to generate your own using our command-line tool.
6 |
7 | ## Demonstration
8 |
9 | 1. Open the Xcode project, and build the project to a simulator of your choice.
10 | 2. Notice how the app is 'Locked' because your simulator is not yet trusted.
11 | 3. Drag the mobileconfig file onto your Simulator.
12 | 4. Safari will open and it will ask if you want it to allow the profile to be downloaded - allow it.
13 | 5. Open Settings on your Simulator.
14 | 6. Select General and then Device Management.
15 | 7. Select 'DeviceAuthority Sample' (the profile we just copied over)
16 | 8. Select Install, Install and Install again.
17 | 9. You can then re-open or rebuild the iOS app onto the same simulator.
18 | 10. Now notice how the app is 'Unlocked' because the device has the secure profile on it.
19 |
20 | At any time you can return to the Device Management page in Settings and remove the profile. This will put the application back to 'Locked'.
21 |
22 | It is up to you as to when and how you choose to verify the device. You may choose to do it once on app startup, or perhaps every time the settings page in your app is opened. This is personal taste and depends on your app architecture.
--------------------------------------------------------------------------------
/Sample/Sources/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | //
4 | // Copyright 2023 • Sidetrack Tech Limited
5 | //
6 |
7 | import DeviceAuthority
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 | @State private var isSecureDevice: Bool = false
12 |
13 | var body: some View {
14 | VStack {
15 | HStack {
16 | Image(systemName: isSecureDevice ? "lock.open.fill" : "lock.fill")
17 | Text(isSecureDevice ? "Unlocked" : "Locked")
18 | .fontWeight(.semibold)
19 | }
20 | .font(.largeTitle)
21 | .foregroundColor(.accentColor)
22 | }
23 | .multilineTextAlignment(.center)
24 | .padding()
25 | .onAppear {
26 | let api = 0
27 | let authority = DeviceAuthority(name: "SwiftDeviceAuthority-Leaf")
28 |
29 | switch api {
30 | case 0: // Swift Concurrency
31 | Task {
32 | do {
33 | try await authority.determineAuthorisationStatus()
34 | isSecureDevice = true
35 | } catch {
36 | print(error)
37 | }
38 | }
39 |
40 | case 1: // Async Callback
41 | authority.determineAuthorisationStatus { result in
42 | switch result {
43 | case .failure(let error):
44 | print(error)
45 |
46 | case .success:
47 | isSecureDevice = true
48 | }
49 | }
50 |
51 | case 2: // Sync
52 | DispatchQueue.global(qos: .userInteractive).async {
53 | do {
54 | try authority.determineAuthorisationStatusSync()
55 |
56 | DispatchQueue.main.async {
57 | self.isSecureDevice = true
58 | }
59 | } catch {
60 | print(error)
61 | }
62 | }
63 |
64 | default: // Unknown
65 | print("🔴 Unknown API has been specified, human error.")
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Device Authority for iOS
2 |
3 | In [our blog post](https://blog.sidetrack.app/debugging-in-production) we discussed the ability to create configuration profiles on iOS to secure access to certain functionality. This acts as a form of keycard, allowing the right people access to the right areas - useful for both indie developers and large companies who want to grant access to debug menus to employees only.
4 |
5 | This Swift package aims to make it as easy as possible to adopt this pattern, building on top of the step-by-step instructions we published in [this Gist](https://gist.github.com/Sherlouk/ba24f6366cd2cb1f9ad9c400ca18ad09).
6 |
7 | ## Installation
8 |
9 | ### Command Line Tool
10 |
11 | Each [release](https://github.com/getsidetrack/swift-device-authority/releases) includes a binary which you can download and execute.
12 |
13 | Alternatively, you can clone the repository and `swift build` in the root of the project.
14 |
15 | ### DeviceAuthority Framework
16 |
17 | You can add the DeviceAuthority package to your application using Swift Package Manager.
18 |
19 | File > Add Packages > Paste URL `https://github.com/getsidetrack/swift-device-authority` > Add Package.
20 |
21 | ## Usage
22 |
23 | ### Command Line Tool
24 |
25 | There are two commands provided by the command-line tool. Neither take parameters, but will ask you for inputs (defaults are provided where possible).
26 |
27 | ```shell
28 | $ swift-device-authority create-authority
29 | $ swift-device-authority create-leaf
30 | ```
31 |
32 | Creating the authority will provide you with the mobileconfig which can be installed onto your iOS device or simulator.
33 |
34 | Creating the leaf will provide you with the certificate which needs to be embedded within your iOS application.
35 |
36 | All files will be saved in the current working directory. Only the mobileconfig and Leaf certificate are required, unless you intend on creating multiple leafs in the future (in which case you need to keep all authority files).
37 |
38 | ### DeviceAuthority Framework
39 |
40 | Once installed, import `DeviceAuthority` and instantiate the `DeviceAuthority` struct with your Leaf certificate name (by default this will be 'SwiftDeviceAuthority-Leaf' but you can change this to anything you wish).
41 |
42 | There are then three functions you can call, each provide the same functionality but vary with how they handle async code. See the Sample app for more information.
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/DeviceAuthority.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/swift-device-authority.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
45 |
47 |
53 |
54 |
55 |
56 |
62 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Sample/Resources/SwiftDeviceAuthority.mobileconfig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PayloadContent
6 |
7 |
8 | PayloadCertificateFileName
9 | DeviceAuthority.cer
10 | PayloadContent
11 |
12 | MIIE5jCCAs4CCQDspsS0D7kmrzANBgkqhkiG9w0BAQsFADA1MRIw
13 | EAYDVQQKDAlTaWRldHJhY2sxHzAdBgNVBAMMFkRldmljZUF1dGhv
14 | cml0eSBTYW1wbGUwHhcNMjMwMTA2MTYyNDA3WhcNMzMwMTAzMTYy
15 | NDA3WjA1MRIwEAYDVQQKDAlTaWRldHJhY2sxHzAdBgNVBAMMFkRl
16 | dmljZUF1dGhvcml0eSBTYW1wbGUwggIiMA0GCSqGSIb3DQEBAQUA
17 | A4ICDwAwggIKAoICAQDrEepKHJZgkIAF+dRp94dkqf8ttv49xzbL
18 | cHcOy0lhNgcUgSfvbXvas5+Bc2G0dRVcu93KdUy9aXOvL+qDp+Qx
19 | 5P3ct4cRHlCLE8KGQxkaYeiEZ3N8YA+/DzW7Gt50VxjTBxDKtWNZ
20 | SFp5p8T1x0D+A4VQhcM++V368fVwpgVNMSFSV2eLrwOEHih3UFv5
21 | r9ZedWhCQK+ns7gXGJVH0HS9KDy2/sM2NmUpvHct+N6PQ9FSbFTX
22 | +o62L1mEmZyXiVMJ594XHCqz/08w0TTYHouAh+h9nhNFpByLPAww
23 | h8TT0vFGb9UZkNT60zVgMmA3lfYIb9q358UZGX3TvwsdsRNl9aIz
24 | A9ScJV814Kq19iQFYAed6Uo8bYYGlDgOwLmyyImRNn12nCHCIRss
25 | jC9HvGe/xgsLwcWXqdIo80/tvFWsjCVaXl2jC5T9w1CO+BkbGHFW
26 | gyw1ocqQ9NFCaM3yx8rJSXkbBelSEq3stMh5DhzcBWaTtURx6tn4
27 | YHUvHLArPIi09OLSEu8Yr0ZM8i8bn30Jl4kcxLkKggK82mtMJci6
28 | ZLW0L45aCHbk9z6jT4b6hBxm/ti2k2hBKtBOw2ChK+FY08oUcHlG
29 | QCI4fGuTbmbG+NHmRvkNSSt5IAA3Q2Lt29A3WiPEDaqWRYwJuaS8
30 | ifMk2EFYdqs/H6O2omK5fHtVPQIDAQABMA0GCSqGSIb3DQEBCwUA
31 | A4ICAQCOVFYnU3WR756DrJ4cqCc5Il0CYXiROw1YcLva2PBmc8EB
32 | xWks9Gm91/ZgZDGyPjLNms4vZs0Y33BAdBHHOgRNopgP4XJIk0w1
33 | DlPwlx30ZTcYEmqwRNCHcoZhCC75RvDaUDsQmc7RwwbbP1xUdyhx
34 | svcXMkdGl2Nlj7ogzCwF4DYrN7Z2Ctx/imn4Pf1InBYxoWj1S7dF
35 | FF216PkYLtTakHE8uWrCn8Gp0UvlbwqxOE6RBbWymYxFhmtuwqjU
36 | rnM+/CWCcpZNO9FiuKY1h73Zkr460ZFnQXz5slB04SiPtA7mTw8z
37 | WUaGjo0D82xDe92x8Udrw7nkiWKa21MaLqv4JnGSWDjfjzRvEIfI
38 | +vcEtBgS8aslx+dArtpvB7dhQiV9PFVWrEwKZbtTWDAFA2SmL4PS
39 | O0DCLbf4VmmQmtB/ALwyWDUEUiGuOeGMMvh8LMr/SDJquqEFK2Pd
40 | Ro1X00LGjNt/mSfhZ9AAatjEZZ8i9WnmQ+vJJ9YfPXqYlARJrKh3
41 | +Gt/fr+eBOWDZPVOv0U4oIVmZvGeJzwNjs74c7YRGNB+U9jCPR/X
42 | vJGcwRovaxj72clf4N5d7+ay3WuMeEE+Z8ipu/OWXKHA/6l5+TAn
43 | FqgK5VsfWOhkfd/xAyN3Xz83yBrWBT7DCovwc/OyUGfERRoSDdNt
44 | 57yv46U0b7gAbQ==
45 |
46 | PayloadDescription
47 | Adds a CA root certificate
48 | PayloadDisplayName
49 | Swift Device Authority
50 | PayloadIdentifier
51 | com.apple.security.root.9525C50C-2824-42AC-B0DF-55D32E619920
52 | PayloadType
53 | com.apple.security.root
54 | PayloadUUID
55 | 9525C50C-2824-42AC-B0DF-55D32E619920
56 | PayloadVersion
57 | 1
58 |
59 |
60 | PayloadDescription
61 | Demonstrates functionality within the DeviceAuthority package's sample app.
62 | PayloadDisplayName
63 | DeviceAuthority Sample
64 | PayloadIdentifier
65 | sidetrack.device-authority.2D0528E1-12FA-4206-AD61-2C6A25166614
66 | PayloadOrganization
67 | Sidetrack
68 | PayloadRemovalDisallowed
69 |
70 | PayloadType
71 | Configuration
72 | PayloadUUID
73 | 2D0528E1-12FA-4206-AD61-2C6A25166614
74 | PayloadVersion
75 | 1
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Sample/DeviceAuthoritySample.xcodeproj/xcshareddata/xcschemes/DeviceAuthoritySample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Sources/CommandLine/MobileConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MobileConfiguration.swift
3 | //
4 | // Copyright 2023 • Sidetrack Tech Limited
5 | //
6 |
7 | import Foundation
8 | import ShellOut
9 |
10 | struct MobileConfigurationExporter {
11 | let name: String
12 | let abstract: String
13 | let organisation: String
14 | let file: String
15 |
16 | func export() throws -> Data {
17 | guard let certificateFileData = FileManager.default.contents(atPath: file) else {
18 | fatalError()
19 | }
20 |
21 | let certificateFileString = String(decoding: certificateFileData, as: UTF8.self)
22 | .components(separatedBy: .newlines)
23 | .dropFirst()
24 | .dropLast(2)
25 | .joined(separator: "\n")
26 |
27 | guard let certificateData = Data(base64Encoded: certificateFileString, options: .ignoreUnknownCharacters) else {
28 | fatalError()
29 | }
30 |
31 | let config = MobileConfiguration(
32 | name: name,
33 | abstract: abstract,
34 | organisation: organisation,
35 | certificate: certificateData
36 | )
37 |
38 | let encoder = PropertyListEncoder()
39 | encoder.outputFormat = .xml
40 |
41 | return try encoder.encode(config)
42 | }
43 | }
44 |
45 | struct MobileConfiguration: Encodable {
46 | enum CodingKeys: String, CodingKey {
47 | case content = "PayloadContent"
48 | case description = "PayloadDescription"
49 | case displayName = "PayloadDisplayName"
50 | case identifier = "PayloadIdentifier"
51 | case organization = "PayloadOrganization"
52 | case removalDisallowed = "PayloadRemovalDisallowed"
53 | case type = "PayloadType"
54 | case uuid = "PayloadUUID"
55 | case version = "PayloadVersion"
56 | }
57 |
58 | let content: [MobileConfigurationCertificate]
59 | let description: String
60 | let displayName: String
61 | let identifier: String
62 | let organization: String
63 | let removalDisallowed: Bool
64 | let type: String
65 | let uuid: String
66 | let version: Int
67 |
68 | init(name: String, abstract: String, organisation: String, certificate: Data) {
69 | let uniqueId = UUID().uuidString
70 |
71 | content = [.init(certificate: certificate)]
72 | description = abstract
73 | displayName = name
74 | identifier = "sidetrack.device-authority.\(uniqueId)"
75 | organization = organisation
76 | removalDisallowed = false
77 | type = "Configuration"
78 | uuid = uniqueId
79 | version = 1
80 | }
81 | }
82 |
83 | struct MobileConfigurationCertificate: Encodable {
84 | enum CodingKeys: String, CodingKey {
85 | case fileName = "PayloadCertificateFileName"
86 | case content = "PayloadContent"
87 | case description = "PayloadDescription"
88 | case displayName = "PayloadDisplayName"
89 | case identifier = "PayloadIdentifier"
90 | case type = "PayloadType"
91 | case uuid = "PayloadUUID"
92 | case version = "PayloadVersion"
93 | }
94 |
95 | let fileName: String
96 | let content: Data
97 | let description: String
98 | let displayName: String
99 | let identifier: String
100 | let type: String
101 | let uuid: String
102 | let version: Int
103 |
104 | init(certificate: Data) {
105 | let uniqueId = UUID().uuidString
106 |
107 | fileName = "DeviceAuthority.cer"
108 | content = certificate
109 | description = "Adds a CA root certificate"
110 | displayName = "Swift Device Authority"
111 | identifier = "com.apple.security.root.\(uniqueId)"
112 | type = "com.apple.security.root"
113 | uuid = uniqueId
114 | version = 1
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/DeviceAuthority-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
57 |
63 |
64 |
65 |
66 |
67 |
72 |
73 |
75 |
81 |
82 |
83 |
84 |
85 |
95 |
96 |
102 |
103 |
104 |
105 |
111 |
112 |
118 |
119 |
120 |
121 |
123 |
124 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/Sources/CommandLine/CreateLeafCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreateLeafCommand.swift
3 | //
4 | // Copyright 2023 • Sidetrack Tech Limited
5 | //
6 |
7 | import ConsoleKit
8 | import Foundation
9 | import ShellOut
10 |
11 | struct CreateLeafCommand: AsyncCommand {
12 | struct Signature: CommandSignature {}
13 |
14 | var help: String {
15 | "Creates a new leaf certificate"
16 | }
17 |
18 | func run(using context: CommandContext, signature _: Signature) async throws {
19 | // TODO: lots of input-duplication with CreateAuthorityCommand, how can we reuse?
20 |
21 | context.console.output("Welcome to the " + "Device Authority".consoleText(.success) + " command-line tool.\n")
22 |
23 | // NAME
24 | context.console.output(
25 | "Certificate Name".consoleText(isBold: true) + .newLine +
26 | "This will be embedded within the certificate.".consoleText(.info) + .newLine +
27 | .newLine +
28 | "> ".consoleText(.warning),
29 | newLine: false
30 | )
31 |
32 | var name = context.console.input()
33 |
34 | if name.isEmpty {
35 | name = "Swift Device Authority"
36 | context.console.warning("No input given, using default: '\(name)'")
37 | }
38 |
39 | // ORGANISATION
40 |
41 | context.console.output(
42 | "\nOrganisation Name".consoleText(isBold: true) + .newLine +
43 | "This will be embedded within the certificate.".consoleText(.info) + .newLine +
44 | .newLine +
45 | "> ".consoleText(.info),
46 | newLine: false
47 | )
48 |
49 | let organisation = context.console.input()
50 |
51 | if organisation.isEmpty {
52 | context.console.warning("No input given, organisation will be empty")
53 | }
54 |
55 | // PASSWORD
56 |
57 | context.console.output(
58 | "\nCertificate Password".consoleText(isBold: true) + .newLine +
59 | "This must match the one used to generate the Certificate Authority (CA).".consoleText(.info) + .newLine +
60 | .newLine +
61 | "> ".consoleText(.error),
62 | newLine: false
63 | )
64 |
65 | var failCount = 0
66 | var password: String?
67 |
68 | while password == nil {
69 | let tempPassword = context.console.input(isSecure: true)
70 |
71 | guard tempPassword.count >= 6 else {
72 | context.console.clear(lines: failCount == 0 ? 1 : 2)
73 | let exasperationString = failCount == 0 ? "." : String(repeating: "!", count: failCount)
74 | context.console.error("Input must be at least 6 character long\(exasperationString)")
75 | context.console.error("> ", newLine: false)
76 | failCount += 1
77 | continue
78 | }
79 |
80 | password = tempPassword
81 | }
82 |
83 | // DAYS
84 |
85 | context.console.output(
86 | "\nCertificate Validity Duration".consoleText(isBold: true) + .newLine +
87 | "The number of days in which the certificate will be valid for.".consoleText(.info) + .newLine +
88 | .newLine +
89 | "> ".consoleText(.warning),
90 | newLine: false
91 | )
92 |
93 | var daysInput = context.console.input()
94 |
95 | if daysInput.isEmpty {
96 | daysInput = "3650"
97 | context.console.warning("No input given, using default: \(daysInput) (10 years)")
98 | }
99 |
100 | guard let days = Int(daysInput) else {
101 | context.console.error("Input ('\(daysInput)') is not a valid integer number.")
102 | exit(1)
103 | }
104 |
105 | guard days >= 1 else {
106 | context.console.error("Input (\(days)) must be at least 1.")
107 | exit(1)
108 | }
109 |
110 | // VERIFY
111 |
112 | // TODO: summarise inputs for user to confirm before generating files
113 | guard context.console.confirm("\nAre you sure?") else {
114 | context.console.error("Failed to verify, stopping...")
115 | exit(1)
116 | }
117 |
118 | // STEPS
119 |
120 | // Create Leaf
121 | context.console.info("\nCreating a new Leaf certificate")
122 |
123 | let subject = [
124 | "CN": name,
125 | "O": organisation,
126 | ]
127 | .filter { !$0.value.isEmpty } // filter empty items
128 | .map { $0.key + "=" + $0.value } // key=value
129 | .joined(separator: "/")
130 |
131 | try shellOut(to: [
132 | "openssl req -new -nodes",
133 | "-out SwiftDeviceAuthority-Leaf.csr",
134 | "-newkey rsa:4096",
135 | "-keyout SwiftDeviceAuthority-Leaf.key",
136 | "-subj '/\(subject)'",
137 | ].joined(separator: " "))
138 | context.console.success("Completed")
139 |
140 | // Sign Leaf
141 | context.console.info("\nSigning Leaf certificate using Root CA")
142 |
143 | try shellOut(to: [
144 | "openssl x509 -req",
145 | "-in SwiftDeviceAuthority-Leaf.csr",
146 | "-CA SwiftDeviceAuthority.crt",
147 | "-CAkey SwiftDeviceAuthority.key",
148 | "-CAcreateserial",
149 | "-out SwiftDeviceAuthority-Leaf.cer",
150 | "-outform DER",
151 | "-days \(days)",
152 | "-sha256",
153 | "-passin pass:\(password!)",
154 | ].joined(separator: " "))
155 | context.console.success("Completed")
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Sources/CommandLine/CreateAuthorityCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreateAuthorityCommand.swift
3 | //
4 | // Copyright 2023 • Sidetrack Tech Limited
5 | //
6 |
7 | import ConsoleKit
8 | import Foundation
9 | import ShellOut
10 |
11 | struct CreateAuthorityCommand: AsyncCommand {
12 | struct Signature: CommandSignature {}
13 |
14 | var help: String {
15 | "Creates a new certificate authority, and mobile configuration profile"
16 | }
17 |
18 | func run(using context: CommandContext, signature _: Signature) async throws {
19 | context.console.output("Welcome to the " + "Device Authority".consoleText(.success) + " command-line tool.\n")
20 |
21 | // NAME
22 | context.console.output(
23 | "Profile Name".consoleText(isBold: true) + .newLine +
24 | "This will be visible within iOS system settings.".consoleText(.info) + .newLine +
25 | .newLine +
26 | "> ".consoleText(.warning),
27 | newLine: false
28 | )
29 |
30 | var name = context.console.input()
31 |
32 | if name.isEmpty {
33 | name = "Swift Device Authority"
34 | context.console.warning("No input given, using default: '\(name)'")
35 | }
36 |
37 | // DESCRIPTION
38 |
39 | context.console.output(
40 | "\nProfile Description".consoleText(isBold: true) + .newLine +
41 | "This will be visible within iOS system settings.".consoleText(.info) + .newLine +
42 | .newLine +
43 | "> ".consoleText(.info),
44 | newLine: false
45 | )
46 |
47 | let description = context.console.input()
48 |
49 | if description.isEmpty {
50 | context.console.warning("No input given, description will be empty")
51 | }
52 |
53 | // ORGANISATION
54 |
55 | context.console.output(
56 | "\nOrganisation Name".consoleText(isBold: true) + .newLine +
57 | "This will be embedded within the certificate.".consoleText(.info) + .newLine +
58 | .newLine +
59 | "> ".consoleText(.info),
60 | newLine: false
61 | )
62 |
63 | let organisation = context.console.input()
64 |
65 | if organisation.isEmpty {
66 | context.console.warning("No input given, organisation will be empty")
67 | }
68 |
69 | // PASSWORD
70 |
71 | context.console.output(
72 | "\nCertificate Password".consoleText(isBold: true) + .newLine +
73 | "This will be needed in order to generate Leaf certificates.".consoleText(.info) + .newLine +
74 | .newLine +
75 | "> ".consoleText(.error),
76 | newLine: false
77 | )
78 |
79 | var failCount = 0
80 | var password: String?
81 |
82 | while password == nil {
83 | let tempPassword = context.console.input(isSecure: true)
84 |
85 | guard tempPassword.count >= 6 else {
86 | context.console.clear(lines: failCount == 0 ? 1 : 2)
87 | let exasperationString = failCount == 0 ? "." : String(repeating: "!", count: failCount)
88 | context.console.error("Input must be at least 6 character long\(exasperationString)")
89 | context.console.error("> ", newLine: false)
90 | failCount += 1
91 | continue
92 | }
93 |
94 | password = tempPassword
95 | }
96 |
97 | // DAYS
98 |
99 | context.console.output(
100 | "\nCertificate Validity Duration".consoleText(isBold: true) + .newLine +
101 | "The number of days in which the certificate will be valid for.".consoleText(.info) + .newLine +
102 | .newLine +
103 | "> ".consoleText(.warning),
104 | newLine: false
105 | )
106 |
107 | var daysInput = context.console.input()
108 |
109 | if daysInput.isEmpty {
110 | daysInput = "3650"
111 | context.console.warning("No input given, using default: \(daysInput) (10 years)")
112 | }
113 |
114 | guard let days = Int(daysInput) else {
115 | context.console.error("Input ('\(daysInput)') is not a valid integer number.")
116 | exit(1)
117 | }
118 |
119 | guard days >= 1 else {
120 | context.console.error("Input (\(days)) must be at least 1.")
121 | exit(1)
122 | }
123 |
124 | // VERIFY
125 |
126 | // TODO: summarise inputs for user to confirm before generating files
127 | guard context.console.confirm("\nAre you sure?") else {
128 | context.console.error("Failed to verify, stopping...")
129 | exit(1)
130 | }
131 |
132 | // STEPS
133 |
134 | // Create key
135 |
136 | context.console.info("\nCreating a unique RSA private key with your chosen password")
137 | try shellOut(to: "openssl genrsa -aes256 -out SwiftDeviceAuthority.key -passout pass:\(password!) 4096")
138 | context.console.success("Completed")
139 |
140 | // Create authority
141 | context.console.info("\nCreating new Certificate Authority using generated key")
142 |
143 | let subject = [
144 | "CN": name,
145 | "O": organisation,
146 | ]
147 | .filter { !$0.value.isEmpty } // filter empty items
148 | .map { $0.key + "=" + $0.value } // key=value
149 | .joined(separator: "/")
150 |
151 | try shellOut(to: [
152 | "openssl req -x509 -new -nodes",
153 | "-key SwiftDeviceAuthority.key",
154 | "-sha256",
155 | "-days \(days)",
156 | "-out SwiftDeviceAuthority.crt",
157 | "-subj '/\(subject)'",
158 | "-passin pass:\(password!)",
159 | ].joined(separator: " "))
160 | context.console.success("Completed")
161 |
162 | // Write file
163 | context.console.info("\nCreating mobile configuration profile with new authority")
164 | let outputPath = URL(fileURLWithPath: "SwiftDeviceAuthority.mobileconfig")
165 | try MobileConfigurationExporter(
166 | name: name,
167 | abstract: description,
168 | organisation: organisation,
169 | file: "SwiftDeviceAuthority.crt"
170 | ).export().write(to: outputPath)
171 | context.console.success("Completed")
172 |
173 | let outputDirectory = outputPath.deletingLastPathComponent().absoluteString.replacingOccurrences(of: "file://", with: "")
174 | context.console.success("\nSaved SwiftDeviceAuthority files to \(outputDirectory)")
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/Sources/DeviceAuthority/DeviceAuthority.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeviceAuthority.swift
3 | //
4 | // Copyright 2023 • Sidetrack Tech Limited
5 | //
6 |
7 | import Foundation
8 | import Security
9 |
10 | public struct DeviceAuthority {
11 | public let name: String
12 | public let bundle: Bundle
13 |
14 | public init(name: String, bundle: Bundle = .main) {
15 | self.name = name
16 | self.bundle = bundle
17 | }
18 |
19 | // Async/Await (Swift Concurrency) was released in iOS 13
20 | @available(iOS 13.0.0, *)
21 | public func determineAuthorisationStatus() async throws {
22 | try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
23 | do {
24 | let certificate = try loadCertificate() // provided by client
25 | let trust = try createTrust(from: certificate)
26 |
27 | let queue = DispatchQueue.global(qos: .userInteractive)
28 | queue.async {
29 | // Queue completion is called on *must* be same as the queue the function itself is called on.
30 | // Without this, it will crash.
31 | SecTrustEvaluateAsyncWithError(trust, queue) { _, success, error in
32 | if let error = createError(from: error) {
33 | continuation.resume(throwing: error)
34 | return
35 | }
36 |
37 | if success == false {
38 | continuation.resume(throwing: AuthorisationStatusError.failedWithoutError)
39 | return
40 | }
41 |
42 | continuation.resume()
43 | }
44 | }
45 | } catch {
46 | continuation.resume(throwing: error)
47 | }
48 | }
49 | }
50 |
51 | // Completion-style signature for compatibility purposes
52 | public func determineAuthorisationStatus(completion: @escaping (Result) -> Void) {
53 | do {
54 | let certificate = try loadCertificate() // provided by client
55 | let trust = try createTrust(from: certificate)
56 |
57 | SecTrustEvaluateAsync(trust, .global(qos: .userInteractive)) { trust, result in
58 | // Unfortunately, this API does not give us rich APIs out of the box.
59 | if let error = createError(from: result, trust: trust) {
60 | completion(.failure(error))
61 | return
62 | }
63 |
64 | completion(.success(()))
65 | }
66 | } catch {
67 | completion(.failure(error))
68 | }
69 | }
70 |
71 | // Sync API
72 | public func determineAuthorisationStatusSync() throws {
73 | precondition(Thread.isMainThread == false, "This method should not be called on the main thread.")
74 |
75 | let certificate = try loadCertificate() // provided by client
76 | let trust = try createTrust(from: certificate)
77 |
78 | if #available(iOS 12.0, *) {
79 | var error: CFError?
80 |
81 | guard SecTrustEvaluateWithError(trust, &error) else {
82 | if let error = createError(from: error) {
83 | throw error
84 | } else {
85 | throw AuthorisationStatusError.failedWithoutError
86 | }
87 | }
88 | } else {
89 | var result: SecTrustResultType = .unspecified
90 | SecTrustEvaluate(trust, &result)
91 |
92 | if let error = createError(from: result, trust: trust) {
93 | throw error
94 | }
95 | }
96 | }
97 |
98 | // MARK: - Helpers
99 |
100 | internal func loadCertificate() throws -> SecCertificate {
101 | guard let path = bundle.path(forResource: name, ofType: "cer") else {
102 | throw AuthorisationStatusError.missingCertificate
103 | }
104 |
105 | guard let data = NSData(contentsOfFile: path), !data.isEmpty else {
106 | throw AuthorisationStatusError.missingCertificate
107 | }
108 |
109 | guard let certificate = SecCertificateCreateWithData(nil, data as CFData) else {
110 | // Without this check, `SecTrustCreateWithCertificates` will throw a -50 error (errSecParam)
111 | throw AuthorisationStatusError.invalidCertificate
112 | }
113 |
114 | return certificate
115 | }
116 |
117 | internal func createTrust(from certificate: SecCertificate) throws -> SecTrust {
118 | let policy = SecPolicyCreateBasicX509()
119 |
120 | var trust: SecTrust?
121 | SecTrustCreateWithCertificates([certificate] as CFArray, policy, &trust)
122 |
123 | guard let unwrappedTrust = trust else {
124 | // This will get triggered if your certificate is encoded using PEM.
125 | // `SecTrustCreateWithCertificates` expects certificates to use the DER encoding strategy.
126 | throw AuthorisationStatusError.invalidCertificate
127 | }
128 |
129 | return unwrappedTrust
130 | }
131 |
132 | // MARK: - Error Helpers
133 |
134 | internal func createError(from resultType: SecTrustResultType, trust: SecTrust) -> AuthorisationStatusError? {
135 | if resultType == .proceed {
136 | return nil
137 | }
138 |
139 | let trustResult = SecTrustCopyResult(trust) as? [String: Any]
140 | let trustResultDetails = trustResult?["TrustResultDetails"] as? [[String: Any]]
141 |
142 | if trustResultDetails?.first?.keys.contains("MissingIntermediate") == true {
143 | return AuthorisationStatusError.untrusted
144 | } else {
145 | return AuthorisationStatusError.failedWithResult(resultType)
146 | }
147 | }
148 |
149 | internal func createError(from error: CFError?) -> Error? {
150 | guard let error = error else {
151 | return nil
152 | }
153 |
154 | let domain = CFErrorGetDomain(error) as String
155 | let code = CFErrorGetCode(error)
156 |
157 | if domain == NSOSStatusErrorDomain, code == -25318 { // NSOSStatusErrorDomain: errSecCreateChainFailed
158 | // “” certificate is not trusted
159 | return AuthorisationStatusError.untrusted
160 | }
161 |
162 | return error
163 | }
164 | }
165 |
166 | public enum AuthorisationStatusError: LocalizedError {
167 | // A certificate could not be found with the provided name
168 | case missingCertificate
169 |
170 | // A certificate was found, but was in an invalid format
171 | case invalidCertificate
172 |
173 | // This device is not trusted.
174 | case untrusted
175 |
176 | // This device failed to evaluate, but we could identify why.
177 | case failedWithResult(SecTrustResultType)
178 |
179 | // This device failed to evaluate, but no error was thrown.
180 | case failedWithoutError
181 |
182 | public var errorDescription: String? {
183 | switch self {
184 | case .missingCertificate:
185 | return NSLocalizedString(
186 | "A certificate could not be found with the provided name",
187 | comment: "device-authority.missing-certificate"
188 | )
189 | case .invalidCertificate:
190 | return NSLocalizedString(
191 | "A certificate was found, but was in an invalid format",
192 | comment: "device-authority.invalid-certificate"
193 | )
194 | case .untrusted:
195 | return NSLocalizedString(
196 | "This device is not trusted.",
197 | comment: "device-authority.untrusted"
198 | )
199 | case .failedWithResult:
200 | return NSLocalizedString(
201 | "This device failed to evaluate, but we could identify why.",
202 | comment: "device-authority.failed-with-result"
203 | )
204 | case .failedWithoutError:
205 | return NSLocalizedString(
206 | "This device failed to evaluate, but no error was thrown.",
207 | comment: "device-authority.failed-without-error"
208 | )
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/Sample/DeviceAuthoritySample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 16102FAB29678CD500FC1D30 /* DeviceAuthorityApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16102FAA29678CD500FC1D30 /* DeviceAuthorityApp.swift */; };
11 | 16102FAD29678CD500FC1D30 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16102FAC29678CD500FC1D30 /* ContentView.swift */; };
12 | 16102FAF29678CD700FC1D30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 16102FAE29678CD700FC1D30 /* Assets.xcassets */; };
13 | 16102FC22967914A00FC1D30 /* DeviceAuthority in Frameworks */ = {isa = PBXBuildFile; productRef = 16102FC12967914A00FC1D30 /* DeviceAuthority */; };
14 | 16102FC72967947800FC1D30 /* SwiftDeviceAuthority-Leaf.cer in Resources */ = {isa = PBXBuildFile; fileRef = 16102FC52967946700FC1D30 /* SwiftDeviceAuthority-Leaf.cer */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | 16102FA729678CD500FC1D30 /* DeviceAuthoritySample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DeviceAuthoritySample.app; sourceTree = BUILT_PRODUCTS_DIR; };
19 | 16102FAA29678CD500FC1D30 /* DeviceAuthorityApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAuthorityApp.swift; sourceTree = ""; };
20 | 16102FAC29678CD500FC1D30 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
21 | 16102FAE29678CD700FC1D30 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
22 | 16102FBF2967912700FC1D30 /* DeviceAuthority */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DeviceAuthority; path = ..; sourceTree = ""; };
23 | 16102FC52967946700FC1D30 /* SwiftDeviceAuthority-Leaf.cer */ = {isa = PBXFileReference; lastKnownFileType = text; path = "SwiftDeviceAuthority-Leaf.cer"; sourceTree = ""; };
24 | /* End PBXFileReference section */
25 |
26 | /* Begin PBXFrameworksBuildPhase section */
27 | 16102FA429678CD500FC1D30 /* Frameworks */ = {
28 | isa = PBXFrameworksBuildPhase;
29 | buildActionMask = 2147483647;
30 | files = (
31 | 16102FC22967914A00FC1D30 /* DeviceAuthority in Frameworks */,
32 | );
33 | runOnlyForDeploymentPostprocessing = 0;
34 | };
35 | /* End PBXFrameworksBuildPhase section */
36 |
37 | /* Begin PBXGroup section */
38 | 16102F9E29678CD500FC1D30 = {
39 | isa = PBXGroup;
40 | children = (
41 | 16102FBF2967912700FC1D30 /* DeviceAuthority */,
42 | 16102FA829678CD500FC1D30 /* Products */,
43 | 16102FC42967944D00FC1D30 /* Resources */,
44 | 16102FA929678CD500FC1D30 /* Sources */,
45 | );
46 | sourceTree = "";
47 | };
48 | 16102FA829678CD500FC1D30 /* Products */ = {
49 | isa = PBXGroup;
50 | children = (
51 | 16102FA729678CD500FC1D30 /* DeviceAuthoritySample.app */,
52 | );
53 | name = Products;
54 | sourceTree = "";
55 | };
56 | 16102FA929678CD500FC1D30 /* Sources */ = {
57 | isa = PBXGroup;
58 | children = (
59 | 16102FAA29678CD500FC1D30 /* DeviceAuthorityApp.swift */,
60 | 16102FAC29678CD500FC1D30 /* ContentView.swift */,
61 | 16102FAE29678CD700FC1D30 /* Assets.xcassets */,
62 | );
63 | path = Sources;
64 | sourceTree = "";
65 | };
66 | 16102FC42967944D00FC1D30 /* Resources */ = {
67 | isa = PBXGroup;
68 | children = (
69 | 16102FC52967946700FC1D30 /* SwiftDeviceAuthority-Leaf.cer */,
70 | );
71 | path = Resources;
72 | sourceTree = "";
73 | };
74 | /* End PBXGroup section */
75 |
76 | /* Begin PBXNativeTarget section */
77 | 16102FA629678CD500FC1D30 /* DeviceAuthoritySample */ = {
78 | isa = PBXNativeTarget;
79 | buildConfigurationList = 16102FB529678CD700FC1D30 /* Build configuration list for PBXNativeTarget "DeviceAuthoritySample" */;
80 | buildPhases = (
81 | 16102FA329678CD500FC1D30 /* Sources */,
82 | 16102FA429678CD500FC1D30 /* Frameworks */,
83 | 16102FA529678CD500FC1D30 /* Resources */,
84 | );
85 | buildRules = (
86 | );
87 | dependencies = (
88 | );
89 | name = DeviceAuthoritySample;
90 | packageProductDependencies = (
91 | 16102FC12967914A00FC1D30 /* DeviceAuthority */,
92 | );
93 | productName = DeviceAuthority;
94 | productReference = 16102FA729678CD500FC1D30 /* DeviceAuthoritySample.app */;
95 | productType = "com.apple.product-type.application";
96 | };
97 | /* End PBXNativeTarget section */
98 |
99 | /* Begin PBXProject section */
100 | 16102F9F29678CD500FC1D30 /* Project object */ = {
101 | isa = PBXProject;
102 | attributes = {
103 | BuildIndependentTargetsInParallel = YES;
104 | LastSwiftUpdateCheck = 1410;
105 | LastUpgradeCheck = 1410;
106 | TargetAttributes = {
107 | 16102FA629678CD500FC1D30 = {
108 | CreatedOnToolsVersion = 14.1;
109 | };
110 | };
111 | };
112 | buildConfigurationList = 16102FA229678CD500FC1D30 /* Build configuration list for PBXProject "DeviceAuthoritySample" */;
113 | compatibilityVersion = "Xcode 13.0";
114 | developmentRegion = en;
115 | hasScannedForEncodings = 0;
116 | knownRegions = (
117 | en,
118 | );
119 | mainGroup = 16102F9E29678CD500FC1D30;
120 | productRefGroup = 16102FA829678CD500FC1D30 /* Products */;
121 | projectDirPath = "";
122 | projectRoot = "";
123 | targets = (
124 | 16102FA629678CD500FC1D30 /* DeviceAuthoritySample */,
125 | );
126 | };
127 | /* End PBXProject section */
128 |
129 | /* Begin PBXResourcesBuildPhase section */
130 | 16102FA529678CD500FC1D30 /* Resources */ = {
131 | isa = PBXResourcesBuildPhase;
132 | buildActionMask = 2147483647;
133 | files = (
134 | 16102FC72967947800FC1D30 /* SwiftDeviceAuthority-Leaf.cer in Resources */,
135 | 16102FAF29678CD700FC1D30 /* Assets.xcassets in Resources */,
136 | );
137 | runOnlyForDeploymentPostprocessing = 0;
138 | };
139 | /* End PBXResourcesBuildPhase section */
140 |
141 | /* Begin PBXSourcesBuildPhase section */
142 | 16102FA329678CD500FC1D30 /* Sources */ = {
143 | isa = PBXSourcesBuildPhase;
144 | buildActionMask = 2147483647;
145 | files = (
146 | 16102FAD29678CD500FC1D30 /* ContentView.swift in Sources */,
147 | 16102FAB29678CD500FC1D30 /* DeviceAuthorityApp.swift in Sources */,
148 | );
149 | runOnlyForDeploymentPostprocessing = 0;
150 | };
151 | /* End PBXSourcesBuildPhase section */
152 |
153 | /* Begin XCBuildConfiguration section */
154 | 16102FB329678CD700FC1D30 /* Debug */ = {
155 | isa = XCBuildConfiguration;
156 | buildSettings = {
157 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
158 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
159 | ONLY_ACTIVE_ARCH = YES;
160 | SDKROOT = iphoneos;
161 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
162 | };
163 | name = Debug;
164 | };
165 | 16102FB629678CD700FC1D30 /* Debug */ = {
166 | isa = XCBuildConfiguration;
167 | buildSettings = {
168 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
169 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
170 | CODE_SIGN_STYLE = Automatic;
171 | CURRENT_PROJECT_VERSION = 1;
172 | GENERATE_INFOPLIST_FILE = YES;
173 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
174 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
175 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
176 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
177 | LD_RUNPATH_SEARCH_PATHS = (
178 | "$(inherited)",
179 | "@executable_path/Frameworks",
180 | );
181 | MARKETING_VERSION = 1.0;
182 | PRODUCT_BUNDLE_IDENTIFIER = "app.sidetrack.device-authority";
183 | PRODUCT_NAME = "$(TARGET_NAME)";
184 | SWIFT_VERSION = 5.0;
185 | TARGETED_DEVICE_FAMILY = "1,2";
186 | };
187 | name = Debug;
188 | };
189 | /* End XCBuildConfiguration section */
190 |
191 | /* Begin XCConfigurationList section */
192 | 16102FA229678CD500FC1D30 /* Build configuration list for PBXProject "DeviceAuthoritySample" */ = {
193 | isa = XCConfigurationList;
194 | buildConfigurations = (
195 | 16102FB329678CD700FC1D30 /* Debug */,
196 | );
197 | defaultConfigurationIsVisible = 0;
198 | defaultConfigurationName = Debug;
199 | };
200 | 16102FB529678CD700FC1D30 /* Build configuration list for PBXNativeTarget "DeviceAuthoritySample" */ = {
201 | isa = XCConfigurationList;
202 | buildConfigurations = (
203 | 16102FB629678CD700FC1D30 /* Debug */,
204 | );
205 | defaultConfigurationIsVisible = 0;
206 | defaultConfigurationName = Debug;
207 | };
208 | /* End XCConfigurationList section */
209 |
210 | /* Begin XCSwiftPackageProductDependency section */
211 | 16102FC12967914A00FC1D30 /* DeviceAuthority */ = {
212 | isa = XCSwiftPackageProductDependency;
213 | productName = DeviceAuthority;
214 | };
215 | /* End XCSwiftPackageProductDependency section */
216 | };
217 | rootObject = 16102F9F29678CD500FC1D30 /* Project object */;
218 | }
219 |
--------------------------------------------------------------------------------