Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
34 changes: 21 additions & 13 deletions unixfs/authentication_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,25 +97,33 @@ func (r *verifiedReader) readTypedMetadata(ctx context.Context, root cid.Cid) (*
}
return verified, meta, nil
}
func (r *verifiedReader) readChunkMetadata(ctx context.Context, meta engine.Metadata) (unixfsmodel.ChunkMetadata, error) {
func (r *verifiedReader) readChunkMetadata(ctx context.Context, meta engine.Metadata, body []byte) (unixfsmodel.ChunkMetadata, []byte, error) {
if !meta.PayloadCID.Defined() {
return unixfsmodel.ChunkMetadata{}, fmt.Errorf("missing file chunk metadata")
return unixfsmodel.ChunkMetadata{}, nil, fmt.Errorf("missing file chunk metadata")
}
hash, err := mh.Decode(meta.PayloadCID.Hash())
if err != nil {
return unixfsmodel.ChunkMetadata{}, err
}
body := hash.Digest
if hash.Code != mh.IDENTITY {
body, err = r.getBoundBlock(ctx, meta.PayloadCID)
if body == nil {
hash, err := mh.Decode(meta.PayloadCID.Hash())
if err != nil {
return unixfsmodel.ChunkMetadata{}, err
return unixfsmodel.ChunkMetadata{}, nil, err
}
body = hash.Digest
if hash.Code != mh.IDENTITY {
body, err = r.getBoundBlock(ctx, meta.PayloadCID)
if err != nil {
return unixfsmodel.ChunkMetadata{}, nil, err
}
}
}
// Retained bytes must still match the authenticated CID and count; decoded
// fields from a prior Stat are not authoritative.
geometry, err := unixfsmodel.ParseChunkMetadata(meta.PayloadCID, body, meta.Count)
if err != nil {
return unixfsmodel.ChunkMetadata{}, nil, err
}
return unixfsmodel.ParseChunkMetadata(meta.PayloadCID, body, meta.Count)
return geometry, body, nil
}

func (r *verifiedReader) readTypedRange(ctx context.Context, root cid.Cid, start uint64, length *uint64, metadata *protocol.AuthenticationVerification) (*ReadResult, error) {
func (r *verifiedReader) readTypedRange(ctx context.Context, root cid.Cid, start uint64, length *uint64, metadata *protocol.AuthenticationVerification, metadataBody []byte) (*ReadResult, error) {
var meta engine.Metadata
var err error
if metadata == nil {
Expand All @@ -137,7 +145,7 @@ func (r *verifiedReader) readTypedRange(ctx context.Context, root cid.Cid, start
if err != nil {
return nil, err
}
geometry, err := r.readChunkMetadata(ctx, meta)
geometry, _, err := r.readChunkMetadata(ctx, meta, metadataBody)
if err != nil {
return nil, err
}
Expand Down
53 changes: 53 additions & 0 deletions unixfs/authentication_reader_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package unixfs

import (
"bytes"
"strings"
"testing"

unixfsmodel "github.com/dewebprotocol/malt-client/unixfs/model"
"github.com/dewebprotocol/malt-core/engine"
cid "github.com/ipfs/go-cid"
mh "github.com/multiformats/go-multihash"
)

func TestReadChunkMetadataRevalidatesRetainedBytes(t *testing.T) {
body, err := unixfsmodel.EncodeChunkMetadata(128, 16, 8)
if err != nil {
t.Fatal(err)
}
key, err := (cid.Prefix{Version: 1, Codec: cid.Raw, MhType: mh.SHA2_256, MhLength: -1}).Sum(body)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
body []byte
count uint64
wantErr string
}{
{name: "valid", body: body, count: 8},
{name: "changed-bytes", body: bytes.Replace(body, []byte(`"128"`), []byte(`"127"`), 1), count: 8, wantErr: "payload CID"},
{name: "changed-count", body: body, count: 9, wantErr: "authenticated count"},
{name: "empty-bytes", body: []byte{}, count: 8, wantErr: "payload CID"},
} {
t.Run(tc.name, func(t *testing.T) {
// Reuse must work without any CAS getter, including when retained
// evidence is invalid: reject it instead of fetching a replacement.
reader := &verifiedReader{}
geometry, retained, err := reader.readChunkMetadata(t.Context(), engine.Metadata{PayloadCID: key, Count: tc.count}, tc.body)
if tc.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("retained metadata error = %v, want %q", err, tc.wantErr)
}
return
}
if err != nil {
t.Fatal(err)
}
if geometry.TotalSize != 128 || geometry.ChunkSize != 16 || !bytes.Equal(retained, body) {
t.Fatalf("retained metadata changed: %#v, %q", geometry, retained)
}
})
}
}
8 changes: 5 additions & 3 deletions unixfs/verified_facade.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type Stat struct {
PayloadBinding *Resolution `json:"payload_binding,omitempty"`
rawBody []byte
rawBodyLoaded bool
chunkMetadataBody []byte
}

// ReadResult contains only bytes that have been bound to locally verified
Expand Down Expand Up @@ -438,13 +439,14 @@ func (r *verifiedReader) Stat(ctx context.Context, trustedRoot cid.Cid, rawPath
if err != nil {
return nil, err
}
geometry, err := r.readChunkMetadata(ctx, meta)
geometry, body, err := r.readChunkMetadata(ctx, meta, nil)
if err != nil {
return nil, err
}
stat.Size = geometry.TotalSize
stat.ChunkSize = geometry.ChunkSize
stat.AuthenticationMetadata = proof
stat.chunkMetadataBody = body
case "raw":
body, err := r.getBoundBlock(ctx, stat.Payload)
if err != nil {
Expand Down Expand Up @@ -595,7 +597,7 @@ func (r *verifiedReader) readStatFile(ctx context.Context, stat *Stat, offset ui
}
switch stat.PayloadKind {
case "positional":
result, err := r.readTypedRange(ctx, stat.Payload, offset, length, stat.AuthenticationMetadata)
result, err := r.readTypedRange(ctx, stat.Payload, offset, length, stat.AuthenticationMetadata, stat.chunkMetadataBody)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -643,7 +645,7 @@ func (r *verifiedReader) ReadPositionalPayloadRange(ctx context.Context, trusted
if parseErr != nil || descriptor.Layout != maltcid.Positional {
return nil, fmt.Errorf("%w: target %s is not a positional Root", ErrNotFile, trustedPositionalRoot)
}
return r.readTypedRange(ctx, trustedPositionalRoot, offset, &length, nil)
return r.readTypedRange(ctx, trustedPositionalRoot, offset, &length, nil, nil)
}

func (r *verifiedReader) getBoundBlock(ctx context.Context, key cid.Cid) ([]byte, error) {
Expand Down
55 changes: 55 additions & 0 deletions unixfs/verified_facade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,61 @@ func TestVerifiedReaderRejectsUntypedDirectoryManifests(t *testing.T) {
}
}

func TestVerifiedReaderFetchesChunkMetadataOnce(t *testing.T) {
remote := newRealRemote(t)
body := bytes.Repeat([]byte("0123456789abcdef"), 8)
root := materializeTree(t, remote, map[string][]byte{"file.bin": body}, 16)
reader, err := unixfs.NewReader(unixfs.ReaderOptions{Remote: remote, Blocks: remote})
if err != nil {
t.Fatal(err)
}
stat, err := reader.Stat(t.Context(), root, "file.bin")
if err != nil {
t.Fatal(err)
}
metadata, err := stat.AuthenticationMetadata.Result.Binding.Proof.RootMetadata()
if err != nil {
t.Fatal(err)
}

for _, tc := range []struct {
name string
full bool
offset uint64
length uint64
want []byte
}{
{name: "full", full: true, want: body},
{name: "one-byte", length: 1, want: body[:1]},
{name: "cross-chunk", offset: 15, length: 18, want: body[15:33]},
{name: "empty-range", offset: 15},
{name: "past-end", offset: uint64(len(body)) + 1, length: 1},
} {
t.Run(tc.name, func(t *testing.T) {
blocks := &countingBlocks{inner: remote, gets: make(map[string]int)}
reader, err := unixfs.NewReader(unixfs.ReaderOptions{Remote: remote, Blocks: blocks})
if err != nil {
t.Fatal(err)
}
var result *unixfs.ReadResult
if tc.full {
result, err = reader.ReadFile(t.Context(), root, "file.bin")
} else {
result, err = reader.ReadFileRange(t.Context(), root, "file.bin", tc.offset, tc.length)
}
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(result.Body, tc.want) || result.TotalSize != uint64(len(body)) || result.ChunkSize != 16 {
t.Fatalf("read differs from authenticated file: %#v", result)
}
if got := blocks.gets[metadata.PayloadCID.KeyString()]; got != 1 {
t.Fatalf("chunk metadata fetched %d times, want 1", got)
}
})
}
}

func TestVerifiedReaderFetchesEachAuthenticatedRangeBlockOnce(t *testing.T) {
remote := newRealRemote(t)
chunk := []byte("0123456789abcdef")
Expand Down
Loading