Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a2d8b83
minor refactor
mkleene Jun 18, 2025
4b6a10e
now at least we are adding things from grants
mkleene Jun 26, 2025
075b4ac
just saving
mkleene Jun 30, 2025
744220e
just saving
mkleene Jun 30, 2025
c8b458f
saving
mkleene Jun 30, 2025
9fb0bd6
get tests passing again
mkleene Jun 30, 2025
b71f57e
pull the planning inside of autoconfigure
mkleene Jul 1, 2025
2d4f3e6
cleanup
mkleene Jul 1, 2025
65a4987
debug
mkleene Jul 1, 2025
2cfd3ec
only plan when we mean to
mkleene Jul 1, 2025
98653b9
restructure
mkleene Jul 1, 2025
e900df3
add some tests
mkleene Jul 2, 2025
78031d8
one more test
mkleene Jul 2, 2025
8c0bcf4
add test for resolving keys
mkleene Jul 2, 2025
c8370e7
added a split
mkleene Jul 2, 2025
41f3d6a
fix up tests
mkleene Jul 3, 2025
491cbf3
a couple more tests
mkleene Jul 3, 2025
26016f3
remove condition we do not need
mkleene Jul 3, 2025
1ba4cd5
add test for default kases
mkleene Jul 3, 2025
95232ad
add more tests
mkleene Jul 3, 2025
7f5a2d4
more tests
mkleene Jul 3, 2025
76085d3
sonarcloud
mkleene Jul 3, 2025
d34056e
more sonar
mkleene Jul 3, 2025
dfc3638
one more tiny one
mkleene Jul 3, 2025
93910da
more sonar
mkleene Jul 3, 2025
f94c1df
gemini suggestions
mkleene Jul 3, 2025
4e6be46
more gemini
mkleene Jul 3, 2025
f1a3244
even more (incorrect) gemini
mkleene Jul 3, 2025
f11faf9
Merge branch 'main' into base-key
mkleene Jul 10, 2025
bd64b41
Merge branch 'main' into base-key
mkleene Jul 21, 2025
a5f0c64
Merge remote-tracking branch 'origin/main' into base-key
mkleene Jul 21, 2025
aaebc79
clarify
mkleene Jul 21, 2025
f8aff1b
thread the algorithm through
mkleene Jul 22, 2025
e233de4
clarify a bit
mkleene Jul 22, 2025
cb94c03
add a key
mkleene Jul 22, 2025
98b95d9
test errors
mkleene Jul 22, 2025
e20ad25
remove unused methods
mkleene Jul 22, 2025
c442ff8
pump those coverage numbers
mkleene Jul 22, 2025
4e3b907
fix test
mkleene Jul 22, 2025
9124da8
Update KeyType.java
mkleene Jul 22, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import io.opentdf.platform.sdk.*;

import java.util.Collections;
import java.util.concurrent.ExecutionException;

import java.util.List;

Expand Down
2 changes: 1 addition & 1 deletion sdk/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<kotlin.version>2.1.0</kotlin.version>
<connect.version>0.7.2</connect.version>
<okhttp.version>4.12.0</okhttp.version>
<platform.branch>protocol/go/v0.3.0</platform.branch>
<platform.branch>protocol/go/v0.5.0</platform.branch>
</properties>
<dependencies>
<!-- Logging Dependencies -->
Expand Down
478 changes: 300 additions & 178 deletions sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java

Large diffs are not rendered by default.

41 changes: 39 additions & 2 deletions sdk/src/main/java/io/opentdf/platform/sdk/Config.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package io.opentdf.platform.sdk;

import io.opentdf.platform.policy.Algorithm;
import io.opentdf.platform.policy.KeyAccessServer;
import io.opentdf.platform.policy.SimpleKasKey;
import io.opentdf.platform.policy.Value;
import io.opentdf.platform.sdk.Autoconfigure.AttributeValueFQN;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.opentdf.platform.sdk.Autoconfigure.algProto2String;

/**
* Configuration class for setting various configurations related to TDF.
Expand All @@ -22,6 +30,7 @@ public class Config {
public static final String KAS_PUBLIC_KEY_PATH = "/kas_public_key";
public static final String DEFAULT_MIME_TYPE = "application/octet-stream";
public static final int MAX_COLLECTION_ITERATION = (1 << 24) - 1;
private static Logger logger = LoggerFactory.getLogger(Config.class);

public enum TDFFormat {
JSONFormat,
Expand All @@ -33,8 +42,6 @@ public enum IntegrityAlgorithm {
GMAC
}

public static final int K_HTTP_OK = 200;

public static class KASInfo implements Cloneable {
public String URL;
public String PublicKey;
Expand Down Expand Up @@ -71,6 +78,36 @@ public String toString() {
}
return sb.append("}").toString();
}

public static List<KASInfo> fromKeyAccessServer(KeyAccessServer kas) {
var keys = kas.getPublicKey().getCached().getKeysList();
if (keys.isEmpty()) {
logger.warn("Invalid KAS key mapping for kas [{}]: publicKey is empty", kas.getUri());
return Collections.emptyList();
}
return keys.stream().flatMap(ki -> {
if (ki.getPem().isEmpty()) {
logger.warn("Invalid KAS key mapping for kas [{}]: publicKey PEM is empty", kas.getUri());
return Stream.empty();
}
Config.KASInfo kasInfo = new Config.KASInfo();
kasInfo.URL = kas.getUri();
kasInfo.KID = ki.getKid();
kasInfo.Algorithm = algProto2String(ki.getAlg());
kasInfo.PublicKey = ki.getPem();
return Stream.of(kasInfo);
}).collect(Collectors.toList());
}

public static KASInfo fromSimpleKasKey(SimpleKasKey ki) {
Config.KASInfo kasInfo = new Config.KASInfo();
kasInfo.URL = ki.getKasUri();
kasInfo.KID = ki.getPublicKey().getKid();
kasInfo.Algorithm = algProto2String(ki.getPublicKey().getAlgorithm());
kasInfo.PublicKey = ki.getPublicKey().getPem();

return kasInfo;
}
}

public static class AssertionVerificationKeys {
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve)

@Override
public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) {
Config.KASInfo cachedValue = this.kasKeyCache.get(kasInfo.URL, kasInfo.Algorithm);
Config.KASInfo cachedValue = this.kasKeyCache.get(kasInfo.URL, kasInfo.Algorithm, kasInfo.KID);
if (cachedValue != null) {
return cachedValue;
}
Expand Down
12 changes: 7 additions & 5 deletions sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ public void clear() {
this.cache = new HashMap<>();
}

public Config.KASInfo get(String url, String algorithm) {
log.debug("retrieving kasinfo for url = [{}], algorithm = [{}]", url, algorithm);
KASKeyRequest cacheKey = new KASKeyRequest(url, algorithm);
public Config.KASInfo get(String url, String algorithm, String kid) {
log.debug("retrieving kasinfo for url = [{}], algorithm = [{}], kid = [{}]", url, algorithm, kid);
KASKeyRequest cacheKey = new KASKeyRequest(url, algorithm, kid);
LocalDateTime now = LocalDateTime.now();
TimeStampedKASInfo cachedValue = cache.get(cacheKey);

Expand All @@ -49,7 +49,7 @@ public Config.KASInfo get(String url, String algorithm) {

public void store(Config.KASInfo kasInfo) {
log.debug("storing kasInfo into the cache {}", kasInfo);
KASKeyRequest cacheKey = new KASKeyRequest(kasInfo.URL, kasInfo.Algorithm);
KASKeyRequest cacheKey = new KASKeyRequest(kasInfo.URL, kasInfo.Algorithm, kasInfo.KID);
cache.put(cacheKey, new TimeStampedKASInfo(kasInfo, LocalDateTime.now()));
}
}
Expand Down Expand Up @@ -85,10 +85,12 @@ public TimeStampedKASInfo(Config.KASInfo kasInfo, LocalDateTime timestamp) {
class KASKeyRequest {
private String url;
private String algorithm;
private String kid;

public KASKeyRequest(String url, String algorithm) {
public KASKeyRequest(String url, String algorithm, String kid) {
this.url = url;
this.algorithm = algorithm;
this.kid = kid;
}

// Override equals and hashCode to ensure proper functioning of the HashMap
Expand Down
191 changes: 191 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/Planner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
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 io.opentdf.platform.policy.Algorithm;
import io.opentdf.platform.policy.SimpleKasKey;
import io.opentdf.platform.policy.SimpleKasPublicKey;
import io.opentdf.platform.policy.Value;
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationRequest;
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

public class Planner {
private static final String BASE_KEY = "base_key";
private final Config.TDFConfig tdfConfig;
private final SDK.Services services;


private static final Logger logger = LoggerFactory.getLogger(Planner.class);

public Planner(Config.TDFConfig config, SDK.Services services) {
this.tdfConfig = Objects.requireNonNull(config);
this.services = Objects.requireNonNull(services);
}

private static String getUUID() {
return UUID.randomUUID().toString();
}

Map<String, List<Config.KASInfo>> getSplits(Config.TDFConfig tdfConfig) {
List<Autoconfigure.KeySplitStep> splitPlan;
if (tdfConfig.autoconfigure) {
if (tdfConfig.splitPlan != null && !tdfConfig.splitPlan.isEmpty()) {
throw new IllegalArgumentException("cannot use autoconfigure with a split plan provided in the TDFConfig");
}
splitPlan = getAutoconfigurePlan(tdfConfig);
} else if (tdfConfig.splitPlan == null || tdfConfig.splitPlan.isEmpty()) {
splitPlan = generatePlanFromProvidedKases(tdfConfig.kasInfoList);
} else {
splitPlan = tdfConfig.splitPlan;
}

if (tdfConfig.kasInfoList.isEmpty() && splitPlan.isEmpty()) {
throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred");
}
return resolveKeys(splitPlan);
}

private List<Autoconfigure.KeySplitStep> getAutoconfigurePlan(Config.TDFConfig tdfConfig) {
Autoconfigure.Granter granter = new Autoconfigure.Granter(new ArrayList<>());
if (tdfConfig.attributeValues != null && !tdfConfig.attributeValues.isEmpty()) {
granter = Autoconfigure.newGranterFromAttributes(services.kas().getKeyCache(), tdfConfig.attributeValues.toArray(new Value[0]));
} else if (tdfConfig.attributes != null && !tdfConfig.attributes.isEmpty()) {
granter = Autoconfigure.newGranterFromService(services.attributes(), services.kas().getKeyCache(),
tdfConfig.attributes.toArray(new Autoconfigure.AttributeValueFQN[0]));
}
return granter.getSplits(defaultKases(tdfConfig), Planner::getUUID, this::fetchBaseKey);
}

List<Autoconfigure.KeySplitStep> generatePlanFromProvidedKases(List<Config.KASInfo> kases) {
if (kases.size() == 1) {
var kasInfo = kases.get(0);
return Collections.singletonList(new Autoconfigure.KeySplitStep(kasInfo.URL, "", kasInfo.KID));
}
List<Autoconfigure.KeySplitStep> splitPlan = new ArrayList<>();
for (var kasInfo : kases) {
splitPlan.add(new Autoconfigure.KeySplitStep(kasInfo.URL, getUUID(), kasInfo.KID));
}
return splitPlan;
}

Optional<SimpleKasKey> fetchBaseKey() {
var responseMessage = services.wellknown()
.getWellKnownConfigurationBlocking(GetWellKnownConfigurationRequest.getDefaultInstance(), Collections.emptyMap())
.execute();
GetWellKnownConfigurationResponse response;
try {
response = RequestHelper.getOrThrow(responseMessage);
} catch (ConnectException e) {
logger.error("unable to retrieve configuration from well known endpoint", e);
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);
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 (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);
return Optional.empty();
}

return Optional.of(SimpleKasKey.newBuilder()
.setKasUri(baseKey.kasUrl)
.setPublicKey(
SimpleKasPublicKey.newBuilder()
.setKid(baseKey.publicKey.kid)
.setAlgorithm(baseKey.publicKey.algorithm)
.setPem(baseKey.publicKey.pem)
.build())
.build());
}

private static Gson gson = new Gson();

private static class BaseKey {
@SerializedName("kas_url")
String kasUrl;

@SerializedName("public_key")
Key publicKey;

private static class Key {
String kid;
String pem;
Algorithm algorithm;
}
}


Map<String, List<Config.KASInfo>> resolveKeys(List<Autoconfigure.KeySplitStep> splitPlan) {
Map<String, List<Config.KASInfo>> conjunction = new HashMap<>();
var latestKASInfo = new HashMap<String, Config.KASInfo>();
// Seed anything passed in manually
for (Config.KASInfo kasInfo : tdfConfig.kasInfoList) {
if (kasInfo.PublicKey != null && !kasInfo.PublicKey.isEmpty()) {
latestKASInfo.put(kasInfo.URL, kasInfo);
}
}

for (Autoconfigure.KeySplitStep splitInfo: splitPlan) {
// Public key was passed in with kasInfoList
// TODO First look up in attribute information / add to split plan?
Config.KASInfo ki = latestKASInfo.get(splitInfo.kas);
if (ki == null || ki.PublicKey == null || ki.PublicKey.isBlank() || (splitInfo.kid != null && !splitInfo.kid.equals(ki.KID))) {
logger.info("no public key provided for KAS at {}, retrieving", splitInfo.kas);
var getKI = new Config.KASInfo();
getKI.URL = splitInfo.kas;
if (!tdfConfig.autoconfigure) {
getKI.Algorithm = tdfConfig.wrappingKeyType.toString();
}
getKI.KID = splitInfo.kid;
getKI = services.kas().getPublicKey(getKI);
latestKASInfo.put(splitInfo.kas, getKI);
ki = getKI;
}
conjunction.computeIfAbsent(splitInfo.splitID, s -> new ArrayList<>()).add(ki);
}
return conjunction;
}

static List<String> defaultKases(Config.TDFConfig config) {
List<String> allk = new ArrayList<>();
List<String> defk = new ArrayList<>();

for (Config.KASInfo kasInfo : config.kasInfoList) {
if (kasInfo.Default != null && kasInfo.Default) {
defk.add(kasInfo.URL);
} else if (defk.isEmpty()) {
allk.add(kasInfo.URL);
}
}
return defk.isEmpty() ? allk : defk;
}
}
3 changes: 3 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDK.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.opentdf.platform.policy.namespaces.NamespaceServiceClientInterface;
import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceClientInterface;
import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceClientInterface;
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface;

import javax.net.ssl.TrustManager;
import java.io.IOException;
Expand Down Expand Up @@ -75,6 +76,8 @@ public interface Services extends AutoCloseable {

KeyAccessServerRegistryServiceClientInterface kasRegistry();

WellKnownServiceClientInterface wellknown();

KAS kas();
}

Expand Down
7 changes: 7 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationRequest;
import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse;
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClient;
import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface;
import nl.altindag.ssl.SSLFactory;
import nl.altindag.ssl.pem.util.PemUtils;
import okhttp3.OkHttpClient;
Expand Down Expand Up @@ -251,6 +252,7 @@ ServicesAndInternals buildServices() {
var resourceMappingService = new ResourceMappingServiceClient(client);
var authorizationService = new AuthorizationServiceClient(client);
var kasRegistryService = new KeyAccessServerRegistryServiceClient(client);
var wellKnownService = new WellKnownServiceClient(client);

var services = new SDK.Services() {
@Override
Expand Down Expand Up @@ -290,6 +292,11 @@ public KeyAccessServerRegistryServiceClient kasRegistry() {
return kasRegistryService;
}

@Override
public WellKnownServiceClientInterface wellknown() {
return wellKnownService;
}

@Override
public SDK.KAS kas() {
return kasClient;
Expand Down
Loading
Loading