Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 33 additions & 36 deletions sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
84 changes: 49 additions & 35 deletions sdk/src/main/java/io/opentdf/platform/sdk/Planner.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -84,6 +83,12 @@ List<Autoconfigure.KeySplitTemplate> generatePlanFromProvidedKases(List<Config.K
return splitPlan;
}

/**
* Fetches the base key from the platform's structured well-known configuration.
*
* @return a supported base key, or empty when the response type or required field values are invalid
* @throws SDKException if the configuration cannot be retrieved
*/
static Optional<SimpleKasKey> fetchBaseKey(WellKnownServiceClientInterface wellknown) {
var responseMessage = wellknown
.getWellKnownConfigurationBlocking(GetWellKnownConfigurationRequest.getDefaultInstance(), Collections.emptyMap())
Expand All @@ -95,54 +100,63 @@ static Optional<SimpleKasKey> 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();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

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<String, List<Config.KASInfo>> resolveKeys(List<Autoconfigure.KeySplitTemplate> splitPlan) {
Expand Down
41 changes: 40 additions & 1 deletion sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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));
}
}
}
Loading
Loading