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
1 change: 1 addition & 0 deletions cmd/mpc-ceremony/decision.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func decisionCommandResult(
Decision: string(decision.Decision),
DecisionID: decision.DecisionID,
ReleaseID: decision.Release.ReleaseID,
ReleaseManifestSHA256: decision.Release.Manifest.Artifact.Digest.SHA256,
CandidateID: decision.Release.CandidateID,
SourceCommit: decision.SourceRelease.SourceCommit,
SourceSignedTag: decision.SourceRelease.SignedTag,
Expand Down
31 changes: 29 additions & 2 deletions cmd/mpc-ceremony/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ func (workflowExecutor) Execute(ctx context.Context, invocation Invocation) (Com
return executeClose(mpcceremony.Phase2, invocation.Options.(CloseOptions))
case CommandPhase2Beacon:
return executeBeacon(mpcceremony.Phase2, invocation.Options.(BeaconOptions))
case CommandRehearsalEvidence:
return executeRehearsalEvidence(invocation.Options.(RehearsalEvidenceOptions))
case CommandOpsPrepareCustody:
return executeCustody(invocation.Options.(CustodyOptions))
case CommandFinalizePrepare:
return executePrepareFinalization(invocation.Options.(PrepareFinalizationOptions))
case CommandFinalizeComplete:
return executeFinalize(invocation.Options.(FinalizeOptions))
case CommandReplay:
return executeReplay(invocation.Options.(AuditOptions))
case CommandAudit:
return executeAudit(invocation.Options.(AuditOptions))
case CommandReleaseSign:
Expand Down Expand Up @@ -562,6 +568,26 @@ func executePrepareFinalization(options PrepareFinalizationOptions) (CommandResu
}, nil
}

func executeReplay(options AuditOptions) (CommandResult, error) {
trust := trustPaths(options.CeremonyPath, options.CeremonySignaturePath, options.CoordinatorPublicKeyFile)
if err := verifyRunningTrust(trust); err != nil {
return CommandResult{}, err
}
paths, err := replayPaths(trust, options.Replay)
if err != nil {
return CommandResult{}, err
}
circuit, err := compileCircuitForCeremony(trust)
if err != nil {
return CommandResult{}, err
}
id, err := mpcceremony.ReplayCandidate(paths, circuit, options.CandidateBundleDir)
if err != nil {
return CommandResult{}, err
}
return CommandResult{CeremonyID: id, Summary: "independently replayed both phases and reproduced final parameters; no audit signed"}, nil
}

func executeAudit(options AuditOptions) (CommandResult, error) {
trust := trustPaths(
options.CeremonyPath,
Expand Down Expand Up @@ -684,8 +710,9 @@ func executeReleaseVerify(options ReleaseVerifyOptions) (CommandResult, error) {
return CommandResult{}, err
}
return CommandResult{
CeremonyID: result.Transcript.CeremonyID,
Summary: "verified the release signature, bundled audits, native keys, Cardano export, and ceremony coherence",
CeremonyID: result.Transcript.CeremonyID,
ReleaseManifestSHA256: result.ManifestSHA256,
Summary: "verified the release signature, bundled audits, native keys, Cardano export, and ceremony coherence",
Outputs: map[string]string{
"keys_dir": options.KeysDir,
},
Expand Down
3 changes: 3 additions & 0 deletions cmd/mpc-ceremony/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestParticipantCLIHelpHasExplicitSafeFlagAllowlist(t *testing.T) {
{"finalize", "prepare"},
{"finalize", "complete"},
{"audit"},
{"replay"},
{"release"},
{"release", "sign"},
{"release", "verify"},
Expand Down Expand Up @@ -217,6 +218,7 @@ func TestFinalizationAuditAndReleaseCommandsAreWired(t *testing.T) {
{Command: CommandFinalizePrepare, Options: PrepareFinalizationOptions{}},
{Command: CommandFinalizeComplete, Options: FinalizeOptions{}},
{Command: CommandAudit, Options: AuditOptions{}},
{Command: CommandReplay, Options: AuditOptions{}},
{Command: CommandReleaseSign, Options: ReleaseSignOptions{}},
{Command: CommandReleaseVerify, Options: ReleaseVerifyOptions{}},
{Command: CommandDecisionPrepare, Options: DecisionPrepareOptions{}},
Expand Down Expand Up @@ -257,6 +259,7 @@ func TestEveryCommandRejectsWalletAndWitnessSecretInputs(t *testing.T) {
{"phase2", "beacon"},
{"finalize"},
{"audit"},
{"replay"},
{"release", "sign"},
{"release", "verify"},
{"decision", "sign"},
Expand Down
22 changes: 20 additions & 2 deletions cmd/mpc-ceremony/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ const redactedCLIValue = "<redacted>"
// messages remain useful, but values supplied by the caller are never echoed.
func redactCLIError(message string, args []string) string {
safeCommandArguments := identifyCLICommandArguments(args)
markOperationalGrammar(args, safeCommandArguments)
candidates := make(map[string]struct{})
for index, arg := range args {
if _, safe := safeCommandArguments[index]; safe {
Expand Down Expand Up @@ -253,7 +254,7 @@ command:
topLevel := map[string]struct{}{
"audit": {}, "decision": {}, "finalize": {}, "help": {}, "init": {},
"inspect": {}, "ops": {}, "phase1": {}, "phase2": {}, "rehearsal": {},
"release": {},
"release": {}, "replay": {},
}
if _, ok := topLevel[args[index]]; !ok {
return safe
Expand All @@ -274,9 +275,10 @@ command:
"chain": {}, "definition": {}, "enrollment": {}, "help": {}, "participant": {},
},
"ops": {
"export-signing": {}, "help": {}, "import-signature": {},
"export-signing": {}, "help": {}, "import-signature": {}, "sign": {}, "prepare-enrollment": {}, "prepare-handoff": {}, "prepare-receipt": {},
"prepare-mirror-receipt": {}, "prepare-public-witness-receipt": {}, "prepare-bundle": {}, "verify": {},
},
"finalize": {"prepare": {}, "complete": {}, "rehearsal-evidence": {}},
"release": {"help": {}, "sign": {}, "verify": {}},
"rehearsal": {"help": {}, "init": {}},
}
Expand Down Expand Up @@ -338,3 +340,19 @@ func writeParseError(message string, args []string, stdout, stderr io.Writer) in
}
return 2
}

// Only fixed operational grammar is public. Unknown values and all paths remain
// redacted, including values following a recognized flag.
func markOperationalGrammar(args []string, safe map[int]struct{}) {
for index, arg := range args {
if arg == "--related-record" || arg == "--record-type" || arg == "--reviewed-sha256" || arg == "--evidence-root" {
safe[index] = struct{}{}
}
if index > 0 && args[index-1] == "--record-type" {
switch arg {
case "handoff", "receipt", "enrollment", "public-witness", "mirror-receipt", "beacon-evidence", "evidence-bundle":
safe[index] = struct{}{}
}
}
}
}
217 changes: 217 additions & 0 deletions cmd/mpc-ceremony/ops_custody.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package main

import (
"crypto/sha256"
"errors"
"fmt"
"golang.org/x/crypto/blake2b"
"io"
"os"
"path/filepath"
"proof-tool/internal/mpcceremony"
"time"
)

type CustodyOptions struct {
CeremonyPath, CeremonySignaturePath, CoordinatorPublicKeyFile string
Root, Chain, ChainSignature, Participant, Direction, Candidate, OutDir string
Handoff, HandoffSignature, SenderPublicKey string
Receipt bool
}

func parseCustody(args []string, receipt bool) (CustodyOptions, error) {
o := CustodyOptions{Receipt: receipt}
f := commandFlagSet("ops prepare-custody")
addCeremonyTrustFlags(f, &o.CeremonyPath, &o.CeremonySignaturePath, &o.CoordinatorPublicKeyFile)
f.StringVar(&o.Root, "transcript-root", "", "public files at this station")
f.StringVar(&o.OutDir, "out-dir", "", "fresh public signing packet")
if receipt {
f.StringVar(&o.Handoff, "handoff", "", "exact canonical handoff")
f.StringVar(&o.HandoffSignature, "handoff-signature", "", "sender's detached signature")
f.StringVar(&o.SenderPublicKey, "sender-public-key-file", "", "separately trusted sender key")
} else {
f.StringVar(&o.Chain, "chain", "", "authenticated current chain before this turn")
f.StringVar(&o.ChainSignature, "chain-signature", "", "current chain signature")
f.StringVar(&o.Participant, "participant-id", "", "next scheduled participant")
f.StringVar(&o.Direction, "direction", "outbound", "outbound or return")
f.StringVar(&o.Candidate, "candidate-dir", "", "completed public candidate for return handoff")
}
if err := parseFlags(f, args); err != nil {
return o, err
}
if o.CeremonyPath == "" || o.CeremonySignaturePath == "" || o.CoordinatorPublicKeyFile == "" || o.Root == "" || o.OutDir == "" {
return o, errors.New("ceremony trust, transcript root and fresh output directory are required")
}
if receipt && (o.Handoff == "" || o.HandoffSignature == "" || o.SenderPublicKey == "") {
return o, errors.New("receipt requires the handoff, sender signature and trusted sender public key")
}
if !receipt && (o.Chain == "" || o.ChainSignature == "" || o.Participant == "" || (o.Direction != "outbound" && o.Direction != "return") || (o.Direction == "return" && o.Candidate == "")) {
return o, errors.New("handoff requires the current chain, participant, and outbound or return direction; return also requires the candidate")
}
return o, nil
}
func executeCustody(o CustodyOptions) (CommandResult, error) {
trusted, err := mpcceremony.LoadSignedDefinition(mpcceremony.TrustPaths{DefinitionPath: o.CeremonyPath, DefinitionSignaturePath: o.CeremonySignaturePath, CoordinatorPublicKeyPath: o.CoordinatorPublicKeyFile})
if err != nil {
return CommandResult{}, err
}
now := time.Now().UTC().Format(time.RFC3339Nano)
var record any
kind := mpcceremony.RecordHandoff
if o.Receipt {
if _, err := executeOpsVerify(OpsVerifyOptions{RecordType: "handoff", RecordPath: o.Handoff, SignaturePath: o.HandoffSignature, CeremonyPath: o.CeremonyPath, CeremonySignaturePath: o.CeremonySignaturePath, CoordinatorPublicKeyFile: o.CoordinatorPublicKeyFile, SignerPublicKeyFile: o.SenderPublicKey}); err != nil {
return CommandResult{}, err
}
raw, err := readRegularOperationalFile(o.Handoff, maxOperationalRecordBytes)
if err != nil {
return CommandResult{}, err
}
parsed, err := mpcceremony.ParseOperationalRecord(mpcceremony.RecordHandoff, raw)
if err != nil {
return CommandResult{}, err
}
handoff := parsed.(*mpcceremony.TransferHandoff)
for _, ref := range handoff.Files {
if err := checkCustodyFile(o.Root, ref); err != nil {
return CommandResult{}, err
}
}
receipt, err := mpcceremony.NewTransferReceipt(*handoff, raw, mpcceremony.ReceiptReceiver, now)
if err != nil {
return CommandResult{}, err
}
record = receipt
kind = mpcceremony.RecordReceipt
} else {
chain, err := mpcceremony.LoadSignedChain(trusted, mpcceremony.PhaseTranscriptPaths{RootDir: o.Root, ChainPath: o.Chain, ChainSignaturePath: o.ChainSignature})
if err != nil {
return CommandResult{}, err
}
index := len(chain.Records) + 1
var policy mpcceremony.PhasePolicy
if chain.Phase == mpcceremony.Phase1 {
policy = trusted.Definition.Phase1Policy
} else {
policy = trusted.Definition.Phase2Policy
}
if index > len(policy.Participants) || policy.Participants[index-1] != o.Participant {
return CommandResult{}, errors.New("participant is not the next signed turn")
}
participant, ok := trusted.Definition.ParticipantByID(o.Participant)
if !ok {
return CommandResult{}, errors.New("participant is not assigned")
}
head, err := chain.HeadRecordID()
if err != nil {
return CommandResult{}, err
}
payload, err := chain.HeadPayload()
if err != nil {
return CommandResult{}, err
}
sender, recipient := trusted.Definition.Coordinator, participant.Identity
files := []mpcceremony.ArtifactRef{payload}
if o.Direction == "outbound" {
if err := checkCustodyFile(o.Root, payload); err != nil {
return CommandResult{}, err
}
} else {
sender, recipient = recipient, sender
raw, err := readRegularOperationalFile(filepath.Join(o.Candidate, "attestation.json"), maxOperationalRecordBytes)
if err != nil {
return CommandResult{}, err
}
var att mpcceremony.ContributionAttestation
if err := mpcceremony.UnmarshalCanonical(raw, &att); err != nil {
return CommandResult{}, err
}
if att.CeremonyID != trusted.Definition.CeremonyID || att.Phase != chain.Phase || int(att.Index) != index || att.ParticipantID != o.Participant || att.PreviousAcceptanceID != head {
return CommandResult{}, errors.New("candidate does not match this turn")
}
files = nil
for _, name := range []string{"attestation.json", "attestation.sig", "contribution.bin", "erasure.json", "erasure.sig"} {
path := filepath.Join(o.Candidate, name)
info, err := os.Lstat(path)
if err != nil || !info.Mode().IsRegular() {
return CommandResult{}, errors.New("return candidate must contain regular public files including cleanup acknowledgment")
}
digest, err := custodyDigest(path)
if err != nil {
return CommandResult{}, err
}
files = append(files, mpcceremony.ArtifactRef{Name: fmt.Sprintf("%s/contributions/%04d/%s", chain.Phase, index, name), Digest: digest})
}
}
handoff, err := mpcceremony.NewTransferHandoff(trusted.Definition, chain.Phase, uint8(index), head, files, sender, recipient, now, time.Now().UTC().Add(time.Hour).Format(time.RFC3339Nano))
if err != nil {
return CommandResult{}, err
}
record = handoff
}
canonical, err := mpcceremony.MarshalCanonical(record)
if err != nil {
return CommandResult{}, err
}
request, err := mpcceremony.NewOperationalSigningRequest(kind, canonical)
if err != nil {
return CommandResult{}, err
}
requestBytes, err := mpcceremony.MarshalCanonical(request)
if err != nil {
return CommandResult{}, err
}
path, requestPath, err := writeOperationalSigningExport(o.OutDir, canonical, requestBytes)
if err != nil {
return CommandResult{}, err
}
return CommandResult{CeremonyID: trusted.Definition.CeremonyID, Summary: fmt.Sprintf("Prepared current-time %s; review and sign exact bytes before the next action. File hashes do not prove physical transfer or erasure.", kind), Outputs: map[string]string{"canonical": path, "signing_request": requestPath, "reviewed_sha256": fmt.Sprintf("%x", sha256.Sum256(canonical))}}, nil
}
func checkCustodyFile(root string, ref mpcceremony.ArtifactRef) error {
if err := ref.Validate(); err != nil {
return err
}
path := filepath.Join(root, filepath.FromSlash(ref.Name))
rel, err := filepath.Rel(root, path)
if err != nil || rel == ".." || len(rel) >= 3 && rel[:3] == "../" {
return errors.New("custody file escapes public root")
}
for current := path; ; current = filepath.Dir(current) {
info, err := os.Lstat(current)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
return errors.New("custody files cannot traverse symlinks")
}
if current == filepath.Clean(root) {
break
}
if filepath.Dir(current) == current {
return errors.New("invalid custody root")
}
}
digest, err := custodyDigest(path)
if err != nil {
return err
}
if digest != ref.Digest {
return errors.New("retained file differs from handoff digest")
}
return nil
}

func custodyDigest(path string) (mpcceremony.Digest, error) {
f, err := os.Open(path)
if err != nil {
return mpcceremony.Digest{}, err
}
defer f.Close()
info, err := f.Stat()
if err != nil || !info.Mode().IsRegular() {
return mpcceremony.Digest{}, errors.New("custody payload must be a regular file")
}
sha := sha256.New()
blake, _ := blake2b.New256(nil)
size, err := io.Copy(io.MultiWriter(sha, blake), f)
return mpcceremony.Digest{SHA256: fmt.Sprintf("sha256:%x", sha.Sum(nil)), Blake2b256: fmt.Sprintf("blake2b256:%x", blake.Sum(nil)), Size: size}, err
}
Loading
Loading