Links a LUKSO Universal Profile (UP) to an Italian electronic identity card (CIE), and marks the UP with a soulbound "verified" badge. No name, tax code or card data goes on-chain.
Experimental service by ChainIntegrate, live on LUKSO mainnet at up-cie-link.chainintegrate.it. Not a government service and not an identity document: the badge only states that, at a given time, whoever controls the UP also held a valid CIE and its PIN.
- The backend prepares a short statement containing the UP address, a one-time nonce and an expiry.
- The UP signs the statement (ERC-1271: the backend checks it with the UP's
isValidSignature): with the UP Browser extension on a computer or, on a phone, with LUKSO's Universal Profiles app or UP Wallet for Android (WalletConnect). - The CIE signs the same statement with the holder's key on the chip, unlocked by the card PIN (today with the CieSign app; next with an Android app reading the card over NFC, whose channel stays disabled until the app ships).
- The backend verifies the CIE signature, the certificate chain up to the Ministero dell'Interno CA, and revocation (OCSP, fail-closed). It derives a pseudonymous uniqueness key from the tax code (HMAC with a server-side secret) and discards every personal value.
- The attester mints a soulbound LSP8 badge on the UP: source, verification time, expiry (the first day of the
month in which the card expires, so the exact date, which is the holder's birthday, is not published, and the
badge never outlives the card) and status.
Other contracts can call
isVerified(up), orisVerifiedPrimary(up)for strict "one person, one UP" (additional UPs are allowed only by exception). Note: the badge is tied to the UP, so whoever knows the UP's owner learns that they have a verified CIE; a migration between two UPs is visible on-chain as two close operations.
Both signatures cover the same bytes, so the link between the UP and the CIE is cryptographic, not just a session on our server.
Contract on LUKSO mainnet: 0xc945c5FABBc19dBd04f90a1a4878cfA0F78C5e38 (from block 8661334).
Gate on isVerified(up) or isVerifiedPrimary(up), never on owning the token. The badge is a permanent record:
it stays on the UP after it expires, is invalidated (renounced, moved to another UP, certificate revoked) or is
revoked, and only these two reads tell whether it is valid now.
isVerified(up): valid and not expired.isVerifiedPrimary(up): the same, and the UP is the person's primary one; use it for "one person, one vote" (votes, airdrops). UPs of the same person are never linked on-chain.getAttestation(up): source, verification time, expiry (the first day of the month in which the card expires), status, primary.- One token per UP:
tokenIdOf(up)is the UP address asbytes32, so relinking restores the same token. No transfers, no burns, no operators. - On-chain invalidation reasons are always the generic
INVALIDATED: a lost or stolen card is not exposed. - Events to index:
Attested,Invalidated,Revoked,Reinstated.
| Phase | Content | State |
|---|---|---|
| 0 | Analysis, options, decisions (docs/00-analisi.md) |
done |
| Spike | Verify a real CIE signature (spike/) |
done, all checks passed on a real CIE |
| 1 | Attester contract, tests, deployment (contracts/) |
done; mainnet 0xc945c5FABBc19dBd04f90a1a4878cfA0F78C5e38 (block 8661334, 2026-10-27), LSP4 metadata and creator data (LSP4Creators/LSP12IssuedAssets) set; Blockscout shows a partial match (same bytecode; the exact source is commit 8746055), full verification to retry |
| 2 | Backend (Node/Express): statements, ERC-1271, CIE verification, minting, jobs, monitor, backups | done |
| 3 | Frontend: connect UP, sign, show badge, self-service data export/erasure, admin page | done; live on mainnet since 2026-10-27 (first mainnet badges: a primary UP and an additional UP by exception, from a phone). Wallets: UP Browser extension (computer), UP Wallet for Android (phone, or QR code from a computer; real tests on 2026-10-26, whole link done from the phone) and LUKSO's Universal Profiles app (phones only, opened by its own WalletConnect link; on 2026-10-27 pairing and UP signature on mainnet verified, the badge itself was not minted through it). Not on the admin page, which is desktop-only. User guide (/guida), link previews (Open Graph). Earlier on testnet: first real UP + CIE link on 2026-10-15; renounce, data export, re-link after renouncing and data erasure tested by the owner |
| 4 | Android app (NFC signature), end-to-end demo | to do |
| Server | Service on the VPS: PM2, separate signer, monitor and backup timers, nginx + TLS (docs/operations.md) |
done (2026-10-15); switched to mainnet on 2026-10-27, testnet data deleted |
| Audit | Three security/privacy reviews (2026-10-14, 2026-10-15, 2026-10-27) and their fixes (docs/audit/) |
done: every finding fixed or decided (third review: UCL-44 … UCL-54 handled on 2026-10-27); separate signer process (UCL-08) and complete privacy notice (UCL-14) done on 2026-10-27; CIE certificate policy checked (UCL-13, the service is a relying party as the policy describes it); off-site backups (UCL-25) deferred to a reasonable number of users |
| EU eID | Same attestation with other European eID cards that carry a signing certificate | exploring: looking for testers |
| Mainnet | Contract on mainnet with creator data, user guide, switch of the service (docs/operations.md §6) |
done on 2026-10-27 (plain deployment, no Nick factory; same attester key); testnet retired, minting stopped there. Off-site backups (UCL-25) still deferred |
contracts/ Hardhat 3 project: UpCieBadge (LSP8 soulbound), tests, deploy/verify/metadata scripts, deployments/
backend/ Node/Express service: statements, UP and CIE verification, minting queue, indexer, monitor, backups
signer/ Separate process holding the attester key and the server secrets (no network, local socket, rate limits)
frontend/ Static pages: link, verify, my badge, guide, privacy, legal notes (third-party code only for the phone
wallets: WalletConnect client, vendored, loaded on demand)
deploy/ PM2, systemd timers (monitor, backup), nginx site; server operations in docs/operations.md
spike/ Stand-alone scripts to create a statement and verify a CIE CAdES signature
docs/ Analysis, design notes and decisions (Italian); docs/audit: audit report and fix tracking
.devcontainer/ GitHub Codespaces setup (Node 22, dependencies, recommended secrets)
cd contracts && npm ci --ignore-scripts && npx hardhat test
cd ../signer && npm ci --ignore-scripts
cd ../backend && npm ci && npm test # needs the contracts built; the browser test needs PlaywrightDeployment and verification are described in contracts/README.md; the mainnet switch of the
service in docs/operations.md §6.
The CIE verification spike is described in spike/README.md.
- On-chain: only UP address, source, timestamps (expiry rounded to a month) and status. No name, no tax code, no hash of them. Invalidation reasons on-chain are generic.
- Off-chain: only an HMAC of the tax code (for "one person, one UP") and, for later revocation checks, the certificate serial and issuer, encrypted. Signed files and statements are not stored. Retention limits are implemented; holders export or erase their data themselves from the "My badge" page, signing the request with their UP (see the privacy notice in the frontend).
- Logs never contain certificates, tax codes or signatures. Adults only (age read from the tax code in memory).
- The attester key and the server secrets are not in the web backend: a separate signer process (own user, no
network, root-owned code) holds them, signs only the badge contract's
attest/invalidateand rate-limits mints and tax-code fingerprints, so a compromised backend cannot copy them.
Apache-2.0, see LICENSE and NOTICE. Copyright 2026 ChainIntegrate.