Skip to content

Commit a664511

Browse files
committed
Add CZITI_TEST_INSECURE_KEYS compile flag for CI integration tests
1 parent 9360468 commit a664511

File tree

5 files changed

+130
-12
lines changed

5 files changed

+130
-12
lines changed

.github/workflows/CI.yml

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,53 @@ jobs:
114114
xcodebuild test -scheme CZitiTests -derivedDataPath ./DerivedData/CZiti \
115115
-sdk macosx -destination 'platform=macOS' ONLY_ACTIVE_ARCH=YES | xcpretty
116116
117-
# Builds the integration test tool to catch compile regressions. End-to-end
118-
# execution against `ziti edge quickstart` is deferred until the macOS data
119-
# protection keychain / entitlement issue is resolved (see ziti-test-runner/main.swift).
120-
- name: Build ziti-test-runner
117+
# Build ziti-test-runner with the insecure-keys test flag so enrollment can generate
118+
# ephemeral keys and store them in the .zid file instead of the macOS data protection
119+
# keychain (which requires provisioning-profile-backed entitlements CI doesn't have).
120+
# NEVER use this flag in a release build.
121+
- name: Build ziti-test-runner (insecure test keys)
121122
run: |
122123
set -o pipefail
123124
xcodebuild build -configuration Debug -scheme ziti-test-runner \
124125
-derivedDataPath ./DerivedData/CZiti -sdk macosx \
125126
-destination 'platform=macOS' ONLY_ACTIVE_ARCH=YES \
127+
SWIFT_ACTIVE_COMPILATION_CONDITIONS='$(inherited) CZITI_TEST_INSECURE_KEYS' \
126128
CODE_SIGNING_ALLOWED=NO | xcpretty
127129
130+
- name: Install ziti CLI
131+
run: |
132+
set -euo pipefail
133+
ZITI_URL=$(curl -s https://api.github.com/repos/openziti/ziti/releases/latest \
134+
| grep browser_download_url | grep darwin-arm64 | head -1 | cut -d\" -f4)
135+
echo "Downloading $ZITI_URL"
136+
curl -sL "$ZITI_URL" -o /tmp/ziti.tgz
137+
sudo tar -xzf /tmp/ziti.tgz -C /usr/local/bin --strip-components=1 ziti/ziti
138+
ziti version
139+
140+
- name: Integration test (OTT enrollment against quickstart)
141+
run: |
142+
set -euo pipefail
143+
rm -rf /tmp/qs && mkdir /tmp/qs
144+
nohup ziti edge quickstart --home /tmp/qs > /tmp/qs.log 2>&1 &
145+
QS_PID=$!
146+
trap "kill $QS_PID 2>/dev/null || true; cat /tmp/qs.log | tail -60" EXIT
147+
148+
# Wait for controller
149+
for i in $(seq 1 60); do
150+
if curl -sk https://localhost:1280/edge/client/v1/version >/dev/null 2>&1; then
151+
echo "controller ready after ${i}s"; break
152+
fi
153+
sleep 1
154+
done
155+
156+
# Login and create an OTT identity
157+
ziti edge login localhost:1280 -u admin -p admin -y
158+
ziti edge create identity ztr-integ -o /tmp/ztr.jwt
159+
160+
# Run the integration test tool
161+
./DerivedData/CZiti/Build/Products/Debug/ziti-test-runner \
162+
--timeout 60 /tmp/ztr.jwt
163+
128164
- name: Create Frameworks
129165
run: |
130166
./make_dist.sh

CZitiTests/ZitiIdentityTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,33 @@ class ZitiIdentityTests: XCTestCase {
8585
let decoded = try JSONDecoder().decode(ZitiIdentity.self, from: data)
8686
XCTAssertEqual(decoded.startDisabled, true)
8787
}
88+
89+
func testKeyDefaultsToNil() throws {
90+
let id = ZitiIdentity(id: "x", ztAPIs: ["https://ctrl:1280"])
91+
XCTAssertNil(id.key)
92+
93+
let data = try JSONEncoder().encode(id)
94+
let decoded = try JSONDecoder().decode(ZitiIdentity.self, from: data)
95+
XCTAssertNil(decoded.key)
96+
}
97+
98+
func testKeyRoundTrip() throws {
99+
let pem = "-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJB...\n-----END RSA PRIVATE KEY-----\n"
100+
let id = ZitiIdentity(id: "x", ztAPIs: ["https://ctrl:1280"])
101+
id.key = pem
102+
103+
let data = try JSONEncoder().encode(id)
104+
let decoded = try JSONDecoder().decode(ZitiIdentity.self, from: data)
105+
XCTAssertEqual(decoded.key, pem)
106+
}
107+
108+
func testDecodeOldZidWithoutKeyField() throws {
109+
// Existing .zid files in the wild never have a key field. Must still decode.
110+
let json = """
111+
{"id":"legacy","ztAPI":"https://ctrl:1280","certs":"CERT","ca":"CA"}
112+
"""
113+
let decoded = try JSONDecoder().decode(ZitiIdentity.self, from: json.data(using: .utf8)!)
114+
XCTAssertEqual(decoded.id, "legacy")
115+
XCTAssertNil(decoded.key)
116+
}
88117
}

lib/Ziti.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,20 +368,28 @@ import CZitiPrivate
368368
}
369369

370370
let certs = dropFirst("pem:", respCert)
371+
#if !CZITI_TEST_INSECURE_KEYS
372+
// Release build: persist the signed cert in the keychain alongside the private key.
371373
_ = zkc.deleteCertificate(silent: true)
372374
guard zkc.storeCertificate(fromPem: certs) == nil else {
373375
let errStr = "Unable to store certificate\n"
374376
log.error(errStr, function:"enroll()")
375377
enrollCallback(nil, ZitiError(errStr))
376378
return
377379
}
380+
#endif
378381

379382
var ca = resp.id.ca
380383
if let idCa = resp.id.ca {
381384
ca = dropFirst("pem:", idCa)
382385
}
383386

384387
let zid = ZitiIdentity(id: subj, ztAPIs: resp.ztAPIs, certs: certs, ca: ca)
388+
#if CZITI_TEST_INSECURE_KEYS
389+
// Insecure test build: stash the ephemeral PEM in the identity so run() can use
390+
// it without a keychain lookup.
391+
zid.key = pem
392+
#endif
385393
log.info("Enrolled id:\(subj) with controller: \(zid.ztAPI)", function:"enroll()")
386394

387395
enrollCallback(zid, nil)
@@ -955,10 +963,20 @@ import CZitiPrivate
955963
log.debug("identity \(id.id) does not have certificates. this is ok if using external authentication")
956964
}
957965

958-
// Get private key
966+
// Get private key. Under CZITI_TEST_INSECURE_KEYS, prefer a PEM stored in the
967+
// identity file so ad-hoc-signed CLI tools can use enrolled identities without
968+
// touching the data protection keychain.
959969
var privKeyPEMPtr: UnsafeMutablePointer<Int8>? = nil
960-
if let privKey = zkc.getPrivateKey() {
961-
let privKeyPEM = zkc.getKeyPEM(privKey)
970+
let privKeyPEM: String? = {
971+
#if CZITI_TEST_INSECURE_KEYS
972+
if let idKey = id.key, !idKey.isEmpty { return idKey }
973+
#endif
974+
if let privKey = zkc.getPrivateKey() {
975+
return zkc.getKeyPEM(privKey)
976+
}
977+
return nil
978+
}()
979+
if let privKeyPEM = privKeyPEM {
962980
privKeyPEMPtr = UnsafeMutablePointer<Int8>.allocate(capacity: privKeyPEM.count + 1)
963981
privKeyPEMPtr!.initialize(from: privKeyPEM, count: privKeyPEM.count + 1)
964982
} else {

lib/ZitiIdentity.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,27 @@ import Foundation
5454
/// Certificates (PEM)
5555
@objc public var certs:String?
5656

57+
/// Private key (PEM).
58+
///
59+
/// Only populated by builds compiled with `CZITI_TEST_INSECURE_KEYS`. Release builds
60+
/// keep the private key in the keychain and leave this field nil. If this field is
61+
/// populated in a loaded identity, `Ziti.run()` (in an insecure-keys build) will use
62+
/// it directly instead of reading from the keychain. This field is never populated
63+
/// or read by release builds.
64+
@objc public var key:String?
65+
5766
/// CA pool verified as part of enrollment that can be used to establish trust with of the Ziti controller
5867
@objc public var ca:String?
59-
68+
6069
/// Request `Ziti` to start this identity in disabled state
6170
public var startDisabled:Bool? = false
62-
71+
6372
/// Initialize a `ZitiIdentity` given the provided identity infomation
6473
///
6574
/// - Parameters:
6675
/// - id: unique identifier of this identity
6776
/// - ztAPIs: URLs for accessing Ziti controller API
68-
/// - certCNs: common names of certififcates
77+
/// - certCNs: common names of certififcates
6978
/// - name: name currently configured for this identity
7079
/// - ca: CA pool that can be used to verify trust of the Ziti controller
7180
@objc public init(id:String, ztAPIs:[String], name:String?=nil, certs:String?=nil, ca:String?=nil) {

lib/ZitiKeychain.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ limitations under the License.
1616

1717
import Foundation
1818

19+
#if CZITI_TEST_INSECURE_KEYS
20+
// Lazy-initialized so the warning prints at most once per process, the first time
21+
// anything in this module actually generates an ephemeral key.
22+
internal let _cztiInsecureBuildWarningPrinted: Void = {
23+
let msg = "⚠️ CZITI_TEST_INSECURE_KEYS build: private keys are generated ephemerally and written into .zid files in plaintext. NEVER SHIP THIS BUILD.\n"
24+
FileHandle.standardError.write(Data(msg.utf8))
25+
}()
26+
#endif
27+
1928
/// This class manages access to the Keychain, creating and storing keys and certificates needed to access a Ziti network.
2029
///
2130
/// This is primarily an internally used class, though certain methods are marked public in order to support senarios where the enrollment is
@@ -41,11 +50,27 @@ public class ZitiKeychain : NSObject {
4150

4251
private let keySize = 3072
4352
func createPrivateKey() -> SecKey? {
53+
#if CZITI_TEST_INSECURE_KEYS
54+
// Insecure test build: generate an ephemeral key with no keychain interaction.
55+
// Caller is expected to stash the extracted PEM in the ZitiIdentity for persistence.
56+
_ = _cztiInsecureBuildWarningPrinted // fire once, first time anyone mints a key
57+
let parameters: [CFString: Any] = [
58+
kSecAttrKeyType: kSecAttrKeyTypeRSA,
59+
kSecAttrKeySizeInBits: keySize,
60+
kSecAttrIsPermanent: false,
61+
]
62+
var error: Unmanaged<CFError>?
63+
guard let privateKey = SecKeyCreateRandomKey(parameters as CFDictionary, &error) else {
64+
log.error("Unable to create ephemeral private key for \(tag): \(error!.takeRetainedValue() as Error)")
65+
return nil
66+
}
67+
return privateKey
68+
#else
4469
let privateKeyParams: [CFString: Any] = [ // iOS
4570
kSecAttrIsPermanent: true,
4671
kSecAttrLabel: tag,
4772
kSecAttrApplicationTag: atag]
48-
73+
4974
var parameters: [CFString: Any] = [
5075
kSecAttrKeyType: kSecAttrKeyTypeRSA,
5176
kSecAttrKeySizeInBits: keySize,
@@ -57,13 +82,14 @@ public class ZitiKeychain : NSObject {
5782
if #available(iOS 13.0, OSX 10.15, *) {
5883
parameters[kSecUseDataProtectionKeychain] = true
5984
}
60-
85+
6186
var error: Unmanaged<CFError>?
6287
guard let privateKey = SecKeyCreateRandomKey(parameters as CFDictionary, &error) else {
6388
log.error("Unable to create private key for \(tag): \(error!.takeRetainedValue() as Error)")
6489
return nil
6590
}
6691
return privateKey
92+
#endif
6793
}
6894

6995
func getPrivateKey() -> SecKey? {

0 commit comments

Comments
 (0)