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 8fd07c8c..29b7c4d5 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java @@ -31,8 +31,10 @@ import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue; import io.opentdf.platform.policy.Attribute; import io.opentdf.platform.policy.Value; +import io.opentdf.platform.policy.KasPublicKey; import io.opentdf.platform.policy.AttributeValueSelector; import io.opentdf.platform.policy.AttributeRuleTypeEnum; +import io.opentdf.platform.policy.KasPublicKeyAlgEnum; // Attribute rule types: operators! class RuleType { @@ -48,24 +50,29 @@ public class Autoconfigure { public static Logger logger = LoggerFactory.getLogger(Autoconfigure.class); - public static class SplitStep { + public static class KeySplitStep { public String kas; public String splitID; - public SplitStep(String kas, String splitId) { + public KeySplitStep(String kas, String splitId) { this.kas = kas; this.splitID = splitId; } + @Override + public String toString() { + return "KeySplitStep{kas=" + this.kas + ", splitID=" + this.splitID + "}"; + } + @Override public boolean equals(Object obj) { if (this == obj) { return true; } - if (obj == null || !(obj instanceof SplitStep)) { + if (obj == null || !(obj instanceof KeySplitStep)) { return false; } - SplitStep ss = (SplitStep) obj; + KeySplitStep ss = (KeySplitStep) obj; if ((this.kas.equals(ss.kas)) && (this.splitID.equals(ss.splitID))){ return true; } @@ -287,7 +294,7 @@ public KeyAccessGrant byAttribute(AttributeValueFQN fqn) { } - public List plan(List defaultKas, Supplier genSplitID) throws AutoConfigureException { + public List plan(List defaultKas, Supplier genSplitID) throws AutoConfigureException { AttributeBooleanExpression b = constructAttributeBoolean(); BooleanKeyExpression k = insertKeysForAttribute(b); if (k == null) { @@ -301,21 +308,21 @@ public List plan(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 SplitStep(defaultKas.get(0), "")); + return Collections.singletonList(new KeySplitStep(defaultKas.get(0), "")); } else { - List result = new ArrayList<>(); + List result = new ArrayList<>(); for (String kas : defaultKas) { - result.add(new SplitStep(kas, genSplitID.get())); + result.add(new KeySplitStep(kas, genSplitID.get())); } return result; } } - 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 SplitStep(o.kas, splitID)); + steps.add(new KeySplitStep(o.kas, splitID)); } } return steps; @@ -668,7 +675,7 @@ public static String ruleToOperator(AttributeRuleTypeEnum e) { } // Gets a list of directory of KAS grants for a list of attribute FQNs - public static Granter newGranterFromService(AttributesServiceFutureStub as, AttributeValueFQN... fqns) throws AutoConfigureException, InterruptedException, ExecutionException { + public static Granter newGranterFromService(AttributesServiceFutureStub as, KASKeyCache keyCache, AttributeValueFQN... fqns) throws AutoConfigureException, InterruptedException, ExecutionException { String[] fqnsStr = new String[fqns.length]; for (int i = 0; i < fqns.length; i++) { fqnsStr[i] = fqns[i].toString(); @@ -699,9 +706,11 @@ public static Granter newGranterFromService(AttributesServiceFutureStub as, Attr Value v = pair.getValue(); if (v != null && !v.getGrantsList().isEmpty()) { grants.addAllGrants(fqn, v.getGrantsList(), def); + storeKeysToCache(v.getGrantsList(), keyCache); } else { if (def != null) { grants.addAllGrants(fqn, def.getGrantsList(), def); + storeKeysToCache(def.getGrantsList(), keyCache); } } } @@ -738,4 +747,34 @@ public static Granter newGranterFromAttributes(Value... attrs) throws AutoConfig return grants; } + 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; + } + 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); + } + } + } + + private 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 a7a081c9..ea4fbac5 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -34,6 +34,7 @@ public static class KASInfo { public String PublicKey; public String KID; public Boolean Default; + public String Algorithm; } @@ -88,7 +89,7 @@ public static class TDFConfig { public List kasInfoList; public List assertionConfigList; public String mimeType; - public List splitPlan; + public List splitPlan; public TDFConfig() { this.autoconfigure = true; @@ -163,7 +164,7 @@ public static Consumer withKasInformation(KASInfo... kasInfoList) { }; } - public static Consumer withSplitPlan(Autoconfigure.SplitStep... p) { + public static Consumer withSplitPlan(Autoconfigure.KeySplitStep... p) { return (TDFConfig config) -> { config.splitPlan = new ArrayList<>(Arrays.asList(p)); config.autoconfigure = false; 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 2eddec57..2473b506 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java @@ -1,7 +1,6 @@ package io.opentdf.platform.sdk; import com.google.gson.Gson; -import com.google.gson.annotations.SerializedName; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; @@ -12,6 +11,7 @@ import io.grpc.ManagedChannel; import io.opentdf.platform.kas.AccessServiceGrpc; import io.opentdf.platform.kas.PublicKeyRequest; +import io.opentdf.platform.kas.PublicKeyResponse; import io.opentdf.platform.kas.RewrapRequest; import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; @@ -36,6 +36,8 @@ public class KASClient implements SDK.KAS, AutoCloseable { private final AsymDecryption decryptor; private final String publicKeyPEM; + private KASKeyCache kasKeyCache; + /*** * A client that communicates with KAS * @param channelFactory A function that produces channels that can be used to communicate @@ -51,6 +53,8 @@ public KASClient(Function channelFactory, RSAKey dpopKe var encryptionKeypair = CryptoUtils.generateRSAKeypair(); decryptor = new AsymDecryption(encryptionKeypair.getPrivate()); publicKeyPEM = CryptoUtils.getRSAPublicKeyPEM(encryptionKeypair.getPublic()); + + this.kasKeyCache = new KASKeyCache(); } @Override @@ -61,17 +65,26 @@ public String getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) } @Override - public String getPublicKey(Config.KASInfo kasInfo) { - return getStub(kasInfo.URL) - .publicKey(PublicKeyRequest.getDefaultInstance()) - .getPublicKey(); + public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { + Config.KASInfo cachedValue = this.kasKeyCache.get(kasInfo.URL, kasInfo.Algorithm); + if (cachedValue != null) { + return cachedValue; + } + PublicKeyResponse resp = getStub(kasInfo.URL).publicKey(PublicKeyRequest.getDefaultInstance()); + + var kiCopy = new Config.KASInfo(); + kiCopy.KID = resp.getKid(); + kiCopy.PublicKey = resp.getPublicKey(); + kiCopy.URL = kasInfo.URL; + kiCopy.Algorithm = kasInfo.Algorithm; + + this.kasKeyCache.store(kiCopy); + return kiCopy; } @Override - public String getKid(Config.KASInfo kasInfo) { - return getStub(kasInfo.URL) - .publicKey(PublicKeyRequest.getDefaultInstance()) - .getKid(); + public KASKeyCache getKeyCache(){ + return this.kasKeyCache; } private String normalizeAddress(String urlString) { diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java b/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java new file mode 100644 index 00000000..3d4c5988 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASKeyCache.java @@ -0,0 +1,82 @@ +package io.opentdf.platform.sdk; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +public class KASKeyCache { + Map cache; + + public KASKeyCache() { + this.cache = new HashMap<>(); + } + + public void clear() { + this.cache = new HashMap<>(); + } + + public Config.KASInfo get(String url, String algorithm) { + KASKeyRequest cacheKey = new KASKeyRequest(url, algorithm); + LocalDateTime now = LocalDateTime.now(); + TimeStampedKASInfo cachedValue = cache.get(cacheKey); + + if (cachedValue == null) { + return null; + } + + LocalDateTime aMinAgo = now.minus(5, ChronoUnit.MINUTES); + if (aMinAgo.isAfter(cachedValue.timestamp)) { + cache.remove(cacheKey); + return null; + } + + return cachedValue.kasInfo; + } + + public void store(Config.KASInfo kasInfo) { + KASKeyRequest cacheKey = new KASKeyRequest(kasInfo.URL, kasInfo.Algorithm); + cache.put(cacheKey, new TimeStampedKASInfo(kasInfo, LocalDateTime.now())); + } +} + +class TimeStampedKASInfo { + Config.KASInfo kasInfo; + LocalDateTime timestamp; + + public TimeStampedKASInfo(Config.KASInfo kasInfo, LocalDateTime timestamp) { + this.kasInfo = kasInfo; + this.timestamp = timestamp; + } +} + +class KASKeyRequest { + private String url; + private String algorithm; + + public KASKeyRequest(String url, String algorithm) { + this.url = url; + this.algorithm = algorithm; + } + + // 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; + KASKeyRequest that = (KASKeyRequest) o; + if (algorithm == null){ + return url.equals(that.url); + } + return url.equals(that.url) && algorithm.equals(that.algorithm); + } + + @Override + public int hashCode() { + int result = 31 * url.hashCode(); + if (algorithm != null) { + result = result + algorithm.hashCode(); + } + return result; + } +} \ No newline at end of file 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 426b6c3b..87d6557e 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java @@ -38,11 +38,11 @@ public void close() throws Exception { } public interface KAS extends AutoCloseable { - String getPublicKey(Config.KASInfo kasInfo); - String getKid(Config.KASInfo kasInfo); + Config.KASInfo getPublicKey(Config.KASInfo kasInfo); String getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve); byte[] unwrap(Manifest.KeyAccess keyAccess, String policy); byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL); + KASKeyCache getKeyCache(); } // TODO: add KAS 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 d83b5cdd..0b2631a4 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -181,10 +181,10 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { 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()); + List splitPlan = new ArrayList<>(tdfConfig.kasInfoList.size()); int i = 0; for (Config.KASInfo kasInfo : tdfConfig.kasInfoList) { - Autoconfigure.SplitStep step = new Autoconfigure.SplitStep(kasInfo.URL, ""); + Autoconfigure.KeySplitStep step = new Autoconfigure.KeySplitStep(kasInfo.URL, ""); if (tdfConfig.kasInfoList.size() > 1) { step.splitID = String.format("s-%d", i++); } @@ -207,7 +207,7 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { Map> conjunction = new HashMap<>(); List splitIDs = new ArrayList<>(); - for (Autoconfigure.SplitStep splitInfo : tdfConfig.splitPlan) { + 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); @@ -215,8 +215,8 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { logger.info("no public key provided for KAS at {}, retrieving", splitInfo.kas); var getKI = new Config.KASInfo(); getKI.URL = splitInfo.kas; - getKI.PublicKey = kas.getPublicKey(getKI); - getKI.KID = kas.getKid(getKI); + getKI.Algorithm = "rsa:2048"; + getKI = kas.getPublicKey(getKI); latestKASInfo.put(splitInfo.kas, getKI); ki = getKI; } @@ -384,7 +384,7 @@ public TDFObject createTDF(InputStream payload, if (tdfConfig.attributeValues != null && !tdfConfig.attributeValues.isEmpty()) { granter = Autoconfigure.newGranterFromAttributes(tdfConfig.attributeValues.toArray(new Value[0])); } else if (tdfConfig.attributes != null && !tdfConfig.attributes.isEmpty()) { - granter = Autoconfigure.newGranterFromService(attrService, tdfConfig.attributes.toArray(new AttributeValueFQN[0])); + granter = Autoconfigure.newGranterFromService(attrService, kas.getKeyCache(), tdfConfig.attributes.toArray(new AttributeValueFQN[0])); } if (granter == null) { @@ -547,8 +547,9 @@ private void fillInPublicKeyInfo(List kasInfoList, SDK.KAS kas) continue; } logger.info("no public key provided for KAS at {}, retrieving", kasInfo.URL); - kasInfo.PublicKey = kas.getPublicKey(kasInfo); - kasInfo.KID = kas.getKid(kasInfo); + Config.KASInfo getKasInfo = kas.getPublicKey(kasInfo); + kasInfo.PublicKey = getKasInfo.PublicKey; + kasInfo.KID = getKasInfo.KID; } } @@ -563,7 +564,7 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, Config.AssertionVeri Set knownSplits = new HashSet(); Set foundSplits = new HashSet();; - Map skippedSplits = new HashMap<>(); + Map skippedSplits = new HashMap<>(); boolean mixedSplits = manifest.encryptionInformation.keyAccessObj.size() > 1 && (manifest.encryptionInformation.keyAccessObj.get(0).sid != null) && !manifest.encryptionInformation.keyAccessObj.get(0).sid.isEmpty(); @@ -572,7 +573,7 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, Config.AssertionVeri if (manifest.payload.isEncrypted) { for (Manifest.KeyAccess keyAccess: manifest.encryptionInformation.keyAccessObj) { - Autoconfigure.SplitStep ss = new Autoconfigure.SplitStep(keyAccess.url, keyAccess.sid); + Autoconfigure.KeySplitStep ss = new Autoconfigure.KeySplitStep(keyAccess.url, keyAccess.sid); byte[] unwrappedKey; if (!mixedSplits) { unwrappedKey = kas.unwrap(keyAccess, manifest.encryptionInformation.policy); @@ -615,7 +616,7 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, Config.AssertionVeri List exceptionList = new ArrayList<>(skippedSplits.size() + 1); exceptionList.add(new Exception("splitKey.unable to reconstruct split key: " + skippedSplits)); - for (Map.Entry entry : skippedSplits.entrySet()) { + for (Map.Entry entry : skippedSplits.entrySet()) { exceptionList.add(entry.getValue()); } 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 1ac8a486..7f761ac5 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/AutoconfigureTest.java @@ -3,43 +3,66 @@ 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.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import io.opentdf.platform.policy.Attribute; import io.opentdf.platform.policy.Value; +import io.opentdf.platform.policy.attributes.AttributesServiceGrpc; +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.AttributeBooleanExpression; import io.opentdf.platform.sdk.Autoconfigure.Granter.BooleanKeyExpression; +import io.opentdf.platform.sdk.Autoconfigure.KeySplitStep; import io.opentdf.platform.sdk.Autoconfigure.Granter; -import io.opentdf.platform.sdk.Autoconfigure.SplitStep; import io.opentdf.platform.policy.Namespace; +import io.opentdf.platform.policy.PublicKey; import io.opentdf.platform.policy.KeyAccessServer; import io.opentdf.platform.policy.AttributeRuleTypeEnum; +import io.opentdf.platform.policy.KasPublicKey; +import io.opentdf.platform.policy.KasPublicKeyAlgEnum; +import io.opentdf.platform.policy.KasPublicKeySet; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.google.common.util.concurrent.SettableFuture; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.regex.Matcher; import java.util.regex.Pattern; public class AutoconfigureTest { - private static final String KAS_AU = "http://kas.au/"; - private static final String KAS_CA = "http://kas.ca/"; - private static final String KAS_UK = "http://kas.uk/"; - private static final String KAS_NZ = "http://kas.nz/"; - private static final String KAS_US = "http://kas.us/"; - private static final String KAS_US_HCS = "http://hcs.kas.us/"; - private static final String KAS_US_SA = "http://si.kas.us/"; + private static final String KAS_AU = "https://kas.au/"; + private static final String KAS_CA = "https://kas.ca/"; + private static final String KAS_UK = "https://kas.uk/"; + private static final String KAS_NZ = "https://kas.nz/"; + private static final String KAS_US = "https://kas.us/"; + private static final String KAS_US_HCS = "https://hcs.kas.us/"; + private static final String KAS_US_SA = "https://si.kas.us/"; private static final String AUTHORITY = "https://virtru.com/"; + public static final String OTHER_AUTH = "https://other.com/"; + public static final String SPECIFIED_KAS = "https://attr.kas.com/"; + public static final String EVEN_MORE_SPECIFIC_KAS = "https://value.kas.com/"; private static Autoconfigure.AttributeNameFQN CLS; private static Autoconfigure.AttributeNameFQN N2K; private static Autoconfigure.AttributeNameFQN REL; + private static Autoconfigure.AttributeNameFQN UNSPECKED; + private static Autoconfigure.AttributeNameFQN SPECKED; private static Autoconfigure.AttributeValueFQN clsA; private static Autoconfigure.AttributeValueFQN clsS; @@ -51,6 +74,10 @@ public class AutoconfigureTest { private static Autoconfigure.AttributeValueFQN rel2gbr; private static Autoconfigure.AttributeValueFQN rel2nzl; private static Autoconfigure.AttributeValueFQN rel2usa; + private static Autoconfigure.AttributeValueFQN uns2uns; + private static Autoconfigure.AttributeValueFQN uns2spk; + private static Autoconfigure.AttributeValueFQN spk2uns; + private static Autoconfigure.AttributeValueFQN spk2spk; @BeforeAll public static void setup() throws AutoConfigureException { @@ -58,6 +85,9 @@ public static void setup() throws AutoConfigureException { CLS = new Autoconfigure.AttributeNameFQN("https://virtru.com/attr/Classification"); N2K = new Autoconfigure.AttributeNameFQN("https://virtru.com/attr/Need%20to%20Know"); 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"); + clsA = new Autoconfigure.AttributeValueFQN("https://virtru.com/attr/Classification/value/Allowed"); clsS = new Autoconfigure.AttributeValueFQN("https://virtru.com/attr/Classification/value/Secret"); @@ -71,6 +101,11 @@ public static void setup() throws AutoConfigureException { rel2gbr = new Autoconfigure.AttributeValueFQN("https://virtru.com/attr/Releasable%20To/value/GBR"); rel2nzl = new Autoconfigure.AttributeValueFQN("https://virtru.com/attr/Releasable%20To/value/NZL"); rel2usa = new Autoconfigure.AttributeValueFQN("https://virtru.com/attr/Releasable%20To/value/USA"); + + uns2uns = new Autoconfigure.AttributeValueFQN("https://other.com/attr/unspecified/value/unspecked"); + uns2spk = new Autoconfigure.AttributeValueFQN("https://other.com/attr/unspecified/value/specked"); + spk2uns = new Autoconfigure.AttributeValueFQN("https://other.com/attr/specified/value/unspecked"); + spk2spk = new Autoconfigure.AttributeValueFQN("https://other.com/attr/specified/value/specked"); } private static String spongeCase(String s) { @@ -132,22 +167,31 @@ private Autoconfigure.AttributeValueFQN messUpV(Autoconfigure.AttributeValueFQN } 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(); String key = fqn.getKey(); if (key.equals(CLS.getKey())){ - return Attribute.newBuilder().setId("CLS").setNamespace( - Namespace.newBuilder().setId("v").setName("virtru.com").setFqn("https://virtru.com").build()) + return Attribute.newBuilder().setId("CLS").setNamespace(ns1) .setName("Classification").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY).setFqn(fqn.toString()).build(); } else if (key.equals(N2K.getKey())) { - return Attribute.newBuilder().setId("N2K").setNamespace( - Namespace.newBuilder().setId("v").setName("virtru.com").setFqn("https://virtru.com").build()) + return Attribute.newBuilder().setId("N2K").setNamespace(ns1) .setName("Need to Know").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF).setFqn(fqn.toString()).build(); } else if (key.equals(REL.getKey())) { - return Attribute.newBuilder().setId("REL").setNamespace( - Namespace.newBuilder().setId("v").setName("virtru.com").setFqn("https://virtru.com").build()) + return Attribute.newBuilder().setId("REL").setNamespace(ns1) .setName("Releasable To").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF).setFqn(fqn.toString()).build(); } + else if (key.equals(SPECKED.getKey())) { + 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()) + .build(); + } + else if (key.equals(UNSPECKED.getKey())) { + return Attribute.newBuilder().setId("UNS").setNamespace(ns2) + .setName("unspecified").setRule(AttributeRuleTypeEnum.ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF).setFqn(fqn.toString()).build(); + } else { return null; } @@ -155,9 +199,7 @@ else if (key.equals(REL.getKey())) { private Value mockValueFor(Autoconfigure.AttributeValueFQN fqn) throws AutoConfigureException { Autoconfigure.AttributeNameFQN an = fqn.prefix(); - System.out.println(an); Attribute a = mockAttributeFor(an); - System.out.println(a); String v = fqn.value(); Value p = Value.newBuilder() .setId(a.getId() + ":" + v) @@ -214,16 +256,28 @@ else if (an.getKey().equals(REL.getKey())) { else if (an.getKey().equals(CLS.getKey())){ // defaults only } + else if (an.getKey().equals(SPECKED.getKey())){ + if (fqn.value().toLowerCase().equals("specked")){ + p = p.toBuilder().addGrants(KeyAccessServer.newBuilder().setUri(EVEN_MORE_SPECIFIC_KAS).build()) + .build(); + } + } + else if (an.getKey().equals(UNSPECKED.getKey())){ + if (fqn.value().toLowerCase().equals("specked")){ + p = p.toBuilder().addGrants(KeyAccessServer.newBuilder().setUri(EVEN_MORE_SPECIFIC_KAS).build()) + .build(); + } + } return p; } @Test public void testAttributeFromURL() throws AutoConfigureException { for (TestCase tc : List.of( - new TestCase("letter", "http://e/attr/a", "http://e", "a"), - new TestCase("number", "http://e/attr/1", "http://e", "1"), - new TestCase("emoji", "http://e/attr/%F0%9F%98%81", "http://e", "😁"), - new TestCase("dash", "http://a-b.com/attr/b-c", "http://a-b.com", "b-c") + new TestCase("letter", "https://e/attr/a", "https://e", "a"), + new TestCase("number", "https://e/attr/1", "https://e", "1"), + new TestCase("emoji", "https://e/attr/%F0%9F%98%81", "https://e", "😁"), + new TestCase("dash", "https://a-b.com/attr/b-c", "https://a-b.com", "b-c") )) { Autoconfigure.AttributeNameFQN a = new Autoconfigure.AttributeNameFQN(tc.getU()); assertThat(a.authority()).isEqualTo(tc.getAuth()); @@ -234,13 +288,13 @@ public void testAttributeFromURL() throws AutoConfigureException { @Test public void testAttributeFromMalformedURL() { for (TestCase tc : List.of( - new TestCase("no name", "http://e/attr"), + new TestCase("no name", "https://e/attr"), new TestCase("invalid prefix 1", "hxxp://e/attr/a"), new TestCase("invalid prefix 2", "e/attr/a"), new TestCase("invalid prefix 3", "file://e/attr/a"), - new TestCase("invalid prefix 4", "http:///attr/a"), + new TestCase("invalid prefix 4", "https:///attr/a"), new TestCase("bad encoding", "https://a/attr/%😁"), - new TestCase("with value", "http://e/attr/a/value/b") + new TestCase("with value", "https://e/attr/a/value/b") )) { assertThatThrownBy(() -> new Autoconfigure.AttributeNameFQN(tc.getU())) .isInstanceOf(AutoConfigureException.class); @@ -250,12 +304,12 @@ public void testAttributeFromMalformedURL() { @Test public void testAttributeValueFromURL() { List testCases = List.of( - new TestCase("number", "http://e/attr/a/value/1", "http://e", "a", "1"), - new TestCase("space", "http://e/attr/a/value/%20", "http://e", "a", " "), - new TestCase("emoji", "http://e/attr/a/value/%F0%9F%98%81", "http://e", "a", "😁"), - new TestCase("numberdef", "http://e/attr/1/value/one", "http://e", "1", "one"), - new TestCase("valuevalue", "http://e/attr/value/value/one", "http://e", "value", "one"), - new TestCase("dash", "http://a-b.com/attr/b-c/value/c-d", "http://a-b.com", "b-c", "c-d") + new TestCase("number", "https://e/attr/a/value/1", "https://e", "a", "1"), + new TestCase("space", "https://e/attr/a/value/%20", "https://e", "a", " "), + new TestCase("emoji", "https://e/attr/a/value/%F0%9F%98%81", "https://e", "a", "😁"), + new TestCase("numberdef", "https://e/attr/1/value/one", "https://e", "1", "one"), + new TestCase("valuevalue", "https://e/attr/value/value/one", "https://e", "value", "one"), + new TestCase("dash", "https://a-b.com/attr/b-c/value/c-d", "https://a-b.com", "b-c", "c-d") ); for (TestCase tc : testCases) { @@ -271,8 +325,8 @@ public void testAttributeValueFromURL() { @Test public void testAttributeValueFromMalformedURL() { List testCases = List.of( - new TestCase("no name", "http://e/attr/value/1"), - new TestCase("no value", "http://e/attr/who/value"), + new TestCase("no name", "https://e/attr/value/1"), + new TestCase("no value", "https://e/attr/who/value"), new TestCase("invalid prefix 1", "hxxp://e/attr/a/value/1"), new TestCase("invalid prefix 2", "e/attr/a/a/value/1"), new TestCase("bad encoding", "https://a/attr/emoji/value/%😁") @@ -296,7 +350,6 @@ public void testConfigurationServicePutGet() { ); for (ConfigurationTestCase tc : testCases) { - System.out.println(tc.name); assertDoesNotThrow(() -> { List v = valuesToPolicy(tc.getPolicy().toArray(new AttributeValueFQN[0])); Granter grants = Autoconfigure.newGranterFromAttributes(v.toArray(new Value[0])); @@ -326,9 +379,9 @@ public void testReasonerConstructAttributeBoolean() { List.of(clsS, rel2can), List.of(KAS_US), "https://virtru.com/attr/Classification/value/Secret&https://virtru.com/attr/Releasable%20To/value/CAN", - "[DEFAULT]&(http://kas.ca/)", - "(http://kas.ca/)", - List.of(new SplitStep(KAS_CA, "")) + "[DEFAULT]&(https://kas.ca/)", + "(https://kas.ca/)", + List.of(new KeySplitStep(KAS_CA, "")) ), new ReasonerTestCase( "one defaulted attr", @@ -337,7 +390,7 @@ public void testReasonerConstructAttributeBoolean() { "https://virtru.com/attr/Classification/value/Secret", "[DEFAULT]", "", - List.of(new SplitStep(KAS_US, "")) + List.of(new KeySplitStep(KAS_US, "")) ), new ReasonerTestCase( "empty policy", @@ -346,7 +399,7 @@ public void testReasonerConstructAttributeBoolean() { "∅", "", "", - List.of(new SplitStep(KAS_US, "")) + List.of(new KeySplitStep(KAS_US, "")) ), new ReasonerTestCase( "old school splits", @@ -355,25 +408,25 @@ public void testReasonerConstructAttributeBoolean() { "∅", "", "", - List.of(new SplitStep(KAS_AU, "1"), new SplitStep(KAS_CA, "2"), new SplitStep(KAS_US, "3")) + List.of(new KeySplitStep(KAS_AU, "1"), new KeySplitStep(KAS_CA, "2"), new KeySplitStep(KAS_US, "3")) ), new ReasonerTestCase( "simple with all three ops", List.of(clsS, rel2gbr, n2kInt), List.of(KAS_US), "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]&(http://kas.uk/)&(http://kas.uk/)", - "(http://kas.uk/)", - List.of(new SplitStep(KAS_UK, "")) + "[DEFAULT]&(https://kas.uk/)&(https://kas.uk/)", + "(https://kas.uk/)", + List.of(new KeySplitStep(KAS_UK, "")) ), new ReasonerTestCase( "compartments", List.of(clsS, rel2gbr, rel2usa, n2kHCS, n2kSI), List.of(KAS_US), "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]&(http://kas.uk/⋁http://kas.us/)&(http://hcs.kas.us/⋀http://si.kas.us/)", - "(http://kas.uk/⋁http://kas.us/)&(http://hcs.kas.us/)&(http://si.kas.us/)", - List.of(new SplitStep(KAS_UK, "1"), new SplitStep(KAS_US, "1"), new SplitStep(KAS_US_HCS, "2"), new SplitStep(KAS_US_SA, "3")) + "[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")) ), new ReasonerTestCase( "compartments - case insensitive", @@ -382,9 +435,9 @@ public void testReasonerConstructAttributeBoolean() { ), List.of(KAS_US), "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]&(http://kas.uk/⋁http://kas.us/)&(http://hcs.kas.us/⋀http://si.kas.us/)", - "(http://kas.uk/⋁http://kas.us/)&(http://hcs.kas.us/)&(http://si.kas.us/)", - List.of(new SplitStep(KAS_UK, "1"), new SplitStep(KAS_US, "1"), new SplitStep(KAS_US_HCS, "2"), new SplitStep(KAS_US_SA, "3")) + "[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")) ) ); @@ -403,7 +456,7 @@ public void testReasonerConstructAttributeBoolean() { assertThat(reduced).isEqualTo(tc.getReduced()); var wrapper = new Object(){ int i = 0; }; - List plan = reasoner.plan(tc.getDefaults(), () -> { + List plan = reasoner.plan(tc.getDefaults(), () -> { return String.valueOf(wrapper.i++ + 1); } @@ -415,6 +468,120 @@ public void testReasonerConstructAttributeBoolean() { } } + GetAttributeValuesByFqnsResponse getResponse(GetAttributeValuesByFqnsRequest req) { + GetAttributeValuesByFqnsResponse.Builder builder = GetAttributeValuesByFqnsResponse.newBuilder(); + + for (String v : req.getFqnsList()) { + AttributeValueFQN vfqn; + try { + vfqn = new AttributeValueFQN(v); + } catch (Exception e) { + return null; // Or throw the exception as needed + } + + Value val = mockValueFor(vfqn); + + builder.putFqnAttributeValues(v, GetAttributeValuesByFqnsResponse.AttributeAndValue.newBuilder() + .setAttribute(val.getAttribute()) + .setValue(val) + .build()); + } + + return builder.build(); + } + + @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.spk & uns.uns => spk", + List.of(uns2spk, uns2uns), + List.of(KAS_US), + List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "")) + ), + new ReasonerTestCase( + "uns.uns & uns.spk => spk", + List.of(uns2uns, uns2spk), + List.of(KAS_US), + List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "")) + ), + new ReasonerTestCase( + "uns.uns & uns.spk => spk", + List.of(uns2uns, spk2spk), + List.of(KAS_US), + List.of(new KeySplitStep(EVEN_MORE_SPECIFIC_KAS, "")) + ) + ); + + for (ReasonerTestCase tc : testCases) { + assertDoesNotThrow(() -> { + AttributesServiceGrpc.AttributesServiceFutureStub attributeGrpcStub = mock(AttributesServiceGrpc.AttributesServiceFutureStub.class); + lenient().when(attributeGrpcStub.getAttributeValuesByFqns(any(GetAttributeValuesByFqnsRequest.class))).thenAnswer( + invocation -> { + GetAttributeValuesByFqnsResponse resp = getResponse((GetAttributeValuesByFqnsRequest) invocation.getArguments()[0]); + SettableFuture future = SettableFuture.create(); + future.set(resp); // Set the request as the future's result + return future; + }); + + Granter reasoner = Autoconfigure.newGranterFromService(attributeGrpcStub, new KASKeyCache(), tc.getPolicy().toArray(new AttributeValueFQN[0])); + assertThat(reasoner).isNotNull(); + + var wrapper = new Object(){ int i = 0; }; + List plan = reasoner.plan(tc.getDefaults(), () -> { + return String.valueOf(wrapper.i++ + 1); + } + + ); + assertThat(plan.size()).isEqualTo(tc.getPlan().size()); + assertThat(plan).hasSameElementsAs(tc.getPlan()); + } + ); + } + } + private static class TestCase { private final String n; @@ -498,9 +665,9 @@ 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) { + ReasonerTestCase(String name, List policy, List defaults, String ats, String keyed, String reduced, List plan) { this.name = name; this.policy = policy; this.defaults = defaults; @@ -510,6 +677,16 @@ private static class ReasonerTestCase { this.plan = plan; } + ReasonerTestCase(String name, List policy, List defaults, List plan) { + this.name = name; + this.policy = policy; + this.defaults = defaults; + this.plan = plan; + this.ats = null; + this.keyed = null; + this.reduced = null; + } + public String getN() { return name; } @@ -534,8 +711,205 @@ public String getReduced() { return reduced; } - public List getPlan() { + public List getPlan() { return plan; } } + + @Test + void testStoreKeysToCache_NoKeys() { + KASKeyCache keyCache = Mockito.mock(KASKeyCache.class); + KeyAccessServer kas1 = KeyAccessServer.newBuilder().setPublicKey( + PublicKey.newBuilder().setCached( + KasPublicKeySet.newBuilder()) + ).build(); + + + List kases = List.of(kas1); + + Autoconfigure.storeKeysToCache(kases, keyCache); + + verify(keyCache, never()).store(any(Config.KASInfo.class)); + } + + @Test + void testStoreKeysToCache_WithKeys() { + // Create a real KASKeyCache instance instead of mocking it + KASKeyCache keyCache = new KASKeyCache(); + + // Create the KasPublicKey object + KasPublicKey kasPublicKey1 = KasPublicKey.newBuilder() + .setAlg(KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1) + .setKid("test-kid") + .setPem("public-key-pem") + .build(); + + // Add the KasPublicKey to a list + List kasPublicKeys = new ArrayList<>(); + kasPublicKeys.add(kasPublicKey1); + + // Create the KeyAccessServer object + KeyAccessServer kas1 = KeyAccessServer.newBuilder() + .setPublicKey(PublicKey.newBuilder() + .setCached(KasPublicKeySet.newBuilder() + .addAllKeys(kasPublicKeys) + .build()) + ) + .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); + + // Verify that the key was stored in the cache + Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); + 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); + } + + @Test + void testStoreKeysToCache_MultipleKasEntries() { + // Create a real KASKeyCache instance instead of mocking it + KASKeyCache keyCache = new KASKeyCache(); + + // Create the KasPublicKey object + KasPublicKey kasPublicKey1 = KasPublicKey.newBuilder() + .setAlg(KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1) + .setKid("test-kid") + .setPem("public-key-pem") + .build(); + KasPublicKey kasPublicKey2 = KasPublicKey.newBuilder() + .setAlg(KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048) + .setKid("test-kid-2") + .setPem("public-key-pem-2") + .build(); + + // Add the KasPublicKey to a list + List kasPublicKeys = new ArrayList<>(); + kasPublicKeys.add(kasPublicKey1); + kasPublicKeys.add(kasPublicKey2); + + // Create the KeyAccessServer object + KeyAccessServer kas1 = KeyAccessServer.newBuilder() + .setPublicKey(PublicKey.newBuilder() + .setCached(KasPublicKeySet.newBuilder() + .addAllKeys(kasPublicKeys) + .build()) + ) + .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); + + // Verify that the key was stored in the cache + Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); + 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"); + assertNotNull(storedKASInfo2); + assertEquals("https://example.com/kas", storedKASInfo2.URL); + assertEquals("test-kid-2", storedKASInfo2.KID); + assertEquals("rsa:2048", storedKASInfo2.Algorithm); + assertEquals("public-key-pem-2", storedKASInfo2.PublicKey); + } + + + GetAttributeValuesByFqnsResponse getResponseWithGrants(GetAttributeValuesByFqnsRequest req, List grants) { + GetAttributeValuesByFqnsResponse.Builder builder = GetAttributeValuesByFqnsResponse.newBuilder(); + + for (String v : req.getFqnsList()) { + AttributeValueFQN vfqn; + try { + vfqn = new AttributeValueFQN(v); + } catch (Exception e) { + return null; // Or throw the exception as needed + } + + Value val = Value.newBuilder(mockValueFor(vfqn)).addAllGrants(grants).build(); + + builder.putFqnAttributeValues(v, GetAttributeValuesByFqnsResponse.AttributeAndValue.newBuilder() + .setAttribute(val.getAttribute()) + .setValue(val) + .build()); + } + + return builder.build(); + } + + + @Test + void testKeyCacheFromGrants() throws InterruptedException, ExecutionException { + // Create the KasPublicKey object + KasPublicKey kasPublicKey1 = KasPublicKey.newBuilder() + .setAlg(KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1) + .setKid("test-kid") + .setPem("public-key-pem") + .build(); + KasPublicKey kasPublicKey2 = KasPublicKey.newBuilder() + .setAlg(KasPublicKeyAlgEnum.KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048) + .setKid("test-kid-2") + .setPem("public-key-pem-2") + .build(); + + // Add the KasPublicKey to a list + List kasPublicKeys = new ArrayList<>(); + kasPublicKeys.add(kasPublicKey1); + kasPublicKeys.add(kasPublicKey2); + + // Create the KeyAccessServer object + KeyAccessServer kas1 = KeyAccessServer.newBuilder() + .setPublicKey(PublicKey.newBuilder() + .setCached(KasPublicKeySet.newBuilder() + .addAllKeys(kasPublicKeys) + .build()) + ) + .setUri("https://example.com/kas") + .build(); + + AttributesServiceGrpc.AttributesServiceFutureStub attributeGrpcStub = mock(AttributesServiceGrpc.AttributesServiceFutureStub.class); + lenient().when(attributeGrpcStub.getAttributeValuesByFqns(any(GetAttributeValuesByFqnsRequest.class))).thenAnswer( + invocation -> { + GetAttributeValuesByFqnsResponse resp = getResponseWithGrants((GetAttributeValuesByFqnsRequest) invocation.getArguments()[0], List.of(kas1)); + SettableFuture future = SettableFuture.create(); + future.set(resp); // Set the request as the future's result + return future; + }); + + KASKeyCache keyCache = new KASKeyCache(); + + Granter reasoner = Autoconfigure.newGranterFromService(attributeGrpcStub, keyCache, List.of(clsS, rel2gbr, rel2usa, n2kHCS, n2kSI).toArray(new AttributeValueFQN[0])); + assertThat(reasoner).isNotNull(); + + // Verify that the key was stored in the cache + Config.KASInfo storedKASInfo = keyCache.get("https://example.com/kas", "ec:secp256r1"); + 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"); + assertNotNull(storedKASInfo2); + assertEquals("https://example.com/kas", storedKASInfo2.URL); + assertEquals("test-kid-2", storedKASInfo2.KID); + assertEquals("rsa:2048", storedKASInfo2.Algorithm); + assertEquals("public-key-pem-2", storedKASInfo2.PublicKey); + + } + + } diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/KASClientTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/KASClientTest.java index 14b79bfc..8f816548 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/KASClientTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/KASClientTest.java @@ -61,7 +61,7 @@ public void publicKey(PublicKeyRequest request, StreamObserver keypairs = new ArrayList<>(); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java index f2e8b069..af4d32cb 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -51,15 +51,13 @@ public void setup() { public void close() {} @Override - public String getPublicKey(Config.KASInfo kasInfo) { + public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { int index = Integer.parseInt(kasInfo.URL); - - return CryptoUtils.getRSAPublicKeyPEM(keypairs.get(index).getPublic()); - } - - @Override - public String getKid(Config.KASInfo kasInfo) { - return "r1"; + var kiCopy = new Config.KASInfo(); + kiCopy.KID = "r1"; + kiCopy.PublicKey = CryptoUtils.getRSAPublicKeyPEM(keypairs.get(index).getPublic()); + kiCopy.URL = kasInfo.URL; + return kiCopy; } @Override @@ -83,6 +81,11 @@ public String getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) { return null; } + + @Override + public KASKeyCache getKeyCache(){ + return new KASKeyCache(); + } }; AttributesServiceGrpc.AttributesServiceFutureStub attributeGrpcStub;