-
Notifications
You must be signed in to change notification settings - Fork 2
feat(sdk)!: add base key and support for key grants in ZTDF #271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 14 commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
a2d8b83
minor refactor
mkleene 4b6a10e
now at least we are adding things from grants
mkleene 075b4ac
just saving
mkleene 744220e
just saving
mkleene c8b458f
saving
mkleene 9fb0bd6
get tests passing again
mkleene b71f57e
pull the planning inside of autoconfigure
mkleene 2d4f3e6
cleanup
mkleene 65a4987
debug
mkleene 2cfd3ec
only plan when we mean to
mkleene 98653b9
restructure
mkleene e900df3
add some tests
mkleene 78031d8
one more test
mkleene 8c0bcf4
add test for resolving keys
mkleene c8370e7
added a split
mkleene 41f3d6a
fix up tests
mkleene 491cbf3
a couple more tests
mkleene 26016f3
remove condition we do not need
mkleene 1ba4cd5
add test for default kases
mkleene 95232ad
add more tests
mkleene 7f5a2d4
more tests
mkleene 76085d3
sonarcloud
mkleene d34056e
more sonar
mkleene dfc3638
one more tiny one
mkleene 93910da
more sonar
mkleene f94c1df
gemini suggestions
mkleene 4e6be46
more gemini
mkleene f1a3244
even more (incorrect) gemini
mkleene f11faf9
Merge branch 'main' into base-key
mkleene bd64b41
Merge branch 'main' into base-key
mkleene a5f0c64
Merge remote-tracking branch 'origin/main' into base-key
mkleene aaebc79
clarify
mkleene f8aff1b
thread the algorithm through
mkleene e233de4
clarify a bit
mkleene cb94c03
add a key
mkleene 98b95d9
test errors
mkleene e20ad25
remove unused methods
mkleene c442ff8
pump those coverage numbers
mkleene 4e3b907
fix test
mkleene 9124da8
Update KeyType.java
mkleene File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
478 changes: 300 additions & 178 deletions
478
sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.