誠 Makoto
Menu
Go binding

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.

The code this page is about. Repeated from 2. The complete program below.
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 614eb137303b

What 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 614eb137303b

3. 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 errorClass and message.
3 · internal error
Same stderr shape with errorClass set to internal. 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/exec reports as *exec.ExitError. Unwrap it and read ExitCode(); 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/v6 fetches only what a loader allows; register each referenced schema with AddResource first, 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, and E_EXPECTED_ARTIFACT at step 11
The expectation flags are missing or stale. The freshness-anchors check 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 cwd set to the core checkout.
The report shows skipped for a check you care about
An earlier check failed and did not establish its prerequisite. prerequisiteChecks names it. A skipped check was not tested; never read it as a pass.
uv: command not found inside a service or CI job
The binding needs uv on PATH for the process that spawns the verifier. Install it in the image or call the checkout’s .venv/bin/makoto directly after uv 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