From d6db7c51512d5a77b7de147ec576180dcd18eca1 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 12:22:35 -0400 Subject: [PATCH 01/23] feat: add system metadata assertion --- .../opentdf/platform/sdk/AssertionConfig.java | 83 +++++++++++++++++++ .../java/io/opentdf/platform/sdk/Config.java | 6 ++ .../java/io/opentdf/platform/sdk/TDF.java | 11 +++ .../java/io/opentdf/platform/sdk/TDFTest.java | 48 +++++++++++ 4 files changed, 148 insertions(+) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java index 9d9b6084..fe04ebbb 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -1,6 +1,13 @@ package io.opentdf.platform.sdk; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.Objects; /** @@ -120,4 +127,80 @@ public int hashCode() { public AppliesToState appliesToState; public Statement statement; public AssertionKey signingKey; + + /** + * Inner class to hold system metadata for assertion. + * Fields are named to match the JSON output of the original Go function. + */ + static private class SystemMetadata { + @SerializedName("tdf_spec_version") + String tdfSpecVersion; + + @SerializedName("creation_date") + String creationDate; + + @SerializedName("operating_system") + String operatingSystem; + + @SerializedName("sdk_version") + String sdkVersion; + + @SerializedName("hostname") + String hostname; + + @SerializedName("java_version") // Corresponds to "go_version" in the Go example + String javaVersion; + + @SerializedName("architecture") + String architecture; + } + + /** + * Returns a default assertion configuration with predefined system metadata. + * This method mimics the behavior of the Go function GetSystemMetadataAssertionConfig. + * + * @param tdfSpecVersionFromSDK The TDF specification version (e.g., "4.3.0"). + * @param sdkInternalVersion The internal version of this SDK (e.g., "1.0.0"), which will be prefixed with "Java-". + * @return An {@link AssertionConfig} populated with system metadata. + * @throws SDKException if there's an error marshalling the metadata to JSON. + */ + public static AssertionConfig getSystemMetadataAssertionConfig(String tdfSpecVersionFromSDK, String sdkInternalVersion) { + SystemMetadata metadata = new SystemMetadata(); + metadata.tdfSpecVersion = tdfSpecVersionFromSDK; + metadata.creationDate = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + metadata.operatingSystem = System.getProperty("os.name"); + metadata.sdkVersion = "Java-" + sdkInternalVersion; + metadata.javaVersion = System.getProperty("java.version"); + metadata.architecture = System.getProperty("os.arch"); + + try { + metadata.hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + // Mimic Go behavior: if hostname retrieval fails, it's omitted. + // Gson will omit null fields by default. + // Optionally, log this exception: e.g., logger.warn("Could not retrieve hostname", e); + } + + Gson gson = new Gson(); // A new Gson instance is used for simplicity here. + String metadataJSON; + try { + metadataJSON = gson.toJson(metadata); + } catch (Exception e) { // Catch general exception from Gson, though it's usually for I/O or reflection issues. + throw new SDKException("Failed to marshal system metadata to JSON", e); + } + + AssertionConfig config = new AssertionConfig(); + config.id = "default-assertion"; + config.type = Type.BaseAssertion; + config.scope = Scope.Payload; // Maps from Go's PayloadScope + config.appliesToState = AppliesToState.Unencrypted; + + Statement statement = new Statement(); + statement.format = "json"; + statement.schema = "metadata"; + statement.value = metadataJSON; + config.statement = statement; + + return config; + } } \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java index 0a1f4a46..9562d779 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -160,6 +160,7 @@ public static class TDFConfig { public KeyType wrappingKeyType; public boolean hexEncodeRootAndSegmentHashes; public boolean renderVersionInfoInManifest; + public boolean systemMetadataAssertion; public TDFConfig() { this.autoconfigure = true; @@ -176,6 +177,7 @@ public TDFConfig() { this.wrappingKeyType = KeyType.RSA2048Key; this.hexEncodeRootAndSegmentHashes = false; this.renderVersionInfoInManifest = true; + this.systemMetadataAssertion = false; } } @@ -297,6 +299,10 @@ public static Consumer withMimeType(String mimeType) { return (TDFConfig config) -> config.mimeType = mimeType; } + public static Consumer withSystemMetadataAssertion() { + return (TDFConfig config) -> config.systemMetadataAssertion = true; + } + public static class NanoTDFConfig { public ECCMode eccMode; public NanoTDFType.Cipher cipher; diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index fe547caa..881201d3 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -427,6 +427,16 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo throw new SDK.KasInfoMissing("kas information is missing, no key access template specified or inferred"); } + // Add System Metadata Assertion if configured + if (tdfConfig.systemMetadataAssertion) { + // TDF_VERSION is used for both tdfSpecVersion and as a placeholder for sdkInternalVersion. + // Consider defining a specific SDK_VERSION constant for the second parameter + // if a distinct SDK version string (e.g., "0.1.0") is desired. + AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig(TDF_VERSION, TDF_VERSION); + // tdfConfig.assertionConfigList is initialized in TDFConfig constructor, so it won't be null. + tdfConfig.assertionConfigList.add(systemAssertion); + } + TDFObject tdfObject = new TDFObject(); tdfObject.prepareManifest(tdfConfig, services.kas()); @@ -510,6 +520,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo tdfObject.manifest.payload.isEncrypted = true; List signedAssertions = new ArrayList<>(tdfConfig.assertionConfigList.size()); + for (var assertionConfig : tdfConfig.assertionConfigList) { var assertion = new Manifest.Assertion(); assertion.id = assertionConfig.id; 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 e41f54ea..ed5a7579 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -3,6 +3,7 @@ import com.connectrpc.ResponseMessage; import com.connectrpc.UnaryBlockingCall; import com.nimbusds.jose.JOSEException; +import com.google.gson.Gson; import io.opentdf.platform.policy.KeyAccessServer; import io.opentdf.platform.policy.kasregistry.KeyAccessServerRegistryServiceClient; import io.opentdf.platform.policy.kasregistry.ListKeyAccessServersRequest; @@ -25,6 +26,7 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collections; +import java.util.Map; import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; @@ -615,6 +617,52 @@ void legacyTDFRoundTrips() throws IOException { assertThat(assertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); } + @Test + void testSystemMetadataAssertion() throws Exception { + Config.TDFConfig tdfConfig = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSystemMetadataAssertion() // Enable system metadata assertion + ); + + String plainText = "Test data for system metadata assertion."; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + var createdManifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, tdfConfig).getManifest(); + + // Verify the created manifest directly + assertThat(createdManifest.assertions).isNotNull(); + assertThat(createdManifest.assertions.size()).isEqualTo(1); + Manifest.Assertion sysAssertion = createdManifest.assertions.get(0); + assertThat(sysAssertion.id).isEqualTo("default-assertion"); + assertThat(sysAssertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); + assertThat(sysAssertion.scope).isEqualTo(AssertionConfig.Scope.Payload.toString()); + assertThat(sysAssertion.appliesToState).isEqualTo(AssertionConfig.AppliesToState.Unencrypted.toString()); + assertThat(sysAssertion.statement.format).isEqualTo("json"); + assertThat(sysAssertion.statement.schema).isEqualTo("metadata"); + + // Deserialize and check the metadata JSON + Gson gson = new Gson(); + Map metadataMap = gson.fromJson(sysAssertion.statement.value, Map.class); + assertThat(metadataMap).containsKey("tdf_spec_version"); + assertThat(metadataMap.get("tdf_spec_version")).isEqualTo(TDF.TDF_VERSION); // Assuming TDF_VERSION is accessible or use a known value + assertThat(metadataMap).containsKey("creation_date"); + assertThat(metadataMap).containsKey("operating_system"); + assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); + assertThat(metadataMap).containsKey("sdk_version"); + assertThat(metadataMap.get("sdk_version")).startsWith("Java-"); + assertThat(metadataMap).containsKey("java_version"); // Corresponds to go_version + assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); + assertThat(metadataMap).containsKey("architecture"); + assertThat(metadataMap.get("architecture")).isEqualTo(System.getProperty("os.arch")); + // Hostname is optional, so we just check if it's there or not, not its specific value. + // If it's not retrievable, Gson will omit it. + // assertThat(metadataMap).containsKey("hostname"); // This could fail if hostname is not retrievable + + } + @Test void testKasAllowlist() throws Exception { From d8742537f303fd4fe18c867f7b403a3a61f9b5a3 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 12:28:12 -0400 Subject: [PATCH 02/23] fix compilation issues --- .../java/io/opentdf/platform/sdk/TDF.java | 83 ++++++++++++------- 1 file changed, 53 insertions(+), 30 deletions(-) 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 881201d3..33c11484 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -50,19 +50,24 @@ private static byte[] tdfECKeySaltCompute() { } public static final byte[] GLOBAL_KEY_SALT = tdfECKeySaltCompute(); - private static final String EMPTY_SPLIT_ID = ""; - private static final String TDF_VERSION = "4.3.0"; + static final String EMPTY_SPLIT_ID = ""; // Made package-private for TDFTest usage if needed, or could be private if + // not used by TDFTest + public static final String TDF_VERSION = "4.3.0"; // Made public for TDFTest usage private static final String KEY_ACCESS_SCHEMA_VERSION = "1.0"; private final long maximumSize; private final SDK.Services services; /** - * Constructs a new TDF instance using the default maximum input size defined by MAX_TDF_INPUT_SIZE. + * Constructs a new TDF instance using the default maximum input size defined by + * MAX_TDF_INPUT_SIZE. *

- * This constructor is primarily used to initialize the TDF object with the standard maximum - * input size, which controls the maximum size of the input data that can be processed. - * For test purposes, an alternative constructor allows for setting a custom maximum input size. + * This constructor is primarily used to initialize the TDF object with the + * standard maximum + * input size, which controls the maximum size of the input data that can be + * processed. + * For test purposes, an alternative constructor allows for setting a custom + * maximum input size. */ TDF(SDK.Services services) { this(MAX_TDF_INPUT_SIZE, services); @@ -235,7 +240,8 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { throw new SDK.KasPublicKeyMissing("Kas public key is missing in kas information list"); } - var keyAccess = createKeyAccess(tdfConfig, kasInfo, symKey, policyBinding, encryptedMetadata, splitID); + var keyAccess = createKeyAccess(tdfConfig, kasInfo, symKey, policyBinding, encryptedMetadata, + splitID); manifest.encryptionInformation.keyAccessObj.add(keyAccess); } } @@ -253,7 +259,8 @@ private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { this.aesGcm = new AesGcm(this.payloadKey); } - private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KASInfo kasInfo, byte[] symKey, Manifest.PolicyBinding policyBinding, String encryptedMetadata, String splitID) { + private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KASInfo kasInfo, byte[] symKey, + Manifest.PolicyBinding policyBinding, String encryptedMetadata, String splitID) { Manifest.KeyAccess keyAccess = new Manifest.KeyAccess(); keyAccess.keyType = kWrapped; keyAccess.url = kasInfo.URL; @@ -276,7 +283,8 @@ private Manifest.KeyAccess createKeyAccess(Config.TDFConfig tdfConfig, Config.KA return keyAccess; } - private ECKeyWrappedKeyInfo createECWrappedKey(Config.TDFConfig tdfConfig, Config.KASInfo kasInfo, byte[] symKey) { + private ECKeyWrappedKeyInfo createECWrappedKey(Config.TDFConfig tdfConfig, Config.KASInfo kasInfo, + byte[] symKey) { var curveName = tdfConfig.wrappingKeyType.getCurveName(); var keyPair = new ECKeyPair(curveName, ECKeyPair.ECAlgorithm.ECDH); @@ -294,14 +302,13 @@ private ECKeyWrappedKeyInfo createECWrappedKey(Config.TDFConfig tdfConfig, Confi return wrappedKeyInfo; } - private String createRSAWrappedKey(Config.KASInfo kasInfo, byte[] symKey) { + private String createRSAWrappedKey(Config.KASInfo kasInfo, byte[] symKey) { AsymEncryption asymEncrypt = new AsymEncryption(kasInfo.PublicKey); byte[] wrappedKey = asymEncrypt.encrypt(symKey); return Base64.getEncoder().encodeToString(wrappedKey); } } - private static final Base64.Decoder decoder = Base64.getDecoder(); public static class Reader { @@ -340,14 +347,17 @@ public void readPayload(OutputStream outputStream) throws SDK.SegmentSignatureMi for (Manifest.Segment segment : manifest.encryptionInformation.integrityInformation.segments) { if (segment.encryptedSegmentSize > Config.MAX_SEGMENT_SIZE) { - throw new IllegalStateException("Segment size " + segment.encryptedSegmentSize + " exceeded limit " + Config.MAX_SEGMENT_SIZE); - } // MIN_SEGMENT_SIZE NOT validated out due to tests needing small segment sizes with existing payloads + throw new IllegalStateException("Segment size " + segment.encryptedSegmentSize + " exceeded limit " + + Config.MAX_SEGMENT_SIZE); + } // MIN_SEGMENT_SIZE NOT validated out due to tests needing small segment sizes + // with existing payloads byte[] readBuf = new byte[(int) segment.encryptedSegmentSize]; int bytesRead = tdfReader.readPayloadBytes(readBuf); if (readBuf.length != bytesRead) { - throw new IllegalStateException("unable to read bytes for segment (wanted " + segment.encryptedSegmentSize + " but got " + bytesRead + ")"); + throw new IllegalStateException("unable to read bytes for segment (wanted " + + segment.encryptedSegmentSize + " but got " + bytesRead + ")"); } var isLegacyTdf = manifest.tdfVersion == null || manifest.tdfVersion.isEmpty(); @@ -393,13 +403,15 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte } if (kGMACPayloadLength > data.length) { - throw new IllegalArgumentException("tried to calculate GMAC on too small a payload. payload is "+ data.length + "bytes while GMAC is " + kGMACPayloadLength + " bytes"); + throw new IllegalArgumentException("tried to calculate GMAC on too small a payload. payload is " + + data.length + "bytes while GMAC is " + kGMACPayloadLength + " bytes"); } return Arrays.copyOfRange(data, data.length - kGMACPayloadLength, data.length); } - TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException { + TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) + throws SDKException, IOException { if (tdfConfig.autoconfigure) { Autoconfigure.Granter granter = new Autoconfigure.Granter(new ArrayList<>()); @@ -429,11 +441,14 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo // Add System Metadata Assertion if configured if (tdfConfig.systemMetadataAssertion) { - // TDF_VERSION is used for both tdfSpecVersion and as a placeholder for sdkInternalVersion. + // TDF_VERSION is used for both tdfSpecVersion and as a placeholder for + // sdkInternalVersion. // Consider defining a specific SDK_VERSION constant for the second parameter // if a distinct SDK version string (e.g., "0.1.0") is desired. - AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig(TDF_VERSION, TDF_VERSION); - // tdfConfig.assertionConfigList is initialized in TDFConfig constructor, so it won't be null. + AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig(TDF_VERSION, + TDF_VERSION); + // tdfConfig.assertionConfigList is initialized in TDFConfig constructor, so it + // won't be null. tdfConfig.assertionConfigList.add(systemAssertion); } @@ -488,7 +503,8 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo Manifest.RootSignature rootSignature = new Manifest.RootSignature(); - byte[] rootSig = calculateSignature(aggregateHash.toByteArray(), tdfObject.payloadKey, tdfConfig.integrityAlgorithm); + byte[] rootSig = calculateSignature(aggregateHash.toByteArray(), tdfObject.payloadKey, + tdfConfig.integrityAlgorithm); byte[] encodedRootSig = tdfConfig.hexEncodeRootAndSegmentHashes ? Hex.encodeHexString(rootSig).getBytes(StandardCharsets.UTF_8) : rootSig; @@ -553,8 +569,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo } var hashValues = new Manifest.Assertion.HashValues( assertionHashAsHex, - encodedHash - ); + encodedHash); try { assertion.sign(hashValues, assertionSigningKey); } catch (KeyLengthException e) { @@ -593,13 +608,16 @@ Reader loadTDF(SeekableByteChannel tdf, String platformUrl) throws SDKException, return loadTDF(tdf, Config.newTDFReaderConfig(), platformUrl); } - Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig, String platformUrl) throws SDKException, IOException { - if (!tdfReaderConfig.ignoreKasAllowlist && (tdfReaderConfig.kasAllowlist == null || tdfReaderConfig.kasAllowlist.isEmpty())) { + Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig, String platformUrl) + throws SDKException, IOException { + if (!tdfReaderConfig.ignoreKasAllowlist + && (tdfReaderConfig.kasAllowlist == null || tdfReaderConfig.kasAllowlist.isEmpty())) { ListKeyAccessServersRequest request = ListKeyAccessServersRequest.newBuilder() .build(); ListKeyAccessServersResponse response; try { - response = RequestHelper.getOrThrow(services.kasRegistry().listKeyAccessServersBlocking(request, Collections.emptyMap()).execute()); + response = RequestHelper.getOrThrow( + services.kasRegistry().listKeyAccessServersBlocking(request, Collections.emptyMap()).execute()); } catch (ConnectException e) { throw new SDKException("error getting kas servers", e); } @@ -641,13 +659,17 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig) 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 SDK.KasAllowlistException("No KAS allowlist provided and no KeyAccessServerRegistry available"); + logger.error( + "KasAllowlist: No KAS allowlist provided and no KeyAccessServerRegistry available, {} is not allowed", + realAddress); + throw new SDK.KasAllowlistException( + "No KAS allowlist provided and no KeyAccessServerRegistry available"); } else if (!tdfReaderConfig.kasAllowlist.contains(realAddress)) { logger.error("KasAllowlist: kas url {} is not allowed", realAddress); - throw new SDK.KasAllowlistException("KasAllowlist: kas url "+realAddress+" is not allowed"); + throw new SDK.KasAllowlistException("KasAllowlist: kas url " + realAddress + " is not allowed"); } - unwrappedKey = services.kas().unwrap(keyAccess, manifest.encryptionInformation.policy, tdfReaderConfig.sessionKeyType); + unwrappedKey = services.kas().unwrap(keyAccess, manifest.encryptionInformation.policy, + tdfReaderConfig.sessionKeyType); } catch (Exception e) { skippedSplits.put(ss, e); continue; @@ -740,7 +762,8 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig) int encryptedSegSize = manifest.encryptionInformation.integrityInformation.encryptedSegmentSizeDefault; if (segmentSize != encryptedSegSize - (kGcmIvSize + kAesBlockSize)) { - throw new IllegalStateException("segment size mismatch. encrypted segment size differs from plaintext segment size. the TDF is invalid"); + throw new IllegalStateException( + "segment size mismatch. encrypted segment size differs from plaintext segment size. the TDF is invalid"); } var aggregateHashByteArrayBytes = aggregateHash.toByteArray(); From a7b5ec6eb880ae04f01cb8d0130f3ea9c4286524 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 12:42:18 -0400 Subject: [PATCH 03/23] more changes --- .../opentdf/platform/sdk/AssertionConfig.java | 61 +++---- .../java/io/opentdf/platform/sdk/TDF.java | 18 +- .../java/io/opentdf/platform/sdk/Version.java | 6 +- .../java/io/opentdf/platform/sdk/TDFTest.java | 157 ++++++++++-------- 4 files changed, 132 insertions(+), 110 deletions(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java index fe04ebbb..088f54c4 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -1,6 +1,5 @@ package io.opentdf.platform.sdk; - import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; @@ -11,7 +10,8 @@ import java.util.Objects; /** - * Represents the configuration for assertions, encapsulating various types, scopes, states, keys, + * Represents the configuration for assertions, encapsulating various types, + * scopes, states, keys, * and statements involved in assertion handling. */ public class AssertionConfig { @@ -134,58 +134,61 @@ public int hashCode() { */ static private class SystemMetadata { @SerializedName("tdf_spec_version") - String tdfSpecVersion; + final String tdfSpecVersion; @SerializedName("creation_date") - String creationDate; + final String creationDate; @SerializedName("operating_system") - String operatingSystem; + final String operatingSystem; @SerializedName("sdk_version") - String sdkVersion; - - @SerializedName("hostname") - String hostname; + final String sdkVersion; @SerializedName("java_version") // Corresponds to "go_version" in the Go example - String javaVersion; + final String javaVersion; @SerializedName("architecture") - String architecture; + final String architecture; + + SystemMetadata(String tdfSpecVersion, String creationDate, String operatingSystem, + String sdkVersion, String javaVersion, String architecture) { + this.tdfSpecVersion = tdfSpecVersion; + this.creationDate = creationDate; + this.operatingSystem = operatingSystem; + this.sdkVersion = sdkVersion; + this.javaVersion = javaVersion; + this.architecture = architecture; + } } /** * Returns a default assertion configuration with predefined system metadata. - * This method mimics the behavior of the Go function GetSystemMetadataAssertionConfig. + * This method mimics the behavior of the Go function + * GetSystemMetadataAssertionConfig. * * @param tdfSpecVersionFromSDK The TDF specification version (e.g., "4.3.0"). - * @param sdkInternalVersion The internal version of this SDK (e.g., "1.0.0"), which will be prefixed with "Java-". + * @param sdkInternalVersion The internal version of this SDK (e.g., + * "1.0.0"), which will be prefixed with "Java-". * @return An {@link AssertionConfig} populated with system metadata. * @throws SDKException if there's an error marshalling the metadata to JSON. */ - public static AssertionConfig getSystemMetadataAssertionConfig(String tdfSpecVersionFromSDK, String sdkInternalVersion) { - SystemMetadata metadata = new SystemMetadata(); - metadata.tdfSpecVersion = tdfSpecVersionFromSDK; - metadata.creationDate = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); - metadata.operatingSystem = System.getProperty("os.name"); - metadata.sdkVersion = "Java-" + sdkInternalVersion; - metadata.javaVersion = System.getProperty("java.version"); - metadata.architecture = System.getProperty("os.arch"); + public static AssertionConfig getSystemMetadataAssertionConfig(String tdfSpecVersionFromSDK, + String sdkInternalVersion) { + String creationDate = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + String operatingSystem = System.getProperty("os.name"); + String sdkVersion = "Java-" + sdkInternalVersion; + String javaVersion = System.getProperty("java.version"); + String architecture = System.getProperty("os.arch"); - try { - metadata.hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - // Mimic Go behavior: if hostname retrieval fails, it's omitted. - // Gson will omit null fields by default. - // Optionally, log this exception: e.g., logger.warn("Could not retrieve hostname", e); - } + SystemMetadata metadata = new SystemMetadata(tdfSpecVersionFromSDK, creationDate, operatingSystem, + sdkVersion, javaVersion, architecture); Gson gson = new Gson(); // A new Gson instance is used for simplicity here. String metadataJSON; try { metadataJSON = gson.toJson(metadata); - } catch (Exception e) { // Catch general exception from Gson, though it's usually for I/O or reflection issues. + } catch (com.google.gson.JsonIOException | com.google.gson.JsonSyntaxException e) { throw new SDKException("Failed to marshal system metadata to JSON", e); } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java index 33c11484..8a733de9 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -52,7 +52,10 @@ private static byte[] tdfECKeySaltCompute() { public static final byte[] GLOBAL_KEY_SALT = tdfECKeySaltCompute(); static final String EMPTY_SPLIT_ID = ""; // Made package-private for TDFTest usage if needed, or could be private if // not used by TDFTest - public static final String TDF_VERSION = "4.3.0"; // Made public for TDFTest usage + /** + * The TDF specification version this SDK implements. + */ + public static final String TDF_SPEC_VERSION = "4.3.0"; private static final String KEY_ACCESS_SCHEMA_VERSION = "1.0"; private final long maximumSize; @@ -148,7 +151,7 @@ private PolicyObject createPolicyObject(List at private static final Base64.Encoder encoder = Base64.getEncoder(); private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { - manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_VERSION : null; + manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_SPEC_VERSION : null; manifest.encryptionInformation.keyAccessType = kSplitKeyType; manifest.encryptionInformation.keyAccessObj = new ArrayList<>(); @@ -441,14 +444,9 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo // Add System Metadata Assertion if configured if (tdfConfig.systemMetadataAssertion) { - // TDF_VERSION is used for both tdfSpecVersion and as a placeholder for - // sdkInternalVersion. - // Consider defining a specific SDK_VERSION constant for the second parameter - // if a distinct SDK version string (e.g., "0.1.0") is desired. - AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig(TDF_VERSION, - TDF_VERSION); - // tdfConfig.assertionConfigList is initialized in TDFConfig constructor, so it - // won't be null. + AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig( + TDF_SPEC_VERSION, // This is the TDF specification version + SdkInfo.VERSION); // This is the SDK's own version from pom.xml tdfConfig.assertionConfigList.add(systemAssertion); } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Version.java b/sdk/src/main/java/io/opentdf/platform/sdk/Version.java index f0d5f809..b09177d1 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Version.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Version.java @@ -16,7 +16,8 @@ class Version implements Comparable { private final String prereleaseAndMetadata; private static final Logger log = LoggerFactory.getLogger(Version.class); - Pattern SEMVER_PATTERN = Pattern.compile("^(?0|[1-9]\\d*)\\.(?0|[1-9]\\d*)\\.(?0|[1-9]\\d*)(?\\D.*)?$"); + Pattern SEMVER_PATTERN = Pattern.compile( + "^(?0|[1-9]\\d*)\\.(?0|[1-9]\\d*)\\.(?0|[1-9]\\d*)(?\\D.*)?$"); @Override public String toString() { @@ -63,7 +64,8 @@ public int compareTo(@Nonnull Version o) { @Override public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; + if (o == null || getClass() != o.getClass()) + return false; Version version = (Version) o; return major == version.major && minor == version.minor && patch == version.patch; } 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 ed5a7579..1ab1665a 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -81,7 +81,7 @@ public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessio } var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); if (sessionKeyType.isEc()) { - var kasPrivateKey = CryptoUtils.getPrivateKeyPEM(keypairs.get(index).getPrivate()); + var kasPrivateKey = CryptoUtils.getPrivateKeyPEM(keypairs.get(index).getPrivate()); var privateKey = ECKeyPair.privateKeyFromPem(kasPrivateKey); var clientEphemeralPublicKey = keyAccess.ephemeralPublicKey; var publicKey = ECKeyPair.publicKeyFromPem(clientEphemeralPublicKey); @@ -133,11 +133,11 @@ static void setupKeyPairsAndMocks() { List kasRegEntries = new ArrayList<>(); for (Config.KASInfo kasInfo : getRSAKASInfos()) { kasRegEntries.add(KeyAccessServer.newBuilder() - .setUri(kasInfo.URL).build()); + .setUri(kasInfo.URL).build()); } for (Config.KASInfo kasInfo : getECKASInfos()) { kasRegEntries.add(KeyAccessServer.newBuilder() - .setUri(kasInfo.URL).build()); + .setUri(kasInfo.URL).build()); } ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() .addAllKeyAccessServers(kasRegEntries) @@ -148,7 +148,8 @@ static void setupKeyPairsAndMocks() { .thenReturn(new UnaryBlockingCall<>() { @Override public ResponseMessage execute() { - return new ResponseMessage.Success<>(mockResponse, Collections.emptyMap(), Collections.emptyMap()); + return new ResponseMessage.Success<>(mockResponse, Collections.emptyMap(), + Collections.emptyMap()); } @Override @@ -192,29 +193,30 @@ public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReade List tdfConfigPairs = List.of( new TDFConfigPair( - Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(getRSAKASInfos()), + Config.newTDFConfig(Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), Config.withMetaData("here is some metadata"), - Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), + Config.withDataAttributes("https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"), Config.withAssertionConfig(assertion1)), - Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys)) - ), + Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys))), new TDFConfigPair( - Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(getECKASInfos()), + Config.newTDFConfig(Config.withAutoconfigure(false), Config.withKasInformation(getECKASInfos()), Config.withMetaData("here is some metadata"), Config.WithWrappingKeyAlg(KeyType.EC256Key), - Config.withDataAttributes("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"), + Config.withDataAttributes("https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"), Config.withAssertionConfig(assertion1)), Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys), - Config.WithSessionKeyType(KeyType.EC256Key)) - ) - ); + Config.WithSessionKeyType(KeyType.EC256Key)))); for (TDFConfigPair configPair : tdfConfigPairs) { String plainText = "this is extremely sensitive stuff!!!"; InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); var manifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig).getManifest(); assertThat(manifest.assertions).asList().hasSize(1); @@ -226,11 +228,13 @@ public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReade assertThat(assertion.statement.format).isEqualTo("base64binary"); assertThat(manifest.payload.isEncrypted).isTrue(); - var size = manifest.encryptionInformation.integrityInformation.segments.stream().map(s -> s.segmentSize).reduce(0L, Long::sum); + var size = manifest.encryptionInformation.integrityInformation.segments.stream().map(s -> s.segmentSize) + .reduce(0L, Long::sum); assertThat(size).isEqualTo(plainText.getBytes().length); var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), configPair.tdfReaderConfig, platformUrl); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + configPair.tdfReaderConfig, platformUrl); assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); reader.readPayload(unwrappedData); @@ -242,8 +246,10 @@ public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReade var policyObject = reader.readPolicyObject(); assertThat(policyObject).isNotNull(); - assertThat(policyObject.body.dataAttributes.stream().map(a -> a.attribute).collect(Collectors.toList())).asList() - .containsExactlyInAnyOrder("https://example.org/attr/a/value/b", "https://example.org/attr/c/value/d"); + assertThat(policyObject.body.dataAttributes.stream().map(a -> a.attribute).collect(Collectors.toList())) + .asList() + .containsExactlyInAnyOrder("https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"); } } @@ -264,7 +270,7 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { keypair.getPrivate()); var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas"+ 0; + rsaKasInfo.URL = "https://example.com/kas" + 0; Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), @@ -275,7 +281,8 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); var assertionVerificationKeys = new Config.AssertionVerificationKeys(); @@ -285,7 +292,8 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { var unwrappedData = new ByteArrayOutputStream(); Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( Config.withAssertionVerificationKeys(assertionVerificationKeys)); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, + platformUrl); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -318,7 +326,8 @@ void testWithAssertionVerificationDisabled() throws Exception { InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); var assertionVerificationKeys = new Config.AssertionVerificationKeys(); @@ -327,22 +336,25 @@ void testWithAssertionVerificationDisabled() throws Exception { var unwrappedData = new ByteArrayOutputStream(); var dataToUnwrap = new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()); - var emptyConfig= Config.newTDFReaderConfig(); + var emptyConfig = Config.newTDFReaderConfig(); var thrown = assertThrows(SDKException.class, () -> { tdf.loadTDF(dataToUnwrap, emptyConfig, platformUrl); }); assertThat(thrown.getCause()).isInstanceOf(JOSEException.class); - // try with assertion verification disabled and not passing the assertion verification keys + // 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()), readerConfig, platformUrl); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, + platformUrl); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) .withFailMessage("extracted data does not match") .isEqualTo(plainText); } + @Test void testSimpleTDFWithAssertionWithHS256() throws Exception { String assertion1Id = "assertion1"; @@ -368,7 +380,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 = "https://example.com/kas"+ 0; + rsaKasInfo.URL = "https://example.com/kas" + 0; Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), @@ -379,11 +391,13 @@ void testSimpleTDFWithAssertionWithHS256() throws Exception { InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), Config.newTDFReaderConfig(), platformUrl); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + Config.newTDFReaderConfig(), platformUrl); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -431,7 +445,7 @@ void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { assertionConfig1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas"+ 0; + rsaKasInfo.URL = "https://example.com/kas" + 0; Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), @@ -442,16 +456,17 @@ void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); byte[] notkey = new byte[32]; secureRandom.nextBytes(notkey); var assertionVerificationKeys = new Config.AssertionVerificationKeys(); assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, - notkey); + notkey); Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withAssertionVerificationKeys(assertionVerificationKeys)); + Config.withAssertionVerificationKeys(assertionVerificationKeys)); try { tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); @@ -472,11 +487,12 @@ public void testCreatingTDFWithMultipleSegments() throws Exception { Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); // data should be large enough to have multiple complete and a partial segment - var data = new byte[(int)(Config.MIN_SEGMENT_SIZE * 2.8)]; + var data = new byte[(int) (Config.MIN_SEGMENT_SIZE * 2.8)]; random.nextBytes(data); var plainTextInputStream = new ByteArrayInputStream(data); var tdfOutputStream = new ByteArrayOutputStream(); - var tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + var tdf = new TDF( + new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); var unwrappedData = new ByteArrayOutputStream(); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); @@ -549,7 +565,8 @@ public void testCreateTDFWithMimeType() throws Exception { InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); @@ -581,7 +598,8 @@ void legacyTDFRoundTrips() throws IOException { InputStream plainTextInputStream = new ByteArrayInputStream(data); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); var dataOutputStream = new ByteArrayOutputStream(); @@ -590,14 +608,14 @@ void legacyTDFRoundTrips() throws IOException { var integrityInformation = reader.getManifest().encryptionInformation.integrityInformation; assertThat(reader.getManifest().tdfVersion).isNull(); var decodedSignature = Base64.getDecoder().decode(integrityInformation.rootSignature.signature); - for (var b: decodedSignature) { + for (var b : decodedSignature) { assertThat(isHexChar(b)) .withFailMessage("non-hex byte in signature: " + b) .isTrue(); } - for (var s: integrityInformation.segments) { + for (var s : integrityInformation.segments) { var decodedSegmentSignature = Base64.getDecoder().decode(s.hash); - for (var b: decodedSegmentSignature) { + for (var b : decodedSegmentSignature) { assertThat(isHexChar(b)) .withFailMessage("non-hex byte in segment signature: " + b) .isTrue(); @@ -629,7 +647,8 @@ void testSystemMetadataAssertion() throws Exception { InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); var createdManifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, tdfConfig).getManifest(); // Verify the created manifest directly @@ -647,20 +666,16 @@ void testSystemMetadataAssertion() throws Exception { Gson gson = new Gson(); Map metadataMap = gson.fromJson(sysAssertion.statement.value, Map.class); assertThat(metadataMap).containsKey("tdf_spec_version"); - assertThat(metadataMap.get("tdf_spec_version")).isEqualTo(TDF.TDF_VERSION); // Assuming TDF_VERSION is accessible or use a known value + assertThat(metadataMap.get("tdf_spec_version")).isEqualTo(TDF.TDF_SPEC_VERSION); assertThat(metadataMap).containsKey("creation_date"); assertThat(metadataMap).containsKey("operating_system"); assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); - assertThat(metadataMap).containsKey("sdk_version"); + assertThat(metadataMap)containsKey("sdk_version"); assertThat(metadataMap.get("sdk_version")).startsWith("Java-"); assertThat(metadataMap).containsKey("java_version"); // Corresponds to go_version assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); assertThat(metadataMap).containsKey("architecture"); assertThat(metadataMap.get("architecture")).isEqualTo(System.getProperty("os.arch")); - // Hostname is optional, so we just check if it's there or not, not its specific value. - // If it's not retrievable, Gson will omit it. - // assertThat(metadataMap).containsKey("hostname"); // This could fail if hostname is not retrievable - } @Test @@ -669,7 +684,7 @@ void testKasAllowlist() throws Exception { KeyAccessServerRegistryServiceClient kasRegistryServiceNoUrl = mock(KeyAccessServerRegistryServiceClient.class); List kasRegEntries = new ArrayList<>(); kasRegEntries.add(KeyAccessServer.newBuilder() - .setUri("http://example.com/kas0").build()); + .setUri("http://example.com/kas0").build()); ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() .addAllKeyAccessServers(kasRegEntries) @@ -678,20 +693,20 @@ void testKasAllowlist() throws Exception { // Stub the listKeyAccessServers method when(kasRegistryServiceNoUrl.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), any())) .thenReturn(new UnaryBlockingCall<>() { - @Override - public ResponseMessage execute() { - return new ResponseMessage.Success<>(mockResponse, Collections.emptyMap(), Collections.emptyMap()); - } - - @Override - public void cancel() { - // we never do this during tests - } - } - ); + @Override + public ResponseMessage execute() { + return new ResponseMessage.Success<>(mockResponse, Collections.emptyMap(), + Collections.emptyMap()); + } + + @Override + public void cancel() { + // we never do this during tests + } + }); var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas"+Integer.toString(0); + rsaKasInfo.URL = "https://example.com/kas" + Integer.toString(0); Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), @@ -701,14 +716,16 @@ public void cancel() { InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryServiceNoUrl).build()); + TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryServiceNoUrl).build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); var unwrappedData = new ByteArrayOutputStream(); // should throw error because the kas url is not in the allowlist try { - tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), Config.newTDFReaderConfig(), platformUrl); + tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), Config.newTDFReaderConfig(), + platformUrl); throw new RuntimeException("expected allowlist error to be thrown"); } catch (Exception e) { assertThat(e).hasMessageContaining("KasAllowlist"); @@ -716,33 +733,35 @@ public void cancel() { // with custom allowlist should succeed Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.WithKasAllowlist("https://example.com")); + Config.WithKasAllowlist("https://example.com")); tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); // with ignore allowlist should succeed readerConfig = Config.newTDFReaderConfig( - Config.WithIgnoreKasAllowlist(true)); - Reader reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); + Config.WithIgnoreKasAllowlist(true)); + Reader reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, + 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); + 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(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryServiceNoUrl).build()); + tdf = new TDF(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryServiceNoUrl) + .build()); tdf.createTDF(plainTextInputStream, tdfOutputStream, config); unwrappedData = new ByteArrayOutputStream(); - reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), Config.newTDFReaderConfig(), platformUrl); + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + Config.newTDFReaderConfig(), platformUrl); reader.readPayload(unwrappedData); assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) @@ -756,7 +775,7 @@ private static Config.KASInfo[] getKASInfos(Predicate filter) { for (int i = 0; i < keypairs.size(); i++) { if (filter.test(i)) { var kasInfo = new Config.KASInfo(); - kasInfo.URL = "https://example.com/kas"+Integer.toString(i); + kasInfo.URL = "https://example.com/kas" + Integer.toString(i); kasInfo.PublicKey = null; kasInfos.add(kasInfo); } From e3955047fa0ff37664abc61fd494a30d3343e1f7 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 12:43:06 -0400 Subject: [PATCH 04/23] add version.properties --- .../main/resources/io/opentdf/platform/sdk/version.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 sdk/src/main/resources/io/opentdf/platform/sdk/version.properties diff --git a/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties b/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties new file mode 100644 index 00000000..f4f04119 --- /dev/null +++ b/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties @@ -0,0 +1 @@ +version.properties=${project.version} \ No newline at end of file From c048381a56fe27a4b425b08ed7af4e3cf7ab162f Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 12:46:07 -0400 Subject: [PATCH 05/23] add resources --- sdk/pom.xml | 78 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/sdk/pom.xml b/sdk/pom.xml index 795d34ba..77e6b320 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 io.opentdf.platform:sdk sdk @@ -11,7 +13,8 @@ jar 0.22.1 - https://github.com/CodeIntelligenceTesting/jazzer/releases/download/v${jazzer.version} + + https://github.com/CodeIntelligenceTesting/jazzer/releases/download/v${jazzer.version} 2.1.0 0.7.2 4.12.0 @@ -287,6 +290,12 @@ + + + src/main/resources + true + + @@ -362,17 +371,18 @@ - - - - + + + + - - - - + + + + @@ -475,7 +485,8 @@ - + fuzz @@ -499,18 +510,21 @@ - + - + - + - - - - + + + + @@ -552,22 +566,28 @@ - - - - + + + + - - + + - + - + - - + + @@ -581,4 +601,4 @@ - + \ No newline at end of file From 22dae5741deefb1cc65373ae4947d1b67cf9c88a Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 12:49:06 -0400 Subject: [PATCH 06/23] add sdk info class --- .../java/io/opentdf/platform/sdk/SdkInfo.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/SdkInfo.java diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SdkInfo.java b/sdk/src/main/java/io/opentdf/platform/sdk/SdkInfo.java new file mode 100644 index 00000000..4fe62707 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/SdkInfo.java @@ -0,0 +1,43 @@ +package io.opentdf.platform.sdk; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Provides information about the SDK, such as its version. + * The version is read from a properties file populated during the Maven build + * process. + */ +public final class SdkInfo { + private static final Logger logger = LoggerFactory.getLogger(SdkInfo.class); + private static final String VERSION_PROPERTIES_FILE = "version.properties"; // Relative to this class's package + private static final String SDK_VERSION_PROPERTY = "sdk.version"; + + public static final String VERSION; + + static { + String versionString = "unknown"; // Default if properties can't be read + Properties props = new Properties(); + try (InputStream input = SdkInfo.class.getResourceAsStream(VERSION_PROPERTIES_FILE)) { + if (input == null) { + logger.error("Unable to find " + VERSION_PROPERTIES_FILE + + ". SDK version will be 'unknown'. Ensure it's in src/main/resources/io/opentdf/platform/sdk/"); + } else { + props.load(input); + versionString = props.getProperty(SDK_VERSION_PROPERTY, "unknown"); + } + } catch (IOException ex) { + logger.error("Error loading " + VERSION_PROPERTIES_FILE + ". SDK version will be 'unknown'.", ex); + } + VERSION = versionString; + logger.info("OpenTDF SDK Version: {}", VERSION); + } + + private SdkInfo() { + // Private constructor to prevent instantiation of this utility class + } +} \ No newline at end of file From 8b561ed6fb510bf5524c62be4d47cd95a4804fd1 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 12:51:58 -0400 Subject: [PATCH 07/23] fix missing period --- 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 1ab1665a..09190d3d 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -670,7 +670,7 @@ void testSystemMetadataAssertion() throws Exception { assertThat(metadataMap).containsKey("creation_date"); assertThat(metadataMap).containsKey("operating_system"); assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); - assertThat(metadataMap)containsKey("sdk_version"); + assertThat(metadataMap).containsKey("sdk_version"); assertThat(metadataMap.get("sdk_version")).startsWith("Java-"); assertThat(metadataMap).containsKey("java_version"); // Corresponds to go_version assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); From 883fa4f68eb503f8e8043c96b0cfa632ebcb32ed Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 12:59:50 -0400 Subject: [PATCH 08/23] fix test --- .../test/java/io/opentdf/platform/sdk/TDFTest.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 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 09190d3d..48b1690d 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -35,12 +35,12 @@ import java.util.stream.Collectors; import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT; -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.mock; import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThat; public class TDFTest { protected static KeyAccessServerRegistryServiceClient kasRegistryService; @@ -664,18 +664,17 @@ void testSystemMetadataAssertion() throws Exception { // Deserialize and check the metadata JSON Gson gson = new Gson(); + // This line is crucial. It correctly types the variable. Map metadataMap = gson.fromJson(sysAssertion.statement.value, Map.class); + + // Now, these assertions will work correctly because AssertJ knows metadataMap + // is a Map. assertThat(metadataMap).containsKey("tdf_spec_version"); - assertThat(metadataMap.get("tdf_spec_version")).isEqualTo(TDF.TDF_SPEC_VERSION); assertThat(metadataMap).containsKey("creation_date"); assertThat(metadataMap).containsKey("operating_system"); - assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); assertThat(metadataMap).containsKey("sdk_version"); - assertThat(metadataMap.get("sdk_version")).startsWith("Java-"); - assertThat(metadataMap).containsKey("java_version"); // Corresponds to go_version - assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); + assertThat(metadataMap).containsKey("java_version"); assertThat(metadataMap).containsKey("architecture"); - assertThat(metadataMap.get("architecture")).isEqualTo(System.getProperty("os.arch")); } @Test From 2d82df3c6c3ff5450b2df6a13cbb6c5531049473 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 13:07:19 -0400 Subject: [PATCH 09/23] validate assertion values --- .../test/java/io/opentdf/platform/sdk/TDFTest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 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 48b1690d..c2ea02cc 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -2,6 +2,7 @@ import com.connectrpc.ResponseMessage; import com.connectrpc.UnaryBlockingCall; +import com.google.gson.reflect.TypeToken; import com.nimbusds.jose.JOSEException; import com.google.gson.Gson; import io.opentdf.platform.policy.KeyAccessServer; @@ -664,17 +665,22 @@ void testSystemMetadataAssertion() throws Exception { // Deserialize and check the metadata JSON Gson gson = new Gson(); - // This line is crucial. It correctly types the variable. - Map metadataMap = gson.fromJson(sysAssertion.statement.value, Map.class); + java.lang.reflect.Type mapType = new TypeToken>() { + }.getType(); + Map metadataMap = gson.fromJson(sysAssertion.statement.value, mapType); - // Now, these assertions will work correctly because AssertJ knows metadataMap - // is a Map. assertThat(metadataMap).containsKey("tdf_spec_version"); + assertThat(metadataMap.get("tdf_spec_version")).isEqualTo(TDF.TDF_SPEC_VERSION); assertThat(metadataMap).containsKey("creation_date"); + assertThat(metadataMap.get("creation_date")).isNotBlank(); assertThat(metadataMap).containsKey("operating_system"); + assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); assertThat(metadataMap).containsKey("sdk_version"); + assertThat(metadataMap.get("sdk_version")).isEqualTo("Java-" + SdkInfo.VERSION); assertThat(metadataMap).containsKey("java_version"); + assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); assertThat(metadataMap).containsKey("architecture"); + assertThat(metadataMap.get("architecture")).isEqualTo(System.getProperty("os.arch")); } @Test From 32e1e398d971dfa5fc45536ef3204af22d1c6c87 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 15:43:58 -0400 Subject: [PATCH 10/23] minor fixes --- .../java/io/opentdf/platform/Command.java | 74 +- .../opentdf/platform/sdk/version.properties | 2 +- .../java/io/opentdf/platform/sdk/TDFTest.java | 1472 +++++++++-------- 3 files changed, 798 insertions(+), 750 deletions(-) diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index a7044c28..960057df 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -54,15 +54,11 @@ class Versions { public static final String TDF_SPEC = "4.3.0"; } -@CommandLine.Command( - name = "tdf", - subcommands = {HelpCommand.class}, - version = - "{\"version\":\"" + Versions.SDK + "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}" -) +@CommandLine.Command(name = "tdf", subcommands = { HelpCommand.class }, version = "{\"version\":\"" + Versions.SDK + + "\",\"tdfSpecVersion\":\"" + Versions.TDF_SPEC + "\"}") class Command { - @Option(names = {"-V", "--version"}, versionHelp = true, description = "display version info") + @Option(names = { "-V", "--version" }, versionHelp = true, description = "display version info") boolean versionInfoRequested; private static final String PRIVATE_KEY_HEADER = "-----BEGIN PRIVATE KEY-----"; @@ -85,7 +81,8 @@ class Command { @Option(names = { "-p", "--platform-endpoint" }, required = true) private String platformEndpoint; - private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, boolean publicKey) throws RuntimeException{ + private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, boolean publicKey) + throws RuntimeException { if (alg == AssertionConfig.AssertionKeyAlg.HS256) { if (key instanceof String) { key = ((String) key).getBytes(StandardCharsets.UTF_8); @@ -101,14 +98,14 @@ private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, b } String pem = (String) key; String pemWithNewlines = pem.replace("\\n", "\n"); - if (publicKey){ - String base64EncodedPem= pemWithNewlines - .replaceAll(PEM_HEADER, "") - .replaceAll(PEM_FOOTER, "") - .replaceAll("\\s", "") - .replaceAll("\r\n", "") - .replaceAll("\n", "") - .trim(); + if (publicKey) { + String base64EncodedPem = pemWithNewlines + .replaceAll(PEM_HEADER, "") + .replaceAll(PEM_FOOTER, "") + .replaceAll("\\s", "") + .replaceAll("\r\n", "") + .replaceAll("\n", "") + .trim(); byte[] decoded = Base64.getDecoder().decode(base64EncodedPem); X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded); KeyFactory kf = null; @@ -122,7 +119,7 @@ private Object correctKeyType(AssertionConfig.AssertionKeyAlg alg, Object key, b } catch (InvalidKeySpecException e) { throw new RuntimeException(e); } - }else { + } else { String privateKeyPEM = pemWithNewlines .replace(PRIVATE_KEY_HEADER, "") .replace(PRIVATE_KEY_FOOTER, "") @@ -173,6 +170,7 @@ void encrypt( List> configs = new ArrayList<>(); configs.add(Config.withKasInformation(kasInfos)); + configs.add(Config.withSystemMetadataAssertion()); metadata.map(Config::withMetaData).ifPresent(configs::add); autoconfigure.map(Config::withAutoconfigure).ifPresent(configs::add); encapKeyType.map(Config::WithWrappingKeyAlg).ifPresent(configs::add); @@ -191,8 +189,9 @@ void encrypt( String fileJson = new String(Files.readAllBytes(Paths.get(assertionConfig))); assertionConfigs = gson.fromJson(fileJson, AssertionConfig[].class); } catch (JsonSyntaxException e2) { - throw new RuntimeException("Failed to parse assertion from file, expects an list of assertions", e2); - } catch(Exception e3) { + throw new RuntimeException("Failed to parse assertion from file, expects an list of assertions", + e2); + } catch (Exception e3) { throw new RuntimeException("Could not parse assertion as json string or path to file", e3); } } @@ -238,11 +237,15 @@ private SDK buildSDK() { @CommandLine.Command(name = "decrypt") 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, + @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, @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, - @Option(names = { "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) + @Option(names = { + "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) throws Exception { try (var sdk = buildSDK()) { var opts = new ArrayList>(); @@ -254,7 +257,8 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, AssertionVerificationKeys assertionVerificationKeys; try { - assertionVerificationKeys = gson.fromJson(assertionVerificationInput, AssertionVerificationKeys.class); + assertionVerificationKeys = gson.fromJson(assertionVerificationInput, + AssertionVerificationKeys.class); } catch (JsonSyntaxException e) { // try it as a file path try { @@ -263,16 +267,20 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, } catch (JsonSyntaxException e2) { throw new RuntimeException("Failed to parse assertion verification keys from file", e2); } catch (Exception e3) { - throw new RuntimeException("Could not parse assertion verification keys as json string or path to file", e3); + throw new RuntimeException( + "Could not parse assertion verification keys as json string or path to file", + e3); } } - for (Map.Entry entry : assertionVerificationKeys.keys.entrySet()) { + for (Map.Entry entry : assertionVerificationKeys.keys + .entrySet()) { try { Object correctedKey = correctKeyType(entry.getValue().alg, entry.getValue().key, true); entry.setValue(new AssertionConfig.AssertionKey(entry.getValue().alg, correctedKey)); } catch (Exception e) { - throw new RuntimeException("Error with assertion verification key: " + e.getMessage(), e); + throw new RuntimeException("Error with assertion verification key: " + e.getMessage(), + e); } } opts.add(Config.withAssertionVerificationKeys(assertionVerificationKeys)); @@ -296,8 +304,10 @@ void decrypt(@Option(names = { "-f", "--file" }, required = true) Path tdfPath, @CommandLine.Command(name = "metadata") 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 { + @Option(names = { "--kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional kasAllowlistStr, + @Option(names = { + "--ignore-kas-allowlist" }, defaultValue = Option.NULL_VALUE) Optional ignoreAllowlist) + throws IOException { var sdk = buildSDK(); var opts = new ArrayList>(); try (var in = FileChannel.open(tdfPath, StandardOpenOption.READ)) { @@ -344,8 +354,10 @@ void createNanoTDF( @CommandLine.Command(name = "decryptnano") 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 { + @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)) { diff --git a/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties b/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties index f4f04119..325bf768 100644 --- a/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties +++ b/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties @@ -1 +1 @@ -version.properties=${project.version} \ No newline at end of file +sdk.version=${project.version} \ No newline at end of file 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 c2ea02cc..5f47f452 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -44,761 +44,797 @@ import static org.assertj.core.api.Assertions.assertThat; public class TDFTest { - protected static KeyAccessServerRegistryServiceClient kasRegistryService; - protected static String platformUrl = "http://localhost:8080"; + protected static KeyAccessServerRegistryServiceClient kasRegistryService; + protected static String platformUrl = "http://localhost:8080"; - protected static SDK.KAS kas = new SDK.KAS() { - @Override - public void close() { - } + protected static SDK.KAS kas = new SDK.KAS() { + @Override + public void close() { + } + + @Override + public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { + // 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("^" + Pattern.quote(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()); + kiCopy.URL = kasInfo.URL; + return kiCopy; + } + + @Override + public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { + + try { + int index; + // if the keyAccess.url contains the platform url, remove it + if (keyAccess.url.startsWith(platformUrl)) { + index = Integer.parseInt(keyAccess.url + .replaceFirst("^" + Pattern.quote(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()); + var privateKey = ECKeyPair.privateKeyFromPem(kasPrivateKey); + var clientEphemeralPublicKey = keyAccess.ephemeralPublicKey; + var publicKey = ECKeyPair.publicKeyFromPem(clientEphemeralPublicKey); + byte[] symKey = ECKeyPair.computeECDHKey(publicKey, privateKey); + + var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey); + + AesGcm gcm = new AesGcm(sessionKey); + AesGcm.Encrypted encrypted = new AesGcm.Encrypted(bytes); + return gcm.decrypt(encrypted); + } else { + var decryptor = new AsymDecryption(keypairs.get(index).getPrivate()); + return decryptor.decrypt(bytes); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) { + return null; + } + + @Override + public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) { + return null; + } + + @Override + public KASKeyCache getKeyCache() { + return new KASKeyCache(); + } + }; - @Override - public Config.KASInfo getPublicKey(Config.KASInfo kasInfo) { - // 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("^" + Pattern.quote(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()); - kiCopy.URL = kasInfo.URL; - return kiCopy; + private static ArrayList keypairs = new ArrayList<>(); + + @BeforeAll + static void setupKeyPairsAndMocks() { + for (int i = 0; i < 2 + new Random().nextInt(5); i++) { + if (i % 2 == 0) { + keypairs.add(CryptoUtils.generateRSAKeypair()); + } else { + keypairs.add(CryptoUtils.generateECKeypair(KeyType.EC256Key.getCurveName())); + } + } + + kasRegistryService = mock(KeyAccessServerRegistryServiceClient.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.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), any())) + .thenReturn(new UnaryBlockingCall<>() { + @Override + public ResponseMessage execute() { + return new ResponseMessage.Success<>(mockResponse, + Collections.emptyMap(), + Collections.emptyMap()); + } + + @Override + public void cancel() { + // this never happens in tests + } + }); } - @Override - public byte[] unwrap(Manifest.KeyAccess keyAccess, String policy, KeyType sessionKeyType) { + @Test + void testSimpleTDFEncryptAndDecrypt() throws Exception { + + class TDFConfigPair { + public final Config.TDFConfig tdfConfig; + public final Config.TDFReaderConfig tdfReaderConfig; - try { - int index; - // if the keyAccess.url contains the platform url, remove it - if (keyAccess.url.startsWith(platformUrl)) { - index = Integer.parseInt(keyAccess.url.replaceFirst("^" + Pattern.quote(platformUrl) + "/kas", "")); - } else { - index = Integer.parseInt(keyAccess.url.replaceFirst("^https://example.com/kas", "")); + public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReaderConfig) { + this.tdfConfig = tdfConfig; + this.tdfReaderConfig = tdfReaderConfig; + } } - var bytes = Base64.getDecoder().decode(keyAccess.wrappedKey); - if (sessionKeyType.isEc()) { - var kasPrivateKey = CryptoUtils.getPrivateKeyPEM(keypairs.get(index).getPrivate()); - var privateKey = ECKeyPair.privateKeyFromPem(kasPrivateKey); - var clientEphemeralPublicKey = keyAccess.ephemeralPublicKey; - var publicKey = ECKeyPair.publicKeyFromPem(clientEphemeralPublicKey); - byte[] symKey = ECKeyPair.computeECDHKey(publicKey, privateKey); - - var sessionKey = ECKeyPair.calculateHKDF(GLOBAL_KEY_SALT, symKey); - - AesGcm gcm = new AesGcm(sessionKey); - AesGcm.Encrypted encrypted = new AesGcm.Encrypted(bytes); - return gcm.decrypt(encrypted); - } else { - var decryptor = new AsymDecryption(keypairs.get(index).getPrivate()); - return decryptor.decrypt(bytes); + + SecureRandom secureRandom = new SecureRandom(); + byte[] key = new byte[32]; + secureRandom.nextBytes(key); + + var assertion1 = new AssertionConfig(); + assertion1.id = "assertion1"; + assertion1.type = AssertionConfig.Type.BaseAssertion; + assertion1.scope = AssertionConfig.Scope.TrustedDataObj; + assertion1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertion1.statement = new AssertionConfig.Statement(); + assertion1.statement.format = "base64binary"; + assertion1.statement.schema = "text"; + assertion1.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertion1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); + + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey( + AssertionConfig.AssertionKeyAlg.HS256, + key); + + List tdfConfigPairs = List.of( + new TDFConfigPair( + Config.newTDFConfig(Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withMetaData("here is some metadata"), + Config.withDataAttributes( + "https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"), + Config.withAssertionConfig(assertion1)), + Config.newTDFReaderConfig(Config.withAssertionVerificationKeys( + assertionVerificationKeys))), + new TDFConfigPair( + Config.newTDFConfig(Config.withAutoconfigure(false), + Config.withKasInformation(getECKASInfos()), + Config.withMetaData("here is some metadata"), + Config.WithWrappingKeyAlg(KeyType.EC256Key), + Config.withDataAttributes( + "https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"), + Config.withAssertionConfig(assertion1)), + Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys( + assertionVerificationKeys), + Config.WithSessionKeyType(KeyType.EC256Key)))); + + for (TDFConfigPair configPair : tdfConfigPairs) { + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + var manifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig) + .getManifest(); + + assertThat(manifest.assertions).asList().hasSize(1); + var assertion = manifest.assertions.get(0); + assertThat(assertion.appliesToState).isEqualTo("unencrypted"); + assertThat(assertion.type).isEqualTo("base"); + assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); + assertThat(assertion.statement.schema).isEqualTo("text"); + assertThat(assertion.statement.format).isEqualTo("base64binary"); + + assertThat(manifest.payload.isEncrypted).isTrue(); + var size = manifest.encryptionInformation.integrityInformation.segments.stream() + .map(s -> s.segmentSize) + .reduce(0L, Long::sum); + assertThat(size).isEqualTo(plainText.getBytes().length); + + var unwrappedData = new ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + configPair.tdfReaderConfig, platformUrl); + assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); + + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + assertThat(reader.getMetadata()).isEqualTo("here is some metadata"); + + var policyObject = reader.readPolicyObject(); + assertThat(policyObject).isNotNull(); + assertThat(policyObject.body.dataAttributes.stream().map(a -> a.attribute) + .collect(Collectors.toList())) + .asList() + .containsExactlyInAnyOrder("https://example.org/attr/a/value/b", + "https://example.org/attr/c/value/d"); } - } catch (Exception e) { - throw new RuntimeException(e); - } } - @Override - public KASInfo getECPublicKey(Config.KASInfo kasInfo, NanoTDFType.ECCurve curve) { - return null; + @Test + void testSimpleTDFWithAssertionWithRS256() throws Exception { + String assertion1Id = "assertion1"; + var keypair = CryptoUtils.generateRSAKeypair(); + var assertionConfig = new AssertionConfig(); + assertionConfig.id = assertion1Id; + assertionConfig.type = AssertionConfig.Type.BaseAssertion; + assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig.statement = new AssertionConfig.Statement(); + assertionConfig.statement.format = "base64binary"; + assertionConfig.statement.schema = "text"; + assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPrivate()); + + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = "https://example.com/kas" + 0; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(rsaKasInfo), + Config.withAssertionConfig(assertionConfig)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.keys.put(assertion1Id, + new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPublic())); + + var unwrappedData = new ByteArrayOutputStream(); + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(assertionVerificationKeys)); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, + platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); } - @Override - public byte[] unwrapNanoTDF(NanoTDFType.ECCurve curve, String header, String kasURL) { - return null; + @Test + void testWithAssertionVerificationDisabled() throws Exception { + String assertion1Id = "assertion1"; + var keypair = CryptoUtils.generateRSAKeypair(); + var assertionConfig = new AssertionConfig(); + assertionConfig.id = assertion1Id; + assertionConfig.type = AssertionConfig.Type.BaseAssertion; + assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig.statement = new AssertionConfig.Statement(); + assertionConfig.statement.format = "base64binary"; + assertionConfig.statement.schema = "text"; + assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPrivate()); + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withAssertionConfig(assertionConfig)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.keys.put(assertion1Id, + new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, + keypair.getPublic())); + + var unwrappedData = new ByteArrayOutputStream(); + var dataToUnwrap = new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()); + var emptyConfig = Config.newTDFReaderConfig(); + var thrown = assertThrows(SDKException.class, () -> { + tdf.loadTDF(dataToUnwrap, emptyConfig, platformUrl); + }); + assertThat(thrown.getCause()).isInstanceOf(JOSEException.class); + + // 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()), readerConfig, + platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); } - @Override - public KASKeyCache getKeyCache() { - return new KASKeyCache(); + @Test + void testSimpleTDFWithAssertionWithHS256() throws Exception { + String assertion1Id = "assertion1"; + var assertionConfig1 = new AssertionConfig(); + assertionConfig1.id = assertion1Id; + assertionConfig1.type = AssertionConfig.Type.BaseAssertion; + assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig1.statement = new AssertionConfig.Statement(); + assertionConfig1.statement.format = "base64binary"; + assertionConfig1.statement.schema = "text"; + assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; + + String assertion2Id = "assertion2"; + var assertionConfig2 = new AssertionConfig(); + assertionConfig2.id = assertion2Id; + assertionConfig2.type = AssertionConfig.Type.HandlingAssertion; + assertionConfig2.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig2.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig2.statement = new AssertionConfig.Statement(); + assertionConfig2.statement.format = "json"; + assertionConfig2.statement.schema = "urn:nato:stanag:5636:A:1:elements:json"; + assertionConfig2.statement.value = "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"; + + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = "https://example.com/kas" + 0; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(rsaKasInfo), + Config.withAssertionConfig(assertionConfig1, assertionConfig2)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var unwrappedData = new ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + Config.newTDFReaderConfig(), platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + + var manifest = reader.getManifest(); + var assertions = manifest.assertions; + assertThat(assertions.size()).isEqualTo(2); + for (var assertion : assertions) { + if (assertion.id.equals(assertion1Id)) { + assertThat(assertion.statement.format).isEqualTo("base64binary"); + assertThat(assertion.statement.schema).isEqualTo("text"); + assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); + assertThat(assertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); + } else if (assertion.id.equals(assertion2Id)) { + assertThat(assertion.statement.format).isEqualTo("json"); + assertThat(assertion.statement.schema) + .isEqualTo("urn:nato:stanag:5636:A:1:elements:json"); + assertThat(assertion.statement.value).isEqualTo( + "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"); + assertThat(assertion.type).isEqualTo(AssertionConfig.Type.HandlingAssertion.toString()); + } else { + throw new RuntimeException("unexpected assertion id: " + assertion.id); + } + } } - }; - - private static ArrayList keypairs = new ArrayList<>(); - - @BeforeAll - static void setupKeyPairsAndMocks() { - for (int i = 0; i < 2 + new Random().nextInt(5); i++) { - if (i % 2 == 0) { - keypairs.add(CryptoUtils.generateRSAKeypair()); - } else { - keypairs.add(CryptoUtils.generateECKeypair(KeyType.EC256Key.getCurveName())); - } + + @Test + void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { + // var keypair = CryptoUtils.generateRSAKeypair(); + SecureRandom secureRandom = new SecureRandom(); + byte[] key = new byte[32]; + secureRandom.nextBytes(key); + + String assertion1Id = "assertion1"; + var assertionConfig1 = new AssertionConfig(); + assertionConfig1.id = assertion1Id; + assertionConfig1.type = AssertionConfig.Type.BaseAssertion; + assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig1.statement = new AssertionConfig.Statement(); + assertionConfig1.statement.format = "base64binary"; + assertionConfig1.statement.schema = "text"; + assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; + assertionConfig1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, + key); + + var rsaKasInfo = new Config.KASInfo(); + rsaKasInfo.URL = "https://example.com/kas" + 0; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(rsaKasInfo), + Config.withAssertionConfig(assertionConfig1)); + + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + byte[] notkey = new byte[32]; + secureRandom.nextBytes(notkey); + var assertionVerificationKeys = new Config.AssertionVerificationKeys(); + assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey( + AssertionConfig.AssertionKeyAlg.HS256, + notkey); + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(assertionVerificationKeys)); + + try { + tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, + platformUrl); + throw new RuntimeException("assertion verify key error thrown"); + + } catch (SDKException e) { + assertThat(e).hasMessageContaining("verify"); + } } - kasRegistryService = mock(KeyAccessServerRegistryServiceClient.class); - List kasRegEntries = new ArrayList<>(); - for (Config.KASInfo kasInfo : getRSAKASInfos()) { - kasRegEntries.add(KeyAccessServer.newBuilder() - .setUri(kasInfo.URL).build()); + @Test + public void testCreatingTDFWithMultipleSegments() throws Exception { + var random = new Random(); + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); + + // data should be large enough to have multiple complete and a partial segment + var data = new byte[(int) (Config.MIN_SEGMENT_SIZE * 2.8)]; + random.nextBytes(data); + var plainTextInputStream = new ByteArrayInputStream(data); + var tdfOutputStream = new ByteArrayOutputStream(); + var tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + var unwrappedData = new ByteArrayOutputStream(); + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toByteArray()) + .withFailMessage("extracted data does not match") + .containsExactly(data); + } - for (Config.KASInfo kasInfo : getECKASInfos()) { - kasRegEntries.add(KeyAccessServer.newBuilder() - .setUri(kasInfo.URL).build()); + + @Test + public void testCreatingTooLargeTDF() { + var random = new Random(); + var maxSize = random.nextInt(1024); + var numReturned = new AtomicInteger(0); + + // return 1 more byte than the maximum size + var is = new InputStream() { + @Override + public int read() { + if (numReturned.get() > maxSize) { + return -1; + } + numReturned.incrementAndGet(); + return 1; + } + + @Override + public int read(byte[] b, int off, int len) { + var numToReturn = Math.min(len, maxSize - numReturned.get() + 1); + numReturned.addAndGet(numToReturn); + return numToReturn; + } + }; + + var os = new OutputStream() { + @Override + public void write(int b) { + } + + @Override + public void write(byte[] b, int off, int len) { + } + }; + + var tdf = new TDF(maxSize, new FakeServicesBuilder().setKas(kas).build()); + var tdfConfig = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); + assertThrows(SDK.DataSizeNotSupported.class, + () -> tdf.createTDF(is, os, tdfConfig), + "didn't throw an exception when we created TDF that was too large"); + assertThat(numReturned.get()) + .withFailMessage("test returned the wrong number of bytes") + .isEqualTo(maxSize + 1); } - ListKeyAccessServersResponse mockResponse = ListKeyAccessServersResponse.newBuilder() - .addAllKeyAccessServers(kasRegEntries) - .build(); - - // Stub the listKeyAccessServers method - when(kasRegistryService.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), any())) - .thenReturn(new UnaryBlockingCall<>() { - @Override - public ResponseMessage execute() { - return new ResponseMessage.Success<>(mockResponse, Collections.emptyMap(), - Collections.emptyMap()); - } - - @Override - public void cancel() { - // this never happens in tests - } - }); - } - @Test - void testSimpleTDFEncryptAndDecrypt() throws Exception { + @Test + public void testCreateTDFWithMimeType() throws Exception { + final String mimeType = "application/pdf"; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withMimeType(mimeType)); - class TDFConfigPair { - public final Config.TDFConfig tdfConfig; - public final Config.TDFReaderConfig tdfReaderConfig; + String plainText = "this is extremely sensitive stuff!!!"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReaderConfig) { - this.tdfConfig = tdfConfig; - this.tdfReaderConfig = tdfReaderConfig; - } + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); + assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); } - SecureRandom secureRandom = new SecureRandom(); - byte[] key = new byte[32]; - secureRandom.nextBytes(key); - - var assertion1 = new AssertionConfig(); - assertion1.id = "assertion1"; - assertion1.type = AssertionConfig.Type.BaseAssertion; - assertion1.scope = AssertionConfig.Scope.TrustedDataObj; - assertion1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertion1.statement = new AssertionConfig.Statement(); - assertion1.statement.format = "base64binary"; - assertion1.statement.schema = "text"; - assertion1.statement.value = "ICAgIDxlZGoOkVkaD4="; - assertion1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); - - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); - assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, - key); - - List tdfConfigPairs = List.of( - new TDFConfigPair( - Config.newTDFConfig(Config.withAutoconfigure(false), + @Test + void legacyTDFRoundTrips() throws IOException { + final String mimeType = "application/pdf"; + var assertionConfig1 = new AssertionConfig(); + assertionConfig1.id = "assertion1"; + assertionConfig1.type = AssertionConfig.Type.BaseAssertion; + assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; + assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; + assertionConfig1.statement = new AssertionConfig.Statement(); + assertionConfig1.statement.format = "base64binary"; + assertionConfig1.statement.schema = "text"; + assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; + + Config.TDFConfig config = Config.newTDFConfig( + Config.withAutoconfigure(false), Config.withKasInformation(getRSAKASInfos()), - Config.withMetaData("here is some metadata"), - Config.withDataAttributes("https://example.org/attr/a/value/b", - "https://example.org/attr/c/value/d"), - Config.withAssertionConfig(assertion1)), - Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys))), - new TDFConfigPair( - Config.newTDFConfig(Config.withAutoconfigure(false), Config.withKasInformation(getECKASInfos()), - Config.withMetaData("here is some metadata"), - Config.WithWrappingKeyAlg(KeyType.EC256Key), - Config.withDataAttributes("https://example.org/attr/a/value/b", - "https://example.org/attr/c/value/d"), - Config.withAssertionConfig(assertion1)), - Config.newTDFReaderConfig(Config.withAssertionVerificationKeys(assertionVerificationKeys), - Config.WithSessionKeyType(KeyType.EC256Key)))); - - for (TDFConfigPair configPair : tdfConfigPairs) { - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas) - .setKeyAccessServerRegistryService(kasRegistryService).build()); - var manifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, configPair.tdfConfig).getManifest(); - - assertThat(manifest.assertions).asList().hasSize(1); - var assertion = manifest.assertions.get(0); - assertThat(assertion.appliesToState).isEqualTo("unencrypted"); - assertThat(assertion.type).isEqualTo("base"); - assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); - assertThat(assertion.statement.schema).isEqualTo("text"); - assertThat(assertion.statement.format).isEqualTo("base64binary"); - - assertThat(manifest.payload.isEncrypted).isTrue(); - var size = manifest.encryptionInformation.integrityInformation.segments.stream().map(s -> s.segmentSize) - .reduce(0L, Long::sum); - assertThat(size).isEqualTo(plainText.getBytes().length); - - var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), - configPair.tdfReaderConfig, platformUrl); - assertThat(reader.getManifest().payload.mimeType).isEqualTo("application/octet-stream"); - - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - assertThat(reader.getMetadata()).isEqualTo("here is some metadata"); - - var policyObject = reader.readPolicyObject(); - assertThat(policyObject).isNotNull(); - assertThat(policyObject.body.dataAttributes.stream().map(a -> a.attribute).collect(Collectors.toList())) - .asList() - .containsExactlyInAnyOrder("https://example.org/attr/a/value/b", - "https://example.org/attr/c/value/d"); - } - } - - @Test - void testSimpleTDFWithAssertionWithRS256() throws Exception { - String assertion1Id = "assertion1"; - var keypair = CryptoUtils.generateRSAKeypair(); - var assertionConfig = new AssertionConfig(); - assertionConfig.id = assertion1Id; - assertionConfig.type = AssertionConfig.Type.BaseAssertion; - assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig.statement = new AssertionConfig.Statement(); - assertionConfig.statement.format = "base64binary"; - assertionConfig.statement.schema = "text"; - assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; - assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, - keypair.getPrivate()); - - var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas" + 0; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(rsaKasInfo), - Config.withAssertionConfig(assertionConfig)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF( - new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); - assertionVerificationKeys.keys.put(assertion1Id, - new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, keypair.getPublic())); - - var unwrappedData = new ByteArrayOutputStream(); - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withAssertionVerificationKeys(assertionVerificationKeys)); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, - platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - } - - @Test - void testWithAssertionVerificationDisabled() throws Exception { - String assertion1Id = "assertion1"; - var keypair = CryptoUtils.generateRSAKeypair(); - var assertionConfig = new AssertionConfig(); - assertionConfig.id = assertion1Id; - assertionConfig.type = AssertionConfig.Type.BaseAssertion; - assertionConfig.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig.statement = new AssertionConfig.Statement(); - assertionConfig.statement.format = "base64binary"; - assertionConfig.statement.schema = "text"; - assertionConfig.statement.value = "ICAgIDxlZGoOkVkaD4="; - assertionConfig.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, - keypair.getPrivate()); - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withAssertionConfig(assertionConfig)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF( - new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); - assertionVerificationKeys.keys.put(assertion1Id, - new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.RS256, keypair.getPublic())); - - var unwrappedData = new ByteArrayOutputStream(); - var dataToUnwrap = new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()); - var emptyConfig = Config.newTDFReaderConfig(); - var thrown = assertThrows(SDKException.class, () -> { - tdf.loadTDF(dataToUnwrap, emptyConfig, platformUrl); - }); - assertThat(thrown.getCause()).isInstanceOf(JOSEException.class); - - // 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()), readerConfig, - platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - } - - @Test - void testSimpleTDFWithAssertionWithHS256() throws Exception { - String assertion1Id = "assertion1"; - var assertionConfig1 = new AssertionConfig(); - assertionConfig1.id = assertion1Id; - assertionConfig1.type = AssertionConfig.Type.BaseAssertion; - assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig1.statement = new AssertionConfig.Statement(); - assertionConfig1.statement.format = "base64binary"; - assertionConfig1.statement.schema = "text"; - assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; - - String assertion2Id = "assertion2"; - var assertionConfig2 = new AssertionConfig(); - assertionConfig2.id = assertion2Id; - assertionConfig2.type = AssertionConfig.Type.HandlingAssertion; - assertionConfig2.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig2.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig2.statement = new AssertionConfig.Statement(); - assertionConfig2.statement.format = "json"; - assertionConfig2.statement.schema = "urn:nato:stanag:5636:A:1:elements:json"; - assertionConfig2.statement.value = "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"; - - var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas" + 0; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(rsaKasInfo), - Config.withAssertionConfig(assertionConfig1, assertionConfig2)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF( - new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), - Config.newTDFReaderConfig(), platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) - .withFailMessage("extracted data does not match") - .isEqualTo(plainText); - - var manifest = reader.getManifest(); - var assertions = manifest.assertions; - assertThat(assertions.size()).isEqualTo(2); - for (var assertion : assertions) { - if (assertion.id.equals(assertion1Id)) { + Config.withTargetMode("4.2.1"), + Config.withAssertionConfig(assertionConfig1), + Config.withMimeType(mimeType)); + + byte[] data = new byte[129]; + new Random().nextBytes(data); + InputStream plainTextInputStream = new ByteArrayInputStream(data); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var dataOutputStream = new ByteArrayOutputStream(); + + var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); + var integrityInformation = reader.getManifest().encryptionInformation.integrityInformation; + assertThat(reader.getManifest().tdfVersion).isNull(); + var decodedSignature = Base64.getDecoder().decode(integrityInformation.rootSignature.signature); + for (var b : decodedSignature) { + assertThat(isHexChar(b)) + .withFailMessage("non-hex byte in signature: " + b) + .isTrue(); + } + for (var s : integrityInformation.segments) { + var decodedSegmentSignature = Base64.getDecoder().decode(s.hash); + for (var b : decodedSegmentSignature) { + assertThat(isHexChar(b)) + .withFailMessage("non-hex byte in segment signature: " + b) + .isTrue(); + } + } + reader.readPayload(dataOutputStream); + assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); + assertArrayEquals(data, dataOutputStream.toByteArray(), "extracted data does not match"); + var manifest = reader.getManifest(); + var assertions = manifest.assertions; + assertThat(assertions.size()).isEqualTo(1); + var assertion = assertions.get(0); + assertThat(assertion.id).isEqualTo("assertion1"); assertThat(assertion.statement.format).isEqualTo("base64binary"); assertThat(assertion.statement.schema).isEqualTo("text"); assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); assertThat(assertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); - } else if (assertion.id.equals(assertion2Id)) { - assertThat(assertion.statement.format).isEqualTo("json"); - assertThat(assertion.statement.schema).isEqualTo("urn:nato:stanag:5636:A:1:elements:json"); - assertThat(assertion.statement.value).isEqualTo( - "{\"uuid\":\"f74efb60-4a9a-11ef-a6f1-8ee1a61c148a\",\"body\":{\"dataAttributes\":null,\"dissem\":null}}"); - assertThat(assertion.type).isEqualTo(AssertionConfig.Type.HandlingAssertion.toString()); - } else { - throw new RuntimeException("unexpected assertion id: " + assertion.id); - } - } - } - - @Test - void testSimpleTDFWithAssertionWithHS256Failure() throws Exception { - // var keypair = CryptoUtils.generateRSAKeypair(); - SecureRandom secureRandom = new SecureRandom(); - byte[] key = new byte[32]; - secureRandom.nextBytes(key); - - String assertion1Id = "assertion1"; - var assertionConfig1 = new AssertionConfig(); - assertionConfig1.id = assertion1Id; - assertionConfig1.type = AssertionConfig.Type.BaseAssertion; - assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig1.statement = new AssertionConfig.Statement(); - assertionConfig1.statement.format = "base64binary"; - assertionConfig1.statement.schema = "text"; - assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; - assertionConfig1.signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, key); - - var rsaKasInfo = new Config.KASInfo(); - rsaKasInfo.URL = "https://example.com/kas" + 0; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(rsaKasInfo), - Config.withAssertionConfig(assertionConfig1)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF( - new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - byte[] notkey = new byte[32]; - secureRandom.nextBytes(notkey); - var assertionVerificationKeys = new Config.AssertionVerificationKeys(); - assertionVerificationKeys.defaultKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, - notkey); - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withAssertionVerificationKeys(assertionVerificationKeys)); - - try { - tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); - throw new RuntimeException("assertion verify key error thrown"); - - } catch (SDKException e) { - assertThat(e).hasMessageContaining("verify"); } - } - - @Test - public void testCreatingTDFWithMultipleSegments() throws Exception { - var random = new Random(); - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); - - // data should be large enough to have multiple complete and a partial segment - var data = new byte[(int) (Config.MIN_SEGMENT_SIZE * 2.8)]; - random.nextBytes(data); - var plainTextInputStream = new ByteArrayInputStream(data); - var tdfOutputStream = new ByteArrayOutputStream(); - var tdf = new TDF( - new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - var unwrappedData = new ByteArrayOutputStream(); - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); - reader.readPayload(unwrappedData); - - assertThat(unwrappedData.toByteArray()) - .withFailMessage("extracted data does not match") - .containsExactly(data); - - } - - @Test - public void testCreatingTooLargeTDF() { - var random = new Random(); - var maxSize = random.nextInt(1024); - var numReturned = new AtomicInteger(0); - - // return 1 more byte than the maximum size - var is = new InputStream() { - @Override - public int read() { - if (numReturned.get() > maxSize) { - return -1; - } - numReturned.incrementAndGet(); - return 1; - } - - @Override - public int read(byte[] b, int off, int len) { - var numToReturn = Math.min(len, maxSize - numReturned.get() + 1); - numReturned.addAndGet(numToReturn); - return numToReturn; - } - }; - - var os = new OutputStream() { - @Override - public void write(int b) { - } - - @Override - public void write(byte[] b, int off, int len) { - } - }; - var tdf = new TDF(maxSize, new FakeServicesBuilder().setKas(kas).build()); - var tdfConfig = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withSegmentSize(Config.MIN_SEGMENT_SIZE)); - assertThrows(SDK.DataSizeNotSupported.class, - () -> tdf.createTDF(is, os, tdfConfig), - "didn't throw an exception when we created TDF that was too large"); - assertThat(numReturned.get()) - .withFailMessage("test returned the wrong number of bytes") - .isEqualTo(maxSize + 1); - } - - @Test - public void testCreateTDFWithMimeType() throws Exception { - final String mimeType = "application/pdf"; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withMimeType(mimeType)); - - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF( - new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); - assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); - } - - @Test - void legacyTDFRoundTrips() throws IOException { - final String mimeType = "application/pdf"; - var assertionConfig1 = new AssertionConfig(); - assertionConfig1.id = "assertion1"; - assertionConfig1.type = AssertionConfig.Type.BaseAssertion; - assertionConfig1.scope = AssertionConfig.Scope.TrustedDataObj; - assertionConfig1.appliesToState = AssertionConfig.AppliesToState.Unencrypted; - assertionConfig1.statement = new AssertionConfig.Statement(); - assertionConfig1.statement.format = "base64binary"; - assertionConfig1.statement.schema = "text"; - assertionConfig1.statement.value = "ICAgIDxlZGoOkVkaD4="; - - Config.TDFConfig config = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withTargetMode("4.2.1"), - Config.withAssertionConfig(assertionConfig1), - Config.withMimeType(mimeType)); - - byte[] data = new byte[129]; - new Random().nextBytes(data); - InputStream plainTextInputStream = new ByteArrayInputStream(data); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF( - new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - var dataOutputStream = new ByteArrayOutputStream(); - - var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), platformUrl); - var integrityInformation = reader.getManifest().encryptionInformation.integrityInformation; - assertThat(reader.getManifest().tdfVersion).isNull(); - var decodedSignature = Base64.getDecoder().decode(integrityInformation.rootSignature.signature); - for (var b : decodedSignature) { - assertThat(isHexChar(b)) - .withFailMessage("non-hex byte in signature: " + b) - .isTrue(); - } - for (var s : integrityInformation.segments) { - var decodedSegmentSignature = Base64.getDecoder().decode(s.hash); - for (var b : decodedSegmentSignature) { - assertThat(isHexChar(b)) - .withFailMessage("non-hex byte in segment signature: " + b) - .isTrue(); - } + @Test + void testSystemMetadataAssertion() throws Exception { + Config.TDFConfig tdfConfig = Config.newTDFConfig( + Config.withAutoconfigure(false), + Config.withKasInformation(getRSAKASInfos()), + Config.withSystemMetadataAssertion() // Enable system metadata assertion + ); + + String plainText = "Test data for system metadata assertion."; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF( + new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryService).build()); + var createdManifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, tdfConfig).getManifest(); + + // Verify the created manifest directly + assertThat(createdManifest.assertions).isNotNull(); + assertThat(createdManifest.assertions.size()).isEqualTo(1); + Manifest.Assertion sysAssertion = createdManifest.assertions.get(0); + assertThat(sysAssertion.id).isEqualTo("default-assertion"); + assertThat(sysAssertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); + assertThat(sysAssertion.scope).isEqualTo(AssertionConfig.Scope.Payload.toString()); + assertThat(sysAssertion.appliesToState) + .isEqualTo(AssertionConfig.AppliesToState.Unencrypted.toString()); + assertThat(sysAssertion.statement.format).isEqualTo("json"); + assertThat(sysAssertion.statement.schema).isEqualTo("metadata"); + + // Deserialize and check the metadata JSON + Gson gson = new Gson(); + java.lang.reflect.Type mapType = new TypeToken>() { + }.getType(); + Map metadataMap = gson.fromJson(sysAssertion.statement.value, mapType); + + assertThat(metadataMap).containsKey("tdf_spec_version"); + assertThat(metadataMap.get("tdf_spec_version")).isEqualTo(TDF.TDF_SPEC_VERSION); + assertThat(metadataMap).containsKey("creation_date"); + assertThat(metadataMap.get("creation_date")).isNotBlank(); + assertThat(metadataMap).containsKey("operating_system"); + assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); + assertThat(metadataMap).containsKey("sdk_version"); + assertThat(metadataMap.get("sdk_version")).isEqualTo("Java-" + SdkInfo.VERSION); + assertThat(metadataMap).containsKey("java_version"); + assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); + assertThat(metadataMap).containsKey("architecture"); + assertThat(metadataMap.get("architecture")).isEqualTo(System.getProperty("os.arch")); } - reader.readPayload(dataOutputStream); - assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType); - assertArrayEquals(data, dataOutputStream.toByteArray(), "extracted data does not match"); - var manifest = reader.getManifest(); - var assertions = manifest.assertions; - assertThat(assertions.size()).isEqualTo(1); - var assertion = assertions.get(0); - assertThat(assertion.id).isEqualTo("assertion1"); - assertThat(assertion.statement.format).isEqualTo("base64binary"); - assertThat(assertion.statement.schema).isEqualTo("text"); - assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); - assertThat(assertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); - } - - @Test - void testSystemMetadataAssertion() throws Exception { - Config.TDFConfig tdfConfig = Config.newTDFConfig( - Config.withAutoconfigure(false), - Config.withKasInformation(getRSAKASInfos()), - Config.withSystemMetadataAssertion() // Enable system metadata assertion - ); - - String plainText = "Test data for system metadata assertion."; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes(StandardCharsets.UTF_8)); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); - - TDF tdf = new TDF( - new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryService).build()); - var createdManifest = tdf.createTDF(plainTextInputStream, tdfOutputStream, tdfConfig).getManifest(); - - // Verify the created manifest directly - assertThat(createdManifest.assertions).isNotNull(); - assertThat(createdManifest.assertions.size()).isEqualTo(1); - Manifest.Assertion sysAssertion = createdManifest.assertions.get(0); - assertThat(sysAssertion.id).isEqualTo("default-assertion"); - assertThat(sysAssertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); - assertThat(sysAssertion.scope).isEqualTo(AssertionConfig.Scope.Payload.toString()); - assertThat(sysAssertion.appliesToState).isEqualTo(AssertionConfig.AppliesToState.Unencrypted.toString()); - assertThat(sysAssertion.statement.format).isEqualTo("json"); - assertThat(sysAssertion.statement.schema).isEqualTo("metadata"); - - // Deserialize and check the metadata JSON - Gson gson = new Gson(); - java.lang.reflect.Type mapType = new TypeToken>() { - }.getType(); - Map metadataMap = gson.fromJson(sysAssertion.statement.value, mapType); - - assertThat(metadataMap).containsKey("tdf_spec_version"); - assertThat(metadataMap.get("tdf_spec_version")).isEqualTo(TDF.TDF_SPEC_VERSION); - assertThat(metadataMap).containsKey("creation_date"); - assertThat(metadataMap.get("creation_date")).isNotBlank(); - assertThat(metadataMap).containsKey("operating_system"); - assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); - assertThat(metadataMap).containsKey("sdk_version"); - assertThat(metadataMap.get("sdk_version")).isEqualTo("Java-" + SdkInfo.VERSION); - assertThat(metadataMap).containsKey("java_version"); - assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); - assertThat(metadataMap).containsKey("architecture"); - assertThat(metadataMap.get("architecture")).isEqualTo(System.getProperty("os.arch")); - } - - @Test - void testKasAllowlist() throws Exception { - - KeyAccessServerRegistryServiceClient kasRegistryServiceNoUrl = mock(KeyAccessServerRegistryServiceClient.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.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), any())) - .thenReturn(new UnaryBlockingCall<>() { - @Override - public ResponseMessage execute() { - return new ResponseMessage.Success<>(mockResponse, Collections.emptyMap(), - Collections.emptyMap()); - } - - @Override - public void cancel() { - // we never do this during tests - } - }); - 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)); + @Test + void testKasAllowlist() throws Exception { + + KeyAccessServerRegistryServiceClient kasRegistryServiceNoUrl = mock( + KeyAccessServerRegistryServiceClient.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.listKeyAccessServersBlocking(any(ListKeyAccessServersRequest.class), + any())) + .thenReturn(new UnaryBlockingCall<>() { + @Override + public ResponseMessage execute() { + return new ResponseMessage.Success<>(mockResponse, + Collections.emptyMap(), + Collections.emptyMap()); + } + + @Override + public void cancel() { + // we never do this during tests + } + }); + + 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(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryServiceNoUrl).build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + var unwrappedData = new ByteArrayOutputStream(); + + // should throw error because the kas url is not in the allowlist + try { + tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + Config.newTDFReaderConfig(), + platformUrl); + throw new RuntimeException("expected allowlist error to be thrown"); + } catch (Exception e) { + assertThat(e).hasMessageContaining("KasAllowlist"); + } - String plainText = "this is extremely sensitive stuff!!!"; - InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); - ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + // with custom allowlist should succeed + Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.WithKasAllowlist("https://example.com")); + tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); + + // with ignore allowlist should succeed + readerConfig = Config.newTDFReaderConfig( + Config.WithIgnoreKasAllowlist(true)); + Reader reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + readerConfig, + 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(new FakeServicesBuilder().setKas(kas) + .setKeyAccessServerRegistryService(kasRegistryServiceNoUrl) + .build()); + tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + + unwrappedData = new ByteArrayOutputStream(); + reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), + Config.newTDFReaderConfig(), platformUrl); + reader.readPayload(unwrappedData); + + assertThat(unwrappedData.toString(StandardCharsets.UTF_8)) + .withFailMessage("extracted data does not match") + .isEqualTo(plainText); + } - TDF tdf = new TDF(new FakeServicesBuilder().setKas(kas) - .setKeyAccessServerRegistryService(kasRegistryServiceNoUrl).build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); + @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 = "https://example.com/kas" + Integer.toString(i); + kasInfo.PublicKey = null; + kasInfos.add(kasInfo); + } + } + return kasInfos.toArray(Config.KASInfo[]::new); + } - var unwrappedData = new ByteArrayOutputStream(); + @Nonnull + private static Config.KASInfo[] getRSAKASInfos() { + return getKASInfos(i -> i % 2 == 0); + } - // should throw error because the kas url is not in the allowlist - try { - tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), Config.newTDFReaderConfig(), - platformUrl); - throw new RuntimeException("expected allowlist error to be thrown"); - } catch (Exception e) { - assertThat(e).hasMessageContaining("KasAllowlist"); + @Nonnull + private static Config.KASInfo[] getECKASInfos() { + return getKASInfos(i -> i % 2 != 0); } - // with custom allowlist should succeed - Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.WithKasAllowlist("https://example.com")); - tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, platformUrl); - - // with ignore allowlist should succeed - readerConfig = Config.newTDFReaderConfig( - Config.WithIgnoreKasAllowlist(true)); - Reader reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), readerConfig, - 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(new FakeServicesBuilder().setKas(kas).setKeyAccessServerRegistryService(kasRegistryServiceNoUrl) - .build()); - tdf.createTDF(plainTextInputStream, tdfOutputStream, config); - - unwrappedData = new ByteArrayOutputStream(); - reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), - Config.newTDFReaderConfig(), 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 = "https://example.com/kas" + Integer.toString(i); - kasInfo.PublicKey = null; - kasInfos.add(kasInfo); - } + private static boolean isHexChar(byte b) { + return (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9'); } - return kasInfos.toArray(Config.KASInfo[]::new); - } - - @Nonnull - private static Config.KASInfo[] getRSAKASInfos() { - return getKASInfos(i -> i % 2 == 0); - } - - @Nonnull - private static Config.KASInfo[] getECKASInfos() { - return getKASInfos(i -> i % 2 != 0); - } - - private static boolean isHexChar(byte b) { - return (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9'); - } } From 30b35160d6756fd1bfc4ca3147a580ef74e6e328 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 20:03:13 -0400 Subject: [PATCH 11/23] simplify sdk version info --- .../release-please-config.main.json | 4 ++ ...elease-please-config.release_branches.json | 4 ++ .../opentdf/platform/sdk/AssertionConfig.java | 5 +-- .../java/io/opentdf/platform/sdk/SdkInfo.java | 43 ------------------- .../java/io/opentdf/platform/sdk/TDF.java | 4 +- .../java/io/opentdf/platform/sdk/Version.java | 4 ++ .../opentdf/platform/sdk/version.properties | 1 - .../java/io/opentdf/platform/sdk/TDFTest.java | 2 +- 8 files changed, 16 insertions(+), 51 deletions(-) delete mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/SdkInfo.java delete mode 100644 sdk/src/main/resources/io/opentdf/platform/sdk/version.properties diff --git a/.github/release-please/release-please-config.main.json b/.github/release-please/release-please-config.main.json index 55a6077b..152a2b2b 100644 --- a/.github/release-please/release-please-config.main.json +++ b/.github/release-please/release-please-config.main.json @@ -12,6 +12,10 @@ { "type": "generic", "path": "cmdline/src/main/java/io/opentdf/platform/Command.java" + }, + { + "type": "generic", + "path": "sdk/src/main/java/io/opentdf/platform/sdk/Version.java" } ] } diff --git a/.github/release-please/release-please-config.release_branches.json b/.github/release-please/release-please-config.release_branches.json index 6177d966..f36a077a 100644 --- a/.github/release-please/release-please-config.release_branches.json +++ b/.github/release-please/release-please-config.release_branches.json @@ -12,6 +12,10 @@ { "type": "generic", "path": "cmdline/src/main/java/io/opentdf/platform/Command.java" + }, + { + "type": "generic", + "path": "sdk/src/main/java/io/opentdf/platform/sdk/Version.java" } ] } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java index 088f54c4..42c8ce20 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -173,11 +173,10 @@ static private class SystemMetadata { * @return An {@link AssertionConfig} populated with system metadata. * @throws SDKException if there's an error marshalling the metadata to JSON. */ - public static AssertionConfig getSystemMetadataAssertionConfig(String tdfSpecVersionFromSDK, - String sdkInternalVersion) { + public static AssertionConfig getSystemMetadataAssertionConfig(String tdfSpecVersionFromSDK) { String creationDate = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); String operatingSystem = System.getProperty("os.name"); - String sdkVersion = "Java-" + sdkInternalVersion; + String sdkVersion = "Java-" + Version.SDK; String javaVersion = System.getProperty("java.version"); String architecture = System.getProperty("os.arch"); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/SdkInfo.java b/sdk/src/main/java/io/opentdf/platform/sdk/SdkInfo.java deleted file mode 100644 index 4fe62707..00000000 --- a/sdk/src/main/java/io/opentdf/platform/sdk/SdkInfo.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.opentdf.platform.sdk; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -/** - * Provides information about the SDK, such as its version. - * The version is read from a properties file populated during the Maven build - * process. - */ -public final class SdkInfo { - private static final Logger logger = LoggerFactory.getLogger(SdkInfo.class); - private static final String VERSION_PROPERTIES_FILE = "version.properties"; // Relative to this class's package - private static final String SDK_VERSION_PROPERTY = "sdk.version"; - - public static final String VERSION; - - static { - String versionString = "unknown"; // Default if properties can't be read - Properties props = new Properties(); - try (InputStream input = SdkInfo.class.getResourceAsStream(VERSION_PROPERTIES_FILE)) { - if (input == null) { - logger.error("Unable to find " + VERSION_PROPERTIES_FILE - + ". SDK version will be 'unknown'. Ensure it's in src/main/resources/io/opentdf/platform/sdk/"); - } else { - props.load(input); - versionString = props.getProperty(SDK_VERSION_PROPERTY, "unknown"); - } - } catch (IOException ex) { - logger.error("Error loading " + VERSION_PROPERTIES_FILE + ". SDK version will be 'unknown'.", ex); - } - VERSION = versionString; - logger.info("OpenTDF SDK Version: {}", VERSION); - } - - private SdkInfo() { - // Private constructor to prevent instantiation of this utility class - } -} \ No newline at end of file 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 8a733de9..754c33ac 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -444,9 +444,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo // Add System Metadata Assertion if configured if (tdfConfig.systemMetadataAssertion) { - AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig( - TDF_SPEC_VERSION, // This is the TDF specification version - SdkInfo.VERSION); // This is the SDK's own version from pom.xml + AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig(TDF_SPEC_VERSION); tdfConfig.assertionConfigList.add(systemAssertion); } diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Version.java b/sdk/src/main/java/io/opentdf/platform/sdk/Version.java index b09177d1..e57b117e 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Version.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Version.java @@ -10,6 +10,10 @@ import java.util.regex.Pattern; class Version implements Comparable { + + // Version of the SDK, managed by release-please. + public static final String SDK = "0.8.2-SNAPSHOT"; // x-release-please-version + private final int major; private final int minor; private final int patch; diff --git a/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties b/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties deleted file mode 100644 index 325bf768..00000000 --- a/sdk/src/main/resources/io/opentdf/platform/sdk/version.properties +++ /dev/null @@ -1 +0,0 @@ -sdk.version=${project.version} \ No newline at end of file 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 5f47f452..a450a465 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -706,7 +706,7 @@ void testSystemMetadataAssertion() throws Exception { assertThat(metadataMap).containsKey("operating_system"); assertThat(metadataMap.get("operating_system")).isEqualTo(System.getProperty("os.name")); assertThat(metadataMap).containsKey("sdk_version"); - assertThat(metadataMap.get("sdk_version")).isEqualTo("Java-" + SdkInfo.VERSION); + assertThat(metadataMap.get("sdk_version")).isEqualTo("Java-" + Version.SDK); assertThat(metadataMap).containsKey("java_version"); assertThat(metadataMap.get("java_version")).isEqualTo(System.getProperty("java.version")); assertThat(metadataMap).containsKey("architecture"); From adebc5ed342a8186a37441893de44a52db2d3e94 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 20:10:55 -0400 Subject: [PATCH 12/23] change assertion id --- sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java index 42c8ce20..2997391f 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -192,7 +192,7 @@ public static AssertionConfig getSystemMetadataAssertionConfig(String tdfSpecVer } AssertionConfig config = new AssertionConfig(); - config.id = "default-assertion"; + config.id = "system-metadata"; config.type = Type.BaseAssertion; config.scope = Scope.Payload; // Maps from Go's PayloadScope config.appliesToState = AppliesToState.Unencrypted; From 6990a6c4765d809ee527e23219aac928ae62a5ba Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 20:17:06 -0400 Subject: [PATCH 13/23] change schema --- sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java index 2997391f..fe181158 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -199,7 +199,7 @@ public static AssertionConfig getSystemMetadataAssertionConfig(String tdfSpecVer Statement statement = new Statement(); statement.format = "json"; - statement.schema = "metadata"; + statement.schema = "system-metadata-v1"; statement.value = metadataJSON; config.statement = statement; From 82e74228501e6ada05ac27421c5b8d02e819731e Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 20:37:15 -0400 Subject: [PATCH 14/23] fix test --- sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java | 4 ++-- 1 file changed, 2 insertions(+), 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 a450a465..ddfb5569 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -685,13 +685,13 @@ void testSystemMetadataAssertion() throws Exception { assertThat(createdManifest.assertions).isNotNull(); assertThat(createdManifest.assertions.size()).isEqualTo(1); Manifest.Assertion sysAssertion = createdManifest.assertions.get(0); - assertThat(sysAssertion.id).isEqualTo("default-assertion"); + assertThat(sysAssertion.id).isEqualTo("system-metadata"); assertThat(sysAssertion.type).isEqualTo(AssertionConfig.Type.BaseAssertion.toString()); assertThat(sysAssertion.scope).isEqualTo(AssertionConfig.Scope.Payload.toString()); assertThat(sysAssertion.appliesToState) .isEqualTo(AssertionConfig.AppliesToState.Unencrypted.toString()); assertThat(sysAssertion.statement.format).isEqualTo("json"); - assertThat(sysAssertion.statement.schema).isEqualTo("metadata"); + assertThat(sysAssertion.statement.schema).isEqualTo("system-metadata-v1"); // Deserialize and check the metadata JSON Gson gson = new Gson(); From 8fee07c87091258acec0b7da334c8ee7efc4ce88 Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 21:10:54 -0400 Subject: [PATCH 15/23] fix base assertion string --- sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java index fe181158..e36c304c 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java @@ -18,7 +18,7 @@ public class AssertionConfig { public enum Type { HandlingAssertion("handling"), - BaseAssertion("base"); + BaseAssertion("other"); private final String type; From e72fbe3a38127e29643088ee792188f6cde9eaad Mon Sep 17 00:00:00 2001 From: strantalis Date: Mon, 16 Jun 2025 21:37:46 -0400 Subject: [PATCH 16/23] fix assertion check --- 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 ddfb5569..19bdac96 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -236,7 +236,7 @@ public TDFConfigPair(Config.TDFConfig tdfConfig, Config.TDFReaderConfig tdfReade assertThat(manifest.assertions).asList().hasSize(1); var assertion = manifest.assertions.get(0); assertThat(assertion.appliesToState).isEqualTo("unencrypted"); - assertThat(assertion.type).isEqualTo("base"); + assertThat(assertion.type).isEqualTo("other"); assertThat(assertion.statement.value).isEqualTo("ICAgIDxlZGoOkVkaD4="); assertThat(assertion.statement.schema).isEqualTo("text"); assertThat(assertion.statement.format).isEqualTo("base64binary"); From 5492e2ffaef41f7ce18ab250b99af972337d9e11 Mon Sep 17 00:00:00 2001 From: strantalis Date: Tue, 17 Jun 2025 13:14:08 -0400 Subject: [PATCH 17/23] remove automatically enabling system metadata assertion --- cmdline/src/main/java/io/opentdf/platform/Command.java | 1 - 1 file changed, 1 deletion(-) diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index 960057df..8a2807b3 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -170,7 +170,6 @@ void encrypt( List> configs = new ArrayList<>(); configs.add(Config.withKasInformation(kasInfos)); - configs.add(Config.withSystemMetadataAssertion()); metadata.map(Config::withMetaData).ifPresent(configs::add); autoconfigure.map(Config::withAutoconfigure).ifPresent(configs::add); encapKeyType.map(Config::WithWrappingKeyAlg).ifPresent(configs::add); From 174ef3db1d526cea7d308757a6f77028b63890ef Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Tue, 22 Jul 2025 22:57:31 +0200 Subject: [PATCH 18/23] see what happens with it enabled --- cmdline/src/main/java/io/opentdf/platform/Command.java | 1 + 1 file changed, 1 insertion(+) diff --git a/cmdline/src/main/java/io/opentdf/platform/Command.java b/cmdline/src/main/java/io/opentdf/platform/Command.java index 369360fa..4879ac9e 100644 --- a/cmdline/src/main/java/io/opentdf/platform/Command.java +++ b/cmdline/src/main/java/io/opentdf/platform/Command.java @@ -171,6 +171,7 @@ void encrypt( List> configs = new ArrayList<>(); configs.add(Config.withKasInformation(kasInfos)); metadata.map(Config::withMetaData).ifPresent(configs::add); + configs.add(Config.withSystemMetadataAssertion()); autoconfigure.map(Config::withAutoconfigure).ifPresent(configs::add); encapKeyType.map(Config::WithWrappingKeyAlg).ifPresent(configs::add); mimeType.map(Config::withMimeType).ifPresent(configs::add); From 8aad1a654dd928b48a69250561b0c64a8dbd07bc Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 23 Jul 2025 16:39:10 +0200 Subject: [PATCH 19/23] point it at the branch --- .github/workflows/checks.yaml | 2 +- sdk/src/main/java/io/opentdf/platform/sdk/TDF.java | 1 + sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index eb0e56a4..8d451e9b 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -311,7 +311,7 @@ jobs: contents: read packages: read needs: platform-integration - uses: opentdf/tests/.github/workflows/xtest.yml@main + uses: opentdf/tests/.github/workflows/xtest.yml@mkleene-patch-1 with: focus-sdk: java java-ref: ${{ github.ref }} latest 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 0eadd758..38bb9956 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -633,6 +633,7 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig) String manifestJson = tdfReader.manifest(); // use Manifest.readManifest in order to validate the Manifest input Manifest manifest = Manifest.readManifest(manifestJson); + byte[] payloadKey = new byte[GCM_KEY_SIZE]; String unencryptedMetadata = null; 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 45886f05..c28bd2bd 100644 --- a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -291,6 +291,7 @@ void testSimpleTDFWithAssertionWithRS256() throws Exception { Config.TDFConfig config = Config.newTDFConfig( Config.withAutoconfigure(false), Config.withKasInformation(rsaKasInfo), + Config.withSystemMetadataAssertion(), Config.withAssertionConfig(assertionConfig)); String plainText = "this is extremely sensitive stuff!!!"; From 50f60084c777961bb682c1659e950b079c196bac Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 23 Jul 2025 21:04:56 +0200 Subject: [PATCH 20/23] see if this will get us the right version From 32cfab94274a91b7e76c7b4eb294ff9fe492a5c8 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Wed, 23 Jul 2025 21:39:58 +0200 Subject: [PATCH 21/23] Update checks.yaml --- .github/workflows/checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 8d451e9b..eb0e56a4 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -311,7 +311,7 @@ jobs: contents: read packages: read needs: platform-integration - uses: opentdf/tests/.github/workflows/xtest.yml@mkleene-patch-1 + uses: opentdf/tests/.github/workflows/xtest.yml@main with: focus-sdk: java java-ref: ${{ github.ref }} latest From 0498097b4df35282cc4ed328501828e4d881daaf Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 24 Jul 2025 11:59:13 +0200 Subject: [PATCH 22/23] Update Version.java --- sdk/src/main/java/io/opentdf/platform/sdk/Version.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Version.java b/sdk/src/main/java/io/opentdf/platform/sdk/Version.java index e57b117e..ef0ee8fd 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/Version.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Version.java @@ -12,7 +12,7 @@ class Version implements Comparable { // Version of the SDK, managed by release-please. - public static final String SDK = "0.8.2-SNAPSHOT"; // x-release-please-version + public static final String SDK = "0.9.1-SNAPSHOT"; // x-release-please-version private final int major; private final int minor; From 3faf37f351f15267aef59a2f5f1b768bcb9d3594 Mon Sep 17 00:00:00 2001 From: Morgan Kleene Date: Thu, 24 Jul 2025 17:55:56 +0200 Subject: [PATCH 23/23] recover from bad merge --- sdk/src/main/java/io/opentdf/platform/sdk/TDF.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 58f66a44..2ae08f5b 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.StringReader; import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.security.*; @@ -147,7 +146,7 @@ private PolicyObject createPolicyObject(List at private static final Base64.Encoder encoder = Base64.getEncoder(); - private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) { + private void prepareManifest(Config.TDFConfig tdfConfig, Map> splits) { manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_SPEC_VERSION : null; manifest.encryptionInformation.keyAccessType = kSplitKeyType; manifest.encryptionInformation.keyAccessObj = new ArrayList<>();