Onceproof is a small, self-hosted issuer and one-time verifier for opaque application proofs.
A trusted issuer can mint a short-lived proof. A separate verifier can
accept that proof exactly once, only for its bound client, subject, purpose,
and audience. The service persists the consume decision before returning
verified: true.
Onceproof does not solve CAPTCHAs, establish human presence, replace an identity provider, or issue tokens for third-party systems. It transports a decision made by your trusted issuer across a narrow, replay-resistant boundary.
One-time verification needs a shared replay ledger even when a token is signed. Onceproof therefore uses a 256-bit opaque bearer value and gives the ledger one job: move it atomically from active to consumed.
- Raw proofs and API credentials are never stored in SQLite.
- Issuance and verification use different credentials.
- Proofs are bound to
client_id,subject,purpose, andaudience. - Expiry and consume are checked under
BEGIN IMMEDIATE. - Exact verifier retries can use an idempotency key without reopening replay; the original decision is retained for at least 24 hours.
- Per-client issuer and verifier quotas are persisted in the same ledger and survive credential rotation.
- Receipts say
issued; only a successful verifier response saysverified_once.
Requires Python 3.11 or newer.
git clone https://github.com/HOLYAC/onceproof.git
cd onceproof
python -m pip install .
onceproof init ./onceproof-instance
onceproof check --config ./onceproof-instance/onceproof.tomlCreate a client. The credential file is made durable before its database rows are activated, is created once, and is never overwritten. Raw credential delivery to stdout is refused. POSIX installs use owner-only mode; Windows operators must apply the service account ACL described in the operations guide:
onceproof client create \
--config ./onceproof-instance/onceproof.toml \
--name checkout \
--audience https://api.example.test/checkout \
--credentials-out ./checkout.credentials.jsonStart the loopback-only reference server:
onceproof serve --config ./onceproof-instance/onceproof.tomlIssue a proof with the opi1 credential:
curl --fail-with-body http://127.0.0.1:8787/v1/proofs \
-H 'Authorization: Bearer <ISSUANCE_SECRET>' \
-H 'Content-Type: application/json' \
-d '{"subject":"user_123","purpose":"checkout","audience":"https://api.example.test/checkout"}'Verify it with the distinct opv1 credential:
curl --fail-with-body http://127.0.0.1:8787/v1/verifications \
-H 'Authorization: Bearer <VERIFICATION_SECRET>' \
-H 'Idempotency-Key: checkout-42' \
-H 'Content-Type: application/json' \
-d '{"proof":"<PROOF>","subject":"user_123","purpose":"checkout","audience":"https://api.example.test/checkout"}'The first matching verification returns verified_once. A second request
without the original idempotency key returns replay_rejected.
Safe defaults are deliberate:
onceproof initcreates a separate keyring file and no demo client.- Initialization creates the schema explicitly; later startup refuses a missing or empty database instead of silently creating a new ledger.
- The default bind is
127.0.0.1. - A non-loopback bind is rejected unless
allow_public_bind = trueis set. - The single-worker Uvicorn server does not terminate TLS. Put it behind a TLS reverse proxy before any network exposure.
- An instance lock enforces one HTTP process per SQLite database and blocks root-key mutation while that process is running.
- Back up the database and keyring together. Neither is sufficient alone.
See Operations for rotation, backup, cleanup, and incident commands. See Threat model before deploying. Maintainers use the executable release procedure.
Inspect the live instance without exposing secrets:
onceproof doctor --config ./onceproof-instance/onceproof.toml
onceproof key inspect --config ./onceproof-instance/onceproof.toml
onceproof db integrity-check --config ./onceproof-instance/onceproof.tomlThe OpenAPI 3.1 contract is shipped at
src/onceproof/openapi.json. Tests drive real
ASGI requests, validate bodies and security headers against it, compare the
published status matrix, and reject false boundary upgrades.
The two public commands are:
POST /v1/proofs: authenticated with an issuer credential.POST /v1/verifications: authenticated with a verifier credential.
Health and readiness are intentionally separate:
GET /healthz: the HTTP process is alive.GET /readyz: the database and current key are usable.
Rotate one role without exposing or changing the other:
onceproof client rotate \
--config ./onceproof-instance/onceproof.toml \
--client-id <CLIENT_ID> \
--role verification \
--grace-seconds 300 \
--credentials-out ./verification.rotation.jsonRevoke an entire client, both credentials, and its outstanding proofs:
onceproof client revoke \
--config ./onceproof-instance/onceproof.toml \
--client-id <CLIENT_ID> \
--yes-revokeRoot hash-key rotation is two-phase. Add a new current key, rotate client credentials, wait for old proofs and retention to expire, run cleanup, then retire the unused key. Retirement fails while any database row still depends on it.
Create a consistent database backup with SQLite's backup API:
onceproof db backup \
--config ./onceproof-instance/onceproof.toml \
--output ./onceproof-backup
onceproof db verify-backup --input ./onceproof-backupThe command holds the keyring lifecycle lock while SQLite snapshots and emits
the database, matching keyring, and hash manifest into one private directory.
The manifest is published last; an interrupted directory is reconciled on
retry. A restored snapshot cannot serve until db activate-restore
irreversibly revokes every authority carried by that older point in time.
The image initializes /var/lib/onceproof/instance on its first server start,
binds 0.0.0.0:8787 only through the explicit public-bind initialization flag,
and exposes a readiness health check. Mount /var/lib/onceproof as a durable,
encrypted volume:
docker build --file Containerfile --tag onceproof:release .
docker run --name onceproof -p 8787:8787 \
-v onceproof-data:/var/lib/onceproof \
onceproof:releaseTLS and source-rate limiting still belong at the reverse proxy.
python -m pip install -e ".[test]"
python -m unittest discover -s tests
coverage run -m unittest discover -s tests
coverage report
python -m build
python -m twine check dist/*The core ledger uses the Python standard library. Uvicorn is the only runtime
dependency; jsonschema, coverage, and release tooling are test-only.
0.1.0a1 is an alpha single-node reference implementation. Its security claims
are bounded to the executable invariants in the test suite and threat model.
Please report vulnerabilities through GitHub private vulnerability reporting,
not a public issue.
Apache-2.0 licensed.