Skip to content
Closed
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 {
/**
* Creates and signs an assertion, binding it to the manifest.
*
* @param manifest The manifest of the TDF.
* @param aggregateHash The aggregate hash of the TDF payload.
* @return The signed assertion.
* @throws SDK.AssertionException If an error occurs during binding.
*/
Assertion bind(Manifest manifest, byte[] aggregateHash) throws SDK.AssertionException;
}
17 changes: 17 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/AssertionUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.opentdf.platform.sdk;

public class AssertionUtils {

Check warning on line 3 in sdk/src/main/java/io/opentdf/platform/sdk/AssertionUtils.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a private constructor to hide the implicit public one.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AZpPkx1qyidRHwHYenk3&open=AZpPkx1qyidRHwHYenk3&pullRequest=309
/**
* Computes the data to be signed for an assertion.
*
* @param aggregateHash The hash of the TDF payload segments.
* @param assertionHash The hash of the assertion itself.
* @return The concatenated hash.
*/
public static byte[] computeAssertionSignature(byte[] aggregateHash, byte[] assertionHash) {
byte[] completeHash = new byte[aggregateHash.length + assertionHash.length];
System.arraycopy(aggregateHash, 0, completeHash, 0, aggregateHash.length);
System.arraycopy(assertionHash, 0, completeHash, aggregateHash.length, assertionHash.length);
return completeHash;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.opentdf.platform.sdk;

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

public interface AssertionValidator {
/**
* Returns the schema URI that this validator can handle.
*
* @return The schema URI.
*/
String getSchema();

/**
* Performs a cryptographic check of the assertion.
*
* @param assertion The assertion to verify.
* @param reader The TDF reader.
* @param aggregateHash The aggregate hash of the TDF payload.
* @throws SDK.AssertionException If the verification fails.
*/
void verify(Assertion assertion, TDFReader reader, byte[] aggregateHash) throws SDK.AssertionException;

/**
* Performs a policy check of the assertion.
*
* @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;
}
17 changes: 16 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 @@ -93,7 +93,7 @@
kasInfo.Algorithm = KeyType.fromPublicKeyAlgorithm(ki.getAlg()).toString();
kasInfo.PublicKey = ki.getPem();
return Stream.of(kasInfo);
}).collect(Collectors.toList());
}).collect(Collectors.toCollection(ArrayList::new));
}

public static KASInfo fromSimpleKasKey(SimpleKasKey ki) {
Expand Down Expand Up @@ -132,6 +132,8 @@
KeyType sessionKeyType;
Set<String> kasAllowlist;
boolean ignoreKasAllowlist;
Map<String, AssertionValidator> validators = new HashMap<>();
VerificationMode verificationMode = VerificationMode.FAIL_FAST;
}

@SafeVarargs
Expand Down Expand Up @@ -174,6 +176,14 @@
return (TDFReaderConfig config) -> config.ignoreKasAllowlist = ignore;
}

public static Consumer<TDFReaderConfig> withAssertionValidator(String schema, AssertionValidator validator) {
return (TDFReaderConfig config) -> config.validators.put(schema, validator);
}

public static Consumer<TDFReaderConfig> withVerificationMode(VerificationMode mode) {
return (TDFReaderConfig config) -> config.verificationMode = mode;
}


public static class TDFConfig {
public Boolean autoconfigure;
Expand All @@ -195,6 +205,7 @@
public boolean hexEncodeRootAndSegmentHashes;
public boolean renderVersionInfoInManifest;
public boolean systemMetadataAssertion;
public Map<String, AssertionBinder> binders = new HashMap<>();

Check warning on line 208 in sdk/src/main/java/io/opentdf/platform/sdk/Config.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make binders a static final constant or non-public and provide accessors if needed.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AZpPkx1YyidRHwHYenk2&open=AZpPkx1YyidRHwHYenk2&pullRequest=309
Copy link
Member

Choose a reason for hiding this comment

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

can we make this private?


public TDFConfig() {
this.autoconfigure = true;
Expand Down Expand Up @@ -293,6 +304,10 @@
};
}

public static Consumer<TDFConfig> withAssertionBinder(String schema, AssertionBinder binder) {
return (TDFConfig config) -> config.binders.put(schema, binder);
}

public static Consumer<TDFConfig> withMetaData(String metaData) {
return (TDFConfig config) -> config.metaData = metaData;
}
Expand Down
173 changes: 132 additions & 41 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -365,16 +365,50 @@
return Arrays.copyOfRange(data, data.length - kGMACPayloadLength, data.length);
}

private static class InternalSystemMetadataAssertionBinder implements AssertionBinder {
private final byte[] payloadKey;

public InternalSystemMetadataAssertionBinder(byte[] payloadKey) {
this.payloadKey = payloadKey;
}

@Override
public Manifest.Assertion bind(Manifest manifest, byte[] aggregateHash) throws SDK.AssertionException {
try {
AssertionConfig config = AssertionConfig.getSystemMetadataAssertionConfig(TDF_SPEC_VERSION);

Manifest.Assertion assertion = new Manifest.Assertion();
assertion.id = config.id;
assertion.type = config.type.toString();
assertion.scope = config.scope.toString();
assertion.statement = config.statement;
assertion.appliesToState = config.appliesToState.toString();

String assertionHashAsHex = assertion.hash();
byte[] assertionHash = Hex.decodeHex(assertionHashAsHex);

byte[] completeHash = AssertionUtils.computeAssertionSignature(aggregateHash, assertionHash);
String encodedHash = Base64.getEncoder().encodeToString(completeHash);

Manifest.Assertion.HashValues hashValues = new Manifest.Assertion.HashValues(
assertionHashAsHex,
encodedHash);

AssertionConfig.AssertionKey signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, payloadKey);

assertion.sign(hashValues, signingKey);

return assertion;
} catch (IOException | org.apache.commons.codec.DecoderException | com.nimbusds.jose.KeyLengthException e) {
throw new SDK.AssertionException("failed to bind system metadata assertion", e.getMessage());
}
}
}

TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException {

Check warning on line 408 in sdk/src/main/java/io/opentdf/platform/sdk/TDF.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 126 to 64, Complexity from 15 to 14, Nesting Level from 3 to 2, Number of Variables from 38 to 6.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AZpPkx2HyidRHwHYenk5&open=AZpPkx2HyidRHwHYenk5&pullRequest=309
Planner planner = new Planner(tdfConfig, services, Autoconfigure::createGranter);
Map<String, List<KASInfo>> splits = planner.getSplits();

// Add System Metadata Assertion if configured
if (tdfConfig.systemMetadataAssertion) {
AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig(TDF_SPEC_VERSION);
tdfConfig.assertionConfigList.add(systemAssertion);
}

TDFObject tdfObject = new TDFObject();
tdfObject.prepareManifest(tdfConfig, splits);

Expand Down Expand Up @@ -458,9 +492,21 @@
tdfObject.manifest.payload.url = TDFWriter.TDF_PAYLOAD_FILE_NAME;
tdfObject.manifest.payload.isEncrypted = true;

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

if (tdfConfig.systemMetadataAssertion) {
try {
InternalSystemMetadataAssertionBinder binder = new InternalSystemMetadataAssertionBinder(tdfObject.payloadKey);
Manifest.Assertion assertion = binder.bind(tdfObject.manifest, aggregateHash.toByteArray());
signedAssertions.add(assertion);
} catch (SDK.AssertionException e) {
throw new SDKException("error binding system metadata assertion", e);
}
}

for (var assertionConfig : tdfConfig.assertionConfigList) {


var assertion = new Manifest.Assertion();
assertion.id = assertionConfig.id;
assertion.type = assertionConfig.type.toString();
Expand Down Expand Up @@ -494,7 +540,14 @@
assertionHashAsHex,
encodedHash);
try {
assertion.sign(hashValues, assertionSigningKey);
if (tdfConfig.binders.containsKey(assertionConfig.statement.schema)) {
var binder = tdfConfig.binders.get(assertionConfig.statement.schema);
binder.bind(tdfObject.manifest, completeHash);
signedAssertions.add(assertion);
} else {
assertion.sign(hashValues, assertionSigningKey);
}

} catch (KeyLengthException e) {
throw new SDKException("error signing assertion hash", e);
}
Expand Down Expand Up @@ -676,54 +729,92 @@

var aggregateHashByteArrayBytes = aggregateHash.toByteArray();
// Validate assertions
for (var assertion : manifest.assertions) {

Check warning on line 732 in sdk/src/main/java/io/opentdf/platform/sdk/TDF.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce the total number of break and continue statements in this loop to use at most one.

See more on https://sonarcloud.io/project/issues?id=opentdf_java-sdk&issues=AZpPkx2HyidRHwHYenk4&open=AZpPkx2HyidRHwHYenk4&pullRequest=309
// Skip assertion verification if disabled
if (tdfReaderConfig.disableAssertionVerification) {
break;
}

// Set default to HS256
var assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, payloadKey);
Config.AssertionVerificationKeys assertionVerificationKeys = tdfReaderConfig.assertionVerificationKeys;
if (!assertionVerificationKeys.isEmpty()) {
var keyForAssertion = assertionVerificationKeys.getKey(assertion.id);
if (keyForAssertion != null) {
assertionKey = keyForAssertion;
}
}
AssertionValidator validator = tdfReaderConfig.validators.get(assertion.statement.schema);

Manifest.Assertion.HashValues hashValues;
try {
hashValues = assertion.verify(assertionKey);
} catch (ParseException | JOSEException e) {
throw new SDKException("error validating assertion hash", e);
}
var hashOfAssertionAsHex = assertion.hash();
if (validator != null) {
try {
validator.verify(assertion, tdfReader, aggregateHash.toByteArray());
validator.validate(assertion, tdfReader);
} catch (SDK.AssertionException e) {
if (tdfReaderConfig.verificationMode == VerificationMode.STRICT || tdfReaderConfig.verificationMode == VerificationMode.FAIL_FAST) {
throw new SDKException("assertion validation failed in " + tdfReaderConfig.verificationMode + " mode", e);
}
// In permissive mode, we log the error and continue
logger.warn("Assertion validation failed for assertion id {}", assertion.id, e);
}
} else {
if (tdfReaderConfig.verificationMode == VerificationMode.STRICT) {
throw new SDKException("No validator found for assertion id " + assertion.id + " in strict mode");
}
// Permissive and FailFast mode, we attempt DEK fallback
logger.warn("No validator for assertion {}, attempting DEK fallback", assertion.id);

if (!Objects.equals(hashOfAssertionAsHex, hashValues.getAssertionHash())) {
throw new SDK.AssertionException("assertion hash mismatch", assertion.id);
}
// Fallback to DEK-based verification
// Set default to HS256
var assertionKey = getAssertionKey(tdfReaderConfig, assertion, payloadKey);

byte[] hashOfAssertion;
if (isLegacyTdf) {
hashOfAssertion = hashOfAssertionAsHex.getBytes(StandardCharsets.UTF_8);
} else {
Manifest.Assertion.HashValues hashValues;
try {
hashOfAssertion = Hex.decodeHex(hashOfAssertionAsHex);
} catch (DecoderException e) {
throw new SDKException("error decoding assertion hash", e);
hashValues = assertion.verify(assertionKey);
} catch (ParseException | JOSEException e) {
if (tdfReaderConfig.verificationMode == VerificationMode.FAIL_FAST) {
throw new SDKException("error validating assertion hash", e);
}
logger.warn("Error validating assertion hash for assertion id {}", assertion.id, e);
continue; // permissive
}
}
var signature = new byte[aggregateHashByteArrayBytes.length + hashOfAssertion.length];
System.arraycopy(aggregateHashByteArrayBytes, 0, signature, 0, aggregateHashByteArrayBytes.length);
System.arraycopy(hashOfAssertion, 0, signature, aggregateHashByteArrayBytes.length, hashOfAssertion.length);
var encodeSignature = Base64.getEncoder().encodeToString(signature);
var hashOfAssertionAsHex = assertion.hash();

if (!Objects.equals(encodeSignature, hashValues.getSignature())) {
throw new SDK.AssertionException("failed integrity check on assertion signature", assertion.id);
if (!Objects.equals(hashOfAssertionAsHex, hashValues.getAssertionHash())) {
if (tdfReaderConfig.verificationMode == VerificationMode.FAIL_FAST) {
throw new SDK.AssertionException("assertion hash mismatch", assertion.id);
}
logger.warn("Assertion hash mismatch for assertion id {}", assertion.id);
continue; // permissive
}

byte[] hashOfAssertion;
if (isLegacyTdf) {
hashOfAssertion = hashOfAssertionAsHex.getBytes(StandardCharsets.UTF_8);
} else {
try {
hashOfAssertion = Hex.decodeHex(hashOfAssertionAsHex);
} catch (DecoderException e) {
throw new SDKException("error decoding assertion hash", e);
}
}
var signature = new byte[aggregateHashByteArrayBytes.length + hashOfAssertion.length];
System.arraycopy(aggregateHashByteArrayBytes, 0, signature, 0, aggregateHashByteArrayBytes.length);
System.arraycopy(hashOfAssertion, 0, signature, aggregateHashByteArrayBytes.length, hashOfAssertion.length);
var encodeSignature = Base64.getEncoder().encodeToString(signature);

if (!Objects.equals(encodeSignature, hashValues.getSignature())) {
if (tdfReaderConfig.verificationMode == VerificationMode.FAIL_FAST) {
throw new SDK.AssertionException("failed integrity check on assertion signature", assertion.id);
}
logger.warn("Failed integrity check on assertion signature for assertion id {}", assertion.id);
}
}
}

return new Reader(tdfReader, manifest, payloadKey, unencryptedMetadata);
}

private static AssertionConfig.AssertionKey getAssertionKey(Config.TDFReaderConfig tdfReaderConfig, Manifest.Assertion assertion, byte[] payloadKey) {
var assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, payloadKey);
Config.AssertionVerificationKeys assertionVerificationKeys = tdfReaderConfig.assertionVerificationKeys;
if (!assertionVerificationKeys.isEmpty()) {
var keyForAssertion = assertionVerificationKeys.getKey(assertion.id);
if (keyForAssertion != null) {
assertionKey = keyForAssertion;
}
}
return assertionKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.opentdf.platform.sdk;

public enum VerificationMode {
PERMISSIVE,
FAIL_FAST,
STRICT
}
Loading