diff --git a/sdk/internal/archive/tdf3_reader.go b/sdk/internal/archive/tdf3_reader.go index 56ffc8a482..77daf1ab20 100644 --- a/sdk/internal/archive/tdf3_reader.go +++ b/sdk/internal/archive/tdf3_reader.go @@ -5,7 +5,8 @@ import ( ) type TDFReader struct { - archiveReader Reader + archiveReader Reader + manifestMaxSize int64 } const ( @@ -14,22 +15,33 @@ const ( manifestMaxSize = 1024 * 1024 * 10 // 10 MB ) +type TDFReaderOptions func(*TDFReader) + +func WithTDFManifestMaxSize(size int64) TDFReaderOptions { + return func(tdfReader *TDFReader) { + tdfReader.manifestMaxSize = size + } +} + // NewTDFReader Create tdf reader instance. -func NewTDFReader(readSeeker io.ReadSeeker) (TDFReader, error) { +func NewTDFReader(readSeeker io.ReadSeeker, opt ...TDFReaderOptions) (TDFReader, error) { archiveReader, err := NewReader(readSeeker) if err != nil { return TDFReader{}, err } - tdfArchiveReader := TDFReader{} + tdfArchiveReader := TDFReader{manifestMaxSize: manifestMaxSize} tdfArchiveReader.archiveReader = archiveReader + for _, o := range opt { + o(&tdfArchiveReader) + } return tdfArchiveReader, nil } // Manifest Return the manifest of the tdf. func (tdfReader TDFReader) Manifest() (string, error) { - fileContent, err := tdfReader.archiveReader.ReadAllFileData(TDFManifestFileName, manifestMaxSize) + fileContent, err := tdfReader.archiveReader.ReadAllFileData(TDFManifestFileName, tdfReader.manifestMaxSize) if err != nil { return "", err } diff --git a/sdk/tdf.go b/sdk/tdf.go index 94feec1bf7..31428d1f33 100644 --- a/sdk/tdf.go +++ b/sdk/tdf.go @@ -758,8 +758,13 @@ func allowListFromKASRegistry(ctx context.Context, logger *slog.Logger, kasRegis // LoadTDF loads the tdf and prepare for reading the payload from TDF func (s SDK) LoadTDF(reader io.ReadSeeker, opts ...TDFReaderOption) (*Reader, error) { + config, err := newTDFReaderConfig(opts...) + if err != nil { + return nil, fmt.Errorf("newTDFReaderConfig failed: %w", err) + } + // create tdf reader - tdfReader, err := archive.NewTDFReader(reader) + tdfReader, err := archive.NewTDFReader(reader, archive.WithTDFManifestMaxSize(config.maxManifestSize)) if err != nil { return nil, fmt.Errorf("archive.NewTDFReader failed: %w", err) } @@ -768,11 +773,6 @@ func (s SDK) LoadTDF(reader io.ReadSeeker, opts ...TDFReaderOption) (*Reader, er opts = append([]TDFReaderOption{withSessionKey(s.kasSessionKey)}, opts...) } - config, err := newTDFReaderConfig(opts...) - if err != nil { - return nil, fmt.Errorf("newAssertionConfig failed: %w", err) - } - useGlobalFulfillableObligations := len(config.fulfillableObligationFQNs) == 0 && len(s.fulfillableObligationFQNs) > 0 if useGlobalFulfillableObligations { config.fulfillableObligationFQNs = s.fulfillableObligationFQNs diff --git a/sdk/tdf_config.go b/sdk/tdf_config.go index 081cf57e91..b77e85030c 100644 --- a/sdk/tdf_config.go +++ b/sdk/tdf_config.go @@ -12,14 +12,15 @@ import ( ) const ( - tdf3KeySize = 2048 - defaultSegmentSize = 2 * 1024 * 1024 // 2mb - maxSegmentSize = defaultSegmentSize * 2 - minSegmentSize = 16 * 1024 - DefaultRSAKeySize = 2048 - ECKeySize256 = 256 - ECKeySize384 = 384 - ECKeySize521 = 521 + tdf3KeySize = 2048 + defaultMaxManifestSize = 10 * 1024 * 1024 // 10 MB + defaultSegmentSize = 2 * 1024 * 1024 // 2mb + maxSegmentSize = defaultSegmentSize * 2 + minSegmentSize = 16 * 1024 + DefaultRSAKeySize = 2048 + ECKeySize256 = 256 + ECKeySize384 = 384 + ECKeySize521 = 521 ) type TDFFormat = int @@ -271,6 +272,7 @@ type TDFReaderConfig struct { kasAllowlist AllowList // KAS URLs that are allowed to be used for reading TDFs ignoreAllowList bool // If true, the kasAllowlist will be ignored, and all KAS URLs will be allowed fulfillableObligationFQNs []string + maxManifestSize int64 } type AllowList map[string]bool @@ -344,6 +346,7 @@ func (a AllowList) Add(kasURL string) error { func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { c := &TDFReaderConfig{ disableAssertionVerification: false, + maxManifestSize: defaultMaxManifestSize, } for _, o := range opt { @@ -364,6 +367,21 @@ func newTDFReaderConfig(opt ...TDFReaderOption) (*TDFReaderConfig, error) { return c, nil } +// WithMaxManifestSize sets the maximum allowed manifest size for the TDF reader. +// By default, the maximum manifest size is 10 MB. +// The manifest size is proportional to the sum of the sizes of the policy and the number of segments in the payload. +// Setting this limit helps prevent denial of service attacks due to large policies or overly segmented files. +// Use this option to override the default limit; the size parameter specifies the maximum size in bytes. +func WithMaxManifestSize(size int64) TDFReaderOption { + return func(c *TDFReaderConfig) error { + if size <= 0 { + return errors.New("max manifest size must be greater than 0") + } + c.maxManifestSize = size + return nil + } +} + func WithAssertionVerificationKeys(keys AssertionVerificationKeys) TDFReaderOption { return func(c *TDFReaderConfig) error { c.verifiers = keys diff --git a/sdk/tdf_test.go b/sdk/tdf_test.go index ddbc91f995..cd0c9178dd 100644 --- a/sdk/tdf_test.go +++ b/sdk/tdf_test.go @@ -42,6 +42,7 @@ import ( "github.com/opentdf/platform/protocol/go/policy/kasregistry/kasregistryconnect" wellknownpb "github.com/opentdf/platform/protocol/go/wellknownconfiguration" wellknownconnect "github.com/opentdf/platform/protocol/go/wellknownconfiguration/wellknownconfigurationconnect" + "github.com/opentdf/platform/sdk/internal/archive" "google.golang.org/grpc/codes" "google.golang.org/grpc/resolver" "google.golang.org/grpc/status" @@ -3100,3 +3101,54 @@ func TestGetKasErrorToReturn(t *testing.T) { require.Equal(t, defaultError, result) }) } + +func (s *TDFSuite) Test_LargeManifest_WithMaxManifest() { + const maxManifestSize = 1024 * 1024 // 1MB + + // Helper to create a large manifest JSON string + createLargeManifest := func(size int) []byte { + manifest := map[string]interface{}{ + "payload": map[string]interface{}{ + "data": string(bytes.Repeat([]byte{'a'}, size)), + }, + "tdf_spec_version": TDFSpecVersion, + } + b, err := json.Marshal(manifest) + s.Require().NoError(err) + return b + } + + // Helper to create a TDF file in memory for testing + createTestTDF := func(manifest []byte, payload []byte) *bytes.Reader { + tdfBuffer := new(bytes.Buffer) + tdfWriter := archive.NewTDFWriter(tdfBuffer) + + // Add payload + err := tdfWriter.SetPayloadSize(int64(len(payload))) + s.Require().NoError(err) + err = tdfWriter.AppendPayload(payload) + s.Require().NoError(err) + + // Add manifest + err = tdfWriter.AppendManifest(string(manifest)) + s.Require().NoError(err) + + _, err = tdfWriter.Finish() + s.Require().NoError(err) + + return bytes.NewReader(tdfBuffer.Bytes()) + } + + // Case 1: Manifest just below the limit + manifestBelow := createLargeManifest(maxManifestSize - 100) + tdfBelow := createTestTDF(manifestBelow, []byte("payload")) + _, err := s.sdk.LoadTDF(tdfBelow, WithMaxManifestSize(maxManifestSize)) + s.Require().NoError(err, "Manifest below max size should load successfully") + + // Case 2: Manifest just above the limit + manifestAbove := createLargeManifest(maxManifestSize + 100) + tdfAbove := createTestTDF(manifestAbove, []byte("payload")) + _, err = s.sdk.LoadTDF(tdfAbove, WithMaxManifestSize(maxManifestSize)) + s.Require().Error(err, "Manifest above max size should fail to load") + s.Require().ErrorContains(err, "size too large") +}