Skip to content

Commit

Permalink
WIP: Throw specific exception for SSL Pinning failure
Browse files Browse the repository at this point in the history
  • Loading branch information
osipxd committed Oct 17, 2024
1 parent baabc7d commit cc70f73
Showing 1 changed file with 31 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
/*
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.darwin.certificates

import io.ktor.client.engine.darwin.*
import io.ktor.util.logging.*
import kotlinx.cinterop.*
import kotlinx.io.*
import platform.CoreCrypto.*
import platform.CoreFoundation.*
import platform.Foundation.*
import platform.Security.*

private val LOG = KtorSimpleLogger("io.ktor.client.engine.darwin.certificates.CertificatePinner")

/**
* Constrains which certificates are trusted. Pinning certificates defends against attacks on
* certificate authorities. It also prevents connections through man-in-the-middle certificate
Expand Down Expand Up @@ -112,36 +116,36 @@ public data class CertificatePinner(
private val validateTrust: Boolean
) : ChallengeHandler {

@OptIn(ExperimentalForeignApi::class)
override fun invoke(
session: NSURLSession,
task: NSURLSessionTask,
challenge: NSURLAuthenticationChallenge,
completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Unit
) {
if (applyPinning(challenge)) {
completionHandler(NSURLSessionAuthChallengeUseCredential, challenge.proposedCredential)
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, null)
}
}

@OptIn(ExperimentalForeignApi::class)
private fun applyPinning(challenge: NSURLAuthenticationChallenge): Boolean {
val hostname = challenge.protectionSpace.host
val matchingPins = findMatchingPins(hostname)

if (matchingPins.isEmpty()) {
println("CertificatePinner: No pins found for host")
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, null)
return
LOG.trace { "No pins found for host" }
return false
}

if (challenge.protectionSpace.authenticationMethod !=
NSURLAuthenticationMethodServerTrust
) {
println("CertificatePinner: Authentication method not suitable for pinning")
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, null)
return
if (challenge.protectionSpace.authenticationMethod != NSURLAuthenticationMethodServerTrust) {
LOG.trace { "Authentication method not suitable for pinning" }
return false
}

val trust = challenge.protectionSpace.serverTrust
if (trust == null) {
println("CertificatePinner: Server trust is not available")
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, null)
return
}
?: throw CertificatePinnerException("Server trust is not available")

if (validateTrust) {
val hostCFString = CFStringCreateWithCString(null, hostname, kCFStringEncodingUTF8)
Expand All @@ -150,11 +154,7 @@ public data class CertificatePinner(
SecTrustSetPolicies(trust, policy)
}
}
if (!trust.trustIsValid()) {
println("CertificatePinner: Server trust is invalid")
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, null)
return
}
if (!trust.trustIsValid()) throw CertificatePinnerException("Server trust is invalid")
}

val certCount = SecTrustGetCertificateCount(trust)
Expand All @@ -163,19 +163,14 @@ public data class CertificatePinner(
}

if (certificates.size != certCount.toInt()) {
println("CertificatePinner: Unknown certificates")
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, null)
return
throw CertificatePinnerException("Unknown certificates")
}

val result = hasOnePinnedCertificate(certificates)
if (result) {
completionHandler(NSURLSessionAuthChallengeUseCredential, challenge.proposedCredential)
} else {
if (!hasOnePinnedCertificate(certificates)) {
val message = buildErrorMessage(certificates, hostname)
println(message)
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, null)
throw CertificatePinnerException(message)
}
return true
}

/**
Expand All @@ -199,17 +194,16 @@ public data class CertificatePinner(

pin.hash == sha256
}

CertificatesInfo.HASH_ALGORITHM_SHA_1 -> {
if (sha1 == null) {
sha1 = publicKey.toSha1String()
}

pin.hash == sha1
}
else -> {
println("CertificatePinner: Unsupported hashAlgorithm: ${pin.hashAlgorithm}")
false
}

else -> throw IllegalArgumentException("Unsupported hashAlgorithm: ${pin.hashAlgorithm}")
}
}
}
Expand Down Expand Up @@ -436,3 +430,5 @@ public data class CertificatePinner(
)
}
}

public class CertificatePinnerException(message: String) : IOException(message)

0 comments on commit cc70f73

Please sign in to comment.