├── 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 | --------------------------------------------------------------------------------