Skip to content

Commit d3488d2

Browse files
committed
feat(sdk): custom assertions
Signed-off-by: Scott Hamrick <2623452+cshamrick@users.noreply.github.com>
1 parent 9bd9ce5 commit d3488d2

File tree

6 files changed

+211
-76
lines changed

6 files changed

+211
-76
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import io.opentdf.platform.sdk.Manifest.Assertion;
4+
5+
public interface AssertionBinder {
6+
/**
7+
* Creates and signs an assertion, binding it to the manifest.
8+
*
9+
* @param manifest The manifest of the TDF.
10+
* @param aggregateHash The aggregate hash of the TDF payload.
11+
* @return The signed assertion.
12+
* @throws SDK.AssertionException If an error occurs during binding.
13+
*/
14+
Assertion bind(Manifest manifest, byte[] aggregateHash) throws SDK.AssertionException;
15+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.opentdf.platform.sdk;
2+
3+
public class AssertionUtils {
4+
/**
5+
* Computes the data to be signed for an assertion.
6+
*
7+
* @param aggregateHash The hash of the TDF payload segments.
8+
* @param assertionHash The hash of the assertion itself.
9+
* @return The concatenated hash.
10+
*/
11+
public static byte[] computeAssertionSignature(byte[] aggregateHash, byte[] assertionHash) {
12+
byte[] completeHash = new byte[aggregateHash.length + assertionHash.length];
13+
System.arraycopy(aggregateHash, 0, completeHash, 0, aggregateHash.length);
14+
System.arraycopy(assertionHash, 0, completeHash, aggregateHash.length, assertionHash.length);
15+
return completeHash;
16+
}
17+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import io.opentdf.platform.sdk.Manifest.Assertion;
4+
5+
public interface AssertionValidator {
6+
/**
7+
* Returns the schema URI that this validator can handle.
8+
*
9+
* @return The schema URI.
10+
*/
11+
String getSchema();
12+
13+
/**
14+
* Performs a cryptographic check of the assertion.
15+
*
16+
* @param assertion The assertion to verify.
17+
* @param reader The TDF reader.
18+
* @param aggregateHash The aggregate hash of the TDF payload.
19+
* @throws SDK.AssertionException If the verification fails.
20+
*/
21+
void verify(Assertion assertion, TDFReader reader, byte[] aggregateHash) throws SDK.AssertionException;
22+
23+
/**
24+
* Performs a policy check of the assertion.
25+
*
26+
* @param assertion The assertion to validate.
27+
* @param reader The TDF reader.
28+
* @throws SDK.AssertionException If the validation fails.
29+
*/
30+
void validate(Assertion assertion, TDFReader reader) throws SDK.AssertionException;
31+
}

sdk/src/main/java/io/opentdf/platform/sdk/Config.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public static List<KASInfo> fromKeyAccessServer(KeyAccessServer kas) {
9393
kasInfo.Algorithm = KeyType.fromPublicKeyAlgorithm(ki.getAlg()).toString();
9494
kasInfo.PublicKey = ki.getPem();
9595
return Stream.of(kasInfo);
96-
}).collect(Collectors.toList());
96+
}).collect(Collectors.toCollection(ArrayList::new));
9797
}
9898

9999
public static KASInfo fromSimpleKasKey(SimpleKasKey ki) {
@@ -132,6 +132,8 @@ public static class TDFReaderConfig {
132132
KeyType sessionKeyType;
133133
Set<String> kasAllowlist;
134134
boolean ignoreKasAllowlist;
135+
Map<String, AssertionValidator> validators = new HashMap<>();
136+
VerificationMode verificationMode = VerificationMode.FAIL_FAST;
135137
}
136138

137139
@SafeVarargs
@@ -174,6 +176,14 @@ public static Consumer<TDFReaderConfig> WithIgnoreKasAllowlist(boolean ignore) {
174176
return (TDFReaderConfig config) -> config.ignoreKasAllowlist = ignore;
175177
}
176178

179+
public static Consumer<TDFReaderConfig> withAssertionValidator(String schema, AssertionValidator validator) {
180+
return (TDFReaderConfig config) -> config.validators.put(schema, validator);
181+
}
182+
183+
public static Consumer<TDFReaderConfig> withVerificationMode(VerificationMode mode) {
184+
return (TDFReaderConfig config) -> config.verificationMode = mode;
185+
}
186+
177187

178188
public static class TDFConfig {
179189
public Boolean autoconfigure;
@@ -195,6 +205,7 @@ public static class TDFConfig {
195205
public boolean hexEncodeRootAndSegmentHashes;
196206
public boolean renderVersionInfoInManifest;
197207
public boolean systemMetadataAssertion;
208+
public List<AssertionBinder> binders = new ArrayList<>();
198209

199210
public TDFConfig() {
200211
this.autoconfigure = true;
@@ -293,6 +304,10 @@ public static Consumer<TDFConfig> withAssertionConfig(io.opentdf.platform.sdk.As
293304
};
294305
}
295306

307+
public static Consumer<TDFConfig> withAssertionBinder(AssertionBinder binder) {
308+
return (TDFConfig config) -> config.binders.add(binder);
309+
}
310+
296311
public static Consumer<TDFConfig> withMetaData(String metaData) {
297312
return (TDFConfig config) -> config.metaData = metaData;
298313
}

sdk/src/main/java/io/opentdf/platform/sdk/TDF.java

Lines changed: 125 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -365,16 +365,50 @@ private static byte[] calculateSignature(byte[] data, byte[] secret, Config.Inte
365365
return Arrays.copyOfRange(data, data.length - kGMACPayloadLength, data.length);
366366
}
367367

368+
private static class InternalSystemMetadataAssertionBinder implements AssertionBinder {
369+
private final byte[] payloadKey;
370+
371+
public InternalSystemMetadataAssertionBinder(byte[] payloadKey) {
372+
this.payloadKey = payloadKey;
373+
}
374+
375+
@Override
376+
public Manifest.Assertion bind(Manifest manifest, byte[] aggregateHash) throws SDK.AssertionException {
377+
try {
378+
AssertionConfig config = AssertionConfig.getSystemMetadataAssertionConfig(TDF_SPEC_VERSION);
379+
380+
Manifest.Assertion assertion = new Manifest.Assertion();
381+
assertion.id = config.id;
382+
assertion.type = config.type.toString();
383+
assertion.scope = config.scope.toString();
384+
assertion.statement = config.statement;
385+
assertion.appliesToState = config.appliesToState.toString();
386+
387+
String assertionHashAsHex = assertion.hash();
388+
byte[] assertionHash = Hex.decodeHex(assertionHashAsHex);
389+
390+
byte[] completeHash = AssertionUtils.computeAssertionSignature(aggregateHash, assertionHash);
391+
String encodedHash = Base64.getEncoder().encodeToString(completeHash);
392+
393+
Manifest.Assertion.HashValues hashValues = new Manifest.Assertion.HashValues(
394+
assertionHashAsHex,
395+
encodedHash);
396+
397+
AssertionConfig.AssertionKey signingKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, payloadKey);
398+
399+
assertion.sign(hashValues, signingKey);
400+
401+
return assertion;
402+
} catch (IOException | org.apache.commons.codec.DecoderException | com.nimbusds.jose.KeyLengthException e) {
403+
throw new SDK.AssertionException("failed to bind system metadata assertion", e.getMessage());
404+
}
405+
}
406+
}
407+
368408
TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFConfig tdfConfig) throws SDKException, IOException {
369409
Planner planner = new Planner(tdfConfig, services, Autoconfigure::createGranter);
370410
Map<String, List<KASInfo>> splits = planner.getSplits();
371411

372-
// Add System Metadata Assertion if configured
373-
if (tdfConfig.systemMetadataAssertion) {
374-
AssertionConfig systemAssertion = AssertionConfig.getSystemMetadataAssertionConfig(TDF_SPEC_VERSION);
375-
tdfConfig.assertionConfigList.add(systemAssertion);
376-
}
377-
378412
TDFObject tdfObject = new TDFObject();
379413
tdfObject.prepareManifest(tdfConfig, splits);
380414

@@ -458,47 +492,25 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo
458492
tdfObject.manifest.payload.url = TDFWriter.TDF_PAYLOAD_FILE_NAME;
459493
tdfObject.manifest.payload.isEncrypted = true;
460494

461-
List<Manifest.Assertion> signedAssertions = new ArrayList<>(tdfConfig.assertionConfigList.size());
462-
463-
for (var assertionConfig : tdfConfig.assertionConfigList) {
464-
var assertion = new Manifest.Assertion();
465-
assertion.id = assertionConfig.id;
466-
assertion.type = assertionConfig.type.toString();
467-
assertion.scope = assertionConfig.scope.toString();
468-
assertion.statement = assertionConfig.statement;
469-
assertion.appliesToState = assertionConfig.appliesToState.toString();
495+
List<Manifest.Assertion> signedAssertions = new ArrayList<>();
470496

471-
var assertionHashAsHex = assertion.hash();
472-
byte[] assertionHash;
473-
if (tdfConfig.hexEncodeRootAndSegmentHashes) {
474-
assertionHash = assertionHashAsHex.getBytes(StandardCharsets.UTF_8);
475-
} else {
476-
try {
477-
assertionHash = Hex.decodeHex(assertionHashAsHex);
478-
} catch (DecoderException e) {
479-
throw new SDKException("error decoding assertion hash", e);
480-
}
497+
if (tdfConfig.systemMetadataAssertion) {
498+
try {
499+
InternalSystemMetadataAssertionBinder binder = new InternalSystemMetadataAssertionBinder(tdfObject.payloadKey);
500+
Manifest.Assertion assertion = binder.bind(tdfObject.manifest, aggregateHash.toByteArray());
501+
signedAssertions.add(assertion);
502+
} catch (SDK.AssertionException e) {
503+
throw new SDKException("error binding system metadata assertion", e);
481504
}
482-
byte[] completeHash = new byte[aggregateHash.size() + assertionHash.length];
483-
System.arraycopy(aggregateHash.toByteArray(), 0, completeHash, 0, aggregateHash.size());
484-
System.arraycopy(assertionHash, 0, completeHash, aggregateHash.size(), assertionHash.length);
485-
486-
var encodedHash = Base64.getEncoder().encodeToString(completeHash);
505+
}
487506

488-
var assertionSigningKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256,
489-
tdfObject.aesGcm.getKey());
490-
if (assertionConfig.signingKey != null && assertionConfig.signingKey.isDefined()) {
491-
assertionSigningKey = assertionConfig.signingKey;
492-
}
493-
var hashValues = new Manifest.Assertion.HashValues(
494-
assertionHashAsHex,
495-
encodedHash);
507+
for (var binder : tdfConfig.binders) {
496508
try {
497-
assertion.sign(hashValues, assertionSigningKey);
498-
} catch (KeyLengthException e) {
499-
throw new SDKException("error signing assertion hash", e);
509+
var assertion = binder.bind(tdfObject.manifest, aggregateHash.toByteArray());
510+
signedAssertions.add(assertion);
511+
} catch (SDK.AssertionException e) {
512+
throw new SDKException("error binding assertion", e);
500513
}
501-
signedAssertions.add(assertion);
502514
}
503515

504516
tdfObject.manifest.assertions = signedAssertions;
@@ -682,48 +694,86 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig)
682694
break;
683695
}
684696

685-
// Set default to HS256
686-
var assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, payloadKey);
687-
Config.AssertionVerificationKeys assertionVerificationKeys = tdfReaderConfig.assertionVerificationKeys;
688-
if (!assertionVerificationKeys.isEmpty()) {
689-
var keyForAssertion = assertionVerificationKeys.getKey(assertion.id);
690-
if (keyForAssertion != null) {
691-
assertionKey = keyForAssertion;
692-
}
693-
}
697+
AssertionValidator validator = tdfReaderConfig.validators.get(assertion.type);
694698

695-
Manifest.Assertion.HashValues hashValues;
696-
try {
697-
hashValues = assertion.verify(assertionKey);
698-
} catch (ParseException | JOSEException e) {
699-
throw new SDKException("error validating assertion hash", e);
700-
}
701-
var hashOfAssertionAsHex = assertion.hash();
699+
if (validator != null) {
700+
try {
701+
validator.verify(assertion, tdfReader, aggregateHash.toByteArray());
702+
validator.validate(assertion, tdfReader);
703+
} catch (SDK.AssertionException e) {
704+
if (tdfReaderConfig.verificationMode == VerificationMode.STRICT || tdfReaderConfig.verificationMode == VerificationMode.FAIL_FAST) {
705+
throw new SDKException("assertion validation failed in " + tdfReaderConfig.verificationMode + " mode", e);
706+
}
707+
// In permissive mode, we log the error and continue
708+
logger.warn("Assertion validation failed for assertion id {}", assertion.id, e);
709+
}
710+
} else {
711+
if (tdfReaderConfig.verificationMode == VerificationMode.STRICT) {
712+
throw new SDKException("No validator found for assertion id " + assertion.id + " in strict mode");
713+
}
714+
// Permissive and FailFast mode, we attempt DEK fallback
715+
logger.warn("No validator for assertion {}, attempting DEK fallback", assertion.id);
702716

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

707-
byte[] hashOfAssertion;
708-
if (isLegacyTdf) {
709-
hashOfAssertion = hashOfAssertionAsHex.getBytes(StandardCharsets.UTF_8);
710-
} else {
721+
Manifest.Assertion.HashValues hashValues;
711722
try {
712-
hashOfAssertion = Hex.decodeHex(hashOfAssertionAsHex);
713-
} catch (DecoderException e) {
714-
throw new SDKException("error decoding assertion hash", e);
723+
hashValues = assertion.verify(assertionKey);
724+
} catch (ParseException | JOSEException e) {
725+
if (tdfReaderConfig.verificationMode == VerificationMode.FAIL_FAST) {
726+
throw new SDKException("error validating assertion hash", e);
727+
}
728+
logger.warn("Error validating assertion hash for assertion id {}", assertion.id, e);
729+
continue; // permissive
730+
}
731+
var hashOfAssertionAsHex = assertion.hash();
732+
733+
if (!Objects.equals(hashOfAssertionAsHex, hashValues.getAssertionHash())) {
734+
if (tdfReaderConfig.verificationMode == VerificationMode.FAIL_FAST) {
735+
throw new SDK.AssertionException("assertion hash mismatch", assertion.id);
736+
}
737+
logger.warn("Assertion hash mismatch for assertion id {}", assertion.id);
738+
continue; // permissive
715739
}
716-
}
717-
var signature = new byte[aggregateHashByteArrayBytes.length + hashOfAssertion.length];
718-
System.arraycopy(aggregateHashByteArrayBytes, 0, signature, 0, aggregateHashByteArrayBytes.length);
719-
System.arraycopy(hashOfAssertion, 0, signature, aggregateHashByteArrayBytes.length, hashOfAssertion.length);
720-
var encodeSignature = Base64.getEncoder().encodeToString(signature);
721740

722-
if (!Objects.equals(encodeSignature, hashValues.getSignature())) {
723-
throw new SDK.AssertionException("failed integrity check on assertion signature", assertion.id);
741+
byte[] hashOfAssertion;
742+
if (isLegacyTdf) {
743+
hashOfAssertion = hashOfAssertionAsHex.getBytes(StandardCharsets.UTF_8);
744+
} else {
745+
try {
746+
hashOfAssertion = Hex.decodeHex(hashOfAssertionAsHex);
747+
} catch (DecoderException e) {
748+
throw new SDKException("error decoding assertion hash", e);
749+
}
750+
}
751+
var signature = new byte[aggregateHashByteArrayBytes.length + hashOfAssertion.length];
752+
System.arraycopy(aggregateHashByteArrayBytes, 0, signature, 0, aggregateHashByteArrayBytes.length);
753+
System.arraycopy(hashOfAssertion, 0, signature, aggregateHashByteArrayBytes.length, hashOfAssertion.length);
754+
var encodeSignature = Base64.getEncoder().encodeToString(signature);
755+
756+
if (!Objects.equals(encodeSignature, hashValues.getSignature())) {
757+
if (tdfReaderConfig.verificationMode == VerificationMode.FAIL_FAST) {
758+
throw new SDK.AssertionException("failed integrity check on assertion signature", assertion.id);
759+
}
760+
logger.warn("Failed integrity check on assertion signature for assertion id {}", assertion.id);
761+
}
724762
}
725763
}
726764

727765
return new Reader(tdfReader, manifest, payloadKey, unencryptedMetadata);
728766
}
767+
768+
private static AssertionConfig.AssertionKey getAssertionKey(Config.TDFReaderConfig tdfReaderConfig, Manifest.Assertion assertion, byte[] payloadKey) {
769+
var assertionKey = new AssertionConfig.AssertionKey(AssertionConfig.AssertionKeyAlg.HS256, payloadKey);
770+
Config.AssertionVerificationKeys assertionVerificationKeys = tdfReaderConfig.assertionVerificationKeys;
771+
if (!assertionVerificationKeys.isEmpty()) {
772+
var keyForAssertion = assertionVerificationKeys.getKey(assertion.id);
773+
if (keyForAssertion != null) {
774+
assertionKey = keyForAssertion;
775+
}
776+
}
777+
return assertionKey;
778+
}
729779
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.opentdf.platform.sdk;
2+
3+
public enum VerificationMode {
4+
PERMISSIVE,
5+
FAIL_FAST,
6+
STRICT
7+
}

0 commit comments

Comments
 (0)