Makoto
Menu
Case 03 · dataset release

Ship the data and the evidence needed to refuse it.

A release process can attach more than a checksum. It can publish signed origin and transformation statements, required profiles, an exact handoff, and the verifier inputs a consumer needs.

The code this page is about. Repeated from The output that catches it below.
$ makoto verify bundle handoffs/unauthorized-signer/bundle \
  --policy receiver/policy.json \
  --schema-catalog receiver/catalog.json \
  --expected-manifest sha256:6cce864b287264e55e4a22549d5d534547f778f497157510af68fc6c4f1204a1 \
  --expected-head sha256:99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce \
  --expected-artifact receiver/expected/unauthorized-signer.artifact.json \
  --evaluation-time 2026-09-16T16:00:00Z
PASS        load-safely
PASS        parse-strictly
PASS        index-payloads
PASS        core-schemas
PASS        signatures
FAIL        authorization-thresholds
PASS        metadata-profiles
FAIL        authorization
PASS        graph-dependency-artifacts
SKIPPED     graph
SKIPPED     roots-and-heads
FAIL        completeness-anchor
PASS        freshness-anchors
SKIPPED     artifact-bytes
SKIPPED     artifact-profiles
PASS        decision
DENY        decision
manifest sha256:6cce864b287264e55e4a22549d5d534547f778f497157510af68fc6c4f1204a1
E_SIGNER_UNAUTHORIZED: no matching policy rule met its signature threshold
E_MANIFEST_SET: final artifact is not an exact terminal subject of its declared head
E_MANIFEST_SET: signed manifest sets do not equal the verified graph and bundle index
$ echo $?
1

Evidence boundary. Every terminal block on this page was captured by running the command shown against the checked-in reference fixtures with the Makoto core verifier at the pinned commit 276008e, which the gate below runs. Verification-summary tables are computed from the published reports with its property rules. The verification report is byte-for-byte the file published under the end-to-end proof. Nothing below was typed by hand.

The problem

A team releases data every sprint: refreshed training sets, configuration bundles, public extracts. Each release should carry evidence of where the data came from and who vouched for it. When that evidence is a manual step it gets skipped, and when it is only a checksum it proves nothing about who produced the bytes.

The harder case is a release that looks perfect. Here the data is byte-identical to the legitimate release, the signed claim is byte-identical to the legitimate claim, and the signature is cryptographically valid. The only difference is whose key made it. A consumer that checks "is this signature valid?" instead of "is this signer allowed to make this claim?" mirrors it.

  1. The raw example

    The release receipt is a signed handoff manifest. Decoded, it commits to the exact final artifact digest, the graph head that produced it, the root it traces to, and every statement in between.

    $ jq -r .payload handoffs/unauthorized-signer/bundle/manifest.dsse.json | base64 -d \
      | jq '{bundleId, recipient, issuedAt, artifacts, heads, roots, statements}'
    {
      "bundleId": "urn:makoto:demo:negative:unauthorized-signer",
      "recipient": "example:downstream-team",
      "issuedAt": "2026-09-16T16:00:00Z",
      "artifacts": [
        {
          "digest": {
            "sha256": "ecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e"
          },
          "head": {
            "sha256": "99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce"
          },
          "mediaType": "application/json",
          "name": "customers.public.json"
        }
      ],
      "heads": [
        {
          "sha256": "99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce"
        }
      ],
      "roots": [
        {
          "sha256": "56b7be4394fe09c62ec7a3d5763cecc251e9696f267f35b2acc717b0d170a27a"
        }
      ],
      "statements": [
        {
          "sha256": "1f28b72bcd4c1e9b7df71403ac6bb1670c2f2b09628ca6d76a2fa384db9a0848"
        },
        {
          "sha256": "56b7be4394fe09c62ec7a3d5763cecc251e9696f267f35b2acc717b0d170a27a"
        },
        {
          "sha256": "99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce"
        }
      ]
    }
  2. How it failed

    The data in the poisoned release hashes to the same digest as the real one, and the head statement has the same digest, 99aee76d…, as the legitimate public-safe statement. What changed is the key that signed that statement.

    $ final=artifacts/final/71895faa8bce0d0426b2e447bb12d15e50e9f9d27135ebabbb94bc9d59426d4e.bin
    $ shasum -a 256 handoffs/positive/bundle/$final
    ecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e  handoffs/positive/bundle/artifacts/final/71895faa8bce0d0426b2e447bb12d15e50e9f9d27135ebabbb94bc9d59426d4e.bin
    $ shasum -a 256 handoffs/unauthorized-signer/bundle/artifacts/final/*.bin
    ecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e  handoffs/unauthorized-signer/bundle/artifacts/final/71895faa8bce0d0426b2e447bb12d15e50e9f9d27135ebabbb94bc9d59426d4e.bin
    $ envelope() { b=handoffs/$1/bundle; d=$(jq -r '.artifacts[0].statementDigest.sha256' $b/bundle.json); cat $b/attestations/$d.dsse.json; }
    $ envelope positive | jq -r '.signatures[].keyid'
    sha256:8cef065b7af83669150b7d32704d3d3e75c3e9aee0d4f6740544c68e81b377aa
    $ envelope unauthorized-signer | jq -r '.signatures[].keyid'
    sha256:3774845b9147b50cf00771ca20eb29cc3043c078f79ee13ebd0c63062a133b4d

    The legitimate release was signed by 8cef065b…. The poisoned one by 3774845b…. Both signatures verify against their own public keys.

  3. Put Makoto in the path

    The consumer mirrors a release internally only after the gate allows it. The gate is the same verifier run as every other case; what makes this case bite is the receiver's trust policy.

    The workflow

    # Consumer-side gate for a published dataset release. The
    # release is mirrored internally only after Makoto allows it.
    #
    # Copy to .github/workflows/makoto-gate.yml in the repository that
    # receives handoffs. Layout it expects:
    #   handoffs/<name>/bundle/                  what the sender transferred
    #   receiver/policy.json                     receiver-owned trust policy
    #   receiver/catalog.json, receiver/resources/  pinned private schemas
    #   receiver/expected/<name>.artifact.json   expected final artifact
    name: Makoto gate before release mirror
    
    on:
      workflow_dispatch:
      push:
        paths:
          - "handoffs/**"
    
    permissions:
      contents: read
    
    env:
      # Pinned so these fixtures replay identically. Delete this line to
      # evaluate against the runner clock.
      EVALUATION_TIME: "2026-09-16T16:00:00Z"
    
    jobs:
      gate:
        runs-on: ubuntu-latest
        strategy:
          fail-fast: false
          matrix:
            include:
              # The sender publishes manifest and head digests out of band.
              # The receiver pins them here, never inside the bundle.
              - handoff: unauthorized-signer
                manifest: sha256:6cce864b287264e55e4a22549d5d534547f778f497157510af68fc6c4f1204a1
                head: sha256:99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce
        steps:
          - uses: actions/checkout@v4
          # Pin the verifier to an exact core commit, like any dependency.
          # Run it from a checkout: the CLI resolves its core schemas from
          # the repository tree. This is the core commit the hosted
          # schemas are pinned to.
          - uses: actions/checkout@v4
            with:
              repository: makoto-project/makoto
              ref: 276008eeec0f10d5322c4c627b8c103222fee841
              path: .makoto-core
              persist-credentials: false
          - uses: astral-sh/setup-uv@v6
          - name: Verify the received bundle
            id: verify
            shell: bash
            env:
              HANDOFF: ${{ matrix.handoff }}
              EXPECTED_MANIFEST: ${{ matrix.manifest }}
              EXPECTED_HEAD: ${{ matrix.head }}
            run: |
              verify() {
                uv run --project .makoto-core makoto verify bundle "handoffs/$HANDOFF/bundle" \
                  --policy receiver/policy.json \
                  --schema-catalog receiver/catalog.json \
                  --expected-manifest "$EXPECTED_MANIFEST" \
                  --expected-head "$EXPECTED_HEAD" \
                  --expected-artifact "receiver/expected/$HANDOFF.artifact.json" \
                  --evaluation-time "$EVALUATION_TIME" "$@"
              }
              status=0
              verify --json > makoto-report.json || status=$?
              verify || true
              exit "$status"
          - name: Keep the verification report
            if: always()
            uses: actions/upload-artifact@v4
            with:
              name: makoto-report-${{ matrix.handoff }}
              path: makoto-report.json
    
      mirror:
        # Runs only when every handoff in the matrix was allowed.
        needs: gate
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - name: Mirror the verified release
            run: echo "replace with your existing step"

    Download makoto-gate.yml.

    The gate keeps the JSON report as a build artifact, so a later job can summarize it into a signed verification summary without running verification again.

    Knowing a key is not authorizing it

    This receiver has deliberately configured the attacker's public key, so the signature is not simply unknown. The policy lists five keys, but the only rule for the public-safe operation authorizes exactly one of them.

    $ jq -r '.keys | to_entries[] | "\(.key)  \(.value.label)"' receiver/policy.json
    sha256:3774845b9147b50cf00771ca20eb29cc3043c078f79ee13ebd0c63062a133b4d  insecure demo-only attacker key
    sha256:47dea58ea00fae9417ee19d76755bfef690899021132effb04fe1f9e4f0c8059  insecure demo-only normalize key
    sha256:8cef065b7af83669150b7d32704d3d3e75c3e9aee0d4f6740544c68e81b377aa  insecure demo-only public key
    sha256:d016df3d83373617c06b5e1d6359caa06eeba8b3af2c7077234f109f2194516c  insecure demo-only handoff key
    sha256:fd110d301d2f077de1414b8f99f441b1403fab207b2052fbd2c065e4ee8e7dc2  insecure demo-only origin key
    $ jq '.rules[] | select(.operationTypes | index("urn:makoto:demo:operation:public-safe")) | {id, authorizedKeyIds, minimumSignatures}' receiver/policy.json
    {
      "id": "urn:makoto:demo:rule:public-safe",
      "authorizedKeyIds": [
        "sha256:8cef065b7af83669150b7d32704d3d3e75c3e9aee0d4f6740544c68e81b377aa"
      ],
      "minimumSignatures": 1
    }

    Inspect the attacker-known policy used for this run.

  4. The output that catches it

    signatures passes: the math is sound. authorization-thresholds and authorization fail, because no rule for this operation lists the signing key. Checks that depend on an authorized graph are skipped, and the job exits 1.

    $ makoto verify bundle handoffs/unauthorized-signer/bundle \
      --policy receiver/policy.json \
      --schema-catalog receiver/catalog.json \
      --expected-manifest sha256:6cce864b287264e55e4a22549d5d534547f778f497157510af68fc6c4f1204a1 \
      --expected-head sha256:99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce \
      --expected-artifact receiver/expected/unauthorized-signer.artifact.json \
      --evaluation-time 2026-09-16T16:00:00Z
    PASS        load-safely
    PASS        parse-strictly
    PASS        index-payloads
    PASS        core-schemas
    PASS        signatures
    FAIL        authorization-thresholds
    PASS        metadata-profiles
    FAIL        authorization
    PASS        graph-dependency-artifacts
    SKIPPED     graph
    SKIPPED     roots-and-heads
    FAIL        completeness-anchor
    PASS        freshness-anchors
    SKIPPED     artifact-bytes
    SKIPPED     artifact-profiles
    PASS        decision
    DENY        decision
    manifest sha256:6cce864b287264e55e4a22549d5d534547f778f497157510af68fc6c4f1204a1
    E_SIGNER_UNAUTHORIZED: no matching policy rule met its signature threshold
    E_MANIFEST_SET: final artifact is not an exact terminal subject of its declared head
    E_MANIFEST_SET: signed manifest sets do not equal the verified graph and bundle index
    $ echo $?
    1

    The machine-readable report

    Output of jq '{decision, primaryError, errors, notPassed: [.checks[] | select(.status != "pass") | {id, status}]}' makoto-report.unauthorized-signer.json against the report the workflow step wrote.

    {
      "decision": "deny",
      "primaryError": "E_SIGNER_UNAUTHORIZED",
      "errors": [
        {
          "causedByCheck": "authorization-thresholds",
          "code": "E_SIGNER_UNAUTHORIZED",
          "context": {
            "statementDigest": {
              "sha256": "99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce"
            }
          },
          "message": "no matching policy rule met its signature threshold",
          "step": 6
        },
        {
          "causedByCheck": "completeness-anchor",
          "code": "E_MANIFEST_SET",
          "context": {
            "artifactDigest": {
              "sha256": "ecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e"
            },
            "bundleId": "urn:makoto:demo:negative:unauthorized-signer",
            "head": {
              "sha256": "99aee76db46a720405da6d0015d427c791b0d4e02367b9e2bc36ce82ae812bce"
            },
            "subjectName": "customers.public.json"
          },
          "message": "final artifact is not an exact terminal subject of its declared head",
          "step": 11
        },
        {
          "causedByCheck": "completeness-anchor",
          "code": "E_MANIFEST_SET",
          "context": {
            "bundleId": "urn:makoto:demo:negative:unauthorized-signer"
          },
          "message": "signed manifest sets do not equal the verified graph and bundle index",
          "step": 11
        }
      ],
      "notPassed": [
        {
          "id": "authorization-thresholds",
          "status": "fail"
        },
        {
          "id": "authorization",
          "status": "fail"
        },
        {
          "id": "graph",
          "status": "skipped"
        },
        {
          "id": "roots-and-heads",
          "status": "skipped"
        },
        {
          "id": "completeness-anchor",
          "status": "fail"
        },
        {
          "id": "artifact-bytes",
          "status": "skipped"
        },
        {
          "id": "artifact-profiles",
          "status": "skipped"
        }
      ]
    }

    What a verification summary records

    A verification summary over this report is FAILED with E_REPORT_DENIED, because the decision is deny. Its evaluation sidecar still records each verified property's exact rule against the report's checks:

    PropertyWhat its rule reads in this reportResult
    MAKOTO_AUTHORIZEDsignatures pass, authorization-thresholds fail, authorization fail; handoff authorization passNot established: E_PROPERTY_AUTHORIZED
    MAKOTO_GRAPH_COMPLETEindex-payloads pass, graph-dependency-artifacts pass, graph skipped, roots-and-heads skipped, completeness-anchor failNot established: E_PROPERTY_GRAPH_COMPLETE
    MAKOTO_FRESHNESS_ANCHOREDfreshness-anchors pass; methods: expected-artifacts pass, expected-heads pass, expected-manifest pass, max-age not_checked, nonce not_checkedEstablished
    MAKOTO_SCHEMA_CONFORMANTcore-schemas pass, metadata-profiles pass, artifact-profiles skipped; the policy requires 2 digest-pinned profilesNot established: E_PROPERTY_SCHEMA_CONFORMANT
    MAKOTO_REPRODUCEDNeeds a second allow report from an independent Transform L2-or-higher platform; none is suppliedNot established: E_PROPERTY_REPRODUCED

    Computed from the published report and the policy whose digest it records, with the property rules in core at the pinned commit. Track levels are not shown: they also need the statements themselves and any platform assessments.

    Open the complete unauthorized-signer report.

A release has two independently reviewed artifacts

Data artifact Evidence handoff
Exact files or signed dataset manifest, media types, sizes, and digests. Signed statement graph, roots, heads, required profiles, and final artifact bindings.
Published through the normal data distribution channel. Published with immutable digests and a receiver-verifiable policy example.
Can be mirrored without changing its identity. Can be verified without access to the producer's CI or warehouse.

CI is a trigger, not the attester

A workflow event can initiate generation after release bytes are final. The signer identity, key custody, source claim, exact predecessor graph, and expected release manifest still require explicit policy. A green CI badge alone is not provenance.

Concrete release receipt

InputThe exact dataset files or deterministic dataset manifest selected for publication.
Release stepCI triggers generation only after bytes are final; a release signer attests the exact graph and handoff.
Consumer stepThe recipient downloads the release and hashes those bytes against the signed final-artifact binding.
Expected failuresA changed file denies on artifact digest (case 01); a correctly signed receipt from an untrusted key denies on authorization (this case).

What else this handles

  • Evidence generated on every release tag instead of remembered by a person.
  • Existing pipelines on any CI system, because the gate is one command and a report file.
  • Batch releases of many files through a signed dataset manifest.
  • Rotating or revoking a release signer by editing receiver policy, without touching published data.

Key insight. Authenticity asks whether a signature is valid for a key. Authorization asks whether the receiver trusts that key for this claim. A release gate that only asks the first question will mirror a perfect forgery.

Release the failure cases too

Consumers need to see the same verifier deny a mutated artifact and unauthorized signer.