From 6389e12ebe4e27c3f1f43435e8b361c9b54b6ba6 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Thu, 24 Apr 2025 15:51:55 -0400 Subject: [PATCH 1/7] allowlist implementation --- .github/workflows/checks.yaml | 38 ++-- README.md | 2 +- .../java/io/opentdf/platform/Command.java | 34 +++- .../platform/DecryptCollectionExample.java | 6 +- .../io/opentdf/platform/DecryptExample.java | 7 +- .../java/io/opentdf/platform/sdk/Config.java | 91 ++++++++++ .../io/opentdf/platform/sdk/KASClient.java | 2 +- .../java/io/opentdf/platform/sdk/NanoTDF.java | 50 +++++- .../java/io/opentdf/platform/sdk/SDK.java | 22 ++- .../io/opentdf/platform/sdk/SDKBuilder.java | 5 +- .../java/io/opentdf/platform/sdk/TDF.java | 44 +++++ .../java/io/opentdf/platform/sdk/Fuzzing.java | 3 +- .../io/opentdf/platform/sdk/NanoTDFTest.java | 121 ++++++++++++- .../io/opentdf/platform/sdk/TDFE2ETest.java | 7 +- .../java/io/opentdf/platform/sdk/TDFTest.java | 163 ++++++++++++++++-- 15 files changed, 536 insertions(+), 59 deletions(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 085549b9..070a0468 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -152,21 +152,21 @@ jobs: java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ - encrypt --kas-url=localhost:8080 --mime-type=text/plain --attr https://example.com/attr/attr1/value/value1 --autoconfigure=false -f data -m 'here is some metadata' > test.tdf + encrypt --kas-url=http://localhost:8080 --mime-type=text/plain --attr https://example.com/attr/attr1/value/value1 --autoconfigure=false -f data -m 'here is some metadata' > test.tdf java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ decrypt -f test.tdf > decrypted java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ metadata -f test.tdf > metadata @@ -188,14 +188,14 @@ jobs: java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ encryptnano --kas-url=http://localhost:8080 --attr https://example.com/attr/attr1/value/value1 -f data -m 'here is some metadata' > nano.ntdf java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ decryptnano -f nano.ntdf > decrypted @@ -215,14 +215,14 @@ jobs: java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ - encrypt --kas-url=localhost:8080 --mime-type=text/plain --with-assertions=$ASSERTIONS --autoconfigure=false -f data -m 'here is some metadata' > test.tdf + encrypt --kas-url=http://localhost:8080 --mime-type=text/plain --with-assertions=$ASSERTIONS --autoconfigure=false -f data -m 'here is some metadata' > test.tdf java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ decrypt -f test.tdf > decrypted @@ -246,14 +246,14 @@ jobs: java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ - encrypt --kas-url=localhost:8080 --mime-type=text/plain --with-assertions="$SIGNED_ASSERTIONS_HS256" --autoconfigure=false -f data -m 'here is some metadata' > test.tdf + encrypt --kas-url=http://localhost:8080 --mime-type=text/plain --with-assertions="$SIGNED_ASSERTIONS_HS256" --autoconfigure=false -f data -m 'here is some metadata' > test.tdf java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ decrypt --with-assertion-verification-keys="$SIGNED_ASSERTION_VERIFICATON_HS256" -f test.tdf > decrypted @@ -267,14 +267,14 @@ jobs: java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ - encrypt --kas-url=localhost:8080 --mime-type=text/plain --with-assertions "$SIGNED_ASSERTIONS_RS256" --autoconfigure=false -f data -m 'here is some metadata' > test.tdf + encrypt --kas-url=http://localhost:8080 --mime-type=text/plain --with-assertions "$SIGNED_ASSERTIONS_RS256" --autoconfigure=false -f data -m 'here is some metadata' > test.tdf java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ decrypt --with-assertion-verification-keys "$SIGNED_ASSERTION_VERIFICATON_RS256" -f test.tdf > decrypted @@ -300,21 +300,21 @@ jobs: java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ - encrypt --kas-url=localhost:8080,localhost:8282 -f data -m 'here is some metadata' > test.tdf + encrypt --kas-url=http://localhost:8080,localhost:8282 -f data -m 'here is some metadata' > test.tdf java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ decrypt -f test.tdf > decrypted java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ - --platform-endpoint=localhost:8080 \ + --platform-endpoint=http://localhost:8080 \ -h\ metadata -f test.tdf > metadata diff --git a/README.md b/README.md index 2de45dcd..ebd5ac08 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ public class Example { // Decrypt a file try (SeekableByteChannel in = FileChannel.open("input.ciphertext", StandardOpenOption.READ)) { - TDF.Reader reader = new TDF().loadTDF(in, sdk.getServices().kas()); + TDF.Reader reader = new TDF().loadTDF(in, sdk.getServices().kas(), sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); reader.readPayload(System.out); } }} diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index e8b88f6e..c5fce933 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -24,6 +24,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.PrintWriter; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; @@ -245,8 +246,10 @@ private SDK buildSDK() { void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, @Option(names = { "--rewrap-key-type" }, defaultValue = Option.NULL_VALUE, description = "Preferred rewrap algorithm, one of ${COMPLETION-CANDIDATES}") Optional rewrapKeyType, @Option(names = { "--with-assertion-verification-disabled" }, defaultValue = "false") boolean disableAssertionVerification, - @Option(names = { "--with-assertion-verification-keys" }, defaultValue = Option.NULL_VALUE) Optional assertionVerification) - throws IOException, TDF.FailedToCreateGMAC, JOSEException, ParseException, NoSuchAlgorithmException, DecoderException { + @Option(names = { "--with-assertion-verification-keys" }, defaultValue = Option.NULL_VALUE) Optional assertionVerification, + @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, + @Option(names = { "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) + throws IOException, TDF.FailedToCreateGMAC, JOSEException, ParseException, NoSuchAlgorithmException, DecoderException, InterruptedException, ExecutionException, URISyntaxException { var sdk = buildSDK(); var opts = new ArrayList>(); try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) { @@ -286,8 +289,15 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, } rewrapKeyType.map(Config::WithSessionKeyType).ifPresent(opts::add); + if (ignoreAllowlist.isPresent()) { + opts.add(Config.WithIgnoreKasAllowlist(ignoreAllowlist.get())); + } + if (kasAllowlistStr.isPresent()) { + opts.add(Config.WithKasAllowlist(kasAllowlistStr.get().split(","))); + } + var readerConfig = Config.newTDFReaderConfig(opts.toArray(new Consumer[0])); - var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig); + var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig, sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); reader.readPayload(stdout); } } @@ -295,12 +305,12 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, @CommandLine.Command(name = "metadata") void readMetadata(@Option(names = { "-f", "--file" }, required = true) Path tdfPath) throws IOException, - TDF.FailedToCreateGMAC, JOSEException, NoSuchAlgorithmException, ParseException, DecoderException { + TDF.FailedToCreateGMAC, JOSEException, NoSuchAlgorithmException, ParseException, DecoderException, InterruptedException, ExecutionException, URISyntaxException { var sdk = buildSDK(); try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) { try (var stdout = new PrintWriter(System.out)) { - var reader = new TDF().loadTDF(in, sdk.getServices().kas()); + var reader = new TDF().loadTDF(in, sdk.getServices().kas(), sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); stdout.write(reader.getMetadata() == null ? "" : reader.getMetadata()); } } @@ -337,7 +347,9 @@ void createNanoTDF( } @CommandLine.Command(name = "decryptnano") - void readNanoTDF(@Option(names = { "-f", "--file" }, required = true) Path nanoTDFPath) throws Exception { + void readNanoTDF(@Option(names = { "-f", "--file" }, required = true) Path nanoTDFPath, + @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, + @Option(names = { "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) throws Exception { var sdk = buildSDK(); try (var in = FileChannel.open(nanoTDFPath, StandardOpenOption.READ)) { try (var stdout = new BufferedOutputStream(System.out)) { @@ -345,7 +357,15 @@ void readNanoTDF(@Option(names = { "-f", "--file" }, required = true) Path nanoT ByteBuffer buffer = ByteBuffer.allocate((int) in.size()); in.read(buffer); buffer.flip(); - ntdf.readNanoTDF(buffer, stdout, sdk.getServices().kas()); + var opts = new ArrayList>(); + if (ignoreAllowlist.isPresent()) { + opts.add(Config.WithNanoIgnoreKasAllowlist(ignoreAllowlist.get())); + } + if (kasAllowlistStr.isPresent()) { + opts.add(Config.WithNanoKasAllowlist(kasAllowlistStr.get().split(","))); + } + var readerConfig = Config.newNanoTDFReaderConfig(opts.toArray(new Consumer[0])); + ntdf.readNanoTDF(buffer, stdout, sdk.getServices().kas(), readerConfig, sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); } } } diff --git a/examples/src/main/java/io/opentdf/platform/DecryptCollectionExample.java b/examples/src/main/java/io/opentdf/platform/DecryptCollectionExample.java index 9e2d1b08..7a03c764 100644 --- a/examples/src/main/java/io/opentdf/platform/DecryptCollectionExample.java +++ b/examples/src/main/java/io/opentdf/platform/DecryptCollectionExample.java @@ -9,12 +9,14 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; +import java.util.concurrent.ExecutionException; public class DecryptCollectionExample { - public static void main(String[] args) throws IOException, NanoTDF.NanoTDFMaxSizeLimit, NanoTDF.UnsupportedNanoTDFFeature, NanoTDF.InvalidNanoTDFConfig, NoSuchAlgorithmException, InterruptedException { + public static void main(String[] args) throws IOException, NanoTDF.NanoTDFMaxSizeLimit, NanoTDF.UnsupportedNanoTDFFeature, NanoTDF.InvalidNanoTDFConfig, NoSuchAlgorithmException, InterruptedException, ExecutionException, URISyntaxException { String clientId = "opentdf-sdk"; String clientSecret = "secret"; String platformEndpoint = "localhost:8080"; @@ -33,7 +35,7 @@ public static void main(String[] args) throws IOException, NanoTDF.NanoTDFMaxSiz for (int i = 0; i < 50; i++) { FileInputStream fis = new FileInputStream(String.format("out/my.%d_ciphertext", i)); - nanoTDFClient.readNanoTDF(ByteBuffer.wrap(fis.readAllBytes()), System.out, sdk.getServices().kas()); + nanoTDFClient.readNanoTDF(ByteBuffer.wrap(fis.readAllBytes()), System.out, sdk.getServices().kas(), sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); fis.close(); } diff --git a/examples/src/main/java/io/opentdf/platform/DecryptExample.java b/examples/src/main/java/io/opentdf/platform/DecryptExample.java index 92698ab2..e2f43f7c 100644 --- a/examples/src/main/java/io/opentdf/platform/DecryptExample.java +++ b/examples/src/main/java/io/opentdf/platform/DecryptExample.java @@ -7,10 +7,13 @@ import java.nio.file.Paths; import com.nimbusds.jose.JOSEException; import java.io.IOException; +import java.net.URISyntaxException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.ParseException; +import java.util.concurrent.ExecutionException; + import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; @@ -21,7 +24,7 @@ public class DecryptExample { public static void main(String[] args) throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, TDF.FailedToCreateGMAC, - JOSEException, ParseException, NoSuchAlgorithmException, DecoderException, org.apache.commons.cli.ParseException { + JOSEException, ParseException, NoSuchAlgorithmException, DecoderException, org.apache.commons.cli.ParseException, InterruptedException, ExecutionException, URISyntaxException { // Create Options object Options options = new Options(); @@ -53,7 +56,7 @@ public static void main(String[] args) throws IOException, Path path = Paths.get("my.ciphertext"); try (var in = FileChannel.open(path, StandardOpenOption.READ)) { - var reader = new TDF().loadTDF(in, sdk.getServices().kas(), Config.newTDFReaderConfig(Config.WithSessionKeyType(sessionKeyType))); + var reader = new TDF().loadTDF(in, sdk.getServices().kas(), Config.newTDFReaderConfig(Config.WithSessionKeyType(sessionKeyType)), sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); reader.readPayload(System.out); } 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 3e46649d..bf3b9d2a 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -8,8 +8,11 @@ import io.opentdf.platform.policy.Value; +import java.net.URI; +import java.net.URISyntaxException; import java.util.*; import java.util.function.Consumer; +import java.util.stream.Collectors; /** * Configuration class for setting various configurations related to TDF. @@ -98,6 +101,8 @@ public static class TDFReaderConfig { AssertionVerificationKeys assertionVerificationKeys = new AssertionVerificationKeys(); boolean disableAssertionVerification; KeyType sessionKeyType; + Set kasAllowlist; + boolean ignoreKasAllowlist; } @SafeVarargs @@ -105,6 +110,7 @@ public static TDFReaderConfig newTDFReaderConfig(Consumer... op TDFReaderConfig config = new TDFReaderConfig(); config.disableAssertionVerification = false; config.sessionKeyType = KeyType.RSA2048Key; + config.kasAllowlist = new HashSet<>(); for (Consumer option : options) { option.accept(config); } @@ -123,6 +129,30 @@ public static Consumer withDisableAssertionVerification(boolean public static Consumer WithSessionKeyType(KeyType keyType) { return (TDFReaderConfig config) -> config.sessionKeyType = keyType; } + public static Consumer WithKasAllowlist(String... kasAllowlist) { + return (TDFReaderConfig config) -> { + config.kasAllowlist = Arrays.stream(kasAllowlist) + .map(s -> { + try { + return getKasAddress(s); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid URI: " + s, e); + } + }).collect(Collectors.toCollection(HashSet::new)); + }; + } + + public static Consumer withKasAllowlist(Set kasAllowlist) { + return (TDFReaderConfig config) -> { + config.kasAllowlist = kasAllowlist; + }; + } + + public static Consumer WithIgnoreKasAllowlist(boolean ignore) { + return (TDFReaderConfig config) -> config.ignoreKasAllowlist = ignore; + } + + public static class TDFConfig { public Boolean autoconfigure; public int defaultSegmentSize; @@ -354,6 +384,43 @@ public static Consumer WithECDSAPolicyBinding(boolean enable) { return (NanoTDFConfig config) -> config.eccMode.setECDSABinding(enable); } + public static class NanoTDFReaderConfig { + Set kasAllowlist; + boolean ignoreKasAllowlist; + } + + public static NanoTDFReaderConfig newNanoTDFReaderConfig(Consumer... options) { + NanoTDFReaderConfig config = new NanoTDFReaderConfig(); + for (Consumer option : options) { + option.accept(config); + } + return config; + } + + public static Consumer WithNanoKasAllowlist(String... kasAllowlist) { + return (NanoTDFReaderConfig config) -> { + // apply getKasAddress to each kasAllowlist entry and add to hashset + config.kasAllowlist = Arrays.stream(kasAllowlist) + .map(s -> { + try { + return getKasAddress(s); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid URI: " + s, e); + } + }).collect(Collectors.toCollection(HashSet::new)); + }; + } + + public static Consumer withNanoKasAllowlist(Set kasAllowlist) { + return (NanoTDFReaderConfig config) -> { + config.kasAllowlist = kasAllowlist; + }; + } + + public static Consumer WithNanoIgnoreKasAllowlist(boolean ignore) { + return (NanoTDFReaderConfig config) -> config.ignoreKasAllowlist = ignore; + } + public static class HeaderInfo { private final Header header; private final AesGcm key; @@ -409,4 +476,28 @@ public synchronized void updateHeaderInfo(HeaderInfo headerInfo) { this.notifyAll(); } } + + public static String getKasAddress(String kasURL) throws URISyntaxException { + // Prepend "https://" if no scheme is provided + if (!kasURL.contains("://")) { + kasURL = "https://" + kasURL; + } + + URI uri = new URI(kasURL); + + // Default to "https" if no scheme is provided + String scheme = uri.getScheme(); + if (scheme == null) { + scheme = "https"; + } + + // Default to port 443 if no port is provided + int port = uri.getPort(); + if (port == -1) { + port = 443; + } + + // Reconstruct the URL with only the scheme, host, and port + return new URI(scheme, null, uri.getHost(), port, null, null, null).toString(); + } } 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 def107fc..73f5a5c4 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/KASClient.java @@ -105,7 +105,7 @@ public KASKeyCache getKeyCache() { return this.kasKeyCache; } - private String normalizeAddress(String urlString) { + public static String normalizeAddress(String urlString) { URL url; try { url = new URL(urlString); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java index 5f92069c..74e0631c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java @@ -1,14 +1,20 @@ package io.opentdf.platform.sdk; +import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceGrpc.KeyAccessServerRegistryServiceFutureStub; +import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; +import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse; +import io.opentdf.platform.sdk.TDF.KasAllowlistException; import io.opentdf.platform.sdk.nanotdf.*; import java.io.IOException; import java.io.OutputStream; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.security.*; import java.util.*; +import java.util.concurrent.ExecutionException; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -199,9 +205,39 @@ public int createNanoTDF(ByteBuffer data, OutputStream outputStream, return nanoTDFSize; } + public void readNanoTDF(ByteBuffer nanoTDF, OutputStream outputStream, + SDK.KAS kas) throws IOException, URISyntaxException { + readNanoTDF(nanoTDF, outputStream,kas, Config.newNanoTDFReaderConfig()); + } + + public void readNanoTDF(ByteBuffer nanoTDF, OutputStream outputStream, + SDK.KAS kas, KeyAccessServerRegistryServiceFutureStub kasRegistryService, String platformUrl) throws IOException, InterruptedException, ExecutionException, URISyntaxException { + readNanoTDF(nanoTDF, outputStream,kas, Config.newNanoTDFReaderConfig(), kasRegistryService, platformUrl); + } public void readNanoTDF(ByteBuffer nanoTDF, OutputStream outputStream, - SDK.KAS kas) throws IOException { + SDK.KAS kas, Config.NanoTDFReaderConfig nanoTdfReaderConfig, KeyAccessServerRegistryServiceFutureStub kasRegistryService, String platformUrl) throws IOException, InterruptedException, ExecutionException, URISyntaxException { + if (!nanoTdfReaderConfig.ignoreKasAllowlist && (nanoTdfReaderConfig.kasAllowlist == null || nanoTdfReaderConfig.kasAllowlist.isEmpty())) { + ListKeyAccessServersRequest request = ListKeyAccessServersRequest.newBuilder() + .build(); + ListKeyAccessServersResponse response = kasRegistryService.listKeyAccessServers(request).get(); + nanoTdfReaderConfig.kasAllowlist = new HashSet<>(); + var kases = response.getKeyAccessServersList(); + + for (var entry : kases) { + nanoTdfReaderConfig.kasAllowlist.add(Config.getKasAddress(entry.getUri())); + } + + logger.info("platformUrl: {}", platformUrl); + nanoTdfReaderConfig.kasAllowlist.add(Config.getKasAddress(platformUrl)); + logger.info("KasAllowlist: kas url list is {}", nanoTdfReaderConfig.kasAllowlist); + } + readNanoTDF(nanoTDF, outputStream, kas, nanoTdfReaderConfig); + } + + + public void readNanoTDF(ByteBuffer nanoTDF, OutputStream outputStream, + SDK.KAS kas, Config.NanoTDFReaderConfig nanoTdfReaderConfig) throws IOException, URISyntaxException { Header header = new Header(nanoTDF); CollectionKey cachedKey = collectionStore.getKey(header); @@ -218,6 +254,18 @@ public void readNanoTDF(ByteBuffer nanoTDF, OutputStream outputStream, String kasUrl = header.getKasLocator().getResourceUrl(); + var realAddress = Config.getKasAddress(kasUrl); + if (nanoTdfReaderConfig.ignoreKasAllowlist) { + logger.warn("Ignoring KasAllowlist for url {}", realAddress); + } else if (nanoTdfReaderConfig.kasAllowlist == null || nanoTdfReaderConfig.kasAllowlist.isEmpty()) { + logger.error("KasAllowlist: No KAS allowlist provided and no KeyAccessServerRegistry available, {} is not allowed", realAddress); + throw new KasAllowlistException("No KAS allowlist provided and no KeyAccessServerRegistry available"); + } else if (!nanoTdfReaderConfig.kasAllowlist.contains(realAddress)) { + logger.error("KasAllowlist: kas url {} is not allowed for allowlist {}", realAddress, nanoTdfReaderConfig.kasAllowlist); + throw new KasAllowlistException("KasAllowlist: kas url "+realAddress+" is not allowed"); + } + + key = kas.unwrapNanoTDF(header.getECCMode().getEllipticCurveType(), base64HeaderData, kasUrl); 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 db19066e..d23d106f 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDK.java @@ -1,7 +1,11 @@ package io.opentdf.platform.sdk; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannel; +import io.grpc.MethodDescriptor; import io.opentdf.platform.authorization.AuthorizationServiceGrpc; import io.opentdf.platform.authorization.AuthorizationServiceGrpc.AuthorizationServiceFutureStub; import io.opentdf.platform.policy.attributes.AttributesServiceGrpc; @@ -12,6 +16,8 @@ import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceGrpc.ResourceMappingServiceFutureStub; import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceGrpc; import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceGrpc.SubjectMappingServiceFutureStub; +import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceGrpc; +import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceGrpc.KeyAccessServerRegistryServiceFutureStub; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,6 +37,7 @@ public class SDK implements AutoCloseable { private final Services services; private final TrustManager trustManager; private final ClientInterceptor authInterceptor; + private final String platformUrl; private static final Logger log = LoggerFactory.getLogger(SDK.class); @@ -76,6 +83,8 @@ public interface Services extends AutoCloseable { ResourceMappingServiceFutureStub resourceMappings(); + KeyAccessServerRegistryServiceFutureStub kasRegistry(); + KAS kas(); static Services newServices(ManagedChannel channel, KAS kas) { @@ -84,6 +93,7 @@ static Services newServices(ManagedChannel channel, KAS kas) { var subjectMappingService = SubjectMappingServiceGrpc.newFutureStub(channel); var resourceMappingService = ResourceMappingServiceGrpc.newFutureStub(channel); var authorizationService = AuthorizationServiceGrpc.newFutureStub(channel); + var kasRegistryService = KeyAccessServerRegistryServiceGrpc.newFutureStub(channel); return new Services() { @Override @@ -117,6 +127,11 @@ public AuthorizationServiceFutureStub authorization() { return authorizationService; } + @Override + public KeyAccessServerRegistryServiceFutureStub kasRegistry() { + return kasRegistryService; + } + @Override public KAS kas() { return kas; @@ -133,7 +148,8 @@ public Optional getAuthInterceptor() { return Optional.ofNullable(authInterceptor); } - SDK(Services services, TrustManager trustManager, ClientInterceptor authInterceptor) { + SDK(Services services, TrustManager trustManager, ClientInterceptor authInterceptor, String platformUrl) { + this.platformUrl = platformUrl; this.services = services; this.trustManager = trustManager; this.authInterceptor = authInterceptor; @@ -165,4 +181,8 @@ public static boolean isTDF(SeekableByteChannel channel) { return entries.stream().anyMatch(e -> "0.manifest.json".equals(e.getName())) && entries.stream().anyMatch(e -> "0.payload".equals(e.getName())); } + + public String getPlatformUrl() { + return platformUrl; + } } 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 9f4e2cd9..074f6ab7 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SDKBuilder.java @@ -243,7 +243,7 @@ ServicesAndInternals buildServices() { public SDK build() { var services = buildServices(); - return new SDK(services.services, services.trustManager, services.interceptor); + return new SDK(services.services, services.trustManager, services.interceptor, platformEndpoint); } /** @@ -257,6 +257,9 @@ public SDK build() { * @return {@type ManagedChannelBuilder} configured with the SDK options */ private ManagedChannelBuilder getManagedChannelBuilder(String endpoint) { + // normalize the endpoint, ends with just host:port + endpoint = KASClient.normalizeAddress(endpoint); + ManagedChannelBuilder channelBuilder; if (sslFactory != null && !usePlainText) { channelBuilder = Grpc.newChannelBuilder(endpoint, TlsChannelCredentials.newBuilder() 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 f74b3cb6..30f15ef4 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -6,6 +6,12 @@ import io.opentdf.platform.policy.Value; import io.opentdf.platform.policy.attributes.AttributesServiceGrpc.AttributesServiceFutureStub; +import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceGrpc.KeyAccessServerRegistryServiceFutureStub; +import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; +import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse; +// import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsRequest; +// import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse; +// import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue; import io.opentdf.platform.sdk.Config.TDFConfig; import io.opentdf.platform.sdk.Autoconfigure.AttributeValueFQN; import io.opentdf.platform.sdk.Config.KASInfo; @@ -22,6 +28,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; +import java.net.URISyntaxException; import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.security.*; @@ -173,6 +180,12 @@ public KasBadRequestException(String errorMessage) { } } + public static class KasAllowlistException extends SDKException { + public KasAllowlistException(String errorMessage) { + super(errorMessage); + } + } + public static class AssertionException extends TamperException { public AssertionException(String errorMessage, String id) { super("assertion id: "+ id + "; " + errorMessage); @@ -649,6 +662,26 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas) return loadTDF(tdf, kas, Config.newTDFReaderConfig()); } + public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, KeyAccessServerRegistryServiceFutureStub kasRegistryService, String platformUrl) + throws DecoderException, IOException, ParseException, NoSuchAlgorithmException, JOSEException, InterruptedException, ExecutionException, URISyntaxException { + return loadTDF(tdf, kas, Config.newTDFReaderConfig(), kasRegistryService, platformUrl); + } + + public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, Config.TDFReaderConfig tdfReaderConfig, KeyAccessServerRegistryServiceFutureStub kasRegistryService, String platformUrl) + throws DecoderException, IOException, ParseException, NoSuchAlgorithmException, JOSEException, InterruptedException, ExecutionException, URISyntaxException { + if (!tdfReaderConfig.ignoreKasAllowlist && (tdfReaderConfig.kasAllowlist == null || tdfReaderConfig.kasAllowlist.isEmpty())) { + ListKeyAccessServersRequest request = ListKeyAccessServersRequest.newBuilder() + .build(); + ListKeyAccessServersResponse response = kasRegistryService.listKeyAccessServers(request).get(); + tdfReaderConfig.kasAllowlist = new HashSet<>(); + + for (var entry : response.getKeyAccessServersList()) { + tdfReaderConfig.kasAllowlist.add(Config.getKasAddress(entry.getUri())); + } + tdfReaderConfig.kasAllowlist.add(Config.getKasAddress(platformUrl)); + } + return loadTDF(tdf, kas, tdfReaderConfig); + } public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, Config.TDFReaderConfig tdfReaderConfig) @@ -678,6 +711,17 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, } knownSplits.add(ss.splitID); try { + var realAddress = Config.getKasAddress(keyAccess.url); + if (tdfReaderConfig.ignoreKasAllowlist) { + logger.warn("Ignoring KasAllowlist for url {}", realAddress); + } else if (tdfReaderConfig.kasAllowlist == null || tdfReaderConfig.kasAllowlist.isEmpty()) { + logger.error("KasAllowlist: No KAS allowlist provided and no KeyAccessServerRegistry available, {} is not allowed", realAddress); + throw new KasAllowlistException("No KAS allowlist provided and no KeyAccessServerRegistry available"); + } else if (!tdfReaderConfig.kasAllowlist.contains(realAddress)) { + logger.error("KasAllowlist: kas url {} is not allowed", realAddress); + logger.error("KasAllowlist: kas list is {}", tdfReaderConfig.kasAllowlist); + throw new KasAllowlistException("KasAllowlist: kas url "+realAddress+" is not allowed"); + } unwrappedKey = kas.unwrap(keyAccess, manifest.encryptionInformation.policy, tdfReaderConfig.sessionKeyType); } catch (Exception e) { skippedSplits.put(ss, e); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java b/sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java index 5fcb90fc..6b406038 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; import java.text.ParseException; @@ -32,7 +33,7 @@ public void write(byte[] b, int off, int len) { }; @FuzzTest(maxDuration=TEST_DURATION) - public void fuzzNanoTDF(FuzzedDataProvider data) throws IOException { + public void fuzzNanoTDF(FuzzedDataProvider data) throws IOException, URISyntaxException { byte[] fuzzBytes = data.consumeRemainingAsBytes(); NanoTDF nanoTDF = new NanoTDF(); nanoTDF.readNanoTDF(ByteBuffer.wrap(fuzzBytes), IGNORE_OUTPUT_STREAM, NanoTDFTest.kas); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java index 0c8203ae..e6bf21ca 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/NanoTDFTest.java @@ -1,11 +1,17 @@ package io.opentdf.platform.sdk; +import io.opentdf.platform.policy.KeyAccessServer; +import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceGrpc.KeyAccessServerRegistryServiceFutureStub; +import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; +import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse; import io.opentdf.platform.sdk.Config.KASInfo; +import io.opentdf.platform.sdk.Config.NanoTDFReaderConfig; import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import io.opentdf.platform.sdk.nanotdf.Header; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; import java.nio.charset.StandardCharsets; import org.apache.commons.io.output.ByteArrayOutputStream; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.io.FileInputStream; @@ -16,9 +22,13 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Random; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class NanoTDFTest { @@ -36,6 +46,15 @@ public class NanoTDFTest { "-----END PRIVATE KEY-----"; private static final String KID = "r1"; + + protected static KeyAccessServerRegistryServiceFutureStub kasRegistryService; + protected static List registeredKases = List.of( + "https://api.example.com/kas", + "https://other.org/kas2", + "http://localhost:8181/kas", + "https://localhost:8383/kas" + ); + protected static String platformUrl = "http://localhost:8080"; protected static SDK.KAS kas = new SDK.KAS() { @Override @@ -103,6 +122,26 @@ public KASKeyCache getKeyCache(){ } }; + @BeforeAll + static void setupMocks() { + kasRegistryService = mock(KeyAccessServerRegistryServiceFutureStub.class); + List kasRegEntries = new ArrayList<>(); + for (String kasUrl : registeredKases ) { + kasRegEntries.add(KeyAccessServer.newBuilder() + .setUri(kasUrl).build()); + } + ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() + .addAllKeyAccessServers(kasRegEntries) + .build(); + + // Stub the listKeyAccessServers method + when(kasRegistryService.listKeyAccessServers(any(ListKeyAccessServersRequest.class))) + .thenReturn(com.google.common.util.concurrent.Futures.immediateFuture(mockResponse)); + io.grpc.Channel mockChannel = mock(io.grpc.Channel.class); + when(mockChannel.authority()).thenReturn("mock:8080"); + when(kasRegistryService.getChannel()).thenReturn(mockChannel); + } + private static ArrayList keypairs = new ArrayList<>(); @Test @@ -130,7 +169,7 @@ void encryptionAndDecryptionWithValidKey() throws Exception { byte[] nanoTDFBytes = tdfOutputStream.toByteArray(); ByteArrayOutputStream plainTextStream = new ByteArrayOutputStream(); nanoTDF = new NanoTDF(); - nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, kas); + nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, kas, kasRegistryService, platformUrl); String out = new String(plainTextStream.toByteArray(), StandardCharsets.UTF_8); assertThat(out).isEqualTo(plainText); @@ -150,11 +189,89 @@ void encryptionAndDecryptionWithValidKey() throws Exception { byte[] nTDFBytes = outputStream.toByteArray(); ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); - nanoTDF.readNanoTDF(ByteBuffer.wrap(nTDFBytes), dataStream, kas); + nanoTDF.readNanoTDF(ByteBuffer.wrap(nTDFBytes), dataStream, kas, kasRegistryService, platformUrl); assertThat(dataStream.toByteArray()).isEqualTo(data); } } + void runBasicTest(String kasUrl, boolean allowed, KeyAccessServerRegistryServiceFutureStub kasReg, NanoTDFReaderConfig decryptConfig) throws Exception { + var kasInfos = new ArrayList<>(); + var kasInfo = new Config.KASInfo(); + kasInfo.URL = kasUrl; + kasInfo.PublicKey = null; + kasInfos.add(kasInfo); + + Config.NanoTDFConfig config = Config.newNanoTDFConfig( + Config.withNanoKasInformation(kasInfos.toArray(new Config.KASInfo[0])), + Config.witDataAttributes("https://example.com/attr/Classification/value/S", + "https://example.com/attr/Classification/value/X") + ); + + String plainText = "Virtru!!"; + ByteBuffer byteBuffer = ByteBuffer.wrap(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + NanoTDF nanoTDF = new NanoTDF(); + nanoTDF.createNanoTDF(byteBuffer, tdfOutputStream, config, kas); + + byte[] nanoTDFBytes = tdfOutputStream.toByteArray(); + ByteArrayOutputStream plainTextStream = new ByteArrayOutputStream(); + nanoTDF = new NanoTDF(); + if (allowed) { + if (decryptConfig != null) { + nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, kas, decryptConfig); + } else { + nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, kas, kasReg, platformUrl); + } + String out = new String(plainTextStream.toByteArray(), StandardCharsets.UTF_8); + assertThat(out).isEqualTo(plainText); + } else { + try { + if (decryptConfig != null) { + nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, kas, decryptConfig); + } else { + nanoTDF.readNanoTDF(ByteBuffer.wrap(nanoTDFBytes), plainTextStream, kas, kasReg, platformUrl); + } + assertThat(false).isTrue(); + } catch (SDKException e) { + assertThat(e.getMessage()).contains("KasAllowlist"); + } + } + + + } + + @Test + void kasAllowlistTests() throws Exception { + var kasUrlsSuccess = List.of( + "https://api.example.com/kas", + "https://other.org/kas2", + "http://localhost:8181/kas", + "https://localhost:8383/kas", + platformUrl+"/kas" + ); + var kasUrlsFail = List.of( + "http://api.example.com/kas", + "http://other.org/kas", + "https://localhost:8181/kas2", + "https://localhost:8282/kas2", + "https://localhost:8080/kas" + ); + for (String kasUrl : kasUrlsSuccess) { + runBasicTest(kasUrl, true, kasRegistryService, null); + } + for (String kasUrl : kasUrlsFail) { + runBasicTest(kasUrl, false, kasRegistryService, null); + } + + // test with kasAllowlist + runBasicTest("http://api.example.com/kas", true, null, Config.newNanoTDFReaderConfig(Config.WithNanoKasAllowlist("http://api.example.com/kas"))); + runBasicTest(platformUrl+"/kas", false, null, Config.newNanoTDFReaderConfig(Config.WithNanoKasAllowlist("http://api.example.com/kas"))); + + // test ignore kasAllowlist + runBasicTest(platformUrl+"/kas", true, null, Config.newNanoTDFReaderConfig(Config.WithNanoKasAllowlist("http://api.example.com/kas"), Config.WithNanoIgnoreKasAllowlist(true))); + } + @Test void collection() throws Exception { var kasInfos = new ArrayList<>(); diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFE2ETest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFE2ETest.java index a3a86b9a..fb2441b7 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFE2ETest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFE2ETest.java @@ -33,8 +33,7 @@ public void createAndDecryptTdfIT() throws Exception { .clientSecret("opentdf-sdk", "secret") .useInsecurePlaintextConnection(true) .platformEndpoint("localhost:8080") - .buildServices() - .services; + .build(); var kasInfo = new Config.KASInfo(); kasInfo.URL = "localhost:8080"; @@ -56,10 +55,10 @@ public void createAndDecryptTdfIT() throws Exception { ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); TDF tdf = new TDF(); - tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig, sdk.kas(), sdk.attributes()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig, sdk.getServices().kas(), sdk.getServices().attributes()); var unwrappedData = new java.io.ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), sdk.kas(), configPair.tdfReaderConfig); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), sdk.getServices().kas(), configPair.tdfReaderConfig, sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)).isEqualTo("text"); 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 8a03dd09..ee337bd1 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -5,6 +5,11 @@ import io.opentdf.platform.sdk.TDF.Reader; import io.opentdf.platform.sdk.nanotdf.ECKeyPair; import io.opentdf.platform.sdk.nanotdf.NanoTDFType; +import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceGrpc.KeyAccessServerRegistryServiceFutureStub; +import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; +import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse; +import io.opentdf.platform.policy.KeyAccessServer; +import com.google.common.util.concurrent.ListenableFuture; import org.apache.commons.codec.DecoderException; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; import org.junit.jupiter.api.BeforeAll; @@ -16,7 +21,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.security.Key; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -34,8 +41,15 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class TDFTest { + protected static KeyAccessServerRegistryServiceFutureStub kasRegistryService; + protected static String platformUrl = "http://localhost:8080"; + protected static SDK.KAS kas = new SDK.KAS() { @Override public void close() { @@ -43,7 +57,14 @@ public void close() { @Override public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { - int index = Integer.parseInt(kasInfo.URL); + // handle platform url + int index; + // if the kasinfo url contains the platform url, remove it + if (kasInfo.URL.startsWith(platformUrl)) { + index = Integer.parseInt(kasInfo.URL.replaceFirst("^"+platformUrl+"/kas", "")); + } else { + index = Integer.parseInt(kasInfo.URL.replaceFirst("^https://example.com/kas", "")); + } var kiCopy = new Config.KASInfo(); kiCopy.KID = "r1"; kiCopy.PublicKey = CryptoUtils.getPublicKeyPEM(keypairs.get(index).getPublic()); @@ -55,7 +76,13 @@ public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { try { - int index = Integer.parseInt(keyAccess.url); + int index; + // if the keyAccess.url contains the platform url, remove it + if (keyAccess.url.startsWith(platformUrl)) { + index = Integer.parseInt(keyAccess.url.replaceFirst("^"+platformUrl+"/kas", "")); + } else { + index = Integer.parseInt(keyAccess.url.replaceFirst("^https://example.com/kas", "")); + } var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); if (sessionKeyType.isEc()) { var kasPrivateKey = CryptoUtils.getPrivateKeyPEM(keypairs.get(index).getPrivate()); @@ -97,7 +124,7 @@ public KASKeyCache getKeyCache() { private static ArrayList keypairs = new ArrayList<>(); @BeforeAll - static void createKeypairs() { + static void setupKeyPairsAndMocks() { for (int i = 0; i < 2 + new Random().nextInt(5); i++) { if (i % 2 == 0) { keypairs.add(CryptoUtils.generateRSAKeypair()); @@ -105,6 +132,27 @@ static void createKeypairs() { keypairs.add(CryptoUtils.generateECKeypair(KeyType.EC256Key.getCurveName())); } } + + kasRegistryService = mock(KeyAccessServerRegistryServiceFutureStub.class); + List kasRegEntries = new ArrayList<>(); + for (Config.KASInfo kasInfo : getRSAKASInfos()) { + kasRegEntries.add(KeyAccessServer.newBuilder() + .setUri(kasInfo.URL).build()); + } + for (Config.KASInfo kasInfo : getECKASInfos()) { + kasRegEntries.add(KeyAccessServer.newBuilder() + .setUri(kasInfo.URL).build()); + } + ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() + .addAllKeyAccessServers(kasRegEntries) + .build(); + + // Stub the listKeyAccessServers method + when(kasRegistryService.listKeyAccessServers(any(ListKeyAccessServersRequest.class))) + .thenReturn(com.google.common.util.concurrent.Futures.immediateFuture(mockResponse)); + io.grpc.Channel mockChannel = mock(io.grpc.Channel.class); + when(mockChannel.authority()).thenReturn("mock:8080"); + when(kasRegistryService.getChannel()).thenReturn(mockChannel); } @Test @@ -168,7 +216,7 @@ public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReade var unwrappedData = new ByteArrayOutputStream(); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, - configPair.tdfReaderConfig); + configPair.tdfReaderConfig, kasRegistryService, platformUrl); assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); reader.readPayload(unwrappedData); @@ -202,7 +250,7 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { keypair.getPrivate()); var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = Integer.toString(0); + rsaKasInfo.URL = "https://example.com/kas"+Integer.toString(0); Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), @@ -224,7 +272,7 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( Config.withAssertionVerificationKeys(assertionVerificationKeys)); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, - readerConfig); + readerConfig, kasRegistryService, platformUrl); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -267,14 +315,14 @@ void testWithAssertionVerificationDisabled() throws Exception { var unwrappedData = new ByteArrayOutputStream(); assertThrows(JOSEException.class, () -> { tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, - Config.newTDFReaderConfig()); + Config.newTDFReaderConfig(), kasRegistryService, platformUrl); }); // try with assertion verification disabled and not passing the assertion verification keys Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( Config.withDisableAssertionVerification(true)); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, - readerConfig); + readerConfig, kasRegistryService, platformUrl); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -306,7 +354,7 @@ void testSimpleTDFWithAssertionWithHS256() throws Exception { assertionConfig2.statement.value = "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"; var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = Integer.toString(0); + rsaKasInfo.URL = "https://example.com/kas"+Integer.toString(0); Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), @@ -322,7 +370,7 @@ void testSimpleTDFWithAssertionWithHS256() throws Exception { var unwrappedData = new ByteArrayOutputStream(); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), - kas, Config.newTDFReaderConfig()); + kas, Config.newTDFReaderConfig(), kasRegistryService, platformUrl); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -370,7 +418,7 @@ void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { assertionConfig1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = Integer.toString(0); + rsaKasInfo.URL = "https://example.com/kas"+Integer.toString(0); Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), @@ -395,7 +443,7 @@ void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { var unwrappedData = new ByteArrayOutputStream(); Reader reader; try { - reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, readerConfig); + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, readerConfig, kasRegistryService, platformUrl); throw new RuntimeException("assertion verify key error thrown"); } catch (SDKException e) { @@ -420,7 +468,7 @@ public void testCreatingTDFWithMultipleSegments() throws Exception { var tdf = new TDF(); tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, null); var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, kasRegistryService, platformUrl); reader.readPayload(unwrappedData); assertThat(unwrappedData.toByteArray()) @@ -493,12 +541,12 @@ public void testCreateTDFWithMimeType() throws Exception { TDF tdf = new TDF(); tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, null); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, kasRegistryService, platformUrl); assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); } @Test - public void legacyTDFRoundTrips() throws DecoderException, IOException, ExecutionException, JOSEException, InterruptedException, ParseException, NoSuchAlgorithmException { + public void legacyTDFRoundTrips() throws DecoderException, IOException, ExecutionException, JOSEException, InterruptedException, ParseException, NoSuchAlgorithmException, URISyntaxException { final String mimeType = "application/pdf"; var assertionConfig1 = new AssertionConfig(); assertionConfig1.id = "assertion1"; @@ -527,7 +575,7 @@ public void legacyTDFRoundTrips() throws DecoderException, IOException, Executio var dataOutputStream = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, kasRegistryService, platformUrl); var integrityInformation = reader.getManifest().encryptionInformation.integrityInformation; assertThat(reader.getManifest().tdfVersion).isNull(); var decodedSignature = Base64.getDecoder().decode(integrityInformation.rootSignature.signature); @@ -558,13 +606,94 @@ public void legacyTDFRoundTrips() throws DecoderException, IOException, Executio assertThat(assertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); } + @Test + void testKasAllowlist() throws Exception { + + KeyAccessServerRegistryServiceFutureStub kasRegistryServiceNoUrl = mock(KeyAccessServerRegistryServiceFutureStub.class); + List kasRegEntries = new ArrayList<>(); + kasRegEntries.add(KeyAccessServer.newBuilder() + .setUri("http://example.com/kas0").build()); + + ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() + .addAllKeyAccessServers(kasRegEntries) + .build(); + + // Stub the listKeyAccessServers method + when(kasRegistryServiceNoUrl.listKeyAccessServers(any(ListKeyAccessServersRequest.class))) + .thenReturn(com.google.common.util.concurrent.Futures.immediateFuture(mockResponse)); + io.grpc.Channel mockChannel = mock(io.grpc.Channel.class); + when(mockChannel.authority()).thenReturn("mock:8080"); + when(kasRegistryServiceNoUrl.getChannel()).thenReturn(mockChannel); + + + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = "https://example.com/kas"+Integer.toString(0); + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(rsaKasInfo)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, null); + + var unwrappedData = new ByteArrayOutputStream(); + Reader reader; + + // should throw error because the kas url is not in the allowlist + try { + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, Config.newTDFReaderConfig(), kasRegistryServiceNoUrl, platformUrl); + throw new RuntimeException("expected allowlist error to be thrown"); + } catch (Exception e) { + assertThat(e).hasMessageContaining("KasAllowlist"); + } + + // with custom allowlist should succeed + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.WithKasAllowlist("https://example.com")); + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, readerConfig, kasRegistryServiceNoUrl, platformUrl); + + // with ignore allowlist should succeed + readerConfig = Config.newTDFReaderConfig( + Config.WithIgnoreKasAllowlist(true)); + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, readerConfig, kasRegistryServiceNoUrl, platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + + + // use the platform url as kas url, should succeed + var platformKasInfo = new Config.KASInfo(); + platformKasInfo.URL = platformUrl+"/kas"+Integer.toString(0); + config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(platformKasInfo)); + plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + tdfOutputStream = new ByteArrayOutputStream(); + tdf = new TDF(); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, null); + + unwrappedData = new ByteArrayOutputStream(); + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas, Config.newTDFReaderConfig(), kasRegistryServiceNoUrl, platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + } + @Nonnull private static Config.KASInfo[] getKASInfos(Predicate filter) { var kasInfos = new ArrayList(); for (int i = 0; i < keypairs.size(); i++) { if (filter.test(i)) { var kasInfo = new Config.KASInfo(); - kasInfo.URL = Integer.toString(i); + kasInfo.URL = "https://example.com/kas"+Integer.toString(i); kasInfo.PublicKey = null; kasInfos.add(kasInfo); } From ada5baeb1f1ac7a6efa4054b577fb3cde49e1815 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Thu, 24 Apr 2025 16:02:25 -0400 Subject: [PATCH 2/7] fix multi-kas integration test --- .github/workflows/checks.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 070a0468..0b472b2a 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -302,21 +302,21 @@ jobs: --client-secret=secret \ --platform-endpoint=http://localhost:8080 \ -h\ - encrypt --kas-url=http://localhost:8080,localhost:8282 -f data -m 'here is some metadata' > test.tdf + encrypt --kas-url=http://localhost:8080,http://localhost:8282 -f data -m 'here is some metadata' > test.tdf java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ --platform-endpoint=http://localhost:8080 \ -h\ - decrypt -f test.tdf > decrypted + decrypt -f test.tdf --kas-allowlist http://localhost:8080,http://localhost:8282 > decrypted java -jar target/cmdline.jar \ --client-id=opentdf-sdk \ --client-secret=secret \ --platform-endpoint=http://localhost:8080 \ -h\ - metadata -f test.tdf > metadata + metadata -f test.tdf --kas-allowlist http://localhost:8080,http://localhost:8282 > metadata if ! diff -q data decrypted; then printf 'decrypted data is incorrect [%s]' "$(< decrypted)" From 849da9316f6e2e24c9e6fad01ea0d2ddce2870c2 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Thu, 24 Apr 2025 16:12:56 -0400 Subject: [PATCH 3/7] add options to the metadata cli command --- .../main/java/io/opentdf/platform/Command.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index c5fce933..85e55ee7 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -304,13 +304,24 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, } @CommandLine.Command(name = "metadata") - void readMetadata(@Option(names = { "-f", "--file" }, required = true) Path tdfPath) throws IOException, + void readMetadata(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, + @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, + @Option(names = { "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) throws IOException, TDF.FailedToCreateGMAC, JOSEException, NoSuchAlgorithmException, ParseException, DecoderException, InterruptedException, ExecutionException, URISyntaxException { var sdk = buildSDK(); - + var opts = new ArrayList>(); try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) { try (var stdout = new PrintWriter(System.out)) { - var reader = new TDF().loadTDF(in, sdk.getServices().kas(), sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); + + if (ignoreAllowlist.isPresent()) { + opts.add(Config.WithIgnoreKasAllowlist(ignoreAllowlist.get())); + } + if (kasAllowlistStr.isPresent()) { + opts.add(Config.WithKasAllowlist(kasAllowlistStr.get().split(","))); + } + + var readerConfig = Config.newTDFReaderConfig(opts.toArray(new Consumer[0])); + var reader = new TDF().loadTDF(in, sdk.getServices().kas(), readerConfig, sdk.getServices().kasRegistry(), sdk.getPlatformUrl()); stdout.write(reader.getMetadata() == null ? "" : reader.getMetadata()); } } From bb13abbbf3fae56d3c149824d4944b70211d074d Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Fri, 25 Apr 2025 10:32:55 -0400 Subject: [PATCH 4/7] remove unnecessary logs --- sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java | 4 +--- sdk/src/main/java/io/opentdf/platform/sdk/TDF.java | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java index 74e0631c..315ce915 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/NanoTDF.java @@ -228,9 +228,7 @@ public void readNanoTDF(ByteBuffer nanoTDF, OutputStream outputStream, nanoTdfReaderConfig.kasAllowlist.add(Config.getKasAddress(entry.getUri())); } - logger.info("platformUrl: {}", platformUrl); nanoTdfReaderConfig.kasAllowlist.add(Config.getKasAddress(platformUrl)); - logger.info("KasAllowlist: kas url list is {}", nanoTdfReaderConfig.kasAllowlist); } readNanoTDF(nanoTDF, outputStream, kas, nanoTdfReaderConfig); } @@ -261,7 +259,7 @@ public void readNanoTDF(ByteBuffer nanoTDF, OutputStream outputStream, logger.error("KasAllowlist: No KAS allowlist provided and no KeyAccessServerRegistry available, {} is not allowed", realAddress); throw new KasAllowlistException("No KAS allowlist provided and no KeyAccessServerRegistry available"); } else if (!nanoTdfReaderConfig.kasAllowlist.contains(realAddress)) { - logger.error("KasAllowlist: kas url {} is not allowed for allowlist {}", realAddress, nanoTdfReaderConfig.kasAllowlist); + logger.error("KasAllowlist: kas url {} is not allowed", realAddress); throw new KasAllowlistException("KasAllowlist: kas url "+realAddress+" is not allowed"); } 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 30f15ef4..598522eb 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -9,9 +9,6 @@ import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceGrpc.KeyAccessServerRegistryServiceFutureStub; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse; -// import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsRequest; -// import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse; -// import io.opentdf.platform.policy.attributes.GetAttributeValuesByFqnsResponse.AttributeAndValue; import io.opentdf.platform.sdk.Config.TDFConfig; import io.opentdf.platform.sdk.Autoconfigure.AttributeValueFQN; import io.opentdf.platform.sdk.Config.KASInfo; @@ -719,7 +716,6 @@ public Reader loadTDF(SeekableByteChannel tdf, SDK.KAS kas, throw new KasAllowlistException("No KAS allowlist provided and no KeyAccessServerRegistry available"); } else if (!tdfReaderConfig.kasAllowlist.contains(realAddress)) { logger.error("KasAllowlist: kas url {} is not allowed", realAddress); - logger.error("KasAllowlist: kas list is {}", tdfReaderConfig.kasAllowlist); throw new KasAllowlistException("KasAllowlist: kas url "+realAddress+" is not allowed"); } unwrappedKey = kas.unwrap(keyAccess, manifest.encryptionInformation.policy, tdfReaderConfig.sessionKeyType); From 1c83f431eb3ec5a3d0269e8d51e524f4fcfee2f8 Mon Sep 17 00:00:00 2001 From: Elizabeth Healy <35498075+elizabethhealy@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:22:03 -0400 Subject: [PATCH 5/7] copilot pattern quote suggestion Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ee337bd1..75ca1c9d 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -61,7 +61,7 @@ public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { int index; // if the kasinfo url contains the platform url, remove it if (kasInfo.URL.startsWith(platformUrl)) { - index = Integer.parseInt(kasInfo.URL.replaceFirst("^"+platformUrl+"/kas", "")); + index = Integer.parseInt(kasInfo.URL.replaceFirst("^" + Pattern.quote(platformUrl) + "/kas", "")); } else { index = Integer.parseInt(kasInfo.URL.replaceFirst("^https://example.com/kas", "")); } From ea8217ee1d8b182cb6368d096a098f0506e3853c Mon Sep 17 00:00:00 2001 From: Elizabeth Healy <35498075+elizabethhealy@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:22:18 -0400 Subject: [PATCH 6/7] copilot patten quote suggestion Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 75ca1c9d..e5af4707 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -79,7 +79,7 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessio int index; // if the keyAccess.url contains the platform url, remove it if (keyAccess.url.startsWith(platformUrl)) { - index = Integer.parseInt(keyAccess.url.replaceFirst("^"+platformUrl+"/kas", "")); + index = Integer.parseInt(keyAccess.url.replaceFirst("^" + Pattern.quote(platformUrl) + "/kas", "")); } else { index = Integer.parseInt(keyAccess.url.replaceFirst("^https://example.com/kas", "")); } From 23c8298a75f7e9152215ab87777691188bca598d Mon Sep 17 00:00:00 2001 From: Elizabeth Healy Date: Fri, 25 Apr 2025 16:24:19 -0400 Subject: [PATCH 7/7] fix copilot suggestions --- sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 e5af4707..592694e6 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -9,7 +9,6 @@ import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersResponse; import io.opentdf.platform.policy.KeyAccessServer; -import com.google.common.util.concurrent.ListenableFuture; import org.apache.commons.codec.DecoderException; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; import org.junit.jupiter.api.BeforeAll; @@ -35,6 +34,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT; @@ -42,7 +42,6 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when;