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
16 changes: 15 additions & 1 deletion cmd/relay/access_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ func checkStorageObjects(config access.StorageConfig, clientFor func(store.Clien
if got, err := os.ReadFile(authenticated); err != nil || !bytes.Equal(got, payload) {
return errors.New("authenticated published read returned different probe bytes")
}
versioned := filepath.Join(dir, "published-versioned")
if _, err := published.GetVersionedAtMost(key, versioned, int64(len(payload))); err != nil {
return fmt.Errorf("published bucket version-pinned read probe: %w", err)
}
if got, err := os.ReadFile(versioned); err != nil || !bytes.Equal(got, payload) {
return errors.New("version-pinned published read returned different probe bytes")
}
public := clientFor(store.Client{PublicBaseURL: config.PublishedBaseURL})
var publicErr error
for attemptNumber := 0; attemptNumber < 5; attemptNumber++ {
Expand Down Expand Up @@ -185,6 +192,13 @@ func checkStorageObjects(config access.StorageConfig, clientFor func(store.Clien
}
}
}()
versioned = filepath.Join(dir, "inbox-versioned")
if _, err := inbox.GetVersionedAtMost(key, versioned, int64(len(payload))); err != nil {
return fmt.Errorf("inbox bucket version-pinned read probe: %w", err)
}
if got, err := os.ReadFile(versioned); err != nil || !bytes.Equal(got, payload) {
return errors.New("version-pinned inbox read returned different probe bytes")
}
if config.Provider != "r2" {
unsigned := clientFor(store.Client{Endpoint: config.Endpoint, Region: config.Region, Bucket: config.InboxBucket, NoSign: true})
publiclyVisible, headErr := unsigned.Head(key)
Expand Down Expand Up @@ -624,7 +638,7 @@ func issueAWSWithRunner(config access.StorageConfig, identity, prefix string, tt
"Version": "2012-10-17",
"Statement": []map[string]any{{
"Effect": "Allow",
"Action": []string{"s3:PutObject", "s3:GetObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"},
"Action": []string{"s3:PutObject", "s3:GetObject", "s3:GetObjectVersion", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"},
"Resource": objectARN,
}},
})
Expand Down
12 changes: 12 additions & 0 deletions cmd/relay/aws_grant_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func TestAWSLiveGrantScopeAndExpiry(t *testing.T) {
t.Fatal(err)
}
requireAWSLiveConfiguration(t, config)
if os.Getenv("RELAY_AWS_LIVE_CREDENTIALS_FILE") == "" {
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", freshAWSLiveCredentials(t))
} else {
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", os.Getenv("RELAY_AWS_LIVE_CREDENTIALS_FILE"))
}
id, err := randomID()
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -69,6 +74,9 @@ func TestAWSLiveGrantScopeAndExpiry(t *testing.T) {
if err := scoped.Get(base+"allowed/probe", allowed); err != nil {
t.Fatal("allowed read failed")
}
if _, err := scoped.GetVersionedAtMost(base+"allowed/probe", filepath.Join(dir, "allowed-versioned"), int64(len(data))); err != nil {
t.Fatalf("allowed version-pinned read failed: %v", err)
}
got, err := os.ReadFile(allowed)
if err != nil || !bytes.Equal(got, data) {
t.Fatal("allowed bytes differ")
Expand All @@ -90,6 +98,10 @@ func TestAWSLiveGrantScopeAndExpiry(t *testing.T) {
t.Fatalf("outside write inconclusive; inspect %s/%s", cleaner.Bucket, key)
}
}
if os.Getenv("RELAY_AWS_LIVE_SCOPE_ONLY") == "1" {
t.Log("scoped grant and version-pinned read passed; expiry was intentionally not tested")
return
}
t.Logf("Allowed read/write and outside-prefix/other-bucket denial passed; waiting until %s", expires.Add(5*time.Second).UTC().Format(time.RFC3339))
for time.Now().Before(expires.Add(5 * time.Second)) {
remaining := time.Until(expires.Add(5 * time.Second))
Expand Down
7 changes: 6 additions & 1 deletion cmd/relay/aws_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ func TestAWSLiveIsolatedStoragePreflight(t *testing.T) {
if _, err := settings.infrastructure(); err != nil {
t.Fatal(err)
}
credentials := require("RELAY_AWS_LIVE_CREDENTIALS_FILE")
credentials := os.Getenv("RELAY_AWS_LIVE_CREDENTIALS_FILE")
if credentials == "" {
// The isolated login wrapper can refresh a short-lived, protected test
// snapshot without exporting secret bytes through a shell or test log.
credentials = freshAWSLiveCredentials(t)
}
if _, err := readProtectedCredentialBytes(credentials, 1<<20); err != nil {
t.Fatal("dedicated AWS credentials file unavailable or unsafe")
}
Expand Down
17 changes: 15 additions & 2 deletions cmd/relay/aws_storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestAWSGrantRequestsOnlyIntendedInboxPrefix(t *testing.T) {
if len(policy.Statement) != 1 || policy.Statement[0].Effect != "Allow" || policy.Statement[0].Resource != "arn:aws:s3:::private-fixture/setup-probes/test/allowed/*" {
t.Fatal("session policy escaped intended prefix")
}
if strings.Join(policy.Statement[0].Action, ",") != "s3:PutObject,s3:GetObject,s3:AbortMultipartUpload,s3:ListMultipartUploadParts" {
if strings.Join(policy.Statement[0].Action, ",") != "s3:PutObject,s3:GetObject,s3:GetObjectVersion,s3:AbortMultipartUpload,s3:ListMultipartUploadParts" {
t.Fatal("unexpected session permissions")
}
return json.Marshal(map[string]any{"Credentials": map[string]string{"AccessKeyId": "test-access", "SecretAccessKey": "test-secret", "SessionToken": "test-session", "Expiration": expires.Format(time.RFC3339)}})
Expand All @@ -61,7 +61,7 @@ func TestAWSGrantRequestsOnlyIntendedInboxPrefix(t *testing.T) {
}

func TestAWSStoragePreflightAndFailureCleanup(t *testing.T) {
for _, fault := range []string{"", "public-inbox", "inbox-not-found", "inbox-network-error", "authenticated-corruption", "public-corruption", "public-unavailable", "inbox-write-denied", "delete-denied", "ambiguous-write", "collision"} {
for _, fault := range []string{"", "public-inbox", "inbox-not-found", "inbox-network-error", "authenticated-corruption", "public-corruption", "public-unavailable", "version-denied", "inbox-write-denied", "delete-denied", "ambiguous-write", "collision"} {
t.Run(fault, func(t *testing.T) {
config, err := storageSettingsFixture().infrastructure()
if err != nil {
Expand Down Expand Up @@ -107,6 +107,19 @@ func TestAWSStoragePreflightAndFailureCleanup(t *testing.T) {
}
return os.WriteFile(file, raw, 0600)
},
GetVersionedAtMost: func(key, file string, maximum int64) (store.ObjectVersion, error) {
if fault == "version-denied" {
return store.ObjectVersion{}, errors.New("AccessDenied: s3:GetObjectVersion")
}
raw, ok := objects[bucket+"/"+key]
if !ok || int64(len(raw)) > maximum {
return store.ObjectVersion{}, errors.New("missing or oversized probe")
}
if err := os.WriteFile(file, raw, 0600); err != nil {
return store.ObjectVersion{}, err
}
return store.ObjectVersion{ETag: "probe", Size: int64(len(raw))}, nil
},
Head: func(key string) (bool, error) {
if !c.NoSign || bucket != config.InboxBucket {
t.Fatal("privacy check not anonymous or wrong bucket")
Expand Down
102 changes: 102 additions & 0 deletions cmd/relay/docker_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@ func (d *dockerDriver) rewriteReadOnlyArgs(args []string) ([]string, []dockerMou
}

func (d *dockerDriver) rewriteArgs(args []string, writable map[string]string) ([]string, []dockerMount, error) {
artifactRoot, err := d.artifactRootMount(args)
if err != nil {
return nil, nil, err
}
exact := map[string]string{
d.definition: "/relay/trust/ceremony.json", d.definitionSig: "/relay/trust/ceremony.sig",
d.coordinatorKey: "/relay/trust/coordinator.hex", d.signingKey: "/relay/key/participant.key",
Expand All @@ -748,13 +752,26 @@ func (d *dockerDriver) rewriteArgs(args []string, writable map[string]string) ([
}
rewritten := append([]string(nil), args...)
mountBySource := make(map[string]dockerMount)
if artifactRoot.source != "" {
mountBySource[artifactRoot.source] = dockerMount{Source: artifactRoot.source, Destination: artifactRoot.destination, ReadOnly: true}
}
for i, arg := range rewritten {
if mapped, ok := exact[arg]; ok && arg != "" {
rewritten[i] = mapped
mountBySource[arg] = dockerMount{Source: arg, Destination: mapped, ReadOnly: writable[arg] == ""}
continue
}
if filepath.IsAbs(arg) {
if artifactRoot.source != "" {
mapped, contained, pathErr := artifactRoot.child(arg)
if pathErr != nil {
return nil, nil, pathErr
}
if contained {
rewritten[i] = mapped
continue
}
}
mapped, err := pathWithin(d.root, arg, "/relay/input")
if err != nil {
if d.inspectionRoot == "" || arg == d.inspectionRoot {
Expand Down Expand Up @@ -787,6 +804,91 @@ func (d *dockerDriver) rewriteArgs(args []string, writable map[string]string) ([
return rewritten, mounts, nil
}

type dockerArtifactRootMount struct {
root string // caller spelling, used for lexical child containment
source string
destination string
}

// artifactRootMount recognizes the proof-tool's explicit public artifact root.
// Its contents must remain one real, read-only Docker mount so --checkpoint
// children resolve beneath --artifact-root inside the container. No other
// inspection-root directory receives this broader mounting behaviour.
func (d *dockerDriver) artifactRootMount(args []string) (dockerArtifactRootMount, error) {
var root string
for i := 0; i < len(args); i++ {
if args[i] != "--artifact-root" {
continue
}
if i+1 == len(args) || root != "" {
return dockerArtifactRootMount{}, errors.New("Docker inspection requires one explicit artifact root")
}
root = args[i+1]
i++
}
if root == "" {
return dockerArtifactRootMount{}, nil
}
if !filepath.IsAbs(root) || filepath.Clean(root) != root {
return dockerArtifactRootMount{}, errors.New("Docker artifact root must be an absolute clean directory")
}
// The normal ceremony public root already has a dedicated input mount.
if _, err := pathWithin(d.root, root, "/relay/input"); err == nil {
return dockerArtifactRootMount{}, nil
}
if d.inspectionRoot == "" || root == d.inspectionRoot {
return dockerArtifactRootMount{}, errors.New("refuse unrecognized Docker artifact root")
}
if _, err := pathWithin(d.inspectionRoot, root, "/relay/extra"); err != nil {
return dockerArtifactRootMount{}, errors.New("refuse artifact root outside the inspection workspace")
}
info, err := os.Lstat(root)
if err != nil {
return dockerArtifactRootMount{}, err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return dockerArtifactRootMount{}, errors.New("Docker artifact root must be a real directory")
}
resolved, err := filepath.EvalSymlinks(root)
if err != nil {
return dockerArtifactRootMount{}, err
}
return dockerArtifactRootMount{root: root, source: filepath.Clean(resolved), destination: "/relay/artifacts"}, nil
}

// child maps an existing non-symlink child under the explicit artifact-root
// mount. It rejects a symlink at any component, restoring the exact-path
// containment checks that separate mounts previously provided.
func (m dockerArtifactRootMount) child(path string) (string, bool, error) {
if m.source == "" {
return "", false, nil
}
mapped, err := pathWithin(m.root, path, m.destination)
if err != nil {
return "", false, nil
}
relative, err := filepath.Rel(m.root, path)
if err != nil {
return "", false, err
}
parts := strings.Split(relative, string(filepath.Separator))
current := m.root
for _, part := range parts {
if part == "." || part == "" {
continue
}
current = filepath.Join(current, part)
info, err := os.Lstat(current)
if err != nil {
return "", false, err
}
if info.Mode()&os.ModeSymlink != 0 {
return "", false, errors.New("Docker artifact arguments cannot traverse symbolic links")
}
}
return mapped, true, nil
}

func (d *dockerDriver) baseRunArgs(remove bool, mounts []dockerMount) []string {
args := []string{"run"}
if remove {
Expand Down
54 changes: 54 additions & 0 deletions cmd/relay/docker_driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,60 @@ func TestDockerInspectionMountsOnlyExactRequestedWorkFile(t *testing.T) {
}
}

func TestDockerInspectionKeepsArtifactRootAndChildrenInOneMount(t *testing.T) {
work := t.TempDir()
root := filepath.Join(work, "ceremony", "public")
stage := filepath.Join(work, "sync", "artifacts")
checkpoint := filepath.Join(stage, "checkpoints", "final", "checkpoint.json")
if err := os.MkdirAll(filepath.Dir(checkpoint), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(checkpoint, []byte("fixture"), 0o600); err != nil {
t.Fatal(err)
}
driver := dockerDriver{root: root, inspectionRoot: work}
// The checkpoint intentionally comes first: proof-tool command ordering must
// not decide whether it remains below the explicit artifact root.
rewritten, mounts, err := driver.rewriteReadOnlyArgs([]string{"--checkpoint", checkpoint, "--artifact-root", stage})
if err != nil {
t.Fatal(err)
}
resolvedStage, err := filepath.EvalSymlinks(stage)
if err != nil {
t.Fatal(err)
}
if len(mounts) != 1 || mounts[0].Source != resolvedStage || !mounts[0].ReadOnly {
t.Fatalf("artifact root was not mounted once read-only: %+v", mounts)
}
if want := "/relay/artifacts"; rewritten[3] != want {
t.Fatalf("artifact root = %q, want %q", rewritten[3], want)
}
if want := "/relay/artifacts/checkpoints/final/checkpoint.json"; rewritten[1] != want {
t.Fatalf("checkpoint escaped its artifact-root mount: got %q, want %q", rewritten[3], want)
}
}

func TestDockerInspectionRejectsSymlinkUnderArtifactRoot(t *testing.T) {
work := t.TempDir()
root := filepath.Join(work, "ceremony", "public")
stage := filepath.Join(work, "sync", "artifacts")
if err := os.MkdirAll(stage, 0o700); err != nil {
t.Fatal(err)
}
outside := filepath.Join(t.TempDir(), "checkpoint.json")
if err := os.WriteFile(outside, []byte("fixture"), 0o600); err != nil {
t.Fatal(err)
}
link := filepath.Join(stage, "checkpoint.json")
if err := os.Symlink(outside, link); err != nil {
t.Fatal(err)
}
driver := dockerDriver{root: root, inspectionRoot: work}
if _, _, err := driver.rewriteReadOnlyArgs([]string{"--artifact-root", stage, "--checkpoint", link}); err == nil {
t.Fatal("accepted an artifact child symlink")
}
}

func TestDockerContributionAdoptsContainerAfterLostCreateResponse(t *testing.T) {
o, pos, driver, fake := dockerContributionFixture(t)
fake.createErrAfter = true
Expand Down
13 changes: 7 additions & 6 deletions cmd/relay/r2_scope_probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ import (
)

type scopeProbeStore struct {
Bucket string
PutNoReplace func(string, string) error
Get func(string, string) error
Delete func(string) error
Head func(string) (bool, error)
Bucket string
PutNoReplace func(string, string) error
Get func(string, string) error
GetVersionedAtMost func(string, string, int64) (store.ObjectVersion, error)
Delete func(string) error
Head func(string) (bool, error)
}

func newScopeProbeStore(c store.Client) scopeProbeStore {
return scopeProbeStore{Bucket: c.Bucket, PutNoReplace: c.PutNoReplace, Get: c.Get, Delete: c.Delete, Head: c.Head}
return scopeProbeStore{Bucket: c.Bucket, PutNoReplace: c.PutNoReplace, Get: c.Get, GetVersionedAtMost: c.GetVersionedAtMost, Delete: c.Delete, Head: c.Head}
}

// Only fresh random probe keys are touched, including denial tests. No listing
Expand Down
2 changes: 1 addition & 1 deletion cmd/relay/storage_preflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func runCheckStorage(args []string) error {
if err != nil || digest != after {
return errors.New("infrastructure settings changed during checks")
}
checks := []string{"published object write and authenticated read", "anonymous published read with exact bytes", "inbox object write", "probe deletion"}
checks := []string{"published object write and authenticated version-pinned read", "anonymous published read with exact bytes", "inbox object write and authenticated version-pinned read", "probe deletion"}
if c.Provider == "r2" {
checks = append(checks, "R2 inbox has no enabled managed or custom public domain")
checks = append(checks, "inbox parent read/write denied on the selected public bucket; other account buckets were not tested")
Expand Down
Loading
Loading