diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java index 3ecca5f3..1328f4ef 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java @@ -10,52 +10,60 @@ import static io.opentdf.platform.sdk.ECCurve.SECP521R1; public enum KeyType { - RSA2048Key("rsa:2048"), - RSA4096Key("rsa:4096"), - EC256Key("ec:secp256r1", SECP256R1), - EC384Key("ec:secp384r1", SECP384R1), - EC521Key("ec:secp521r1", SECP521R1), + RSA2048Key("rsa:2048", Algorithm.ALGORITHM_RSA_2048), + RSA4096Key("rsa:4096", Algorithm.ALGORITHM_RSA_4096), + EC256Key("ec:secp256r1", Algorithm.ALGORITHM_EC_P256, SECP256R1), + EC384Key("ec:secp384r1", Algorithm.ALGORITHM_EC_P384, SECP384R1), + EC521Key("ec:secp521r1", Algorithm.ALGORITHM_EC_P521, SECP521R1), /** * Hybrid X-Wing (X25519 + ML-KEM-768). Requires {@code sdk-pqc-bc} (or another * {@link io.opentdf.platform.sdk.spi.KemProvider} implementation) on the runtime * classpath; not available under the fips Maven profile. */ - HybridXWingKey("hpqt:xwing"), + HybridXWingKey("hpqt:xwing", Algorithm.ALGORITHM_HPQT_XWING), /** * Hybrid P-256 + ML-KEM-768 per draft-ietf-lamps-pq-composite-kem-14. Requires * {@code sdk-pqc-bc} (or another {@link io.opentdf.platform.sdk.spi.KemProvider} * implementation) on the runtime classpath; not available under the fips Maven profile. */ - HybridSecp256r1MLKEM768Key("hpqt:secp256r1-mlkem768"), + HybridSecp256r1MLKEM768Key("hpqt:secp256r1-mlkem768", Algorithm.ALGORITHM_HPQT_SECP256R1_MLKEM768), /** * Hybrid P-384 + ML-KEM-1024 per draft-ietf-lamps-pq-composite-kem-14. Requires * {@code sdk-pqc-bc} (or another {@link io.opentdf.platform.sdk.spi.KemProvider} * implementation) on the runtime classpath; not available under the fips Maven profile. */ - HybridSecp384r1MLKEM1024Key("hpqt:secp384r1-mlkem1024"), + HybridSecp384r1MLKEM1024Key("hpqt:secp384r1-mlkem1024", Algorithm.ALGORITHM_HPQT_SECP384R1_MLKEM1024), /** * Pure ML-KEM-768 (FIPS 203). Requires {@code sdk-pqc-bc} (or another * {@link io.opentdf.platform.sdk.spi.KemProvider} implementation) on the runtime * classpath; not available under the fips Maven profile. */ - MLKEM768Key("mlkem:768"), + MLKEM768Key("mlkem:768", Algorithm.ALGORITHM_MLKEM_768), /** * Pure ML-KEM-1024 (FIPS 203). Requires {@code sdk-pqc-bc} (or another * {@link io.opentdf.platform.sdk.spi.KemProvider} implementation) on the runtime * classpath; not available under the fips Maven profile. */ - MLKEM1024Key("mlkem:1024"); + MLKEM1024Key("mlkem:1024", Algorithm.ALGORITHM_MLKEM_1024); private final String keyType; + private final Algorithm algorithm; private final ECCurve curve; - KeyType(String keyType, ECCurve ecCurve) { + KeyType(String keyType, Algorithm algorithm, ECCurve ecCurve) { this.keyType = keyType; + this.algorithm = algorithm; this.curve = ecCurve; } - KeyType(String keyType) { - this(keyType, null); + KeyType(String keyType, Algorithm algorithm) { + this(keyType, algorithm, null); + } + + /** Returns the policy algorithm represented by this key type. */ + @Nonnull + public Algorithm getAlgorithm() { + return algorithm; } @Nonnull @@ -80,34 +88,23 @@ public static KeyType fromString(String keyType) { throw new IllegalArgumentException("No enum constant for key type: " + keyType); } + /** + * Returns the key type associated with a policy algorithm. + * + * @param algorithm the policy algorithm to convert + * @return the corresponding key type + * @throws IllegalArgumentException if the algorithm is null or unsupported + */ public static KeyType fromAlgorithm(Algorithm algorithm) { if (algorithm == null) { throw new IllegalArgumentException("Algorithm cannot be null"); } - switch (algorithm) { - case ALGORITHM_RSA_2048: - return KeyType.RSA2048Key; - case ALGORITHM_RSA_4096: - return KeyType.RSA4096Key; - case ALGORITHM_EC_P256: - return KeyType.EC256Key; - case ALGORITHM_EC_P384: - return KeyType.EC384Key; - case ALGORITHM_EC_P521: - return KeyType.EC521Key; - case ALGORITHM_HPQT_XWING: - return KeyType.HybridXWingKey; - case ALGORITHM_HPQT_SECP256R1_MLKEM768: - return KeyType.HybridSecp256r1MLKEM768Key; - case ALGORITHM_HPQT_SECP384R1_MLKEM1024: - return KeyType.HybridSecp384r1MLKEM1024Key; - case ALGORITHM_MLKEM_768: - return KeyType.MLKEM768Key; - case ALGORITHM_MLKEM_1024: - return KeyType.MLKEM1024Key; - default: - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + for (KeyType type : KeyType.values()) { + if (type.algorithm == algorithm) { + return type; + } } + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); } public static KeyType fromPublicKeyAlgorithm(KasPublicKeyAlgEnum algorithm) { diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java index 477d315a..3499cb13 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -1,9 +1,8 @@ package io.opentdf.platform.sdk; import com.connectrpc.ConnectException; -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; -import com.google.gson.annotations.SerializedName; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; import io.opentdf.platform.policy.Algorithm; import io.opentdf.platform.policy.SimpleKasKey; import io.opentdf.platform.policy.SimpleKasPublicKey; @@ -84,6 +83,12 @@ List generatePlanFromProvidedKases(List fetchBaseKey(WellKnownServiceClientInterface wellknown) { var responseMessage = wellknown .getWellKnownConfigurationBlocking(GetWellKnownConfigurationRequest.getDefaultInstance(), Collections.emptyMap()) @@ -95,54 +100,63 @@ static Optional fetchBaseKey(WellKnownServiceClientInterface wellk throw new SDKException("unable to retrieve base key from well known endpoint", e); } - String baseKeyJson; - try { - baseKeyJson = response - .getConfiguration() - .getFieldsOrThrow(BASE_KEY) - .getStringValue(); - } catch (IllegalArgumentException e) { - logger.info( "no `" + BASE_KEY + "` found in well known configuration.", e); + var baseKeyValue = response.getConfiguration().getFieldsMap().get(BASE_KEY); + if (baseKeyValue == null) { + logger.info("no `{}` found in well known configuration", BASE_KEY); return Optional.empty(); } - BaseKey baseKey; - try { - baseKey = gson.fromJson(baseKeyJson, BaseKey.class); - } catch (JsonSyntaxException e) { - throw new SDKException("base key in well known configuration is malformed [" + baseKeyJson + "]", e); + if (baseKeyValue.getKindCase() != Value.KindCase.STRUCT_VALUE) { + logger.error("base key in well known configuration has an invalid type; base key will not be used"); + return Optional.empty(); + } + + var baseKey = baseKeyValue.getStructValue(); + var publicKeyValue = baseKey.getFieldsMap().get("public_key"); + if (publicKeyValue == null || publicKeyValue.getKindCase() != Value.KindCase.STRUCT_VALUE) { + logger.error("base key in well known configuration is missing valid required fields; base key will not be used"); + return Optional.empty(); + } + + var publicKey = publicKeyValue.getStructValue(); + var kasUri = stringField(baseKey, "kas_uri"); + var kid = stringField(publicKey, "kid"); + var pem = stringField(publicKey, "pem"); + var algorithmName = stringField(publicKey, "algorithm"); + if (isMissing(kasUri) || isMissing(kid) || isMissing(pem) || isMissing(algorithmName)) { + logger.error("base key in well known configuration is missing valid required fields; base key will not be used"); + return Optional.empty(); } - if (baseKey == null || baseKey.kasUrl == null || baseKey.publicKey == null || baseKey.publicKey.kid == null || baseKey.publicKey.pem == null || baseKey.publicKey.algorithm == null) { - logger.error("base key in well known configuration is missing required fields [{}]. base key will not be used", baseKeyJson); + Algorithm algorithm; + try { + algorithm = KeyType.fromString(algorithmName).getAlgorithm(); + } catch (IllegalArgumentException e) { + logger.error("base key in well known configuration has an unsupported algorithm; base key will not be used"); return Optional.empty(); } return Optional.of(SimpleKasKey.newBuilder() - .setKasUri(baseKey.kasUrl) + .setKasUri(kasUri) .setPublicKey( SimpleKasPublicKey.newBuilder() - .setKid(baseKey.publicKey.kid) - .setAlgorithm(baseKey.publicKey.algorithm) - .setPem(baseKey.publicKey.pem) + .setKid(kid) + .setAlgorithm(algorithm) + .setPem(pem) .build()) .build()); } - private static final Gson gson = new Gson(); - - private static class BaseKey { - @SerializedName("kas_url") - String kasUrl; - - @SerializedName("public_key") - Key publicKey; + /** Returns the named protobuf string value, or null when the field is absent or has another type. */ + private static String stringField(Struct fields, String name) { + var value = fields.getFieldsMap().get(name); + return value != null && value.getKindCase() == Value.KindCase.STRING_VALUE + ? value.getStringValue() : null; + } - private static class Key { - String kid; - String pem; - Algorithm algorithm; - } + /** Treats null and blank strings as missing required field values. */ + private static boolean isMissing(String value) { + return value == null || value.isBlank(); } Map> resolveKeys(List splitPlan) { diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java index 980404f1..36b159a1 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java @@ -1,10 +1,20 @@ package io.opentdf.platform.sdk; +import io.opentdf.platform.policy.Algorithm; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullSource; import static io.opentdf.platform.policy.Algorithm.ALGORITHM_EC_P256; import static io.opentdf.platform.policy.Algorithm.ALGORITHM_EC_P384; import static io.opentdf.platform.policy.Algorithm.ALGORITHM_EC_P521; +import static io.opentdf.platform.policy.Algorithm.ALGORITHM_HPQT_SECP256R1_MLKEM768; +import static io.opentdf.platform.policy.Algorithm.ALGORITHM_HPQT_SECP384R1_MLKEM1024; +import static io.opentdf.platform.policy.Algorithm.ALGORITHM_HPQT_XWING; +import static io.opentdf.platform.policy.Algorithm.ALGORITHM_MLKEM_1024; +import static io.opentdf.platform.policy.Algorithm.ALGORITHM_MLKEM_768; import static io.opentdf.platform.policy.Algorithm.ALGORITHM_RSA_2048; import static io.opentdf.platform.policy.Algorithm.ALGORITHM_RSA_4096; import static io.opentdf.platform.policy.KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1; @@ -36,6 +46,35 @@ void testFromAlgorithm() { assertEquals(KeyType.EC256Key, KeyType.fromAlgorithm(ALGORITHM_EC_P256)); assertEquals(KeyType.EC384Key, KeyType.fromAlgorithm(ALGORITHM_EC_P384)); assertEquals(KeyType.EC521Key, KeyType.fromAlgorithm(ALGORITHM_EC_P521)); + assertEquals(KeyType.HybridXWingKey, KeyType.fromAlgorithm(ALGORITHM_HPQT_XWING)); + assertEquals(KeyType.HybridSecp256r1MLKEM768Key, KeyType.fromAlgorithm(ALGORITHM_HPQT_SECP256R1_MLKEM768)); + assertEquals(KeyType.HybridSecp384r1MLKEM1024Key, KeyType.fromAlgorithm(ALGORITHM_HPQT_SECP384R1_MLKEM1024)); + assertEquals(KeyType.MLKEM768Key, KeyType.fromAlgorithm(ALGORITHM_MLKEM_768)); + assertEquals(KeyType.MLKEM1024Key, KeyType.fromAlgorithm(ALGORITHM_MLKEM_1024)); + } + + @ParameterizedTest + @NullSource + @EnumSource(value = Algorithm.class, names = {"ALGORITHM_UNSPECIFIED", "UNRECOGNIZED"}) + void testFromAlgorithmInvalid(Algorithm algorithm) { + var failure = assertThrows(IllegalArgumentException.class, () -> KeyType.fromAlgorithm(algorithm)); + + assertEquals(algorithm == null ? "Algorithm cannot be null" : "Unsupported algorithm: " + algorithm, + failure.getMessage()); + } + + @ParameterizedTest + @CsvSource({"EC256Key, SECP256R1", "EC384Key, SECP384R1", "EC521Key, SECP521R1"}) + void testEcCurve(KeyType keyType, ECCurve expectedCurve) { + assertTrue(keyType.isEc()); + assertEquals(expectedCurve, keyType.getECCurve()); + } + + @ParameterizedTest + @EnumSource(value = KeyType.class, names = {"RSA2048Key", "HybridSecp256r1MLKEM768Key"}) + void testNonEcKeyHasNoCurve(KeyType keyType) { + assertFalse(keyType.isEc()); + assertThrows(IllegalStateException.class, keyType::getECCurve); } @Test @@ -46,4 +85,4 @@ void testFromPublicKeyAlgEnum() { assertEquals(KeyType.EC384Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1)); assertEquals(KeyType.EC521Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1)); } -} \ No newline at end of file +} diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java index c7316dd6..8d9753df 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -1,79 +1,222 @@ package io.opentdf.platform.sdk; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; import com.google.protobuf.Struct; import com.google.protobuf.Value; import io.opentdf.platform.policy.Algorithm; +import io.opentdf.platform.policy.Attribute; +import io.opentdf.platform.policy.AttributeRuleTypeEnum; import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse; import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class PlannerTest { - @Test - void fetchBaseKey() { - var wellknownService = Mockito.mock(WellKnownServiceClientInterface.class); - var baseKeyJson = "{\"kas_url\":\"https://example.com/base_key\",\"public_key\":{\"algorithm\":\"ALGORITHM_RSA_2048\",\"kid\":\"thekid\",\"pem\": \"thepem\"}}"; - var val = Value.newBuilder().setStringValue(baseKeyJson).build(); - var config = Struct.newBuilder().putFields("base_key", val).build(); - var response = GetWellKnownConfigurationResponse - .newBuilder() - .setConfiguration(config) - .build(); - - Mockito.when(wellknownService.getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap())) - .thenReturn(TestUtil.successfulUnaryCall(response)); - + @ParameterizedTest + @CsvSource({ + "rsa:2048, ALGORITHM_RSA_2048", + "rsa:4096, ALGORITHM_RSA_4096", + "ec:secp256r1, ALGORITHM_EC_P256", + "ec:secp384r1, ALGORITHM_EC_P384", + "ec:secp521r1, ALGORITHM_EC_P521", + "hpqt:xwing, ALGORITHM_HPQT_XWING", + "hpqt:secp256r1-mlkem768, ALGORITHM_HPQT_SECP256R1_MLKEM768", + "hpqt:secp384r1-mlkem1024, ALGORITHM_HPQT_SECP384R1_MLKEM1024", + "mlkem:768, ALGORITHM_MLKEM_768", + "mlkem:1024, ALGORITHM_MLKEM_1024" + }) + void fetchBaseKeyFromStructuredConfiguration(String wireAlgorithm, Algorithm expectedAlgorithm) { + var wellknownService = wellknownService(baseKeyConfiguration(structuredBaseKey(wireAlgorithm))); var baseKey = Planner.fetchBaseKey(wellknownService); - assertThat(baseKey).isNotEmpty(); + + assertThat(baseKey).isPresent(); var simpleKasKey = baseKey.get(); assertThat(simpleKasKey.getKasUri()).isEqualTo("https://example.com/base_key"); - assertThat(simpleKasKey.getPublicKey().getAlgorithm()).isEqualTo(Algorithm.ALGORITHM_RSA_2048); + assertThat(simpleKasKey.getPublicKey().getAlgorithm()).isEqualTo(expectedAlgorithm); assertThat(simpleKasKey.getPublicKey().getKid()).isEqualTo("thekid"); assertThat(simpleKasKey.getPublicKey().getPem()).isEqualTo("thepem"); } @Test void fetchBaseKeyWithNoBaseKey() { - var wellknownService = Mockito.mock(WellKnownServiceClientInterface.class); - var response = GetWellKnownConfigurationResponse - .newBuilder() - .setConfiguration(Struct.newBuilder().build()) - .build(); + assertThat(Planner.fetchBaseKey(wellknownService(Struct.getDefaultInstance()))).isEmpty(); + } - Mockito.when(wellknownService.getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap())) - .thenReturn(TestUtil.successfulUnaryCall(response)); + @ParameterizedTest + @ValueSource(strings = {"kas_uri", "public_key", "public_key.algorithm", "public_key.kid", "public_key.pem"}) + void fetchBaseKeyWithMissingRequiredField(String field) { + var baseKey = replaceBaseKeyField(structuredBaseKey("rsa:2048"), field, null); - var baseKey = Planner.fetchBaseKey(wellknownService); - assertThat(baseKey).isEmpty(); + assertThat(Planner.fetchBaseKey(wellknownService(baseKeyConfiguration(baseKey)))).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"kas_uri", "public_key", "public_key.algorithm", "public_key.kid", "public_key.pem"}) + void fetchBaseKeyWithWrongRequiredFieldType(String field) { + var baseKey = replaceBaseKeyField(structuredBaseKey("rsa:2048"), field, + Value.newBuilder().setNumberValue(123).build()); + + assertThat(Planner.fetchBaseKey(wellknownService(baseKeyConfiguration(baseKey)))).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"kas_uri", "public_key.algorithm", "public_key.kid", "public_key.pem"}) + void fetchBaseKeyWithBlankRequiredField(String field) { + var baseKey = replaceBaseKeyField(structuredBaseKey("rsa:2048"), field, + Value.newBuilder().setStringValue(" ").build()); + + assertThat(Planner.fetchBaseKey(wellknownService(baseKeyConfiguration(baseKey)))).isEmpty(); + } + + @ParameterizedTest + @MethodSource("invalidBaseKeyValues") + void fetchBaseKeyWithWrongValueType(Value baseKey) { + var configuration = Struct.newBuilder().putFields("base_key", baseKey).build(); + + assertThat(Planner.fetchBaseKey(wellknownService(configuration))).isEmpty(); + } + + private static Stream invalidBaseKeyValues() { + return Stream.of( + Value.getDefaultInstance(), + Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), + Value.newBuilder().setNumberValue(123).build(), + Value.newBuilder().setBoolValue(true).build(), + Value.newBuilder().setListValue(ListValue.getDefaultInstance()).build(), + Value.newBuilder().setStringValue("{\"kas_uri\":\"https://example.com/base_key\"," + + "\"public_key\":{\"algorithm\":\"rsa:2048\",\"kid\":\"thekid\",\"pem\":\"thepem\"}}") + .build()); + } + + @ParameterizedTest + @ValueSource(strings = {"rsa:1024", "unknown", "ALGORITHM_RSA_2048", "ALGORITHM_UNSPECIFIED", "UNRECOGNIZED"}) + void fetchBaseKeyRejectsUnsupportedAlgorithm(String algorithm) { + var wellknownService = wellknownService(baseKeyConfiguration(structuredBaseKey(algorithm))); + + assertThat(Planner.fetchBaseKey(wellknownService)).isEmpty(); } @Test - void fetchBaseKeyWithMissingFields() { - var wellknownService = Mockito.mock(WellKnownServiceClientInterface.class); - // Missing 'kid', 'pem', and 'algorithm' in public_key - var baseKeyJson = "{\"kas_url\":\"https://example.com/base_key\",\"public_key\":{}}"; - var val = Value.newBuilder().setStringValue(baseKeyJson).build(); - var config = Struct.newBuilder().putFields("base_key", val).build(); - var response = GetWellKnownConfigurationResponse - .newBuilder() - .setConfiguration(config) + void autoconfigureUsesFetchedBaseKeyForUnmappedAttribute() { + var wellknownService = wellknownService(baseKeyConfiguration(structuredBaseKey("ec:secp256r1"))); + var kas = Mockito.mock(SDK.KAS.class); + var resolvedKey = new Config.KASInfo(); + resolvedKey.URL = "https://example.com/base_key"; + resolvedKey.KID = "thekid"; + resolvedKey.Algorithm = "ec:secp256r1"; + resolvedKey.PublicKey = "thepem"; + Mockito.when(kas.getPublicKey(Mockito.any())).thenReturn(resolvedKey); + var services = new FakeServicesBuilder().setWellknownService(wellknownService).setKas(kas).build(); + var config = configWithUnmappedAttribute(); + // A different configured default makes loss of the base key's algorithm observable. + config.wrappingKeyType = KeyType.RSA2048Key; + var planner = new Planner(config, services, Autoconfigure::createGranter); + + var splits = planner.getSplits(); + + Assertions.assertThat(splits).containsOnlyKeys(""); + Assertions.assertThat(splits.get("")).containsExactly(resolvedKey); + var request = ArgumentCaptor.forClass(Config.KASInfo.class); + Mockito.verify(kas).getPublicKey(request.capture()); + assertThat(request.getValue().URL).isEqualTo("https://example.com/base_key"); + assertThat(request.getValue().KID).isEqualTo("thekid"); + assertThat(request.getValue().Algorithm).isEqualTo("ec:secp256r1"); + Mockito.verify(wellknownService).getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap()); + } + + @Test + void autoconfigureFailsWithoutBaseKeyOrDefaultKasForUnmappedAttribute() { + var kas = Mockito.mock(SDK.KAS.class); + var services = new FakeServicesBuilder() + .setWellknownService(wellknownService(Struct.getDefaultInstance())) + .setKas(kas) + .build(); + var planner = new Planner(configWithUnmappedAttribute(), services, Autoconfigure::createGranter); + + assertThatThrownBy(planner::getSplits) + .isInstanceOf(AutoConfigureException.class) + .hasMessage("no default KAS specified; required for grantless plans"); + Mockito.verify(kas, Mockito.never()).getPublicKey(Mockito.any()); + } + + private static Config.TDFConfig configWithUnmappedAttribute() { + var attribute = Attribute.newBuilder() + .setFqn("https://example.com/attr/classification") + .setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) .build(); + var value = io.opentdf.platform.policy.Value.newBuilder() + .setFqn("https://example.com/attr/classification/value/internal") + .setValue("internal") + .setAttribute(attribute) + .build(); + var config = new Config.TDFConfig(); + config.autoconfigure = true; + config.attributeValues = List.of(value); + return config; + } - Mockito.when(wellknownService.getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap())) + private static WellKnownServiceClientInterface wellknownService(Struct configuration) { + var service = Mockito.mock(WellKnownServiceClientInterface.class); + var response = GetWellKnownConfigurationResponse.newBuilder().setConfiguration(configuration).build(); + Mockito.when(service.getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap())) .thenReturn(TestUtil.successfulUnaryCall(response)); + return service; + } - var baseKey = Planner.fetchBaseKey(wellknownService); - assertThat(baseKey).isEmpty(); + private static Struct baseKeyConfiguration(Struct baseKey) { + return Struct.newBuilder() + .putFields("base_key", Value.newBuilder().setStructValue(baseKey).build()) + .build(); + } + + private static Struct structuredBaseKey(String algorithm) { + var publicKey = Struct.newBuilder() + .putFields("algorithm", Value.newBuilder().setStringValue(algorithm).build()) + .putFields("kid", Value.newBuilder().setStringValue("thekid").build()) + .putFields("pem", Value.newBuilder().setStringValue("thepem").build()) + .build(); + return Struct.newBuilder() + .putFields("kas_uri", Value.newBuilder().setStringValue("https://example.com/base_key").build()) + .putFields("public_key", Value.newBuilder().setStructValue(publicKey).build()) + .build(); + } + + /** Replaces a top-level or nested public-key field; a null replacement removes the field. */ + private static Struct replaceBaseKeyField(Struct baseKey, String field, Value replacement) { + var builder = baseKey.toBuilder(); + if (field.startsWith("public_key.")) { + var publicKey = baseKey.getFieldsOrThrow("public_key").getStructValue().toBuilder(); + var name = field.substring("public_key.".length()); + if (replacement == null) { + publicKey.removeFields(name); + } else { + publicKey.putFields(name, replacement); + } + builder.putFields("public_key", Value.newBuilder().setStructValue(publicKey).build()); + } else if (replacement == null) { + builder.removeFields(field); + } else { + builder.putFields(field, replacement); + } + return builder.build(); } @Test @@ -268,4 +411,4 @@ void usesProvidedSplitPlanWhenNotAutoconfigure() { Assertions.assertThat(splits.get("split1")).extracting("URL").containsExactly("https://kas1.example.com"); Assertions.assertThat(splits.get("split2")).extracting("URL").containsExactly("https://kas2.example.com"); } -} \ No newline at end of file +} diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/SDKTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/SDKTest.java index 138bd535..289f04bc 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/SDKTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/SDKTest.java @@ -172,8 +172,16 @@ void testReadingProtocolClient() { void testGettingBaseKey() { var platformServicesClient = mock(ProtocolClient.class); var wellknownService = Mockito.mock(WellKnownServiceClientInterface.class); - var baseKeyJson = "{\"kas_url\":\"https://example.com/base_key\",\"public_key\":{\"algorithm\":\"ALGORITHM_RSA_2048\",\"kid\":\"thekid\",\"pem\": \"thepem\"}}"; - var val = Value.newBuilder().setStringValue(baseKeyJson).build(); + var publicKey = Struct.newBuilder() + .putFields("algorithm", Value.newBuilder().setStringValue("rsa:2048").build()) + .putFields("kid", Value.newBuilder().setStringValue("thekid").build()) + .putFields("pem", Value.newBuilder().setStringValue("thepem").build()) + .build(); + var baseKeyStruct = Struct.newBuilder() + .putFields("kas_uri", Value.newBuilder().setStringValue("https://example.com/base_key").build()) + .putFields("public_key", Value.newBuilder().setStructValue(publicKey).build()) + .build(); + var val = Value.newBuilder().setStructValue(baseKeyStruct).build(); var config = Struct.newBuilder().putFields("base_key", val).build(); var response = GetWellKnownConfigurationResponse .newBuilder()