Skip to main content

PileumConnect

🌐 https://connect.pileum.eu

note

Users willing to use this service need :

CNIe front CNIe back

Overview​

PileumConnect is a public OpenID Connect Provider designed for proof of personhood & privacy.

Users must own a french electronic national identity card to register an account and prove their identity. The user's identity is processed directly in the browser, ensuring it is never transmitted over the internet. Accounts must be re-verified every year.

Any OpenID Connect Relying Party can use the service to authenticate its users : the only user information provided is a Pairwise Pseudonymous Identifier (PPID). PPID is the identifier of an User to a Relying Party that cannot be correlated with the User's PPID at another Relying Party.

User verification procedure​

Users must provide an identity attestation (which is PDF file) electronically-signed by the french government. This PDF is processed in the browser, and a Zero Knowledge proof is created to prove personhood to PileumConnect, without disclosing user identity.

FAQ​

Is this service affiliated with the government ?​

No.

How is processed my identity attestation ?​

Your identity (extracted from the PDF attestation file) is never transmitted over the Internet. Instead, your core identity (names, birth date, birth place) is processed directly in your browser to produce a unique identifier.

On our end, we only store the association between your passkey and your identifier.

Why is this service not offered on-chain (i.e. as a smart contract) ?​

For privacy reason : an attacker could brute-force identifiers published on-chain to reveal real identities. We made multiple attempts at the issuer level advocating for changes that would allow a full on-chain solution, but no luck so far.

How to recover my account ?​

You can recover your account by going through the verification procedure again.

Why going through the verification procedure every year ?​

To ensure that you still exist down here.

Developers (OIDC RP)​

Any Relying Party can rely on PileumConnect to authenticate its users. Current service configuration is available at :

https://connect.pileum.eu/.well-known/openid-configuration

The only supported OIDC mode is Implicit flow, with the following parameters :

  • response_type: id_token or eip712
  • scope: openid

Authentication Request​

When the RP wishes to Authenticate the End-User, the Client prepares an Authentication Request to be sent to the Authorization Endpoint. This request uses the Implicit flow with the following parameters:

GET https://connect.pileum.eu/api/auth?
client_id=5eb4e6846e1e31c668486dec12762dc2e406398bb5d14bef329fff4a9ce95b75
&response_type=id_token
&redirect_uri=https%3A%2F%2Frelying-party.fr%2Fcallback
&scope=openid
&state=c3a7fbe63294f6f4fedce7dc5fa29b3c
&nonce=1be0695758d9ad41e676865558739b35
  • client_id: SHA256 hash of redirect_uri hostname.

Authentication Response​

If the End-User authenticates successfully & grants the request, the Client is redirected to redirect_uri:

GET https://relying-party.fr/callback?
state=c3a7fbe63294f6f4fedce7dc5fa29b3c
&id_token=eyJhbGciOiJ[...]

Parameter id_token is a JSON Web Token (JWT) signed with ES256 :

id_token:JWT
{
"alg" : "ES256",
"kid" : "oidc"
}
{
"aud" : "5eb4e6846e1e31c668486dec12762dc2e406398bb5d14bef329fff4a9ce95b75",
"exp" : 1708482941,
"iat" : 1708396549,
"iss" : "https://connect.pileum.eu",
"nonce" : "1be0695758d9ad41e676865558739b35",
"sub" : "aade285ad68dba3bc48a1e129e3992b5fb52e6e84480eb69107adce256277acc"
}

Property sub is the PPID, derived from redirect_uri hostname.

Web3 : EIP-712​

info

This feature is custom and is not supported by the OpenID Connect specifications.

If the Relying Party is a Smart Contract, response_type=eip712 may be preferred. With this option, the returned id_token is an EIP-712 structure.

Authentication Request​

When using response_type=eip712, parameter client_id must be a JSON object encoded in Base64-URL, with the following fields :

client_id for response_type=eip712
{
"name" : "MySmartContract",
"version" : "1",
"chainId" : 31337,
"contract" : "0x5fbdb2315678afecb367f032d93f642f64180aa3",
"account" : "0x7A997970C01813dc3A010C7d01b50e0d17dc79C0",
"scope" : "0xe78222479cb676528ed8a36f9fd71706a3aaea59040e64d97aa910994897fe3e"
}
  • name, version, chainId and contract fields are part of EIP712Domain.
  • account: EOA address of user that is being authenticated.
  • scope: hex-encoded uint256 value, used to further differentiate the sub by associating it with a specific scope, enhancing privacy and segregation of PPID across different scopes within the same EIP712Domain.

Authentication Response​

If user authenticates successfully, the Client is redirected to redirect_uri. Field id_token is a JSON object encoded in Base64-URL :

id_token for response_type=eip712
{
"account" : "0x7A997970C01813dc3A010C7d01b50e0d17dc79C0",
"sub" : "0xb60ae5311a0d065e60c2386ef4c81edddc24fa687503e35d297dc1d1ff704cfe",
"scope" : "0xe78222479cb676528ed8a36f9fd71706a3aaea59040e64d97aa910994897fe3e",
"nonce" : "0x1",
"iat" : 1708984962,
"exp" : 1740281565,
"v" : 27
"r" : "0x075bc5c4ca57b9dd18331654cb97d5ad03e6aca6bc68ff5cbdf9ba3c2afdb060",
"s" : "0xcdd7697eb7d4567c134dff02a81bc29395e5c8c0786c27d544126c104d3e39fe",
}
  • account: EOA address of user that is being authenticated.
  • sub: the PPID, derived from EIP712Domain and scope.
  • scope: hex-encoded uint256 value, used to further differentiate the sub by associating it with a specific scope within the same EIP712Domain.
  • nonce: value provided in the initial OIDC authorization request.
  • iat: UTC timestamp (in seconds)
  • exp: UTC timestamp (in seconds)
  • v, r, s: EIP712 signature

EIP712 signature verification​

The signed id_token can be verified within a smart contract using the public key available in the JWKS. Here's a simple example of smart contract that requires a valid EIP712 signature to mint an NFT (ERC721) :

warning

In this example, a simple expiration check on iat is used to mitigate replay attacks. For stricter scenarios, the use of nonce might be required to prevent replay attacks.

pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MySmartContract is ERC721, EIP712, Ownable {
string private constant TOKEN_NAME = "MySmartContract";
string private constant TOKEN_SYM = "MSC";
string private constant TOKEN_VER = "1";
uint256 private constant SCOPE = 1337;

bytes32 private constant PILEUMAUTH_TYPEHASH =
keccak256(
"PileumAuth(address account,uint256 sub,uint256 scope,uint256 nonce,uint128 iat,uint128 exp)"
);

constructor(address initialOwner) payable
ERC721(TOKEN_NAME, TOKEN_SYM)
EIP712(TOKEN_NAME, TOKEN_VER)
Ownable(initialOwner)
{}

function safeMint(
address account,
uint256 sub,
uint256 scope,
uint256 nonce,
uint128 iat,
uint128 exp,
uint8 v,
bytes32 r,
bytes32 s
) public {
require(exp > iat, "Invalid expiration");
require(block.timestamp < iat + 3600, "Message has expired");
require(scope == SCOPE, "Invalid scope");
address signer = ECDSA.recover(
_hashTypedDataV4(
keccak256(
abi.encode(PILEUMAUTH_TYPEHASH, account, sub, scope, nonce, iat, exp)
)
),
v,
r,
s
);
require(signer == owner(), "Invalid signature");
_safeMint(account, sub);
}
}