Skip to content

Commit 17f0a66

Browse files
authored
Merge pull request #266 from openziti/use.all.certs.from.enrollment.resp
Use all certificates from enrollment response
2 parents 743e9ee + 77a7f00 commit 17f0a66

File tree

3 files changed

+99
-15
lines changed

3 files changed

+99
-15
lines changed

lib/Ziti.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,8 @@ import CZitiPrivate
332332
// Store certificate
333333
let cert = dropFirst("pem:", resp.id.cert)
334334
_ = zkc.deleteCertificate(silent: true)
335-
guard zkc.storeCertificate(fromPem: cert) == nil else {
335+
let (err, cns) = zkc.storeCertificates(cert)
336+
guard err == nil else {
336337
let errStr = "Unable to store certificate\n"
337338
log.error(errStr, function:"enroll()")
338339
enrollCallback(nil, ZitiError(errStr))
@@ -345,8 +346,8 @@ import CZitiPrivate
345346
ca = dropFirst("pem:", idCa)
346347
}
347348

348-
let zid = ZitiIdentity(id: subj, ztAPIs: resp.ztAPIs, ca: ca)
349-
log.info("Enrolled id:\(subj) with controller: \(zid.ztAPI)", function:"enroll()")
349+
let zid = ZitiIdentity(id: subj, ztAPIs: resp.ztAPIs, certCNs: cns, ca: ca)
350+
log.info("Enrolled id:\(subj) with controller: \(zid.ztAPI) with cns: \(zid.getCertCNs())", function:"enroll()")
350351

351352
enrollCallback(zid, nil)
352353
}
@@ -383,14 +384,14 @@ import CZitiPrivate
383384
@objc public func run(_ postureChecks:ZitiPostureChecks?, _ initCallback: @escaping InitCallback) {
384385
// Get certificate
385386
let zkc = ZitiKeychain(tag: id.id)
386-
let (maybeCert, zErr) = zkc.getCertificate()
387-
guard let cert = maybeCert, zErr == nil else {
388-
let errStr = zErr != nil ? zErr!.localizedDescription : "unable to retrieve certificate from keychain"
387+
let (maybeCerts, zErr) = zkc.getCertificates(id.getCertCNs())
388+
guard let certs = maybeCerts, zErr == nil else {
389+
let errStr = zErr != nil ? zErr!.localizedDescription : "unable to retrieve certificates from keychain"
389390
log.error(errStr)
390391
initCallback(zErr ?? ZitiError(errStr))
391392
return
392393
}
393-
let certPEM = zkc.convertToPEM("CERTIFICATE", der: cert)
394+
let certPEM = zkc.convertToPEM("CERTIFICATE", ders: certs)
394395

395396
// Get private key
396397
guard let privKey = zkc.getPrivateKey() else {

lib/ZitiIdentity.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ import Foundation
3434
/// Initially the `sub` field from the one-time enrollment JWT. Used by `Ziti` to store and retrieve identity-related items in the Keychain
3535
@objc public let id:String
3636

37+
/// Certificate CNs
38+
///
39+
/// Common names for the certificates in this identities certificate chain. The first element will always match `id`.
40+
/// Used by `Ziti` to retrieve certificates from the Keychain.
41+
@objc public var certCNs:[String]?
42+
3743
/// scheme, host, and port used to communicate with Ziti controller
3844
@objc public var ztAPI:String
3945

@@ -55,17 +61,29 @@ import Foundation
5561
///
5662
/// - Parameters:
5763
/// - id: unique identifier of this identity
58-
/// - ztAPI: URL for accessing Ziti controller API
64+
/// - ztAPIs: URLs for accessing Ziti controller API
65+
/// - certCNs: common names of certififcates
5966
/// - name: name currently configured for this identity
6067
/// - ca: CA pool that can be used to verify trust of the Ziti controller
61-
@objc public init(id:String, ztAPIs:[String], name:String?=nil, ca:String?=nil) {
68+
@objc public init(id:String, ztAPIs:[String], certCNs:[String]?=nil, name:String?=nil, ca:String?=nil) {
6269
self.id = id
70+
self.certCNs = [ id ]
71+
if let certCNs {
72+
for cn in certCNs {
73+
if cn == id { continue }
74+
self.certCNs?.append(cn)
75+
}
76+
}
6377
self.ztAPI = ztAPIs.first ?? ""
6478
self.ztAPIs = ztAPIs
6579
self.name = name
6680
self.ca = ca
6781
}
6882

83+
@objc public func getCertCNs() -> [String] {
84+
return certCNs ?? [self.id]
85+
}
86+
6987
/// Save this object to a JSON file
7088
///
7189
/// This file can be used to initialize a `Ziti` object.

lib/ZitiKeychain.swift

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,23 +178,60 @@ public class ZitiKeychain : NSObject {
178178
///
179179
/// - Returns: `true` if the certificates are successfully added to the keychain, otherwise `false`
180180
public func addCaPool(_ caPool:String) -> Bool {
181-
let certs = extractCerts(caPool)
181+
let (err, _) = storeCertificates(caPool, setAttrLabel: false)
182+
return err == nil
183+
}
184+
185+
/// Add the certificates in the provided pem string to the keychain
186+
///
187+
/// - Parameters
188+
/// - pem: PEM formatted certificate (or certificate chain)
189+
///
190+
/// - Returns
191+
/// - An error, if one occurred.
192+
/// - An array of strings that contains the common name for each certificate in the pem.
193+
public func storeCertificates(_ pem:String) -> (ZitiError?, [String]?) {
194+
return storeCertificates(pem, setAttrLabel: true)
195+
}
196+
197+
private func storeCertificates(_ pem:String, setAttrLabel:Bool = true) -> (ZitiError?, [String]?) {
198+
var cns:[String] = []
199+
let certs = extractCerts(pem)
182200
for cert in certs {
201+
var maybeCN: CFString?
202+
var status = SecCertificateCopyCommonName(cert, &maybeCN)
203+
guard let cn = maybeCN, status == errSecSuccess else {
204+
let errStr = SecCopyErrorMessageString(status, nil) as String? ?? "\(status)"
205+
log.error("Unable to retrieve certificate common name for \(tag): \(errStr)")
206+
return (ZitiError("Unable to get certificate common name for \(tag): \(errStr)", errorCode: Int(status)), nil)
207+
}
208+
let cnStr = String(describing: cn)
183209
var parameters: [CFString: Any] = [
184210
kSecClass: kSecClassCertificate,
185211
kSecValueRef: cert]
212+
if (setAttrLabel) {
213+
// Setting kSecAttrLabel for a certificate is not effective, at least not on macOS. The cert is always stored with its
214+
// common name as the label. This is true both for the file-based keychain (which can be verified with Keychain Access)
215+
// and data protection (cloud) keychain (which cannot be inspected, but verified by adding/getting).
216+
//
217+
// Work around this by explicitly using the cn as the label, so the label is predictable on macOS and iOS, and hopefully
218+
// on future macOS versions when/if `kSecAttrLabel` is respected. CNs
219+
// to the caller so the app knows which certs to look for when it needs them.
220+
parameters[kSecAttrLabel] = cnStr
221+
}
186222
if #available(iOS 13.0, OSX 10.15, *) {
187223
parameters[kSecUseDataProtectionKeychain] = true
188224
}
189-
let status = SecItemAdd(parameters as CFDictionary, nil)
225+
status = SecItemAdd(parameters as CFDictionary, nil)
190226
guard status == errSecSuccess || status == errSecDuplicateItem else {
191227
let errStr = SecCopyErrorMessageString(status, nil) as String? ?? "\(status)"
192-
log.error("Unable to store certificate for \(tag): \(errStr)")
193-
return false
228+
log.error("Unable to store certificate for \(cnStr): \(errStr)")
229+
return (ZitiError("Unable to store certificate for \(cnStr): \(errStr)", errorCode: Int(status)), nil)
194230
}
195231
log.info("Added cert to keychain: \(String(describing: SecCertificateCopySubjectSummary(cert)))")
232+
cns.append(cnStr)
196233
}
197-
return true
234+
return (nil, cns)
198235
}
199236

200237
#if os(macOS) // if #available(iOS 13.0, OSX 10.15, *)
@@ -221,6 +258,7 @@ public class ZitiKeychain : NSObject {
221258
}
222259
#endif
223260

261+
@available(*, deprecated, message: "This function only stores the first certificate in `pem`. Use storeCertificates(pem:String) to store all certificates.")
224262
func storeCertificate(fromPem pem:String) -> ZitiError? {
225263
let (_, zErr) = storeCertificate(fromDer: convertToDER(pem))
226264
if zErr != nil {
@@ -229,6 +267,7 @@ public class ZitiKeychain : NSObject {
229267
return zErr
230268
}
231269

270+
@available(*, deprecated)
232271
func storeCertificate(fromDer der:Data) -> (SecCertificate?, ZitiError?) {
233272
guard let certificate = SecCertificateCreateWithData(nil, der as CFData) else {
234273
let errStr = "Unable to create certificate from data for \(tag)"
@@ -251,11 +290,16 @@ public class ZitiKeychain : NSObject {
251290
return (certificate, nil)
252291
}
253292

293+
@available(*, deprecated, message: "This function only gets one certificate, but an identity may have more than one. Use getCertificates( certCNs:[String]) instead")
254294
func getCertificate() -> (Data?, ZitiError?) {
295+
return getCertificate(tag)
296+
}
297+
298+
private func getCertificate(_ label:String) -> (Data?, ZitiError?) {
255299
let params: [CFString: Any] = [
256300
kSecClass: kSecClassCertificate,
257301
kSecReturnRef: kCFBooleanTrue!,
258-
kSecAttrLabel: tag]
302+
kSecAttrLabel: label]
259303

260304
var cert: CFTypeRef?
261305
let status = SecItemCopyMatching(params as CFDictionary, &cert)
@@ -272,6 +316,19 @@ public class ZitiKeychain : NSObject {
272316
return (certData, nil)
273317
}
274318

319+
func getCertificates(_ certCNs:[String]) -> ([Data]?, ZitiError?) {
320+
var certs:[Data] = []
321+
for cn in certCNs {
322+
let (cert, err) = getCertificate(cn)
323+
guard let cert = cert, err == nil else {
324+
return (nil, err)
325+
}
326+
certs.append(cert)
327+
}
328+
329+
return (certs, nil)
330+
}
331+
275332
func deleteCertificate(silent:Bool=false) -> ZitiError? {
276333
var params: [CFString: Any] = [
277334
kSecClass: kSecClassCertificate,
@@ -305,6 +362,14 @@ public class ZitiKeychain : NSObject {
305362
return nil
306363
}
307364

365+
func convertToPEM(_ type:String, ders:[Data]) -> String {
366+
var pem = ""
367+
ders.forEach { der in
368+
pem.append(convertToPEM("CERTIFICATE", der: der))
369+
}
370+
return pem
371+
}
372+
308373
func convertToPEM(_ type:String, der:Data) -> String {
309374
guard let str = der.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
310375
return ""

0 commit comments

Comments
 (0)