diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
index 75fe2040043c79c6bf15c8924f645f6b8bc96263..09353815926329df5bc8708202254f4a1786fd4a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
@@ -8,6 +8,7 @@ import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper
 import de.rki.coronawarnapp.appconfig.mapping.ExposureDetectionConfigMapper
 import de.rki.coronawarnapp.appconfig.mapping.ExposureWindowRiskCalculationConfigMapper
 import de.rki.coronawarnapp.appconfig.mapping.KeyDownloadParametersMapper
+import de.rki.coronawarnapp.appconfig.mapping.LogUploadConfigMapper
 import de.rki.coronawarnapp.appconfig.mapping.SurveyConfigMapper
 import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient
 import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl
@@ -73,6 +74,10 @@ class AppConfigModule {
     fun analyticsMapper(mapper: AnalyticsConfigMapper):
         AnalyticsConfig.Mapper = mapper
 
+    @Provides
+    fun logUploadMapper(mapper: LogUploadConfigMapper):
+        LogUploadConfig.Mapper = mapper
+
     companion object {
         private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10)
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6d2b9a41f0c815703b3b15ee2d1bab3ea603b314
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.appconfig
+
+import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper
+
+interface LogUploadConfig {
+
+    val safetyNetRequirements: SafetyNetRequirements
+
+    interface Mapper : ConfigMapper<LogUploadConfig>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
index a55c67fc24a41d1c6d884ce5298a325ce5ee02e5..92a3d6849f65f8f840adc2c7da261670658721b1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
@@ -5,6 +5,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
 import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
+import de.rki.coronawarnapp.appconfig.LogUploadConfig
 import de.rki.coronawarnapp.appconfig.SurveyConfig
 import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 
@@ -20,4 +21,6 @@ interface ConfigMapping :
     val survey: SurveyConfig
 
     val analytics: AnalyticsConfig
+
+    val logUpload: LogUploadConfig
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
index 6c50b4cb724b4ef47b321eda5bdf788b3c186ff2..e9bf15f00f367c91d89bc5fca9e0d7c38cc16507 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
@@ -6,6 +6,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
 import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
+import de.rki.coronawarnapp.appconfig.LogUploadConfig
 import de.rki.coronawarnapp.appconfig.SurveyConfig
 import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 import timber.log.Timber
@@ -18,7 +19,8 @@ class ConfigParser @Inject constructor(
     private val exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper,
     private val exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper,
     private val surveyConfigMapper: SurveyConfig.Mapper,
-    private val analyticsConfigMapper: AnalyticsConfig.Mapper
+    private val analyticsConfigMapper: AnalyticsConfig.Mapper,
+    private val logUploadConfigMapper: LogUploadConfig.Mapper,
 ) {
 
     fun parse(configBytes: ByteArray): ConfigMapping = try {
@@ -30,7 +32,8 @@ class ConfigParser @Inject constructor(
                 exposureDetectionConfig = exposureDetectionConfigMapper.map(it),
                 exposureWindowRiskCalculationConfig = exposureWindowRiskCalculationConfigMapper.map(it),
                 survey = surveyConfigMapper.map(it),
-                analytics = analyticsConfigMapper.map(it)
+                analytics = analyticsConfigMapper.map(it),
+                logUpload = logUploadConfigMapper.map(it)
             )
         }
     } catch (e: Exception) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
index f7063cbff7fd53434f584136dfbdf603b0524ae0..9ebbe211688cb22f5222f1b29e9d2d2b64577004 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
@@ -5,6 +5,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
 import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
+import de.rki.coronawarnapp.appconfig.LogUploadConfig
 import de.rki.coronawarnapp.appconfig.SurveyConfig
 import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 
@@ -15,7 +16,8 @@ data class DefaultConfigMapping(
     val exposureDetectionConfig: ExposureDetectionConfig,
     val exposureWindowRiskCalculationConfig: ExposureWindowRiskCalculationConfig,
     override val survey: SurveyConfig,
-    override val analytics: AnalyticsConfig
+    override val analytics: AnalyticsConfig,
+    override val logUpload: LogUploadConfig
 ) : ConfigMapping,
     CWAConfig by cwaConfig,
     KeyDownloadConfig by keyDownloadConfig,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6d30be7e16d471111f4bf239190da15c30d3f325
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt
@@ -0,0 +1,38 @@
+package de.rki.coronawarnapp.appconfig.mapping
+
+import dagger.Reusable
+import de.rki.coronawarnapp.appconfig.LogUploadConfig
+import de.rki.coronawarnapp.appconfig.SafetyNetRequirements
+import de.rki.coronawarnapp.appconfig.SafetyNetRequirementsContainer
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class LogUploadConfigMapper @Inject constructor() : LogUploadConfig.Mapper {
+
+    override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): LogUploadConfig {
+        if (!rawConfig.hasErrorLogSharingParameters()) {
+            Timber.w("No error log sharing parameters found, returning defaults.")
+            return LogUploadConfigContainer()
+        }
+        return LogUploadConfigContainer(
+            safetyNetRequirements = rawConfig.mapSafetyNet()
+        )
+    }
+
+    private fun AppConfigAndroid.ApplicationConfigurationAndroid.mapSafetyNet(): SafetyNetRequirementsContainer {
+        return privacyPreservingAnalyticsParameters.ppac.let {
+            SafetyNetRequirementsContainer(
+                requireBasicIntegrity = it.requireBasicIntegrity,
+                requireCTSProfileMatch = it.requireCTSProfileMatch,
+                requireEvaluationTypeBasic = it.requireEvaluationTypeBasic,
+                requireEvaluationTypeHardwareBacked = it.requireEvaluationTypeHardwareBacked
+            )
+        }
+    }
+
+    data class LogUploadConfigContainer(
+        override val safetyNetRequirements: SafetyNetRequirements = SafetyNetRequirementsContainer()
+    ) : LogUploadConfig
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt
index e4b7c3f1469951470ca05c7afd99f879ca70e6d0..c8a451d8003001bc7b7e40e6c82e299642b45d22 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt
@@ -2,7 +2,7 @@ package de.rki.coronawarnapp.bugreporting
 
 import android.content.Context
 import com.google.gson.Gson
-import de.rki.coronawarnapp.bugreporting.uploadhistory.UploadHistory
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory
 import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.util.preferences.FlowPreference
 import de.rki.coronawarnapp.util.preferences.clearAndNotify
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
index 542382df7aeae2ba283643c398c4b1c8fcff02d2..70cf264d396708e97412548538bd50aafa167d42 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.bugreporting
 
 import dagger.Module
 import dagger.Provides
+import dagger.Reusable
 import de.rki.coronawarnapp.bugreporting.censors.BugCensor
 import de.rki.coronawarnapp.bugreporting.censors.DiaryLocationCensor
 import de.rki.coronawarnapp.bugreporting.censors.DiaryPersonCensor
@@ -9,14 +10,54 @@ import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor
 import de.rki.coronawarnapp.bugreporting.censors.RegistrationTokenCensor
 import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLoggerScope
 import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadApiV1
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthApiV1
+import de.rki.coronawarnapp.environment.bugreporting.LogUploadHttpClient
+import de.rki.coronawarnapp.environment.bugreporting.LogUploadServerUrl
+import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNHttpClient
+import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNServerUrl
 import de.rki.coronawarnapp.util.CWADebug
 import kotlinx.coroutines.CoroutineScope
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import retrofit2.converter.protobuf.ProtoConverterFactory
 import timber.log.Timber
 import javax.inject.Singleton
 
 @Module
 class BugReportingSharedModule {
 
+    @Reusable
+    @Provides
+    fun logUploadApi(
+        @LogUploadHttpClient client: OkHttpClient,
+        @LogUploadServerUrl url: String,
+        protoConverterFactory: ProtoConverterFactory,
+        gsonConverterFactory: GsonConverterFactory
+    ): LogUploadApiV1 = Retrofit.Builder()
+        .client(client)
+        .baseUrl(url)
+        .addConverterFactory(protoConverterFactory)
+        .addConverterFactory(gsonConverterFactory)
+        .build()
+        .create(LogUploadApiV1::class.java)
+
+    @Reusable
+    @Provides
+    fun logUploadAuthApi(
+        @DataDonationCDNHttpClient client: OkHttpClient,
+        @DataDonationCDNServerUrl url: String,
+        protoConverterFactory: ProtoConverterFactory,
+        gsonConverterFactory: GsonConverterFactory
+    ): LogUploadAuthApiV1 = Retrofit.Builder()
+        .client(client)
+        .baseUrl(url)
+        .addConverterFactory(protoConverterFactory)
+        .addConverterFactory(gsonConverterFactory)
+        .build()
+        .create(LogUploadAuthApiV1::class.java)
+
     @Singleton
     @Provides
     fun debugLogger() = CWADebug.debugLogger
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharing.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt
similarity index 90%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharing.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt
index 31df4941d7ce7521bfc216685519d1509e9e867f..12d91f7ef912e45f33766f9838c2a8499e784c01 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharing.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt
@@ -1,8 +1,9 @@
-package de.rki.coronawarnapp.bugreporting.debuglog.sharing
+package de.rki.coronawarnapp.bugreporting.debuglog.export
 
 import android.content.ContentResolver
 import android.content.Intent
 import android.net.Uri
+import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter
 import de.rki.coronawarnapp.util.files.determineMimeType
 import okio.buffer
 import okio.sink
@@ -12,7 +13,7 @@ import javax.inject.Inject
 import javax.inject.Singleton
 
 @Singleton
-class SAFLogSharing @Inject constructor() {
+class SAFLogExport @Inject constructor() {
     private var lastId = 1
     private val requestMap = mutableMapOf<Int, Request>()
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt
similarity index 90%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt
index 1cb67b8deb4352f0fcbd6c54db2722ed38e80ace..8bf4d816bfdff47cc35fe8a7535243b206097d78 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.bugreporting.debuglog.sharing
+package de.rki.coronawarnapp.bugreporting.debuglog.internal
 
 import android.content.Context
 import dagger.Reusable
@@ -27,8 +27,9 @@ class LogSnapshotter @Inject constructor(
      */
     fun snapshot(): Snapshot {
         Timber.tag(TAG).d("snapshot()")
+
         snapshotDir.listFiles()?.forEach {
-            Timber.tag(TAG).w("Deleting stale snapshot: %s", it)
+            if (it.delete()) Timber.tag(TAG).w("Deleted stale snapshot: %s", it)
         }
 
         val now = timeStamper.nowUTC
@@ -46,9 +47,7 @@ class LogSnapshotter @Inject constructor(
         return Snapshot(path = zipFile)
     }
 
-    data class Snapshot(
-        val path: File
-    ) {
+    data class Snapshot(val path: File) {
         fun delete() = path.delete()
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt
index 5ead8f2fd6c84190a44a3f36a9476dc05d190902..cad3fa356498e026688e470506e56296c256ed6a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt
@@ -8,8 +8,8 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.bugreporting.BugReportingSettings
 import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
-import de.rki.coronawarnapp.bugreporting.debuglog.sharing.LogSnapshotter
-import de.rki.coronawarnapp.bugreporting.debuglog.sharing.SAFLogSharing
+import de.rki.coronawarnapp.bugreporting.debuglog.export.SAFLogExport
+import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.util.CWADebug
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
@@ -27,7 +27,7 @@ class DebugLogViewModel @AssistedInject constructor(
     private val enfClient: ENFClient,
     bugReportingSettings: BugReportingSettings,
     private val logSnapshotter: LogSnapshotter,
-    private val safLogSharing: SAFLogSharing,
+    private val safLogExport: SAFLogExport,
     private val contentResolver: ContentResolver,
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
@@ -51,8 +51,8 @@ class DebugLogViewModel @AssistedInject constructor(
     }.asLiveData(context = dispatcherProvider.Default)
 
     val errorEvent = SingleLiveEvent<Throwable>()
-    val shareEvent = SingleLiveEvent<SAFLogSharing.Request>()
-    val logStoreResult = SingleLiveEvent<SAFLogSharing.Request.Result>()
+    val shareEvent = SingleLiveEvent<SAFLogExport.Request>()
+    val logStoreResult = SingleLiveEvent<SAFLogExport.Request.Result>()
 
     fun onPrivacyButtonPress() {
         routeToScreen.postValue(DebugLogNavigationEvents.NavigateToPrivacyFragment)
@@ -82,15 +82,10 @@ class DebugLogViewModel @AssistedInject constructor(
         }
     }
 
-    fun onUploadLog() = launchWithProgress {
-        Timber.d("uploadLog()")
-        throw NotImplementedError("TODO")
-    }
-
     fun onStoreLog() = launchWithProgress(finishProgressAction = false) {
         Timber.d("storeLog()")
         val snapshot = logSnapshotter.snapshot()
-        val shareRequest = safLogSharing.createSAFRequest(snapshot)
+        val shareRequest = safLogExport.createSAFRequest(snapshot)
         shareEvent.postValue(shareRequest)
     }
 
@@ -100,7 +95,7 @@ class DebugLogViewModel @AssistedInject constructor(
             return@launchWithProgress
         }
 
-        val request = safLogSharing.getRequest(requestCode)
+        val request = safLogExport.getRequest(requestCode)
         if (request == null) {
             Timber.w("Unknown request with code $requestCode")
             return@launchWithProgress
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt
index 4a776f13f98c0c7a0549ead9fb376d4388291e69..6aac39aad2378643f4bd87797f38c2fb34e66bb8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt
@@ -3,10 +3,13 @@ package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload
 import android.os.Bundle
 import android.view.View
 import android.view.accessibility.AccessibilityEvent
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
 import androidx.fragment.app.Fragment
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.BugreportingDebuglogUploadFragmentBinding
 import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.tryHumanReadableError
 import de.rki.coronawarnapp.util.ui.doNavigate
 import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.popBackStack
@@ -20,12 +23,14 @@ class DebugLogUploadFragment : Fragment(R.layout.bugreporting_debuglog_upload_fr
     @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
     private val vm: DebugLogUploadViewModel by cwaViewModels { viewModelFactory }
     private val binding: BugreportingDebuglogUploadFragmentBinding by viewBindingLazy()
+    private lateinit var uploadDialog: LogUploadBlockingDialog
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
-        binding.apply {
+        uploadDialog = LogUploadBlockingDialog(requireContext())
 
-            debugLogShareButton.setOnClickListener {
+        binding.apply {
+            uploadAction.setOnClickListener {
                 vm.onUploadLog()
             }
 
@@ -37,7 +42,23 @@ class DebugLogUploadFragment : Fragment(R.layout.bugreporting_debuglog_upload_fr
         }
 
         vm.routeToScreen.observe2(this) {
-            doNavigate(it)
+            when (it) {
+                null -> popBackStack()
+                else -> doNavigate(it)
+            }
+        }
+
+        vm.errorEvent.observe2(this) {
+            AlertDialog.Builder(requireContext()).apply {
+                val errorForHumans = it.tryHumanReadableError(requireContext())
+                setTitle(errorForHumans.title ?: getString(R.string.errors_generic_headline))
+                setMessage(errorForHumans.description)
+            }.show()
+        }
+
+        vm.uploadInProgress.observe2(this) { uploadDialog.setState(it) }
+        vm.uploadSuccess.observe2(this) {
+            Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt
index 0f08dc57c0858b022d14d2fba0fcc1a0cea4c310..1f058f22ae5daa60cc0edaf43782e53264a10944 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt
@@ -1,21 +1,35 @@
 package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload
 
+import androidx.lifecycle.MutableLiveData
 import androidx.navigation.NavDirections
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.ui.SingleLiveEvent
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.SnapshotUploader
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
 
 class DebugLogUploadViewModel @AssistedInject constructor(
     dispatcherProvider: DispatcherProvider,
+    private val snapshotUploader: SnapshotUploader
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
+    val routeToScreen: SingleLiveEvent<NavDirections?> = SingleLiveEvent()
+    val uploadInProgress = MutableLiveData(false)
+    val errorEvent = SingleLiveEvent<Throwable>()
+    val uploadSuccess = SingleLiveEvent<String>()
 
-    val routeToScreen = SingleLiveEvent<NavDirections>()
-
-    fun onUploadLog() {
-        // TODO Implement Uploading
+    fun onUploadLog() = launch {
+        uploadInProgress.postValue(true)
+        try {
+            snapshotUploader.uploadSnapshot()
+            uploadSuccess.postValue("\uD83D\uDC4D")
+            routeToScreen.postValue(null)
+        } catch (e: Throwable) {
+            errorEvent.postValue(e)
+        } finally {
+            uploadInProgress.postValue(false)
+        }
     }
 
     fun onPrivacyButtonPress() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt
new file mode 100644
index 0000000000000000000000000000000000000000..07c7289b695d34fc2f25ba4bb98b7bf14544ec9f
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt
@@ -0,0 +1,23 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload
+
+import android.content.Context
+import androidx.appcompat.app.AlertDialog
+import de.rki.coronawarnapp.R
+
+class LogUploadBlockingDialog(val context: Context) {
+
+    private val dialog by lazy {
+        AlertDialog.Builder(context).apply {
+            setCancelable(false)
+            setView(R.layout.bugreporting_debuglog_upload_dialog)
+        }.create()
+    }
+
+    fun setState(show: Boolean) {
+        if (show && !dialog.isShowing) {
+            dialog.show()
+        } else if (!show && dialog.isShowing) {
+            dialog.dismiss()
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/HistoryItemAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt
similarity index 58%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/HistoryItemAdapter.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt
index 940dc79f79ef1d8918c83f2433e1c2947819a3dd..969b307edf350297b18bfc3090774b8b5b72f5c3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/HistoryItemAdapter.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt
@@ -1,13 +1,17 @@
-package de.rki.coronawarnapp.bugreporting.uploadhistory.ui
+package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history
 
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
 import android.view.ViewGroup
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.bugreporting.uploadhistory.LogUpload
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload
 import de.rki.coronawarnapp.databinding.BugreportingUploadHistoryItemBinding
 import de.rki.coronawarnapp.ui.lists.BaseAdapter
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
 import de.rki.coronawarnapp.util.lists.BindableVH
 import org.joda.time.format.DateTimeFormat
+import timber.log.Timber
 
 class HistoryItemAdapter : BaseAdapter<HistoryItemAdapter.CachedKeyViewHolder>() {
 
@@ -36,6 +40,23 @@ class HistoryItemAdapter : BaseAdapter<HistoryItemAdapter.CachedKeyViewHolder>()
         ) -> Unit = { item, _ ->
             title.text = FORMATTER.print(item.uploadedAt.toUserTimeZone())
             description.text = "ID ${item.id}"
+            itemView.setOnClickListener {
+                try {
+                    val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+                    clipboard.setPrimaryClip(
+                        ClipData.newPlainText(
+                            context.getString(R.string.debugging_debuglog_share_log_title),
+                            """
+                                ${context.getString(R.string.debugging_debuglog_share_log_title)}
+                                ${title.text}
+                                ${description.text} 
+                            """.trimIndent()
+                        )
+                    )
+                } catch (e: Throwable) {
+                    Timber.e(e, "Failed to copy ID to clipboard.")
+                }
+            }
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt
similarity index 95%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryFragment.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt
index 2a8a68daee045046c296603bda19fcddc131abe9..a608c49a4547607b6d9e6b43249bcaf42429444e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.bugreporting.uploadhistory.ui
+package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history
 
 import android.os.Bundle
 import android.view.View
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt
similarity index 90%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryModule.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt
index e5948de68962f9e6b64b1cb266ad67bfe4035cba..7ecbd6b53ece2cf03d47022633f75a3e1a21fb70 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.bugreporting.uploadhistory.ui
+package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history
 
 import dagger.Binds
 import dagger.Module
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt
similarity index 87%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryViewModel.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt
index 45041e40cce6ff592e361930a1f0d7e575eeade2..93d6191cb3d580b873b522771439ed0b840b41c4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/ui/LogUploadHistoryViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt
@@ -1,11 +1,11 @@
-package de.rki.coronawarnapp.bugreporting.uploadhistory.ui
+package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history
 
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.asLiveData
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.bugreporting.BugReportingSettings
-import de.rki.coronawarnapp.bugreporting.uploadhistory.LogUpload
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b2c1dac5df52dc5dac5f970de091243340d8954a
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt
@@ -0,0 +1,58 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload
+
+import de.rki.coronawarnapp.bugreporting.BugReportingSettings
+import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadServer
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthorizer
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class SnapshotUploader @Inject constructor(
+    private val snapshotter: LogSnapshotter,
+    private val uploadServer: LogUploadServer,
+    private val authorizer: LogUploadAuthorizer,
+    private val bugReportingSettings: BugReportingSettings
+) {
+
+    suspend fun uploadSnapshot(): LogUpload {
+        Timber.tag(TAG).v("uploadSnapshot()")
+
+        val authorizedOtp = authorizer.getAuthorizedOTP().also {
+            Timber.tag(TAG).d("Authorized OTP obtained: %s", it)
+        }
+
+        val snapshot = snapshotter.snapshot().also {
+            Timber.tag(TAG).d("Snapshot created: %s", it)
+        }
+
+        val logUpload = try {
+            uploadServer.uploadLog(authorizedOtp, snapshot).also {
+                Timber.tag(TAG).d("Log uploaded: %s", it)
+            }
+        } finally {
+            snapshot.delete().also {
+                Timber.tag(TAG).d("Snapshot was deleted after upload: %b", it)
+            }
+        }
+
+        bugReportingSettings.uploadHistory.update { oldHistory ->
+            val newLogs = oldHistory.logs.toMutableList()
+            if (newLogs.size >= 10) {
+                newLogs.removeFirst().also {
+                    Timber.tag(TAG).d("Removed oldest entry from history: %s", it)
+                }
+            }
+            newLogs.add(logUpload)
+            oldHistory.copy(logs = newLogs)
+        }
+
+        return logUpload
+    }
+
+    companion object {
+        private const val TAG = "SnapshotUploader"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/LogUpload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt
similarity index 60%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/LogUpload.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt
index 798ea60b434c73f3810b5bb7fd378c7411606224..e5edc8b30101ce568dd0980d554355356e2510cf 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/LogUpload.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.bugreporting.uploadhistory
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.history
 
 import org.joda.time.Instant
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/UploadHistory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt
similarity index 52%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/UploadHistory.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt
index 6a51bfa21b5798453265917bcf3402c84f2ff4d1..fa8dc33641b885984586503f6ebfe2690c77b4a9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/uploadhistory/UploadHistory.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.bugreporting.uploadhistory
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.history
 
 data class UploadHistory(
     val logs: List<LogUpload> = emptyList()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..12223408be7053f0b72f1b934383610b097d892e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt
@@ -0,0 +1,24 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server
+
+import com.google.gson.annotations.SerializedName
+import okhttp3.MultipartBody
+import retrofit2.http.Header
+import retrofit2.http.Multipart
+import retrofit2.http.POST
+import retrofit2.http.Part
+
+interface LogUploadApiV1 {
+
+    @Multipart
+    @POST("/api/logs")
+    suspend fun uploadLog(
+        @Header("cwa-otp") otp: String,
+        @Part logZip: MultipartBody.Part
+    ): UploadResponse
+
+    data class UploadResponse(
+        @SerializedName("id") val id: String,
+        @SerializedName("hash") val hash: String?,
+        @SerializedName("errorCode") val errorCode: String?
+    )
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4cc673a1dca2acf4a692fb2ab56c7fedaaf9a7fa
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt
@@ -0,0 +1,42 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server
+
+import dagger.Lazy
+import dagger.Reusable
+import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp
+import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.util.files.determineMimeType
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class LogUploadServer @Inject constructor(
+    private val uploadApiProvider: Lazy<LogUploadApiV1>,
+    private val timeStamper: TimeStamper
+) {
+
+    private val uploadApi: LogUploadApiV1
+        get() = uploadApiProvider.get()
+
+    suspend fun uploadLog(uploadOtp: LogUploadOtp, snapshot: LogSnapshotter.Snapshot): LogUpload {
+        val response = uploadApi.uploadLog(
+            otp = uploadOtp.otp,
+            logZip = MultipartBody.Part.createFormData(
+                name = "file",
+                filename = snapshot.path.name,
+                body = snapshot.path.asRequestBody(snapshot.path.determineMimeType().toMediaType())
+            )
+        )
+        Timber.tag(TAG).d("Upload response: %s", response)
+
+        return LogUpload(id = response.id, uploadedAt = timeStamper.nowUTC)
+    }
+
+    companion object {
+        private const val TAG = "LogUploadServer"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ef9ea5988795eccfa701d6bd790727d7c3ab69f7
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt
@@ -0,0 +1,22 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth
+
+import com.google.gson.annotations.SerializedName
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface LogUploadAuthApiV1 {
+
+    data class AuthResponse(
+        @SerializedName("expirationDate") val expirationDate: String
+    )
+
+    data class AuthError(
+        @SerializedName("errorCode") val errorCode: String?
+    )
+
+    @POST("version/v1/android/log")
+    suspend fun authOTP(
+        @Body requestBody: ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid
+    ): AuthResponse
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8a0a77ce39de5095615995934125dbd28da67e2c
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt
@@ -0,0 +1,63 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth
+
+import dagger.Lazy
+import dagger.Reusable
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtp
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid
+import kotlinx.coroutines.flow.first
+import org.joda.time.Instant
+import timber.log.Timber
+import java.util.UUID
+import javax.inject.Inject
+
+@Reusable
+class LogUploadAuthorizer @Inject constructor(
+    private val authApiProvider: Lazy<LogUploadAuthApiV1>,
+    private val deviceAttestation: DeviceAttestation,
+    private val configProvider: AppConfigProvider
+) {
+
+    private val authApi: LogUploadAuthApiV1
+        get() = authApiProvider.get()
+
+    suspend fun getAuthorizedOTP(otp: UUID = UUID.randomUUID()): LogUploadOtp {
+        Timber.tag(TAG).d("getAuthorizedOTP() trying to authorize %s", otp)
+
+        val elsOtp = ElsOtp.ELSOneTimePassword.newBuilder().apply {
+            setOtp(otp.toString())
+        }.build()
+
+        val appConfig = configProvider.currentConfig.first()
+
+        val attestationRequest = object : DeviceAttestation.Request {
+            override val configData: ConfigData = appConfig
+            override val checkDeviceTime: Boolean = false
+            override val scenarioPayload: ByteArray = elsOtp.toByteArray()
+        }
+        val attestionResult = deviceAttestation.attest(attestationRequest)
+        Timber.tag(TAG).d("Attestation passed, requesting authorization from server for %s", attestionResult)
+
+        attestionResult.requirePass(appConfig.logUpload.safetyNetRequirements)
+
+        val elsRequest = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder().apply {
+            authentication = attestionResult.accessControlProtoBuf
+            payload = elsOtp
+        }.build()
+
+        // TODO This was written without backend available, retest.
+        val authResponse = authApi.authOTP(elsRequest).also {
+            Timber.tag(TAG).v("Auth response received: %s", it)
+        }
+
+        return LogUploadOtp(otp = otp.toString(), expirationDate = Instant.parse(authResponse.expirationDate)).also {
+            Timber.tag(TAG).d("%s created", it)
+        }
+    }
+
+    companion object {
+        private const val TAG = "LogUploadOtpServer"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8c9532da72554a2533d82b6a3aa78187a5087b69
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt
@@ -0,0 +1,8 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth
+
+import org.joda.time.Instant
+
+data class LogUploadOtp(
+    val otp: String,
+    val expirationDate: Instant
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt
index a35fb01fba3801ff429094d5c863dc25478f1dab..dcf91e1f6f93e69204fb752aacc52d38168133aa 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt
@@ -4,6 +4,7 @@ import de.rki.coronawarnapp.appconfig.SafetyNetRequirements
 import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException.Type
 import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid
 import okio.ByteString.Companion.toByteString
+import timber.log.Timber
 
 internal data class AttestationContainer(
     private val ourSalt: ByteArray,
@@ -16,6 +17,8 @@ internal data class AttestationContainer(
         }.build()
 
     override fun requirePass(reqs: SafetyNetRequirements) {
+        Timber.v("requirePass(%s)", reqs)
+
         if (reqs.requireBasicIntegrity && !report.basicIntegrity) {
             throw SafetyNetException(
                 Type.BASIC_INTEGRITY_REQUIRED,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt
index e29f43cc5c8ea7baca9a7cd4b3f4a409ce4e2b83..3e39b6d5075d56c1e8f146d99047c01b90accc54 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt
@@ -51,26 +51,11 @@ class CWASafetyNet @Inject constructor(
             throw SafetyNetException(Type.PLAY_SERVICES_VERSION_MISMATCH, "Google Play Services too old.")
         }
 
-        appConfigProvider.getAppConfig().apply {
-            if (deviceTimeState == ConfigData.DeviceTimeState.ASSUMED_CORRECT) {
-                throw SafetyNetException(Type.DEVICE_TIME_UNVERIFIED, "Device time is unverified")
-            }
-            if (deviceTimeState == ConfigData.DeviceTimeState.INCORRECT) {
-                throw SafetyNetException(Type.DEVICE_TIME_INCORRECT, "Device time is incorrect")
-            }
-        }
-
-        val skip24hCheck = CWADebug.isDeviceForTestersBuild && testSettings.skipSafetyNetTimeCheck.value
-        val nowUTC = timeStamper.nowUTC
-        val firstReliableTimeStamp = cwaSettings.firstReliableDeviceTime
-        val timeSinceOnboarding = Duration(firstReliableTimeStamp, nowUTC)
-        Timber.d("firstReliableTimeStamp=%s, now=%s", firstReliableTimeStamp, nowUTC)
-        Timber.d("skip24hCheck=%b, timeSinceOnboarding=%dh", skip24hCheck, timeSinceOnboarding.standardHours)
-
-        if (firstReliableTimeStamp == Instant.EPOCH) {
-            throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "No first reliable timestamp available")
-        } else if (!skip24hCheck && timeSinceOnboarding < Duration.standardHours(24)) {
-            throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "Time since first reliable timestamp <24h")
+        if (request.checkDeviceTime) {
+            Timber.tag(TAG).d("Checking device time.")
+            requireCorrectDeviceTime(request.configData)
+        } else {
+            Timber.tag(TAG).d("Device time check not required.")
         }
 
         val salt = generateSalt()
@@ -105,6 +90,32 @@ class CWASafetyNet @Inject constructor(
         return AttestationContainer(salt, report)
     }
 
+    private suspend fun requireCorrectDeviceTime(suppliedConfig: ConfigData?) {
+        val configData = suppliedConfig ?: appConfigProvider.getAppConfig()
+
+        configData.apply {
+            if (deviceTimeState == ConfigData.DeviceTimeState.ASSUMED_CORRECT) {
+                throw SafetyNetException(Type.DEVICE_TIME_UNVERIFIED, "Device time is unverified")
+            }
+            if (deviceTimeState == ConfigData.DeviceTimeState.INCORRECT) {
+                throw SafetyNetException(Type.DEVICE_TIME_INCORRECT, "Device time is incorrect")
+            }
+        }
+
+        val skip24hCheck = CWADebug.isDeviceForTestersBuild && testSettings.skipSafetyNetTimeCheck.value
+        val nowUTC = timeStamper.nowUTC
+        val firstReliableTimeStamp = cwaSettings.firstReliableDeviceTime
+        val timeSinceOnboarding = Duration(firstReliableTimeStamp, nowUTC)
+        Timber.d("firstReliableTimeStamp=%s, now=%s", firstReliableTimeStamp, nowUTC)
+        Timber.d("skip24hCheck=%b, timeSinceOnboarding=%dh", skip24hCheck, timeSinceOnboarding.standardHours)
+
+        if (firstReliableTimeStamp == Instant.EPOCH) {
+            throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "No first reliable timestamp available")
+        } else if (!skip24hCheck && timeSinceOnboarding < Duration.standardHours(24)) {
+            throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "Time since first reliable timestamp <24h")
+        }
+    }
+
     companion object {
         private const val TAG = "CWASafetyNet"
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt
index 6222c05f6c04605ec0e68b27c5a570cb8bf96be4..945f7ef47deb26608595a11193d3c3d66d96c3ad 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt
@@ -1,5 +1,6 @@
 package de.rki.coronawarnapp.datadonation.safetynet
 
+import de.rki.coronawarnapp.appconfig.ConfigData
 import de.rki.coronawarnapp.appconfig.SafetyNetRequirements
 import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid
 
@@ -13,6 +14,13 @@ interface DeviceAttestation {
     suspend fun attest(request: Request): Result
 
     interface Request {
+
+        val configData: ConfigData?
+            get() = null
+
+        val checkDeviceTime: Boolean
+            get() = true
+
         /**
          * e.g. for EventSurvey, a UUID, base64 encoded.
          */
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt
index b83bd416320695e997e6f5e36bfda2a3888614ab..b99710e6d4a6bd5a07a5bb7d08931aecf4594746 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.environment
 
 import dagger.Module
+import de.rki.coronawarnapp.environment.bugreporting.BugReportingServerModule
 import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNModule
 import de.rki.coronawarnapp.environment.download.DownloadCDNModule
 import de.rki.coronawarnapp.environment.submission.SubmissionCDNModule
@@ -11,7 +12,8 @@ import de.rki.coronawarnapp.environment.verification.VerificationCDNModule
         DownloadCDNModule::class,
         SubmissionCDNModule::class,
         VerificationCDNModule::class,
-        DataDonationCDNModule::class
+        DataDonationCDNModule::class,
+        BugReportingServerModule::class
     ]
 )
 class EnvironmentModule
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
index 76bfaa4a444f2fdf833d31b60efe3f1de711923a..1476022658f27fc6711345379d875e055c8098d3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
@@ -7,6 +7,7 @@ import com.google.gson.JsonObject
 import com.google.gson.JsonPrimitive
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DATA_DONATION
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DOWNLOAD
+import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.LOG_UPLOAD
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.SAFETYNET_API_KEY
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.SUBMISSION
 import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.USE_EUR_KEY_PKGS
@@ -33,6 +34,7 @@ class EnvironmentSetup @Inject constructor(
         DOWNLOAD("DOWNLOAD_CDN_URL"),
         VERIFICATION_KEYS("PUB_KEYS_SIGNATURE_VERIFICATION"),
         DATA_DONATION("DATA_DONATION_CDN_URL"),
+        LOG_UPLOAD("LOG_UPLOAD_SERVER_URL"),
         SAFETYNET_API_KEY("SAFETYNET_API_KEY")
     }
 
@@ -123,6 +125,9 @@ class EnvironmentSetup @Inject constructor(
     val safetyNetApiKey: String
         get() = getEnvironmentValue(SAFETYNET_API_KEY).asString
 
+    val logUploadServerUrl: String
+        get() = getEnvironmentValue(LOG_UPLOAD).asString
+
     companion object {
         private const val PKEY_CURRENT_ENVINROMENT = "environment.current"
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..652903409aa6e11fc50990477e90b5a77bfa4d28
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt
@@ -0,0 +1,28 @@
+package de.rki.coronawarnapp.environment.bugreporting
+
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+import de.rki.coronawarnapp.environment.BaseEnvironmentModule
+import de.rki.coronawarnapp.environment.EnvironmentSetup
+import de.rki.coronawarnapp.http.HttpClientDefault
+import okhttp3.OkHttpClient
+import javax.inject.Singleton
+
+@Module
+class BugReportingServerModule : BaseEnvironmentModule() {
+
+    @Reusable
+    @LogUploadHttpClient
+    @Provides
+    fun cdnHttpClient(@HttpClientDefault defaultHttpClient: OkHttpClient): OkHttpClient =
+        defaultHttpClient.newBuilder().build()
+
+    @Singleton
+    @LogUploadServerUrl
+    @Provides
+    fun provideBugReportingServerUrl(environment: EnvironmentSetup): String {
+        val url = environment.logUploadServerUrl
+        return requireValidUrl(url)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt
new file mode 100644
index 0000000000000000000000000000000000000000..039fd9ef69478b3a464e3ff7061281a72eebb7a6
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt
@@ -0,0 +1,8 @@
+package de.rki.coronawarnapp.environment.bugreporting
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class LogUploadHttpClient
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..932c084aa72c7476b86d585d6894c25e87f198c4
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt
@@ -0,0 +1,8 @@
+package de.rki.coronawarnapp.environment.bugreporting
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class LogUploadServerUrl
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt
index 9082a6018ae3a0061bbdfb049661b47c8cfd5c98..d80f14654c58ce92655b071703e815822d1a9d9b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt
@@ -7,7 +7,7 @@ import dagger.multibindings.IntoMap
 import de.rki.coronawarnapp.bugreporting.debuglog.ui.DebugLogFragmentModule
 import de.rki.coronawarnapp.bugreporting.debuglog.ui.legal.DebugLogLegalModule
 import de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.DebugLogUploadFragmentModule
-import de.rki.coronawarnapp.bugreporting.uploadhistory.ui.LogUploadHistoryModule
+import de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history.LogUploadHistoryModule
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7a719f9e6982e7503cc6c138eb329dd4ce278869
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <ProgressBar
+        android:id="@+id/progress_indicator"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        style="@style/Widget.AppCompat.ProgressBar"
+        android:layout_marginTop="16dp"
+        android:layout_marginBottom="16dp"
+        android:progressTint="@color/colorAccentTintIcon"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/progress_message"
+        style="@style/body1"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginEnd="16dp"
+        android:text="@string/debugging_debuglog_share_log_title"
+        app:layout_constraintBottom_toBottomOf="@+id/progress_indicator"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toEndOf="@+id/progress_indicator"
+        app:layout_constraintTop_toTopOf="@+id/progress_indicator" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml
index db9ac122d831b73eafe4462148217b80694f99c3..236c03b46084c06bf16e5abe51d1809b155e74c8 100644
--- a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml
+++ b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml
@@ -24,7 +24,7 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:layout_marginBottom="@dimen/spacing_small"
-        app:layout_constraintBottom_toTopOf="@id/debug_log_share_button"
+        app:layout_constraintBottom_toTopOf="@id/upload_action"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/toolbar">
@@ -125,7 +125,7 @@
     </ScrollView>
 
     <android.widget.Button
-        android:id="@+id/debug_log_share_button"
+        android:id="@+id/upload_action"
         style="@style/buttonPrimary"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml
index 54d5d5f1b066470162689e2f6e514020550a759c..f9a3c78a3a47fa9780ae2c173d4246d38d66c808 100644
--- a/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml
+++ b/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml
@@ -3,7 +3,8 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:background="?selectableItemBackground">
 
     <TextView
         android:id="@+id/title"
@@ -23,6 +24,7 @@
         android:layout_height="wrap_content"
         android:layout_marginTop="4dp"
         android:layout_marginBottom="16dp"
+        android:textIsSelectable="true"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="@id/title"
         app:layout_constraintStart_toStartOf="@id/title"
diff --git a/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml b/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml
index 40cf09352152cdeeae077ace3141b38048ff4d62..3ae19f36bc16dedab8b84a46e54bdb24e113bbe3 100644
--- a/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml
+++ b/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml
@@ -23,7 +23,7 @@
         android:layout_height="wrap_content"
         android:layout_marginStart="16dp"
         android:layout_marginEnd="16dp"
-        android:text="@string/submission_status_card_title_fetching"
+        android:text="@string/submission_test_result_pending_steps_waiting_body"
         app:layout_constraintBottom_toBottomOf="@+id/progress_indicator"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintHorizontal_bias="0.0"
diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
index 718e593f8862ccfcb8f28982da6e0a08ede9ac9d..3f864197f4d746404191f1e46ff49d39f8d020f4 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -588,7 +588,7 @@
     </fragment>
     <fragment
         android:id="@+id/logUploadHistoryFragment"
-        android:name="de.rki.coronawarnapp.bugreporting.uploadhistory.ui.LogUploadHistoryFragment"
+        android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history.LogUploadHistoryFragment"
         android:label="LogUploadHistoryFragment"
         tools:layout="@layout/bugreporting_upload_history_fragment" />
     <fragment
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
index 46082ad4a64534c09ffb7b569c17bff9572ab56a..ab457e8182cffade0934f233d9fd08a9edecacb8 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
@@ -6,6 +6,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
 import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
+import de.rki.coronawarnapp.appconfig.LogUploadConfig
 import de.rki.coronawarnapp.appconfig.SurveyConfig
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
@@ -28,6 +29,7 @@ class ConfigParserTest : BaseTest() {
     @MockK lateinit var exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper
     @MockK lateinit var surveyConfigMapper: SurveyConfig.Mapper
     @MockK lateinit var analyticsConfigMapper: AnalyticsConfig.Mapper
+    @MockK lateinit var logUploadConfigMapper: LogUploadConfig.Mapper
 
     private val appConfig171 = File("src/test/resources/appconfig_1_7_1.bin")
     private val appConfig180 = File("src/test/resources/appconfig_1_8_0.bin")
@@ -42,6 +44,7 @@ class ConfigParserTest : BaseTest() {
         every { exposureWindowRiskCalculationConfigMapper.map(any()) } returns mockk()
         every { surveyConfigMapper.map(any()) } returns mockk()
         every { analyticsConfigMapper.map(any()) } returns mockk()
+        every { logUploadConfigMapper.map(any()) } returns mockk()
 
         appConfig171.exists() shouldBe true
         appConfig180.exists() shouldBe true
@@ -53,7 +56,8 @@ class ConfigParserTest : BaseTest() {
         exposureDetectionConfigMapper = exposureDetectionConfigMapper,
         exposureWindowRiskCalculationConfigMapper = exposureWindowRiskCalculationConfigMapper,
         surveyConfigMapper = surveyConfigMapper,
-        analyticsConfigMapper = analyticsConfigMapper
+        analyticsConfigMapper = analyticsConfigMapper,
+        logUploadConfigMapper = logUploadConfigMapper
     )
 
     @Test
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt
index ccfd7db38dfbdd8f6dee4d5a3a36fae138583fdb..7fec96f255ee4de159f184d69c4b8e8426a9eeaa 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt
@@ -1,8 +1,8 @@
 package de.rki.coronawarnapp.bugreporting
 
 import android.content.Context
-import de.rki.coronawarnapp.bugreporting.uploadhistory.LogUpload
-import de.rki.coronawarnapp.bugreporting.uploadhistory.UploadHistory
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory
 import de.rki.coronawarnapp.util.serialization.SerializationModule
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharingTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt
similarity index 89%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharingTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt
index eb323f351d0bdd530bb575518706fbf5ddbe731f..d7716d1931e92db753827a4b2983257ce7bcc9c7 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharingTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt
@@ -1,6 +1,7 @@
-package de.rki.coronawarnapp.bugreporting.debuglog.sharing
+package de.rki.coronawarnapp.bugreporting.debuglog.export
 
 import android.content.ContentResolver
+import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.shouldNotBe
 import io.mockk.MockKAnnotations
@@ -14,7 +15,7 @@ import testhelpers.BaseIOTest
 import timber.log.Timber
 import java.io.File
 
-class SAFLogSharingTest : BaseIOTest() {
+class SAFLogExportTest : BaseIOTest() {
 
     @MockK lateinit var contentResolver: ContentResolver
 
@@ -40,7 +41,7 @@ class SAFLogSharingTest : BaseIOTest() {
         Timber.uprootAll()
     }
 
-    private fun createInstance() = SAFLogSharing()
+    private fun createInstance() = SAFLogExport()
 
     @Test
     fun `request creation and write`() {
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt
similarity index 97%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt
index f823097df42336229e965cff9951b9a82ef3263c..b4ccfd775f08b740cb3f13e00e4ee8e1b7c3384f 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.bugreporting.debuglog.sharing
+package de.rki.coronawarnapp.bugreporting.debuglog.internal
 
 import android.content.Context
 import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..83d4946117e51104c6b3cfc5af4e2f32fed5c492
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt
@@ -0,0 +1,100 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload
+
+import de.rki.coronawarnapp.bugreporting.BugReportingSettings
+import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadServer
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthorizer
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.verify
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import testhelpers.preferences.mockFlowPreference
+import java.io.IOException
+
+class SnapshotUploaderTest : BaseTest() {
+
+    @MockK lateinit var snapshotter: LogSnapshotter
+    @MockK lateinit var uploadServer: LogUploadServer
+    @MockK lateinit var authorizer: LogUploadAuthorizer
+    @MockK lateinit var bugReportingSettings: BugReportingSettings
+    @MockK lateinit var snapshot: LogSnapshotter.Snapshot
+
+    private val logUploadOtp = LogUploadOtp(
+        otp = "otp",
+        expirationDate = Instant.EPOCH
+    )
+
+    private val uploadHistoryPref = mockFlowPreference(UploadHistory())
+
+    private val expectedLogUpload = LogUpload(
+        id = "123e4567-e89b-12d3-a456-426652340000",
+        uploadedAt = Instant.parse("2020-08-20T23:00:00.000Z")
+    )
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { bugReportingSettings.uploadHistory } returns uploadHistoryPref
+
+        coEvery { authorizer.getAuthorizedOTP(otp = any()) } returns logUploadOtp
+        coEvery { snapshotter.snapshot() } returns snapshot
+        coEvery { uploadServer.uploadLog(logUploadOtp, snapshot) } returns expectedLogUpload
+
+        every { snapshot.delete() } returns true
+    }
+
+    private fun createInstance() = SnapshotUploader(
+        snapshotter = snapshotter,
+        uploadServer = uploadServer,
+        authorizer = authorizer,
+        bugReportingSettings = bugReportingSettings
+    )
+
+    @Test
+    fun `upload a snapshot`() = runBlockingTest {
+        val instance = createInstance()
+        instance.uploadSnapshot() shouldBe expectedLogUpload
+
+        uploadHistoryPref.value shouldBe UploadHistory(logs = listOf(expectedLogUpload))
+    }
+
+    @Test
+    fun `snapshots are deleted on errors too`() = runBlockingTest {
+        coEvery { uploadServer.uploadLog(logUploadOtp, snapshot) } throws IOException()
+
+        val instance = createInstance()
+
+        shouldThrow<IOException> {
+            instance.uploadSnapshot()
+        }
+        verify { snapshot.delete() }
+    }
+
+    @Test
+    fun `upload history is capped at 10`() = runBlockingTest {
+        val existingEntries = (1..10L).map { LogUpload(id = "$it", Instant.ofEpochMilli(it)) }
+        uploadHistoryPref.update { UploadHistory(logs = existingEntries) }
+
+        val instance = createInstance()
+        instance.uploadSnapshot() shouldBe expectedLogUpload
+
+        uploadHistoryPref.value shouldBe UploadHistory(
+            logs = existingEntries.subList(
+                1,
+                10
+            ) + listOf(expectedLogUpload)
+        )
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3fd06b0bdf5ba30d9f165014f305e0fde8db4408
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt
@@ -0,0 +1,132 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server
+
+import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule
+import de.rki.coronawarnapp.environment.download.DownloadCDNModule
+import de.rki.coronawarnapp.exception.http.CwaWebException
+import de.rki.coronawarnapp.http.HttpModule
+import de.rki.coronawarnapp.util.files.determineMimeType
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import kotlinx.coroutines.runBlocking
+import okhttp3.ConnectionSpec
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseIOTest
+import testhelpers.extensions.toJsonResponse
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+class LogUploadApiTest : BaseIOTest() {
+
+    private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName)
+    private lateinit var webServer: MockWebServer
+    private lateinit var serverAddress: String
+    private val otp = "15cff19f-af26-41bc-94f2-c1a65075e894"
+    private val expectedId = "11111111-af26-41bc-94f2-000000000000"
+    private val expectedHash = "22222222-af26-41bc-94f2-000000000000"
+    private val testSnapshotFile = File(testDir, "snapshot.zip")
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        webServer = MockWebServer()
+        webServer.start()
+        serverAddress = "http://${webServer.hostName}:${webServer.port}"
+
+        testDir.mkdirs()
+        testSnapshotFile.writeText("We needed this months ago.")
+        testSnapshotFile.exists() shouldBe true
+    }
+
+    @AfterEach
+    fun teardown() {
+        webServer.shutdown()
+        testDir.deleteRecursively()
+    }
+
+    private fun createAPI(): LogUploadApiV1 {
+        val httpModule = HttpModule()
+        val defaultHttpClient = httpModule.defaultHttpClient()
+        val gsonConverterFactory = httpModule.provideGSONConverter()
+        val protoConverterFactory = httpModule.provideProtoConverter()
+
+        val cdnHttpClient = DownloadCDNModule()
+            .cdnHttpClient(defaultHttpClient)
+            .newBuilder()
+            .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS))
+            .build()
+
+        return BugReportingSharedModule().logUploadApi(
+            client = cdnHttpClient,
+            url = serverAddress,
+            gsonConverterFactory = gsonConverterFactory,
+            protoConverterFactory = protoConverterFactory
+        )
+    }
+
+    @Test
+    fun `happy upload`(): Unit = runBlocking {
+        """
+            {
+                id :  "$expectedId",
+                hash: "$expectedHash"
+            }
+        """.toJsonResponse().apply { webServer.enqueue(this) }
+
+        val api = createAPI()
+        api.uploadLog(
+            otp = otp,
+            logZip = MultipartBody.Part.createFormData(
+                name = "file",
+                filename = testSnapshotFile.name,
+                body = testSnapshotFile.asRequestBody(testSnapshotFile.determineMimeType().toMediaType())
+            )
+        )
+
+        webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply {
+            path shouldBe "/api/logs"
+            val boundary = this.headers["Content-Type"]!!.replace("multipart/form-data; boundary=", "")
+            body.readUtf8().replace("\r\n", "\n") shouldBe """
+                --$boundary
+                Content-Disposition: form-data; name="file"; filename="snapshot.zip"
+                Content-Type: application/zip
+                Content-Length: 26
+                
+                We needed this months ago.
+                --$boundary--
+                
+            """.trimIndent()
+        }
+    }
+
+    @Test
+    fun `server returns 500`(): Unit = runBlocking {
+        """
+            {
+                id :  "$expectedId",
+                hash: "$expectedHash"
+            }
+        """.toJsonResponse().apply { webServer.enqueue(MockResponse().setResponseCode(500)) }
+
+        val api = createAPI()
+
+        shouldThrow<CwaWebException> {
+            api.uploadLog(
+                otp = otp,
+                logZip = MultipartBody.Part.createFormData(
+                    name = "file",
+                    filename = testSnapshotFile.name,
+                    body = testSnapshotFile.asRequestBody(testSnapshotFile.determineMimeType().toMediaType())
+                )
+            )
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ecd46ea090b546a1f439c070fae6633847173fbb
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt
@@ -0,0 +1,66 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server
+
+import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload
+import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp
+import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.runBlocking
+import org.joda.time.Duration
+import org.joda.time.Instant
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseIOTest
+import java.io.File
+
+class LogUploadServerTest : BaseIOTest() {
+
+    private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName)
+    @MockK private lateinit var timeStamper: TimeStamper
+    @MockK private lateinit var uploadApiV1: LogUploadApiV1
+    private val uploadOtp = LogUploadOtp(
+        otp = "1",
+        expirationDate = Instant.EPOCH.plus(Duration.standardDays(1))
+    )
+    private val snapshot = LogSnapshotter.Snapshot(
+        path = File(testDir, "snapshot.zip")
+    )
+    private val uploadResponse = LogUploadApiV1.UploadResponse(
+        id = "123",
+        hash = null,
+        errorCode = null
+    )
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        coEvery { uploadApiV1.uploadLog(any(), any()) } returns uploadResponse
+        every { timeStamper.nowUTC } returns Instant.ofEpochMilli(1234)
+    }
+
+    @AfterEach
+    fun teardown() {
+        testDir.deleteRecursively()
+    }
+
+    fun createInstance() = LogUploadServer(
+        timeStamper = timeStamper,
+        uploadApiProvider = { uploadApiV1 }
+    )
+
+    @Test
+    fun `log upload`() = runBlocking {
+        val instance = createInstance()
+
+        instance.uploadLog(uploadOtp = uploadOtp, snapshot = snapshot) shouldBe LogUpload(
+            id = "123",
+            uploadedAt = Instant.ofEpochMilli(1234)
+        )
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..46c4a59e9e75e3aca26e6b4b81030744f78e27e1
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt
@@ -0,0 +1,104 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth
+
+import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule
+import de.rki.coronawarnapp.environment.download.DownloadCDNModule
+import de.rki.coronawarnapp.exception.http.CwaWebException
+import de.rki.coronawarnapp.http.HttpModule
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtp
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import kotlinx.coroutines.runBlocking
+import okhttp3.ConnectionSpec
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import testhelpers.extensions.toJsonResponse
+import java.util.concurrent.TimeUnit
+
+class LogUploadAuthApiTest : BaseTest() {
+
+    private lateinit var webServer: MockWebServer
+    private lateinit var serverAddress: String
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        webServer = MockWebServer()
+        webServer.start()
+        serverAddress = "http://${webServer.hostName}:${webServer.port}"
+    }
+
+    @AfterEach
+    fun teardown() {
+        webServer.shutdown()
+    }
+
+    private fun createAPI(): LogUploadAuthApiV1 {
+        val httpModule = HttpModule()
+        val defaultHttpClient = httpModule.defaultHttpClient()
+        val gsonConverterFactory = httpModule.provideGSONConverter()
+        val protoConverterFactory = httpModule.provideProtoConverter()
+
+        val cdnHttpClient = DownloadCDNModule()
+            .cdnHttpClient(defaultHttpClient)
+            .newBuilder()
+            .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS))
+            .build()
+
+        return BugReportingSharedModule().logUploadAuthApi(
+            client = cdnHttpClient,
+            url = serverAddress,
+            gsonConverterFactory = gsonConverterFactory,
+            protoConverterFactory = protoConverterFactory
+        )
+    }
+
+    @Test
+    fun `test auth`(): Unit = runBlocking {
+        """
+            {
+                expirationDate : "2020-08-20T14:00:00.000Z"
+            }
+        """.toJsonResponse().apply { webServer.enqueue(this) }
+
+        val elsPayload = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder()
+            .setPayload(ElsOtp.ELSOneTimePassword.newBuilder().setOtp("15cff19f-af26-41bc-94f2-c1a65075e894"))
+            .setAuthentication(PpacAndroid.PPACAndroid.newBuilder().setSafetyNetJws("abc").setSalt("def"))
+            .build()
+
+        val api = createAPI()
+        api.authOTP(requestBody = elsPayload)
+
+        webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply {
+            path shouldBe "/version/v1/android/log"
+            body.readByteArray() shouldBe elsPayload.toByteArray()
+        }
+    }
+
+    @Test
+    fun `server returns 500`(): Unit = runBlocking {
+        """
+            {
+                errorCode: "Nope"
+            }
+        """.toJsonResponse().apply { webServer.enqueue(MockResponse().setResponseCode(500)) }
+
+        val elsPayload = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder()
+            .setPayload(ElsOtp.ELSOneTimePassword.newBuilder().setOtp("15cff19f-af26-41bc-94f2-c1a65075e894"))
+            .setAuthentication(PpacAndroid.PPACAndroid.newBuilder().setSafetyNetJws("abc").setSalt("def"))
+            .build()
+
+        val api = createAPI()
+
+        shouldThrow<CwaWebException> {
+            api.authOTP(requestBody = elsPayload)
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b2a8b3c9fa39ca76780f3ce666666641dd33f53b
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt
@@ -0,0 +1,85 @@
+package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth
+
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.LogUploadConfig
+import de.rki.coronawarnapp.appconfig.SafetyNetRequirements
+import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.slot
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Instant
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseIOTest
+import java.io.File
+import java.util.UUID
+
+class LogUploadAuthorizerTest : BaseIOTest() {
+
+    private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName)
+
+    @MockK private lateinit var authApiV1: LogUploadAuthApiV1
+    @MockK private lateinit var deviceAttestation: DeviceAttestation
+    @MockK private lateinit var configProvider: AppConfigProvider
+    @MockK private lateinit var attestationResult: DeviceAttestation.Result
+    @MockK private lateinit var configData: ConfigData
+    @MockK private lateinit var logUploadConfig: LogUploadConfig
+    @MockK private lateinit var safetyNetRequirements: SafetyNetRequirements
+
+    private val attestationRequestSlot = slot<DeviceAttestation.Request>()
+
+    private val uploadResponse = LogUploadAuthApiV1.AuthResponse(
+        expirationDate = Instant.EPOCH.toString()
+    )
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { configData.logUpload } returns logUploadConfig
+        every { logUploadConfig.safetyNetRequirements } returns safetyNetRequirements
+
+        coEvery { authApiV1.authOTP(any()) } returns uploadResponse
+        coEvery { deviceAttestation.attest(capture(attestationRequestSlot)) } returns attestationResult
+        attestationResult.apply {
+            every { requirePass(safetyNetRequirements) } just Runs
+            every { accessControlProtoBuf } returns PpacAndroid.PPACAndroid.getDefaultInstance()
+        }
+        coEvery { configProvider.currentConfig } returns flowOf(configData)
+    }
+
+    @AfterEach
+    fun teardown() {
+        testDir.deleteRecursively()
+    }
+
+    fun createInstance() = LogUploadAuthorizer(
+        authApiProvider = { authApiV1 },
+        deviceAttestation = deviceAttestation,
+        configProvider = configProvider
+    )
+
+    @Test
+    fun `otp generation`() = runBlockingTest {
+        val expectedOtp = UUID.fromString("15cff19f-af26-41bc-94f2-c1a65075e894")
+        val instance = createInstance()
+
+        instance.getAuthorizedOTP(otp = expectedOtp).apply {
+            otp shouldBe expectedOtp.toString()
+            expirationDate shouldBe Instant.EPOCH
+        }
+
+        attestationRequestSlot.captured.configData shouldBe configData
+        attestationRequestSlot.captured.checkDeviceTime shouldBe false
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt
index 45debcbe03e1a8677e5a02cbeb8a92856c899910..fae3d1cedacb466af3cbfdcc9f4cea702038a6f1 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt
@@ -17,6 +17,8 @@ import de.rki.coronawarnapp.util.gplay.GoogleApiVersion
 import io.kotest.assertions.throwables.shouldNotThrowAny
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import io.mockk.Called
 import io.mockk.MockKAnnotations
 import io.mockk.coEvery
 import io.mockk.coVerify
@@ -257,9 +259,35 @@ class CWASafetyNetTest : BaseTest() {
         exception.type shouldBe SafetyNetException.Type.TIME_SINCE_ONBOARDING_UNVERIFIED
     }
 
+    @Test
+    fun `device time checks can be disabled via request`() = runBlockingTest {
+        every { appConfigData.deviceTimeState } returns ConfigData.DeviceTimeState.ASSUMED_CORRECT
+        every { timeStamper.nowUTC } returns Instant.EPOCH
+
+        val request = TestAttestationRequest(
+            "Computer says no.".toByteArray(),
+            checkDeviceTime = false
+        )
+        createInstance().attest(request) shouldNotBe null
+    }
+
+    @Test
+    fun `the request can contain a app config that should be used`() = runBlockingTest {
+        val request = TestAttestationRequest(
+            "Computer says no.".toByteArray(),
+            configData = appConfigData
+        )
+        createInstance().attest(request) shouldNotBe null
+
+        coVerify { appConfigProvider wasNot Called }
+    }
+
     data class TestAttestationRequest(
-        override val scenarioPayload: ByteArray
+        override val scenarioPayload: ByteArray,
+        override val configData: ConfigData? = null,
+        override val checkDeviceTime: Boolean = true
     ) : DeviceAttestation.Request {
+
         override fun equals(other: Any?): Boolean {
             if (this === other) return true
             if (javaClass != other?.javaClass) return false
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0e637c3c5dcff600ac28730a5140acd691a35d45
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt
@@ -0,0 +1,17 @@
+package de.rki.coronawarnapp.datadonation.safetynet
+
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class DeviceAttestationTest : BaseTest() {
+
+    @Test
+    fun `request has sane defaults`() {
+        val impl = object : DeviceAttestation.Request {
+            override val scenarioPayload: ByteArray = "".toByteArray()
+        }
+        impl.checkDeviceTime shouldBe true
+        impl.configData shouldBe null
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
index d3eb6d4aed8530a325264a5c608bd4e91fc4c2ca..62534f7290aa26c8cc7948ecfc0b71cd7b3166f1 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
@@ -65,7 +65,8 @@ class EnvironmentSetupTest : BaseTest() {
                 verificationCdnUrl shouldBe "https://verification-${env.rawKey}"
                 appConfigVerificationKey shouldBe "12345678-${env.rawKey}"
                 safetyNetApiKey shouldBe "placeholder-${env.rawKey}"
-                dataDonationCdnUrl shouldBe "https://placeholder-${env.rawKey}"
+                dataDonationCdnUrl shouldBe "https://datadonation-${env.rawKey}"
+                logUploadServerUrl shouldBe "https://logupload-${env.rawKey}"
             }
         }
     }
@@ -121,8 +122,9 @@ class EnvironmentSetupTest : BaseTest() {
         EnvironmentSetup.EnvKey.DOWNLOAD.rawKey shouldBe "DOWNLOAD_CDN_URL"
         EnvironmentSetup.EnvKey.VERIFICATION_KEYS.rawKey shouldBe "PUB_KEYS_SIGNATURE_VERIFICATION"
         EnvironmentSetup.EnvKey.DATA_DONATION.rawKey shouldBe "DATA_DONATION_CDN_URL"
+        EnvironmentSetup.EnvKey.LOG_UPLOAD.rawKey shouldBe "LOG_UPLOAD_SERVER_URL"
         EnvironmentSetup.EnvKey.SAFETYNET_API_KEY.rawKey shouldBe "SAFETYNET_API_KEY"
-        EnvironmentSetup.EnvKey.values().size shouldBe 7
+        EnvironmentSetup.EnvKey.values().size shouldBe 8
     }
 
     companion object {
@@ -141,7 +143,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "SUBMISSION_CDN_URL": "https://submission-PROD",
                     "DOWNLOAD_CDN_URL": "https://download-PROD",
                     "VERIFICATION_CDN_URL": "https://verification-PROD",
-                    "DATA_DONATION_CDN_URL": "https://placeholder-PROD",
+                    "DATA_DONATION_CDN_URL": "https://datadonation-PROD",
+                    "LOG_UPLOAD_SERVER_URL": "https://logupload-PROD",
                     "SAFETYNET_API_KEY": "placeholder-PROD",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-PROD"
                 },
@@ -150,7 +153,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "SUBMISSION_CDN_URL": "https://submission-DEV",
                     "DOWNLOAD_CDN_URL": "https://download-DEV",
                     "VERIFICATION_CDN_URL": "https://verification-DEV",
-                    "DATA_DONATION_CDN_URL": "https://placeholder-DEV",
+                    "DATA_DONATION_CDN_URL": "https://datadonation-DEV",
+                    "LOG_UPLOAD_SERVER_URL": "https://logupload-DEV",
                     "SAFETYNET_API_KEY": "placeholder-DEV",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-DEV"
                 },
@@ -159,7 +163,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "SUBMISSION_CDN_URL": "https://submission-INT",
                     "DOWNLOAD_CDN_URL": "https://download-INT",
                     "VERIFICATION_CDN_URL": "https://verification-INT",
-                    "DATA_DONATION_CDN_URL": "https://placeholder-INT",
+                    "DATA_DONATION_CDN_URL": "https://datadonation-INT",
+                    "LOG_UPLOAD_SERVER_URL": "https://logupload-INT",
                     "SAFETYNET_API_KEY": "placeholder-INT",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-INT"
                 },
@@ -168,7 +173,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "SUBMISSION_CDN_URL": "https://submission-WRU",
                     "DOWNLOAD_CDN_URL": "https://download-WRU",
                     "VERIFICATION_CDN_URL": "https://verification-WRU",
-                    "DATA_DONATION_CDN_URL": "https://placeholder-WRU",
+                    "DATA_DONATION_CDN_URL": "https://datadonation-WRU",
+                    "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU",
                     "SAFETYNET_API_KEY": "placeholder-WRU",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU"
                 },
@@ -177,7 +183,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "SUBMISSION_CDN_URL": "https://submission-WRU-XD",
                     "DOWNLOAD_CDN_URL": "https://download-WRU-XD",
                     "VERIFICATION_CDN_URL": "https://verification-WRU-XD",
-                    "DATA_DONATION_CDN_URL": "https://placeholder-WRU-XD",
+                    "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XD",
+                    "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XD",
                     "SAFETYNET_API_KEY": "placeholder-WRU-XD",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XD"
                 },
@@ -186,7 +193,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "SUBMISSION_CDN_URL": "https://submission-WRU-XA",
                     "DOWNLOAD_CDN_URL": "https://download-WRU-XA",
                     "VERIFICATION_CDN_URL": "https://verification-WRU-XA",
-                    "DATA_DONATION_CDN_URL": "https://placeholder-WRU-XA",
+                    "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XA",
+                    "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XA",
                     "SAFETYNET_API_KEY": "placeholder-WRU-XA",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XA"
                 },
@@ -195,7 +203,8 @@ class EnvironmentSetupTest : BaseTest() {
                     "SUBMISSION_CDN_URL": "https://submission-LOCAL",
                     "DOWNLOAD_CDN_URL": "https://download-LOCAL",
                     "VERIFICATION_CDN_URL": "https://verification-LOCAL",
-                    "DATA_DONATION_CDN_URL": "https://placeholder-LOCAL",
+                    "DATA_DONATION_CDN_URL": "https://datadonation-LOCAL",
+                    "LOG_UPLOAD_SERVER_URL": "https://logupload-LOCAL",
                     "SAFETYNET_API_KEY": "placeholder-LOCAL",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-LOCAL"
                 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ef3488e935830a29da318b1712d7c3ae34d36c52
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt
@@ -0,0 +1,50 @@
+package de.rki.coronawarnapp.environment.bugreporting
+
+import de.rki.coronawarnapp.environment.EnvironmentSetup
+import io.kotest.assertions.throwables.shouldNotThrowAny
+import io.kotest.assertions.throwables.shouldThrowAny
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseIOTest
+
+class BugReportingServerModuleTest : BaseIOTest() {
+
+    private val validUrl = "https://logupload"
+    private val inValidUrl = "http://invalid"
+
+    @MockK lateinit var environmentSetup: EnvironmentSetup
+
+    @BeforeEach
+    fun setUp() {
+        MockKAnnotations.init(this)
+    }
+
+    private fun createModule() = BugReportingServerModule()
+
+    @Test
+    fun `sideeffect free instantiation`() {
+        shouldNotThrowAny {
+            createModule()
+        }
+    }
+
+    @Test
+    fun `valid downloaded URL comes from environment`() {
+        every { environmentSetup.logUploadServerUrl } returns validUrl
+        val module = createModule()
+        module.provideBugReportingServerUrl(environmentSetup) shouldBe validUrl
+    }
+
+    @Test
+    fun `invalid downloaded URL comes from environment`() {
+        every { environmentSetup.logUploadServerUrl } returns inValidUrl
+        val module = createModule()
+        shouldThrowAny {
+            module.provideBugReportingServerUrl(environmentSetup) shouldBe validUrl
+        }
+    }
+}
diff --git a/prod_environments.json b/prod_environments.json
index 314c1c69483fa3a3f609a7913150a6b449efa95b..9cd0aaa9c3de612c39e495bb2f90885b167fb7cc 100644
--- a/prod_environments.json
+++ b/prod_environments.json
@@ -5,6 +5,7 @@
     "DOWNLOAD_CDN_URL": "https://svc90.main.px.t-online.de",
     "VERIFICATION_CDN_URL": "https://verification.coronawarn.app",
     "DATA_DONATION_CDN_URL": "https://data.coronawarn.app",
+    "LOG_UPLOAD_SERVER_URL": "https://placeholder",
     "SAFETYNET_API_KEY": "placeholder",
     "PUB_KEYS_SIGNATURE_VERIFICATION": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg=="
   }