Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SDKS-8420: Certificate pinning feature #551

Merged
merged 45 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a0cf377
Release 2.25.1
javrudsky Jun 3, 2024
4abd25c
Updated Changelog for release 2.25.1
javrudsky Jun 3, 2024
d5bad83
Merge pull request #542 from splitio/release_2.25.1
javrudsky Jun 3, 2024
4b34bc9
Added pin validator
javrudsky Jul 1, 2024
6447ef5
Improving pin validation function and tests
javrudsky Jul 2, 2024
c0c2ea8
Added tests and cleaned code a bit
javrudsky Jul 3, 2024
1205201
Adding certificate pinning config and tests
javrudsky Jul 5, 2024
10f4083
Merge pull request #543 from splitio/SDKS-8507_pin_validator
javrudsky Jul 5, 2024
43377de
Improved documentation for the component
javrudsky Jul 5, 2024
6f52871
Merge pull request #544 from splitio/SDKS-8508_pin_config
javrudsky Jul 5, 2024
105484d
Add Certificate pinning integration to config
javrudsky Jul 5, 2024
11cb718
Adding authenticator based on pinned credentials
javrudsky Jul 5, 2024
aefef43
Fixing linter warning
javrudsky Jul 5, 2024
9c35a0b
Merge pull request #545 from splitio/SDKS-8509_cert_pinning_sdk_integ…
javrudsky Jul 10, 2024
23c767f
Wiring pinned certificate validation with http component layer
javrudsky Jul 10, 2024
e35cacd
Improved pin checker integration. Avoiding retries for Splits when va…
javrudsky Jul 12, 2024
7048b85
Merge pull request #546 from splitio/SDKS-8509_cert_pinning_sdk_integ…
javrudsky Jul 12, 2024
1c86d44
Merge pull request #547 from splitio/SDKS-8509_cert_pinning_sdk_integ…
javrudsky Jul 12, 2024
b16f953
Changing to a more direct approach to avoid hitting banned hosts
javrudsky Jul 12, 2024
127fb61
Terminating services when a host is invalidated by pinned credential …
javrudsky Jul 15, 2024
d1ad63d
Switching to polling when auth or streaming pinned credential fails
javrudsky Jul 15, 2024
d00d57a
Taking into account that Unique Keys are sent to telemetry endpoint
javrudsky Jul 15, 2024
1c172a6
Merge pull request #548 from splitio/SDKS-8509_cert_pinning_sdk_integ…
javrudsky Jul 15, 2024
5bc924d
Removing unused tearDown
javrudsky Jul 15, 2024
b27bde6
Updated tests in order to compile with new protocols
javrudsky Jul 16, 2024
9f3db8b
Added tests for request manager pinned credential validation
javrudsky Jul 16, 2024
c661d0a
Adding tests to sync manager for the case that a pinned enpdoint vali…
javrudsky Jul 17, 2024
7a5f8aa
Add synchronizer tests for certificate pinning
javrudsky Jul 17, 2024
834551b
Merge pull request #549 from splitio/SDKS-8509_cert_pinning_sdk_integ…
javrudsky Jul 17, 2024
2412131
Pinning validation final setup
javrudsky Jul 17, 2024
2320646
Release 2.26.0-rc1
javrudsky Jul 17, 2024
4f1c005
Adding some missing OS checks
javrudsky Jul 17, 2024
1276949
Release 2.26.0-rc3
javrudsky Jul 17, 2024
22205b7
Adding security framework to project
javrudsky Jul 19, 2024
cce655d
Release 2.26.0-rc4
javrudsky Jul 19, 2024
1d026c5
Fixing some tests and improve some threading code
javrudsky Jul 19, 2024
7bc9ae2
Release 2.26.0-rc5
javrudsky Jul 19, 2024
bd94a2d
Fixing notification helper parameter
javrudsky Jul 19, 2024
9916799
Release 2.26.0-rc6 - Same that r5 but for Cocoapods setup
javrudsky Jul 19, 2024
664b328
Merge pull request #550 from splitio/SDKS-8509_cert_pinning_sdk_integ…
javrudsky Jul 23, 2024
6407cde
watchos target files
javrudsky Jul 24, 2024
8471a30
Merge branch 'release_2.26.0-rc6' into SDKS-8420_cert_pinning_baseline
javrudsky Jul 24, 2024
86cc24d
Removing unused component
javrudsky Jul 24, 2024
d471687
Fixing failing tests
javrudsky Jul 24, 2024
cfc6e27
Rmoving watchos tests
javrudsky Jul 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
2.25.1 (Jun 3, 2024)
- Updated impressions deduplication to apply across sessions.

2.25.0: (May 21, 2024)
- Added support for targeting rules based on semantic versions (https://semver.org/).
- Added special impression label "targeting rule type unsupported by sdk" when the matcher type is not supported by the SDK, which returns 'control' treatment.
Expand Down
2 changes: 1 addition & 1 deletion Split.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|
s.name = 'Split'
s.module_name = 'Split'
s.version = '2.25.0'
s.version = '2.26.0-rc6'
s.summary = 'iOS SDK for Split'
s.description = <<-DESC
This SDK is designed to work with Split, the platform for controlled rollouts, serving features to your users via the Split feature flag to manage your complete customer experience.
Expand Down
226 changes: 214 additions & 12 deletions Split.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

165 changes: 165 additions & 0 deletions Split/Api/CertificatePinningConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//
// CertificatePinningConfig.swift
// Split
//
// Created by Javier L. Avrudsky on 04/08/2020.
// Copyright © 2020 Split. All rights reserved.
//

import Foundation

/// Custom error type for certificate pinning errors, conforming to LocalizedError.
@objc
public class CertificatePinningError: NSObject, LocalizedError {
private let message: String

/// Initializes a new instance of CertificatePinningError with a custom message.
/// - Parameter message: The error message.
init(message: String) {
self.message = message
}

/// Provides a localized description of the error.
public var errorDescription: String? {
return message
}
}

/// Configuration class for Certificate Pinning.
@objc public class CertificatePinningConfig: NSObject {
private(set) var pins: [CredentialPin]

/// Initializes a new instance of CertificatePinningConfig with an array of pins.
/// - Parameter pins: Array of CredentialPin objects.
init(pins: [CredentialPin]) {
self.pins = pins
}

/// Provides a builder for CertificatePinningConfig.
@objc(builder)
public static func builder() -> Builder {
return Builder()
}

/// Builder class for constructing a CertificatePinningConfig.
@objc(CertificatePinningConfigBuilder)
public class Builder: NSObject {

private enum PinType {
case key
case certificate
}

private struct Pin {
let host: String
let data: String
let type: PinType
}

private let splitValidator = SplitNameValidator()
private var builderPins = [Pin]()

// Visible for testing variable
var bundle: Bundle = Bundle.main

/// Builds and returns a CertificatePinningConfig with the added pins.
/// - Throws: CertificatePinningError if any pin cannot be parsed.
/// - Returns: A configured CertificatePinningConfig instance.
@objc
public func build() throws -> CertificatePinningConfig {
var pins = [CredentialPin]()
for pin in builderPins {
let credential = (pin.type == .certificate ? try parseCertificate(pin: pin) : try parseHash(pin: pin))
pins.append(credential)
}
return CertificatePinningConfig(pins: pins)
}

/// Adds a certificate pin for the specified host.
/// The certificate file must be in DER format.
/// - Parameters:
/// - host: The host for which to add the pin.
/// - certificateName: The name of the certificate file (without the `.der` extension) located in the app bundle.
/// The method will automatically look for a `.der` file with this name in the main bundle.
/// Ensure the certificate file is correctly included in your app's bundle resources.
/// - Returns: The Builder instance for chaining.
@discardableResult
@objc(addPinForHost:certificateName:)
public func addPin(host: String, certificateName: String) -> CertificatePinningConfig.Builder {
builderPins.append(Pin(host: host, data: certificateName, type: .certificate))
return self
}

/// Adds a key hash pin for the specified host.
/// - Parameters:
/// - host: The host for which to add the pin.
/// - keyHash: A string representing the base64-encoded key hash along with its hashing algorithm.
/// The format must follow the pattern `"algorithm/hash"`, where:
/// - `algorithm` is the name of the hashing algorithm used (e.g., `sha256`, `sha1`).
/// - `hash` is the base64-encoded hash value generated by applying the specified algorithm to the key.
///
/// Example Format:
/// - `"sha256/g8gd29aGVsbybEP="`
///
/// Detailed Example:
/// - If you have a key hash generated using the SHA-256 algorithm, it should look something like:
/// `"sha256/aGVsbG8gd29ybGQ="`, where `"aGVsbG8gd29ybGQ="` is the base64-encoded result of the hash.
/// - Returns: The Builder instance for chaining.
@discardableResult
@objc(addPinForHost:hash:)
public func addPin(host: String, keyHash: String) -> CertificatePinningConfig.Builder {
builderPins.append(Pin(host: host, data: keyHash, type: .key))
return self
}

/// Parses a certificate pin into a CredentialPin.
/// - Parameter pin: The pin to parse.
/// - Throws: CertificatePinningError if the certificate cannot be parsed.
/// - Returns: A CredentialPin object.
private func parseCertificate(pin: Pin) throws -> CredentialPin {
// Add pin from certificate
// It is important to take into account that this method could delay a bit the init process
// TODO: Measure time
guard let spki = TlsCertificateParser.spki(from: pin.data, bundle: bundle) else {
throw errLog("Couldn't get SPKI from \(pin.data).der")
}

return CredentialPin(host: pin.host,
hash: AlgoHelper.computeHash(spki.data, algo: .sha256),
algo: .sha256)
}

/// Parses a key hash pin into a CredentialPin.
/// - Parameter pin: The pin to parse.
/// - Throws: CertificatePinningError if the key hash is invalid.
/// - Returns: A CredentialPin object.
private func parseHash(pin: Pin) throws -> CredentialPin {
let hash = pin.data
guard let separatorIndex = hash.firstIndex(of: "/") else {
throw errLog("Unable to add pin for host \(pin.host), invalid key hash")
}

let algoName = String(hash[hash.startIndex..<separatorIndex])
guard let algo = KeyHashAlgo(rawValue: algoName) else {
throw errLog("Key hash algorithm not supported for pin: \(algoName)")
}

let keyHash = String(hash[hash.index(after: separatorIndex)..<hash.endIndex])
guard let dataHash = Data(base64Encoded: keyHash) else {
throw errLog("Key hash not valid for host \(pin.host) and algorithm: \(algoName)")
}
if dataHash.isEmpty {
throw errLog("Key hash is empty for host \(pin.host) algorithm: \(algoName)")
}
return CredentialPin(host: pin.host, hash: dataHash, algo: algo)
}

/// Logs an error and returns a CertificatePinningError.
/// - Parameter message: The error message.
/// - Returns: A CertificatePinningError object.
private func errLog(_ message: String) -> CertificatePinningError {
Logger.e(message)
return CertificatePinningError(message: message)
}
}
}
3 changes: 3 additions & 0 deletions Split/Api/DefaultSplitFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public class DefaultSplitFactory: NSObject, SplitFactory {
userKey: params.key.matchingKey)

HttpSessionConfig.default.httpsAuthenticator = params.config.httpsAuthenticator
if let pinningConfig = params.config.certificatePinningConfig {
HttpSessionConfig.default.pinChecker = DefaultTlsPinChecker(pins: pinningConfig.pins)
}

// Creating Events Manager first speeds up init process
let eventsManager = components.getSplitEventsManagerCoordinator()
Expand Down
2 changes: 1 addition & 1 deletion Split/Api/LocalhostSplitFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public class LocalhostSplitFactory: NSObject, SplitFactory {

private static func splitsDataSource(config: SplitClientConfig, bundle: Bundle) -> LocalhostDataSource {
let dataFolderName = SplitDatabaseHelper.sanitizeForFolderName(config.localhostDataFolder)
let fileStorage = FileStorage(dataFolderName: dataFolderName)
let fileStorage = DefaultFileStorage(dataFolderName: dataFolderName)
var loaderConfig = FeatureFlagsFileLoaderConfig()
loaderConfig.refreshInterval = config.offlineRefreshRate

Expand Down
17 changes: 17 additions & 0 deletions Split/Api/SplitCertPinningAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// SplitCertPinningAuthenticator.swift
// Split
//
// Created by Javier Avrudsky on 05/07/2024.
// Copyright © 2024 Split. All rights reserved.
//

import Foundation

typealias AuthCompletion = (URLSession.AuthChallengeDisposition, URLCredential?) -> Void

class SplitCertPinningAuthenticator {



}
26 changes: 26 additions & 0 deletions Split/Api/SplitClientConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,32 @@ public class SplitClientConfig: NSObject {
/// This is useful when using two factories with the same SDK Key to avoid having issues with the shared data
@objc public var prefix: String?


/// The `CertificatePinningConfig` class is used to configure certificate pinning for a given set of hosts.
/// It holds an array of credentials, each of which represents a pin for a specific host,
/// either in the form of a certificate name or a base64-encoded key hash.
/// This configuration ensures that only trusted certificates are used for secure communication.
///
/// ### Usage Example:
/// ```swift
/// // Create a new builder instance
/// let builder = CertificatePinningConfig.builder()
///
/// // Add certificate pin for a host
/// builder.addPin(host: "example.com", certificateName: "example_cert")
///
/// // Add key hash pin for a host
/// builder.addPin(host: "example.com", keyHash: "sha256/aGVsbG8gd29ybGQ=")
///
/// // Build the CertificatePinningConfig
/// let config = try builder.build()
/// ```
///
/// This example demonstrates how to add certificate and key hash pins for a host and build the configuration.
///
/// - Note: The certificate files must be in DER format and included in the app bundle.
@objc public var certificatePinningConfig: CertificatePinningConfig?

///
/// Update this variable to enable / disable telemetry for testing
///
Expand Down
2 changes: 1 addition & 1 deletion Split/Common/Utils/Base64Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

class Base64Utils {
static let kOffsetLenght = 4
static let kOffsetLength = 4
class func decodeBase64URL(base64: String?) -> String? {
guard let base64 = base64 else {
return nil
Expand Down
49 changes: 49 additions & 0 deletions Split/Common/Utils/FileUtil.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// FileUtil.swift
// Split
//
// Created by Javier Avrudsky on 04/07/2024.
// Copyright © 2024 Split. All rights reserved.
//

import Foundation

// FileHelper
struct FileUtil {

static func copySourceFile(name: String, type: String, fileStorage: FileStorage, bundle: Bundle) -> Bool {

guard let fileContent = loadFile(name: name, type: type, bundle: bundle) else {
return false
}
fileStorage.write(fileName: "\(name).\(type)", content: fileContent)
return true
}

static func loadFile(name fileName: String, type fileType: String, bundle: Bundle) -> String? {
var fileContent: String?
if let filepath = bundle.path(forResource: fileName, ofType: fileType) {
do {
fileContent = try String(contentsOfFile: filepath, encoding: .utf8)
} catch {
Logger.e("Could not load file: \(filepath)")
}
}
return fileContent
}

static func loadFileData(name: String, type fileType: String, bundle: Bundle)-> Data? {

guard let filepath = bundle.path(forResource: name, ofType: fileType) else {
return nil
}

let uri = URL(fileURLWithPath: filepath)
do {
return try Data(contentsOf: uri)
} catch {
print("File Read Error for file \(filepath)")
return nil
}
}
}
15 changes: 11 additions & 4 deletions Split/Common/Utils/NotificationsHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import WatchKit
import TVUIKit
#endif

typealias ObserverAction = () -> Void
typealias ObserverAction = (AnyObject?) -> Void

enum AppNotification: String {
case didEnterBackground
case didBecomeActive
case pinnedCredentialValidationFail
}

/// ** NotificationHelper **
Expand All @@ -38,16 +39,18 @@ enum AppNotification: String {
/// that way the code becomes streight and simple.

protocol NotificationHelper {

func addObserver(for notification: AppNotification, action: @escaping ObserverAction)
func removeAllObservers()
func post(notification: AppNotification, info: AnyObject?)
}

class DefaultNotificationHelper: NotificationHelper {

private let queue = DispatchQueue(label: "split-notification-helper", attributes: .concurrent)
private var actions = [String: [ObserverAction]]()

static let pinnedCredentialValidationFailed = NSNotification.Name("pinnedCredentialValidationFailed")

#if os(iOS) || os(tvOS)

#if swift(>=4.2)
Expand Down Expand Up @@ -107,14 +110,14 @@ class DefaultNotificationHelper: NotificationHelper {
NotificationCenter.default.removeObserver(self, name: Self.didBecomeActiveNotification, object: nil)
}

private func executeActions(for notification: AppNotification) {
private func executeActions(for notification: AppNotification, info: AnyObject? = nil) {
var actions: [ObserverAction]?
queue.sync {
actions = self.actions[notification.rawValue]
}
if let actions = actions {
for action in actions {
action()
action(info)
}
}
}
Expand All @@ -135,4 +138,8 @@ class DefaultNotificationHelper: NotificationHelper {
self.actions.removeAll()
}
}

func post(notification: AppNotification, info: AnyObject?) {
executeActions(for: AppNotification.pinnedCredentialValidationFail, info: info)
}
}
2 changes: 1 addition & 1 deletion Split/Common/Utils/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

class Version {
private static let kSdkPlatform: String = "ios"
private static let kVersion = "2.25.0"
private static let kVersion = "2.26.0-rc6"

static var semantic: String {
return kVersion
Expand Down
4 changes: 2 additions & 2 deletions Split/FetcherEngine/Refresh/RetryableSyncWorker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker {
reconnectBackoffCounter: reconnectBackoffCounter)
}

override func fetchFromRemote() -> Bool {
override func fetchFromRemote() throws -> Bool {
do {
let oldSegments = mySegmentsStorage.getAll()
if let segments = try self.mySegmentsFetcher.execute(userKey: self.userKey, headers: getHeaders()) {
Expand Down Expand Up @@ -190,7 +190,7 @@ class RetryableSplitsSyncWorker: BaseRetryableSyncWorker {
super.init(eventsManager: eventsManager, reconnectBackoffCounter: reconnectBackoffCounter)
}

override func fetchFromRemote() -> Bool {
override func fetchFromRemote() throws -> Bool {
var changeNumber = splitsStorage.changeNumber
var clearCache = false
if changeNumber != -1 {
Expand Down
Loading
Loading