Skip to content

Conversation

@pflynn-virtru
Copy link
Member

Proposed Changes

This pull request introduces comprehensive support for custom assertion providers in the OpenTDF SDK, with a focus on extensibility, developer experience, and practical examples. The changes include a new architectural decision record (ADR) documenting the custom assertion provider approach, a detailed troubleshooting guide, an improved examples CLI README, and a new example command for adding assertions to TDF files.

Major features and documentation improvements:

Custom Assertion Provider Architecture

  • Added ADR (adr/decisions/2025-10-16-custom-assertion-providers.md) defining the "Binder/Validator Pattern" for custom assertion providers, enabling integration with hardware security modules, smart cards, and cloud KMS. The ADR details interface design, registration, security considerations, and cross-SDK compatibility.

Developer Guidance and Troubleshooting

  • Added docs/Assertions-Troubleshooting.md with solutions to common issues when working with assertion providers, including key loading errors, validation failures, and configuration mistakes. Provides actionable steps and debug tips.

Examples and CLI Enhancements

  • Significantly expanded examples/README.md with a quick start, detailed descriptions of available examples, assertion provider usage, configuration options, and security notes. Clarifies how to use and extend the examples CLI for custom assertion workflows.
  • Introduced a new assertion command (examples/cmd/assertion.go) to the examples CLI, allowing users to add custom assertions (e.g., using a "magic word" provider) to existing TDF files without re-encrypting payload data. Demonstrates the Binder interface in practice.

Checklist

  • I have added or updated unit tests
  • I have added or updated integration tests (if appropriate)
  • I have added or updated documentation

Testing Instructions

Implement pluggable assertion signing and validation providers to enable
integration with external signing mechanisms like HSMs, smart cards (CAC/PIV),
and cloud KMS services.

## What's New

### Core Interfaces
- Add `AssertionSigningProvider` interface for custom signing implementations
- Add `AssertionValidationProvider` interface for custom validation logic
- Maintain full backward compatibility with existing DEK-based assertions

### Built-in Providers
- `DefaultSigningProvider/DefaultValidationProvider`: Existing DEK-based behavior
- `X509SigningProvider/X509ValidationProvider`: X.509 certificate support with x5c headers
- `PKCS11Provider`: Template for hardware token integration

### SDK Integration
- Add `WithAssertionSigningProvider()` option for TDF creation
- Add `WithReaderAssertionValidationProvider()` option for TDF reading
- Automatically fall back to default providers when none specified

### Examples
- Add comprehensive assertion CLI commands (sign, verify, list)
- Support reading assertions from TDF files
- Add `--x509-verify` flag to decrypt command for X.509 validation

## Technical Details

The implementation follows a provider pattern that allows developers to:
1. Supply custom signing logic while maintaining SDK compatibility
2. Integrate with hardware security modules and smart cards
3. Use X.509 certificates for identity-based assertions
4. Maintain complete backward compatibility with existing code

All providers use the standard SDK assertion binding (`assertionHash` and
`assertionSig` claims) ensuring full interoperability between tools.

## Testing

- Added provider interface tests with mock implementations
- Added X.509 provider tests with self-signed certificates
- Verified interoperability with otdfctl-created TDFs
- Tested backward compatibility with existing assertions
Enhance `--magic-word` handling with a more robust assertion provider setup, including default validation using `NoopAssertionValidationProvider` and state-aware `MagicWordAssertionProvider`.
… command

Streamline assertion provider integration:
- Remove unnecessary comments and redundant field (`AssertionProvider`) in `MagicWordAssertionProvider`.
- Adjust `WithAssertionProviderFactory` to use a pointer for consistency.
- Update regex in `decrypt.go` for precise assertion matching.
Streamline SDK by removing unused `assertion_binding.go`, including obsolete legacy binding logic and functions for TDF assertion handling.
…c key signing providers

Transition to `PublicKeySigningProvider`, replacing `DefaultSigningProvider` for consistent key-based signing. Introduce `SystemMetadataAssertionProvider` for system metadata handling. Refine assertion provider mechanism in SDK and CLI examples.
Introduce `Verify` method in `Assertion` to validate binding signatures. Update `AssertionProvider` and `Reader` interfaces to support enhanced validation mechanisms. Replace obsolete aggregate hash logic with root signature for assertion binding. Simplify and extend implementation for better clarity and modularity.
…y-based assertions

Transition `PayloadKeyProvider` to `PublicKeySigningProvider` for key-based assertions. Introduce `KeyAssertionProvider` for improved assertion setup. Update related interfaces and examples to use RSA keys for signing and validation. Streamline CLI commands for flexible assertion provider setup.
…ertionRegistry`

Introduce `AssertionRegistry` as a replacement for `AssertionProviderFactory`, streamlining assertion management. Update SDK and examples to use assertion builders and validators. Replace provider terminology with builder-focused nomenclature for consistency. Modify TDF assertion setup and CLI examples for improved clarity and usability.
…igning

Transition from `SignWithProvider` to `Sign` for improved assertion signing. Simplify and streamline signature generation using direct key-based mechanisms. Update related assertion handling and verification methods to align with new design.
…lidation

Streamline assertion handling by replacing `KeyAssertionProvider` with `KeyAssertionBuilder` and `KeyAssertionValidator`. Remove obsolete signing and validation providers for clarity. Update CLI and examples to support private key-based validation registry setup. Refine TDF assertion validation flow for improved security and modularity.
…thAssertionRegistryReader`

Update SDK and examples to align with updated nomenclature. Replace references to `WithAssertionProviderFactory` with `WithAssertionRegistryReader` for consistency. Adjust decrypt command and related SDK logic to reflect this change.
…handling

Transition from `AssertionBuilder` to `AssertionBinder` for enhanced clarity and modularity in assertion handling. Update SDK and examples to streamline assertion registration and binding. Adjust encryption/decryption commands, SDK logic, and TDF configuration to reflect binder-based design.
…n handling

Replace legacy assertion provider interfaces with the binder/validator pattern for improved clarity, modularity, and flexibility. Enable developers to implement custom signing (binders) and validation (validators). Update architecture to support regex-based dispatcher for mixed assertion types, maintain backward compatibility, and improve efficiency with post-creation binding.
…ooting guides

Remove inline key parsing logic from examples, centralizing it into reusable functions for clarity and maintainability. Update CLI commands for assertion signing/validation with improved error handling. Add comprehensive documentation for assertion troubleshooting and format details.
…prove context usage

Remove obsolete `AssertionSigningProvider` and `AssertionValidationProvider` interfaces in favor of binder/validator pattern. Replace unused and redundant hashing logic. Standardize function signatures by eliminating unused parameters. Enhance clarity in examples with added `//nolint` annotations and improved error messaging.
…ata assertions

Introduce support for backwards-compatible dual-mode validation, handling both v1 (legacy) and v2 (current) schema versions. Implement `verifyLegacyAssertion` for v1 signature validation using concatenated hashes. Add comprehensive test cases to ensure accurate schema detection and validation behavior. Update documentation to detail schema versions, validation logic, and migration strategies.
…ttern and context integration

Integrate `configBasedAssertionBinder` and `KeyAssertionValidator` for enhanced modularity in assertion creation and verification. Register binders and validators dynamically based on assertion configs and keys. Improve compatibility by maintaining DEK-based validation for assertions without explicit verifiers. Adjust tests to focus on functional correctness, removing brittle size validations.
…validation

Introduce `slog` for improved visibility into assertion processes. Add debug and error logs during signature parsing, schema auto-detection, and verification outcomes. Enhance troubleshooting with context-aware details like assertion ID, schema, signature length, and error messages.
Skip unnecessary setup if assertion verification is disabled. Add verification for binding signatures in `SystemMetadataAssertionProvider` to enforce signature trustworthiness. Refine legacy compatibility handling by validating only assertions with bindings.
… for `Binding`

Implement `MarshalJSON()` in `Assertion` to omit empty `binding` fields from serialization, aligning with cleaner JSON output standards. Introduce `IsEmpty()` method in `Binding` to simplify emptiness checks.
Update signing key logic to mandate cryptographic bindings for all assertions. Refine error handling and improve signature compatibility by supporting both v1 (legacy) and v2 (current) formats with structured logging. Adjust tests to include signing keys and verify updated binding requirements.
…icit keys

Implement automatic signing of unsigned assertions using the DEK (payload key) to enforce mandatory cryptographic bindings. Enhance backward compatibility with legacy SDK formats and maintain secure assertion handling across TDFs. Update decision records to reflect new auto-signing logic.
pflynn-virtru and others added 4 commits October 22, 2025 12:59
…gnature handling

Refactor assertion components to incorporate `ShouldUseHexEncoding` for encoding format detection directly from the manifest. Simplify constructors for `KeyAssertionBinder`, `DEKAssertionValidator`, and `SystemMetadataAssertionProvider` by removing redundant `useHex` and `aggregateHash` parameters. Update tests, examples, and validation logic for improved clarity, consistency, and tampering protection.
…nifest processing

Unified assertion signature computation by introducing `VerifyAssertionSignatureFormat` helper. Refactored `DEKAssertionValidator`, `SystemMetadataAssertionProvider`, and related test helpers to eliminate redundant logic and use standardized manifest processing. Enhanced tampering detection and cross-SDK compatibility with updated encoding format handling.
Adds diagrams for assertions

---------

Signed-off-by: Scott Hamrick <2623452+cshamrick@users.noreply.github.com>
…v2 schema support

Add support for the new `SystemMetadataSchemaV2`, allowing JSON values to be marshaled and unmarshaled as raw objects instead of escaped strings. Update `SystemMetadataAssertionProvider`, tests, and related logic to ensure backward compatibility with V1 schema while supporting the new schema format. Enhance validation, marshaling, and unmarshaling logic for cross-SDK interoperability and precise handling of schema-specific behaviors.
@pflynn-virtru pflynn-virtru requested review from a team as code owners November 10, 2025 18:32
@github-actions github-actions bot added comp:sdk A software development kit, including library, for client applications and inter-service communicati docs Documentation comp:examples size/xl labels Nov 10, 2025
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @pflynn-virtru, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the OpenTDF SDK's assertion capabilities by introducing a flexible "Binder/Validator Pattern" for custom assertion providers. This architectural change allows for seamless integration with various hardware security modules and cloud key management services, greatly improving the SDK's extensibility and developer experience. The update also includes comprehensive documentation, a troubleshooting guide, and a new CLI example for managing assertions in TDF files, alongside core SDK changes to support flexible JSON assertion values and efficient in-place manifest updates.

Highlights

  • Custom Assertion Providers: Introduced a flexible "Binder/Validator Pattern" for custom assertion providers, enabling integration with hardware security modules (HSMs), smart cards, and cloud Key Management Services (KMS). This allows developers to define their own signing and validation logic.
  • Enhanced Documentation and Examples: Added a new Architectural Decision Record (ADR) detailing the custom assertion provider approach, a comprehensive troubleshooting guide for assertion issues, and significantly expanded the examples CLI README. A new assertion command was added to the examples CLI.
  • New CLI Command for Assertions: A new assertion add command in the examples CLI allows users to add custom assertions to existing TDF files without re-encrypting the payload data, demonstrating the new Binder interface in practice.
  • Flexible Statement Value Handling: The Statement.Value field in assertions now directly supports JSON objects, improving flexibility and aligning with modern TDF specifications (SystemMetadataSchemaV2), while maintaining backward compatibility for older string-encoded values.
  • In-Place TDF Manifest Updates: Added a WriteTDFWithUpdatedManifest function that allows modifying a TDF's manifest (e.g., adding assertions) without re-encrypting the entire payload, preserving other ZIP entries byte-for-byte for efficiency.
  • Assertion Verification Modes: Introduced AssertionVerificationMode (Permissive, FailFast, Strict) to configure how assertion verification errors are handled during TDF reading, providing a balance between security and compatibility for different environments.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.


A binder, a validator, new paths unfold, For secrets to keep, a story untold. With JSON in hand, and modes set just right, OpenTDF's shield, shines ever so bright.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

…vider-statement-value-json

# Conflicts:
#	sdk/tdf.go
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a robust and extensible framework for custom assertion providers in the OpenTDF SDK, a significant and well-executed enhancement. The changes, including the new AssertionBinder and AssertionValidator interfaces, the schema-based routing, and the different verification modes, are well-designed and align with the detailed ADR. The addition of comprehensive documentation, troubleshooting guides, and CLI examples greatly improves developer experience.

My review focuses on a few key areas: ensuring documentation consistency with the implementation, improving the efficiency of a core hashing function, and clarifying some logic in the example code. Overall, this is an excellent contribution that significantly advances the SDK's capabilities.

Comment on lines +62 to +147
### Registering Custom Assertion Providers

The SDK supports custom signing and validation logic for assertions through a flexible provider model. The recommended way to manage multiple, conditional providers is to use the `ProviderFactory`.

The `ProviderFactory` lets you register different providers for different assertions, matched via regular expressions on the assertion ID. It implements the `AssertionSigningProvider` and `AssertionValidationProvider` interfaces itself, so you can configure it once and pass it directly to the SDK.

This allows you to, for example, use a hardware-based signing provider for assertions with a `piv-` prefix, and a default provider for all others.

**Example:**

First, you need an implementation of the `AssertionSigningProvider` and/or `AssertionValidationProvider` interfaces. For this example, we'll create a simple mock.

```go
// my_custom_provider.go
package main

import (
"context"
"github.com/opentdf/platform/sdk"
)

// CustomSigner is a custom implementation of an assertion signing provider.
type CustomSigner struct {
SignatureValue string
}

func (s *CustomSigner) Sign(ctx context.Context, assertion *sdk.Assertion, hash, sig string) (string, error) {
// In a real implementation, you would use a hardware token, an external service, etc.
return s.SignatureValue, nil
}

func (s *CustomSigner) GetSigningKeyReference() string {
return "custom-signer-key-ref"
}

func (s *CustomSigner) GetAlgorithm() string {
return "CUSTOM_SIG"
}
```

Next, register this provider with the factory and pass the factory to the SDK during TDF creation.

```go
// main.go
package main

import (
"fmt"
"github.com/opentdf/platform/sdk"
)

func main() {
// 1. Create a new provider factory.
factory := sdk.NewProviderFactory()

// 2. Create an instance of your custom provider.
mySigner := &CustomSigner{SignatureValue: "a-very-special-signature"}

// 3. Register the provider with a regex pattern.
// This will match any assertion ID that starts with "custom-".
err := factory.RegisterSigningProvider(`^custom-`, mySigner)
if err != nil {
panic(err)
}

// You can also set a default provider for assertions that don't match any pattern.
// factory.SetDefaultSigningProvider(sdk.NewDefaultSigningProvider(...))

// 4. When creating a TDF, pass the factory directly as a provider.
// The factory will dispatch to the correct underlying provider internally.
_, err = s.CreateTDF(
ciphertext,
plaintext,
sdk.WithDataAttributes("https://example.com/attr/Classification/value/Open"),
sdk.WithAssertionSigningProvider(factory), // Pass the factory here
)

// Similarly, for reading TDFs:
// reader, err := s.LoadTDF(input, sdk.WithAssertionValidationProvider(factory))

fmt.Println("SDK configured to use custom provider factory.")
}
```

The factory will test assertion IDs against registered patterns in the order they were registered and use the first one that matches. If no patterns match, a default provider will be used if one has been set.

Copy link
Contributor

Choose a reason for hiding this comment

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

high

The documentation for "Registering Custom Assertion Providers" appears to be out of sync with the implementation in this pull request. The example uses AssertionSigningProvider and a ProviderFactory, whereas the new implementation introduces AssertionBinder/AssertionValidator interfaces and registers them via WithAssertionBinder/WithAssertionValidator options.

To avoid confusion for developers, please update this documentation to reflect the new Binder/Validator pattern, interfaces, and registration methods introduced in this PR. The example should demonstrate the use of WithAssertionBinder and WithAssertionValidator.

Comment on lines +80 to +87
// TODO SECURITY: Add schema claim to cryptographically bind schema to assertion
// This prevents schema substitution attacks where an attacker changes Statement.Schema
// to route the assertion to a different validator with weaker security checks.
// The schema is included in both the JWT (signed) and the Statement (hashed),
// providing defense-in-depth against tampering.
// if err := tok.Set("assertionSchema", a.Statement.Schema); err != nil {
// return fmt.Errorf("failed to set assertion schema: %w", err)
// }
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 is a great security consideration. To ensure it's not lost, I recommend creating a follow-up ticket to track the implementation of adding the assertionSchema claim to the JWT. This will be a valuable defense-in-depth measure against schema substitution attacks.

Comment on lines +103 to 104
autoconfigure = false
if !autoconfigure {
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 logic here is a bit confusing. autoconfigure is unconditionally set to false, which makes the following if !autoconfigure condition always true. If the intention is to always disable autoconfiguration for this example command, it would be clearer to remove the conditional check and add a comment explaining why it's disabled.

@github-actions
Copy link
Contributor

Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 188.693794ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 108.961001ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 384.804288ms
Throughput 259.87 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 40.450803679s
Average Latency 402.434681ms
Throughput 123.61 requests/second

NANOTDF Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 28.092613033s
Average Latency 279.889026ms
Throughput 177.98 requests/second

@github-actions
Copy link
Contributor

@github-actions
Copy link
Contributor

Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 148.229064ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 78.108854ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 364.272576ms
Throughput 274.52 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 38.123945486s
Average Latency 379.618363ms
Throughput 131.15 requests/second

NANOTDF Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 26.975868924s
Average Latency 268.823623ms
Throughput 185.35 requests/second

@github-actions
Copy link
Contributor

@github-actions
Copy link
Contributor

Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 167.893556ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 96.545338ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 365.874157ms
Throughput 273.32 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 38.694401952s
Average Latency 385.49083ms
Throughput 129.22 requests/second

NANOTDF Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 27.431476705s
Average Latency 273.333076ms
Throughput 182.27 requests/second

@github-actions
Copy link
Contributor

@pflynn-virtru pflynn-virtru changed the base branch from main to feature/assertion-provider November 12, 2025 15:30
@pflynn-virtru pflynn-virtru requested review from a team as code owners November 12, 2025 15:30
@pflynn-virtru pflynn-virtru changed the base branch from feature/assertion-provider to main November 12, 2025 15:31
@pflynn-virtru pflynn-virtru marked this pull request as draft November 12, 2025 15:32
@pflynn-virtru pflynn-virtru changed the base branch from main to feature/assertion-provider November 25, 2025 14:23
@pflynn-virtru pflynn-virtru changed the base branch from feature/assertion-provider to main November 25, 2025 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:examples comp:sdk A software development kit, including library, for client applications and inter-service communicati docs Documentation size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants