Skip to content

Register the API-key marker with secret scanners and expose self-revoke #561

Open

Description

@alukach

Alongside. Starts on day one; the only item in the epic on someone else's clock.

Do

  • Choose a distinctive marker for ADR-013 API keys. A JWT begins eyJ like every other JWT, so scanner registration needs something to match — a string prefix the proxy strips before verification, or a claim-based pattern. The choice belongs in ADR: the Service Account principal; amend ADR-013 for service-account-bound keys data.source.coop#230.
  • Register it with secret-scanning partners (GitHub, and others as relevant) and implement their verification endpoint.
  • Expose a public self-revoke endpoint so a stranger who finds a leaked key can kill it without holding rights on the account.

Why now
Partner registration is a multi-week external process. The marker must be fixed before #548's schema. The legacy prefix was a bare SC (src/lib/actions/crypto.ts), far too low-entropy to register.

Blocks #548.


Part of #491.

Activity

  1. alukach commented on Sep 29, 2026

    @alukach
    ContributorAuthor

    Registration log: secret scanners

    What each scanner needs to recognise a Source Cooperative API key, and the steps to register with it. Keep this comment current: when the key format or an endpoint changes, update it and every registration below. (Written 2026-09-29.)

    The key format, as registered

    A key is sck_, 30 random base62 characters, and six more that are their checksum: 40 characters in all. The checksum is the CRC-32 of the 30 random characters (IEEE, as zlib computes it), written in base62 with the digits 0-9A-Za-z, most significant first, left-padded with 0 to six. This is GitHub's own token layout. It is defined by ADR-013 (source-cooperative/data.source.coop#242) and in code by API_KEY_PATTERN and isApiKey in src/types/service-account-key.ts (#596).

    • Regular expression: sck_[0-9A-Za-z]{36}, anchored in our code as ^sck_[0-9A-Za-z]{36}$. It should not match inside a longer run of letters and digits, so its boundaries are the start or end of the text or any character outside [0-9A-Za-z].
    • Reference check, for any scanner that can run one:
    import zlib
    
    ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    
    def is_source_coop_api_key(token: str) -> bool:
        if len(token) != 40 or not token.startswith("sck_"):
            return False
        body, checksum = token[4:34], token[34:]
        if not all(c in ALPHABET for c in body + checksum):
            return False
        n, digits = zlib.crc32(body.encode()), ""
        for _ in range(6):
            n, r = divmod(n, 62)
            digits = ALPHABET[r] + digits
        return digits == checksum
    • Test vectors (random part → checksum): 30 × a → 1yLcDB; 0123456789ABCDEFGHIJabcdefghij → 4Us3aw (a CRC above 2^31); 29 × 0 then 1 → 010Ohw (keeps its leading zero). A key is sck_ + random part + checksum; none is spelled out here, so this issue doesn't trip the scanners it describes.
    • Our own repositories assemble their test keys at run time or from pieces, so they contain no whole key for a scanner to report.

    GitHub secret scanning partner program

    Docs: https://docs.github.com/en/code-security/tutorials/secret-scanning-partner-program

    Send to secret-scanning@github.com:

    GitHub asks for Ours
    Secret type name Source Cooperative API Key (type identifier source_coop_api_key)
    Regular expression sck_[0-9A-Za-z]{36}, with the checksum above for GitHub to apply if it can
    Test account A Source Cooperative login that manages a service account, so GitHub can issue its own keys (see the steps)
    Endpoint URL https://source.coop/api/v1/secret-scanning/github

    What the endpoint does (#581): it takes GitHub's POST, checks Github-Public-Key-Signature (base64 DER ECDSA, P-256 with SHA-256, over the raw body) against the key Github-Public-Key-Identifier names at https://api.github.com/meta/public_keys/secret_scanning, and answers 401 to anything it can't verify. It revokes every reported token that passes isApiKey and answers 200 with [{token_raw, token_type, label}], true_positive for a key that exists, whether it was revoked now or before, and false_positive for one that doesn't. Tokens failing the pattern or checksum are left out without a lookup. Each revocation is logged at warn with via: "github" and the url and source where GitHub found it.

    Steps

    • Merge and deploy feat(accounts): end API keys in a CRC-32 checksum #596 (the key format) and feat(accounts): revoke leaked API keys via self-revoke and GitHub secret scanning #581 (the endpoint). feat(sts): exchange opaque API keys at /.sts by hash lookup data.source.coop#235 makes keys exchangeable; registration doesn't need it, but a leaked key only matters once it does.
    • Create the test account: an individual account with a password login on a shared mailbox, owning a service account that holds no memberships, so a key GitHub issues reaches nothing. Send its credentials to GitHub privately, never in this issue.
    • Email secret-scanning@github.com with the table above.
    • Ask GitHub:
      • whether it redelivers a report our endpoint refuses with 401. An instance fetches GitHub's public keys at most once every ten minutes, so a report signed with a newly published key can be refused until that window passes;
      • to read our feedback labels (token_raw + label);
      • whether it can apply our checksum to its matches, which would cut false positives before they reach us;
      • whether the pattern will be push-protected.
    • Once enrolled, issue a key from the test account, commit it to a public test repository, and confirm the endpoint logs via: "github" and the key's row shows it revoked. Note the date here.
    • If the key format ever changes, update the registration before issuing keys in the new format.

    GitLab leaked-credential partner program

    Docs: https://docs.gitlab.com/user/application_security/secret_detection/automatic_response/

    GitLab notifies partners of credentials leaked in public projects on GitLab.com; interest goes through GitLab epic 4944. It posts [{type, token, url}] signed with Gitlab-Public-Key-Signature (ECDSA with SHA-256) against GitLab's published keys, and retries on any status of 400 or above, so the endpoint must answer 2xx once it has processed a batch. The partner's API must be idempotent (revocation already is) and rate-limited (the Vercel WAF follow-up on #581).

    gitleaks

    Docs: https://github.com/gitleaks/gitleaks/blob/master/CONTRIBUTING.md

    An open-source scanner run locally and in CI. A rule is a pull request to gitleaks/gitleaks.

    • Add cmd/generate/config/rules/sourcecoop.go using GenerateUniqueTokenRegex (the sck_ prefix is distinctive) with keyword sck_, true positives from generateSampleSecret, and false positives such as the checksum-less 47-character format. Add it to configRules in cmd/generate/config/main.go, run make config/gitleaks.toml, and open the PR.

    TruffleHog

    Docs: https://github.com/trufflesecurity/trufflehog/blob/main/hack/docs/Adding_Detectors_external.md

    A detector is a pull request to trufflesecurity/trufflehog, and it must verify candidates against a live, non-destructive API call.

    • Add a DetectorType to proto/detector_type.proto, run make protos and go run hack/generate/generate.go detector <DetectorType>, register the detector in pkg/engine/defaults/defaults.go, and implement the regex, the checksum and at least five tests.
    • Verification: a form-body POST to https://data.source.coop/.sts with Action=AssumeRoleWithWebIdentity, RoleArn=_default and the key as WebIdentityToken. A 200 means the key is live; InvalidIdentityToken "API key was not accepted" means it's revoked, expired or unknown; "API key is malformed" means it isn't a key. Never verify through the self-revoke route, which revokes the key. A verified exchange updates the key's "last used" in the UI. This needs feat(sts): exchange opaque API keys at /.sts by hash lookup data.source.coop#235 deployed.

    Not yet looked into

    GitGuardian and Yelp's detect-secrets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions