react-native-audio-browser 0.2.0-next.1 → 0.2.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -18
- package/android/build.gradle +13 -10
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +76 -21
- package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
- package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
- package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
- package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +1 -2
- package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +1 -2
- package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +6 -7
- package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
- package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +11 -11
- package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +9 -5
- package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
- package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +0 -1
- package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +2 -4
- package/android/src/main/java/com/audiobrowser/player/Player.kt +30 -8
- package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
- package/android/src/main/java/com/audiobrowser/player/TransformingDataSource.kt +2 -1
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
- package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
- package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +8 -7
- package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +52 -1
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +167 -8
- package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +2 -1
- package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +2 -4
- package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +100 -56
- package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
- package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
- package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
- package/ios/Browser/BrowserManager.swift +2 -27
- package/ios/Browser/SearchDrillIn.swift +1 -1
- package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
- package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
- package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
- package/ios/HybridAudioBrowser.swift +87 -56
- package/ios/Model/NitroTypeStubs.swift +18 -2
- package/ios/Model/RemoteCommand.swift +18 -0
- package/ios/Model/TrackPlayerError.swift +2 -2
- package/ios/Option/SessionCategories.swift +3 -2
- package/ios/Player/MediaIntentCriteria.swift +4 -4
- package/ios/Player/MediaLoader.swift +3 -1
- package/ios/Player/PlaybackCoordinator.swift +4 -1
- package/ios/Player/PlaybackErrorHandler.swift +4 -4
- package/ios/Player/QueueManager.swift +10 -3
- package/ios/Player/RetryManager.swift +79 -18
- package/ios/Player/ShuffleOrder.swift +10 -0
- package/ios/Player/SleepTimerManager.swift +43 -15
- package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
- package/ios/RemoteCommand/RemoteCommandController.swift +9 -6
- package/ios/Tests/LRUCacheTests.swift +111 -0
- package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
- package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
- package/ios/Tests/MediaLoaderTests.swift +10 -13
- package/ios/Tests/PlaybackCoordinatorTests.swift +10 -7
- package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
- package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
- package/ios/Tests/QueueManagerTests.swift +50 -4
- package/ios/Tests/RemoteCommandControllerTests.swift +156 -0
- package/ios/Tests/RemoteCommandTests.swift +67 -0
- package/ios/Tests/RetryManagerTests.swift +89 -0
- package/ios/Tests/SectionScopeTests.swift +2 -2
- package/ios/Tests/SleepTimerManagerTests.swift +22 -19
- package/ios/Tests/Support/ManualSleepTimerScheduler.swift +47 -0
- package/ios/Tests/Support/TestTypes.swift +1 -1
- package/ios/Tests/TabBarEntriesTests.swift +7 -7
- package/ios/TrackPlayer.swift +6 -1
- package/ios/TrackPlayerCallbacks.swift +5 -38
- package/ios/Util/LRUCache.swift +4 -1
- package/lib/commonjs/AudioBrowser.js +0 -11
- package/lib/commonjs/AudioBrowser.js.map +1 -1
- package/lib/commonjs/CLAUDE.md +52 -0
- package/lib/commonjs/features/browser.js +1 -1
- package/lib/commonjs/features/browser.js.map +1 -1
- package/lib/commonjs/features/carConnection.js +1 -1
- package/lib/commonjs/features/equalizer.js +1 -1
- package/lib/commonjs/features/errors.js +3 -3
- package/lib/commonjs/features/favorites.js +1 -1
- package/lib/commonjs/features/gate.js +16 -1
- package/lib/commonjs/features/gate.js.map +1 -1
- package/lib/commonjs/features/metadata.js +20 -3
- package/lib/commonjs/features/metadata.js.map +1 -1
- package/lib/commonjs/features/network.js +1 -1
- package/lib/commonjs/features/nowPlaying.js +1 -1
- package/lib/commonjs/features/output.js +1 -1
- package/lib/commonjs/features/playback/playWhenReady.js +1 -1
- package/lib/commonjs/features/playback/playing.js +1 -1
- package/lib/commonjs/features/playback/progress.js +1 -1
- package/lib/commonjs/features/playback/state.js +1 -1
- package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
- package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/commonjs/features/playback/volume.js +1 -1
- package/lib/commonjs/features/player/options.js +48 -34
- package/lib/commonjs/features/player/options.js.map +1 -1
- package/lib/commonjs/features/player/setup.js +15 -8
- package/lib/commonjs/features/player/setup.js.map +1 -1
- package/lib/commonjs/features/player/validateOptions.js +21 -0
- package/lib/commonjs/features/player/validateOptions.js.map +1 -0
- package/lib/commonjs/features/queue/activeTrack.js +1 -1
- package/lib/commonjs/features/queue/queue.js +2 -2
- package/lib/commonjs/features/queue/repeatMode.js +1 -1
- package/lib/commonjs/features/queue/shuffle.js +1 -1
- package/lib/commonjs/features/remoteControls.js +13 -92
- package/lib/commonjs/features/remoteControls.js.map +1 -1
- package/lib/commonjs/features/sleepTimer.js +13 -1
- package/lib/commonjs/features/sleepTimer.js.map +1 -1
- package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
- package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
- package/lib/commonjs/utils/LazyNativeEmitter.js +8 -5
- package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/commonjs/web/NativeAudioBrowser.js +1 -1
- package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
- package/lib/commonjs/web/browser/BrowserManager.js +1 -1
- package/lib/module/AudioBrowser.js +1 -1
- package/lib/module/AudioBrowser.js.map +1 -1
- package/lib/module/CLAUDE.md +52 -0
- package/lib/module/features/browser.js +1 -1
- package/lib/module/features/browser.js.map +1 -1
- package/lib/module/features/carConnection.js +1 -1
- package/lib/module/features/equalizer.js +1 -1
- package/lib/module/features/errors.js +3 -3
- package/lib/module/features/favorites.js +1 -1
- package/lib/module/features/gate.js +16 -1
- package/lib/module/features/gate.js.map +1 -1
- package/lib/module/features/metadata.js +20 -3
- package/lib/module/features/metadata.js.map +1 -1
- package/lib/module/features/network.js +1 -1
- package/lib/module/features/nowPlaying.js +1 -1
- package/lib/module/features/output.js +1 -1
- package/lib/module/features/playback/playWhenReady.js +1 -1
- package/lib/module/features/playback/playing.js +1 -1
- package/lib/module/features/playback/progress.js +1 -1
- package/lib/module/features/playback/state.js +1 -1
- package/lib/module/features/playback/trackPlaybackTime.js +0 -5
- package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
- package/lib/module/features/playback/volume.js +1 -1
- package/lib/module/features/player/options.js +47 -32
- package/lib/module/features/player/options.js.map +1 -1
- package/lib/module/features/player/setup.js +14 -7
- package/lib/module/features/player/setup.js.map +1 -1
- package/lib/module/features/player/validateOptions.js +17 -0
- package/lib/module/features/player/validateOptions.js.map +1 -0
- package/lib/module/features/queue/activeTrack.js +1 -1
- package/lib/module/features/queue/queue.js +2 -2
- package/lib/module/features/queue/repeatMode.js +1 -1
- package/lib/module/features/queue/shuffle.js +1 -1
- package/lib/module/features/remoteControls.js +13 -92
- package/lib/module/features/remoteControls.js.map +1 -1
- package/lib/module/features/sleepTimer.js +13 -1
- package/lib/module/features/sleepTimer.js.map +1 -1
- package/lib/module/test-utils/reactNativeStub.js +58 -0
- package/lib/module/test-utils/reactNativeStub.js.map +1 -0
- package/lib/module/utils/LazyNativeEmitter.js +8 -5
- package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
- package/lib/module/web/NativeAudioBrowser.js +1 -1
- package/lib/module/web/NativeAudioBrowser.js.map +1 -1
- package/lib/module/web/browser/BrowserManager.js +1 -1
- package/lib/typescript/src/AudioBrowser.d.ts +0 -1
- package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
- package/lib/typescript/src/features/carConnection.d.ts +1 -1
- package/lib/typescript/src/features/equalizer.d.ts +1 -1
- package/lib/typescript/src/features/errors.d.ts +3 -3
- package/lib/typescript/src/features/favorites.d.ts +1 -1
- package/lib/typescript/src/features/gate.d.ts +18 -2
- package/lib/typescript/src/features/gate.d.ts.map +1 -1
- package/lib/typescript/src/features/metadata.d.ts +20 -3
- package/lib/typescript/src/features/metadata.d.ts.map +1 -1
- package/lib/typescript/src/features/network.d.ts +1 -1
- package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
- package/lib/typescript/src/features/output.d.ts +1 -1
- package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
- package/lib/typescript/src/features/playback/playing.d.ts +1 -1
- package/lib/typescript/src/features/playback/progress.d.ts +1 -1
- package/lib/typescript/src/features/playback/state.d.ts +1 -1
- package/lib/typescript/src/features/playback/volume.d.ts +1 -1
- package/lib/typescript/src/features/player/options.d.ts +87 -37
- package/lib/typescript/src/features/player/options.d.ts.map +1 -1
- package/lib/typescript/src/features/player/setup.d.ts +14 -4
- package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
- package/lib/typescript/src/features/player/validateOptions.d.ts +9 -0
- package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
- package/lib/typescript/src/features/queue/activeTrack.d.ts +1 -1
- package/lib/typescript/src/features/queue/queue.d.ts +2 -2
- package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
- package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
- package/lib/typescript/src/features/remoteControls.d.ts +23 -22
- package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
- package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
- package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +8 -4
- package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
- package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +5 -5
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +11 -11
- package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
- package/nitrogen/generated/android/c++/JOptions.hpp +5 -5
- package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
- package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +36 -51
- package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +6 -6
- package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +8 -8
- package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
- package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
- package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
- package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
- package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
- package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
- package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
- package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
- package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
- package/package.json +12 -4
- package/src/AudioBrowser.ts +1 -1
- package/src/CLAUDE.md +52 -0
- package/src/features/battery.test.ts +79 -0
- package/src/features/browser.ts +1 -1
- package/src/features/carConnection.ts +1 -1
- package/src/features/equalizer.ts +1 -1
- package/src/features/errors.ts +3 -3
- package/src/features/favorites.ts +1 -1
- package/src/features/gate.ts +16 -1
- package/src/features/hook-wiring.test.ts +254 -0
- package/src/features/metadata.ts +20 -3
- package/src/features/network.ts +1 -1
- package/src/features/nowPlaying.ts +1 -1
- package/src/features/output.ts +1 -1
- package/src/features/playback/playWhenReady.ts +1 -1
- package/src/features/playback/playing.ts +1 -1
- package/src/features/playback/progress.test.ts +169 -0
- package/src/features/playback/progress.ts +1 -1
- package/src/features/playback/state.ts +1 -1
- package/src/features/playback/trackPlaybackTime.test.ts +16 -6
- package/src/features/playback/trackPlaybackTime.ts +0 -5
- package/src/features/playback/volume.ts +1 -1
- package/src/features/player/options.ts +99 -57
- package/src/features/player/setup.test.ts +12 -4
- package/src/features/player/setup.ts +19 -9
- package/src/features/player/validateOptions.ts +19 -0
- package/src/features/queue/activeTrack.ts +1 -1
- package/src/features/queue/queue.ts +2 -2
- package/src/features/queue/repeatMode.ts +1 -1
- package/src/features/queue/shuffle.ts +1 -1
- package/src/features/remoteControls.ts +13 -92
- package/src/features/sleepTimer.test.ts +172 -0
- package/src/features/sleepTimer.ts +9 -1
- package/src/nitro-stub-drift.test.ts +237 -0
- package/src/public-surface.test.ts +107 -0
- package/src/specs/audio-browser.nitro.ts +1 -1
- package/src/test-utils/reactNativeStub.ts +60 -0
- package/src/utils/LazyNativeEmitter.ts +9 -6
- package/src/utils/useDebug.test.ts +182 -0
- package/src/utils/useNativeUpdatedValue.test.ts +103 -0
- package/src/web/NativeAudioBrowser.ts +1 -1
- package/src/web/browser/BrowserManager.ts +1 -1
- package/ios/Model/MediaURL.swift +0 -67
- package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
- package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
- package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
|
@@ -7,12 +7,98 @@ import java.security.cert.PKIXParameters
|
|
|
7
7
|
import java.security.cert.TrustAnchor
|
|
8
8
|
import java.security.cert.X509Certificate
|
|
9
9
|
import java.util.Date
|
|
10
|
+
import javax.security.auth.x500.X500Principal
|
|
10
11
|
import org.junit.Assert.assertEquals
|
|
11
12
|
import org.junit.Assert.assertThrows
|
|
12
13
|
import org.junit.Test
|
|
14
|
+
import org.mockito.Mockito.mock
|
|
15
|
+
import org.mockito.Mockito.`when`
|
|
13
16
|
|
|
14
17
|
class AiaCertChaserTest {
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A certificate whose AIA extension value is exactly [extensionValue].
|
|
21
|
+
*
|
|
22
|
+
* Subject and issuer are stubbed to distinct principals on purpose: an unstubbed mock answers
|
|
23
|
+
* null to both, which `completeChain` would read as a self-signed root and stop on before
|
|
24
|
+
* fetching anything.
|
|
25
|
+
*/
|
|
26
|
+
private fun certWithAia(extensionValue: ByteArray): X509Certificate =
|
|
27
|
+
mock(X509Certificate::class.java).also {
|
|
28
|
+
`when`(it.getExtensionValue("1.3.6.1.5.5.7.1.1")).thenReturn(extensionValue)
|
|
29
|
+
`when`(it.subjectX500Principal).thenReturn(X500Principal("CN=leaf"))
|
|
30
|
+
`when`(it.issuerX500Principal).thenReturn(X500Principal("CN=issuer"))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A DER tag-length-value, with the length in whichever form fits. */
|
|
34
|
+
private fun der(tag: Int, content: ByteArray): ByteArray {
|
|
35
|
+
val length =
|
|
36
|
+
when (val n = content.size) {
|
|
37
|
+
in 0..0x7F -> byteArrayOf(n.toByte())
|
|
38
|
+
in 0x80..0xFF -> byteArrayOf(0x81.toByte(), n.toByte())
|
|
39
|
+
in 0x100..0xFFFF -> byteArrayOf(0x82.toByte(), (n shr 8).toByte(), n.toByte())
|
|
40
|
+
else -> byteArrayOf(0x83.toByte(), (n shr 16).toByte(), (n shr 8).toByte(), n.toByte())
|
|
41
|
+
}
|
|
42
|
+
return byteArrayOf(tag.toByte()) + length + content
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A well-formed AIA extension value listing [urls] as caIssuers locations. */
|
|
46
|
+
private fun aiaExtension(urls: List<String>): ByteArray {
|
|
47
|
+
val caIssuersOid = byteArrayOf(0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02)
|
|
48
|
+
val descriptions =
|
|
49
|
+
urls.fold(ByteArray(0)) { acc, url ->
|
|
50
|
+
acc + der(0x30, der(0x06, caIssuersOid) + der(0x86, url.toByteArray(Charsets.US_ASCII)))
|
|
51
|
+
}
|
|
52
|
+
return der(0x04, der(0x30, descriptions))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `the AIA fixture builder round-trips through the parser`() {
|
|
57
|
+
// Guards the two tests below: if this builder were malformed they would pass by parsing
|
|
58
|
+
// nothing at all, rather than by the cap doing its job.
|
|
59
|
+
val urls = listOf("http://ca1.example/i.crt", "http://ca2.example/i.crt")
|
|
60
|
+
|
|
61
|
+
assertEquals(urls, AiaCertChaser.extractCaIssuerUrls(certWithAia(aiaExtension(urls))))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test(timeout = 5_000)
|
|
65
|
+
fun `a negative long-form length does not hang the parser`() {
|
|
66
|
+
// OCTET STRING { SEQUENCE { tag 0x00, long-form length 0xFFFFFFFA = -6 } }.
|
|
67
|
+
// The length's four bytes shift into a negative Int, so `pos += len` rewinds the
|
|
68
|
+
// reader by exactly the six bytes it just consumed: the tag is not a SEQUENCE, the
|
|
69
|
+
// loop `continue`s, and the reader is back where it started with more input to read.
|
|
70
|
+
val aia =
|
|
71
|
+
byteArrayOf(
|
|
72
|
+
0x04,
|
|
73
|
+
0x08,
|
|
74
|
+
0x30,
|
|
75
|
+
0x06,
|
|
76
|
+
0x00,
|
|
77
|
+
0x84.toByte(),
|
|
78
|
+
0xFF.toByte(),
|
|
79
|
+
0xFF.toByte(),
|
|
80
|
+
0xFF.toByte(),
|
|
81
|
+
0xFA.toByte(),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test(timeout = 5_000)
|
|
88
|
+
fun `a length running past the buffer is treated as no AIA`() {
|
|
89
|
+
// SEQUENCE claiming 0x7F content bytes inside a 2-byte buffer.
|
|
90
|
+
val aia = byteArrayOf(0x04, 0x04, 0x30, 0x02, 0x30, 0x7F)
|
|
91
|
+
|
|
92
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test(timeout = 5_000)
|
|
96
|
+
fun `truncated DER is treated as no AIA`() {
|
|
97
|
+
val aia = byteArrayOf(0x04, 0x08, 0x30)
|
|
98
|
+
|
|
99
|
+
assertEquals(emptyList<String>(), AiaCertChaser.extractCaIssuerUrls(certWithAia(aia)))
|
|
100
|
+
}
|
|
101
|
+
|
|
16
102
|
@Test
|
|
17
103
|
fun `extracts CA Issuers URL from real leaf certificate AIA extension`() {
|
|
18
104
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
@@ -27,7 +113,7 @@ class AiaCertChaserTest {
|
|
|
27
113
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
28
114
|
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
29
115
|
// Fetcher knows only the R13 URL (the leaf's AIA pointer); empty for the root URL.
|
|
30
|
-
val fetch = { url: String ->
|
|
116
|
+
val fetch = { url: String, _: Long ->
|
|
31
117
|
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
32
118
|
}
|
|
33
119
|
|
|
@@ -44,8 +130,8 @@ class AiaCertChaserTest {
|
|
|
44
130
|
|
|
45
131
|
// A PKCS#7-style bundle where the genuine issuer is not first must still be selected.
|
|
46
132
|
val completed =
|
|
47
|
-
AiaCertChaser.completeChain(listOf(leaf)) {
|
|
48
|
-
if (
|
|
133
|
+
AiaCertChaser.completeChain(listOf(leaf)) { url, _ ->
|
|
134
|
+
if (url == "http://r13.i.lencr.org/") listOf(unrelated, r13) else emptyList()
|
|
49
135
|
}
|
|
50
136
|
|
|
51
137
|
assertEquals(listOf(leaf, r13), completed)
|
|
@@ -57,7 +143,7 @@ class AiaCertChaserTest {
|
|
|
57
143
|
var fetches = 0
|
|
58
144
|
|
|
59
145
|
val completed =
|
|
60
|
-
AiaCertChaser.completeChain(listOf(root)) {
|
|
146
|
+
AiaCertChaser.completeChain(listOf(root)) { _, _ ->
|
|
61
147
|
fetches++
|
|
62
148
|
emptyList()
|
|
63
149
|
}
|
|
@@ -66,11 +152,115 @@ class AiaCertChaserTest {
|
|
|
66
152
|
assertEquals(0, fetches)
|
|
67
153
|
}
|
|
68
154
|
|
|
155
|
+
@Test
|
|
156
|
+
fun `completeChain stops at a chain that already reaches a trust anchor`() {
|
|
157
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
158
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
159
|
+
val root = CertFixtures.cert("isrg-root-x1.pem")
|
|
160
|
+
val fetched = mutableListOf<String>()
|
|
161
|
+
|
|
162
|
+
// R13's own AIA points at the ISRG root, so without knowing the anchors this walks one hop
|
|
163
|
+
// too far and pays for a second blocking fetch on the ordinary rescue path.
|
|
164
|
+
val completed =
|
|
165
|
+
AiaCertChaser.completeChain(
|
|
166
|
+
listOf(leaf),
|
|
167
|
+
anchorSubjects = setOf(root.subjectX500Principal),
|
|
168
|
+
) { url, _ ->
|
|
169
|
+
fetched.add(url)
|
|
170
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else listOf(root)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
assertEquals(listOf(leaf, r13), completed)
|
|
174
|
+
assertEquals(listOf("http://r13.i.lencr.org/"), fetched)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Test
|
|
178
|
+
fun `completeChain fetches nothing when the presented chain is already anchored`() {
|
|
179
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
180
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
181
|
+
val root = CertFixtures.cert("isrg-root-x1.pem")
|
|
182
|
+
var fetches = 0
|
|
183
|
+
|
|
184
|
+
// The shape of every non-path failure — expiry, hostname mismatch, CT — where the server
|
|
185
|
+
// sent a complete chain and validation failed for some other reason.
|
|
186
|
+
val completed =
|
|
187
|
+
AiaCertChaser.completeChain(
|
|
188
|
+
listOf(leaf, r13),
|
|
189
|
+
anchorSubjects = setOf(root.subjectX500Principal),
|
|
190
|
+
) { _, _ ->
|
|
191
|
+
fetches++
|
|
192
|
+
emptyList()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
assertEquals(listOf(leaf, r13), completed)
|
|
196
|
+
assertEquals(0, fetches)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Test
|
|
200
|
+
fun `completeChain tries only a few of a certificate's CA-Issuers URLs`() {
|
|
201
|
+
// The extension is authored by the server that just failed validation, and nothing limits
|
|
202
|
+
// how many URLs it lists. Each one costs a blocking connect on the handshake thread, so
|
|
203
|
+
// hundreds of them are a denial of service whatever each individual fetch costs.
|
|
204
|
+
val manyUrls = (1..500).map { "http://ca$it.example/issuer.crt" }
|
|
205
|
+
val cert = certWithAia(aiaExtension(manyUrls))
|
|
206
|
+
val fetched = mutableListOf<String>()
|
|
207
|
+
|
|
208
|
+
AiaCertChaser.completeChain(listOf(cert)) { url, _ ->
|
|
209
|
+
fetched.add(url)
|
|
210
|
+
emptyList()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
assertEquals(AiaCertChaser.MAX_URLS_PER_CERT, fetched.size)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Test
|
|
217
|
+
fun `duplicate CA-Issuers URLs do not consume the URL cap`() {
|
|
218
|
+
// 500 copies of one dead URL followed by a distinct one: the cap must buy distinct hosts,
|
|
219
|
+
// not three visits to the same dead one.
|
|
220
|
+
val urls = List(500) { "http://ca.example/issuer.crt" } + "http://ca-b.example/issuer.crt"
|
|
221
|
+
val cert = certWithAia(aiaExtension(urls))
|
|
222
|
+
val fetched = mutableListOf<String>()
|
|
223
|
+
|
|
224
|
+
AiaCertChaser.completeChain(listOf(cert)) { url, _ ->
|
|
225
|
+
fetched.add(url)
|
|
226
|
+
emptyList()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
assertEquals(listOf("http://ca.example/issuer.crt", "http://ca-b.example/issuer.crt"), fetched)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@Test
|
|
233
|
+
fun `completeChain stops fetching once its wall-clock budget is spent`() {
|
|
234
|
+
val cert = certWithAia(aiaExtension((1..500).map { "http://ca$it.example/issuer.crt" }))
|
|
235
|
+
var fetches = 0
|
|
236
|
+
var nanos = 0L
|
|
237
|
+
val deadlines = mutableListOf<Long>()
|
|
238
|
+
|
|
239
|
+
// Each fetch "takes" 8s against a 20s budget, so the walk must stop after a handful even
|
|
240
|
+
// though the URL cap alone would have allowed more.
|
|
241
|
+
AiaCertChaser.completeChain(
|
|
242
|
+
listOf(cert),
|
|
243
|
+
maxUrlsPerCert = 500,
|
|
244
|
+
budgetMs = 20_000,
|
|
245
|
+
nowNanos = { nanos },
|
|
246
|
+
) { _, deadline ->
|
|
247
|
+
fetches++
|
|
248
|
+
deadlines.add(deadline)
|
|
249
|
+
nanos += 8_000_000_000
|
|
250
|
+
emptyList()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
assertEquals(3, fetches)
|
|
254
|
+
// Every fetch is handed the same walk-wide deadline, so it can clip its own I/O to it —
|
|
255
|
+
// the budget bounds the fetches themselves, not just how many of them start.
|
|
256
|
+
assertEquals(listOf(20_000_000_000L, 20_000_000_000L, 20_000_000_000L), deadlines)
|
|
257
|
+
}
|
|
258
|
+
|
|
69
259
|
@Test
|
|
70
260
|
fun `completeChain stops when the issuer cannot be fetched`() {
|
|
71
261
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
72
262
|
|
|
73
|
-
val completed = AiaCertChaser.completeChain(listOf(leaf)) { emptyList() }
|
|
263
|
+
val completed = AiaCertChaser.completeChain(listOf(leaf)) { _, _ -> emptyList() }
|
|
74
264
|
|
|
75
265
|
assertEquals(listOf(leaf), completed)
|
|
76
266
|
}
|
|
@@ -88,8 +278,8 @@ class AiaCertChaserTest {
|
|
|
88
278
|
|
|
89
279
|
// After AIA chasing splices in R13, the chain validates to the trusted root.
|
|
90
280
|
val completed =
|
|
91
|
-
AiaCertChaser.completeChain(listOf(leaf)) {
|
|
92
|
-
if (
|
|
281
|
+
AiaCertChaser.completeChain(listOf(leaf)) { url, _ ->
|
|
282
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
93
283
|
}
|
|
94
284
|
validatePath(completed, root, date) // must not throw
|
|
95
285
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
package com.audiobrowser.tls
|
|
2
2
|
|
|
3
|
+
import java.net.Socket
|
|
3
4
|
import java.security.cert.CertificateException
|
|
4
5
|
import java.security.cert.X509Certificate
|
|
6
|
+
import javax.net.ssl.SSLEngine
|
|
7
|
+
import javax.net.ssl.X509ExtendedTrustManager
|
|
5
8
|
import javax.net.ssl.X509TrustManager
|
|
6
9
|
import org.junit.Assert.assertEquals
|
|
7
10
|
import org.junit.Assert.assertThrows
|
|
@@ -31,8 +34,8 @@ class AiaChasingTrustManagerTest {
|
|
|
31
34
|
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
32
35
|
val delegate = FakeDelegate(trustsChainOfSize = 2)
|
|
33
36
|
val tm =
|
|
34
|
-
AiaChasingTrustManager(delegate) {
|
|
35
|
-
if (
|
|
37
|
+
AiaChasingTrustManager(delegate) { url, _ ->
|
|
38
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// Must not throw: the leaf-only chain is rejected, AIA fills in R13, the [leaf, R13] chain
|
|
@@ -46,7 +49,7 @@ class AiaChasingTrustManagerTest {
|
|
|
46
49
|
fun `rethrows the original failure when no intermediate can be fetched`() {
|
|
47
50
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
48
51
|
val delegate = FakeDelegate(trustsChainOfSize = 2)
|
|
49
|
-
val tm = AiaChasingTrustManager(delegate) { emptyList() }
|
|
52
|
+
val tm = AiaChasingTrustManager(delegate) { _, _ -> emptyList() }
|
|
50
53
|
|
|
51
54
|
assertThrows(CertificateException::class.java) { tm.checkServerTrusted(arrayOf(leaf), "RSA") }
|
|
52
55
|
|
|
@@ -60,7 +63,7 @@ class AiaChasingTrustManagerTest {
|
|
|
60
63
|
val delegate = FakeDelegate(trustsChainOfSize = 1)
|
|
61
64
|
var fetches = 0
|
|
62
65
|
val tm =
|
|
63
|
-
AiaChasingTrustManager(delegate) {
|
|
66
|
+
AiaChasingTrustManager(delegate) { _, _ ->
|
|
64
67
|
fetches++
|
|
65
68
|
emptyList()
|
|
66
69
|
}
|
|
@@ -70,4 +73,322 @@ class AiaChasingTrustManagerTest {
|
|
|
70
73
|
assertEquals(0, fetches)
|
|
71
74
|
assertEquals(listOf(1), delegate.checkedChainSizes)
|
|
72
75
|
}
|
|
76
|
+
|
|
77
|
+
// -- the API 24+ variant, which is what the platform actually calls into --
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stands in for Android's `RootTrustManager` under a per-domain network security config: the
|
|
81
|
+
* hostname-aware overloads work, and the two-argument one refuses to answer at all.
|
|
82
|
+
*/
|
|
83
|
+
private class FakeDomainConfigDelegate(private val trustsChainOfSize: Int) :
|
|
84
|
+
X509ExtendedTrustManager() {
|
|
85
|
+
val hostnameAwareChecks = mutableListOf<Int>()
|
|
86
|
+
|
|
87
|
+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
88
|
+
throw CertificateException(
|
|
89
|
+
"Domain specific configurations require that hostname aware " +
|
|
90
|
+
"checkServerTrusted(X509Certificate[], String, String) is used"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
override fun checkServerTrusted(
|
|
94
|
+
chain: Array<X509Certificate>,
|
|
95
|
+
authType: String,
|
|
96
|
+
socket: Socket?,
|
|
97
|
+
) {
|
|
98
|
+
hostnameAwareChecks.add(chain.size)
|
|
99
|
+
if (chain.size != trustsChainOfSize) {
|
|
100
|
+
throw CertificateException("Trust anchor for certification path not found.")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun checkServerTrusted(
|
|
105
|
+
chain: Array<X509Certificate>,
|
|
106
|
+
authType: String,
|
|
107
|
+
engine: SSLEngine?,
|
|
108
|
+
) = checkServerTrusted(chain, authType, null as Socket?)
|
|
109
|
+
|
|
110
|
+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
111
|
+
|
|
112
|
+
override fun checkClientTrusted(
|
|
113
|
+
chain: Array<X509Certificate>,
|
|
114
|
+
authType: String,
|
|
115
|
+
socket: Socket?,
|
|
116
|
+
) = Unit
|
|
117
|
+
|
|
118
|
+
override fun checkClientTrusted(
|
|
119
|
+
chain: Array<X509Certificate>,
|
|
120
|
+
authType: String,
|
|
121
|
+
engine: SSLEngine?,
|
|
122
|
+
) = Unit
|
|
123
|
+
|
|
124
|
+
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun `a complete chain rejected for some other reason is not chased`() {
|
|
129
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
130
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
131
|
+
val root = CertFixtures.cert("isrg-root-x1.pem")
|
|
132
|
+
// Trusts nothing — an expired chain, a hostname mismatch, a CT refusal all look like this:
|
|
133
|
+
// the delegate rejects a chain that is not missing anything.
|
|
134
|
+
val delegate =
|
|
135
|
+
object : X509TrustManager {
|
|
136
|
+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
137
|
+
throw CertificateException("certificate expired")
|
|
138
|
+
|
|
139
|
+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
140
|
+
|
|
141
|
+
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf(root)
|
|
142
|
+
}
|
|
143
|
+
var fetches = 0
|
|
144
|
+
val tm =
|
|
145
|
+
AiaChasingTrustManager(delegate) { _, _ ->
|
|
146
|
+
fetches++
|
|
147
|
+
emptyList()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
assertThrows(CertificateException::class.java) {
|
|
151
|
+
tm.checkServerTrusted(arrayOf(leaf, r13), "RSA")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Chasing could not have helped, so it must not have reached out to a host named by a
|
|
155
|
+
// certificate that just failed validation.
|
|
156
|
+
assertEquals(0, fetches)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
fun `the original failure is kept when the completed chain fails too`() {
|
|
161
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
162
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
163
|
+
// Trusts no chain of any size, so the retry fails as well.
|
|
164
|
+
val delegate = FakeDelegate(trustsChainOfSize = -1)
|
|
165
|
+
val tm = AiaChasingTrustManager(delegate) { _, _ -> listOf(r13) }
|
|
166
|
+
|
|
167
|
+
val thrown =
|
|
168
|
+
assertThrows(CertificateException::class.java) { tm.checkServerTrusted(arrayOf(leaf), "RSA") }
|
|
169
|
+
|
|
170
|
+
assertEquals(1, thrown.suppressed.size)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@Test
|
|
174
|
+
fun `client checks are forwarded, not routed through the server path`() {
|
|
175
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
176
|
+
var clientChecks = 0
|
|
177
|
+
val delegate =
|
|
178
|
+
object : X509ExtendedTrustManager() {
|
|
179
|
+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
180
|
+
throw CertificateException("server path must not be used for a client check")
|
|
181
|
+
|
|
182
|
+
override fun checkServerTrusted(
|
|
183
|
+
chain: Array<X509Certificate>,
|
|
184
|
+
authType: String,
|
|
185
|
+
socket: Socket?,
|
|
186
|
+
) = throw CertificateException("server path must not be used for a client check")
|
|
187
|
+
|
|
188
|
+
override fun checkServerTrusted(
|
|
189
|
+
chain: Array<X509Certificate>,
|
|
190
|
+
authType: String,
|
|
191
|
+
engine: SSLEngine?,
|
|
192
|
+
) = throw CertificateException("server path must not be used for a client check")
|
|
193
|
+
|
|
194
|
+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
|
195
|
+
clientChecks++
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
override fun checkClientTrusted(
|
|
199
|
+
chain: Array<X509Certificate>,
|
|
200
|
+
authType: String,
|
|
201
|
+
socket: Socket?,
|
|
202
|
+
) {
|
|
203
|
+
clientChecks++
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
override fun checkClientTrusted(
|
|
207
|
+
chain: Array<X509Certificate>,
|
|
208
|
+
authType: String,
|
|
209
|
+
engine: SSLEngine?,
|
|
210
|
+
) {
|
|
211
|
+
clientChecks++
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
|
215
|
+
}
|
|
216
|
+
val tm = AiaChasingExtendedTrustManager(delegate) { _, _ -> emptyList() }
|
|
217
|
+
|
|
218
|
+
tm.checkClientTrusted(arrayOf(leaf), "RSA")
|
|
219
|
+
tm.checkClientTrusted(arrayOf(leaf), "RSA", null as Socket?)
|
|
220
|
+
tm.checkClientTrusted(arrayOf(leaf), "RSA", null as SSLEngine?)
|
|
221
|
+
|
|
222
|
+
assertEquals(3, clientChecks)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A delegate carrying the Android-specific hostname-aware method, as `RootTrustManager` does. */
|
|
226
|
+
@Suppress("unused")
|
|
227
|
+
private class HostAwareDelegate(private val trustsChainOfSize: Int) : X509TrustManager {
|
|
228
|
+
val hosts = mutableListOf<String>()
|
|
229
|
+
|
|
230
|
+
fun checkServerTrusted(
|
|
231
|
+
chain: Array<X509Certificate>,
|
|
232
|
+
authType: String,
|
|
233
|
+
host: String,
|
|
234
|
+
): List<X509Certificate> {
|
|
235
|
+
hosts.add(host)
|
|
236
|
+
if (chain.size != trustsChainOfSize) {
|
|
237
|
+
throw CertificateException("Trust anchor for certification path not found.")
|
|
238
|
+
}
|
|
239
|
+
return chain.toList()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
243
|
+
throw CertificateException("the host-aware overload should have been used")
|
|
244
|
+
|
|
245
|
+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
246
|
+
|
|
247
|
+
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@Test
|
|
251
|
+
fun `the X509TrustManagerExtensions signature is present and forwards the host`() {
|
|
252
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
253
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
254
|
+
val delegate = HostAwareDelegate(trustsChainOfSize = 2)
|
|
255
|
+
val tm =
|
|
256
|
+
AiaChasingExtendedTrustManager(delegate) { url, _ ->
|
|
257
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The method X509TrustManagerExtensions looks up reflectively must exist on the wrapper...
|
|
261
|
+
val method =
|
|
262
|
+
tm.javaClass.getMethod(
|
|
263
|
+
"checkServerTrusted",
|
|
264
|
+
Array<X509Certificate>::class.java,
|
|
265
|
+
String::class.java,
|
|
266
|
+
String::class.java,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// ...and must reach the delegate's own host-aware method, so pinning still sees the host.
|
|
270
|
+
val accepted = tm.checkServerTrusted(arrayOf(leaf), "RSA", "ca.example")
|
|
271
|
+
|
|
272
|
+
assertEquals(listOf(leaf, r13), accepted)
|
|
273
|
+
assertEquals(listOf("ca.example", "ca.example"), delegate.hosts)
|
|
274
|
+
assertEquals(List::class.java, method.returnType)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@Test
|
|
278
|
+
fun `the host-aware signature falls back to the extended overloads when the delegate lacks it`() {
|
|
279
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
280
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
281
|
+
// An extended delegate without the Android-specific String-host method, whose two-argument
|
|
282
|
+
// form refuses outright — the domain-config case. The fallback must route through its
|
|
283
|
+
// extended overloads, and must report the AIA-completed chain that actually validated.
|
|
284
|
+
val delegate = FakeDomainConfigDelegate(trustsChainOfSize = 2)
|
|
285
|
+
val tm =
|
|
286
|
+
AiaChasingExtendedTrustManager(delegate) { url, _ ->
|
|
287
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
val accepted = tm.checkServerTrusted(arrayOf(leaf), "RSA", "ca.example")
|
|
291
|
+
|
|
292
|
+
assertEquals(listOf(leaf, r13), accepted)
|
|
293
|
+
assertEquals(listOf(1, 2), delegate.hostnameAwareChecks)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** A host-aware delegate that validates fine but reports an empty accepted chain. */
|
|
297
|
+
@Suppress("unused")
|
|
298
|
+
private class SilentHostAwareDelegate(private val trustsChainOfSize: Int) : X509TrustManager {
|
|
299
|
+
fun checkServerTrusted(
|
|
300
|
+
chain: Array<X509Certificate>,
|
|
301
|
+
authType: String,
|
|
302
|
+
host: String,
|
|
303
|
+
): List<X509Certificate> {
|
|
304
|
+
if (chain.size != trustsChainOfSize) {
|
|
305
|
+
throw CertificateException("Trust anchor for certification path not found.")
|
|
306
|
+
}
|
|
307
|
+
return emptyList()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
311
|
+
throw CertificateException("the host-aware overload should have been used")
|
|
312
|
+
|
|
313
|
+
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
|
|
314
|
+
|
|
315
|
+
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@Test
|
|
319
|
+
fun `a delegate reporting an empty accepted chain still yields the chain that validated`() {
|
|
320
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
321
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
322
|
+
val delegate = SilentHostAwareDelegate(trustsChainOfSize = 2)
|
|
323
|
+
val tm =
|
|
324
|
+
AiaChasingExtendedTrustManager(delegate) { url, _ ->
|
|
325
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// An empty list from the delegate on success must not be passed through: the checked chain
|
|
329
|
+
// is what it accepted, and a pinning consumer given [] would fail a valid connection.
|
|
330
|
+
val accepted = tm.checkServerTrusted(arrayOf(leaf), "RSA", "ca.example")
|
|
331
|
+
|
|
332
|
+
assertEquals(listOf(leaf, r13), accepted)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@Test
|
|
336
|
+
fun `forwards to the hostname-aware overload the platform requires`() {
|
|
337
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
338
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
339
|
+
val delegate = FakeDomainConfigDelegate(trustsChainOfSize = 2)
|
|
340
|
+
val tm =
|
|
341
|
+
AiaChasingExtendedTrustManager(delegate) { url, _ ->
|
|
342
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// The socket overload must reach the delegate's socket overload. Routing it through the
|
|
346
|
+
// two-argument form instead would throw the domain-config refusal, which is what a plain
|
|
347
|
+
// X509TrustManager wrapper causes for every connection in the process.
|
|
348
|
+
tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
|
|
349
|
+
|
|
350
|
+
assertEquals(listOf(1, 2), delegate.hostnameAwareChecks)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@Test
|
|
354
|
+
fun `the engine overload is hostname-aware too`() {
|
|
355
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
356
|
+
val delegate = FakeDomainConfigDelegate(trustsChainOfSize = 1)
|
|
357
|
+
|
|
358
|
+
AiaChasingExtendedTrustManager(delegate) { _, _ -> emptyList() }
|
|
359
|
+
.checkServerTrusted(arrayOf(leaf), "RSA", null as SSLEngine?)
|
|
360
|
+
|
|
361
|
+
assertEquals(listOf(1), delegate.hostnameAwareChecks)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@Test
|
|
365
|
+
fun `a delegate without extended overloads still works`() {
|
|
366
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
367
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
368
|
+
val delegate = FakeDelegate(trustsChainOfSize = 2)
|
|
369
|
+
val tm =
|
|
370
|
+
AiaChasingExtendedTrustManager(delegate) { url, _ ->
|
|
371
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
|
|
375
|
+
|
|
376
|
+
assertEquals(listOf(1, 2), delegate.checkedChainSizes)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@Test
|
|
380
|
+
fun `the extended variant does not weaken trust`() {
|
|
381
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
382
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
383
|
+
// Trusts nothing, whatever the chain.
|
|
384
|
+
val delegate = FakeDomainConfigDelegate(trustsChainOfSize = -1)
|
|
385
|
+
val tm = AiaChasingExtendedTrustManager(delegate) { _, _ -> listOf(r13) }
|
|
386
|
+
|
|
387
|
+
assertThrows(CertificateException::class.java) {
|
|
388
|
+
tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// The completed chain was offered to the delegate and refused, rather than assumed good.
|
|
392
|
+
assertEquals(listOf(1, 2), delegate.hostnameAwareChecks)
|
|
393
|
+
}
|
|
73
394
|
}
|