From 78a0b92d65fac1451c1dec0fbf1b1ef23bca2c93 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 31 Mar 2026 10:36:54 -0700 Subject: [PATCH 1/7] feat(docs): document TDF assertions in SDK docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive assertions section to tdf.mdx covering types, scopes, AppliesToState, statements, signing/verification, system metadata assertions, and end-to-end examples in Go/Java/JS. Add type reference entries for AssertionConfig, Assertion, Statement, AssertionKey, and AssertionVerificationKeys. Add WithSystemMetadataAssertion to encrypt options. Fix Go scope constant (TrustedDataObj → TrustedDataObjScope). Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- code_samples/tdf/encrypt_options.mdx | 48 +++- docs/sdks/tdf.mdx | 368 ++++++++++++++++++++++++++- 2 files changed, 413 insertions(+), 3 deletions(-) diff --git a/code_samples/tdf/encrypt_options.mdx b/code_samples/tdf/encrypt_options.mdx index ef2d46fa..d7d38b3a 100644 --- a/code_samples/tdf/encrypt_options.mdx +++ b/code_samples/tdf/encrypt_options.mdx @@ -391,7 +391,7 @@ import "github.com/opentdf/platform/sdk" assertionCfg := sdk.AssertionConfig{ ID: "assertion-1", Type: sdk.HandlingAssertion, - Scope: sdk.TrustedDataObj, + Scope: sdk.TrustedDataObjScope, AppliesToState: sdk.Unencrypted, Statement: sdk.Statement{ Format: "application/json", @@ -417,7 +417,7 @@ import ( assertionCfg := sdk.AssertionConfig{ ID: "assertion-1", Type: sdk.HandlingAssertion, - Scope: sdk.TrustedDataObj, + Scope: sdk.TrustedDataObjScope, AppliesToState: sdk.Unencrypted, Statement: sdk.Statement{ Format: "application/json", @@ -517,6 +517,50 @@ Signed assertions can be verified on decrypt using [Assertion Verification Keys] --- +### System Metadata Assertion + +Automatically attach a system metadata assertion containing TDF spec version, creation timestamp, SDK version, OS, and architecture. Useful for audit trails and debugging. + + + + +```go +import "github.com/opentdf/platform/sdk" + +manifest, err := client.CreateTDF(&buf, plaintext, + sdk.WithKasInformation(sdk.KASInfo{URL: platformEndpoint}), + sdk.WithSystemMetadataAssertion(), +) +``` + + + + +```java +import io.opentdf.platform.sdk.Config; + +Config.TDFConfig config = Config.newTDFConfig( + Config.withKasInformation(kasInfo), + Config.withSystemMetadataAssertion() +); +``` + + + + +```typescript +const encryptParams = new EncryptParamsBuilder() + .withSystemMetadataAssertion(true) + .build(); +``` + + + + +The assertion uses ID `"system-metadata"`, schema `"system-metadata-v1"`, type `BaseAssertion`, scope `PayloadScope`, and state `Unencrypted`. It is bound with the DEK (HS256) by default. + +--- + ### Wrapping Key Algorithm When a TDF is created, the SDK generates a random symmetric Data Encryption Key (DEK) to encrypt the payload. The DEK is then asymmetrically encrypted ("wrapped") using the KAS's public key, so that only the KAS can unwrap it during decryption. This option controls which asymmetric algorithm is used for that wrapping step. diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index f746d9f5..462a6096 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -21,8 +21,9 @@ This page covers the core TDF operations: - **[TDF Reader](#tdf-reader)** — methods on the reader object returned by `LoadTDF` - **[Encrypt Options](#encrypt-options)** — full option reference for `CreateTDF` - **[Decrypt Options](#decrypt-options)** — full option reference for `LoadTDF` +- **[Assertions](#assertions)** — signed metadata: types, scopes, signing, and verification - **[Session Encryption](#session-encryption)** — provide your own RSA key for KAS response encryption -- **[Type Reference](#type-reference)** — `KASInfo`, `PolicyObject`, `Manifest` +- **[Type Reference](#type-reference)** — `KASInfo`, `PolicyObject`, `Manifest`, `AssertionConfig` - **[Experimental: Streaming Writer](#experimental-streaming-writer)** — segment-based API for large files and out-of-order assembly --- @@ -1135,6 +1136,301 @@ client, err := sdk.New("http://localhost:8080", --- +## Assertions + +**Assertions** are signed statements attached to a TDF that carry structured metadata alongside the encrypted payload. Use them to embed handling instructions, audit labels, retention policies, or any application-specific metadata that should travel with the data and be cryptographically verifiable on decrypt. + +Every assertion is bound to the TDF's data encryption key (DEK) by default, preventing it from being removed or copied to a different TDF. You can optionally sign assertions with your own RSA key for independent verification. + +### Assertion Types + +| Constant | Value | Purpose | +|----------|-------|---------| +| `HandlingAssertion` | `"handling"` | Data handling and processing instructions — e.g., retention policies, access restrictions, DLP rules. | +| `BaseAssertion` | `"other"` | General-purpose metadata — e.g., audit labels, content identifiers, provenance records. | + +### Scopes + +| Constant | Value | Description | +|----------|-------|-------------| +| `TrustedDataObjScope` | `"tdo"` | Assertion applies to the entire TDF object. | +| `PayloadScope` | `"payload"` | Assertion applies only to the encrypted payload. | + +### AppliesToState + +| Constant | Value | When to process | +|----------|-------|-----------------| +| `Encrypted` | `"encrypted"` | Process **before** decryption — useful for access-control checks, audit logging, or routing decisions that don't require seeing the plaintext. | +| `Unencrypted` | `"unencrypted"` | Process **after** decryption — useful for content filtering, retention enforcement, or any logic that inspects the plaintext. | + +### Statement + +Each assertion carries a `Statement` with three fields: + +| Field | Description | +|-------|-------------| +| `Format` | Payload encoding format (e.g., `"application/json"`, `"text/plain"`). | +| `Schema` | Schema identifier for the value (e.g., `"retention-policy-v1"`). | +| `Value` | The assertion payload as a string — typically a JSON object. | + +### Signing and Verification + +By default, every assertion is bound to the TDF using the DEK with HMAC-SHA256 (`HS256`). This binding ensures the assertion cannot be tampered with or detached, but verification requires the DEK — which means only entities that can decrypt the TDF can verify the binding. + +To enable **independent verification** (where a verifier doesn't need the DEK), sign the assertion with an RSA private key (`RS256`) at encrypt time and provide the corresponding public key at decrypt time. + +| Algorithm | Constant | Key type | Use case | +|-----------|----------|----------|----------| +| HMAC-SHA256 | `AssertionKeyAlgHS256` | Symmetric (byte slice / DEK) | Default binding — tamper detection for anyone who can decrypt. | +| RSA-SHA256 | `AssertionKeyAlgRS256` | RSA key pair or `crypto.Signer` | Independent verification — any holder of the public key can verify. | + +### System Metadata Assertions + +The SDKs can automatically attach a **system metadata assertion** containing information about the environment that created the TDF — spec version, creation timestamp, SDK version, OS, and architecture. This is useful for audit trails and debugging without requiring application-level code. + + + + +```go +import "github.com/opentdf/platform/sdk" + +manifest, err := client.CreateTDF(&buf, plaintext, + sdk.WithKasInformation(sdk.KASInfo{URL: kasURL}), + sdk.WithSystemMetadataAssertion(), +) +``` + +To inspect or customize the config before passing it to `WithAssertions`: + +```go +cfg, err := sdk.GetSystemMetadataAssertionConfig() +// cfg.Statement.Value contains JSON like: +// {"tdf_spec_version":"4.3.0","creation_date":"2026-03-31T12:00:00Z", +// "operating_system":"linux","sdk_version":"Go-0.3.4", +// "go_version":"go1.23.0","architecture":"amd64"} +``` + + + + +```java +import io.opentdf.platform.sdk.Config; + +Config.TDFConfig config = Config.newTDFConfig( + Config.withKasInformation(kasInfo), + Config.withSystemMetadataAssertion() +); +sdk.createTDF(inputStream, outputStream, config); +``` + + + + +```typescript +const encryptParams = new EncryptParamsBuilder() + .withSystemMetadataAssertion(true) + .build(); +``` + + + + +The system metadata assertion uses ID `"system-metadata"`, schema `"system-metadata-v1"`, type `BaseAssertion`, scope `PayloadScope`, and `AppliesToState` `Unencrypted`. + +### End-to-End Example + +Create a signed assertion at encrypt time, then verify it at decrypt time. + + + + +```go +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "io" + "log" + + "github.com/opentdf/platform/sdk" +) + +// Generate an RSA key pair for assertion signing. +rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) +if err != nil { + log.Fatal(err) +} + +// --- Encrypt with a signed assertion --- + +assertionCfg := sdk.AssertionConfig{ + ID: "handling-1", + Type: sdk.HandlingAssertion, + Scope: sdk.TrustedDataObjScope, + AppliesToState: sdk.Unencrypted, + Statement: sdk.Statement{ + Format: "application/json", + Schema: "retention-policy-v1", + Value: `{"retain_until":"2027-01-01","classification":"internal"}`, + }, + SigningKey: sdk.AssertionKey{ + Alg: sdk.AssertionKeyAlgRS256, + Key: rsaKey, + }, +} + +var buf bytes.Buffer +_, err = client.CreateTDF(&buf, plaintext, + sdk.WithKasInformation(sdk.KASInfo{URL: kasURL}), + sdk.WithAssertions(assertionCfg), +) +if err != nil { + log.Fatal(err) +} + +// --- Decrypt and verify the assertion --- + +verificationKeys := sdk.AssertionVerificationKeys{ + Keys: map[string]sdk.AssertionKey{ + "handling-1": { + Alg: sdk.AssertionKeyAlgRS256, + Key: &rsaKey.PublicKey, + }, + }, +} + +tdfReader, err := client.LoadTDF( + bytes.NewReader(buf.Bytes()), + sdk.WithAssertionVerificationKeys(verificationKeys), +) +if err != nil { + log.Fatal(err) // Verification failure surfaces here. +} + +decrypted, err := io.ReadAll(tdfReader) +if err != nil { + log.Fatal(err) +} + +// Access assertions from the manifest. +for _, a := range tdfReader.Manifest().Assertions { + log.Printf("Assertion %s [%s]: %s", a.ID, a.Type, a.Statement.Value) +} +``` + + + + +```java +import io.opentdf.platform.sdk.AssertionConfig; +import io.opentdf.platform.sdk.Config; +import io.opentdf.platform.sdk.TDF; +import java.security.KeyPairGenerator; +import java.util.Map; + +// Generate an RSA key pair for assertion signing. +var keyGen = KeyPairGenerator.getInstance("RSA"); +keyGen.initialize(2048); +var keyPair = keyGen.generateKeyPair(); + +// --- Encrypt with a signed assertion --- + +var statement = new AssertionConfig.Statement(); +statement.format = "application/json"; +statement.schema = "retention-policy-v1"; +statement.value = "{\"retain_until\":\"2027-01-01\",\"classification\":\"internal\"}"; + +var assertionCfg = new AssertionConfig(); +assertionCfg.id = "handling-1"; +assertionCfg.type = AssertionConfig.Type.HandlingAssertion; +assertionCfg.scope = AssertionConfig.Scope.TrustedDataObj; +assertionCfg.appliesToState = AssertionConfig.AppliesToState.Unencrypted; +assertionCfg.statement = statement; +assertionCfg.signingKey = new AssertionConfig.AssertionKey( + AssertionConfig.AssertionKeyAlg.RS256, + keyPair.getPrivate() +); + +Config.TDFConfig config = Config.newTDFConfig( + Config.withKasInformation(kasInfo), + Config.withAssertionConfig(assertionCfg) +); +sdk.createTDF(inputStream, outputStream, config); + +// --- Decrypt and verify the assertion --- + +var verifyKey = new AssertionConfig.AssertionKey( + AssertionConfig.AssertionKeyAlg.RS256, + keyPair.getPublic() +); + +var verificationKeys = new Config.AssertionVerificationKeys(); +verificationKeys.keys = Map.of("handling-1", verifyKey); + +Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(verificationKeys) +); +TDF.Reader reader = sdk.loadTDF(fileChannel, readerConfig); +// Verification failure throws during loadTDF. +``` + + + + +```typescript +// Generate an RSA key pair for assertion signing. +const keyPair = await crypto.subtle.generateKey( + { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + true, + ['sign', 'verify'], +); + +// --- Encrypt with a signed assertion --- + +const tdf = await client.createTDF({ + source: { type: 'buffer', location: plaintext }, + defaultKASEndpoint: kasURL, + assertionConfigs: [ + { + id: 'handling-1', + type: 'HandlingAssertion', + scope: 'TrustedDataObj', + appliesToState: 'Unencrypted', + statement: { + format: 'application/json', + schema: 'retention-policy-v1', + value: '{"retain_until":"2027-01-01","classification":"internal"}', + }, + }, + ], + signers: { + 'handling-1': keyPair.privateKey, + }, +}); + +// --- Decrypt and verify the assertion --- + +const result = await client.read({ + source: { type: 'buffer', location: tdf }, + assertionVerificationKeys: { + keys: { + 'handling-1': { + alg: 'RS256', + key: keyPair.publicKey, + }, + }, + }, +}); +// Verification failure throws during read. +``` + + + + +See the [Encrypt Options](#encrypt-options) and [Decrypt Options](#decrypt-options) sections for the full option reference, including per-option details for `WithAssertions` and `WithAssertionVerificationKeys`. + +--- + ## Type Reference The following types are returned by or passed to the methods above. @@ -1330,6 +1626,76 @@ for _, a := range manifest.Assertions { --- +### AssertionConfig + +`AssertionConfig` is the input type passed to `WithAssertions` when creating a TDF. It defines the assertion content, classification, and optional signing key. + +**Fields** + +| Field | Go | Java | Required | Description | +|-------|-----|------|----------|-------------| +| ID | `ID string` | `String id` | Yes | Unique identifier for the assertion. Used to look up verification keys on decrypt. | +| Type | `Type AssertionType` | `Type type` | Yes | `HandlingAssertion` (`"handling"`) or `BaseAssertion` (`"other"`). | +| Scope | `Scope Scope` | `Scope scope` | Yes | `TrustedDataObjScope` (`"tdo"`) or `PayloadScope` (`"payload"`). | +| AppliesToState | `AppliesToState AppliesToState` | `AppliesToState appliesToState` | Yes | `Encrypted` or `Unencrypted`. | +| Statement | `Statement Statement` | `Statement statement` | No | The assertion content. See [Statement (type)](#statement-type). | +| SigningKey | `SigningKey AssertionKey` | `AssertionKey signingKey` | No | Custom signing key. If empty, the assertion is bound with the DEK using HS256. See [AssertionKey](#assertionkey). | + +--- + +### Assertion + +`Assertion` is the signed form stored in the TDF [manifest](#manifest-object). Accessible via `tdfReader.Manifest().Assertions` after decryption. + +**Fields** + +| Field | Type | Description | +|-------|------|-------------| +| `ID` | `string` | Assertion identifier (matches the `AssertionConfig.ID` used at encrypt time). | +| `Type` | `AssertionType` | `"handling"` or `"other"`. | +| `Scope` | `Scope` | `"tdo"` or `"payload"`. | +| `AppliesToState` | `AppliesToState` | `"encrypted"` or `"unencrypted"`. | +| `Statement` | `Statement` | The assertion content. | +| `Binding` | `Binding` | Cryptographic binding — `Method` (`"jws"`) and `Signature` (JWS token). | + +--- + +### Statement (type) + +**Fields** + +| Field | Type | Description | +|-------|------|-------------| +| `Format` | `string` | Payload encoding format (e.g., `"application/json"`, `"text/plain"`). | +| `Schema` | `string` | Schema identifier for the value (e.g., `"retention-policy-v1"`). | +| `Value` | `string` | The assertion payload — typically a JSON object serialized as a string. | + +--- + +### AssertionKey + +**Fields** + +| Field | Type | Description | +|-------|------|-------------| +| `Alg` | `AssertionKeyAlg` | `AssertionKeyAlgRS256` (`"RS256"`) for RSA-SHA256, or `AssertionKeyAlgHS256` (`"HS256"`) for HMAC-SHA256. | +| `Key` | `interface{}` / `Object` | For RS256: an RSA private key (encrypt) or public key (verify). For HS256: a byte slice / shared secret. Supports `crypto.Signer` for hardware-backed keys. | + +--- + +### AssertionVerificationKeys + +Passed to `WithAssertionVerificationKeys` when loading a TDF. Maps assertion IDs to the public keys used to verify their signatures. + +**Fields** + +| Field | Type | Description | +|-------|------|-------------| +| `DefaultKey` | `AssertionKey` | Fallback key used when no ID-specific key is found. | +| `Keys` | `map[string]AssertionKey` | Map of assertion ID to verification key. | + +--- + --- From 5754b11d2980c3ea4f32143e35668d83ce3f4412 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 31 Mar 2026 10:44:52 -0700 Subject: [PATCH 2/7] fix(docs): use lowercase "state" in system metadata description Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- docs/sdks/tdf.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index 462a6096..88cf6584 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -1235,7 +1235,7 @@ const encryptParams = new EncryptParamsBuilder() -The system metadata assertion uses ID `"system-metadata"`, schema `"system-metadata-v1"`, type `BaseAssertion`, scope `PayloadScope`, and `AppliesToState` `Unencrypted`. +The system metadata assertion uses ID `"system-metadata"`, schema `"system-metadata-v1"`, type `BaseAssertion`, scope `PayloadScope`, and state `Unencrypted`. ### End-to-End Example From ef09e2118d37452a4da5bb7a1c5546806f3f7a61 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 31 Mar 2026 10:54:48 -0700 Subject: [PATCH 3/7] refactor(docs): extract assertion examples into code_samples Move system metadata and end-to-end assertion examples from inline tdf.mdx into code_samples/tdf/assertion_examples.mdx, following the repo convention for long code examples. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- code_samples/tdf/assertion_examples.mdx | 247 ++++++++++++++++++++++++ docs/sdks/tdf.mdx | 246 +---------------------- 2 files changed, 249 insertions(+), 244 deletions(-) create mode 100644 code_samples/tdf/assertion_examples.mdx diff --git a/code_samples/tdf/assertion_examples.mdx b/code_samples/tdf/assertion_examples.mdx new file mode 100644 index 00000000..755f12f4 --- /dev/null +++ b/code_samples/tdf/assertion_examples.mdx @@ -0,0 +1,247 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +### System Metadata + +The SDKs can automatically attach a **system metadata assertion** containing information about the environment that created the TDF — spec version, creation timestamp, SDK version, OS, and architecture. This is useful for audit trails and debugging without requiring application-level code. + + + + +```go +import "github.com/opentdf/platform/sdk" + +manifest, err := client.CreateTDF(&buf, plaintext, + sdk.WithKasInformation(sdk.KASInfo{URL: kasURL}), + sdk.WithSystemMetadataAssertion(), +) +``` + +To inspect or customize the config before passing it to `WithAssertions`: + +```go +cfg, err := sdk.GetSystemMetadataAssertionConfig() +// cfg.Statement.Value contains JSON like: +// {"tdf_spec_version":"4.3.0","creation_date":"2026-03-31T12:00:00Z", +// "operating_system":"linux","sdk_version":"Go-0.3.4", +// "go_version":"go1.23.0","architecture":"amd64"} +``` + + + + +```java +import io.opentdf.platform.sdk.Config; + +Config.TDFConfig config = Config.newTDFConfig( + Config.withKasInformation(kasInfo), + Config.withSystemMetadataAssertion() +); +sdk.createTDF(inputStream, outputStream, config); +``` + + + + +```typescript +const encryptParams = new EncryptParamsBuilder() + .withSystemMetadataAssertion(true) + .build(); +``` + + + + +The system metadata assertion uses ID `"system-metadata"`, schema `"system-metadata-v1"`, type `BaseAssertion`, scope `PayloadScope`, and state `Unencrypted`. + +### End-to-End Example + +Create a signed assertion at encrypt time, then verify it at decrypt time. + + + + +```go +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "io" + "log" + + "github.com/opentdf/platform/sdk" +) + +// Generate an RSA key pair for assertion signing. +rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) +if err != nil { + log.Fatal(err) +} + +// --- Encrypt with a signed assertion --- + +assertionCfg := sdk.AssertionConfig{ + ID: "handling-1", + Type: sdk.HandlingAssertion, + Scope: sdk.TrustedDataObjScope, + AppliesToState: sdk.Unencrypted, + Statement: sdk.Statement{ + Format: "application/json", + Schema: "retention-policy-v1", + Value: `{"retain_until":"2027-01-01","classification":"internal"}`, + }, + SigningKey: sdk.AssertionKey{ + Alg: sdk.AssertionKeyAlgRS256, + Key: rsaKey, + }, +} + +var buf bytes.Buffer +_, err = client.CreateTDF(&buf, plaintext, + sdk.WithKasInformation(sdk.KASInfo{URL: kasURL}), + sdk.WithAssertions(assertionCfg), +) +if err != nil { + log.Fatal(err) +} + +// --- Decrypt and verify the assertion --- + +verificationKeys := sdk.AssertionVerificationKeys{ + Keys: map[string]sdk.AssertionKey{ + "handling-1": { + Alg: sdk.AssertionKeyAlgRS256, + Key: &rsaKey.PublicKey, + }, + }, +} + +tdfReader, err := client.LoadTDF( + bytes.NewReader(buf.Bytes()), + sdk.WithAssertionVerificationKeys(verificationKeys), +) +if err != nil { + log.Fatal(err) // Verification failure surfaces here. +} + +decrypted, err := io.ReadAll(tdfReader) +if err != nil { + log.Fatal(err) +} + +// Access assertions from the manifest. +for _, a := range tdfReader.Manifest().Assertions { + log.Printf("Assertion %s [%s]: %s", a.ID, a.Type, a.Statement.Value) +} +``` + + + + +```java +import io.opentdf.platform.sdk.AssertionConfig; +import io.opentdf.platform.sdk.Config; +import io.opentdf.platform.sdk.TDF; +import java.security.KeyPairGenerator; +import java.util.Map; + +// Generate an RSA key pair for assertion signing. +var keyGen = KeyPairGenerator.getInstance("RSA"); +keyGen.initialize(2048); +var keyPair = keyGen.generateKeyPair(); + +// --- Encrypt with a signed assertion --- + +var statement = new AssertionConfig.Statement(); +statement.format = "application/json"; +statement.schema = "retention-policy-v1"; +statement.value = "{\"retain_until\":\"2027-01-01\",\"classification\":\"internal\"}"; + +var assertionCfg = new AssertionConfig(); +assertionCfg.id = "handling-1"; +assertionCfg.type = AssertionConfig.Type.HandlingAssertion; +assertionCfg.scope = AssertionConfig.Scope.TrustedDataObj; +assertionCfg.appliesToState = AssertionConfig.AppliesToState.Unencrypted; +assertionCfg.statement = statement; +assertionCfg.signingKey = new AssertionConfig.AssertionKey( + AssertionConfig.AssertionKeyAlg.RS256, + keyPair.getPrivate() +); + +Config.TDFConfig config = Config.newTDFConfig( + Config.withKasInformation(kasInfo), + Config.withAssertionConfig(assertionCfg) +); +sdk.createTDF(inputStream, outputStream, config); + +// --- Decrypt and verify the assertion --- + +var verifyKey = new AssertionConfig.AssertionKey( + AssertionConfig.AssertionKeyAlg.RS256, + keyPair.getPublic() +); + +var verificationKeys = new Config.AssertionVerificationKeys(); +verificationKeys.keys = Map.of("handling-1", verifyKey); + +Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( + Config.withAssertionVerificationKeys(verificationKeys) +); +TDF.Reader reader = sdk.loadTDF(fileChannel, readerConfig); +// Verification failure throws during loadTDF. +``` + + + + +```typescript +// Generate an RSA key pair for assertion signing. +const keyPair = await crypto.subtle.generateKey( + { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + true, + ['sign', 'verify'], +); + +// --- Encrypt with a signed assertion --- + +const tdf = await client.createTDF({ + source: { type: 'buffer', location: plaintext }, + defaultKASEndpoint: kasURL, + assertionConfigs: [ + { + id: 'handling-1', + type: 'HandlingAssertion', + scope: 'TrustedDataObj', + appliesToState: 'Unencrypted', + statement: { + format: 'application/json', + schema: 'retention-policy-v1', + value: '{"retain_until":"2027-01-01","classification":"internal"}', + }, + }, + ], + signers: { + 'handling-1': keyPair.privateKey, + }, +}); + +// --- Decrypt and verify the assertion --- + +const result = await client.read({ + source: { type: 'buffer', location: tdf }, + assertionVerificationKeys: { + keys: { + 'handling-1': { + alg: 'RS256', + key: keyPair.publicKey, + }, + }, + }, +}); +// Verification failure throws during read. +``` + + + + +See the [Encrypt Options](#encrypt-options) and [Decrypt Options](#decrypt-options) sections for the full option reference, including per-option details for `WithAssertions` and `WithAssertionVerificationKeys`. diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index 88cf6584..95406dda 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -7,6 +7,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import EncryptOptions from '../../code_samples/tdf/encrypt_options.mdx' import DecryptOptions from '../../code_samples/tdf/decrypt_options.mdx' +import AssertionExamples from '../../code_samples/tdf/assertion_examples.mdx' # TDF @@ -1184,250 +1185,7 @@ To enable **independent verification** (where a verifier doesn't need the DEK), | HMAC-SHA256 | `AssertionKeyAlgHS256` | Symmetric (byte slice / DEK) | Default binding — tamper detection for anyone who can decrypt. | | RSA-SHA256 | `AssertionKeyAlgRS256` | RSA key pair or `crypto.Signer` | Independent verification — any holder of the public key can verify. | -### System Metadata Assertions - -The SDKs can automatically attach a **system metadata assertion** containing information about the environment that created the TDF — spec version, creation timestamp, SDK version, OS, and architecture. This is useful for audit trails and debugging without requiring application-level code. - - - - -```go -import "github.com/opentdf/platform/sdk" - -manifest, err := client.CreateTDF(&buf, plaintext, - sdk.WithKasInformation(sdk.KASInfo{URL: kasURL}), - sdk.WithSystemMetadataAssertion(), -) -``` - -To inspect or customize the config before passing it to `WithAssertions`: - -```go -cfg, err := sdk.GetSystemMetadataAssertionConfig() -// cfg.Statement.Value contains JSON like: -// {"tdf_spec_version":"4.3.0","creation_date":"2026-03-31T12:00:00Z", -// "operating_system":"linux","sdk_version":"Go-0.3.4", -// "go_version":"go1.23.0","architecture":"amd64"} -``` - - - - -```java -import io.opentdf.platform.sdk.Config; - -Config.TDFConfig config = Config.newTDFConfig( - Config.withKasInformation(kasInfo), - Config.withSystemMetadataAssertion() -); -sdk.createTDF(inputStream, outputStream, config); -``` - - - - -```typescript -const encryptParams = new EncryptParamsBuilder() - .withSystemMetadataAssertion(true) - .build(); -``` - - - - -The system metadata assertion uses ID `"system-metadata"`, schema `"system-metadata-v1"`, type `BaseAssertion`, scope `PayloadScope`, and state `Unencrypted`. - -### End-to-End Example - -Create a signed assertion at encrypt time, then verify it at decrypt time. - - - - -```go -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - "io" - "log" - - "github.com/opentdf/platform/sdk" -) - -// Generate an RSA key pair for assertion signing. -rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) -if err != nil { - log.Fatal(err) -} - -// --- Encrypt with a signed assertion --- - -assertionCfg := sdk.AssertionConfig{ - ID: "handling-1", - Type: sdk.HandlingAssertion, - Scope: sdk.TrustedDataObjScope, - AppliesToState: sdk.Unencrypted, - Statement: sdk.Statement{ - Format: "application/json", - Schema: "retention-policy-v1", - Value: `{"retain_until":"2027-01-01","classification":"internal"}`, - }, - SigningKey: sdk.AssertionKey{ - Alg: sdk.AssertionKeyAlgRS256, - Key: rsaKey, - }, -} - -var buf bytes.Buffer -_, err = client.CreateTDF(&buf, plaintext, - sdk.WithKasInformation(sdk.KASInfo{URL: kasURL}), - sdk.WithAssertions(assertionCfg), -) -if err != nil { - log.Fatal(err) -} - -// --- Decrypt and verify the assertion --- - -verificationKeys := sdk.AssertionVerificationKeys{ - Keys: map[string]sdk.AssertionKey{ - "handling-1": { - Alg: sdk.AssertionKeyAlgRS256, - Key: &rsaKey.PublicKey, - }, - }, -} - -tdfReader, err := client.LoadTDF( - bytes.NewReader(buf.Bytes()), - sdk.WithAssertionVerificationKeys(verificationKeys), -) -if err != nil { - log.Fatal(err) // Verification failure surfaces here. -} - -decrypted, err := io.ReadAll(tdfReader) -if err != nil { - log.Fatal(err) -} - -// Access assertions from the manifest. -for _, a := range tdfReader.Manifest().Assertions { - log.Printf("Assertion %s [%s]: %s", a.ID, a.Type, a.Statement.Value) -} -``` - - - - -```java -import io.opentdf.platform.sdk.AssertionConfig; -import io.opentdf.platform.sdk.Config; -import io.opentdf.platform.sdk.TDF; -import java.security.KeyPairGenerator; -import java.util.Map; - -// Generate an RSA key pair for assertion signing. -var keyGen = KeyPairGenerator.getInstance("RSA"); -keyGen.initialize(2048); -var keyPair = keyGen.generateKeyPair(); - -// --- Encrypt with a signed assertion --- - -var statement = new AssertionConfig.Statement(); -statement.format = "application/json"; -statement.schema = "retention-policy-v1"; -statement.value = "{\"retain_until\":\"2027-01-01\",\"classification\":\"internal\"}"; - -var assertionCfg = new AssertionConfig(); -assertionCfg.id = "handling-1"; -assertionCfg.type = AssertionConfig.Type.HandlingAssertion; -assertionCfg.scope = AssertionConfig.Scope.TrustedDataObj; -assertionCfg.appliesToState = AssertionConfig.AppliesToState.Unencrypted; -assertionCfg.statement = statement; -assertionCfg.signingKey = new AssertionConfig.AssertionKey( - AssertionConfig.AssertionKeyAlg.RS256, - keyPair.getPrivate() -); - -Config.TDFConfig config = Config.newTDFConfig( - Config.withKasInformation(kasInfo), - Config.withAssertionConfig(assertionCfg) -); -sdk.createTDF(inputStream, outputStream, config); - -// --- Decrypt and verify the assertion --- - -var verifyKey = new AssertionConfig.AssertionKey( - AssertionConfig.AssertionKeyAlg.RS256, - keyPair.getPublic() -); - -var verificationKeys = new Config.AssertionVerificationKeys(); -verificationKeys.keys = Map.of("handling-1", verifyKey); - -Config.TDFReaderConfig readerConfig = Config.newTDFReaderConfig( - Config.withAssertionVerificationKeys(verificationKeys) -); -TDF.Reader reader = sdk.loadTDF(fileChannel, readerConfig); -// Verification failure throws during loadTDF. -``` - - - - -```typescript -// Generate an RSA key pair for assertion signing. -const keyPair = await crypto.subtle.generateKey( - { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, - true, - ['sign', 'verify'], -); - -// --- Encrypt with a signed assertion --- - -const tdf = await client.createTDF({ - source: { type: 'buffer', location: plaintext }, - defaultKASEndpoint: kasURL, - assertionConfigs: [ - { - id: 'handling-1', - type: 'HandlingAssertion', - scope: 'TrustedDataObj', - appliesToState: 'Unencrypted', - statement: { - format: 'application/json', - schema: 'retention-policy-v1', - value: '{"retain_until":"2027-01-01","classification":"internal"}', - }, - }, - ], - signers: { - 'handling-1': keyPair.privateKey, - }, -}); - -// --- Decrypt and verify the assertion --- - -const result = await client.read({ - source: { type: 'buffer', location: tdf }, - assertionVerificationKeys: { - keys: { - 'handling-1': { - alg: 'RS256', - key: keyPair.publicKey, - }, - }, - }, -}); -// Verification failure throws during read. -``` - - - - -See the [Encrypt Options](#encrypt-options) and [Decrypt Options](#decrypt-options) sections for the full option reference, including per-option details for `WithAssertions` and `WithAssertionVerificationKeys`. + --- From 08bc10874dddeb4d5e79274d08e1ca1e242e6e7e Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Tue, 31 Mar 2026 11:17:19 -0700 Subject: [PATCH 4/7] fix(docs): move cross-reference line from code sample to tdf.mdx Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Mary Dickson --- code_samples/tdf/assertion_examples.mdx | 2 -- docs/sdks/tdf.mdx | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/code_samples/tdf/assertion_examples.mdx b/code_samples/tdf/assertion_examples.mdx index 755f12f4..16b287f4 100644 --- a/code_samples/tdf/assertion_examples.mdx +++ b/code_samples/tdf/assertion_examples.mdx @@ -243,5 +243,3 @@ const result = await client.read({ - -See the [Encrypt Options](#encrypt-options) and [Decrypt Options](#decrypt-options) sections for the full option reference, including per-option details for `WithAssertions` and `WithAssertionVerificationKeys`. diff --git a/docs/sdks/tdf.mdx b/docs/sdks/tdf.mdx index 95406dda..a8820bb7 100644 --- a/docs/sdks/tdf.mdx +++ b/docs/sdks/tdf.mdx @@ -1187,6 +1187,8 @@ To enable **independent verification** (where a verifier doesn't need the DEK), +See the [Encrypt Options](#encrypt-options) and [Decrypt Options](#decrypt-options) sections for the full option reference, including per-option details for `WithAssertions` and `WithAssertionVerificationKeys`. + --- ## Type Reference From 85929d50f614059ebd9cdc7140a6f9acf78459f4 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 3 Apr 2026 09:06:31 -0700 Subject: [PATCH 5/7] fix(docs): rename assertions encrypt option header to avoid anchor conflict The encrypt options "Assertions" header produced the same #assertions anchor as the main Assertions section, causing TOC links to jump to the wrong spot. Renamed to "WithAssertions" to match the SDK option name and avoid the duplicate anchor. Co-Authored-By: Claude Opus 4.6 (1M context) --- code_samples/tdf/encrypt_options.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code_samples/tdf/encrypt_options.mdx b/code_samples/tdf/encrypt_options.mdx index d7d38b3a..cbbffc39 100644 --- a/code_samples/tdf/encrypt_options.mdx +++ b/code_samples/tdf/encrypt_options.mdx @@ -7,7 +7,7 @@ The following options can be passed to the encrypt call to control how the TDF i --- -### Data Attributes +### WithDataAttributes Attach one or more attribute value FQNs to the TDF policy. Access to the data will be governed by these attributes — only entities that hold matching attribute values will be permitted to decrypt. @@ -233,7 +233,7 @@ Steps with the same `sid` share a key segment. Steps with different `sid` values --- -### Metadata +### WithMetadata Attach a plaintext metadata string to the TDF. Metadata is stored unencrypted in the TDF manifest and is readable by anyone with access to the file — do not store sensitive values here. Common uses include audit labels, content identifiers, or application-specific tags. @@ -378,7 +378,7 @@ const tdf = await client.createTDF({ --- -### Assertions +### WithAssertions Assertions are signed statements attached to the TDF that carry structured metadata — audit labels, handling instructions, content identifiers — and can be cryptographically verified on decrypt. From 64bd34d77b6933e2c7d484ada4c6cbc837854a59 Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 3 Apr 2026 09:13:04 -0700 Subject: [PATCH 6/7] docs(docs): add anchor collision prevention convention to AGENTS.md Documents the naming convention for code_samples headers to avoid duplicate anchors when imported into doc pages. References #273 for planned CI enforcement. Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 8c351c3b..60dd7742 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ Docs-only checks: - Indentation: 2 spaces; newlines: LF (see `.editorconfig`). - Docs: prefer `.mdx`; name new pages `kebab-case.mdx`, and use `index.mdx` for section landing pages. - Keep long examples in `code_samples/` and reference them from docs instead of duplicating. +- **Anchor collision prevention**: Headers in `code_samples/` files render on the same page as the doc that imports them. Use SDK option names for encrypt/decrypt option headers (e.g. `### WithAssertions`, `### WithDataAttributes`, `### WithMetadata`) to avoid colliding with concept-level headers in the parent doc (e.g. `## Assertions`, `### Data Attributes`). See [#273](https://github.com/opentdf/docs/issues/273) for planned CI enforcement. ## Testing Guidelines From 8f8ec420d10ea839db1a48c6f11dcb73c6829adf Mon Sep 17 00:00:00 2001 From: Mary Dickson Date: Fri, 3 Apr 2026 09:37:31 -0700 Subject: [PATCH 7/7] chore(docs): update vendored OpenAPI specs Co-Authored-By: Claude Opus 4.6 (1M context) --- .../policy/namespaces/namespaces.openapi.yaml | 34 +++++++++++++++++++ specs/policy/selectors.openapi.yaml | 7 ++++ 2 files changed, 41 insertions(+) diff --git a/specs/policy/namespaces/namespaces.openapi.yaml b/specs/policy/namespaces/namespaces.openapi.yaml index 51632ed0..4a5b7e35 100644 --- a/specs/policy/namespaces/namespaces.openapi.yaml +++ b/specs/policy/namespaces/namespaces.openapi.yaml @@ -364,6 +364,13 @@ components: - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1 - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1 - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1 + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC policy.SourceType: type: string title: SourceType @@ -375,6 +382,15 @@ components: Describes whether this kas is managed by the organization or if they imported the kas information from an external party. These two modes are necessary in order to encrypt a tdf dek with an external parties kas public key. + policy.namespaces.SortNamespacesType: + type: string + title: SortNamespacesType + enum: + - SORT_NAMESPACES_TYPE_UNSPECIFIED + - SORT_NAMESPACES_TYPE_NAME + - SORT_NAMESPACES_TYPE_FQN + - SORT_NAMESPACES_TYPE_CREATED_AT + - SORT_NAMESPACES_TYPE_UPDATED_AT common.Metadata: type: object properties: @@ -901,6 +917,13 @@ components: title: pagination description: Optional $ref: '#/components/schemas/policy.PageRequest' + sort: + type: array + items: + $ref: '#/components/schemas/policy.namespaces.NamespacesSort' + title: sort + maxItems: 1 + description: Optional title: ListNamespacesRequest additionalProperties: false policy.namespaces.ListNamespacesResponse: @@ -950,6 +973,17 @@ components: title: NamespaceKeyAccessServer additionalProperties: false description: Deprecated + policy.namespaces.NamespacesSort: + type: object + properties: + field: + title: field + $ref: '#/components/schemas/policy.namespaces.SortNamespacesType' + direction: + title: direction + $ref: '#/components/schemas/policy.SortDirection' + title: NamespacesSort + additionalProperties: false policy.namespaces.RemoveKeyAccessServerFromNamespaceRequest: type: object properties: diff --git a/specs/policy/selectors.openapi.yaml b/specs/policy/selectors.openapi.yaml index e2d3c3a7..4afe988b 100644 --- a/specs/policy/selectors.openapi.yaml +++ b/specs/policy/selectors.openapi.yaml @@ -4,6 +4,13 @@ info: paths: {} components: schemas: + policy.SortDirection: + type: string + title: SortDirection + enum: + - SORT_DIRECTION_UNSPECIFIED + - SORT_DIRECTION_ASC + - SORT_DIRECTION_DESC policy.AttributeDefinitionSelector: type: object properties: