Skip to content

Commit ea9b278

Browse files
feat(sdk): DSPX-1465 refactor TDF architecture with streaming support and segment-based writing (#2785)
### Proposed Changes * introduces an experimental package for streaming TDF creation * adds new packages for TDF writers, assertions, key splitting, and related functionality * allows for out-of-order segment writing and includes features like cryptographic assertions and attribute-based access controls ### 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
1 parent f2678cc commit ea9b278

26 files changed

+8529
-0
lines changed

sdk/experimental/tdf/assertion.go

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
// Experimental: This package is EXPERIMENTAL and may change or be removed at any time
2+
3+
package tdf
4+
5+
import (
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
10+
"github.com/gowebpki/jcs"
11+
"github.com/lestrrat-go/jwx/v2/jwa"
12+
"github.com/lestrrat-go/jwx/v2/jwt"
13+
"github.com/opentdf/platform/lib/ocrypto"
14+
)
15+
16+
const (
17+
// SystemMetadataAssertionID is the standard ID for system metadata assertions
18+
SystemMetadataAssertionID = "system-metadata"
19+
// SystemMetadataSchemaV1 defines the schema version for system metadata
20+
SystemMetadataSchemaV1 = "system-metadata-v1"
21+
// kAssertionSignature is the JWT claim key for assertion signatures
22+
kAssertionSignature = "assertionSig"
23+
// kAssertionHash is the JWT claim key for assertion hashes
24+
kAssertionHash = "assertionHash"
25+
)
26+
27+
// AssertionConfig defines an assertion to be included in the TDF during creation.
28+
//
29+
// AssertionConfig extends Assertion with a signing key, enabling creation
30+
// of cryptographically signed assertions. The signing key is used during
31+
// TDF creation but is not stored in the final TDF.
32+
//
33+
// Required fields:
34+
// - ID: Unique identifier for the assertion
35+
// - Type: The kind of assertion (BaseAssertion, HandlingAssertion)
36+
// - Scope: What the assertion applies to (PayloadScope, TrustedDataObjScope)
37+
// - AppliesToState: When the assertion is relevant (Encrypted, Unencrypted)
38+
// - Statement: The assertion content and metadata
39+
//
40+
// Optional fields:
41+
// - SigningKey: Custom signing key (defaults to DEK with HS256)
42+
//
43+
// Example:
44+
//
45+
// assertion := AssertionConfig{
46+
// ID: "retention-policy",
47+
// Type: HandlingAssertion,
48+
// Scope: PayloadScope,
49+
// AppliesToState: Unencrypted,
50+
// Statement: Statement{
51+
// Format: "json",
52+
// Schema: "retention-v1",
53+
// Value: `{"retain_days": 90, "auto_delete": true}`,
54+
// },
55+
// }
56+
type AssertionConfig struct {
57+
ID string `validate:"required"`
58+
Type AssertionType `validate:"required"`
59+
Scope Scope `validate:"required"`
60+
AppliesToState AppliesToState `validate:"required"`
61+
Statement Statement
62+
SigningKey AssertionKey
63+
}
64+
65+
// Assertion represents a cryptographically signed assertion in the TDF manifest.
66+
//
67+
// Assertions provide integrity verification and handling instructions that are
68+
// cryptographically bound to the TDF. They cannot be modified or copied to
69+
// another TDF without detection due to the cryptographic binding.
70+
//
71+
// The assertion structure includes:
72+
// - Metadata: ID, type, scope, and state applicability
73+
// - Statement: The actual assertion content in structured format
74+
// - Binding: Cryptographic signature ensuring integrity
75+
//
76+
// Assertions are verified during TDF reading to ensure they haven't been
77+
// tampered with since TDF creation.
78+
type Assertion struct {
79+
ID string `json:"id"`
80+
Type AssertionType `json:"type"`
81+
Scope Scope `json:"scope"`
82+
AppliesToState AppliesToState `json:"appliesToState,omitempty"`
83+
Statement Statement `json:"statement"`
84+
Binding Binding `json:"binding,omitempty"`
85+
}
86+
87+
var errAssertionVerifyKeyFailure = errors.New("assertion: failed to verify with provided key")
88+
89+
// Sign signs the assertion with the given hash and signature using the key.
90+
// It returns an error if the signing fails.
91+
// The assertion binding is updated with the method and the signature.
92+
func (a *Assertion) Sign(hash, sig string, key AssertionKey) error {
93+
tok := jwt.New()
94+
if err := tok.Set(kAssertionHash, hash); err != nil {
95+
return fmt.Errorf("failed to set assertion hash: %w", err)
96+
}
97+
if err := tok.Set(kAssertionSignature, sig); err != nil {
98+
return fmt.Errorf("failed to set assertion signature: %w", err)
99+
}
100+
101+
// sign the hash and signature
102+
signedTok, err := jwt.Sign(tok, jwt.WithKey(jwa.KeyAlgorithmFrom(key.Alg.String()), key.Key))
103+
if err != nil {
104+
return fmt.Errorf("signing assertion failed: %w", err)
105+
}
106+
107+
// set the binding
108+
a.Binding.Method = JWS.String()
109+
a.Binding.Signature = string(signedTok)
110+
111+
return nil
112+
}
113+
114+
// Verify checks the binding signature of the assertion and
115+
// returns the hash and the signature. It returns an error if the verification fails.
116+
func (a Assertion) Verify(key AssertionKey) (string, string, error) {
117+
tok, err := jwt.Parse([]byte(a.Binding.Signature),
118+
jwt.WithKey(jwa.KeyAlgorithmFrom(key.Alg.String()), key.Key),
119+
)
120+
if err != nil {
121+
return "", "", fmt.Errorf("%w: %w", errAssertionVerifyKeyFailure, err)
122+
}
123+
hashClaim, found := tok.Get(kAssertionHash)
124+
if !found {
125+
return "", "", errors.New("hash claim not found")
126+
}
127+
hash, ok := hashClaim.(string)
128+
if !ok {
129+
return "", "", errors.New("hash claim is not a string")
130+
}
131+
132+
sigClaim, found := tok.Get(kAssertionSignature)
133+
if !found {
134+
return "", "", errors.New("signature claim not found")
135+
}
136+
sig, ok := sigClaim.(string)
137+
if !ok {
138+
return "", "", errors.New("signature claim is not a string")
139+
}
140+
return hash, sig, nil
141+
}
142+
143+
// GetHash returns the hash of the assertion in hex format.
144+
func (a Assertion) GetHash() ([]byte, error) {
145+
// Clear out the binding
146+
a.Binding = Binding{}
147+
148+
// Marshal the assertion to JSON
149+
assertionJSON, err := json.Marshal(a)
150+
if err != nil {
151+
return nil, fmt.Errorf("json.Marshal failed: %w", err)
152+
}
153+
154+
// Unmarshal the JSON into a map to manipulate it
155+
var jsonObject map[string]interface{}
156+
if err := json.Unmarshal(assertionJSON, &jsonObject); err != nil {
157+
return nil, fmt.Errorf("json.Unmarshal failed: %w", err)
158+
}
159+
160+
// Remove the binding key
161+
delete(jsonObject, "binding")
162+
163+
// Marshal the map back to JSON
164+
assertionJSON, err = json.Marshal(jsonObject)
165+
if err != nil {
166+
return nil, fmt.Errorf("json.Marshal failed: %w", err)
167+
}
168+
169+
// Transform the JSON using JCS
170+
transformedJSON, err := jcs.Transform(assertionJSON)
171+
if err != nil {
172+
return nil, fmt.Errorf("jcs.Transform failed: %w", err)
173+
}
174+
175+
return ocrypto.SHA256AsHex(transformedJSON), nil
176+
}
177+
178+
func (s *Statement) UnmarshalJSON(data []byte) error {
179+
// Define a custom struct for deserialization
180+
type Alias Statement
181+
aux := &struct {
182+
Value json.RawMessage `json:"value,omitempty"`
183+
*Alias
184+
}{
185+
Alias: (*Alias)(s),
186+
}
187+
188+
if err := json.Unmarshal(data, &aux); err != nil {
189+
return err
190+
}
191+
192+
// Attempt to decode Value as an object
193+
var temp map[string]interface{}
194+
if json.Unmarshal(aux.Value, &temp) == nil {
195+
// Re-encode the object as a string and assign to Value
196+
objAsString, err := json.Marshal(temp)
197+
if err != nil {
198+
return err
199+
}
200+
s.Value = string(objAsString)
201+
} else {
202+
// Assign raw string to Value
203+
var str string
204+
if err := json.Unmarshal(aux.Value, &str); err != nil {
205+
return fmt.Errorf("value is neither a valid JSON object nor a string: %s", string(aux.Value))
206+
}
207+
s.Value = str
208+
}
209+
210+
return nil
211+
}
212+
213+
// Statement includes information applying to the scope of the assertion.
214+
// It could contain rights, handling instructions, or general metadata.
215+
type Statement struct {
216+
// Format describes the payload encoding format. (e.g. json)
217+
Format string `json:"format,omitempty" validate:"required"`
218+
// Schema describes the schema of the payload. (e.g. tdf)
219+
Schema string `json:"schema,omitempty" validate:"required"`
220+
// Value is the payload of the assertion.
221+
Value string `json:"value,omitempty" validate:"required"`
222+
}
223+
224+
// Binding enforces cryptographic integrity of the assertion.
225+
// So the can't be modified or copied to another tdf.
226+
type Binding struct {
227+
// Method used to bind the assertion. (e.g. jws)
228+
Method string `json:"method,omitempty"`
229+
// Signature of the assertion.
230+
Signature string `json:"signature,omitempty"`
231+
}
232+
233+
// AssertionType represents the category of assertion being made.
234+
//
235+
// Different assertion types serve different purposes in TDF handling:
236+
// - HandlingAssertion: Instructions for data processing, retention, deletion
237+
// - BaseAssertion: General-purpose assertions including metadata, audit info
238+
type AssertionType string
239+
240+
const (
241+
// HandlingAssertion provides instructions for data handling and processing.
242+
// Examples: retention policies, deletion schedules, processing requirements
243+
HandlingAssertion AssertionType = "handling"
244+
// BaseAssertion is a general-purpose assertion type for metadata and other content.
245+
// Examples: audit information, system metadata, custom business logic
246+
BaseAssertion AssertionType = "other"
247+
)
248+
249+
// String returns the string representation of the assertion type.
250+
func (at AssertionType) String() string {
251+
return string(at)
252+
}
253+
254+
// Scope defines what component of the TDF the assertion applies to.
255+
//
256+
// Scope determines which part of the TDF structure the assertion governs:
257+
// - TrustedDataObjScope: Assertion applies to the entire TDF object
258+
// - PayloadScope: Assertion applies only to the encrypted payload data
259+
type Scope string
260+
261+
const (
262+
// TrustedDataObjScope indicates the assertion applies to the complete TDF object.
263+
// This includes manifest, key access objects, and payload.
264+
TrustedDataObjScope Scope = "tdo"
265+
// PayloadScope indicates the assertion applies only to the payload data.
266+
// This is the most common scope for data handling assertions.
267+
PayloadScope Scope = "payload"
268+
)
269+
270+
// String returns the string representation of the scope.
271+
func (s Scope) String() string {
272+
return string(s)
273+
}
274+
275+
// AppliesToState indicates when the assertion is relevant in the TDF lifecycle.
276+
//
277+
// This determines whether the assertion should be processed before or after
278+
// decryption, enabling different handling patterns:
279+
// - Encrypted: Process before decryption (e.g., access logging)
280+
// - Unencrypted: Process after decryption (e.g., content filtering)
281+
type AppliesToState string
282+
283+
const (
284+
// Encrypted means the assertion should be processed before payload decryption.
285+
// Used for access control, audit logging, and pre-processing requirements.
286+
Encrypted AppliesToState = "encrypted"
287+
// Unencrypted means the assertion should be processed after payload decryption.
288+
// Used for content analysis, post-processing, and data handling requirements.
289+
Unencrypted AppliesToState = "unencrypted"
290+
)
291+
292+
// String returns the string representation of the applies to state.
293+
func (ats AppliesToState) String() string {
294+
return string(ats)
295+
}
296+
297+
// BindingMethod represents the cryptographic method used to bind assertions to the TDF.
298+
//
299+
// The binding method ensures assertions cannot be modified or transferred
300+
// to other TDFs without detection.
301+
type BindingMethod string
302+
303+
const (
304+
// JWS (JSON Web Signature) is the standard method for assertion binding.
305+
// Uses JWT-based cryptographic signatures for tamper detection.
306+
JWS BindingMethod = "jws"
307+
)
308+
309+
// String returns the string representation of the binding method.
310+
func (bm BindingMethod) String() string {
311+
return string(bm)
312+
}
313+
314+
// AssertionKeyAlg represents the cryptographic algorithm for assertion signing keys.
315+
//
316+
// Different algorithms provide different security and compatibility characteristics:
317+
// - RS256: RSA-based signatures, widely supported, good for public key scenarios
318+
// - HS256: HMAC-based signatures, simpler, good for shared key scenarios
319+
type AssertionKeyAlg string
320+
321+
const (
322+
// AssertionKeyAlgRS256 uses RSA-SHA256 for assertion signatures.
323+
// Suitable when assertions need to be verified by parties without access to signing keys.
324+
AssertionKeyAlgRS256 AssertionKeyAlg = "RS256"
325+
// AssertionKeyAlgHS256 uses HMAC-SHA256 for assertion signatures.
326+
// More efficient, suitable when the same key used for TDF encryption can sign assertions.
327+
AssertionKeyAlgHS256 AssertionKeyAlg = "HS256"
328+
)
329+
330+
// String returns the string representation of the algorithm.
331+
func (a AssertionKeyAlg) String() string {
332+
return string(a)
333+
}
334+
335+
// AssertionKey represents a cryptographic key for signing and verifying assertions.
336+
//
337+
// The key can be either RSA or HMAC-based depending on the algorithm:
338+
// - RS256: Key should be an RSA private key (*rsa.PrivateKey or jwk.Key)
339+
// - HS256: Key should be a byte slice containing the shared secret
340+
//
341+
// Example usage:
342+
//
343+
// // HMAC key using TDF's Data Encryption Key
344+
// hmacKey := AssertionKey{
345+
// Alg: AssertionKeyAlgHS256,
346+
// Key: dek, // 32-byte AES key
347+
// }
348+
//
349+
// // RSA key for public key scenarios
350+
// rsaKey := AssertionKey{
351+
// Alg: AssertionKeyAlgRS256,
352+
// Key: privateKey, // *rsa.PrivateKey
353+
// }
354+
type AssertionKey struct {
355+
// Alg specifies the cryptographic algorithm for this key
356+
Alg AssertionKeyAlg
357+
// Key contains the actual key material (type depends on algorithm)
358+
Key interface{}
359+
}
360+
361+
// Algorithm returns the cryptographic algorithm of the key.
362+
func (k AssertionKey) Algorithm() AssertionKeyAlg {
363+
return k.Alg
364+
}
365+
366+
// IsEmpty returns true if the key has no algorithm or key material configured.
367+
// Used to check if a default signing key should be used instead.
368+
func (k AssertionKey) IsEmpty() bool {
369+
return k.Key == nil && k.Alg == ""
370+
}
371+
372+
// AssertionVerificationKeys represents the verification keys for assertions.
373+
type AssertionVerificationKeys struct {
374+
// Default key to use if the key for the assertion ID is not found.
375+
DefaultKey AssertionKey
376+
// Map of assertion ID to key.
377+
Keys map[string]AssertionKey
378+
}
379+
380+
// Get returns the key for the given assertion ID or the default key if the key is not found.
381+
// If the default key is not set, it returns an empty key.
382+
func (k AssertionVerificationKeys) Get(assertionID string) (AssertionKey, error) {
383+
if key, ok := k.Keys[assertionID]; ok {
384+
return key, nil
385+
}
386+
if k.DefaultKey.IsEmpty() {
387+
return AssertionKey{}, nil
388+
}
389+
return k.DefaultKey, nil
390+
}
391+
392+
// IsEmpty returns true if the default key and the keys map are empty.
393+
func (k AssertionVerificationKeys) IsEmpty() bool {
394+
return k.DefaultKey.IsEmpty() && len(k.Keys) == 0
395+
}

0 commit comments

Comments
 (0)