From 223e1f848917b3e81d461fb62a62e6282151f14c Mon Sep 17 00:00:00 2001 From: Sanjay Vasandani Date: Wed, 21 Jul 2021 16:56:05 -0700 Subject: [PATCH] Implement v2alpha public API DataProviders service. --- .../kingdom/service/api/v2alpha/BUILD.bazel | 15 ++ .../api/v2alpha/DataProvidersService.kt | 152 +++++++++++ .../kingdom/service/api/v2alpha/BUILD.bazel | 20 ++ .../api/v2alpha/DataProvidersServiceTest.kt | 254 ++++++++++++++++++ 4 files changed, 441 insertions(+) create mode 100644 src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/DataProvidersService.kt create mode 100644 src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/DataProvidersServiceTest.kt diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel index 8dd0029c654..ab8255a2564 100644 --- a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel @@ -21,6 +21,21 @@ kt_jvm_library( ], ) +kt_jvm_library( + name = "data_providers_service", + srcs = ["DataProvidersService.kt"], + deps = [ + "//src/main/kotlin/org/wfanet/measurement/api:public_api_version", + "//src/main/kotlin/org/wfanet/measurement/api/v2alpha:resource_key", + "//src/main/proto/wfa/measurement/api/v2alpha:data_providers_service_kt_jvm_grpc", + "//src/main/proto/wfa/measurement/internal/kingdom:data_providers_service_kt_jvm_grpc", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/crypto:security_provider", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/grpc", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/identity", + ], +) + kt_jvm_library( name = "recurring_exchanges_service", srcs = ["RecurringExchangesService.kt"], diff --git a/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/DataProvidersService.kt b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/DataProvidersService.kt new file mode 100644 index 00000000000..639d809d12d --- /dev/null +++ b/src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/DataProvidersService.kt @@ -0,0 +1,152 @@ +// Copyright 2021 The Cross-Media Measurement Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.wfanet.measurement.kingdom.service.api.v2alpha + +import com.google.protobuf.ByteString +import io.grpc.Status +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import org.wfanet.measurement.api.Version +import org.wfanet.measurement.api.v2alpha.CreateDataProviderRequest +import org.wfanet.measurement.api.v2alpha.DataProvider +import org.wfanet.measurement.api.v2alpha.DataProviderCertificateKey +import org.wfanet.measurement.api.v2alpha.DataProviderKey +import org.wfanet.measurement.api.v2alpha.DataProvidersGrpcKt.DataProvidersCoroutineImplBase as DataProvidersCoroutineService +import org.wfanet.measurement.api.v2alpha.GetDataProviderRequest +import org.wfanet.measurement.api.v2alpha.SignedData +import org.wfanet.measurement.common.crypto.readCertificate +import org.wfanet.measurement.common.crypto.subjectKeyIdentifier +import org.wfanet.measurement.common.grpc.grpcRequire +import org.wfanet.measurement.common.grpc.grpcRequireNotNull +import org.wfanet.measurement.common.identity.apiIdToExternalId +import org.wfanet.measurement.common.identity.externalIdToApiId +import org.wfanet.measurement.common.toProtoTime +import org.wfanet.measurement.internal.kingdom.Certificate as InternalCertificate +import org.wfanet.measurement.internal.kingdom.DataProvider as InternalDataProvider +import org.wfanet.measurement.internal.kingdom.DataProvidersGrpcKt.DataProvidersCoroutineStub +import org.wfanet.measurement.internal.kingdom.GetDataProviderRequest as InternalGetDataProviderRequest + +private val API_VERSION = Version.V2_ALPHA + +class DataProvidersService(private val internalClient: DataProvidersCoroutineStub) : + DataProvidersCoroutineService() { + override suspend fun createDataProvider(request: CreateDataProviderRequest): DataProvider { + val dataProvider = request.dataProvider + grpcRequire(with(dataProvider.publicKey) { !data.isEmpty && !signature.isEmpty }) { + "public_key is not fully specified" + } + grpcRequire(!dataProvider.preferredCertificateDer.isEmpty) { + "preferred_certificate_der is not specified" + } + + val x509Certificate: X509Certificate = + try { + readCertificate(dataProvider.preferredCertificateDer) + } catch (e: CertificateException) { + throw Status.INVALID_ARGUMENT + .withCause(e) + .withDescription("Cannot parse preferred_certificate_der") + .asRuntimeException() + } + val skid: ByteString = + grpcRequireNotNull(x509Certificate.subjectKeyIdentifier) { + "Cannot find Subject Key Identifier of preferred certificate" + } + + val internalResponse: InternalDataProvider = + internalClient.createDataProvider( + buildInternalDataProvider { + preferredCertificate { + subjectKeyIdentifier = skid + notValidBefore = x509Certificate.notBefore.toInstant().toProtoTime() + notValidAfter = x509Certificate.notAfter.toInstant().toProtoTime() + detailsBuilder.x509Der = dataProvider.preferredCertificateDer + } + details { + apiVersion = API_VERSION.string + publicKey = dataProvider.publicKey.data + publicKeySignature = dataProvider.publicKey.signature + } + } + // TODO(world-federation-of-advertisers/cross-media-measurement#119): Add authenticated user + // as owner. + ) + return internalResponse.toDataProvider() + } + + override suspend fun getDataProvider(request: GetDataProviderRequest): DataProvider { + val key: DataProviderKey = + grpcRequireNotNull(DataProviderKey.fromName(request.name)) { + "Resource name unspecified or invalid" + } + // TODO(world-federation-of-advertisers/cross-media-measurement#119): Pass credentials for + // ownership check. + val internalResponse: InternalDataProvider = + internalClient.getDataProvider( + buildInternalGetDataProviderRequest { + externalDataProviderId = apiIdToExternalId(key.dataProviderId) + } + ) + return internalResponse.toDataProvider() + } +} + +@DslMarker @Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) internal annotation class Builder + +internal inline fun buildDataProvider(fill: (@Builder DataProvider.Builder).() -> Unit) = + DataProvider.newBuilder().apply(fill).build() + +internal inline fun DataProvider.Builder.publicKey(fill: (@Builder SignedData.Builder).() -> Unit) { + publicKeyBuilder.apply(fill) +} + +internal inline fun buildInternalDataProvider( + fill: (@Builder InternalDataProvider.Builder).() -> Unit +) = InternalDataProvider.newBuilder().apply(fill).build() + +internal inline fun InternalDataProvider.Builder.preferredCertificate( + fill: (@Builder InternalCertificate.Builder).() -> Unit +) { + preferredCertificateBuilder.apply(fill) +} + +internal inline fun InternalDataProvider.Builder.details( + fill: (@Builder InternalDataProvider.Details.Builder).() -> Unit +) { + detailsBuilder.apply(fill) +} + +internal inline fun buildInternalGetDataProviderRequest( + fill: (@Builder InternalGetDataProviderRequest.Builder).() -> Unit +) = InternalGetDataProviderRequest.newBuilder().apply(fill).build() + +private fun InternalDataProvider.toDataProvider(): DataProvider { + check(Version.fromString(details.apiVersion) == API_VERSION) { + "Incompatible API version ${details.apiVersion}" + } + val internalDataProvider = this + val dataProviderId: String = externalIdToApiId(externalDataProviderId) + val certificateId: String = externalIdToApiId(preferredCertificate.externalCertificateId) + + return buildDataProvider { + name = DataProviderKey(dataProviderId).toName() + preferredCertificate = DataProviderCertificateKey(dataProviderId, certificateId).toName() + preferredCertificateDer = internalDataProvider.preferredCertificate.details.x509Der + publicKey { + data = internalDataProvider.details.publicKey + signature = internalDataProvider.details.publicKeySignature + } + } +} diff --git a/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel b/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel index 05c492c0c82..8916626bab7 100644 --- a/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel +++ b/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/BUILD.bazel @@ -13,6 +13,26 @@ kt_jvm_test( ], ) +kt_jvm_test( + name = "DataProvidersServiceTest", + srcs = ["DataProvidersServiceTest.kt"], + data = ["@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/crypto/testing/testdata:certs"], + friends = ["//src/main/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha:data_providers_service"], + test_class = "org.wfanet.measurement.kingdom.service.api.v2alpha.DataProvidersServiceTest", + deps = [ + "//src/main/proto/wfa/measurement/internal/kingdom:data_providers_service_kt_jvm_grpc", + "@wfa_common_jvm//imports/java/com/google/common/truth", + "@wfa_common_jvm//imports/java/com/google/common/truth/extensions/proto", + "@wfa_common_jvm//imports/java/org/junit", + "@wfa_common_jvm//imports/kotlin/kotlin/test", + "@wfa_common_jvm//imports/kotlin/kotlinx/coroutines:core", + "@wfa_common_jvm//imports/kotlin/org/mockito/kotlin", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/crypto:security_provider", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/grpc/testing", + "@wfa_common_jvm//src/main/kotlin/org/wfanet/measurement/common/testing", + ], +) + kt_jvm_test( name = "ExchangesServiceTest", srcs = ["ExchangesServiceTest.kt"], diff --git a/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/DataProvidersServiceTest.kt b/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/DataProvidersServiceTest.kt new file mode 100644 index 00000000000..68f370b15d9 --- /dev/null +++ b/src/test/kotlin/org/wfanet/measurement/kingdom/service/api/v2alpha/DataProvidersServiceTest.kt @@ -0,0 +1,254 @@ +// Copyright 2021 The Cross-Media Measurement Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.wfanet.measurement.kingdom.service.api.v2alpha + +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.ProtoTruth.assertThat +import com.google.protobuf.ByteString +import io.grpc.Status +import io.grpc.StatusRuntimeException +import java.nio.file.Path +import java.nio.file.Paths +import java.security.PrivateKey +import java.security.cert.X509Certificate +import kotlin.test.assertFailsWith +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.kotlin.UseConstructor +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.wfanet.measurement.api.Version +import org.wfanet.measurement.api.v2alpha.CreateDataProviderRequest +import org.wfanet.measurement.api.v2alpha.DataProvider +import org.wfanet.measurement.api.v2alpha.EncryptionPublicKey +import org.wfanet.measurement.api.v2alpha.GetDataProviderRequest +import org.wfanet.measurement.api.v2alpha.SignedData +import org.wfanet.measurement.common.crypto.readCertificate +import org.wfanet.measurement.common.crypto.readPrivateKey +import org.wfanet.measurement.common.crypto.subjectKeyIdentifier +import org.wfanet.measurement.common.getRuntimePath +import org.wfanet.measurement.common.grpc.testing.GrpcTestServerRule +import org.wfanet.measurement.common.testing.verifyProtoArgument +import org.wfanet.measurement.common.toProtoTime +import org.wfanet.measurement.internal.kingdom.DataProvider as InternalDataProvider +import org.wfanet.measurement.internal.kingdom.DataProvidersGrpcKt.DataProvidersCoroutineImplBase as InternalDataProvidersService +import org.wfanet.measurement.internal.kingdom.DataProvidersGrpcKt.DataProvidersCoroutineStub as InternalDataProvidersClient + +private val TESTDATA_DIR = + Paths.get( + "wfa_common_jvm", + "src", + "main", + "kotlin", + "org", + "wfanet", + "measurement", + "common", + "crypto", + "testing", + "testdata" + ) +private const val DATA_PROVIDER_ID = 123L +private const val CERTIFICATE_ID = 456L +private const val DATA_PROVIDER_NAME = "dataProviders/AAAAAAAAAHs" +private const val CERTIFICATE_NAME = "$DATA_PROVIDER_NAME/certificates/AAAAAAAAAcg" + +@RunWith(JUnit4::class) +class DataProvidersServiceTest { + private val internalServiceMock: InternalDataProvidersService = + mock(useConstructor = UseConstructor.parameterless()) { + onBlocking { createDataProvider(any()) }.thenReturn(INTERNAL_DATA_PROVIDER) + onBlocking { getDataProvider(any()) }.thenReturn(INTERNAL_DATA_PROVIDER) + } + + @get:Rule val grpcTestServerRule = GrpcTestServerRule { addService(internalServiceMock) } + + private lateinit var service: DataProvidersService + + @Before + fun initService() { + service = DataProvidersService(InternalDataProvidersClient(grpcTestServerRule.channel)) + } + + @Test + fun `create fills created resource names`() { + val request = buildCreateDataProviderRequest { + dataProviderBuilder.apply { + preferredCertificateDer = SERVER_CERTIFICATE_DER + publicKey = SIGNED_PUBLIC_KEY + } + } + + val createdDataProvider = runBlocking { service.createDataProvider(request) } + + val expectedDataProvider = + request.dataProvider.rebuild { + name = DATA_PROVIDER_NAME + preferredCertificate = CERTIFICATE_NAME + } + assertThat(createdDataProvider).isEqualTo(expectedDataProvider) + verifyProtoArgument(internalServiceMock, InternalDataProvidersService::createDataProvider) + .isEqualTo( + INTERNAL_DATA_PROVIDER.rebuild { + clearExternalDataProviderId() + clearExternalPublicKeyCertificateId() + preferredCertificate { + clearExternalDataProviderId() + clearExternalCertificateId() + } + } + ) + } + + @Test + fun `create throws INVALID_ARGUMENT when preferred certificate DER is missing`() { + val request = buildCreateDataProviderRequest { + dataProviderBuilder.apply { publicKey = SIGNED_PUBLIC_KEY } + } + + val exception = + assertFailsWith { + runBlocking { service.createDataProvider(request) } + } + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.status.description).isEqualTo("preferred_certificate_der is not specified") + } + + @Test + fun `create throws INVALID_ARGUMENT when public key is missing`() { + val request = buildCreateDataProviderRequest { + dataProviderBuilder.apply { preferredCertificateDer = SERVER_CERTIFICATE_DER } + } + + val exception = + assertFailsWith { + runBlocking { service.createDataProvider(request) } + } + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.status.description).isEqualTo("public_key is not fully specified") + } + + @Test + fun `get returns resource`() { + val dataProvider = runBlocking { + service.getDataProvider(buildGetDataProviderRequest { name = DATA_PROVIDER_NAME }) + } + + val expectedDataProvider = buildDataProvider { + name = DATA_PROVIDER_NAME + preferredCertificate = CERTIFICATE_NAME + preferredCertificateDer = SERVER_CERTIFICATE_DER + publicKey = SIGNED_PUBLIC_KEY + } + assertThat(dataProvider).isEqualTo(expectedDataProvider) + verifyProtoArgument(internalServiceMock, InternalDataProvidersService::getDataProvider) + .isEqualTo(buildInternalGetDataProviderRequest { externalDataProviderId = DATA_PROVIDER_ID }) + } + + @Test + fun `get throws INVALID_ARGUMENT when name is missing`() { + val exception = + assertFailsWith { + runBlocking { service.getDataProvider(GetDataProviderRequest.getDefaultInstance()) } + } + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.status.description).isEqualTo("Resource name unspecified or invalid") + } + + @Test + fun `get throws INVALID_ARGUMENT when name is invalid`() { + val exception = + assertFailsWith { + runBlocking { service.getDataProvider(buildGetDataProviderRequest { name = "foo" }) } + } + assertThat(exception.status.code).isEqualTo(Status.Code.INVALID_ARGUMENT) + assertThat(exception.status.description).isEqualTo("Resource name unspecified or invalid") + } + + companion object { + private val serverCertificate: X509Certificate + init { + val pemPath: Path = checkNotNull(getRuntimePath(TESTDATA_DIR.resolve("server.pem"))) + serverCertificate = readCertificate(pemPath.toFile()) + } + private val SERVER_CERTIFICATE_DER = ByteString.copyFrom(serverCertificate.encoded) + + private val serverPrivateKey: PrivateKey + init { + val keyPath: Path = checkNotNull(getRuntimePath(TESTDATA_DIR.resolve("server.key"))) + serverPrivateKey = readPrivateKey(keyPath.toFile(), serverCertificate.publicKey.algorithm) + } + + private val PUBLIC_KEY_DER: ByteString + init { + val publicKeyPath: Path = checkNotNull(getRuntimePath(TESTDATA_DIR.resolve("ec-public.der"))) + PUBLIC_KEY_DER = publicKeyPath.toFile().inputStream().use { ByteString.readFrom(it) } + } + + private val PUBLIC_KEY = buildEncryptionPublicKey { + type = EncryptionPublicKey.Type.EC_P256 + publicKeyInfo = PUBLIC_KEY_DER + } + + private val SIGNED_PUBLIC_KEY = buildSignedData { + data = PUBLIC_KEY.toByteString() + signature = ByteString.copyFromUtf8("Fake signature of public key") + } + + private val INTERNAL_DATA_PROVIDER: InternalDataProvider = buildInternalDataProvider { + externalDataProviderId = DATA_PROVIDER_ID + externalPublicKeyCertificateId = CERTIFICATE_ID + details { + apiVersion = Version.V2_ALPHA.string + publicKey = SIGNED_PUBLIC_KEY.data + publicKeySignature = SIGNED_PUBLIC_KEY.signature + } + preferredCertificate { + externalDataProviderId = DATA_PROVIDER_ID + externalCertificateId = CERTIFICATE_ID + subjectKeyIdentifier = serverCertificate.subjectKeyIdentifier + notValidBefore = serverCertificate.notBefore.toInstant().toProtoTime() + notValidAfter = serverCertificate.notAfter.toInstant().toProtoTime() + detailsBuilder.x509Der = SERVER_CERTIFICATE_DER + } + } + } +} + +private inline fun buildCreateDataProviderRequest( + fill: (@Builder CreateDataProviderRequest.Builder).() -> Unit +) = CreateDataProviderRequest.newBuilder().apply(fill).build() + +private inline fun buildGetDataProviderRequest( + fill: (@Builder GetDataProviderRequest.Builder).() -> Unit +) = GetDataProviderRequest.newBuilder().apply(fill).build() + +private inline fun buildEncryptionPublicKey( + fill: (@Builder EncryptionPublicKey.Builder).() -> Unit +) = EncryptionPublicKey.newBuilder().apply(fill).build() + +private inline fun buildSignedData(fill: (@Builder SignedData.Builder).() -> Unit) = + SignedData.newBuilder().apply(fill).build() + +private inline fun DataProvider.rebuild(fill: (@Builder DataProvider.Builder).() -> Unit) = + toBuilder().apply(fill).build() + +private inline fun InternalDataProvider.rebuild( + fill: (@Builder InternalDataProvider.Builder).() -> Unit +) = toBuilder().apply(fill).build()