diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index 761db73a44df54c8143232bc19ada8a6b7d459c3..310ecaab8c2c7de378a364e7c8a5d9f03df861d6 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -64,6 +64,10 @@ android {
                 arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
             }
         }
+
+        buildConfigField "int", "VERSION_MAJOR", VERSION_MAJOR
+        buildConfigField "int", "VERSION_MINOR", VERSION_MINOR
+        buildConfigField "int", "VERSION_PATCH", VERSION_PATCH
     }
 
     def signingPropFile = file("../keystore.properties")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsModule.kt
index 279bcbd92dfbf3f4fa48a3ebf707092cde5130f2..3c74b38013665b234f286bf142c3e5b6e4fc2f81 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsModule.kt
@@ -5,6 +5,7 @@ import dagger.Provides
 import dagger.Reusable
 import dagger.multibindings.IntoSet
 import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule
+import de.rki.coronawarnapp.datadonation.analytics.modules.clientmetadata.ClientMetadataDonor
 import de.rki.coronawarnapp.datadonation.analytics.modules.exposureriskmetadata.ExposureRiskMetadataDonor
 import de.rki.coronawarnapp.datadonation.analytics.modules.usermetadata.UserMetadataDonor
 import de.rki.coronawarnapp.datadonation.analytics.server.DataDonationAnalyticsApiV1
@@ -60,6 +61,10 @@ class AnalyticsModule {
     @Provides
     fun userMetadata(module: UserMetadataDonor): DonorModule = module
 
+    @IntoSet
+    @Provides
+    fun clientMetadata(module: ClientMetadataDonor): DonorModule = module
+
     @Provides
     @Singleton
     fun analyticsLogger(logger: DefaultLastAnalyticsSubmissionLogger): LastAnalyticsSubmissionLogger = logger
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a20b1f8ed8c1e28d18361e34119bb0c63c77241e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonor.kt
@@ -0,0 +1,69 @@
+package de.rki.coronawarnapp.datadonation.analytics.modules.clientmetadata
+
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule
+import de.rki.coronawarnapp.environment.BuildConfigWrap
+import de.rki.coronawarnapp.nearby.ENFClient
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
+import de.rki.coronawarnapp.util.ApiLevel
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ClientMetadataDonor @Inject constructor(
+    private val apiLevel: ApiLevel,
+    private val appConfigProvider: AppConfigProvider,
+    private val enfClient: ENFClient
+) : DonorModule {
+
+    override suspend fun beginDonation(request: DonorModule.Request): DonorModule.Contribution {
+        val config = appConfigProvider.currentConfig.first()
+
+        val version = ClientVersion()
+
+        val clientMetadataBuilder = PpaData.PPAClientMetadataAndroid.newBuilder()
+            .setCwaVersion(version.toPPASemanticVersion())
+            .setAndroidApiLevel(apiLevel.currentLevel.toLong())
+            .setAppConfigETag(config.identifier)
+
+        enfClient.getENFClientVersion()?.let {
+            clientMetadataBuilder.setEnfVersion(it)
+        }
+
+        return ClientMetadataContribution(
+            contributionProto = clientMetadataBuilder.build()
+        )
+    }
+
+    override suspend fun deleteData() {
+        // Nothing to be deleted
+    }
+
+    data class ClientMetadataContribution(
+        val contributionProto: PpaData.PPAClientMetadataAndroid
+    ) : DonorModule.Contribution {
+        override suspend fun injectData(protobufContainer: PpaData.PPADataAndroid.Builder) {
+            protobufContainer.clientMetadata = contributionProto
+        }
+
+        override suspend fun finishDonation(successful: Boolean) {
+            // No post processing needed for Client Metadata
+        }
+    }
+
+    data class ClientVersion(val major: Int, val minor: Int, val patch: Int) {
+        constructor() : this(
+            BuildConfigWrap.VERSION_MAJOR,
+            BuildConfigWrap.VERSION_MINOR,
+            BuildConfigWrap.VERSION_PATCH
+        )
+
+        fun toPPASemanticVersion(): PpaData.PPASemanticVersion =
+            PpaData.PPASemanticVersion.newBuilder()
+                .setMajor(major)
+                .setMinor(minor)
+                .setPatch(patch)
+                .build()
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/BuildConfigWrap.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/BuildConfigWrap.kt
index bc8252d1b7b923721b0e77cd6c85c08f43c4e678..8dbdda01a81613628f5c0ad3173d64397918f5d0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/BuildConfigWrap.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/BuildConfigWrap.kt
@@ -10,4 +10,8 @@ object BuildConfigWrap {
     val ENVIRONMENT_TYPE_DEFAULT = BuildConfig.ENVIRONMENT_TYPE_DEFAULT
 
     val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
+
+    val VERSION_MAJOR: Int = BuildConfig.VERSION_MAJOR
+    val VERSION_MINOR: Int = BuildConfig.VERSION_MINOR
+    val VERSION_PATCH: Int = BuildConfig.VERSION_PATCH
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3a04905ccfd3d5dc2e72ddae701529c783d7d621
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt
@@ -0,0 +1,87 @@
+package de.rki.coronawarnapp.datadonation.analytics.modules.clientmetadata
+
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule
+import de.rki.coronawarnapp.environment.BuildConfigWrap
+import de.rki.coronawarnapp.nearby.ENFClient
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
+import de.rki.coronawarnapp.util.ApiLevel
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockkObject
+import kotlinx.coroutines.flow.flowOf
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import testhelpers.coroutines.runBlockingTest2
+
+class ClientMetadataDonorTest : BaseTest() {
+    @MockK lateinit var apiLevel: ApiLevel
+    @MockK lateinit var appConfigProvider: AppConfigProvider
+    @MockK lateinit var configData: ConfigData
+    @MockK lateinit var enfClient: ENFClient
+
+    private val eTag = "testETag"
+    private val enfVersion = 1611L
+    private val androidVersionCode = 42L
+
+    private val versionMajor = 1
+    private val versionMinor = 11
+    private val versionPatch = 1
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        mockkObject(BuildConfigWrap)
+        every { BuildConfigWrap.VERSION_MAJOR } returns versionMajor
+        every { BuildConfigWrap.VERSION_MINOR } returns versionMinor
+        every { BuildConfigWrap.VERSION_PATCH } returns versionPatch
+
+        every { apiLevel.currentLevel } returns androidVersionCode.toInt()
+        every { configData.identifier } returns eTag
+        coEvery { appConfigProvider.currentConfig } returns flowOf(configData)
+        coEvery { enfClient.getENFClientVersion() } returns enfVersion
+    }
+
+    @AfterEach
+    fun tearDown() {
+        clearAllMocks()
+    }
+
+    private fun createInstance() = ClientMetadataDonor(
+        apiLevel = apiLevel,
+        appConfigProvider = appConfigProvider,
+        enfClient = enfClient
+    )
+
+    @Test
+    fun `client metadata is properly collected`() {
+        val version = ClientMetadataDonor.ClientVersion().toPPASemanticVersion()
+
+        val expectedMetadata = PpaData.PPAClientMetadataAndroid.newBuilder()
+            .setAppConfigETag(eTag)
+            .setEnfVersion(enfVersion)
+            .setCwaVersion(version)
+            .setAndroidApiLevel(androidVersionCode)
+            .build()
+
+        val parentBuilder = PpaData.PPADataAndroid.newBuilder()
+
+        runBlockingTest2 {
+            val contribution = createInstance().beginDonation(object : DonorModule.Request {})
+            contribution.injectData(parentBuilder)
+            contribution.finishDonation(true)
+        }
+
+        val parentProto = parentBuilder.build()
+
+        parentProto.clientMetadata shouldBe expectedMetadata
+    }
+}