Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
348555b
feat: add storage-first ceremony foundation
mellowcroc Sep 15, 2026
e29cb99
docs: make beacon lead signed ceremony policy
mellowcroc Sep 15, 2026
ff6743b
docs: distinguish legacy beacon timing rule
mellowcroc Sep 15, 2026
fb4e661
feat: add configurable ceremony assurance policy
mellowcroc Sep 15, 2026
c55ee34
test: preserve full ceremony policy context
mellowcroc Sep 15, 2026
8460619
fix: permit authenticated nested evidence output
mellowcroc Sep 15, 2026
9f35c36
fix: identify invalid evidence directory
mellowcroc Sep 15, 2026
24fb82e
test: drive resolved assurance recipe
mellowcroc Sep 15, 2026
2d6a8ef
fix: project complete storage-first lifecycle
mellowcroc Sep 15, 2026
ff6b6aa
feat: show complete authenticated storage position
mellowcroc Sep 15, 2026
714f47d
build: pin released storage-first proof tool
mellowcroc Sep 15, 2026
2025af9
fix: replay transcript before release signing
mellowcroc Sep 15, 2026
f7e344f
test: supply replay evidence to release signer
mellowcroc Sep 15, 2026
797b1d1
Add transport-only delivery foundation and trusted-services plan
mellowcroc Sep 15, 2026
6960fd0
Model bounded delivery recovery and record reviewed V4 implementation
mellowcroc Sep 15, 2026
9e0828b
Record reviewed checkpoint artifact verification scope
mellowcroc Sep 15, 2026
bd8f57e
Record real contribution checkpoint test and remaining scope
mellowcroc Sep 15, 2026
46bdbd9
Record reviewed custody and assurance completion gates
mellowcroc Sep 15, 2026
df071d0
Track reviewed enrollment and mirror implementation
mellowcroc Sep 15, 2026
adcdda7
Record witness and beacon evidence gate verification
mellowcroc Sep 15, 2026
2886baf
Record verified V4 final-candidate replay boundary
mellowcroc Sep 15, 2026
6090264
Record audit verification and terminal governance requirements
mellowcroc Sep 15, 2026
5f5196a
Record reviewed bundle, termination and final-review boundaries
mellowcroc Sep 15, 2026
94f72d0
Record reviewed V4 final verification and test boundaries
mellowcroc Sep 15, 2026
66bace4
Specify reviewed release package and exact dependency boundary
mellowcroc Sep 15, 2026
fc9c1fc
Record V4 package verification and reviewed recovery safeguards
mellowcroc Sep 15, 2026
60e71ae
Document reviewed compact release checkpoint and checksum bounds
mellowcroc Sep 15, 2026
285a6d3
Record private release checkpoint validation and inventory safeguards
mellowcroc Sep 15, 2026
833e730
Plan versioned production decision binding for V4 packages
mellowcroc Sep 15, 2026
5891127
Record reviewed V3 decision source and verification boundaries
mellowcroc Sep 15, 2026
b42a3b3
Update trusted-flow implementation boundary after V4 CLI verification
mellowcroc Sep 15, 2026
51a4e67
Add reviewed V4 structural storage synchronization bridge
mellowcroc Sep 15, 2026
15c2284
Verify V4 guidance facts with one ancestry pass
mellowcroc Sep 15, 2026
e336731
Record reviewed V4 turn-guidance invariants
mellowcroc Sep 15, 2026
eea3214
Derive V4 turn guidance from verified progress and exact retained work
mellowcroc Sep 15, 2026
f19851b
Reconstruct distinct computed and signed candidate inventories
mellowcroc Sep 15, 2026
ab60ae4
Document reviewed V4 operation recovery integration
mellowcroc Sep 16, 2026
60037e7
Record isolated V4 operation intents and restart boundaries
mellowcroc Sep 16, 2026
7730785
Connect V4 contribution and cleanup operation handlers
mellowcroc Sep 16, 2026
8127f82
Route V4 normal guides to authenticated backend status
mellowcroc Sep 16, 2026
423a7aa
Download V4 receipt inputs from normal participant guide
mellowcroc Sep 16, 2026
b79a699
Align Relay with simplified storage-first turns
mellowcroc Sep 16, 2026
3385238
Connect storage-first participant workflow
mellowcroc Sep 16, 2026
dbf0d4f
Connect storage-first coordinator turns
mellowcroc Sep 16, 2026
0785c13
Publish complete initial storage state
mellowcroc Sep 16, 2026
9f6760c
Connect storage-first enrollment journey
mellowcroc Sep 16, 2026
2dc2ac8
Connect storage-first phase lifecycle
mellowcroc Sep 16, 2026
c7ce12f
Validate initial V4 state on live R2
mellowcroc Sep 16, 2026
781bc94
Connect V4 coordinator final replay
mellowcroc Sep 16, 2026
0b37505
Freeze V4 release review in storage state
mellowcroc Sep 16, 2026
1da60cf
Connect V4 release signer storage journey
mellowcroc Sep 16, 2026
d98f0c2
Test V4 release signer profile binding
mellowcroc Sep 16, 2026
4dea069
Reverify retained release package before upload
mellowcroc Sep 16, 2026
d66141a
Complete live V4 release publication
mellowcroc Sep 16, 2026
5cc59ac
Sync complete final release inventories
mellowcroc Sep 16, 2026
ae17887
Require multi-relay beacon evidence in V4 flow
mellowcroc Sep 16, 2026
7032e9d
Collect every required V4 role enrollment
mellowcroc Sep 16, 2026
2319e7a
Use one verified beacon response in V4
mellowcroc Sep 16, 2026
ec6fefe
Complete storage-first V4 lifecycle validation
mellowcroc Sep 16, 2026
38f8ea9
Document storage-first rebase compatibility
mellowcroc Sep 17, 2026
ae8a3f4
Reject reuse of retired V4 candidates
mellowcroc Sep 17, 2026
51d57c1
Clarify V4 retired allocation recovery
mellowcroc Sep 17, 2026
16ee7ac
Bind retained V4 inputs with both digests
mellowcroc Sep 17, 2026
91e61b7
Record BLAKE runtime dependency checksums
mellowcroc Sep 17, 2026
3d01bac
Pin BLAKE dependencies in release inventory
mellowcroc Sep 17, 2026
4c93ecc
Guide rejection of invalid V4 candidates
mellowcroc Sep 17, 2026
acfaa57
Require verified candidate rejection inputs
mellowcroc Sep 17, 2026
48eb57d
Recover verified V4 candidate downloads
mellowcroc Sep 17, 2026
049ec8f
Harden V4 candidate download recovery
mellowcroc Sep 17, 2026
9762490
Test V4 candidate recovery guidance
mellowcroc Sep 17, 2026
7366b7c
Allow fresh work after rejected V4 candidates
mellowcroc Sep 17, 2026
26679cb
Pin released storage-first proof tool
mellowcroc Sep 17, 2026
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
7 changes: 4 additions & 3 deletions .github/workflows/publish-role-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,12 @@ jobs:
run: |
set -euo pipefail
chmod +x launchers/relay-linux-amd64
launchers/relay-linux-amd64 tessera release-manifest --role-images relay-role-images.release.json --out ceremony-software-manifest-v2.json
launchers/relay-linux-amd64 tessera release-manifest-v3 --role-images relay-role-images.release.json --out ceremony-software-manifest-v3.json
- name: Attest setup software manifest
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-path: ceremony-software-manifest-v2.json
subject-path: |
ceremony-software-manifest-v3.json
- uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: ${{ env.RELEASE_TAG }}
Expand All @@ -284,5 +285,5 @@ jobs:
body_path: release/release-notes.md
files: |
relay-role-images.release.json
ceremony-software-manifest-v2.json
ceremony-software-manifest-v3.json
launchers/*
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,11 @@ acceptable release pin.
Before merging Relay, update `release/release-notes.md`, run the normal Go,
shell, launcher, release, and ceremony checks, and report the exact PR head to
Tessera's compatibility workflow. Relay pull requests use a 12-second,
explicitly non-production witness window while still retrieving two real future
explicitly non-production beacon lead while still retrieving two real future
Quicknet rounds. Protected-main and daily scheduled checks use Tessera's
180-second rehearsal window. Production's 24-hour minimum is unchanged. Keep
180-second rehearsal lead. Production defaults to 24 hours, but the exact lead
is signed ceremony policy; shorter production settings require a prominent
warning and explicit coordinator review. Keep
the real end-to-end path through contributions, cleanup, both beacons, audit,
release signing, archive packing, and public replay; it detects incompatibilities
that isolated repository tests can miss.
Expand Down
158 changes: 155 additions & 3 deletions cmd/relay/access_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"time"

"github.com/zksecurity/relay/internal/access"
"github.com/zksecurity/relay/internal/storagefirst"
"github.com/zksecurity/relay/internal/store"
"github.com/zksecurity/relay/internal/transcript"
)
Expand Down Expand Up @@ -214,6 +215,8 @@ func isAccessDenied(err error) bool {
func runGrant(args []string) error {
set := flag.NewFlagSet("coordinator grant", flag.ContinueOnError)
var storagePath, role, identity, ttlText, minimumText, legacyMinimumText, enrollment, enrollmentSignature, out string
var checkpointDigest, submissionKind, phase, attemptID string
var index uint
set.StringVar(&storagePath, "storage", "", "storage configuration from configure-storage")
set.StringVar(&role, "role", "", "participant, witness, mirror, auditor, release, decision")
set.StringVar(&identity, "identity", "", "authenticated ceremony or enrollment identity")
Expand All @@ -223,6 +226,11 @@ func runGrant(args []string) error {
set.StringVar(&enrollment, "enrollment", "", "signed operational enrollment for roles other than participant")
set.StringVar(&enrollmentSignature, "enrollment-signature", "", "detached enrollment signature")
set.StringVar(&out, "out", "", "fresh secret grant file")
set.StringVar(&checkpointDigest, "checkpoint-digest", "", "authenticated V4 allocation checkpoint digest")
set.StringVar(&submissionKind, "submission-kind", "", "V4 submission kind")
set.StringVar(&phase, "phase", "", "V4 ceremony phase")
set.UintVar(&index, "index", 0, "V4 one-based contribution index")
set.StringVar(&attemptID, "attempt-id", "", "V4 allocated attempt ID")
if err := set.Parse(args); err != nil {
return err
}
Expand All @@ -235,6 +243,10 @@ func runGrant(args []string) error {
if storagePath == "" || role == "" || identity == "" || ttlText == "" || minimumText == "" || out == "" {
return errors.New("--storage, --role, --identity, --credential-ttl, --minimum-remaining and --out are required")
}
v4 := checkpointDigest != "" || submissionKind != "" || phase != "" || index != 0 || attemptID != ""
if v4 && (checkpointDigest == "" || submissionKind == "" || phase == "" || index == 0 || index > 255 || attemptID == "") {
return errors.New("V4 grants require --checkpoint-digest, --submission-kind, --phase, --index and --attempt-id together")
}
config, err := loadStorageConfig(storagePath)
if err != nil {
return err
Expand All @@ -247,18 +259,81 @@ func runGrant(args []string) error {
if err != nil || minimum <= 0 || minimum > ttl {
return errors.New("--minimum-remaining must be positive and no greater than --credential-ttl")
}
prefix, err := access.Prefix(config.CeremonyID, role, identity)
var prefix string
if v4 {
if submissionKind == access.SubmissionKindCandidate && role != access.RoleParticipant {
return errors.New("V4 candidate grants support only participants")
}
if submissionKind == access.SubmissionKindRelease && role != access.RoleRelease {
return errors.New("V4 release grants support only the release signer")
}
if submissionKind != access.SubmissionKindCandidate && submissionKind != access.SubmissionKindEnrollment && submissionKind != access.SubmissionKindRelease {
return errors.New("V4 grants support candidate, enrollment or release uploads")
}
prefix, err = (storagefirst.DeliveryScope{CeremonyID: config.CeremonyID, AttemptID: attemptID, Kind: submissionKind}).Prefix()
} else {
prefix, err = access.Prefix(config.CeremonyID, role, identity)
}
if err != nil {
return err
}
if err := authenticateGrantIdentity(config, role, identity, enrollment, enrollmentSignature); err != nil {
return err
if v4 && submissionKind == access.SubmissionKindEnrollment {
if err := authenticateEnrollmentGrantAssignment(config, role, identity, int(index)); err != nil {
return err
}
} else {
if err := authenticateGrantIdentity(config, role, identity, enrollment, enrollmentSignature); err != nil {
return err
}
}
now := time.Now().UTC().Truncate(time.Second)
requestID := ""
if v4 {
requestID, err = randomID()
if err != nil {
return err
}
// Validate every non-secret field and prove the destination directory is
// writable before asking R2/AWS to mint a live credential. A failed
// local validation must never leave an unseen cloud grant behind.
preflight := access.StorageFirstGrant{
Schema: access.GrantSchemaV2, Provider: config.Provider, CeremonyID: config.CeremonyID,
GrantRequestID: requestID, CheckpointDigest: checkpointDigest, SubmissionKind: submissionKind,
Phase: phase, Index: uint8(index), IdentityID: identity, AttemptID: attemptID,
Endpoint: config.Endpoint, Region: config.Region, InboxBucket: config.InboxBucket,
Prefix: prefix + "/", ManifestKey: prefix + "/manifest.json",
IssuedAt: now.Format(time.RFC3339), ExpiresAt: now.Add(ttl).Format(time.RFC3339),
Credentials: access.SessionCredentials{AccessKeyID: "preflight", SecretAccessKey: "preflight", SessionToken: "preflight"},
}
if err := preflight.Validate(); err != nil {
return err
}
if err := preflightFreshGrantOutput(out); err != nil {
return err
}
}
credentials, expires, err := issueCredentials(config, identity, prefix, ttl, now)
if err != nil {
return err
}
if v4 {
grant := access.StorageFirstGrant{
Schema: access.GrantSchemaV2, Provider: config.Provider, CeremonyID: config.CeremonyID,
GrantRequestID: requestID, CheckpointDigest: checkpointDigest, SubmissionKind: submissionKind,
Phase: phase, Index: uint8(index), IdentityID: identity, AttemptID: attemptID,
Endpoint: config.Endpoint, Region: config.Region, InboxBucket: config.InboxBucket,
Prefix: prefix + "/", ManifestKey: prefix + "/manifest.json",
IssuedAt: now.Format(time.RFC3339), ExpiresAt: expires.UTC().Format(time.RFC3339), Credentials: credentials,
}
if err := grant.Validate(); err != nil {
return err
}
if err := writeJSONNoReplace(out, grant, 0o600); err != nil {
return err
}
fmt.Printf("issued %s upload grant for %s\nprefix: %s\nexpires: %s\n", submissionKind, identity, grant.Prefix, grant.ExpiresAt)
return nil
}
grant := access.Grant{
Schema: access.GrantSchema, Provider: config.Provider, CeremonyID: config.CeremonyID,
Role: role, IdentityID: identity, Endpoint: config.Endpoint, Region: config.Region,
Expand All @@ -276,6 +351,77 @@ func runGrant(args []string) error {
return nil
}

func authenticateEnrollmentGrantAssignment(config access.StorageConfig, role, identity string, index int) error {
inspector := transcript.Inspector{
Executable: config.CeremonyBinary, CeremonyPath: config.CeremonyPath,
CeremonySignaturePath: config.CeremonySignature, CoordinatorPublicKeyPath: config.CoordinatorPublicKey,
}
definition, err := inspector.Definition()
if err != nil {
return err
}
if definition.CeremonyID != config.CeremonyID {
return errors.New("authenticated definition does not match the storage ceremony")
}
want := "participant"
if role != access.RoleParticipant {
var ok bool
want, ok = ceremonyEnrollmentRole(role)
if !ok {
return errors.New("role cannot receive a formal enrollment grant")
}
}
journey, err := definition.RequireJourney()
if err != nil {
return err
}
for _, expected := range journey.RequiredEnrollments {
if expected.Identity.ID == identity && expected.Role == want && expected.RoleIndex == index {
return nil
}
}
return errors.New("identity, role and index are not an exact signed enrollment assignment")
}

func preflightFreshGrantOutput(path string) error {
if !filepath.IsAbs(path) || filepath.Clean(path) != path {
return errors.New("V4 grant output must be an absolute clean path")
}
if _, err := os.Lstat(path); err == nil {
return errors.New("V4 grant output already exists")
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
parent := filepath.Dir(path)
if err := ensurePrivateDirectory(parent); err != nil {
return fmt.Errorf("prepare protected grant directory: %w", err)
}
probe, err := os.CreateTemp(parent, ".relay-grant-preflight-")
if err != nil {
return fmt.Errorf("test protected grant output: %w", err)
}
name := probe.Name()
if err := probe.Chmod(0600); err != nil {
_ = probe.Close()
_ = os.Remove(name)
return err
}
closeErr := probe.Close()
removeErr := os.Remove(name)
if closeErr != nil || removeErr != nil {
return errors.Join(closeErr, removeErr)
}
return syncDirectory(parent)
}

func loadStorageFirstGrant(path string) (access.StorageFirstGrant, error) {
raw, err := readProtectedCredentialBytes(path, 1<<20)
if err != nil {
return access.StorageFirstGrant{}, err
}
return access.Decode(raw, access.StorageFirstGrant.Validate)
}

func authenticateGrantIdentity(config access.StorageConfig, role, identity, enrollment, enrollmentSignature string) error {
inspector := transcript.Inspector{
Executable: config.CeremonyBinary, CeremonyPath: config.CeremonyPath,
Expand Down Expand Up @@ -531,6 +677,12 @@ func grantClient(grant access.Grant) store.Client {
return store.Client{Endpoint: grant.Endpoint, Region: grant.Region, Bucket: grant.InboxBucket, Credentials: &credentials}
}

func storageFirstGrantClient(grant access.StorageFirstGrant) store.Client {
credentials := store.Credentials{AccessKeyID: grant.Credentials.AccessKeyID,
SecretAccessKey: grant.Credentials.SecretAccessKey, SessionToken: grant.Credentials.SessionToken}
return store.Client{Endpoint: grant.Endpoint, Region: grant.Region, Bucket: grant.InboxBucket, Credentials: &credentials}
}

func loadStorageConfig(path string) (access.StorageConfig, error) {
raw, err := os.ReadFile(path)
if err != nil {
Expand Down
Loading
Loading