Pomegranate Instructions
Pomegranate
NOTE: original text here: https://viewsource.win/fiatjaf.com/pomegranate/_/master/~/README.djot
Promenade-like Offering of Multisig Enhanced by Google Resources for Auth such that Nostr Accounts are Treated Easily This is what happens when you combine the ideas of both https://viewsource.win/fiatjaf.com/promenade%3E and https://github.com/coracle-social/pomade%3E and then take those to a new level. Demo/admin app: https://pomegranate-admin.netlify.app
Setup flow
- User logs in with Google on the
centralserver, gets a signed token - User inputs or generates a new secret key, performs the FROST sharding of that key into multiple pieces and a threshold (e.g., 2-of-3, 3-of-5)
- User contacts
centralwith a payload containing their chosen operators (together with their public key shard) and the threshold - User contacts each operator and sends their secret key shard, its own email address and the address of
central - Operators confirm the information with
centraland reply to the user - Once all operators have confirmed,
centralmarks that user as operational - User can now create multiple "profiles" on
central, each is a different reusable NIP-46 "bunker" URI with a unique handler key and optional restrictions (kind filters, author filters, expiration) - Now that bunker can be used in any Nostr client to create signatures on behalf of the user using the normal NIP-46 flow using
centralas the relay - When
centralreceives a NIP-46 request (kind 27133), it decrypts it, validates restrictions (allowed kinds, authors, expiration), then forwards to operator servers - Operators create partial signatures and return them to
central centralcombines the threshold of signatures and produces the final signed event, which is then returned via the proper NIP-46 response
Implementation guide
Clients that want to add a "Login with Google" button can do the following: (everything that would imply binary is encoded as hex.) (requests and response bodies are JSON.)
- Pick a
centralserver URL (hardcoding a default is fine, having an advanced menu where users can provide their own is optional). - Open Google login popup using
window.open()against<central-url>/login/google. centralpopup sends back{ token: "..." }withpostMessage(). That token should be used asAuthorization: Token <token>in every following call to thatcentralserver.
const popup = window.open(`${centralURL}/login/google`, "OAuth", "width=600,height=600")
window.addEventListener("message", function handler(event) {
if (event.origin !== centralURL || !event.data?.token) return
window.removeEventListener("message", handler)
popup?.close()
const token = event.data.token
// use as "Authorization: Token <token>"
}, { once: true })- Extract email from token: the token is a base64-encoded Nostr event of kind
20443. The"email"tag has the email as the value.
const evt = JSON.parse(atob(token))
const email = evt.tags.find(tag => tag[0] === "email")?.[1]- Check if users have already performed a similar Google login elsewhere: once client has obtained the user email address (from any successful
centrallogin), it is possible to search for an event kind16440using a filter{"#m": [argon2id(email, "pomegranate", {t: 1, m: 65536, p: 4})]}. Such event will come with a"central"tag, if that points to a differentcentralserver, display a button telling the user that a setup was found at this other server and do step 3 again. - Once the user is authenticated against the correct
centralserver, callGET <central-url>/accounton it, if the user already has an account there the response will be a JSON object{"operators": ["<url>", ...], "threshold": <number>, "pubkey": "<nostr-user-pubkey>"}, otherwise it will be a 404. - If the response exists, jump to step 15; if it is a 404 we have to create an account.
- First the client must choose a set of operators (signers) and threshold, this may be done by hardcoding default parameters, optionally allowing the user to pick their own.
- The client also needs a random string to act as the session identifier.
- The client also needs a Nostr secret key for the user, the user could provide their own but generally the client will create a new random key at this point.
- Finally the client has to perform the actual FROST key-sharding algorithm, according to the number of operators (
<n>) and threshold chosen (<m>) (2-of-3,3-of-5etc). The implementation for this exists in JavaScript in Golang. This will yield a set of<n>"shards" (one for each operator), each with a public part and a secret part. These same libraries also include encoders and decoders that can be used to convert such shards to bytes and hex (they will have to be sent as hex to the servers in the next steps). - Once those parameters are available, call
POST /registeroncentralwith the headerX-Pomegranate-Sessionset tosession(the string from step 9) and the request body set to an event ofkind:20445signed by the user key. The event should contain a tag["threshold", "<m>"]and one tag per operator:["operator", "<operator-url>", "<public-shard-hex>"]. - Register on each operator: for each selected operator, call
POST <operator-url>/po/registerwith the headerX-Pomegranate-Operator-Token: sha256(session + ":" + operatorURL)and the request body set to an event of kind20444signed by the user key. The event should contain a tag["central", <central-url>], a tag["email", <user-email>]andcontentset to<private-shard-hex> - Once the last call to
/registeron the last operator returns,GET <central-url>/accountcan be called again to check if the account has been created (the operators will talk tocentralin the background). - As soon as the account exists, a call
GET <central-url>/profilescan be made. It should return an array of[{"name": "<...>", "handler_pubkey": "<...>"}, ...]. If a profile named"default"doesn't exist, create it with a callPOST <central-url>/profileswith body{"name": "default"}and fetch again. - Build a NIP-46 bunker URL as
bunker://<handler_pubkey>?relay=<encodeURIComponent(replace(central-url, "http", "ws"))>and use that normally. - Erase the secret key from memory.
Key recovery
Users can recover their secret keys as long as the operators are responsive, by relying only on their Google accounts:
- Users can visit any operator's
/po/recover/googleendpoint with their Google credentials - The operator returns their stored shard
- With threshold shards from enough operators, the secret key can be reconstructed
- Once reconstructed, the secret key can be resharded and redistributed to other operators, or used in raw format
Trust assumptions
The idea here is that it is very hard for a secret key to be compromised. Even if the entire setup is broken the user always has the option to move on to a new setup as long as it retains exclusive control over the key. There are only two ways the user secret key can be compromised:
- enough (meeting the threshold) operators collude to rebuild the key on their own and use it for their own purposes
- Google decides to fake its OAuth API to trick the operators to think they are talking to the real user, then steal the shards and rebuild the key
The operator cannot ever access the secret key, nor any of the Nostr clients (except for the client that is creating the key, in the moment it is created, although this can be mitigated in the future using a decentralized key generation process).
Losing keys
If the user doesn't have an independent key backup and enough operators (such that the threshold can't be met anymore) go unresponsive then the key cannot be recovered and no signatures can be created again. A mitigation to this would be to have a small relative threshold that is still big enough to be safe (like a 4-of-9?), and also for central to monitor the responsiveness of the operators and notify the user if any of them is offline, so the user can perform a recovery-and-reshard process.