Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/welcome-certified.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Accueil du nouveau certifié, au merge de sa PR d'inscription.
# Aucun email n'est collecté (identité = compte GitHub) : la remise se fait par un
# commentaire qui @mentionne le membre -> GitHub le notifie, avec le lien de son kit.
name: Bienvenue au certifié

on:
pull_request:
types: [closed]

permissions:
pull-requests: write

jobs:
welcome:
# Uniquement les PR d'inscription réellement mergées.
if: >-
github.event.pull_request.merged == true
&& startsWith(github.event.pull_request.title, 'Inscription :')
runs-on: ubuntu-latest
steps:
- name: Commenter le kit au nouveau certifié
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
TITLE: ${{ github.event.pull_request.title }}
SITE: https://verify.ai-driven-dev.fr
run: |
set -euo pipefail
# Handle = le @login dans le titre « Inscription : Nom (@handle) ».
handle="$(printf '%s' "$TITLE" | grep -oE '@[A-Za-z0-9-]+' | head -1 | tr -d '@' || true)"
if [ -z "$handle" ]; then
echo "Handle introuvable dans le titre, on n'accueille pas."
exit 0
fi
body="$(cat <<EOF
🎉 @$handle, vous êtes certifié·e **AI-Driven Development** !

Votre kit, public et prêt à l'emploi : **$SITE/u/$handle/kit**

En 2 minutes :
1. **Ajoutez la certification à LinkedIn** — le bouton « Ajouter à LinkedIn » préremplit tout (dates comprises). Joignez l'« Image du certificat » en média : LinkedIn ne l'ajoute pas seul.
2. **Photo de profil** cerclée de l'anneau certifié.
3. **Signature email** à coller (Gmail / Outlook).

Votre page de preuve publique : $SITE/u/$handle
EOF
)"
gh pr comment "$PR" --body "$body"
1 change: 1 addition & 0 deletions MAINTAINERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Ce que fait un mainteneur, étape par étape. Rien d'autre n'est requis au quoti
4. **Merger** la PR. → le badge est émis et publié automatiquement. Fin.

> Rien n'est public tant que la PR n'est pas mergée. Le merge **est** l'autorisation.
> Au merge, le membre est **accueilli en commentaire** sur sa PR (GitHub le notifie), avec le lien de son kit — aucun email requis.

## Traiter un retrait (RGPD)

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ npm run demo # signe un badge de démo et le sert -> http://localhost:8000/u/de

| Chemin | Contenu |
|---|---|
| `docs/ARCHITECTURE.md` | L'architecture et les flux (diagrammes) |
| `docs/ARCHITECTURE.md` | Architecture, flux et **diagrammes** (deux hôtes, émission, vérif, retrait, cycle de vie de la clé) |
| `docs/urls.md` | Cartographie des URLs (permanent gravé vs présentation) |
| `docs/verification.md` | Vérifier un badge indépendamment (page, 1EdTech, hors ligne) |
| `MAINTAINERS.md` | Guide des mainteneurs, étape par étape |
| `docs/PRD.md` | Spécification + contraintes techniques (CT-1…CT-14) |
| `docs/PRD.md` | Référence : exigences + contraintes (CT-1…CT-14), rationale |
| `docs/rgpd/` | Information (art. 13) + justification non-AIPD |
| `.github/scripts/` | Intake, émission, révocation, annuaire (+ tests) |
| `site/` | Code de vérification navigateur (+ tests) |
Expand Down
36 changes: 30 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,18 @@ flowchart TD
B --> C["récupère la clé via le kid (Pages)"]
C --> D{"Signature valide ?"}
D -->|non| INV["Invalide"]
D -->|oui| E{"Révoqué ? (status list)"}
D -->|oui| S{"Statut vérifiable ?<br/>(status list joignable + signée)"}
S -->|non| UNK["Signature valide,<br/>révocation non vérifiée (prudence)"]
S -->|oui| E{"Révoqué ?"}
E -->|oui| REV["Révoqué"]
E -->|non| F{"Expiré ?"}
F -->|oui| EXP["Expiré"]
F -->|non| OK["Valide"]
```

> Fail-safe : on n'affirme jamais « valide » sans avoir écarté une révocation. Si le
> status list est injoignable ou non authentifiable, l'état est **prudent**, pas valide.

## Retrait RGPD

```mermaid
Expand All @@ -80,9 +85,28 @@ sequenceDiagram
RGPD = `rm -rf data/members/<handle>/`. La révocation survit dans `data/revoked.json`
(juste des entiers, non personnels). Schéma des champs : `PRD.md`.

## Clé de signature
## Clé de signature — cycle de vie

Une paire RS256. Le `kid` = thumbprint RFC 7638 de la clé publique, donc il ne peut
jamais diverger de la clé (l'émission le re-dérive à chaque build).

```mermaid
flowchart LR
Gen["Cérémonie de clé<br/>(workflow manuel, gate Habilité)"]
Gen -->|"privée (PKCS8)"| Priv["secret env 'signing'<br/>(jamais dans le dépôt)"]
Gen -->|"publique (JWK nu)"| Pub["keys/&lt;kid&gt;.json<br/>publiée par PR"]
Priv -->|"au merge, en CI"| Sign["signe credentials + status list<br/>kid = URL Pages de la clé"]
Pub -->|"à vie — CT-7"| Verif["déréférencée par le kid des badges"]
Gen -. "rotation = re-run" .-> New["nouvelle kid<br/>l'ancienne publique RESTE publiée"]
```

Une paire RS256. La **privée** vit en secret d'environnement CI (`signing`), utilisée
seulement au merge ; jamais dans le dépôt. La **publique** est publiée à vie
(`keys/<kid>.json`) : retirer une ancienne clé rendrait ses badges invérifiables.
Rotation : un workflow manuel gaté par une review Habilité (`key-ceremony`).
- **Génération** : workflow manuel `key-ceremony`, gaté par une review *Habilité*. La
privée part en secret `SIGNING_PRIVATE_KEY` (env `signing`) ; la publique est publiée
par une PR de clé. L'émission ne se fait qu'après le merge de cette PR.
- **Rotation** : re-jouer la cérémonie. Une nouvelle `kid` sert les émissions suivantes.
- **Révoquer une clé — nuance** : on ne supprime **jamais** `keys/<kid>.json`. Retirer
une clé publique rendrait invérifiables **tous** les badges signés avec (CT-7). Donc :
- « retirer » une clé = **rotation** (arrêter de signer avec l'ancienne) ;
- **compromission** = rotation **et** révocation, via la status list, de **tous** les
badges signés par la clé compromise (un attaquant pourrait forger avec elle), puis
ré-émission des membres légitimes sous la nouvelle clé.
6 changes: 3 additions & 3 deletions docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,12 @@ déjà exportée garde son ancienne échéance et reste valide jusque-là.

**Retrait RGPD (#36).** Un retrait **supprime tout le dossier** `data/members/<handle>/` (fiche + photo LFS) — l'effacement, pas une réécriture d'historique. Pour que le badge reste révoqué à vie malgré la disparition de la fiche, son `status_index` est inscrit dans un **registre de révocation** `data/revoked.json` (une liste d'entiers — pas de donnée personnelle). La liste de statuts (CT-4) se construit à partir de ce registre. Une preuve déjà exportée reste présentable mais apparaît **révoquée** à la vérification (CT-5, seule voie).

Le site public `ai-driven-dev.fr` (`/communaute`) affiche l'annuaire ; il consomme le flux **`directory.json`** publié par la CI sur `verify.ai-driven-dev.fr` (nom, LinkedIn, photo, site, description, URL de vérif). Les photos sont servies sur `verify.ai-driven-dev.fr/photos/<handle>.webp`. Les membres révoqués sont exclus du flux.
Le site public `ai-driven-dev.fr` (`/communaute`) affiche l'annuaire ; il consomme le flux **`directory.json`** publié par la CI sur l'ancre Pages `ai-driven-dev.github.io/badges` (nom, LinkedIn, photo, site, description, URL de vérif). Les photos sont servies sur `ai-driven-dev.github.io/badges/photos/<handle>.webp`. Les membres révoqués sont exclus du flux. (Hôtes : cf. `ARCHITECTURE.md`.)

**Généré au merge, pas saisi** (le job d'émission le calcule à partir de la date de merge, pour qu'un demandeur ne fixe pas lui-même sa validité) :
**Généré à l'émission, pas saisi** (calculé par la CI pour qu'un demandeur ne fixe pas lui-même sa validité) :

- `badge_id` — identifiant unique du credential ;
- `certified_on` — date d'émission (= date de merge) ;
- `certified_on` — date d'émission = **date du commit qui a ajouté le record** (déterministe ; `renewed_on` prime au renouvellement) ;
- `expires_on` — `certified_on` + 1 an (CT-8).

Ces valeurs ne figurent pas dans le YAML d'intake : elles sont scellées dans le credential signé et exposées par la page de vérification.
Expand Down
6 changes: 6 additions & 0 deletions docs/spikes/sp1-domain-hosting/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# SP1 — Domaine + hébergement

> **⚠️ Superseded — spike historique.** Décrit l'ancien schéma **mono-host**
> (`verify.ai-driven-dev.fr` = Pages). Le système a depuis **splitté en deux hôtes** :
> ancre permanente sur `ai-driven-dev.github.io/badges` (Pages), présentation sur
> `verify.ai-driven-dev.fr` (VPS). **Source à jour : [ARCHITECTURE.md](../../ARCHITECTURE.md)
> + [urls.md](../../urls.md).** Conservé pour la traçabilité, pas comme référence.

**Statut : tranché.** Décision écrite ci-dessous. Débloque E1 (socle/infra) et E4 (vérification).

## Décision
Expand Down
60 changes: 60 additions & 0 deletions docs/urls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Cartographie des URLs

Deux hôtes, deux rôles. Ne jamais confondre : les uns sont **gravés à vie** dans les
badges, les autres sont juste de la **présentation** remplaçable.

| Base | Rôle | Hébergement |
| --- | --- | --- |
| `DATA_BASE` = `https://ai-driven-dev.github.io/badges` | **Ancre permanente** : clés, statut, issuer, preuves, annuaire, photos. Doit vivre à vie. | GitHub Pages (statique, dispo max) |
| `SITE_BASE` = `https://verify.ai-driven-dev.fr` | **Présentation** : la belle page de vérif, le kit, les images dérivées. | VPS (Astro SSR) |

> `DATA_BASE`/`SITE_BASE` sont définis dans `.github/scripts/lib/credential.mjs`.
> En **prod**, la signature grave `DATA_BASE` (Pages) dans chaque credential — donc un
> badge reste **vérifiable même si le VPS tombe**. Le VPS ne fait que lire Pages et l'habiller.
> En **local**, `npm run demo` (`.github/scripts/demo.mjs`) signe un badge de démo et le sert
> pour tester sans DNS ; ce n'est PAS le schéma de prod.

## 1. Permanent — gravé dans chaque badge (immuable, à vie)

Changer une seule de ces URLs **invalide tous les badges déjà émis**. Servi sous `DATA_BASE`.

| URL | Contenu |
| --- | --- |
| `/issuer.json` | Profil de l'émetteur (`type: Profile`) — `issuer.id` du credential. |
| `/keys/<kid>.json` | 1 JWK **nu** par clé (pas de JWKS array). Publié à vie, même après rotation (CT-7). |
| `/status/1` | Bitstring Status List (révocation, CT-4). Référencé par `credentialStatus`. |
| `/achievements/certified-member` | Définition de l'achievement. |
| `/u/<handle>/credential.jwt` | La preuve VC-JWT signée, téléchargeable (CT-6). |

## 2. Flux public — régénéré par la CI (pas gravé, mais public)

Servi sous `DATA_BASE`. Reconstruit à chaque émission / retrait.

| URL | Contenu |
| --- | --- |
| `/directory.json` | Annuaire des certifiés **actifs** (révoqués exclus, cf. `directory.mjs` `isActive`). Consommé par le site (`/communaute`). |
| `/photos/<handle>.webp` | Photo du membre (copiée depuis LFS au build). |

## 3. Présentation — le site (remplaçable, non gravé)

Servi sous `SITE_BASE` (VPS). Lit les données ci-dessus et les rend au design. Peut évoluer
librement sans toucher aux badges. Les images sont rendues serveur (sharp).

| URL | Contenu | Params |
| --- | --- | --- |
| `/u/<handle>` | Page de vérification publique (vérif crypto **dans le navigateur**). | — |
| `/u/<handle>/kit` | Kit self-service du membre (assets à copier / télécharger). | — |
| `/u/<handle>/badge.png` | Sceau **générique** (favicon, avatar). | `size` (16–1024), `role` (`certifie`\|`habilite`) |
| `/u/<handle>/og.png` | Carte d'aperçu de lien (Open Graph, 1200×630) — déplie le lien collé en carte de marque. Sert aussi de média certificat. | — |
| `/u/<handle>/linkedin.png` | Photo du membre cerclée de l'anneau + pastille de vérification. | `size` (120–1200), `check` (`0` = sans pastille) |
| `/u/<handle>/banner.png` | Coin « certifié » transparent à poser sur SA bannière. | — |
| `/u/<handle>/qr.png` | QR code vers la page de preuve. | `size` |
| `/u/<handle>/photo.png` | Photo du membre en rond, même origine (aperçus, signature email). | `size` (48–1024) |

## Règle d'or

- Une URL de la **section 1** est **irréversible** : `issuer.id`, les URLs de `kid`, les ids
d'achievement et de statut sont fixés à l'émission. Le domaine `DATA_BASE` doit être renouvelé
indéfiniment.
- Les URLs de la **section 3** sont **cosmétiques** : on peut changer le rendu, ajouter des
formats, migrer d'hôte — aucun badge n'en dépend pour être vérifié.
12 changes: 8 additions & 4 deletions docs/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ ne fait que servir des fichiers statiques ; il ne « valide » rien.

## Avec un outil que vous choisissez (#32)

1. Téléchargez la preuve : `https://verify.ai-driven-dev.fr/u/<handle>/credential.jwt`.
Les URL permanentes sont sur l'**ancre Pages** (`ai-driven-dev.github.io/badges`),
gravées dans le badge — pas sur le site de présentation (cf. [ARCHITECTURE.md](ARCHITECTURE.md)).

1. Téléchargez la preuve : `https://ai-driven-dev.github.io/badges/u/<handle>/credential.jwt`.
2. Soumettez-la au **validateur public 1EdTech** : <https://vc.1ed.tech> (verifier « Open Badges 3.0 »).
3. Il récupère la clé publique via le `kid` du JWT (`https://verify.ai-driven-dev.fr/keys/<kid>.json`,
3. Il récupère la clé publique via le `kid` du JWT (`https://ai-driven-dev.github.io/badges/keys/<kid>.json`,
un JWK nu) et confirme la signature — **sans passer par notre page**.

La conformité au format est prouvée : voir `docs/spikes/sp4-conformance/`.
Expand All @@ -30,5 +33,6 @@ vérifiable même après rotation.
- **Signature** : le badge a été émis par la clé privée d'AIDD, non forgé ni altéré.
- **Titulaire** : le sujet est un compte GitHub (`https://github.com/<handle>`) — le
contrôle du compte a été prouvé à l'émission (CT-3).
- **Validité** : dates d'émission et d'expiration (1 an), et statut de révocation
(Bitstring Status List, à venir #25).
- **Validité** : dates d'émission et d'expiration (1 an), et statut de **révocation**
(Bitstring Status List v1.0). Si le status list est injoignable, la page reste
**prudente** — jamais « valide » sans avoir écarté une révocation.