diff --git a/ktor-client/ktor-client-darwin/api/ktor-client-darwin.klib.api b/ktor-client/ktor-client-darwin/api/ktor-client-darwin.klib.api index 1a48105abd7..0a45f8b93f4 100644 --- a/ktor-client/ktor-client-darwin/api/ktor-client-darwin.klib.api +++ b/ktor-client/ktor-client-darwin/api/ktor-client-darwin.klib.api @@ -9,6 +9,10 @@ // - Show declarations: true // Library unique name: +final class io.ktor.client.engine.darwin.certificates/CertificatePinnerException : kotlinx.io/IOException { // io.ktor.client.engine.darwin.certificates/CertificatePinnerException|null[0] + constructor (kotlin/String) // io.ktor.client.engine.darwin.certificates/CertificatePinnerException.|(kotlin.String){}[0] +} + final class io.ktor.client.engine.darwin.certificates/PinnedCertificate { // io.ktor.client.engine.darwin.certificates/PinnedCertificate|null[0] constructor (kotlin/String, kotlin/String, kotlin/String) // io.ktor.client.engine.darwin.certificates/PinnedCertificate.|(kotlin.String;kotlin.String;kotlin.String){}[0] diff --git a/ktor-client/ktor-client-darwin/darwin/src/io/ktor/client/engine/darwin/certificates/CertificatePinner.kt b/ktor-client/ktor-client-darwin/darwin/src/io/ktor/client/engine/darwin/certificates/CertificatePinner.kt index a2df4b7db53..94454e6af23 100644 --- a/ktor-client/ktor-client-darwin/darwin/src/io/ktor/client/engine/darwin/certificates/CertificatePinner.kt +++ b/ktor-client/ktor-client-darwin/darwin/src/io/ktor/client/engine/darwin/certificates/CertificatePinner.kt @@ -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 @@ -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) @@ -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) @@ -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 } /** @@ -199,6 +194,7 @@ public data class CertificatePinner( pin.hash == sha256 } + CertificatesInfo.HASH_ALGORITHM_SHA_1 -> { if (sha1 == null) { sha1 = publicKey.toSha1String() @@ -206,10 +202,8 @@ public data class CertificatePinner( pin.hash == sha1 } - else -> { - println("CertificatePinner: Unsupported hashAlgorithm: ${pin.hashAlgorithm}") - false - } + + else -> throw IllegalArgumentException("Unsupported hashAlgorithm: ${pin.hashAlgorithm}") } } } @@ -436,3 +430,5 @@ public data class CertificatePinner( ) } } + +public class CertificatePinnerException(message: String) : IOException(message)