Petite application PHP (sans dépendance) et base SQLite qui recueille les signatures du manifeste pour une identité numérique libre et ouverte, en remplacement du formulaire Framaforms. Licence EUPL 1.2.
- La personne remplit le formulaire (
public/index.php) : prénom, nom, e-mail, fonction et organisation facultatives, consentement à la publication. - Elle reçoit un e-mail qui récapitule les informations saisies, avec un lien valable 48 heures (double consentement). Le lien ouvre une page qui les récapitule à nouveau, avec un bouton : seul ce bouton (POST) confirme, pour que les antivirus de messagerie qui suivent les liens ne confirment pas à la place de la personne.
- Après confirmation, un second e-mail contient le lien de retrait, qui supprime la signature et les données, et le lien vers sa preuve horodatée (
public/preuve.php) :- une attestation JSON de la signature (manifeste, nom, fonction, organisation, choix de publication, date de confirmation, sel aléatoire ; jamais l'adresse e-mail) est figée à la confirmation, et son empreinte SHA-256 est horodatée (RFC 3161) par l'autorité d'horodatage de staging d'OTSPI (
api.staging.open-eidas.eu) — preuve non qualifiée, sans valeur juridique ; - la page de preuve affiche la date certifiée et les détails du jeton, que le serveur vérifie sans dépendance (
src/horodatage.php: signature CMS de l'unité d'horodatage, empreinte, usage « Time Stamping », émission par la chaîne épingléesrc/tsa-staging-ca.pem), propose l'attestation (.json) et le jeton (.tsr) au téléchargement, et renvoie au vérificateur de demo.open-eidas.eu ou àopenssl ts -verify; - la page de preuve propose aussi une attestation PDF (
src/pdf.php, sans dépendance) : une page A4 avec les données de la signature et de son horodatage, l'attestation JSON et son jeton.tsren pièces jointes (ISO 32000), et un horodatage PAdES du PDF lui-même (/SubFilter /ETSI.RFC3161, reconnu par Adobe Acrobat Reader, pyHanko et la démonstration d'OTSPI). Construit au premier téléchargement et conservé ; sans horodatage si l'autorité ne répond pas, puis reconstruit ; - si l'autorité ne répond pas (5 secondes au plus), la confirmation n'est pas bloquée : la tâche quotidienne réessaie. Elle horodate aussi les signatures antérieures et leur envoie leur preuve (100 e-mails au plus par passage).
php bin/horodatage.phpfait le même rattrapage à la demande.
- une attestation JSON de la signature (manifeste, nom, fonction, organisation, choix de publication, date de confirmation, sel aléatoire ; jamais l'adresse e-mail) est figée à la confirmation, et son empreinte SHA-256 est horodatée (RFC 3161) par l'autorité d'horodatage de staging d'OTSPI (
- Si la personne a accepté la publication, la signature attend une modération : l'adresse
contactreçoit une notification avec un lien vers l'administration (connexion par clé de sécurité ; les anciens liensmoderation.phpy redirigent), ou en ligne de commandephp bin/moderation.php lister | recapitulatif | valider ID | masquer ID | supprimer ID(masquer : signature comptée, nom jamais publié ; supprimer : usurpation, abus). Seule l'adresse e-mail est vérifiée par l'application, pas l'identité déclarée. - Administration :
https://manifesto-sign.otspi.org/admin.php, ouverte uniquement avec une clé de sécurité (WebAuthn / FIDO2, une YubiKey) et son code PIN : la vérification de l'utilisateur est exigée, ce qui fait deux facteurs (possession de la clé, PIN). Ni mot de passe ni lien de connexion. La page liste les signatures par état (à modérer, publiées, non publiées, en attente de confirmation) avec les actions valider, masquer et supprimer ; pour les demandes en attente de confirmation, renvoyer le lien de confirmation (nouveau lien de 48 heures, au plus toutes les 10 minutes) ou supprimer la demande. Actions en POST depuis le site, suppression confirmée.- Enregistrer la première clé : tant qu'aucune clé n'existe, la page propose d'envoyer à la seule adresse
contactun lien d'enregistrement valable 24 heures et à usage unique (au plus 3 envois par 10 minutes). Ensuite, ce bouton disparaît : une clé supplémentaire (recommandée, gardée en lieu sûr) s'ajoute depuis l'onglet « Clés de sécurité » d'une session ouverte, ou par un nouveau lien envoyé depuis le serveur avecphp bin/admin.php invitation(clé perdue). Chaque enregistrement déclenche une alerte à l'adressecontact. - Clés : l'onglet « Clés de sécurité » les liste (modèle par AAGUID, dernière utilisation) et permet de les révoquer, sauf la dernière ; la révocation ferme les sessions ouvertes avec la clé.
config.phppeut limiter les modèles acceptés (admin_aaguids). En ligne de commande :php bin/admin.php cles | revoquer ID | deconnecter. - Session : cookie
__Host-HttpOnly, Secure, SameSite=Lax, propre à l'administration ; fermée après 30 minutes d'inactivité, 8 heures au plus. Les défis WebAuthn sont signés (HMAC, 10 minutes), liés à leur usage et à usage unique ; le compteur de signatures de la clé est contrôlé.
- Enregistrer la première clé : tant qu'aucune clé n'existe, la page propose d'envoyer à la seule adresse
public/signataires.phpexpose en JSON la liste publique : signatures confirmées, avec consentement à la publication et validées, sans adresse e-mail. Le total n'inclut pas les signatures en attente de modération. La page du manifeste de www.otspi.org la lit en direct (en-tête CORS limité àhttps://www.otspi.org) : une signature apparaît quelques minutes après sa validation (cache de 5 minutes).cron/purge.php(tâche quotidienne) supprime les demandes non confirmées de plus de 7 jours, ainsi que les invitations, défis et sessions d'administration expirés ; il horodate les signatures en attente et envoie leur preuve, inscrit l'entrée du jour au registre horodaté et envoie la sauvegarde chiffrée. Au moindre problème (autorité d'horodatage injoignable, registre en attente ou rompu, sauvegarde en échec), une alerte part à l'adressecontact, au plus une par passage.- Registre horodaté (
public/registre.php, lien en pied de page) : chaque nuit, une entrée fige le total, le nombre de noms publiés et l'empreinte SHA-256 de la liste publique telle que la sertsignataires.php, chaînée à l'entrée précédente par son empreinte, puis horodatée (RFC 3161). Qui a conservé la liste d'un jour peut prouver qu'elle n'a pas été modifiée ; la chaîne interdit de réécrire l'historique sans que cela se voie. Le registre ne conserve aucun nom : une signature retirée disparaît de la base, seules les empreintes restent. - Modération : l'adresse
contactreçoit chaque nuit un récapitulatif des signatures à modérer (moderation_notify='immediat'pour revenir à un e-mail par signature ;php bin/moderation.php recapitulatifpour l'envoyer tout de suite). L'onglet « À modérer » permet de valider ou masquer par lot. Pour chaque adresse, l'administration indique si son domaine correspond à l'organisation déclarée (email_org_hint(): inria.fr pour Inria, univ-lyon1.fr pour Lyon 1, sigles compris), s'il s'agit d'une messagerie grand public, ou d'un autre domaine : c'est une estimation pour la modération, jamais publiée. - Statistiques et export (onglet « Statistiques » de l'administration) : chiffres clés (confirmées, publiées, à modérer, en attente, taux de confirmation), signatures confirmées par jour sur 60 jours (info-bulle au survol, tableau des données), répartition par type d'adresse, et export CSV de toutes les signatures (point-virgule, BOM ; cellules commençant par
= + - @neutralisées contre l'injection de formules). - Partage : après confirmation, la page propose le partage natif du téléphone, des liens LinkedIn, Bluesky, Mastodon et e-mail (simples liens, rien n'est chargé avant le clic), la copie du lien et le code d'intégration du compteur
compteur.php: une image SVG « Manifeste OTSPI | N signataires » (?lang=enen anglais), sans script ni traceur, au total de la liste publique. - Sauvegarde (
src/sauvegarde.php, chaque nuit ouphp bin/sauvegarde.php) : copie cohérente de la base (VACUUM INTO), compressée et chiffrée (CMS, AES-256) pour le seul certificatsrc/sauvegarde-certificat.pem, envoyée en pièce jointe àbackup_to(à défautcontact). Le serveur ne peut pas relire ses sauvegardes : la clé privée est gardée hors du serveur (poste de l'administrateur, à dupliquer dans un coffre de mots de passe). Restauration :openssl cms -decrypt -binary -inform DER -in signatures-AAAA-MM-JJ.sqlite.gz.p7m -inkey cle-privee.pem | gunzip > signatures.sqlite
E-mails : chaque message part en deux versions (multipart/alternative) : le texte brut rédigé dans src/texts.php et un habillage HTML qui en est tiré automatiquement (mail_html() dans src/lib.php : bandeau OTSPI, récapitulatif en tableau, liens en boutons avec l'adresse rappelée dessous, thème sombre), sans image ni ressource externe, donc sans pixel de suivi. En mode essai (mail_dry_run), le HTML est journalisé dans mail_log suivi de .html.
Protections : jeton de formulaire signé avec délai minimal, à usage unique (aucun cookie, aucune session), preuve de travail calculée par le navigateur pendant la saisie (assets/pow.js : SHA-256 du jeton avec 18 bits nuls en tête, soit moins d'une seconde sur un ordinateur ; réglable par pow_bits ; aucun service tiers, JavaScript requis pour signer), refus des adresses d'e-mail jetables (src/disposable-domains.txt, sous-domaines compris), champ piège pour les robots, refus des envois venant d'un autre site (Sec-Fetch-Site, à défaut Origin), limitation de débit par empreinte salée de l'adresse IP ou du préfixe /64 en IPv6 (conservée une heure), plafond global de 200 e-mails de confirmation par heure (mail_hourly_cap), e-mails sans texte libre hors du récapitulatif et liens refusés dans les champs, une seule signature par boîte (les alias +… sont acceptés mais ignorés pour l'unicité), un seul e-mail de confirmation toutes les 10 minutes par adresse, réponse identique que l'adresse soit connue ou non, refus des caractères de contrôle, bidirectionnels et de largeur nulle dans les noms, requêtes préparées, échappement HTML, erreurs journalisées et jamais affichées, en-têtes de sécurité et CSP stricte.
Mesure d'audience : seuls le formulaire (index.php) et la page « signature confirmée » chargent assets/analytics.js (Matomo Tag Manager de stats.otspi.org, sans cookie) et ouvrent leur CSP à ce domaine. Le formulaire charge aussi assets/pow.js (preuve de travail, sans appel extérieur). Les pages dont l'adresse porte un jeton personnel (lien de confirmation, retrait, modération) ne sont pas mesurées. Le formulaire déclare aussi des événements anonymes (attributs data-track et data-track-load, voir analytics.js) : clic sur « Envoyer », code d'erreur éventuel (err_…, jamais les valeurs saisies), page « demande envoyée » et page « signature confirmée » (réponse au POST de confirmation, dont l'adresse confirm.php?lang=… ne porte pas le jeton). analytics.js est commun aux sites d'OTSPI : sa source est dans le dépôt vitrine ; après une copie, incrémenter ?v= dans page() (src/lib.php).
- État de santé :
public/sante.phprenvoie en JSON, sans donnée personnelle, l'état de la base, de la tâche quotidienne, de la dernière sauvegarde, du registre horodaté, de l'horodatage des signatures et de l'autorité d'horodatage (interrogée au plus toutes les 5 minutes) ; HTTP 200 si tout va bien, 503 sinon. Un contrôle jamais encore exécuté est « en attente ». - Surveillance externe :
.github/workflows/surveillance.ymllit cette page toutes les 30 minutes, à l'adresse/sante(réécrite par.htaccess: depuis les datacenters, le pare-feu d'o2switch coupe toute requête vers une adresse en.php, ce qui explique aussi les anciens échecs sursignataires.phpdepuis GitHub Actions) (trois essais) ; en cas d'alerte, le job échoue et GitHub prévient par e-mail la personne qui a activé le workflow planifié. - Durée de conservation :
campagne_fin(config.php, AAAA-MM-JJ) déclenche la suppression de toutes les signatures deux ans après, comme l'annonce le texte d'information, avec un avis à l'adressecontact30 et 7 jours avant. - Droit d'accès : la page de retrait propose de télécharger, en JSON, toutes les données enregistrées sur la personne avant de décider.
- Copier le dépôt hors du dossier web, le dossier
public/étant la racine du site. Les dossierssrc/,data/,bin/etcron/ne doivent pas être servis. - Copier
config.example.phpenconfig.phpet le renseigner (secret aléatoire, adresse, expéditeur).config.phpetdata/ne sont jamais versionnés. - Planifier
cron/purge.phpune fois par jour avec la version en ligne de commande de PHP, en chemin absolu : chez o2switch,23 3 * * * /usr/local/bin/php ~/otspi-signatures/cron/purge.php >> ~/otspi-signatures/data/purge.log 2>&1(dans l'environnement du cron, la commandephpest php-cgi ; les scripts l'acceptent, mais leur journal se remplit alors d'en-têtes HTTP). Les dates sont à l'heure de Paris (timezonedansconfig.phppour changer), le PHP de l'hébergeur étant réglé sur UTC. - Prérequis : PHP 8.1 ou plus avec PDO SQLite et OpenSSL,
allow_url_fopenactivé (appel à l'autorité d'horodatage) ; la fonctionmail()opérationnelle, avec SPF et DKIM sur le domaine d'expéditeur.
Le workflow .github/workflows/deploy.yml passe le test de bout en bout à chaque pull request et à chaque push sur main ; sur main, il envoie ensuite bin/, cron/, src/ et public/ en FTPS (TLS obligatoire, certificat vérifié), puis contrôle le site en lecture seule. config.php et data/ ne sont jamais touchés ; le code retiré du dépôt est supprimé du serveur (sauf public/.well-known/, public/cgi-bin/ et public/error_log, créés par le serveur).
Réglages du dépôt GitHub :
- secrets
O2_SIGN_FTP_USERNAMEetO2_SIGN_FTP_PASSWORD: compte FTP dédié, cantonné au répertoire de l'application (le parent depublic/) ; - variable
O2_FTP_HOST: hôte FTP o2switch ; variable facultativeO2_SIGN_FTP_DIRsi l'application n'est pas à la racine du compte FTP ; - environnement
production(créé au premier déploiement) : on peut y exiger une approbation avant chaque mise en ligne.
Avant un déploiement qui modifie le schéma, sauvegarder data/signatures.sqlite avec ses fichiers -wal et -shm.
bash tests/flow.sh : test de bout en bout (Docker requis) couvrant l'inscription, la confirmation, la modération, l'administration par clé de sécurité (clé virtuelle tests/authenticator.php : enregistrement, PIN absent, signature altérée, autre origine, rejeu, compteur, révocation), la liste publique, le retrait et les protections anti-abus.
bash tests/navigateur.sh : tests dans un vrai navigateur (Chromium piloté par Playwright, image Docker officielle, partageant le réseau du conteneur PHP pour rester sur http://localhost) : preuve de travail calculée par le navigateur, copie du lien de partage, enregistrement et connexion avec une clé de sécurité virtuelle (refusée sans code PIN), sélection par lot, aucune erreur JavaScript ni CSP. La mesure d'audience y est neutralisée. La CI les passe avant tout déploiement.
Base légale : consentement. Conservation : demandes non confirmées 7 jours ; signatures pendant la campagne puis deux ans au plus. L'adresse e-mail n'est jamais publiée ni transmise. Le texte d'information affiché sur le formulaire est dans src/texts.php et doit rester aligné avec les mentions légales de www.otspi.org.