Skip to content

openziti/ziti-sdk-swift

Ziggy using the ziti-sdk-swift

Ziti SDK for Swift

Build Status

An SDK for accessing Ziti from macOS and iOS applications using the Swift programming language.

This SDK provides a Swift-friendly wrapper of the Ziti Tunnel C SDK, an implementation of URLProtocol for intercepting HTTP and HTTPS traffic, and examples of using the SDK in an application.

Usage

The Ziti class is the main entry point for accessing Ziti networks. An instance of Ziti requires a ZitiIdentity at time of initialization.

Use Ziti.createConnection() to create instances of ZitiConnection to ZitiConnection.dial(_:_:_:) services or ZitiConnection.listen(_:_:_:) for service connections.

Use ZitiUrlProtocol to intercept HTTP and HTTPS connections and route them over a Ziti network.

Enrollment

A ZitiIdentity is created as part of the enrollment process with a Ziti network. Ziti support enrollment using a one-time JWT supplied by your Ziti network administror.

The Ziti.enroll(_:_:) method validates the JWT is properly signed, creates a private key and stores it in the keychain, initiates a Certificate Signing Request (CSR) with the controller, and stores the resultant certificate in the keychain.

Swift

import CZiti

let jwtFile = <...>
let outFile = <...>

Ziti.enroll(jwtFile) { zid, zErr in
    guard let zid = zid else {
        fputs("Invalid enrollment response, \(String(describing: zErr))\n", stderr)
        exit(-1)
    }
    guard zid.save(outFile) else {
        fputs("Unable to save to file \(outFile)\n", stderr)
        exit(-1)
    }
    
    print("Successfully enrolled id \"\(zid.id)\" with controller \"\(zid.ztAPI)\"")
}

Objective-C

#import "CZiti-Swift.h"

NSString *jwtFile = <...>
NSString *outFile = <...>

[Ziti enroll:jwtFile : ^(ZitiIdentity *zid, ZitiError *zErr) {
    if (zErr != NULL) {
        // Handle error
        return;
    }

    if (![zid save:outFile]) {
        // Handle error
        return;
    }
}];

The identity file saved to outfile in the example code above contains information for contacting the Ziti controller and locally accessing the private key and certificate in the keychain.

Running Ziti

A typical application flow would:

  1. Check a well-known location for a stored identity file
  2. If not present, initiate an enrollment (e.g., prompt the user for location of a one-time JWT enrollment file, or scan in a QR code)
  3. When identity file is available, use it to create and run an instance of Ziti

Ziti executes on a loop, similar to Foundation's Runloop. The Ziti.run(_:) method essentially enters an infinite loop processing Ziti events, and will only exit after Ziti is shut down.

The Ziti.runAsync(_:) method is provided as a convenience to spawn a new thread and call Ziti.run(_:).

Swift

let zidFile = <...>

guard let ziti = Ziti(fromFile: zidFile) else {
    print("Unable to create Ziti identity from file \(zidFile)")
    return
}

ziti.runAsync { zErr in
    guard zErr == nil else {
        print("Unable to run Ziti: \(String(describing: zErr!))")
        return
    }
    print("Successfully initialized Ziti!")
}

Objective-C

NSString *zidFile = <...>

Ziti *ziti = [[Ziti alloc] initFromFile:[self zidFile]];
    
if (ziti != NULL) {
    [ziti runAsync: ^(ZitiError *zErr) {
        if (zErr != NULL) {
            // Handle error
            return;
        }
        [ZitiUrlProtocol register:ziti :10000];
    }];
}

To execute code on the thread running Ziti use the perform(_:) method.

Using ZitiUrlProtocol

The SDK also includes ZitiUrlProtocol, which implements a URLProtocol that interceptes HTTP and HTTPS requests for Ziti services and routes them over a Ziti network.

ZitiUrlProtocol should be instantiated as part of the Ziti.InitCallback of Ziti.run(_:) to ensure Ziti is initialized before starting to intercept services.

Swift

ziti.runAsync { zErr in
    guard zErr == nil else {
        // Handle error
        return
    }
    ZitiUrlProtocol.register(ziti)
}

Objective-C

[ziti runAsync: ^(ZitiError *zErr) {
    if (zErr != NULL) {
        // Handle error
        return;
    }
    [ZitiUrlProtocol register:ziti :10000];
}];

If using your own URLSession insteal of URLSession.shared, ZitiUrlProtocol will need to be configured in your URLSession's configuration:

    let configuration = URLSessionConfiguration.default
    configuration.protocolClasses?.insert(ZitiUrlProtocol.self, at: 0)
    urlSession = URLSession(configuration:configuration)

See also the documentation included in the CZiti module available in the Xcode Quick Help pane.

Adding CZiti as a Dependency

CZiti is built into an XCFramework (CZiti.xcframework) that includes a static library (libCZiti.a) for each platform and architecture.

Note that that CZiti is not built for Bitcode, and when building for a device the Build Settings - Build Options should set Enable Bitcode to No.

Note that CZiti depends on libresolv.9.tbd, and requires access to outbound network connections and the Apple Keychain.

Via Swift Package Manager

See ziti-sdk-swift-dist for access to CZiti.xcframework built from this repository and made available as a .binaryTarget.

Using a locally built CZiti

To exercise modifications to the CZiti framework in your application, first add the CZiti module as a dependency to your app as described above, then override the CZiti framework with a local package. See Editing a package dependency as a local package for an overview of this process.

I used the following steps to override CZiti with a local build in the ziti-tunnel-apple project:

  1. Build the CZiti framework. You may want to build for debugging:

    $ CONFIGURATION=Debug ./build_all.sh

    When complete, the build will be located in ./dist/CZiti.xcframework.

  2. Create a Package.swift for the local CZiti framework:

    cat > ./dist/Package.swift <<EOF
    // swift-tools-version: 5.7
    import PackageDescription
    
    let package = Package(
        name: "CZiti",
        platforms: [ .macOS(.v10_14), .iOS(.v13) ],
        products: [ .library( name: "CZiti", targets: ["CZiti"]) ],
        targets: [
            .binaryTarget(
                name: "CZiti",
                path: "./CZiti.xcframework")
        ]
     )
    EOF
  3. Move or copy the ./dist directory into your application's top-level source directory, renaming it to 'ziti-sdk-swift-dist'. The final directory structure should look like this:

     - YourApplication/
       - ziti-sdk-swift-dist/
         - Package.swift
         - CZiti.xcframework/
    

    The apple documentation doesn't mention this, but I was unable to get Xcode to recognize that the local package overrides the released CZiti framework unless the parent directory of the local CZiti framework was named 'ziti-sdk-swift-dist' (to match the github repo the CZiti releases come from), and local CZiti framework was in the application's directory.

  4. Add the local package to your application. This can be done by clicking "Add Local..." while adding a package dependency to your application. Select the 'ziti-sdk-swift-dist` directory in the dialog.

    You should see the CZiti entry disappear from your project's Package Dependencies in the project navigator when the local CZiti package is referenced.

Via CocoaPods

DEPECATED AS OF v0.30.11, please convert to use of Swift Package Manager

If you are using Cocoapods, update your Podfile:

target 'Some-macOS-Target'
  use_frameworks!
  platform :macos, '10.15'
  pod 'CZiti-macOS', '~> 0.1'
end

target 'SomeTarget-iOS-Target'
  use_frameworks!
  platform :ios, '13.4'
  pod 'CZiti-iOS', '~> 0.1'
end

For further information on Cocoapods, check their official documentation.

Via libCZiti.a

  • Follow the build steps below to create libCZiti.a
  • Add libCZiti.a library to your project's Frameworks and Libraries, and ensure it is listed in your project's Build Phases under Link Binary with Libraries.
  • Your Library Search Path and Swift Compiler Seatch Paths - Import Paths should include the directory containing libCZiti.a and CZiti.swiftmodule/
  • When this project is built from Xcode, the CZiti-Swift.h file is copied to $(PROJECT_ROOT)/include/$(PLATFORM) (e.g., ./include/iphoneos). CZiti-Swift.h can also be found the the DerivedSources directory under ./DerivedData following a build from either Xcode or via build_all.sh. This file is needed to use CZiti from Objective-C. Your Search Paths - Header Search Paths must include the directory containing CZiti-Swift.h.
  • Inspect the sample apps' configurations in this repository for relevant build settings for libraries and paths

Examples

This repository includes a few examples of using the library:

  • ziti-mac-enroller is a utility that will enroll an identity using a supplied one-time JWT token. It can optionally update the keychain to trust for the CA pool used by the Ziti controller
  • sample-mac-host is a command-line utility that can operate as either a client or a server for a specified Ziti server
  • sample-ios exercises ZitiUrlProtocol to intercept URLSesson requests, route them over Ziti, and display the results
  • sample-ios-objc demonstrates using Objective-C to exercise ZitiUrlProtocol

Building

Update xcconfig Settings

Create a file called Configs/workspace-settings-overrides.xcconfig and populate with appropriate values. See Configs/workplace-settings.xcconfig for all possible values.

DEVELOPMENT_TEAM = XXXXXXXXXX
ORGANIZATION_PREFIX = ...

Execute Build Script

The project depends on the Ziti Tunnel C SDK, which is built directly into the library. It is maintained as a submodule at ./deps/ziti-tunnel-sdk-c. Be sure to follow the vcpkg setup steps in deps/ziti-tunnel-sdk-c/BUILD.md. This project expects builds to be built in ./deps/ziti-tunnel-sdk-c/build-macosx-x86_64 and ./deps/ziti-tunnel-sdk-c/build-macosx-arm64 for macOS and ./deps/ziti-sdk-c/build-iphoneos-arm64 for iOS (or build-iphonesimulator-x86_64or build-iphonesimulator-arm64 for the simulator).

This project contains the buid_all.sh script that will build the project from the command-line for macosx, iphoneos, and iphonesimulator platforms.

Once the static libraries are built, the build_all.sh script executes make_dist.sh, creating an XCFramework called CZiti.xcframework, under the project's ./dist directory.

The scripts require the following executables to be on the caller's path:

  • xcodebuild used to build CZiti-* schemes in CZiti.xcodeproj, avaialble as part of your Xcode installation
  • xcpretty also used to build CZiti-* schemes in CZiti.xcodeproj. (Can be installed via gem install xcpretty)
  • cmake used for building the Ziti Tunnel C SDK dependency. (Can be installed via brew install cmake)
$ git clone --recurse-submodules https://github.com/openziti/ziti-sdk-swift.git
$ cd ziti-sdk-swift
$ /bin/sh build_all.sh

By default, the scripts build for Release configuration. To build for Debug, execute

$ CONFIGURATION=Debug /bin/sh build_all.sh

The resultant libCZiti.a and CZiti.swiftmodule are available in the appropriate sub-directory of ./DerivedData.

Getting Help

Please use these community resources for getting help.

  • Read the docs
  • Participate in discussion on Discourse
  • Use GitHub issues for tracking bugs and feature requests.

Copyright NetFoundry Inc.