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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/fil-forge/delegator v0.0.0-20260619085531-c96197bec34c
github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390
github.com/fil-forge/go-ipni-tools v0.0.0-20260519194815-545b9421aec0
github.com/fil-forge/libforge v0.0.0-20260723212548-3e5e6ba95711
github.com/fil-forge/libforge v0.0.0-20260730113809-b13386bc395a
github.com/fil-forge/piri-signing-service v0.0.0-20260619151844-6c3d40a4bf54
github.com/fil-forge/ucantone v0.0.0-20260727203046-ccb77059de44
github.com/filecoin-project/curio v1.28.3-0.20260717015646-38ca280c43a2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,8 @@ github.com/fil-forge/go-ipni-tools v0.0.0-20260519194815-545b9421aec0 h1:HAfXUPv
github.com/fil-forge/go-ipni-tools v0.0.0-20260519194815-545b9421aec0/go.mod h1:3NRV/7wc4/0uzzrGdI7NoN/yeF1UvqKRwMyjBqGc5s0=
github.com/fil-forge/go-ucanto v0.0.0-20260507172450-5cb5d073f8ab h1:2J2cDThqTKP6/0k3SfdlSxfyPa3aLqjTYnmvbEcryfg=
github.com/fil-forge/go-ucanto v0.0.0-20260507172450-5cb5d073f8ab/go.mod h1:lZF3UXZ2hGLKYmXdquG50JqI9pRlUrV6lubGtgOYfwc=
github.com/fil-forge/libforge v0.0.0-20260723212548-3e5e6ba95711 h1:xO5gwfL3W2wqaLvwFUDj4MuWX9wHK9TGV8F3jFe/mZM=
github.com/fil-forge/libforge v0.0.0-20260723212548-3e5e6ba95711/go.mod h1:0kXihIQ4L2uZ00nR5XrZ/Y8Db7Ht/qQNuiWslwMJ95M=
github.com/fil-forge/libforge v0.0.0-20260730113809-b13386bc395a h1:voLQ76lKOQNRg7F+Aah2y0rN67jpDjFsiFMSpxE1Yf4=
github.com/fil-forge/libforge v0.0.0-20260730113809-b13386bc395a/go.mod h1:0kXihIQ4L2uZ00nR5XrZ/Y8Db7Ht/qQNuiWslwMJ95M=
github.com/fil-forge/piri-signing-service v0.0.0-20260619151844-6c3d40a4bf54 h1:KzQzCcEBBF6AfhZOwbQIPh5W2HwCgLS8VFTmch0oMgo=
github.com/fil-forge/piri-signing-service v0.0.0-20260619151844-6c3d40a4bf54/go.mod h1:O3GqUy+G7hb+oiGW6Wp77W2lRmZs7bewM9s/Vy4D6qo=
github.com/fil-forge/ucantone v0.0.0-20260727203046-ccb77059de44 h1:ofvb2Qq7++VPRelGsLbtnd1ZMKVT4n4QGoa79BhJ6VQ=
Expand Down
76 changes: 76 additions & 0 deletions pkg/ucanhandlers/blob/release.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package blob

import (
"bytes"
"context"
"fmt"

Expand All @@ -16,6 +17,8 @@ import (
"github.com/fil-forge/ucantone/did"
"github.com/fil-forge/ucantone/errors"
"github.com/fil-forge/ucantone/server"
"github.com/fil-forge/ucantone/ucan"
"github.com/fil-forge/ucantone/validator"

pdptypes "github.com/fil-forge/piri/pkg/pdp/types"
"github.com/fil-forge/piri/pkg/store"
Expand All @@ -30,6 +33,7 @@ import (
type ReleaseDeps struct {
fx.In
ID identity.Identity
Resolver did.Resolver
Allocations AllocationRemover
Acceptances AcceptanceRemover
ClaimStore invocationstore.InvocationStore
Expand Down Expand Up @@ -75,6 +79,13 @@ func NewBlobReleaseHandler(deps ReleaseDeps) server.Route {
return rsp.SetFailure(err)
}

// The release must be caused by a /blob/remove invocation rooted at
// the space — the node verifies it rather than trusting the upload
// service's word that the space asked for removal.
if err := validateReleaseCause(req.Context(), deps.Resolver, req, args); err != nil {
return rsp.SetFailure(err)
}

if err := Release(req.Context(), deps, &ReleaseRequest{
Space: args.Space,
Digest: args.Digest,
Expand All @@ -86,6 +97,71 @@ func NewBlobReleaseHandler(deps ReleaseDeps) server.Route {
})
}

// validateReleaseCause resolves the release's cause — the /blob/remove
// invocation linked by args.Cause — from the request container and verifies
// it justifies the release: it must be a /blob/remove task whose subject is
// args.Space and whose digest is args.Digest, carrying a valid proof chain
// rooted at the space. An envelope missing from the container or a
// non-/blob/remove task fails with UnknownCause; a subject/digest mismatch
// or a cause that fails UCAN validation fails with InvalidCause.
func validateReleaseCause(
ctx context.Context,
resolver did.Resolver,
req *binding.Request[*blob.ReleaseArguments],
args *blob.ReleaseArguments,
) error {
// args.Cause links the /blob/remove task, so match on the task link,
// not the invocation envelope link.
var cause ucan.Invocation
for _, inv := range req.Metadata().Invocations() {
if inv.Task().Link() == args.Cause {
cause = inv
break
}
}
if cause == nil {
return blob.ErrUnknownCause
}
if cause.Command() != blob.Remove.Command {
return blob.ErrUnknownCause
}

var removeArgs blob.RemoveArguments
if err := removeArgs.UnmarshalCBOR(bytes.NewReader(cause.ArgumentsBytes())); err != nil {
return errors.New(
blob.InvalidCauseErrorName,
"decoding %s args: %s", blob.Remove.Command, err,
)
}
if cause.Task().Subject() != args.Space {
return errors.New(
blob.InvalidCauseErrorName,
"cause subject %s is not space %s", cause.Task().Subject(), args.Space,
)
}
if !bytes.Equal(removeArgs.Digest, args.Digest) {
return errors.New(
blob.InvalidCauseErrorName,
"cause digest %s is not %s",
digestutil.Format(removeArgs.Digest), digestutil.Format(args.Digest),
)
}

// Re-validate the cause as a standalone UCAN invocation: signature,
// time bounds, and a proof chain rooted at the space. Proof envelopes
// ride in the request container alongside the cause.
if err := validator.ValidateInvocation(ctx, cause,
validator.WithProofResolver(validator.ProofsFromContainer(req.Metadata())),
validator.WithDIDResolver(resolver),
); err != nil {
return errors.New(
blob.InvalidCauseErrorName,
"cause not authorized: %s", err,
)
}
return nil
}

type ReleaseRequest struct {
Space did.DID
Digest multihash.Multihash
Expand Down
Loading