A workrr certificate is an in-toto Statement v1
inside a DSSE envelope, signed keylessly through Sigstore and recorded in the public
Rekor transparency log. This document is the contract every implementation (TypeScript,
Go) must follow. Two implementations given the same artifact, policy, and ruleset must
produce byte-identical checks.
- Predicate type:
https://trust.workrr.ai/certificate/v0.1 - JSON Schema: predicate-v0.1.schema.json
- Design rationale: docs/design.md
1. Artifact digest
The subject digest is always sha256.
- Single file: sha256 of the file's bytes.
predicate.artifact.digestAlgorithm = "file-sha256". - Directory:
workrr-dirhash-v1:- Walk the directory recursively. Skip any path segment named
.git. Don't follow symlinks. A symlink is hashed as its link-target string. - For each regular file, compute
sha256(bytes)as lowercase hex. - Relative paths use
/separators and are sorted by UTF-8 byte order. - Build the manifest: one line per file,
<hex> <path>\n(two spaces). - The digest is
sha256(manifest).
- Walk the directory recursively. Skip any path segment named
The subject name is the artifact's declared name (SKILL.md name, server.json
name) or else the directory/file basename.
2. Artifact type
Detected in this order: SKILL.md present → skill; server.json present →
mcp-server; AGENTS.md or CLAUDE.md present → agent-context; else other.
A single file named SKILL.md, AGENTS.md, or CLAUDE.md maps the same way.
3. Checks
Every check records exactly what was used, so anyone can re-run it.
| Check | Result values | Recorded inputs |
|---|---|---|
provenance |
pass · fail |
source URI, commit, path; whether the artifact is unchanged from that commit |
policy |
pass · fail |
policy digest (sha256 of canonical JSON), violations |
scan |
pass · fail |
ruleset version + digest, findings (rule id, severity, file, line) |
tests |
pass · fail · none |
command, exit code, duration |
Scan wording. A pass means "no known-pattern findings of severity high or
critical." It never means safe or injection-free. Findings of severity warn are
recorded and don't fail the check.
Canonical JSON. Object keys sorted lexicographically, no whitespace, UTF-8.
Precisely: keys sorted by UTF-8 byte order; strings and numbers serialized as
ECMAScript JSON.stringify does (only ", \ and U+0000–U+001F are escaped;
1e+21, 1e-7, -0 → 0); absent optional fields are omitted, never null.
Determinism rules. Gold compares checks byte for byte, so these are part of the
contract, not implementation details:
- Scan input. Files containing a NUL byte are skipped. Other files are decoded as UTF-8 with WHATWG replacement (one U+FFFD per maximal invalid subpart; a BOM is kept). Symlinks are read through to their target; unreadable files are skipped.
- Scan patterns are ECMAScript regular expressions, run with their flags plus
m. Withoutu, they operate on UTF-16 code units. A finding's line is 1 + the number of\nbefore the match start; each rule reports at most one finding per line. - Findings order: file (UTF-8 byte order), then line, then rule id.
- SKILL.md frontmatter is the YAML between a leading
---line (an optional leading UTF-8 BOM U+FEFF is ignored) and the next---line, parsed with the YAML 1.2 core schema (no merge keys; duplicate keys are a parse error). A frontmatter field counts only if it is a non-blank string. - Symlinks inside an artifact directory must resolve within the artifact root; any symlink whose target escapes the artifact directory fails the policy check and is not read by the scanner.
Cross-implementation test vectors live in test-vectors/; every implementation must pass them.
4. Level
Display names say how much scrutiny each level means: Scanned (scanned),
Signed (banded), Tested (silver), Reproduced (gold). The wire values
below are what certificates carry.
| Level | Rule |
|---|---|
silver |
provenance, policy, scan, and tests all pass |
banded |
provenance, policy, scan pass; tests none (not deterministically testable). Receives a deterministic A–E trust band (predicate.band). |
scanned |
a public scan (docs/public-scans.md): an observer certifier run by workrr in its own repository read a public commit. Provenance, policy, scan pass; tests always none (a scan never runs the project's code). Not signed by the maintainer. Carries the same A–E band as banded. Ranks below banded. |
| (no certificate) | any check fail — the certifier exits non-zero and nothing is signed as a certificate. A public scan that fails a security check signs a finding instead (§8). |
gold |
Assigned only by verifiers, never by certifiers: two silver certificates for the same digest, signed by accepted certifier identities on two different forges, with identical checks. |
Trust bands (A–E). When level is banded, the certifier computes a deterministic
A–E grade over four intrinsic signals (1 point each; 4 → A, 3 → B, 2 → C,
1 → D, 0 → E), unless overridden by external outcome evidence:
- Zero scan warnings:
scan.findingshas nowarn-severity findings. - Scoped tools:
SKILL.mddeclaresallowed-toolsor the policy pinsallowedTools. - Structured instructions: for a
skill,descriptionstates when to trigger (use when) and the body has a Markdown heading or numbered step (1.); for other types, a top-levelREADMEorREADME.mdis present. - Declared license: a top-level
LICENSE/LICENCE/COPYINGfile or a non-blanklicensefrontmatter field is present.
5. Signing and logging
- Certificates are signed keylessly with Sigstore Fulcio from the certifier's CI OIDC identity, and recorded in public Rekor.
- On GitHub the identity is the reusable workflow
https://github.com/workrr-trust/certify/.github/workflows/certify.yml@<ref>, with Build Signer Digest (OID1.3.6.1.4.1.57264.1.10) =job_workflow_sha. For an annotated tag that is the tag object, not the peeled commit. - The built-in trust policy accepts only
certify.yml@refs/tags/v0.1.0(digest9fbe3e453cbd80bef0301f08db79408bb12c7d64) andcertify.yml@refs/tags/v0.1.1(digest78d8c1a98c929b7b1c1b532c25256582a84887b1).refs/heads/mainis not accepted. No GitLab signer is listed.
6. Verification
A verifier must:
- Recompute the artifact digest (§1) and match it to the Statement subject.
- Check
predicateTypeis a supported version. - Verify the Sigstore bundle: certificate chain, signature, and Rekor inclusion.
- Match the signer identity (SAN, issuer, Build Signer Digest) against the trust policy.
A SAN of
refs/heads/main, an unlisted tag, or a digest other than the one pinned for that tag is rejected. The policy embedded in this build lists thecertify.ymlrelease tags and thescan.ymlobserver tag (src/trust-policy.ts). - Cross-check
source.uriandsource.commitin the predicate against the signing certificate's FulcioSourceRepositoryURI(1.3.6.1.4.1.57264.1.12) andSourceRepositoryDigest(1.3.6.1.4.1.57264.1.13) extensions when present, and derive the certified level (silverorbanded) directly from the signedchecksresults rather than trustingpredicate.levelalone. Observer signers (trust policy"role": "observer", the public-scan workflow) are the exception: their FulcioSourceRepositoryURImust behttps://github.com/workrr-trust/certify,predicate.levelmust bescannedandchecks.tests.resultmust benone. The predicate'ssourceis then the observed repository and is not cross-checked against Fulcio. Such a bundle is reported asscannedonly when no publisher bundle verifies, and it never counts towardgold. A publisher signer's certificate that claimsscannedis rejected. - Read
checks.policy.policyNameandchecks.policy.policyDigestand report both. The nameworkrr-baseline-v0.1is reserved: that name with any other digest is rejected. Any other digest is not a baseline result, and the verifier must not reportsilver,gold, orbandedfor it. A signed trust policy'sbaselinePolicyDigestsmap is an additional allow-list when that document is in use; it does not relax this rule. - If the caller passes a source repository, it must match
source.uriafter normalizing.git, embedded credentials, andgit@host:org/repo.owner/repoandhost/owner/repomatch that URL. A mismatch is a failed verification and is not reported at a baseline level. - Check the certificate isn't revoked (§7).
--offlineskips this and must say so. - For
gold: repeat 1–7 for a second bundle from a different forge, require both policies to be the known baseline, and comparechecksbyte-for-byte as canonical JSON. The built-in policy has no second forge, so it does not award gold.
7. Signed trust policy and revocation
The trust policy is a versioned JSON document. Verifiers fetch it, check its signature, and cache it. The service that hosts it is not a trust anchor.
7.1 Policy document
Media type of the signed bytes: application/vnd.workrr.trust-policy+json.
The bytes are canonical JSON (§3) with no trailing newline.
| Field | Rule |
|---|---|
schema |
workrr-trust-policy-v1 |
sequence |
Integer ≥ 1. A verifier rejects a document whose sequence is lower than the sequence embedded in that build or lower than a sequence it has already verified. |
issuedAt |
YYYY-MM-DDThh:mm:ssZ |
signers |
Non-empty list of certifier identities: forge (github or gitlab), issuer, subjectAlternativeName (regular expression), buildSignerDigests (list of 40-hex commits; empty means any commit of that workflow). |
baselinePolicyDigests |
Map of policy name → sha256: + 64 hex. When a signed policy is in use, a certificate whose checks.policy.policyDigest is outside this map fails. |
revocationFeed |
https URL of the revocation collection, with no /since suffix. |
revocationWatermark |
Integer ≥ 0. Entries 1..watermark existed when this policy was signed. |
revocationHead |
64-hex sha256 of the canonical entry at revocationWatermark. Sixty-four zeros when the watermark is 0. |
sequence only goes up. An older signed document is a rollback:
ROLLBACK: trust policy sequence 1 is older than 3
The same rule applies to a revocation checkpoint sequence the client has already verified.
7.2 Signature
The on-wire envelope is application/vnd.workrr.signed-blob+json;version=1:
{
"mediaType": "application/vnd.workrr.signed-blob+json;version=1",
"payloadType": "application/vnd.workrr.trust-policy+json",
"payload": "<standard base64 of the canonical document>",
"signature": { "algorithm": "sigstore", "bundle": { } }
}
payload is standard base64 (with padding) of the exact bytes that were signed.
Those bytes must be canonical JSON. The signature is over those bytes, not over
the envelope.
Production signatures are Sigstore bundles from cosign sign-blob --bundle,
made by a release-tag run of:
.github/workflows/trust-policy.ymlfor a policy.github/workflows/revoke.ymlfor a revocation entry or checkpoint
Both identities use issuer https://token.actions.githubusercontent.com and a
SAN of
https://github.com/workrr-trust/certify/.github/workflows/<workflow>.yml@refs/tags/vX.Y.Z
refs/heads/main is not accepted. On a revocation entry or checkpoint the
document's signer field must equal that certificate SAN.
The shared test vectors sign with ed25519 instead, so both implementations can check tamper and rollback without Fulcio. Production verifiers do not accept that key.
7.3 Revocation entry
Payload type application/vnd.workrr.revocation-entry+json. Schema
workrr-revocation-v1.
| Field | Rule |
|---|---|
sequence |
Integer ≥ 1, contiguous from the previous entry. |
subject |
Exactly one of artifactDigest (64 lowercase hex) or logIndex (decimal Rekor index, no leading zeros). |
reason |
One line, 1–200 characters, no ASCII controls. |
timestamp |
YYYY-MM-DDThh:mm:ssZ |
signer |
https URI of the identity that signed the entry. |
prev |
64-hex sha256 of the previous entry's canonical JSON, or 64 zeros for sequence 1. |
A checkpoint (application/vnd.workrr.revocation-checkpoint+json, schema
workrr-revocation-checkpoint-v1) has sequence, head (sha256 of the entry
at that sequence), timestamp, and signer. It is signed the same way.
A client that loads a policy with watermark w and head h fetches
<revocationFeed>/since/<w> and requires:
- every entry and the checkpoint to verify;
- entry sequences
w+1, w+2, …withprevchaining from h; - the checkpoint
sequenceandheadto match the last entry; - the checkpoint sequence not to go backwards.
A match fails that bundle with REVOKED: <reason>. If every otherwise-valid
bundle is revoked, the result status is REVOKED.
A feed that answers but does not verify fails closed. A feed that cannot be
reached does not. The client adds revocation was not checked.
7.4 Fetch, cache, offline
GET <trust-api>/v1/policy, then the revocation feed above.- A verified policy is cached for one hour. Revocations are fetched on each verification.
- If the policy cannot be fetched and a previously verified cache entry exists,
the cache is used even past the TTL, with
trust policy fetch failed; using cached trust policy. - If there is no cache, the policy embedded in the binary is used, with
trust policy fetch failed; using the embedded trust policy. The embedded policy's sequence is the floor. It starts at 0 until a release sets it. - A policy whose signature does not verify is not used. The embedded policy is
used, with
trust policy signature invalid; using the embedded trust policy. --offlinedoes not contact the trust API. It uses the embedded policy, does not check revocation, and prints:
offline: using the embedded trust policy; revocation was not checked
A revoked certificate can still verify under --offline. Passing
--trust-policy a legacy {version, signers} file is a local override; the
signature is not checked and the command says so. A signed blob passed that way
is verified.
The host (https://trust-api.workrr.ai, or WORKRR_TRUST_API) is transport.
Replacing it changes availability, not who can sign.
8. Findings
A public scan that fails a high or critical scan rule, or the policy's forbidden
file type or escaping symlink rules, signs an in-toto Statement with predicate type
https://trust.workrr.ai/finding/v0.1. Its predicate carries artifact, source
and checks as in §3 (tests always none), plus kind (concealment when any
failing rule is WT002, WT003, WT007 or WT008; otherwise check), findings (one
item per failure: check, rule, severity, file, line where applicable,
detail), certifier and scannedAt.
A finding is valid only from an observer signer whose Fulcio source repository
is workrr-trust/certify. To accept one, a verifier checks the signature and Rekor
inclusion as in §6, then reproduces it: it runs the same checks on the named commit
and requires identical findings and kind under the same ruleset and policy
digests. Certificate verifiers reject this predicate type. Format and policy:
docs/findings.md.
Versioning
Breaking changes bump the predicate type URI. Verifiers must reject unknown predicate types.