Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand Down Expand Up @@ -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;
}
}
6 changes: 6 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -176,6 +177,7 @@ public TDFConfig() {
this.wrappingKeyType = KeyType.RSA2048Key;
this.hexEncodeRootAndSegmentHashes = false;
this.renderVersionInfoInManifest = true;
this.systemMetadataAssertion = false;
}
}

Expand Down Expand Up @@ -297,6 +299,10 @@ public static Consumer<TDFConfig> withMimeType(String mimeType) {
return (TDFConfig config) -> config.mimeType = mimeType;
}

public static Consumer<TDFConfig> withSystemMetadataAssertion() {
return (TDFConfig config) -> config.systemMetadataAssertion = true;
}

public static class NanoTDFConfig {
public ECCMode eccMode;
public NanoTDFType.Cipher cipher;
Expand Down
11 changes: 11 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down Expand Up @@ -510,6 +520,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo
tdfObject.manifest.payload.isEncrypted = true;

List<Manifest.Assertion> signedAssertions = new ArrayList<>(tdfConfig.assertionConfigList.size());

for (var assertionConfig : tdfConfig.assertionConfigList) {
var assertion = new Manifest.Assertion();
assertion.id = assertionConfig.id;
Expand Down
48 changes: 48 additions & 0 deletions sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, String> 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 {

Expand Down
Loading