diff --git a/ElementX/Sources/Other/Extensions/ClientBuilder.swift b/ElementX/Sources/Other/Extensions/ClientBuilder.swift index 6ca8cb335a..f5f7d3016f 100644 --- a/ElementX/Sources/Other/Extensions/ClientBuilder.swift +++ b/ElementX/Sources/Other/Extensions/ClientBuilder.swift @@ -51,6 +51,38 @@ extension ClientBuilder { builder = builder.proxy(url: httpProxy) } + // Tchap: check certificate pinning if activated. + if TchapFeatureFlag.Configuration.certificatePinning.isActivated(for: .all) { + let pemCertificates = InfoPlistReader.app.embeddedPemCertificates + if pemCertificates.count > 0 { + // `addRootCertificates(certificates: [Data])` awaits a list of Data type values containing Certificates in DER or PEM format. + // Actually, Certificates in PEM format don't work in ElementX implementation (it works in Rust direct test). + // But it works with Certificates in DER format. + // As DER format is not practical to store in info.plist, we store the certificates in PEM format in info.plist, + // and convert it in DER format in Swift to take the functional path of DER into Rust. + + // Try to convert String based PEM to DER Data and check if no Certificate conversion failed. + // This step require the removal of header and footer and any newline. + + let derCertificates = pemCertificates.compactMap { + Data(base64Encoded: + $0.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "") + .replacingOccurrences(of: "-----END CERTIFICATE-----", with: "") + .replacingOccurrences(of: "\n", with: "")) + } + + // If necessary, to get the real certificate format: + // let certificate = SecCertificateCreateWithData(nil, certDataDER as CFData) + // Then, if necessary to get the public key: + // let publicKey = SecCertificateCopyKey(certificate) + + // If any failure occured, ignore ALL certificates. + if derCertificates.count == pemCertificates.count { + builder = builder.disableBuiltInRootCertificates() + .addRootCertificates(certificates: derCertificates) + } + } + } return appHooks.clientBuilderHook.configure(builder) } } diff --git a/ElementX/Sources/Other/InfoPlistReader.swift b/ElementX/Sources/Other/InfoPlistReader.swift index ce8e529db2..c2227cbc7d 100644 --- a/ElementX/Sources/Other/InfoPlistReader.swift +++ b/ElementX/Sources/Other/InfoPlistReader.swift @@ -116,6 +116,45 @@ struct InfoPlistReader { return utType } + // Tchap: add `pinnedCertificates` property + + // MARK: - Tchap Certificate Pinning + + private func recursiveSearchCertificates(for searchedKey: String, into: [String: Any]) -> [String] { + var result = [String]() + into.forEach { key, value in + if key == searchedKey, + let stringValue = value as? String { + result.append(stringValue) + } else if let subDict = value as? [String: Any] { + result.append(contentsOf: recursiveSearchCertificates(for: searchedKey, into: subDict)) + } else if let subArray = value as? [[String: Any]] { + subArray.forEach { + result.append(contentsOf: recursiveSearchCertificates(for: searchedKey, into: $0)) + } + } + } + return result + } + + var embeddedTransportSecurity: Any? { + bundle.object(forInfoDictionaryKey: "NSAppTransportSecurity") + } + + var embeddedSpkiCertificates: [String] { + guard let transportSecurityData = embeddedTransportSecurity as? [String: Any] else { + return [] + } + return recursiveSearchCertificates(for: "SPKI-SHA256-BASE64", into: transportSecurityData) + } + + var embeddedPemCertificates: [String] { + guard let transportSecurityData = embeddedTransportSecurity as? [String: Any] else { + return [] + } + return recursiveSearchCertificates(for: "PEM", into: transportSecurityData) + } + // MARK: - Private private func infoPlistValue(forKey key: String) -> T { diff --git a/TchapX-NSE/development/SupportingFiles/target.yml b/TchapX-NSE/development/SupportingFiles/target.yml index 03c6299214..d90cad9230 100644 --- a/TchapX-NSE/development/SupportingFiles/target.yml +++ b/TchapX-NSE/development/SupportingFiles/target.yml @@ -72,7 +72,7 @@ targets: CODE_SIGN_ENTITLEMENTS: TchapX-NSE/development/SupportingFiles/NSE.entitlements SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h OTHER_SWIFT_FLAGS: - - "-DIS_NSE -DIS_ENVIRONMENT_DEVELOPMENT" + - "-DIS_NSE -DIS_TCHAP_DEVELOPMENT" sources: - path: ../../../NSE/Sources @@ -120,4 +120,5 @@ targets: - path: ../../../ElementX/Sources/AppHooks/AppHooks.swift - path: ../../../ElementX/Sources/AppHooks/Hooks/ClientBuilderHook.swift - path: ../../../TchapX/main/Sources/Generated/TchapAssets.swift - - path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift \ No newline at end of file + - path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift + - path: ../../../TchapX/main/Sources/Other/TchapFeatureFlag.swift \ No newline at end of file diff --git a/TchapX-NSE/production/SupportingFiles/target.yml b/TchapX-NSE/production/SupportingFiles/target.yml index 8453e85fae..81c4a5cb53 100644 --- a/TchapX-NSE/production/SupportingFiles/target.yml +++ b/TchapX-NSE/production/SupportingFiles/target.yml @@ -72,7 +72,7 @@ targets: CODE_SIGN_ENTITLEMENTS: TchapX-NSE/production/SupportingFiles/NSE.entitlements SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h OTHER_SWIFT_FLAGS: - - "-DIS_NSE -DIS_ENVIRONMENT_PRODUCTION" + - "-DIS_NSE -DIS_TCHAP_PRODUCTION" sources: - path: ../../../NSE/Sources @@ -120,4 +120,5 @@ targets: - path: ../../../ElementX/Sources/AppHooks/AppHooks.swift - path: ../../../ElementX/Sources/AppHooks/Hooks/ClientBuilderHook.swift - path: ../../../TchapX/main/Sources/Generated/TchapAssets.swift - - path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift \ No newline at end of file + - path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift + - path: ../../../TchapX/main/Sources/Other/TchapFeatureFlag.swift \ No newline at end of file diff --git a/TchapX-NSE/staging/SupportingFiles/target.yml b/TchapX-NSE/staging/SupportingFiles/target.yml index 0cff2de792..86a61b8de6 100644 --- a/TchapX-NSE/staging/SupportingFiles/target.yml +++ b/TchapX-NSE/staging/SupportingFiles/target.yml @@ -72,7 +72,7 @@ targets: CODE_SIGN_ENTITLEMENTS: TchapX-NSE/staging/SupportingFiles/NSE.entitlements SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h OTHER_SWIFT_FLAGS: - - "-DIS_NSE -DIS_ENVIRONMENT_STAGING" + - "-DIS_NSE -DIS_TCHAP_STAGING" sources: - path: ../../../NSE/Sources @@ -120,4 +120,5 @@ targets: - path: ../../../ElementX/Sources/AppHooks/AppHooks.swift - path: ../../../ElementX/Sources/AppHooks/Hooks/ClientBuilderHook.swift - path: ../../../TchapX/main/Sources/Generated/TchapAssets.swift - - path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift \ No newline at end of file + - path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift + - path: ../../../TchapX/main/Sources/Other/TchapFeatureFlag.swift \ No newline at end of file diff --git a/TchapX/SupportingFiles/target-production.yml b/TchapX/SupportingFiles/target-production.yml index 31dc264d2f..039468aff6 100644 --- a/TchapX/SupportingFiles/target-production.yml +++ b/TchapX/SupportingFiles/target-production.yml @@ -110,6 +110,22 @@ targets: LSItemContentTypes: $(PILLS_UT_TYPE_IDENTIFIER) LSSupportsOpeningDocumentsInPlace: false mapLibreAPIKey: $(MAPLIBRE_API_KEY) + + # Data generated from Tchap Android `certignaservicesrootca.cer` embedded certificate. + # Certificate of a website can be obtained as follow: ex +'/BEGIN CERTIFICATE/,/END CERTIFICATE/p' <(echo | openssl s_client -showcerts -connect tchap.gouv.fr:443) -scq > tchap.gouv.fr.crt + # Generate PEM: openssl x509 -inform der -in certignaservicesrootca.cer -out certignaservicesrootca.pem + # Generate SPKI: cat certignaservicesrootca.pem | openssl x509 -inform pem -noout -outform pem -pubkey | openssl pkey -pubin -inform pem -outform der | openssl dgst -sha256 -binary | openssl enc -base64 + NSAppTransportSecurity: { + NSPinnedDomains: { + "tchap.gouv.fr": { + NSIncludesSubdomains: true, + NSPinnedCAIdentities: [ + "SPKI-SHA256-BASE64": "Vvyvg4+bOEbI6aO7K28ioVsUfckLqCSKqlIqZTEB/uE=", + "PEM": "-----BEGIN CERTIFICATE-----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-----END CERTIFICATE-----" + ] + } + } + } settings: base: diff --git a/TchapX/main/Sources/Other/TchapFeatureFlag.swift b/TchapX/main/Sources/Other/TchapFeatureFlag.swift index ffbbb23c00..31ccfc56de 100644 --- a/TchapX/main/Sources/Other/TchapFeatureFlag.swift +++ b/TchapX/main/Sources/Other/TchapFeatureFlag.swift @@ -35,7 +35,7 @@ import Foundation struct TchapFeatureFlag { let allowedInstances: [Instance] - func isActivated(for homeServer: String) -> Bool { + func isActivated(for homeServer: TchapFeatureFlag.Instance) -> Bool { // Return false if no instance suppports the feature. if allowedInstances.isEmpty { return false @@ -50,7 +50,7 @@ struct TchapFeatureFlag { // Example of homeServer for authenticiation: `matrix.agent.dinum.tchap.gouv.fr` // We must remove the `matrix.` prefix. let homeServerPrefixes = ["matrix.", "https://matrix."] - var sanitizedHomeServer = Substring(homeServer) + var sanitizedHomeServer = Substring(homeServer.rawValue) homeServerPrefixes.forEach { if sanitizedHomeServer.hasPrefix($0) { sanitizedHomeServer = sanitizedHomeServer.dropFirst($0.count) @@ -96,13 +96,14 @@ extension TchapFeatureFlag { extension TchapFeatureFlag { enum Configuration { // Use empty Enum rather than empty Struct. (Linter advice) #if IS_TCHAP_PRODUCTION - static let certificatePinning = TchapFeatureFlag(allowedInstances: [.agriculture, .agent]) + // certificatePinning can only be activated for .all or none because it is used before any activae session. + static let certificatePinning = TchapFeatureFlag(allowedInstances: [.all]) static let proConnectAuthentication = TchapFeatureFlag(allowedInstances: []) #elseif IS_TCHAP_STAGING static let certificatePinning = TchapFeatureFlag(allowedInstances: [.agriculture, .agent]) static let proConnectAuthentication = TchapFeatureFlag(allowedInstances: []) #elseif IS_TCHAP_DEVELOPMENT - static let certificatePinning = TchapFeatureFlag(allowedInstances: [.agriculture, .agent]) + static let certificatePinning = TchapFeatureFlag(allowedInstances: []) static let proConnectAuthentication = TchapFeatureFlag(allowedInstances: []) #endif } diff --git a/TchapX/production/SupportingFiles/Info.plist b/TchapX/production/SupportingFiles/Info.plist index fb51c555a9..bd2cf669af 100644 --- a/TchapX/production/SupportingFiles/Info.plist +++ b/TchapX/production/SupportingFiles/Info.plist @@ -64,6 +64,28 @@ LSSupportsOpeningDocumentsInPlace + NSAppTransportSecurity + + NSPinnedDomains + + tchap.gouv.fr + + NSIncludesSubdomains + + NSPinnedCAIdentities + + + SPKI-SHA256-BASE64 + Vvyvg4+bOEbI6aO7K28ioVsUfckLqCSKqlIqZTEB/uE= + + + PEM + -----BEGIN CERTIFICATE-----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-----END CERTIFICATE----- + + + + + NSCameraUsageDescription To take pictures or videos and send them as a message $(APP_DISPLAY_NAME) needs access to the camera. NSFaceIDUsageDescription diff --git a/project-tchap-x.yml b/project-tchap-x.yml index 9706a53d5d..fdef8311fe 100644 --- a/project-tchap-x.yml +++ b/project-tchap-x.yml @@ -24,7 +24,7 @@ options: settings: DEVELOPMENT_TEAM: NVMQD635C6 - MARKETING_VERSION: 0.2.0 + MARKETING_VERSION: 0.3.0 CURRENT_PROJECT_VERSION: 1 include: