DAY 002 / TURBOPASS

TurboPass: removing the FFI bridge without changing the cryptography

How I rebuilt a Privacy Pass-style token server in Rust, targeted its documented API and storage contracts, and moved issuer-key rotation into durable Temporal workflows.

01 / THE REBUILD BRIEF

I started with “the per-thread bridge thingy.”

I had worked on Brave’s Challenge Bypass server before, and I remembered a constraint at the boundary between its Go service and Rust Ristretto implementation. I did not remember the precise mechanism. That uncertainty became the first research task—not a license to rewrite the cryptography.

The brief was deliberately conservative: create a new repository, preserve the documented deployed API and storage contract—with explicit gaps—use the same Rust cryptography directly, and move issuer-key rotation into Temporal’s Rust SDK. The existing server clone would remain untouched.

The implementation contractyaml
keep:
  - documented v1 / v2 / v3 HTTP behavior, with listed gaps
  - PostgreSQL and DynamoDB records
  - token, key, proof, and signature encodings
  - challenge-bypass-ristretto 2.1.0

replace:
  - Go + cgo server runtime → native Rust
  - in-process rotation cron → Temporal Schedule + worker

do_not:
  - redesign the protocol
  - move all state into a new database

02 / REPOSITORY ARCHAEOLOGY

The real system was wider than “a server backed by DynamoDB.”

I traced behavior at legacy server commit 6991552, rather than treating the original Privacy Pass sample as the current contract. Brave’s fork had accumulated three HTTP generations, PostgreSQL issuer and key state, two redemption stores, authentication, metrics, Kafka/Avro paths, and two rotation loops.

My prior familiarity with the repository is also public: in upstream commit 3b20a67, I hoisted issuer retrieval out of the redemption-item loop. That establishes a specific earlier contribution to the server; it does not imply authorship of the FFI, replay design, or this Temporal architecture.

That changed the architecture immediately. “Use the same DynamoDB” was only partly true: DynamoDB held v2/v3 replay records, while PostgreSQL held issuers, signing keys, and v1 redemption records. Moving all of that state would have broken direct coexistence with the Go service and required a separate replication and cutover plan.

BRIDGE BASELINE

Ristretto FFI

The Rust C exports, Go wrappers, thread-local error slot, and exact direct crate pin.

CRYPTOGRAPHIC BASELINE

Ristretto 2.1.0

The unchanged signing, proof, encoding, derivation, and verification implementation.

ORCHESTRATION BASELINE

Temporal Rust SDK

Durable workflow/activity execution and Schedules, with a Public Preview caveat.

03 / THE BRIDGE

The thread restriction belonged to error transport, not Ristretto.

The Rust FFI crate stores its latest error in a thread_local! LAST_ERROR slot. On a sentinel failure, an exported operation can set that slot and Go makes a separate C call to retrieve and clear the message. A goroutine is not guaranteed to resume on the same operating-system thread between those calls.

The Go wrapper therefore pins each fallible operation with runtime.LockOSThread and later unlocks it. The pinned FFI version contains 38 such lock/unlock pairs across 38 exported Go crypto wrapper operations. Batch issuance can cross that boundary repeatedly for signing and proof work; rotation crosses it for key generation, serialization, and public-key derivation.

Condensed legacy bridge control flowGo · source
// Condensed control flow—not the cryptographic algorithm.
runtime.LockOSThread()
defer runtime.UnlockOSThread()

result := C.fallible_crypto_call(...)
if result == nil {
    // A second C call must read Rust's thread-local LAST_ERROR
    return wrapLastError()
}
Legacy FFI bridge compared with TurboPass native Rust callsThe legacy path pins a Go goroutine to an operating-system thread, crosses a C interface into Rust, and on a sentinel failure makes a second call to read and clear a thread-local error. TurboPass calls the same Rust cryptography crate directly and receives a Result.BEFORE · GO + CGO + RUSTGO WRAPPERLockOSThreadC ABIcrypto callRUST FFILAST_ERROR on failureC ABIread + clear errorsentinel failurefailure path stays on one OS thread until read + clearAFTER · TURBOPASSRUST SERVICEdirect function callSAME CRYPTO CRATEResult<T, CryptoError>no C ABI · no cgo · no thread-local error shuttle
The bridge removed—not the cryptographyOn a sentinel failure, the legacy bridge needed a second C call on the same native thread to read and clear Rust's thread-local error. TurboPass calls the same cryptography crate directly and receives a normal Rust Result.

The two-call mechanism is visible in the pinned Rust error slot and accessors, the Go error retrieval helper, and the representative thread-pinned wrappers.

04 / THE NATIVE BOUNDARY

Removing the bridge meant calling the same crate directly.

TurboPass pins challenge-bypass-ristretto to exactly 2.1.0—the version used behind the legacy FFI package. Normal Rust Result values now carry errors. There is no C ABI, cgo transition, opaque FFI ownership wrapper, finalizer, or thread-local error shuttle in the service path.

Cargo.tomlTOML · source
[dependencies]
challenge-bypass-ristretto = { version = "=2.1.0", features = ["base64"] }
Native signing and batch-proof constructionRust · source
let signing_key = decode_signing_key(encoded_signing_key)?;
let signed_tokens = blinded_tokens
    .iter()
    .map(|token| signing_key.sign(token).map_err(CryptoError::Sign))
    .collect::<Result<Vec<_>, _>>()?;

let mut rng = OsRng;
let proof = BatchDLEQProof::new::<Sha512, _>(
    &mut rng,
    blinded_tokens,
    &signed_tokens,
    &signing_key,
)
.map_err(CryptoError::CreateProof)?;

“Direct” does not mean “unbounded.” Curve work is synchronous and CPU-heavy, so the API moves it onto Tokio’s blocking pool behind a semaphore sized to advertised host parallelism. A timed-out request cannot cancel curve work already running; its permit stays attached until that operation actually finishes.

Bounded synchronous cryptographyRust · source
let permit = crypto_slots.acquire_owned().await?;

tokio::task::spawn_blocking(move || {
    // The permit stays with the synchronous work until it really finishes.
    operation().map(|result| (permit, result))
})
.await?

05 / TOKEN LIFECYCLE

Blinding stays local; issuance and redemption are synchronous.

After an issuer exists, there are two kinds of public request and no polling ID. The client creates and blinds tokens locally, then sends one batch issuance request. TurboPass returns the signed blinded tokens, public key, and batch proof in the response. The client verifies that proof, unblinds locally, and constructs the redemption signature.

Public API lifecycleHTTP
# 1. The client blinds a batch locally.

# 2. One synchronous issuance call returns signed tokens + proof.
POST /v1/blindedToken/{type}  # v1
POST /v2/blindedToken/{type}  # v2 and compatible v3 issuance

# 3. The client verifies the proof and unblinds locally.

# 4. One synchronous redemption call per token.
POST /v1/blindedToken/{type}/redemption  # v1 and v2
POST /v3/blindedToken/{type}/redemption  # v3

Redemption is one request per token. A 32-token batch therefore uses one issuance call followed by 32 redemption calls. Temporal does not participate in any of them; it rotates the issuer keys that the API later reads.

TurboPass token issuance and redemption sequenceThe client blinds tokens locally, sends one issuance request, verifies and unblinds the response locally, then sends one redemption request for each token. TurboPass records replay state in PostgreSQL for version one or DynamoDB for versions two and three.CLIENTTURBOPASSSTOREcreate + blind locallyone batch issuance POSTsigned tokens + public key + proofverify proof + unblindsign redemption locallyredemption POST × tokenconditional replay marker200 or replay conflictPostgres v1 · DynamoDB v2/v3
One batch issuance, then one redemption per tokenIssuance and redemption are synchronous public API operations. Blinding, proof verification, and unblinding stay on the client; there is no polling ID or Temporal workflow in the request path.
v1 / v2 duplicate
A replay is a conflict and returns HTTP 409.
v3 same binding
The same token and payload are idempotent and return HTTP 200.
v3 changed binding
Reusing the token for a different payload returns HTTP 409.
Test-helper routes
/prepare and /complete belong to the private load helper, not the public TurboPass API.

06 / COMPATIBILITY CONTRACT

Compatibility meant observable behavior, not similar endpoint names.

The implementation targets the documented v1, v2, and v3 route bodies, statuses, optional trailing slashes, production bearer authentication, the 1 MiB request-body limit, and the 60-second request timeout. It also preserves a non-obvious route: v3 issuance still uses the compatible v2 blinded-token endpoint, then v3 has its own redemption endpoint. The decoder and Kafka gaps below remain explicit exceptions.

BoundaryPreserved contract
HTTPv1/v2/v3 paths, JSON fields, status codes, trailing slash, auth, limits
Cryptosame crate/version, encodings, proof behavior, derivation order, identity rejection
PostgreSQLexisting issuer, key, and v1 redemption tables; year-one sentinel normalized
DynamoDBsame UUID derivation, attributes, TTL, conditional insertion, fallback reads
Rotationsame key-window semantics, now with locks, bounded retries, and poisoned-issuer isolation

Reproducing behavior exposed server bugs that could be repaired without changing the wire or protocol. A v3 issuer with zero keys could be skipped forever because MAX(end_at) returned NULL. A NULL last_rotated_at could starve legacy issuers. The Go service wrote an omitted expiry as SQL year one while treating that value as “no expiry” elsewhere.

01

Zero-key issuers self-heal

Rotation starts at the greatest of valid_from, the activity cutoff, and any historical horizon instead of backfilling expired windows.

02

Legacy expiry semantics are normalized

Existing year-one timestamps become no-expiry at the read boundary; new writes use SQL NULL.

03

Rotation cannot allocate an absurd horizon

A configured buffer + overlap above 4096 keys fails that issuer before allocation or cryptographic work exhausts the worker.

04

“Overlap” keeps its legacy meaning

It creates additional contiguous key windows. Renaming it to mean overlapping time intervals would break deployed behavior.

07 / STORAGE + REPLAY

PostgreSQL owns keys; DynamoDB stores v2/v3 replay markers.

TurboPass applies repeatable create-if-missing compatibility SQL, then uses the legacy-compatible table and attribute contract. That allows Go and Rust processes to observe the same issuer horizon during a canary instead of creating two systems of record.

Preserved persistence splittext
PostgreSQL
├── v3_issuers       # issuer configuration
├── v3_issuer_keys   # signing and public keys
└── redemptions      # v1 replay records

DynamoDB
├── configured primary table   # v2/v3 writes + reads
└── optional legacy table      # coexistence reads only
    shared item contract:
    ├── id            (partition key, UUID v5)
    ├── issuerId
    ├── preImage
    ├── timestamp
    ├── payload       (S, or NULL when empty)
    ├── TTL           (case-sensitive numeric attribute)
    └── offset

DynamoDB replay prevention is not a read-then-write transaction. In one table and Region, a conditional PutItem with attribute_not_exists(id) admits one writer for that exact partition key while the marker exists. On a conditional failure, the existing payload distinguishes an equivalent v3 binding from reuse against a different payload. Multi-Region Global Table behavior would require separate validation.

Conditional replay-marker insertionRust · source
client
    .put_item()
    .table_name(table)
    .set_item(Some(item))
    .condition_expression("attribute_not_exists(id)")
    .return_values_on_condition_check_failure(AllOld)
    .send()
    .await

Reads try the configured primary table and fall back to the legacy table only after a miss. Before a primary write, TurboPass performs a strongly consistent legacy read so a historical token cannot simply be replayed into the new table. The read and write cannot be atomic across two tables: a legacy writer can land after the pre-read. The cutover must fence and drain legacy writers, verify every writer uses the primary table, and only then enable TurboPass writes.

08 / TEMPORAL’S JOB

Temporal owns rotation, not the public request path.

One Rust codebase produces three operational processes. turbopass-api serves compatible HTTP and metrics. turbopass-worker runs the rotation workflow and activity. turbopass-schedule is a one-shot reconciler that creates or updates stable Schedules, then exits.

The v3 horizon schedule runs every minute; legacy v1/v2 rotation and v3 pruning run hourly. Both use overlap policy Skip, so a tick that overlaps a running sweep is discarded rather than queued; recovery relies on a later horizon-reconciliation run. The catch-up window is one interval and failures do not pause future runs, so outages are not an unlimited backlog. Each action starts a short workflow instead of growing an endless history or coupling API startup to a cron loop.

A deliberately small deterministic workflowRust · source
#[workflow_methods]
impl IssuerRotationWorkflow {
    #[run(name = "turbopass.rotate-issuer-keys.v1")]
    pub async fn run(
        context: &mut WorkflowContext<Self>,
        input: RotationWorkflowInput,
    ) -> WorkflowResult<RotationReport> {
        if input.schema_version != ROTATION_SCHEMA_VERSION {
            return Err(ApplicationFailure::non_retryable(anyhow!(
                "unsupported issuer rotation input schema version"
            )).into());
        }

        let workflow_time = context.workflow_time().ok_or_else(|| {
            ApplicationFailure::non_retryable(anyhow!(
                "Temporal workflow time is unavailable"
            ))
        })?;
        let activity_input = RotationActivityInput {
            rotation_id: context.workflow_id().to_owned(),
            cutoff: DateTime::<Utc>::from(workflow_time),
            mode: input.mode,
        };

        let report = context
            .execute_activity(
                IssuerRotationActivities::rotate_key_horizon,
                activity_input,
                rotation_activity_options(),
            )
            .await?;
        Ok(report)
    }
}
Temporal issuer rotation trust boundaryA Temporal schedule starts a deterministic workflow. The workflow executes one bounded activity. The activity reads and locks issuer rows, generates missing keys with the native cryptography crate, and commits them to PostgreSQL. Only aggregate counts return to workflow history.TEMPORAL HISTORY · SECRET-FREESCHEDULEminute / hour · skip overlapWORKFLOWmode + deterministic cutoffREPORTaggregate counts onlyACTIVITY + DATABASE · SECRETS STAY HEREROTATION ACTIVITYclock · RNG · crypto · SQLCRYPTO CRATEgenerate + derive keysPOSTGRESQLlock + contiguous commit
Durable orchestration with a secret-free historyTemporal history contains versioned inputs, aggregate counts, and sanitized failures—not signing keys. Keys persist in PostgreSQL and enter API or worker process memory only when needed.

09 / RETRY-SAFE ROTATION

The transaction—not the workflow—is the idempotency boundary.

Temporal activities are at-least-once. A worker can commit work and lose its response, causing the activity to run again. Rotation is safe because each issuer is re-read and locked inside its own PostgreSQL transaction, then the missing horizon is recomputed before any insert.

Condensed per-issuer activity boundaryRust · source
for issuer_id in due_issuers {
    let mut transaction = pool.begin().await?;

    // Re-read and lock this issuer inside the retry boundary.
    let issuer = select_issuer_for_update(&mut transaction, issuer_id).await?;
    let missing = recompute_missing_horizon(&mut transaction, &issuer, cutoff).await?;

    for window in missing {
        let pair = generate_key_pair();
        insert_contiguous_key(&mut transaction, &issuer, window, pair).await?;
    }

    transaction.commit().await?;
}

For the same effective cutoff and unchanged issuer state, a committed issuer transaction is normally a no-op on retry; otherwise the retry recomputes and fills only the newly missing horizon. That issuer’s uncommitted inserts roll back. An activity attempt can still commit earlier issuers before a later retryable failure, so counters from the final successful attempt can omit work committed earlier. A poisoned issuer does not roll back healthy issuers or pruning, but the final sweep returns a non-retryable activity error, the workflow fails, and no report is returned. Transient database failures receive a bounded six-attempt policy with capped backoff.

  1. 01

    Discover due IDs without carrying secrets

    The activity scans candidates and processes each issuer independently.

  2. 02

    Lock and re-evaluate

    Another worker’s committed horizon becomes visible before this attempt writes.

  3. 03

    Generate contiguous windows

    Native Rust creates missing keys and derives public keys inside the transaction.

  4. 04

    Commit once, report aggregates

    PostgreSQL is the key source of truth; Temporal receives only safe counters.

10 / COMPLETE LOCAL STACK

The second pass turned separate services into one reproducible system.

The initial implementation passed Rust checks and a disposable PostgreSQL/API smoke test, but local DynamoDB and Temporal integration were still gates. I then added a Compose environment containing PostgreSQL, DynamoDB Local and its initializer, Temporal’s dev server, the API, worker, one-shot scheduler, native load client, and Artillery.

TurboPass Docker Compose development topologyArtillery coordinates lifecycle traffic and calls a private Rust load client. Public requests reach the TurboPass API, which uses PostgreSQL and DynamoDB Local. The Temporal service and reconciled Schedule start workflows processed by the rotation worker, which updates PostgreSQL; the one-shot schedule reconciler exits.ARTILLERYHTTP orchestrationRUST LOAD CLIENTblind · verify · unblindTURBOPASS APIcompatible public routesROTATION WORKERTemporal activityTEMPORALschedules + historiesPOSTGRESQLissuers · keys · v1 replayDYNAMODB LOCALv2/v3 replayONE COMMAND · DISPOSABLE STATEmake compose-up → make loadtest-smoke
The disposable end-to-end stackThe Compose environment exercises the real public API through Artillery while a test-only Rust client helper owns blinding secrets. PostgreSQL, DynamoDB Local, and Temporal provide the three state boundaries.
Bring up and exercise the systemshell · source
# Start PostgreSQL, DynamoDB Local, Temporal, API, worker, and schedules.
make compose-up

# Run one complete virtual user through each v1/v2/v3 lifecycle.
make loadtest-smoke

# Run the default one-minute mixed lifecycle load.
make loadtest

# Exercise a 32-token batch with RFC 9497 redemption derivation.
TOKEN_BATCH_SIZE=32 TOKEN_DERIVATION=rfc9497 make loadtest

The core stack binds development ports to loopback and persists three Docker volumes. The scheduler exiting successfully is expected: its job is to reconcile the two Schedules, while the API and worker keep running. Development credentials and the Temporal dev server make this a disposable integration environment—not a production deployment file.

Public repository maptext · source
turbopass/
├── src/
│   ├── api.rs              # compatible routes + bounded native crypto
│   ├── crypto.rs           # thin policy over the upstream crate
│   ├── storage.rs          # PostgreSQL + DynamoDB contracts
│   ├── rotation.rs         # workflow, activity, retries, schedules
│   └── bin/
│       ├── api.rs
│       ├── worker.rs
│       ├── schedule.rs
│       └── load-client.rs  # test-only native client helper
├── migrations/             # repeatable create-if-missing SQL baseline
├── infra/dynamodb/         # local table initialization
├── loadtest/               # Artillery scenarios + processor
├── compose.yaml            # complete disposable topology
└── docs/
    ├── RESEARCH.md
    ├── ARCHITECTURE.md
    └── COMPATIBILITY.md

11 / LIFECYCLE LOAD TESTING

Artillery drives HTTP; Rust keeps the client cryptography real.

A useful load test had to do more than hit health endpoints. Each virtual user creates an issuer, blinds tokens, requests issuance, verifies the batch proof, unblinds, signs a redemption payload, and redeems every token. The three scenarios cover the actual v1, v2, and v3-compatible route combinations.

I did not reimplement the client cryptography in JavaScript. A test-only Rust helper uses the same pinned crate, keeps original token and blinding state in memory, and exposes a one-use opaque handle. Handles expire after five minutes and are consumed on completion. Compose publishes the helper to the host only on 127.0.0.1; it is also reachable to Artillery on the private Compose network. It must never become a public service.

LifecycleCreateIssueRedeemReplay store
v1POST /v1/issuerPOST /v1/blindedToken/:typePOST /v1/…/redemptionPostgreSQL
v2POST /v2/issuerPOST /v2/blindedToken/:typePOST /v1/…/redemptionDynamoDB
v3POST /v3/issuerPOST /v2/blindedToken/:typePOST /v3/…/redemptionDynamoDB

12 / VERIFIED RESULT

What public source and the recorded development session substantiate.

37tracked files
3runtime processes
9Compose services
56 / 56Rust tests passing
Consolidated independent repository verificationtext
$ cargo test --all-features --locked
# 56 total: 49 library + 3 load-client + 2 schedule + 2 worker

$ cargo fmt --all -- --check
# exit 0

$ cargo clippy --all-targets --all-features --locked -- -D warnings
# exit 0

$ cargo check --all-targets --no-default-features --locked
# exit 0

I independently cloned public commit f18da56 and reran its locked checks. The same SHA also has a passing hosted CI run and CodeQL setup run. Separately, the private development record reports the complete Compose stack becoming healthy, both Schedules being created, workflow and activity pollers running, PostgreSQL receiving v1 redemption state, and DynamoDB receiving v2/v3 state. Those service logs and load reports are not committed, so this is session evidence rather than independently reproducible evidence at the public SHA.

Before the full stack existed, a narrower PostgreSQL/API smoke test confirmed the migration ledger, v1 and v3 issuer creation, one v1 key, the expected three-key v3 horizon, and zero discontinuities between v3 windows. Those checks were useful because they inspected durable state rather than trusting only HTTP 200 responses.

13 / CURRENT TRUTH

A complete local system is not yet a production migration.

TurboPass is a working compatibility implementation with an end-to-end local harness. Its remaining risks sit at rollout boundaries: SDK maturity, production histories, SQL baselines, two-table coexistence, an ambiguous Kafka schema, metric migration, cryptographic review, and workload evidence.

GateCurrent evidenceRequired next proof
Temporal maturityRust SDK 0.7.0 is Public Preview and pinned exactly.Replay captured histories, version workflow behavior, then canary the worker separately.
Real infrastructureThe development session records a complete local pass, but the public repository commits no service logs or load report.Repeat against production-shaped managed services, credentials, limits, and failure modes.
PostgreSQL baselineThe migration creates final tables when absent; it does not backfill older issuer tables or validate an existing same-named schema.Audit and rehearse the exact schema, confirm any backfill, and pre-apply DDL or grant first-run migration rights.
DynamoDB cutoverA legacy-table read and primary-table conditional write cannot be atomic across two tables.Fence and drain legacy writers, verify every writer targets the primary table, then enable TurboPass writes.
Kafka / AvroThe legacy schema and generated Go field name disagree.Capture authoritative registry subjects and real messages before porting the consumer.
Cryptographic assuranceSelected vectors and compatibility tests provide preservation evidence, not a differential proof or independent audit.Run Go-versus-Rust tests, retain the upstream caveat, and obtain review appropriate to production risk.
PerformanceThe repository contains a load harness but no committed capacity report.Save reproducible reports and production-shaped batch distributions before publishing numbers.
ObservabilityCore metrics exist, but TurboPass does not reproduce every legacy database/crypto histogram and operation counter.Map dashboards and alerts to the new metric contract before canarying traffic or rotation.

The upstream cryptography project also describes its security contract as work in progress and not audited. Reuse avoids an accidental protocol fork; it does not create a new security assurance. Any move toward RFC 9578 or the active batched-token Internet-Draft belongs in a new protocol version because their ciphersuites and binary framing are not transparent replacements for this deployed JSON/base64 Ristretto API.

14 / FILE GUIDE

Where to read the implementation.

15 / WHAT IS NEXT

The next milestone is migration evidence, not more architecture.

  1. 01Capture representative Go responses and run differential HTTP contract tests.
  2. 02Replay real Temporal histories before changing the pinned SDK or workflow commands.
  3. 03Exercise production-shaped PostgreSQL and DynamoDB limits, failures, and table cutover.
  4. 04Resolve the authoritative Kafka/Avro schema from deployed registry evidence.
  5. 05Commit reproducible load reports before making capacity or latency claims.

The breakthrough was locating the boundary precisely: the same cryptography called directly, the same state kept where it belongs, and rotation made explicit, retryable, and observable.

PRIMARY SOURCES

Evidence used for this note.

The user-supplied Codex and shared ChatGPT conversations establish the implementation chronology and recorded local integration results. Public source links above establish the code and upstream behavior. This note summarizes outcomes and observable evidence; it does not reproduce private model deliberation.

CONTINUE EXPLORING

Inspect the bridge removal and follow one token end to end.

The public repository contains the compatible API, durable rotation worker, complete local stack, and lifecycle load harness described here.