Skip to content

Commit 49e90ea

Browse files
committed
feat(sdk): Implement pluggable assertion binding and validation framework
Signed-off-by: Scott Hamrick <2623452+cshamrick@users.noreply.github.com>
1 parent 5ede7dd commit 49e90ea

15 files changed

+697
-105
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+
* Bind creates and signs an assertion, binding it to the given manifest.
8+
* // The implementation is responsible for both configuring the assertion and binding it.
9+
*
10+
* @param manifest The manifest.
11+
* @return The assertion.
12+
* @throws SDK.AssertionException If an error occurs during binding.
13+
*/
14+
Assertion bind(Manifest manifest) throws SDK.AssertionException;
15+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
import java.util.concurrent.CopyOnWriteArrayList;
8+
9+
public class AssertionRegistry {
10+
private final List<AssertionBinder> binders;
11+
private final Map<String, AssertionValidator> validators;
12+
13+
public AssertionRegistry() {
14+
this.binders = new CopyOnWriteArrayList<>();
15+
this.validators = new ConcurrentHashMap<>();
16+
}
17+
18+
public void registerBinder(AssertionBinder binder) {
19+
binders.add(binder);
20+
}
21+
22+
public void registerValidator(AssertionValidator validator) {
23+
String schema = validator.getSchema();
24+
validators.put(schema, validator);
25+
}
26+
27+
public List<AssertionBinder> getBinders() {
28+
return Collections.unmodifiableList(binders);
29+
}
30+
31+
public void setBinders(List<AssertionBinder> binders) {
32+
this.binders.clear();
33+
this.binders.addAll(binders);
34+
}
35+
36+
public Map<String, AssertionValidator> getValidators() {
37+
return Collections.unmodifiableMap(validators);
38+
}
39+
40+
public void setValidators(Map<String, AssertionValidator> validators) {
41+
this.validators.clear();
42+
this.validators.putAll(validators);
43+
}
44+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import com.nimbusds.jose.JOSEException;
4+
import io.opentdf.platform.sdk.Manifest.Assertion;
5+
6+
import java.io.IOException;
7+
import java.text.ParseException;
8+
9+
public interface AssertionValidator {
10+
/**
11+
* // Schema returns the schema URI this validator handles.
12+
* // The schema identifies the assertion format and version.
13+
* // Examples: "urn:opentdf:system:metadata:v1", "urn:opentdf:key:assertion:v1"
14+
*
15+
* @return The schema URI.
16+
*/
17+
String getSchema();
18+
19+
void setVerificationMode(AssertionVerificationMode verificationMode);
20+
21+
/**
22+
* // Verify checks the assertion's cryptographic binding.
23+
* //
24+
* // Example:
25+
* // assertionHash, _ := a.GetHash()
26+
* // manifest := r.Manifest()
27+
* // expectedSig, _ := manifest.ComputeAssertionSignature(assertionHash)
28+
*
29+
* @param assertion The assertion to verify.
30+
* @param manifest The manifest.
31+
* @throws SDK.AssertionException If the verification fails.
32+
*/
33+
void verify(Assertion assertion, Manifest manifest) throws SDK.AssertionException;
34+
35+
/**
36+
* // Validate checks the assertion's policy and trust requirements
37+
*
38+
* @param assertion The assertion to validate.
39+
* @param reader The TDF reader.
40+
* @throws SDK.AssertionException If the validation fails.
41+
*/
42+
void validate(Assertion assertion, TDFReader reader) throws SDK.AssertionException;
43+
}
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 AssertionVerificationMode {
4+
PERMISSIVE,
5+
FAIL_FAST,
6+
STRICT
7+
}

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,22 @@ public static class TDFReaderConfig {
132132
KeyType sessionKeyType;
133133
Set<String> kasAllowlist;
134134
boolean ignoreKasAllowlist;
135+
private AssertionVerificationMode assertionVerificationMode = AssertionVerificationMode.FAIL_FAST;
136+
private final AssertionRegistry assertionRegistry = new AssertionRegistry();
137+
138+
public AssertionVerificationMode getAssertionVerificationMode() {
139+
return assertionVerificationMode;
140+
}
141+
142+
public void setAssertionVerificationMode(AssertionVerificationMode assertionVerificationMode) {
143+
this.assertionVerificationMode = assertionVerificationMode;
144+
}
145+
146+
public AssertionRegistry getAssertionRegistry() {
147+
return assertionRegistry;
148+
}
149+
150+
135151
}
136152

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

149165
public static Consumer<TDFReaderConfig> withAssertionVerificationKeys(
150166
AssertionVerificationKeys assertionVerificationKeys) {
151-
return (TDFReaderConfig config) -> config.assertionVerificationKeys = assertionVerificationKeys;
167+
return (TDFReaderConfig config) -> {
168+
config.assertionVerificationKeys = assertionVerificationKeys;
169+
170+
// ONLY register wildcard validator if assertion verification is enabled
171+
// This maintains backward compatibility with the disableAssertionVerification flag
172+
if (!config.disableAssertionVerification) {
173+
// Register a wildcard KeyAssertionValidator that handles any schema
174+
// when verification keys are provided
175+
KeyAssertionValidator keyAssertionValidator = new KeyAssertionValidator(assertionVerificationKeys);
176+
config.getAssertionRegistry().registerValidator(keyAssertionValidator);
177+
}
178+
};
152179
}
153180

154181
public static Consumer<TDFReaderConfig> withDisableAssertionVerification(boolean disable) {
@@ -195,6 +222,7 @@ public static class TDFConfig {
195222
public boolean hexEncodeRootAndSegmentHashes;
196223
public boolean renderVersionInfoInManifest;
197224
public boolean systemMetadataAssertion;
225+
private AssertionRegistry assertionRegistry;
198226

199227
public TDFConfig() {
200228
this.autoconfigure = true;
@@ -212,6 +240,11 @@ public TDFConfig() {
212240
this.hexEncodeRootAndSegmentHashes = false;
213241
this.renderVersionInfoInManifest = true;
214242
this.systemMetadataAssertion = false;
243+
this.assertionRegistry = new AssertionRegistry();
244+
}
245+
246+
public AssertionRegistry getAssertionRegistry() {
247+
return assertionRegistry;
215248
}
216249
}
217250

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

290323
public static Consumer<TDFConfig> withAssertionConfig(io.opentdf.platform.sdk.AssertionConfig... assertionList) {
291324
return (TDFConfig config) -> {
325+
// add to assertionConfigList for backward compatibility
292326
Collections.addAll(config.assertionConfigList, assertionList);
327+
// register a binder for each assertionConfig
328+
for (AssertionConfig assertionConfig : assertionList) {
329+
ConfigBasedAssertionBinder binder = new ConfigBasedAssertionBinder(assertionConfig);
330+
config.getAssertionRegistry().registerBinder(binder);
331+
}
293332
};
294333
}
295334

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import com.nimbusds.jose.KeyLengthException;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.IOException;
7+
8+
public class ConfigBasedAssertionBinder implements AssertionBinder {
9+
private final AssertionConfig assertionConfig;
10+
11+
public ConfigBasedAssertionBinder(AssertionConfig assertionConfig) {
12+
this.assertionConfig = assertionConfig;
13+
}
14+
15+
@Override
16+
public Manifest.Assertion bind(Manifest manifest) throws SDK.AssertionException {
17+
Manifest.Assertion assertion = new Manifest.Assertion();
18+
assertion.id = assertionConfig.id;
19+
assertion.type = assertionConfig.type.toString();
20+
assertion.scope = assertionConfig.scope.toString();
21+
assertion.statement = assertionConfig.statement;
22+
assertion.appliesToState = assertionConfig.appliesToState.toString();
23+
24+
try {
25+
ByteArrayOutputStream aggregateHash = Manifest.computeAggregateHash(manifest.encryptionInformation.integrityInformation.segments, manifest.payload.isEncrypted);
26+
boolean hexEncodeRootAndSegmentHashes = manifest.tdfVersion == null || manifest.tdfVersion.isEmpty();
27+
Manifest.Assertion.HashValues hashValues = Manifest.Assertion.calculateAssertionHashValues(aggregateHash, assertion, hexEncodeRootAndSegmentHashes);
28+
if (assertionConfig.signingKey != null && assertionConfig.signingKey.isDefined()) {
29+
assertion.sign(hashValues, assertionConfig.signingKey);
30+
}
31+
// otherwise no explicit signing key provided - use the payload key (DEK)
32+
// this is handled by passing the payload key from the TDF creation context
33+
// for now, return the unsigned assertion - it will be signed by a DEK-based binder
34+
} catch (IOException e) {
35+
throw new SDK.AssertionException("error reading assertion hash", assertionConfig.id);
36+
} catch (KeyLengthException e) {
37+
throw new SDK.AssertionException("error signing assertion", assertionConfig.id);
38+
}
39+
return assertion;
40+
}
41+
42+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import com.nimbusds.jose.JOSEException;
4+
5+
import javax.annotation.Nonnull;
6+
import java.io.IOException;
7+
import java.text.ParseException;
8+
import java.util.Objects;
9+
10+
public class DEKAssertionValidator implements AssertionValidator {
11+
12+
private AssertionVerificationMode verificationMode = AssertionVerificationMode.FAIL_FAST;
13+
14+
private AssertionConfig.AssertionKey dekKey;
15+
16+
public DEKAssertionValidator(AssertionConfig.AssertionKey dekKey) {
17+
this.dekKey = dekKey;
18+
}
19+
20+
@Override
21+
public String getSchema() {
22+
return "";
23+
}
24+
25+
@Override
26+
public void setVerificationMode(@Nonnull AssertionVerificationMode verificationMode) {
27+
this.verificationMode = verificationMode;
28+
}
29+
30+
@Override
31+
public void verify(Manifest.Assertion assertion, Manifest manifest) throws SDK.AssertionException {
32+
try {
33+
Manifest.Assertion.HashValues hashValues = assertion.verify(dekKey);
34+
var hashOfAssertionAsHex = assertion.hash();
35+
if (!Objects.equals(hashOfAssertionAsHex, hashValues.getAssertionHash())) {
36+
throw new SDK.AssertionException("assertion hash mismatch", assertion.id);
37+
}
38+
} catch (JOSEException e) {
39+
throw new SDKException("error validating assertion hash", e);
40+
} catch (ParseException e) {
41+
throw new SDK.AssertionException("error parsing assertion hash", assertion.id);
42+
} catch (IOException e) {
43+
throw new SDK.AssertionException("error reading assertion hash", assertion.id);
44+
}
45+
}
46+
47+
// Validate does nothing - DEK-based validation doesn't check trust/policy.
48+
@Override
49+
public void validate(Manifest.Assertion assertion, TDFReader reader) throws SDK.AssertionException {}
50+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import com.nimbusds.jose.Algorithm;
4+
import com.nimbusds.jose.JWSHeader;
5+
import com.nimbusds.jose.KeyLengthException;
6+
import com.nimbusds.jose.jwk.RSAKey;
7+
8+
import java.io.ByteArrayOutputStream;
9+
import java.io.IOException;
10+
import java.security.interfaces.RSAPublicKey;
11+
import java.util.Optional;
12+
13+
14+
public class KeyAssertionBinder implements AssertionBinder {
15+
16+
public static final String KEY_ASSERTION_ID = "assertion-key";
17+
public static final String KEY_ASSERTION_SCHEMA = "urn:opentdf:key:assertion:v1";
18+
19+
private final AssertionConfig.AssertionKey privateKey;
20+
private final AssertionConfig.AssertionKey publicKey;
21+
private final String statementValue;
22+
23+
public KeyAssertionBinder(AssertionConfig.AssertionKey privateKey, AssertionConfig.AssertionKey publicKey, String statementValue) {
24+
this.privateKey = privateKey;
25+
this.publicKey = publicKey;
26+
this.statementValue = statementValue;
27+
}
28+
29+
@Override
30+
public Manifest.Assertion bind(Manifest manifest) throws SDK.AssertionException {
31+
Manifest.Assertion assertion = new Manifest.Assertion();
32+
assertion.id = KEY_ASSERTION_ID;
33+
assertion.type = "other";
34+
assertion.scope = "payload";
35+
assertion.statement = new AssertionConfig.Statement();
36+
assertion.statement.format = "json";
37+
assertion.statement.schema = KEY_ASSERTION_SCHEMA;
38+
assertion.statement.value = statementValue;
39+
assertion.appliesToState = "unencrypted";
40+
41+
RSAKey publicKeyJwk = new RSAKey.Builder((RSAPublicKey) publicKey.key)
42+
.algorithm(Algorithm.parse(publicKey.alg.toString()))
43+
.build();
44+
45+
var protectedHeaders = new java.util.HashMap<String, Object>();
46+
// set key id to public key algorithm in protected headers
47+
protectedHeaders.put("kid", publicKey.alg.toString());
48+
// set jwk as a protected header
49+
protectedHeaders.put("jwk", publicKeyJwk.toJSONObject());
50+
51+
try {
52+
ByteArrayOutputStream aggregateHash = Manifest.computeAggregateHash(manifest.encryptionInformation.integrityInformation.segments, manifest.payload.isEncrypted);
53+
boolean hexEncodeRootAndSegmentHashes = manifest.tdfVersion == null || manifest.tdfVersion.isEmpty();
54+
Manifest.Assertion.HashValues hashValues = Manifest.Assertion.calculateAssertionHashValues(aggregateHash, assertion, hexEncodeRootAndSegmentHashes);
55+
try {
56+
assertion.sign(hashValues, privateKey, Optional.of(protectedHeaders));
57+
} catch (KeyLengthException e) {
58+
throw new SDK.AssertionException("error signing assertion hash", assertion.id);
59+
}
60+
} catch (IOException e) {
61+
throw new SDK.AssertionException("error calculating assertion hash", assertion.id);
62+
}
63+
64+
return assertion;
65+
}
66+
}

0 commit comments

Comments
 (0)