Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/AssertionBinder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.opentdf.platform.sdk;

import io.opentdf.platform.sdk.Manifest.Assertion;

public interface AssertionBinder {
/**
* Bind creates and signs an assertion, binding it to the given manifest.
* // The implementation is responsible for both configuring the assertion and binding it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This line contains a comment that seems to be a leftover from another language's source code (e.g., Go). It's not in standard Javadoc format and is redundant with the main description. It should be removed for clarity.

*
* @param manifest The manifest.
* @return The assertion.
* @throws SDK.AssertionException If an error occurs during binding.
*/
Assertion bind(Manifest manifest) throws SDK.AssertionException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.opentdf.platform.sdk;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class AssertionRegistry {
private final List<AssertionBinder> binders;
private final Map<String, AssertionValidator> validators;

public AssertionRegistry() {
this.binders = new CopyOnWriteArrayList<>();
this.validators = new ConcurrentHashMap<>();
}

public void registerBinder(AssertionBinder binder) {
binders.add(binder);
}

public void registerValidator(AssertionValidator validator) {
String schema = validator.getSchema();
validators.put(schema, validator);
}

public List<AssertionBinder> getBinders() {
return Collections.unmodifiableList(binders);
}

public void setBinders(List<AssertionBinder> binders) {
this.binders.clear();
this.binders.addAll(binders);
}

public Map<String, AssertionValidator> getValidators() {
return Collections.unmodifiableMap(validators);
}

public void setValidators(Map<String, AssertionValidator> validators) {
this.validators.clear();
this.validators.putAll(validators);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.opentdf.platform.sdk;

import com.nimbusds.jose.JOSEException;
import io.opentdf.platform.sdk.Manifest.Assertion;

import java.io.IOException;
import java.text.ParseException;
Comment on lines +3 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The following imports are unused in this interface and should be removed to keep the code clean: com.nimbusds.jose.JOSEException, java.io.IOException, java.text.ParseException.


public interface AssertionValidator {
/**
* // Schema returns the schema URI this validator handles.
* // The schema identifies the assertion format and version.
* // Examples: "urn:opentdf:system:metadata:v1", "urn:opentdf:key:assertion:v1"
*
* @return The schema URI.
*/
String getSchema();

void setVerificationMode(AssertionVerificationMode verificationMode);

/**
* // Verify checks the assertion's cryptographic binding.
* //
* // Example:
* // assertionHash, _ := a.GetHash()
* // manifest := r.Manifest()
* // expectedSig, _ := manifest.ComputeAssertionSignature(assertionHash)
*
* @param assertion The assertion to verify.
* @param manifest The manifest.
* @throws SDK.AssertionException If the verification fails.
*/
void verify(Assertion assertion, Manifest manifest) throws SDK.AssertionException;

/**
* // Validate checks the assertion's policy and trust requirements
Comment on lines +11 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Javadoc comments in this interface contain non-standard formatting (e.g., // prefixes) and examples from a different programming language (Go). Please update the comments for getSchema, verify, and validate to follow standard Javadoc conventions for clarity and consistency within the Java codebase.

*
* @param assertion The assertion to validate.
* @param reader The TDF reader.
* @throws SDK.AssertionException If the validation fails.
*/
void validate(Assertion assertion, TDFReader reader) throws SDK.AssertionException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.opentdf.platform.sdk;

public enum AssertionVerificationMode {
PERMISSIVE,
FAIL_FAST,
STRICT
}
41 changes: 40 additions & 1 deletion sdk/src/main/java/io/opentdf/platform/sdk/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ public static class TDFReaderConfig {
KeyType sessionKeyType;
Set<String> kasAllowlist;
boolean ignoreKasAllowlist;
private AssertionVerificationMode assertionVerificationMode = AssertionVerificationMode.FAIL_FAST;
private final AssertionRegistry assertionRegistry = new AssertionRegistry();

public AssertionVerificationMode getAssertionVerificationMode() {
return assertionVerificationMode;
}

public void setAssertionVerificationMode(AssertionVerificationMode assertionVerificationMode) {
this.assertionVerificationMode = assertionVerificationMode;
}

public AssertionRegistry getAssertionRegistry() {
return assertionRegistry;
}


}

@SafeVarargs
Expand All @@ -148,7 +164,18 @@ public static TDFReaderConfig newTDFReaderConfig(Consumer<TDFReaderConfig>... op

public static Consumer<TDFReaderConfig> withAssertionVerificationKeys(
AssertionVerificationKeys assertionVerificationKeys) {
return (TDFReaderConfig config) -> config.assertionVerificationKeys = assertionVerificationKeys;
return (TDFReaderConfig config) -> {
config.assertionVerificationKeys = assertionVerificationKeys;

// ONLY register wildcard validator if assertion verification is enabled
// This maintains backward compatibility with the disableAssertionVerification flag
if (!config.disableAssertionVerification) {
// Register a wildcard KeyAssertionValidator that handles any schema
// when verification keys are provided
KeyAssertionValidator keyAssertionValidator = new KeyAssertionValidator(assertionVerificationKeys);
config.getAssertionRegistry().registerValidator(keyAssertionValidator);
}
};
}

public static Consumer<TDFReaderConfig> withDisableAssertionVerification(boolean disable) {
Expand Down Expand Up @@ -195,6 +222,7 @@ public static class TDFConfig {
public boolean hexEncodeRootAndSegmentHashes;
public boolean renderVersionInfoInManifest;
public boolean systemMetadataAssertion;
private AssertionRegistry assertionRegistry;

public TDFConfig() {
this.autoconfigure = true;
Expand All @@ -212,6 +240,11 @@ public TDFConfig() {
this.hexEncodeRootAndSegmentHashes = false;
this.renderVersionInfoInManifest = true;
this.systemMetadataAssertion = false;
this.assertionRegistry = new AssertionRegistry();
}

public AssertionRegistry getAssertionRegistry() {
return assertionRegistry;
}
}

Expand Down Expand Up @@ -289,7 +322,13 @@ public static Consumer<TDFConfig> withSplitPlan(Autoconfigure.KeySplitStep... p)

public static Consumer<TDFConfig> withAssertionConfig(io.opentdf.platform.sdk.AssertionConfig... assertionList) {
return (TDFConfig config) -> {
// add to assertionConfigList for backward compatibility
Collections.addAll(config.assertionConfigList, assertionList);
// register a binder for each assertionConfig
for (AssertionConfig assertionConfig : assertionList) {
ConfigBasedAssertionBinder binder = new ConfigBasedAssertionBinder(assertionConfig);
config.getAssertionRegistry().registerBinder(binder);
}
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.opentdf.platform.sdk;

import com.nimbusds.jose.KeyLengthException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class ConfigBasedAssertionBinder implements AssertionBinder {
private final AssertionConfig assertionConfig;

public ConfigBasedAssertionBinder(AssertionConfig assertionConfig) {
this.assertionConfig = assertionConfig;
}

@Override
public Manifest.Assertion bind(Manifest manifest) throws SDK.AssertionException {
Manifest.Assertion assertion = new Manifest.Assertion();
assertion.id = assertionConfig.id;
assertion.type = assertionConfig.type.toString();
assertion.scope = assertionConfig.scope.toString();
assertion.statement = assertionConfig.statement;
assertion.appliesToState = assertionConfig.appliesToState.toString();

try {
ByteArrayOutputStream aggregateHash = Manifest.computeAggregateHash(manifest.encryptionInformation.integrityInformation.segments, manifest.payload.isEncrypted);
boolean hexEncodeRootAndSegmentHashes = manifest.tdfVersion == null || manifest.tdfVersion.isEmpty();
Manifest.Assertion.HashValues hashValues = Manifest.Assertion.calculateAssertionHashValues(aggregateHash, assertion, hexEncodeRootAndSegmentHashes);
if (assertionConfig.signingKey != null && assertionConfig.signingKey.isDefined()) {
assertion.sign(hashValues, assertionConfig.signingKey);
}
// otherwise no explicit signing key provided - use the payload key (DEK)
// this is handled by passing the payload key from the TDF creation context
// for now, return the unsigned assertion - it will be signed by a DEK-based binder
Comment on lines +31 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These comments describe implementation details of another part of the system. While helpful for the developer, they are better suited for a broader design document or a more central part of the code. In this context, they might become outdated or confusing. Consider removing them to keep the code focused on its direct responsibilities.

} catch (IOException e) {
throw new SDK.AssertionException("error reading assertion hash", assertionConfig.id);
} catch (KeyLengthException e) {
throw new SDK.AssertionException("error signing assertion", assertionConfig.id);
}
return assertion;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.opentdf.platform.sdk;

import com.nimbusds.jose.JOSEException;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.text.ParseException;
import java.util.Objects;

public class DEKAssertionValidator implements AssertionValidator {

private AssertionVerificationMode verificationMode = AssertionVerificationMode.FAIL_FAST;

private AssertionConfig.AssertionKey dekKey;

public DEKAssertionValidator(AssertionConfig.AssertionKey dekKey) {
this.dekKey = dekKey;
}

@Override
public String getSchema() {
return "";
}

@Override
public void setVerificationMode(@Nonnull AssertionVerificationMode verificationMode) {
this.verificationMode = verificationMode;
}

@Override
public void verify(Manifest.Assertion assertion, Manifest manifest) throws SDK.AssertionException {
try {
Manifest.Assertion.HashValues hashValues = assertion.verify(dekKey);
var hashOfAssertionAsHex = assertion.hash();
if (!Objects.equals(hashOfAssertionAsHex, hashValues.getAssertionHash())) {
throw new SDK.AssertionException("assertion hash mismatch", assertion.id);
}
} catch (JOSEException e) {
throw new SDKException("error validating assertion hash", e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The verify method in the AssertionValidator interface is declared to throw SDK.AssertionException. However, a generic SDKException is thrown here. For consistency and to adhere to the interface contract, please throw SDK.AssertionException instead. This will also provide more specific information about the failure context (i.e., that it's an assertion failure).

Suggested change
throw new SDKException("error validating assertion hash", e);
throw new SDK.AssertionException("error validating assertion hash", assertion.id);

} catch (ParseException e) {
throw new SDK.AssertionException("error parsing assertion hash", assertion.id);
} catch (IOException e) {
throw new SDK.AssertionException("error reading assertion hash", assertion.id);
}
}

// Validate does nothing - DEK-based validation doesn't check trust/policy.
@Override
public void validate(Manifest.Assertion assertion, TDFReader reader) throws SDK.AssertionException {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.opentdf.platform.sdk;

import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.JWSHeader;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The import com.nimbusds.jose.JWSHeader is unused in this file. Please remove it to maintain code cleanliness.

import com.nimbusds.jose.KeyLengthException;
import com.nimbusds.jose.jwk.RSAKey;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.interfaces.RSAPublicKey;
import java.util.Optional;


public class KeyAssertionBinder implements AssertionBinder {

public static final String KEY_ASSERTION_ID = "assertion-key";
public static final String KEY_ASSERTION_SCHEMA = "urn:opentdf:key:assertion:v1";

private final AssertionConfig.AssertionKey privateKey;
private final AssertionConfig.AssertionKey publicKey;
private final String statementValue;

public KeyAssertionBinder(AssertionConfig.AssertionKey privateKey, AssertionConfig.AssertionKey publicKey, String statementValue) {
this.privateKey = privateKey;
this.publicKey = publicKey;
this.statementValue = statementValue;
}

@Override
public Manifest.Assertion bind(Manifest manifest) throws SDK.AssertionException {
Manifest.Assertion assertion = new Manifest.Assertion();
assertion.id = KEY_ASSERTION_ID;
assertion.type = "other";
assertion.scope = "payload";
assertion.statement = new AssertionConfig.Statement();
assertion.statement.format = "json";
assertion.statement.schema = KEY_ASSERTION_SCHEMA;
assertion.statement.value = statementValue;
assertion.appliesToState = "unencrypted";

RSAKey publicKeyJwk = new RSAKey.Builder((RSAPublicKey) publicKey.key)
.algorithm(Algorithm.parse(publicKey.alg.toString()))
.build();

var protectedHeaders = new java.util.HashMap<String, Object>();
// set key id to public key algorithm in protected headers
protectedHeaders.put("kid", publicKey.alg.toString());
// set jwk as a protected header
protectedHeaders.put("jwk", publicKeyJwk.toJSONObject());

try {
ByteArrayOutputStream aggregateHash = Manifest.computeAggregateHash(manifest.encryptionInformation.integrityInformation.segments, manifest.payload.isEncrypted);
boolean hexEncodeRootAndSegmentHashes = manifest.tdfVersion == null || manifest.tdfVersion.isEmpty();
Manifest.Assertion.HashValues hashValues = Manifest.Assertion.calculateAssertionHashValues(aggregateHash, assertion, hexEncodeRootAndSegmentHashes);
try {
assertion.sign(hashValues, privateKey, Optional.of(protectedHeaders));
} catch (KeyLengthException e) {
throw new SDK.AssertionException("error signing assertion hash", assertion.id);
}
} catch (IOException e) {
throw new SDK.AssertionException("error calculating assertion hash", assertion.id);
}
Comment on lines +51 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The nested try-catch block can be flattened into a single try block with multiple catch clauses. This improves readability by reducing nesting depth.

        try {
            ByteArrayOutputStream aggregateHash = Manifest.computeAggregateHash(manifest.encryptionInformation.integrityInformation.segments, manifest.payload.isEncrypted);
            boolean hexEncodeRootAndSegmentHashes = manifest.tdfVersion == null || manifest.tdfVersion.isEmpty();
            Manifest.Assertion.HashValues hashValues = Manifest.Assertion.calculateAssertionHashValues(aggregateHash, assertion, hexEncodeRootAndSegmentHashes);
            assertion.sign(hashValues, privateKey, Optional.of(protectedHeaders));
        } catch (IOException e) {
            throw new SDK.AssertionException("error calculating assertion hash", assertion.id);
        } catch (KeyLengthException e) {
            throw new SDK.AssertionException("error signing assertion hash", assertion.id);
        }


return assertion;
}
}
Loading
Loading