-
Notifications
You must be signed in to change notification settings - Fork 2
feat(sdk): Implement pluggable assertion binding and validation framework #317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
| * | ||
| * @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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Javadoc comments in this interface contain non-standard formatting (e.g., |
||
| * | ||
| * @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 | ||
| } |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| } 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The nested 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; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.