From a2d8b83457cff7967f411cd86835ad065fa45e7e Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 18 Jun 2025 08:20:01 -0400 Subject: [PATCH 01/37] minor refactor --- .../opentdf/platform/sdk/Autoconfigure.java | 118 ++++++++---------- .../java/io/opentdf/platform/sdk/TDF.java | 46 ++----- 2 files changed, 57 insertions(+), 107 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index b60d0725..66accc1c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -14,6 +14,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -53,11 +54,11 @@ class RuleType { * This class includes functionality to create granter instances based on * attributes either from a list of attribute values or from a service. */ -public class Autoconfigure { +class Autoconfigure { - public static Logger logger = LoggerFactory.getLogger(Autoconfigure.class); + private static Logger logger = LoggerFactory.getLogger(Autoconfigure.class); - public static class KeySplitStep { + static class KeySplitStep { public String kas; public String splitID; @@ -93,7 +94,7 @@ public int hashCode() { } // Utility class for an attribute name FQN. - public static class AttributeNameFQN { + static class AttributeNameFQN { private final String url; private final String key; @@ -156,7 +157,7 @@ public String name() throws AutoConfigureException { } // Utility class for an attribute value FQN. - public static class AttributeValueFQN { + static class AttributeValueFQN { private final String url; private final String key; @@ -203,11 +204,11 @@ public int hashCode() { return Objects.hash(key); } - public String getKey() { + String getKey() { return key; } - public String authority() { + String authority() { Pattern pattern = Pattern.compile("^(https?://[\\w./-]+)/attr/\\S*/value/\\S*$"); Matcher matcher = pattern.matcher(url); if (!matcher.find()) { @@ -216,7 +217,7 @@ public String authority() { return matcher.group(1); } - public AttributeNameFQN prefix() throws AutoConfigureException { + AttributeNameFQN prefix() throws AutoConfigureException { Pattern pattern = Pattern.compile("^(https?://[\\w./-]+/attr/\\S*)/value/\\S*$"); Matcher matcher = pattern.matcher(url); if (!matcher.find()) { @@ -225,7 +226,7 @@ public AttributeNameFQN prefix() throws AutoConfigureException { return new AttributeNameFQN(matcher.group(1)); } - public String value() { + String value() { Pattern pattern = Pattern.compile("^https?://[\\w./-]+/attr/\\S*/value/(\\S*)$"); Matcher matcher = pattern.matcher(url); if (!matcher.find()) { @@ -238,21 +239,21 @@ public String value() { } } - public String name() { + String name() { Pattern pattern = Pattern.compile("^https?://[\\w./-]+/attr/(\\S*)/value/\\S*$"); Matcher matcher = pattern.matcher(url); if (!matcher.find()) { throw new RuntimeException("invalid attributeInstance"); } try { - return URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException | IllegalArgumentException e) { + return URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { throw new RuntimeException("invalid attributeInstance", e); } } } - public static class KeyAccessGrant { + static class KeyAccessGrant { public Attribute attr; public List kases; @@ -295,11 +296,11 @@ public void addAllGrants(AttributeValueFQN fqn, List gs, Attrib } } - public KeyAccessGrant byAttribute(AttributeValueFQN fqn) { + KeyAccessGrant byAttribute(AttributeValueFQN fqn) { return grants.get(fqn.key); } - public List plan(List defaultKas, Supplier genSplitID) + @Nonnull List plan(List defaultKas, Supplier genSplitID) throws AutoConfigureException { AttributeBooleanExpression b = constructAttributeBoolean(); BooleanKeyExpression k = insertKeysForAttribute(b); @@ -311,17 +312,7 @@ public List plan(List defaultKas, Supplier genSpli int l = k.size(); if (l == 0) { // default behavior: split key across all default KAS - if (defaultKas.isEmpty()) { - throw new AutoConfigureException("no default KAS specified; required for grantless plans"); - } else if (defaultKas.size() == 1) { - return Collections.singletonList(new KeySplitStep(defaultKas.get(0), "")); - } else { - List result = new ArrayList<>(); - for (String kas : defaultKas) { - result.add(new KeySplitStep(kas, genSplitID.get())); - } - return result; - } + return generatePlanFromDefaultKases(defaultKas, genSplitID); } List steps = new ArrayList<>(); @@ -334,6 +325,20 @@ public List plan(List defaultKas, Supplier genSpli return steps; } + static List generatePlanFromDefaultKases(List defaultKas, Supplier genSplitID) { + if (defaultKas.isEmpty()) { + throw new AutoConfigureException("no default KAS specified; required for grantless plans"); + } else if (defaultKas.size() == 1) { + return Collections.singletonList(new KeySplitStep(defaultKas.get(0), "")); + } else { + List result = new ArrayList<>(); + for (String kas : defaultKas) { + result.add(new KeySplitStep(kas, genSplitID.get())); + } + return result; + } + } + BooleanKeyExpression insertKeysForAttribute(AttributeBooleanExpression e) throws AutoConfigureException { List kcs = new ArrayList<>(e.must.size()); @@ -348,6 +353,7 @@ BooleanKeyExpression insertKeysForAttribute(AttributeBooleanExpression e) throws List kases = grant.kases; if (kases.isEmpty()) { + // TODO: replace this with a reference to the base key kases = List.of(RuleType.EMPTY_TERM); } @@ -368,6 +374,13 @@ BooleanKeyExpression insertKeysForAttribute(AttributeBooleanExpression e) throws return new BooleanKeyExpression(kcs); } + /** + * Constructs an AttributeBooleanExpression from the policy, splitting each attribute + * into its own clause. Each clause contains the attribute definition and a list of + * values. + * @return + * @throws AutoConfigureException + */ AttributeBooleanExpression constructAttributeBoolean() throws AutoConfigureException { Map prefixes = new HashMap<>(); List sortedPrefixes = new ArrayList<>(); @@ -378,7 +391,7 @@ AttributeBooleanExpression constructAttributeBoolean() throws AutoConfigureExcep clause.values.add(aP); } else if (byAttribute(aP) != null) { var x = new SingleAttributeClause(byAttribute(aP).attr, - new ArrayList(Arrays.asList(aP))); + new ArrayList<>(Arrays.asList(aP))); prefixes.put(a.getKey(), x); sortedPrefixes.add(a.getKey()); } @@ -391,39 +404,6 @@ AttributeBooleanExpression constructAttributeBoolean() throws AutoConfigureExcep return new AttributeBooleanExpression(must); } - static class AttributeMapping { - - private Map dict; - - public AttributeMapping() { - this.dict = new HashMap<>(); - } - - public void put(Attribute ad) throws AutoConfigureException { - if (this.dict == null) { - this.dict = new HashMap<>(); - } - - AttributeNameFQN prefix = new AttributeNameFQN(ad.getFqn()); - - if (this.dict.containsKey(prefix)) { - throw new AutoConfigureException("Attribute prefix already found: [" + prefix.toString() + "]"); - } - - this.dict.put(prefix, ad); - } - - public Attribute get(AttributeNameFQN prefix) throws AutoConfigureException { - Attribute ad = this.dict.get(prefix); - if (ad == null) { - throw new AutoConfigureException("Unknown attribute type: [" + prefix.toString() + "], not in [" - + this.dict.keySet().toString() + "]"); - } - return ad; - } - - } - static class SingleAttributeClause { private Attribute def; @@ -435,9 +415,9 @@ public SingleAttributeClause(Attribute def, List values) { } } - class AttributeBooleanExpression { + static class AttributeBooleanExpression { - private List must; + private final List must; public AttributeBooleanExpression(List must) { this.must = must; @@ -478,7 +458,7 @@ public String toString() { } - public class PublicKeyInfo { + public static class PublicKeyInfo { private String kas; public PublicKeyInfo(String kas) { @@ -494,9 +474,9 @@ public void setKas(String kas) { } } - public class KeyClause { - private String operator; - private List values; + public static class KeyClause { + private final String operator; + private final List values; public KeyClause(String operator, List values) { this.operator = operator; @@ -531,8 +511,8 @@ public String toString() { } } - public class BooleanKeyExpression { - private List values; + public static class BooleanKeyExpression { + private final List values; public BooleanKeyExpression(List values) { this.values = values; @@ -612,7 +592,7 @@ public Disjunction sortedNoDupes(List l) { } - class Disjunction extends ArrayList { + static class Disjunction extends ArrayList { public boolean less(Disjunction r) { int m = Math.min(this.size(), r.size()); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index fe547caa..b5b75466 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -28,6 +28,7 @@ import java.security.*; import java.text.ParseException; import java.util.*; +import java.util.stream.Collectors; /** * The TDF class is responsible for handling operations related to @@ -151,22 +152,6 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { String base64PolicyObject = encoder .encodeToString(gson.toJson(policyObject).getBytes(StandardCharsets.UTF_8)); Map latestKASInfo = new HashMap<>(); - if (tdfConfig.splitPlan == null || tdfConfig.splitPlan.isEmpty()) { - // Default split plan: Split keys across all KASes - List splitPlan = new ArrayList<>(tdfConfig.kasInfoList.size()); - int i = 0; - for (Config.KASInfo kasInfo : tdfConfig.kasInfoList) { - Autoconfigure.KeySplitStep step = new Autoconfigure.KeySplitStep(kasInfo.URL, ""); - if (tdfConfig.kasInfoList.size() > 1) { - step.splitID = String.format("s-%d", i++); - } - splitPlan.add(step); - if (kasInfo.PublicKey != null && !kasInfo.PublicKey.isEmpty()) { - latestKASInfo.put(kasInfo.URL, kasInfo); - } - } - tdfConfig.splitPlan = splitPlan; - } // Seed anything passed in manually for (Config.KASInfo kasInfo : tdfConfig.kasInfoList) { @@ -177,7 +162,6 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { // split plan: restructure by conjunctions Map> conjunction = new HashMap<>(); - List splitIDs = new ArrayList<>(); for (Autoconfigure.KeySplitStep splitInfo : tdfConfig.splitPlan) { // Public key was passed in with kasInfoList @@ -192,18 +176,11 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { latestKASInfo.put(splitInfo.kas, getKI); ki = getKI; } - if (conjunction.containsKey(splitInfo.splitID)) { - conjunction.get(splitInfo.splitID).add(ki); - } else { - List newList = new ArrayList<>(); - newList.add(ki); - conjunction.put(splitInfo.splitID, newList); - splitIDs.add(splitInfo.splitID); - } + conjunction.computeIfAbsent(splitInfo.splitID, s -> new ArrayList<>()).add(ki); } - List symKeys = new ArrayList<>(splitIDs.size()); - for (String splitID : splitIDs) { + List symKeys = new ArrayList<>(conjunction.size()); + for (String splitID : conjunction.keySet()) { // Symmetric key byte[] symKey = new byte[GCM_KEY_SIZE]; sRandom.nextBytes(symKey); @@ -401,6 +378,7 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException { + List dk = defaultKases(tdfConfig); if (tdfConfig.autoconfigure) { Autoconfigure.Granter granter = new Autoconfigure.Granter(new ArrayList<>()); if (tdfConfig.attributeValues != null && !tdfConfig.attributeValues.isEmpty()) { @@ -413,14 +391,9 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo if (granter == null) { throw new AutoConfigureException("Failed to create Granter"); // Replace with appropriate error handling } - - List dk = defaultKases(tdfConfig); tdfConfig.splitPlan = granter.plan(dk, () -> UUID.randomUUID().toString()); - - if (tdfConfig.splitPlan == null) { - throw new AutoConfigureException("Failed to generate Split Plan"); // Replace with appropriate error - // handling - } + } else { + tdfConfig.splitPlan = Autoconfigure.Granter.generatePlanFromDefaultKases(dk, () -> UUID.randomUUID().toString()); } if (tdfConfig.kasInfoList.isEmpty() && (tdfConfig.splitPlan == null || tdfConfig.splitPlan.isEmpty())) { @@ -572,10 +545,7 @@ static List defaultKases(TDFConfig config) { allk.add(kasInfo.URL); } } - if (defk.isEmpty()) { - return allk; - } - return defk; + return defk.isEmpty() ? allk : defk; } Reader loadTDF(SeekableByteChannel tdf, String platformUrl) throws SDKException, IOException { From 4b6a10e0e5abb4d6daf7e1e96ccb3bbeea8a18ab Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 26 Jun 2025 13:14:57 +0200 Subject: [PATCH 02/37] now at least we are adding things from grants --- sdk/pom.xml | 2 +- .../opentdf/platform/sdk/Autoconfigure.java | 82 +++++++++++++------ .../java/io/opentdf/platform/sdk/Config.java | 39 +++++++++ .../java/io/opentdf/platform/sdk/TDF.java | 2 +- .../platform/sdk/AutoconfigureTest.java | 18 ++-- 5 files changed, 104 insertions(+), 39 deletions(-) diff --git a/sdk/pom.xml b/sdk/pom.xml index 795d34ba..a17f99ef 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -15,7 +15,7 @@ 2.1.0 0.7.2 4.12.0 - protocol/go/v0.3.0 + protocol/go/v0.5.0 diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 66accc1c..f4cb0d76 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -1,16 +1,21 @@ package io.opentdf.platform.sdk; import com.connectrpc.ResponseMessageKt; +import io.opentdf.platform.policy.Algorithm; import io.opentdf.platform.policy.Attribute; import io.opentdf.platform.policy.AttributeRuleTypeEnum; import io.opentdf.platform.policy.AttributeValueSelector; +import io.opentdf.platform.policy.KasKey; import io.opentdf.platform.policy.KasPublicKey; import io.opentdf.platform.policy.KasPublicKeyAlgEnum; import io.opentdf.platform.policy.KeyAccessServer; +import io.opentdf.platform.policy.PublicKey; +import io.opentdf.platform.policy.SimpleKasKey; import io.opentdf.platform.policy.Value; import io.opentdf.platform.policy.attributes.AttributesServiceClientInterface; import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsRequest; import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse; +import org.checkerframework.checker.units.qual.A; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,10 +35,12 @@ import java.util.Objects; import java.util.Set; import java.util.StringJoiner; +import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * The RuleType class defines a set of constants that represent various types of attribute rules. @@ -273,7 +280,7 @@ public Granter(List policy) { } public Map getGrants() { - return new HashMap(grants); + return new HashMap<>(grants); } public List getPolicy() { @@ -663,7 +670,7 @@ public static String ruleToOperator(AttributeRuleTypeEnum e) { // Given a policy (list of data attributes or tags), // get a set of grants from attribute values to KASes. // Unlike `NewGranterFromService`, this works offline. - public static Granter newGranterFromAttributes(Value... attrValues) throws AutoConfigureException { + public static Granter newGranterFromAttributes(KASKeyCache keyCache, Value... attrValues) throws AutoConfigureException { var attrsAndValues = Arrays.stream(attrValues).map(v -> { if (!v.hasAttribute()) { throw new AutoConfigureException("tried to use an attribute that is not initialized"); @@ -674,7 +681,7 @@ public static Granter newGranterFromAttributes(Value... attrValues) throws AutoC .build(); }).collect(Collectors.toList()); - return getGranter(null, attrsAndValues); + return getGranter(keyCache, attrsAndValues); } // Gets a list of directory of KAS grants for a list of attribute FQNs @@ -722,42 +729,69 @@ private static List getGrants(GetAttributeValuesByFqnsResponse. } - private static Granter getGranter(@Nullable KASKeyCache keyCache, List values) { + private static Granter getGranter(KASKeyCache keyCache, List values) { Granter grants = new Granter(values.stream().map(GetAttributeValuesByFqnsResponse.AttributeAndValue::getValue).map(Value::getFqn).map(AttributeValueFQN::new).collect(Collectors.toList())); - - for (var attributeAndValue: values) { - var attributeGrants = getGrants(attributeAndValue); + for (var attributeAndValue : values) { String fqnstr = attributeAndValue.getValue().getFqn(); AttributeValueFQN fqn = new AttributeValueFQN(fqnstr); + + var attributeGrants = getGrants(attributeAndValue); + var grantKasInfos = attributeGrants.stream().map(Config.KASInfo::fromKeyAccessServer).reduce((l1, l2) -> new ArrayList<>(){ + { + addAll(l1); + addAll(l2); + } + }).orElse(Collections.emptyList()); + storeKeysToCache(grantKasInfos, keyCache); grants.addAllGrants(fqn, attributeGrants, attributeAndValue.getAttribute()); - if (keyCache != null) { - storeKeysToCache(attributeGrants, keyCache); - } + + var mappedKeys = getMappedKeys(attributeAndValue); + var mappedKasInfos = mappedKeys.stream().map(Config.KASInfo::fromSimpleKasKey).collect(Collectors.toList()); + + storeKeysToCache(mappedKasInfos, keyCache); } return grants; } + private static List getMappedKeys(GetAttributeValuesByFqnsResponse.AttributeAndValue attributeAndValue) { + var val = attributeAndValue.getValue(); + var attribute = attributeAndValue.getAttribute(); + Function printKasKey = k -> String.format("%s %s", k.getKasUri(), k.getPublicKey().getKid()); - static void storeKeysToCache(List kases, KASKeyCache keyCache) { - for (KeyAccessServer kas : kases) { - List keys = kas.getPublicKey().getCached().getKeysList(); - if (keys.isEmpty()) { - logger.debug("No cached key in policy service for KAS: " + kas.getUri()); - continue; + if (!val.getKasKeysList().isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug("adding kas keys from attribute value [{}]: {}", val.getFqn(), val.getKasKeysList().stream().map(printKasKey).collect(Collectors.toList())); } - for (KasPublicKey ki : keys) { - Config.KASInfo kasInfo = new Config.KASInfo(); - kasInfo.URL = kas.getUri(); - kasInfo.KID = ki.getKid(); - kasInfo.Algorithm = algProto2String(ki.getAlg()); - kasInfo.PublicKey = ki.getPem(); - keyCache.store(kasInfo); + return val.getKasKeysList(); + } else if (!attribute.getKasKeysList().isEmpty()) { + var kasKeys = attribute.getKasKeysList(); + if (logger.isDebugEnabled()) { + logger.debug("adding kas keys from attribute [{}]: {}", attribute.getFqn(), kasKeys.stream().map(printKasKey).collect(Collectors.toList())); + } + return kasKeys; + } else if (!attribute.getNamespace().getKasKeysList().isEmpty()) { + var kasKeys = attribute.getNamespace().getKasKeysList(); + if (logger.isDebugEnabled()) { + logger.debug("adding kas keys from namespace [{}]: [{}]", attribute.getNamespace().getName(), kasKeys.stream().map(printKasKey).collect(Collectors.toList())); } + return kasKeys; + } else { + // this is needed to mark the fact that we have an empty + if (logger.isDebugEnabled()) { + logger.debug("didn't find any kas keys on value, attribute, or namespace for attribute value [{}]", val.getFqn()); + } + return Collections.emptyList(); + } + } + + static void storeKeysToCache(List kases, KASKeyCache keyCache) { + if (keyCache != null) { + kases.forEach(keyCache::store); } } - private static String algProto2String(KasPublicKeyAlgEnum e) { + static String algProto2String(KasPublicKeyAlgEnum e) { switch (e) { case KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1: return "ec:secp256r1"; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index 0a1f4a46..b6b62d9a 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -1,13 +1,21 @@ package io.opentdf.platform.sdk; +import io.opentdf.platform.policy.KasPublicKeyAlgEnum; +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. @@ -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, @@ -71,6 +80,36 @@ public String toString() { } return sb.append("}").toString(); } + + public static List 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(Enum.valueOf(KasPublicKeyAlgEnum.class, ki.getPublicKey().getAlgorithm().name())); + kasInfo.PublicKey = ki.getPublicKey().getPem(); + + return kasInfo; + } } public static class AssertionVerificationKeys { diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index b5b75466..b4fcf210 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -382,7 +382,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo if (tdfConfig.autoconfigure) { Autoconfigure.Granter granter = new Autoconfigure.Granter(new ArrayList<>()); if (tdfConfig.attributeValues != null && !tdfConfig.attributeValues.isEmpty()) { - granter = Autoconfigure.newGranterFromAttributes(tdfConfig.attributeValues.toArray(new Value[0])); + 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 AttributeValueFQN[0])); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index 59cd0912..93f92285 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -368,7 +368,7 @@ public void testConfigurationServicePutGet() { for (ConfigurationTestCase tc : testCases) { assertDoesNotThrow(() -> { List v = valuesToPolicy(tc.getPolicy().toArray(new AttributeValueFQN[0])); - Granter grants = Autoconfigure.newGranterFromAttributes(v.toArray(new Value[0])); + Granter grants = Autoconfigure.newGranterFromAttributes(null, v.toArray(new Value[0])); assertThat(grants).isNotNull(); assertThat(grants.getGrants()).hasSize(tc.getSize()); assertThat(policyToStringKeys(tc.getPolicy())).containsAll(grants.getGrants().keySet()); @@ -451,7 +451,7 @@ public void testReasonerConstructAttributeBoolean() { new KeySplitStep(KAS_US_HCS, "2"), new KeySplitStep(KAS_US_SA, "3")))); for (ReasonerTestCase tc : testCases) { - Granter reasoner = Autoconfigure.newGranterFromAttributes( + Granter reasoner = Autoconfigure.newGranterFromAttributes(null, valuesToPolicy(tc.getPolicy().toArray(new AttributeValueFQN[0])).toArray(new Value[0])); assertThat(reasoner).isNotNull(); @@ -748,9 +748,7 @@ void testStoreKeysToCache_NoKeys() { KasPublicKeySet.newBuilder())) .build(); - List kases = List.of(kas1); - - Autoconfigure.storeKeysToCache(kases, keyCache); + Autoconfigure.storeKeysToCache(Config.KASInfo.fromKeyAccessServer(kas1), keyCache); verify(keyCache, never()).store(any(Config.KASInfo.class)); } @@ -780,11 +778,8 @@ void testStoreKeysToCache_WithKeys() { .setUri("https://example.com/kas") .build(); - // Add the KeyAccessServer to a list - List kases = List.of(kas1); - // Call the method under test - Autoconfigure.storeKeysToCache(kases, keyCache); + Autoconfigure.storeKeysToCache(Config.KASInfo.fromKeyAccessServer(kas1), keyCache); // Verify that the key was stored in the cache Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); @@ -826,11 +821,8 @@ void testStoreKeysToCache_MultipleKasEntries() { .setUri("https://example.com/kas") .build(); - // Add the KeyAccessServer to a list - List kases = List.of(kas1); - // Call the method under test - Autoconfigure.storeKeysToCache(kases, keyCache); + Autoconfigure.storeKeysToCache(Config.KASInfo.fromKeyAccessServer(kas1), keyCache); // Verify that the key was stored in the cache Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); From 075b4ac4499d8335d1855f50833df16c24f17b77 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Mon, 30 Jun 2025 15:27:18 +0200 Subject: [PATCH 03/37] just saving --- .../opentdf/platform/sdk/Autoconfigure.java | 56 +++++++++++++-- .../java/io/opentdf/platform/sdk/Config.java | 1 + .../platform/sdk/AutoconfigureTest.java | 70 +++++++++---------- 3 files changed, 85 insertions(+), 42 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index f4cb0d76..a8151ca8 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -274,6 +274,7 @@ public KeyAccessGrant(Attribute attr, List kases) { static class Granter { private final List policy; private final Map grants = new HashMap<>(); + private final Map> mappedKeys = new HashMap<>(); public Granter(List policy) { this.policy = policy; @@ -291,13 +292,52 @@ public void addGrant(AttributeValueFQN fqn, String kas, Attribute attr) { grants.computeIfAbsent(fqn.key, k -> new KeyAccessGrant(attr, new ArrayList<>())).kases.add(kas); } - public void addAllGrants(AttributeValueFQN fqn, List gs, Attribute attr) { - if (gs.isEmpty()) { + public void addAllGrants(AttributeValueFQN fqn, List granted, List mapped, Attribute attr) { + for (var mappedKey: mapped) { + mappedKeys.computeIfAbsent(fqn.key, k -> new ArrayList<>()).add(Config.KASInfo.fromSimpleKasKey(mappedKey)); + grants.computeIfAbsent(fqn.key, k -> new KeyAccessGrant(attr, new ArrayList<>())).kases.add(mappedKey.getKasUri()); + } + + if (!mappedKeys.isEmpty()) { + return; + } + + for (var grantedKey: granted) { + grants.computeIfAbsent(fqn.key, k -> new KeyAccessGrant(attr, new ArrayList<>())).kases.add(grantedKey.getUri()); + if (!grantedKey.getKasKeysList().isEmpty()) { + for (var kas : grantedKey.getKasKeysList()) { + mappedKeys.computeIfAbsent(fqn.key, k -> new ArrayList<>()).add(Config.KASInfo.fromSimpleKasKey(kas)); + } + continue; + } + var cachedGrantKeys = grantedKey.getPublicKey().getCached().getKeysList(); + if (cachedGrantKeys.isEmpty()) { + logger.info("no keys cached in policy service"); + continue; + } + for (var cachedGrantKey: cachedGrantKeys) { + var mappedKey = new Config.KASInfo(); + mappedKey.URL = grantedKey.getUri(); + mappedKey.KID = cachedGrantKey.getKid(); + mappedKey.Algorithm = Autoconfigure.algProto2String(cachedGrantKey.getAlg()); + mappedKey.PublicKey = cachedGrantKey.getPem(); + mappedKey.Default = false; + mappedKeys.computeIfAbsent(fqn.key, k -> new ArrayList<>()).add(mappedKey); + } + } + + if (mappedKeys.isEmpty() && grants.isEmpty()) { + grants.put(fqn.key, new KeyAccessGrant(attr, new ArrayList<>())); + } + } + + public void addMappedKeys(AttributeValueFQN fqn, List keys, Attribute attr) { + if (keys.isEmpty()) { grants.putIfAbsent(fqn.key, new KeyAccessGrant(attr, new ArrayList<>())); } else { - for (KeyAccessServer g : gs) { - if (g != null) { - addGrant(fqn, g.getUri(), attr); + for (SimpleKasKey key : keys) { + if (key != null) { + addGrant(fqn, key.getKasUri(), attr); } } } @@ -731,11 +771,13 @@ private static List getGrants(GetAttributeValuesByFqnsResponse. private static Granter getGranter(KASKeyCache keyCache, List values) { Granter grants = new Granter(values.stream().map(GetAttributeValuesByFqnsResponse.AttributeAndValue::getValue).map(Value::getFqn).map(AttributeValueFQN::new).collect(Collectors.toList())); - for (var attributeAndValue : values) { + for (var attributeAndValue: values) { String fqnstr = attributeAndValue.getValue().getFqn(); AttributeValueFQN fqn = new AttributeValueFQN(fqnstr); var attributeGrants = getGrants(attributeAndValue); + grants.addAllGrants(fqn, attributeGrants, attributeAndValue.getValue().getKasKeysList(), attributeAndValue.getAttribute()); + var grantKasInfos = attributeGrants.stream().map(Config.KASInfo::fromKeyAccessServer).reduce((l1, l2) -> new ArrayList<>(){ { addAll(l1); @@ -743,10 +785,10 @@ private static Granter getGranter(KASKeyCache keyCache, List testCases = List.of( - new ReasonerTestCase( - "uns.uns => default", - List.of(uns2uns), - List.of(KAS_US), - List.of(new KeySplitStep(KAS_US, ""))), - new ReasonerTestCase( - "uns.spk => spk", - List.of(uns2spk), - List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), - new ReasonerTestCase( - "spk.uns => spk", - List.of(spk2uns), - List.of(KAS_US), - List.of(new KeySplitStep(SPECIFIED_KAS, ""))), - new ReasonerTestCase( - "spk.spk => value.spk", - List.of(spk2spk), - List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), - new ReasonerTestCase( - "spk.spk & spk.uns => value.spk || attr.spk", - List.of(spk2spk, spk2uns), - List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"), new KeySplitStep(SPECIFIED_KAS, "1"))), - new ReasonerTestCase( - "spk.uns & spk.spk => value.spk || attr.spk", - List.of(spk2uns, spk2spk), - List.of(KAS_US), - List.of(new KeySplitStep(SPECIFIED_KAS, "1"), new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"))), - new ReasonerTestCase( - "uns.spk & spk.spk => value.spk", - List.of(spk2spk, uns2spk), - List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), +// new ReasonerTestCase( +// "uns.uns => default", +// List.of(uns2uns), +// List.of(KAS_US), +// List.of(new KeySplitStep(KAS_US, ""))), +// new ReasonerTestCase( +// "uns.spk => spk", +// List.of(uns2spk), +// List.of(KAS_US), +// List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), +// new ReasonerTestCase( +// "spk.uns => spk", +// List.of(spk2uns), +// List.of(KAS_US), +// List.of(new KeySplitStep(SPECIFIED_KAS, ""))), +// new ReasonerTestCase( +// "spk.spk => value.spk", +// List.of(spk2spk), +// List.of(KAS_US), +// List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), +// new ReasonerTestCase( +// "spk.spk & spk.uns => value.spk || attr.spk", +// List.of(spk2spk, spk2uns), +// List.of(KAS_US), +// List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"), new KeySplitStep(SPECIFIED_KAS, "1"))), +// new ReasonerTestCase( +// "spk.uns & spk.spk => value.spk || attr.spk", +// List.of(spk2uns, spk2spk), +// List.of(KAS_US), +// List.of(new KeySplitStep(SPECIFIED_KAS, "1"), new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"))), +// new ReasonerTestCase( +// "uns.spk & spk.spk => value.spk", +// List.of(spk2spk, uns2spk), +// List.of(KAS_US), +// List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), new ReasonerTestCase( "uns.spk & uns.uns => spk", List.of(uns2spk, uns2uns), From 744220e8b989f448270e8b0a181333f4be9c25ed Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Mon, 30 Jun 2025 15:59:23 +0200 Subject: [PATCH 04/37] just saving --- .../opentdf/platform/sdk/Autoconfigure.java | 60 ++----------------- 1 file changed, 6 insertions(+), 54 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index a8151ca8..64450373 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -1,26 +1,20 @@ package io.opentdf.platform.sdk; import com.connectrpc.ResponseMessageKt; -import io.opentdf.platform.policy.Algorithm; import io.opentdf.platform.policy.Attribute; import io.opentdf.platform.policy.AttributeRuleTypeEnum; import io.opentdf.platform.policy.AttributeValueSelector; -import io.opentdf.platform.policy.KasKey; -import io.opentdf.platform.policy.KasPublicKey; import io.opentdf.platform.policy.KasPublicKeyAlgEnum; import io.opentdf.platform.policy.KeyAccessServer; -import io.opentdf.platform.policy.PublicKey; import io.opentdf.platform.policy.SimpleKasKey; import io.opentdf.platform.policy.Value; import io.opentdf.platform.policy.attributes.AttributesServiceClientInterface; import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsRequest; import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse; -import org.checkerframework.checker.units.qual.A; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; @@ -35,12 +29,10 @@ import java.util.Objects; import java.util.Set; import java.util.StringJoiner; -import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * The RuleType class defines a set of constants that represent various types of attribute rules. @@ -775,58 +767,18 @@ private static Granter getGranter(KASKeyCache keyCache, List new ArrayList<>(){ - { - addAll(l1); - addAll(l2); - } - }).orElse(Collections.emptyList()); - storeKeysToCache(grantKasInfos, keyCache); - - var mappedKeys = getMappedKeys(attributeAndValue); - var mappedKasInfos = mappedKeys.stream().map(Config.KASInfo::fromSimpleKasKey).collect(Collectors.toList()); - // TODO: add all of the grants here - - storeKeysToCache(mappedKasInfos, keyCache); + grants.addAllGrants(fqn, value.getGrantsList(), value.getKasKeysList(), value.getAttribute()); + grants.addAllGrants(fqn, attribute.getGrantsList(), attribute.getKasKeysList(), attribute); + grants.addAllGrants(fqn, namespace.getGrantsList(), namespace.getKasKeysList(), attribute); } return grants; } - private static List getMappedKeys(GetAttributeValuesByFqnsResponse.AttributeAndValue attributeAndValue) { - var val = attributeAndValue.getValue(); - var attribute = attributeAndValue.getAttribute(); - Function printKasKey = k -> String.format("%s %s", k.getKasUri(), k.getPublicKey().getKid()); - - if (!val.getKasKeysList().isEmpty()) { - if (logger.isDebugEnabled()) { - logger.debug("adding kas keys from attribute value [{}]: {}", val.getFqn(), val.getKasKeysList().stream().map(printKasKey).collect(Collectors.toList())); - } - return val.getKasKeysList(); - } else if (!attribute.getKasKeysList().isEmpty()) { - var kasKeys = attribute.getKasKeysList(); - if (logger.isDebugEnabled()) { - logger.debug("adding kas keys from attribute [{}]: {}", attribute.getFqn(), kasKeys.stream().map(printKasKey).collect(Collectors.toList())); - } - return kasKeys; - } else if (!attribute.getNamespace().getKasKeysList().isEmpty()) { - var kasKeys = attribute.getNamespace().getKasKeysList(); - if (logger.isDebugEnabled()) { - logger.debug("adding kas keys from namespace [{}]: [{}]", attribute.getNamespace().getName(), kasKeys.stream().map(printKasKey).collect(Collectors.toList())); - } - return kasKeys; - } else { - // this is needed to mark the fact that we have an empty - if (logger.isDebugEnabled()) { - logger.debug("didn't find any kas keys on value, attribute, or namespace for attribute value [{}]", val.getFqn()); - } - return Collections.emptyList(); - } - } - static void storeKeysToCache(List kases, KASKeyCache keyCache) { if (keyCache != null) { kases.forEach(keyCache::store); From c8b458f9de794dad4afd1f800f94e95b4af00088 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Mon, 30 Jun 2025 17:50:53 +0200 Subject: [PATCH 05/37] saving --- .../test/java/io/opentdf/platform/sdk/AutoconfigureTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index 42920eef..ca467c6d 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -547,7 +547,7 @@ public void testReasonerSpecificity() { List.of(KAS_US), List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), new ReasonerTestCase( - "uns.uns & uns.spk => spk", + "uns.uns & spk.spk => spk", List.of(uns2uns, spk2spk), List.of(KAS_US), List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), From 9fb0bd63b460530d103daa78a101519fb36e52ba Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Mon, 30 Jun 2025 19:02:36 +0200 Subject: [PATCH 06/37] get tests passing again --- .../opentdf/platform/sdk/Autoconfigure.java | 81 ++++++------------ .../platform/sdk/AutoconfigureTest.java | 85 ++++++++++--------- 2 files changed, 68 insertions(+), 98 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 64450373..9f87da4b 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -33,6 +33,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * The RuleType class defines a set of constants that represent various types of attribute rules. @@ -280,18 +281,15 @@ public List getPolicy() { return policy; } - public void addGrant(AttributeValueFQN fqn, String kas, Attribute attr) { - grants.computeIfAbsent(fqn.key, k -> new KeyAccessGrant(attr, new ArrayList<>())).kases.add(kas); - } - - public void addAllGrants(AttributeValueFQN fqn, List granted, List mapped, Attribute attr) { + public boolean addAllGrants(AttributeValueFQN fqn, List granted, List mapped, Attribute attr, KASKeyCache keyCache) { for (var mappedKey: mapped) { mappedKeys.computeIfAbsent(fqn.key, k -> new ArrayList<>()).add(Config.KASInfo.fromSimpleKasKey(mappedKey)); grants.computeIfAbsent(fqn.key, k -> new KeyAccessGrant(attr, new ArrayList<>())).kases.add(mappedKey.getKasUri()); } + storeKeysToCache(granted, mapped, keyCache); - if (!mappedKeys.isEmpty()) { - return; + if (!mapped.isEmpty()) { + return true; } for (var grantedKey: granted) { @@ -318,21 +316,11 @@ public void addAllGrants(AttributeValueFQN fqn, List granted, L } } - if (mappedKeys.isEmpty() && grants.isEmpty()) { + if (!grants.containsKey(fqn.key)) { grants.put(fqn.key, new KeyAccessGrant(attr, new ArrayList<>())); } - } - public void addMappedKeys(AttributeValueFQN fqn, List keys, Attribute attr) { - if (keys.isEmpty()) { - grants.putIfAbsent(fqn.key, new KeyAccessGrant(attr, new ArrayList<>())); - } else { - for (SimpleKasKey key : keys) { - if (key != null) { - addGrant(fqn, key.getKasUri(), attr); - } - } - } + return !granted.isEmpty(); } KeyAccessGrant byAttribute(AttributeValueFQN fqn) { @@ -730,37 +718,6 @@ public static Granter newGranterFromService(AttributesServiceClientInterface as, return getGranter(keyCache, new ArrayList<>(av.getFqnAttributeValuesMap().values())); } - private static List getGrants(GetAttributeValuesByFqnsResponse.AttributeAndValue attributeAndValue) { - var val = attributeAndValue.getValue(); - var attribute = attributeAndValue.getAttribute(); - - if (!val.getGrantsList().isEmpty()) { - if (logger.isDebugEnabled()) { - logger.debug("adding grants from attribute value [{}]: {}", val.getFqn(), val.getGrantsList().stream().map(KeyAccessServer::getUri).collect(Collectors.toList())); - } - return val.getGrantsList(); - } else if (!attribute.getGrantsList().isEmpty()) { - var attributeGrants = attribute.getGrantsList(); - if (logger.isDebugEnabled()) { - logger.debug("adding grants from attribute [{}]: {}", attribute.getFqn(), attributeGrants.stream().map(KeyAccessServer::getId).collect(Collectors.toList())); - } - return attributeGrants; - } else if (!attribute.getNamespace().getGrantsList().isEmpty()) { - var nsGrants = attribute.getNamespace().getGrantsList(); - if (logger.isDebugEnabled()) { - logger.debug("adding grants from namespace [{}]: [{}]", attribute.getNamespace().getName(), nsGrants.stream().map(KeyAccessServer::getId).collect(Collectors.toList())); - } - return nsGrants; - } else { - // this is needed to mark the fact that we have an empty - if (logger.isDebugEnabled()) { - logger.debug("didn't find any grants on value, attribute, or namespace for attribute value [{}]", val.getFqn()); - } - return Collections.emptyList(); - } - - } - private static Granter getGranter(KASKeyCache keyCache, List values) { Granter grants = new Granter(values.stream().map(GetAttributeValuesByFqnsResponse.AttributeAndValue::getValue).map(Value::getFqn).map(AttributeValueFQN::new).collect(Collectors.toList())); for (var attributeAndValue: values) { @@ -771,18 +728,30 @@ private static Granter getGranter(KASKeyCache keyCache, List kases, KASKeyCache keyCache) { - if (keyCache != null) { - kases.forEach(keyCache::store); + static void storeKeysToCache(List kases, List kasKeys, KASKeyCache keyCache) { + if (keyCache == null) { + return; + } + for (var kas : kases) { + Config.KASInfo.fromKeyAccessServer(kas).forEach(keyCache::store); } + kasKeys.stream().map(Config.KASInfo::fromSimpleKasKey).forEach(keyCache::store); } static String algProto2String(KasPublicKeyAlgEnum e) { diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index ca467c6d..e13bcad1 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -501,41 +501,41 @@ GetAttributeValuesByFqnsResponse getResponse(GetAttributeValuesByFqnsRequest req @Test public void testReasonerSpecificity() { List testCases = List.of( -// new ReasonerTestCase( -// "uns.uns => default", -// List.of(uns2uns), -// List.of(KAS_US), -// List.of(new KeySplitStep(KAS_US, ""))), -// new ReasonerTestCase( -// "uns.spk => spk", -// List.of(uns2spk), -// List.of(KAS_US), -// List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), -// new ReasonerTestCase( -// "spk.uns => spk", -// List.of(spk2uns), -// List.of(KAS_US), -// List.of(new KeySplitStep(SPECIFIED_KAS, ""))), -// new ReasonerTestCase( -// "spk.spk => value.spk", -// List.of(spk2spk), -// List.of(KAS_US), -// List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), -// new ReasonerTestCase( -// "spk.spk & spk.uns => value.spk || attr.spk", -// List.of(spk2spk, spk2uns), -// List.of(KAS_US), -// List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"), new KeySplitStep(SPECIFIED_KAS, "1"))), -// new ReasonerTestCase( -// "spk.uns & spk.spk => value.spk || attr.spk", -// List.of(spk2uns, spk2spk), -// List.of(KAS_US), -// List.of(new KeySplitStep(SPECIFIED_KAS, "1"), new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"))), -// new ReasonerTestCase( -// "uns.spk & spk.spk => value.spk", -// List.of(spk2spk, uns2spk), -// List.of(KAS_US), -// List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + new ReasonerTestCase( + "uns.uns => default", + List.of(uns2uns), + List.of(KAS_US), + List.of(new KeySplitStep(KAS_US, ""))), + new ReasonerTestCase( + "uns.spk => spk", + List.of(uns2spk), + List.of(KAS_US), + List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + new ReasonerTestCase( + "spk.uns => spk", + List.of(spk2uns), + List.of(KAS_US), + List.of(new KeySplitStep(SPECIFIED_KAS, ""))), + new ReasonerTestCase( + "spk.spk => value.spk", + List.of(spk2spk), + List.of(KAS_US), + List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + new ReasonerTestCase( + "spk.spk & spk.uns => value.spk || attr.spk", + List.of(spk2spk, spk2uns), + List.of(KAS_US), + List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"), new KeySplitStep(SPECIFIED_KAS, "1"))), + new ReasonerTestCase( + "spk.uns & spk.spk => value.spk || attr.spk", + List.of(spk2uns, spk2spk), + List.of(KAS_US), + List.of(new KeySplitStep(SPECIFIED_KAS, "1"), new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"))), + new ReasonerTestCase( + "uns.spk & spk.spk => value.spk", + List.of(spk2spk, uns2spk), + List.of(KAS_US), + List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), new ReasonerTestCase( "uns.spk & uns.uns => spk", List.of(uns2spk, uns2uns), @@ -598,12 +598,13 @@ public void cancel() { var wrapper = new Object() { int i = 0; }; - List plan = reasoner.plan(tc.getDefaults(), () -> { - return String.valueOf(wrapper.i++ + 1); - } + List plan = reasoner.plan(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1) ); - assertThat(plan).hasSameElementsAs(tc.getPlan()); + assertThat(plan) + .as(tc.name) + .hasSameElementsAs(tc.getPlan()); + } } @@ -748,7 +749,7 @@ void testStoreKeysToCache_NoKeys() { KasPublicKeySet.newBuilder())) .build(); - Autoconfigure.storeKeysToCache(Config.KASInfo.fromKeyAccessServer(kas1), keyCache); + Autoconfigure.storeKeysToCache(List.of(kas1), Collections.emptyList(), keyCache); verify(keyCache, never()).store(any(Config.KASInfo.class)); } @@ -779,7 +780,7 @@ void testStoreKeysToCache_WithKeys() { .build(); // Call the method under test - Autoconfigure.storeKeysToCache(Config.KASInfo.fromKeyAccessServer(kas1), keyCache); + Autoconfigure.storeKeysToCache(List.of(kas1), Collections.emptyList(), keyCache); // Verify that the key was stored in the cache Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); @@ -822,7 +823,7 @@ void testStoreKeysToCache_MultipleKasEntries() { .build(); // Call the method under test - Autoconfigure.storeKeysToCache(Config.KASInfo.fromKeyAccessServer(kas1), keyCache); + Autoconfigure.storeKeysToCache(List.of(kas1), Collections.emptyList(), keyCache); // Verify that the key was stored in the cache Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); From b71f57e11efc8c2a69ea0c95429154af195a6232 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 1 Jul 2025 16:25:32 +0200 Subject: [PATCH 07/37] pull the planning inside of autoconfigure --- .../io/opentdf/platform/GetEntitlements.java | 1 - .../opentdf/platform/sdk/Autoconfigure.java | 196 ++++++++++++++---- .../java/io/opentdf/platform/sdk/Config.java | 7 +- .../io/opentdf/platform/sdk/KASClient.java | 2 +- .../io/opentdf/platform/sdk/KASKeyCache.java | 12 +- .../java/io/opentdf/platform/sdk/Planner.java | 93 +++++++++ .../java/io/opentdf/platform/sdk/TDF.java | 76 +------ .../platform/sdk/AutoconfigureTest.java | 25 +-- .../opentdf/platform/sdk/KASKeyCacheTest.java | 18 +- 9 files changed, 282 insertions(+), 148 deletions(-) create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/Planner.java diff --git a/examples/src/main/java/io/opentdf/platform/GetEntitlements.java b/examples/src/main/java/io/opentdf/platform/GetEntitlements.java index 0f0ab735..f9479577 100644 --- a/examples/src/main/java/io/opentdf/platform/GetEntitlements.java +++ b/examples/src/main/java/io/opentdf/platform/GetEntitlements.java @@ -7,7 +7,6 @@ import io.opentdf.platform.sdk.*; import java.util.Collections; -import java.util.concurrent.ExecutionException; import java.util.List; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 9f87da4b..67e67068 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -1,6 +1,7 @@ package io.opentdf.platform.sdk; import com.connectrpc.ResponseMessageKt; +import io.opentdf.platform.policy.Algorithm; import io.opentdf.platform.policy.Attribute; import io.opentdf.platform.policy.AttributeRuleTypeEnum; import io.opentdf.platform.policy.AttributeValueSelector; @@ -15,6 +16,7 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; @@ -27,13 +29,13 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.StringJoiner; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * The RuleType class defines a set of constants that represent various types of attribute rules. @@ -59,37 +61,39 @@ class Autoconfigure { private static Logger logger = LoggerFactory.getLogger(Autoconfigure.class); static class KeySplitStep { - public String kas; - public String splitID; + final String kas; + final String splitID; + final String kid; - public KeySplitStep(String kas, String splitId) { - this.kas = kas; - this.splitID = splitId; + KeySplitStep(String kas, String splitId) { + this(kas, splitId, null); } - @Override - public String toString() { - return "KeySplitStep{kas=" + this.kas + ", splitID=" + this.splitID + "}"; + KeySplitStep(String kas, String splitId, @Nullable String kid) { + this.kas = Objects.requireNonNull(kas); + this.splitID = Objects.requireNonNull(splitId); + this.kid = kid; } @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || !(obj instanceof KeySplitStep)) { - return false; - } - KeySplitStep ss = (KeySplitStep) obj; - if ((this.kas.equals(ss.kas)) && (this.splitID.equals(ss.splitID))) { - return true; - } - return false; + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + KeySplitStep that = (KeySplitStep) o; + return Objects.equals(kas, that.kas) && Objects.equals(splitID, that.splitID) && Objects.equals(kid, that.kid); } @Override public int hashCode() { - return Objects.hash(kas, splitID); + return Objects.hash(kas, splitID, kid); + } + + @Override + public String toString() { + return "KeySplitStep{" + + "kas='" + kas + '\'' + + ", splitID='" + splitID + '\'' + + ", kid='" + kid + '\'' + + '}'; } } @@ -268,31 +272,38 @@ static class Granter { private final List policy; private final Map grants = new HashMap<>(); private final Map> mappedKeys = new HashMap<>(); + private boolean hasGrants = false; + private boolean hasMappedKeys = false; - public Granter(List policy) { + Granter(List policy) { this.policy = policy; } - public Map getGrants() { + Map getGrants() { return new HashMap<>(grants); } - public List getPolicy() { + List getPolicy() { return policy; } - public boolean addAllGrants(AttributeValueFQN fqn, List granted, List mapped, Attribute attr, KASKeyCache keyCache) { + boolean addAllGrants(AttributeValueFQN fqn, List granted, List mapped, Attribute attr, KASKeyCache keyCache) { + boolean foundMappedKey = false; for (var mappedKey: mapped) { + foundMappedKey = true; mappedKeys.computeIfAbsent(fqn.key, k -> new ArrayList<>()).add(Config.KASInfo.fromSimpleKasKey(mappedKey)); grants.computeIfAbsent(fqn.key, k -> new KeyAccessGrant(attr, new ArrayList<>())).kases.add(mappedKey.getKasUri()); } storeKeysToCache(granted, mapped, keyCache); - if (!mapped.isEmpty()) { + if (foundMappedKey) { + hasMappedKeys = true; return true; } + boolean foundGrantedKey = false; for (var grantedKey: granted) { + foundGrantedKey = true; grants.computeIfAbsent(fqn.key, k -> new KeyAccessGrant(attr, new ArrayList<>())).kases.add(grantedKey.getUri()); if (!grantedKey.getKasKeysList().isEmpty()) { for (var kas : grantedKey.getKasKeysList()) { @@ -302,7 +313,7 @@ public boolean addAllGrants(AttributeValueFQN fqn, List granted } var cachedGrantKeys = grantedKey.getPublicKey().getCached().getKeysList(); if (cachedGrantKeys.isEmpty()) { - logger.info("no keys cached in policy service"); + logger.debug("no keys cached in policy service"); continue; } for (var cachedGrantKey: cachedGrantKeys) { @@ -320,14 +331,39 @@ public boolean addAllGrants(AttributeValueFQN fqn, List granted grants.put(fqn.key, new KeyAccessGrant(attr, new ArrayList<>())); } - return !granted.isEmpty(); + if (foundGrantedKey) { + hasGrants = true; + } + return foundGrantedKey; } KeyAccessGrant byAttribute(AttributeValueFQN fqn) { return grants.get(fqn.key); } - @Nonnull List plan(List defaultKas, Supplier genSplitID) + List getSplits(List defaultKases, Supplier genSplitID, Supplier> baseKeySupplier) throws AutoConfigureException { + if (hasMappedKeys) { + return planFromAttributes(genSplitID); + } + if (hasGrants) { + return plan(genSplitID); + } + + var baseKey = baseKeySupplier.get(); + if (baseKey.isPresent()) { + var key = baseKey.get(); + String kas = key.getKasUri(); + String splitID = ""; + String kid = key.getPublicKey().getKid(); + return Collections.singletonList(new KeySplitStep(kas, splitID, kid)); + } + + logger.warn("no grants or mapped keys found, generating plan from default KASes. this is deprecated"); + return generatePlanFromDefaultKases(defaultKases, genSplitID); + } + + @Nonnull + List plan(Supplier genSplitID) throws AutoConfigureException { AttributeBooleanExpression b = constructAttributeBoolean(); BooleanKeyExpression k = insertKeysForAttribute(b); @@ -338,8 +374,7 @@ KeyAccessGrant byAttribute(AttributeValueFQN fqn) { k = k.reduce(); int l = k.size(); if (l == 0) { - // default behavior: split key across all default KAS - return generatePlanFromDefaultKases(defaultKas, genSplitID); + throw new IllegalStateException("generated an empty plan"); } List steps = new ArrayList<>(); @@ -352,6 +387,31 @@ KeyAccessGrant byAttribute(AttributeValueFQN fqn) { return steps; } + @Nonnull + List planFromAttributes(Supplier genSplitID) + throws AutoConfigureException { + AttributeBooleanExpression b = constructAttributeBoolean(); + BooleanKeyExpression k = assignKeysTo(b); + if (k == null) { + throw new AutoConfigureException("Error assigning keys to attribute"); + } + + k = k.reduce(); + int l = k.size(); + if (l == 0) { + return Collections.emptyList(); + } + + List steps = new ArrayList<>(); + for (KeyClause v : k.values) { + String splitID = (l > 1) ? genSplitID.get() : ""; + for (PublicKeyInfo o : v.values) { + steps.add(new KeySplitStep(o.kas, splitID, o.kid)); + } + } + return steps; + } + static List generatePlanFromDefaultKases(List defaultKas, Supplier genSplitID) { if (defaultKas.isEmpty()) { throw new AutoConfigureException("no default KAS specified; required for grantless plans"); @@ -394,13 +454,46 @@ BooleanKeyExpression insertKeysForAttribute(AttributeBooleanExpression e) throws logger.warn("Unknown attribute rule type: " + clause); } - KeyClause kc = new KeyClause(op, kcv); - kcs.add(kc); + kcs.add(new KeyClause(op, kcv)); } return new BooleanKeyExpression(kcs); } + BooleanKeyExpression assignKeysTo(AttributeBooleanExpression e) { + var keyClauses = new ArrayList(); + for (var clause : e.must) { + ArrayList keys = new ArrayList<>(); + if (clause.values.isEmpty()) { + logger.warn("No values found for attribute: " + clause.def.getFqn()); + continue; + } + for (var value : clause.values) { + var mapped = mappedKeys.get(value.key); + if (mapped == null) { + logger.warn("No keys found for attribute value {} ", value); + continue; + } + for (var kasInfo : mapped) { + if (kasInfo.URL == null || kasInfo.URL.isEmpty()) { + logger.warn("No KAS URL found for attribute value {}", value); + continue; + } + keys.add(new PublicKeyInfo(kasInfo.URL, kasInfo.KID)); + } + } + + String op = ruleToOperator(clause.def.getRule()); + if (op.equals(RuleType.UNSPECIFIED)) { + logger.warn("Unknown attribute rule type {}", op); + } + + keyClauses.add(new KeyClause(op, keys)); + } + + return new BooleanKeyExpression(keyClauses); + } + /** * Constructs an AttributeBooleanExpression from the policy, splitting each attribute * into its own clause. Each clause contains the attribute definition and a list of @@ -485,23 +578,29 @@ public String toString() { } - public static class PublicKeyInfo { - private String kas; + static class PublicKeyInfo { + private final String kas; + private final String kid; + + PublicKeyInfo(String kas) { + this(kas, null); + } - public PublicKeyInfo(String kas) { + PublicKeyInfo(String kas, String kid) { this.kas = kas; + this.kid = kid; } - public String getKas() { - return kas; + Optional getKID() { + return Optional.ofNullable(kid); } - public void setKas(String kas) { - this.kas = kas; + String getKas() { + return kas; } } - public static class KeyClause { + static class KeyClause { private final String operator; private final List values; @@ -538,7 +637,7 @@ public String toString() { } } - public static class BooleanKeyExpression { + static class BooleanKeyExpression { private final List values; public BooleanKeyExpression(List values) { @@ -754,6 +853,17 @@ static void storeKeysToCache(List kases, List kas kasKeys.stream().map(Config.KASInfo::fromSimpleKasKey).forEach(keyCache::store); } + static String algProto2String(Algorithm e) { + switch (e) { + case ALGORITHM_EC_P521: + return "ec:p521"; + case ALGORITHM_RSA_2048: + return "rsa:2048"; + default: + throw new IllegalArgumentException("Unknown algorithm: " + e); + } + } + static String algProto2String(KasPublicKeyAlgEnum e) { switch (e) { case KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1: diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index 37e397a1..52890484 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -1,7 +1,6 @@ package io.opentdf.platform.sdk; -import io.opentdf.platform.policy.KasPublicKey; -import io.opentdf.platform.policy.KasPublicKeyAlgEnum; +import io.opentdf.platform.policy.Algorithm; import io.opentdf.platform.policy.KeyAccessServer; import io.opentdf.platform.policy.SimpleKasKey; import io.opentdf.platform.policy.Value; @@ -43,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; @@ -106,7 +103,7 @@ public static KASInfo fromSimpleKasKey(SimpleKasKey ki) { Config.KASInfo kasInfo = new Config.KASInfo(); kasInfo.URL = ki.getKasUri(); kasInfo.KID = ki.getPublicKey().getKid(); - kasInfo.Algorithm = algProto2String(Enum.valueOf(KasPublicKeyAlgEnum.class, ki.getPublicKey().getAlgorithm().name())); + kasInfo.Algorithm = algProto2String(ki.getPublicKey().getAlgorithm()); kasInfo.PublicKey = ki.getPublicKey().getPem(); return kasInfo; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java index bafc3f2c..bfa1ffdd 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java @@ -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; } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java index 5879dd05..a67ad346 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java @@ -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); @@ -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())); } } @@ -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 diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java new file mode 100644 index 00000000..62133b56 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -0,0 +1,93 @@ +package io.opentdf.platform.sdk; + +import io.opentdf.platform.policy.Value; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public class Planner { + private final Config.TDFConfig tdfConfig; + private final SDK.Services sdkServices; + private final Autoconfigure.Granter granter; + + private static final Logger logger = LoggerFactory.getLogger(Planner.class); + + public Planner(Config.TDFConfig config, SDK.Services services) { + tdfConfig = Objects.requireNonNull(config); + sdkServices = Objects.requireNonNull(services) ; + + List dk = defaultKases(tdfConfig); + if (tdfConfig.autoconfigure) { + if (tdfConfig.splitPlan != null && !tdfConfig.splitPlan.isEmpty()) { + throw new IllegalArgumentException("cannot use autoconfigure with a split plan provided in the 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])); + } + + this.granter = granter; + tdfConfig.splitPlan = granter.plan(() -> UUID.randomUUID().toString()); + } else { + tdfConfig.splitPlan = Autoconfigure.Granter.generatePlanFromDefaultKases(dk, () -> UUID.randomUUID().toString()); + this.granter = null; + } + + if (tdfConfig.kasInfoList.isEmpty() && tdfConfig.splitPlan.isEmpty()) { + throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred"); + } + } + + Map> getSplits(Config.TDFConfig tdfConfig) { + var latestKASInfo = new HashMap(); + // Seed anything passed in manually + for (Config.KASInfo kasInfo : tdfConfig.kasInfoList) { + if (kasInfo.PublicKey != null && !kasInfo.PublicKey.isEmpty()) { + latestKASInfo.put(kasInfo.URL, kasInfo); + } + } + + // split plan: restructure by conjunctions + Map> conjunction = new HashMap<>(); + + for (Autoconfigure.KeySplitStep splitInfo : tdfConfig.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; + getKI.Algorithm = tdfConfig.wrappingKeyType.toString(); + getKI = sdkServices.kas().getPublicKey(getKI); + latestKASInfo.put(splitInfo.kas, getKI); + ki = getKI; + } + conjunction.computeIfAbsent(splitInfo.splitID, s -> new ArrayList<>()).add(ki); + } + return conjunction; + } + + static List defaultKases(Config.TDFConfig config) { + List allk = new ArrayList<>(); + List 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; + } +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index b4fcf210..25bf67d3 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -5,11 +5,9 @@ import com.google.gson.GsonBuilder; import com.nimbusds.jose.*; -import io.opentdf.platform.policy.Value; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse; import io.opentdf.platform.sdk.Config.TDFConfig; -import io.opentdf.platform.sdk.Autoconfigure.AttributeValueFQN; import io.opentdf.platform.sdk.Config.KASInfo; import org.apache.commons.codec.DecoderException; @@ -28,7 +26,6 @@ import java.security.*; import java.text.ParseException; import java.util.*; -import java.util.stream.Collectors; /** * The TDF class is responsible for handling operations related to @@ -143,7 +140,7 @@ private PolicyObject createPolicyObject(List at private static final Base64.Encoder encoder = Base64.getEncoder(); - private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { + private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas, Map> splits) { manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_VERSION : null; manifest.encryptionInformation.keyAccessType = kSplitKeyType; manifest.encryptionInformation.keyAccessObj = new ArrayList<>(); @@ -151,36 +148,10 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { PolicyObject policyObject = createPolicyObject(tdfConfig.attributes); String base64PolicyObject = encoder .encodeToString(gson.toJson(policyObject).getBytes(StandardCharsets.UTF_8)); - Map latestKASInfo = new HashMap<>(); - // Seed anything passed in manually - for (Config.KASInfo kasInfo : tdfConfig.kasInfoList) { - if (kasInfo.PublicKey != null && !kasInfo.PublicKey.isEmpty()) { - latestKASInfo.put(kasInfo.URL, kasInfo); - } - } - - // split plan: restructure by conjunctions - Map> conjunction = new HashMap<>(); - - for (Autoconfigure.KeySplitStep splitInfo : tdfConfig.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()) { - logger.info("no public key provided for KAS at {}, retrieving", splitInfo.kas); - var getKI = new Config.KASInfo(); - getKI.URL = splitInfo.kas; - getKI.Algorithm = tdfConfig.wrappingKeyType.toString(); - getKI = kas.getPublicKey(getKI); - latestKASInfo.put(splitInfo.kas, getKI); - ki = getKI; - } - conjunction.computeIfAbsent(splitInfo.splitID, s -> new ArrayList<>()).add(ki); - } - List symKeys = new ArrayList<>(conjunction.size()); - for (String splitID : conjunction.keySet()) { + List symKeys = new ArrayList<>(splits.size()); + for (String splitID : splits.keySet()) { // Symmetric key byte[] symKey = new byte[GCM_KEY_SIZE]; sRandom.nextBytes(symKey); @@ -207,7 +178,7 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { encryptedMetadata = encoder.encodeToString(metadata.getBytes(StandardCharsets.UTF_8)); } - for (Config.KASInfo kasInfo : conjunction.get(splitID)) { + for (Config.KASInfo kasInfo : splits.get(splitID)) { if (kasInfo.PublicKey == null || kasInfo.PublicKey.isEmpty()) { throw new SDK.KasPublicKeyMissing("Kas public key is missing in kas information list"); } @@ -278,7 +249,6 @@ private String createRSAWrappedKey(Config.KASInfo kasInfo, byte[] symKey) { } } - private static final Base64.Decoder decoder = Base64.getDecoder(); public static class Reader { @@ -303,7 +273,6 @@ public Manifest getManifest() { this.aesGcm = new AesGcm(payloadKey); this.payloadKey = payloadKey; this.unencryptedMetadata = unencryptedMetadata; - } public void readPayload(OutputStream outputStream) throws SDK.SegmentSignatureMismatch, IOException { @@ -378,30 +347,10 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException { - List dk = defaultKases(tdfConfig); - if (tdfConfig.autoconfigure) { - 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 AttributeValueFQN[0])); - } - - if (granter == null) { - throw new AutoConfigureException("Failed to create Granter"); // Replace with appropriate error handling - } - tdfConfig.splitPlan = granter.plan(dk, () -> UUID.randomUUID().toString()); - } else { - tdfConfig.splitPlan = Autoconfigure.Granter.generatePlanFromDefaultKases(dk, () -> UUID.randomUUID().toString()); - } - - if (tdfConfig.kasInfoList.isEmpty() && (tdfConfig.splitPlan == null || tdfConfig.splitPlan.isEmpty())) { - throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred"); - } + Planner planner = new Planner(tdfConfig, services); TDFObject tdfObject = new TDFObject(); - tdfObject.prepareManifest(tdfConfig, services.kas()); + tdfObject.prepareManifest(tdfConfig, services.kas(), planner.getSplits(tdfConfig)); long encryptedSegmentSize = tdfConfig.defaultSegmentSize + kGcmIvSize + kAesBlockSize; TDFWriter tdfWriter = new TDFWriter(outputStream); @@ -534,19 +483,6 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo return tdfObject; } - static List defaultKases(TDFConfig config) { - List allk = new ArrayList<>(); - List defk = new ArrayList<>(); - - for (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; - } Reader loadTDF(SeekableByteChannel tdf, String platformUrl) throws SDKException, IOException { return loadTDF(tdf, Config.newTDFReaderConfig(), platformUrl); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index e13bcad1..4ff22cc4 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -39,6 +39,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.regex.Matcher; @@ -467,12 +468,10 @@ public void testReasonerConstructAttributeBoolean() { var wrapper = new Object() { int i = 0; }; - List plan = reasoner.plan(tc.getDefaults(), () -> { - return String.valueOf(wrapper.i++ + 1); - } - - ); - assertThat(plan).isEqualTo(tc.getPlan()); + List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), () -> Optional.empty()); + assertThat(plan) + .as(tc.name) + .isEqualTo(tc.getPlan()); } } @@ -598,9 +597,7 @@ public void cancel() { var wrapper = new Object() { int i = 0; }; - List plan = reasoner.plan(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1) - - ); + List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), () -> Optional.empty()); assertThat(plan) .as(tc.name) .hasSameElementsAs(tc.getPlan()); @@ -783,7 +780,7 @@ void testStoreKeysToCache_WithKeys() { Autoconfigure.storeKeysToCache(List.of(kas1), Collections.emptyList(), keyCache); // Verify that the key was stored in the cache - Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); + Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1", "test-kid"); assertNotNull(storedKASInfo); assertEquals("https://example.com/kas", storedKASInfo.URL); assertEquals("test-kid", storedKASInfo.KID); @@ -826,14 +823,14 @@ void testStoreKeysToCache_MultipleKasEntries() { Autoconfigure.storeKeysToCache(List.of(kas1), Collections.emptyList(), keyCache); // Verify that the key was stored in the cache - Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); + Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1", "test-kid"); assertNotNull(storedKASInfo); assertEquals("https://example.com/kas", storedKASInfo.URL); assertEquals("test-kid", storedKASInfo.KID); assertEquals("ec:secp256r1", storedKASInfo.Algorithm); assertEquals("public-key-pem", storedKASInfo.PublicKey); - Config.KASInfo storedKASInfo2 = keyCache.get("https://example.com/kas", "rsa:2048"); + Config.KASInfo storedKASInfo2 = keyCache.get("https://example.com/kas", "rsa:2048", "test-kid-2"); assertNotNull(storedKASInfo2); assertEquals("https://example.com/kas", storedKASInfo2.URL); assertEquals("test-kid-2", storedKASInfo2.KID); @@ -913,14 +910,14 @@ public ResponseMessage execute() { assertThat(reasoner).isNotNull(); // Verify that the key was stored in the cache - Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); + Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1", "test-kid"); assertNotNull(storedKASInfo); assertEquals("https://example.com/kas", storedKASInfo.URL); assertEquals("test-kid", storedKASInfo.KID); assertEquals("ec:secp256r1", storedKASInfo.Algorithm); assertEquals("public-key-pem", storedKASInfo.PublicKey); - Config.KASInfo storedKASInfo2 = keyCache.get("https://example.com/kas", "rsa:2048"); + Config.KASInfo storedKASInfo2 = keyCache.get("https://example.com/kas", "rsa:2048", "test-kid-2"); assertNotNull(storedKASInfo2); assertEquals("https://example.com/kas", storedKASInfo2.URL); assertEquals("test-kid-2", storedKASInfo2.KID); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/KASKeyCacheTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/KASKeyCacheTest.java index 5550678a..f1c63369 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/KASKeyCacheTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/KASKeyCacheTest.java @@ -35,7 +35,7 @@ void testStoreAndGet_WithinTimeLimit() { kasKeyCache.store(kasInfo1); // Retrieve the item within the time limit - Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", "rsa:2048"); + Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", "rsa:2048", "kid1"); // Ensure the item was correctly retrieved assertNotNull(result); @@ -51,12 +51,12 @@ void testStoreAndGet_AfterTimeLimit() { kasKeyCache.store(kasInfo1); // Simulate time passing by modifying the timestamp directly - KASKeyRequest cacheKey = new KASKeyRequest("https://example.com/kas1", "rsa:2048"); + KASKeyRequest cacheKey = new KASKeyRequest("https://example.com/kas1", "rsa:2048", "kid2"); TimeStampedKASInfo timeStampedKASInfo = new TimeStampedKASInfo(kasInfo1, LocalDateTime.now().minus(6, ChronoUnit.MINUTES)); kasKeyCache.cache.put(cacheKey, timeStampedKASInfo); // Attempt to retrieve the item after the time limit - Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", "rsa:2048"); + Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", "rsa:2048", "kid1"); // Ensure the item was not retrieved (it should have expired) assertNull(result); @@ -72,7 +72,7 @@ void testStoreAndGet_WithNullAlgorithm() { kasKeyCache.store(kasInfo1); // Retrieve the item with a null algorithm - Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", null); + Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", null, null); // Ensure the item was correctly retrieved assertNotNull(result); @@ -91,7 +91,7 @@ void testClearCache() { kasKeyCache.clear(); // Attempt to retrieve the item after clearing the cache - Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", "rsa:2048"); + Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", "rsa:2048", "kid1"); // Ensure the item was not retrieved (the cache should be empty) assertNull(result); @@ -104,8 +104,8 @@ void testStoreMultipleItemsAndGet() { kasKeyCache.store(kasInfo2); // Retrieve each item and ensure they were correctly stored and retrieved - Config.KASInfo result1 = kasKeyCache.get("https://example.com/kas1", "rsa:2048"); - Config.KASInfo result2 = kasKeyCache.get("https://example.com/kas2", "ec:secp256r1"); + Config.KASInfo result1 = kasKeyCache.get("https://example.com/kas1", "rsa:2048", "kid1"); + Config.KASInfo result2 = kasKeyCache.get("https://example.com/kas2", "ec:secp256r1", "kid2"); assertNotNull(result1); assertEquals("https://example.com/kas1", result1.URL); @@ -119,8 +119,8 @@ void testStoreMultipleItemsAndGet() { @Test void testEqualsAndHashCode() { // Create two identical KASKeyRequest objects - KASKeyRequest keyRequest1 = new KASKeyRequest("https://example.com/kas1", "rsa:2048"); - KASKeyRequest keyRequest2 = new KASKeyRequest("https://example.com/kas1", "rsa:2048"); + KASKeyRequest keyRequest1 = new KASKeyRequest("https://example.com/kas1", "rsa:2048", "kid1"); + KASKeyRequest keyRequest2 = new KASKeyRequest("https://example.com/kas1", "rsa:2048", "kid1"); // Ensure that equals and hashCode work as expected assertEquals(keyRequest1, keyRequest2); From 2d4f3e67d8f40d34bc3478d70f93703288d2bc28 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 1 Jul 2025 16:40:57 +0200 Subject: [PATCH 08/37] cleanup --- .../main/java/io/opentdf/platform/sdk/Autoconfigure.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 67e67068..9a68bb9e 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -579,8 +579,8 @@ public String toString() { } static class PublicKeyInfo { - private final String kas; - private final String kid; + final String kas; + final String kid; PublicKeyInfo(String kas) { this(kas, null); @@ -591,10 +591,6 @@ static class PublicKeyInfo { this.kid = kid; } - Optional getKID() { - return Optional.ofNullable(kid); - } - String getKas() { return kas; } From 65a498711e40f3733e9b1ae58db9f6c26c86ee34 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 1 Jul 2025 16:42:12 +0200 Subject: [PATCH 09/37] debug --- sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 9a68bb9e..f00b05b9 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -343,9 +343,11 @@ KeyAccessGrant byAttribute(AttributeValueFQN fqn) { List getSplits(List defaultKases, Supplier genSplitID, Supplier> baseKeySupplier) throws AutoConfigureException { if (hasMappedKeys) { + logger.debug("generating plan from mapped keys"); return planFromAttributes(genSplitID); } if (hasGrants) { + logger.debug("generating plan from grants"); return plan(genSplitID); } From 2cfd3ec22cd1a006b79500fc1e3ee5d0277ed089 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 1 Jul 2025 16:44:46 +0200 Subject: [PATCH 10/37] only plan when we mean to --- sdk/src/main/java/io/opentdf/platform/sdk/Planner.java | 2 -- sdk/src/main/java/io/opentdf/platform/sdk/TDF.java | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) 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 62133b56..382ca2c2 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -36,9 +36,7 @@ public Planner(Config.TDFConfig config, SDK.Services services) { } this.granter = granter; - tdfConfig.splitPlan = granter.plan(() -> UUID.randomUUID().toString()); } else { - tdfConfig.splitPlan = Autoconfigure.Granter.generatePlanFromDefaultKases(dk, () -> UUID.randomUUID().toString()); this.granter = null; } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 25bf67d3..9e08c6fd 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -348,9 +348,10 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException { Planner planner = new Planner(tdfConfig, services); + Map> splits = planner.getSplits(tdfConfig); TDFObject tdfObject = new TDFObject(); - tdfObject.prepareManifest(tdfConfig, services.kas(), planner.getSplits(tdfConfig)); + tdfObject.prepareManifest(tdfConfig, services.kas(), splits); long encryptedSegmentSize = tdfConfig.defaultSegmentSize + kGcmIvSize + kAesBlockSize; TDFWriter tdfWriter = new TDFWriter(outputStream); From 98653b9bc4f05cd37ef8593231e1d4916515c1a6 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 1 Jul 2025 18:11:04 +0200 Subject: [PATCH 11/37] restructure --- .../java/io/opentdf/platform/sdk/Planner.java | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) 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 382ca2c2..7e8d4b02 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -9,43 +9,67 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import static io.opentdf.platform.sdk.Autoconfigure.Granter.generatePlanFromDefaultKases; + public class Planner { private final Config.TDFConfig tdfConfig; private final SDK.Services sdkServices; - private final Autoconfigure.Granter granter; private static final Logger logger = LoggerFactory.getLogger(Planner.class); public Planner(Config.TDFConfig config, SDK.Services services) { tdfConfig = Objects.requireNonNull(config); sdkServices = Objects.requireNonNull(services) ; + } - List dk = defaultKases(tdfConfig); - if (tdfConfig.autoconfigure) { - if (tdfConfig.splitPlan != null && !tdfConfig.splitPlan.isEmpty()) { - throw new IllegalArgumentException("cannot use autoconfigure with a split plan provided in the 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])); - } + private static String getUUID() { + return UUID.randomUUID().toString(); + } - this.granter = granter; + Map> getSplits(Config.TDFConfig tdfConfig) { + List splitPlan; + List defaultKases = defaultKases(tdfConfig); + if (tdfConfig.autoconfigure) { + splitPlan = getAutoconfigurePlan(tdfConfig); + } else if (tdfConfig.splitPlan == null || tdfConfig.splitPlan.isEmpty()) { + splitPlan = defaultKases.isEmpty() + ? createPlanFromBaseKey() + : generatePlanFromDefaultKases(defaultKases, Planner::getUUID); } else { - this.granter = null; + splitPlan = tdfConfig.splitPlan; } if (tdfConfig.kasInfoList.isEmpty() && tdfConfig.splitPlan.isEmpty()) { throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred"); } + + // split plan: restructure by conjunctions + return fillInKeys(tdfConfig, splitPlan); } - Map> getSplits(Config.TDFConfig tdfConfig) { + private List getAutoconfigurePlan(Config.TDFConfig tdfConfig) { + if (tdfConfig.splitPlan != null && !tdfConfig.splitPlan.isEmpty()) { + throw new IllegalArgumentException("cannot use autoconfigure with a split plan provided in the TDFConfig"); + } + Autoconfigure.Granter granter = new Autoconfigure.Granter(new ArrayList<>()); + if (tdfConfig.attributeValues != null && !tdfConfig.attributeValues.isEmpty()) { + granter = Autoconfigure.newGranterFromAttributes(sdkServices.kas().getKeyCache(), tdfConfig.attributeValues.toArray(new Value[0])); + } else if (tdfConfig.attributes != null && !tdfConfig.attributes.isEmpty()) { + granter = Autoconfigure.newGranterFromService(sdkServices.attributes(), sdkServices.kas().getKeyCache(), + tdfConfig.attributes.toArray(new Autoconfigure.AttributeValueFQN[0])); + } + return granter.getSplits(defaultKases(tdfConfig), Planner::getUUID, Optional::empty); + } + + private List createPlanFromBaseKey() { + return null; + } + + private Map> fillInKeys(Config.TDFConfig tdfConfig, List splitPlan) { + Map> conjunction = new HashMap<>(); var latestKASInfo = new HashMap(); // Seed anything passed in manually for (Config.KASInfo kasInfo : tdfConfig.kasInfoList) { @@ -54,10 +78,7 @@ Map> getSplits(Config.TDFConfig tdfConfig) { } } - // split plan: restructure by conjunctions - Map> conjunction = new HashMap<>(); - - for (Autoconfigure.KeySplitStep splitInfo : tdfConfig.splitPlan) { + 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); From e900df3b6e61a02f6027e34f51dcd3e4e4bf2465 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 2 Jul 2025 12:41:05 +0200 Subject: [PATCH 12/37] add some tests --- .../opentdf/platform/sdk/Autoconfigure.java | 2 + .../java/io/opentdf/platform/sdk/Planner.java | 114 +++++++++++++++--- .../java/io/opentdf/platform/sdk/SDK.java | 3 + .../io/opentdf/platform/sdk/SDKBuilder.java | 7 ++ .../java/io/opentdf/platform/sdk/TDF.java | 2 - .../io/opentdf/platform/sdk/FakeServices.java | 51 +++++--- .../platform/sdk/FakeServicesBuilder.java | 46 ++++--- .../io/opentdf/platform/sdk/PlannerTest.java | 88 ++++++++++++++ .../io/opentdf/platform/sdk/TestUtil.java | 22 ++++ 9 files changed, 276 insertions(+), 59 deletions(-) create mode 100644 sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java create mode 100644 sdk/src/test/java/io/opentdf/platform/sdk/TestUtil.java diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index f00b05b9..629d4f55 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -361,6 +361,8 @@ List getSplits(List defaultKases, Supplier genSpli } logger.warn("no grants or mapped keys found, generating plan from default KASes. this is deprecated"); + // this is a little bit weird because we don't take into account the KIDs here. This is the way + // that it works in return generatePlanFromDefaultKases(defaultKases, genSplitID); } 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 7e8d4b02..5e9f89bc 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -1,10 +1,20 @@ 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; @@ -12,17 +22,17 @@ import java.util.Optional; import java.util.UUID; -import static io.opentdf.platform.sdk.Autoconfigure.Granter.generatePlanFromDefaultKases; - public class Planner { + private static final String BASE_KEY = "base_key"; private final Config.TDFConfig tdfConfig; - private final SDK.Services sdkServices; + private final SDK.Services services; + private static final Logger logger = LoggerFactory.getLogger(Planner.class); public Planner(Config.TDFConfig config, SDK.Services services) { - tdfConfig = Objects.requireNonNull(config); - sdkServices = Objects.requireNonNull(services) ; + this.tdfConfig = Objects.requireNonNull(config); + this.services = Objects.requireNonNull(services); } private static String getUUID() { @@ -31,13 +41,13 @@ private static String getUUID() { Map> getSplits(Config.TDFConfig tdfConfig) { List splitPlan; - List defaultKases = defaultKases(tdfConfig); 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 = defaultKases.isEmpty() - ? createPlanFromBaseKey() - : generatePlanFromDefaultKases(defaultKases, Planner::getUUID); + splitPlan = generatePlanFromProvidedKases(tdfConfig.kasInfoList); } else { splitPlan = tdfConfig.splitPlan; } @@ -45,29 +55,93 @@ Map> getSplits(Config.TDFConfig tdfConfig) { if (tdfConfig.kasInfoList.isEmpty() && tdfConfig.splitPlan.isEmpty()) { throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred"); } - - // split plan: restructure by conjunctions return fillInKeys(tdfConfig, splitPlan); } private List getAutoconfigurePlan(Config.TDFConfig tdfConfig) { - if (tdfConfig.splitPlan != null && !tdfConfig.splitPlan.isEmpty()) { - throw new IllegalArgumentException("cannot use autoconfigure with a split plan provided in the TDFConfig"); - } Autoconfigure.Granter granter = new Autoconfigure.Granter(new ArrayList<>()); if (tdfConfig.attributeValues != null && !tdfConfig.attributeValues.isEmpty()) { - granter = Autoconfigure.newGranterFromAttributes(sdkServices.kas().getKeyCache(), tdfConfig.attributeValues.toArray(new Value[0])); + granter = Autoconfigure.newGranterFromAttributes(services.kas().getKeyCache(), tdfConfig.attributeValues.toArray(new Value[0])); } else if (tdfConfig.attributes != null && !tdfConfig.attributes.isEmpty()) { - granter = Autoconfigure.newGranterFromService(sdkServices.attributes(), sdkServices.kas().getKeyCache(), + granter = Autoconfigure.newGranterFromService(services.attributes(), services.kas().getKeyCache(), tdfConfig.attributes.toArray(new Autoconfigure.AttributeValueFQN[0])); } - return granter.getSplits(defaultKases(tdfConfig), Planner::getUUID, Optional::empty); + return granter.getSplits(defaultKases(tdfConfig), Planner::getUUID, this::fetchBaseKey); + } + + List generatePlanFromProvidedKases(List kases) { + if (kases.size() == 1) { + var kasInfo = kases.get(0); + return Collections.singletonList(new Autoconfigure.KeySplitStep(kasInfo.URL, "", kasInfo.KID)); + } + List splitPlan = new ArrayList<>(); + for (var kasInfo : kases) { + splitPlan.add(new Autoconfigure.KeySplitStep(kasInfo.URL, getUUID(), kasInfo.KID)); + } + return splitPlan; + } + + Optional 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) { + throw new SDKException("base key in well known configuration is missing required fields [" + baseKeyJson + "]"); + } + + 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 List createPlanFromBaseKey() { - return null; + 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; + } } + private Map> fillInKeys(Config.TDFConfig tdfConfig, List splitPlan) { Map> conjunction = new HashMap<>(); var latestKASInfo = new HashMap(); @@ -87,7 +161,7 @@ private Map> fillInKeys(Config.TDFConfig tdfConfig, var getKI = new Config.KASInfo(); getKI.URL = splitInfo.kas; getKI.Algorithm = tdfConfig.wrappingKeyType.toString(); - getKI = sdkServices.kas().getPublicKey(getKI); + getKI = services.kas().getPublicKey(getKI); latestKASInfo.put(splitInfo.kas, getKI); ki = getKI; } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java index 9195fca5..a0db542d 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java @@ -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; @@ -75,6 +76,8 @@ public interface Services extends AutoCloseable { KeyAccessServerRegistryServiceClientInterface kasRegistry(); + WellKnownServiceClientInterface wellknown(); + KAS kas(); } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java index dd83f75a..03b1943c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java @@ -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; @@ -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 @@ -290,6 +292,11 @@ public KeyAccessServerRegistryServiceClient kasRegistry() { return kasRegistryService; } + @Override + public WellKnownServiceClientInterface wellknown() { + return wellKnownService; + } + @Override public SDK.KAS kas() { return kasClient; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 9e08c6fd..6f0e7a15 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -7,7 +7,6 @@ import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse; -import io.opentdf.platform.sdk.Config.TDFConfig; import io.opentdf.platform.sdk.Config.KASInfo; import org.apache.commons.codec.DecoderException; @@ -346,7 +345,6 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte } TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException { - Planner planner = new Planner(tdfConfig, services); Map> splits = planner.getSplits(tdfConfig); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/FakeServices.java b/sdk/src/test/java/io/opentdf/platform/sdk/FakeServices.java index b3573593..29397915 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/FakeServices.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/FakeServices.java @@ -1,31 +1,40 @@ package io.opentdf.platform.sdk; import io.opentdf.platform.authorization.AuthorizationServiceClient; +import io.opentdf.platform.authorization.AuthorizationServiceClientInterface; import io.opentdf.platform.policy.attributes.AttributesServiceClient; +import io.opentdf.platform.policy.attributes.AttributesServiceClientInterface; import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient; +import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClientInterface; import io.opentdf.platform.policy.namespaces.NamespaceServiceClient; +import io.opentdf.platform.policy.namespaces.NamespaceServiceClientInterface; import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceClient; +import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceClientInterface; import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceClient; +import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceClientInterface; +import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface; import java.util.Objects; public class FakeServices implements SDK.Services { - private final AuthorizationServiceClient authorizationService; - private final AttributesServiceClient attributesService; - private final NamespaceServiceClient namespaceService; - private final SubjectMappingServiceClient subjectMappingService; - private final ResourceMappingServiceClient resourceMappingService; - private final KeyAccessServerRegistryServiceClient keyAccessServerRegistryServiceFutureStub; + private final AuthorizationServiceClientInterface authorizationService; + private final AttributesServiceClientInterface attributesService; + private final NamespaceServiceClientInterface namespaceService; + private final SubjectMappingServiceClientInterface subjectMappingService; + private final ResourceMappingServiceClientInterface resourceMappingService; + private final KeyAccessServerRegistryServiceClientInterface keyAccessServerRegistryServiceFutureStub; + private final WellKnownServiceClientInterface wellKnownService; private final SDK.KAS kas; public FakeServices( - AuthorizationServiceClient authorizationService, - AttributesServiceClient attributesService, - NamespaceServiceClient namespaceService, - SubjectMappingServiceClient subjectMappingService, - ResourceMappingServiceClient resourceMappingService, - KeyAccessServerRegistryServiceClient keyAccessServerRegistryServiceFutureStub, + AuthorizationServiceClientInterface authorizationService, + AttributesServiceClientInterface attributesService, + NamespaceServiceClientInterface namespaceService, + SubjectMappingServiceClientInterface subjectMappingService, + ResourceMappingServiceClientInterface resourceMappingService, + KeyAccessServerRegistryServiceClientInterface keyAccessServerRegistryServiceFutureStub, + WellKnownServiceClientInterface wellKnownServiceClient, SDK.KAS kas) { this.authorizationService = authorizationService; this.attributesService = attributesService; @@ -33,39 +42,45 @@ public FakeServices( this.subjectMappingService = subjectMappingService; this.resourceMappingService = resourceMappingService; this.keyAccessServerRegistryServiceFutureStub = keyAccessServerRegistryServiceFutureStub; + this.wellKnownService = wellKnownServiceClient; this.kas = kas; } @Override - public AuthorizationServiceClient authorization() { + public AuthorizationServiceClientInterface authorization() { return Objects.requireNonNull(authorizationService); } @Override - public AttributesServiceClient attributes() { + public AttributesServiceClientInterface attributes() { return Objects.requireNonNull(attributesService); } @Override - public NamespaceServiceClient namespaces() { + public NamespaceServiceClientInterface namespaces() { return Objects.requireNonNull(namespaceService); } @Override - public SubjectMappingServiceClient subjectMappings() { + public SubjectMappingServiceClientInterface subjectMappings() { return Objects.requireNonNull(subjectMappingService); } @Override - public ResourceMappingServiceClient resourceMappings() { + public ResourceMappingServiceClientInterface resourceMappings() { return Objects.requireNonNull(resourceMappingService); } @Override - public KeyAccessServerRegistryServiceClient kasRegistry() { + public KeyAccessServerRegistryServiceClientInterface kasRegistry() { return Objects.requireNonNull(keyAccessServerRegistryServiceFutureStub); } + @Override + public WellKnownServiceClientInterface wellknown() { + return Objects.requireNonNull(wellKnownService); + } + @Override public SDK.KAS kas() { return Objects.requireNonNull(kas); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/FakeServicesBuilder.java b/sdk/src/test/java/io/opentdf/platform/sdk/FakeServicesBuilder.java index 2a80f53d..558aee3b 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/FakeServicesBuilder.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/FakeServicesBuilder.java @@ -1,47 +1,54 @@ package io.opentdf.platform.sdk; -import io.opentdf.platform.authorization.AuthorizationServiceClient; -import io.opentdf.platform.policy.attributes.AttributesServiceClient; -import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient; -import io.opentdf.platform.policy.namespaces.NamespaceServiceClient; -import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceClient; -import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceClient; +import io.opentdf.platform.authorization.AuthorizationServiceClientInterface; +import io.opentdf.platform.policy.attributes.AttributesServiceClientInterface; +import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClientInterface; +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; public class FakeServicesBuilder { - private AuthorizationServiceClient authorizationService; - private AttributesServiceClient attributesService; - private NamespaceServiceClient namespaceService; - private SubjectMappingServiceClient subjectMappingService; - private ResourceMappingServiceClient resourceMappingService; - private KeyAccessServerRegistryServiceClient keyAccessServerRegistryServiceFutureStub; + private AuthorizationServiceClientInterface authorizationService; + private AttributesServiceClientInterface attributesService; + private NamespaceServiceClientInterface namespaceService; + private SubjectMappingServiceClientInterface subjectMappingService; + private ResourceMappingServiceClientInterface resourceMappingService; + private KeyAccessServerRegistryServiceClientInterface keyAccessServerRegistryServiceFutureStub; + private WellKnownServiceClientInterface wellKnownServiceClient; private SDK.KAS kas; - public FakeServicesBuilder setAuthorizationService(AuthorizationServiceClient authorizationService) { + public FakeServicesBuilder setAuthorizationService(AuthorizationServiceClientInterface authorizationService) { this.authorizationService = authorizationService; return this; } - public FakeServicesBuilder setAttributesService(AttributesServiceClient attributesService) { + public FakeServicesBuilder setAttributesService(AttributesServiceClientInterface attributesService) { this.attributesService = attributesService; return this; } - public FakeServicesBuilder setNamespaceService(NamespaceServiceClient namespaceService) { + public FakeServicesBuilder setNamespaceService(NamespaceServiceClientInterface namespaceService) { this.namespaceService = namespaceService; return this; } - public FakeServicesBuilder setSubjectMappingService(SubjectMappingServiceClient subjectMappingService) { + public FakeServicesBuilder setSubjectMappingService(SubjectMappingServiceClientInterface subjectMappingService) { this.subjectMappingService = subjectMappingService; return this; } - public FakeServicesBuilder setResourceMappingService(ResourceMappingServiceClient resourceMappingService) { + public FakeServicesBuilder setResourceMappingService(ResourceMappingServiceClientInterface resourceMappingService) { this.resourceMappingService = resourceMappingService; return this; } - public FakeServicesBuilder setKeyAccessServerRegistryService(KeyAccessServerRegistryServiceClient keyAccessServerRegistryServiceFutureStub) { + public FakeServicesBuilder setWellknownService(WellKnownServiceClientInterface wellKnownServiceClient) { + this.wellKnownServiceClient = wellKnownServiceClient; + return this; + } + + public FakeServicesBuilder setKeyAccessServerRegistryService(KeyAccessServerRegistryServiceClientInterface keyAccessServerRegistryServiceFutureStub) { this.keyAccessServerRegistryServiceFutureStub = keyAccessServerRegistryServiceFutureStub; return this; } @@ -52,6 +59,7 @@ public FakeServicesBuilder setKas(SDK.KAS kas) { } public FakeServices build() { - return new FakeServices(authorizationService, attributesService, namespaceService, subjectMappingService, resourceMappingService, keyAccessServerRegistryServiceFutureStub, kas); + return new FakeServices(authorizationService, attributesService, namespaceService, subjectMappingService, + resourceMappingService, keyAccessServerRegistryServiceFutureStub, wellKnownServiceClient, kas); } } \ 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 new file mode 100644 index 00000000..8429ff18 --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -0,0 +1,88 @@ +package io.opentdf.platform.sdk; + +import com.google.protobuf.Struct; +import com.google.protobuf.Value; +import io.opentdf.platform.policy.Algorithm; +import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse; +import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +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)); + + var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setWellknownService(wellknownService).build()); + + var baseKey = planner.fetchBaseKey(); + assertThat(baseKey).isNotEmpty(); + 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().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(); + + Mockito.when(wellknownService.getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap())) + .thenReturn(TestUtil.successfulUnaryCall(response)); + + var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setWellknownService(wellknownService).build()); + + var baseKey = planner.fetchBaseKey(); + assertThat(baseKey).isEmpty(); + } + + @Test + void generatePlanFromProvidedKases() { + var kas1 = new Config.KASInfo(); + kas1.URL = "https://kas1.example.com"; + kas1.KID = "kid1"; + kas1.Algorithm = "rsa:2048"; + + var kas2 = new Config.KASInfo(); + kas2.URL = "https://kas2.example.com"; + kas2.KID = "kid2"; + kas2.Algorithm = "ec:secp256"; + + var tdfConfig = new Config.TDFConfig(); + tdfConfig.kasInfoList.add(kas1); + tdfConfig.kasInfoList.add(kas2); + + var planner = new Planner(tdfConfig, new FakeServicesBuilder().build()); + List splitPlan = planner.generatePlanFromProvidedKases(tdfConfig.kasInfoList); + + assertThat(splitPlan).asList().hasSize(2); + assertThat(splitPlan.get(0).kas).isEqualTo("https://kas1.example.com"); + assertThat(splitPlan.get(0).kid).isEqualTo("kid1"); + + assertThat(splitPlan.get(1).kas).isEqualTo("https://kas2.example.com"); + assertThat(splitPlan.get(1).kid).isEqualTo("kid2"); + + assertThat(splitPlan.get(0).splitID).isNotEqualTo(splitPlan.get(1).splitID); + } +} \ No newline at end of file diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TestUtil.java b/sdk/src/test/java/io/opentdf/platform/sdk/TestUtil.java new file mode 100644 index 00000000..3a369aef --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TestUtil.java @@ -0,0 +1,22 @@ +package io.opentdf.platform.sdk; + +import com.connectrpc.ResponseMessage; +import com.connectrpc.UnaryBlockingCall; + +import java.util.Collections; + +public class TestUtil { + static UnaryBlockingCall successfulUnaryCall(T result) { + return new UnaryBlockingCall() { + @Override + public ResponseMessage execute() { + return new ResponseMessage.Success<>(result, Collections.emptyMap(), Collections.emptyMap()); + } + + @Override + public void cancel() { + + } + }; + } +} From 78031d8c1d51472cc43e8eb5170eefb4830c7726 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 2 Jul 2025 16:32:13 +0200 Subject: [PATCH 13/37] one more test --- .../opentdf/platform/sdk/Autoconfigure.java | 61 +++++++++++++---- .../java/io/opentdf/platform/sdk/Planner.java | 8 ++- .../platform/sdk/AutoconfigureTest.java | 65 +++++++++++++++++++ 3 files changed, 118 insertions(+), 16 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 629d4f55..e2f6da67 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -294,7 +295,6 @@ boolean addAllGrants(AttributeValueFQN fqn, List granted, List< mappedKeys.computeIfAbsent(fqn.key, k -> new ArrayList<>()).add(Config.KASInfo.fromSimpleKasKey(mappedKey)); grants.computeIfAbsent(fqn.key, k -> new KeyAccessGrant(attr, new ArrayList<>())).kases.add(mappedKey.getKasUri()); } - storeKeysToCache(granted, mapped, keyCache); if (foundMappedKey) { hasMappedKeys = true; @@ -582,7 +582,7 @@ public String toString() { } - static class PublicKeyInfo { + static class PublicKeyInfo implements Comparable { final String kas; final String kid; @@ -598,6 +598,44 @@ static class PublicKeyInfo { String getKas() { return kas; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PublicKeyInfo that = (PublicKeyInfo) o; + return Objects.equals(kas, that.kas) && Objects.equals(kid, that.kid); + } + + @Override + public int hashCode() { + return Objects.hash(kas, kid); + } + + @Override + public String toString() { + return "PublicKeyInfo{" + + "kas='" + kas + '\'' + + ", kid='" + kid + '\'' + + '}'; + } + + @Override + public int compareTo(PublicKeyInfo o) { + if (this.kas.equals(o.kas)) { + if (this.kid == null && o.kid == null) { + return 0; + } + if (this.kid == null) { + return -1; + } + if (o.kid == null) { + return 1; + } + return this.kid.compareTo(o.kid); + } else { + return this.kas.compareTo(o.kas); + } + } } static class KeyClause { @@ -678,7 +716,7 @@ public BooleanKeyExpression reduce() { continue; } Disjunction terms = new Disjunction(); - terms.add(k.getKas()); + terms.add(k); if (!within(conjunction, terms)) { conjunction.add(terms); } @@ -690,25 +728,22 @@ public BooleanKeyExpression reduce() { } List newValues = new ArrayList<>(); - for (List d : conjunction) { + for (List d : conjunction) { List pki = new ArrayList<>(); - for (String k : d) { - pki.add(new PublicKeyInfo(k)); - } + pki.addAll(d); newValues.add(new KeyClause(RuleType.ANY_OF, pki)); } return new BooleanKeyExpression(newValues); } public Disjunction sortedNoDupes(List l) { - Set set = new HashSet<>(); + Set set = new HashSet<>(); Disjunction list = new Disjunction(); for (PublicKeyInfo e : l) { - String kas = e.getKas(); - if (!kas.equals(RuleType.EMPTY_TERM) && !set.contains(kas)) { - set.add(kas); - list.add(kas); + if (!Objects.equals(e.getKas(), RuleType.EMPTY_TERM) && !set.contains(e)) { + set.add(e); + list.add(e); } } @@ -718,7 +753,7 @@ public Disjunction sortedNoDupes(List l) { } - static class Disjunction extends ArrayList { + static class Disjunction extends ArrayList { public boolean less(Disjunction r) { int m = Math.min(this.size(), r.size()); 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 5e9f89bc..d5e5ac8e 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -52,7 +52,7 @@ Map> getSplits(Config.TDFConfig tdfConfig) { splitPlan = tdfConfig.splitPlan; } - if (tdfConfig.kasInfoList.isEmpty() && tdfConfig.splitPlan.isEmpty()) { + if (tdfConfig.kasInfoList.isEmpty() && splitPlan.isEmpty()) { throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred"); } return fillInKeys(tdfConfig, splitPlan); @@ -112,12 +112,14 @@ Optional fetchBaseKey() { } if (baseKey == null || baseKey.kasUrl == null || baseKey.publicKey == null || baseKey.publicKey.kid == null || baseKey.publicKey.pem == null || baseKey.publicKey.algorithm == null) { - throw new SDKException("base key in well known configuration is missing required fields [" + baseKeyJson + "]"); + 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() + .setPublicKey( + SimpleKasPublicKey.newBuilder() .setKid(baseKey.publicKey.kid) .setAlgorithm(baseKey.publicKey.algorithm) .setPem(baseKey.publicKey.pem) diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index 4ff22cc4..8d24920e 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -13,6 +13,7 @@ import com.connectrpc.ResponseMessage; import com.connectrpc.UnaryBlockingCall; +import io.opentdf.platform.policy.Algorithm; import io.opentdf.platform.policy.Attribute; import io.opentdf.platform.policy.AttributeRuleTypeEnum; import io.opentdf.platform.policy.KasPublicKey; @@ -21,6 +22,8 @@ import io.opentdf.platform.policy.KeyAccessServer; import io.opentdf.platform.policy.Namespace; import io.opentdf.platform.policy.PublicKey; +import io.opentdf.platform.policy.SimpleKasKey; +import io.opentdf.platform.policy.SimpleKasPublicKey; import io.opentdf.platform.policy.Value; import io.opentdf.platform.policy.attributes.AttributesServiceClient; import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsRequest; @@ -31,6 +34,7 @@ import io.opentdf.platform.sdk.Autoconfigure.KeySplitStep; import io.opentdf.platform.sdk.Autoconfigure.Granter; +import org.assertj.core.api.AtomicIntegerArrayAssert; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -39,8 +43,10 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -57,6 +63,16 @@ public class AutoconfigureTest { public static final String SPECIFIED_KAS = "https://attr.kas.com/"; public static final String EVEN_MORE_SPECIFIC_KAS = "https://value.kas.com/"; private static final String NAMESPACE_KAS = "https://namespace.kas.com/"; + private static final SimpleKasKey NAMESPACE_KAS_KEY = SimpleKasKey.newBuilder().setKasUri("https://mapped.example.com").setKasId("mapped").setPublicKey( + SimpleKasPublicKey.newBuilder().setAlgorithm(Algorithm.ALGORITHM_EC_P521).setPem("namespacekey").setKid("namespacekeykid").build() + ).build(); + private static final SimpleKasKey ATTRIBUTE_KEY = SimpleKasKey.newBuilder().setKasUri("https://mapped.example.com").setKasId("mapped").setPublicKey( + SimpleKasPublicKey.newBuilder().setAlgorithm(Algorithm.ALGORITHM_EC_P521).setPem("attrpem").setKid("attrkeykid").build() + ).build(); + private static final SimpleKasKey VALUE_KEY = SimpleKasKey.newBuilder().setKasUri("https://mapped.example.com").setKasId("mapped").setPublicKey( + SimpleKasPublicKey.newBuilder().setAlgorithm(Algorithm.ALGORITHM_EC_P521).setPem("valuepem").setKid("valuekeykid").build() + ).build(); + private static Autoconfigure.AttributeNameFQN UNMAPPED; private static Autoconfigure.AttributeNameFQN SPKSPECKED; private static Autoconfigure.AttributeNameFQN SPKUNSPECKED; @@ -65,6 +81,8 @@ public class AutoconfigureTest { private static Autoconfigure.AttributeNameFQN REL; private static Autoconfigure.AttributeNameFQN UNSPECKED; private static Autoconfigure.AttributeNameFQN SPECKED; + private static Autoconfigure.AttributeNameFQN MAPPED; + private static Autoconfigure.AttributeNameFQN SPKMAPPED; private static Autoconfigure.AttributeValueFQN clsA; private static Autoconfigure.AttributeValueFQN clsS; @@ -85,6 +103,9 @@ public class AutoconfigureTest { private static Autoconfigure.AttributeValueFQN spk2spk2uns; private static Autoconfigure.AttributeValueFQN spk2spk2spk; + private static Autoconfigure.AttributeValueFQN mp2uns2uns; + private static Autoconfigure.AttributeValueFQN mp2uns2mp; + @BeforeAll public static void setup() throws AutoConfigureException { // Initialize the FQNs (Fully Qualified Names) @@ -93,8 +114,11 @@ public static void setup() throws AutoConfigureException { REL = new Autoconfigure.AttributeNameFQN("https://virtru.com/attr/Releasable%20To"); UNSPECKED = new Autoconfigure.AttributeNameFQN("https://other.com/attr/unspecified"); SPECKED = new Autoconfigure.AttributeNameFQN("https://other.com/attr/specified"); + MAPPED = new Autoconfigure.AttributeNameFQN("https://other.com/attr/mapped"); + UNMAPPED = new Autoconfigure.AttributeNameFQN("https://mapped.com/attr/unspecified"); SPKUNSPECKED = new Autoconfigure.AttributeNameFQN("https://hasgrants.com/attr/unspecified"); SPKSPECKED = new Autoconfigure.AttributeNameFQN("https://hasgrants.com/attr/specified"); + SPKMAPPED = new Autoconfigure.AttributeNameFQN("https://hasgrants.com/attr/mapped"); clsA = new Autoconfigure.AttributeValueFQN("https://virtru.com/attr/Classification/value/Allowed"); clsS = new Autoconfigure.AttributeValueFQN("https://virtru.com/attr/Classification/value/Secret"); @@ -118,6 +142,9 @@ public static void setup() throws AutoConfigureException { spk2uns2spk = new Autoconfigure.AttributeValueFQN("https://hasgrants.com/attr/unspecified/value/specked"); spk2spk2uns = new Autoconfigure.AttributeValueFQN("https://hasgrants.com/attr/specified/value/unspecked"); spk2spk2spk = new Autoconfigure.AttributeValueFQN("https://hasgrants.com/attr/specified/value/specked"); + + mp2uns2uns = new Autoconfigure.AttributeValueFQN("https://mapped.com/attr/unspecified/value/unspecked"); + mp2uns2mp = new Autoconfigure.AttributeValueFQN("https://mapped.com/attr/unspecified/value/mapped"); } private static String spongeCase(String s) { @@ -182,6 +209,7 @@ private Attribute mockAttributeFor(Autoconfigure.AttributeNameFQN fqn) { Namespace ns1 = Namespace.newBuilder().setId("v").setName("virtru.com").setFqn("https://virtru.com").build(); Namespace ns2 = Namespace.newBuilder().setId("o").setName("other.com").setFqn("https://other.com").build(); Namespace ns3 = Namespace.newBuilder().setId("h").setName("hasgrants.com").addGrants(KeyAccessServer.newBuilder().setUri(NAMESPACE_KAS).build()).setFqn("https://hasgrants.com").build(); + Namespace ns4 = Namespace.newBuilder().setId("m").setName("mapped.com").addKasKeys(NAMESPACE_KAS_KEY).build(); String key = fqn.getKey(); if (key.equals(CLS.getKey())) { @@ -217,6 +245,17 @@ private Attribute mockAttributeFor(Autoconfigure.AttributeNameFQN fqn) { .setName("unspecified").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) .setName(fqn.toString()) .build(); + } else if (key.equals(MAPPED.getKey())) { + return Attribute.newBuilder().setId(MAPPED.getKey()).setNamespace(ns4) + .setName("mapped attribute").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) + .setKasKeys(0, ATTRIBUTE_KEY) + .setName(fqn.toString()) + .build(); + } else if (key.equals(UNMAPPED.getKey())) { + return Attribute.newBuilder().setId(UNMAPPED.getKey()).setNamespace(ns4) + .setName("unmapped attribute").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) + .setName(fqn.toString()) + .build(); } throw new IllegalArgumentException("Key not recognized: " + key); @@ -288,7 +327,13 @@ private Value mockValueFor(Autoconfigure.AttributeValueFQN fqn) throws AutoConfi p = p.toBuilder().addGrants(KeyAccessServer.newBuilder().setUri(EVEN_MORE_SPECIFIC_KAS).build()) .build(); } + } else if (Objects.equals(UNMAPPED.getKey(), an.getKey())) { + if (fqn.value().equalsIgnoreCase("mapped")) { + p = p.toBuilder().addKasKeys(VALUE_KEY) + .build(); + } } + return p; } @@ -475,6 +520,26 @@ public void testReasonerConstructAttributeBoolean() { } } + @Test + void testUsingAttributeMappedAtNamespace() { + Granter granter = Autoconfigure.newGranterFromAttributes(new KASKeyCache(), mockValueFor(mp2uns2uns)); + var counter = new AtomicInteger(0); + var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), () -> Optional.empty()); + assertThat(splitPlan).isEqualTo(List.of(new KeySplitStep("https://mapped.example.com", "", NAMESPACE_KAS_KEY.getPublicKey().getKid()))); + } + + @Test + void testUsingAttributeMappedAtMultiplePlaces() { + var attributes = new Value[] { mockValueFor(mp2uns2uns), mockValueFor(mp2uns2mp) }; + Granter granter = Autoconfigure.newGranterFromAttributes(new KASKeyCache(), attributes); + var counter = new AtomicInteger(0); + var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), () -> Optional.empty()); + assertThat(splitPlan).isEqualTo(List.of( + new KeySplitStep(NAMESPACE_KAS_KEY.getKasUri(), "0", NAMESPACE_KAS_KEY.getPublicKey().getKid()), + new KeySplitStep(VALUE_KEY.getKasUri(), "0", VALUE_KEY.getPublicKey().getKid()) + )); + } + GetAttributeValuesByFqnsResponse getResponse(GetAttributeValuesByFqnsRequest req) { GetAttributeValuesByFqnsResponse.Builder builder = GetAttributeValuesByFqnsResponse.newBuilder(); From 8c0bcf41f8c58fe8e6806e3cc96eee83889296fd Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 2 Jul 2025 22:18:24 +0200 Subject: [PATCH 14/37] add test for resolving keys --- .../java/io/opentdf/platform/sdk/Planner.java | 9 ++-- .../java/io/opentdf/platform/sdk/TDF.java | 4 +- .../io/opentdf/platform/sdk/PlannerTest.java | 49 ++++++++++++++++++- 3 files changed, 56 insertions(+), 6 deletions(-) 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 d5e5ac8e..16641337 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -55,7 +55,7 @@ Map> getSplits(Config.TDFConfig tdfConfig) { if (tdfConfig.kasInfoList.isEmpty() && splitPlan.isEmpty()) { throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred"); } - return fillInKeys(tdfConfig, splitPlan); + return resolveKeys(splitPlan); } private List getAutoconfigurePlan(Config.TDFConfig tdfConfig) { @@ -144,7 +144,7 @@ private static class Key { } - private Map> fillInKeys(Config.TDFConfig tdfConfig, List splitPlan) { + Map> resolveKeys(List splitPlan) { Map> conjunction = new HashMap<>(); var latestKASInfo = new HashMap(); // Seed anything passed in manually @@ -162,7 +162,10 @@ private Map> fillInKeys(Config.TDFConfig tdfConfig, logger.info("no public key provided for KAS at {}, retrieving", splitInfo.kas); var getKI = new Config.KASInfo(); getKI.URL = splitInfo.kas; - getKI.Algorithm = tdfConfig.wrappingKeyType.toString(); + if (!tdfConfig.autoconfigure) { + getKI.Algorithm = tdfConfig.wrappingKeyType.toString(); + } + getKI.KID = splitInfo.kid; getKI = services.kas().getPublicKey(getKI); latestKASInfo.put(splitInfo.kas, getKI); ki = getKI; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 6f0e7a15..3e2df7c1 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -139,7 +139,7 @@ private PolicyObject createPolicyObject(List at private static final Base64.Encoder encoder = Base64.getEncoder(); - private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas, Map> splits) { + private void prepareManifest(Config.TDFConfig tdfConfig, Map> splits) { manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_VERSION : null; manifest.encryptionInformation.keyAccessType = kSplitKeyType; manifest.encryptionInformation.keyAccessObj = new ArrayList<>(); @@ -349,7 +349,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo Map> splits = planner.getSplits(tdfConfig); TDFObject tdfObject = new TDFObject(); - tdfObject.prepareManifest(tdfConfig, services.kas(), splits); + tdfObject.prepareManifest(tdfConfig, splits); long encryptedSegmentSize = tdfConfig.defaultSegmentSize + kGcmIvSize + kAesBlockSize; TDFWriter tdfWriter = new TDFWriter(outputStream); 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 8429ff18..191595c7 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -9,9 +9,11 @@ import org.mockito.Mockito; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.*; class PlannerTest { @@ -85,4 +87,49 @@ void generatePlanFromProvidedKases() { assertThat(splitPlan.get(0).splitID).isNotEqualTo(splitPlan.get(1).splitID); } + + @Test + void testFillingInKeysWithAutoConfigure() { + var kas = Mockito.mock(SDK.KAS.class); + Mockito.when(kas.getPublicKey(Mockito.any())).thenAnswer(invocation -> { + Config.KASInfo kasInfo = invocation.getArgument(0, Config.KASInfo.class); + var ret = new Config.KASInfo(); + ret.URL = kasInfo.URL; + assertThat(kasInfo.Algorithm).isNullOrEmpty(); + if (Objects.equals(kasInfo.URL, "https://kas1.example.com")) { + ret.PublicKey = "pem1"; + ret.Algorithm = "rsa:2048"; + ret.KID = "kid1"; + } else if (Objects.equals(kasInfo.URL, "https://kas2.example.com")) { + ret.PublicKey = "pem2"; + ret.Algorithm = "ec:secp256r1"; + ret.KID = "kid2"; + } else { + throw new IllegalArgumentException("Unexpected KAS URL: " + kasInfo.URL); + } + return ret; + }); + var tdfConfig = new Config.TDFConfig(); + tdfConfig.autoconfigure = true; + tdfConfig.wrappingKeyType = KeyType.RSA2048Key; + var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setKas(kas).build()); + var plan = List.of( + new Autoconfigure.KeySplitStep("https://kas1.example.com", "split1", null), + new Autoconfigure.KeySplitStep("https://kas2.example.com", "split2", "kid2") + ); + Map> filledInPlan = planner.resolveKeys(plan); + assertThat(filledInPlan.keySet().stream().collect(Collectors.toList())).asList().containsExactlyInAnyOrder("split1", "split2"); + assertThat(filledInPlan.get("split1")).asList().hasSize(1); + var split1KasInfo = filledInPlan.get("split1").get(0); + assertThat(split1KasInfo.URL).isEqualTo("https://kas1.example.com"); + assertThat(split1KasInfo.KID).isEqualTo("kid1"); + assertThat(split1KasInfo.Algorithm).isEqualTo("rsa:2048"); + assertThat(split1KasInfo.PublicKey).isEqualTo("pem1"); + assertThat(filledInPlan.get("split2")).asList().hasSize(1); + var split2KasInfo = filledInPlan.get("split2").get(0); + assertThat(split2KasInfo.URL).isEqualTo("https://kas2.example.com"); + assertThat(split2KasInfo.KID).isEqualTo("kid2"); + assertThat(split2KasInfo.Algorithm).isEqualTo("ec:secp256r1"); + assertThat(split2KasInfo.PublicKey).isEqualTo("pem2"); + } } \ No newline at end of file From c8370e739f8a7b7ebb4260fa596552382e84ab3b Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 2 Jul 2025 22:26:43 +0200 Subject: [PATCH 15/37] added a split --- .../io/opentdf/platform/sdk/PlannerTest.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 191595c7..914158b6 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -104,6 +104,10 @@ void testFillingInKeysWithAutoConfigure() { ret.PublicKey = "pem2"; ret.Algorithm = "ec:secp256r1"; ret.KID = "kid2"; + } else if (Objects.equals(kasInfo.URL, "https://kas3.example.com")) { + ret.PublicKey = "pem3"; + ret.Algorithm = "rsa:4096"; + ret.KID = "kid3"; } else { throw new IllegalArgumentException("Unexpected KAS URL: " + kasInfo.URL); } @@ -115,7 +119,8 @@ void testFillingInKeysWithAutoConfigure() { var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setKas(kas).build()); var plan = List.of( new Autoconfigure.KeySplitStep("https://kas1.example.com", "split1", null), - new Autoconfigure.KeySplitStep("https://kas2.example.com", "split2", "kid2") + new Autoconfigure.KeySplitStep("https://kas2.example.com", "split2", "kid2"), + new Autoconfigure.KeySplitStep("https://kas3.example.com", "split2", "kid3") ); Map> filledInPlan = planner.resolveKeys(plan); assertThat(filledInPlan.keySet().stream().collect(Collectors.toList())).asList().containsExactlyInAnyOrder("split1", "split2"); @@ -125,11 +130,14 @@ void testFillingInKeysWithAutoConfigure() { assertThat(split1KasInfo.KID).isEqualTo("kid1"); assertThat(split1KasInfo.Algorithm).isEqualTo("rsa:2048"); assertThat(split1KasInfo.PublicKey).isEqualTo("pem1"); - assertThat(filledInPlan.get("split2")).asList().hasSize(1); - var split2KasInfo = filledInPlan.get("split2").get(0); + assertThat(filledInPlan.get("split2")).asList().hasSize(2); + var split2KasInfo = filledInPlan.get("split2").stream().filter(kasInfo -> "kid2".equals(kasInfo.KID)).findFirst().get(); assertThat(split2KasInfo.URL).isEqualTo("https://kas2.example.com"); - assertThat(split2KasInfo.KID).isEqualTo("kid2"); assertThat(split2KasInfo.Algorithm).isEqualTo("ec:secp256r1"); assertThat(split2KasInfo.PublicKey).isEqualTo("pem2"); + var split3KasInfo = filledInPlan.get("split2").stream().filter(kasInfo -> "kid3".equals(kasInfo.KID)).findFirst().get(); + assertThat(split3KasInfo.URL).isEqualTo("https://kas3.example.com"); + assertThat(split3KasInfo.Algorithm).isEqualTo("rsa:4096"); + assertThat(split3KasInfo.PublicKey).isEqualTo("pem3"); } } \ No newline at end of file From 41f3d6a40086554646f6874419d1d768681db5e1 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 09:54:58 +0200 Subject: [PATCH 16/37] fix up tests --- .../io/opentdf/platform/sdk/KASKeyCache.java | 29 ++++++++++--------- .../opentdf/platform/sdk/KASKeyCacheTest.java | 16 ++++++++-- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java index a67ad346..75bae93c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java @@ -7,6 +7,7 @@ import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * Class representing a cache for KAS (Key Access Server) information. @@ -31,7 +32,7 @@ public Config.KASInfo get(String url, String algorithm, String kid) { TimeStampedKASInfo cachedValue = cache.get(cacheKey); if (cachedValue == null) { - log.debug("didn't find kasinfo for url = [{}], algorithm = [{}]", url, algorithm); + log.debug("didn't find kasinfo for key= [{}]", cacheKey); return null; } @@ -93,24 +94,26 @@ public KASKeyRequest(String url, String algorithm, String kid) { this.kid = kid; } - // Override equals and hashCode to ensure proper functioning of the HashMap @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || !(o instanceof KASKeyRequest)) return false; + if (o == null || getClass() != o.getClass()) return false; KASKeyRequest that = (KASKeyRequest) o; - if (algorithm == null){ - return url.equals(that.url); - } - return url.equals(that.url) && algorithm.equals(that.algorithm); + return Objects.equals(url, that.url) && Objects.equals(algorithm, that.algorithm) && Objects.equals(kid, that.kid); } @Override public int hashCode() { - int result = 31 * url.hashCode(); - if (algorithm != null) { - result = result + algorithm.hashCode(); - } - return result; + return Objects.hash(url, algorithm, kid); + } + + @Override + public String toString() { + return "KASKeyRequest{" + + "url='" + url + '\'' + + ", algorithm='" + algorithm + '\'' + + ", kid='" + kid + '\'' + + '}'; } + + // Override equals and hashCode to ensure proper functioning of the HashMap } \ No newline at end of file diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/KASKeyCacheTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/KASKeyCacheTest.java index f1c63369..fdee682e 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/KASKeyCacheTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/KASKeyCacheTest.java @@ -51,7 +51,7 @@ void testStoreAndGet_AfterTimeLimit() { kasKeyCache.store(kasInfo1); // Simulate time passing by modifying the timestamp directly - KASKeyRequest cacheKey = new KASKeyRequest("https://example.com/kas1", "rsa:2048", "kid2"); + KASKeyRequest cacheKey = new KASKeyRequest("https://example.com/kas1", "rsa:2048", "kid1"); TimeStampedKASInfo timeStampedKASInfo = new TimeStampedKASInfo(kasInfo1, LocalDateTime.now().minus(6, ChronoUnit.MINUTES)); kasKeyCache.cache.put(cacheKey, timeStampedKASInfo); @@ -62,6 +62,18 @@ void testStoreAndGet_AfterTimeLimit() { assertNull(result); } + @Test + void testStoreAndGet_DifferentKIDs() { + // Store an item in the cache + kasKeyCache.store(kasInfo1); + + // Attempt to retrieve the item with a different KID + Config.KASInfo result = kasKeyCache.get(kasInfo1.URL, kasInfo1.Algorithm, kasInfo1.KID + "different"); + + // Ensure the item was not retrieved (it should have expired) + assertNull(result); + } + @Test void testStoreAndGet_WithNullAlgorithm() { // Store an item in the cache with a null algorithm @@ -72,7 +84,7 @@ void testStoreAndGet_WithNullAlgorithm() { kasKeyCache.store(kasInfo1); // Retrieve the item with a null algorithm - Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", null, null); + Config.KASInfo result = kasKeyCache.get("https://example.com/kas1", null, "kid1"); // Ensure the item was correctly retrieved assertNotNull(result); From 491cbf3d055beacade5f0cc1ae5a395616db6cc0 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 10:19:18 +0200 Subject: [PATCH 17/37] a couple more tests --- .../opentdf/platform/sdk/Autoconfigure.java | 2 +- .../java/io/opentdf/platform/sdk/Planner.java | 2 +- .../platform/sdk/AutoconfigureTest.java | 39 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index e2f6da67..734ad43c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -362,7 +362,7 @@ List getSplits(List defaultKases, Supplier genSpli logger.warn("no grants or mapped keys found, generating plan from default KASes. this is deprecated"); // this is a little bit weird because we don't take into account the KIDs here. This is the way - // that it works in + // that it works in the go SDK but it seems a bit odd return generatePlanFromDefaultKases(defaultKases, genSplitID); } 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 16641337..e32b46fc 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -127,7 +127,7 @@ Optional fetchBaseKey() { .build()); } - private static Gson gson = new Gson(); + private static final Gson gson = new Gson(); private static class BaseKey { @SerializedName("kas_url") diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index 8d24920e..b3b92c33 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -47,6 +48,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -990,4 +992,41 @@ public ResponseMessage execute() { assertEquals("public-key-pem-2", storedKASInfo2.PublicKey); } + @Test + void testUsingBaseKeyWhenNoMappedKeysOrGrants() { + Autoconfigure.Granter granter = Autoconfigure.newGranterFromAttributes(null); + SimpleKasKey key = SimpleKasKey.newBuilder() + .setKasUri("https://example.com/kas") + .setPublicKey( + SimpleKasPublicKey.newBuilder() + .setKid("thenewkid") + .setPem("anotherpem") + .setAlgorithm(Algorithm.ALGORITHM_EC_P521) + ).build(); + + var splits = granter.getSplits( + List.of("https://example.org/kas2"), + () -> { throw new IllegalStateException("the plan should have a single element"); }, + () -> Optional.of(key)); + assertThat(splits).hasSize(1); + assertThat(splits.get(0)).isEqualTo(new KeySplitStep("https://example.com/kas", "", "thenewkid")); + } + + @Test + void testUsingDefaultKasesWhenNothingElseProvided() { + Autoconfigure.Granter granter = Autoconfigure.newGranterFromAttributes(null); + var counter = new AtomicInteger(); + Supplier splitGen = () -> String.valueOf(counter.getAndIncrement()); + var splits = granter.getSplits( + List.of("https://example.org/kas1", "https://example.org/kas2"), + splitGen, + Optional::empty); + + assertThat(splits).hasSize(2); + assertThat(splits).asList() + .containsExactly( + new KeySplitStep("https://example.org/kas1", "0", null), + new KeySplitStep("https://example.org/kas2", "1", null) + ); + } } From 26016f3a5d249cc650b958e5a603354b694aa837 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 10:26:17 +0200 Subject: [PATCH 18/37] remove condition we do not need --- sdk/src/main/java/io/opentdf/platform/sdk/Planner.java | 4 +--- sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) 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 e32b46fc..1f1ff654 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -162,9 +162,7 @@ Map> resolveKeys(List s 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.Algorithm = tdfConfig.wrappingKeyType.toString(); getKI.KID = splitInfo.kid; getKI = services.kas().getPublicKey(getKI); latestKASInfo.put(splitInfo.kas, getKI); 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 914158b6..54d70bbc 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -95,7 +95,6 @@ void testFillingInKeysWithAutoConfigure() { Config.KASInfo kasInfo = invocation.getArgument(0, Config.KASInfo.class); var ret = new Config.KASInfo(); ret.URL = kasInfo.URL; - assertThat(kasInfo.Algorithm).isNullOrEmpty(); if (Objects.equals(kasInfo.URL, "https://kas1.example.com")) { ret.PublicKey = "pem1"; ret.Algorithm = "rsa:2048"; From 1ba4cd523bdc20e0ec962d026f6be4c5265f8a24 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 10:39:42 +0200 Subject: [PATCH 19/37] add test for default kases --- .../io/opentdf/platform/sdk/PlannerTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) 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 54d70bbc..231bb345 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -5,6 +5,7 @@ import io.opentdf.platform.policy.Algorithm; 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.mockito.Mockito; @@ -139,4 +140,50 @@ void testFillingInKeysWithAutoConfigure() { assertThat(split3KasInfo.Algorithm).isEqualTo("rsa:4096"); assertThat(split3KasInfo.PublicKey).isEqualTo("pem3"); } + + @Test + void returnsOnlyDefaultKasesIfPresent() { + var kas1 = new Config.KASInfo(); + kas1.URL = "https://kas1.example.com"; + kas1.Default = true; + + var kas2 = new Config.KASInfo(); + kas2.URL = "https://kas2.example.com"; + kas2.Default = false; + + var kas3 = new Config.KASInfo(); + kas3.URL = "https://kas3.example.com"; + kas3.Default = true; + + var config = new Config.TDFConfig(); + config.kasInfoList.addAll(List.of(kas1, kas2, kas3)); + + List result = Planner.defaultKases(config); + + Assertions.assertThat(result).containsExactlyInAnyOrder("https://kas1.example.com", "https://kas3.example.com"); + } + + @Test + void returnsAllKasesIfNoDefault() { + var kas1 = new Config.KASInfo(); + kas1.URL = "https://kas1.example.com"; + kas1.Default = false; + + var kas2 = new Config.KASInfo(); + kas2.URL = "https://kas2.example.com"; + kas2.Default = null; // not set + + var config = new Config.TDFConfig(); + config.kasInfoList.addAll(List.of(kas1, kas2)); + + List result = Planner.defaultKases(config); + Assertions.assertThat(result).containsExactlyInAnyOrder("https://kas1.example.com", "https://kas2.example.com"); + } + + @Test + void returnsEmptyListIfNoKases() { + var config = new Config.TDFConfig(); + List result = Planner.defaultKases(config); + Assertions.assertThat(result).isEmpty(); + } } \ No newline at end of file From 95232adbc68f82c73e902d72a16c08c992217b94 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 12:28:56 +0200 Subject: [PATCH 20/37] add more tests --- .../opentdf/platform/sdk/Autoconfigure.java | 17 ++- .../java/io/opentdf/platform/sdk/Planner.java | 26 ++-- .../java/io/opentdf/platform/sdk/TDF.java | 2 +- .../platform/sdk/AutoconfigureTest.java | 118 +++++++++++++++--- .../io/opentdf/platform/sdk/PlannerTest.java | 79 +++++++++++- 5 files changed, 201 insertions(+), 41 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 734ad43c..f33a0d20 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -824,7 +824,7 @@ public static String ruleToOperator(AttributeRuleTypeEnum e) { // Given a policy (list of data attributes or tags), // get a set of grants from attribute values to KASes. // Unlike `NewGranterFromService`, this works offline. - public static Granter newGranterFromAttributes(KASKeyCache keyCache, Value... attrValues) throws AutoConfigureException { + static Granter newGranterFromAttributes(KASKeyCache keyCache, Value... attrValues) throws AutoConfigureException { var attrsAndValues = Arrays.stream(attrValues).map(v -> { if (!v.hasAttribute()) { throw new AutoConfigureException("tried to use an attribute that is not initialized"); @@ -839,7 +839,7 @@ public static Granter newGranterFromAttributes(KASKeyCache keyCache, Value... at } // Gets a list of directory of KAS grants for a list of attribute FQNs - public static Granter newGranterFromService(AttributesServiceClientInterface as, KASKeyCache keyCache, AttributeValueFQN... fqns) throws AutoConfigureException { + static Granter newGranterFromService(AttributesServiceClientInterface as, KASKeyCache keyCache, AttributeValueFQN... fqns) throws AutoConfigureException { GetAttributeValuesByFqnsRequest request = GetAttributeValuesByFqnsRequest.newBuilder() .addAllFqns(Arrays.stream(fqns).map(AttributeValueFQN::toString).collect(Collectors.toList())) .setWithValue(AttributeValueSelector.newBuilder().setWithKeyAccessGrants(true).build()) @@ -852,6 +852,18 @@ public static Granter newGranterFromService(AttributesServiceClientInterface as, return getGranter(keyCache, new ArrayList<>(av.getFqnAttributeValuesMap().values())); } + + static Autoconfigure.Granter createGranter(SDK.Services services, 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; + } + private static Granter getGranter(KASKeyCache keyCache, List values) { Granter grants = new Granter(values.stream().map(GetAttributeValuesByFqnsResponse.AttributeAndValue::getValue).map(Value::getFqn).map(AttributeValueFQN::new).collect(Collectors.toList())); for (var attributeAndValue: values) { @@ -862,7 +874,6 @@ private static Granter getGranter(KASKeyCache keyCache, List granterFactory) { this.tdfConfig = Objects.requireNonNull(config); this.services = Objects.requireNonNull(services); } @@ -45,7 +47,7 @@ Map> getSplits(Config.TDFConfig tdfConfig) { if (tdfConfig.splitPlan != null && !tdfConfig.splitPlan.isEmpty()) { throw new IllegalArgumentException("cannot use autoconfigure with a split plan provided in the TDFConfig"); } - splitPlan = getAutoconfigurePlan(tdfConfig); + splitPlan = getAutoconfigurePlan(services, tdfConfig); } else if (tdfConfig.splitPlan == null || tdfConfig.splitPlan.isEmpty()) { splitPlan = generatePlanFromProvidedKases(tdfConfig.kasInfoList); } else { @@ -58,17 +60,12 @@ Map> getSplits(Config.TDFConfig tdfConfig) { return resolveKeys(splitPlan); } - private List 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); + private static List getAutoconfigurePlan(SDK.Services services, Config.TDFConfig tdfConfig) { + Autoconfigure.Granter granter = Autoconfigure.createGranter(services, tdfConfig); + return granter.getSplits(defaultKases(tdfConfig), Planner::getUUID, () -> Planner.fetchBaseKey(services.wellknown())); } + List generatePlanFromProvidedKases(List kases) { if (kases.size() == 1) { var kasInfo = kases.get(0); @@ -81,8 +78,8 @@ List generatePlanFromProvidedKases(List fetchBaseKey() { - var responseMessage = services.wellknown() + static Optional fetchBaseKey(WellKnownServiceClientInterface wellknown) { + var responseMessage = wellknown .getWellKnownConfigurationBlocking(GetWellKnownConfigurationRequest.getDefaultInstance(), Collections.emptyMap()) .execute(); GetWellKnownConfigurationResponse response; @@ -143,7 +140,6 @@ private static class Key { } } - Map> resolveKeys(List splitPlan) { Map> conjunction = new HashMap<>(); var latestKASInfo = new HashMap(); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 3e2df7c1..a5e3cb2c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -345,7 +345,7 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte } TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException { - Planner planner = new Planner(tdfConfig, services); + Planner planner = new Planner(tdfConfig, services, Autoconfigure::createGranter); Map> splits = planner.getSplits(tdfConfig); TDFObject tdfObject = new TDFObject(); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index b3b92c33..19cee99e 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -38,6 +38,7 @@ import org.assertj.core.api.AtomicIntegerArrayAssert; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.platform.commons.JUnitException; import org.mockito.Mockito; import java.util.ArrayList; @@ -532,7 +533,7 @@ void testUsingAttributeMappedAtNamespace() { @Test void testUsingAttributeMappedAtMultiplePlaces() { - var attributes = new Value[] { mockValueFor(mp2uns2uns), mockValueFor(mp2uns2mp) }; + var attributes = new Value[]{mockValueFor(mp2uns2uns), mockValueFor(mp2uns2mp)}; Granter granter = Autoconfigure.newGranterFromAttributes(new KASKeyCache(), attributes); var counter = new AtomicInteger(0); var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), () -> Optional.empty()); @@ -756,7 +757,7 @@ private static class ReasonerTestCase { private final List plan; ReasonerTestCase(String name, List policy, List defaults, String ats, String keyed, - String reduced, List plan) { + String reduced, List plan) { this.name = name; this.policy = policy; this.defaults = defaults; @@ -809,8 +810,8 @@ public List getPlan() { void testStoreKeysToCache_NoKeys() { KASKeyCache keyCache = Mockito.mock(KASKeyCache.class); KeyAccessServer kas1 = KeyAccessServer.newBuilder().setPublicKey( - PublicKey.newBuilder().setCached( - KasPublicKeySet.newBuilder())) + PublicKey.newBuilder().setCached( + KasPublicKeySet.newBuilder())) .build(); Autoconfigure.storeKeysToCache(List.of(kas1), Collections.emptyList(), keyCache); @@ -906,7 +907,7 @@ void testStoreKeysToCache_MultipleKasEntries() { } GetAttributeValuesByFqnsResponse getResponseWithGrants(GetAttributeValuesByFqnsRequest req, - List grants) { + List grants) { GetAttributeValuesByFqnsResponse.Builder builder = GetAttributeValuesByFqnsResponse.newBuilder(); for (String v : req.getFqnsList()) { @@ -958,13 +959,15 @@ void testKeyCacheFromGrants() { AttributesServiceClient attributesServiceClient = mock(AttributesServiceClient.class); when(attributesServiceClient.getAttributeValuesByFqnsBlocking(any(), any())).thenAnswer(invocation -> { - var request = (GetAttributeValuesByFqnsRequest)invocation.getArgument(0); - return new UnaryBlockingCall(){ + var request = (GetAttributeValuesByFqnsRequest) invocation.getArgument(0); + return new UnaryBlockingCall() { @Override public ResponseMessage execute() { return new ResponseMessage.Success<>(getResponseWithGrants(request, List.of(kas1)), Collections.emptyMap(), Collections.emptyMap()); } - @Override public void cancel() { + + @Override + public void cancel() { // not really calling anything } }; @@ -996,17 +999,19 @@ public ResponseMessage execute() { void testUsingBaseKeyWhenNoMappedKeysOrGrants() { Autoconfigure.Granter granter = Autoconfigure.newGranterFromAttributes(null); SimpleKasKey key = SimpleKasKey.newBuilder() - .setKasUri("https://example.com/kas") - .setPublicKey( - SimpleKasPublicKey.newBuilder() - .setKid("thenewkid") - .setPem("anotherpem") - .setAlgorithm(Algorithm.ALGORITHM_EC_P521) - ).build(); + .setKasUri("https://example.com/kas") + .setPublicKey( + SimpleKasPublicKey.newBuilder() + .setKid("thenewkid") + .setPem("anotherpem") + .setAlgorithm(Algorithm.ALGORITHM_EC_P521) + ).build(); var splits = granter.getSplits( List.of("https://example.org/kas2"), - () -> { throw new IllegalStateException("the plan should have a single element"); }, + () -> { + throw new IllegalStateException("the plan should have a single element"); + }, () -> Optional.of(key)); assertThat(splits).hasSize(1); assertThat(splits.get(0)).isEqualTo(new KeySplitStep("https://example.com/kas", "", "thenewkid")); @@ -1029,4 +1034,85 @@ void testUsingDefaultKasesWhenNothingElseProvided() { new KeySplitStep("https://example.org/kas2", "1", null) ); } + + @Test + void createsGranterFromAttributeValues() { + // Arrange + Config.TDFConfig config = new Config.TDFConfig(); + config.attributeValues = List.of(mockValueFor(spk2spk), mockValueFor(rel2gbr)); + + SDK.Services services = mock(SDK.Services.class); + SDK.KAS kas = mock(SDK.KAS.class); + when(services.kas()).thenReturn(kas); + when(services.attributes()).thenThrow(new IllegalStateException("should never use the attribute service when attributes are provided")); + when(kas.getKeyCache()).thenReturn(null); // No cache needed for this test + + // Act + Autoconfigure.Granter granter = Autoconfigure.createGranter(services, config); + + // Assert + assertThat(granter).isNotNull(); + assertThat(granter.getPolicy()).hasSize(2); + assertThat(granter.getPolicy()).containsExactlyInAnyOrder( + new AttributeValueFQN("https://other.com/attr/specified/value/specked"), + new AttributeValueFQN("https://virtru.com/attr/Releasable%20To/value/GBR") + ); + } + + @Test + void createsGranterFromService() { + // Arrange + SDK.Services services = mock(SDK.Services.class); + SDK.KAS kas = mock(SDK.KAS.class); + AttributesServiceClient attributesServiceClient = mock(AttributesServiceClient.class); + + // Prepare a request and a mocked response + List policy = List.of( + new AttributeValueFQN("https://other.com/attr/specified/value/specked"), + new AttributeValueFQN("https://virtru.com/attr/Releasable%20To/value/GBR") + ); +// GetAttributeValuesByFqnsRequest request = GetAttributeValuesByFqnsRequest.newBuilder() +// .addAllFqns(policy.stream().map(AttributeValueFQN::toString).collect(Collectors.toList())) +// .build(); + + when(services.kas()).thenReturn(kas); + when(services.attributes()).thenReturn(attributesServiceClient); + + // Mock the attribute service to return a response with the expected values + when(attributesServiceClient.getAttributeValuesByFqnsBlocking(any(), any())).thenAnswer(invocation -> { + return new UnaryBlockingCall() { + @Override + public ResponseMessage execute() { + GetAttributeValuesByFqnsResponse.Builder builder = GetAttributeValuesByFqnsResponse.newBuilder(); + for (AttributeValueFQN fqn : policy) { + Value value = Value.newBuilder() + .setId(fqn.toString()) + .setFqn(fqn.toString()) + .build(); + builder.putFqnAttributeValues(fqn.toString(), + GetAttributeValuesByFqnsResponse.AttributeAndValue.newBuilder() + .setValue(value) + .build()); + } + return new ResponseMessage.Success<>(builder.build(), Collections.emptyMap(), Collections.emptyMap()); + } + + @Override + public void cancel() { + } + }; + }); + + // Act + Autoconfigure.Granter granter = Autoconfigure.createGranter(services, new Config.TDFConfig() {{ + attributeValues = null; // force use of service + attributes = policy; + }}); + + // Assert + assertThat(granter).isNotNull(); + // The policy should be empty because attributeValues is null, but the test ensures the service is called + // If you want to check the service call, verify it: + verify(services).attributes(); + } } 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 231bb345..1b647240 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -32,9 +32,8 @@ void fetchBaseKey() { Mockito.when(wellknownService.getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap())) .thenReturn(TestUtil.successfulUnaryCall(response)); - var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setWellknownService(wellknownService).build()); - var baseKey = planner.fetchBaseKey(); + var baseKey = Planner.fetchBaseKey(wellknownService); assertThat(baseKey).isNotEmpty(); var simpleKasKey = baseKey.get(); assertThat(simpleKasKey.getKasUri()).isEqualTo("https://example.com/base_key"); @@ -54,9 +53,26 @@ void fetchBaseKeyWithNoBaseKey() { Mockito.when(wellknownService.getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap())) .thenReturn(TestUtil.successfulUnaryCall(response)); - var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setWellknownService(wellknownService).build()); + var baseKey = Planner.fetchBaseKey(wellknownService); + assertThat(baseKey).isEmpty(); + } - var baseKey = planner.fetchBaseKey(); + @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) + .build(); + + Mockito.when(wellknownService.getWellKnownConfigurationBlocking(Mockito.any(), Mockito.anyMap())) + .thenReturn(TestUtil.successfulUnaryCall(response)); + + var baseKey = Planner.fetchBaseKey(wellknownService); assertThat(baseKey).isEmpty(); } @@ -76,7 +92,7 @@ void generatePlanFromProvidedKases() { tdfConfig.kasInfoList.add(kas1); tdfConfig.kasInfoList.add(kas2); - var planner = new Planner(tdfConfig, new FakeServicesBuilder().build()); + var planner = new Planner(tdfConfig, new FakeServicesBuilder().build(), (ignore1, ignored2) -> { throw new IllegalArgumentException("no granter needed"); }); List splitPlan = planner.generatePlanFromProvidedKases(tdfConfig.kasInfoList); assertThat(splitPlan).asList().hasSize(2); @@ -116,7 +132,7 @@ void testFillingInKeysWithAutoConfigure() { var tdfConfig = new Config.TDFConfig(); tdfConfig.autoconfigure = true; tdfConfig.wrappingKeyType = KeyType.RSA2048Key; - var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setKas(kas).build()); + var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setKas(kas).build(), (ignore1, ignored2) -> { throw new IllegalArgumentException("no granter needed"); }); var plan = List.of( new Autoconfigure.KeySplitStep("https://kas1.example.com", "split1", null), new Autoconfigure.KeySplitStep("https://kas2.example.com", "split2", "kid2"), @@ -186,4 +202,55 @@ void returnsEmptyListIfNoKases() { List result = Planner.defaultKases(config); Assertions.assertThat(result).isEmpty(); } + + @Test + void usesProvidedSplitPlanWhenNotAutoconfigure() { + var kas = Mockito.mock(SDK.KAS.class); + Mockito.when(kas.getPublicKey(Mockito.any())).thenAnswer(invocation -> { + Config.KASInfo kasInfo = invocation.getArgument(0, Config.KASInfo.class); + var ret = new Config.KASInfo(); + ret.URL = kasInfo.URL; + if (Objects.equals(kasInfo.URL, "https://kas1.example.com")) { + ret.PublicKey = "pem1"; + ret.Algorithm = "rsa:2048"; + ret.KID = "kid1"; + } else if (Objects.equals(kasInfo.URL, "https://kas2.example.com")) { + ret.PublicKey = "pem2"; + ret.Algorithm = "ec:secp256r1"; + ret.KID = "kid2"; + } else { + throw new IllegalArgumentException("Unexpected KAS URL: " + kasInfo.URL); + } + return ret; + }); + // Arrange + var kas1 = new Config.KASInfo(); + kas1.URL = "https://kas1.example.com"; + kas1.KID = "kid1"; + kas1.Algorithm = "rsa:2048"; + + var kas2 = new Config.KASInfo(); + kas2.URL = "https://kas2.example.com"; + kas2.KID = "kid2"; + kas2.Algorithm = "ec:secp256"; + + var splitStep1 = new Autoconfigure.KeySplitStep(kas1.URL, "split1", kas1.KID); + var splitStep2 = new Autoconfigure.KeySplitStep(kas2.URL, "split2", kas2.KID); + + var tdfConfig = new Config.TDFConfig(); + tdfConfig.autoconfigure = false; + tdfConfig.kasInfoList.add(kas1); + tdfConfig.kasInfoList.add(kas2); + tdfConfig.splitPlan = List.of(splitStep1, splitStep2); + + var planner = new Planner(tdfConfig, new FakeServicesBuilder().setKas(kas).build(), (ignore1, ignored2) -> { throw new IllegalArgumentException("no granter needed"); }); + + // Act + Map> splits = planner.getSplits(tdfConfig); + + // Assert + Assertions.assertThat(splits).hasSize(2); + 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 From 7f5a2d4b0f4343dde04d2204d194ae5af03e9ae6 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 13:04:33 +0200 Subject: [PATCH 21/37] more tests --- .../java/io/opentdf/platform/sdk/Planner.java | 6 ++- .../platform/sdk/AutoconfigureTest.java | 50 ++++++++++++++++--- .../io/opentdf/platform/sdk/PlannerTest.java | 44 ++++++++++------ 3 files changed, 77 insertions(+), 23 deletions(-) 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 3e4d968d..3f25995c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -28,6 +28,7 @@ public class Planner { private static final String BASE_KEY = "base_key"; private final Config.TDFConfig tdfConfig; private final SDK.Services services; + private final BiFunction granterFactory; private static final Logger logger = LoggerFactory.getLogger(Planner.class); @@ -35,6 +36,7 @@ public class Planner { public Planner(Config.TDFConfig config, SDK.Services services, BiFunction granterFactory) { this.tdfConfig = Objects.requireNonNull(config); this.services = Objects.requireNonNull(services); + this.granterFactory = granterFactory; } private static String getUUID() { @@ -60,8 +62,8 @@ Map> getSplits(Config.TDFConfig tdfConfig) { return resolveKeys(splitPlan); } - private static List getAutoconfigurePlan(SDK.Services services, Config.TDFConfig tdfConfig) { - Autoconfigure.Granter granter = Autoconfigure.createGranter(services, tdfConfig); + private List getAutoconfigurePlan(SDK.Services services, Config.TDFConfig tdfConfig) { + Autoconfigure.Granter granter = granterFactory.apply(services, tdfConfig); return granter.getSplits(defaultKases(tdfConfig), Planner::getUUID, () -> Planner.fetchBaseKey(services.wellknown())); } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index 19cee99e..48d178fb 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -35,10 +34,8 @@ import io.opentdf.platform.sdk.Autoconfigure.KeySplitStep; import io.opentdf.platform.sdk.Autoconfigure.Granter; -import org.assertj.core.api.AtomicIntegerArrayAssert; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.platform.commons.JUnitException; import org.mockito.Mockito; import java.util.ArrayList; @@ -49,6 +46,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.regex.Matcher; @@ -1071,9 +1069,6 @@ void createsGranterFromService() { new AttributeValueFQN("https://other.com/attr/specified/value/specked"), new AttributeValueFQN("https://virtru.com/attr/Releasable%20To/value/GBR") ); -// GetAttributeValuesByFqnsRequest request = GetAttributeValuesByFqnsRequest.newBuilder() -// .addAllFqns(policy.stream().map(AttributeValueFQN::toString).collect(Collectors.toList())) -// .build(); when(services.kas()).thenReturn(kas); when(services.attributes()).thenReturn(attributesServiceClient); @@ -1115,4 +1110,47 @@ public void cancel() { // If you want to check the service call, verify it: verify(services).attributes(); } + + @Test + void getSplits_usesAutoconfigurePlan_whenAutoconfigureTrue() { + var tdfConfig = new Config.TDFConfig(); + tdfConfig.autoconfigure = true; + tdfConfig.kasInfoList = new ArrayList<>(); + tdfConfig.splitPlan = null; + + var kas = Mockito.mock(SDK.KAS.class); + Mockito.when(kas.getKeyCache()).thenReturn(new KASKeyCache()); + Config.KASInfo kasInfo = new Config.KASInfo() {{ + URL = "https://kas.example.com"; + Algorithm = "ec:secp256r1"; + KID = "kid"; + }}; + Mockito.when(kas.getPublicKey(any())).thenReturn(kasInfo); + + var services = new FakeServicesBuilder().setKas(kas).build(); + + // Mock granterFactory to return a granter with a known split plan + var expectedSplit = new Autoconfigure.KeySplitStep("https://kas.example.com", "", "kid"); + var granter = Mockito.mock(Autoconfigure.Granter.class); + Mockito.when(granter.getSplits( + Mockito.anyList(), + Mockito.any(), + Mockito.any())) + .thenReturn(List.of(expectedSplit)); + + BiFunction granterFactory = + (s, c) -> granter; + + var planner = new Planner(tdfConfig, services, granterFactory); + + // Act + var splits = planner.getSplits(tdfConfig); + + // Assert + assertThat(splits).containsKey(""); + assertThat(splits.get("")).hasSize(1); + assertThat(splits.get("").get(0).URL).isEqualTo("https://kas.example.com"); + assertThat(splits.get("").get(0).KID).isEqualTo("kid"); + assertThat(splits.get("").get(0).Algorithm).isEqualTo("ec:secp256r1"); + } } 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 1b647240..3bb6a268 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -3,6 +3,7 @@ import com.google.protobuf.Struct; import com.google.protobuf.Value; import io.opentdf.platform.policy.Algorithm; +import io.opentdf.platform.policy.PublicKey; import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse; import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface; import org.assertj.core.api.Assertions; @@ -132,29 +133,42 @@ void testFillingInKeysWithAutoConfigure() { var tdfConfig = new Config.TDFConfig(); tdfConfig.autoconfigure = true; tdfConfig.wrappingKeyType = KeyType.RSA2048Key; - var planner = new Planner(new Config.TDFConfig(), new FakeServicesBuilder().setKas(kas).build(), (ignore1, ignored2) -> { throw new IllegalArgumentException("no granter needed"); }); + tdfConfig.kasInfoList = List.of( + new Config.KASInfo() {{ + URL = "https://kas4.example.com"; + KID = "kid4"; + Algorithm = "ec:secp384r1"; + PublicKey = "pem4"; + }} + ); + var planner = new Planner(tdfConfig, new FakeServicesBuilder().setKas(kas).build(), (ignore1, ignored2) -> { throw new IllegalArgumentException("no granter needed"); }); var plan = List.of( new Autoconfigure.KeySplitStep("https://kas1.example.com", "split1", null), + new Autoconfigure.KeySplitStep("https://kas4.example.com", "split1", "kid4"), new Autoconfigure.KeySplitStep("https://kas2.example.com", "split2", "kid2"), new Autoconfigure.KeySplitStep("https://kas3.example.com", "split2", "kid3") ); Map> filledInPlan = planner.resolveKeys(plan); assertThat(filledInPlan.keySet().stream().collect(Collectors.toList())).asList().containsExactlyInAnyOrder("split1", "split2"); - assertThat(filledInPlan.get("split1")).asList().hasSize(1); - var split1KasInfo = filledInPlan.get("split1").get(0); - assertThat(split1KasInfo.URL).isEqualTo("https://kas1.example.com"); - assertThat(split1KasInfo.KID).isEqualTo("kid1"); - assertThat(split1KasInfo.Algorithm).isEqualTo("rsa:2048"); - assertThat(split1KasInfo.PublicKey).isEqualTo("pem1"); + assertThat(filledInPlan.get("split1")).asList().hasSize(2); + var kasInfo1 = filledInPlan.get("split1").stream().filter(k -> "kid1".equals(k.KID)).findFirst().get(); + assertThat(kasInfo1.URL).isEqualTo("https://kas1.example.com"); + assertThat(kasInfo1.Algorithm).isEqualTo("rsa:2048"); + assertThat(kasInfo1.PublicKey).isEqualTo("pem1"); + var kasInfo4 = filledInPlan.get("split1").stream().filter(k -> "kid4".equals(k.KID)).findFirst().get(); + assertThat(kasInfo4.URL).isEqualTo("https://kas4.example.com"); + assertThat(kasInfo4.Algorithm).isEqualTo("ec:secp384r1"); + assertThat(kasInfo4.PublicKey).isEqualTo("pem4"); + assertThat(filledInPlan.get("split2")).asList().hasSize(2); - var split2KasInfo = filledInPlan.get("split2").stream().filter(kasInfo -> "kid2".equals(kasInfo.KID)).findFirst().get(); - assertThat(split2KasInfo.URL).isEqualTo("https://kas2.example.com"); - assertThat(split2KasInfo.Algorithm).isEqualTo("ec:secp256r1"); - assertThat(split2KasInfo.PublicKey).isEqualTo("pem2"); - var split3KasInfo = filledInPlan.get("split2").stream().filter(kasInfo -> "kid3".equals(kasInfo.KID)).findFirst().get(); - assertThat(split3KasInfo.URL).isEqualTo("https://kas3.example.com"); - assertThat(split3KasInfo.Algorithm).isEqualTo("rsa:4096"); - assertThat(split3KasInfo.PublicKey).isEqualTo("pem3"); + var kasInfo2 = filledInPlan.get("split2").stream().filter(kasInfo -> "kid2".equals(kasInfo.KID)).findFirst().get(); + assertThat(kasInfo2.URL).isEqualTo("https://kas2.example.com"); + assertThat(kasInfo2.Algorithm).isEqualTo("ec:secp256r1"); + assertThat(kasInfo2.PublicKey).isEqualTo("pem2"); + var kasInfo3 = filledInPlan.get("split2").stream().filter(kasInfo -> "kid3".equals(kasInfo.KID)).findFirst().get(); + assertThat(kasInfo3.URL).isEqualTo("https://kas3.example.com"); + assertThat(kasInfo3.Algorithm).isEqualTo("rsa:4096"); + assertThat(kasInfo3.PublicKey).isEqualTo("pem3"); } @Test From 76085d3f7a4075ba1bf2ce830b431d13dea1dfca Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 13:24:12 +0200 Subject: [PATCH 22/37] sonarcloud --- .../opentdf/platform/sdk/Autoconfigure.java | 25 +++---- .../java/io/opentdf/platform/sdk/Planner.java | 1 - .../java/io/opentdf/platform/sdk/TDF.java | 7 +- .../platform/sdk/AutoconfigureTest.java | 66 ++++++++----------- .../io/opentdf/platform/sdk/FakeServices.java | 6 -- 5 files changed, 47 insertions(+), 58 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index f33a0d20..0f504a45 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -61,6 +60,10 @@ class Autoconfigure { private static Logger logger = LoggerFactory.getLogger(Autoconfigure.class); + private Autoconfigure() { + // Prevent instantiation, this class is a utility class that is only used statically + } + static class KeySplitStep { final String kas; final String splitID; @@ -288,7 +291,7 @@ List getPolicy() { return policy; } - boolean addAllGrants(AttributeValueFQN fqn, List granted, List mapped, Attribute attr, KASKeyCache keyCache) { + boolean addAllGrants(AttributeValueFQN fqn, List granted, List mapped, Attribute attr) { boolean foundMappedKey = false; for (var mappedKey: mapped) { foundMappedKey = true; @@ -312,10 +315,11 @@ boolean addAllGrants(AttributeValueFQN fqn, List granted, List< continue; } var cachedGrantKeys = grantedKey.getPublicKey().getCached().getKeysList(); - if (cachedGrantKeys.isEmpty()) { - logger.debug("no keys cached in policy service"); - continue; + + if (logger.isDebugEnabled()) { + logger.debug("found {} keys cached in policy service", cachedGrantKeys.size()); } + for (var cachedGrantKey: cachedGrantKeys) { var mappedKey = new Config.KASInfo(); mappedKey.URL = grantedKey.getUri(); @@ -444,7 +448,6 @@ BooleanKeyExpression insertKeysForAttribute(AttributeBooleanExpression e) throws List kases = grant.kases; if (kases.isEmpty()) { - // TODO: replace this with a reference to the base key kases = List.of(RuleType.EMPTY_TERM); } @@ -469,13 +472,13 @@ BooleanKeyExpression assignKeysTo(AttributeBooleanExpression e) { for (var clause : e.must) { ArrayList keys = new ArrayList<>(); if (clause.values.isEmpty()) { - logger.warn("No values found for attribute: " + clause.def.getFqn()); + logger.warn("No values found for attribute {}", clause.def.getFqn()); continue; } for (var value : clause.values) { var mapped = mappedKeys.get(value.key); if (mapped == null) { - logger.warn("No keys found for attribute value {} ", value); + logger.warn("No keys found for attribute value {}", value); continue; } for (var kasInfo : mapped) { @@ -874,16 +877,16 @@ private static Granter getGranter(KASKeyCache keyCache, List fetchBaseKey(WellKnownServiceClientInterface wellk 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); } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index a5e3cb2c..cfe75d26 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -150,7 +150,9 @@ private void prepareManifest(Config.TDFConfig tdfConfig, Map symKeys = new ArrayList<>(splits.size()); - for (String splitID : splits.keySet()) { + for (var split : splits.entrySet()) { + String splitID = split.getKey(); + // Symmetric key byte[] symKey = new byte[GCM_KEY_SIZE]; sRandom.nextBytes(symKey); @@ -177,7 +179,8 @@ private void prepareManifest(Config.TDFConfig tdfConfig, Map kasInfos = split.getValue(); + for (Config.KASInfo kasInfo : kasInfos) { if (kasInfo.PublicKey == null || kasInfo.PublicKey.isEmpty()) { throw new SDK.KasPublicKeyMissing("Kas public key is missing in kas information list"); } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index 48d178fb..5ebe4041 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -1,16 +1,5 @@ package io.opentdf.platform.sdk; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.connectrpc.ResponseMessage; import com.connectrpc.UnaryBlockingCall; import io.opentdf.platform.policy.Algorithm; @@ -29,11 +18,10 @@ import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsRequest; import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse; import io.opentdf.platform.sdk.Autoconfigure.AttributeValueFQN; +import io.opentdf.platform.sdk.Autoconfigure.Granter; import io.opentdf.platform.sdk.Autoconfigure.Granter.AttributeBooleanExpression; import io.opentdf.platform.sdk.Autoconfigure.Granter.BooleanKeyExpression; import io.opentdf.platform.sdk.Autoconfigure.KeySplitStep; -import io.opentdf.platform.sdk.Autoconfigure.Granter; - import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -48,9 +36,20 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class AutoconfigureTest { @@ -514,7 +513,7 @@ public void testReasonerConstructAttributeBoolean() { var wrapper = new Object() { int i = 0; }; - List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), () -> Optional.empty()); + List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), Optional::empty); assertThat(plan) .as(tc.name) .isEqualTo(tc.getPlan()); @@ -525,7 +524,7 @@ public void testReasonerConstructAttributeBoolean() { void testUsingAttributeMappedAtNamespace() { Granter granter = Autoconfigure.newGranterFromAttributes(new KASKeyCache(), mockValueFor(mp2uns2uns)); var counter = new AtomicInteger(0); - var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), () -> Optional.empty()); + var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), Optional::empty); assertThat(splitPlan).isEqualTo(List.of(new KeySplitStep("https://mapped.example.com", "", NAMESPACE_KAS_KEY.getPublicKey().getKid()))); } @@ -534,7 +533,7 @@ void testUsingAttributeMappedAtMultiplePlaces() { var attributes = new Value[]{mockValueFor(mp2uns2uns), mockValueFor(mp2uns2mp)}; Granter granter = Autoconfigure.newGranterFromAttributes(new KASKeyCache(), attributes); var counter = new AtomicInteger(0); - var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), () -> Optional.empty()); + var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), Optional::empty); assertThat(splitPlan).isEqualTo(List.of( new KeySplitStep(NAMESPACE_KAS_KEY.getKasUri(), "0", NAMESPACE_KAS_KEY.getPublicKey().getKid()), new KeySplitStep(VALUE_KEY.getKasUri(), "0", VALUE_KEY.getPublicKey().getKid()) @@ -1075,27 +1074,18 @@ void createsGranterFromService() { // Mock the attribute service to return a response with the expected values when(attributesServiceClient.getAttributeValuesByFqnsBlocking(any(), any())).thenAnswer(invocation -> { - return new UnaryBlockingCall() { - @Override - public ResponseMessage execute() { - GetAttributeValuesByFqnsResponse.Builder builder = GetAttributeValuesByFqnsResponse.newBuilder(); - for (AttributeValueFQN fqn : policy) { - Value value = Value.newBuilder() - .setId(fqn.toString()) - .setFqn(fqn.toString()) - .build(); - builder.putFqnAttributeValues(fqn.toString(), - GetAttributeValuesByFqnsResponse.AttributeAndValue.newBuilder() - .setValue(value) - .build()); - } - return new ResponseMessage.Success<>(builder.build(), Collections.emptyMap(), Collections.emptyMap()); - } - - @Override - public void cancel() { - } - }; + GetAttributeValuesByFqnsResponse.Builder builder = GetAttributeValuesByFqnsResponse.newBuilder(); + for (AttributeValueFQN fqn : policy) { + Value value = Value.newBuilder() + .setId(fqn.toString()) + .setFqn(fqn.toString()) + .build(); + builder.putFqnAttributeValues(fqn.toString(), + GetAttributeValuesByFqnsResponse.AttributeAndValue.newBuilder() + .setValue(value) + .build()); + } + return TestUtil.successfulUnaryCall(builder.build()); }); // Act diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/FakeServices.java b/sdk/src/test/java/io/opentdf/platform/sdk/FakeServices.java index 29397915..2851b22b 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/FakeServices.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/FakeServices.java @@ -1,16 +1,10 @@ package io.opentdf.platform.sdk; -import io.opentdf.platform.authorization.AuthorizationServiceClient; import io.opentdf.platform.authorization.AuthorizationServiceClientInterface; -import io.opentdf.platform.policy.attributes.AttributesServiceClient; import io.opentdf.platform.policy.attributes.AttributesServiceClientInterface; -import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient; import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClientInterface; -import io.opentdf.platform.policy.namespaces.NamespaceServiceClient; import io.opentdf.platform.policy.namespaces.NamespaceServiceClientInterface; -import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceClient; import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceClientInterface; -import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceClient; import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceClientInterface; import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface; From d34056e79588d478949091e6a15c4fe2b48b7057 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 13:35:16 +0200 Subject: [PATCH 23/37] more sonar --- .../main/java/io/opentdf/platform/sdk/Config.java | 1 - .../io/opentdf/platform/sdk/AutoconfigureTest.java | 12 ++++++------ .../test/java/io/opentdf/platform/sdk/TestUtil.java | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index 52890484..34e4fa27 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -1,6 +1,5 @@ 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; diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index 5ebe4041..d37f2727 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -662,7 +662,7 @@ public void cancel() { var wrapper = new Object() { int i = 0; }; - List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), () -> Optional.empty()); + List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), Optional::empty); assertThat(plan) .as(tc.name) .hasSameElementsAs(tc.getPlan()); @@ -1024,11 +1024,11 @@ void testUsingDefaultKasesWhenNothingElseProvided() { splitGen, Optional::empty); - assertThat(splits).hasSize(2); - assertThat(splits).asList() - .containsExactly( - new KeySplitStep("https://example.org/kas1", "0", null), - new KeySplitStep("https://example.org/kas2", "1", null) + assertThat(splits) + .hasSize(2) + .asList().containsExactly( + new KeySplitStep("https://example.org/kas1", "0", null), + new KeySplitStep("https://example.org/kas2", "1", null) ); } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TestUtil.java b/sdk/src/test/java/io/opentdf/platform/sdk/TestUtil.java index 3a369aef..53076007 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TestUtil.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TestUtil.java @@ -15,7 +15,7 @@ public ResponseMessage execute() { @Override public void cancel() { - + // in tests we don't need to preserve server resources, so no-op } }; } From dfc3638994f29c991dce5788ee189e2378250767 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 13:39:16 +0200 Subject: [PATCH 24/37] one more tiny one --- .../main/java/io/opentdf/platform/sdk/Autoconfigure.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 0f504a45..298eb439 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -868,7 +868,13 @@ static Autoconfigure.Granter createGranter(SDK.Services services, Config.TDFConf } private static Granter getGranter(KASKeyCache keyCache, List values) { - Granter grants = new Granter(values.stream().map(GetAttributeValuesByFqnsResponse.AttributeAndValue::getValue).map(Value::getFqn).map(AttributeValueFQN::new).collect(Collectors.toList())); + List attributeValues = values.stream() + .map(GetAttributeValuesByFqnsResponse.AttributeAndValue::getValue) + .map(Value::getFqn) + .map(AttributeValueFQN::new) + .collect(Collectors.toList()); + + Granter grants = new Granter(attributeValues); for (var attributeAndValue: values) { String fqnstr = attributeAndValue.getValue().getFqn(); AttributeValueFQN fqn = new AttributeValueFQN(fqnstr); From 93910dac5890d162dcd58c1a9718d673eaad4ff1 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 13:41:14 +0200 Subject: [PATCH 25/37] more sonar --- sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java | 1 - 1 file changed, 1 deletion(-) 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 3bb6a268..117db1a7 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -3,7 +3,6 @@ import com.google.protobuf.Struct; import com.google.protobuf.Value; import io.opentdf.platform.policy.Algorithm; -import io.opentdf.platform.policy.PublicKey; import io.opentdf.platform.wellknownconfiguration.GetWellKnownConfigurationResponse; import io.opentdf.platform.wellknownconfiguration.WellKnownServiceClientInterface; import org.assertj.core.api.Assertions; From f94c1dfab2f3a0b699c13e3fc18347f32dd48e7d Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 14:42:43 +0200 Subject: [PATCH 26/37] gemini suggestions --- .../main/java/io/opentdf/platform/sdk/Autoconfigure.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 298eb439..e6b1ad37 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -254,9 +254,9 @@ String name() { throw new RuntimeException("invalid attributeInstance"); } try { - return URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8); - } catch (IllegalArgumentException e) { - throw new RuntimeException("invalid attributeInstance", e); + return URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("illegal attribute instance", e); } } } @@ -867,7 +867,7 @@ static Autoconfigure.Granter createGranter(SDK.Services services, Config.TDFConf return granter; } - private static Granter getGranter(KASKeyCache keyCache, List values) { + private static Granter getGranter(@Nullable KASKeyCache keyCache, List values) { List attributeValues = values.stream() .map(GetAttributeValuesByFqnsResponse.AttributeAndValue::getValue) .map(Value::getFqn) From 4e6be4669a213b85f012f2639ea0609bdf508fc5 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 15:18:18 +0200 Subject: [PATCH 27/37] more gemini --- sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index e6b1ad37..9fc5694f 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -255,7 +255,7 @@ String name() { } try { return URLDecoder.decode(matcher.group(1), StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { + } catch (UnsupportedEncodingException | IllegalArgumentException e) { throw new RuntimeException("illegal attribute instance", e); } } @@ -382,7 +382,7 @@ List plan(Supplier genSplitID) k = k.reduce(); int l = k.size(); if (l == 0) { - throw new IllegalStateException("generated an empty plan"); + throw new AutoConfigureException("generated an empty plan"); } List steps = new ArrayList<>(); From f1a3244eb2381c5e36aad2ade9c30c7d36f2033d Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 3 Jul 2025 15:30:20 +0200 Subject: [PATCH 28/37] even more (incorrect) gemini --- sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 9fc5694f..90e8b92e 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -891,8 +891,9 @@ private static Granter getGranter(@Nullable KASKeyCache keyCache, List Date: Mon, 21 Jul 2025 22:29:01 +0200 Subject: [PATCH 29/37] clarify --- sdk/src/main/java/io/opentdf/platform/sdk/Planner.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 a40a1ddd..a326a535 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -161,9 +161,9 @@ Map> resolveKeys(List s getKI.URL = splitInfo.kas; getKI.Algorithm = tdfConfig.wrappingKeyType.toString(); getKI.KID = splitInfo.kid; - getKI = services.kas().getPublicKey(getKI); - latestKASInfo.put(splitInfo.kas, getKI); - ki = getKI; + var retrievedKI = services.kas().getPublicKey(getKI); + latestKASInfo.put(splitInfo.kas, retrievedKI); + ki = retrievedKI; } conjunction.computeIfAbsent(splitInfo.splitID, s -> new ArrayList<>()).add(ki); } From f8aff1b69995823a8fc49421da0719a08f49738a Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 19:55:51 +0200 Subject: [PATCH 30/37] thread the algorithm through --- .../opentdf/platform/sdk/Autoconfigure.java | 149 +++++++++--------- .../java/io/opentdf/platform/sdk/Config.java | 11 +- .../java/io/opentdf/platform/sdk/KeyType.java | 53 +++++++ .../java/io/opentdf/platform/sdk/Planner.java | 34 ++-- .../java/io/opentdf/platform/sdk/TDF.java | 6 +- .../platform/sdk/AutoconfigureTest.java | 84 +++++----- .../io/opentdf/platform/sdk/PlannerTest.java | 24 +-- .../opentdf/platform/sdk/ZipReaderTest.java | 2 + 8 files changed, 222 insertions(+), 141 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index 90e8b92e..c7622c84 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -56,7 +56,7 @@ class RuleType { * This class includes functionality to create granter instances based on * attributes either from a list of attribute values or from a service. */ -class Autoconfigure { +public class Autoconfigure { private static Logger logger = LoggerFactory.getLogger(Autoconfigure.class); @@ -64,31 +64,61 @@ private Autoconfigure() { // Prevent instantiation, this class is a utility class that is only used statically } - static class KeySplitStep { + static class KeySplitTemplate { final String kas; final String splitID; final String kid; + final KeyType keyType; - KeySplitStep(String kas, String splitId) { - this(kas, splitId, null); + @Override + public String toString() { + return "KeySplitTemplate{" + + "kas='" + kas + '\'' + + ", splitID='" + splitID + '\'' + + ", kid='" + kid + '\'' + + ", keyType=" + keyType + + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + KeySplitTemplate that = (KeySplitTemplate) o; + return Objects.equals(kas, that.kas) && Objects.equals(splitID, that.splitID) && Objects.equals(kid, that.kid) && keyType == that.keyType; } - KeySplitStep(String kas, String splitId, @Nullable String kid) { + @Override + public int hashCode() { + return Objects.hash(kas, splitID, kid, keyType); + } + + public KeySplitTemplate(String kas, String splitID, String kid, KeyType keyType) { + this.kas = kas; + this.splitID = splitID; + this.kid = kid; + this.keyType = keyType; + } + } + + public static class KeySplitStep { + final String kas; + final String splitID; + + KeySplitStep(String kas, String splitId) { this.kas = Objects.requireNonNull(kas); this.splitID = Objects.requireNonNull(splitId); - this.kid = kid; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; KeySplitStep that = (KeySplitStep) o; - return Objects.equals(kas, that.kas) && Objects.equals(splitID, that.splitID) && Objects.equals(kid, that.kid); + return Objects.equals(kas, that.kas) && Objects.equals(splitID, that.splitID); } @Override public int hashCode() { - return Objects.hash(kas, splitID, kid); + return Objects.hash(kas, splitID); } @Override @@ -96,7 +126,6 @@ public String toString() { return "KeySplitStep{" + "kas='" + kas + '\'' + ", splitID='" + splitID + '\'' + - ", kid='" + kid + '\'' + '}'; } } @@ -324,7 +353,7 @@ boolean addAllGrants(AttributeValueFQN fqn, List granted, List< var mappedKey = new Config.KASInfo(); mappedKey.URL = grantedKey.getUri(); mappedKey.KID = cachedGrantKey.getKid(); - mappedKey.Algorithm = Autoconfigure.algProto2String(cachedGrantKey.getAlg()); + mappedKey.Algorithm = KeyType.fromAlgorithm(cachedGrantKey.getAlg()).toString(); mappedKey.PublicKey = cachedGrantKey.getPem(); mappedKey.Default = false; mappedKeys.computeIfAbsent(fqn.key, k -> new ArrayList<>()).add(mappedKey); @@ -345,7 +374,7 @@ KeyAccessGrant byAttribute(AttributeValueFQN fqn) { return grants.get(fqn.key); } - List getSplits(List defaultKases, Supplier genSplitID, Supplier> baseKeySupplier) throws AutoConfigureException { + List getSplits(List defaultKases, Supplier genSplitID, Supplier> baseKeySupplier) throws AutoConfigureException { if (hasMappedKeys) { logger.debug("generating plan from mapped keys"); return planFromAttributes(genSplitID); @@ -361,7 +390,8 @@ List getSplits(List defaultKases, Supplier genSpli String kas = key.getKasUri(); String splitID = ""; String kid = key.getPublicKey().getKid(); - return Collections.singletonList(new KeySplitStep(kas, splitID, kid)); + Algorithm algorithm = key.getPublicKey().getAlgorithm(); + return Collections.singletonList(new KeySplitTemplate(kas, splitID, kid, KeyType.fromAlgorithm(algorithm))); } logger.warn("no grants or mapped keys found, generating plan from default KASes. this is deprecated"); @@ -371,7 +401,7 @@ List getSplits(List defaultKases, Supplier genSpli } @Nonnull - List plan(Supplier genSplitID) + List plan(Supplier genSplitID) throws AutoConfigureException { AttributeBooleanExpression b = constructAttributeBoolean(); BooleanKeyExpression k = insertKeysForAttribute(b); @@ -385,18 +415,18 @@ List plan(Supplier genSplitID) throw new AutoConfigureException("generated an empty plan"); } - List steps = new ArrayList<>(); + List steps = new ArrayList<>(); for (KeyClause v : k.values) { String splitID = (l > 1) ? genSplitID.get() : ""; for (PublicKeyInfo o : v.values) { - steps.add(new KeySplitStep(o.kas, splitID)); + steps.add(new KeySplitTemplate(o.kas, splitID, o.kid, null)); } } return steps; } @Nonnull - List planFromAttributes(Supplier genSplitID) + List planFromAttributes(Supplier genSplitID) throws AutoConfigureException { AttributeBooleanExpression b = constructAttributeBoolean(); BooleanKeyExpression k = assignKeysTo(b); @@ -410,25 +440,25 @@ List planFromAttributes(Supplier genSplitID) return Collections.emptyList(); } - List steps = new ArrayList<>(); + List steps = new ArrayList<>(); for (KeyClause v : k.values) { String splitID = (l > 1) ? genSplitID.get() : ""; for (PublicKeyInfo o : v.values) { - steps.add(new KeySplitStep(o.kas, splitID, o.kid)); + steps.add(new KeySplitTemplate(o.kas, splitID, o.kid, o.algorithm != null ? KeyType.fromString(o.algorithm) : null)); } } return steps; } - static List generatePlanFromDefaultKases(List defaultKas, Supplier genSplitID) { + static List generatePlanFromDefaultKases(List defaultKas, Supplier genSplitID) { if (defaultKas.isEmpty()) { throw new AutoConfigureException("no default KAS specified; required for grantless plans"); } else if (defaultKas.size() == 1) { - return Collections.singletonList(new KeySplitStep(defaultKas.get(0), "")); + return Collections.singletonList(new KeySplitTemplate(defaultKas.get(0), "", null, null)); } else { - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (String kas : defaultKas) { - result.add(new KeySplitStep(kas, genSplitID.get())); + result.add(new KeySplitTemplate(kas, genSplitID.get(), null, null)); } return result; } @@ -486,7 +516,7 @@ BooleanKeyExpression assignKeysTo(AttributeBooleanExpression e) { logger.warn("No KAS URL found for attribute value {}", value); continue; } - keys.add(new PublicKeyInfo(kasInfo.URL, kasInfo.KID)); + keys.add(new PublicKeyInfo(kasInfo.URL, kasInfo.KID, kasInfo.Algorithm)); } } @@ -582,20 +612,21 @@ public String toString() { } return sb.toString(); } - } static class PublicKeyInfo implements Comparable { final String kas; final String kid; + final String algorithm; PublicKeyInfo(String kas) { - this(kas, null); + this(kas, null, null); } - PublicKeyInfo(String kas, String kid) { - this.kas = kas; + PublicKeyInfo(String kas, String kid, String algorithm) { + this.kas = Objects.requireNonNull(kas); this.kid = kid; + this.algorithm = algorithm; } String getKas() { @@ -606,38 +637,34 @@ String getKas() { public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; PublicKeyInfo that = (PublicKeyInfo) o; - return Objects.equals(kas, that.kas) && Objects.equals(kid, that.kid); + return Objects.equals(kas, that.kas) && Objects.equals(kid, that.kid) && Objects.equals(algorithm, that.algorithm); } @Override public int hashCode() { - return Objects.hash(kas, kid); - } - - @Override - public String toString() { - return "PublicKeyInfo{" + - "kas='" + kas + '\'' + - ", kid='" + kid + '\'' + - '}'; + return Objects.hash(kas, kid, algorithm); } @Override public int compareTo(PublicKeyInfo o) { - if (this.kas.equals(o.kas)) { - if (this.kid == null && o.kid == null) { - return 0; - } - if (this.kid == null) { - return -1; - } - if (o.kid == null) { - return 1; - } - return this.kid.compareTo(o.kid); - } else { + if (this.kas.compareTo(o.kas) != 0) { return this.kas.compareTo(o.kas); } + if ((this.kid == null) != (o.kid == null)) { + return this.kid == null ? -1 : 1; + } + if (this.kid != null) { + if (this.kid.compareTo(o.kid) != 0) { + return this.kid.compareTo(o.kid); + } + } + if ((this.algorithm == null) != (o.algorithm == null)) { + return this.algorithm == null ? -1 : 1; + } + if (this.algorithm != null) { + return this.algorithm.compareTo(o.algorithm); + } + return 0; } } @@ -908,28 +935,4 @@ static void storeKeysToCache(List kases, List kas } kasKeys.stream().map(Config.KASInfo::fromSimpleKasKey).forEach(keyCache::store); } - - static String algProto2String(Algorithm e) { - switch (e) { - case ALGORITHM_EC_P521: - return "ec:p521"; - case ALGORITHM_RSA_2048: - return "rsa:2048"; - default: - throw new IllegalArgumentException("Unknown algorithm: " + e); - } - } - - static String algProto2String(KasPublicKeyAlgEnum e) { - switch (e) { - case KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1: - return "ec:secp256r1"; - case KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048: - return "rsa:2048"; - case KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED: - default: - return ""; - } - } - } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index 34e4fa27..816e3ad1 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -14,8 +14,6 @@ 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. * Contains nested classes and enums for specific configuration settings. @@ -92,7 +90,7 @@ public static List fromKeyAccessServer(KeyAccessServer kas) { Config.KASInfo kasInfo = new Config.KASInfo(); kasInfo.URL = kas.getUri(); kasInfo.KID = ki.getKid(); - kasInfo.Algorithm = algProto2String(ki.getAlg()); + kasInfo.Algorithm = KeyType.fromAlgorithm(ki.getAlg()).toString(); kasInfo.PublicKey = ki.getPem(); return Stream.of(kasInfo); }).collect(Collectors.toList()); @@ -102,7 +100,7 @@ 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.Algorithm = KeyType.fromAlgorithm(ki.getPublicKey().getAlgorithm()).toString(); kasInfo.PublicKey = ki.getPublicKey().getPem(); return kasInfo; @@ -275,6 +273,11 @@ public static Consumer withKasInformation(KASInfo... kasInfoList) { }; } + /** + * Deprecated since 9.1.0, will be removed. To produce key shares use + * the key mapping feature + */ + @Deprecated(since = "9.1.0", forRemoval = true) public static Consumer withSplitPlan(Autoconfigure.KeySplitStep... p) { return (TDFConfig config) -> { config.splitPlan = new ArrayList<>(Arrays.asList(p)); 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 0f9cbd3d..5251f11c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java @@ -1,7 +1,13 @@ package io.opentdf.platform.sdk; +import io.opentdf.platform.policy.Algorithm; +import io.opentdf.platform.policy.KasPublicKeyAlgEnum; + import javax.annotation.Nonnull; +import java.util.Optional; + +import static io.opentdf.platform.policy.Algorithm.ALGORITHM_EC_P521; import static io.opentdf.platform.sdk.NanoTDFType.ECCurve.SECP256R1; import static io.opentdf.platform.sdk.NanoTDFType.ECCurve.SECP384R1; import static io.opentdf.platform.sdk.NanoTDFType.ECCurve.SECP521R1; @@ -46,6 +52,53 @@ public static KeyType fromString(String keyType) { throw new IllegalArgumentException("No enum constant for key type: " + keyType); } + 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_EC_P256: + return KeyType.EC256Key; + case ALGORITHM_EC_P384: + return KeyType.EC384Key; + case ALGORITHM_EC_P521: + return KeyType.EC521Key; + default: + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } + + public static KeyType fromAlgorithm(KasPublicKeyAlgEnum algorithm) { + if (algorithm == null) { + throw new IllegalArgumentException("Algorithm cannot be null"); + } + switch (algorithm) { + case KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048: + return KeyType.RSA2048Key; + case KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1: + return KeyType.EC256Key; + case KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1: + return KeyType.EC384Key; + case KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1: + return KeyType.EC521Key; + default: + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + } + } + + public static KeyType getKeyTypeToUse(String keyType, Config.TDFConfig config) { + if (keyType == null || keyType.isEmpty()) { + return config.wrappingKeyType != null ? config.wrappingKeyType : KeyType.RSA2048Key; + } + try { + return KeyType.fromString(keyType); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid key type: " + keyType, e); + } + } + public boolean isEc() { return this.curve != null; } 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 a326a535..f278bada 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.UUID; import java.util.function.BiFunction; +import java.util.stream.Collectors; public class Planner { @@ -33,7 +34,7 @@ public class Planner { private static final Logger logger = LoggerFactory.getLogger(Planner.class); - public Planner(Config.TDFConfig config, SDK.Services services, BiFunction granterFactory) { + Planner(Config.TDFConfig config, SDK.Services services, BiFunction granterFactory) { this.tdfConfig = Objects.requireNonNull(config); this.services = Objects.requireNonNull(services); this.granterFactory = granterFactory; @@ -44,7 +45,7 @@ private static String getUUID() { } Map> getSplits(Config.TDFConfig tdfConfig) { - List splitPlan; + List 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"); @@ -53,7 +54,9 @@ Map> getSplits(Config.TDFConfig tdfConfig) { } else if (tdfConfig.splitPlan == null || tdfConfig.splitPlan.isEmpty()) { splitPlan = generatePlanFromProvidedKases(tdfConfig.kasInfoList); } else { - splitPlan = tdfConfig.splitPlan; + splitPlan = tdfConfig.splitPlan.stream() + .map(k -> new Autoconfigure.KeySplitTemplate(k.kas, k.splitID, null, null)) + .collect(Collectors.toList()); } if (tdfConfig.kasInfoList.isEmpty() && splitPlan.isEmpty()) { @@ -62,20 +65,21 @@ Map> getSplits(Config.TDFConfig tdfConfig) { return resolveKeys(splitPlan); } - private List getAutoconfigurePlan(SDK.Services services, Config.TDFConfig tdfConfig) { + private List getAutoconfigurePlan(SDK.Services services, Config.TDFConfig tdfConfig) { Autoconfigure.Granter granter = granterFactory.apply(services, tdfConfig); return granter.getSplits(defaultKases(tdfConfig), Planner::getUUID, () -> Planner.fetchBaseKey(services.wellknown())); } - List generatePlanFromProvidedKases(List kases) { + List generatePlanFromProvidedKases(List kases) { if (kases.size() == 1) { var kasInfo = kases.get(0); - return Collections.singletonList(new Autoconfigure.KeySplitStep(kasInfo.URL, "", kasInfo.KID)); + return Collections.singletonList(new Autoconfigure.KeySplitTemplate(kasInfo.URL, "", kasInfo.KID, null)); } - List splitPlan = new ArrayList<>(); + List splitPlan = new ArrayList<>(); for (var kasInfo : kases) { - splitPlan.add(new Autoconfigure.KeySplitStep(kasInfo.URL, getUUID(), kasInfo.KID)); + var keyType = kasInfo.Algorithm == null ? null : KeyType.fromString(kasInfo.Algorithm); + splitPlan.add(new Autoconfigure.KeySplitTemplate(kasInfo.URL, getUUID(), kasInfo.KID, keyType)); } return splitPlan; } @@ -141,7 +145,7 @@ private static class Key { } } - Map> resolveKeys(List splitPlan) { + Map> resolveKeys(List splitPlan) { Map> conjunction = new HashMap<>(); var latestKASInfo = new HashMap(); // Seed anything passed in manually @@ -151,7 +155,7 @@ Map> resolveKeys(List s } } - for (Autoconfigure.KeySplitStep splitInfo: splitPlan) { + for (var 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); @@ -159,11 +163,11 @@ Map> resolveKeys(List s logger.info("no public key provided for KAS at {}, retrieving", splitInfo.kas); var getKI = new Config.KASInfo(); getKI.URL = splitInfo.kas; - getKI.Algorithm = tdfConfig.wrappingKeyType.toString(); - getKI.KID = splitInfo.kid; - var retrievedKI = services.kas().getPublicKey(getKI); - latestKASInfo.put(splitInfo.kas, retrievedKI); - ki = retrievedKI; + getKI.Algorithm = splitInfo.keyType == null + ? (tdfConfig.wrappingKeyType == null ? null : tdfConfig.wrappingKeyType.toString()) + : splitInfo.keyType.toString(); + ki = services.kas().getPublicKey(getKI); + latestKASInfo.put(splitInfo.kas, ki); } conjunction.computeIfAbsent(splitInfo.splitID, s -> new ArrayList<>()).add(ki); } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 197761e6..60f8a5e8 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -213,7 +213,11 @@ private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KA keyAccess.sid = splitID; keyAccess.schemaVersion = KEY_ACCESS_SCHEMA_VERSION; - if (tdfConfig.wrappingKeyType.isEc()) { + var algorithm = kasInfo.Algorithm == null || kasInfo.Algorithm.isEmpty() + ? tdfConfig.wrappingKeyType.toString() + : kasInfo.Algorithm; + + if (KeyType.fromString(algorithm).isEc()) { var ecKeyWrappedKeyInfo = createECWrappedKey(tdfConfig, kasInfo, symKey); keyAccess.wrappedKey = ecKeyWrappedKeyInfo.wrappedKey; keyAccess.ephemeralPublicKey = ecKeyWrappedKeyInfo.publicKey; diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index d37f2727..7cf20bdf 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -442,7 +442,7 @@ public void testReasonerConstructAttributeBoolean() { "https://virtru.com/attr/Classification/value/Secret&https://virtru.com/attr/Releasable%20To/value/CAN", "[DEFAULT]&(https://kas.ca/)", "(https://kas.ca/)", - List.of(new KeySplitStep(KAS_CA, ""))), + List.of(new Autoconfigure.KeySplitTemplate(KAS_CA, "", null, null))), new ReasonerTestCase( "one defaulted attr", List.of(clsS), @@ -450,7 +450,7 @@ public void testReasonerConstructAttributeBoolean() { "https://virtru.com/attr/Classification/value/Secret", "[DEFAULT]", "", - List.of(new KeySplitStep(KAS_US, ""))), + List.of(new Autoconfigure.KeySplitTemplate(KAS_US, "", null, null))), new ReasonerTestCase( "empty policy", List.of(), @@ -458,7 +458,7 @@ public void testReasonerConstructAttributeBoolean() { "∅", "", "", - List.of(new KeySplitStep(KAS_US, ""))), + List.of(new Autoconfigure.KeySplitTemplate(KAS_US, "", null, null))), new ReasonerTestCase( "old school splits", List.of(), @@ -466,8 +466,9 @@ public void testReasonerConstructAttributeBoolean() { "∅", "", "", - List.of(new KeySplitStep(KAS_AU, "1"), new KeySplitStep(KAS_CA, "2"), - new KeySplitStep(KAS_US, "3"))), + List.of(new Autoconfigure.KeySplitTemplate(KAS_AU, "1", null, null), + new Autoconfigure.KeySplitTemplate(KAS_CA, "2", null, null), + new Autoconfigure.KeySplitTemplate(KAS_US, "3", null, null))), new ReasonerTestCase( "simple with all three ops", List.of(clsS, rel2gbr, n2kInt), @@ -475,7 +476,7 @@ public void testReasonerConstructAttributeBoolean() { "https://virtru.com/attr/Classification/value/Secret&https://virtru.com/attr/Releasable%20To/value/GBR&https://virtru.com/attr/Need%20to%20Know/value/INT", "[DEFAULT]&(https://kas.uk/)&(https://kas.uk/)", "(https://kas.uk/)", - List.of(new KeySplitStep(KAS_UK, ""))), + List.of(new Autoconfigure.KeySplitTemplate(KAS_UK, "", null, null))), new ReasonerTestCase( "compartments", List.of(clsS, rel2gbr, rel2usa, n2kHCS, n2kSI), @@ -483,8 +484,10 @@ public void testReasonerConstructAttributeBoolean() { "https://virtru.com/attr/Classification/value/Secret&https://virtru.com/attr/Releasable%20To/value/{GBR,USA}&https://virtru.com/attr/Need%20to%20Know/value/{HCS,SI}", "[DEFAULT]&(https://kas.uk/⋁https://kas.us/)&(https://hcs.kas.us/⋀https://si.kas.us/)", "(https://kas.uk/⋁https://kas.us/)&(https://hcs.kas.us/)&(https://si.kas.us/)", - List.of(new KeySplitStep(KAS_UK, "1"), new KeySplitStep(KAS_US, "1"), - new KeySplitStep(KAS_US_HCS, "2"), new KeySplitStep(KAS_US_SA, "3"))), + List.of(new Autoconfigure.KeySplitTemplate(KAS_UK, "1", null, null), + new Autoconfigure.KeySplitTemplate(KAS_US, "1", null, null), + new Autoconfigure.KeySplitTemplate(KAS_US_HCS, "2", null, null), + new Autoconfigure.KeySplitTemplate(KAS_US_SA, "3", null, null))), new ReasonerTestCase( "compartments - case insensitive", List.of( @@ -493,8 +496,10 @@ public void testReasonerConstructAttributeBoolean() { "https://virtru.com/attr/Classification/value/Secret&https://virtru.com/attr/Releasable%20To/value/{GBR,USA}&https://virtru.com/attr/Need%20to%20Know/value/{HCS,SI}", "[DEFAULT]&(https://kas.uk/⋁https://kas.us/)&(https://hcs.kas.us/⋀https://si.kas.us/)", "(https://kas.uk/⋁https://kas.us/)&(https://hcs.kas.us/)&(https://si.kas.us/)", - List.of(new KeySplitStep(KAS_UK, "1"), new KeySplitStep(KAS_US, "1"), - new KeySplitStep(KAS_US_HCS, "2"), new KeySplitStep(KAS_US_SA, "3")))); + List.of(new Autoconfigure.KeySplitTemplate(KAS_UK, "1", null, null), + new Autoconfigure.KeySplitTemplate(KAS_US, "1", null, null), + new Autoconfigure.KeySplitTemplate(KAS_US_HCS, "2", null, null), + new Autoconfigure.KeySplitTemplate(KAS_US_SA, "3", null, null)))); for (ReasonerTestCase tc : testCases) { Granter reasoner = Autoconfigure.newGranterFromAttributes(null, @@ -513,7 +518,7 @@ public void testReasonerConstructAttributeBoolean() { var wrapper = new Object() { int i = 0; }; - List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), Optional::empty); + List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), Optional::empty); assertThat(plan) .as(tc.name) .isEqualTo(tc.getPlan()); @@ -525,7 +530,7 @@ void testUsingAttributeMappedAtNamespace() { Granter granter = Autoconfigure.newGranterFromAttributes(new KASKeyCache(), mockValueFor(mp2uns2uns)); var counter = new AtomicInteger(0); var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), Optional::empty); - assertThat(splitPlan).isEqualTo(List.of(new KeySplitStep("https://mapped.example.com", "", NAMESPACE_KAS_KEY.getPublicKey().getKid()))); + assertThat(splitPlan).isEqualTo(List.of(new Autoconfigure.KeySplitTemplate("https://mapped.example.com", "", NAMESPACE_KAS_KEY.getPublicKey().getKid(), KeyType.EC521Key))); } @Test @@ -535,8 +540,8 @@ void testUsingAttributeMappedAtMultiplePlaces() { var counter = new AtomicInteger(0); var splitPlan = granter.getSplits(Collections.emptyList(), () -> Integer.toString(counter.getAndIncrement()), Optional::empty); assertThat(splitPlan).isEqualTo(List.of( - new KeySplitStep(NAMESPACE_KAS_KEY.getKasUri(), "0", NAMESPACE_KAS_KEY.getPublicKey().getKid()), - new KeySplitStep(VALUE_KEY.getKasUri(), "0", VALUE_KEY.getPublicKey().getKid()) + new Autoconfigure.KeySplitTemplate(NAMESPACE_KAS_KEY.getKasUri(), "0", NAMESPACE_KAS_KEY.getPublicKey().getKid(), KeyType.EC521Key), + new Autoconfigure.KeySplitTemplate(VALUE_KEY.getKasUri(), "0", VALUE_KEY.getPublicKey().getKid(), KeyType.EC521Key) )); } @@ -569,72 +574,77 @@ public void testReasonerSpecificity() { "uns.uns => default", List.of(uns2uns), List.of(KAS_US), - List.of(new KeySplitStep(KAS_US, ""))), + List.of(new Autoconfigure.KeySplitTemplate(KAS_US, "", null, null))), new ReasonerTestCase( "uns.spk => spk", List.of(uns2spk), List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "", null, null))), new ReasonerTestCase( "spk.uns => spk", List.of(spk2uns), List.of(KAS_US), - List.of(new KeySplitStep(SPECIFIED_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(SPECIFIED_KAS, "", null, null))), new ReasonerTestCase( "spk.spk => value.spk", List.of(spk2spk), List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "", null, null))), new ReasonerTestCase( "spk.spk & spk.uns => value.spk || attr.spk", List.of(spk2spk, spk2uns), List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"), new KeySplitStep(SPECIFIED_KAS, "1"))), + List.of(new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "1", null, null), + new Autoconfigure.KeySplitTemplate(SPECIFIED_KAS, "1", null, null))), new ReasonerTestCase( "spk.uns & spk.spk => value.spk || attr.spk", List.of(spk2uns, spk2spk), List.of(KAS_US), - List.of(new KeySplitStep(SPECIFIED_KAS, "1"), new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"))), + List.of(new Autoconfigure.KeySplitTemplate(SPECIFIED_KAS, "1", null, null), + new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "1", null, null))), new ReasonerTestCase( "uns.spk & spk.spk => value.spk", List.of(spk2spk, uns2spk), List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "", null, null))), new ReasonerTestCase( "uns.spk & uns.uns => spk", List.of(uns2spk, uns2uns), List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "", null, null))), new ReasonerTestCase( "uns.uns & uns.spk => spk", List.of(uns2uns, uns2spk), List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "", null, null))), new ReasonerTestCase( "uns.uns & spk.spk => spk", List.of(uns2uns, spk2spk), List.of(KAS_US), - List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "", null, null))), new ReasonerTestCase( "spk.uns.uns => ns.spk", List.of(spk2uns2uns, uns2uns), List.of(KAS_US), - List.of(new KeySplitStep(NAMESPACE_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(NAMESPACE_KAS, "", null, null))), new ReasonerTestCase( "spk.uns.uns & uns.uns => ns.spk", List.of(spk2uns2uns, uns2uns), List.of(KAS_US), - List.of(new KeySplitStep(NAMESPACE_KAS, ""))), + List.of(new Autoconfigure.KeySplitTemplate(NAMESPACE_KAS, "", null, null))), new ReasonerTestCase( "spk.uns.uns & uns.spk => ns.spk && spk", List.of(spk2uns2uns, uns2spk), List.of(KAS_US), - List.of(new KeySplitStep(NAMESPACE_KAS, "1"), new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "2"))), + List.of(new Autoconfigure.KeySplitTemplate(NAMESPACE_KAS, "1", null, null), + new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "2", null, null))), new ReasonerTestCase( "spk.uns.uns & spk.spk.uns && spk.uns.spk => ns.spk || attr.spk || value.spk", List.of(spk2uns2uns, spk2spk2uns, spk2uns2spk), List.of(KAS_US), - List.of(new KeySplitStep(NAMESPACE_KAS, "1"), new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "1"), new KeySplitStep(SPECIFIED_KAS, "2"))) + List.of(new Autoconfigure.KeySplitTemplate(NAMESPACE_KAS, "1", null, null), + new Autoconfigure.KeySplitTemplate(EVEN_MORE_SPECIFIC_KAS, "1", null, null), + new Autoconfigure.KeySplitTemplate(SPECIFIED_KAS, "2", null, null))) ); for (ReasonerTestCase tc : testCases) { @@ -662,7 +672,7 @@ public void cancel() { var wrapper = new Object() { int i = 0; }; - List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), Optional::empty); + List plan = reasoner.getSplits(tc.getDefaults(), () -> String.valueOf(wrapper.i++ + 1), Optional::empty); assertThat(plan) .as(tc.name) .hasSameElementsAs(tc.getPlan()); @@ -751,10 +761,10 @@ private static class ReasonerTestCase { private final String ats; private final String keyed; private final String reduced; - private final List plan; + private final List plan; ReasonerTestCase(String name, List policy, List defaults, String ats, String keyed, - String reduced, List plan) { + String reduced, List plan) { this.name = name; this.policy = policy; this.defaults = defaults; @@ -764,7 +774,7 @@ private static class ReasonerTestCase { this.plan = plan; } - ReasonerTestCase(String name, List policy, List defaults, List plan) { + ReasonerTestCase(String name, List policy, List defaults, List plan) { this.name = name; this.policy = policy; this.defaults = defaults; @@ -798,7 +808,7 @@ public String getReduced() { return reduced; } - public List getPlan() { + public List getPlan() { return plan; } } @@ -1011,7 +1021,7 @@ void testUsingBaseKeyWhenNoMappedKeysOrGrants() { }, () -> Optional.of(key)); assertThat(splits).hasSize(1); - assertThat(splits.get(0)).isEqualTo(new KeySplitStep("https://example.com/kas", "", "thenewkid")); + assertThat(splits.get(0)).isEqualTo(new Autoconfigure.KeySplitTemplate("https://example.com/kas", "", "thenewkid", KeyType.EC521Key)); } @Test @@ -1027,8 +1037,8 @@ void testUsingDefaultKasesWhenNothingElseProvided() { assertThat(splits) .hasSize(2) .asList().containsExactly( - new KeySplitStep("https://example.org/kas1", "0", null), - new KeySplitStep("https://example.org/kas2", "1", null) + new Autoconfigure.KeySplitTemplate("https://example.org/kas1", "0", null, null), + new Autoconfigure.KeySplitTemplate("https://example.org/kas2", "1", null, null) ); } @@ -1120,7 +1130,7 @@ void getSplits_usesAutoconfigurePlan_whenAutoconfigureTrue() { var services = new FakeServicesBuilder().setKas(kas).build(); // Mock granterFactory to return a granter with a known split plan - var expectedSplit = new Autoconfigure.KeySplitStep("https://kas.example.com", "", "kid"); + var expectedSplit = new Autoconfigure.KeySplitTemplate("https://kas.example.com", "", "kid", null); var granter = Mockito.mock(Autoconfigure.Granter.class); Mockito.when(granter.getSplits( Mockito.anyList(), 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 117db1a7..e710c7b4 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -81,26 +81,27 @@ void generatePlanFromProvidedKases() { var kas1 = new Config.KASInfo(); kas1.URL = "https://kas1.example.com"; kas1.KID = "kid1"; - kas1.Algorithm = "rsa:2048"; var kas2 = new Config.KASInfo(); kas2.URL = "https://kas2.example.com"; kas2.KID = "kid2"; - kas2.Algorithm = "ec:secp256"; + kas2.Algorithm = "ec:secp256r1"; var tdfConfig = new Config.TDFConfig(); tdfConfig.kasInfoList.add(kas1); tdfConfig.kasInfoList.add(kas2); var planner = new Planner(tdfConfig, new FakeServicesBuilder().build(), (ignore1, ignored2) -> { throw new IllegalArgumentException("no granter needed"); }); - List splitPlan = planner.generatePlanFromProvidedKases(tdfConfig.kasInfoList); + List splitPlan = planner.generatePlanFromProvidedKases(tdfConfig.kasInfoList); assertThat(splitPlan).asList().hasSize(2); assertThat(splitPlan.get(0).kas).isEqualTo("https://kas1.example.com"); assertThat(splitPlan.get(0).kid).isEqualTo("kid1"); + assertThat(splitPlan.get(0).keyType).isNull(); assertThat(splitPlan.get(1).kas).isEqualTo("https://kas2.example.com"); assertThat(splitPlan.get(1).kid).isEqualTo("kid2"); + assertThat(splitPlan.get(1).keyType).isEqualTo(KeyType.EC256Key); assertThat(splitPlan.get(0).splitID).isNotEqualTo(splitPlan.get(1).splitID); } @@ -122,8 +123,9 @@ void testFillingInKeysWithAutoConfigure() { ret.KID = "kid2"; } else if (Objects.equals(kasInfo.URL, "https://kas3.example.com")) { ret.PublicKey = "pem3"; - ret.Algorithm = "rsa:4096"; + ret.Algorithm = "ec:secp384r1"; ret.KID = "kid3"; + assertThat(kasInfo.Algorithm).isEqualTo("ec:secp384r1"); } else { throw new IllegalArgumentException("Unexpected KAS URL: " + kasInfo.URL); } @@ -142,10 +144,10 @@ void testFillingInKeysWithAutoConfigure() { ); var planner = new Planner(tdfConfig, new FakeServicesBuilder().setKas(kas).build(), (ignore1, ignored2) -> { throw new IllegalArgumentException("no granter needed"); }); var plan = List.of( - new Autoconfigure.KeySplitStep("https://kas1.example.com", "split1", null), - new Autoconfigure.KeySplitStep("https://kas4.example.com", "split1", "kid4"), - new Autoconfigure.KeySplitStep("https://kas2.example.com", "split2", "kid2"), - new Autoconfigure.KeySplitStep("https://kas3.example.com", "split2", "kid3") + new Autoconfigure.KeySplitTemplate("https://kas1.example.com", "split1", null, null), + new Autoconfigure.KeySplitTemplate("https://kas4.example.com", "split1", "kid4", null), + new Autoconfigure.KeySplitTemplate("https://kas2.example.com", "split2", "kid2", null), + new Autoconfigure.KeySplitTemplate("https://kas3.example.com", "split2", null, KeyType.EC384Key) ); Map> filledInPlan = planner.resolveKeys(plan); assertThat(filledInPlan.keySet().stream().collect(Collectors.toList())).asList().containsExactlyInAnyOrder("split1", "split2"); @@ -166,7 +168,7 @@ void testFillingInKeysWithAutoConfigure() { assertThat(kasInfo2.PublicKey).isEqualTo("pem2"); var kasInfo3 = filledInPlan.get("split2").stream().filter(kasInfo -> "kid3".equals(kasInfo.KID)).findFirst().get(); assertThat(kasInfo3.URL).isEqualTo("https://kas3.example.com"); - assertThat(kasInfo3.Algorithm).isEqualTo("rsa:4096"); + assertThat(kasInfo3.Algorithm).isEqualTo("ec:secp384r1"); assertThat(kasInfo3.PublicKey).isEqualTo("pem3"); } @@ -247,8 +249,8 @@ void usesProvidedSplitPlanWhenNotAutoconfigure() { kas2.KID = "kid2"; kas2.Algorithm = "ec:secp256"; - var splitStep1 = new Autoconfigure.KeySplitStep(kas1.URL, "split1", kas1.KID); - var splitStep2 = new Autoconfigure.KeySplitStep(kas2.URL, "split2", kas2.KID); + var splitStep1 = new Autoconfigure.KeySplitStep(kas1.URL, "split1"); + var splitStep2 = new Autoconfigure.KeySplitStep(kas2.URL, "split2"); var tdfConfig = new Config.TDFConfig(); tdfConfig.autoconfigure = false; diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java index a66e0f84..087743db 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ZipReaderTest.java @@ -146,5 +146,7 @@ public void testReadingAndWritingRandomFiles() throws IOException { entry.getData().transferTo(zipData); assertThat(zipData.toByteArray()).isEqualTo(namesToData.get(entry.getName())); } + + assertThat(reader.getEntries().size()).isEqualTo(namesToData.size()); } } \ No newline at end of file From e233de48a6e76cba5baba3286217ca3f1c33ddf1 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 20:14:20 +0200 Subject: [PATCH 31/37] clarify a bit --- .../java/io/opentdf/platform/sdk/Autoconfigure.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index c7622c84..e7060417 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -5,7 +5,6 @@ import io.opentdf.platform.policy.Attribute; import io.opentdf.platform.policy.AttributeRuleTypeEnum; import io.opentdf.platform.policy.AttributeValueSelector; -import io.opentdf.platform.policy.KasPublicKeyAlgEnum; import io.opentdf.platform.policy.KeyAccessServer; import io.opentdf.platform.policy.SimpleKasKey; import io.opentdf.platform.policy.Value; @@ -381,7 +380,7 @@ List getSplits(List defaultKases, Supplier gen } if (hasGrants) { logger.debug("generating plan from grants"); - return plan(genSplitID); + return planUsingGrants(genSplitID); } var baseKey = baseKeySupplier.get(); @@ -401,7 +400,7 @@ List getSplits(List defaultKases, Supplier gen } @Nonnull - List plan(Supplier genSplitID) + List planUsingGrants(Supplier genSplitID) throws AutoConfigureException { AttributeBooleanExpression b = constructAttributeBoolean(); BooleanKeyExpression k = insertKeysForAttribute(b); @@ -419,7 +418,8 @@ List plan(Supplier genSplitID) for (KeyClause v : k.values) { String splitID = (l > 1) ? genSplitID.get() : ""; for (PublicKeyInfo o : v.values) { - steps.add(new KeySplitTemplate(o.kas, splitID, o.kid, null)); + // grants only have KAS URLs, no KIDs or algorithms + steps.add(new KeySplitTemplate(o.kas, splitID, null, null)); } } return steps; @@ -444,7 +444,8 @@ List planFromAttributes(Supplier genSplitID) for (KeyClause v : k.values) { String splitID = (l > 1) ? genSplitID.get() : ""; for (PublicKeyInfo o : v.values) { - steps.add(new KeySplitTemplate(o.kas, splitID, o.kid, o.algorithm != null ? KeyType.fromString(o.algorithm) : null)); + KeyType keyType = o.algorithm != null ? KeyType.fromString(o.algorithm) : null; + steps.add(new KeySplitTemplate(o.kas, splitID, o.kid, keyType)); } } return steps; From cb94c03ca3761069a3ff9480c853d279dde063f6 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 20:49:16 +0200 Subject: [PATCH 32/37] add a key --- .../java/io/opentdf/platform/sdk/AutoconfigureTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index 7cf20bdf..fb732ef5 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -225,10 +225,15 @@ private Attribute mockAttributeFor(Autoconfigure.AttributeNameFQN fqn) { .setName("Releasable To").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) .setFqn(fqn.toString()).build(); } else if (key.equals(SPECKED.getKey())) { + var kasKey = SimpleKasKey.newBuilder() + .setKasUri(SPECKED.getKey()) + .setPublicKey(SimpleKasPublicKey.newBuilder().setPem("speckedpem") + .setAlgorithm(Algorithm.ALGORITHM_EC_P521) + .setKid("speckedkeykid")).build(); return Attribute.newBuilder().setId("SPK").setNamespace(ns2) .setName("specified").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF) .setFqn(fqn.toString()) - .addGrants(KeyAccessServer.newBuilder().setUri(SPECIFIED_KAS).build()) + .addGrants(KeyAccessServer.newBuilder().setUri(SPECIFIED_KAS).addKasKeys(kasKey)) .build(); } else if (key.equals(UNSPECKED.getKey())) { return Attribute.newBuilder().setId("UNS").setNamespace(ns2) From 98b95d9d71c018e40caaa2e01b34b484201c7e69 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 21:13:05 +0200 Subject: [PATCH 33/37] test errors --- .../java/io/opentdf/platform/sdk/Planner.java | 6 +++--- .../java/io/opentdf/platform/sdk/TDF.java | 2 +- .../platform/sdk/AutoconfigureTest.java | 19 ++++++++++++++++++- .../io/opentdf/platform/sdk/PlannerTest.java | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) 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 f278bada..b07c735c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Planner.java @@ -44,7 +44,7 @@ private static String getUUID() { return UUID.randomUUID().toString(); } - Map> getSplits(Config.TDFConfig tdfConfig) { + Map> getSplits() { List splitPlan; if (tdfConfig.autoconfigure) { if (tdfConfig.splitPlan != null && !tdfConfig.splitPlan.isEmpty()) { @@ -59,8 +59,8 @@ Map> getSplits(Config.TDFConfig tdfConfig) { .collect(Collectors.toList()); } - if (tdfConfig.kasInfoList.isEmpty() && splitPlan.isEmpty()) { - throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred"); + if (splitPlan.isEmpty()) { + throw new SDK.KasInfoMissing("no plan was constructed via autoconfigure, explicit split plan or provided kases"); } return resolveKeys(splitPlan); } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 60f8a5e8..80522d38 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -352,7 +352,7 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException { Planner planner = new Planner(tdfConfig, services, Autoconfigure::createGranter); - Map> splits = planner.getSplits(tdfConfig); + Map> splits = planner.getSplits(); TDFObject tdfObject = new TDFObject(); tdfObject.prepareManifest(tdfConfig, splits); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java index fb732ef5..53fe1c06 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -45,6 +45,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -1149,7 +1150,7 @@ void getSplits_usesAutoconfigurePlan_whenAutoconfigureTrue() { var planner = new Planner(tdfConfig, services, granterFactory); // Act - var splits = planner.getSplits(tdfConfig); + var splits = planner.getSplits(); // Assert assertThat(splits).containsKey(""); @@ -1158,4 +1159,20 @@ void getSplits_usesAutoconfigurePlan_whenAutoconfigureTrue() { assertThat(splits.get("").get(0).KID).isEqualTo("kid"); assertThat(splits.get("").get(0).Algorithm).isEqualTo("ec:secp256r1"); } + + @Test + void testInvalidConfigurations() { + var config = new Config.TDFConfig(); + config.autoconfigure = true; + config.splitPlan = List.of(new KeySplitStep("kas1", "")); + Planner planner = new Planner(config, new FakeServicesBuilder().build(), (a, b) -> { throw new IllegalStateException("no way"); }); + Exception thrown = assertThrows(IllegalArgumentException.class, () -> planner.getSplits()); + assertThat(thrown.getMessage()).contains("cannot use autoconfigure with a split plan provided in the TDFConfig"); + + + config = new Config.TDFConfig() {{ autoconfigure = false; kasInfoList = Collections.EMPTY_LIST; splitPlan = null; }}; + var otherPlanner = new Planner(config, new FakeServicesBuilder().build(), (a, b) -> { throw new IllegalStateException("no way"); }); + thrown = assertThrows(SDK.KasInfoMissing.class, () -> otherPlanner.getSplits()); + assertThat(thrown.getMessage()).contains("no plan was constructed via autoconfigure, explicit split plan or provided kases"); + } } 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 e710c7b4..c7316dd6 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/PlannerTest.java @@ -261,7 +261,7 @@ void usesProvidedSplitPlanWhenNotAutoconfigure() { var planner = new Planner(tdfConfig, new FakeServicesBuilder().setKas(kas).build(), (ignore1, ignored2) -> { throw new IllegalArgumentException("no granter needed"); }); // Act - Map> splits = planner.getSplits(tdfConfig); + Map> splits = planner.getSplits(); // Assert Assertions.assertThat(splits).hasSize(2); From e20ad25fc451aeb1e71850f602825e337126a4c5 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 21:33:29 +0200 Subject: [PATCH 34/37] remove unused methods --- .../java/io/opentdf/platform/sdk/KeyType.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) 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 5251f11c..a19c255e 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java @@ -5,9 +5,6 @@ import javax.annotation.Nonnull; -import java.util.Optional; - -import static io.opentdf.platform.policy.Algorithm.ALGORITHM_EC_P521; import static io.opentdf.platform.sdk.NanoTDFType.ECCurve.SECP256R1; import static io.opentdf.platform.sdk.NanoTDFType.ECCurve.SECP384R1; import static io.opentdf.platform.sdk.NanoTDFType.ECCurve.SECP521R1; @@ -88,18 +85,7 @@ public static KeyType fromAlgorithm(KasPublicKeyAlgEnum algorithm) { } } - public static KeyType getKeyTypeToUse(String keyType, Config.TDFConfig config) { - if (keyType == null || keyType.isEmpty()) { - return config.wrappingKeyType != null ? config.wrappingKeyType : KeyType.RSA2048Key; - } - try { - return KeyType.fromString(keyType); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid key type: " + keyType, e); - } - } - public boolean isEc() { - return this.curve != null; + return this.curve == SECP256R1 || this.curve == SECP384R1 || this.curve == SECP521R1; } } \ No newline at end of file From c442ff8cbd311a10feb2a726c4ae2856747d95fb Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 22:20:15 +0200 Subject: [PATCH 35/37] pump those coverage numbers --- .../opentdf/platform/sdk/Autoconfigure.java | 2 +- .../java/io/opentdf/platform/sdk/Config.java | 2 +- .../java/io/opentdf/platform/sdk/KeyType.java | 2 +- .../io/opentdf/platform/sdk/KeyTypeTest.java | 40 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java index e7060417..e17b69cd 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -352,7 +352,7 @@ boolean addAllGrants(AttributeValueFQN fqn, List granted, List< var mappedKey = new Config.KASInfo(); mappedKey.URL = grantedKey.getUri(); mappedKey.KID = cachedGrantKey.getKid(); - mappedKey.Algorithm = KeyType.fromAlgorithm(cachedGrantKey.getAlg()).toString(); + mappedKey.Algorithm = KeyType.fromPublicKeyAlgorithm(cachedGrantKey.getAlg()).toString(); mappedKey.PublicKey = cachedGrantKey.getPem(); mappedKey.Default = false; mappedKeys.computeIfAbsent(fqn.key, k -> new ArrayList<>()).add(mappedKey); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index 816e3ad1..88757be0 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -90,7 +90,7 @@ public static List fromKeyAccessServer(KeyAccessServer kas) { Config.KASInfo kasInfo = new Config.KASInfo(); kasInfo.URL = kas.getUri(); kasInfo.KID = ki.getKid(); - kasInfo.Algorithm = KeyType.fromAlgorithm(ki.getAlg()).toString(); + kasInfo.Algorithm = KeyType.fromPublicKeyAlgorithm(ki.getAlg()).toString(); kasInfo.PublicKey = ki.getPem(); return Stream.of(kasInfo); }).collect(Collectors.toList()); 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 a19c255e..90d1251d 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java @@ -67,7 +67,7 @@ public static KeyType fromAlgorithm(Algorithm algorithm) { } } - public static KeyType fromAlgorithm(KasPublicKeyAlgEnum algorithm) { + public static KeyType fromPublicKeyAlgorithm(KasPublicKeyAlgEnum algorithm) { if (algorithm == null) { throw new IllegalArgumentException("Algorithm cannot be null"); } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java new file mode 100644 index 00000000..69e8917c --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java @@ -0,0 +1,40 @@ +package io.opentdf.platform.sdk; + +import io.opentdf.platform.policy.Algorithm; +import org.junit.jupiter.api.Test; + +import static io.opentdf.platform.policy.KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1; +import static io.opentdf.platform.policy.KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1; +import static io.opentdf.platform.policy.KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1; +import static org.junit.jupiter.api.Assertions.*; + +class KeyTypeTest { + @Test + void testFromString() { + assertEquals(KeyType.RSA2048Key, KeyType.fromString("rsa:2048")); + assertEquals(KeyType.EC256Key, KeyType.fromString("ec:secp256r1")); + assertEquals(KeyType.EC384Key, KeyType.fromString("ec:secp384r1")); + assertEquals(KeyType.EC521Key, KeyType.fromString("ec:secp521r1")); + } + + @Test + void testFromStringInvalid() { + assertThrows(IllegalArgumentException.class, () -> KeyType.fromString("invalid:key")); + } + + @Test + void testFromAlgorithm() { + assertEquals(KeyType.RSA2048Key, KeyType.fromAlgorithm(Algorithm.ALGORITHM_RSA_2048)); + assertEquals(KeyType.EC256Key, KeyType.fromAlgorithm(Algorithm.ALGORITHM_EC_P256)); + assertEquals(KeyType.EC384Key, KeyType.fromAlgorithm(Algorithm.ALGORITHM_EC_P384)); + assertEquals(KeyType.EC521Key, KeyType.fromAlgorithm(Algorithm.ALGORITHM_EC_P521)); + } + + @Test + void testFromPublicKeyAlgEnum() { + assertEquals(KeyType.RSA2048Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1)); + assertEquals(KeyType.EC256Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1)); + 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 From 4e3b907366c608ed556eabeb8f26e363e2e2a5af Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 22:27:39 +0200 Subject: [PATCH 36/37] fix test --- .../io/opentdf/platform/sdk/KeyTypeTest.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 69e8917c..6dc72f47 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/KeyTypeTest.java @@ -1,11 +1,15 @@ package io.opentdf.platform.sdk; -import io.opentdf.platform.policy.Algorithm; import org.junit.jupiter.api.Test; +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_RSA_2048; import static io.opentdf.platform.policy.KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1; import static io.opentdf.platform.policy.KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1; import static io.opentdf.platform.policy.KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1; +import static io.opentdf.platform.policy.KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048; import static org.junit.jupiter.api.Assertions.*; class KeyTypeTest { @@ -24,15 +28,15 @@ void testFromStringInvalid() { @Test void testFromAlgorithm() { - assertEquals(KeyType.RSA2048Key, KeyType.fromAlgorithm(Algorithm.ALGORITHM_RSA_2048)); - assertEquals(KeyType.EC256Key, KeyType.fromAlgorithm(Algorithm.ALGORITHM_EC_P256)); - assertEquals(KeyType.EC384Key, KeyType.fromAlgorithm(Algorithm.ALGORITHM_EC_P384)); - assertEquals(KeyType.EC521Key, KeyType.fromAlgorithm(Algorithm.ALGORITHM_EC_P521)); + assertEquals(KeyType.RSA2048Key, KeyType.fromAlgorithm(ALGORITHM_RSA_2048)); + assertEquals(KeyType.EC256Key, KeyType.fromAlgorithm(ALGORITHM_EC_P256)); + assertEquals(KeyType.EC384Key, KeyType.fromAlgorithm(ALGORITHM_EC_P384)); + assertEquals(KeyType.EC521Key, KeyType.fromAlgorithm(ALGORITHM_EC_P521)); } @Test void testFromPublicKeyAlgEnum() { - assertEquals(KeyType.RSA2048Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1)); + assertEquals(KeyType.RSA2048Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048)); assertEquals(KeyType.EC256Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1)); assertEquals(KeyType.EC384Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1)); assertEquals(KeyType.EC521Key, KeyType.fromPublicKeyAlgorithm(KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1)); From 9124da8c51987ec534c99b920c8d2f23dbc26557 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 22:34:53 +0200 Subject: [PATCH 37/37] Update KeyType.java --- sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 90d1251d..3f7973a6 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KeyType.java @@ -86,6 +86,6 @@ public static KeyType fromPublicKeyAlgorithm(KasPublicKeyAlgEnum algorithm) { } public boolean isEc() { - return this.curve == SECP256R1 || this.curve == SECP384R1 || this.curve == SECP521R1; + return this.curve != null; } -} \ No newline at end of file +}