Email has DKIM, SPF, and DMARC — a cryptographic proof of who the sender is. SMS, push, RCS, and WhatsApp do not. Verify is the fix: every outbound message from a registered business carries an Ed25519 signature. Consumers verify independently, offline, against a public-key registry. Valid signature = real. Invalid = scam. No telecom middleman, no per-carrier deal, no proprietary green-tick.
TRAI DLT was supposed to be the answer. It isn't. It registers sender headers, not message authenticity. Headers are still spoofable via international SS7 routing, and the content of a registered sender's SMS carries no cryptographic proof the user can check.
You get an SMS: "SBI: Your account debited ₹50,000. If not you, call 1800XXXXXX." There is no cryptographic way for you to tell if it is really from SBI or from a scammer rotating through international SMS gateways. DLT registration only proves the header was allocated, not that the message you received is real. Scammers register look-alike company names ("SBI-Kyc", "HDFC-Alert") within the DLT system itself, and when that path is closed they simply send from international gateways that bypass DLT entirely.
India's CyberDost/MHA data for 2024 puts direct SMS-phishing losses at ₹1,750 crore, with total cyber-crime losses across all vectors at ₹10,319 crore. RBI's 2024 report estimates ~67% of reported Indian cyber-frauds begin with an OTP or transaction SMS the victim believed was from their bank. SBI alone flagged 38,000 KYC re-verification scam incidents in Q1 2024. The Maharashtra State Electricity Distribution Company publicly warned of a ₹400 crore attempted “electricity bill due” SMS wave in April 2024. Email solved this in 2007 with DKIM. SMS/push/RCS/WhatsApp did not.
[zoza:bid=...&sig=...] tag proves origin. Scam SMS lacks a valid signature.A business registers once, signs every message, and publishes only its public key. A consumer pastes or scans the message and checks the signature against a cached registry — no server round-trip required. Same primitive as DKIM, adapted for channels that aren't SMTP.
The business calls POST /v1/sign with the message body, channel (sms,
email, push, rcs, whatsapp), and bearer API
key. Verify generates a fresh 16-byte message ID, builds the signing payload
(channel : mid : content), and signs under the business's Ed25519 private key with
a domain-separation context string (verify_sms, verify_email, etc).
The 64-byte signature comes back. The business appends the compact tag
[zoza:bid=...&sig=...&mid=...&ch=...] to the message body and sends via
its existing SMS gateway.
On the consumer side: the paste / scan / extension parses the tag, looks up the business's
public key in a cached registry JSON (refreshed every 5 minutes via
GET /v1/registry), rebuilds the exact payload, and runs
ed25519.Verify. Valid → green "Verified: SBI" badge. Invalid → red
"Not verified". The whole verification path runs locally in the browser / app — the
server never sees which message the consumer looked at.
verify_sms vs verify_email prevent cross-channel replay./v1/registry, CDN-cacheable, auditable.POST /v1/sign → sig = Ed25519.Sign(sk, "sms:mid:body")[zoza:bid=...&sig=...&mid=...&ch=sms]Ed25519.Verify(pk, payload, sig) — runs locallyDKIM/SPF/DMARC solved this for SMTP. TRAI DLT solved the wrong half — sender allocation, not message authenticity. RCS and WhatsApp green-tick are proprietary, per-account, and the consumer has no independent way to verify. Here is what exists today, side by side.
| Solution | Layer | Per-message crypto sig | Offline verify | Portable across carriers | User-verifiable independently |
|---|---|---|---|---|---|
| TRAI DLT India, 2020; telecom header registry |
SMS sender ID + template text allocation | No — header allocation only | No | Within India only | No — telco attests, user can't check |
| RCS Business Messaging Google, 2023; verified-badge in RCS |
RCS (Android-only) | No — badge is operator-attested | No — server round-trip | Operator-dependent | No |
| WhatsApp Green Tick Meta; business account verification |
WhatsApp (proprietary) | No — account-level badge | No | WhatsApp-only | No — phishing pages still spoofable |
| DKIM + SPF + DMARC IETF; email message-layer signatures |
SMTP email only | Yes (DKIM domain sig) | Yes (DNS TXT cache) | Yes (MTA-to-MTA) | Via MUA headers |
| BIMI + Apple Business Connect Logo + brand indicators for email |
Email logo display layer | No — logo display, not per-msg | No | Email-only | No |
| Twilio / MSG91 verified sender Short-code + toll-free registries |
SMS sender-ID registry | No — identity at allocation | No | Per-carrier | No |
| Zoza Verify zoza.world/about/verify |
Cross-channel: SMS + email + push + RCS + WhatsApp | Yes — Ed25519 per message | Yes — cached registry | Yes — carrier-independent | Yes — open spec, open registry |
DLT, WhatsApp Green Tick, and BIMI are not failures — they are answers to different questions. DLT answers "who was allocated this header?". WhatsApp Green Tick answers "did this account complete a business-identity KYC?". BIMI answers "can I show this brand's logo in Gmail?". None of them answer the question Verify asks: "was this exact message body signed by the sender's private key, and can the recipient prove it offline, without trusting a carrier or a tech platform?" That question is what DKIM answered for SMTP in 2007. Verify is the SMS / push / RCS / WhatsApp analogue.
Small, careful design choices that make the difference between a marketing claim and a shipping defense. All four are in the v0.1 code.
DKIM is per-domain-per-mail. WhatsApp Green Tick is per-WA-account. RCS verified is per-RCS-profile. A business has to manage a different trust artefact for each channel, and a consumer cannot carry a single trust anchor across them. Verify gives each business one Ed25519 keypair that signs across every channel it registers for.
A bank can sign its SMS, its transactional email, its push notifications, and its WhatsApp broadcasts with the same key. A consumer verifies the same way across all of them.
SMS is unforgiving: 160 GSM-7 chars per segment, or 70 UCS-2. Any signing tag that eats more
than a third of the payload forces a multi-segment concat, which doubles the telecom bill
and breaks on old feature phones. Verify's tag format
[zoza:bid=...&sig=...&mid=...&ch=sms] is hex-efficient and bounded
so a standard OTP or transactional alert still fits in a single segment.
Shipped as FormatCompactSig / ParseCompactSig in
verify.go. Planned: base64url tag variant to shave ~30% off the signature length.
A naive "sign the body" scheme lets an attacker capture a signed SMS body and re-present it
as a signed push or a signed WhatsApp. Verify binds the channel into the signing context
— SignWithContext("verify_sms", ...) produces a signature that
VerifySignatureWithContext("verify_email", ...) rejects as invalid.
Same primitive as the Signal Protocol's per-step domain-separation labels. Shipped in
SignMessage / VerifyMessage. Prevents "repurpose a legit alert as
a push to a different user" attacks even when the attacker has perfect capture.
GET /v1/registry returns the full JSON array of active business public keys
(~4KB per business, cacheable). Consumer apps, extensions, and the Zoza Messenger in-app
verifier refresh this on a 5-minute TTL and then verify signatures locally. Works on 2G, on
airplane-mode re-connect, and when the Verify API is itself under load.
Server-zero-knowledge by construction: Zoza can see that the registry was downloaded (routine CDN log), but never which messages a user verified. Same surveillance-resistance guarantee as DKIM offers for email.
Every registered business gets its own Ed25519 private key, stored in Fly secrets and isolated per-business-id. If SBI's signing key leaks, only SBI-signed messages during the leak window are at risk — not HDFC, not Flipkart, not IRCTC. Rotation invalidates old signatures immediately; the registry entry for that business is republished with the new public key.
POST /v1/sign/batch — hundreds of messages per callA bank sending a million OTPs a day needs throughput. The batch endpoint accepts an array of (channel, body) tuples and returns an array of signed tags in one HTTP call. Amortizes TLS handshake and auth overhead across the batch; still one distinct Ed25519 signature per message, still context-bound, still tagged with unique message IDs.
Signing cost: roughly 50k Ed25519 signatures per second per CPU core on modern x86, measured in-process. Network latency, not crypto, is the wall for hosted use.
Today the verified flag on a business is an admin-gated boolean. Planned v0.2:
the business adds a TXT record at _zoza-verify.<domain>
containing the business ID's public key fingerprint. Our worker resolves the record,
confirms control of the domain, and only then flips the flag. Same pattern as DKIM selector
records and Let's Encrypt DNS-01 challenges.
Blocker: needs the admin UI surface and a DNS worker. Planned alongside the browser extension.
Sign-then-verify with Ed25519 and context-separation is trivially sound on paper, but a Tamarin model nails down key-compromise / rotation / registry-staleness edge cases. Drafted (shares scaffolding with the Messenger and AI Agent Tamarin work); scheduled for v0.3.
Required before we put "formally verified" on this page.
Pick the layer that matches your volume and stack. Same Ed25519 primitive underneath every one.
Install @zoza/verify (JS — live on npm). Go and Python SDKs are planned for v0.2; call the REST API from those languages today (POST /v1/sign). Call sign(channel, body), get
back the tag string. Append to your existing SMS / push / WhatsApp gateway payload. No
change to your send pipeline, no new vendor in the path.
POST /v1/sign with a JSON body, bearer API key. Works from curl, Postman,
Zapier, n8n, any backend. Same endpoints power the SDKs above — pick REST if you just
want to wire a proof-of-concept in 10 minutes.
Embed <zoza-verify-widget> on your portal. Consumer pastes the full SMS;
widget parses the tag, loads the cached registry, runs
ed25519.Verify in-browser, shows green / red. No Zoza round-trip on the verify
step. Same logic runs in the planned browser extension and Zoza Messenger in-app.
Each card shows how the attack works in the wild + which defense is shipped / partial / out-of-scope. Where Verify does not solve the problem, we say so.
Scammer rents access to an international SMS gateway that honours arbitrary sender IDs. Routes SMS with header "VM-SBIBK" or "HDFCB-ALRT" — identical to legitimate allocated IDs. DLT does not inspect international ingress; the message lands on victims' phones indistinguishable from the real bank.
Real bank SMS carries [zoza:bid=biz_sbi&sig=...]. Spoofed SMS from SS7 does not — the attacker lacks the Ed25519 private key. The consumer app / paste-verify flags the missing or invalid signature. Header spoofing becomes visible.
"SBI: Dear customer, your account will be suspended. Update KYC here: bit.ly/xxx". Classic impersonation. ~67% of Indian cyber-frauds start with a bank-impersonation SMS (RBI 2024). Victim clicks, enters card + OTP on a cloned page, loses everything.
Legitimate SBI SMS is signed. The scam SMS has no signature (or a structurally-invalid one). "Is this SMS from your bank?" becomes a yes/no a consumer can answer cryptographically, not by reading style cues they're manipulated not to see.
"Your OTP for HDFC is 4782. Do not share with anyone." sent from a scam gateway with spoofed header. Victim receives a real-looking transaction SMS while on the phone with a scammer who asks them to read it back. Scammer completes the real transaction using the OTP.
A real bank OTP carries a signed tag. A scam OTP does not. When consumers learn to check the signature before reading the code out, social-engineering-with-a-forged-OTP stops working. (Does not stop the scammer reading a legitimate OTP off a trusting victim — that's coercion, out-of-scope.)
"Your parcel is held at customs. Pay ₹95 to release: xxx.click/parcel". Consumer pays the 'customs fee' on a cloned page; card details harvested for larger fraud downstream. Kaspersky tracked this as the #1 global SMS scam pattern in 2023.
Legitimate DHL / India Post / Blue Dart tracking SMS registers and signs. Scam tracking SMS cannot produce a valid signature under the real carrier's key. Consumer paste-verify shows red. (Requires the courier company to be a registered Verify business; GTM priority target.)
"Update KYC within 24 hrs or account will be blocked. Click: xxx". Plays on bank-mandated KYC refreshes that are real — victim is primed to expect the SMS. SBI alone publicly flagged 38,000 incidents in Q1 2024; total across all Indian banks likely multiples of that.
Legitimate KYC reminder signed by the bank's registered key. Scam KYC SMS has no signature. The "is this urgent thing real?" question has a cryptographic answer.
"Dear consumer, your electricity will be disconnected tonight. Pay now: xxx" — mass-sent during summer peak when disconnection fear is high. MSEDCL publicly warned in April 2024 of ₹400 crore in attempted fraud via this single template; enormous conversion during power-cut seasons.
State electricity boards register once, sign every bill-due SMS. Scam variants of the same template lack a valid signature. High-volume targets like MSEDCL / TANGEDCO / BSES are priority onboarding: one registration, millions of signed SMS a month.
"IT refund of ₹28,750 approved. Click to credit: xxx". Peaks in refund season. CBDT issued a public June 2024 advisory warning taxpayers not to click. Victim enters bank details on a cloned Income Tax portal; account drained within hours.
CBDT / IT Department registers and signs every legitimate refund / assessment SMS. Scam variants have no signature. This is a category where a single government registration blocks a yearly fraud wave worth hundreds of crores.
"Your international parcel is held at customs. Pay ₹95 to release." Scammer uses real-sounding courier / customs language, small enough 'fee' to feel plausible, card-detail harvest at the payment page. Volume-play scam — scammer sends millions, converts at a fraction of a percent.
Customs / postal / courier SMS signed under their registered keys. Scam SMS unsigned. Widget / extension shows red. Worked example: if India Post onboards, every genuine tracking SMS becomes distinguishable from every forgery.
Attacker socials the telco into swapping victim's SIM to attacker's card. Telco sends a legitimate "SIM activated" SMS; attacker races to harvest OTPs before victim reacts. Variant: attacker also sends a fake "it's okay, ignore the previous SMS" from a spoofed sender to suppress the real warning.
Defends against the fake cancellation ("ignore previous" from spoofed sender) — that SMS won't carry a valid signature under the telco's key. Does NOT prevent the underlying SIM-swap itself; that's a telco identity-fraud problem Verify can't solve. Partial status is honest.
Attacker registers a look-alike business inside RCS Business Messaging or WhatsApp Business, obtains a verified-badge, then abuses it. Victim sees the blue tick next to "Amazon Prime Support" (actually a look-alike), trusts the channel, falls for the scam.
Amazon's real messages carry a Verify signature under Amazon's registered key. The scam look-alike business cannot produce that signature — it does not hold Amazon's private key. The Verify layer sits beneath the platform badge, so platform-badge-abuse becomes detectable.
Verify proves who sent the message. It does not: (a) prevent a legitimate signer from sending a predatory-but-legal promotion — that's the signer's ethics problem, not a crypto problem; (b) protect a victim who is coerced into sharing an OTP they received from a genuine signed SMS — that's a social-engineering vector no signature scheme can solve; (c) stop a user from choosing to click an unsigned link despite the red badge — education + consumer-app friction are the remaining layers. Verify removes the "is this sender real?" question. The "should I trust what they're asking me to do?" question remains human.
Passing tests is not the same as "works in production under adversarial conditions." Six concrete failure modes we've thought through — with the exact behaviour today.
Verification runs against a locally-cached registry. Once the JSON is loaded (~4KB per
business, ~200KB for 50 businesses), the consumer can verify unlimited messages with zero
network round-trips. Works on 2G, in a Delhi metro tunnel, on a plane at cruise. Registry
refresh is a single 5-minute-TTL GET; a failed refresh just keeps the last
known good copy.
The compact tag is ~140 hex chars — a single 160-char GSM-7 segment carries a 20-word transactional message plus the tag. Multi-language (UCS-2) bodies need the bank to trim ~20 chars; we ship a planned base64url variant that buys 30% back. Mobile verify widget is ~12KB gzip, fine on a 2020 mid-range Android.
A business's marketing platform gets pwned and the attacker signs real business-issued SMS for a window. The signatures are cryptographically valid — Verify cannot tell malicious from legitimate use of a real key. Mitigations: per-business anomaly detection (volume / time-of-day), SLA for 1-hour rotation, 24h key-TTL option, and hash-chained audit of every sign call in the admin UI. Honest: during the window, forged messages are indistinguishable from real ones.
If a scammer is on the phone with the victim asking them to read out an OTP from a legitimately-signed bank SMS, no signature scheme helps. Verify authenticates the sender, not the recipient's judgement. Coercion-resistance belongs to bank side-channels (app confirm, biometric), not to the SMS layer.
Business calls POST /v1/businesses/{id}/rotate (planned v0.2 — today
re-register). New keypair issued; registry entry updated; all signatures created under the
old key start failing verify within the next 5-minute registry sync. Historical messages can
be re-signed under the new key via the batch endpoint if needed. Audit log records the
rotation with the old->new pubkey transition, hash-chained.
Ed25519 is NIST FIPS 186-5 approved (Feb 2023). EU eIDAS recognises Ed25519 as a Qualified Electronic Signature algorithm under Regulation 910/2014. India DPDP Act 2023 classes Verify as a Data Processor for the business (customer is Data Fiduciary). TRAI UCC Regulation 2018 + RBI circulars on digital communications both encourage cryptographic authentication without mandating a specific scheme. CERT-In empanelment queued.
We don't claim certifications we don't hold. Here's what's done, in progress, and scheduled.
Digital Personal Data Protection Act 2023. Verify is Data Processor; the signing business is the Data Fiduciary. We hold only the business's registered public key (public by definition), a signing counter, and audit metadata — never message content or consumer identifiers. Grievance officer designated.
Telecom Commercial Communications Customer Preference Regulation 2018 and the DLT rollout mandate sender-ID registration. Verify layers message-authenticity on top: DLT answers "is this header allocated?", Verify answers "did this exact body come from the claimed sender?". Not in conflict.
RBI Master Direction on Digital Payments Security and its 2024 circulars on customer-facing communications emphasise authenticated sender verification. Verify provides the cryptographic primitive; BFSI customers integrate via the SDK on the existing SMS / push / WhatsApp gateways.
Regulation (EU) 910/2014 — eIDAS — recognises Ed25519 as a Qualified Electronic Signature algorithm in its 2022 update. For B2C messaging the "qualified" path is typically not required; Verify sits at the "advanced electronic signature" tier.
NIST FIPS 186-5 formally approved Ed25519 (and Ed448) for U.S. federal use in February 2023. Same curve, same algorithm, same test vectors we use. Ends the historical "is Ed25519 FIPS-compliant?" footnote.
CERT-In empanelment as a security auditor for customer deployments is on the 2026 roadmap; conformance letter for a specific pilot deployment can be provided via a third-party empanelled firm in the interim. Reporting requirements under the 2022 CERT-In Directive are tracked.
Drop-in cryptographic signing for any outbound message pipeline. Full developer docs at zoza.world/developers/verify.html.
// 1. Sign an outbound SMS (Node.js example)
import fetch from 'node-fetch';
const API_KEY = process.env.ZOZA_VERIFY_API_KEY; // "zv_..."
async function sendSignedSMS(phone, body) {
// Sign the message
const r = await fetch('https://verify-api.zoza.world/v1/sign', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ channel: 'sms', content: body }),
});
const { signed_message, compact_tag } = await r.json();
// Append the tag and send via your existing SMS gateway
const payload = `${body} ${compact_tag}`;
return myExistingSMSGateway.send(phone, payload);
}
// Example output on the wire:
// "SBI: Your a/c debited Rs 50,000. Call 1800111109 if not you.
// [zoza:bid=biz_sbi_prod&sig=a4f2e8c1...&mid=3f9a&ch=sms]"
// 2. Verify a received SMS (browser / extension / Zoza Messenger)
import { parseTag, verifyOffline } from '@zoza/verify';
const registry = await fetch('https://verify-api.zoza.world/v1/registry')
.then(r => r.json()); // cached locally, 5-min TTL
async function checkSMS(fullSMS) {
const tag = parseTag(fullSMS); // extracts [zoza:...]
const body = fullSMS.replace(tag.raw, '').trim();
const pubkey = registry[tag.bid]?.public_key;
if (!pubkey) return { valid: false, reason: 'unknown sender' };
return verifyOffline(pubkey, tag, body); // Ed25519.Verify, local
}
// Valid response: { valid: true, business_name: "SBI Bank", verified: true }
// Invalid response: { valid: false, reason: "invalid signature" }
// 3. Business registration (one-time, via REST)
POST https://verify-api.zoza.world/v1/businesses
Authorization: Bearer <onboarding token from Zoza>
Content-Type: application/json
{
"name": "SBI Bank",
"domain": "sbi.co.in",
"channels": ["sms", "email", "push", "whatsapp"]
}
// Response
{
"id": "biz_sbi_prod_xxxxxxxxxxxx",
"name": "SBI Bank",
"domain": "sbi.co.in",
"api_key": "zv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"public_key": "ed25519:a4f2e8c1...",
"channels": ["sms", "email", "push", "whatsapp"]
}
// STORE api_key IN YOUR SECRET MANAGER. It is shown once and never again.
// The public_key is also published at GET /v1/registry for offline verify.
Every production action — business registered, admin approved, key rotated — appends to an append-only hash-chained log. Verify the chain in your browser. No trust required. Consumer verify calls are client-side by design — we literally cannot log which SMS you checked.
Every business registration + admin action, SHA-256 hash-chained. JavaScript verifier runs in your browser — no API call.
Monthly signed statement: no gov requests, no key seizures. Absence = compelled request received. Ed25519 verifiable.
What we store (business-registered pubkeys, sig counts). What we never see (which messages consumers verify).
Vendors that pretend they have everything are lying. Here is every adjacent thing Verify does not yet do, with the specific blocker on each. We update this page the same day a gate closes.
GET /v1/registry returns the full JSON blob (cached 5 min at the edge). For 10k+ registered businesses we need a Merkle-tree delta or an ETag-based incremental. Planned when registry size justifies it.POST /v1/businesses/{id}/rotate preserves the ID, issues a new keypair, invalidates the old in the next registry refresh.verified flag today is an admin boolean. Planned v0.2: _zoza-verify.<domain> TXT record resolved by a worker, cryptographic proof of control before flag flips. Blocker: admin UI surface + DNS worker deployment.
We'll never call a gate "closed" without a dated commit, a passing test, and an explicit
note on this page. If something on the "not yet built" list ships, this page updates the
same day. Changelog is git history of
frontend-web/public/about/verify.html — auditable forever.
Verify is live at verify-api.zoza.world. 21 Go tests passing, five channels
supported, offline verification shipped. Register a business, sign a message, paste it into
the widget — the whole round-trip is a coffee break.