Code-authorship receipts

Asqav signs code-authorship evidence in two tiers. The voluntary tier signs a digest the caller supplies. The authoritative tier re-fetches the commit from GitHub, re-computes the diff on the server, and signs the digest the server derived. The two tiers are different endpoints with different trust properties, and a verifier weighs them differently.

Tier 1, voluntary: POST /api/v1/attest signs a caller-supplied sha256:<64 hex> digest under the platform service key. The server re-derives nothing, so this is attestation, not capture, and it is not unbypassable.

Tier 2, authoritative: POST /api/v1/code-authorship re-fetches the commit from GitHub, re-computes the diff, and signs the SHA-256 of the exact bytes it derived. The record is stamped capture_layer=github_sha_pull and receipt_type=authoritative.

The difference: in the authoritative tier the signed subject is a value the server computed from GitHub, not a value the client claimed. Anyone holding the commit_sha re-computes the same digest. That reproducibility is what makes the authoritative tier unbypassable.

Why two tiers

Autonomous coding agents open their own pull requests and merge them. The git author string on a commit is mutable, so a name in a commit header proves nothing about who wrote the change or under whose authorization. When an auditor asks which agent wrote which change, git history alone cannot answer.

The voluntary tier is a CI runner or agent vouching for its own artifact. The server signs the digest it is handed, so the receipt proves the key holder asserted that exact digest at time T. It is a useful signed record, and it is honest about its limits: the caller chose the digest, so the receipt cannot prove the digest names a real artifact, or that the artifact is what the caller says.

The authoritative tier takes the signed subject off the client's control. The server fetches the commit from GitHub itself and signs what it sees. A lying client and an honest client get the same signed subject for the same commit, because the client supplies nothing that feeds into it.

Tier 1: voluntary attestation (POST /v1/attest)

The attest endpoint signs a client-supplied digest into an in-toto Statement wrapped in a DSSE envelope, under one dedicated platform service identity with an ML-DSA-65 post-quantum key. The classification is observation and voluntary: the server signs whatever digest the client hands it and does not re-derive the subject from any authoritative source.

What it proves: the platform service key signed that exact digest at time T, chained into the service agent's hash chain and logged in the transparency log. What it does not prove: that the digest corresponds to any real artifact, or that the caller's description of the artifact is true. A caller can post any well-formed digest, so this tier is a voluntary attestation, not a capture, and a caller can never use it to mint an authoritative record.

bash
curl -X POST https://api.asqav.com/api/v1/attest \
  -H "X-API-Key: $ASQAV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "digest": "sha256:c4d9f1a7e0b3c6d8f2a5e7b9c1d3f5a7e9b1c3d5f7a9e1b3c5d7f9a1e3b5c7d9",
    "subject_name": "ci-gate",
    "predicate": { "pipeline": "build.yml", "ref": "refs/heads/main" }
  }'

The default predicateType is https://asqav.com/attestation/ci-gate/v1, and a caller may supply its own. Extra predicate fields ride the statement for correlation and carry no server verification. The response is the DSSE envelope plus a receipt: the statement hash, the signature, the key id, a transparency-log inclusion proof, and an optional inline post-quantum timestamp when anchor: true.

Tier 2: authoritative code-authorship (POST /v1/code-authorship)

The code-authorship endpoint closes the forge gap the voluntary tier leaves open. The server re-derives the signed subject from GitHub and never signs a client-supplied digest. A client that posts a fake change_digest still gets a receipt whose signed subject is the value the server computed from GitHub, and the mismatch is reported in the predicate as digest_match: false. The advisory value is compared, never signed.

bash
curl -X POST https://api.asqav.com/api/v1/code-authorship \
  -H "X-API-Key: $ASQAV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "repo": "acme/payments-service",
    "commit_sha": "9f3c1d8b4a6e2f0c5d7b9a1e3c4f6d8b0a2e4c6f",
    "change_digest": "sha256:c4d9f1a7e0b3c6d8f2a5e7b9c1d3f5a7e9b1c3d5f7a9e1b3c5d7f9a1e3b5c7d9",
    "change_class": "feature",
    "author": "agt_release_bot"
  }'

Only repo (GitHub owner/name) and commit_sha (the full 40-character SHA) are required. base_sha, change_digest, change_class, and author are advisory: they ride the predicate for correlation and never influence the signed subject. A client-supplied capture_topology is dropped on this route, so a caller cannot spoof the capture layer.

The response carries the signed DSSE/in-toto envelope, the receipt, and two fields that make the tier's promise checkable on the spot: server_digest (the hex SHA-256 the server re-derived and signed) and digest_match (true only when the advisory change_digest agreed with it).

The re-derivation rule

The server's digest is reproducible byte for byte. That is the design: a third party verifies an authoritative receipt by re-computing the digest itself and requiring equality. The rule the server applies is the rule any verifier repeats.

  1. Resolve the diff base, in this exact order (never guess a base). (a) Use the request base_sha when supplied. (b) Otherwise resolve the associated pull request with GET /repos/{repo}/commits/{commit_sha}/pulls and use the base.sha recorded on the PR whose head.sha equals commit_sha (the single or first PR when several touch the commit). The PR base is the merge-base anchor GitHub records for the change, robust across merge and squash commits. (c) Otherwise, with no associated PR, read GET /repos/{repo}/commits/{commit_sha}: a parentless commit is the repository's initial commit and diffs against the git empty tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904, and a commit WITH parents but NO PR is refused 422 (base_sha_required) rather than guessing parents[0], which is unreliable for merge and squash commits. The empty tree is a SERVER-RESOLVED base only: a request that supplies it is refused 422 (base_sha_empty_tree_refused), because only that parentless read proves the commit has nothing behind it.
  2. Fetch the RAW unified diff with GET /repos/{repo}/compare/{base_sha}...{commit_sha} (header Accept: application/vnd.github.diff), with ONE exception. GitHub cannot resolve the git empty tree as a compare ref: /compare/4b825dc...{sha} and /git/trees/4b825dc... both answer 404, on public and private repositories alike. An initial commit's diff therefore comes from GET /repos/{repo}/commits/{commit_sha}, whose raw diff IS the root commit's against-nothing diff. That same endpoint serves a NON-root commit's diff against its FIRST PARENT, which is why step 1 refuses a client-supplied empty tree. Take the EXACT response body bytes either way. Do not parse, reformat, project, or re-encode them.
  3. server_digest is the lowercase hex SHA-256 of those exact raw diff bytes.
  4. Size is a real ceiling, not an implicit one. GitHub documents that for the diff media type "Larger diffs may time out and return a 5xx status code", and a root commit is the likeliest object to reach it. A non-200 is refused 502 (github_fetch_failed, carrying github_status) with nothing signed, so this path covers the root commits GitHub can render and fails closed on the ones it cannot.

The signed in-toto subject[].digest.sha256 IS this server_digest. The client's advisory change_digest is only compared against it (digest_match) and is never signed. A verifier reproduces the digest by repeating steps 1 through 3 against the same GitHub state, for example curl -H 'Accept: application/vnd.github.diff' <compare-url> and hashing the body, reading the predicate base_sha to pick the URL: the empty tree means the commit-URL form of step 2, every other base the compare-URL form. A client cannot move the subject, because no client-supplied value feeds into it: a forged advisory digest yields digest_match: false and an unchanged signed subject.

The server signs the raw diff and not a JSON files[] projection on purpose. The raw diff is one opaque byte string, so large and binary diffs re-derive identically. A projection truncates large and binary patches (GitHub caps the files array and omits binary patch bodies), so two reviewers could hash different projections of the same change. The raw diff has nothing to truncate, which is what keeps the digest reproducible byte for byte.

capture_layer is server-derived

Every authoritative receipt stamps capture_layer=github_sha_pull into the predicate, and the stored record carries receipt_type=authoritative. The ingress route dictates both values. A client never supplies them, and a client value sent anyway is dropped before the payload is built.

In-process capture stays observation-class. An SDK running inside the agent process observes values the caller controls, so it mints voluntary attestations, never the authoritative class. The authoritative subject exists only when the server's own re-fetch produced it. That asymmetry is the point: the class an in-process or passive caller can never mint is the class that proves the server saw the change on GitHub.

The route fails closed throughout:

What the receipt binds

The signed in-toto Statement names one subject: subject[0].name is {repo}@{commit_sha}, and subject[0].digest.sha256 is the server-re-derived digest. The predicateType is https://asqav.com/attestation/code-authorship/v1. Decoded, an authoritative statement reads:

json
{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "acme/payments-service@9f3c1d8b4a6e2f0c5d7b9a1e3c4f6d8b0a2e4c6f",
      "digest": { "sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" }
    }
  ],
  "predicateType": "https://asqav.com/attestation/code-authorship/v1",
  "predicate": {
    "repo": "acme/payments-service",
    "commit_sha": "9f3c1d8b4a6e2f0c5d7b9a1e3c4f6d8b0a2e4c6f",
    "base_sha": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
    "capture_layer": "github_sha_pull",
    "asset_class": "code",
    "change_class": "feature",
    "advisory_client_digest": "sha256:c4d9f1a7e0b3c6d8f2a5e7b9c1d3f5a7e9b1c3d5f7a9e1b3c5d7f9a1e3b5c7d9",
    "digest_match": false,
    "author": "agt_release_bot",
    "verified_at": "2026-07-28T14:11:02.481922Z"
  }
}

The predicate fields and where each comes from:

FieldWhat it carriesSource
repoThe GitHub owner/name the server re-fetched.Request, validated
commit_shaThe full 40-character commit SHA the receipt binds to.Request, validated
base_shaThe diff base the server resolved: the request value, the associated PR's base.sha, or the empty tree for an initial commit. A parent-bearing commit with no PR is refused 422, so no base is guessed.Server-resolved
capture_layerAlways github_sha_pull on this route.Server-stamped
asset_classAlways code.Server-stamped
change_classA change classification (for example feature or fix), carried for correlation.Advisory
advisory_client_digestThe caller's change_digest, carried for correlation and never signed as the subject.Advisory
digest_matchTrue only when the advisory digest equals the server digest.Server-computed
authorAn author identifier, carried in the predicate and not verified.Advisory
verified_atThe instant the server re-derived the digest and signed.Server-stamped

The statement is signed as a DSSE envelope: the signed bytes are the DSSE pre-authentication encoding over the payload type application/vnd.in-toto+json and the canonical JSON statement, under the platform service ML-DSA-65 key. On the wire the canonical JSON sorts the keys, so the example above shows the logical statement, not its serialized byte order. The record is chained into the service agent's hash chain and logged in the transparency log, with an optional inline post-quantum timestamp over the exact signed bytes when anchoring was requested.

How to verify one offline

An authoritative receipt verifies with no Asqav call, in five independent steps:

  1. Base64-decode envelope.payload to the in-toto Statement. Recompute the DSSE pre-authentication encoding over payloadType and those exact bytes, and check the ML-DSA-65 signature in envelope.signatures[0].sig against the public key whose kid matches keyid. The key resolves from https://api.asqav.com/.well-known/jwks.json, or from the receipt's own jwks_url at /api/v1/attest/jwks. A valid signature proves the statement is exactly what the platform key signed.
  2. Re-derive the subject. Repeat the re-derivation rule above against GitHub for the same repo and commit_sha (and base_sha), and require equality with subject[0].digest.sha256. This is the step a voluntary attestation can never pass on a forged digest, and it is what makes the authoritative tier unbypassable.
  3. Confirm receipt.statementHash is the SHA-256 of the decoded statement bytes, and verify the transparency-log inclusion proof (logIndex and inclusionProof) when present.
  4. When receipt.tsa is present, verify the post-quantum timestamp over the same signed bytes to pin existence at or before the stamped time.
  5. Read digest_match as advisory. A false value means the caller's self-computed digest disagreed with the server's, and the signed subject is the server's value either way.

Every step runs against the receipt, the public key directory, and GitHub. Nobody needs to take Asqav's word, because the signed value is one any reviewer re-computes from the same commit. See Offline / Air-Gapped Verification and Third-Party Notary for the unaffiliated-verifier posture.

Map to evidence needs

A code-authorship receipt is evidence for an audit, not a compliance guarantee. It supports change-management and record-keeping obligations without claiming to satisfy them on its own:

In every case Asqav supplies the evidence layer. The authoritative receipt proves the change bytes the server saw on GitHub and held their place in the log at time T. Whether the change was correct, the review sufficient, or the attribution accurate stays the deploying organization's judgment.

Related documentation