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

36 implementer le certificate pinning #61

Merged
merged 7 commits into from
Jan 30, 2025
32 changes: 32 additions & 0 deletions ElementX/Sources/Other/Extensions/ClientBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
39 changes: 39 additions & 0 deletions ElementX/Sources/Other/InfoPlistReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(forKey key: String) -> T {
Expand Down
5 changes: 3 additions & 2 deletions TchapX-NSE/development/SupportingFiles/target.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
- path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift
- path: ../../../TchapX/main/Sources/Other/TchapFeatureFlag.swift
5 changes: 3 additions & 2 deletions TchapX-NSE/production/SupportingFiles/target.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
- path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift
- path: ../../../TchapX/main/Sources/Other/TchapFeatureFlag.swift
5 changes: 3 additions & 2 deletions TchapX-NSE/staging/SupportingFiles/target.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
- path: ../../../TchapX/main/Sources/Generated/TchapStrings.swift
- path: ../../../TchapX/main/Sources/Other/TchapFeatureFlag.swift
16 changes: 16 additions & 0 deletions TchapX/SupportingFiles/target-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 5 additions & 4 deletions TchapX/main/Sources/Other/TchapFeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down
22 changes: 22 additions & 0 deletions TchapX/production/SupportingFiles/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@
<false/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>tchap.gouv.fr</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>Vvyvg4+bOEbI6aO7K28ioVsUfckLqCSKqlIqZTEB/uE=</string>
</dict>
<dict>
<key>PEM</key>
<string>-----BEGIN CERTIFICATE-----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-----END CERTIFICATE-----</string>
</dict>
</array>
</dict>
</dict>
</dict>
<key>NSCameraUsageDescription</key>
<string>To take pictures or videos and send them as a message $(APP_DISPLAY_NAME) needs access to the camera.</string>
<key>NSFaceIDUsageDescription</key>
Expand Down
2 changes: 1 addition & 1 deletion project-tchap-x.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ options:

settings:
DEVELOPMENT_TEAM: NVMQD635C6
MARKETING_VERSION: 0.2.0
MARKETING_VERSION: 0.3.0
CURRENT_PROJECT_VERSION: 1

include:
Expand Down
Loading