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/attestsigns a caller-suppliedsha256:<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-authorshipre-fetches the commit from GitHub, re-computes the diff, and signs the SHA-256 of the exact bytes it derived. The record is stampedcapture_layer=github_sha_pullandreceipt_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_share-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.
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.
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.
- Resolve the diff base, in this exact order (never guess a base). (a) Use the request
base_shawhen supplied. (b) Otherwise resolve the associated pull request withGET /repos/{repo}/commits/{commit_sha}/pullsand use thebase.sharecorded on the PR whosehead.shaequalscommit_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, readGET /repos/{repo}/commits/{commit_sha}: a parentless commit is the repository's initial commit and diffs against the git empty tree4b825dc642cb6eb9a060e54bf8d69288fbee4904, and a commit WITH parents but NO PR is refused 422 (base_sha_required) rather than guessingparents[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. - Fetch the RAW unified diff with
GET /repos/{repo}/compare/{base_sha}...{commit_sha}(headerAccept: 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 fromGET /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. server_digestis the lowercase hex SHA-256 of those exact raw diff bytes.- 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, carryinggithub_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:
- A malformed
repoor SHA returns 422, and a missing scope returns 403 (default deny). - A GitHub fetch failure returns 503 when GitHub is unreachable and 502 when GitHub cannot resolve the diff, with nothing signed in either case.
- An unseeded or revoked service signing key returns 503, so a receipt is never issued under a key a verifier cannot resolve.
- Private repositories need a configured
ASQAV_GITHUB_TOKEN. Without it, a private re-fetch fails closed rather than falling back to a client-supplied digest. - A personal or unrecognized GitHub token type returns 422
github_token_type_not_authoritativeon the authoritative path: only a fine-grainedgithub_pat_or a GitHub App installation token (ghs_) is accepted for signing. The match is case-insensitive and fails closed ongho_,ghp_, and any other prefix.
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:
{
"_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:
| Field | What it carries | Source |
|---|---|---|
repo | The GitHub owner/name the server re-fetched. | Request, validated |
commit_sha | The full 40-character commit SHA the receipt binds to. | Request, validated |
base_sha | The 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_layer | Always github_sha_pull on this route. | Server-stamped |
asset_class | Always code. | Server-stamped |
change_class | A change classification (for example feature or fix), carried for correlation. | Advisory |
advisory_client_digest | The caller's change_digest, carried for correlation and never signed as the subject. | Advisory |
digest_match | True only when the advisory digest equals the server digest. | Server-computed |
author | An author identifier, carried in the predicate and not verified. | Advisory |
verified_at | The 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:
- Base64-decode
envelope.payloadto the in-toto Statement. Recompute the DSSE pre-authentication encoding overpayloadTypeand those exact bytes, and check the ML-DSA-65 signature inenvelope.signatures[0].sigagainst the public key whosekidmatcheskeyid. The key resolves fromhttps://api.asqav.com/.well-known/jwks.json, or from the receipt's ownjwks_urlat/api/v1/attest/jwks. A valid signature proves the statement is exactly what the platform key signed. - Re-derive the subject. Repeat the re-derivation rule above against GitHub for the same
repoandcommit_sha(andbase_sha), and require equality withsubject[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. - Confirm
receipt.statementHashis the SHA-256 of the decoded statement bytes, and verify the transparency-log inclusion proof (logIndexandinclusionProof) when present. - When
receipt.tsais present, verify the post-quantum timestamp over the same signed bytes to pin existence at or before the stamped time. - Read
digest_matchas 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:
- SOC 2 change management asks for evidence that changes are tracked and authorized. An authoritative receipt records which change the server re-fetched from GitHub at time T, in a form an auditor can re-compute offline, which supports the change-tracking evidence a review asks for.
- FINRA and SEC records rules call for records that are tamper-evident and independently verifiable. A code-authorship receipt is a point-in-time record of a captured change, signed under a post-quantum key and logged in a transparency log.
- EU AI Act Article 12 calls for automatic recording of events over an AI system's lifetime. A code change authored by an agent in the software development lifecycle is one such event, captured as a tamper-evident record.
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.