// Dev proofs — D3 of the console release defences (spec // 2026-10-06-console-release-defences-design.md §4.2-§4.4). // // A proving deployment (dev) signs a Proof once a build has run healthily on // its console for a bake period. An operator carries it to a requiring // deployment's bucket, and a requiring box adopts a listed app's build only // with a proof for those exact bytes from a prover it trusts (proofgate.go). // A proof binds app, platform and the artifact's sha256. version is // informational, because version names get reused after pruning. // // The signature covers the canonical JSON of every field except sig: // json.Marshal of proofBody, whose field order the struct fixes and whose // times are UTC. Verify re-marshals the parsed fields, so a proof whose bytes // were reformatted in transit still verifies and one whose fields were edited // does not. The proving key is its own ed25519 key, never a release key. package release import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "regexp" "time" ) const proofSchema = 1 type Proof struct { Schema int `json:"schema"` App string `json:"app"` Platform string `json:"platform"` SHA256 string `json:"sha256"` Version string `json:"version"` Prover string `json:"prover"` ActiveSince time.Time `json:"active_since"` ProvenAt time.Time `json:"proven_at"` Sig string `json:"sig"` } // proofBody is the signed form: every field of Proof except Sig, in order. type proofBody struct { Schema int `json:"schema"` App string `json:"app"` Platform string `json:"platform"` SHA256 string `json:"sha256"` Version string `json:"version"` Prover string `json:"prover"` ActiveSince time.Time `json:"active_since"` ProvenAt time.Time `json:"proven_at"` } func (p *Proof) canonical() ([]byte, error) { return json.Marshal(proofBody{Schema: p.Schema, App: p.App, Platform: p.Platform, SHA256: p.SHA256, Version: p.Version, Prover: p.Prover, ActiveSince: p.ActiveSince.UTC(), ProvenAt: p.ProvenAt.UTC()}) } var ( proverRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,31}$`) sha256Re = regexp.MustCompile(`^[0-9a-f]{64}$`) platformRe = regexp.MustCompile(`^[a-z0-9]+-[a-z0-9]+$`) ) // ValidProver is a prover name: lower case, digits and dashes, at most 32. func ValidProver(s string) bool { return proverRe.MatchString(s) } // AdoptPlatform is the artifact key this box installs (adoptPlatform), for // callers outside the package that mint or describe proofs. func AdoptPlatform() string { return adoptPlatform } func (p *Proof) shape() error { switch { case p.Schema != proofSchema: return fmt.Errorf("proof: schema %d, want %d", p.Schema, proofSchema) case !ValidApp(p.App): return fmt.Errorf("proof: invalid app %q", p.App) case !platformRe.MatchString(p.Platform): return fmt.Errorf("proof: invalid platform %q", p.Platform) case !sha256Re.MatchString(p.SHA256): return fmt.Errorf("proof: sha256 %q is not 64 lower-case hex digits", p.SHA256) case !ValidVersion(p.Version): return fmt.Errorf("proof: invalid version %q", p.Version) case !ValidProver(p.Prover): return fmt.Errorf("proof: invalid prover %q", p.Prover) case p.ActiveSince.IsZero() || p.ProvenAt.IsZero(): return fmt.Errorf("proof: active_since and proven_at are both required") } return nil } // SignProof stamps the schema and signs p with the proving key. func SignProof(priv ed25519.PrivateKey, p Proof) (*Proof, error) { if len(priv) != ed25519.PrivateKeySize { return nil, fmt.Errorf("proof: no usable proving key") } p.Schema, p.Sig = proofSchema, "" if err := p.shape(); err != nil { return nil, err } body, err := p.canonical() if err != nil { return nil, err } p.Sig = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, body)) return &p, nil } // ParseProof checks shape only. Whether it verifies is the requiring box's // question (Check), because only the box holds the prover's public key. func ParseProof(b []byte) (*Proof, error) { var p Proof if err := json.Unmarshal(b, &p); err != nil { return nil, fmt.Errorf("proof: %v", err) } if err := p.shape(); err != nil { return nil, err } if sig, err := base64.StdEncoding.DecodeString(p.Sig); err != nil || len(sig) != ed25519.SignatureSize { return nil, fmt.Errorf("proof: sig is not a base64 ed25519 signature") } return &p, nil } func (p *Proof) Verify(pub ed25519.PublicKey) error { if len(pub) != ed25519.PublicKeySize { return fmt.Errorf("proof: no usable prover public key") } sig, err := base64.StdEncoding.DecodeString(p.Sig) if err != nil { return fmt.Errorf("proof: sig: %v", err) } body, err := p.canonical() if err != nil { return err } if !ed25519.Verify(pub, body, sig) { return fmt.Errorf("proof signature does not verify against the configured key") } return nil } // Check is Verify plus the binding: this proof is for exactly app, platform // and sha, from prover. func (p *Proof) Check(pub ed25519.PublicKey, app, platform, sha, prover string) error { if err := p.Verify(pub); err != nil { return err } switch { case p.App != app: return fmt.Errorf("proof is for app %s, not %s", p.App, app) case p.Platform != platform: return fmt.Errorf("proof is for platform %s, not %s", p.Platform, platform) case p.SHA256 != sha: return fmt.Errorf("proof is for sha256 %s, not %s", p.SHA256, sha) case p.Prover != prover: return fmt.Errorf("proof names prover %s, not %s", p.Prover, prover) } return nil } // FileSHA256 hashes the bytes at path (following a symlink), the one way D3 // learns a sha256 (spec §4.2, codex 3). func FileSHA256(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } func ProofsPrefix(acct, app, sha string) string { return appPrefix(acct, app) + "proofs/" + sha + "/" } func ProofKey(acct, app, sha, prover string) string { return ProofsPrefix(acct, app, sha) + prover + ".json" } func ProofPendingKey(acct, app, sha, prover string) string { return ProofsPrefix(acct, app, sha) + prover + ".pending.json" } func GetProof(ctx context.Context, s Store, acct, app, sha, prover string) (*Proof, error) { if !sha256Re.MatchString(sha) || !ValidProver(prover) || !ValidApp(app) { return nil, fmt.Errorf("proof: invalid key %s/%s/%s", app, sha, prover) } raw, _, err := s.Get(ctx, ProofKey(acct, app, sha, prover)) if err != nil { return nil, err } return ParseProof(raw) } func PutProof(ctx context.Context, s Store, acct string, p *Proof) error { if err := p.shape(); err != nil { return err } b, err := json.MarshalIndent(p, "", " ") if err != nil { return err } return putImmutable(ctx, s, ProofKey(acct, p.App, p.SHA256, p.Prover), append(b, '\n')) } // ProofPending is the proving box's note that a listed build is live and // baking, so `carlos proof get` can say how long it has to go. Advisory: no // box reads it. type ProofPending struct { Schema int `json:"schema"` App string `json:"app"` Platform string `json:"platform"` SHA256 string `json:"sha256"` Version string `json:"version"` Prover string `json:"prover"` LiveSince time.Time `json:"live_since"` After string `json:"after"` } func PutProofPending(ctx context.Context, s Store, acct string, p ProofPending) error { if !ValidApp(p.App) || !sha256Re.MatchString(p.SHA256) || !ValidProver(p.Prover) { return fmt.Errorf("proof pending: invalid record %s/%s/%s", p.App, p.SHA256, p.Prover) } p.Schema = proofSchema b, err := json.MarshalIndent(p, "", " ") if err != nil { return err } _, err = s.Put(ctx, ProofPendingKey(acct, p.App, p.SHA256, p.Prover), append(b, '\n'), "") if errors.Is(err, ErrCASConflict) { return nil // the first sighting stands } return err } func GetProofPending(ctx context.Context, s Store, acct, app, sha, prover string) (*ProofPending, error) { if !sha256Re.MatchString(sha) || !ValidProver(prover) || !ValidApp(app) { return nil, fmt.Errorf("proof pending: invalid key %s/%s/%s", app, sha, prover) } raw, _, err := s.Get(ctx, ProofPendingKey(acct, app, sha, prover)) if err != nil { return nil, err } var p ProofPending if err := json.Unmarshal(raw, &p); err != nil { return nil, fmt.Errorf("proof pending: %v", err) } return &p, nil }