Google Verification
ohshii_governance accepts a Google account as a verification method. It is
the Standard-strength provider: it opens standard proposals, delegation,
hotkey registration, the creation flows and sponsor attribution, and it does
not open the LGE purchase-tier ladder, critical proposals or the
veto-override attestation. Those need a Strong method.
This page is the reference for end users (“what happens to my data?”), developers (“how is this wired?”) and DAO voters (“how is it configured?”). For the ecosystem-wide model — the four providers, the two strengths and how the gate composes them — see Governance → Identity verification.
What Google verification is, stated plainly. It binds one Google account to one wallet. That raises the cost of running many identities, because each one now needs a separate Google account. It is not proof of personhood: one person can hold several Google accounts. That is why it carries Standard strength and why the actions with the highest capture value stay behind a Strong method. See Capture Resistance.
Internet Identity sessions only. The proof OhShii reads is an attribute bundle signed by Internet Identity, so Google verification is available only to a session signed in with Internet Identity. A wallet connected through Plug cannot complete this flow and is told so in those words rather than being shown a failure (NFID Wallet sign-in is not offered). A Strong method has no such limitation.
At a glance
Section titled “At a glance”| Strength | Standard |
| Proof | An Internet Identity attribute bundle asserting a verified Google email address |
| Uniqueness anchor | One Google account ↔ one wallet, first come first served |
| Third party contacted by the canister | None. OhShii makes no call to Google; it checks Internet Identity’s signature |
| Secret held by OhShii | None. There is no OAuth client secret on this path |
| Wallets supported | Internet Identity sessions only |
| Endpoints | start_google_verification, register_google_verification, is_google_verified |
What the canister actually checks, in order: the message carries an attribute bundle; the bundle is signed by Internet Identity; it was issued to an origin this canister accepts; it is inside the freshness window; it carries the one-time value this wallet was handed a moment earlier; it carries no identity from another sign-in provider; and it carries a verified Google address. Only then is the address normalised, salted, hashed, and the wallet marked verified.
1. Configuration states
Section titled “1. Configuration states”Google verification is described by a GoogleConfig record with two
independent flags, and confusing them is the most common misreading:
| Flag | Meaning |
|---|---|
enabled | Does this provider make the verification gate mandatory? |
accepts_new_proofs | Will this provider still bind a new wallet? |
They are orthogonal on purpose. enabled = false, accepts_new_proofs = true is
the “optional” state: verifying is worth doing for what it unlocks, but nobody is
required to verify in order to vote. enabled = true, accepts_new_proofs = false
is a retired provider that still counts for everyone who already holds it.
The remaining fields:
| Field | Meaning |
|---|---|
trusted_signer | The canister whose signature over the bundle is trusted. Defaults to Internet Identity, rdmx6-jaaaa-aaaaa-aaadq-cai |
frontend_origins | The exact origins Internet Identity may have issued the bundle to. Matched as exact strings, with no trailing-slash normalisation, because Internet Identity itself compares exact origins |
attribute_key | The attribute the bundle must carry: openid:https://accounts.google.com:verified_email |
max_bundle_age_secs | How old a bundle may be. Default 300 seconds |
Unconfigured is the shipped default: no accepted origin, gate off, registrations closed. A brand-new provider does not begin accepting proofs merely by being deployed — both flags are opened by a deliberate configuration change: an ONS proposal — and, during the transition phase, also a call by the deploying principals (§7).
get_google_config() is a public query and returns frontend_origins. The
origin list is not a secret: the browser needs it, and Internet Identity compares
origins exactly, so publishing it removes a support problem and reveals nothing.
2. End-user flow
Section titled “2. End-user flow”- Sign in with Internet Identity. If the session is not an Internet Identity session, the flow stops here with a message saying so.
- Start. The wallet calls
start_google_verification(). The canister returns a one-time value bound to that wallet, valid for a short window. - Ask Internet Identity for the attribute. The browser requests the
verified_emailattribute for Google, passing the one-time value. Internet Identity walks the user through Google if it needs to, then signs a bundle. - Register. The wallet calls
register_google_verification()with no arguments — the bundle travels with the message, not as a parameter. - Done. The wallet is Google-verified.
get_my_verification_status()reports the methods held and the resulting strength.
Re-running the flow. Registering the same Google account on the same wallet again succeeds and changes nothing. Registering a different Google account on a wallet that already holds one is refused, and so is registering a Google account already bound to another wallet.
Adding a stronger method later. A Google-verified wallet can add a Strong
method at any time; nothing has to be undone first. Each provider’s “already
verified” short-circuit looks only at that provider’s own record, so holding
Google never blocks a Strong flow. After the upgrade the wallet is Strong
everywhere the ecosystem canister decides, and each campaign DAO refreshes its
cached view on the next gated action, or immediately through that DAO’s
refresh_my_verification.
Why an attempt can be refused
Section titled “Why an attempt can be refused”Every refusal carries a stable prefix so the interface can give an instruction rather than a generic failure. There are twelve:
| Prefix | What it means to the user |
|---|---|
GOOGLE_NO_ATTRIBUTES | No bundle on the message — this is not an Internet Identity session |
GOOGLE_UNTRUSTED_SIGNER | Signed by something other than Internet Identity |
GOOGLE_MALFORMED_BUNDLE | The bundle could not be read |
GOOGLE_ORIGIN_NOT_ALLOWED | Issued to an origin this canister does not accept |
GOOGLE_STALE | Outside the freshness window, in either direction |
GOOGLE_UNKNOWN_NONCE | Never started, already used, or expired — start again |
GOOGLE_NONCE_OWNER_MISMATCH | Started by a different wallet |
GOOGLE_FORBIDDEN_SOURCE | The bundle carries an identity from another sign-in provider |
GOOGLE_NO_VERIFIED_EMAIL | No verified Google address in the bundle |
GOOGLE_EMAIL_REJECTED | That address cannot be used |
GOOGLE_ACCOUNT_ALREADY_BOUND | That Google account is bound to a wallet |
GOOGLE_PRINCIPAL_ALREADY_BOUND | This wallet already holds a different Google account |
Only GOOGLE_ORIGIN_NOT_ALLOWED carries extra text, meant for whoever configures the accepted origins.
GOOGLE_STALE deliberately does not publish how stale the bundle was.
Which addresses count as the same account
Section titled “Which addresses count as the same account”Google treats several written forms of one address as the same mailbox, so the canister does too. Otherwise one Google account could be bound to several wallets by typing it differently each time.
- Case is ignored.
Jane@Gmail.comandjane@gmail.comare the same account. - A plus-tag is ignored.
jane+dao@gmail.comis the same account asjane@gmail.com. googlemail.comisgmail.com.jane@googlemail.comis the same account asjane@gmail.com.- On Gmail addresses, dots are ignored.
j.a.n.e@gmail.comis the same account asjane@gmail.com. This applies to Gmail only, because it is Gmail’s own rule. - Apple private-relay addresses are refused, and so is any address that ends up empty after the rules above.
The consequence to keep in mind: you cannot verify two wallets by adding a plus-tag or a dot. That is the point of the rules, not a side effect of them.
3. Privacy model
Section titled “3. Privacy model”What the canister stores
Section titled “What the canister stores”- That this wallet is Google-verified.
- A salted hash of the normalised address, in a separate set used only to answer “is this Google account already bound to a wallet?”
The address itself is never stored, never logged, never returned by any query, and never included in any error message. The salt is generated by the canister, is written by no configuration setter, and is readable by no query.
The salt is kept across code
upgrades. An upgrade whose code cannot read the stored salt (its value has the
wrong length, or its storage header is not recognised) is refused, and the
canister keeps running its previous code with the salt untouched; the refusal
names the setting, its MemoryId (103) and which of the two faults it found.
A salt that cannot be read is never replaced by a new one: a verification that
needs it is refused instead, so every existing address hash keeps matching.
What OhShii learns
Section titled “What OhShii learns”That some Google account, which it cannot name or reconstruct, is now bound to this wallet. The salted hash is one-way and, because the salt is canister-held and unexposed, it cannot be checked against a guessed address from outside.
What Google learns
Section titled “What Google learns”Nothing from OhShii. The canister never contacts Google. Google sees a normal sign-in initiated by Internet Identity, exactly as it would for any site using Internet Identity’s Google option, and learns nothing about which DAO or campaign the user is going to.
What the logs say
Section titled “What the logs say”User-flow events are the generic identity_verified and
identity_verification_cleared, with empty details — the provider is not named,
and this is the same rule the other three providers follow.
Configuration events do carry details, because the DAO must be able to audit its own decisions: a Google configuration change records the two flags and the number of accepted origins, never the origin list.
Clearing a binding records the wallet, deliberately: governance actions are linkable so that the DAO can audit every reset. The salted digest itself is never printed.
4. Sybil resistance, and its honest limit
Section titled “4. Sybil resistance, and its honest limit”The binding is one Google account, one wallet, first come first served. Someone who wants ten verified wallets needs ten Google accounts. That is a real cost and it is the whole of what this method buys.
It is not personhood. Google accounts can be created in quantity by one person, and the normalisation rules above stop the cheapest trick but not the determined case. Stated as the design rule rather than as reassurance:
- Google raises the price of a Sybil set. It does not bound its size.
- Therefore Google alone never reaches the LGE purchase-tier ladder, a critical proposal, or a veto-override ballot. Those need a Strong method.
- A wallet that holds Google and a Strong method satisfies everything; Strong is a superset of Standard.
5. What Google verification unlocks
Section titled “5. What Google verification unlocks”| Action | Google only (Standard) |
|---|---|
| Vote or create a standard proposal, on the ecosystem DAO and on campaign DAOs | yes |
Delegation (follow, set_accepts_followers) and hotkey registration | yes |
| Create a token, an LGE, an index canister or an imported DAO | yes |
| Sponsor attribution — a referral counts | yes |
| Vote or create a critical proposal | no — needs Strong |
| A delegated ballot landing on a critical proposal | no — that ballot is skipped |
| LGE purchase tier above Guest, guest-fee waiver, exemption from the Guest cap | no — needs Strong |
| Veto-override ballot | no — needs a fresh Strong attestation for that round |
The refusal on a critical action says which method is missing, rather than “not verified” — telling a verified user they are unverified would be wrong.
Guest participation is unaffected. Buying into an LGE as a Guest requires no verification of any kind, exactly as before: the 1,000,000-token cap, the guest fee and the 50-Guest cap per campaign apply to unverified wallets and Google-verified wallets alike. Google only adds capabilities; it is never a precondition for something an unverified wallet can already do.
6. Rate limits and cycle budget
Section titled “6. Rate limits and cycle budget”Both endpoints are update calls and neither is anonymous-callable.
- Per wallet. The three verification
start_*endpoints share one per-caller budget, so starting a flow repeatedly is throttled per wallet rather than per provider. - Canister-wide. A global hourly cap bounds how many of these operations can run in total, independent of how many wallets are attempting them.
- Cycle floor. A pre-check refuses the operation while the canister’s balance is below a reserve, so a flood cannot grind the canister down through this path.
- Bookkeeping. The per-caller records are swept both lazily on insert and by the periodic task, and carry a hard cap, so the accounting itself cannot be grown without bound.
The full table of caps lives in Rate Limit Inventory.
7. Governance control
Section titled “7. Governance control”Everything on this page is set through ONS governance — and, during the transition phase, also by the deploying principals, who can call the same three methods directly.
| Change | How it is made |
|---|---|
| Configure Google, or flip either flag | set_google_config, executed by ohshii_governance through an approved critical proposal (CriticalGovernanceOperation) |
| Clear a wallet’s Google verification | admin_clear_google_verified, same route |
| Release a Google account’s binding so it can be used again | admin_clear_google_email_hash, executed by ohshii_governance through an approved standard proposal (ExecuteAdminMethod): it frees an account binding and verifies nobody by itself |
Across code upgrades: an upgrade whose code cannot read
the stored Google configuration is refused, and the canister keeps running its
previous code with the stored value untouched. While the value cannot be read, it
reads as not configured, disabled and closed to new proofs, and set_google_config
refuses to write.
set_google_config and admin_clear_google_verified (like their Telegram
equivalents) are routed as CriticalGovernanceOperation, the same route the World
ID and DecideID equivalents take, and they are guardian-vetoable like any other
critical proposal.
There is no DAO-held secret on this path. Telegram is checked against a public key; Google is an Internet-Identity-signed bundle. Neither needs an OAuth client secret, unlike the legacy DecideID OIDC path. The only secret in the design is the canister-generated salt, which no setter writes and no query reads. That is why every provider setter here can be a full, public proposal body.
8. Escape hatches, and what they do to a binding
Section titled “8. Escape hatches, and what they do to a binding”Two governance-executed levers exist, they are deliberately separate, and the difference matters:
admin_clear_google_verified(wallet)clears the wallet’s verified flag and keeps the account binding. Revoking a wallet therefore does not hand its Google account back for reuse. A retry after such a revocation is refused withGOOGLE_ACCOUNT_ALREADY_BOUNDrather than silently returning success and leaving the caller unverified.admin_clear_google_email_hash(wallet)releases the binding, so the Google account can be bound again. This is the lever for a user who lost a wallet.
Both are DAO proposals. They exist as a pair because the two situations pull in opposite directions: revoking a bad actor must not return the account to circulation, and helping someone recover must.
Verification already granted is not revoked by configuration. Turning the gate off, or closing the provider to new proofs, changes nothing for a wallet that is already verified — that guarantee is the same one the legacy providers carry.
9. Frontend integration
Section titled “9. Frontend integration”start_google_verification()returns the raw one-time bytes. Pass them to Internet Identity’s attribute request unchanged.register_google_verification()takes no arguments. The bundle rides on the message, so the call must be made through an agent carrying the identity that Internet Identity signed.- Classify a refusal on its prefix, up to the first
:or the end of the string.GOOGLE_NO_ATTRIBUTESshould be rendered as guidance — “Google verification requires an Internet Identity session” — not as an error. is_google_verified(principal)is caller-only and traps on any other argument. For interface state readget_my_verification_status(), which reports every method held and the resulting strength in one call.- The accepted origins must match what the browser is served from, exactly. A
page served from an origin not in
frontend_originsgetsGOOGLE_ORIGIN_NOT_ALLOWEDno matter how well-formed everything else is.
10. Security properties
Section titled “10. Security properties”- The wallet is bound by the one-time value, not by who calls. The value the canister issues is tied to the wallet that asked for it, so a bundle obtained for one wallet cannot be registered by another.
- Freshness is enforced in both directions. A bundle older than the configured window, or dated in the future beyond a small tolerance, is refused.
- One bundle, one registration. The one-time value is consumed, so the same bundle cannot be replayed.
- Origin is exact. No normalisation is applied, because Internet Identity compares origins exactly and normalising here would accept a bundle Internet Identity never issued for that origin.
- Only Google counts. A bundle carrying an identity from another sign-in provider is refused rather than being read as a Google address.
- The address never leaves the canister and never enters a log, an error, or a query response.
- Standard strength is enforced downstream, not assumed. The gate composes provider flags in one place, so no consumer decides for itself what Google is worth.
Not claimed. This is not proof of personhood, it does not survive a determined actor holding many Google accounts, and it is available only to Internet Identity sessions. All three are design consequences, and all three are why the Strong tier exists.
11. Configuring the provider through proposals
Section titled “11. Configuring the provider through proposals”Every step below is an ONS proposal (during the transition phase the deploying principals can also call the same setters); none of it is a console action.
- Before enabling anything, confirm the accepted origins are exactly the origins the app is served from. This is the field that most often makes an otherwise correct flow fail.
- Open registrations first, mandate later. Set
accepts_new_proofs = truewithenabled = false, let wallets verify, and only then consider whether the gate should be mandatory. The two flags exist precisely so this order is available. - Check the public state with
get_google_config()after the proposal executes, and confirm both flags and the origin list read as intended. - Watch the configuration event, which records both flags and the number of origins. A change that does not produce one did not execute.
- Retiring the provider later means
accepts_new_proofs = false. Everyone already verified stays verified.
12. Implementation map
Section titled “12. Implementation map”| Concern | Where |
|---|---|
| Bundle checks, normalisation, error prefixes | src/ohshii_governance/src/google_verification.rs |
| Endpoints, uniqueness checks, escape hatches | src/ohshii_governance/src/lib.rs |
| Verified set, binding set, salt, strength predicates | src/ohshii_governance/src/memory.rs |
GoogleConfig, strength enum | src/ohshii_governance/src/types.rs |
| Interface | src/ohshii_governance/ohshii_governance.did |
See also
Section titled “See also”- Governance — the four-provider model and the gate semantics
- Telegram Verification — the Strong provider
- World ID Verification and DecideID Verification — the two legacy providers
- Capture Resistance — why strength is split the way it is
- Rate Limit Inventory