A Rust implementation of the OpenTDF (Trusted Data Format) specification, providing data-centric security that travels with the data.
OpenTDF-RS enables cryptographic binding of access policies directly to data objects, supporting a Zero Trust security model with continuous verification. This library allows secure data sharing across organizations and industries.
- TDF Archive Creation and Reading
- Cryptographic Operations (AES-256-GCM encryption)
- Policy Binding through HMAC-SHA256
- Streaming Operations for Efficient Data Handling
- Attribute-Based Access Control (ABAC): Fine-grained access control using attributes
- Hierarchical Attributes: Support for attributes with inheritance relationships
- Time-Based Constraints: Policies with validity periods
- Logical Operators: AND, OR, NOT combinations for complex policies
- Comprehensive Audit Logging: Detailed records of access attempts and attribute evaluation
- KAS (Key Access Service) Integration: Full rewrap protocol support for production deployments
- MCP Server with KAS Support: AI agents can decrypt TDF files using KAS
OpenTDF-RS implements ABAC to provide fine-grained access control that is cryptographically bound to protected data.
Represents a uniquely identifiable attribute with namespace and name:
// Create from string in "namespace:name" format
let attr_id = AttributeIdentifier::from_string("gov.example:clearance")?;
// Or create directly
let attr_id = AttributeIdentifier {
namespace: "gov.example".to_string(),
name: "clearance".to_string(),
};
Supports multiple data types for flexible attribute representation:
// String value
let value = AttributeValue::String("TOP_SECRET".to_string());
// Numeric value
let value = AttributeValue::Number(42.0);
// Boolean value
let value = AttributeValue::Boolean(true);
// Date/time value
let value = AttributeValue::DateTime(chrono::Utc::now());
// Array values
let value = AttributeValue::StringArray(vec!["ENGINEERING".to_string(), "EXECUTIVE".to_string()]);
let value = AttributeValue::NumberArray(vec![1.0, 2.0, 3.0]);
Rich set of comparison operators for attribute evaluation:
Operator | Description | Example |
---|---|---|
Equals | Exact match | department == "ENGINEERING" |
NotEquals | Negated match | department != "FINANCE" |
GreaterThan | Numeric comparison | age > 21 |
GreaterThanOrEqual | Numeric comparison | priority >= 5 |
LessThan | Numeric comparison | risk < 3 |
LessThanOrEqual | Numeric comparison | level <= 4 |
Contains | String contains | name contains "Admin" |
In | Value in array | department in ["HR", "LEGAL"] |
AllOf | All array values present | certifications allof ["ISO", "SOC2"] |
AnyOf | Any array values present | skills anyof ["Rust", "C++"] |
NotIn | Value not in array | restricted_country notin ["US", "EU"] |
MinimumOf | Hierarchical minimum | clearance minimumof "SECRET" |
MaximumOf | Hierarchical maximum | clearance maximumof "TOP_SECRET" |
Present | Attribute exists | employee_id present |
NotPresent | Attribute doesn't exist | terminated notpresent |
Build complex policy expressions with logical operators:
// Simple condition
let is_executive = AttributePolicy::condition(
AttributeIdentifier::from_string("gov.example:role")?,
Operator::Equals,
Some("EXECUTIVE".into())
);
// Logical AND
let and_policy = AttributePolicy::and(vec![condition1, condition2, condition3]);
// Logical OR
let or_policy = AttributePolicy::or(vec![condition1, condition2, condition3]);
// Logical NOT (using operator overloading)
let not_policy = !condition1;
// Complex nested policy
let complex_policy = AttributePolicy::or(vec![
AttributePolicy::and(vec![condition1, condition2]),
AttributePolicy::and(vec![condition3, !condition4]),
]);
Complete policy with time constraints and dissemination:
// Create policy with attribute conditions and recipients
let policy = Policy {
uuid: uuid::Uuid::new_v4().to_string(),
valid_from: Some(chrono::Utc::now()),
valid_to: Some(chrono::Utc::now() + chrono::Duration::days(30)),
body: PolicyBody {
attributes: vec![attribute_policy],
dissem: vec!["user@example.com".to_string()],
},
};
The new simplified API makes ABAC policy binding effortless:
use opentdf::{
Tdf, Policy, AttributePolicy, AttributeIdentifier,
AttributeValue, Operator
};
use std::collections::HashMap;
// 1. Create policy with attribute conditions
let clearance = AttributePolicy::condition(
AttributeIdentifier::from_string("gov.example:clearance")?,
Operator::MinimumOf,
Some("SECRET".into())
);
let department = AttributePolicy::condition(
AttributeIdentifier::from_string("gov.example:department")?,
Operator::In,
Some(AttributeValue::StringArray(vec![
"ENGINEERING".to_string(),
"EXECUTIVE".to_string()
]))
);
// Combine with logical AND
let combined_policy = AttributePolicy::and(vec![clearance, department]);
// Create full policy
let policy = Policy::new(
uuid::Uuid::new_v4().to_string(),
vec![combined_policy.clone()],
vec!["user@example.com".to_string()]
);
// 2. Encrypt with ABAC policy - just 4 lines!
Tdf::encrypt(b"Sensitive information")
.kas_url("https://kas.example.com")
.policy(policy)
.to_file("example.tdf")?;
// 3. Later: Evaluate access based on user attributes
let user_attrs = HashMap::from([
(
AttributeIdentifier::from_string("gov.example:clearance")?,
AttributeValue::String("TOP_SECRET".to_string())
),
(
AttributeIdentifier::from_string("gov.example:department")?,
AttributeValue::String("ENGINEERING".to_string())
),
]);
let access_granted = combined_policy.evaluate(&user_attrs)?;
assert!(access_granted, "User should have access based on attributes");
// Require specific role
let role_policy = AttributePolicy::condition(
AttributeIdentifier::from_string("org:role")?,
Operator::Equals,
Some("ADMIN".into())
);
// Allow access for multiple departments
let dept_policy = AttributePolicy::condition(
AttributeIdentifier::from_string("org:department")?,
Operator::In,
Some(AttributeValue::StringArray(vec![
"FINANCE".to_string(),
"LEGAL".to_string(),
"EXECUTIVE".to_string()
]))
);
// Require minimum clearance and time-bound access
let clearance_policy = AttributePolicy::condition(
AttributeIdentifier::from_string("gov:clearance")?,
Operator::MinimumOf,
Some("SECRET".into())
);
let policy = Policy {
uuid: uuid::Uuid::new_v4().to_string(),
// Valid for next 24 hours only
valid_from: Some(chrono::Utc::now()),
valid_to: Some(chrono::Utc::now() + chrono::Duration::hours(24)),
body: PolicyBody {
attributes: vec![clearance_policy],
dissem: vec!["user@example.com".to_string()],
},
};
// Require specific location AND secure network
let location_policy = AttributePolicy::condition(
AttributeIdentifier::from_string("env:location")?,
Operator::Equals,
Some("HEADQUARTERS".into())
);
let network_policy = AttributePolicy::condition(
AttributeIdentifier::from_string("env:network_type")?,
Operator::Equals,
Some("SECURE".into())
);
let security_policy = AttributePolicy::and(vec![location_policy, network_policy]);
// Employees only (NOT contractors)
let contractor_check = AttributePolicy::condition(
AttributeIdentifier::from_string("org:employment_type")?,
Operator::Equals,
Some("CONTRACTOR".into())
);
let employees_only = !contractor_check;
OpenTDF-RS includes full support for the KAS v2 rewrap protocol, enabling production-ready TDF decryption with centralized key management and access control.
The KAS protocol allows:
- Centralized key management: KAS securely stores and manages encryption keys
- Access control enforcement: KAS validates policies and user attributes before releasing keys
- Audit logging: All key access attempts are logged for compliance
- Zero Trust: Keys are never stored with encrypted data
1. Client generates ephemeral EC key pair (P-256)
2. Client builds rewrap request with TDF manifest
3. Client signs request with JWT (ES256)
4. Client POSTs to KAS /v2/rewrap endpoint
5. KAS validates policy and user attributes
6. KAS returns wrapped key + session public key
7. Client unwraps key: ECDH → HKDF → AES-GCM decrypt
8. Client decrypts TDF payload
Enable the KAS feature in your Cargo.toml
:
[dependencies]
opentdf = { version = "0.3.0", features = ["kas"] }
use opentdf::{TdfArchive, kas::KasClient};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create KAS client
let kas_client = KasClient::new(
"http://kas.example.com/kas",
"your-oauth-token-here"
)?;
// Open and decrypt TDF in one call
let plaintext = TdfArchive::open_and_decrypt(
"encrypted-file.tdf",
&kas_client
).await?;
println!("Decrypted: {}", String::from_utf8_lossy(&plaintext));
Ok(())
}
For more control over the decryption process:
use opentdf::{TdfArchive, kas::KasClient};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create KAS client
let kas_client = KasClient::new(
"http://kas.example.com/kas",
"your-oauth-token-here"
)?;
// Open TDF archive
let mut archive = TdfArchive::open("encrypted-file.tdf")?;
let entry = archive.by_index()?;
// Decrypt using KAS
let plaintext = entry.decrypt_with_kas(&kas_client).await?;
// Access manifest and policy
let policy = entry.manifest.get_policy()?;
println!("Policy: {:?}", policy);
Ok(())
}
Integration tests are available that work with a real KAS server:
# Set environment variables
export KAS_URL="http://10.0.0.138:8080/kas"
export KAS_OAUTH_TOKEN="your-token-here"
# Run KAS integration tests
cargo test --features kas --test kas_integration -- --ignored --nocapture
The KAS client provides detailed error information:
use opentdf::kas::{KasClient, KasError};
match kas_client.rewrap_standard_tdf(&manifest).await {
Ok(key) => println!("Successfully unwrapped key"),
Err(KasError::AccessDenied(reason)) => {
eprintln!("Access denied: {}", reason);
}
Err(KasError::AuthenticationFailed) => {
eprintln!("Invalid OAuth token");
}
Err(KasError::HttpError(msg)) => {
eprintln!("HTTP error: {}", msg);
}
Err(e) => {
eprintln!("KAS error: {}", e);
}
}
⚠️ Compatibility Note: opentdf-rs follows the official OpenTDF specification with camelCase field names in TDF manifests (encryptionInformation
,keyAccess
, etc.). OpenTDFKit (Swift) currently uses non-standard snake_case field names and is incompatible. See INTEROPERABILITY.md for detailed compatibility matrix and workarounds.
The Rust KAS client is fully interoperable with:
- platform/sdk (Go): ✅ Fully compatible (spec-compliant)
- otdfctl (Go): ✅ Fully compatible (golden implementation)
- OpenTDF Platform: ✅ Production KAS deployments
- OpenTDFKit (Swift): ❌ Incompatible (uses snake_case - bug in Swift SDK)
TDF files created by spec-compliant SDKs (Go, Rust) can be decrypted across platforms using KAS.
OpenTDF-RS uses industry-standard cryptographic primitives:
- Symmetric Encryption: AES-256-GCM (Authenticated Encryption with Associated Data)
- Key Wrapping (Standard TDF): RSA-2048 with OAEP padding
- Key Agreement (NanoTDF): ECDH with P-256 curve + HKDF-SHA256
- Policy Binding: HMAC-SHA256
- JWT Signing: ES256 (ECDSA with P-256)
RSA-OAEP Padding: The current implementation uses SHA-1 for RSA-OAEP padding to maintain compatibility with the OpenTDF Go SDK (platform). SHA-1 has known collision vulnerabilities (see SHAttered attack).
- Why SHA-1? Required for cross-platform interoperability with existing OpenTDF implementations
- Risk Level: Low in this context (used for padding, not primary security)
- Mitigation: RSA-2048 key size and OAEP construction provide defense-in-depth
- Future: Migration to SHA-256 planned for entire OpenTDF ecosystem
When using KAS in production:
- Always use HTTPS/TLS for KAS communication (never plain HTTP in production)
- Validate TLS certificates - consider certificate pinning for high-security deployments
- Secure OAuth tokens - use short-lived tokens, never hardcode credentials
- Network isolation - deploy KAS in a protected network segment
- Ephemeral keys: Generated fresh for each KAS request, never reused
- Key rotation: Support for KAS key rotation through multiple key access objects
- Audit logging: All key access attempts should be logged for compliance
- Access policies: Enforce attribute-based access control (ABAC) at KAS layer
- TDF archives use authenticated encryption (AES-256-GCM) preventing tampering
- Policy binding cryptographically ties access policies to encrypted data
- Key wrapping ensures payload keys are never stored in plaintext
- Zero Trust: Encryption keys separated from encrypted data
- FIPS 140-2: AES-256-GCM and RSA-2048 are FIPS-approved algorithms
- NIST SP 800-38D: AES-GCM implementation follows NIST guidelines
- Note: SHA-1 usage may impact certain compliance requirements - evaluate for your use case
For security vulnerabilities, please see SECURITY.md (if available) or file an issue.
OpenTDF-RS includes an implementation of the Model Context Protocol (MCP) server, allowing AI assistants and other tools to interact with TDF capabilities via a standardized API.
The MCP server provides the following tools:
Tool Name | Description |
---|---|
tdf_create |
Creates a new TDF archive with encrypted data |
tdf_read |
Reads contents from a TDF archive. Supports optional KAS decryption with kas_url and kas_token parameters |
encrypt |
Encrypts data using TDF encryption methods |
decrypt |
Decrypts TDF-encrypted data |
policy_create |
Creates a new policy for TDF encryption |
policy_validate |
Validates a policy against a TDF archive |
attribute_define |
Defines attribute namespaces with optional hierarchies |
user_attributes |
Sets user attributes for testing access control |
access_evaluate |
Evaluates whether a user with attributes can access protected content with detailed audit records |
policy_binding_verify |
Verifies the cryptographic binding of a policy to a TDF |
All access attempts and attribute evaluations are comprehensively logged for compliance and auditing purposes. The audit logging system captures detailed information about each operation, including:
- The requesting entity identifiers
- Complete sets of attributes presented
- Attribute sources and verification status
- Detailed evaluation results for each attribute in the policy
- Final access decisions with timestamps
- Policy version information
To run the MCP server and interact with it via Claude or other MCP-compatible clients:
cargo run -p opentdf-mcp-server
The server listens on stdio for JSON-RPC messages, making it compatible with tools like Claude Code that use the MCP protocol for communication.
Claude Code can connect to the MCP server to perform TDF operations:
claude --mcp="cargo run -p opentdf-mcp-server"
This starts Claude with the MCP server, allowing you to use TDF capabilities directly within the chat interface.
Example commands:
# Create TDF
/mcp opentdf tdf_create {"data": "SGVsbG8gV29ybGQh", "kas_url": "https://kas.example.com", "policy": {"uuid": "sample-uuid", "body": {"attributes": [{"attribute": "gov.example:clearance", "operator": "MinimumOf", "value": "secret"}], "dissem": ["user@example.com"]}}}
# Read TDF (without decryption)
/mcp opentdf tdf_read {"tdf_data": "<base64-encoded-tdf-data>"}
# Read and decrypt TDF using KAS
/mcp opentdf tdf_read {"tdf_data": "<base64-encoded-tdf-data>", "kas_url": "http://10.0.0.138:8080/kas", "kas_token": "your-oauth-token"}
The MCP server supports comprehensive ABAC functionality testing:
# Basic ABAC testing
node tools/test-mcp.js
# Comprehensive attribute access logging test
node tools/audit-logging-test.js
These scripts demonstrate:
- Attribute namespace definition with hierarchies
- User attribute assignment
- Policy creation with attribute conditions
- TDF creation with policy binding
- Access evaluation based on attributes
- Policy binding verification
- Comprehensive audit logging and compliance reporting
The audit logging test generates detailed compliance reports in the tools/reports
directory. See tools/audit-guide.md
for more information on the audit logging system.
The MCP server implements the JSON-RPC 2.0 protocol over stdio to provide TDF capabilities to clients. When developing or extending the MCP server:
- Tool Definitions: Define tools with both
schema
andinputSchema
fields for compatibility - Protocol Version: Use the latest MCP protocol version (currently "2024-11-05")
- Response Format: Ensure all responses follow the JSON-RPC 2.0 specification
- Error Handling: Use standard JSON-RPC error codes (-32xxx)
- Testing: Use
tools/test-mcp.js
to verify functionality
If adding new tools, remember to:
- Add the tool to both the initialize response and listTools response
- Implement proper parameter validation
- Follow the JSON-RPC request/response flow
- Document the tool in this README
Add to your Cargo.toml:
[dependencies]
opentdf = "0.3.0"
The new simplified API makes TDF encryption incredibly easy:
use opentdf::{Tdf, Policy};
// Create a policy
let policy = Policy::new(
uuid::Uuid::new_v4().to_string(),
vec![],
vec!["user@example.com".to_string()]
);
// Encrypt data - just 4 lines!
Tdf::encrypt(b"Sensitive data")
.kas_url("https://kas.example.com")
.policy(policy)
.to_file("example.tdf")?;
use opentdf::{Tdf, Policy};
let policy = Policy::new(
uuid::Uuid::new_v4().to_string(),
vec![],
vec!["user@example.com".to_string()]
);
Tdf::encrypt_file("input.txt", "output.tdf")
.kas_url("https://kas.example.com")
.policy(policy)
.mime_type("text/plain")
.build()?;
use opentdf::{
Tdf, Policy, AttributePolicy, AttributeIdentifier,
AttributeValue, Operator
};
// Create policy with attribute conditions
let clearance = AttributePolicy::condition(
AttributeIdentifier::from_string("gov.example:clearance")?,
Operator::MinimumOf,
Some("SECRET".into())
);
let policy = Policy::new(
uuid::Uuid::new_v4().to_string(),
vec![clearance],
vec!["user@example.com".to_string()]
);
// Encrypt with ABAC policy
Tdf::encrypt(b"Classified information")
.kas_url("https://kas.example.com")
.policy(policy)
.to_file("classified.tdf")?;
This project is licensed under [LICENSE].