Skip to content

feat(config): add optional system assertions to TDFConfig #2316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions sdk/assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"errors"
"fmt"
"runtime"
"time"

"github.com/gowebpki/jcs"
"github.com/lestrrat-go/jwx/v2/jwa"
Expand Down Expand Up @@ -283,3 +285,37 @@ func (k AssertionVerificationKeys) Get(assertionID string) (AssertionKey, error)
func (k AssertionVerificationKeys) IsEmpty() bool {
return k.DefaultKey.IsEmpty() && len(k.Keys) == 0
}

// GetDefaultAssertionConfig returns a default assertion configuration with predefined values.
func GetDefaultAssertionConfig() AssertionConfig {
// Define the JSON structure
type Metadata struct {
TDFSpecVersion string `json:"TDFSpecVersion"`
CreationDate string `json:"creationDate"`
OS string `json:"OS"`
SDKLang string `json:"sdk"`
}

// Populate the metadata
metadata := Metadata{
TDFSpecVersion: TDFSpecVersion,
CreationDate: time.Now().Format(time.RFC3339),
Copy link
Member

Choose a reason for hiding this comment

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

Wait, do we not have a GetMetadata function? Which maybe also could error out...

OS: runtime.GOOS,
SDKLang: "Go",
}

// Marshal the metadata to JSON
metadataJSON, _ := json.Marshal(metadata)

return AssertionConfig{
ID: "default-assertion",
Type: BaseAssertion,
Scope: Paylaod,
AppliesToState: Unencrypted,
Statement: Statement{
Format: "json",
Schema: "metadata",
Value: string(metadataJSON),
},
}
Comment on lines +308 to +320
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 error returned by json.Marshal is currently being ignored. While marshalling the Metadata struct is unlikely to fail given its current simple structure (all string fields), it's a good practice to handle potential errors. If json.Marshal were to fail (e.g., due to future changes in the Metadata struct or unexpected system issues), metadataJSON would likely be nil. string(nil) results in an empty string "", which is not valid JSON. This could lead to an invalid assertion statement value being silently used.

Consider handling the error by, for example, logging it and defaulting to a valid empty JSON object string like "{}" for the Value field. This ensures the assertion remains structurally valid even if metadata generation fails.

	metadataBytes, err := json.Marshal(metadata)
	if err != nil {
		// It's crucial to log this error for visibility, as it indicates an unexpected issue
		// with marshalling a known-simple struct. This could point to deeper system problems
		// or future incompatibilities if the Metadata struct evolves.
		// e.g., log.Printf("Error marshalling default assertion metadata: %v", err) // Replace with actual logging mechanism
		// Defaulting to an empty JSON object to maintain a valid assertion structure.
		metadataBytes = []byte("{}")
	}

	return AssertionConfig{
		ID:             "default-assertion",
		Type:           BaseAssertion,
		Scope:          Paylaod, // This uses the existing constant `Paylaod`
		AppliesToState: Unencrypted,
		Statement: Statement{
			Format: "json",
			Schema: "metadata",
			Value:  string(metadataBytes),
		},
	}

Copy link
Member

Choose a reason for hiding this comment

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

Missing constexpr.

Yeah, if this fails, you should panic IMO.

MustMarshall was declined as an extension: golang/go#38519

}
4 changes: 4 additions & 0 deletions sdk/tdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ func (s SDK) CreateTDFContext(ctx context.Context, writer io.Writer, reader io.R
tdfObject.manifest.Payload.IsEncrypted = true

var signedAssertion []Assertion
if tdfConfig.addDefaultAssertion {
tdfConfig.assertions = append(tdfConfig.assertions, GetDefaultAssertionConfig())
}

for _, assertion := range tdfConfig.assertions {
// Store a temporary assertion
tmpAssertion := Assertion{}
Expand Down
10 changes: 10 additions & 0 deletions sdk/tdf_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type TDFConfig struct {
keyType ocrypto.KeyType
useHex bool
excludeVersionFromManifest bool
addDefaultAssertion bool
}

func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) {
Expand All @@ -90,6 +91,7 @@ func newTDFConfig(opt ...TDFOption) (*TDFConfig, error) {
integrityAlgorithm: HS256,
segmentIntegrityAlgorithm: GMAC,
keyType: ocrypto.RSA2048Key, // default to RSA
addDefaultAssertion: false,
}

for _, o := range opt {
Expand Down Expand Up @@ -217,6 +219,14 @@ func WithSegmentSize(size int64) TDFOption {
}
}

// WithDefaultAssertion returns an Option that adds a default assertion to the TDF.
func WithDefaultAssertion() TDFOption {
Copy link
Member

Choose a reason for hiding this comment

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

From the ticket,

  • This option must allow developers to provide a predefined set of assertions (key-value pairs or similar structure).
  • The mechanism should support adding common metadata types, such as:
    • SDK Version used for creation
    • Policy Enforcement Point (PEP) Name and Version (if applicable in the context)
    • TDF Creation Timestamp
    • Other relevant contextual information specific to the generating application (like DSP).

So we probably need to allow adding one or more, and also it should be a list or set

return func(c *TDFConfig) error {
c.addDefaultAssertion = true
return nil
}
}

// WithAssertions returns an Option that add assertions to TDF.
func WithAssertions(assertionList ...AssertionConfig) TDFOption {
return func(c *TDFConfig) error {
Expand Down
48 changes: 48 additions & 0 deletions sdk/tdf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -470,6 +471,53 @@ func (s *TDFSuite) Test_SimpleTDF() {
}
}

func (s *TDFSuite) Test_DefaultAssertions() {
// Configure TDF options with default assertions
tdfOptions := []TDFOption{
WithKasInformation(KASInfo{
URL: "https://a.kas/",
PublicKey: "",
}),
WithDefaultAssertion(),
}

// Create TDF
var buf bytes.Buffer
plainText := "Test Data"

inBuf := bytes.NewReader([]byte(plainText))
tdfObj, err := s.sdk.CreateTDF(&buf, inBuf, tdfOptions...)
s.Require().NoError(err)
s.Require().NotNil(tdfObj)

// Load TDF
r, err := s.sdk.LoadTDF(bytes.NewReader(buf.Bytes()))
s.Require().NoError(err)

// Verify default assertion
assertions := r.Manifest().Assertions
s.Require().NoError(err)
s.Require().NotEmpty(assertions)

found := false
for _, assertion := range assertions {
if assertion.ID == "default-assertion" { // Ensure `ID` exists
found = true

// Validate JSON in Statement.Value
var metadata map[string]interface{}
err := json.Unmarshal([]byte(assertion.Statement.Value), &metadata) // Ensure `Statement.Value` exists
s.Require().NoError(err, "Statement Value is not valid JSON")

// Check JSON fields
s.Equal(TDFSpecVersion, metadata["TDFSpecVersion"], "TDFSpecVersion mismatch")
s.Equal(runtime.GOOS, metadata["OS"], "OS mismatch")
s.Equal("Go", metadata["sdk"], "SDKLang mismatch")
}
}
s.True(found, "Default assertion not found")
}

func (s *TDFSuite) Test_TDF_KAS_Allowlist() {
type TestConfig struct {
tdfOptions []TDFOption
Expand Down
Loading