Keep the reference verifier behind an explicit process boundary.
A single main.go: validate the trust policy against the hosted schema, sign an origin statement, run the pinned verifier with os/exec, decode the report into structs, and fail closed on anything that is not a clean allow. It passes go vet and was run on Go 1.26 against the pinned core.
policy structure: valid
signed /var/folders/22/0hyg7lbs2fb34hr0ryq3sl1m0000gn/T/makoto-1865024480/origin.dsse.json
decision=allow primaryError=<none>
signatures 4/4 valid, policy 614eb137303b
decision=deny primaryError=E_ARTIFACT_DIGEST
check artifact-bytes: fail []
step 12 E_ARTIFACT_DIGEST: final artifact bytes differ from the signed digest
signatures 4/4 valid, policy 614eb137303bWhat you are binding to. Makoto ships as source: a Python reference library and the makoto command-line interface in the core repository, pinned here to revision 276008eeec0f. There is no published package on PyPI, npm, or the Go module proxy, and the PyPI name makoto belongs to an unrelated project. Every binding on these pages therefore drives the pinned reference verifier and reads its JSON report. That is the supported automation contract: exit status plus the report, never scraped human wording.
1. Install the verifier and one module
git clone https://github.com/makoto-project/makoto.git
cd makoto
git checkout 276008eeec0f10d5322c4c627b8c103222fee841
uv sync --locked --dev
# Writes the deterministic fixture set: one allowed bundle, seven denials.
./scripts/demo.sh --acceptance
export MAKOTO_CORE="$PWD"mkdir makoto-gate && cd makoto-gate
go mod init example.com/makotogate
go get github.com/santhosh-tekuri/jsonschema/[email protected]2. The complete program
Save this as main.go.
// Command makotogate validates, attests, verifies, and gates on a Makoto report.
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/http"
"os"
"os/exec"
"path/filepath"
"github.com/santhosh-tekuri/jsonschema/v6"
)
const (
schemaBase = "https://usemakoto.dev/schema/v0.2/"
expectedManifest = "sha256:e24aae77ba1374162edff49aa0ab1d8dd7e38c333e134a2128ad1598fec99fb7"
expectedHead = "sha256:99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce"
evaluationTime = "2026-09-16T16:00:00Z"
)
type Check struct {
ID string `json:"id"`
Status string `json:"status"`
PrerequisiteChecks []string `json:"prerequisiteChecks"`
}
type Diagnostic struct {
Code string `json:"code"`
Step int `json:"step"`
CausedByCheck string `json:"causedByCheck"`
Message string `json:"message"`
Context map[string]any `json:"context"`
}
type Report struct {
ReportVersion string `json:"reportVersion"`
Decision string `json:"decision"`
PrimaryError *string `json:"primaryError"`
PolicyDigest map[string]string `json:"policyDigest"`
Checks []Check `json:"checks"`
Errors []Diagnostic `json:"errors"`
Warnings []Diagnostic `json:"warnings"`
Summary map[string]int `json:"summary"`
}
// ToolError means the verifier did not produce a verdict. Never treat it as one.
type ToolError struct{ Detail string }
func (e *ToolError) Error() string { return "makoto tool error: " + e.Detail }
// DeniedError carries the full report for a deny or indeterminate decision.
type DeniedError struct{ Report Report }
func (e *DeniedError) Error() string {
return fmt.Sprintf("%s: %s", e.Report.Decision, deref(e.Report.PrimaryError))
}
func deref(s *string) string {
if s == nil {
return "<none>"
}
return *s
}
func makoto(core string, args ...string) (stdout []byte, stderr []byte, runErr error) {
cmd := exec.Command("uv", append([]string{"run", "--quiet", "makoto"}, args...)...)
cmd.Dir = core
var out, errOut bytes.Buffer
cmd.Stdout, cmd.Stderr = &out, &errOut
runErr = cmd.Run()
return out.Bytes(), errOut.Bytes(), runErr
}
func validateStructure(instancePath, schemaName string) error {
resp, err := http.Get(schemaBase + schemaName)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("schema fetch failed: %s", resp.Status)
}
schemaDoc, err := jsonschema.UnmarshalJSON(resp.Body)
if err != nil {
return err
}
compiler := jsonschema.NewCompiler()
if err := compiler.AddResource(schemaBase+schemaName, schemaDoc); err != nil {
return err
}
schema, err := compiler.Compile(schemaBase + schemaName)
if err != nil {
return err
}
file, err := os.Open(instancePath)
if err != nil {
return err
}
defer file.Close()
instance, err := jsonschema.UnmarshalJSON(file)
if err != nil {
return err
}
return schema.Validate(instance)
}
func attestOrigin(core, artifact, workDir string) (string, error) {
privateKey := filepath.Join(workDir, "producer.private.pem")
out := filepath.Join(workDir, "origin.dsse.json")
steps := [][]string{
{"key", "generate", "--private-out", privateKey, "--public-out", filepath.Join(workDir, "producer.public.pem")},
{"attest", "origin",
"--subject", filepath.Base(artifact) + "=" + artifact,
"--source-kind", "urn:example:source:nightly-export",
"--source-uri", "urn:example:orders:2026-09-16",
"--key", privateKey,
"--out", out},
}
for _, args := range steps {
if _, stderr, err := makoto(core, args...); err != nil {
return "", &ToolError{Detail: string(bytes.TrimSpace(stderr))}
}
}
return out, nil
}
func verifyBundle(core, demo, bundle string) (Report, error) {
output, stderr, runErr := makoto(core,
"verify", "bundle", bundle,
"--policy", filepath.Join(demo, "receiver/policy.json"),
"--schema-catalog", filepath.Join(demo, "receiver/catalog.json"),
"--expected-manifest", expectedManifest,
"--expected-head", expectedHead,
"--expected-artifact", filepath.Join(demo, "receiver/expected-artifact.json"),
"--evaluation-time", evaluationTime,
"--json",
)
// A deny exits 1, so a nonzero process status alone is not a tool failure.
// Only exit codes 0 and 1 carry a report; 2 and 3 mean no verdict exists.
exitCode := 0
if runErr != nil {
exitErr, ok := runErr.(*exec.ExitError)
if !ok {
return Report{}, &ToolError{Detail: runErr.Error()}
}
exitCode = exitErr.ExitCode()
}
if exitCode != 0 && exitCode != 1 {
return Report{}, &ToolError{Detail: fmt.Sprintf("exit %d: %s", exitCode, bytes.TrimSpace(stderr))}
}
var report Report
if err := json.Unmarshal(output, &report); err != nil {
return Report{}, &ToolError{Detail: "malformed report: " + err.Error()}
}
if (exitCode == 0) != (report.Decision == "allow") {
return Report{}, &ToolError{Detail: "exit status and report decision disagree"}
}
if report.Decision != "allow" {
return report, &DeniedError{Report: report}
}
return report, nil
}
func explain(r Report) {
fmt.Printf("decision=%s primaryError=%s\n", r.Decision, deref(r.PrimaryError))
for _, c := range r.Checks {
if c.Status != "pass" {
fmt.Printf(" check %s: %s %v\n", c.ID, c.Status, c.PrerequisiteChecks)
}
}
for _, e := range r.Errors {
fmt.Printf(" step %d %s: %s\n", e.Step, e.Code, e.Message)
}
fmt.Printf(" signatures %d/%d valid, policy %.12s\n",
r.Summary["signaturesValid"], r.Summary["signaturesTotal"], r.PolicyDigest["sha256"])
}
func copyTree(src, dst string) error {
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, _ := filepath.Rel(src, path)
target := filepath.Join(dst, rel)
if d.IsDir() {
return os.MkdirAll(target, 0o755)
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(target, data, 0o644)
})
}
func run() error {
core, err := filepath.Abs(os.Getenv("MAKOTO_CORE"))
if err != nil {
return err
}
demo := filepath.Join(core, "demos/end-to-end/generated")
if err := validateStructure(filepath.Join(demo, "receiver/policy.json"), "trust-policy.schema.json"); err != nil {
return fmt.Errorf("policy structure: %w", err)
}
fmt.Println("policy structure: valid")
work, err := os.MkdirTemp("", "makoto-")
if err != nil {
return err
}
artifact := filepath.Join(work, "orders.csv")
if err := os.WriteFile(artifact, []byte("order_id,total\n1001,42.00\n"), 0o644); err != nil {
return err
}
envelope, err := attestOrigin(core, artifact, work)
if err != nil {
return err
}
fmt.Println("signed", envelope)
report, err := verifyBundle(core, demo, filepath.Join(demo, "positive-bundle"))
if err != nil {
return err
}
explain(report)
tampered := filepath.Join(work, "tampered-bundle")
if err := copyTree(filepath.Join(demo, "positive-bundle"), tampered); err != nil {
return err
}
finals, _ := filepath.Glob(filepath.Join(tampered, "artifacts/final/*.bin"))
if len(finals) != 1 {
return errors.New("fixture should have exactly one final artifact")
}
data, _ := os.ReadFile(finals[0])
if err := os.WriteFile(finals[0], bytes.Replace(data, []byte("]"), []byte(",{}]"), 1), 0o644); err != nil {
return err
}
_, err = verifyBundle(core, demo, tampered)
var denied *DeniedError
if !errors.As(err, &denied) {
return fmt.Errorf("expected a denial, got %v", err)
}
explain(denied.Report)
if deref(denied.Report.PrimaryError) != "E_ARTIFACT_DIGEST" {
return fmt.Errorf("unexpected primary error %s", deref(denied.Report.PrimaryError))
}
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}go vet .
MAKOTO_CORE=/path/to/makoto go run .One run’s output, unedited. Key IDs, payload digests, and temporary paths differ on every run; the decisions, codes, and policy digest do not.
policy structure: valid
signed /var/folders/22/0hyg7lbs2fb34hr0ryq3sl1m0000gn/T/makoto-1865024480/origin.dsse.json
decision=allow primaryError=<none>
signatures 4/4 valid, policy 614eb137303b
decision=deny primaryError=E_ARTIFACT_DIGEST
check artifact-bytes: fail []
step 12 E_ARTIFACT_DIGEST: final artifact bytes differ from the signed digest
signatures 4/4 valid, policy 614eb137303b3. Validate structure against the hosted schema
Register the fetched schema under its own URL, compile once, and validate decoded JSON. The validator’s error lists every failing location.
func validateStructure(instancePath, schemaName string) error {
resp, err := http.Get(schemaBase + schemaName)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("schema fetch failed: %s", resp.Status)
}
schemaDoc, err := jsonschema.UnmarshalJSON(resp.Body)
if err != nil {
return err
}
compiler := jsonschema.NewCompiler()
if err := compiler.AddResource(schemaBase+schemaName, schemaDoc); err != nil {
return err
}
schema, err := compiler.Compile(schemaBase + schemaName)
if err != nil {
return err
}
file, err := os.Open(instancePath)
if err != nil {
return err
}
defer file.Close()
instance, err := jsonschema.UnmarshalJSON(file)
if err != nil {
return err
}
return schema.Validate(instance)
}4. Generate an attestation
exec.Command takes arguments as a slice and never involves a shell. The private key file is written with mode 0600 by the verifier itself.
func makoto(core string, args ...string) (stdout []byte, stderr []byte, runErr error) {
cmd := exec.Command("uv", append([]string{"run", "--quiet", "makoto"}, args...)...)
cmd.Dir = core
var out, errOut bytes.Buffer
cmd.Stdout, cmd.Stderr = &out, &errOut
runErr = cmd.Run()
return out.Bytes(), errOut.Bytes(), runErr
}func attestOrigin(core, artifact, workDir string) (string, error) {
privateKey := filepath.Join(workDir, "producer.private.pem")
out := filepath.Join(workDir, "origin.dsse.json")
steps := [][]string{
{"key", "generate", "--private-out", privateKey, "--public-out", filepath.Join(workDir, "producer.public.pem")},
{"attest", "origin",
"--subject", filepath.Base(artifact) + "=" + artifact,
"--source-kind", "urn:example:source:nightly-export",
"--source-uri", "urn:example:orders:2026-09-16",
"--key", privateKey,
"--out", out},
}
for _, args := range steps {
if _, stderr, err := makoto(core, args...); err != nil {
return "", &ToolError{Detail: string(bytes.TrimSpace(stderr))}
}
}
return out, nil
}5. Verify a bundle and handle a denial
Where the expected digests come from. The manifest digest, head digest, and expected-artifact file are the receiver’s independent anchors. The fixture policy sets requireExpectedManifest, requireExpectedHead, and requireExpectedArtifacts, so leaving them out is a denial, not a shortcut. In production they arrive through a channel the producer’s bundle cannot rewrite: a change ticket, a signed release note, a registry entry. The values below are the fixture’s published digests.
func verifyBundle(core, demo, bundle string) (Report, error) {
output, stderr, runErr := makoto(core,
"verify", "bundle", bundle,
"--policy", filepath.Join(demo, "receiver/policy.json"),
"--schema-catalog", filepath.Join(demo, "receiver/catalog.json"),
"--expected-manifest", expectedManifest,
"--expected-head", expectedHead,
"--expected-artifact", filepath.Join(demo, "receiver/expected-artifact.json"),
"--evaluation-time", evaluationTime,
"--json",
)
// A deny exits 1, so a nonzero process status alone is not a tool failure.
// Only exit codes 0 and 1 carry a report; 2 and 3 mean no verdict exists.
exitCode := 0
if runErr != nil {
exitErr, ok := runErr.(*exec.ExitError)
if !ok {
return Report{}, &ToolError{Detail: runErr.Error()}
}
exitCode = exitErr.ExitCode()
}
if exitCode != 0 && exitCode != 1 {
return Report{}, &ToolError{Detail: fmt.Sprintf("exit %d: %s", exitCode, bytes.TrimSpace(stderr))}
}
var report Report
if err := json.Unmarshal(output, &report); err != nil {
return Report{}, &ToolError{Detail: "malformed report: " + err.Error()}
}
if (exitCode == 0) != (report.Decision == "allow") {
return Report{}, &ToolError{Detail: "exit status and report decision disagree"}
}
if report.Decision != "allow" {
return report, &DeniedError{Report: report}
}
return report, nil
}The report is decoded before the decision is read, and a report that does not decode is a ToolError. Denials come back as *DeniedError with the full report attached, so callers use errors.As:
_, err = verifyBundle(core, demo, tampered)
var denied *DeniedError
if !errors.As(err, &denied) {
return fmt.Errorf("expected a denial, got %v", err)
}
explain(denied.Report)
if deref(denied.Report.PrimaryError) != "E_ARTIFACT_DIGEST" {
return fmt.Errorf("unexpected primary error %s", deref(denied.Report.PrimaryError))
}6. Read the verification report
The structs decode the fields a gate needs. PrimaryError is a pointer because it is JSON null on an allow.
type Check struct {
ID string `json:"id"`
Status string `json:"status"`
PrerequisiteChecks []string `json:"prerequisiteChecks"`
}
type Diagnostic struct {
Code string `json:"code"`
Step int `json:"step"`
CausedByCheck string `json:"causedByCheck"`
Message string `json:"message"`
Context map[string]any `json:"context"`
}
type Report struct {
ReportVersion string `json:"reportVersion"`
Decision string `json:"decision"`
PrimaryError *string `json:"primaryError"`
PolicyDigest map[string]string `json:"policyDigest"`
Checks []Check `json:"checks"`
Errors []Diagnostic `json:"errors"`
Warnings []Diagnostic `json:"warnings"`
Summary map[string]int `json:"summary"`
}func explain(r Report) {
fmt.Printf("decision=%s primaryError=%s\n", r.Decision, deref(r.PrimaryError))
for _, c := range r.Checks {
if c.Status != "pass" {
fmt.Printf(" check %s: %s %v\n", c.ID, c.Status, c.PrerequisiteChecks)
}
}
for _, e := range r.Errors {
fmt.Printf(" step %d %s: %s\n", e.Step, e.Code, e.Message)
}
fmt.Printf(" signatures %d/%d valid, policy %.12s\n",
r.Summary["signaturesValid"], r.Summary["signaturesTotal"], r.PolicyDigest["sha256"])
}Exit status contract
0· allow- Every required check passed under this exact policy and expectation set. stdout holds the report; keep it with the policy digest.
1· deny or indeterminate- stdout still holds a complete report. Refuse the data and route on
primaryError. 2· invalid input- A missing file, a malformed flag, an unreadable policy. No report is written; stderr carries one JSON object with
errorClassandmessage. 3· internal error- Same stderr shape with
errorClassset tointernal. Treat it as an outage, never as a verdict.
Toward a native Go verifier
No maintained Go SDK exists. A native implementation would need to reproduce DSSE pre-authentication encoding over exact payload bytes, JCS canonicalization, digest-pinned schema resolution, the receiver policy model, and the published check order with its diagnostic codes. Until one passes the same positive fixture and all seven denial cases, keep the process boundary: it is the only way to be certain Go and Python agree.
Troubleshooting
- Treating every non-nil
cmd.Run()error as a failure - A denial exits 1, which
os/execreports as*exec.ExitError. Unwrap it and readExitCode(); only 2, 3, a signal, or a launch failure mean no report exists. - Validating statement, origin, or transform documents
- Those schemas reference sibling schemas by URL.
santhosh-tekuri/jsonschema/v6fetches only what a loader allows; register each referenced schema withAddResourcefirst, or configure an HTTP URL loader. The trust-policy, envelope, and report schemas are self-contained. - Every run denies with
E_EXPECTED_MANIFEST,E_EXPECTED_HEAD, andE_EXPECTED_ARTIFACTat step 11 - The expectation flags are missing or stale. The
freshness-anchorscheck fails because the policy requires all three. Supply the digests the producer published for this handoff. - Exit status 2 with
{"errorClass":"invalid-input",...}on stderr - An input path is wrong or unreadable, or a singleton flag was passed twice. Paths are resolved from the process working directory, so pass absolute paths when the verifier runs with
cwdset to the core checkout. - The report shows
skippedfor a check you care about - An earlier check failed and did not establish its prerequisite.
prerequisiteChecksnames it. A skipped check was not tested; never read it as a pass. uv: command not foundinside a service or CI job- The binding needs
uvonPATHfor the process that spawns the verifier. Install it in the image or call the checkout’s.venv/bin/makotodirectly afteruv sync --locked.
The same gate in every language.
Each binding runs the same six steps against the same fixtures and produces the same decision. The report and exit status are the contract; the language is a wrapper.
Bindings: Bash · Python · JavaScript · TypeScript · Go