VRA's open-source audit client uses an oblivious PRF protocol so your IPs never leave your machine in cleartext. We never see them. You get back grades. Production-ready, runs in a Lambda, ~14 seconds for a million IPs.
$ vra audit --input ips.csv --output results.csv ▸ Loaded 1,000,000 IPs from ips.csv ▸ Phase 2 · blind locally ................. 4.8s ▸ Phase 3 · VRA evaluates blinded points .. 2.9s ▸ Phase 4a · unblind locally ............... 3.1s ▸ Phase 4b · streaming lookup .............. 2.0s ▸ Assembling results ..................... 0.4s ✓ Audit complete in 13.2s ✓ 0 IPs transmitted to VRA in cleartext ✓ Wrote results.csv (300,247 graded · 699,753 unmatched) ✓ Peak memory: 62 MB (Lambda-safe)
ip,grade,category,confidence 203.0.113.1,A,residential,98.4 198.51.100.5,—,—,— 203.0.113.42,B,residential,87.2 192.0.2.18,D,datacenter,52.1 # ... 999,996 more rows
Each row of input gets blinded by a random scalar before it ever crosses the wire. VRA does the cryptographic work on the blinded form, sends it back, and your machine unblinds it locally. The math is standard oblivious PRF — same family as private set intersection and password-based key agreement.
For each IP, draw a random scalar and multiply the curve point by it. The output is information-theoretically uncorrelated with the input — no IP recoverable from any blinded point.
Send the 1M blinded points to VRA. We multiply by our secret key and return them. We never see the underlying IPs because the blinding factor is yours alone.
Multiply each evaluated point by the inverse of your blinding scalar. The result is a deterministic OPRF token for each IP — same input always yields the same token under the same key.
Send the OPRF tokens to VRA's lookup endpoint. We match against our labeled table and return grades only for matches. Tokens are not invertible to your IPs.
Procurement and security teams want a precise statement of what VRA learns and what stays yours. Here's the symmetric answer — no marketing language, no hand-waving. If your threat model needs more, the privacy modes section below covers stricter variants.
Your IPs never leave your network. VRA's view is limited to volume, timing, and — in the default streaming variant — your aggregate match rate against our labeled set. Buyers who need stronger guarantees can opt into per-buyer keys or the original local-table protocol.
docker run. Five minutes. First audit.The audit client ships as a Docker image and as language-native libraries. Same protocol, same wire format, same guarantees — pick whichever fits your build pipeline.
Same key as the public API — sign up with email, paste the vra_live_* token. The credential is for billing and rate-limiting only; it never decrypts protocol messages.
docker pull vra/audit:latest or install the language library: pip install vra-audit, go get github.com/vra/audit, cargo add vra-audit.
Point the client at a CSV of IPs. It blinds locally, talks to VRA, unblinds, looks up, and writes the result. Streaming variant is the default — switch with --mode local-table if you need stricter privacy.
Source is on GitHub. Apache 2.0. Trail of Bits review on file. The protocol spec is published, the test vectors are checked in, and any third party can replicate every grade we issue.
# Pull the audit client docker pull vra/audit:latest # Run a streaming audit (default — fastest, lowest memory) docker run \ -e VRA_KEY=$VRA_KEY \ -v $(pwd):/data \ vra/audit audit \ --input /data/ips.csv \ --output /data/results.csv # Stricter privacy: download the local token table once, # then run audits offline against it docker run -v $(pwd):/data vra/audit \ fetch-tokens --output /data/tokens.bin docker run -v $(pwd):/data vra/audit audit \ --input /data/ips.csv \ --tokens /data/tokens.bin \ --output /data/results.csv
# pip install vra-audit from vra_audit import Client client = Client(api_key=os.environ["VRA_KEY"]) # Streaming variant (default) results = client.audit( ips=open("ips.csv").read().splitlines(), mode="streaming", # or "local-table" ) for r in results: if r.grade in ("D", "F"): block(r.ip) # Audit ran in ~14s for 1M IPs. # Peak memory: ~64 MB. No IP was sent in cleartext.
// go get github.com/vra/audit package main import ( "os" "github.com/vra/audit" ) func main() { client := audit.New(os.Getenv("VRA_KEY")) res, err := client.Audit(audit.Request{ InputFile: "ips.csv", Mode: audit.Streaming, // or audit.LocalTable }) if err != nil { panic(err) } for _, r := range res { if r.Grade == "D" || r.Grade == "F" { block(r.IP) } } }
// cargo add vra-audit use vra_audit::{Client, Mode}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let client = Client::from_env()?; let results = client .audit("ips.csv") .mode(Mode::Streaming) // or Mode::LocalTable .run() .await?; for r in results.iter().filter(|r| matches!(r.grade.as_str(), "D" | "F")) { block(&r.ip); } Ok(()) }
--mode.
Same Docker image, same library, same wire protocol — different cryptographic posture. Streaming is the default because it covers nearly every real audit. The stricter modes are there for buyers whose threat model excludes them.
Both produce VRA grades from the same v3.1 methodology. The difference is what crosses the wire — segment IDs and match-files for the API, blinded curve points for the private audit client. Pick the one that matches your data-handling constraints.
No. The blinding step in Phase 2 multiplies each curve point by a random scalar that only your client holds. Without that scalar, the blinded point is information-theoretically uncorrelated with the input — VRA cannot recover the IP no matter how much computational power they apply. After unblinding in Phase 4a, what crosses the wire is a one-way OPRF token, also not invertible to the underlying IP. The only way VRA could see an IP is if you ran a different client that sent the IP in cleartext. The open-source client doesn't.
An oblivious pseudorandom function lets two parties compute F(sk, x) where one party holds sk and the other holds x — and neither learns the other's input. The buyer's x is their IP; VRA's sk is the campaign-scoped secret key. The output is a deterministic pseudorandom token: same IP under the same key always produces the same token, but the token reveals nothing about the IP. Same primitive that backs Apple's Private Relay and Cloudflare's Privacy Pass.
Yes — VRA learns the count of tokens in your batch that matched our labeled table. For most use cases this is harmless: it's an aggregate count, not per-IP information. If your threat model excludes match-rate disclosure (e.g., longitudinal hedge-fund research where the match rate itself reveals strategy), use --mode local-table. The 4 GB token table downloads once per epoch, and after that VRA sees nothing about any individual audit.
Yes — that's the explicit design target for the streaming variant. Peak memory is ~64 MB for a 1M-IP audit, which fits comfortably in any modern serverless tier (Lambda from 128 MB, Cloud Run from 256 MB). The original protocol with the local token table needs ~4 GB and won't fit serverless without significant provisioning. We default to streaming precisely because most production callers are running in serverless.
The OPRF protocol includes a verification step: VRA publishes a discrete-log proof that the evaluation was computed with the advertised key. The client checks this proof on every batch — a mis-evaluation fails verification and aborts the audit. The proof itself is short (one curve point and one scalar), and the public key is pinned in the client. There's also a published audit transcript every epoch that anyone can replay against the public key. Trust is anchored in cryptographic verification, not in operational claims.
Retry Phase 4b alone. The expensive client-side computation (Phase 2 blinding, Phase 4a unblinding) is deterministic given your input IPs and the epoch's secret key, so the unblinded tokens you've already computed don't change. Retrying just re-sends them to the lookup endpoint. The client handles this automatically with exponential backoff; the practical effect is that a transient outage on our side adds seconds, not a re-run.
The audit client is metered per IP audited, not per HTTP request. Streaming variant: ¢0.005 per IP. Local-table variant: ¢0.003 per IP plus a daily ¢25 charge for the token table download. Per-buyer key and mix-net are quoted on volume. Free-tier audit allowance: 10,000 IPs per month, same workspace as the API free tier.
Yes — the Trail of Bits review is published in full, redactions only for specific deployment-config items their team flagged. The protocol spec, test vectors, and reference implementation are also public. If your security team needs a deeper engagement (custom threat model, formal verification of a specific deployment), that's available under our enterprise tier.
Sign up, paste the key, run one docker run. The first 10,000 IPs every month are free, and the cryptographic guarantee is the same on the free tier as on the largest enterprise deployment.