About this post
This is a cross-post of content uploaded to GitHub. I am sharing it here to introduce it to the Ethereum community and facilitate discussion.
Summary
Anoiden is an anonymous single sign-on (SSO) protocol that uses zero-knowledge proofs. This protocol allows users to sign in to a Service Provider (SP) using information from an Identity Provider (IdP) while keeping the user’s identity secret—even if the IdP and SP collude. It utilizes the Semaphore library.
- Connect
- The user creates an account using methods specified by the IdP (such as email or phone number registration) and links their zero-knowledge proof keys.
- Auth
- The SP trusts the IdP and knows its authentication endpoint. The user requests to log in to the SP via the IdP and obtains a nonce.
- The user then creates a proof of their identity and sends it to the IdP. The IdP verifies the proof and, if valid, creates a corresponding signature.
Terminology
- Extension: A browser extension (or app) that manages the user’s keys and creates proofs. It is necessary to keep the identity confidential even if the SP and IdP collude.
- anoiden.js: Provides an interface for website clients to communicate with the extension.
- Identity Provider (IdP): An entity that provides information about the user’s validity to the SP.
- Service Provider (SP): An entity that utilizes the user’s validity provided by the IdP.
Protocol
Registration with IdP (Connect)
After the user completes account registration with the IdP, they can link that account with the key in the extension. Through this linkage, the user can use this IdP when authentication is requested in the future. Additionally, it becomes possible to log in to the IdP anonymously using the key.
The IdP client obtains the signature and public key through anoiden.js:
const {signature, publicKey} = await connect(serviceName, nonce);
serviceName
refers to the Identity Provider’s name and is used for key management within the extension.nonce
is an unpredictable string obtained from the server side of the IdP.
The following diagram shows the flow until the IdP client obtains the user’s signature:
The client sends the obtained signature, public key, and nonce to the server. The server checks the session and verifies whether it issued that nonce to the user. If valid, it verifies the signature, obtains the identifier (Poseidon hash of the public key), adds the identifier to a Merkle tree, and saves the Merkle root after the addition.
Auth
When the SP uses the IdP, it is assumed that it has informed the IdP in advance and has received a client ID from the IdP.
The SP’s client obtains a signature from the IdP through anoiden.js as follows:
const idpSignature = await auth(endpoint, nonce, params);
endpoint
is the endpoint of the IdP.nonce
is obtained from the server side of the SP.params
are arbitrary parameters defined between the SP and IdP, but clientId is required.
After the extension receives the endpoint, nonce, and params, it obtains identifiers from the IdP’s endpoint and uses Semaphore to create a proof as follows:
// https://js.semaphore.pse.dev/functions/_semaphore_protocol_proof.generateProof.html
await generateProof(identity, group, nonce, "signIn");
identity
is the key created for each IdP.group
is an object created from identifiers.nonce
is the nonce passed from the SP.
The extension adds the hostname of the SP to params. The IdP uses the clientId and hostname to identify the SP that requested the signature.
The IdP receives the proof and parameters, verifies the proof, and then creates a signature of the nonce and parameters using a key in a format shared in advance with the SP.
The client sends the received signature to the server, which verifies it on the server side.
Endpoint interface
The IdP’s endpoint has GET and POST methods. GET returns the identifiers of all accounts. POST verifies the proof and confirms the validity of the user.
GET endpoint/identifiers
There are no parameters. The response is in JSON and returns a list of identifiers as strings under the key identifiers.
Example response:
{
"identifiers": ["6423154662976160105169106896701549153516891642211172349909782921108153674476"]
}
POST endpoint/auth
The request body requires a JSON object containing the proof and parameters (params):
{
"proof": "Semaphore proof here",
"params": {
"clientId": "exampleClientId",
"hostname": "example.com",
"other_params": "additional parameters here"
}
}
The response is returned in JSON format and includes the IdP’s signature as a string under the key signature:
{
"signature": "example_signature"
}
Security
The nonce
is used to prevent replay attacks.
The clientId
and hostname
are mechanisms that ensure the IdP passes user validity only to SPs it has authorized. This also helps prevent requests from unauthorized domains.
Concerns
When an SP uses only a single IdP, absolute trust in that IdP is required. However, by utilizing multiple IdPs simultaneously during sign-in (Decentralized SSO), the necessary level of trust in each IdP is reduced.
When the number of identities increases, it becomes inefficient. Therefore, we are considering dividing identity groups, but this raises privacy concerns.