Apostille release evidence and policy API — preview.
Status: API preview, per deployment. A separate hosted profile; Core 0.1 and
Core 0.3 signed formats are unchanged. Check support with GET /api/apostille/v1/status: enabled must be true and
features must contain supply_chain_releases. The feature can be present
while enabled is false.
CI read-only credentials are an API preview with per-deployment availability.
Before using that extension, also require supply_chain_ci_credentials in
features and confirm the credential routes are available on your deployment.
This guide describes the API contract; verify rollout status on the target
service before use. The extension does not change Core signed formats.
The service registers a signed release manifest, evaluates artifact metadata against an administrator's policy, and records an irreversible withdrawal. Consumers verify the actual bytes locally before use.
1. Scope and flow
Publisher's actual files
-> local SHA-256 and byte counts
-> canonical release manifest
-> registered agent's Core origin-statement + admin publication-grant
-> existing POST /submissions -> existing certificate
-> separate admin register_release authorization -> POST /releases
Administrator's independently selected issuer/producer pins
-> signed, versioned policy -> POST /release-policies
Consumer's actual file
-> local hash + independent bundle/manifest verification
-> POST /release-policy/evaluations with metadata and current policy version
-> use the same checked bytes only if local checks and policy permit
Administrator's signed withdraw_release authorization
-> POST /releases/{id}/status-events -> future evaluations are blocked
Limits
allowed_by_policyis a scoped policy result. Signatures and policy checks do not prove a file is safe or malware-free.- The service does not fetch artifact URLs, accept uploads, scan, build or run anything. Artifacts, source, weights and private keys stay with you. Hashes, names, keys and timestamps can still link records.
- Release records never enter IFF's x402 cards, observations, transparency log, monitor webhooks, anchors or reputation. There is no registry crawler, hardware authenticity check or remote attestation.
- Hosted status is an operational snapshot, not an offline revocation proof.
- The consumer must enforce the gate on the same bytes it checked. There is no remote enforcement, secure launcher, restore route, remote install or model activation.
- Withdrawal is irreversible and cannot stop a running program.
2. Release and credential API routes
Base: /api/apostille/v1. See authentication and existing issuance.
All GET routes also support HEAD. Owner routes accept the administrator's
workspace session. Three read/evaluation routes also accept the scoped CI bearer
described in section 7; all other owner routes reject it. Another workspace or
an out-of-scope CI credential gets 404 for a private release or policy. The public
route takes the release's separate public_id.
| Operation | Method / path | Access | Request |
|---|---|---|---|
| Register evidence | POST /releases |
Owner | certificate_id, manifest_base64, visibility, authorization |
| Retrieve evidence and status | GET /releases/{id} |
Owner session or scoped CI | No body |
| Retrieve public evidence and status | GET /public/releases/{id} |
Public | id is the release's public_id; no body |
| Set receiver policy | POST /release-policies |
Owner | policy, authorization |
| Read current receiver policy | GET /release-policies/{id} |
Owner session or scoped CI | id is a policy label; no body |
| Evaluate metadata | POST /release-policy/evaluations |
Owner session or scoped CI | release_id, policy_id, policy_version, artifact_name, artifact_sha256, artifact_size |
| Withdraw | POST /releases/{id}/status-events |
Owner | release_id, action, reason, authorization |
| Create CI credential | POST /release-credentials |
Owner session + signed authorization | name, product, policy_id, expires_at, authorization |
| List CI credential metadata | GET /release-credentials |
Owner session | No body; includes expired/revoked history |
| Revoke CI credential | POST /release-credentials/{id}/revoke |
Owner session | No body; idempotent |
visibility is exactly private or public. action is exactly withdraw.
Withdrawal reasons are security_incident, superseded, or maintainer_request.
The signed body must include release_id, matching the path parameter.
Writes use strict UTF-8 JSON with Content-Type: application/json, limited to
256 KiB. Duplicate keys, unknown fields and JSON numbers are rejected. Artifact
sizes and policy versions are canonical decimal strings; operational
counters stay JSON numbers. Never put a secret in a manifest or authorization.
Response records
A registration returns a Release with id, public_id, certificate_id,
product, version, manifest_sha256, manifest_base64, is_public and
created_at. Owner retrieval returns {release, bundle, current_status}. Public retrieval
returns only public_id, product, version, manifest_base64,
manifest_sha256, created_at, bundle and current_status. The public id
does not work in owner routes.
current_status is {state, withdrawn_at, reason, agent_state, as_of, basis, offline_proof, issuer_key_revocation}. Release state is active or
withdrawn; agent_state is active, revoked or expired. Before
withdrawal, withdrawn_at is null and reason is empty. basis is
hosted_operational_snapshot, offline_proof is false and
issuer_key_revocation is unknown. Active does not mean authorized.
Policy responses contain id, decimal-string version, policy,
policy_sha256, the signed authorization and created_at. Withdrawal returns
{event, state:"withdrawn"}; event has id, release_id, reason and
created_at. New mutations return HTTP 201; an identical retry returns 200.
3. Release manifest
Profile: https://ifandonlyif.io/apostille/supply-chain/release/0.1. Core
artifact media type: application/vnd.iff.release+json.
Shape example, not canonical bytes. Calculate real digests and sizes locally.
{
"profile": "https://ifandonlyif.io/apostille/supply-chain/release/0.1",
"product": "example/agent-runtime",
"version": "1.0.0",
"artifacts": [
{
"name": "agent-runtime-linux-amd64.tar.gz",
"sha256": "<64 lowercase hex characters from the actual file>",
"size": "<canonical decimal byte count>",
"media_type": "application/gzip"
}
]
}
Requirements:
- Canonicalize the manifest with the Core JCS helper, then encode those exact
bytes as unpadded, canonical base64url in
manifest_base64. The decoded manifest is at most 64 KiB with 1–32 artifacts. productis at most 120 ASCII bytes, consisting of slash-separated labels. Each label andversionis 1–80 bytes: an ASCII letter or digit first, then letters, digits,.,_or-. Empty path segments are invalid. Matching is exact: no case folding, no SemVer ordering.- Artifact names are unique, use the same label grammar (128-byte limit), and contain no slash.
- SHA-256 is 64 lowercase hex characters. Size is
0or a decimal string without leading zeros, within signed 64-bit range. Media types are explicit type/subtype (no parameters or wildcards), at most 128 bytes. - The certificate must belong to the workspace, be fresh at registration, use the service's current issuer key for that Core version, and cover exactly these manifest bytes, size and media type. After issuer-key rotation, get a new certificate for new releases; earlier releases can still be evaluated with pinned historical issuer keys. The server loads its stored certificate.
- Workspace, agent, certificate and administrator operation use the same Core
version. Hosted Apostille issues Core 0.3 (ML-DSA-65, post-quantum) only, so
POST /releasesfor a Core 0.1 workspace or certificate returns 410core_0_1_retired. A policy may still list Core 0.1 inaccepted_protocolsto evaluate historical releases; evaluation and withdrawal are unchanged. There is no downgrade, mixed-version registration or Core 0.2 issuance.
(workspace, product, version) identifies one immutable release. A new artifact
digest needs a new version. A retry cannot replace the manifest, certificate,
visibility or authorization.
SBOMs, provenance documents and signature bundles can be named artifacts. This API does not validate SPDX, CycloneDX, SLSA provenance or Cosign signatures, assert a SLSA level, or prove a trusted build produced the bytes.
4. Separate administrator operation authorization
Release registration, policy updates, withdrawal and CI credential creation need this operation object in addition to the administrator session. The certificate submission separately needs its publication grant:
{
"payload_base64": "<canonical authorization payload, unpadded base64url>",
"statement": "<administrator-signed Core Envelope object, not a string>"
}
The decoded authorization payload has exactly these fields:
{
"profile": "https://ifandonlyif.io/apostille/supply-chain/authorization/0.1",
"purpose": "register_release",
"audience": "https://ifandonlyif.io/apostille",
"workspace_id": "<authenticated workspace UUID v4>",
"body_sha256": "<SHA-256 of the canonical operation body>",
"issued_at": "<UTC timestamp at whole seconds>",
"expires_at": "<later UTC timestamp, at most five minutes after issued_at>",
"nonce": "<new UUID v4>"
}
The decoded payload is canonical JSON, at most 4 KiB. The server accepts an
issued_at up to two minutes ahead of its clock and requires
now < expires_at; keep client clocks synchronized. The signed body
excludes authorization:
| Purpose | Canonical body whose SHA-256 is signed |
|---|---|
register_release |
{certificate_id, manifest_base64, visibility} |
withdraw_release |
{release_id, action, reason} |
set_release_policy |
{policy} |
create_release_credential |
{name, product, policy_id, expires_at} |
The administrator signs the payload as a producer-only Core origin-statement
with media type application/vnd.iff.release-authorization+json. agent_id
equals workspace_id; delegation_sha256 is empty. The issuer identity, key ID
and public key must identify the workspace administrator. The statement's digest,
size, media type, issued_at and nonce must match the payload.
Use the released Core signing helpers. Do not use SignChallenge,
SignChallenge03, a login prefix or a login signature. An authorization cannot
be reused across purposes, audiences, workspaces or bodies; the server checks
the nonce atomically.
CI credential creation requires a Core 0.3 administrator. Credential revocation only removes access and needs the owner session, with no additional signed operation. A CI credential cannot create signing requests or authorize mutations.
5. Administrator-selected receiver policy
Exact fields; the example pins are placeholders. Obtain real pins independently of the bundle and key directory. Never copy embedded keys into a policy.
{
"profile": "https://ifandonlyif.io/apostille/supply-chain/policy/0.1",
"id": "production-agent-runtime",
"version": "1",
"product": "example/agent-runtime",
"expected_issuer": "https://ifandonlyif.io/apostille",
"trusted_issuer_key_ids": ["sha256:<independently obtained issuer fingerprint>"],
"trusted_producer_key_ids": ["sha256:<independently obtained producer fingerprint>"],
"accepted_protocols": ["https://ifandonlyif.io/apostille/spec/0.3"],
"require_fresh_certificate": true
}
- IDs use the 1–80-byte label grammar. Product matching is exact.
- Version is a canonical decimal string from
"1"to"1000000". The first version is"1"; each new version increments by one. Saved versions are immutable.GET /release-policies/{id}returns the latest version. - Each key list holds 1–16 distinct
sha256:fingerprints. Issuer and producer pins are independent.accepted_protocolslists Core 0.1, Core 0.3 or both. Nothing is trusted implicitly. require_fresh_certificatemust be present.truerequires the certificate's current validity interval.falsepermits historical certificates but still checks signatures, pins, agent authorization and withdrawal.- Evaluation uses the latest policy version; an older version selects nothing and allows no rollback. Only a separately signed administrator update changes the policy.
Origin certificates last at most 24 hours, bounded by delegation expiry. Expiry does not alter historical signatures; policy decides whether one suffices now.
6. Evaluation and the local file boundary
{
"release_id": "<owner release UUID>",
"policy_id": "production-agent-runtime",
"policy_version": "1",
"artifact_name": "agent-runtime-linux-amd64.tar.gz",
"artifact_sha256": "<hash freshly calculated from the local file>",
"artifact_size": "<byte count freshly calculated from the local file>"
}
The server reads the release, agent, withdrawal and current policy in one snapshot. An HTTP success can still carry a non-permitting decision:
| Decision | Meaning |
|---|---|
allowed_by_policy |
The submitted metadata and evidence satisfy this policy at evaluation time. |
blocked |
A checked condition forbids use, for example withdrawal, a digest mismatch or a failed policy constraint. |
unknown |
Required evidence or current information does not establish permission. |
The response includes decision, reasons, release_id, policy_id,
policy_version, evaluated_at, valid_until, current_status, the submitted
artifact_name/artifact_sha256/artifact_size, and
artifact_bytes_checked: false (the service never sees file bytes). When a
policy exists, policy_version and policy_sha256 describe its current version,
even on a version mismatch; with none, the version is the requested value and the
hash is absent. Bundle verification adds protocol, certificate_freshness and
issuer_trust, absent after an early failure. organization_binding stays unproven and content_truth
stays not_established.
| Decision | Current reason codes |
|---|---|
allowed_by_policy |
policy_checks_passed |
blocked |
release_withdrawn, agent_inactive, product_not_allowed, bundle_or_protocol_rejected, issuer_not_trusted, producer_not_trusted, manifest_binding_invalid, certificate_not_fresh, artifact_mismatch, artifact_not_listed |
unknown |
policy_not_found, policy_version_mismatch, stored_policy_invalid, stored_release_invalid |
Treat any non-permitting decision, including an unknown future reason, as a failed gate. The response reports the first failed condition only.
valid_until is an advisory window of at most 30 seconds, further bounded by
the CI credential's expiry when one is used. Withdrawal,
revocation or a policy update can follow the snapshot immediately; re-evaluate
close to use.
A consumer must check the manifest/bundle and pins independently, hash the actual
file, require allowed_by_policy, and use the same bytes it checked. Do not
verify a pathname and later execute a replaceable file there; use an immutable
staged artifact.
The reference command cmd/iff-release-check is source-only in the private
hosted service repository, with no public binary or Go module. API consumers can
implement the checks with the released Core SDK and this HTTP contract. To build
it (macOS or Linux, Go version in go.mod, from that repository root), supply a
local artifact, a token file and independent issuer/producer pins:
go build -o ./bin/iff-release-check ./cmd/iff-release-check
./bin/iff-release-check \
--base-url https://ifandonlyif.io \
--access-token-file /secure/path/apostille-ci-token \
--release-id '<private-release-uuid>' \
--policy-id production-agent-runtime --policy-version 1 \
--artifact-name agent-runtime-linux-amd64.tar.gz \
--artifact /immutable/staging/agent-runtime-linux-amd64.tar.gz \
--issuer https://ifandonlyif.io/apostille \
--issuer-key-id 'sha256:<independently-obtained-issuer-fingerprint>' \
--producer-key-id 'sha256:<independently-obtained-producer-fingerprint>'
For CI, use a scoped CI credential, never an administrator session or signing key.
The token file holds only the bearer token (no Bearer prefix, no JSON): a
regular file owned by the operator, mode 0600 or 0400, at most 4 KiB;
symlinks are rejected. Other operating systems fail closed. The base URL must be
a public HTTPS origin on port 443 with no path prefix, credentials, query or
fragment. Redirects and environment proxies are disabled. The command neither
uploads nor executes the artifact.
Stdout is one JSON object. Exit 0 means allowed_by_policy; exit 2 means an
input/transport failure; exit 3 means the artifact or decision was rejected.
Check the exit status and deadline, and keep the clock synchronized. Its
artifact_bytes_checked: true describes its own local hash; the API response
always says false. Treat a non-permitting, expired or unavailable result as a
failed gate.
7. CI read-only credentials
Scope and lifetime
An administrator grants a credential access to one workspace, one exact product and one policy label. Scopes are immutable, with no wildcard or cross-workspace access. The policy must already exist for that product.
| Allowed operation | Required scope |
|---|---|
GET/HEAD /releases/{id} |
Same workspace and exact product |
GET/HEAD /release-policies/{id} |
Same workspace, exact policy label and product |
POST /release-policy/evaluations |
Same workspace, exact product and policy label; request must still select the current policy version |
All other owner routes reject CI credentials with HTTP 401 (invalid_session), including /me, certificate
downloads, signing/submission, release registration, policy changes, withdrawal
and credential management. Read access includes private manifests and certificate
bundles for releases of the allowed product; protect the credential accordingly.
The bearer is opaque: iff_sc_ci_ followed by unpadded base64url encoding of 32
random bytes. The database retains only its SHA-256 digest. It is not a JWT or an
offline proof. Expiry and revocation are checked on every request; an in-flight
request may finish after revocation. A credential's expiry does not make a
previous policy decision a continuing deployment authorization.
Creation requires an explicit future expires_at, at most 90 days from creation;
30 days is recommended. Use a canonical whole-second UTC timestamp. name is
1–120 UTF-8 bytes, with no control characters or leading/trailing whitespace.
There are at most 100 active credentials and 1,000 total retained credentials
per workspace. Expired and revoked metadata remains in history.
Administrator setup and one-time delivery
POST /release-credentials takes the four signed body fields below and the
section 4 authorization, with purpose create_release_credential. The
authorization still binds audience, workspace, nonce and a five-minute maximum
lifetime; that lifetime is separate from the credential's expiry.
{
"name": "production-artifact-check",
"product": "example/agent-runtime",
"policy_id": "production-agent-runtime",
"expires_at": "<future whole-second UTC timestamp, within 90 days>",
"authorization": {"payload_base64": "<canonical payload>", "statement": "<Core 0.3 Envelope object>"}
}
The first response is HTTP 201 {credential, token}. Metadata contains id,
name, product, policy_id, created_at, expires_at and optional
revoked_at. The raw token is returned only once. Store it directly in a
private file or secret manager; never print or log the response.
An identical, still-authorized nonce/body retry returns HTTP 200 {credential},
without the token. The server cannot recover a lost token. After a lost response,
list metadata to reconcile, revoke the old credential and create a replacement
with a fresh nonce. A changed body with the same nonce conflicts. The owner-only
list returns {credentials: []}; revoke returns {credential} and is idempotent.
Maintainers with access to the private service checkout can build the
source-only cmd/iff-release-credential administrator helper. It signs and
sends the creation authorization locally; it is not a published binary or a
public go install target. Run it on the administrator's machine, outside CI:
go build -o ./bin/iff-release-credential ./cmd/iff-release-credential
./bin/iff-release-credential \
--base-url https://ifandonlyif.io \
--admin-key-file /secure/admin/apostille-key.json \
--name production-artifact-check \
--product example/agent-runtime \
--policy-id production-agent-runtime \
--out-token /secure/ci/apostille-ci-token
The helper defaults to 30 days; --expires-at selects an explicit expiry up to
90 days. The Core 0.3 key must be an operator-owned regular file, mode 0600 or
0400, at most 4 KiB, with no symlink. Prepare an operator-owned 0700 output
directory; the helper creates the token file as 0600 and refuses to overwrite
an existing file. Stdout contains metadata/status only, never the token, key or
administrator session. --issuer defaults to the base origin plus /apostille.
Exit 0 is success; 2 is an input/session/API failure; 3 means a credential
is known but its token was unavailable or could not be saved. Follow
next_action: list_before_retry after an uncertain response, or
revoke_credential when its ID is known. Do not retry creation blindly.
./bin/iff-release-credential \
--base-url https://ifandonlyif.io \
--admin-key-file /secure/admin/apostille-key.json --list
./bin/iff-release-credential \
--base-url https://ifandonlyif.io \
--admin-key-file /secure/admin/apostille-key.json \
--revoke '<credential-uuid>'
Keep the credential ID in operator records for revocation. CI receives only the scoped token. Rotation creates a replacement with the same intended scope, updates the CI secret, checks it, and revokes the old credential.
CI gate example
Provision a trusted iff-release-check build and the immutable staged artifact
before this step. This GitHub Actions excerpt is for a protected workflow, with
the scoped bearer in the IFF_SC_CI_TOKEN secret. Supply the current policy
version and independent pins through reviewed configuration. An older version
fails closed after a policy update.
steps:
- name: Verify the staged release
shell: bash
env:
IFF_SC_CI_TOKEN: ${{ secrets.IFF_SC_CI_TOKEN }}
run: |
set -euo pipefail
set +x
umask 077
token_dir="$(mktemp -d)"
export IFF_CI_TOKEN_FILE="$token_dir/token"
trap 'rm -f "$IFF_CI_TOKEN_FILE"; rmdir "$token_dir"' EXIT
python3 - <<'PY'
import os
token = os.environ.pop("IFF_SC_CI_TOKEN")
fd = os.open(os.environ["IFF_CI_TOKEN_FILE"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as output:
output.write(token)
PY
unset IFF_SC_CI_TOKEN
./bin/iff-release-check \
--base-url https://ifandonlyif.io \
--access-token-file "$IFF_CI_TOKEN_FILE" \
--release-id '<private-release-uuid>' \
--policy-id production-agent-runtime --policy-version 1 \
--artifact-name agent-runtime-linux-amd64.tar.gz \
--artifact /immutable/staging/agent-runtime-linux-amd64.tar.gz \
--issuer https://ifandonlyif.io/apostille \
--issuer-key-id 'sha256:<independently-obtained-issuer-fingerprint>' \
--producer-key-id 'sha256:<independently-obtained-producer-fingerprint>'
Replace the non-secret placeholders before use. Never expose this secret to untrusted pull-request code or an unreviewed workflow, and never enable shell tracing around secrets. Keep it out of command-line arguments, job output, uploaded artifacts and caches. A CI bearer permits evidence reads and evaluation; the caller still verifies and uses the same local bytes. It proves neither file safety nor payment safety.
8. Go lifecycle example
Use the released github.com/ifandonlyif-io/iff-apostille/apostille core
(Go root v0.4.0-alpha.1, Go 1.27) and its apostille/client package for login,
registration and issuance. Supply your own administrator/agent key files and
independent issuer pins. The Core client has no release wrapper.
// client is an authenticated apostille/client.Client. protocol, admin, agent,
// registration and manifest are supplied by your application; check all errors.
manifestRaw, err := core.Canonical(manifest)
if err != nil { return err }
statement, err := core.CreateStatementFor(protocol, bytes.NewReader(manifestRaw),
"application/vnd.iff.release+json", agent, ®istration, "", now)
if err != nil { return err }
grant, err := core.CreateGrantFor(protocol, statement, registration, admin,
expectedIssuer, "private", now)
if err != nil { return err }
certificate, err := client.Submit(ctx, statement, grant)
if err != nil { return err }
registerBody := map[string]any{
"certificate_id": certificate.ID,
"manifest_base64": base64.RawURLEncoding.EncodeToString(manifestRaw),
"visibility": "private",
}
// Sign registerBody with the authorization construction below, then POST
// {certificate_id, manifest_base64, visibility, authorization} to /releases.
// Sign {policy} with purpose set_release_policy and POST /release-policies.
// Before use, hash the actual local artifact and call /release-policy/evaluations.
// To withdraw, sign {release_id, action:"withdraw", reason:"security_incident"}
// with purpose withdraw_release and POST /releases/{id}/status-events.
This authorization construction runs locally with public Core helpers; the
private key never enters a request. body is one of the bodies in section 4.
now := time.Now().UTC().Truncate(time.Second)
nonce, err := core.NewID()
if err != nil { return err }
bodyRaw, err := core.Canonical(body)
if err != nil { return err }
payload := map[string]any{
"profile": "https://ifandonlyif.io/apostille/supply-chain/authorization/0.1",
"purpose": purpose,
"audience": expectedIssuer,
"workspace_id": workspaceID,
"body_sha256": core.Hash(bodyRaw),
"issued_at": now.Format(core.TimestampLayout),
"expires_at": now.Add(5*time.Minute).Format(core.TimestampLayout),
"nonce": nonce,
}
payloadRaw, err := core.Canonical(payload)
if err != nil { return err }
header, err := core.NewHeaderFor(protocol, core.KindStatement,
core.KeyIdentity(admin.KeyID()), admin, now)
if err != nil { return err }
authorizationStatement, err := admin.SignFor(protocol, core.KindStatement, core.Statement{
Header: header, AgentID: workspaceID, DelegationSHA256: "",
ArtifactSHA256: core.Hash(payloadRaw), ArtifactSize: strconv.Itoa(len(payloadRaw)),
ArtifactMediaType: "application/vnd.iff.release-authorization+json", Nonce: nonce,
})
if err != nil { return err }
authorization := map[string]any{
"payload_base64": base64.RawURLEncoding.EncodeToString(payloadRaw),
"statement": authorizationStatement,
}
// Add authorization to the request without including it in body_sha256.
Use HTTPS to a configured origin with bounded requests and timeouts. Keep tokens
out of URLs, logs and command-line arguments. The public read route needs no
bearer token. Choose public only after reviewing the exact manifest and bundle.
9. Visibility, withdrawal and retries
Public release evidence needs both an explicit public registration authorization and a currently public certificate. Hiding the certificate hides its release; a public workspace does not publish a release. Public responses exclude private workspace/certificate mappings and administrator authorizations.
Withdrawal leaves manifest and certificate bytes unchanged and is kept privately as an immutable record. Downloaded copies stay with their holders.
The administrator nonce and canonical body hash identify retries. The same nonce with different content conflicts; an identical retry returns the original record. The authorization must still be valid on retry, and registration rechecks the certificate. Use fresh authorization for a new mutation. After a lost response, retrieve the existing record; never infer rollback from a timeout. Product/version collisions and skipped or stale policy versions are conflicts, not updates.
HTTP errors
Errors use the {error:true,code,message} envelope; base API authentication and
rate limits apply. An HTTP 200 blocked or unknown evaluation is not an error.
| HTTP | Release error codes | Action |
|---|---|---|
| 400 | invalid_release, invalid_release_manifest, release_certificate_mismatch, invalid_release_status_event, invalid_release_policy, invalid_release_evaluation, invalid_release_credential |
Correct the body, canonical encoding, certificate binding or credential scope/expiry before retrying. |
| 401 | invalid_ci_credential |
The CI bearer is invalid, expired or revoked; do not fall back to an administrator token in CI. |
| 403 | invalid_release_authorization, release_publication_denied, agent_inactive |
Obtain valid scoped administrator authorization or resolve the state; a bearer token does not bypass it. |
| 404 | certificate_not_found, release_not_found, release_policy_not_found, release_credential_not_found |
Check the identifier, workspace and CI scope. Private or out-of-scope records are not disclosed. |
| 409 | release_nonce_conflict, release_conflict, policy_version_conflict, release_already_withdrawn, release_credential_nonce_conflict, release_credential_conflict |
Read the existing state. A conflicting retry cannot overwrite it, change credential scope or restore a withdrawn release. |
| 429 | release_write_limit, release_rate_limit, release_credential_limit |
Respect any supplied Retry-After; reconcile active/history credential limits rather than retrying in a tight loop. |
| 503 | release_service_unavailable, apostille_service_unavailable, release_rate_limit_unavailable, release_credential_unavailable |
Treat the gate as unavailable; retry with bounded backoff and check state after an uncertain write. |
Initial store limits: 10,000 releases per workspace and 100 new release writes
per workspace per UTC day, counting registrations, policy versions and
withdrawals. Withdrawals bypass quota admission. Registration, policy creation
and evaluation share a per-IP release limit (APOSTILLE_RELEASE_RATE_LIMIT_PER_MINUTE,
default 60), separate from the sign-in challenge limit.
10. Threat model and operational acceptance
| Threat | Control and remaining limit |
|---|---|
| File substituted after approval | Manifest digest plus local byte verification detects the mismatch; the consumer must enforce the gate. |
| Wrong producer, issuer or product | Exact independent pins, product matching and current administrator policy. Embedded keys alone do not establish trust. |
| Stolen bearer session | A release mutation also needs a scoped administrator origin-statement over its exact body. |
| Stolen CI credential | Immutable workspace/product/policy scope and expiry limit access; revoke it to stop new requests. Scoped private evidence can still be read, and an in-flight request may finish. No signing or mutation authority is granted. |
| Replayed operation or policy rollback | Tenant-scoped nonces, immutable versions and latest-policy evaluation; offline copies need the receiver's own historical policy. |
| Publisher or trusted build compromised | Valid signatures can cover malicious bytes. This profile does not replace code review, isolated builds or signing-key controls. |
| Hardware substituted | A signed document cannot establish which physical device is in a box or provide hardware attestation. |
| Delayed withdrawal information | Hosted checks report one current snapshot; offline checks do not fetch revocation and cannot infer current authorization. |
Before deployment, run an up/down/up test of the release migration on an isolated database, run API/store tests with both Core versions, and verify private/public isolation, certificate hiding, concurrent version conflicts, policy rollback rejection and irreversible withdrawal. The public HTTP MCP profile stays read-only.
Run migration 12 before serving release requests; a generic health check does not cover the new tables, so verify a private release lifecycle afterwards. To roll back to an image with migrations only through 11, keep schema 12 and either provide the complete migration files or disable migration execution. Never run the destructive down migration on production release records.
The CI credential extension additionally requires migration 13 before enabling its routes. Verify clean up/down/up on an isolated database and test scope isolation, expiry, revocation, one-time token delivery and nonce replay. Preserve credential history on rollback; do not run the down migration on production credentials. Capability flags alone do not establish a completed rollout.
Maintainers with private-repository access: internal/supplychain/types.go
(profile types), profile.go (manifest and policy validation),
authorization.go (operation authorization) and
internal/apostille/supplychain_types.go (storage types).